fix: close platform admin final audit findings

This commit is contained in:
2026-07-27 12:02:08 +08:00
parent 0ba4136e1a
commit 34e8b092b3
70 changed files with 1873 additions and 348 deletions

View File

@@ -1,37 +1,256 @@
<template>
<a-card :title="definition.title" :bordered="false">
<template #extra><a-space><a-button @click="load">刷新</a-button><a-button v-if="canCreate" type="primary" @click="openCreate">新建</a-button></a-space></template>
<a-form :model="filters" layout="inline" class="filters" @submit.prevent="load"><a-form-item label="关键字"><a-input v-model="filters.keyword" allow-clear placeholder="服务端筛选" /></a-form-item><a-button type="primary" html-type="submit">查询</a-button></a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity"><template #columns><a-table-column title="业务标识" data-index="identity" :width="220" ellipsis tooltip /><a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" ellipsis tooltip /><a-table-column title="操作" :width="190" fixed="right"><template #cell="{ record }"><a-space><a-button size="mini" @click="openDetail(record)">详情</a-button><a-button v-if="canEdit" size="mini" @click="openEdit(record)">编辑</a-button><a-button v-if="canArchive" size="mini" status="danger" @click="confirmArchive(record)">归档</a-button></a-space></template></a-table-column></template></a-table>
<template #extra>
<a-space>
<a-button @click="load">刷新</a-button>
<a-button v-if="canCreate" type="primary" @click="openCreate">新建</a-button>
</a-space>
</template>
<a-form :model="filters" layout="inline" class="filters" @submit.prevent="load">
<a-form-item label="关键字">
<a-input v-model="filters.keyword" allow-clear placeholder="服务端筛选" />
</a-form-item>
<a-button type="primary" html-type="submit">查询</a-button>
</a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="业务标识" data-index="identity" :width="220" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" ellipsis tooltip />
<a-table-column title="操作" :width="190" fixed="right">
<template #cell="{ record }">
<a-space>
<a-button size="mini" @click="openDetail(record)">详情</a-button>
<a-button v-if="canEdit" size="mini" @click="openEdit(record)">编辑</a-button>
<a-button v-if="canArchive" size="mini" status="danger" @click="confirmArchive(record)">归档</a-button>
</a-space>
</template>
</a-table-column>
</template>
</a-table>
<div class="pagination"><a-pagination :total="total" :current="page" :page-size="pageSize" show-total @change="changePage" /></div>
</a-card>
<a-drawer :visible="formVisible" :title="editingIdentity ? `编辑${definition.title}` : `新建${definition.title}`" :width="480" @cancel="formVisible = false" @ok="save"><a-form :model="form" layout="vertical"><a-form-item v-for="field in definition.fields" :key="field.key" :label="field.label" :required="field.required"><a-input v-model="form[field.key]" :placeholder="`请输入${field.label}`" /></a-form-item></a-form></a-drawer>
<a-drawer :visible="detailVisible" title="详情" :width="540" @cancel="detailVisible = false"><a-descriptions :column="1" bordered><a-descriptions-item v-for="[key, value] in detailEntries" :key="key" :label="key">{{ value ?? '-' }}</a-descriptions-item></a-descriptions><a-space class="detail-actions"><a-button v-for="action in definition.detailActions" :key="action.name" type="primary" @click="openDetailAction(action)">{{ action.name }}</a-button></a-space></a-drawer>
<a-modal :visible="actionVisible" :title="activeAction?.name" @cancel="actionVisible = false" @ok="submitDetailAction"><a-form :model="actionForm" layout="vertical"><a-form-item v-for="field in activeAction?.fields" :key="field.key" :label="field.label" :required="field.required"><a-input v-model="actionForm[field.key]" /></a-form-item></a-form></a-modal>
<a-drawer :visible="formVisible" :title="editingIdentity ? `编辑${definition.title}` : `新建${definition.title}`" :width="480" @cancel="formVisible = false" @ok="save">
<a-form :model="form" layout="vertical">
<a-form-item v-for="field in definition.fields" :key="field.key" :label="field.label" :required="field.required">
<a-switch v-if="field.type === 'boolean'" v-model="form[field.key]" />
<a-input-number v-else-if="field.type === 'number'" v-model="form[field.key]" />
<a-date-picker v-else-if="field.type === 'date'" v-model="form[field.key]" value-format="YYYY-MM-DD" />
<a-date-picker v-else-if="field.type === 'datetime'" v-model="form[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
<a-textarea v-else-if="field.type === 'json' || field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
<a-input-password v-else-if="field.type === 'password'" v-model="form[field.key]" />
<a-input v-else v-model="form[field.key]" :placeholder="`请输入${field.label}`" />
</a-form-item>
</a-form>
</a-drawer>
<a-drawer :visible="detailVisible" title="详情" :width="540" @cancel="detailVisible = false">
<a-descriptions :column="1" bordered>
<a-descriptions-item v-for="[key, value] in detailEntries" :key="key" :label="key">{{ value ?? '-' }}</a-descriptions-item>
</a-descriptions>
<a-space class="detail-actions">
<a-button v-for="action in definition.detailActions" :key="action.name" type="primary" @click="openDetailAction(action)">{{ action.name }}</a-button>
</a-space>
</a-drawer>
<a-modal :visible="actionVisible" :title="activeAction?.name" @cancel="actionVisible = false" @ok="submitDetailAction">
<a-form :model="actionForm" layout="vertical">
<a-form-item v-for="field in activeAction?.fields" :key="field.key" :label="field.label" :required="field.required">
<a-input-number v-if="field.type === 'number'" v-model="actionForm[field.key]" />
<a-switch v-else-if="field.type === 'boolean'" v-model="actionForm[field.key]" />
<a-textarea v-else-if="field.type === 'json' || field.type === 'textarea'" v-model="actionForm[field.key]" />
<a-input v-else v-model="actionForm[field.key]" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { Message, Modal } from '@arco-design/web-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { resourceApi } from '@/api/resource';
import { buildResourcePayload, isMissingField } from '@/api/resource-form';
import type { DetailAction, ResourceUiDefinition } from '@/api/resources';
type Row = Record<string, unknown>;
const props = defineProps<{ definition: ResourceUiDefinition }>();
const loading = ref(false); const page = ref(1); const pageSize = 20; const total = ref(0); const list = ref<Row[]>([]); const filters = reactive({ keyword: '' });
const formVisible = ref(false); const detailVisible = ref(false); const editingIdentity = ref(''); const form = reactive<Record<string, string>>({}); const detail = ref<Row>({});
const actionVisible = ref(false); const activeAction = ref<DetailAction>(); const actionForm = reactive<Record<string, string>>({});
const canCreate = computed(() => props.definition.mode !== 'readonly'); const canEdit = computed(() => props.definition.mode === 'writable'); const canArchive = computed(() => props.definition.mode === 'writable');
const displayFields = computed(() => props.definition.fields.filter((field) => field.key !== 'status'));
const detailEntries = computed(() => Object.entries(detail.value).filter(([key]) => key !== 'id' && !key.endsWith('_id')));
function resetForm(data?: Row) { props.definition.fields.forEach((field) => { form[field.key] = data?.[field.key] == null ? '' : String(data[field.key]); }); }
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; } catch (error) { Message.error((error as Error).message); } finally { loading.value = false; } }
function openCreate() { editingIdentity.value = ''; resetForm(); formVisible.value = true; }
function openEdit(row: Row) { editingIdentity.value = String(row.identity ?? ''); resetForm(row); formVisible.value = true; }
async function openDetail(row: Row) { try { detail.value = await resourceApi.detail<Row>(props.definition.resource, String(row.identity)); detailVisible.value = true; } catch (error) { Message.error((error as Error).message); } }
function openDetailAction(action: DetailAction) { activeAction.value = action; action.fields.forEach((field) => { actionForm[field.key] = ''; }); actionVisible.value = true; }
async function submitDetailAction() { const action = activeAction.value; if (!action) return; if (action.fields.some((field) => field.required && !actionForm[field.key])) { Message.warning('请填写必填字段'); return; } try { await resourceApi.create(action.resource.replace(':identity', String(detail.value.identity)), actionForm); Message.success('操作成功'); actionVisible.value = false; await openDetail(detail.value); } catch (error) { Message.error((error as Error).message); } }
async function save() { if (props.definition.fields.some((field) => field.required && !form[field.key])) { Message.warning('请填写必填字段'); return; } try { if (editingIdentity.value) await resourceApi.update(props.definition.resource, editingIdentity.value, form); else await resourceApi.create(props.definition.resource, form); Message.success('保存成功'); formVisible.value = false; await load(); } catch (error) { Message.error((error as Error).message); } }
function confirmArchive(row: Row) { Modal.warning({ title: '确认归档', content: '归档后该记录将不再参与日常业务。', onOk: async () => { try { await resourceApi.archive(props.definition.resource, String(row.identity)); Message.success('已归档'); await load(); } catch (error) { Message.error((error as Error).message); } } }); }
async function changePage(next: number) { page.value = next; await load(); }
const loading = ref(false);
const page = ref(1);
const pageSize = 20;
const total = ref(0);
const list = ref<Row[]>([]);
const filters = reactive({ keyword: '' });
const formVisible = ref(false);
const detailVisible = ref(false);
const editingIdentity = ref('');
const form = reactive<Record<string, any>>({});
const detail = ref<Row>({});
const actionVisible = ref(false);
const activeAction = ref<DetailAction>();
const actionForm = reactive<Record<string, any>>({});
const canCreate = computed(() => props.definition.mode !== 'readonly');
const canEdit = computed(() => props.definition.mode === 'writable');
const canArchive = computed(() => props.definition.mode === 'writable');
const displayFields = computed(() =>
props.definition.fields.filter((field) => field.key !== 'status'),
);
const detailEntries = computed(() =>
Object.entries(detail.value).filter(
([key]) => key !== 'id' && !key.endsWith('_id'),
),
);
function resetForm(data?: Row) {
for (const field of props.definition.fields) {
const value = data?.[field.key];
form[field.key] =
field.type === 'json' && value != null && typeof value !== 'string'
? JSON.stringify(value, null, 2)
: value == null
? undefined
: value;
}
}
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;
} catch (error) {
Message.error((error as Error).message);
} finally {
loading.value = false;
}
}
function openCreate() {
editingIdentity.value = '';
resetForm();
formVisible.value = true;
}
function openEdit(row: Row) {
editingIdentity.value = String(row.identity ?? '');
resetForm(row);
formVisible.value = true;
}
async function openDetail(row: Row) {
try {
detail.value = await resourceApi.detail<Row>(
props.definition.resource,
String(row.identity),
);
detailVisible.value = true;
} catch (error) {
Message.error((error as Error).message);
}
}
function openDetailAction(action: DetailAction) {
activeAction.value = action;
for (const field of action.fields) actionForm[field.key] = undefined;
actionVisible.value = true;
}
async function submitDetailAction() {
const action = activeAction.value;
if (!action) return;
if (
action.fields.some(
(field) => field.required && isMissingField(actionForm[field.key]),
)
) {
Message.warning('请填写必填字段');
return;
}
try {
const payload = {
...action.payload,
...buildResourcePayload(action.fields, actionForm),
};
await resourceApi.create(
action.resource.replace(':identity', String(detail.value.identity)),
payload,
);
Message.success('操作成功');
actionVisible.value = false;
await openDetail(detail.value);
} catch (error) {
Message.error((error as Error).message);
}
}
async function save() {
if (
props.definition.fields.some(
(field) => field.required && isMissingField(form[field.key]),
)
) {
Message.warning('请填写必填字段');
return;
}
try {
const payload = buildResourcePayload(props.definition.fields, form);
if (editingIdentity.value)
await resourceApi.update(
props.definition.resource,
editingIdentity.value,
payload,
);
else await resourceApi.create(props.definition.resource, payload);
Message.success('保存成功');
formVisible.value = false;
await load();
} catch (error) {
Message.error((error as Error).message);
}
}
function confirmArchive(row: Row) {
Modal.warning({
title: '确认归档',
content: '归档后该记录将不再参与日常业务。',
onOk: async () => {
try {
await resourceApi.archive(
props.definition.resource,
String(row.identity),
);
Message.success('已归档');
await load();
} catch (error) {
Message.error((error as Error).message);
}
},
});
}
async function changePage(next: number) {
page.value = next;
await load();
}
onMounted(load);
</script>
<style scoped lang="less">.filters { margin-bottom: 16px; } .pagination { display: flex; justify-content: flex-end; margin-top: 16px; }</style>
<style scoped lang="less">
.filters {
margin-bottom: 16px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.detail-actions {
margin-top: 16px;
}
</style>

View File

@@ -1,25 +1,147 @@
<template>
<a-card :title="definition.title" :bordered="false">
<template #extra><a-button @click="load">刷新</a-button></template>
<a-form :model="filters" layout="inline" class="filters" @submit.prevent="load"><a-form-item label="关键字"><a-input v-model="filters.keyword" allow-clear placeholder="服务端筛选" /></a-form-item><a-button type="primary" html-type="submit">查询</a-button></a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity"><template #columns><a-table-column title="业务标识" data-index="identity" :width="220" ellipsis tooltip /><a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" ellipsis tooltip /><a-table-column title="操作" :width="90"><template #cell="{ record }"><a-button size="mini" @click="openDetail(record)">详情</a-button></template></a-table-column></template></a-table>
<a-form :model="filters" layout="inline" class="filters" @submit.prevent="load">
<a-form-item label="关键字"><a-input v-model="filters.keyword" allow-clear placeholder="服务端筛选" /></a-form-item>
<a-button type="primary" html-type="submit">查询</a-button>
</a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="业务标识" data-index="identity" :width="220" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" ellipsis tooltip />
<a-table-column title="操作" :width="90"><template #cell="{ record }"><a-button size="mini" @click="openDetail(record)">详情</a-button></template></a-table-column>
</template>
</a-table>
<div class="pagination"><a-pagination :total="total" :current="page" :page-size="pageSize" show-total @change="changePage" /></div>
</a-card>
<a-drawer :visible="detailVisible" title="详情" :width="540" @cancel="detailVisible = false"><a-descriptions :column="1" bordered><a-descriptions-item v-for="[key, value] in detailEntries" :key="key" :label="key">{{ value ?? '-' }}</a-descriptions-item></a-descriptions></a-drawer>
<a-drawer :visible="detailVisible" title="详情" :width="540" @cancel="detailVisible = false">
<a-descriptions :column="1" bordered><a-descriptions-item v-for="[key, value] in detailEntries" :key="key" :label="key">{{ value ?? '-' }}</a-descriptions-item></a-descriptions>
<a-space v-if="definition.detailActions?.length" class="detail-actions">
<a-button v-for="action in definition.detailActions" :key="action.name" :status="action.payload?.status === 'rejected' ? 'danger' : 'normal'" type="primary" @click="openDetailAction(action)">{{ action.name }}</a-button>
</a-space>
</a-drawer>
<a-modal :visible="actionVisible" :title="activeAction?.name" @cancel="actionVisible = false" @ok="submitDetailAction">
<a-form :model="actionForm" layout="vertical">
<a-form-item v-for="field in activeAction?.fields" :key="field.key" :label="field.label" :required="field.required">
<a-textarea v-if="field.type === 'textarea' || field.type === 'json'" v-model="actionForm[field.key]" />
<a-input v-else v-model="actionForm[field.key]" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { resourceApi } from '@/api/resource';
import type { ResourceUiDefinition } from '@/api/resources';
import { buildResourcePayload, isMissingField } from '@/api/resource-form';
import type { DetailAction, ResourceUiDefinition } from '@/api/resources';
type Row = Record<string, unknown>;
const props = defineProps<{ definition: ResourceUiDefinition }>();
const loading = ref(false); const page = ref(1); const pageSize = 20; const total = ref(0); const list = ref<Row[]>([]); const filters = reactive({ keyword: '' }); const detail = ref<Row>({}); const detailVisible = ref(false);
const displayFields = computed(() => props.definition.fields.filter((field) => field.key !== 'status'));
const detailEntries = computed(() => Object.entries(detail.value).filter(([key]) => key !== 'id' && !key.endsWith('_id')));
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; } catch (error) { Message.error((error as Error).message); } finally { loading.value = false; } }
async function openDetail(row: Row) { try { detail.value = await resourceApi.detail<Row>(props.definition.resource, String(row.identity)); detailVisible.value = true; } catch (error) { Message.error((error as Error).message); } }
async function changePage(next: number) { page.value = next; await load(); }
const loading = ref(false);
const page = ref(1);
const pageSize = 20;
const total = ref(0);
const list = ref<Row[]>([]);
const filters = reactive({ keyword: '' });
const detail = ref<Row>({});
const detailVisible = ref(false);
const actionVisible = ref(false);
const activeAction = ref<DetailAction>();
const actionForm = reactive<Record<string, any>>({});
const displayFields = computed(() =>
props.definition.fields.filter((field) => field.key !== 'status'),
);
const detailEntries = computed(() =>
Object.entries(detail.value).filter(
([key]) => key !== 'id' && !key.endsWith('_id'),
),
);
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;
} catch (error) {
Message.error((error as Error).message);
} finally {
loading.value = false;
}
}
async function openDetail(row: Row) {
try {
detail.value = await resourceApi.detail<Row>(
props.definition.resource,
String(row.identity),
);
detailVisible.value = true;
} catch (error) {
Message.error((error as Error).message);
}
}
function openDetailAction(action: DetailAction) {
activeAction.value = action;
for (const field of action.fields) actionForm[field.key] = undefined;
actionVisible.value = true;
}
async function submitDetailAction() {
const action = activeAction.value;
if (!action) return;
if (
action.fields.some(
(field) => field.required && isMissingField(actionForm[field.key]),
)
) {
Message.warning('请填写必填字段');
return;
}
try {
const payload = {
...action.payload,
...buildResourcePayload(action.fields, actionForm),
};
await resourceApi.create(
action.resource.replace(':identity', String(detail.value.identity)),
payload,
);
Message.success('审批完成');
actionVisible.value = false;
await openDetail(detail.value);
await load();
} catch (error) {
Message.error((error as Error).message);
}
}
async function changePage(next: number) {
page.value = next;
await load();
}
onMounted(load);
</script>
<style scoped lang="less">.filters { margin-bottom: 16px; } .pagination { display: flex; justify-content: flex-end; margin-top: 16px; }</style>
<style scoped lang="less">
.filters {
margin-bottom: 16px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.detail-actions {
margin-top: 16px;
}
</style>

View File

@@ -1,17 +1,138 @@
<template><a-card :title="definition.title" :bordered="false"><template #extra><a-space><a-button @click="load">刷新</a-button><a-button v-if="canWrite" type="primary" @click="openCreate">新增</a-button></a-space></template><a-tree :data="tree" :loading="loading" :field-names="{ key: 'identity', title: 'name', children: 'children' }"><template #title="node"><a-space>{{ node.title }}<a-button v-if="canWrite" size="mini" @click.stop="openEdit(node)">编辑</a-button></a-space></template></a-tree></a-card><a-drawer :visible="formVisible" :title="editingIdentity ? '编辑' + definition.title : '新增' + definition.title" :width="480" @cancel="formVisible = false" @ok="save"><a-form :model="form" layout="vertical"><a-form-item v-for="field in definition.fields" :key="field.key" :label="field.label" :required="field.required"><a-input v-model="form[field.key]" /></a-form-item></a-form></a-drawer></template>
<template>
<a-card :title="definition.title" :bordered="false">
<template #extra>
<a-space>
<a-button @click="load">刷新</a-button>
<a-button v-if="canWrite" type="primary" @click="openCreate">新增</a-button>
</a-space>
</template>
<a-tree :data="tree" :loading="loading" :field-names="{ key: 'identity', title: 'name', children: 'children' }">
<template #title="node">
<a-space>
{{ node.title }}
<a-button v-if="canWrite" size="mini" @click.stop="openEdit(node)">编辑</a-button>
<a-button v-if="canWrite" size="mini" status="danger" @click.stop="confirmArchive(node)">归档</a-button>
</a-space>
</template>
</a-tree>
</a-card>
<a-drawer :visible="formVisible" :title="editingIdentity ? `编辑${definition.title}` : `新增${definition.title}`" :width="480" @cancel="formVisible = false" @ok="save">
<a-form :model="form" layout="vertical">
<a-form-item v-for="field in definition.fields" :key="field.key" :label="field.label" :required="field.required">
<a-input-number v-if="field.type === 'number'" v-model="form[field.key]" />
<a-switch v-else-if="field.type === 'boolean'" v-model="form[field.key]" />
<a-input v-else v-model="form[field.key]" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue';
import { Message, Modal } from '@arco-design/web-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { resourceApi } from '@/api/resource';
import { buildResourcePayload, isMissingField } from '@/api/resource-form';
import type { ResourceUiDefinition } from '@/api/resources';
type Node = Record<string, unknown> & { identity: string; parent_identity?: string; children: Node[] };
const props = defineProps<{ definition: ResourceUiDefinition }>(); const loading = ref(false); const list = ref<Node[]>([]); const formVisible = ref(false); const editingIdentity = ref(''); const form = reactive<Record<string, string>>({});
type Node = Record<string, unknown> & {
identity: string;
parent_identity?: string;
children: Node[];
};
const props = defineProps<{ definition: ResourceUiDefinition }>();
const loading = ref(false);
const list = ref<Node[]>([]);
const formVisible = ref(false);
const editingIdentity = ref('');
const form = reactive<Record<string, any>>({});
const canWrite = computed(() => props.definition.mode === 'writable');
const tree = computed(() => { const byIdentity = new Map<string, Node>(); const roots: Node[] = []; list.value.forEach((item) => byIdentity.set(item.identity, { ...item, children: [] })); byIdentity.forEach((item) => { const parent = item.parent_identity ? byIdentity.get(item.parent_identity) : undefined; if (parent) parent.children.push(item); else roots.push(item); }); return roots; });
function reset(data?: Node) { props.definition.fields.forEach((field) => { form[field.key] = data?.[field.key] == null ? '' : String(data[field.key]); }); }
function openCreate() { editingIdentity.value = ''; reset(); formVisible.value = true; }
function openEdit(node: Node) { editingIdentity.value = node.identity; reset(node); formVisible.value = true; }
async function save() { if (props.definition.fields.some((field) => field.required && !form[field.key])) { Message.warning('请填写必填字段'); return; } try { if (editingIdentity.value) await resourceApi.update(props.definition.resource, editingIdentity.value, form); else await resourceApi.create(props.definition.resource, form); formVisible.value = false; await load(); } catch (error) { Message.error((error as Error).message); } }
async function load() { loading.value = true; try { list.value = (await resourceApi.list<Node>(props.definition.resource, 1, 500)).list; } catch (error) { Message.error((error as Error).message); } finally { loading.value = false; } }
const tree = computed(() => {
const byIdentity = new Map<string, Node>();
const roots: Node[] = [];
for (const item of list.value)
byIdentity.set(item.identity, { ...item, children: [] });
for (const item of byIdentity.values()) {
const parent = item.parent_identity
? byIdentity.get(item.parent_identity)
: undefined;
if (parent) parent.children.push(item);
else roots.push(item);
}
return roots;
});
function reset(data?: Node) {
for (const field of props.definition.fields) {
const value = data?.[field.key];
form[field.key] = value == null ? undefined : value;
}
}
function openCreate() {
editingIdentity.value = '';
reset();
formVisible.value = true;
}
function openEdit(node: Node) {
editingIdentity.value = node.identity;
reset(node);
formVisible.value = true;
}
async function save() {
if (
props.definition.fields.some(
(field) => field.required && isMissingField(form[field.key]),
)
) {
Message.warning('请填写必填字段');
return;
}
try {
const payload = buildResourcePayload(props.definition.fields, form);
if (editingIdentity.value)
await resourceApi.update(
props.definition.resource,
editingIdentity.value,
payload,
);
else await resourceApi.create(props.definition.resource, payload);
formVisible.value = false;
await load();
} catch (error) {
Message.error((error as Error).message);
}
}
function confirmArchive(node: Node) {
Modal.warning({
title: '确认归档',
content: `归档“${String(node.name ?? node.identity)}”后,其历史数据仍会保留。`,
onOk: async () => {
try {
await resourceApi.archive(props.definition.resource, node.identity);
Message.success('已归档');
await load();
} catch (error) {
Message.error((error as Error).message);
}
},
});
}
async function load() {
loading.value = true;
try {
list.value = (
await resourceApi.list<Node>(props.definition.resource, 1, 500)
).list;
} catch (error) {
Message.error((error as Error).message);
} finally {
loading.value = false;
}
}
onMounted(load);
</script>