统一平台资源中文展示并修复模拟订单状态
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user