feat: 标准资源使用独立页面并优化详情布局
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
<!--
|
||||
功能:以响应式信息卡和子表页签展示标准资源详情。
|
||||
版本:v1.1.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)" />
|
||||
<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[]>;
|
||||
accountSummary: boolean;
|
||||
}>();
|
||||
|
||||
type DetailEntry = {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
objectValue: boolean;
|
||||
wide: boolean;
|
||||
};
|
||||
|
||||
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)
|
||||
: displayRawValue(actualKey, value);
|
||||
const objectValue = typeof value === 'object' && value !== null;
|
||||
return [
|
||||
{
|
||||
key,
|
||||
label: resourceFieldLabel(props.definition, actualKey),
|
||||
value: display,
|
||||
objectValue,
|
||||
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: 120px minmax(0, 1fr);
|
||||
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);
|
||||
text-align: right;
|
||||
padding-right: 18px;
|
||||
}
|
||||
.detail-value {
|
||||
color: var(--color-text-1);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.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;
|
||||
grid-template-columns: 100px minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user