Files
platforms/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue
czl231 deea31d79e 修复合同详情字段中文显示
通用详情页复用中文字段字典,并为合同气瓶快照补充上下文名称。
合同状态按模型常量显示中文,数据库字段和 API 保持不变。
已通过资源页面检查、类型检查和生产构建。
2026-08-12 19:49:41 +08:00

236 lines
6.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
功能以响应式信息卡和子表页签展示标准资源详情
版本v1.3.0
-->
<template>
<div class="detail-stack">
<a-card title="基本信息" :bordered="false" class="detail-card">
<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>
<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>
<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="collection.title">
<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="resourceFieldLabel(definition, column, collection.key)"
:width="column.includes('identity') ? 220 : 160"
ellipsis
tooltip
>
<template #cell="{ record }">
<IdentityText v-if="column.includes('identity') && record[column]" :value="String(record[column])" />
<template v-else>{{ displayRawValue(column, record[column]) }}</template>
</template>
</a-table-column>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import IdentityText from '@/components/IdentityText.vue';
import {
displayRawValue,
displayResourceField,
isEmptyDeletedAt,
primaryRecord,
resourceFieldLabel,
} from '@/api/resource-display';
import type { ResourceUiDefinition } from '@/api/resources';
import type { ResourceRow } from '@/api/resource-page-rules';
const props = defineProps<{
definition: ResourceUiDefinition;
detail: ResourceRow;
relationOptions: Record<string, ResourceRow[]>;
fieldOptions?: Record<
string,
Array<{ label: string; value: string | number }>
>;
accountSummary: boolean;
}>();
type DetailEntry = {
key: string;
label: string;
value: string;
objectValue: boolean;
wide: boolean;
identityValue: string;
};
const entries = computed<DetailEntry[]>(() => {
const row = primaryRecord(props.detail);
const excluded = new Set(['id', 'password', 'password_hash', 'avatar']);
if (props.accountSummary) {
for (const key of ['username', 'identity', 'created_at']) excluded.add(key);
}
const preferred = [
'identity',
'status',
...props.definition.fields.map((field) => field.key),
'created_at',
'updated_at',
];
const keys = [...new Set([...preferred, ...Object.keys(row)])];
return keys.flatMap((key) => {
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')) return [];
const value = row[actualKey];
if (Array.isArray(value)) return [];
if (['deleted_at', 'DeletedAt'].includes(key) && isEmptyDeletedAt(value))
return [];
const field = props.definition.fields.find((item) => item.key === key);
const display = field
? displayResourceField(
field,
row,
props.relationOptions,
props.fieldOptions,
)
: displayRawValue(actualKey, value);
const objectValue = typeof value === 'object' && value !== null;
return [
{
key,
label: resourceFieldLabel(props.definition, actualKey),
value: display,
objectValue,
identityValue:
(field?.showIdentityCopy || field?.staffRelation?.showIdentityCopy) && value
? String(value)
: '',
wide:
objectValue ||
/(address|terms|content|body|remark|reason|params|args)$/.test(key),
},
];
});
});
const collections = computed(() =>
Object.entries(props.detail)
.filter(([, value]) => Array.isArray(value) && value.length > 0)
.map(([key, value]) => {
const rows = value as ResourceRow[];
const columns = [
...new Set(
rows.flatMap((row) =>
Object.keys(row).filter(
(column) => column !== 'id' && !column.endsWith('_id'),
),
),
),
].slice(0, 8);
return {
key,
title: resourceFieldLabel(props.definition, key),
rows,
columns,
};
}),
);
</script>
<style scoped lang="less">
.detail-stack {
display: grid;
gap: 12px;
}
.detail-card {
border-radius: 10px;
}
.detail-card :deep(.arco-card-header) {
height: 48px;
padding: 0 24px;
}
.detail-card :deep(.arco-card-body) {
padding: 20px 24px;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px 42px;
padding: 0;
}
.detail-item {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
column-gap: 12px;
align-items: start;
min-height: 38px;
padding: 8px 0;
border-bottom: 1px solid var(--color-border-1);
}
.detail-item-wide {
grid-column: 1 / -1;
}
.detail-label {
color: var(--color-text-3);
white-space: nowrap;
}
.detail-value {
color: var(--color-text-1);
white-space: pre-wrap;
word-break: break-word;
}
.detail-relation-value {
display: grid;
gap: 4px;
}
.json-value {
max-height: 360px;
margin: 0;
padding: 12px;
overflow: auto;
color: var(--color-text-1);
background: var(--color-fill-1);
border-radius: 6px;
white-space: pre-wrap;
}
@media (max-width: 1280px) {
.detail-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.detail-card :deep(.arco-card-header),
.detail-card :deep(.arco-card-body) {
padding-right: 16px;
padding-left: 16px;
}
.detail-grid {
grid-template-columns: 1fr;
gap: 4px;
}
.detail-item,
.detail-item-wide {
grid-column: auto;
}
}
</style>