Files
hms/apps/web/src/components/EntitySelect.tsx
iven 0ee9d22634 fix(plugin): P1 跨插件引用修复 — DateTime generated column + resolve-labels UUID 类型 + EntitySelect manifest→UUID 映射
- manifest.rs: DateTime 类型 generated column 改为 TEXT 存储(PostgreSQL TIMESTAMPTZ cast 非 immutable)
- data_handler.rs: resolve-labels 查询参数从 String 改为 UUID 类型避免类型不匹配
- data_dto.rs: PublicEntityResp 新增 plugin_id 字段
- EntitySelect.tsx: 跨插件查询先通过 registry 解析 manifest_id→plugin UUID
- pluginData.ts: PublicEntity 接口增加 plugin_id
- plugin_tests.rs: 适配 PluginField/PluginEntity 新增字段
2026-04-19 08:44:45 +08:00

142 lines
4.0 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.
import { Select, Spin, Input, Tooltip } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { useState, useEffect, useCallback } from 'react';
import { listPluginData, getPluginEntityRegistry } from '../api/pluginData';
interface EntitySelectProps {
pluginId: string;
entity: string;
labelField: string;
searchFields?: string[];
/** 跨插件引用的目标插件 manifest ID如 "erp-crm" */
refPlugin?: string;
/** 目标插件未安装时的降级显示文本 */
fallbackLabel?: string;
value?: string;
onChange?: (value: string, label: string) => void;
cascadeFrom?: string;
cascadeFilter?: string;
cascadeValue?: string;
placeholder?: string;
}
export default function EntitySelect({
pluginId,
entity,
labelField,
searchFields: _searchFields,
refPlugin,
fallbackLabel,
value,
onChange,
cascadeFrom,
cascadeFilter,
cascadeValue,
placeholder,
}: EntitySelectProps) {
const [options, setOptions] = useState<{ value: string; label: string }[]>([]);
const [loading, setLoading] = useState(false);
const [targetUnavailable, setTargetUnavailable] = useState(false);
const [resolvedPluginId, setResolvedPluginId] = useState<string | null>(null);
// 跨插件时:先解析 manifest_id → plugin UUID
useEffect(() => {
if (!refPlugin) {
setResolvedPluginId(pluginId);
return;
}
let cancelled = false;
(async () => {
try {
const registry = await getPluginEntityRegistry();
const match = registry.find((e) => e.manifest_id === refPlugin && e.entity_name === entity);
if (!cancelled) {
setResolvedPluginId(match ? match.plugin_id : null);
if (!match) setTargetUnavailable(true);
}
} catch {
if (!cancelled) {
setTargetUnavailable(true);
setResolvedPluginId(null);
}
}
})();
return () => { cancelled = true; };
}, [refPlugin, pluginId, entity]);
const effectivePluginId = resolvedPluginId || pluginId;
const fetchData = useCallback(
async (keyword?: string) => {
if (!resolvedPluginId && refPlugin) return;
setLoading(true);
try {
const filter: Record<string, string> | undefined =
cascadeFrom && cascadeFilter && cascadeValue
? { [cascadeFilter]: cascadeValue }
: undefined;
const result = await listPluginData(effectivePluginId, entity, 1, 20, {
search: keyword,
filter,
});
const items = (result.data || []).map((item) => ({
value: item.id,
label: String(item.data?.[labelField] ?? item.id),
}));
setOptions(items);
setTargetUnavailable(false);
} catch {
if (refPlugin) {
setTargetUnavailable(true);
setOptions([]);
}
} finally {
setLoading(false);
}
},
[effectivePluginId, entity, labelField, cascadeFrom, cascadeFilter, cascadeValue, refPlugin, resolvedPluginId],
);
useEffect(() => {
if (resolvedPluginId || !refPlugin) {
fetchData();
}
}, [fetchData, resolvedPluginId, refPlugin]);
// 目标插件未安装 → 降级显示
if (targetUnavailable) {
return (
<Input
value={value || ''}
placeholder={fallbackLabel || `外部引用 (${refPlugin})`}
disabled
suffix={
<Tooltip title="目标插件未安装,此字段暂时不可用">
<QuestionCircleOutlined style={{ color: '#999' }} />
</Tooltip>
}
/>
);
}
return (
<Select
showSearch
value={value}
placeholder={placeholder || '请选择'}
loading={loading}
options={options}
onSearch={(v) => fetchData(v)}
onChange={(v) => {
const opt = options.find((o) => o.value === v);
onChange?.(v, opt?.label || '');
}}
filterOption={false}
notFoundContent={loading ? <Spin size="small" /> : '无数据'}
allowClear
/>
);
}