feat: 实现管理后台 W1-W3 假数据界面

This commit is contained in:
Nixevol
2026-09-30 07:02:33 +08:00
parent 8dde564517
commit 1cff321f2a
33 changed files with 4773 additions and 31 deletions
+332
View File
@@ -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>