Skip to content

通用 Schema-Driven 配置面板框架建设及全平台接入 #731

Description

@hotlong

背景

目前已实现的 ViewConfigPanel 体量大、难以维护,即将进入 Dashboard、Page、Form 等多种配置面板的密集需求期,面向未来,需统一抽象、声明式驱动的新一代配置面板体系,提升开发效率与一致性。

目标

  • 建设一套通用 Schema-Driven 配置面板框架,让所有「View/Dashboard/Form/Page/Report」等配置面板都能以声明式 schema 生成
  • 支持复杂交互通过 escape hatch 可扩展机制自定义渲染
  • 渐进迁移现有 ViewConfigPanel、支持未来所有协议级配置面板场景

主要任务

1. 通用配置原语与渲染器开发

  • 抽取现有 ConfigRow、SectionHeader 到 @object-ui/components(已完成,仅确认依赖版本)
  • 实现 useConfigDraft 通用 hook,封装草稿态、脏检测、保存/丢弃逻辑
  • 定义 ConfigPanelSchema/ConfigSection/ConfigField 类型支持 schema 驱动
  • 实现 ConfigPanelRenderer,负责:
    • 渲染头部、breadcrumb、footer
    • 各 section 支持 collapsible/collapsed
    • 各字段按 type: input/switch/select/filter/sort/custom 渲染
    • 配置变更时透传 onFieldChange
  • 实现 ConfigFieldRenderer 覆盖多种控件,包括但不限于:
    • Input/Switch/Select/Checkbox
    • FieldPicker(字段选择器)
    • Inline FilterBuilder/SortBuilder
    • IconGroup/Slider/ColorPicker/CodeEditor(根据需要拓展)
    • type='custom' 支持传入 render 回调,应对无法自动渲染的复杂场景
  • 官方测试、Storybook 展示

2. DashboardConfigPanel 实现(demo + 实战验证)

  • 设计 DashboardConfigPanelSchema,涵盖 dashboard 配置典型元素(columns/gap/widgets/globalFilter/appearance…)
  • 迁移实现到新版通用框架
  • 交互覆盖:保存/丢弃,section 折叠,子编辑器集成
  • 新增 Storybook 用例
  • 用 Vitest & Playwright 覆盖 E2E/UI 测试

3. ViewConfigPanel 渐进迁移

  • 按 section/功能单元分阶段迁移至通用方案(Page/Data/Appearance/UserActions/Sharing/Accessibility…)
  • 保持复杂交互用 type='custom' (如字段多选、拖拽排序、row height 快捷按钮)
  • 完成 migration 后移除遗留 ViewConfigPanel 大量冗余代码,提高可维护性

4. 其他平台配置面板快速跟进

  • FormConfigPanel(表单/分组/布局/schema 动态)
  • ReportConfigPanel(报表字段/分组/聚合)
  • PageConfigPanel(页面类型/区域/组件树/变量等)
  • 可复用面板基础单元(如字段选择、分组、颜色/图标等选择器)

5. 规范和文档

  • 撰写开发标准(如何写新的 ConfigPanelSchema/Field/Section,何时使用 type: 'custom')
  • 维护/更新设计文档,指导后续团队成员规范化开发
  • 用例与最佳实践集成到 Storybook

技术细节

  • 草稿态、脏检测统一由 useConfigDraft hook 提供
  • 每个字段渲染与编辑都由唯一的 ConfigField 配置定义,抽离所有表单和状态管理细节
  • Panel 渲染器支持单一/多级 section、折叠、子编辑器、footer 等,全局一致的 UX
  • 支持按需传入 objectDef 实现字段动态渲染
  • 复杂交互采用 type='custom' + render prop 定点突破
  • 后续支持自动根据 zod schema 生成 ConfigPanelSchema

验收标准

  • DashboardConfigPanel/新老 ViewConfigPanel 可用性 >= 现有体验,支持全部主流自定义场景
  • 各种类型的配置面板可根据 schema 快速生成,二次开发成本大幅降低
  • 代码层级明显收敛,组件内无重复/低效/难以测试的结构
  • 所有配置项均覆盖测试与文档

附加说明

  • 强调迁移工作的渐进式,不建议 Big Bang 重写
  • 推荐 Storybook + 单元测试体系完善后再推广至报表、页面、AppShell 等更大粒度平台

相关链接:

Activity

  1. self-assigned this
    on Feb 22, 2026
  2. added theissue type on Feb 22, 2026
  3. hotlong commented on Feb 22, 2026

    @hotlong
    ContributorAuthor

    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 → ListView ViewConfigPanel ✅ 已有 🔴 高 (50+ 配置项)
    view.zod.ts → FormView FormConfigPanel 🟡 中 (sections, fields, layout mode)
    dashboard.zod.ts DashboardConfigPanel + WidgetConfigPanel 🔴 高 (layout grid + widget 嵌套)
    report.zod.ts ReportConfigPanel 🟡 中 (report type, columns, grouping)
    page.zod.ts PageConfigPanel + ComponentConfigPanel 🔴 高 (16 page types, regions, components)
    app.zod.ts AppConfigPanel + NavigationConfigPanel 🟡 中 (nav tree, branding, permissions)
    action.zod.ts ActionConfigPanel 🟢 低 (action type + params)
    theme.zod.ts ThemeConfigPanel 🟡 中 (colors, typography, spacing)
    chart.zod.ts ChartConfigPanel 🟡 中 (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️⃣ 新增 useConfigDraft Hook — 从 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 提取 useConfigDraft hook 无 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 代码搜索。

  4. assigned and unassigned on Feb 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions