236 lines
6.5 KiB
Vue
236 lines
6.5 KiB
Vue
<!--
|
||
功能:以响应式信息卡和子表页签展示标准资源详情。
|
||
版本: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>
|