diff --git a/docs/操作日志_智能气阀状态中文展示_20260813.md b/docs/操作日志_智能气阀状态中文展示_20260813.md new file mode 100644 index 0000000..f5b69cd --- /dev/null +++ b/docs/操作日志_智能气阀状态中文展示_20260813.md @@ -0,0 +1,42 @@ +# 智能气阀状态中文展示操作日志 + +操作时间:2026-08-13 +操作类型:修改 +影响模块:平台总后台智能气阀列表、详情与状态操作 + +## 操作前状态 + +列表和详情直接显示 `10`、`28` 等状态编码;状态弹窗打开后下拉框为空,管理员无法同时确认当前状态和需要选择的目标状态。 + +## 具体操作 + +1. 复用统一状态选项,将列表和详情状态编码显示为中文名称。 +2. 状态弹窗显示当前状态中文名称。 +3. 下拉字段改为“目标状态”,打开时保持未选择。 +4. 目标状态选项排除当前状态,避免重复提交。 +5. 新建页面隐藏服务端生成的状态字段,保持原创建协议。 +6. 增加状态展示专项检查。 + +## 操作后状态 + +管理员可以直接识别当前状态,并明确选择一个不同的目标状态。服务端继续执行检修状态和订单占用校验。 + +## 代码变更 + +- `frontend/platform_admin/src/api/resources.ts`:统一状态选项并配置中文展示。 +- `frontend/platform_admin/src/api/resource-page-rules.ts`:新建页隐藏状态字段。 +- `frontend/platform_admin/src/views/resource/ResourceActionDialog.vue`:显示当前状态并过滤目标选项。 +- `frontend/platform_admin/scripts/check-product-lifecycle-display.mjs`:增加专项检查。 +- `frontend/platform_admin/package.json`:注册专项检查命令。 + +## 验证结果 + +- `npm.cmd run product-lifecycle-display:check`:通过,当前状态中文显示且目标选项排除原状态。 +- `npm.cmd run product-ownership:check`:通过,归属互斥功能未回退。 +- `npm.cmd run resource-pages:check`:通过,详情 46 类、新建 25 类、编辑 23 类。 +- `npm.cmd run build`:通过,TypeScript 类型检查与 Vite 生产构建成功。 +- `git diff --check`:通过,未发现空白符错误。 + +## 风险评估 + +不修改数据库、接口结构和后端状态校验;未知历史状态会保留原编码并显示明确提示。 diff --git a/docs/项目文档_智能气阀状态中文展示_v1.0.md b/docs/项目文档_智能气阀状态中文展示_v1.0.md new file mode 100644 index 0000000..5d15a8b --- /dev/null +++ b/docs/项目文档_智能气阀状态中文展示_v1.0.md @@ -0,0 +1,39 @@ +# 项目文档:智能气阀状态中文展示 v1.0 + +## 1. 项目概述 + +智能气阀状态在列表、详情和状态操作中统一使用中文业务名称。状态操作区分当前状态和目标状态,避免管理员理解数字编码或重复提交原状态。 + +## 2. 目录结构说明 + +```text +platforms/ +├── frontend/platform_admin/ +│ ├── src/api/resources.ts +│ ├── src/api/resource-page-rules.ts +│ ├── src/views/resource/ResourceActionDialog.vue +│ └── scripts/check-product-lifecycle-display.mjs +└── docs/ + ├── 项目文档_智能气阀状态中文展示_v1.0.md + └── 操作日志_智能气阀状态中文展示_20260813.md +``` + +## 3. 状态字典 + +- `10`:待处理 +- `28`:在库 +- `29`:运输中 +- `30`:使用中 +- `31`:维修中 +- `27`:已报废 + +## 4. 核心行为 + +- 列表与详情显示中文状态。 +- 状态弹窗展示当前状态,不默认选择目标状态。 +- 目标状态列表不包含当前状态。 +- 后端继续判断检修记录和活动订单是否允许目标状态。 + +## 5. 接口兼容性 + +状态接口仍为 `PATCH /product_info/:identity/lifecycle`,请求仍提交数字 `product_status`。本次不修改智能气阀参数字段。 diff --git a/frontend/platform_admin/package.json b/frontend/platform_admin/package.json index 2d63f37..9422608 100644 --- a/frontend/platform_admin/package.json +++ b/frontend/platform_admin/package.json @@ -24,6 +24,7 @@ "user-address-display:check": "node scripts/check-user-address-relation-display.mjs", "product-ownership:check": "node scripts/check-product-ownership-action.mjs", "product-ownership-display:check": "node scripts/check-product-ownership-display.mjs", + "product-lifecycle-display:check": "node scripts/check-product-lifecycle-display.mjs", "resource-search:check": "node scripts/check-resource-search.mjs", "audit:platform": "node scripts/check-backend-contract.mjs", "lint": "biome lint .", diff --git a/frontend/platform_admin/scripts/check-product-lifecycle-display.mjs b/frontend/platform_admin/scripts/check-product-lifecycle-display.mjs new file mode 100644 index 0000000..6d1c806 --- /dev/null +++ b/frontend/platform_admin/scripts/check-product-lifecycle-display.mjs @@ -0,0 +1,39 @@ +/** + * 功能:检查智能气阀状态的中文展示和目标状态选择规则。 + * 版本:v1.0.0 + */ +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +const resources = await readFile( + fileURLToPath(new URL('../src/api/resources.ts', import.meta.url)), + 'utf8', +); +const dialog = await readFile( + fileURLToPath( + new URL('../src/views/resource/ResourceActionDialog.vue', import.meta.url), + ), + 'utf8', +); +const rules = await readFile( + fileURLToPath(new URL('../src/api/resource-page-rules.ts', import.meta.url)), + 'utf8', +); + +const checks = [ + [resources, "f('product_status', { type: 'select', options: productStatusOptions"], + [resources, "label: '目标状态'"], + [resources, "placeholder: '请选择目标状态'"], + [rules, "createHiddenKeys: ['product_status']"], + [dialog, '当前状态:{{ currentProductStatusLabel }}'], + [dialog, "option.value) !== String(props.record.product_status)"], + [dialog, ':fields="actionFields"'], +]; + +for (const [source, fragment] of checks) { + if (!source.includes(fragment)) { + throw new Error(`智能气阀状态展示检查缺少实现:${fragment}`); + } +} + +console.log('智能气阀状态展示检查通过:中文展示当前状态,目标选项排除原状态。'); diff --git a/frontend/platform_admin/src/api/resource-page-rules.ts b/frontend/platform_admin/src/api/resource-page-rules.ts index a9cfda9..79f5bf5 100644 --- a/frontend/platform_admin/src/api/resource-page-rules.ts +++ b/frontend/platform_admin/src/api/resource-page-rules.ts @@ -94,6 +94,7 @@ const pageRules: Record = { editKeys: ['code', 'name', 'address', 'manager', 'phone'], }, product_info: { + createHiddenKeys: ['product_status'], editKeys: [ 'name', 'producer_account_identity', diff --git a/frontend/platform_admin/src/api/resources.ts b/frontend/platform_admin/src/api/resources.ts index 0751ea1..ec3561b 100644 --- a/frontend/platform_admin/src/api/resources.ts +++ b/frontend/platform_admin/src/api/resources.ts @@ -385,6 +385,14 @@ function define( } const reason = [f('reason', { required: true })]; +const productStatusOptions = [ + { label: '待处理', value: 10 }, + { label: '在库', value: 28 }, + { label: '运输中', value: 29 }, + { label: '使用中', value: 30 }, + { label: '维修中', value: 31 }, + { label: '已报废', value: 27 }, +]; export const resources: ResourceUiDefinition[] = [ { ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' }, walletOwnerType: 'gas' }, @@ -447,8 +455,8 @@ export const resources: ResourceUiDefinition[] = [ define('producer_account', '生产商管理', 'writable', [f('producer_code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('phone'), f('address', { emptyText: '未填写', placeholder: '请输入地址' }), f('username', { required: true }), f('password', { required: true }), f('display_name'), producerAdminRole(), f('remark')]), 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', { emptyText: '未填写', placeholder: '请输入地址' }), f('manager'), f('phone')]), - define('product_info', '智能气阀', 'editable', [f('code', { required: true }), f('name', { required: true }), relation('producer_account_identity', '/producer_account', true, { label: '生产商', listLabel: '生产商', listRelationNameOnly: true, displayRelationLabel: true, showIdentityCopy: true, emptyText: '未填写', placeholder: '请选择生产商' }), relation('product_type_identity', '/product_type', true, { label: '智能气阀类型', listLabel: '智能气阀类型', listRelationNameOnly: true, displayRelationLabel: true, showIdentityCopy: true, emptyText: '未填写', placeholder: '请选择智能气阀类型' }), f('params', { required: true }), relation('warehouse_identity', '/product_warehouse', false, { label: '库房', emptyText: '未填写', placeholder: '请选择库房' }), relation('gas_basic_identity', '/gas_basic', false, { label: '气站', emptyText: '未填写', placeholder: '请选择气站' }), relation('delivery_basic_identity', '/delivery_basic', false, { label: '配送点', emptyText: '未填写', placeholder: '请选择配送点' }), relation('user_account_identity', '/user_account', false, { label: '用户', emptyText: '未填写', placeholder: '请选择用户' }), f('produced_at', { required: true })], 'list', [ - { name: '修改智能气阀状态', resource: '/product_info/:identity/lifecycle', method: 'PATCH', fields: [f('product_status', { required: true, type: 'select', options: [{ label: '待处理', value: 10 }, { label: '在库', value: 28 }, { label: '运输中', value: 29 }, { label: '使用中', value: 30 }, { label: '维修中', value: 31 }, { label: '已报废', value: 27 }] })] }, + define('product_info', '智能气阀', 'editable', [f('code', { required: true }), f('name', { required: true }), f('product_status', { type: 'select', options: productStatusOptions, unknownValueLabel: '未知状态' }), relation('producer_account_identity', '/producer_account', true, { label: '生产商', listLabel: '生产商', listRelationNameOnly: true, displayRelationLabel: true, showIdentityCopy: true, emptyText: '未填写', placeholder: '请选择生产商' }), relation('product_type_identity', '/product_type', true, { label: '智能气阀类型', listLabel: '智能气阀类型', listRelationNameOnly: true, displayRelationLabel: true, showIdentityCopy: true, emptyText: '未填写', placeholder: '请选择智能气阀类型' }), f('params', { required: true }), relation('warehouse_identity', '/product_warehouse', false, { label: '库房', emptyText: '未填写', placeholder: '请选择库房' }), relation('gas_basic_identity', '/gas_basic', false, { label: '气站', emptyText: '未填写', placeholder: '请选择气站' }), relation('delivery_basic_identity', '/delivery_basic', false, { label: '配送点', emptyText: '未填写', placeholder: '请选择配送点' }), relation('user_account_identity', '/user_account', false, { label: '用户', emptyText: '未填写', placeholder: '请选择用户' }), f('produced_at', { required: true })], 'list', [ + { name: '修改智能气阀状态', resource: '/product_info/:identity/lifecycle', method: 'PATCH', fields: [f('product_status', { label: '目标状态', required: true, type: 'select', options: productStatusOptions, placeholder: '请选择目标状态' })] }, { name: '变更智能气阀归属', resource: '/product_info/:identity', method: 'PUT', fields: [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, type: 'select', options: [{ label: '入库', value: 'warehouse' }, { label: '分配', value: 'assigned' }, { label: '归还', value: 'returned' }, { label: '人工调整', value: 'manual' }] }), f('reason', { required: true }), 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', { type: 'select', options: resourceSearchEnumOptions('product_repair', 'result') }), f('target_product_status', { type: 'select', options: [{ label: '在库', value: 28 }, { label: '运输中', value: 29 }, { label: '使用中', value: 30 }, { label: '报废', value: 27 }] }), f('content'), f('operator'), f('remark')]), diff --git a/frontend/platform_admin/src/views/resource/ResourceActionDialog.vue b/frontend/platform_admin/src/views/resource/ResourceActionDialog.vue index d38a676..7acdf1a 100644 --- a/frontend/platform_admin/src/views/resource/ResourceActionDialog.vue +++ b/frontend/platform_admin/src/views/resource/ResourceActionDialog.vue @@ -18,10 +18,13 @@ 智能气阀只能有一个当前归属。选择新归属后会自动清空其他归属,操作会写入审计记录。 + + 当前状态:{{ currentProductStatusLabel }}。请选择一个不同的目标状态,服务端会校验检修和订单占用情况。 + >({}); const submitting = ref(false); const relations = useResourceRelations(); +const isProductLifecycleAction = computed( + () => props.action?.resource === '/product_info/:identity/lifecycle', +); +const actionFields = computed(() => + (props.action?.fields ?? []).map((field) => + isProductLifecycleAction.value && field.key === 'product_status' + ? { + ...field, + options: (field.options ?? []).filter( + (option) => String(option.value) !== String(props.record.product_status), + ), + } + : field, + ), +); const requiredKeys = computed(() => - (props.action?.fields ?? []) + actionFields.value .filter((field) => field.required) .map((field) => field.key), ); const isOwnershipAction = computed( () => props.action?.resource === '/product_info/:identity', ); +const currentProductStatusLabel = computed(() => { + const statusField = props.action?.fields?.find( + (field) => field.key === 'product_status', + ); + return ( + statusField?.options?.find( + (option) => String(option.value) === String(props.record.product_status), + )?.label ?? `未知状态(${String(props.record.product_status ?? '')})` + ); +}); const ownershipKeys = [ 'warehouse_identity', 'gas_basic_identity', @@ -95,12 +123,12 @@ watch( () => [props.visible, props.action?.name] as const, async ([visible]) => { if (!visible || !props.action) return; - for (const field of props.action.fields ?? []) { + for (const field of actionFields.value) { form[field.key] = isOwnershipAction.value ? props.record[field.key] : undefined; } - await relations.preload(props.action.fields ?? []); + await relations.preload(actionFields.value); if (props.action.resource.includes('/menu')) { try { const [menus, assigned] = await Promise.all([ @@ -125,14 +153,14 @@ async function submit() { const action = props.action; if (!action) return; if ( - (action.fields ?? []).some( + actionFields.value.some( (field) => field.required && isMissingField(form[field.key]), ) ) { Message.warning('请填写必填字段'); return; } - const invalidPassword = (action.fields ?? []).find( + const invalidPassword = actionFields.value.find( (field) => field.type === 'password' && !isMissingField(form[field.key]) && @@ -151,7 +179,7 @@ async function submit() { } submitting.value = true; try { - const payload = buildResourcePayload(action.fields ?? [], form, 'create'); + const payload = buildResourcePayload(actionFields.value, form, 'create'); if (isOwnershipAction.value) { for (const key of ownershipKeys) payload[key] = String(form[key] ?? ''); }