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

统一平台、气站和配送点后台的服务关系业务名称展示。
新增气站、配送点、服务人员三级联动及暂未分配语义。
后端补充组织归属、人员角色和启用状态的严格校验。
补充前后端测试、项目文档、操作日志及本机日志忽略规则。
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

@@ -121,7 +121,9 @@ const entries = computed<DetailEntry[]>(() => {
value: display,
objectValue,
identityValue:
field?.staffRelation?.showIdentityCopy && value ? String(value) : '',
(field?.showIdentityCopy || field?.staffRelation?.showIdentityCopy) && value
? String(value)
: '',
wide:
objectValue ||
/(address|terms|content|body|remark|reason|params|args)$/.test(key),

View File

@@ -68,7 +68,18 @@
</a-option>
</a-select>
<div v-else-if="field.type === 'identity' || field.type === 'identity-list'" class="relation-control">
<div
v-if="disabledSet.has(field.key) && field.readonlyRelationText"
class="readonly-relation"
>
<span class="readonly-relation-name">{{ readonlyRelationLabel(field) }}</span>
<IdentityText
v-if="(field.showIdentityCopy || field.staffRelation?.showIdentityCopy) && model[field.key]"
:value="String(model[field.key])"
/>
</div>
<a-select
v-else
v-model="model[field.key]"
:disabled="disabledSet.has(field.key)"
:multiple="field.type === 'identity-list'"
@@ -88,7 +99,7 @@
</a-option>
</a-select>
<div
v-if="disabledSet.has(field.key) && field.staffRelation?.showIdentityCopy && model[field.key]"
v-if="disabledSet.has(field.key) && (field.showIdentityCopy || field.staffRelation?.showIdentityCopy) && !field.readonlyRelationText && model[field.key]"
class="relation-identity"
>
<span>人员标识</span>
@@ -146,6 +157,16 @@ function isWide(field: ResourceField) {
);
}
/** 返回只读关系字段的业务名称,未加载到历史记录时保留唯一标识以避免误导。 */
function readonlyRelationLabel(field: ResourceField) {
const identity = String(model.value[field.key] ?? '');
if (!identity) return field.emptyText ?? '暂未分配';
const option = (props.relationOptions[field.relation ?? ''] ?? []).find(
(item) => String(item.identity ?? '') === identity,
);
return option ? relationOptionLabel(field, option) : identity;
}
/** 为历史未知编码补充只读选项,避免下拉框退回显示原始值。 */
function selectOptions(field: ResourceField) {
const options = [...(field.options ?? [])];
@@ -195,6 +216,19 @@ function selectOptions(field: ResourceField) {
color: var(--color-text-3);
font-size: 12px;
}
.readonly-relation {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
min-height: 32px;
padding: 5px 12px;
background: var(--color-fill-1);
border-radius: var(--border-radius-small);
}
.readonly-relation-name {
color: var(--color-text-1);
}
@media (max-width: 900px) {
.field-grid {
grid-template-columns: 1fr;

View File

@@ -305,7 +305,11 @@ const visibleActions = computed(() =>
),
),
);
const modeLabel = computed(() => recordPageModeLabel(mode.value));
const modeLabel = computed(() =>
definition.value.name === 'user_service_relation'
? `${recordPageModeLabel(mode.value)}${definition.value.title}`
: recordPageModeLabel(mode.value),
);
const errorTitle = computed(() => recordPageErrorTitle(errorStatus.value));
function snapshot() {

View File

@@ -60,6 +60,42 @@ export function linkedRelationFilters(
return parentIdentity ? { [filterKey]: parentIdentity } : undefined;
}
/** 为服务人员关系生成精确组织范围;空组织使用显式未分配条件,避免返回其他组织人员。 */
export function staffOrganizationFilters(
gasIdentity: string,
deliveryIdentity: string,
) {
return {
...(gasIdentity
? { gas_basic_identities: gasIdentity }
: { gas_basic_unassigned: '1' }),
...(deliveryIdentity
? { delivery_basic_identities: deliveryIdentity }
: { delivery_basic_unassigned: '1' }),
};
}
/** 校验服务人员记录与当前气站、配送点完全一致,空字符串代表可以提交。 */
export function staffOrganizationValidationMessage(
gasIdentity: string,
deliveryIdentity: string,
staffIdentity: string,
option: ResourceRow | undefined,
) {
if (!staffIdentity) return '';
if (!option) return '无法确认所选服务人员的组织归属,请重新选择';
if (optionParentIdentity(option, 'gas_basic_identity') !== gasIdentity) {
return '所选服务人员不属于当前气站,请重新选择';
}
if (
optionParentIdentity(option, 'delivery_basic_identity') !==
deliveryIdentity
) {
return '所选服务人员不属于当前配送点,请重新选择';
}
return '';
}
/** 为同一关联资源生成递增版本号,用于识别并丢弃过期响应。 */
export function createRelationRequestVersionGuard() {
const versions = new Map<string, number>();

View File

@@ -10,6 +10,8 @@ import {
optionParentIdentity,
relationIdentity,
shouldClearLinkedOption,
staffOrganizationFilters,
staffOrganizationValidationMessage,
} from './resource-relation-linkage-policy';
import type { ResourceRelations } from './use-resource-relations';
@@ -23,6 +25,7 @@ export function useResourceRelationLinkage(
relations: ResourceRelations,
) {
let active: ActiveLinkage | undefined;
let organizationStaffField: ResourceField | undefined;
let initialized = false;
function findOption(resource: string, identity: string) {
@@ -33,6 +36,9 @@ export function useResourceRelationLinkage(
function configure(fields: ResourceField[]) {
const childField = fields.find((field) => field.relationLinkage);
organizationStaffField = fields.find(
(field) => field.staffRelation?.organizationScoped,
);
if (!childField?.relationLinkage) {
active = undefined;
return;
@@ -45,6 +51,49 @@ export function useResourceRelationLinkage(
};
}
function staffRequest() {
const staffIdentity = relationIdentity(
organizationStaffField ? form[organizationStaffField.key] : '',
);
return {
filters: staffOrganizationFilters(
relationIdentity(form.gas_basic_identity),
relationIdentity(form.delivery_basic_identity),
),
preserveIdentities: staffIdentity ? [staffIdentity] : [],
};
}
async function reloadStaffOptions() {
const field = organizationStaffField;
if (!field?.relation) return;
relations.cancelSearch(field.relation);
await relations.loadField(field, '', staffRequest());
}
/** 组织变化后清理不再匹配的服务人员,并刷新精确范围选项。 */
async function synchronizeStaff() {
const field = organizationStaffField;
if (!field?.relation) return;
const staffIdentity = relationIdentity(form[field.key]);
if (staffIdentity) {
const option =
findOption(field.relation, staffIdentity) ??
(await relations.ensure(field.relation, staffIdentity));
const warning = staffOrganizationValidationMessage(
relationIdentity(form.gas_basic_identity),
relationIdentity(form.delivery_basic_identity),
staffIdentity,
option,
);
if (warning && relationIdentity(form[field.key]) === staffIdentity) {
form[field.key] = '';
Message.info(`${warning},已清空原服务人员`);
}
}
await reloadStaffOptions();
}
function childRequest() {
if (!active?.childField.relationLinkage) return {};
const linkage = active.childField.relationLinkage;
@@ -69,14 +118,21 @@ export function useResourceRelationLinkage(
initialized = false;
configure(fields);
if (!active?.childField.relationLinkage || !active.childField.relation) {
await relations.preload(fields);
await relations.preload(
fields.filter((field) => field.key !== organizationStaffField?.key),
);
await relations.ensureValues(fields, form);
await reloadStaffOptions();
initialized = true;
return;
}
await relations.preload(
fields.filter((field) => field.key !== active?.childField.key),
fields.filter(
(field) =>
field.key !== active?.childField.key &&
field.key !== organizationStaffField?.key,
),
);
const linkage = active.childField.relationLinkage;
const parentIdentity = relationIdentity(form[linkage.parentKey]);
@@ -87,7 +143,14 @@ export function useResourceRelationLinkage(
if (childIdentity) {
await relations.ensure(active.childField.relation, childIdentity);
}
const staffIdentity = relationIdentity(
organizationStaffField ? form[organizationStaffField.key] : '',
);
if (staffIdentity && organizationStaffField?.relation) {
await relations.ensure(organizationStaffField.relation, staffIdentity);
}
await reloadChildOptions();
await reloadStaffOptions();
initialized = true;
const warning = validationMessage();
@@ -158,8 +221,12 @@ export function useResourceRelationLinkage(
const identity = relationIdentity(value);
if (field.key === active.childField.relationLinkage.parentKey) {
await handleParentChange(identity);
await synchronizeStaff();
} else if (field.key === active.childField.key) {
await handleChildChange(identity);
await synchronizeStaff();
} else if (field.key === organizationStaffField?.key) {
await synchronizeStaff();
}
}
@@ -170,6 +237,10 @@ export function useResourceRelationLinkage(
relations.searchField(field, keyword, childRequest());
return;
}
if (field.key === organizationStaffField?.key) {
relations.searchField(field, keyword, staffRequest());
return;
}
relations.searchField(field, keyword);
}
@@ -181,12 +252,22 @@ export function useResourceRelationLinkage(
const parentIdentity = relationIdentity(form[linkage.parentKey]);
const childIdentity = relationIdentity(form[active.childField.key]);
const option = findOption(active.childField.relation, childIdentity);
return linkedRelationValidationMessage(
const linkedMessage = linkedRelationValidationMessage(
parentIdentity,
childIdentity,
option,
linkage.optionParentKey,
);
if (linkedMessage) return linkedMessage;
const staffField = organizationStaffField;
if (!staffField?.relation) return '';
const staffIdentity = relationIdentity(form[staffField.key]);
return staffOrganizationValidationMessage(
parentIdentity,
childIdentity,
staffIdentity,
findOption(staffField.relation, staffIdentity),
);
}
return { preload, change, search, validationMessage };