feat: 实现管理后台 W1-W3 假数据界面
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from "vue";
|
||||
import type { DataTableColumns } from "naive-ui";
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NModal,
|
||||
NScrollbar,
|
||||
NSpace,
|
||||
useDialog,
|
||||
} from "naive-ui";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
addGroupMembers,
|
||||
createGroup,
|
||||
deleteGroup,
|
||||
getGroup,
|
||||
listGroups,
|
||||
removeGroupMember,
|
||||
renameGroup,
|
||||
transferGroup,
|
||||
type GroupDetail,
|
||||
type GroupMember,
|
||||
type GroupSummary,
|
||||
} from "@/api/admin";
|
||||
import { formatLocalMs } from "@/utils/time";
|
||||
import { message } from "@/utils/notify";
|
||||
|
||||
const dialog = useDialog();
|
||||
const loading = ref(false);
|
||||
const rows = ref<GroupSummary[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(50);
|
||||
const query = ref("");
|
||||
|
||||
const createOpen = ref(false);
|
||||
const createForm = reactive({ id: "", name: "", owner_id: "", member_ids: "" });
|
||||
const createLoading = ref(false);
|
||||
|
||||
const detailOpen = ref(false);
|
||||
const detail = ref<GroupDetail | null>(null);
|
||||
const renameName = ref("");
|
||||
const addMembers = ref("");
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const cursor = page.value > 1 ? String((page.value - 1) * pageSize.value) : "";
|
||||
const res = await listGroups(query.value.trim() || undefined, cursor, pageSize.value);
|
||||
rows.value = res.items;
|
||||
total.value = res.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void load();
|
||||
});
|
||||
|
||||
const columns = computed<DataTableColumns<GroupSummary>>(() => [
|
||||
{ title: "编号", key: "id", width: 140 },
|
||||
{ title: "名称", key: "name", width: 140 },
|
||||
{ title: "群主", key: "owner_id", width: 120 },
|
||||
{ title: "成员数", key: "member_count", width: 80 },
|
||||
{
|
||||
title: "创建时间",
|
||||
key: "created_at_ms",
|
||||
width: 160,
|
||||
render: (r) => formatLocalMs(r.created_at_ms),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 200,
|
||||
render: (r) =>
|
||||
h(NSpace, { size: 4 }, () => [
|
||||
h(NButton, { size: "tiny", onClick: () => void openDetail(r.id) }, () => "详情"),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: "tiny",
|
||||
type: "error",
|
||||
secondary: true,
|
||||
onClick: () => onDelete(r),
|
||||
},
|
||||
() => "解散",
|
||||
),
|
||||
]),
|
||||
},
|
||||
]);
|
||||
|
||||
const memberColumns = computed<DataTableColumns<GroupMember>>(() => [
|
||||
{ title: "编号", key: "id" },
|
||||
{ title: "名称", key: "name" },
|
||||
{ title: "在线", key: "online", render: (r) => (r.online ? "是" : "否") },
|
||||
{
|
||||
title: "加入时间",
|
||||
key: "joined_at_ms",
|
||||
render: (r) => formatLocalMs(r.joined_at_ms),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
render: (r) =>
|
||||
h(NSpace, { size: 4 }, () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: "tiny",
|
||||
disabled: detail.value?.owner_id === r.id,
|
||||
onClick: () => void onTransfer(r.id),
|
||||
},
|
||||
() => "设为群主",
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: "tiny",
|
||||
type: "error",
|
||||
secondary: true,
|
||||
disabled: detail.value?.owner_id === r.id,
|
||||
onClick: () => void onRemove(r.id),
|
||||
},
|
||||
() => "移除",
|
||||
),
|
||||
]),
|
||||
},
|
||||
]);
|
||||
|
||||
async function openDetail(id: string) {
|
||||
detail.value = await getGroup(id);
|
||||
renameName.value = detail.value.name;
|
||||
addMembers.value = "";
|
||||
detailOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitCreate() {
|
||||
if (!createForm.name.trim() || !createForm.owner_id.trim()) {
|
||||
message.error("名称与群主必填");
|
||||
return;
|
||||
}
|
||||
createLoading.value = true;
|
||||
try {
|
||||
const member_ids = createForm.member_ids
|
||||
.split(/[,,\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const res = await createGroup({
|
||||
id: createForm.id.trim() || undefined,
|
||||
name: createForm.name.trim(),
|
||||
owner_id: createForm.owner_id.trim(),
|
||||
member_ids,
|
||||
});
|
||||
createOpen.value = false;
|
||||
if (res.failed.length) {
|
||||
message.warning(`已创建,部分成员失败:${res.failed.map((f) => f.id).join(", ")}`);
|
||||
} else {
|
||||
message.success("已创建");
|
||||
}
|
||||
await load();
|
||||
} finally {
|
||||
createLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onDelete(r: GroupSummary) {
|
||||
dialog.warning({
|
||||
title: "解散群",
|
||||
content: `确定解散「${r.name}」?`,
|
||||
positiveText: "解散",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
await deleteGroup(r.id);
|
||||
message.success("已解散");
|
||||
await load();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function onRename() {
|
||||
if (!detail.value) return;
|
||||
await renameGroup(detail.value.id, renameName.value.trim());
|
||||
message.success("已改名");
|
||||
await openDetail(detail.value.id);
|
||||
await load();
|
||||
}
|
||||
|
||||
async function onAddMembers() {
|
||||
if (!detail.value) return;
|
||||
const ids = addMembers.value
|
||||
.split(/[,,\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (!ids.length) return;
|
||||
const res = await addGroupMembers(detail.value.id, ids);
|
||||
if (res.failed.length) {
|
||||
message.warning(`部分失败:${res.failed.map((f) => f.id).join(", ")}`);
|
||||
} else {
|
||||
message.success("已加人");
|
||||
}
|
||||
await openDetail(detail.value.id);
|
||||
await load();
|
||||
}
|
||||
|
||||
async function onRemove(endpointId: string) {
|
||||
if (!detail.value) return;
|
||||
await removeGroupMember(detail.value.id, endpointId);
|
||||
message.success("已移除");
|
||||
await openDetail(detail.value.id);
|
||||
await load();
|
||||
}
|
||||
|
||||
async function onTransfer(endpointId: string) {
|
||||
if (!detail.value) return;
|
||||
await transferGroup(detail.value.id, endpointId);
|
||||
message.success("已转让群主");
|
||||
await openDetail(detail.value.id);
|
||||
await load();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<PageHeader title="群" help="后台建群与加人不必填对话密码。">
|
||||
<template #actions>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="
|
||||
Object.assign(createForm, { id: '', name: '', owner_id: '', member_ids: '' });
|
||||
createOpen = true;
|
||||
"
|
||||
>
|
||||
新建群
|
||||
</n-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<div class="page-body">
|
||||
<n-space style="margin-bottom: 12px">
|
||||
<n-input v-model:value="query" placeholder="名称/编号" style="width: 200px" @keyup.enter="page = 1; load()" />
|
||||
<n-button @click="page = 1; load()">查询</n-button>
|
||||
</n-space>
|
||||
<n-data-table
|
||||
remote
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="rows"
|
||||
:row-key="(r: GroupSummary) => r.id"
|
||||
:max-height="480"
|
||||
:pagination="{
|
||||
page,
|
||||
pageSize,
|
||||
itemCount: total,
|
||||
onChange: (p: number) => {
|
||||
page = p;
|
||||
load();
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<n-modal v-model:show="createOpen" preset="card" title="新建群" style="width: 480px">
|
||||
<n-scrollbar style="max-height: 320px">
|
||||
<n-form label-placement="left" label-width="90" :show-feedback="false">
|
||||
<n-form-item label="编号">
|
||||
<n-input v-model:value="createForm.id" placeholder="留空则生成" />
|
||||
</n-form-item>
|
||||
<n-form-item label="名称">
|
||||
<n-input v-model:value="createForm.name" />
|
||||
</n-form-item>
|
||||
<n-form-item label="群主编号">
|
||||
<n-input v-model:value="createForm.owner_id" />
|
||||
</n-form-item>
|
||||
<n-form-item label="成员" label-placement="top">
|
||||
<n-input v-model:value="createForm.member_ids" type="textarea" :rows="3" placeholder="多个编号用逗号或空格分隔" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-scrollbar>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="createOpen = false">取消</n-button>
|
||||
<n-button type="primary" :loading="createLoading" @click="submitCreate">创建</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="detailOpen" preset="card" title="群详情" style="width: 720px">
|
||||
<n-scrollbar v-if="detail" style="max-height: 420px">
|
||||
<n-form label-placement="left" label-width="80" :show-feedback="false" style="margin-bottom: 12px">
|
||||
<n-form-item label="编号">{{ detail.id }}</n-form-item>
|
||||
<n-form-item label="群主">{{ detail.owner_id }}</n-form-item>
|
||||
<n-form-item label="名称">
|
||||
<n-space>
|
||||
<n-input v-model:value="renameName" style="width: 200px" />
|
||||
<n-button size="small" @click="onRename">改名</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
<n-form-item label="加人" label-placement="top">
|
||||
<n-space vertical style="width: 100%">
|
||||
<n-input v-model:value="addMembers" type="textarea" :rows="2" placeholder="成员编号,逗号分隔" />
|
||||
<n-button size="small" @click="onAddMembers">加人</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-data-table :columns="memberColumns" :data="detail.members" :row-key="(r: GroupMember) => r.id" size="small" />
|
||||
</n-scrollbar>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="detailOpen = false">关闭</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.page-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user