Skip to content

字段多的对象:自适应呈现表面 + 语义 field span(auto/full)+ 表单布局防呆 lint(AI-authored,ADR-0085 对齐) #2578

Description

@os-zhuang

状态:✅ 已实现并浏览器端到端验证(2026-07-04)。 分支 feat/adaptive-layout-2578(framework + objectui),单测 / 类型全绿,待提 PR。下方「开发方案」为设计记录,保留。

✅ 开发效果(Delivered)

一句话

字段多的对象,新增 / 编辑 / 查看详情自动切换到整页窗口;字段少的对象保持抽屉 / 弹窗。全部由平台按字段数自动推断,AI 写元数据时零额外配置——不暴露 recordSurface 这类可写 key,所以 AI 无从写错。

Before → After

维度 开发前 开发后
字段多对象的记录详情 一律抽屉,几十个字段挤在侧栏 可授权字段数 ≥ 12 → 整页(有 URL、可后退);< 12 → 抽屉。移动端一律整页
录入表单多列(section.columns) type:'simple' 忽略 per-section 列数,分组内字段始终单列(#2515) 按声明渲染多列,每个分组按自己的列密度在共享网格内排布
字段级宽度 只有绝对 colSpan (1–4),换表面(手机 / 弹窗 / 整页)就错位 语义 span(auto / full),与派生列数解耦,任何列数都不溢出;遗留 colSpan 渲染期 clamp,永不越界
AI 写错字段引用 / 布局 Zod 合法但静默失效,作者发现不了 编写期 warning + 修复提示(form-field-unknown / absolute-colspan-discouraged),AI 与人手写同一把尺(ADR-0019 / 0078)
协议演进 倾向新增 recordSurface 开关 + 新 ADR 不加 object key、不开新 ADR:字段数机器能推 → 派生;显式覆盖走 assigned page(ADR-0085 §2 准入测试)

浏览器验证(真 console + app-showcase 后端,端到端跑通)

对象 可授权字段 行点击 → 结果 观测到的 URL / 形态
字段动物园 field_zoo 57 整页 /…/showcase_field_zoo/record/<id>,面包屑 + 两列详情,无遮罩
产品 product 6 抽屉 停在列表 ?recordId=<id>,右侧抽屉浮层覆盖虚化的列表

录入表单同时验证多列:Email │ URL 两列、Phone │ Password 两列、Textarea 独占整行(宽控件 span:auto 自动整行)。

用户 / AI 视角

  • AI 建一个几十字段的对象、不写任何表面或列配置 → 记录自动整页 + 表单自动多列 + 长文本自动整行。
  • 想让某个小对象也强制整页 → 指派一个 record page(ADR-0085 认定的 per-page 正道),而不是加新 key。

交付与验证

  • framework f79263a19:deriveRecordSurface(spec/src/data/record-surface.ts)+ FormFieldSchema.span + packages/lint/src/validate-form-layout.ts + ADR-0085 §2 澄清 + changeset。spec 6677 测试、api-surface(regen)、lint 121 全绿。
  • objectui 97726afa4 + a56cb420f:autoLayout.resolveColSpan(span 感知 / clamp / per-section 密度)+ ObjectForm grouped 路径修 console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515 + FormField.span 透传 + app-shell 记录表面接入 deriveRecordSurface。autoLayout 43 + plugin-form 173 测试、turbo type-check 29/29 全绿。
  • 关键实现点:console 的记录表面在 packages/app-shell/src/views/ObjectView.tsx 决定(原默认 drawer),已在其 detailNavigation 默认接入 deriveRecordSurface——底层 plugin-view 改动不够,必须在此层接入才在 console 生效。

已知边界 / 后续


背景

字段特别多的对象(50+ 字段),来自 #2574 的两个诉求:

  1. 多列编辑/显示;
  2. 部分对象在新增/编辑/查看详情时自动切到全屏页面,而不是弹窗。

北极星约束:所有元数据都是 AI 写的,所以设计的核心目标不是"能配",而是让 AI 无从写错。下面每个决策都围绕这一点。


关键决策(为什么不走最直觉的做法)

1. 不新增 recordSurface 之类的对象级 key

按 ADR-0085 §2 的"未来 key 准入测试",呈现表面(modal/page)两条都不过:

  • 判据是字段数,恰恰机器能推 → 机器能推的不该做成 AI 要填的 key;
  • modal-vs-page 是纯重排呈现,不是数据的业务事实。ADR-0085 明确 reject 掉这类 per-surface 开关,并规定 per-page 控制走 assigned page。

→ 呈现表面 = 渲染器启发式(auto) + assigned page 覆盖,不是 authored key。

2. 不新写 ADR / 设计文档

方案完全落在 ADR-0085 已画的边界内(auto 派生 + assigned page),理由进 changeset + 代码注释即可(合"fewer ADRs"取向)。唯一可选动作:往 ADR-0085 §2 的 rejected-keys 清单补一行 "modal-vs-page presentation surface" 例子,防止有人(讨论中我本人就提过又撤回)再提 recordSurface。

3. 绝对 colSpan 在自动适应下 fragile,改语义 span

列数是派生的(手机 1 / modal 2 / 整页 3–4),绝对 colSpan: 3 只在脑补的那个宽度下对,换表面就溢出/错位。
→ 字段级排版原语改为与列数解耦的 span: 'auto' | 'full'(默认 auto)。

  • auto:渲染器按 字段类型 × 派生列数 自动排(标量按密度并排,textarea/richtext/子表等宽控件自动整行)——90% 情况 AI 一个都不用写。
  • full:任何列数下独占整行。
  • half:本期不做(三者里 AI-safety 最弱:奇数列要取整、很多成对关系渲染器能自己推、多一个可选错的旋钮)。等 dogfood 真的暴露"成对字段在宽页被拆散"再议。

4. 多列的地基已经有了

ADR-0085 的 fieldGroups + Field.group + collapse、FormSection.columns(1–4)、RecordDetails.columns 都已在 spec,并已被 validate-semantic-roles.ts 守住字段引用。缺口:表单视图内 sections[].fields 引用 + colSpan 没有对应交叉校验;renderer 侧 #2515(section.columns 没渲染成多列)。


贯穿全案的防呆原则(设计脊椎)

闭合枚举(编译期拒) > 智能默认(让 AI 不用写) > 单一派生器(不漂移) > 两级 lint(warning + hint) > schema.describe 即提示词。

依据:ADR-0078(no-silently-inert)、ADR-0019(lint 对 AI 和人手写同一把尺)、ADR-0085(object-level 语义角色 / 准入测试)。


开发计划

Step 1 — framework(一个 PR;纯函数 + 文档;可单测;无需浏览器;独立可合)

  1. deriveRecordSurface(fieldCount, groups) —— 放 packages/spec/src/data/,与 deriveFieldGroupLayout 并列。从字段数/分组单源派生 page/modal/drawer(移动端强制 page)。是派生,不是 authored key(ADR-0085 §5「one shared derivation, every surface」)。单测覆盖阈值边界、有/无 fieldGroups、mobile。
  2. FormFieldSchema.span: z.enum(['auto','full']).default('auto') —— 加在 packages/spec/src/ui/view.zod.ts。保留 colSpan 兼容。.describe() 写成 AI 引导语:"默认省略(按类型自动);整行用 full;别用绝对列数,表面列数是派生的。"
  3. packages/lint/src/validate-form-layout.ts(照 validate-semantic-roles.ts 模子):
    • form-field-unknown(warning):section 引用了对象上不存在的字段;
    • absolute-colspan-discouraged(warning + hint):用了 colSpan,引导改 span:'full'(自动列数下 overflow 无良定义,故不做比大小)。
      导出进 lint index。
  4. ADR-0085 §2 一行澄清:rejected-keys 清单显式加 "modal-vs-page presentation surface"。
  5. changeset(理由写这里,不新开 ADR)。

验收:单测绿;os validate 对种子坏对象吐出两条新 warning;spec 为 additive(不升 major)。

Step 2 — objectui + 浏览器 dogfood(一个 PR)

  1. ObjectView/ObjectForm 消费 deriveRecordSurface();create/edit/detail 统一走派生表面,替掉硬编码的 layout='drawer' 默认。
  2. 字段渲染:span:'auto' → 宽度按 widget 类型 × 派生列数(宽控件自动整行);span:'full' → 整行;遗留 colSpan 渲染期 clamp = min(colSpan, 当前列数)(降级不破版,ADR-0078)。
  3. 修 console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515(section.columns 未渲染成多列)作为本 PR 一部分。
  4. Dogfood 一个 ~60 字段对象(如 contract):AI 元数据里不写任何表面/colSpan 配置 → 自动全屏 + 多列分组 + textarea 自动整行;并验证 assigned page 覆盖能强制全屏。走"prove-it-runs"浏览器验证。

Non-goals / 已否决的替代方案(避免重复讨论)

  • ❌ recordSurface(或任何 object 级 modal/page 开关)—— 被 ADR-0085 §2 准入测试否决。
  • ❌ 新 ADR / 独立设计文档 —— 不必要,落在 ADR-0085 边界内。
  • ❌ span:'half' —— 暂缓,等 dogfood 暴露真实成对断裂再议。
  • ❌ 绝对 colSpan 作为主要排版原语 —— 仅保留兼容,lint 引导迁移。

Refs

Activity

  1. os-zhuang commented on Jul 4, 2026

    @os-zhuang
    ContributorAuthor

    设计细化(讨论后 · 2026-07-05)

    讨论收敛出一条更硬的原则 + 几个具体改动。

    原则:表现全部运行时派生,AI 零像素授权

    AI 只写数据事实(字段 + fieldGroups / highlightFields 等语义角色)。surface / 宽度 / 列数一律运行时算——它们要么机器能推(字段数),要么只有客户端知道(视口)。AI 不知道客户端浏览器宽度,所以任何像素/宽度都不该进元数据。

    AI 写"数据长什么样",客户端写"这块屏幕这会儿怎么摆"。像素永远不进元数据。

    两个正交轴,都默认 auto

    轴 取值 说明
    mode(在哪开) auto / drawer / modal / page auto:字段数 ≥12→page、<12→drawer;mobile→page
    size(多大,仅浮层) auto / sm / md / lg / xl / full page 是整页路由,无 size

    spec 清理:像素宽度作废/弃用,统一档位

    • 弃用:NavigationConfig.width(string | number 像素)、FormView.drawerWidth(像素)。渲染器保留读取作后备,但标记 deprecated、AI 不写。
    • 统一:一套档位枚举,和已有的 modalSize: sm/default/lg/xl/full 对齐(加 auto 默认、default→md)。抽屉 / 弹窗 / 详情导航共用一套词汇。
    • auto(默认,AI 零书写)= 运行时按字段数选档 + min(档位, 92vw) 夹住视口。
    • 显式档 = AI 安全的粗覆盖(意图非像素、与视口无关;modalSize 已证明粗档位既有用又不会写错)。

    列数:容器查询按实际宽度定,上限随字段数放大

    • 已是容器查询驱动(@container + @md/@2xl/@4xl:grid-cols-N),看容器实际宽度、不看视口。
    • 缺口:inferColumns 上限卡在 2。改为按字段数放大上限(→ 3 / 4),容器查询负责当前宽度实际显示几列。
    • 效果:同一表单,窄抽屉 1 列 → 宽抽屉 2-3 列 → 整页 3-4 列,拉宽缩窄自动跟。

    本轮开发

    1. objectui:inferColumns 上限按字段数放大(≤3→1 / ≤8→2 / ≤15→3 / ≥16→4);列数随容器实际宽度响应。
    2. spec:NavigationConfig.size 档位枚举(auto 默认)+ 弃用像素 width / drawerWidth。
    3. objectui:消费 size + 运行时按字段数派生宽度上限,min(档位, 92vw)。
    4. 浏览器验证:强制 drawer 的 field_zoo 拉宽/缩窄 → 列数跟着变。

    仍待定(下一轮设计,先不做)

    • 新增 / 编辑的表面与返回:详情=路由已定;新增/编辑走全屏弹窗还是路由未定(我倾向全屏弹窗:关掉即返回、无返回栈)。
    • 子表新增 / 编辑:建议永远是父详情页上的弹窗(关闭即回父页 + 子表刷新),不切路由。
  2. os-zhuang commented on Jul 4, 2026

    @os-zhuang
    ContributorAuthor

    ✅ Shipped & merged (all green)

    Delivered effect (browser-verified): field-heavy records open full-page, light ones as a drawer — both derived from field count, zero AI-authored presentation. Forms render multi-column following the real rendered width (1→2→3→4 via container queries). Verified end-to-end: field_zoo (57 fields) → full-page detail + 4-col form at 1680px / 2-col at 760px / 1-col at 430px; product (6 fields) → drawer.

    Deferred to a follow-up

    The create/edit/subtable return-flow model (full-screen modal vs route; subtable-create as a modal over the parent detail) was discussed but intentionally not implemented here — adjacent to this issue's scope. Recommend a dedicated issue.

    Closing — core scope delivered.

  3. os-zhuang commented on Jul 5, 2026

    @os-zhuang
    ContributorAuthor

    Return-flow follow-up filed: #2604 (record create/edit/subtable surface + return model).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions