From 9e28d71295daa6173ea8a57b464b5f6a180c07ee Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 19 Apr 2026 00:54:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(web,plugin):=20P1=20=E8=B7=A8=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=BC=95=E7=94=A8=20=E2=80=94=20=E5=89=8D=E7=AB=AF=20?= =?UTF-8?q?Phase=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plugins.ts: PluginFieldSchema 新增 ref_plugin/ref_fallback_label, PluginEntitySchema 新增 is_public - pluginData.ts: 新增 resolveRefLabels/getPluginEntityRegistry API - EntitySelect: 支持 refPlugin 跨插件查询,目标不可用时降级为禁用 Input - PluginCRUDPage: 表格列解析引用标签(蓝色 Tag),entity_select 表单传 refPlugin/fallbackLabel --- apps/web/src/api/pluginData.ts | 37 +++++++++++++++++++++ apps/web/src/api/plugins.ts | 3 ++ apps/web/src/components/EntitySelect.tsx | 39 ++++++++++++++++++++-- apps/web/src/pages/PluginCRUDPage.tsx | 41 ++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 3 deletions(-) diff --git a/apps/web/src/api/pluginData.ts b/apps/web/src/api/pluginData.ts index e39a079..6ab3d22 100644 --- a/apps/web/src/api/pluginData.ts +++ b/apps/web/src/api/pluginData.ts @@ -171,3 +171,40 @@ export async function getPluginTimeseries( ); return data.data; } + +// ─── 跨插件引用 API ────────────────────────────────────────────────── + +export interface ResolveLabelsResult { + labels: Record>; + meta: Record; +} + +export async function resolveRefLabels( + pluginId: string, + entity: string, + fields: Record, +): Promise { + const { data } = await client.post<{ success: boolean; data: ResolveLabelsResult }>( + `/plugins/${pluginId}/${entity}/resolve-labels`, + { fields }, + ); + return data.data; +} + +export interface PublicEntity { + manifest_id: string; + entity_name: string; + display_name: string; +} + +export async function getPluginEntityRegistry(): Promise { + const { data } = await client.get<{ success: boolean; data: PublicEntity[] }>( + '/plugin-registry/entities', + ); + return data.data; +} diff --git a/apps/web/src/api/plugins.ts b/apps/web/src/api/plugins.ts index 10925cc..30a406c 100644 --- a/apps/web/src/api/plugins.ts +++ b/apps/web/src/api/plugins.ts @@ -139,6 +139,8 @@ export interface PluginFieldSchema { ref_entity?: string; ref_label_field?: string; ref_search_fields?: string[]; + ref_plugin?: string; + ref_fallback_label?: string; cascade_from?: string; cascade_filter?: string; validation?: PluginFieldValidation; @@ -159,6 +161,7 @@ export interface PluginEntitySchema { fields: PluginFieldSchema[]; relations?: PluginRelationSchema[]; data_scope?: boolean; + is_public?: boolean; } export interface PluginSchemaResponse { diff --git a/apps/web/src/components/EntitySelect.tsx b/apps/web/src/components/EntitySelect.tsx index 636e998..edf59cb 100644 --- a/apps/web/src/components/EntitySelect.tsx +++ b/apps/web/src/components/EntitySelect.tsx @@ -1,4 +1,5 @@ -import { Select, Spin } from 'antd'; +import { Select, Spin, Input, Tooltip } from 'antd'; +import { QuestionCircleOutlined } from '@ant-design/icons'; import { useState, useEffect, useCallback } from 'react'; import { listPluginData } from '../api/pluginData'; @@ -7,6 +8,10 @@ interface EntitySelectProps { entity: string; labelField: string; searchFields?: string[]; + /** 跨插件引用的目标插件 manifest ID(如 "erp-crm") */ + refPlugin?: string; + /** 目标插件未安装时的降级显示文本 */ + fallbackLabel?: string; value?: string; onChange?: (value: string, label: string) => void; cascadeFrom?: string; @@ -20,6 +25,8 @@ export default function EntitySelect({ entity, labelField, searchFields: _searchFields, + refPlugin, + fallbackLabel, value, onChange, cascadeFrom, @@ -29,6 +36,10 @@ export default function EntitySelect({ }: EntitySelectProps) { const [options, setOptions] = useState<{ value: string; label: string }[]>([]); const [loading, setLoading] = useState(false); + const [targetUnavailable, setTargetUnavailable] = useState(false); + + // 跨插件时使用目标插件 ID 查询 + const effectivePluginId = refPlugin || pluginId; const fetchData = useCallback( async (keyword?: string) => { @@ -39,7 +50,7 @@ export default function EntitySelect({ ? { [cascadeFilter]: cascadeValue } : undefined; - const result = await listPluginData(pluginId, entity, 1, 20, { + const result = await listPluginData(effectivePluginId, entity, 1, 20, { search: keyword, filter, }); @@ -49,17 +60,39 @@ export default function EntitySelect({ label: String(item.data?.[labelField] ?? item.id), })); setOptions(items); + setTargetUnavailable(false); + } catch { + if (refPlugin) { + setTargetUnavailable(true); + setOptions([]); + } } finally { setLoading(false); } }, - [pluginId, entity, labelField, cascadeFrom, cascadeFilter, cascadeValue], + [effectivePluginId, entity, labelField, cascadeFrom, cascadeFilter, cascadeValue, refPlugin], ); useEffect(() => { fetchData(); }, [fetchData]); + // 目标插件未安装 → 降级显示 + if (targetUnavailable) { + return ( + + + + } + /> + ); + } + return (