统一平台资源中文展示并修复模拟订单状态

This commit is contained in:
czl231
2026-08-15 22:06:42 +08:00
parent a20e228296
commit a663821853
24 changed files with 1048 additions and 175 deletions

View File

@@ -1,6 +1,6 @@
<!--
功能以响应式信息卡和子表页签展示标准资源详情
版本v1.7.0
版本v1.8.0
-->
<template>
<div class="detail-stack">
@@ -8,12 +8,19 @@
<div class="detail-grid">
<div v-for="entry in entries" :key="entry.key" class="detail-item" :class="{ 'detail-item-wide': entry.wide }">
<span class="detail-label">{{ entry.label }}</span>
<pre v-if="entry.objectValue" class="json-value">{{ entry.value }}</pre>
<a-popover v-if="entry.jsonValue" position="left">
<a-button type="text" size="mini">查看内容</a-button>
<template #content><pre class="json-value">{{ entry.value }}</pre></template>
</a-popover>
<IdentityText v-else-if="entry.key === 'identity'" :value="String(entry.value)" />
<div v-else-if="entry.identityValue" class="detail-relation-value">
<span class="detail-value">{{ entry.value }}</span>
<IdentityText :value="entry.identityValue" />
</div>
<RelationNameText
v-else-if="entry.identityValue"
:name="entry.value"
:identity="entry.identityValue"
:identity-label="entry.label"
:clickable="Boolean(entry.relationResource)"
@open="openRelation(entry.relationResource, entry.identityValue)"
/>
<span v-else class="detail-value">{{ entry.value }}</span>
</div>
</div>
@@ -29,20 +36,20 @@
v-for="column in collection.columns"
:key="column"
:title="resourceFieldLabel(definition, column, collection.key)"
:width="collectionColumnWidth(column)"
:width="collectionColumnWidth(collection.key, column)"
ellipsis
tooltip
>
<template #cell="{ record }">
<div v-if="collectionRelationIdentityKey(collection.key, column)" class="collection-relation-value">
<div v-if="collectionRelationIdentityKey(definition.name, collection.key, column)" class="collection-relation-value">
<span>{{ displayRawValue(column, record[column]) }}</span>
<IdentityText
v-if="record[collectionRelationIdentityKey(collection.key, column)]"
:value="String(record[collectionRelationIdentityKey(collection.key, column)])"
v-if="record[collectionRelationIdentityKey(definition.name, collection.key, column)]"
:value="String(record[collectionRelationIdentityKey(definition.name, collection.key, column)])"
/>
</div>
<IdentityText v-else-if="column.includes('identity') && record[column]" :value="String(record[column])" />
<a-popover v-else-if="isCollectionJsonColumn(column)" position="left">
<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">{{ formatCollectionJson(record[column]) }}</pre>
@@ -62,16 +69,25 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import {
displayRawValue,
displayResourceField,
hasResourceFieldLabel,
isEmptyDeletedAt,
primaryRecord,
resourceFieldLabel,
} from '@/api/resource-display';
import {
collectionRelationIdentityKey,
isCollectionJsonField,
isResourceJsonField,
resourceDetailContract,
} from '@/api/resource-detail-contract';
import type { ResourceRow } from '@/api/resource-page-rules';
import type { ResourceUiDefinition } from '@/api/resources';
import IdentityText from '@/components/IdentityText.vue';
import RelationNameText from '@/views/shared/RelationNameText.vue';
const props = defineProps<{
definition: ResourceUiDefinition;
@@ -88,22 +104,34 @@ type DetailEntry = {
key: string;
label: string;
value: string;
objectValue: boolean;
jsonValue: boolean;
wide: boolean;
identityValue: string;
relationResource: string;
};
/** 统一关联记录表列宽:日期时间列保持紧凑,唯一标识列保留完整复制空间。 */
function collectionColumnWidth(column: string) {
if (column.endsWith('_at')) return 150;
if (column.includes('identity')) return 220;
if (isCollectionJsonColumn(column)) return 100;
return 160;
const router = useRouter();
/** 打开关系资源详情;没有详情路由的树形资源保持只读。 */
function openRelation(resource: string, identity: string) {
if (!resource) return;
const target = router
.getRoutes()
.find(
(item) =>
item.meta.resource === resource && item.meta.recordMode === 'detail',
);
if (!target?.name) return;
void router.push({ name: target.name, params: { identity } });
}
/** 标识关联子表中需要完整查看的 JSON 快照字段。 */
function isCollectionJsonColumn(column: string) {
return column === 'product_params';
/** 统一关联记录表列宽:日期时间列保持紧凑,唯一标识列保留完整复制空间。 */
function collectionColumnWidth(collectionKey: string, column: string) {
if (column.endsWith('_at')) return 150;
if (column.includes('identity')) return 220;
if (isCollectionJsonField(props.definition.name, collectionKey, column))
return 100;
return 160;
}
/** 将字符串或对象参数格式化为可读 JSON历史非 JSON 文本保持原样。 */
@@ -119,20 +147,16 @@ function formatCollectionJson(value: unknown) {
}
}
/** 返回集合可读名称对应的稳定标识字段。 */
function collectionRelationIdentityKey(collectionKey: string, column: string) {
if (collectionKey !== 'assignments') return '';
return (
{
gas_basic_display_name: 'gas_basic_identity',
delivery_basic_display_name: 'delivery_basic_identity',
staff_account_display_name: 'staff_account_identity',
}[column] ?? ''
);
}
/** 返回订单详情关联标识对应的服务端可读名称字段。 */
function detailRelationDisplayName(row: ResourceRow, key: string) {
function detailRelationDisplayName(
row: ResourceRow,
key: string,
configuredDisplayKey = '',
) {
const conventionalKey = key.endsWith('_identity')
? `${key.slice(0, -9)}_display_name`
: '';
const displayKey =
{
creator_identity: 'creator_display_name',
@@ -141,13 +165,16 @@ function detailRelationDisplayName(row: ResourceRow, key: string) {
delivery_basic_identity: 'delivery_basic_display_name',
staff_account_identity: 'staff_account_display_name',
operator_identity: 'operator_name',
assigner_identity: 'assigner_name',
}[key] ?? '';
if (!displayKey) return '';
return String(row[displayKey] ?? '').trim();
return String(
row[configuredDisplayKey || displayKey || conventionalKey] ?? '',
).trim();
}
const entries = computed<DetailEntry[]>(() => {
const row = primaryRecord(props.detail);
const contract = resourceDetailContract(props.definition.name);
const excluded = new Set([
'id',
'password',
@@ -156,27 +183,12 @@ const entries = computed<DetailEntry[]>(() => {
'attachment',
// 合同可读名称只负责渲染“配送合同”,不作为独立业务字段重复展示。
'contract_display_name',
...(contract.hiddenKeys ?? []),
]);
if (props.accountSummary) {
for (const key of ['username', 'identity', 'created_at']) excluded.add(key);
}
const leadingKeys =
props.definition.name === 'gasorder_basic'
? [
'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',
]
: ['identity', 'status'];
const leadingKeys = contract.leadingKeys ?? ['identity', 'status'];
const preferred = [
...leadingKeys,
...props.definition.fields.map((field) => field.key),
@@ -202,7 +214,7 @@ const entries = computed<DetailEntry[]>(() => {
excluded.has(key) ||
key.endsWith('_id') ||
key.endsWith('_display_name') ||
(props.definition.name === 'gasorder_basic' && key === 'operator_name')
!hasResourceFieldLabel(props.definition, actualKey)
)
return [];
const value = row[actualKey];
@@ -210,7 +222,12 @@ const entries = computed<DetailEntry[]>(() => {
if (['deleted_at', 'DeletedAt'].includes(key) && isEmptyDeletedAt(value))
return [];
const field = props.definition.fields.find((item) => item.key === key);
const relationDisplayName = detailRelationDisplayName(row, key);
// 资源已声明详情名称键时优先直接使用接口返回值,避免关系候选慢查询导致显示失败。
const relationDisplayName = detailRelationDisplayName(
row,
key,
field?.detailDisplayKey,
);
const display = relationDisplayName
? relationDisplayName
: field
@@ -221,22 +238,32 @@ const entries = computed<DetailEntry[]>(() => {
props.fieldOptions,
)
: displayRawValue(actualKey, value);
const objectValue = typeof value === 'object' && value !== null;
const jsonValue =
isResourceJsonField(key) ||
(typeof value === 'object' && value !== null);
return [
{
key,
label: resourceFieldLabel(props.definition, actualKey),
value: display,
objectValue,
value: jsonValue ? formatCollectionJson(value) : display,
jsonValue,
identityValue:
(relationDisplayName ||
field?.type === 'identity' ||
field?.showIdentityCopy ||
field?.staffRelation?.showIdentityCopy) &&
value
? String(value)
: '',
relationResource:
field?.relation ??
field?.dynamicRelation?.resources[
String(row[field.dynamicRelation.parentKey] ?? '')
] ??
contract.relationResources?.[key] ??
'',
wide:
objectValue ||
jsonValue ||
/(address|terms|content|body|remark|reason|params|args)$/.test(key),
},
];
@@ -246,7 +273,10 @@ const entries = computed<DetailEntry[]>(() => {
const collections = computed(() =>
Object.entries(props.detail)
.filter(([, value]) => Array.isArray(value) && value.length > 0)
.map(([key, value]) => {
.flatMap(([key, value]) => {
const contract = resourceDetailContract(props.definition.name)
.collections?.[key];
if (!contract) return [];
const rows = value as ResourceRow[];
const availableColumns = [
...new Set(
@@ -261,56 +291,15 @@ const collections = computed(() =>
),
),
];
const preferredColumns: Record<string, string[]> = {
'gasorder_contract.products': [
'bound_at',
'product_name',
'product_code',
'product_type_name',
'unit_price',
'product_info_identity',
'unbound_at',
'unbind_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',
],
};
const preferred =
preferredColumns[`${props.definition.name}.${key}`] ?? [];
const fixedColumns = ['assignments', 'items', 'statuses'].includes(key);
const columns = (
fixedColumns
? preferred
: [...new Set([...preferred, ...availableColumns])]
)
.filter((column) => availableColumns.includes(column))
.slice(0, 8);
return {
const columns = contract.columns.filter((column) =>
availableColumns.includes(column),
);
return [{
key,
title: resourceFieldLabel(props.definition, key),
rows,
columns,
};
}];
}),
);
</script>

View File

@@ -11,6 +11,7 @@ import {
} from '@/api/resource-staff-relation';
import type { ResourceField } from '@/api/resources';
import type { ResourceRow } from '@/api/resource-page-rules';
import { fieldRelationResource } from '@/api/resource-display';
import { createRelationRequestVersionGuard } from './resource-relation-linkage-policy';
export type ResourceRelationLoadOptions = {
@@ -133,13 +134,14 @@ export function useResourceRelations(
/** 补载表单或详情中已经保存的关系值,避免分页和筛选导致回显裸标识。 */
async function ensureValues(fields: ResourceField[], values: ResourceRow) {
const requests = fields.flatMap((field) => {
if (!field.relation) return [];
const resource = fieldRelationResource(field, values);
if (!resource) return [];
const value = values[field.key];
const identities = Array.isArray(value) ? value : [value];
return identities
.map((identity) => String(identity ?? ''))
.filter(Boolean)
.map((identity) => ensure(field.relation as string, identity));
.map((identity) => ensure(resource, identity));
});
await Promise.all(requests);
}