feat platform dashboard and organization management

This commit is contained in:
david
2026-07-29 14:51:13 +08:00
parent 35a52c4b06
commit 5c5bc49e86
13 changed files with 466 additions and 94 deletions

View File

@@ -2,6 +2,7 @@
<a-card :title="definition.title" :bordered="false">
<template #extra>
<a-space>
<a-button v-if="managedOwnerIdentity" @click="router.back()">返回机构列表</a-button>
<a-button @click="load">刷新</a-button>
<a-button v-if="canCreate" type="primary" @click="openCreate">新建</a-button>
</a-space>
@@ -34,13 +35,17 @@
{{ displayFieldValue(field, record) }}
</template>
</a-table-column>
<a-table-column title="操作" :width="260" fixed="right">
<a-table-column v-if="definition.accountManagement" title="账户数" :width="90">
<template #cell="{ record }">{{ accountCounts[String(record.identity)] ?? 0 }}</template>
</a-table-column>
<a-table-column title="操作" :width="definition.accountManagement ? 350 : 280" fixed="right">
<template #cell="{ record }">
<a-space>
<a-button v-if="definition.accountManagement" size="mini" type="primary" @click="manageAccounts(record)">账户管理</a-button>
<a-button size="mini" @click="openDetail(record)">详情</a-button>
<a-button v-if="canEdit" size="mini" :disabled="isProtectedRecord(record)" @click="openEdit(record)">编辑</a-button>
<a-button v-if="canChangeStatus" size="mini" :disabled="isProtectedRecord(record)" @click="openStatus(record)">状态</a-button>
<a-button v-if="canArchive" size="mini" status="danger" :disabled="isProtectedRecord(record)" @click="confirmArchive(record)">归档</a-button>
<a-button v-if="canChangeStatus" size="mini" :disabled="isProtectedRecord(record)" @click="openStatus(record)">审核</a-button>
<a-button v-if="canArchive" size="mini" status="danger" :disabled="isProtectedRecord(record)" @click="confirmArchive(record)">删除</a-button>
</a-space>
</template>
</a-table-column>
@@ -147,6 +152,7 @@ const page = ref(Math.max(1, Number(route.query.page) || 1));
const pageSize = 20;
const total = ref(0);
const list = ref<Row[]>([]);
const accountCounts = ref<Record<string, number>>({});
const filters = reactive({
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
});
@@ -163,6 +169,12 @@ const roleOptions = ref<PlatformRole[]>([]);
const relationOptions = reactive<Record<string, Row[]>>({});
const relationLoading = reactive<Record<string, boolean>>({});
const relationSearchTimers = new Map<string, ReturnType<typeof setTimeout>>();
const managedOwnerIdentity = computed(() =>
typeof route.query.owner_identity === 'string' ? route.query.owner_identity : '',
);
const managedRelationKey = computed(() =>
typeof route.query.relation_key === 'string' ? route.query.relation_key : '',
);
const platformRootWriteResources = new Set([
'platform_account',
'platform_role',
@@ -263,14 +275,30 @@ function resetForm(data?: Row) {
async function load() {
loading.value = true;
try {
const result = await resourceApi.list<Row>(
props.definition.resource,
page.value,
pageSize,
filters.keyword ? { keyword: filters.keyword } : {},
);
list.value = result.list;
total.value = result.total;
if (managedOwnerIdentity.value && managedRelationKey.value) {
const accounts = await loadAllRows(props.definition.resource);
const keyword = filters.keyword.trim().toLowerCase();
const filtered = accounts.filter(
(row) =>
String(row[managedRelationKey.value] ?? '') === managedOwnerIdentity.value &&
(!keyword ||
['username', 'display_name', 'role_code'].some((key) =>
String(row[key] ?? '').toLowerCase().includes(keyword),
)),
);
list.value = filtered.slice((page.value - 1) * pageSize, page.value * pageSize);
total.value = filtered.length;
} else {
const result = await resourceApi.list<Row>(
props.definition.resource,
page.value,
pageSize,
filters.keyword ? { keyword: filters.keyword } : {},
);
list.value = result.list;
total.value = result.total;
}
await loadAccountCounts();
} catch (error) {
Message.error((error as Error).message);
} finally {
@@ -278,8 +306,49 @@ async function load() {
}
}
async function loadAllRows(resource: string) {
const rows: Row[] = [];
for (let currentPage = 1; currentPage <= 100; currentPage += 1) {
const result = await resourceApi.list<Row>(resource, currentPage, 100);
rows.push(...result.list);
if (rows.length >= result.total || result.list.length < 100) break;
}
return rows;
}
async function loadAccountCounts() {
const management = props.definition.accountManagement;
if (!management) {
accountCounts.value = {};
return;
}
const accounts = await loadAllRows(management.resource);
accountCounts.value = accounts.reduce<Record<string, number>>((counts, account) => {
const identity = String(account[management.relationKey] ?? '');
if (identity) counts[identity] = (counts[identity] ?? 0) + 1;
return counts;
}, {});
}
function manageAccounts(row: Row) {
const management = props.definition.accountManagement;
if (!management) return;
const routeName = management.resource === '/gas_account'
? 'organization-gas-account'
: 'organization-delivery-account';
router.push({
name: routeName,
query: {
owner_identity: String(row.identity ?? ''),
relation_key: management.relationKey,
},
});
}
async function syncQuery() {
const query: Record<string, string> = {};
if (managedOwnerIdentity.value) query.owner_identity = managedOwnerIdentity.value;
if (managedRelationKey.value) query.relation_key = managedRelationKey.value;
if (page.value > 1) query.page = String(page.value);
if (filters.keyword.trim()) query.keyword = filters.keyword.trim();
await router.replace({ query });
@@ -299,6 +368,9 @@ async function resetSearch() {
function openCreate() {
editingIdentity.value = '';
resetForm();
if (managedOwnerIdentity.value && managedRelationKey.value) {
form[managedRelationKey.value] = managedOwnerIdentity.value;
}
formVisible.value = true;
}
@@ -429,7 +501,7 @@ async function save() {
function openStatus(row: Row) {
detail.value = row;
openDetailAction({
name: '修改状态',
name: '审核状态',
resource: `${props.definition.resource}/:identity/status`,
method: 'PATCH',
fields: [
@@ -441,7 +513,6 @@ function openStatus(row: Row) {
options: [
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
{ label: '归档', value: 3 },
],
},
],
@@ -450,15 +521,15 @@ function openStatus(row: Row) {
function confirmArchive(row: Row) {
Modal.warning({
title: '确认归档',
content: '归档后该记录将不再参与日常业务。',
title: '确认删除',
content: '删除后该记录将被归档,不再参与日常业务。',
onOk: async () => {
try {
await resourceApi.archive(
props.definition.resource,
String(row.identity),
);
Message.success('已归档');
Message.success('已删除');
await load();
} catch (error) {
Message.error((error as Error).message);