Repository navigation
通用 Schema-Driven 配置面板框架建设及全平台接入 #731
Copy link
Copy link
Closed
Labels
Description
Activity
Let me first understand the current state of the objectui repo's view configuration panel implementation, then provide a comprehensive recommendation.根据对
objectstack-ai/objectui和objectstack-ai/spec两个仓库的全面分析,以下是我的综合评估和最优建议方案。
📊 现状分析
你已经有的
资产 位置 状态 ViewConfigPanel apps/console/src/components/ViewConfigPanel.tsx(~1650行)✅ 完整的 View 配置面板 ConfigRow 原语 packages/components/src/custom/config-row.tsx✅ 已抽取为通用组件 SectionHeader 原语 packages/components/src/custom/section-header.tsx✅ 已抽取为通用组件 FilterBuilder / SortBuilder @object-ui/components✅ 可嵌入的子编辑器 ViewDesigner packages/plugin-designer/src/ViewDesigner.tsx✅ 拖拽设计器 Draft State 模式 ViewConfigPanel 内部 ✅ 已验证的 draft → save/discard模式ROADMAP 中已规划但未实现的
### P1.10 Console — Dashboard Config Panel > Airtable-style right-side configuration panel for dashboards. Phased rollout from shared infrastructure to full type-safe editing. **Phase 0 — Component Infrastructure:** - [x] Extract `ConfigRow` / `SectionHeader` from `ViewConfigPanel` into `@object-ui/components` as reusable primitives **Phase 1 — Dashboard-Level Config Panel:** - [ ] Develop `DashboardConfigPanel` supporting data source, layout (columns/gap), filtering, appearance, user filters & actions **Phase 2 — Widget-Level Configuration:** - [ ] Support click-to-select widget → sidebar switches to widget property editor (title, type, data binding, layout) **Phase 3 — Sub-Editor Integration:** - [ ] Integrate `FilterBuilder` for dashboard global filters - [ ] Dropdown filter selector and action button sub-panel visual editing **Phase 4 — Composition & Storybook:** - [ ] Build `DashboardWithConfig` composite component (dashboard + config sidebar) - [ ] Add Storybook stories for `DashboardConfigPanel` and `DashboardWithConfig`
对照 spec 协议,未来需要的所有配置面板
Spec Schema 配置面板 复杂度 view.zod.ts→ ListViewViewConfigPanel ✅ 已有 🔴 高 (50+ 配置项) view.zod.ts→ FormViewFormConfigPanel 🟡 中 (sections, fields, layout mode) dashboard.zod.tsDashboardConfigPanel + WidgetConfigPanel 🔴 高 (layout grid + widget 嵌套) report.zod.tsReportConfigPanel 🟡 中 (report type, columns, grouping) page.zod.tsPageConfigPanel + ComponentConfigPanel 🔴 高 (16 page types, regions, components) app.zod.tsAppConfigPanel + NavigationConfigPanel 🟡 中 (nav tree, branding, permissions) action.zod.tsActionConfigPanel 🟢 低 (action type + params) theme.zod.tsThemeConfigPanel 🟡 中 (colors, typography, spacing) chart.zod.tsChartConfigPanel 🟡 中 (chart type, axes, data binding)
🏗️ 推荐方案:Schema-Driven Config Panel Framework
核心思路
不要为每种配置面板写一个 1500+ 行的大组件,而是构建一个通用的配置面板框架(Config Panel Framework),让具体面板由 Schema 驱动生成。
架构分层
┌─────────────────────────────────────────────────────────────┐ │ Concrete Config Panels │ │ ViewConfigPanel · DashboardConfigPanel · PageConfigPanel │ │ (每个只包含 schema 定义 + 特殊交互逻辑,~100-200行) │ └──────────────────────────┬──────────────────────────────────┘ │ 使用 ┌──────────────────────────▼──────────────────────────────────┐ │ ConfigPanelRenderer │ │ schema → sections → rows → controls (自动渲染) │ │ + draft state management (useConfigDraft hook) │ │ + save/discard/dirty tracking │ └──────────────────────────┬──────────────────────────────────┘ │ 组合 ┌──────────────────────────▼──────────────────────────────────┐ │ Config Primitives (已有 ✅) │ │ ConfigRow · SectionHeader · FilterBuilder · SortBuilder │ │ + 新增原语 │ │ ConfigSelect · ConfigSwitch · ConfigInput · ConfigColor │ │ ConfigFieldPicker · ConfigSlider · ConfigCodeEditor │ └─────────────────────────────────────────────────────────────┘详细设计
1️⃣ 新增
useConfigDraftHook — 从 ViewConfigPanel 提取目前 ViewConfigPanel 中的 draft 逻辑是内联的。将其提取为通用 Hook:
/** * useConfigDraft — Generic draft state management for config panels. * Extracted from ViewConfigPanel's proven pattern. */ export function useConfigDraft<T extends Record<string, any>>( source: T, options?: { mode?: 'create' | 'edit'; onUpdate?: (field: string, value: any) => void; } ) { const [draft, setDraft] = useState<T>({ ...source }); const [isDirty, setIsDirty] = useState(options?.mode === 'create'); // Reset draft when source changes useEffect(() => { setDraft({ ...source }); setIsDirty(options?.mode === 'create'); }, [source]); const updateField = useCallback((field: string, value: any) => { setDraft(prev => ({ ...prev, [field]: value })); setIsDirty(true); options?.onUpdate?.(field, value); }, [options?.onUpdate]); const discard = useCallback(() => { setDraft({ ...source }); setIsDirty(false); }, [source]); return { draft, isDirty, updateField, discard, setDraft }; }
2️⃣ 定义
ConfigPanelSchema— 面板配置的配置/** * Schema-driven config panel definition. * Each concrete panel (View, Dashboard, Page...) provides a schema, * and ConfigPanelRenderer auto-generates the UI. */ export type ControlType = | 'input' // ConfigRow + Input | 'switch' // ConfigRow + Switch | 'select' // ConfigRow + <select> | 'checkbox' // ConfigRow + Checkbox | 'slider' // ConfigRow + Slider (for gap, columns, etc.) | 'color' // ConfigRow + color picker | 'field-picker' // ConfigRow → click → FieldSelector sub-panel | 'filter' // Inline FilterBuilder | 'sort' // Inline SortBuilder | 'icon-group' // Row height style icon buttons | 'custom'; // Render prop for unique controls export interface ConfigField { /** Field key in draft object */ key: string; /** Display label (i18n key) */ label: string; /** Control type */ type: ControlType; /** Default value */ defaultValue?: any; /** Select options */ options?: Array<{ value: string; label: string }>; /** Visibility condition — expression evaluated against draft */ visibleWhen?: (draft: Record<string, any>) => boolean; /** Custom render function for type='custom' */ render?: (value: any, onChange: (v: any) => void, draft: Record<string, any>) => React.ReactNode; /** Placeholder text */ placeholder?: string; /** Help text */ helpText?: string; } export interface ConfigSection { /** Section key for collapse state */ key: string; /** Section title (i18n key) */ title: string; /** Hint text below title */ hint?: string; /** Is this section collapsible? */ collapsible?: boolean; /** Default collapsed state */ defaultCollapsed?: boolean; /** Fields in this section */ fields: ConfigField[]; /** Visibility condition */ visibleWhen?: (draft: Record<string, any>) => boolean; } export interface ConfigPanelSchema { /** Breadcrumb segments */ breadcrumb: string[]; /** All sections */ sections: ConfigSection[]; }
3️⃣
ConfigPanelRenderer— 通用渲染器/** * ConfigPanelRenderer — Schema-driven config panel. * Takes a ConfigPanelSchema and auto-renders the entire panel. * Handles: layout, scroll, header, save/discard, collapsible sections. */ export interface ConfigPanelRendererProps { open: boolean; onClose: () => void; schema: ConfigPanelSchema; draft: Record<string, any>; isDirty: boolean; onFieldChange: (key: string, value: any) => void; onSave: () => void; onDiscard: () => void; /** Additional header content (e.g. record count badge) */ headerExtra?: React.ReactNode; /** Object def for field pickers */ objectDef?: Record<string, any>; className?: string; } export function ConfigPanelRenderer({ open, onClose, schema, draft, isDirty, onFieldChange, onSave, onDiscard, headerExtra, objectDef, className }: ConfigPanelRendererProps) { const { t } = useObjectTranslation(); const [collapsed, setCollapsed] = useState<Record<string, boolean>>({}); if (!open) return null; return ( <div className="absolute inset-y-0 right-0 w-full sm:w-72 lg:w-80 sm:relative border-l bg-background flex flex-col shrink-0 z-20"> {/* Header with breadcrumb */} <div className="px-4 py-3 border-b flex items-center justify-between shrink-0"> <Breadcrumb segments={schema.breadcrumb} /> <Button size="sm" variant="ghost" onClick={onClose}><X className="h-3.5 w-3.5" /></Button> </div> {/* Scrollable sections */} <div className="flex-1 overflow-auto px-4 pb-4"> {schema.sections.map(section => { if (section.visibleWhen && !section.visibleWhen(draft)) return null; const isCollapsed = collapsed[section.key] ?? section.defaultCollapsed; return ( <div key={section.key}> <SectionHeader title={t(section.title)} collapsible={section.collapsible} collapsed={isCollapsed} onToggle={() => setCollapsed(prev => ({ ...prev, [section.key]: !isCollapsed }))} testId={`section-${section.key}`} /> {section.hint && <p className="text-[10px] text-muted-foreground mb-1">{t(section.hint)}</p>} {!isCollapsed && ( <div className="space-y-0.5"> {section.fields.map(field => ( <ConfigFieldRenderer key={field.key} field={field} value={draft[field.key]} onChange={(v) => onFieldChange(field.key, v)} draft={draft} objectDef={objectDef} /> ))} </div> )} </div> ); })} </div> {/* Footer — Save / Discard */} {isDirty && ( <div className="px-4 py-2 border-t flex gap-2"> <Button size="sm" onClick={onSave}><Save className="h-3.5 w-3.5 mr-1" />{t('save')}</Button> <Button size="sm" variant="ghost" onClick={onDiscard}><RotateCcw className="h-3.5 w-3.5 mr-1" />{t('discard')}</Button> </div> )} </div> ); }
4️⃣ 具体面板实现变得极其简洁
以 DashboardConfigPanel 为例:
import { ConfigPanelRenderer, useConfigDraft } from '@object-ui/components'; import type { ConfigPanelSchema } from '@object-ui/components'; const dashboardSchema: ConfigPanelSchema = { breadcrumb: ['Dashboard', 'Layout'], sections: [ { key: 'layout', title: 'console.dashboard.layout', fields: [ { key: 'columns', label: 'console.dashboard.columns', type: 'slider', defaultValue: 3 }, { key: 'gap', label: 'console.dashboard.gap', type: 'slider', defaultValue: 4 }, { key: 'rowHeight', label: 'console.dashboard.rowHeight', type: 'input', defaultValue: 120 }, ], }, { key: 'data', title: 'console.dashboard.data', collapsible: true, fields: [ { key: 'globalFilter', label: 'console.dashboard.globalFilter', type: 'filter' }, { key: 'refreshInterval', label: 'console.dashboard.refreshInterval', type: 'select', options: [ { value: '0', label: 'Manual' }, { value: '30', label: '30s' }, { value: '60', label: '1min' }, { value: '300', label: '5min' }, ]}, ], }, { key: 'appearance', title: 'console.dashboard.appearance', collapsible: true, defaultCollapsed: true, fields: [ { key: 'showDescription', label: 'console.dashboard.showDescription', type: 'switch', defaultValue: true }, { key: 'theme', label: 'console.dashboard.theme', type: 'select', options: [{ value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }, { value: 'auto', label: 'Auto' }]}, ], }, ], }; export function DashboardConfigPanel({ open, onClose, config, onSave }: Props) { const { draft, isDirty, updateField, discard } = useConfigDraft(config); return ( <ConfigPanelRenderer open={open} onClose={onClose} schema={dashboardSchema} draft={draft} isDirty={isDirty} onFieldChange={updateField} onSave={() => onSave(draft)} onDiscard={discard} /> ); }
从 1500 行 → ~60 行!
5️⃣ 迁移路径:渐进式重构 ViewConfigPanel
不需要一次性重写。建议分阶段:
Phase 0 (已完成 ✅): ConfigRow + SectionHeader 提取为 @object-ui/components Phase 1 (建议现在做): 提取 useConfigDraft hook Phase 2 (建议现在做): 定义 ConfigPanelSchema 类型 + ConfigPanelRenderer Phase 3 (新面板): DashboardConfigPanel 用新框架实现(验证框架) Phase 4 (渐进迁移): ViewConfigPanel 渐进迁移到新框架 - 先将简单 section(Page、Advanced)迁移 - 保留复杂 section(Data 中的 inline field selector)用 type='custom' Phase 5 (后续面板): FormConfigPanel → ReportConfigPanel → PageConfigPanel → ...
6️⃣ 完整的包结构建议
packages/components/src/ custom/ config-row.tsx ← ✅ 已有 section-header.tsx ← ✅ 已有 config-panel-renderer.tsx ← 🆕 通用渲染器 config-field-renderer.tsx ← 🆕 单字段渲染器 (switch/input/select/filter/sort/custom) hooks/ useConfigDraft.ts ← 🆕 Draft state management hook types/ config-panel.ts ← 🆕 ConfigPanelSchema, ConfigSection, ConfigField 类型 apps/console/src/components/ ViewConfigPanel.tsx ← 逐步迁移到新框架 DashboardConfigPanel.tsx ← 🆕 第一个��新框架写的面板 FormConfigPanel.tsx ← 🆕 PageConfigPanel.tsx ← 🆕 ...
7️⃣ 对比方案评估
方案 工作量 可维护性 一致性 推荐 A. 每个面板独立写 (现状复制) 🔴 极高 (N × 1500行) 🔴 差 (N份重复逻辑) 🔴 差 (每个面板风格不同) ❌ B. 纯 Schema-Driven (全自动) 🟢 低 🟢 好 🟢 完美一致 ⚠️ 复杂交互受限C. Schema + Custom escape hatch (推荐) 🟡 中 🟢 好 🟢 高度一致 ✅ 最优 D. 基于 Zod Schema 自动推导 🟢 最低 🟡 中 (黑盒) 🟡 中 ⚠️ 过度抽象方案 C 是最优解:
- 80% 的配置项(switch, input, select)通过 schema 声明式生成
- 20% 的复杂交互(inline field selector, drag-to-reorder columns, row height icon groups)通过
type: 'custom'+ render prop 实现 - 保持完全的 Airtable 级 UX 精度,不牺牲灵活性
📋 建议的实施顺序(按 ROADMAP 优先级)
序号 任务 依赖 预估 1 提取 useConfigDrafthook无 0.5d 2 定义 ConfigPanelSchema类型无 0.5d 3 实现 ConfigFieldRenderer(7 种 control type)#2 1d 4 实现 ConfigPanelRenderer#1 #2 #3 1d 5 DashboardConfigPanel (验证框架) #4 1d 6 为所有新组件添加 Storybook stories + 测试 #4 #5 1d 7 ViewConfigPanel 渐��迁移 #4 2d 8 FormConfigPanel #4 1d 9 PageConfigPanel + ComponentConfigPanel #4 2d 10 AppConfigPanel + NavigationConfigPanel #4 1d 总估计:~11 个工作日,覆盖全部 6+ 种配置面板,且后续新增面板只需 ~60 行声明式代码。
⚠️ 注意:以上搜索结果可能不完整。如需查看更多代码,可以访问 GitHub 代码搜索。
背景
目前已实现的 ViewConfigPanel 体量大、难以维护,即将进入 Dashboard、Page、Form 等多种配置面板的密集需求期,面向未来,需统一抽象、声明式驱动的新一代配置面板体系,提升开发效率与一致性。
目标
主要任务
1. 通用配置原语与渲染器开发
2. DashboardConfigPanel 实现(demo + 实战验证)
3. ViewConfigPanel 渐进迁移
4. 其他平台配置面板快速跟进
5. 规范和文档
技术细节
验收标准
附加说明
相关链接: