/** * 功能:为资源表单和详情页加载、缓存并搜索关联资源选项。 * 版本:v1.1.0 */ import { Message } from '@arco-design/web-vue'; import { onBeforeUnmount, reactive } from 'vue'; import { resourceApi } from '@/api/resource'; import { staffRelationFilters, type StaffRelationContext, } from '@/api/resource-staff-relation'; import type { ResourceField } from '@/api/resources'; import type { ResourceRow } from '@/api/resource-page-rules'; import { fieldRelationResource } from '@/api/resource-display'; import { relationDisplayName } from '../shared/resource-list-field-display'; import { createRelationRequestVersionGuard } from './resource-relation-linkage-policy'; export type ResourceRelationLoadOptions = { filters?: Record; preserveIdentities?: string[]; }; export function useResourceRelations( context: () => StaffRelationContext = () => ({}), ) { const options = reactive>({}); const loading = reactive>({}); const timers = new Map>(); const requestGuard = createRelationRequestVersionGuard(); /** 合并需要保留的当前选项,搜索和筛选时不让下拉框退回显示原始标识。 */ function mergePreserved( resource: string, rows: ResourceRow[], identities: string[] = [], ) { const merged = [...rows]; const seen = new Set(rows.map((row) => String(row.identity ?? ''))); for (const identity of identities.filter(Boolean)) { if (seen.has(identity)) continue; const current = (options[resource] ?? []).find( (row) => String(row.identity ?? '') === identity, ); if (current) { merged.push(current); seen.add(identity); } } return merged; } /** 加载一类关联资源,并丢弃已经过期的搜索或筛选响应。 */ async function load( resource: string, keyword = '', request: ResourceRelationLoadOptions = {}, ) { const version = requestGuard.begin(resource); loading[resource] = true; try { const filters: Record = { ...(request.filters ?? {}), ...(keyword ? { keyword } : {}), }; const rows = ( await resourceApi.list(resource, 1, 100, filters) ).list; if (!requestGuard.isLatest(resource, version)) return; options[resource] = mergePreserved( resource, rows, request.preserveIdentities, ); } catch (error) { if (!requestGuard.isLatest(resource, version)) return; Message.error(`关联数据加载失败:${(error as Error).message}`); } finally { if (requestGuard.isLatest(resource, version)) { loading[resource] = false; } } } /** 按字段的显式关系策略加载选项,工作人员字段不得依赖全局默认值。 */ function loadField( field: ResourceField, keyword = '', request: ResourceRelationLoadOptions = {}, ) { if (!field.relation) return Promise.resolve(); return load(field.relation, keyword, { ...request, filters: { ...(field.relationFilters ?? {}), ...staffRelationFilters(field, context()), ...(request.filters ?? {}), }, }); } /** 按唯一标识补载当前选项,解决编辑记录不在列表前 100 条时的回显。 */ async function ensure(resource: string, identity: string) { if (!identity) return undefined; const current = (options[resource] ?? []).find( (row) => String(row.identity ?? '') === identity, ); if (current) return current; try { const row = await resourceApi.detail(resource, identity); const resolved = (options[resource] ?? []).find( (item) => String(item.identity ?? '') === identity, ); if (resolved) return resolved; options[resource] = [...(options[resource] ?? []), row]; return row; } catch (error) { Message.error(`关联数据加载失败:${(error as Error).message}`); return undefined; } } /** 预加载当前页面实际使用的全部关系字段。 */ async function preload(fields: ResourceField[]) { const fieldsByResource = new Map(); for (const field of fields) { if (field.relation && !fieldsByResource.has(field.relation)) { fieldsByResource.set(field.relation, field); } } await Promise.all( [...fieldsByResource.values()].map((field) => loadField(field)), ); } /** 补载表单或详情中已经保存的关系值,避免分页和筛选导致回显裸标识。 */ async function ensureValues(fields: ResourceField[], values: ResourceRow) { const requests = fields.flatMap((field) => { // 服务端已在当前业务记录范围内返回名称时,不再重复补载关联用户。 if (relationDisplayName(field, values)) return []; const resource = fieldRelationResource(field, values); if (!resource) return []; const value = values[field.key]; const identities = Array.isArray(value) ? value : [value]; return identities .map((identity) => String(identity ?? '')) .filter(Boolean) .map((identity) => ensure(resource, identity)); }); await Promise.all(requests); } /** 对远程关系选项进行防抖搜索。 */ function search( resource: string | undefined, keyword: string, request: ResourceRelationLoadOptions = {}, ) { if (!resource) return; cancelSearch(resource); timers.set( resource, setTimeout(() => { timers.delete(resource); void load(resource, keyword.trim(), request); }, 250), ); } /** 按字段的显式关系策略执行防抖搜索。 */ function searchField( field: ResourceField, keyword: string, request: ResourceRelationLoadOptions = {}, ) { if (!field.relation) return; search(field.relation, keyword, { ...request, filters: { ...(field.relationFilters ?? {}), ...staffRelationFilters(field, context()), ...(request.filters ?? {}), }, }); } /** 取消关联资源尚未触发的防抖搜索。 */ function cancelSearch(resource: string) { const timer = timers.get(resource); if (timer) clearTimeout(timer); timers.delete(resource); } /** 清空依赖父级但当前尚无有效父级的候选项。 */ function clear(resource: string | undefined) { if (!resource) return; cancelSearch(resource); requestGuard.begin(resource); options[resource] = []; loading[resource] = false; } onBeforeUnmount(() => { for (const resource of timers.keys()) cancelSearch(resource); }); return { options, loading, load, loadField, ensure, ensureValues, preload, search, searchField, cancelSearch, clear, }; } export type ResourceRelations = ReturnType;