2026-07-27 12:02:08 +08:00
|
|
|
<template>
|
|
|
|
|
<a-card :title="definition.title" :bordered="false">
|
|
|
|
|
<template #extra>
|
|
|
|
|
<a-space>
|
|
|
|
|
<a-button @click="load">刷新</a-button>
|
2026-07-28 14:26:09 +08:00
|
|
|
<a-button v-if="canCreate" type="primary" @click="openCreate">新增</a-button>
|
2026-07-27 12:02:08 +08:00
|
|
|
</a-space>
|
|
|
|
|
</template>
|
|
|
|
|
<a-tree :data="tree" :loading="loading" :field-names="{ key: 'identity', title: 'name', children: 'children' }">
|
|
|
|
|
<template #title="node">
|
|
|
|
|
<a-space>
|
|
|
|
|
{{ node.title }}
|
2026-07-28 14:26:09 +08:00
|
|
|
<a-button v-if="canEdit" size="mini" @click.stop="openEdit(node)">编辑</a-button>
|
2026-07-29 13:18:52 +08:00
|
|
|
<a-button v-if="canChangeStatus" size="mini" @click.stop="confirmStatus(node)">{{ node.status === 1 ? '停用' : '启用' }}</a-button>
|
2026-07-28 14:26:09 +08:00
|
|
|
<a-button v-if="canArchive" size="mini" status="danger" @click.stop="confirmArchive(node)">归档</a-button>
|
2026-07-27 12:02:08 +08:00
|
|
|
</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">
|
2026-07-28 14:26:09 +08:00
|
|
|
<a-input-number v-if="field.type === 'number' || field.type === 'money'" v-model="form[field.key]" :precision="field.type === 'money' ? 2 : 0" />
|
2026-07-27 12:02:08 +08:00
|
|
|
<a-switch v-else-if="field.type === 'boolean'" v-model="form[field.key]" />
|
2026-07-28 14:26:09 +08:00
|
|
|
<a-select v-else-if="field.type === 'identity'" v-model="form[field.key]" allow-clear allow-search>
|
|
|
|
|
<a-option v-for="option in list.filter((item) => item.identity !== editingIdentity)" :key="option.identity" :value="option.identity">
|
2026-07-29 14:25:38 +08:00
|
|
|
{{ option.name ?? option.group_code ?? option.identity }}
|
2026-07-28 14:26:09 +08:00
|
|
|
</a-option>
|
|
|
|
|
</a-select>
|
2026-07-27 12:02:08 +08:00
|
|
|
<a-input v-else v-model="form[field.key]" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-form>
|
|
|
|
|
</a-drawer>
|
|
|
|
|
</template>
|
|
|
|
|
|
2026-07-27 09:26:52 +08:00
|
|
|
<script setup lang="ts">
|
2026-07-27 12:02:08 +08:00
|
|
|
import { Message, Modal } from '@arco-design/web-vue';
|
2026-07-27 10:25:04 +08:00
|
|
|
import { computed, onMounted, reactive, ref } from 'vue';
|
2026-07-27 09:26:52 +08:00
|
|
|
import { resourceApi } from '@/api/resource';
|
2026-07-27 12:02:08 +08:00
|
|
|
import { buildResourcePayload, isMissingField } from '@/api/resource-form';
|
2026-07-27 09:26:52 +08:00
|
|
|
import type { ResourceUiDefinition } from '@/api/resources';
|
2026-07-28 14:26:09 +08:00
|
|
|
import { useUserStore } from '@/store';
|
2026-07-27 12:02:08 +08:00
|
|
|
|
|
|
|
|
type Node = Record<string, unknown> & {
|
|
|
|
|
identity: string;
|
|
|
|
|
parent_identity?: string;
|
|
|
|
|
children: Node[];
|
|
|
|
|
};
|
|
|
|
|
const props = defineProps<{ definition: ResourceUiDefinition }>();
|
2026-07-28 14:26:09 +08:00
|
|
|
const userStore = useUserStore();
|
2026-07-27 12:02:08 +08:00
|
|
|
const loading = ref(false);
|
|
|
|
|
const list = ref<Node[]>([]);
|
|
|
|
|
const formVisible = ref(false);
|
|
|
|
|
const editingIdentity = ref('');
|
|
|
|
|
const form = reactive<Record<string, any>>({});
|
2026-07-28 14:26:09 +08:00
|
|
|
const rootAllowed = computed(
|
|
|
|
|
() => props.definition.name !== 'platform_menu' || userStore.role === 'root',
|
|
|
|
|
);
|
|
|
|
|
const canCreate = computed(
|
|
|
|
|
() => props.definition.canCreate && rootAllowed.value,
|
|
|
|
|
);
|
|
|
|
|
const canEdit = computed(() => props.definition.canEdit && rootAllowed.value);
|
|
|
|
|
const canChangeStatus = computed(
|
|
|
|
|
() => props.definition.canChangeStatus && rootAllowed.value,
|
|
|
|
|
);
|
|
|
|
|
const canArchive = computed(
|
|
|
|
|
() => props.definition.canArchive && rootAllowed.value,
|
|
|
|
|
);
|
2026-07-27 12:02:08 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 14:26:09 +08:00
|
|
|
function confirmStatus(node: Node) {
|
2026-07-29 13:18:52 +08:00
|
|
|
const status = node.status === 1 ? 2 : 1;
|
2026-07-28 14:26:09 +08:00
|
|
|
Modal.warning({
|
2026-07-29 13:18:52 +08:00
|
|
|
title: status === 1 ? '确认启用' : '确认停用',
|
|
|
|
|
content: `确定要${status === 1 ? '启用' : '停用'}“${String(node.name ?? node.identity)}”吗?`,
|
2026-07-28 14:26:09 +08:00
|
|
|
onOk: async () => {
|
|
|
|
|
try {
|
|
|
|
|
await resourceApi.updateStatus(
|
|
|
|
|
props.definition.resource,
|
|
|
|
|
node.identity,
|
|
|
|
|
status,
|
|
|
|
|
);
|
|
|
|
|
Message.success('状态已更新');
|
|
|
|
|
await load();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
Message.error((error as Error).message);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 12:02:08 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 09:26:52 +08:00
|
|
|
onMounted(load);
|
|
|
|
|
</script>
|