功能:优化用户地址账户名称展示

This commit is contained in:
czl231
2026-08-11 21:33:20 +08:00
parent e375e9851b
commit 8d6ffca1e2
9 changed files with 281 additions and 22 deletions

View File

@@ -18,6 +18,7 @@
"avatar-retry:check": "node scripts/check-avatar-upload-cache.mjs",
"staff-organization:check": "node scripts/check-staff-organization-linkage.mjs",
"staff-relations:check": "node scripts/check-staff-relation-policy.mjs",
"user-address-display:check": "node scripts/check-user-address-relation-display.mjs",
"audit:platform": "node scripts/check-backend-contract.mjs",
"lint": "biome lint .",
"lint:fix": "biome lint --write .",

View File

@@ -0,0 +1,31 @@
/**
* 功能:静态检查用户地址列表的账户名称展示、标识降级与复制入口。
* 版本v1.0.0
*/
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
function source(path) {
return readFileSync(resolve(appRoot, path), 'utf8');
}
function expectIncludes(content, marker, message) {
if (!content.includes(marker)) throw new Error(`用户地址展示检查失败:${message}`);
}
const resources = source('src/api/resources.ts');
const listPage = source('src/views/shared/CrudListPage.vue');
const relationName = source('src/views/shared/RelationNameText.vue');
const fieldDisplay = source('src/views/shared/resource-list-field-display.ts');
expectIncludes(resources, "listLabel: '用户账户'", '列表列名未改为用户账户');
expectIncludes(resources, 'listRelationNameOnly: true', '用户地址未启用账户名称展示');
expectIncludes(listPage, '<RelationNameText', '列表未使用账户名称组件');
expectIncludes(fieldDisplay, 'return match ? optionLabel(match) : identity;', '关系名称缺失时未降级显示标识');
expectIncludes(relationName, '用户唯一标识:${identity}', '缺少完整标识悬停提示');
expectIncludes(relationName, '@click.stop="copyIdentity"', '缺少标识复制入口');
console.log('用户地址展示检查通过:账户名称、标识降级、悬停与复制入口均已覆盖。');

View File

@@ -34,6 +34,7 @@ export type ResourceField = {
required?: boolean;
relation?: string;
displayRelationLabel?: boolean;
listRelationNameOnly?: boolean;
displayPrecision?: number;
emptyText?: string;
placeholder?: string;
@@ -358,7 +359,7 @@ export const resources: ResourceUiDefinition[] = [
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), f('gas_basic_identity', { label: '所属气站', type: 'identity', relation: '/gas_basic' }), f('delivery_basic_identity', { label: '所属配送点', type: 'identity', relation: '/delivery_basic', relationLinkage: { parentKey: 'gas_basic_identity', optionParentKey: 'gas_basic_identity', filterKey: 'gas_basic_identities', backfillParent: true } }), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true, { staffRelation: { roles: 'context', lockPrefilled: true, showIdentityCopy: true } }), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true, emptyText: '未填写', placeholder: '未填写' }), f('longitude', { displayPrecision: 6, emptyText: '未填写', placeholder: '未填写' }), f('latitude', { displayPrecision: 6, emptyText: '未填写', placeholder: '未填写' }), f('is_default')]),
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true, { listLabel: '用户账户', listRelationNameOnly: true }), f('address', { required: true, emptyText: '未填写', placeholder: '未填写' }), f('longitude', { displayPrecision: 6, emptyText: '未填写', placeholder: '未填写' }), f('latitude', { displayPrecision: 6, emptyText: '未填写', placeholder: '未填写' }), f('is_default')]),
define('user_service_relation', '用户服务关系', 'writable', [relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('staff_account_identity', '/staff_account', false, { staffRelation: { roles: ['installer', 'delivery', 'operations'], enabledOnly: true } })]),
define('producer_account', '生产商管理', 'writable', [f('producer_code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('phone'), f('address'), f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), f('remark')]),

View File

@@ -1,6 +1,6 @@
<!--
功能展示标准资源列表并将新建详情和编辑入口导航到独立页面
版本v2.1.0
版本v2.2.0
-->
<template>
<a-card :title="listTitle" :bordered="false">
@@ -42,10 +42,10 @@
v-for="field in displayFields"
:key="field.key"
:title="field.listLabel ?? field.label"
:width="columnWidth(field)"
:width="resourceListColumnWidth(definition.name, field)"
:align="isProtectedListAvatarField(definition.name, field.key) ? 'center' : 'left'"
:ellipsis="!isProtectedListAvatarField(definition.name, field.key)"
:tooltip="!isProtectedListAvatarField(definition.name, field.key)"
:ellipsis="!isProtectedListAvatarField(definition.name, field.key) && !field.listRelationNameOnly"
:tooltip="!isProtectedListAvatarField(definition.name, field.key) && !field.listRelationNameOnly"
>
<template #cell="{ record }">
<ProtectedAvatarThumbnail
@@ -56,6 +56,11 @@
:refresh-key="avatarRefreshKey"
:loader="avatarLoader"
/>
<RelationNameText
v-else-if="field.listRelationNameOnly && identityFieldValue(field, record)"
:name="relationListName(field, record, relations.options)"
:identity="identityFieldValue(field, record)"
/>
<IdentityText
v-else-if="field.type === 'identity' && !field.displayRelationLabel && identityFieldValue(field, record)"
:value="identityFieldValue(field, record)"
@@ -168,6 +173,12 @@ import type { ResourceField, ResourceUiDefinition } from '@/api/resources';
import { useUserStore } from '@/store';
import { useResourceRelations } from '../resource/use-resource-relations';
import ProtectedAvatarThumbnail from './ProtectedAvatarThumbnail.vue';
import RelationNameText from './RelationNameText.vue';
import {
identityFieldValue,
relationListName,
resourceListColumnWidth,
} from './resource-list-field-display';
import {
createProtectedListAvatarLoader,
isProtectedListAvatarField,
@@ -446,23 +457,6 @@ async function changePage(next: number) {
await load();
}
function identityFieldValue(field: ResourceField, row: ResourceRow) {
return String(row[field.key] ?? row[`${field.key}_masked`] ?? '');
}
function columnWidth(field: ResourceField) {
if (isProtectedListAvatarField(props.definition.name, field.key)) return 72;
if (field.type === 'datetime' || field.type === 'date') return 180;
if (field.type === 'money' || field.type === 'number') return 140;
if (field.type === 'boolean') return 110;
if (field.type === 'identity' || field.type === 'identity-list') return 240;
if (field.type === 'textarea') return 280;
if (field.key.includes('phone')) return 150;
if (/(name|title|address|content|remark|reason|terms)$/.test(field.key))
return 220;
return 180;
}
/** 使用资源静态选项或运行时平台角色名称显示列表字段。 */
function displayListField(field: ResourceField, row: ResourceRow) {
return displayResourceField(field, row, relations.options, fieldOptions);

View File

@@ -0,0 +1,76 @@
<!-- 功能描述列表以关系名称为主展示并提供完整唯一标识的悬停查看与复制版本v1.0.0 -->
<template>
<div class="relation-name-text">
<a-tooltip :content="`用户唯一标识:${identity}`">
<span class="relation-name">{{ displayName }}</span>
</a-tooltip>
<a-tooltip content="复制用户唯一标识">
<button
class="copy-button"
type="button"
:aria-label="`复制用户唯一标识 ${identity}`"
@click.stop="copyIdentity"
>
<icon-copy />
</button>
</a-tooltip>
</div>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue';
import { IconCopy } from '@arco-design/web-vue/es/icon';
import { computed } from 'vue';
const props = defineProps<{ name: string; identity: string }>();
const displayName = computed(() => props.name || props.identity);
/** 复制完整用户唯一标识,并给出明确操作反馈。 */
async function copyIdentity() {
try {
await navigator.clipboard.writeText(props.identity);
Message.success('用户唯一标识已复制');
} catch {
Message.error('复制失败,请从悬浮提示中复制');
}
}
</script>
<style scoped>
.relation-name-text {
display: flex;
gap: 6px;
align-items: center;
min-width: 0;
}
.relation-name {
min-width: 0;
overflow: hidden;
color: var(--color-text-1);
text-overflow: ellipsis;
white-space: nowrap;
}
.copy-button {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
color: rgb(var(--primary-6));
font-size: 14px;
background: transparent;
border: 0;
border-radius: 4px;
cursor: pointer;
}
.copy-button:hover,
.copy-button:focus-visible {
background: var(--color-fill-2);
outline: none;
}
</style>

View File

@@ -0,0 +1,44 @@
/**
* 功能描述:集中处理标准资源列表字段的关系名称、唯一标识和列宽展示。
* 版本v1.0.0
*/
import { optionLabel } from '@/api/resource-display';
import type { ResourceRow } from '@/api/resource-page-rules';
import type { ResourceField } from '@/api/resources';
import { isProtectedListAvatarField } from './protected-list-avatar-loader';
/** 读取关系列表字段的完整唯一标识。 */
export function identityFieldValue(field: ResourceField, row: ResourceRow) {
return String(row[field.key] ?? row[`${field.key}_masked`] ?? '');
}
/** 获取列表关系的可读名称,关系未加载时降级为完整唯一标识。 */
export function relationListName(
field: ResourceField,
row: ResourceRow,
relationOptions: Record<string, ResourceRow[]>,
) {
const identity = identityFieldValue(field, row);
const match = (relationOptions[field.relation ?? ''] ?? []).find(
(option) => String(option.identity) === identity,
);
return match ? optionLabel(match) : identity;
}
/** 按字段类型返回标准列表列宽。 */
export function resourceListColumnWidth(
definitionName: string,
field: ResourceField,
) {
if (isProtectedListAvatarField(definitionName, field.key)) return 72;
if (field.type === 'datetime' || field.type === 'date') return 180;
if (field.type === 'money' || field.type === 'number') return 140;
if (field.type === 'boolean') return 110;
if (field.type === 'identity' || field.type === 'identity-list') return 240;
if (field.type === 'textarea') return 280;
if (field.key.includes('phone')) return 150;
if (/(name|title|address|content|remark|reason|terms)$/.test(field.key)) {
return 220;
}
return 180;
}