|
| 1 | +## 概述 |
| 2 | + |
| 3 | +本 Issue 汇总基于 ObjectUI 开发完整 ObjectStack 前端界面的最终开发方案。ObjectUI 是 ObjectStack 的 **Server-Driven UI (SDUI) 渲染引擎**,基于 `@objectstack/spec` 的 UI 协议(ViewSchema、PageSchema、AppSchema、ActionSchema、ThemeSchema)将 JSON 元数据渲染为 Shadcn/Tailwind 品质的 React 组件。 |
| 4 | + |
| 5 | +### 定位 |
| 6 | + |
| 7 | +``` |
| 8 | +@objectstack/spec (协议层) → 定义 UI 的 JSON Schema(what) |
| 9 | +@objectstack/client + client-react → 数据/元数据获取 hooks(how to get data) |
| 10 | +ObjectUI (渲染层) → 将 JSON 元数据渲染为 React 组件(how to render) |
| 11 | +apps/studio (开发工具) → 元数据编辑 IDE(设计器) |
| 12 | +``` |
| 13 | + |
| 14 | +**ObjectUI ≠ Studio**: |
| 15 | +- **Studio** = 面向开发者的元数据编辑器(当前仓库 `apps/studio/`) |
| 16 | +- **ObjectUI** = 面向终端用户的业务应用渲染引擎(新建 `packages/objectui/`) |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## 现有基础设施 |
| 21 | + |
| 22 | +### ✅ 已完成 — 协议层(`@objectstack/spec`) |
| 23 | + |
| 24 | +| Schema | 文件 | 描述 | |
| 25 | +|:---|:---|:---| |
| 26 | +| `AppSchema` | `spec/src/ui/app.zod.ts` | App 导航壳:navigation tree、areas、branding | |
| 27 | +| `ViewSchema` (ListView + FormView) | `spec/src/ui/view.zod.ts` | 7 种视图类型 (grid/kanban/gallery/calendar/timeline/gantt/map) + 表单 | |
| 28 | +| `PageSchema` | `spec/src/ui/page.zod.ts` | 16 种页面类型,Region 布局、FlexiPage + Airtable Interface 混合 | |
| 29 | +| `DashboardSchema` | `spec/src/ui/dashboard.zod.ts` | Widget 组合面板:KPI、Chart、Pivot、Matrix | |
| 30 | +| `ActionSchema` | `spec/src/ui/action.zod.ts` | 交互抽象:script / url / modal / flow / api | |
| 31 | +| `ThemeSchema` | `spec/src/ui/theme.zod.ts` | 设计令牌:颜色、排版、间距、圆角、模式 | |
| 32 | +| `PageComponentSchema` | `spec/src/ui/component.zod.ts` | 组件 Props 定义(RecordDetails、RelatedList、Activity 等) | |
| 33 | +| `WidgetSchema` | `spec/src/ui/widget.zod.ts` | 自定义 Widget 生命周期(onMount/onUpdate/onUnmount) | |
| 34 | +| I18n、Responsive、Keyboard、Touch、DnD、Offline、Animation、Notification | `spec/src/ui/*.zod.ts` | 完整的 PWA 级 UI 协议 | |
| 35 | + |
| 36 | +### ✅ 已完成 — 客户端 SDK |
| 37 | + |
| 38 | +| Hook | 包 | 描述 | |
| 39 | +|:---|:---|:---| |
| 40 | +| `useQuery` / `useMutation` | `client-react` | 数据 CRUD | |
| 41 | +| `usePagination` / `useInfiniteQuery` | `client-react` | 分页/无限滚动 | |
| 42 | +| `useObject` / `useView` / `useFields` / `useMetadata` | `client-react` | 元数据获取 | |
| 43 | +| `useDataSubscription` / `useMetadataSubscription` | `client-react` | 实时订阅 | |
| 44 | +| `ObjectStackProvider` / `useClient` | `client-react` | 上下文提供 | |
| 45 | + |
| 46 | +### ✅ 已完成 — REST API |
| 47 | + |
| 48 | +| API | 描述 | |
| 49 | +|:---|:---| |
| 50 | +| `GET /api/v1/meta/objects/:name` | Object Schema (字段、关系、验证) | |
| 51 | +| `GET /api/v1/meta/views/:name` | View 定义 (列、排序、筛选) | |
| 52 | +| `GET /api/v1/meta/apps` | App 列表 (导航、图标、权限) | |
| 53 | +| `GET /api/v1/data/:object` | 数据查询 (OData-style filter/sort/select) | |
| 54 | +| `POST /PUT /DELETE /api/v1/data/:object` | 数据 CRUD | |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +## 开发方案:分 5 个阶段 |
| 59 | + |
| 60 | +### Phase 1: 核心渲染引擎 (`packages/objectui/`) |
| 61 | + |
| 62 | +> **目标**:能将 ViewSchema JSON 渲染为 Shadcn 品质的 React 组件 |
| 63 | +
|
| 64 | +#### 1.1 ViewRenderer — 视图渲染工厂 |
| 65 | + |
| 66 | +``` |
| 67 | +ViewSchema.type → React Component |
| 68 | + 'grid' → <DataGrid /> (TanStack Table + virtual scroll) |
| 69 | + 'kanban' → <KanbanBoard /> (dnd-kit) |
| 70 | + 'gallery' → <GalleryGrid /> (CSS Grid / Masonry) |
| 71 | + 'calendar' → <CalendarView /> (FullCalendar or custom) |
| 72 | + 'timeline' → <TimelineView /> (custom) |
| 73 | + 'gantt' → <GanttChart /> (custom or gantt-task-react) |
| 74 | + 'map' → <MapView /> (Mapbox/Leaflet) |
| 75 | +``` |
| 76 | + |
| 77 | +**核心 API 设计**: |
| 78 | + |
| 79 | +```typescript |
| 80 | +// packages/objectui/src/index.tsx |
| 81 | + |
| 82 | +// 1. View Renderer — 根据 ViewSchema 类型分发 |
| 83 | +export function ViewRenderer({ view, object }: { view: ListView; object: string }) { |
| 84 | + // useQuery + useObject 获取数据和字段定义 |
| 85 | + // 根据 view.type 选择对应组件 |
| 86 | +} |
| 87 | + |
| 88 | +// 2. Form Renderer — 根据 FormViewSchema 渲染表单 |
| 89 | +export function FormRenderer({ form, object, recordId? }: Props) { |
| 90 | + // useObject 获取字段定义 |
| 91 | + // 根据 form.sections 渲染分组字段 |
| 92 | +} |
| 93 | + |
| 94 | +// 3. Field Renderer — 根据 FieldType 渲染单个字段 |
| 95 | +export function FieldRenderer({ field, value, mode }: Props) { |
| 96 | + // mode: 'display' | 'edit' | 'filter' |
| 97 | + // field.type: 'text' | 'number' | 'date' | 'select' | 'reference' | ... |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +**关键实现细节**: |
| 102 | + |
| 103 | +| 组件 | Spec 对应 | 依赖库 | 优先级 | |
| 104 | +|:---|:---|:---|:---:| |
| 105 | +| `<DataGrid />` | `ListViewSchema` (type: grid) | TanStack Table v8 + TanStack Virtual | 🔴 P0 | |
| 106 | +| `<FormRenderer />` | `FormViewSchema` + `FormSectionSchema` | React Hook Form + Zod resolver | 🔴 P0 | |
| 107 | +| `<FieldRenderer />` | `FieldSchema.type` (30+ 字段类型) | Shadcn UI primitives | 🔴 P0 | |
| 108 | +| `<KanbanBoard />` | `KanbanConfigSchema` | dnd-kit | 🟡 P1 | |
| 109 | +| `<CalendarView />` | `CalendarConfigSchema` | date-fns + custom | 🟡 P1 | |
| 110 | +| `<GalleryGrid />` | `GalleryConfigSchema` | CSS Grid | 🟡 P1 | |
| 111 | + |
| 112 | +#### 1.2 FieldType → Widget 映射 |
| 113 | + |
| 114 | +``` |
| 115 | +text → <Input /> |
| 116 | +textarea → <Textarea /> |
| 117 | +number → <NumberInput /> |
| 118 | +currency → <CurrencyInput /> |
| 119 | +percent → <PercentInput /> |
| 120 | +date → <DatePicker /> |
| 121 | +datetime → <DateTimePicker /> |
| 122 | +time → <TimePicker /> |
| 123 | +boolean → <Switch /> or <Checkbox /> |
| 124 | +select → <Select /> (single) |
| 125 | +multiselect → <MultiSelect /> (tags) |
| 126 | +reference → <RecordPicker /> (lookup) |
| 127 | +multi_reference→ <MultiRecordPicker /> |
| 128 | +email → <Input type="email" /> |
| 129 | +url → <Input type="url" /> with preview |
| 130 | +phone → <PhoneInput /> |
| 131 | +rating → <StarRating /> |
| 132 | +image → <ImageUpload /> |
| 133 | +file → <FileUpload /> |
| 134 | +rich_text → <RichTextEditor /> (Tiptap) |
| 135 | +json → <CodeEditor /> (Monaco) |
| 136 | +formula → <FormulaDisplay /> (read-only) |
| 137 | +autonumber → <Badge /> (read-only) |
| 138 | +``` |
| 139 | + |
| 140 | +--- |
| 141 | + |
| 142 | +### Phase 2: App Shell — 应用导航框架 |
| 143 | + |
| 144 | +> **目标**:将 `AppSchema` 渲染为完整的应用壳 |
| 145 | +
|
| 146 | +#### 2.1 AppShell 组件 |
| 147 | + |
| 148 | +```typescript |
| 149 | +// packages/objectui/src/app/AppShell.tsx |
| 150 | + |
| 151 | +export function AppShell({ app }: { app: App }) { |
| 152 | + return ( |
| 153 | + <SidebarProvider> |
| 154 | + <AppSidebar app={app} /> {/* 左侧导航 */} |
| 155 | + <main> |
| 156 | + <SiteHeader app={app} /> {/* 顶部栏 */} |
| 157 | + <Outlet /> {/* 页面内容 */} |
| 158 | + </main> |
| 159 | + </SidebarProvider> |
| 160 | + ); |
| 161 | +} |
| 162 | +``` |
| 163 | + |
| 164 | +#### 2.2 导航渲染 |
| 165 | + |
| 166 | +| AppSchema 字段 | 渲染为 | |
| 167 | +|:---|:---| |
| 168 | +| `app.navigation[]` | 递归侧边栏菜单 (GroupNavItem → 折叠组, ObjectNavItem → 链接) | |
| 169 | +| `app.areas[]` | 顶部 Area 切换器 (类似 Salesforce App Launcher) | |
| 170 | +| `app.branding` | Logo + 主题色 | |
| 171 | +| `app.mobileNavigation` | 移动端底部 Tab / 抽屉菜单 | |
| 172 | + |
| 173 | +#### 2.3 路由系统 |
| 174 | + |
| 175 | +``` |
| 176 | +/app/:appName → App Shell |
| 177 | +/app/:appName/object/:objectName → 默认 ListView |
| 178 | +/app/:appName/object/:objectName/view/:viewName → 指定 ListView |
| 179 | +/app/:appName/object/:objectName/:id → 记录详情 (FormView) |
| 180 | +/app/:appName/dashboard/:dashboardName → Dashboard |
| 181 | +/app/:appName/page/:pageName → Custom Page |
| 182 | +``` |
| 183 | + |
| 184 | +--- |
| 185 | + |
| 186 | +### Phase 3: Page Renderer — 页面组合引擎 |
| 187 | + |
| 188 | +> **目标**:将 `PageSchema` 渲染为组件组合页面 |
| 189 | +
|
| 190 | +#### 3.1 PageRenderer |
| 191 | + |
| 192 | +```typescript |
| 193 | +export function PageRenderer({ page }: { page: Page }) { |
| 194 | + // 1. 根据 page.type 选择布局模板 |
| 195 | + // 2. 遍历 page.regions,每个 region 渲染其 components |
| 196 | + // 3. 每个 component 根据 type 分发到 ComponentRegistry |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +#### 3.2 ComponentRegistry(核心扩展点) |
| 201 | + |
| 202 | +```typescript |
| 203 | +const COMPONENT_REGISTRY: Record<string, React.ComponentType<any>> = { |
| 204 | + // Structure |
| 205 | + 'page:header': PageHeader, |
| 206 | + 'page:tabs': PageTabs, |
| 207 | + 'page:card': PageCard, |
| 208 | + |
| 209 | + // Record Context |
| 210 | + 'record:details': RecordDetails, // → 字段详情面板 |
| 211 | + 'record:highlights': RecordHighlights, // → KPI 高亮卡 |
| 212 | + 'record:related_list': RecordRelatedList, // → 关联记录列表 |
| 213 | + 'record:activity': RecordActivity, // → 活动/评论流 |
| 214 | + |
| 215 | + // AI |
| 216 | + 'ai:chat_window': AIChatWindow, |
| 217 | + 'ai:suggestion': AISuggestion, |
| 218 | + |
| 219 | + // Elements (Airtable Interface parity) |
| 220 | + 'element:text': TextElement, |
| 221 | + 'element:number': NumberElement, |
| 222 | + 'element:button': ButtonElement, |
| 223 | + 'element:filter': FilterElement, |
| 224 | + 'element:form': FormElement, |
| 225 | + 'element:record_picker': RecordPicker, |
| 226 | +}; |
| 227 | +``` |
| 228 | + |
| 229 | +--- |
| 230 | + |
| 231 | +### Phase 4: Dashboard & Reports |
| 232 | + |
| 233 | +> **目标**:将 `DashboardSchema` 渲染为 KPI/图表面板 |
| 234 | +
|
| 235 | +| Widget 类型 | 渲染组件 | 图表库 | |
| 236 | +|:---|:---|:---| |
| 237 | +| `chart` | `<ChartWidget />` | Recharts 或 ECharts | |
| 238 | +| `kpi` | `<KPICard />` | Shadcn Card + 数字动画 | |
| 239 | +| `table` | `<TableWidget />` | 复用 DataGrid | |
| 240 | +| `pivot` | `<PivotTable />` | TanStack Table + grouping | |
| 241 | +| `list` | `<ListWidget />` | 复用 DataGrid (简化版) | |
| 242 | + |
| 243 | +--- |
| 244 | + |
| 245 | +### Phase 5: Interface Builder (Design → Publish) |
| 246 | + |
| 247 | +> **目标**:可视化拖拽构建界面,参考 Issue #823 分析 |
| 248 | +
|
| 249 | +此阶段实现 Airtable-style Interface Designer: |
| 250 | +- **设计模式**:拖拽组件到画布,右侧属性面板 |
| 251 | +- **预览模式**:以终端用户视角预览 |
| 252 | +- **发布流程**:Draft → Staged → Published 三阶段生命周期 |
| 253 | +- **版本管理**:版本快照、回滚、差异对比 |
| 254 | + |
| 255 | +详见 Issue #823 的架构设计。 |
| 256 | + |
| 257 | +--- |
| 258 | + |
| 259 | +## 技术栈决策 |
| 260 | + |
| 261 | +| 领域 | 选择 | 理由 | |
| 262 | +|:---|:---|:---| |
| 263 | +| **UI 框架** | React 19 | 与 client-react、Studio 统一 | |
| 264 | +| **组件库** | Shadcn UI + Radix | 可定制、无锁定、对标 ThemeSchema | |
| 265 | +| **样式** | Tailwind CSS 4 | 设计令牌驱动、响应式 | |
| 266 | +| **表格** | TanStack Table v8 + TanStack Virtual | 虚拟滚动、列排序/筛选/分组/固定 | |
| 267 | +| **表单** | React Hook Form + Zod | Zod-first 验证、与 Spec FieldValidation 对齐 | |
| 268 | +| **拖拽** | dnd-kit | Kanban + Interface Builder + DnD 排序 | |
| 269 | +| **图表** | Recharts (轻量) 或 ECharts (全功能) | Dashboard Widget 渲染 | |
| 270 | +| **富文本** | Tiptap v2 | rich_text 字段类型 | |
| 271 | +| **路由** | TanStack Router | 类型安全、与 Studio 统一 | |
| 272 | +| **状态** | Zustand + React Query (TanStack Query) | 服务器状态 + 本地 UI 状态 | |
| 273 | +| **日期** | date-fns | Calendar/Timeline/Gantt 视图 | |
| 274 | +| **国际化** | 运行时渲染 `I18nLabelSchema` | 协议内置 i18n | |
| 275 | + |
| 276 | +--- |
| 277 | + |
| 278 | +## 包结构 |
| 279 | + |
| 280 | +``` |
| 281 | +packages/objectui/ |
| 282 | +├── src/ |
| 283 | +│ ├── index.tsx # 公开 API 入口 |
| 284 | +│ ├── provider.tsx # <ObjectUIProvider> (配置 + 客户端注入) |
| 285 | +│ │ |
| 286 | +│ ├── app/ # App Shell 层 |
| 287 | +│ │ ├── AppShell.tsx # 主布局 (sidebar + header + outlet) |
| 288 | +│ │ ├── AppSidebar.tsx # 导航渲染 (NavigationItemSchema → 递归菜单) |
| 289 | +│ │ ├── AppLauncher.tsx # App 切换器 |
| 290 | +│ │ └── AppRouter.tsx # 路由配置 |
| 291 | +│ │ |
| 292 | +│ ├── views/ # View Renderers |
| 293 | +│ │ ├── ViewRenderer.tsx # 视图分发工厂 |
| 294 | +│ │ ├── DataGrid.tsx # grid 视图 |
| 295 | +│ │ ├── KanbanBoard.tsx # kanban 视图 |
| 296 | +│ │ ├── GalleryGrid.tsx # gallery 视图 |
| 297 | +│ │ ├── CalendarView.tsx # calendar 视图 |
| 298 | +│ │ ├── TimelineView.tsx # timeline 视图 |
| 299 | +│ │ ├── GanttChart.tsx # gantt 视图 |
| 300 | +│ │ └── MapView.tsx # map 视图 |
| 301 | +│ │ |
| 302 | +│ ├── forms/ # Form Renderers |
| 303 | +│ │ ├── FormRenderer.tsx # 表单布局工厂 |
| 304 | +│ │ ├── FormSection.tsx # 分组渲染 |
| 305 | +│ │ └── FormWizard.tsx # 向导模式 |
| 306 | +│ │ |
| 307 | +│ ├── fields/ # Field Renderers (30+ 字段类型) |
| 308 | +│ │ ├── FieldRenderer.tsx # 字段分发工厂 |
| 309 | +│ │ ├── TextField.tsx |
| 310 | +│ │ ├── NumberField.tsx |
| 311 | +│ │ ├── DateField.tsx |
| 312 | +│ │ ├── SelectField.tsx |
| 313 | +│ │ ├── ReferenceField.tsx # Lookup / Record Picker |
| 314 | +│ │ └── ... |
| 315 | +│ │ |
| 316 | +│ ├── pages/ # Page Renderers |
| 317 | +│ │ ├── PageRenderer.tsx # 页面分发工厂 |
| 318 | +│ │ ├── RecordPage.tsx # 记录详情页 |
| 319 | +│ │ ├── HomePage.tsx # 首页 |
| 320 | +│ │ └── BlankPage.tsx # 自由画布 |
| 321 | +│ │ |
| 322 | +│ ├── dashboard/ # Dashboard Renderers |
| 323 | +│ │ ├── DashboardRenderer.tsx |
| 324 | +│ │ ├── ChartWidget.tsx |
| 325 | +│ │ ├── KPICard.tsx |
| 326 | +│ │ └── PivotTable.tsx |
| 327 | +│ │ |
| 328 | +│ ├── actions/ # Action Execution Engine |
| 329 | +│ │ ├── ActionRunner.tsx |
| 330 | +│ │ └── ActionButton.tsx |
| 331 | +│ │ |
| 332 | +│ └── theme/ # ThemeSchema → CSS Variables |
| 333 | +│ ├── ThemeProvider.tsx |
| 334 | +│ └── tokens.ts |
| 335 | +│ |
| 336 | +├── package.json |
| 337 | +├── tsconfig.json |
| 338 | +└── vitest.config.ts |
| 339 | +``` |
| 340 | + |
| 341 | +--- |
| 342 | + |
| 343 | +## 依赖关系 |
| 344 | + |
| 345 | +``` |
| 346 | +@objectstack/spec ← 协议定义 (Zod schemas) |
| 347 | +@objectstack/client ← HTTP 客户端 (data + metadata API) |
| 348 | +@objectstack/client-react ← React hooks (useQuery, useObject, useView, ...) |
| 349 | + │ |
| 350 | + ▼ |
| 351 | +@objectstack/objectui ← UI 渲染引擎 (本 Issue 开发目标) |
| 352 | + │ |
| 353 | + ▼ |
| 354 | +apps/studio ← 开发者 IDE (导入 objectui 组件) |
| 355 | +apps/portal ← 终端用户业务应用 (未来) |
| 356 | +``` |
| 357 | + |
| 358 | +--- |
| 359 | + |
| 360 | +## 实施优先级 |
| 361 | + |
| 362 | +| 阶段 | 内容 | 预估工期 | 依赖 | |
| 363 | +|:---:|:---|:---:|:---| |
| 364 | +| **P0** | Phase 1: DataGrid + FormRenderer + FieldRenderer (核心三件套) | 4-6 周 | spec + client-react | |
| 365 | +| **P0** | Phase 2: AppShell + Navigation + Router | 2-3 周 | Phase 1 | |
| 366 | +| **P1** | Phase 1 续: Kanban + Calendar + Gallery 视图 | 3-4 周 | Phase 1 | |
| 367 | +| **P1** | Phase 3: PageRenderer + ComponentRegistry | 3-4 周 | Phase 2 | |
| 368 | +| **P1** | Phase 4: Dashboard + Chart Widgets | 2-3 周 | Phase 1 | |
| 369 | +| **P2** | Phase 5: Interface Builder (参见 #823) | 12-16 周 | Phase 1-4 | |
| 370 | +| **P2** | 移动端适配 (mobileNavigation + Touch) | 3-4 周 | Phase 2 | |
| 371 | +| **P2** | 离线模式 (OfflineConfigSchema) | 2-3 周 | Phase 1 | |
| 372 | + |
| 373 | +**总预估**: 核心可用 (P0) = 6-9 周, 完整功能 (P0+P1) = 15-20 周 |
| 374 | + |
| 375 | +--- |
| 376 | + |
| 377 | +## 与现有 Issue 的关系 |
| 378 | + |
| 379 | +| Issue | 关系 | |
| 380 | +|:---|:---| |
| 381 | +| #823 Assessment: Airtable Interface Designer | Phase 5 的架构基础,Interface Builder 设计方案 | |
| 382 | +| #1159 Studio 优化 | Studio 是设计器,ObjectUI 是渲染器,两者互补 | |
| 383 | +| #989 统一前后端 API 查询语法 | ObjectUI 的 DataGrid 依赖 API 查询语法正确性 | |
| 384 | +| #866 Filter operators broken | ObjectUI 的 filter/sort 功能依赖此 fix | |
| 385 | +| #724 基础设施核心服务 | ObjectUI 的 realtime/search/notification 功能依赖 | |
| 386 | + |
| 387 | +--- |
| 388 | + |
| 389 | +## 验收标准 |
| 390 | + |
| 391 | +- [ ] 能基于 `defineStack()` 中的 objects + views + apps 定义,**零代码**渲染出完整 CRUD 应用 |
| 392 | +- [ ] DataGrid 支持排序、筛选、分页、虚拟滚动、行选择、内联编辑 |
| 393 | +- [ ] FormRenderer 支持 simple / tabbed / wizard 布局,含字段级验证 |
| 394 | +- [ ] AppShell 支持递归侧边栏导航、Area 切换、移动端适配 |
| 395 | +- [ ] 所有 30+ 字段类型在 display / edit / filter 三种模式下均有渲染实现 |
| 396 | +- [ ] Dashboard 支持 KPI 卡片 + 图表 + 表格 Widget 组合 |
| 397 | +- [ ] 主题令牌 (ThemeSchema) 驱动所有颜色/排版/间距,支持亮/暗模式 |
| 398 | +- [ ] 完整的 TypeScript 类型推导,无 `any` 泄漏 |
| 399 | +- [ ] 组件测试覆盖率 > 60% |
0 commit comments