Files
platforms/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue

236 lines
6.5 KiB
Vue
Raw Normal View History

<!--
功能以响应式信息卡和子表页签展示标准资源详情
版本v1.2.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)"
: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>