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,
|
deletePluginData,
|
||||||
type PluginDataListOptions,
|
type PluginDataListOptions,
|
||||||
} from '../api/pluginData';
|
} from '../api/pluginData';
|
||||||
|
import EntitySelect from '../components/EntitySelect';
|
||||||
import {
|
import {
|
||||||
getPluginSchema,
|
getPluginSchema,
|
||||||
type PluginFieldSchema,
|
type PluginFieldSchema,
|
||||||
@@ -336,6 +337,25 @@ export default function PluginCRUDPage({
|
|||||||
);
|
);
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
return <TextArea rows={3} />;
|
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:
|
default:
|
||||||
return <Input />;
|
return <Input />;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user