feat(web): EntitySelect 关联选择器 — 远程搜索 + 级联过滤
- 新增 EntitySelect 组件,支持远程搜索和级联过滤 - PluginCRUDPage 表单渲染新增 entity_select widget 支持 - 通过 ref_entity/ref_label_field 配置关联实体 - 通过 cascade_from/cascade_filter 实现级联过滤
This commit is contained in:
80
apps/web/src/components/EntitySelect.tsx
Normal file
80
apps/web/src/components/EntitySelect.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Select, Spin } from 'antd';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { listPluginData } from '../api/pluginData';
|
||||
|
||||
interface EntitySelectProps {
|
||||
pluginId: string;
|
||||
entity: string;
|
||||
labelField: string;
|
||||
searchFields?: 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,
|
||||
value,
|
||||
onChange,
|
||||
cascadeFrom,
|
||||
cascadeFilter,
|
||||
cascadeValue,
|
||||
placeholder,
|
||||
}: EntitySelectProps) {
|
||||
const [options, setOptions] = useState<{ value: string; label: string }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = useCallback(
|
||||
async (keyword?: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const filter: Record<string, string> | undefined =
|
||||
cascadeFrom && cascadeFilter && cascadeValue
|
||||
? { [cascadeFilter]: cascadeValue }
|
||||
: undefined;
|
||||
|
||||
const result = await listPluginData(pluginId, entity, 1, 20, {
|
||||
search: keyword,
|
||||
filter,
|
||||
});
|
||||
|
||||
const items = (result.data || []).map((item) => ({
|
||||
value: item.id,
|
||||
label: String(item.data?.[labelField] ?? item.id),
|
||||
}));
|
||||
setOptions(items);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[pluginId, entity, labelField, cascadeFrom, cascadeFilter, cascadeValue],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
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
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
deletePluginData,
|
||||
type PluginDataListOptions,
|
||||
} from '../api/pluginData';
|
||||
import EntitySelect from '../components/EntitySelect';
|
||||
import {
|
||||
getPluginSchema,
|
||||
type PluginFieldSchema,
|
||||
@@ -336,6 +337,25 @@ export default function PluginCRUDPage({
|
||||
);
|
||||
case 'textarea':
|
||||
return <TextArea rows={3} />;
|
||||
case 'entity_select':
|
||||
return (
|
||||
<EntitySelect
|
||||
pluginId={pluginId}
|
||||
entity={field.ref_entity!}
|
||||
labelField={field.ref_label_field || 'name'}
|
||||
searchFields={field.ref_search_fields}
|
||||
value={formValues[field.name] as string | undefined}
|
||||
onChange={(v) => form.setFieldValue(field.name, v)}
|
||||
cascadeFrom={field.cascade_from}
|
||||
cascadeFilter={field.cascade_filter}
|
||||
cascadeValue={
|
||||
field.cascade_from
|
||||
? (formValues[field.cascade_from] as string | undefined)
|
||||
: undefined
|
||||
}
|
||||
placeholder={field.display_name}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <Input />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user