217 lines
6.8 KiB
TypeScript
217 lines
6.8 KiB
TypeScript
|
|
/**
|
|||
|
|
* 功能描述:加载配送点资源关系候选,并处理合同驱动的父子联动与过期响应。
|
|||
|
|
* 版本: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,
|
|||
|
|
};
|
|||
|
|
}
|