feat(platform-admin): align frontend with backend contracts
This commit is contained in:
@@ -9,7 +9,10 @@ const contract = JSON.parse(
|
||||
readFileSync(resolve(root, 'src/contracts/platform-resources.json'), 'utf8'),
|
||||
);
|
||||
const backend = new Map(contract.resources.map((item) => [item.name, item]));
|
||||
const frontendNames = [...source.matchAll(/define\('([^']+)'/g)].map((match) => match[1]);
|
||||
const frontendDefinitions = [
|
||||
...source.matchAll(/define\('([^']+)',\s*'[^']+',\s*'([^']+)'/g),
|
||||
].map((match) => ({ name: match[1], mode: match[2] }));
|
||||
const frontendNames = frontendDefinitions.map((item) => item.name);
|
||||
|
||||
for (const name of frontendNames) {
|
||||
const item = backend.get(name);
|
||||
@@ -19,6 +22,15 @@ for (const name of frontendNames) {
|
||||
for (const item of contract.resources) {
|
||||
if (!frontendNames.includes(item.name))
|
||||
throw new Error(`缺少后端资源配置:${item.name}`);
|
||||
const frontend = frontendDefinitions.find(
|
||||
(definition) => definition.name === item.name,
|
||||
);
|
||||
if (frontend?.mode !== item.mode)
|
||||
throw new Error(
|
||||
`资源模式不一致:${item.name},后端=${item.mode},前端=${frontend?.mode}`,
|
||||
);
|
||||
if (!routes.includes(`'/${item.name}'`))
|
||||
throw new Error(`缺少后端资源路由:${item.name}`);
|
||||
}
|
||||
|
||||
const forbidden = [
|
||||
|
||||
@@ -40,6 +40,13 @@ export function buildResourcePayload(
|
||||
payload[field.key] = number;
|
||||
break;
|
||||
}
|
||||
case 'money': {
|
||||
const amount = Number(value);
|
||||
if (!Number.isFinite(amount))
|
||||
throw new Error(`${field.label}必须是有效金额`);
|
||||
payload[field.key] = Math.round(amount * 100);
|
||||
break;
|
||||
}
|
||||
case 'boolean':
|
||||
payload[field.key] = value === true || value === 'true';
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export type ResourceMode = 'writable' | 'readonly' | 'append_only' | 'managed';
|
||||
export type ResourceMode =
|
||||
| 'writable'
|
||||
| 'readonly'
|
||||
| 'append_only'
|
||||
| 'editable'
|
||||
| 'managed';
|
||||
export type ResourcePageKind = 'list' | 'tree';
|
||||
export type ResourceFieldType =
|
||||
| 'text'
|
||||
@@ -6,6 +11,7 @@ export type ResourceFieldType =
|
||||
| 'identity'
|
||||
| 'identity-list'
|
||||
| 'number'
|
||||
| 'money'
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'datetime'
|
||||
@@ -38,6 +44,10 @@ export type ResourceUiDefinition = {
|
||||
pageKind: ResourcePageKind;
|
||||
fields: ResourceField[];
|
||||
detailActions?: DetailAction[];
|
||||
canCreate: boolean;
|
||||
canEdit: boolean;
|
||||
canChangeStatus: boolean;
|
||||
canArchive: boolean;
|
||||
};
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
@@ -87,8 +97,8 @@ const fieldLabels: Record<string, string> = {
|
||||
alipay_name: '支付宝账户名',
|
||||
wxpay_id: '微信账号',
|
||||
wxpay_name: '微信账户名',
|
||||
balance: '余额(分)',
|
||||
withdrawal_balance: '可提现余额(分)',
|
||||
balance: '余额(元)',
|
||||
withdrawal_balance: '可提现余额(元)',
|
||||
card_no_last4: '银行卡末四位',
|
||||
bank_name: '银行名称',
|
||||
card_owner: '持卡人',
|
||||
@@ -98,16 +108,16 @@ const fieldLabels: Record<string, string> = {
|
||||
payment_type: '支付业务类型',
|
||||
pay_channel: '支付渠道',
|
||||
pay_type: '支付类型',
|
||||
amount: '金额(分)',
|
||||
fee: '手续费(分)',
|
||||
amount: '金额(元)',
|
||||
fee: '手续费(元)',
|
||||
args: '支付参数',
|
||||
callback_msg: '回调信息',
|
||||
record_no: '流水号',
|
||||
request_no: '请求流水号',
|
||||
direction: '收支方向',
|
||||
trade_type: '交易类型',
|
||||
balance_after: '变动后余额(分)',
|
||||
withdrawal_balance_after: '变动后可提现余额(分)',
|
||||
balance_after: '变动后余额(元)',
|
||||
withdrawal_balance_after: '变动后可提现余额(元)',
|
||||
refund_no: '退款单号',
|
||||
cash_no: '提现单号',
|
||||
channel: '渠道',
|
||||
@@ -119,19 +129,19 @@ const fieldLabels: Record<string, string> = {
|
||||
title: '标题',
|
||||
terms: '合同条款',
|
||||
file_uri: '附件地址',
|
||||
default_delivery_fee: '默认配送费(分)',
|
||||
default_delivery_fee: '默认配送费(元)',
|
||||
signed_at: '签署时间',
|
||||
effective_at: '生效时间',
|
||||
unit_price: '单价(分)',
|
||||
unit_price: '单价(元)',
|
||||
unbound_at: '解绑时间',
|
||||
revision_no: '修订号',
|
||||
creator_type: '创建方类型',
|
||||
creator_identity: '创建方标识',
|
||||
contact_name: '联系人',
|
||||
contact_phone: '联系电话',
|
||||
discount_amount: '优惠金额(分)',
|
||||
total_amount: '总金额(分)',
|
||||
delivery_fee: '配送费(分)',
|
||||
discount_amount: '优惠金额(元)',
|
||||
total_amount: '总金额(元)',
|
||||
delivery_fee: '配送费(元)',
|
||||
assigned_at: '分配时间',
|
||||
from_status: '原状态',
|
||||
to_status: '新状态',
|
||||
@@ -141,7 +151,7 @@ const fieldLabels: Record<string, string> = {
|
||||
confirmed_at: '确认时间',
|
||||
sort_no: '排序',
|
||||
product_code: '商品编码',
|
||||
price_amount: '售价(分)',
|
||||
price_amount: '售价(元)',
|
||||
stock_quantity: '库存',
|
||||
value: '属性值',
|
||||
image_uri: '图片地址',
|
||||
@@ -149,7 +159,7 @@ const fieldLabels: Record<string, string> = {
|
||||
quantity: '数量',
|
||||
selected: '是否选中',
|
||||
product_snapshot: '商品快照',
|
||||
sale_amount: '成交金额(分)',
|
||||
sale_amount: '成交金额(元)',
|
||||
score: '评分',
|
||||
paid_at: '支付时间',
|
||||
settlement_no: '结算单号',
|
||||
@@ -157,7 +167,7 @@ const fieldLabels: Record<string, string> = {
|
||||
period_start: '结算开始时间',
|
||||
period_end: '结算结束时间',
|
||||
bill_date: '账单日期',
|
||||
difference_amount: '差异金额(分)',
|
||||
difference_amount: '差异金额(元)',
|
||||
content_type: '内容类型',
|
||||
body: '正文',
|
||||
version_no: '版本号',
|
||||
@@ -175,11 +185,14 @@ const fieldLabels: Record<string, string> = {
|
||||
};
|
||||
|
||||
const numbers = new Set([
|
||||
'sort_no', 'unit_price', 'amount', 'fee', 'balance', 'withdrawal_balance',
|
||||
'sort_no', 'stock_quantity', 'quantity', 'score', 'version_no',
|
||||
'attempt_no',
|
||||
]);
|
||||
const money = new Set([
|
||||
'unit_price', 'amount', 'fee', 'balance', 'withdrawal_balance',
|
||||
'balance_after', 'withdrawal_balance_after', 'default_delivery_fee',
|
||||
'discount_amount', 'total_amount', 'delivery_fee', 'price_amount',
|
||||
'stock_quantity', 'quantity', 'sale_amount', 'score', 'version_no',
|
||||
'difference_amount', 'attempt_no',
|
||||
'sale_amount', 'difference_amount',
|
||||
]);
|
||||
const booleans = new Set(['is_default', 'is_enabled', 'is_cover', 'selected', 'withdrawable']);
|
||||
const dates = new Set(['bill_date']);
|
||||
@@ -196,6 +209,8 @@ const textareas = new Set([
|
||||
function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
|
||||
const type: ResourceFieldType = key.endsWith('_identity')
|
||||
? 'identity'
|
||||
: money.has(key)
|
||||
? 'money'
|
||||
: numbers.has(key)
|
||||
? 'number'
|
||||
: booleans.has(key)
|
||||
@@ -223,7 +238,19 @@ function define(
|
||||
fields: ResourceField[],
|
||||
pageKind: ResourcePageKind = 'list',
|
||||
detailActions?: DetailAction[],
|
||||
capabilities: Partial<
|
||||
Pick<
|
||||
ResourceUiDefinition,
|
||||
'canCreate' | 'canEdit' | 'canChangeStatus' | 'canArchive'
|
||||
>
|
||||
> = {},
|
||||
): ResourceUiDefinition {
|
||||
const defaults = {
|
||||
canCreate: mode === 'writable' || mode === 'editable' || mode === 'append_only',
|
||||
canEdit: mode === 'writable' || mode === 'editable',
|
||||
canChangeStatus: mode === 'writable' || mode === 'editable',
|
||||
canArchive: mode === 'writable',
|
||||
};
|
||||
return {
|
||||
key: name.replace(/_/g, '-'),
|
||||
name,
|
||||
@@ -232,6 +259,8 @@ function define(
|
||||
mode,
|
||||
pageKind,
|
||||
fields,
|
||||
...defaults,
|
||||
...capabilities,
|
||||
...(detailActions ? { detailActions } : {}),
|
||||
};
|
||||
}
|
||||
@@ -249,24 +278,24 @@ export const resources: ResourceUiDefinition[] = [
|
||||
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
|
||||
define('user_service_relation', '用户服务关系', 'writable', [relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('staff_account_identity', '/staff_account')]),
|
||||
|
||||
define('product_type', '产品类型', 'writable', [f('code', { required: true }), f('name', { required: true })]),
|
||||
define('product_warehouse', '库房', 'writable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone'), f('is_enabled')]),
|
||||
define('product_info', '产品信息', 'managed', [f('code', { required: true }), f('name', { required: true }), relation('product_type_identity', '/product_type', true), f('params', { required: true }), relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('produced_at', { required: true }), f('is_enabled')], 'list', [
|
||||
define('product_type', '产品类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
|
||||
define('product_warehouse', '库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone'), f('is_enabled')]),
|
||||
define('product_info', '产品信息', 'editable', [f('code', { required: true }), f('name', { required: true }), relation('product_type_identity', '/product_type', true), f('params', { required: true }), relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('produced_at', { required: true }), f('is_enabled')], 'list', [
|
||||
{ name: '修改启用状态', resource: '/product_info/:identity/status', method: 'PATCH', fields: [f('status', { required: true, type: 'select', options: [{ label: '启用', value: 'enabled' }, { label: '停用', value: 'disabled' }] })] },
|
||||
]),
|
||||
define('product_repair', '产品检修记录', 'writable', [relation('product_info_identity', '/product_info', true), f('repair_no', { required: true }), f('repair_type', { required: true }), f('started_at', { required: true }), f('completed_at'), f('result'), f('target_status'), f('content'), f('operator'), f('remark')]),
|
||||
define('product_repair', '产品检修记录', 'editable', [relation('product_info_identity', '/product_info', true), f('repair_no', { required: true }), f('repair_type', { required: true }), f('started_at', { required: true }), f('completed_at'), f('result'), f('target_status'), f('content'), f('operator'), f('remark')]),
|
||||
define('product_owner', '产品归属记录', 'append_only', [relation('product_info_identity', '/product_info', true), relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('action', { required: true }), f('occurred_at', { required: true }), f('reason'), f('remark')]),
|
||||
|
||||
define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true }), relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic', true), relation('delivery_basic_identity', '/delivery_basic'), f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [
|
||||
{ name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason },
|
||||
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason] },
|
||||
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason },
|
||||
]),
|
||||
], { canCreate: true, canEdit: true }),
|
||||
define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true), f('unit_price')], 'list', [
|
||||
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
|
||||
]),
|
||||
define('gasorder_contract_revision', '合同修订记录', 'readonly', []),
|
||||
define('gasorder_basic', '气体配送订单', 'managed', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true), f('creator_type', { required: true, type: 'select', options: [{ label: '用户', value: 'user' }, { label: '工作人员', value: 'staff' }, { label: '配送站', value: 'delivery' }, { label: '气站', value: 'gas' }] }), f('creator_identity', { required: true }), relation('user_address_identity', '/user_address', true), f('gasorder_contract_product_identities', { required: true, type: 'identity-list', relation: '/gasorder_contract_product' }), f('contact_name', { required: true }), f('contact_phone', { required: true }), f('discount_amount'), f('remark')], 'list', [
|
||||
define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true), f('creator_type', { required: true, type: 'select', options: [{ label: '用户', value: 'user' }, { label: '工作人员', value: 'staff' }, { label: '配送站', value: 'delivery' }, { label: '气站', value: 'gas' }] }), f('creator_identity', { required: true }), relation('user_address_identity', '/user_address', true), f('gasorder_contract_product_identities', { required: true, type: 'identity-list', relation: '/gasorder_contract_product' }), f('contact_name', { required: true }), f('contact_phone', { required: true }), f('discount_amount'), f('remark')], 'list', [
|
||||
{ name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true), relation('staff_account_identity', '/staff_account', true), ...reason] },
|
||||
{ name: '开始罐装', resource: '/gasorder_basic/:identity/filling', fields: reason },
|
||||
{ name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason },
|
||||
@@ -291,7 +320,7 @@ export const resources: ResourceUiDefinition[] = [
|
||||
define('ec_order_item', '商城订单明细', 'writable', [relation('ec_order_identity', '/ec_order', true), relation('ec_product_identity', '/ec_product', true), f('product_snapshot', { required: true }), f('quantity', { required: true }), f('sale_amount', { required: true })]),
|
||||
define('ec_review', '商品评价', 'writable', [relation('ec_order_identity', '/ec_order', true), relation('ec_product_identity', '/ec_product', true), relation('user_account_identity', '/user_account', true), f('score', { required: true }), f('content', { required: true })]),
|
||||
|
||||
define('wallet_basic', '钱包', 'managed', [f('owner_type'), f('owner_identity'), f('alipay_id'), f('alipay_name'), f('wxpay_id'), f('wxpay_name'), f('balance'), f('withdrawal_balance')], 'list', [
|
||||
define('wallet_basic', '钱包', 'readonly', [f('owner_type'), f('owner_identity'), f('alipay_id'), f('alipay_name'), f('wxpay_id'), f('wxpay_name'), f('balance'), f('withdrawal_balance')], 'list', [
|
||||
{ name: '后台充值', resource: '/wallet_basic/:identity/recharge', fields: [f('request_no', { required: true }), f('amount', { required: true }), f('withdrawable'), ...reason, f('remark')] },
|
||||
{ name: '修改钱包状态', resource: '/wallet_basic/:identity/status', method: 'PATCH', fields: [f('status', { required: true, type: 'select', options: [{ label: '启用', value: 'enabled' }, { label: '停用', value: 'disabled' }, { label: '冻结', value: 'frozen' }] })] },
|
||||
]),
|
||||
@@ -299,7 +328,7 @@ export const resources: ResourceUiDefinition[] = [
|
||||
define('wallet_payment', '钱包支付记录', 'readonly', []),
|
||||
define('wallet_record', '钱包流水', 'readonly', []),
|
||||
define('wallet_refund', '退款记录', 'readonly', []),
|
||||
define('wallet_apply_cash', '提现申请', 'managed', [], 'list', [
|
||||
define('wallet_apply_cash', '提现申请', 'readonly', [], 'list', [
|
||||
{ name: '审核通过', resource: '/wallet_apply_cash/:identity/approve', fields: reason },
|
||||
{ name: '审核驳回', resource: '/wallet_apply_cash/:identity/reject', danger: true, fields: reason },
|
||||
]),
|
||||
|
||||
@@ -10,12 +10,21 @@ function child(
|
||||
title: string,
|
||||
resource: string,
|
||||
menuCode = domain,
|
||||
hidden = false,
|
||||
activeMenu?: string,
|
||||
): AppRouteRecordRaw {
|
||||
return {
|
||||
path,
|
||||
name: `${domain}-${name}`,
|
||||
component: resourcePage,
|
||||
meta: { title, resource, requiresAuth: true, menuCode },
|
||||
meta: {
|
||||
title,
|
||||
resource,
|
||||
requiresAuth: true,
|
||||
menuCode,
|
||||
hideInMenu: hidden,
|
||||
...(activeMenu ? { activeMenu } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,22 +80,30 @@ const routes: AppRouteRecordRaw[] = [
|
||||
child('product', 'product-type', 'type', '产品类型', '/product_type', 'device'),
|
||||
child('product', 'warehouse', 'warehouse', '库房', '/product_warehouse', 'device'),
|
||||
child('product', 'product-info', 'info', '产品信息', '/product_info', 'device'),
|
||||
child('product', 'repair', 'repair', '检修记录', '/product_repair', 'device'),
|
||||
child('product', 'owner', 'owner', '归属记录', '/product_owner', 'device'),
|
||||
child('product', 'repair', 'repair', '检修记录', '/product_repair', 'device', true, 'product-info'),
|
||||
child('product', 'owner', 'owner', '归属记录', '/product_owner', 'device', true, 'product-info'),
|
||||
], 'device'),
|
||||
group('gasorder', 'gasorder', '气体配送订单管理', 'icon-list', 60, [
|
||||
child('gasorder', 'contracts', 'contracts', '合同管理', '/gasorder_contract', 'delivery'),
|
||||
child('gasorder', 'contract-products', 'contract-products', '合同气瓶', '/gasorder_contract_product', 'delivery', true, 'gasorder-contracts'),
|
||||
child('gasorder', 'contract-revisions', 'contract-revisions', '合同修订记录', '/gasorder_contract_revision', 'delivery', true, 'gasorder-contracts'),
|
||||
child('gasorder', 'orders', 'orders', '配送订单', '/gasorder_basic', 'delivery'),
|
||||
child('gasorder', 'order-items', 'order-items', '订单明细', '/gasorder_item', 'delivery', true, 'gasorder-orders'),
|
||||
child('gasorder', 'assignments', 'assignments', '分配记录', '/gasorder_assign', 'delivery', true, 'gasorder-orders'),
|
||||
child('gasorder', 'statuses', 'statuses', '状态记录', '/gasorder_status', 'delivery', true, 'gasorder-orders'),
|
||||
child('gasorder', 'tracks', 'tracks', '运行轨迹', '/gasorder_track', 'delivery'),
|
||||
child('gasorder', 'track-points', 'track-points', '轨迹点', '/gasorder_track_point', 'delivery', true, 'gasorder-tracks'),
|
||||
child('gasorder', 'confirms', 'confirms', '确认记录', '/gasorder_confirm', 'delivery', true, 'gasorder-orders'),
|
||||
child('gasorder', 'payments', 'payments', '订单支付记录', '/gasorder_payment', 'delivery', true, 'gasorder-orders'),
|
||||
], 'delivery'),
|
||||
group('ec', 'ec', '商城管理', 'icon-shopping-cart', 70, [
|
||||
child('ec', 'categories', 'categories', '商品分类', '/ec_category'),
|
||||
child('ec', 'products', 'products', '商品', '/ec_product'),
|
||||
child('ec', 'attributes', 'attributes', '商品属性', '/ec_product_attribute'),
|
||||
child('ec', 'images', 'images', '商品图片', '/ec_product_image'),
|
||||
child('ec', 'attributes', 'attributes', '商品属性', '/ec_product_attribute', 'ec', true, 'ec-products'),
|
||||
child('ec', 'images', 'images', '商品图片', '/ec_product_image', 'ec', true, 'ec-products'),
|
||||
child('ec', 'carts', 'carts', '购物车', '/ec_cart'),
|
||||
child('ec', 'orders', 'orders', '商城订单', '/ec_order'),
|
||||
child('ec', 'order-items', 'order-items', '订单明细', '/ec_order_item'),
|
||||
child('ec', 'order-items', 'order-items', '订单明细', '/ec_order_item', 'ec', true, 'ec-orders'),
|
||||
child('ec', 'reviews', 'reviews', '商品评价', '/ec_review'),
|
||||
]),
|
||||
group('wallet', 'wallet', '钱包管理', 'icon-safe', 80, [
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
<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 :model="filters" layout="inline" class="filters" @submit.prevent="search">
|
||||
<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-button @click="resetSearch">重置</a-button>
|
||||
</a-form>
|
||||
<a-form v-if="definition.name === 'wallet_basic'" :model="walletOwner" layout="inline" class="wallet-owner" @submit.prevent="getOwnerWallet">
|
||||
<a-form-item label="归属类型">
|
||||
@@ -28,26 +29,32 @@
|
||||
<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">
|
||||
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" ellipsis tooltip>
|
||||
<template #cell="{ record }">
|
||||
{{ displayFieldValue(field, record) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" :width="260" 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-button v-if="canEdit" size="mini" :disabled="isProtectedRecord(record)" @click="openEdit(record)">编辑</a-button>
|
||||
<a-button v-if="canChangeStatus" size="mini" :disabled="isProtectedRecord(record)" @click="openStatus(record)">状态</a-button>
|
||||
<a-button v-if="canArchive" size="mini" status="danger" :disabled="isProtectedRecord(record)" @click="confirmArchive(record)">归档</a-button>
|
||||
</a-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-empty v-if="!loading && list.length === 0" description="暂无符合条件的数据" />
|
||||
<div class="pagination"><a-pagination :total="total" :current="page" :page-size="pageSize" show-total show-jumper @change="changePage" /></div>
|
||||
</a-card>
|
||||
|
||||
<a-drawer :visible="formVisible" :title="editingIdentity ? `编辑${definition.title}` : `新建${definition.title}`" :width="480" @cancel="formVisible = false" @ok="save">
|
||||
<a-drawer :visible="formVisible" :title="editingIdentity ? `编辑${definition.title}` : `新建${definition.title}`" :width="520" :ok-loading="saving" @cancel="formVisible = false" @ok="save">
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item v-for="field in formFields" :key="field.key" :label="field.label" :required="isResourceFieldRequired(field, formMode)">
|
||||
<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-input-number v-else-if="field.type === 'number' || field.type === 'money'" v-model="form[field.key]" :precision="field.type === 'money' ? 2 : 0" />
|
||||
<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 === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
||||
@@ -55,7 +62,7 @@
|
||||
<a-select v-else-if="field.type === 'select'" v-model="form[field.key]" allow-clear>
|
||||
<a-option v-for="option in field.options" :key="option.value" :value="option.value">{{ option.label }}</a-option>
|
||||
</a-select>
|
||||
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="form[field.key]" :multiple="field.type === 'identity-list'" allow-clear allow-search>
|
||||
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="form[field.key]" :multiple="field.type === 'identity-list'" allow-clear allow-search :loading="relationLoading[field.relation ?? '']" @search="(value: string) => searchRelation(field.relation, value)">
|
||||
<a-option v-for="option in relationOptions[field.relation ?? ''] ?? []" :key="String(option.identity)" :value="String(option.identity)">
|
||||
{{ optionLabel(option) }}
|
||||
</a-option>
|
||||
@@ -70,27 +77,38 @@
|
||||
|
||||
<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">
|
||||
<a-descriptions-item v-for="[key, value] in detailEntries" :key="key" :label="fieldLabel(key)">
|
||||
<pre v-if="typeof value === 'object'" class="json-value">{{ formatValue(value) }}</pre>
|
||||
<template v-else>{{ value ?? '-' }}</template>
|
||||
<template v-else>{{ displayValue(key, value) }}</template>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-tabs v-if="detailCollections.length" class="detail-collections">
|
||||
<a-tab-pane v-for="collection in detailCollections" :key="collection.key" :title="collection.title">
|
||||
<a-table :data="collection.rows" :pagination="false" size="small">
|
||||
<template #columns>
|
||||
<a-table-column v-for="column in collection.columns" :key="column" :title="fieldLabel(column)">
|
||||
<template #cell="{ record }">{{ displayValue(column, record[column]) }}</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<a-space class="detail-actions">
|
||||
<a-button v-for="action in definition.detailActions" :key="action.name" type="primary" :status="action.danger ? 'danger' : 'normal'" @click="openDetailAction(action)">{{ action.name }}</a-button>
|
||||
</a-space>
|
||||
</a-drawer>
|
||||
|
||||
<a-modal :visible="actionVisible" :title="activeAction?.name" @cancel="actionVisible = false" @ok="submitDetailAction">
|
||||
<a-modal :visible="actionVisible" :title="activeAction?.name" :ok-loading="actionSubmitting" :ok-button-props="{ status: activeAction?.danger ? 'danger' : 'normal' }" @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-input-number v-if="field.type === 'number' || field.type === 'money'" v-model="actionForm[field.key]" :precision="field.type === 'money' ? 2 : 0" />
|
||||
<a-switch v-else-if="field.type === 'boolean'" v-model="actionForm[field.key]" />
|
||||
<a-textarea v-else-if="field.type === 'textarea'" v-model="actionForm[field.key]" />
|
||||
<a-date-picker v-else-if="field.type === 'datetime'" v-model="actionForm[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
|
||||
<a-select v-else-if="field.type === 'select'" v-model="actionForm[field.key]">
|
||||
<a-option v-for="option in field.options" :key="option.value" :value="option.value">{{ option.label }}</a-option>
|
||||
</a-select>
|
||||
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="actionForm[field.key]" :multiple="field.type === 'identity-list'" allow-search>
|
||||
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="actionForm[field.key]" :multiple="field.type === 'identity-list'" allow-search :loading="relationLoading[field.relation ?? '']" @search="(value: string) => searchRelation(field.relation, value)">
|
||||
<a-option v-for="option in relationOptions[field.relation ?? ''] ?? []" :key="String(option.identity)" :value="String(option.identity)">{{ optionLabel(option) }}</a-option>
|
||||
</a-select>
|
||||
<a-input v-else v-model="actionForm[field.key]" />
|
||||
@@ -101,7 +119,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { resourceApi } from '@/api/resource';
|
||||
import { platformApi, type PlatformRole } from '@/api/platform';
|
||||
import {
|
||||
@@ -109,16 +129,28 @@ import {
|
||||
isMissingField,
|
||||
isResourceFieldRequired,
|
||||
} from '@/api/resource-form';
|
||||
import type { DetailAction, ResourceUiDefinition } from '@/api/resources';
|
||||
import type {
|
||||
DetailAction,
|
||||
ResourceField,
|
||||
ResourceUiDefinition,
|
||||
} from '@/api/resources';
|
||||
import { useUserStore } from '@/store';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
const props = defineProps<{ definition: ResourceUiDefinition }>();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const loading = ref(false);
|
||||
const page = ref(1);
|
||||
const saving = ref(false);
|
||||
const actionSubmitting = ref(false);
|
||||
const page = ref(Math.max(1, Number(route.query.page) || 1));
|
||||
const pageSize = 20;
|
||||
const total = ref(0);
|
||||
const list = ref<Row[]>([]);
|
||||
const filters = reactive({ keyword: '' });
|
||||
const filters = reactive({
|
||||
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
|
||||
});
|
||||
const walletOwner = reactive({ type: 'user', identity: '' });
|
||||
const formVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
@@ -130,25 +162,84 @@ const activeAction = ref<DetailAction>();
|
||||
const actionForm = reactive<Record<string, any>>({});
|
||||
const roleOptions = ref<PlatformRole[]>([]);
|
||||
const relationOptions = reactive<Record<string, Row[]>>({});
|
||||
const canCreate = computed(() => ['writable', 'append_only'].includes(props.definition.mode) || ['product_info', 'gasorder_contract', 'gasorder_basic'].includes(props.definition.name));
|
||||
const canEdit = computed(() => props.definition.mode === 'writable' || ['product_info', 'gasorder_contract'].includes(props.definition.name));
|
||||
const canArchive = computed(() => props.definition.mode === 'writable');
|
||||
const relationLoading = reactive<Record<string, boolean>>({});
|
||||
const relationSearchTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
const platformRootWriteResources = new Set([
|
||||
'platform_account',
|
||||
'platform_role',
|
||||
'platform_menu',
|
||||
]);
|
||||
const requiresRootWrite = computed(() =>
|
||||
platformRootWriteResources.has(props.definition.name),
|
||||
);
|
||||
const canCreate = computed(
|
||||
() =>
|
||||
props.definition.canCreate &&
|
||||
(!requiresRootWrite.value || userStore.role === 'root'),
|
||||
);
|
||||
const canEdit = computed(
|
||||
() =>
|
||||
props.definition.canEdit &&
|
||||
(props.definition.name === 'platform_account' ||
|
||||
!requiresRootWrite.value ||
|
||||
userStore.role === 'root'),
|
||||
);
|
||||
const canChangeStatus = computed(
|
||||
() =>
|
||||
props.definition.canChangeStatus &&
|
||||
(!requiresRootWrite.value || userStore.role === 'root'),
|
||||
);
|
||||
const canArchive = computed(
|
||||
() =>
|
||||
props.definition.canArchive &&
|
||||
(!requiresRootWrite.value || userStore.role === 'root'),
|
||||
);
|
||||
const formMode = computed<'create' | 'edit'>(() =>
|
||||
editingIdentity.value ? 'edit' : 'create',
|
||||
);
|
||||
const formFields = computed(() =>
|
||||
props.definition.fields.filter(
|
||||
(field) => formMode.value === 'create' || field.type !== 'password',
|
||||
(field) =>
|
||||
(formMode.value === 'create' || field.type !== 'password') &&
|
||||
!(
|
||||
formMode.value === 'edit' &&
|
||||
props.definition.name === 'platform_account' &&
|
||||
userStore.role !== 'root' &&
|
||||
field.key === 'platform_role_code'
|
||||
),
|
||||
),
|
||||
);
|
||||
const displayFields = computed(() =>
|
||||
props.definition.fields.filter((field) => field.key !== 'status'),
|
||||
props.definition.fields
|
||||
.filter((field) => field.key !== 'password')
|
||||
.slice(0, 6),
|
||||
);
|
||||
const detailEntries = computed(() =>
|
||||
Object.entries(detail.value).filter(
|
||||
([key]) => key !== 'id' && !key.endsWith('_id'),
|
||||
([key, value]) =>
|
||||
key !== 'id' &&
|
||||
!key.endsWith('_id') &&
|
||||
!Array.isArray(value) &&
|
||||
!(value && typeof value === 'object' && key !== 'order' && key !== 'contract'),
|
||||
),
|
||||
);
|
||||
const detailCollections = computed(() =>
|
||||
Object.entries(detail.value)
|
||||
.filter(([, value]) => Array.isArray(value) && value.length > 0)
|
||||
.map(([key, value]) => {
|
||||
const rows = value as Row[];
|
||||
const columns = [
|
||||
...new Set(
|
||||
rows.flatMap((row) =>
|
||||
Object.keys(row).filter(
|
||||
(column) => column !== 'id' && !column.endsWith('_id'),
|
||||
),
|
||||
),
|
||||
),
|
||||
].slice(0, 8);
|
||||
return { key, title: fieldLabel(key), rows, columns };
|
||||
}),
|
||||
);
|
||||
const currentIdentity = computed(() =>
|
||||
String(
|
||||
detail.value.identity ??
|
||||
@@ -161,7 +252,12 @@ const currentIdentity = computed(() =>
|
||||
function resetForm(data?: Row) {
|
||||
for (const field of props.definition.fields) {
|
||||
const value = data?.[field.key];
|
||||
form[field.key] = value == null ? undefined : value;
|
||||
form[field.key] =
|
||||
value == null
|
||||
? undefined
|
||||
: field.type === 'money'
|
||||
? Number(value) / 100
|
||||
: value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +279,24 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
async function syncQuery() {
|
||||
const query: Record<string, string> = {};
|
||||
if (page.value > 1) query.page = String(page.value);
|
||||
if (filters.keyword.trim()) query.keyword = filters.keyword.trim();
|
||||
await router.replace({ query });
|
||||
}
|
||||
|
||||
async function search() {
|
||||
page.value = 1;
|
||||
await syncQuery();
|
||||
await load();
|
||||
}
|
||||
|
||||
async function resetSearch() {
|
||||
filters.keyword = '';
|
||||
await search();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingIdentity.value = '';
|
||||
resetForm();
|
||||
@@ -195,6 +309,10 @@ function openEdit(row: Row) {
|
||||
formVisible.value = true;
|
||||
}
|
||||
|
||||
function isProtectedRecord(row: Row) {
|
||||
return props.definition.name === 'platform_role' && row.is_system === true;
|
||||
}
|
||||
|
||||
async function openDetail(row: Row) {
|
||||
try {
|
||||
detail.value = await resourceApi.detail<Row>(
|
||||
@@ -224,6 +342,14 @@ async function openDetailAction(action: DetailAction) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (action.danger && !(action.fields?.length)) {
|
||||
Modal.warning({
|
||||
title: `确认${action.name}`,
|
||||
content: '该操作会改变业务状态,执行后可能无法直接撤销。',
|
||||
onOk: () => executeDetailAction(action),
|
||||
});
|
||||
return;
|
||||
}
|
||||
actionVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -238,6 +364,11 @@ async function submitDetailAction() {
|
||||
Message.warning('请填写必填字段');
|
||||
return;
|
||||
}
|
||||
await executeDetailAction(action);
|
||||
}
|
||||
|
||||
async function executeDetailAction(action: DetailAction) {
|
||||
actionSubmitting.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
...buildResourcePayload(action.fields ?? [], actionForm),
|
||||
@@ -253,9 +384,11 @@ async function submitDetailAction() {
|
||||
}
|
||||
Message.success('操作成功');
|
||||
actionVisible.value = false;
|
||||
await openDetail(detail.value);
|
||||
await Promise.all([openDetail({ identity: currentIdentity.value }), load()]);
|
||||
} catch (error) {
|
||||
Message.error((error as Error).message);
|
||||
} finally {
|
||||
actionSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,9 +403,10 @@ async function save() {
|
||||
Message.warning('请填写必填字段');
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = buildResourcePayload(
|
||||
props.definition.fields,
|
||||
formFields.value,
|
||||
form,
|
||||
formMode.value,
|
||||
);
|
||||
@@ -288,9 +422,33 @@ async function save() {
|
||||
await load();
|
||||
} catch (error) {
|
||||
Message.error((error as Error).message);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openStatus(row: Row) {
|
||||
detail.value = row;
|
||||
openDetailAction({
|
||||
name: '修改状态',
|
||||
resource: `${props.definition.resource}/:identity/status`,
|
||||
method: 'PATCH',
|
||||
fields: [
|
||||
{
|
||||
key: 'status',
|
||||
label: '目标状态',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ label: '启用', value: 'enabled' },
|
||||
{ label: '停用', value: 'disabled' },
|
||||
{ label: '归档', value: 'archived' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function confirmArchive(row: Row) {
|
||||
Modal.warning({
|
||||
title: '确认归档',
|
||||
@@ -312,6 +470,7 @@ function confirmArchive(row: Row) {
|
||||
|
||||
async function changePage(next: number) {
|
||||
page.value = next;
|
||||
await syncQuery();
|
||||
await load();
|
||||
}
|
||||
|
||||
@@ -336,6 +495,104 @@ function formatValue(value: unknown) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function fieldLabel(key: string) {
|
||||
const aliases: Record<string, string> = {
|
||||
identity: '业务标识',
|
||||
created_at: '创建时间',
|
||||
updated_at: '更新时间',
|
||||
version: '版本',
|
||||
items: '订单明细',
|
||||
assignments: '分配记录',
|
||||
statuses: '状态记录',
|
||||
tracks: '运行轨迹',
|
||||
confirmations: '确认记录',
|
||||
payments: '支付记录',
|
||||
revisions: '修订记录',
|
||||
products: '合同气瓶',
|
||||
order: '订单',
|
||||
contract: '合同',
|
||||
};
|
||||
const normalizedKey = key.endsWith('_masked')
|
||||
? key.slice(0, -'_masked'.length)
|
||||
: key;
|
||||
return (
|
||||
props.definition.fields.find((field) => field.key === normalizedKey)?.label ??
|
||||
aliases[key] ??
|
||||
key
|
||||
);
|
||||
}
|
||||
|
||||
function relationLabel(field: ResourceField, identity: string) {
|
||||
const match = (relationOptions[field.relation ?? ''] ?? []).find(
|
||||
(option) => String(option.identity) === identity,
|
||||
);
|
||||
return match ? `${optionLabel(match)} · ${identity}` : identity;
|
||||
}
|
||||
|
||||
function displayFieldValue(field: ResourceField, row: Row) {
|
||||
const value = row[field.key] ?? row[`${field.key}_masked`];
|
||||
if (
|
||||
(field.type === 'identity' || field.type === 'identity-list') &&
|
||||
typeof value === 'string'
|
||||
)
|
||||
return relationLabel(field, value);
|
||||
return displayValue(field.key, value);
|
||||
}
|
||||
|
||||
function displayValue(key: string, value: unknown) {
|
||||
if (value == null || value === '') return '-';
|
||||
if (typeof value === 'boolean') return value ? '是' : '否';
|
||||
if (
|
||||
key === 'amount' ||
|
||||
key.endsWith('_amount') ||
|
||||
key === 'unit_price' ||
|
||||
key === 'balance' ||
|
||||
key === 'withdrawal_balance' ||
|
||||
key.endsWith('_balance_after')
|
||||
) {
|
||||
const amount = Number(value);
|
||||
return Number.isFinite(amount) ? `¥${(amount / 100).toFixed(2)}` : String(value);
|
||||
}
|
||||
if (
|
||||
key.endsWith('_at') ||
|
||||
key === 'created_at' ||
|
||||
key === 'updated_at'
|
||||
) {
|
||||
const date = dayjs(String(value));
|
||||
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : String(value);
|
||||
}
|
||||
if (typeof value === 'object') return formatValue(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
async function loadRelation(resource: string, keyword = '') {
|
||||
relationLoading[resource] = true;
|
||||
try {
|
||||
relationOptions[resource] = (
|
||||
await resourceApi.list<Row>(
|
||||
resource,
|
||||
1,
|
||||
100,
|
||||
keyword ? { keyword } : {},
|
||||
)
|
||||
).list;
|
||||
} catch (error) {
|
||||
Message.error(`关联数据加载失败:${(error as Error).message}`);
|
||||
} finally {
|
||||
relationLoading[resource] = false;
|
||||
}
|
||||
}
|
||||
|
||||
function searchRelation(resource: string | undefined, keyword: string) {
|
||||
if (!resource) return;
|
||||
const timer = relationSearchTimers.get(resource);
|
||||
if (timer) clearTimeout(timer);
|
||||
relationSearchTimers.set(
|
||||
resource,
|
||||
setTimeout(() => loadRelation(resource, keyword.trim()), 250),
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await load();
|
||||
const actionFields = props.definition.detailActions?.flatMap((item) => item.fields ?? []) ?? [];
|
||||
@@ -345,9 +602,7 @@ onMounted(async () => {
|
||||
.filter((value): value is string => Boolean(value)),
|
||||
);
|
||||
await Promise.all(
|
||||
[...relationPaths].map(async (resource) => {
|
||||
relationOptions[resource] = (await resourceApi.list<Row>(resource, 1, 200)).list;
|
||||
}),
|
||||
[...relationPaths].map((resource) => loadRelation(resource)),
|
||||
);
|
||||
if (props.definition.fields.some((field) => field.key === 'platform_role_code')) {
|
||||
const result = await platformApi.listRole();
|
||||
@@ -357,6 +612,15 @@ onMounted(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.definition.resource,
|
||||
async () => {
|
||||
page.value = 1;
|
||||
filters.keyword = '';
|
||||
await load();
|
||||
},
|
||||
);
|
||||
|
||||
function optionLabel(option: Row) {
|
||||
return String(option.name ?? option.title ?? option.code ?? option.username ?? option.contract_no ?? option.order_no ?? option.identity);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="load">刷新</a-button>
|
||||
<a-button v-if="canWrite" type="primary" @click="openCreate">新增</a-button>
|
||||
<a-button v-if="canCreate" 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-button v-if="canEdit" size="mini" @click.stop="openEdit(node)">编辑</a-button>
|
||||
<a-button v-if="canChangeStatus" size="mini" @click.stop="confirmStatus(node)">{{ node.status === 'enabled' ? '停用' : '启用' }}</a-button>
|
||||
<a-button v-if="canArchive" size="mini" status="danger" @click.stop="confirmArchive(node)">归档</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-tree>
|
||||
@@ -19,8 +20,13 @@
|
||||
<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-input-number v-if="field.type === 'number' || field.type === 'money'" v-model="form[field.key]" :precision="field.type === 'money' ? 2 : 0" />
|
||||
<a-switch v-else-if="field.type === 'boolean'" v-model="form[field.key]" />
|
||||
<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">
|
||||
{{ option.name ?? option.menu_code ?? option.identity }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
<a-input v-else v-model="form[field.key]" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -33,6 +39,7 @@ 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';
|
||||
import { useUserStore } from '@/store';
|
||||
|
||||
type Node = Record<string, unknown> & {
|
||||
identity: string;
|
||||
@@ -40,12 +47,25 @@ type Node = Record<string, unknown> & {
|
||||
children: Node[];
|
||||
};
|
||||
const props = defineProps<{ definition: ResourceUiDefinition }>();
|
||||
const userStore = useUserStore();
|
||||
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 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,
|
||||
);
|
||||
const tree = computed(() => {
|
||||
const byIdentity = new Map<string, Node>();
|
||||
const roots: Node[] = [];
|
||||
@@ -68,6 +88,27 @@ function reset(data?: Node) {
|
||||
}
|
||||
}
|
||||
|
||||
function confirmStatus(node: Node) {
|
||||
const status = node.status === 'enabled' ? 'disabled' : 'enabled';
|
||||
Modal.warning({
|
||||
title: status === 'enabled' ? '确认启用' : '确认停用',
|
||||
content: `确定要${status === 'enabled' ? '启用' : '停用'}“${String(node.name ?? node.identity)}”吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await resourceApi.updateStatus(
|
||||
props.definition.resource,
|
||||
node.identity,
|
||||
status,
|
||||
);
|
||||
Message.success('状态已更新');
|
||||
await load();
|
||||
} catch (error) {
|
||||
Message.error((error as Error).message);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingIdentity.value = '';
|
||||
reset();
|
||||
|
||||
Reference in New Issue
Block a user