功能:完善用户服务关系展示与组织联动

统一平台、气站和配送点后台的服务关系业务名称展示。
新增气站、配送点、服务人员三级联动及暂未分配语义。
后端补充组织归属、人员角色和启用状态的严格校验。
补充前后端测试、项目文档、操作日志及本机日志忽略规则。
This commit is contained in:
czl231
2026-08-12 00:37:58 +08:00
parent a37b147f5f
commit 6bcde94388
24 changed files with 682 additions and 27 deletions

View File

@@ -21,9 +21,15 @@ export type ResourceFieldType =
export type ResourceField = {
key: string;
label: string;
listLabel?: string;
type?: ResourceFieldType;
required?: boolean;
relation?: string;
listRelationNameOnly?: boolean;
emptyText?: string;
placeholder?: string;
showIdentityCopy?: boolean;
readonlyRelationText?: boolean;
options?: Array<{ label: string; value: string | number }>;
defaultValue?: string | number | boolean;
readonly?: boolean;
@@ -271,8 +277,13 @@ function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
return { key, label, type, ...options };
}
function relation(key: string, resource: string, required = false): ResourceField {
return f(key, { type: 'identity', relation: resource, required });
function relation(
key: string,
resource: string,
required = false,
options: Partial<ResourceField> = {},
): ResourceField {
return f(key, { ...options, type: 'identity', relation: resource, required });
}
/** 创建固定管理员角色字段,确保页面仅展示后端支持的角色。 */
@@ -332,7 +343,12 @@ const platformResources: ResourceUiDefinition[] = [
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', 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 }), f('longitude'), f('latitude'), 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')]),
define('user_service_relation', '用户服务关系', 'writable', [
relation('user_account_identity', '/user_account', true, { label: '用户账户', listLabel: '用户账户', listRelationNameOnly: true, showIdentityCopy: true, readonlyRelationText: true }),
relation('gas_basic_identity', '/gas_basic', false, { label: '所属气站', listLabel: '所属气站', listRelationNameOnly: true, emptyText: '暂未分配', placeholder: '请选择所属气站', showIdentityCopy: true }),
relation('delivery_basic_identity', '/delivery_basic', false, { label: '所属配送点', listLabel: '所属配送点', listRelationNameOnly: true, emptyText: '暂未分配', placeholder: '请选择所属配送点', showIdentityCopy: true }),
relation('staff_account_identity', '/staff_account', false, { label: '服务人员', listLabel: '服务人员', listRelationNameOnly: true, emptyText: '暂未分配', placeholder: '请选择服务人员', showIdentityCopy: true }),
]),
define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
define('product_warehouse', '智能气阀库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone')]),

View File

@@ -34,9 +34,10 @@
<a-table-column title="唯一标识" :width="150">
<template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template>
</a-table-column>
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :width="columnWidth(field)" ellipsis tooltip>
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.listLabel ?? field.label" :width="columnWidth(field)" :ellipsis="!field.listRelationNameOnly" :tooltip="!field.listRelationNameOnly">
<template #cell="{ record }">
<IdentityText v-if="field.type === 'identity' && identityFieldValue(field, record)" :value="identityFieldValue(field, record)" />
<RelationNameText v-if="field.listRelationNameOnly && identityFieldValue(field, record)" :name="relationListName(field, record)" :identity="identityFieldValue(field, record)" :identity-label="field.label" />
<IdentityText v-else-if="field.type === 'identity' && identityFieldValue(field, record)" :value="identityFieldValue(field, record)" />
<template v-else>{{ displayFieldValue(field, record) }}</template>
</template>
</a-table-column>
@@ -269,6 +270,7 @@ import type {
} from '@/api/resources';
import { useUserStore } from '@/store';
import IdentityText from '@/components/IdentityText.vue';
import RelationNameText from './RelationNameText.vue';
type Row = Record<string, unknown>;
const props = defineProps<{ definition: ResourceUiDefinition }>();
@@ -1153,6 +1155,15 @@ function identityFieldValue(field: ResourceField, row: Row) {
return String(row[field.key] ?? row[`${field.key}_masked`] ?? '');
}
/** 获取关联记录的业务名称;关系尚未加载时降级显示完整唯一标识。 */
function relationListName(field: ResourceField, row: Row) {
const identity = identityFieldValue(field, row);
const match = (relationOptions[field.relation ?? ''] ?? []).find(
(option) => String(option.identity) === identity,
);
return match ? optionLabel(match) : identity;
}
function columnWidth(field: ResourceField) {
if (field.key === 'identity') return 280;
if (field.type === 'datetime' || field.type === 'date') return 180;

View File

@@ -0,0 +1,39 @@
<!-- 功能描述以业务名称展示关联记录并保留唯一标识的查看与复制能力版本v1.0.0 -->
<template>
<div class="relation-name-text">
<a-tooltip :content="`${identityLabel}${identity}`">
<span class="relation-name">{{ displayName }}</span>
</a-tooltip>
<a-tooltip :content="`复制${identityLabel}`">
<button class="copy-button" type="button" :aria-label="`复制${identityLabel} ${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; identityLabel: string }>();
const displayName = computed(() => props.name || props.identity);
/** 复制完整唯一标识,并给出明确的操作反馈。 */
async function copyIdentity() {
try {
await navigator.clipboard.writeText(props.identity);
Message.success(`${props.identityLabel}已复制`);
} 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>