fix: resolve platform relation identities

This commit is contained in:
2026-07-27 10:11:07 +08:00
parent 98995d6968
commit 2e62238416
11 changed files with 437 additions and 80 deletions

View File

@@ -1195,6 +1195,8 @@ const contracts = [
}
];
const source = fs.readFileSync('src/api/resources.ts', 'utf8');
const backendContractSource = fs.readFileSync('../../backend/api/internal/logic/platform/resource.go', 'utf8');
const backendRouterSource = fs.readFileSync('../../backend/api/internal/routers/platform.go', 'utf8');
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText;
const resourceModule = { exports: {} };
vm.runInNewContext(compiled, { module: resourceModule, exports: resourceModule.exports });
@@ -1208,6 +1210,8 @@ function checkContracts() {
const definition = resources.find((resource) => resource.name === contract.name);
const label = `${contract.domain}/${contract.name}`;
if (!definition || definition.resource !== contract.resource || definition.mode !== contract.mode || definition.pageKind !== contract.pageKind || !sameFields(definition.fields, contract.fields)) failures.push(`${label}: mismatched contract or allowlist`);
if (!backendContractSource.includes(`Domain: "${contract.domain}"`) || !backendContractSource.includes(`Name: "${contract.name}"`)) failures.push(`${label}: absent from backend ExpectedResources`);
if (contract.name !== 'saf_event_disposal' && !backendRouterSource.includes(contract.resource)) failures.push(`${label}: absent from backend router`);
const requiredIdentity = contract.fields.filter((field) => field.required && field.key.endsWith('_identity')).map((field) => field.key);
if (definition && JSON.stringify(definition.requiredIdentities ?? []) !== JSON.stringify(requiredIdentity)) failures.push(`${label}: required identity fields do not match allowlist`);
if (contract.name === 'saf_event_disposal') continue;
@@ -1224,7 +1228,7 @@ function checkContracts() {
const readonlyPage = fs.readFileSync('src/views/shared/ReadOnlyListPage.vue', 'utf8');
if (/(CrudListPage|resourceApi\\.(create|update|archive|updateStatus))/.test(readonlyPage)) failures.push('readonly shared page: contains write surface');
const treePage = fs.readFileSync('src/views/shared/TreePage.vue', 'utf8');
if (!treePage.includes('<a-tree') || !/parent_identity|parent_id/.test(treePage) || treePage.includes('CrudListPage')) failures.push('tree shared page: missing tree semantics');
if (!treePage.includes('<a-tree') || !treePage.includes('parent_identity') || /\bparent_id\b|\bid\b/.test(treePage) || treePage.includes('CrudListPage')) failures.push('tree shared page: must be identity-first');
for (const name of ['ec_category', 'platform_menu']) { const view = path.join('src/views', name === 'ec_category' ? 'ec' : 'platform', name, 'TreePage.vue'); if (!fs.existsSync(view)) failures.push(`${name}: must use TreePage`); }
const safetyEvent = resources.find((resource) => resource.name === 'saf_event');
if (!safetyEvent?.detailActions?.some((item) => item.name === 'saf_event_disposal' && item.resource === '/safety/saf_event/:identity/disposals')) failures.push('safety event: missing disposal detail action');

View File

@@ -4,10 +4,10 @@ import { Message } from '@arco-design/web-vue';
import { computed, onMounted, ref } from 'vue';
import { resourceApi } from '@/api/resource';
import type { ResourceUiDefinition } from '@/api/resources';
type Node = Record<string, unknown> & { identity: string; parent_identity?: string; id?: number; parent_id?: number; children: Node[] };
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 tree = computed(() => { const byIdentity = new Map<string, Node>(); const byInternalId = new Map<number, Node>(); const roots: Node[] = []; list.value.forEach((item) => { const node = { ...item, children: [] }; byIdentity.set(node.identity, node); if (typeof node.id === 'number') byInternalId.set(node.id, node); }); byIdentity.forEach((item) => { const parent = item.parent_identity ? byIdentity.get(item.parent_identity) : typeof item.parent_id === 'number' ? byInternalId.get(item.parent_id) : undefined; if (parent) parent.children.push(item); else roots.push(item); }); return roots; });
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; });
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>