From 4cc17c9efb6993ac65b8b5a234411210a62e1d99 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 28 Jul 2026 14:26:09 +0800 Subject: [PATCH] feat(platform-admin): align frontend with backend contracts --- .../scripts/check-backend-contract.mjs | 14 +- .../platform_admin/src/api/resource-form.ts | 7 + frontend/platform_admin/src/api/resources.ts | 81 +++-- .../src/router/routes/modules/platform.ts | 29 +- .../src/views/shared/CrudListPage.vue | 324 ++++++++++++++++-- .../src/views/shared/TreePage.vue | 51 ++- 6 files changed, 438 insertions(+), 68 deletions(-) diff --git a/frontend/platform_admin/scripts/check-backend-contract.mjs b/frontend/platform_admin/scripts/check-backend-contract.mjs index 3d717a9..baaac1a 100644 --- a/frontend/platform_admin/scripts/check-backend-contract.mjs +++ b/frontend/platform_admin/scripts/check-backend-contract.mjs @@ -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 = [ diff --git a/frontend/platform_admin/src/api/resource-form.ts b/frontend/platform_admin/src/api/resource-form.ts index 93ec25e..130f11c 100644 --- a/frontend/platform_admin/src/api/resource-form.ts +++ b/frontend/platform_admin/src/api/resource-form.ts @@ -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; diff --git a/frontend/platform_admin/src/api/resources.ts b/frontend/platform_admin/src/api/resources.ts index 1f9f89a..7c5af36 100644 --- a/frontend/platform_admin/src/api/resources.ts +++ b/frontend/platform_admin/src/api/resources.ts @@ -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 = { @@ -87,8 +97,8 @@ const fieldLabels: Record = { 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 = { 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 = { 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 = { confirmed_at: '确认时间', sort_no: '排序', product_code: '商品编码', - price_amount: '售价(分)', + price_amount: '售价(元)', stock_quantity: '库存', value: '属性值', image_uri: '图片地址', @@ -149,7 +159,7 @@ const fieldLabels: Record = { quantity: '数量', selected: '是否选中', product_snapshot: '商品快照', - sale_amount: '成交金额(分)', + sale_amount: '成交金额(元)', score: '评分', paid_at: '支付时间', settlement_no: '结算单号', @@ -157,7 +167,7 @@ const fieldLabels: Record = { period_start: '结算开始时间', period_end: '结算结束时间', bill_date: '账单日期', - difference_amount: '差异金额(分)', + difference_amount: '差异金额(元)', content_type: '内容类型', body: '正文', version_no: '版本号', @@ -175,11 +185,14 @@ const fieldLabels: Record = { }; 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 { 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 }, ]), diff --git a/frontend/platform_admin/src/router/routes/modules/platform.ts b/frontend/platform_admin/src/router/routes/modules/platform.ts index 4afec14..35acb2d 100644 --- a/frontend/platform_admin/src/router/routes/modules/platform.ts +++ b/frontend/platform_admin/src/router/routes/modules/platform.ts @@ -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, [ diff --git a/frontend/platform_admin/src/views/shared/CrudListPage.vue b/frontend/platform_admin/src/views/shared/CrudListPage.vue index 478c6a8..a005032 100644 --- a/frontend/platform_admin/src/views/shared/CrudListPage.vue +++ b/frontend/platform_admin/src/views/shared/CrudListPage.vue @@ -6,11 +6,12 @@ 新建 - + 查询 + 重置 @@ -28,26 +29,32 @@ - + + - + - + @@ -55,7 +62,7 @@ {{ option.label }} - + {{ optionLabel(option) }} @@ -70,27 +77,38 @@ - +
{{ formatValue(value) }}
- +
+ + + + + + + {{ action.name }}
- + - + {{ option.label }} - + {{ optionLabel(option) }} @@ -101,7 +119,9 @@