Skip to Content

ModelSideForm

分栏视图:左侧侧栏选择记录,右侧 ModelForm 展示/编辑。

相关文档

快速开始

import { Field } from "@/components/fields"; import { FormBody } from "@/components/views/form/components/FormBody"; import { FormHeader } from "@/components/views/form/components/FormHeader"; import { FormSection } from "@/components/views/form/components/FormSection"; import { FormToolbar } from "@/components/views/form/components/FormToolbar"; import { SideTree } from "@/components/views/shared/side-panel/SideTree"; import { ModelSideForm } from "@/components/views/side-form/ModelSideForm"; export default function SettingPage() { return ( <ModelSideForm modelName="Setting"> {/* Left: side panel for record selection */} <SideTree modelName="SettingGroup" filterField="groupId" labelField="name" parentField="parentId" /> {/* Right: standard ModelForm content */} <FormHeader /> <FormToolbar /> <FormBody> <FormSection label="General"> <Field fieldName="key" /> <Field fieldName="value" /> </FormSection> </FormBody> </ModelSideForm> ); }

工作原理

  1. 子节点拆分为一个侧栏元素(SideTree、SideCard 或 SideList)与表单内容(其余一切)。
  2. 侧栏包在 SidePanelContainerProvider 中,选择事件回流到 ModelSideForm。
  3. 选中记录后挂载带该记录 id 的 ModelForm。表单支持完整读/写生命周期——与独立 [id]/page.tsx 路由中的 ModelForm 相同。
  4. 选择可内部管理或由外部控制。在路由驱动的详情页传入 selectedRecordId 并处理 onSelectedRecordChange 以保持 URL 同步。
  5. 切换记录会重新挂载表单(通过 key 变化),每条记录获得全新表单状态。
  6. 若表单有未保存更改,切换前会弹出确认是否丢弃。
  7. 未选中记录时显示占位提示。

ModelSideForm 内级联 <Field fieldName="a.b"> 声明自动生效,因内嵌 ModelForm 处理遍历器 / 解析 / Provider 管线——见 ModelForm 中的级联字段路径。

属性

属性类型必填默认值说明
modelNamestring是-载入 ModelForm 的模型。
labelstring否-覆盖「未选择 X」/「创建 X」文案与 FormHeader 使用的模型标签。默认为 metaModel.label。
enableWorkflowboolean否false右侧表单工具栏显示工作流操作组(仅编辑模式)。
enableCreateboolean否true创建入口总开关(侧栏 + 空状态 + 工具栏「新建」)。
enableDuplicateboolean否auto右侧工具栏内置复制开关。false 禁用。复制进入内联创建模式,预填源记录可复制字段(非直接插入)。
enableDeleteboolean否auto右侧工具栏内置删除开关。false 禁用。
confirmDeleteMessagestring否Delete this {modelLabel}? This action cannot be undone.右侧工具栏内置删除的确认文案。
selectedRecordIdstring | null否非受控同一 side-form 页面详情路由变体的受控记录 id。
onSelectedRecordChange(id: string | null, record?: Record<string, unknown>) => void否-受控模式下侧栏选择不同记录时触发。
childrenReactNode是-一个侧栏元素 + 标准表单组件。

ModelSideForm 将工具栏相关属性转发给内部 ModelForm。FormToolbar 本身只需渲染自定义操作。

省略 selectedRecordId 时由 ModelSideForm 内部管理选择。提供时右侧表单始终跟随该 id,侧栏仅反映当前列表中的记录。

子节点结构

子节点分为两组:

分组组件渲染位置
侧栏其一:SideTree、SideCard、SideList左侧面板(280px)
表单内容FormHeader、FormToolbar、FormBody、Field 等右侧面板(flex-1)

表单内容子节点直接传给 ModelForm——组合方式与独立 ModelForm 页面相同。

布局

SideFormLayout 渲染两列布局:

┌──────────────┬──────────────────────────────────┐ │ Side Panel │ ModelForm │ │ (280px) │ (flex-1) │ │ │ │ │ SideTree / │ FormHeader │ │ SideCard / │ FormToolbar │ │ SideList │ FormBody │ │ │ FormSection │ │ │ Field ... │ │ │ │ └──────────────┴──────────────────────────────────┘
  • 侧栏:固定 280px,border-r 分隔
  • 表单区:flex-1,独立滚动
  • 两栏占满可用高度

侧栏选项

可使用任意侧栏组件。组件决定记录选择 UI:

组件最适合选择模式
<SideTree>层级数据(部门、分类)树节点
<SideCard>带头/体/脚的富卡片展示卡片点击
<SideList>带 Field 行模板的简单列表列表项

SideTree 示例

<ModelSideForm modelName="SysField"> <SideTree title="System Model" modelName="SysModel" filterField="modelId" labelField="label" parentField="parentId" sortField="modelName" selectionMode="single" defaultExpandedLevel={2} /> <FormHeader /> <FormBody> <FormSection label="Field Info"> <Field fieldName="fieldName" /> <Field fieldName="label" /> <Field fieldName="fieldType" /> </FormSection> </FormBody> </ModelSideForm>

SideList 示例

<ModelSideForm modelName="DesignActivity"> <SideList modelName="DesignActivity" filterField="id" filters={[["status", "=", "IN_PROGRESS"], "OR", ["status", "=", "READY"]]} searchable remoteSearch > <ActivityListItem /> </SideList> <FormHeader /> <FormToolbar /> <FormBody> <FormSection label="General"> <Field fieldName="name" /> <Field fieldName="status" /> </FormSection> </FormBody> </ModelSideForm>

SideList 子节点定义行模板。每行包在 RecordContextProvider 中,自定义组件可用 useRecordContext():

import { useRecordContext } from "@/components/contexts/RecordContext"; import { Badge } from "@/components/ui/badge"; function ActivityListItem() { const { record } = useRecordContext(); return ( <div className="flex w-full items-center justify-between gap-2"> <span className="truncate text-xs">{record.name as string}</span> <Badge variant="outline" className="shrink-0 text-[10px]"> {record.status as string} </Badge> </div> ); }

SideCard 示例

<ModelSideForm modelName="DesignActivity"> <SideCard modelName="DesignApp" filterField="appId" sortField="appName" searchable > <SideCard.Header> <Field fieldName="appName" /> </SideCard.Header> <SideCard.Header align="right"> <Field fieldName="status" /> </SideCard.Header> <Field fieldName="appCode" /> <SideCard.Footer> <Field fieldName="updatedTime" /> </SideCard.Footer> </SideCard> <FormHeader /> <FormBody> <FormSection label="General"> <Field fieldName="name" /> <Field fieldName="description" /> </FormSection> </FormBody> </ModelSideForm>

SideCard.Header 接受 align("left" | "right",默认 "left")。再声明 <SideCard.Header align="right"> 可将元素推到卡片头部行右侧。左右组内可各放多个元素;组内顺序按 JSX 顺序。placement="header" 的 Action 始终在左组;more(...)菜单始终在右组最末端。

表单内容

右侧渲染完整 ModelForm。组合方式与独立表单页相同:

组件用途
FormHeader带模型标签与描述的标题栏
FormToolbar业务操作(保存、删除、自定义)
FormBody带标签页与分区的表单主体
FormSection带标签的网格分区
Field单个表单字段
Action自定义工具栏或表单操作

带工具栏 Actions

import { Action } from "@/components/actions/Action"; import { CheckCircle, XCircle } from "lucide-react"; // Form-scope actions are implicitly disabled in `create` mode — no need to // repeat `mode === "create"` in `disabled`. See [Actions](../actions). <ModelSideForm modelName="DesignActivity"> <SideList filterField="id" searchable> <ActivityListItem /> </SideList> <FormHeader /> <FormToolbar> <Action label="Approve" icon={CheckCircle} operation="approve" placement="toolbar" confirmMessage="Approve this item?" successMessage="Approved." hidden={["status", "!=", "PENDING"]} /> <Action label="Reject" icon={XCircle} operation="reject" placement="more" confirmMessage="Reject this item?" successMessage="Rejected." hidden={["status", "!=", "PENDING"]} /> </FormToolbar> <FormBody> <FormSection label="General"> <Field fieldName="name" /> <Field fieldName="status" /> <Field fieldName="description" /> </FormSection> </FormBody> </ModelSideForm>

多个 FormSection

<ModelSideForm modelName="Employee"> <SideTree modelName="Department" filterField="departmentId" labelField="name" parentField="parentId" /> <FormHeader /> <FormToolbar /> <FormBody> <FormSection label="Basic Info"> <Field fieldName="firstName" /> <Field fieldName="lastName" /> <Field fieldName="email" /> </FormSection> <FormSection label="Employment"> <Field fieldName="departmentId" /> <Field fieldName="positionId" /> <Field fieldName="hireDate" /> </FormSection> <FormSection label="Custom Content"> <MyCustomComponent /> </FormSection> </FormBody> </ModelSideForm>

受控详情路由

选中记录需要出现在 URL 时使用受控模式:

"use client"; import { routes } from "@/app/studio/routes"; import { Field } from "@/components/fields"; import { FormBody } from "@/components/views/form/components/FormBody"; import { FormHeader } from "@/components/views/form/components/FormHeader"; import { FormToolbar } from "@/components/views/form/components/FormToolbar"; import { SideList } from "@/components/views/shared/side-panel/SideList"; import { ModelSideForm } from "@/components/views/side-form/ModelSideForm"; import { fillRouteTemplate } from "@/navigation"; import { useParams, useRouter } from "next/navigation"; export default function DesignActivityDetailPage() { const router = useRouter(); const params = useParams<{ appId: string; activityId: string }>(); return ( <ModelSideForm modelName="DesignActivity" selectedRecordId={params.activityId} onSelectedRecordChange={(nextId) => { if (!nextId) { router.push(fillRouteTemplate(routes.designActivity, { appId: params.appId })!); return; } router.push( fillRouteTemplate(routes.designActivityDetail, { appId: params.appId, activityId: nextId, })!, ); }} > <SideList modelName="DesignActivity" filterField="id" searchable remoteSearch> <ActivityListItem /> </SideList> <FormHeader /> <FormToolbar /> <FormBody> <Field fieldName="name" /> </FormBody> </ModelSideForm> ); }

说明:

  • 即使当前侧栏查询不包含该 id,右侧表单仍可从路由打开记录。
  • 此时侧栏无高亮行,表单仍通过 getById 加载详情记录。
  • 适用于 /design-model 与 /design-activity/[id] 等浏览/详情路由对。

脏状态与记录切换

ModelSideForm 自动跟踪表单是否有未保存更改。表单为脏时点击侧栏另一条记录:

  1. 确认对话框:「You have unsaved changes. Discard them and switch to the selected record?」
  2. 丢弃 → 切换到新记录,旧更改丢失
  3. 继续编辑 → 保持当前记录,侧栏选择不变

防止意外丢数据。每次切换记录会完全重新挂载表单(React key),每条记录获得干净表单状态。

受控模式下选择不同记录会调用 onSelectedRecordChange;父页面负责更新路由或外部状态以喂给 selectedRecordId。

与其他视图对比

功能ModelSideFormModelTableModelCard
数据展示单条记录表单多行表格网格多卡片网格
侧栏必需(选择)可选(过滤)可选(过滤)
记录编辑完整表单编辑可选内联编辑-
点击行为侧栏选择记录导航或内联编辑导航
脏状态保护是仅内联编辑-
搜索/过滤/排序仅侧栏完整工具栏简化工具栏
分页侧栏(客户端)服务端服务端
远程搜索remoteSearch 属性内置内置
最后更新于