From e2e58d3a00b6c912247897e527d9fa6eb157bd13 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 17 Apr 2026 10:56:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20EntitySelect=20=E5=85=B3=E8=81=94?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=20=E2=80=94=20=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=20+=20=E7=BA=A7=E8=81=94=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 EntitySelect 组件,支持远程搜索和级联过滤 - PluginCRUDPage 表单渲染新增 entity_select widget 支持 - 通过 ref_entity/ref_label_field 配置关联实体 - 通过 cascade_from/cascade_filter 实现级联过滤 --- apps/web/src/components/EntitySelect.tsx | 80 ++++++++++++++++++++++++ apps/web/src/pages/PluginCRUDPage.tsx | 20 ++++++ 2 files changed, 100 insertions(+) create mode 100644 apps/web/src/components/EntitySelect.tsx diff --git a/apps/web/src/components/EntitySelect.tsx b/apps/web/src/components/EntitySelect.tsx new file mode 100644 index 0000000..c485f35 --- /dev/null +++ b/apps/web/src/components/EntitySelect.tsx @@ -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 | 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 ( +