完善配送合同关联信息展示与归属校验

三端合同页面改为优先显示用户、气站和配送点业务名称,并保留标识与合同编号复制能力。

平台端增加气站与直属配送点联动,后端禁止平台主管及跨气站配送点绑定合同,同时补充测试和中文文档。
This commit is contained in:
czl231
2026-08-12 01:09:24 +08:00
parent 6bcde94388
commit 3cad8fd0ba
11 changed files with 156 additions and 6 deletions

View File

@@ -30,6 +30,8 @@ export type ResourceField = {
placeholder?: string;
showIdentityCopy?: boolean;
readonlyRelationText?: boolean;
/** 列表中以可悬浮、可复制的紧凑文本展示普通字段。 */
listCopyable?: boolean;
options?: Array<{ label: string; value: string | number }>;
defaultValue?: string | number | boolean;
readonly?: boolean;
@@ -490,7 +492,7 @@ const deliveryOverrides: ResourceUiDefinition[] = [
{ name: '重置密码', resource: '/user_account/:identity/password', method: 'PUT', fields: [f('password', { required: true })] },
]),
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true }), relation('user_account_identity', '/user_account', true), 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', [
define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true, listCopyable: true }), relation('user_account_identity', '/user_account', true, { label: '用户账户', listLabel: '用户账户', listRelationNameOnly: true, showIdentityCopy: true, readonlyRelationText: true }), 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, visibleFor: { field: 'contract_status', values: [10] } },
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } },
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },

View File

@@ -38,6 +38,7 @@
<template #cell="{ record }">
<RelationNameText v-if="field.listRelationNameOnly && identityFieldValue(field, record)" :name="relationListName(field, record)" :identity="identityFieldValue(field, record)" :identity-label="field.label" />
<IdentityText v-else-if="field.type === 'identity' && identityFieldValue(field, record)" :value="identityFieldValue(field, record)" />
<IdentityText v-else-if="field.listCopyable && record[field.key]" :value="String(record[field.key])" />
<template v-else>{{ displayFieldValue(field, record) }}</template>
</template>
</a-table-column>
@@ -189,6 +190,7 @@
<a-descriptions :column="1" bordered>
<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>
<RelationNameText v-else-if="relationDetailField(key) && value" :name="relationListName(relationDetailField(key)!, detail)" :identity="String(value)" :identity-label="relationDetailField(key)!.label" />
<template v-else>{{ displayValue(key, value) }}</template>
</a-descriptions-item>
</a-descriptions>
@@ -1052,6 +1054,11 @@ function displayValue(key: string, value: unknown) {
return String(value);
}
// relationDetailField 将详情中的关联标识映射回资源字段,以复用业务名称展示和标识复制。
function relationDetailField(key: string) {
return props.definition.fields.find((field) => field.key === key && field.relation);
}
/** 将各管理端稳定角色编码转换为中文名称,未知编码保留审计线索。 */
function accountRoleLabel(resource: string | undefined, value: unknown) {
const code = String(value ?? '');

View File

@@ -30,6 +30,8 @@ export type ResourceField = {
placeholder?: string;
showIdentityCopy?: boolean;
readonlyRelationText?: boolean;
/** 列表中以可悬浮、可复制的紧凑文本展示普通字段。 */
listCopyable?: boolean;
options?: Array<{ label: string; value: string | number }>;
defaultValue?: string | number | boolean;
readonly?: boolean;
@@ -451,7 +453,7 @@ const gasOverrides: ResourceUiDefinition[] = [
{ name: '重置密码', resource: '/user_account/:identity/password', method: 'PUT', fields: [f('password', { required: true })] },
]),
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true }), relation('user_account_identity', '/user_account', 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', [
define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true, listCopyable: true }), relation('user_account_identity', '/user_account', true, { label: '用户账户', listLabel: '用户账户', listRelationNameOnly: true, showIdentityCopy: true, readonlyRelationText: true }), relation('delivery_basic_identity', '/delivery_basic', false, { label: '所属配送点', listLabel: '所属配送点', listRelationNameOnly: true, emptyText: '暂未指定', placeholder: '请选择默认配送点,可留空', showIdentityCopy: true }), 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, visibleFor: { field: 'contract_status', values: [10] } },
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } },
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },

View File

@@ -38,6 +38,7 @@
<template #cell="{ record }">
<RelationNameText v-if="field.listRelationNameOnly && identityFieldValue(field, record)" :name="relationListName(field, record)" :identity="identityFieldValue(field, record)" :identity-label="field.label" />
<IdentityText v-else-if="field.type === 'identity' && identityFieldValue(field, record)" :value="identityFieldValue(field, record)" />
<IdentityText v-else-if="field.listCopyable && record[field.key]" :value="String(record[field.key])" />
<template v-else>{{ displayFieldValue(field, record) }}</template>
</template>
</a-table-column>
@@ -189,6 +190,7 @@
<a-descriptions :column="1" bordered>
<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>
<RelationNameText v-else-if="relationDetailField(key) && value" :name="relationListName(relationDetailField(key)!, detail)" :identity="String(value)" :identity-label="relationDetailField(key)!.label" />
<template v-else>{{ displayValue(key, value) }}</template>
</a-descriptions-item>
</a-descriptions>
@@ -1052,6 +1054,11 @@ function displayValue(key: string, value: unknown) {
return String(value);
}
// relationDetailField 将详情中的关联标识映射回资源字段,以复用业务名称展示和标识复制。
function relationDetailField(key: string) {
return props.definition.fields.find((field) => field.key === key && field.relation);
}
/** 将各管理端稳定角色编码转换为中文名称,未知编码保留审计线索。 */
function accountRoleLabel(resource: string | undefined, value: unknown) {
const code = String(value ?? '');

View File

@@ -160,8 +160,8 @@ const resourcesSource = await readFile(
);
assert.equal(
(resourcesSource.match(/relationLinkage:/g) ?? []).length,
2,
'联动配置必须显式启用于工作人员用户服务关系的配送点字段',
3,
'联动配置必须显式启用于工作人员用户服务关系和配送合同的配送点字段',
);
assert.match(resourcesSource, /filterKey: 'gas_basic_identities'/);
assert.match(resourcesSource, /organizationScoped: true/);

View File

@@ -46,6 +46,8 @@ export type ResourceField = {
placeholder?: string;
showIdentityCopy?: boolean;
readonlyRelationText?: boolean;
/** 列表中以可悬浮、可复制的紧凑文本展示普通字段。 */
listCopyable?: boolean;
options?: Array<{ label: string; value: string | number }>;
defaultValue?: string | number | boolean;
readonlyOnCreate?: boolean;
@@ -429,7 +431,28 @@ export const resources: ResourceUiDefinition[] = [
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')]),
define('product_owner', '智能气阀归属记录', 'readonly', []),
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', [
define('gasorder_contract', '配送合同', 'managed', [
f('contract_no', { required: true, listCopyable: true }),
relation('user_account_identity', '/user_account', true, {
label: '用户账户', listLabel: '用户账户', listRelationNameOnly: true,
displayRelationLabel: true, showIdentityCopy: true, readonlyRelationText: true,
}),
relation('gas_basic_identity', '/gas_basic', true, {
label: '所属气站', listLabel: '所属气站', listRelationNameOnly: true,
displayRelationLabel: true, showIdentityCopy: true, readonlyRelationText: true,
}),
relation('delivery_basic_identity', '/delivery_basic', false, {
label: '所属配送点', listLabel: '所属配送点', listRelationNameOnly: true,
displayRelationLabel: true, emptyText: '暂未指定', placeholder: '请选择默认配送点,可留空',
showIdentityCopy: true,
relationLinkage: {
parentKey: 'gas_basic_identity', optionParentKey: 'gas_basic_identity',
filterKey: 'gas_basic_identities', backfillParent: true,
},
}),
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, visibleFor: { field: 'contract_status', values: [10] } },
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12] } },
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },

View File

@@ -66,6 +66,10 @@
v-else-if="field.type === 'identity' && !field.displayRelationLabel && identityFieldValue(field, record)"
:value="identityFieldValue(field, record)"
/>
<IdentityText
v-else-if="field.listCopyable && record[field.key]"
:value="String(record[field.key])"
/>
<template v-else>{{ displayListField(field, record) }}</template>
</template>
</a-table-column>