Files
platforms/frontend/delivery_admin/src/views/resource/use-resource-relations.ts
2026-08-23 11:30:52 +08:00

217 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 功能描述:加载配送点资源关系候选,并处理合同驱动的父子联动与过期响应。
* 版本v1.0.0。
*/
import { Message } from '@arco-design/web-vue';
import { onBeforeUnmount, reactive } from 'vue';
import { resourceApi } from '@/api/resource';
import type { ResourceRow } from '@/api/resource-record-form';
import type { ResourceField } from '@/api/resources';
/** 创建关系候选状态;同一资源仅允许最新请求更新页面。 */
export function useResourceRelations(form: Record<string, any>) {
const options = reactive<Record<string, ResourceRow[]>>({});
const loading = reactive<Record<string, boolean>>({});
const versions = new Map<string, number>();
const timers = new Map<string, ReturnType<typeof setTimeout>>();
function begin(resource: string) {
const version = (versions.get(resource) ?? 0) + 1;
versions.set(resource, version);
return version;
}
function isLatest(resource: string, version: number) {
return versions.get(resource) === version;
}
/** 清空候选并使尚未返回的旧请求失效。 */
function clear(resource: string | undefined) {
if (!resource) return;
begin(resource);
options[resource] = [];
loading[resource] = false;
}
/** 返回字段当前必须携带的静态与父级筛选参数。 */
function filters(field: ResourceField) {
const linkage = field.relationLinkage;
const parent = linkage ? String(form[linkage.parentKey] ?? '').trim() : '';
return {
...(field.relation === '/staff_account' ? { role_code: 'delivery' } : {}),
...(field.relationFilters ?? {}),
...(linkage && parent ? { [linkage.filterKey]: parent } : {}),
};
}
/** 加载一个关系字段,搜索时仍保留当前已选候选的可读文案。 */
async function load(field: ResourceField, keyword = '') {
const resource = field.relation;
if (!resource) return;
const linkage = field.relationLinkage;
if (
linkage?.requiresParent &&
!String(form[linkage.parentKey] ?? '').trim()
) {
clear(resource);
return;
}
const version = begin(resource);
loading[resource] = true;
const selected: string[] = (
Array.isArray(form[field.key]) ? form[field.key] : [form[field.key]]
)
.map((value: unknown) => String(value ?? ''))
.filter(Boolean);
try {
const rows = (
await resourceApi.list<ResourceRow>(resource, 1, 100, {
...filters(field),
...(keyword ? { keyword } : {}),
})
).list;
if (!isLatest(resource, version)) return;
const validIdentities = new Set(
rows.map((row) => String(row.identity ?? '')),
);
if (
field.relationStrictFilter &&
!keyword &&
selected.some((identity) => !validIdentities.has(identity))
) {
form[field.key] = field.type === 'identity-list' ? [] : '';
Message.info(
field.relationInvalidMessage ?? '所选关联数据已失效,已清空',
);
}
const merged = [...rows];
const identities = new Set(validIdentities);
for (const identity of selected) {
if (field.relationStrictFilter && !keyword) continue;
const current = (options[resource] ?? []).find(
(row) => String(row.identity ?? '') === identity,
);
if (current && !identities.has(identity)) merged.push(current);
}
options[resource] = merged;
if (!selected.length && field.relationAutoSelectKey) {
const preferred = rows.find((row) =>
Boolean(row[field.relationAutoSelectKey as string]),
);
if (preferred && isLatest(resource, version))
form[field.key] = String(preferred.identity ?? '');
}
} catch (error) {
if (isLatest(resource, version))
Message.error(`关联数据加载失败:${(error as Error).message}`);
} finally {
if (isLatest(resource, version)) loading[resource] = false;
}
}
/** 先加载普通关系,再加载已有父值约束下的子关系。 */
async function preload(fields: ResourceField[]) {
const unique = new Map<string, ResourceField>();
for (const field of fields) {
if (field.relation && !unique.has(field.relation))
unique.set(field.relation, field);
}
await Promise.all(
[...unique.values()]
.filter((field) => !field.relationLinkage)
.map((field) => load(field)),
);
await Promise.all(
[...unique.values()]
.filter((field) => field.relationLinkage)
.map((field) => load(field)),
);
}
/** 父关系变化时原子清空所有子值,再按新父值加载候选。 */
async function change(field: ResourceField) {
const fields = configuredFields;
const children = fields.filter(
(item) => item.relationLinkage?.parentKey === field.key,
);
await Promise.all(
children.map(async (child) => {
const previous = Array.isArray(form[child.key])
? form[child.key].length > 0
: Boolean(form[child.key]);
form[child.key] = child.type === 'identity-list' ? [] : '';
clear(child.relation);
if (previous)
Message.info(
child.relationLinkage?.parentChangeMessage ??
'上级数据已变更,请重新选择',
);
await load(child);
}),
);
}
let configuredFields: ResourceField[] = [];
function configure(fields: ResourceField[]) {
configuredFields = fields;
}
/** 搜索采用 250ms 防抖,并始终附带当前父级过滤。 */
function search(field: ResourceField, keyword: string) {
const resource = field.relation;
if (!resource) return;
const timer = timers.get(resource);
if (timer) clearTimeout(timer);
timers.set(
resource,
setTimeout(() => {
timers.delete(resource);
void load(field, keyword.trim());
}, 250),
);
}
/** 返回必填候选为空时的业务提示。 */
function unavailableMessage(fields: ResourceField[]) {
for (const field of fields) {
if (!field.required || !field.relation || !field.relationEmptyText)
continue;
const linkage = field.relationLinkage;
if (
linkage?.requiresParent &&
!String(form[linkage.parentKey] ?? '').trim()
)
continue;
if (
!loading[field.relation] &&
(options[field.relation] ?? []).length === 0
)
return field.relationEmptyText;
}
return '';
}
function isLoading(fields: ResourceField[]) {
return fields.some((field) =>
Boolean(field.relation && loading[field.relation]),
);
}
onBeforeUnmount(() => {
for (const timer of timers.values()) clearTimeout(timer);
});
return {
options,
loading,
configure,
preload,
load,
change,
search,
clear,
unavailableMessage,
isLoading,
};
}