Files
platforms/frontend/platform_admin/src/views/resource/use-resource-relations.ts

222 lines
6.9 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.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<string, string>;
preserveIdentities?: string[];
};
export function useResourceRelations(
context: () => StaffRelationContext = () => ({}),
) {
const options = reactive<Record<string, ResourceRow[]>>({});
const loading = reactive<Record<string, boolean>>({});
const timers = new Map<string, ReturnType<typeof setTimeout>>();
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<string, string> = {
...(request.filters ?? {}),
...(keyword ? { keyword } : {}),
};
const rows = (
await resourceApi.list<ResourceRow>(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<ResourceRow>(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<string, ResourceField>();
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<typeof useResourceRelations>;