fix: close platform admin final audit findings
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"preview": "pnpm run build && vite preview --host",
|
||||
"type:check": "vue-tsc -p tsconfig.build.json --noEmit --skipLibCheck",
|
||||
"audit:platform": "node scripts/audit-check.mjs",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --write .",
|
||||
"lint": "biome lint .",
|
||||
"lint:fix": "biome lint --write .",
|
||||
"format": "biome format --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
65
frontend/platform_admin/scripts/final-important.test.mjs
Normal file
65
frontend/platform_admin/scripts/final-important.test.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import vm from 'node:vm';
|
||||
import ts from 'typescript';
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const fromProjectRoot = (...segments) => path.join(projectRoot, ...segments);
|
||||
|
||||
function loadResources() {
|
||||
const compiled = ts.transpileModule(fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8'), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const resourceModule = { exports: {} };
|
||||
vm.runInNewContext(compiled, { module: resourceModule, exports: resourceModule.exports });
|
||||
return resourceModule.exports.resources;
|
||||
}
|
||||
|
||||
test('资源字段声明保留数字、布尔、时间与 JSON 类型', () => {
|
||||
const resources = loadResources();
|
||||
const field = (resource, key) => resources.find((item) => item.name === resource).fields.find((item) => item.key === key);
|
||||
assert.equal(field('ec_product', 'price_amount').type, 'number');
|
||||
assert.equal(field('ec_product_image', 'is_cover').type, 'boolean');
|
||||
assert.equal(field('delivery_track', 'started_at').type, 'datetime');
|
||||
assert.equal(field('dev_telemetry', 'payload').type, 'json');
|
||||
});
|
||||
|
||||
test('表单载荷构造器省略空的可选关系并转换字段类型', async () => {
|
||||
const resourceFormPath = fromProjectRoot('src/api/resource-form.ts');
|
||||
assert.ok(fs.existsSync(resourceFormPath), 'resource-form.ts should define the payload boundary');
|
||||
const source = fs.readFileSync(resourceFormPath, '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 });
|
||||
const fields = [
|
||||
{ key: 'gas_basic_identity', label: '气站', type: 'identity' },
|
||||
{ key: 'quantity', label: '数量', type: 'number' },
|
||||
{ key: 'selected', label: '选中', type: 'boolean' },
|
||||
];
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(resourceModule.exports.buildResourcePayload(fields, {
|
||||
gas_basic_identity: '',
|
||||
quantity: '2',
|
||||
selected: false,
|
||||
}))),
|
||||
{ quantity: 2, selected: false },
|
||||
);
|
||||
});
|
||||
|
||||
test('审批只读页提供同意和驳回操作', () => {
|
||||
const source = fs.readFileSync(fromProjectRoot('src/views/shared/ReadOnlyListPage.vue'), 'utf8');
|
||||
const resources = fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8');
|
||||
assert.match(resources, /aud_approval[\s\S]*\/audit\/aud_approval\/:identity\/approve/);
|
||||
assert.match(source, /submitDetailAction/);
|
||||
});
|
||||
|
||||
test('树页面通过资源归档接口归档节点', () => {
|
||||
const source = fs.readFileSync(fromProjectRoot('src/views/shared/TreePage.vue'), 'utf8');
|
||||
assert.match(source, /resourceApi\.archive/);
|
||||
assert.match(source, /Modal\.(warning|confirm)/);
|
||||
});
|
||||
41
frontend/platform_admin/src/api/resource-form.ts
Normal file
41
frontend/platform_admin/src/api/resource-form.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ResourceField } from './resources';
|
||||
|
||||
export type ResourceFormValue = string | number | boolean | undefined;
|
||||
|
||||
export function isMissingField(value: ResourceFormValue | null): boolean {
|
||||
return value === '' || value === null || value === undefined;
|
||||
}
|
||||
|
||||
export function buildResourcePayload(
|
||||
fields: ResourceField[],
|
||||
form: Record<string, ResourceFormValue>,
|
||||
): Record<string, unknown> {
|
||||
const payload: Record<string, unknown> = {};
|
||||
for (const field of fields) {
|
||||
const value = form[field.key];
|
||||
if (isMissingField(value)) {
|
||||
if (!field.required) continue;
|
||||
payload[field.key] = value;
|
||||
continue;
|
||||
}
|
||||
switch (field.type) {
|
||||
case 'number': {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number))
|
||||
throw new Error(`${field.label}必须是数字`);
|
||||
payload[field.key] = number;
|
||||
break;
|
||||
}
|
||||
case 'boolean':
|
||||
payload[field.key] = value === true || value === 'true';
|
||||
break;
|
||||
case 'json':
|
||||
payload[field.key] =
|
||||
typeof value === 'string' ? JSON.parse(value) : value;
|
||||
break;
|
||||
default:
|
||||
payload[field.key] = value;
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
@@ -1,69 +1,644 @@
|
||||
export type ResourceMode = 'writable' | 'readonly' | 'append_only';
|
||||
export type ResourcePageKind = 'list' | 'tree';
|
||||
export type ResourceField = { key: string; label: string; required?: boolean };
|
||||
export type DetailAction = { name: string; resource: string; fields: ResourceField[] };
|
||||
export type ResourceUiDefinition = { key: string; name: string; resource: string; title: string; mode: ResourceMode; pageKind: ResourcePageKind; fields: ResourceField[]; requiredIdentities: string[]; detailActions?: DetailAction[] };
|
||||
|
||||
const labels: Record<string, string> = { code: '编码', name: '名称', credit_code: '统一信用代码', principal: '负责人', address: '地址', longitude: '经度', latitude: '纬度', username: '用户名', password: '密码', display_name: '显示名称', role_code: '角色编码', delivery_code: '配送编码', phone: '联系电话', avatar: '头像', work_status: '工作状态', credential_type: '资质类型', credential_no: '资质编号', expired_at: '到期时间', real_name: '实名姓名', is_default: '默认地址', device_no: '设备编号', model: '设备型号', online_status: '在线状态', effective_at: '生效时间', recorded_at: '采集时间', payload: '遥测数据', rule_code: '规则编码', version_no: '版本号', threshold: '阈值', action: '处置动作', gray_scope: '灰度范围', event_code: '事件编码', level: '事件等级', title: '标题', sla_at: '处置时限', result: '检查结果', evidence_uri: '凭证地址', reason: '处置原因', sort_no: '排序号', product_code: '商品编码', price_amount: '售价', stock_quantity: '库存', value: '属性值', image_uri: '图片地址', is_cover: '封面图', quantity: '数量', selected: '是否选中', order_no: '订单号', total_amount: '订单金额', product_snapshot: '商品快照', sale_amount: '成交金额', score: '评分', channel: '渠道', amount: '金额', paid_at: '支付时间', settlement_no: '结算单号', subject_type: '结算对象类型', period_start: '结算开始时间', period_end: '结算结束时间', bill_date: '账单日期', difference_amount: '差异金额', content_type: '内容类型', body: '正文', publish_status: '发布状态', template_code: '模板编码', content: '内容', ticket_no: '工单号', category: '分类', priority: '优先级', platform_role_code: '平台角色', data_scope: '数据范围', menu_code: '菜单编码', icon: '图标', path: '路径', balance_amount: '余额', change_amount: '变动金额', balance_after: '变动后余额', report_code: '报表编码', metric_code: '指标编码', captured_at: '采集时间', operator_identity: '操作人标识', resource_type: '资源类型', handled_at: '处理时间', status: '状态' };
|
||||
const field = (value: string): ResourceField => { const key = value.replace(/!$/, ''); return { key, label: labels[key] ?? (key.endsWith('_identity') ? '关联业务标识' : '业务字段'), required: value.endsWith('!') || undefined }; };
|
||||
const titles: Record<string, string> = { gas_basic: '气站管理', gas_account: '气站账户', delivery_basic: '配送点管理', delivery_account: '配送账户', delivery_task: '配送任务', delivery_track: '配送轨迹', delivery_track_point: '轨迹点', staff_account: '服务人员', staff_credential: '人员资质', user_account: '用户账户', user_address: '用户地址', user_service_relation: '用户服务关系', dev_smart_cylinder_valve: '智能钢瓶阀', dev_device_binding: '设备绑定', dev_telemetry: '设备遥测', saf_rule: '安全规则', saf_event: '安全事件', saf_inspection: '安全检查', saf_event_disposal: '事件处置', ec_category: '商品分类', ec_product: '商品管理', ec_product_attribute: '商品属性', ec_product_image: '商品图片', ec_cart: '购物车', ec_order: '订单管理', ec_order_item: '订单明细', ec_review: '商品评价', fin_payment: '支付记录', fin_settlement: '财务结算', fin_reconciliation: '财务对账', cnt_content: '内容管理', ntf_template: '通知模板', cs_ticket: '客服工单', platfrom_account: '平台账户', platform_role: '平台角色', platform_menu: '平台菜单', wallet: '钱包', wallet_ledger: '钱包流水', wallet_recharge: '钱包充值', wallet_withdrawal: '钱包提现', report: '报表', report_item: '报表项目', report_metric_snapshot: '指标快照', aud_operation_log: '操作审计', aud_export_log: '导出审计', aud_approval: '审批审计' };
|
||||
const define = (name: string, resource: string, mode: ResourceMode, pageKind: ResourcePageKind, keys: string[], detailActions?: DetailAction[]): ResourceUiDefinition => {
|
||||
const fields = keys.map(field);
|
||||
return { key: name.replace(/_/g, '-'), name, resource, title: titles[name] ?? '业务资源', mode, pageKind, fields, requiredIdentities: fields.filter((item) => item.required && item.key.endsWith('_identity')).map((item) => item.key), ...(detailActions ? { detailActions } : {}) };
|
||||
export type ResourceFieldType =
|
||||
| 'text'
|
||||
| 'password'
|
||||
| 'identity'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'datetime'
|
||||
| 'json'
|
||||
| 'textarea';
|
||||
export type ResourceField = {
|
||||
key: string;
|
||||
label: string;
|
||||
type: ResourceFieldType;
|
||||
required?: boolean;
|
||||
};
|
||||
const action = (name: string, resource: string, keys: string[]): DetailAction => ({ name, resource, fields: keys.map(field) });
|
||||
export type DetailAction = {
|
||||
name: string;
|
||||
resource: string;
|
||||
fields: ResourceField[];
|
||||
payload?: Record<string, unknown>;
|
||||
};
|
||||
export type ResourceUiDefinition = {
|
||||
key: string;
|
||||
name: string;
|
||||
resource: string;
|
||||
title: string;
|
||||
mode: ResourceMode;
|
||||
pageKind: ResourcePageKind;
|
||||
fields: ResourceField[];
|
||||
requiredIdentities: string[];
|
||||
detailActions?: DetailAction[];
|
||||
};
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
code: '编码',
|
||||
name: '名称',
|
||||
credit_code: '统一信用代码',
|
||||
principal: '负责人',
|
||||
address: '地址',
|
||||
longitude: '经度',
|
||||
latitude: '纬度',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
display_name: '显示名称',
|
||||
role_code: '角色编码',
|
||||
delivery_code: '配送编码',
|
||||
phone: '联系电话',
|
||||
avatar: '头像',
|
||||
work_status: '工作状态',
|
||||
credential_type: '资质类型',
|
||||
credential_no: '资质编号',
|
||||
expired_at: '到期时间',
|
||||
real_name: '实名姓名',
|
||||
is_default: '默认地址',
|
||||
device_no: '设备编号',
|
||||
model: '设备型号',
|
||||
online_status: '在线状态',
|
||||
effective_at: '生效时间',
|
||||
reported_at: '上报时间',
|
||||
payload: '遥测数据',
|
||||
rule_code: '规则编码',
|
||||
version_no: '版本号',
|
||||
threshold: '阈值',
|
||||
action: '处置动作',
|
||||
gray_scope: '灰度范围',
|
||||
event_code: '事件编码',
|
||||
level: '事件等级',
|
||||
title: '标题',
|
||||
sla_at: '处置时限',
|
||||
result: '检查结果',
|
||||
evidence_uri: '凭证地址',
|
||||
reason: '处置原因',
|
||||
sort_no: '排序号',
|
||||
product_code: '商品编码',
|
||||
price_amount: '售价',
|
||||
stock_quantity: '库存',
|
||||
value: '属性值',
|
||||
image_uri: '图片地址',
|
||||
is_cover: '封面图',
|
||||
quantity: '数量',
|
||||
selected: '是否选中',
|
||||
order_no: '订单号',
|
||||
total_amount: '订单金额',
|
||||
product_snapshot: '商品快照',
|
||||
sale_amount: '成交金额',
|
||||
score: '评分',
|
||||
channel: '渠道',
|
||||
amount: '金额',
|
||||
paid_at: '支付时间',
|
||||
settlement_no: '结算单号',
|
||||
subject_type: '结算对象类型',
|
||||
period_start: '结算开始时间',
|
||||
period_end: '结算结束时间',
|
||||
bill_date: '账单日期',
|
||||
difference_amount: '差异金额',
|
||||
content_type: '内容类型',
|
||||
body: '正文',
|
||||
publish_status: '发布状态',
|
||||
template_code: '模板编码',
|
||||
content: '内容',
|
||||
ticket_no: '工单号',
|
||||
category: '分类',
|
||||
priority: '优先级',
|
||||
platform_role_code: '平台角色',
|
||||
data_scope: '数据范围',
|
||||
menu_code: '菜单编码',
|
||||
icon: '图标',
|
||||
path: '路径',
|
||||
balance_amount: '余额',
|
||||
frozen_amount: '冻结金额',
|
||||
balance_after: '变动后余额',
|
||||
report_code: '报表编码',
|
||||
report_type: '报表类型',
|
||||
stat_period: '统计周期',
|
||||
generated_at: '生成时间',
|
||||
dimension: '维度',
|
||||
metric_code: '指标编码',
|
||||
metric_value: '指标值',
|
||||
scope_type: '范围类型',
|
||||
stat_at: '统计时间',
|
||||
applicant_identity: '申请人标识',
|
||||
business_type: '业务类型',
|
||||
business_identity: '业务标识',
|
||||
handler_identity: '处理人标识',
|
||||
opinion: '审批意见',
|
||||
purpose: '用途',
|
||||
field_scope: '字段范围',
|
||||
approved_at: '批准时间',
|
||||
file_uri: '文件地址',
|
||||
operator_identity: '操作人标识',
|
||||
object_identity: '对象标识',
|
||||
resource_type: '资源类型',
|
||||
handled_at: '处理时间',
|
||||
created_at: '创建时间',
|
||||
status: '状态',
|
||||
};
|
||||
const numberFields = new Set([
|
||||
'version_no',
|
||||
'level',
|
||||
'sort_no',
|
||||
'price_amount',
|
||||
'stock_quantity',
|
||||
'quantity',
|
||||
'total_amount',
|
||||
'sale_amount',
|
||||
'score',
|
||||
'amount',
|
||||
'difference_amount',
|
||||
'balance_amount',
|
||||
'frozen_amount',
|
||||
'balance_after',
|
||||
]);
|
||||
const booleanFields = new Set(['is_default', 'is_cover', 'selected']);
|
||||
const dateFields = new Set(['bill_date']);
|
||||
const datetimeFields = new Set([
|
||||
'expired_at',
|
||||
'effective_at',
|
||||
'reported_at',
|
||||
'sla_at',
|
||||
'occurred_at',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
'paid_at',
|
||||
'period_start',
|
||||
'period_end',
|
||||
'generated_at',
|
||||
'stat_at',
|
||||
'approved_at',
|
||||
'handled_at',
|
||||
'created_at',
|
||||
]);
|
||||
const jsonFields = new Set([
|
||||
'payload',
|
||||
'threshold',
|
||||
'gray_scope',
|
||||
'product_snapshot',
|
||||
'field_scope',
|
||||
]);
|
||||
const textareaFields = new Set(['body', 'content', 'reason', 'opinion']);
|
||||
const fieldType = (key: string): ResourceFieldType => {
|
||||
if (key.endsWith('_identity')) return 'identity';
|
||||
if (key === 'password') return 'password';
|
||||
if (numberFields.has(key)) return 'number';
|
||||
if (booleanFields.has(key)) return 'boolean';
|
||||
if (dateFields.has(key)) return 'date';
|
||||
if (datetimeFields.has(key)) return 'datetime';
|
||||
if (jsonFields.has(key)) return 'json';
|
||||
if (textareaFields.has(key)) return 'textarea';
|
||||
return 'text';
|
||||
};
|
||||
const field = (value: string): ResourceField => {
|
||||
const key = value.replace(/!$/, '');
|
||||
return {
|
||||
key,
|
||||
label:
|
||||
labels[key] ?? (key.endsWith('_identity') ? '关联业务标识' : '业务字段'),
|
||||
type: fieldType(key),
|
||||
required: value.endsWith('!') || undefined,
|
||||
};
|
||||
};
|
||||
const titles: Record<string, string> = {
|
||||
gas_basic: '气站管理',
|
||||
gas_account: '气站账户',
|
||||
delivery_basic: '配送点管理',
|
||||
delivery_account: '配送账户',
|
||||
delivery_task: '配送任务',
|
||||
delivery_track: '配送轨迹',
|
||||
delivery_track_point: '轨迹点',
|
||||
staff_account: '服务人员',
|
||||
staff_credential: '人员资质',
|
||||
user_account: '用户账户',
|
||||
user_address: '用户地址',
|
||||
user_service_relation: '用户服务关系',
|
||||
dev_smart_cylinder_valve: '智能钢瓶阀',
|
||||
dev_device_binding: '设备绑定',
|
||||
dev_telemetry: '设备遥测',
|
||||
saf_rule: '安全规则',
|
||||
saf_event: '安全事件',
|
||||
saf_inspection: '安全检查',
|
||||
saf_event_disposal: '事件处置',
|
||||
ec_category: '商品分类',
|
||||
ec_product: '商品管理',
|
||||
ec_product_attribute: '商品属性',
|
||||
ec_product_image: '商品图片',
|
||||
ec_cart: '购物车',
|
||||
ec_order: '订单管理',
|
||||
ec_order_item: '订单明细',
|
||||
ec_review: '商品评价',
|
||||
fin_payment: '支付记录',
|
||||
fin_settlement: '财务结算',
|
||||
fin_reconciliation: '财务对账',
|
||||
cnt_content: '内容管理',
|
||||
ntf_template: '通知模板',
|
||||
cs_ticket: '客服工单',
|
||||
platfrom_account: '平台账户',
|
||||
platform_role: '平台角色',
|
||||
platform_menu: '平台菜单',
|
||||
wallet: '钱包',
|
||||
wallet_ledger: '钱包流水',
|
||||
wallet_recharge: '钱包充值',
|
||||
wallet_withdrawal: '钱包提现',
|
||||
report: '报表',
|
||||
report_item: '报表项目',
|
||||
report_metric_snapshot: '指标快照',
|
||||
aud_operation_log: '操作审计',
|
||||
aud_export_log: '导出审计',
|
||||
aud_approval: '审批审计',
|
||||
};
|
||||
const define = (
|
||||
name: string,
|
||||
resource: string,
|
||||
mode: ResourceMode,
|
||||
pageKind: ResourcePageKind,
|
||||
keys: string[],
|
||||
detailActions?: DetailAction[],
|
||||
): ResourceUiDefinition => {
|
||||
const fields = keys.map(field);
|
||||
return {
|
||||
key: name.replace(/_/g, '-'),
|
||||
name,
|
||||
resource,
|
||||
title: titles[name] ?? '业务资源',
|
||||
mode,
|
||||
pageKind,
|
||||
fields,
|
||||
requiredIdentities: fields
|
||||
.filter((item) => item.required && item.key.endsWith('_identity'))
|
||||
.map((item) => item.key),
|
||||
...(detailActions ? { detailActions } : {}),
|
||||
};
|
||||
};
|
||||
const action = (
|
||||
name: string,
|
||||
resource: string,
|
||||
keys: string[],
|
||||
payload?: Record<string, unknown>,
|
||||
): DetailAction => ({
|
||||
name,
|
||||
resource,
|
||||
fields: keys.map(field),
|
||||
...(payload ? { payload } : {}),
|
||||
});
|
||||
|
||||
/** Exact UI contract for every backend ExpectedResources entry. */
|
||||
export const resources: ResourceUiDefinition[] = [
|
||||
define('gas_basic', '/gas/gas_basic', 'writable', 'list', ['code!', 'name!', 'credit_code', 'principal', 'address', 'longitude', 'latitude']),
|
||||
define('gas_account', '/gas/gas_account', 'writable', 'list', ['username!', 'password!', 'display_name', 'role_code', 'gas_basic_identity!']),
|
||||
define('delivery_basic', '/delivery/delivery_basic', 'writable', 'list', ['delivery_code!', 'name!', 'gas_basic_identity', 'principal', 'address']),
|
||||
define('delivery_account', '/delivery/delivery_account', 'writable', 'list', ['username!', 'password!', 'display_name', 'role_code', 'delivery_basic_identity!']),
|
||||
define('delivery_task', '/delivery/delivery_task', 'writable', 'list', ['ec_order_identity!', 'delivery_basic_identity!', 'staff_account_identity']),
|
||||
define('delivery_track', '/delivery/delivery_track', 'writable', 'list', ['delivery_task_identity!', 'started_at', 'completed_at']),
|
||||
define('delivery_track_point', '/delivery/delivery_track_point', 'writable', 'list', ['delivery_track_identity!', 'point_type!', 'occurred_at!', 'longitude!', 'latitude!']),
|
||||
define('staff_account', '/staff/account', 'writable', 'list', ['username!', 'password!', 'name!', 'phone', 'avatar', 'role_code', 'gas_basic_identity', 'delivery_basic_identity', 'work_status']),
|
||||
define('staff_credential', '/staff/credential', 'writable', 'list', ['staff_account_identity!', 'credential_type!', 'credential_no', 'expired_at']),
|
||||
define('user_account', '/user/account', 'writable', 'list', ['username!', 'password!', 'name!', 'phone', 'avatar', 'real_name']),
|
||||
define('user_address', '/user/address', 'writable', 'list', ['user_account_identity!', 'address!', 'longitude', 'latitude', 'is_default']),
|
||||
define('user_service_relation', '/user/service_relation', 'writable', 'list', ['user_account_identity!', 'gas_basic_identity', 'delivery_basic_identity', 'staff_account_identity']),
|
||||
define('dev_smart_cylinder_valve', '/device/dev_smart_cylinder_valve', 'writable', 'list', ['device_no!', 'model', 'online_status', 'owner_identity']),
|
||||
define('dev_device_binding', '/device/dev_device_binding', 'writable', 'list', ['smart_cylinder_valve_identity!', 'user_account_identity!', 'effective_at', 'expired_at']),
|
||||
define('dev_telemetry', '/device/dev_telemetry', 'readonly', 'list', ['smart_cylinder_valve_identity', 'recorded_at', 'payload']),
|
||||
define('saf_rule', '/safety/saf_rule', 'writable', 'list', ['rule_code!', 'version_no', 'threshold', 'action', 'gray_scope']),
|
||||
define('saf_event', '/safety/saf_event', 'writable', 'list', ['event_code!', 'level!', 'title!', 'smart_cylinder_valve_identity!', 'sla_at'], [action('saf_event_disposal', '/safety/saf_event/:identity/disposals', ['action!', 'reason!'])]),
|
||||
define('saf_inspection', '/safety/saf_inspection', 'writable', 'list', ['user_account_identity!', 'staff_account_identity!', 'result!', 'evidence_uri']),
|
||||
define('saf_event_disposal', '/safety/saf_event/:identity/disposals', 'append_only', 'list', ['action!', 'reason!']),
|
||||
define('ec_category', '/ec/ec_category', 'writable', 'tree', ['parent_identity', 'name!', 'sort_no']),
|
||||
define('ec_product', '/ec/ec_product', 'writable', 'list', ['ec_category_identity!', 'product_code!', 'name!', 'price_amount!', 'stock_quantity']),
|
||||
define('ec_product_attribute', '/ec/ec_product_attribute', 'writable', 'list', ['ec_product_identity!', 'name!', 'value!', 'sort_no']),
|
||||
define('ec_product_image', '/ec/ec_product_image', 'writable', 'list', ['ec_product_identity!', 'image_uri!', 'sort_no', 'is_cover']),
|
||||
define('ec_cart', '/ec/ec_cart', 'writable', 'list', ['user_account_identity!', 'ec_product_identity!', 'quantity!', 'selected']),
|
||||
define('ec_order', '/ec/ec_order', 'writable', 'list', ['user_account_identity!', 'gas_basic_identity', 'delivery_basic_identity', 'order_no!', 'total_amount!']),
|
||||
define('ec_order_item', '/ec/ec_order_item', 'writable', 'list', ['ec_order_identity!', 'ec_product_identity!', 'product_snapshot!', 'quantity!', 'sale_amount!']),
|
||||
define('ec_review', '/ec/ec_review', 'writable', 'list', ['ec_order_identity!', 'ec_product_identity!', 'user_account_identity!', 'score!', 'content!']),
|
||||
define('fin_payment', '/finance/fin_payment', 'writable', 'list', ['ec_order_identity!', 'channel!', 'amount!', 'paid_at']),
|
||||
define('fin_settlement', '/finance/fin_settlement', 'writable', 'list', ['settlement_no!', 'subject_type!', 'subject_identity!', 'period_start!', 'period_end!']),
|
||||
define('fin_reconciliation', '/finance/fin_reconciliation', 'writable', 'list', ['channel!', 'bill_date!', 'difference_amount!']),
|
||||
define('cnt_content', '/content/cnt_content', 'writable', 'list', ['content_type!', 'title!', 'body!', 'version_no', 'publish_status']),
|
||||
define('ntf_template', '/notification/ntf_template', 'writable', 'list', ['template_code!', 'channel!', 'content!']),
|
||||
define('cs_ticket', '/customer_service/cs_ticket', 'writable', 'list', ['user_account_identity!', 'ticket_no!', 'category!', 'priority!']),
|
||||
define('platfrom_account', '/platform/platfrom_account', 'writable', 'list', ['username!', 'password!', 'display_name', 'avatar', 'platform_role_code', 'phone']),
|
||||
define('platform_role', '/platform/platform_role', 'writable', 'list', ['role_code!', 'name!', 'data_scope!']),
|
||||
define('platform_menu', '/platform/platform_menu', 'writable', 'tree', ['parent_identity', 'menu_code!', 'name!', 'icon', 'path', 'sort_no']),
|
||||
define('wallet', '/wallet/wallet', 'readonly', 'list', ['owner_identity', 'balance_amount', 'status']),
|
||||
define('wallet_ledger', '/wallet/wallet_ledger', 'readonly', 'list', ['wallet_identity', 'change_amount', 'balance_after']),
|
||||
define('wallet_recharge', '/wallet/wallet_recharge', 'readonly', 'list', ['wallet_identity', 'amount', 'status']),
|
||||
define('wallet_withdrawal', '/wallet/wallet_withdrawal', 'readonly', 'list', ['wallet_identity', 'amount', 'status']),
|
||||
define('report', '/report/report', 'readonly', 'list', ['report_code', 'name', 'status']),
|
||||
define('report_item', '/report/report_item', 'readonly', 'list', ['report_identity', 'metric_code', 'value']),
|
||||
define('report_metric_snapshot', '/report/report_metric_snapshot', 'readonly', 'list', ['report_identity', 'metric_code', 'value', 'captured_at']),
|
||||
define('aud_operation_log', '/audit/aud_operation_log', 'readonly', 'list', ['operator_identity', 'action', 'object_identity', 'created_at']),
|
||||
define('aud_export_log', '/audit/aud_export_log', 'readonly', 'list', ['operator_identity', 'resource_type', 'created_at']),
|
||||
define('aud_approval', '/audit/aud_approval', 'readonly', 'list', ['operator_identity', 'status', 'handled_at']),
|
||||
define('gas_basic', '/gas/gas_basic', 'writable', 'list', [
|
||||
'code!',
|
||||
'name!',
|
||||
'credit_code',
|
||||
'principal',
|
||||
'address',
|
||||
'longitude',
|
||||
'latitude',
|
||||
]),
|
||||
define('gas_account', '/gas/gas_account', 'writable', 'list', [
|
||||
'username!',
|
||||
'password!',
|
||||
'display_name',
|
||||
'role_code',
|
||||
'gas_basic_identity!',
|
||||
]),
|
||||
define('delivery_basic', '/delivery/delivery_basic', 'writable', 'list', [
|
||||
'delivery_code!',
|
||||
'name!',
|
||||
'gas_basic_identity',
|
||||
'principal',
|
||||
'address',
|
||||
]),
|
||||
define('delivery_account', '/delivery/delivery_account', 'writable', 'list', [
|
||||
'username!',
|
||||
'password!',
|
||||
'display_name',
|
||||
'role_code',
|
||||
'delivery_basic_identity!',
|
||||
]),
|
||||
define('delivery_task', '/delivery/delivery_task', 'writable', 'list', [
|
||||
'ec_order_identity!',
|
||||
'delivery_basic_identity!',
|
||||
'staff_account_identity',
|
||||
]),
|
||||
define('delivery_track', '/delivery/delivery_track', 'writable', 'list', [
|
||||
'delivery_task_identity!',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
]),
|
||||
define(
|
||||
'delivery_track_point',
|
||||
'/delivery/delivery_track_point',
|
||||
'readonly',
|
||||
'list',
|
||||
[
|
||||
'delivery_track_identity',
|
||||
'point_type',
|
||||
'occurred_at',
|
||||
'longitude',
|
||||
'latitude',
|
||||
],
|
||||
),
|
||||
define('staff_account', '/staff/account', 'writable', 'list', [
|
||||
'username!',
|
||||
'password!',
|
||||
'name!',
|
||||
'phone',
|
||||
'avatar',
|
||||
'role_code',
|
||||
'gas_basic_identity',
|
||||
'delivery_basic_identity',
|
||||
'work_status',
|
||||
]),
|
||||
define('staff_credential', '/staff/credential', 'writable', 'list', [
|
||||
'staff_account_identity!',
|
||||
'credential_type!',
|
||||
'credential_no',
|
||||
'expired_at',
|
||||
]),
|
||||
define('user_account', '/user/account', 'writable', 'list', [
|
||||
'username!',
|
||||
'password!',
|
||||
'name!',
|
||||
'phone',
|
||||
'avatar',
|
||||
'real_name',
|
||||
]),
|
||||
define('user_address', '/user/address', 'writable', 'list', [
|
||||
'user_account_identity!',
|
||||
'address!',
|
||||
'longitude',
|
||||
'latitude',
|
||||
'is_default',
|
||||
]),
|
||||
define(
|
||||
'user_service_relation',
|
||||
'/user/service_relation',
|
||||
'writable',
|
||||
'list',
|
||||
[
|
||||
'user_account_identity!',
|
||||
'gas_basic_identity',
|
||||
'delivery_basic_identity',
|
||||
'staff_account_identity',
|
||||
],
|
||||
),
|
||||
define(
|
||||
'dev_smart_cylinder_valve',
|
||||
'/device/dev_smart_cylinder_valve',
|
||||
'writable',
|
||||
'list',
|
||||
['device_no!', 'model', 'online_status', 'owner_identity'],
|
||||
),
|
||||
define(
|
||||
'dev_device_binding',
|
||||
'/device/dev_device_binding',
|
||||
'writable',
|
||||
'list',
|
||||
[
|
||||
'smart_cylinder_valve_identity!',
|
||||
'user_account_identity!',
|
||||
'effective_at',
|
||||
'expired_at',
|
||||
],
|
||||
),
|
||||
define('dev_telemetry', '/device/dev_telemetry', 'readonly', 'list', [
|
||||
'smart_cylinder_valve_identity',
|
||||
'reported_at',
|
||||
'payload',
|
||||
]),
|
||||
define('saf_rule', '/safety/saf_rule', 'writable', 'list', [
|
||||
'rule_code!',
|
||||
'version_no',
|
||||
'threshold',
|
||||
'action',
|
||||
'gray_scope',
|
||||
]),
|
||||
define(
|
||||
'saf_event',
|
||||
'/safety/saf_event',
|
||||
'writable',
|
||||
'list',
|
||||
[
|
||||
'event_code!',
|
||||
'level!',
|
||||
'title!',
|
||||
'smart_cylinder_valve_identity!',
|
||||
'sla_at',
|
||||
],
|
||||
[
|
||||
action('saf_event_disposal', '/safety/saf_event/:identity/disposals', [
|
||||
'action!',
|
||||
'reason!',
|
||||
]),
|
||||
],
|
||||
),
|
||||
define('saf_inspection', '/safety/saf_inspection', 'writable', 'list', [
|
||||
'user_account_identity!',
|
||||
'staff_account_identity!',
|
||||
'result!',
|
||||
'evidence_uri',
|
||||
]),
|
||||
define(
|
||||
'saf_event_disposal',
|
||||
'/safety/saf_event/:identity/disposals',
|
||||
'append_only',
|
||||
'list',
|
||||
['action!', 'reason!'],
|
||||
),
|
||||
define('ec_category', '/ec/ec_category', 'writable', 'tree', [
|
||||
'parent_identity',
|
||||
'name!',
|
||||
'sort_no',
|
||||
]),
|
||||
define('ec_product', '/ec/ec_product', 'writable', 'list', [
|
||||
'ec_category_identity!',
|
||||
'product_code!',
|
||||
'name!',
|
||||
'price_amount!',
|
||||
'stock_quantity',
|
||||
]),
|
||||
define(
|
||||
'ec_product_attribute',
|
||||
'/ec/ec_product_attribute',
|
||||
'writable',
|
||||
'list',
|
||||
['ec_product_identity!', 'name!', 'value!', 'sort_no'],
|
||||
),
|
||||
define('ec_product_image', '/ec/ec_product_image', 'writable', 'list', [
|
||||
'ec_product_identity!',
|
||||
'image_uri!',
|
||||
'sort_no',
|
||||
'is_cover',
|
||||
]),
|
||||
define('ec_cart', '/ec/ec_cart', 'writable', 'list', [
|
||||
'user_account_identity!',
|
||||
'ec_product_identity!',
|
||||
'quantity!',
|
||||
'selected',
|
||||
]),
|
||||
define('ec_order', '/ec/ec_order', 'writable', 'list', [
|
||||
'user_account_identity!',
|
||||
'gas_basic_identity',
|
||||
'delivery_basic_identity',
|
||||
'order_no!',
|
||||
'total_amount!',
|
||||
]),
|
||||
define('ec_order_item', '/ec/ec_order_item', 'writable', 'list', [
|
||||
'ec_order_identity!',
|
||||
'ec_product_identity!',
|
||||
'product_snapshot!',
|
||||
'quantity!',
|
||||
'sale_amount!',
|
||||
]),
|
||||
define('ec_review', '/ec/ec_review', 'writable', 'list', [
|
||||
'ec_order_identity!',
|
||||
'ec_product_identity!',
|
||||
'user_account_identity!',
|
||||
'score!',
|
||||
'content!',
|
||||
]),
|
||||
define('fin_payment', '/finance/fin_payment', 'writable', 'list', [
|
||||
'ec_order_identity!',
|
||||
'channel!',
|
||||
'amount!',
|
||||
'paid_at',
|
||||
]),
|
||||
define('fin_settlement', '/finance/fin_settlement', 'writable', 'list', [
|
||||
'settlement_no!',
|
||||
'subject_type!',
|
||||
'subject_identity!',
|
||||
'period_start!',
|
||||
'period_end!',
|
||||
]),
|
||||
define(
|
||||
'fin_reconciliation',
|
||||
'/finance/fin_reconciliation',
|
||||
'writable',
|
||||
'list',
|
||||
['channel!', 'bill_date!', 'difference_amount!'],
|
||||
),
|
||||
define('cnt_content', '/content/cnt_content', 'writable', 'list', [
|
||||
'content_type!',
|
||||
'title!',
|
||||
'body!',
|
||||
'version_no',
|
||||
'publish_status',
|
||||
]),
|
||||
define('ntf_template', '/notification/ntf_template', 'writable', 'list', [
|
||||
'template_code!',
|
||||
'channel!',
|
||||
'content!',
|
||||
]),
|
||||
define('cs_ticket', '/customer_service/cs_ticket', 'writable', 'list', [
|
||||
'user_account_identity!',
|
||||
'ticket_no!',
|
||||
'category!',
|
||||
'priority!',
|
||||
]),
|
||||
define('platfrom_account', '/platform/platfrom_account', 'writable', 'list', [
|
||||
'username!',
|
||||
'password!',
|
||||
'display_name',
|
||||
'avatar',
|
||||
'platform_role_code',
|
||||
'phone',
|
||||
]),
|
||||
define('platform_role', '/platform/platform_role', 'writable', 'list', [
|
||||
'role_code!',
|
||||
'name!',
|
||||
'data_scope!',
|
||||
]),
|
||||
define('platform_menu', '/platform/platform_menu', 'writable', 'tree', [
|
||||
'parent_identity',
|
||||
'menu_code!',
|
||||
'name!',
|
||||
'icon',
|
||||
'path',
|
||||
'sort_no',
|
||||
]),
|
||||
define('wallet', '/wallet/wallet', 'readonly', 'list', [
|
||||
'owner_identity',
|
||||
'balance_amount',
|
||||
'frozen_amount',
|
||||
'status',
|
||||
]),
|
||||
define('wallet_ledger', '/wallet/wallet_ledger', 'readonly', 'list', [
|
||||
'wallet_identity',
|
||||
'amount',
|
||||
'balance_after',
|
||||
]),
|
||||
define('wallet_recharge', '/wallet/wallet_recharge', 'readonly', 'list', [
|
||||
'wallet_identity',
|
||||
'amount',
|
||||
'status',
|
||||
]),
|
||||
define('wallet_withdrawal', '/wallet/wallet_withdrawal', 'readonly', 'list', [
|
||||
'wallet_identity',
|
||||
'amount',
|
||||
'status',
|
||||
]),
|
||||
define('report', '/report/report', 'readonly', 'list', [
|
||||
'report_code',
|
||||
'report_type',
|
||||
'stat_period',
|
||||
'generated_at',
|
||||
'status',
|
||||
]),
|
||||
define('report_item', '/report/report_item', 'readonly', 'list', [
|
||||
'report_identity',
|
||||
'dimension',
|
||||
'metric_code',
|
||||
'metric_value',
|
||||
]),
|
||||
define(
|
||||
'report_metric_snapshot',
|
||||
'/report/report_metric_snapshot',
|
||||
'readonly',
|
||||
'list',
|
||||
['metric_code', 'scope_type', 'stat_at', 'metric_value'],
|
||||
),
|
||||
define('aud_operation_log', '/audit/aud_operation_log', 'readonly', 'list', [
|
||||
'operator_identity',
|
||||
'action',
|
||||
'object_identity',
|
||||
'created_at',
|
||||
]),
|
||||
define('aud_export_log', '/audit/aud_export_log', 'readonly', 'list', [
|
||||
'applicant_identity',
|
||||
'purpose',
|
||||
'field_scope',
|
||||
'approved_at',
|
||||
'file_uri',
|
||||
]),
|
||||
define(
|
||||
'aud_approval',
|
||||
'/audit/aud_approval',
|
||||
'readonly',
|
||||
'list',
|
||||
[
|
||||
'business_type',
|
||||
'business_identity',
|
||||
'applicant_identity',
|
||||
'opinion',
|
||||
'handler_identity',
|
||||
'status',
|
||||
'handled_at',
|
||||
],
|
||||
[
|
||||
action('同意', '/audit/aud_approval/:identity/approve', ['opinion'], {
|
||||
status: 'approved',
|
||||
}),
|
||||
action('驳回', '/audit/aud_approval/:identity/approve', ['opinion!'], {
|
||||
status: 'rejected',
|
||||
}),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
export const resourceByName = Object.fromEntries(resources.map((definition) => [definition.resource, definition])) as Record<string, ResourceUiDefinition>;
|
||||
export const resourceByName = Object.fromEntries(
|
||||
resources.map((definition) => [definition.resource, definition]),
|
||||
) as Record<string, ResourceUiDefinition>;
|
||||
export function getResource(resourcePath: string): ResourceUiDefinition {
|
||||
const definition = resourceByName[resourcePath];
|
||||
if (!definition) throw new Error('Unknown resource: ' + resourcePath);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user