完善配送端详情字段中文化

This commit is contained in:
czl231
2026-08-23 09:24:47 +08:00
parent bebf9b52b7
commit cbaedafaf1
8 changed files with 757 additions and 117 deletions

View File

@@ -93,6 +93,9 @@ assert(listPage.includes("field.key === 'staff_account_identity'"), '人员范
assert(recordPage.includes('已锁定,不可更换'), '人员资质独立页未锁定所属配送人员');
const attachmentApi = read('src/api/contract-attachment.ts');
const attachmentState = read('src/views/resource/use-contract-attachment.ts');
const detailContent = read('src/views/resource/ResourceDetailContent.vue');
const detailContract = read('src/api/resource-detail-contract.ts');
const resourceDisplay = read('src/api/resource-display.ts');
assert(definitions.includes("type: 'contract-file'"), '合同附件仍按普通文本字段渲染');
assert(recordPage.includes('title="合同附件"'), '合同详情页缺少附件状态卡片');
assert(recordPage.includes('downloadContractAttachment'), '合同详情页缺少独立下载入口');
@@ -101,5 +104,19 @@ assert(attachmentApi.includes('/gasorder_contract/attachment/upload'), '前端
assert(attachmentApi.includes('/:identity') === false, '前端附件接口不得拼接路由模板字面量');
assert(attachmentState.includes('delete payload.file_uri'), '新版管理端仍可能提交裸附件 URI');
assert(attachmentState.includes('payload.attachment_receipt'), '附件保存未提交签名收据');
assert(
detailContract.indexOf("'bound_at'") < detailContract.indexOf("'product_name'") &&
detailContract.indexOf("'product_name'") < detailContract.indexOf("'product_code'"),
'合同气瓶缺少固定业务列序',
);
assert(detailContract.includes("relationIdentityKeys: { product_name: 'product_info_identity' }"), '智能气阀名称未合并唯一标识');
assert(detailContract.includes("relationIdentityKeys: { operator_name: 'operator_identity' }"), '合同修订操作人未合并唯一标识');
assert(detailContent.includes("key.endsWith('_display_name')"), '详情页仍可能重复显示英文辅助名称字段');
assert(detailContent.includes('hasDetailFieldLabel'), '详情页未启用中文字段白名单');
assert(detailContent.includes('查看参数'), '关联记录缺少完整 JSON 查看入口');
assert(detailContent.includes('智能气阀名称取当前设备资料'), '合同气瓶名称与快照语义未说明');
assert(resourceDisplay.includes("user_service_active: '签约用户仍属合同气站'"), '签约服务关系缺少中文业务标签');
assert(resourceDisplay.includes("activate: '启用合同'"), '合同修订动作未中文化');
assert(recordPage.includes("record.value.user_service_active === false"), '失效服务关系下未限制合同动作');
console.log(`独立资源页面契约通过:详情 ${listResources.length},新建 ${creatable.size},编辑 ${editable.size}`);

View File

@@ -0,0 +1,186 @@
/**
* 功能描述:声明配送端聚合详情的固定字段顺序、关联表列和 JSON 展示规则。
* 版本v1.0.0。
*/
export type ResourceCollectionContract = {
columns: string[];
jsonColumns?: string[];
relationIdentityKeys?: Record<string, string>;
};
export type ResourceDetailContract = {
leadingKeys?: string[];
hiddenKeys?: string[];
collections?: Record<string, ResourceCollectionContract>;
};
const contracts: Record<string, ResourceDetailContract> = {
gasorder_contract: {
leadingKeys: [
'contract_no',
'contract_status',
'title',
'user_account_identity',
'user_service_active',
'gas_basic_identity',
'delivery_basic_identity',
'default_delivery_fee',
'signed_at',
'effective_at',
'expired_at',
'terms',
'identity',
'status',
'created_at',
'updated_at',
],
collections: {
products: {
columns: [
'bound_at',
'product_name',
'product_code',
'product_type_name',
'product_params',
'unit_price',
'unbound_at',
'unbind_reason',
],
jsonColumns: ['product_params'],
relationIdentityKeys: { product_name: 'product_info_identity' },
},
revisions: {
columns: [
'occurred_at',
'action',
'contract_status',
'effective_at',
'expired_at',
'operator_name',
'reason',
],
relationIdentityKeys: { operator_name: 'operator_identity' },
},
},
},
gasorder_basic: {
leadingKeys: [
'order_no',
'request_no',
'order_status',
'gasorder_contract_identity',
'creator_type',
'creator_identity',
'user_account_identity',
'gas_basic_identity',
'delivery_basic_identity',
'staff_account_identity',
'identity',
'status',
],
collections: {
items: {
columns: [
'product_name',
'product_code',
'product_type_name',
'product_params',
'unit_price',
],
jsonColumns: ['product_params'],
},
assignments: {
columns: [
'assigned_at',
'gas_basic_display_name',
'delivery_basic_display_name',
'staff_account_display_name',
'assigner_name',
'reason',
],
},
statuses: {
columns: [
'occurred_at',
'from_status',
'to_status',
'operator_name',
'reason',
],
},
tracks: {
columns: [
'attempt_no',
'staff_account_display_name',
'started_at',
'completed_at',
],
relationIdentityKeys: {
staff_account_display_name: 'staff_account_identity',
},
},
confirmations: {
columns: [
'confirmed_at',
'confirm_type',
'recipient_name',
'recipient_phone',
'remark',
],
},
payments: {
columns: [
'attempt_no',
'payment_no',
'payment_order_identity',
'amount',
'payment_status',
],
},
},
},
};
const jsonFieldKeys = new Set([
'params',
'product_params',
'product_snapshot',
'args',
'callback_msg',
]);
/** 返回资源专属详情契约;普通资源使用中文字段白名单动态排序。 */
export function resourceDetailContract(name: string): ResourceDetailContract {
return contracts[name] ?? {};
}
/** 判断主详情字段是否使用完整 JSON 查看器。 */
export function isResourceJsonField(key: string) {
return jsonFieldKeys.has(key);
}
/** 返回集合名称字段对应的稳定唯一标识字段。 */
export function collectionRelationIdentityKey(
resourceName: string,
collectionKey: string,
column: string,
) {
return (
contracts[resourceName]?.collections?.[collectionKey]
?.relationIdentityKeys?.[column] ?? ''
);
}
/** 判断集合字段是否使用完整 JSON 查看器。 */
export function isCollectionJsonField(
resourceName: string,
collectionKey: string,
column: string,
) {
return Boolean(
contracts[resourceName]?.collections?.[
collectionKey
]?.jsonColumns?.includes(column),
);
}

View File

@@ -1,10 +1,13 @@
/**
* 功能描述:统一配送点资源独立页面的字段名称和值展示。
* 版本v1.0.0。
* 版本v1.1.0。
*/
import dayjs from 'dayjs';
import { resourceFieldLabel, type ResourceUiDefinition } from './resources';
import type { ResourceRow } from './resource-record-form';
import {
resourceFieldLabel as defaultResourceFieldLabel,
type ResourceUiDefinition,
} from './resources';
const aliases: Record<string, string> = {
id: 'ID',
@@ -19,34 +22,175 @@ const aliases: Record<string, string> = {
payments: '支付记录',
revisions: '修订记录',
products: '合同气瓶',
addresses: '用户地址',
contract: '合同',
order: '订单',
user_service_active: '签约用户仍属合同气站',
user_account_display_name: '用户账户',
gas_basic_display_name: '气站',
delivery_basic_display_name: '配送点',
staff_account_display_name: '配送人员',
assigner_name: '分配人',
};
/** 聚合子表字段按父资源和集合覆盖全局同名字段语义。 */
const collectionAliases: Record<string, Record<string, string>> = {
'gasorder_contract.products': {
bound_at: '绑定时间',
product_name: '智能气阀名称',
product_code: '气瓶编码',
product_type_name: '气瓶类型名称',
product_params: '气瓶规格参数',
unit_price: '约定充装单价(元)',
unbound_at: '解绑时间',
unbind_reason: '解绑原因',
},
'gasorder_contract.revisions': {
occurred_at: '发生时间',
action: '合同动作',
contract_status: '合同状态',
effective_at: '生效时间',
expired_at: '到期时间',
operator_name: '操作人',
reason: '变更原因',
},
'gasorder_basic.items': {
product_name: '智能气阀名称',
product_code: '设备编码',
product_type_name: '设备类型',
product_params: '设备参数',
unit_price: '成交单价(元)',
},
'gasorder_basic.assignments': {
assigned_at: '分配时间',
gas_basic_display_name: '气站',
delivery_basic_display_name: '配送点',
staff_account_display_name: '配送人员',
assigner_name: '分配人',
reason: '分配原因',
},
'gasorder_basic.statuses': {
occurred_at: '发生时间',
from_status: '原状态',
to_status: '新状态',
operator_name: '操作人',
reason: '原因',
},
'gasorder_basic.tracks': {
attempt_no: '配送尝试次数',
staff_account_display_name: '配送人员',
started_at: '开始时间',
completed_at: '完成时间',
},
'gasorder_basic.confirmations': {
confirmed_at: '确认时间',
confirm_type: '确认方式',
recipient_name: '签收人姓名',
recipient_phone: '签收人电话',
remark: '备注',
},
'gasorder_basic.payments': {
attempt_no: '支付尝试次数',
payment_no: '支付单号',
payment_order_identity: '支付记录唯一标识',
amount: '支付金额(元)',
payment_status: '支付状态',
},
};
const moneyKeys = new Set([
'amount', 'unit_price', 'balance', 'withdrawal_balance', 'delivery_fee',
'discount_amount', 'total_amount', 'default_delivery_fee', 'fee',
'amount',
'unit_price',
'balance',
'withdrawal_balance',
'delivery_fee',
'discount_amount',
'total_amount',
'default_delivery_fee',
'fee',
]);
/** 返回聚合详情中的主记录。 */
export function primaryRecord(detail: ResourceRow): ResourceRow {
for (const key of ['order', 'contract', 'track', 'user']) {
if (detail[key] && typeof detail[key] === 'object') return detail[key] as ResourceRow;
if (detail[key] && typeof detail[key] === 'object')
return detail[key] as ResourceRow;
}
return detail;
}
/** 返回字段中文名称。 */
export function detailFieldLabel(key: string) {
return aliases[key] ?? resourceFieldLabel(key);
export function detailFieldLabel(
definition: ResourceUiDefinition,
key: string,
collectionKey = '',
) {
const normalized = key.endsWith('_masked') ? key.slice(0, -7) : key;
return (
collectionAliases[`${definition.name}.${collectionKey}`]?.[normalized] ??
definition.fields.find((field) => field.key === normalized)?.label ??
aliases[normalized] ??
defaultResourceFieldLabel(normalized)
);
}
/** 判断字段是否具有明确中文契约,未知响应键不得直接回显。 */
export function hasDetailFieldLabel(
definition: ResourceUiDefinition,
key: string,
collectionKey = '',
) {
const normalized = key.endsWith('_masked') ? key.slice(0, -7) : key;
return detailFieldLabel(definition, key, collectionKey) !== normalized;
}
/** 返回通用记录状态名称。 */
export function recordStatusLabel(status: number) {
return { 0: '待审核', 1: '启用', 2: '停用', 3: '已归档', 4: '已冻结' }[status] ?? '未知状态';
return (
{ 0: '待审核', 1: '启用', 2: '停用', 3: '已归档', 4: '已冻结' }[status] ??
`未知状态(${status}`
);
}
/** 返回通用记录状态颜色。 */
export function recordStatusColor(status: number) {
return { 0: 'orange', 1: 'green', 2: 'red', 3: 'gray', 4: 'purple' }[status] ?? 'gray';
return (
{ 0: 'orange', 1: 'green', 2: 'red', 3: 'gray', 4: 'purple' }[status] ??
'gray'
);
}
/** 返回合同业务状态名称。 */
export function contractStatusLabel(status: number) {
return (
{
0: '草稿',
10: '待处理',
11: '生效中',
12: '已过期',
13: '已终止',
}[status] ?? `未知状态(${status}`
);
}
/** 返回合同业务状态颜色。 */
export function contractStatusColor(status: number) {
return (
{ 0: 'gray', 10: 'orange', 11: 'green', 12: 'red', 13: 'gray' }[status] ??
'gray'
);
}
/** 返回合同修订动作名称。 */
export function contractActionLabel(action: unknown) {
const value = String(action);
return (
{
activate: '启用合同',
renew: '续签合同',
terminate: '终止合同',
}[value] ?? `未知动作(${value}`
);
}
/** 将接口字段值转换为中文可读内容。 */
@@ -57,17 +201,53 @@ export function displayResourceValue(
) {
if (value == null || value === '') return '-';
const field = definition.fields.find((item) => item.key === key);
const option = field?.options?.find((item) => String(item.value) === String(value));
const option = field?.options?.find(
(item) => String(item.value) === String(value),
);
if (option) return option.label;
if (key === 'role_code') {
return { delivery: '配送人员', installer: '安装人员', operations: '运维人员' }[String(value)]
?? String(value);
return (
{ delivery: '配送人员', installer: '安装人员', operations: '运维人员' }[
String(value)
] ?? String(value)
);
}
if (typeof value === 'boolean') return value ? '是' : '否';
if (key === 'status') return recordStatusLabel(Number(value));
if (moneyKeys.has(key) || key.endsWith('_amount') || key.endsWith('_balance_after')) {
if (key === 'contract_status') return contractStatusLabel(Number(value));
if (key === 'action') return contractActionLabel(value);
if (
[
'order_status',
'previous_order_status',
'from_status',
'to_status',
].includes(key)
) {
return (
{
0: '草稿',
16: '已创建',
18: '已分配',
19: '充装中',
20: '已就绪',
21: '异常',
22: '已取消',
23: '已完成',
33: '配送中',
34: '待确认',
}[Number(value)] ?? `未知状态(${String(value)}`
);
}
if (
moneyKeys.has(key) ||
key.endsWith('_amount') ||
key.endsWith('_balance_after')
) {
const amount = Number(value);
return Number.isFinite(amount) ? `¥${(amount / 100).toFixed(2)}` : String(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));
@@ -77,10 +257,23 @@ export function displayResourceValue(
return String(value);
}
/** 判断软删除时间是否为空对象或空值。 */
export function isEmptyDeletedAt(value: unknown) {
if (value == null || value === '') return true;
if (typeof value !== 'object') return false;
return Object.values(value as Record<string, unknown>).every(
(item) => item == null || item === '' || item === false || item === 0,
);
}
/** 返回合同编辑阻止原因。 */
export function recordEditReason(definition: ResourceUiDefinition, row: ResourceRow) {
export function recordEditReason(
definition: ResourceUiDefinition,
row: ResourceRow,
) {
if (definition.name !== 'gasorder_contract') return '';
if (Number(row.contract_status) !== 0) return '只有草稿状态的合同可以编辑';
if (![0, 1].includes(Number(row.status))) return '停用、已归档或已冻结的合同不可编辑';
if (![0, 1].includes(Number(row.status)))
return '停用、已归档或已冻结的合同不可编辑';
return '';
}

View File

@@ -1,44 +1,91 @@
<!-- 功能描述以信息表和集合页签展示配送点资源详情版本v1.0.0 -->
<!-- 功能描述按中文白名单和固定子表契约展示配送点资源详情版本v1.1.0 -->
<template>
<div class="detail-stack">
<a-card title="基本信息" :bordered="false" class="detail-card">
<div class="detail-grid">
<div
v-for="[key, value] in entries"
:key="key"
v-for="entry in entries"
:key="entry.key"
class="detail-item"
:class="{ 'detail-item-wide': wideKeys.has(key) || isObject(value) }"
:class="{ 'detail-item-wide': entry.wide }"
>
<span class="detail-label">{{ detailFieldLabel(key) }}</span>
<a-popover v-if="isObject(value)" position="left">
<span class="detail-label">{{ entry.label }}</span>
<a-popover v-if="entry.jsonValue" position="left">
<a-button type="text" size="mini">查看内容</a-button>
<template #content>
<pre class="json-value">{{ displayResourceValue(definition, key, value) }}</pre>
</template>
<template #content><pre class="json-value">{{ entry.value }}</pre></template>
</a-popover>
<IdentityText v-else-if="isIdentity(key, value)" :value="String(value)" />
<span v-else class="detail-value">{{ displayResourceValue(definition, key, value) }}</span>
<a-tag
v-else-if="entry.key === 'contract_status'"
:color="contractStatusColor(Number(entry.rawValue))"
>{{ entry.value }}</a-tag>
<a-tag
v-else-if="entry.key === 'user_service_active'"
:color="entry.rawValue === true ? 'green' : 'red'"
>{{ entry.value }}</a-tag>
<IdentityText
v-else-if="entry.key === 'identity' || (entry.identityValue && !entry.relationName)"
:value="entry.identityValue || String(entry.rawValue)"
/>
<RelationNameText
v-else-if="entry.identityValue && entry.relationName"
:name="entry.relationName"
:identity="entry.identityValue"
:identity-label="entry.label"
/>
<span v-else class="detail-value">{{ entry.value }}</span>
</div>
</div>
</a-card>
<a-card v-if="collections.length" title="关联记录" :bordered="false" class="detail-card">
<a-tabs>
<a-tab-pane v-for="collection in collections" :key="collection.key" :title="detailFieldLabel(collection.key)">
<a-tab-pane
v-for="collection in collections"
:key="collection.key"
:title="collection.title"
>
<a-alert
v-if="definition.name === 'gasorder_contract' && collection.key === 'products'"
class="collection-note"
type="info"
show-icon
>
智能气阀名称取当前设备资料气瓶编码和类型名称为合同绑定快照
</a-alert>
<div class="collection-table-shell">
<a-table :data="collection.rows" :pagination="false" size="small" table-layout-fixed>
<template #columns>
<a-table-column
v-for="column in collection.columns"
:key="column"
:title="detailFieldLabel(column)"
:width="column.endsWith('_at') ? 150 : 180"
:title="detailFieldLabel(definition, column, collection.key)"
:width="collectionColumnWidth(collection.key, column)"
ellipsis
tooltip
>
<template #cell="{ record }">
<IdentityText v-if="isIdentity(column, record[column])" :value="String(record[column])" />
<template v-else>{{ displayResourceValue(definition, column, record[column]) }}</template>
<template #cell="{ record: row }">
<RelationNameText
v-if="collectionIdentityKey(collection.key, column) && row[collectionIdentityKey(collection.key, column)]"
:name="displayResourceValue(definition, column, row[column])"
:identity="String(row[collectionIdentityKey(collection.key, column)])"
:identity-label="detailFieldLabel(definition, column, collection.key)"
/>
<IdentityText
v-else-if="column.includes('identity') && row[column]"
:value="String(row[column])"
/>
<a-popover
v-else-if="isCollectionJsonField(definition.name, collection.key, column)"
position="left"
>
<a-button type="text" size="mini">查看参数</a-button>
<template #content>
<pre class="collection-json-value">{{ formatJson(row[column]) }}</pre>
</template>
</a-popover>
<template v-else>
{{ displayResourceValue(definition, column, row[column]) }}
</template>
</template>
</a-table-column>
</template>
@@ -53,89 +100,202 @@
<script setup lang="ts">
import { computed } from 'vue';
import {
contractStatusColor,
detailFieldLabel,
displayResourceValue,
hasDetailFieldLabel,
isEmptyDeletedAt,
primaryRecord,
} from '@/api/resource-display';
import {
collectionRelationIdentityKey,
isCollectionJsonField,
isResourceJsonField,
resourceDetailContract,
} from '@/api/resource-detail-contract';
import type { ResourceRow } from '@/api/resource-record-form';
import type { ResourceUiDefinition } from '@/api/resources';
import IdentityText from '@/components/IdentityText.vue';
import RelationNameText from '@/views/shared/RelationNameText.vue';
const props = defineProps<{ definition: ResourceUiDefinition; detail: ResourceRow }>();
const wideKeys = new Set(['address', 'terms', 'remark', 'reason', 'content', 'body']);
type DetailEntry = {
key: string;
label: string;
value: string;
rawValue: unknown;
jsonValue: boolean;
wide: boolean;
identityValue: string;
relationName: string;
};
const props = defineProps<{
definition: ResourceUiDefinition;
detail: ResourceRow;
}>();
/** 返回聚合记录中标识字段对应的服务端可读名称。 */
function relationDisplayName(row: ResourceRow, key: string) {
const conventionalKey = key.endsWith('_identity')
? `${key.slice(0, -9)}_display_name`
: '';
const displayKey =
{
user_account_identity: 'user_account_display_name',
gas_basic_identity: 'gas_basic_display_name',
delivery_basic_identity: 'delivery_basic_display_name',
staff_account_identity: 'staff_account_display_name',
creator_identity: 'creator_display_name',
operator_identity: 'operator_name',
}[key] ?? conventionalKey;
return displayKey ? String(row[displayKey] ?? '').trim() : '';
}
/** 将对象、合法 JSON 字符串或历史普通文本转换为完整可读内容。 */
function formatJson(value: unknown) {
if (value == null || value === '') return '暂无参数';
if (typeof value === 'object') return JSON.stringify(value, null, 2);
const text = String(value).trim();
if (!text) return '暂无参数';
try {
return JSON.stringify(JSON.parse(text), null, 2);
} catch {
return text;
}
}
/** 返回关联表名称列绑定的稳定唯一标识字段。 */
function collectionIdentityKey(collectionKey: string, column: string) {
return collectionRelationIdentityKey(
props.definition.name,
collectionKey,
column,
);
}
/** 保持日期紧凑、关系可复制、JSON 按钮清晰,其余列使用业务可读宽度。 */
function collectionColumnWidth(collectionKey: string, column: string) {
if (column.endsWith('_at')) return 170;
if (collectionIdentityKey(collectionKey, column)) return 210;
if (column.includes('identity')) return 210;
if (isCollectionJsonField(props.definition.name, collectionKey, column))
return 120;
return 160;
}
const entries = computed<DetailEntry[]>(() => {
const row = primaryRecord(props.detail);
const contract = resourceDetailContract(props.definition.name);
const excluded = new Set([
'id',
'password',
'password_hash',
'avatar',
'attachment',
...(contract.hiddenKeys ?? []),
]);
const preferred = [
...(contract.leadingKeys ?? ['identity', 'status']),
...props.definition.fields.map((field) => field.key),
'created_at',
'updated_at',
];
return [...new Set([...preferred, ...Object.keys(row)])].flatMap((key) => {
// 脱敏辅助键只作为原业务字段的回退值,不能再生成重复详情项。
if (
key.endsWith('_masked') &&
props.definition.fields.some((field) => field.key === key.slice(0, -7))
)
return [];
const maskedKey = `${key}_masked`;
const actualKey = Object.prototype.hasOwnProperty.call(row, key)
? key
: Object.prototype.hasOwnProperty.call(row, maskedKey)
? maskedKey
: '';
if (
!actualKey ||
excluded.has(key) ||
key.endsWith('_id') ||
key.endsWith('_display_name') ||
!hasDetailFieldLabel(props.definition, actualKey)
)
return [];
const rawValue = row[actualKey];
if (Array.isArray(rawValue)) return [];
if (['deleted_at', 'DeletedAt'].includes(key) && isEmptyDeletedAt(rawValue))
return [];
const relationName = relationDisplayName(row, key);
const jsonValue =
isResourceJsonField(key) ||
Boolean(rawValue && typeof rawValue === 'object');
const value =
relationName ||
(jsonValue
? formatJson(rawValue)
: displayResourceValue(props.definition, actualKey, rawValue));
const field = props.definition.fields.find((item) => item.key === key);
const identityValue =
rawValue &&
(relationName ||
key === 'identity' ||
key.endsWith('_identity') ||
field?.type === 'identity' ||
field?.showIdentityCopy)
? String(rawValue)
: '';
return [
{
key,
label: detailFieldLabel(props.definition, actualKey),
value,
rawValue,
jsonValue,
identityValue,
relationName,
wide:
jsonValue ||
/(address|terms|content|body|remark|reason|params|args)$/.test(key),
},
];
});
});
const record = computed(() => primaryRecord(props.detail));
const entries = computed(() =>
Object.entries(record.value).filter(
([key, value]) =>
key !== 'id' &&
key !== 'avatar' &&
key !== 'password' &&
key !== 'password_hash' &&
key !== 'DeletedAt' &&
key !== 'deleted_at' &&
!key.endsWith('_id') &&
!key.endsWith('_masked') &&
!Array.isArray(value),
),
);
const collections = computed(() =>
Object.entries(props.detail)
.filter(([, value]) => Array.isArray(value) && value.length > 0)
.map(([key, value]) => {
.flatMap(([key, value]) => {
const rows = value as ResourceRow[];
const columns = [
...new Set(
rows.flatMap((row) =>
Object.keys(row).filter(
(column) => column !== 'id' && column !== 'DeletedAt' &&
column !== 'deleted_at' && !column.endsWith('_id'),
),
),
),
].slice(0, 10);
return { key, rows, columns };
const collectionContract = resourceDetailContract(props.definition.name)
.collections?.[key];
if (!collectionContract && !hasDetailFieldLabel(props.definition, key))
return [];
const available = [...new Set(rows.flatMap((row) => Object.keys(row)))];
const columns = (collectionContract?.columns ?? available).filter(
(column) =>
available.includes(column) &&
column !== 'id' &&
!column.endsWith('_id') &&
!['deleted_at', 'DeletedAt'].includes(column) &&
hasDetailFieldLabel(props.definition, column, key),
);
if (!columns.length) return [];
return [
{ key, title: detailFieldLabel(props.definition, key), rows, columns },
];
}),
);
/** 判断字段是否应使用可复制唯一标识组件。 */
function isIdentity(key: string, value: unknown) {
return Boolean(value && (key === 'identity' || key.endsWith('_identity')));
}
/** 判断详情值是否需要折叠到弹出层查看。 */
function isObject(value: unknown) {
return Boolean(value && typeof value === 'object');
}
</script>
<style scoped>
.detail-stack {
display: grid;
width: 100%;
min-width: 0;
gap: 20px;
}
.detail-card {
width: 100%;
min-width: 0;
max-width: 100%;
overflow: hidden;
border-radius: 10px;
}
.detail-card :deep(.arco-card-header) {
height: 48px;
padding: 0 24px;
}
.detail-card :deep(.arco-card-body) {
min-width: 0;
padding: 20px 24px;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px 42px;
}
<style scoped lang="less">
.detail-stack { display: grid; width: 100%; min-width: 0; gap: 20px; }
.detail-card { width: 100%; min-width: 0; max-width: 100%; overflow: hidden; border-radius: 10px; }
.detail-card :deep(.arco-card-header) { height: 48px; padding: 0 24px; }
.detail-card :deep(.arco-card-body) { min-width: 0; padding: 20px 24px; }
.detail-card :deep(.arco-tabs),
.detail-card :deep(.arco-tabs-content),
.detail-card :deep(.arco-tabs-pane) { min-width: 0; max-width: 100%; }
.detail-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px 42px; }
.detail-item {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
@@ -148,26 +308,24 @@ function isObject(value: unknown) {
}
.detail-item-wide { grid-column: 1 / -1; }
.detail-label { color: var(--color-text-3); white-space: nowrap; }
.detail-value {
min-width: 0;
.detail-value { min-width: 0; color: var(--color-text-1); white-space: pre-wrap; word-break: break-word; }
.json-value,
.collection-json-value {
min-width: 280px;
max-width: min(560px, 70vw);
max-height: 360px;
margin: 0;
padding: 12px;
overflow: auto;
color: var(--color-text-1);
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
background: var(--color-fill-1);
border-radius: 6px;
white-space: pre-wrap;
word-break: break-word;
}
.json-value {
max-height: 320px;
margin: 0;
overflow: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.collection-table-shell {
width: 100%;
min-width: 0;
max-width: 100%;
overflow-x: auto;
}
.collection-note { margin-bottom: 12px; }
.collection-table-shell { width: 100%; min-width: 0; max-width: 100%; overflow-x: auto; }
@media (max-width: 1280px) {
.detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

View File

@@ -216,8 +216,15 @@ const canEditRecord = computed(
const visibleActions = computed(() =>
(definition.value.detailActions ?? []).filter(
(action) =>
!action.visibleFor ||
action.visibleFor.values.includes(record.value[action.visibleFor.field] as string | number),
!(
definition.value.name === 'gasorder_contract' &&
record.value.user_service_active === false &&
action.name !== '终止合同'
) &&
(
!action.visibleFor ||
action.visibleFor.values.includes(record.value[action.visibleFor.field] as string | number)
),
),
);
const unsaved = useUnsavedRecord(