From e9821a3b7fa68d2108bdfe18866b16a073e52b2b Mon Sep 17 00:00:00 2001 From: chiguyong Date: Fri, 3 Jul 2026 12:59:41 +0800 Subject: [PATCH 01/12] docs(bitable): add comparative evaluation requirements with ce-code-review P1 fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增三向对比评估需求文档(agentkit bitable vs Twenty vs 飞书),并应用 ce-code-review 产出的全部 P1 缺口修复(共 9 项): - P1-1: R8 字段类型计数对齐 16+1=17(KD6 与 R8 同步) - P1-2: 新增 R8 字段类型验收矩阵(17 行表,含 V2->V3 迁移列) - P1-3: KTD7 引用具体文件 formula/parser.py 替代裸引用 - P1-4: R-ID 命名空间冲突,加日期前缀 2026-06-29-R1..R5 - P1-5: created-time 统一为 datetime(通用类型 + 默认字段使用 datetime) - P1-6: 新增 P0 验收标准段落(R1-R5 Given/When/Then) - P1-7: 新增测试策略段落 + 测试文件映射表(R1-R5、R8、R15) - P1-8: R15 拆解为 R15a/R15b/R15c + 新增 Agent 对等评估方法段落 - P1-9: R4 补充后端扩展(group_by/conditional_formatting schema)+ agent 对等说明 同时包含 2 项 gated_auto 修复: - 组件计数 14 -> 15 - 移除文档中的全部 emoji,替换为 [OK] ce-code-review run-id: 20260703-123134-c7c2b2ea --- ...ble-comparative-evaluation-requirements.md | 377 ++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 docs/brainstorms/2026-07-03-bitable-comparative-evaluation-requirements.md diff --git a/docs/brainstorms/2026-07-03-bitable-comparative-evaluation-requirements.md b/docs/brainstorms/2026-07-03-bitable-comparative-evaluation-requirements.md new file mode 100644 index 0000000..f78d649 --- /dev/null +++ b/docs/brainstorms/2026-07-03-bitable-comparative-evaluation-requirements.md @@ -0,0 +1,377 @@ +--- +date: 2026-07-03 +topic: bitable-comparative-evaluation +--- + +# 多维表格三向对比评估与优先级建议 + +## Summary + +评估 agentkit bitable 当前实现与 Twenty、飞书多维表格在样式/功能/UX 三维的差距,按 C 先行(UX 打磨)→ A 跟进(功能广度)策略给出分优先级建议,并把 P0 差距交接给 ce-plan。Agent-as-data-author 作为贯穿差异化主线不阻塞任何一条。 + +--- + +## Problem Frame + +agentkit bitable 后端 v1 已基本齐全:`BitableFile→Table→Field/Record/View` 三层骨架、28 个 REST 端点、公式引擎(10 函数 + AST 白名单 + DAG + 异步重算)、三类采集(Excel/DB/API)全部就位。但前端产品形态残缺——`ViewType` 枚举列 5 种只渲染 grid、列头菜单"编辑"仍跳右侧抽屉、无记录详情抽屉、无分组/条件格式、视觉未达飞书/Twenty 水准。 + +既有的 2026-06-24 模块需求与 2026-06-29 UI 完善需求两份文档已规划方向,但缺乏一份**基于实际代码盘点 + 竞品对标**的差距评估来驱动下一轮优先级决策。本评估填补这个空缺:用 grounding 实证当前实现状态(非规划状态),用竞品研究锁定差距坐标,用优先级策略锚定 ce-plan 交接范围。 + +--- + +## Current Implementation Inventory + +以下为代码盘点的实证状态(已逐条验证)。 + +**后端(已就位):** + +- 容器层级:`BitableFile`(`src/agentkit/bitable/models.py:65-84`)→ `Table`(`file_id` 外键)→ `Field`/`Record`/`View`,schema V2(`src/agentkit/bitable/db.py:40`,含 `bitable_files` 表 + `file_id` 列迁移) +- REST 端点:28 个,前缀 `/api/v1/bitable`(`src/agentkit/server/routes/bitable.py`),JWT 或 `X-Internal-Token` 认证,404-before-403 所有权检查 +- 字段类型(后端 `FieldType` 枚举):`text`/`number`/`date`/`select`/`multiselect`/`attachment`/`image`/`formula`/`lookup`(9 种) +- 字段所有权:`FieldOwner.agent`/`user`,upsert 只更新 agent 列,用户列不被覆盖 +- 默认字段:5 个模板(标题 `text` / 状态 `select` / 日期 `date` / 创建人 `text` / 创建时间 `date`)——注意 `FieldType` 枚举无 `user`/`datetime` 类型,创建人与创建时间实际是 `text`/`date` +- 公式引擎:10 函数(`SUM`/`AVG`/`COUNT`/`MIN`/`MAX`/`ABS`/`ROUND`/`IF`/`LEN`/`CONCAT`),AST 节点白名单(见 `formula/parser.py`,永不 eval/exec),DAG 拓扑排序 + 环检测,异步 `RecalcWorker`(0.5s 轮询、300s reaper、600s 过期阈值) +- 跨表引用:单向 `lookup`(`lookup_target` 配 `table_id`/`field_id`/`filter_field_id`/`filter_value`) +- 采集:Excel(SSRF 守护的 URL 抓取 + openpyxl,`src/agentkit/bitable/ingestion/excel.py`)、数据库(SQLAlchemy 反射,`database.py`)、API(形状变换,`api_collector.py`) +- CLI:4 命令(list-tables / create-table / import-excel / query,`src/agentkit/cli/bitable.py`) + +**前端(部分就位):** + +- 文件层:`BitableFileListView.vue`(文件列表 + `FileCard` + `FileCreateModal`)、`BitableFileDetailView.vue`(文件详情 + 顶栏 + 左侧表列表 + 主区 grid) +- grid 视图:`BitableGrid.vue`(vxe-grid 封装,列头菜单 `ColumnHeaderMenu` 编辑/隐藏/删除,select/multiselect 编辑器 `SelectCellEditor`/`SelectDisplay`,附件/图片单元格 `AttachmentCell`/`ImageCell`) +- 字段配置:`FieldManagePanel.vue`(右侧抽屉,宽 480)+ `FieldConfigForm.vue`(8 类型:text/number/date/select/multiselect/formula/attachment/image——**UI 无 `lookup` 选项**,尽管后端支持) +- 视图切换:`ViewSwitcher.vue`(editable-card tabs,**无视图类型选择**,`handleCreateView` 硬编码 `'grid'`,`src/agentkit/server/frontend/src/views/BitableFileDetailView.vue:242`) +- 视图配置:`ViewConfigPanel.vue`(筛选 `FilterBuilder` + 排序 + 隐藏字段,**无分组、无条件格式**) +- store:`src/agentkit/server/frontend/src/stores/bitable.ts`(Pinia,含 recalc 轮询 2s 间隔) +- API 客户端:`src/agentkit/server/frontend/src/api/bitable.ts`(**无 `deleteView` 方法**,后端也无 DELETE 视图端点) + +**未就位:** + +- 视图组件:仅 `BitableGrid`,无 `KanbanView`/`GalleryView`/`GanttView`/`FormView` +- 记录详情抽屉:无(仅 grid 单元格编辑) +- 列内联字段配置:无(列头"编辑"跳右侧 `FieldManagePanel`,非内联) +- 分组、条件格式:无 +- 仪表盘、自动化、表单、甘特:无 +- 字段级/记录级权限:无(仅文件/表所有权) +- 前端采集入口 UI:无(后端三类采集已就位,前端无触发入口) + +**测试覆盖:** + +- 单元测试:`tests/unit/bitable/` 13 个文件(test_service/test_routes/test_recalc/test_models/test_ingestion_excel/test_formula_parser/test_formula_engine/test_file_crud/test_default_fields/test_db/test_cli/test_bitable_tool/test_attachment) +- e2e 测试:`src/agentkit/server/frontend/e2e/` 含 3 个 bitable spec(bitable-view / bitable-file-flow / bitable-field-ops) + +**既有规划执行进度(对照 2026-06-29 UI 完善需求):** + +- 阶段一:2026-06-29-R1 文件层 [OK]、2026-06-29-R2 默认字段 [OK](受限:无 user/datetime 类型)、2026-06-29-R3 表内字段操作 部分(列头菜单存在但编辑跳抽屉)、2026-06-29-R4 select 编辑器 [OK]、2026-06-29-R5 三层导航 [OK] +- 阶段二/三/四:未开始 + +--- + +## Three-Way Comparison + +| 维度 | agentkit bitable(当前) | Twenty | 飞书多维表格 | +|---|---|---|---| +| **样式** | | | | +| 视觉密度 | 中等,vxe-grid 默认 | 中低,Notion/Linear 风 | 中高,业务工具质感 | +| 配色体系 | 功能性,无强设计语言 | 浅色 + 蓝紫主调,克制留白 | 浅色 + 蓝紫,彩色 chip 标签活泼 | +| 字段类型图标 | 无 | 有 | 有,清晰 | +| 彩色标签 chip | select 有 `SelectDisplay`,未达飞书水准 | 有 | 有,活泼成熟 | +| 整体质感 | "薄、形态残缺" | Figma 级,开源最佳之一 | 业务工具成熟质感 | +| **功能** | | | | +| 容器层级 | `BitableFile→Table` [OK] | `Object→Record`(无文件层) | `App→Table` [OK] | +| 字段类型数 | 9(后端)/ 8(前端 UI 无 lookup) | 20+ | 25+ | +| 视图类型 | 枚举 5,实际只渲染 grid | 3(Table/Kanban/Calendar) | 5-6(grid/kanban/gallery/gantt/form/+calendar) | +| 公式 | 10 函数 + AST 白名单 + DAG + 异步重算 | 无原生(Workflow + Code Action 替代) | 丰富函数库 + LOOKUP/ROLLUP/FILTER + AI 生成 | +| 跨表关联 | 单向 lookup | Relation(多向)+ 关系上挂属性 | 单向/双向 link + Lookup + Rollup | +| 采集/导入 | [OK] Excel/DB/API 三类(agent 驱动) | 无(CRM 手动录入) | Excel 导入 + OpenAPI(无 agent) | +| 字段所有权 | [OK] agent/user 列 + upsert 保留用户列 | 无 | 无 | +| 仪表盘 | 无 | 基础 | 12+ 图表 + 关联多表 + 千人千面 + AI 解释 | +| 表单视图 | 无 | 无 | [OK](NPS/评分 + AI 搭建) | +| 自动化 | 无 | Workflows(Beta)+ Serverless + MCP | 6 触发 × 9 动作 + AI 节点 + 字段捷径 | +| 单表容量 | PG 部门级(<10 万行规划) | PG(未公开) | 百万热行 | +| **UX** | | | | +| 字段管理入口 | 列头菜单(编辑跳右侧抽屉)+ 字段管理抽屉 | Settings 后台集中 + 表头 `+` | 列头 `+`/字段配置面板/记录详情页/AI 生成 | +| 记录详情 | 无(仅 grid 单元格) | 右侧抽屉(Notes/Tasks/活动线) | 展开记录详情面板 | +| 内联编辑 | grid 单元格可编辑,字段配置非内联 | 单元格内联编辑 | 单元格内联 + 字段内联配置 | +| 命令面板 | 无 | ⌘K 全局命令 | 无(飞书系全局搜索替代) | +| 视图切换 | tabs,无类型选择 | 视图保存 + 可见性控制 | 视图切换 + 类型绑定字段自动生成 | +| 分组 | 无 | 有 | 有(多字段分组) | +| 条件格式 | 无 | 无 | 有 | +| 筛选/排序 | [OK] 有 | [OK] AND/OR + 多字段 | [OK] + 字段名搜索 | +| 协作 | 单用户 | Notes/Tasks/@mention + 邮件日历同步 | 实时 + 评论 + 机器人通知 + 飞书 IM | +| 移动端 | 响应式 web | 响应式 web(无原生 App) | 飞书 App 原生 + 移动 web | +| 权限 | 文件/表所有权 | RBAC(对象/字段/记录)+ SSO + 审计 | 高级权限(颗粒级)+ AI 配置 + 组织架构继承 | +| 默认字段 | [OK] 5 字段(但创建人=text、创建时间=date) | 无默认集 | 主索引字段(1-2 列) | +| AI 能力 | Agent-as-data-author(差异化) | AI Agents + MCP Server(Cloud) | 全家桶(搭建/问数/公式/选项/配色/解释/侧边栏) | + +--- + +## Gap Analysis + +### 样式差距 + +- **G1.** 无字段类型图标(飞书/Twenty 均有,列头视觉辨识度低) +- **G2.** 彩色标签 chip 未达飞书水准(select 选项有 `SelectDisplay` 但配色/质感粗糙) +- **G3.** 整体质感"薄"(密度/留白/配色无统一设计语言,未达 Twenty 的 Figma 级或飞书的业务成熟感) + +### 功能差距 + +- **G4.** 视图只有 grid(枚举 5 种但只渲染 1 种)——飞书 5-6 种、Twenty 3 种 +- **G5.** 字段类型 9 种(前端 UI 8 种无 lookup)——飞书 25+、Twenty 20+;关键缺:`user`/`checkbox`/`url`/`email`/`phone`/`auto-number`/`datetime`/`modified-by`/`location`/`barcode`/`rating`/`progress`/`currency`/`rich-text`/`date-range`/`json`/双向关联 +- **G6.** 默认字段"创建人"=`text`、"创建时间"=`date`——因 `FieldType` 无 `user`/`datetime`,默认字段集本身暴露字段类型缺口 +- **G7.** 公式仅 10 函数——缺 `LOOKUP`/`ROLLUP`/`FILTER`/`SORT`/日期函数/条件函数/字符串函数;飞书有丰富函数库 + AI 生成公式 +- **G8.** 跨表关联仅单向 lookup——无双向关联、无 rollup 聚合 +- **G9.** 无仪表盘——飞书 12+ 图表类型 + 关联多表 + 千人千面 +- **G10.** 无表单视图——飞书有(含 NPS/评分 + AI 搭建) +- **G11.** 无自动化触发器——飞书 6 触发 × 9 动作 + AI 节点;Twenty 有 Workflows + Serverless + MCP +- **G12.** 无甘特视图——飞书有(基于日期+状态) +- **G13.** 单表容量部门级——飞书百万热行(v3 规划列式/分区,尚未实施) + +### UX 差距 + +- **G14.** 字段配置非内联——列头"编辑"跳右侧抽屉,飞书/Twenty 支持列头直接管理 +- **G15.** 无记录详情抽屉——飞书/Twenty 均有(行点击展开详情面板) +- **G16.** 无视图类型选择——`ViewSwitcher` 无类型切换,`handleCreateView` 硬编码 grid +- **G17.** 无分组——飞书/Twenty 均有(多字段分组) +- **G18.** 无条件格式——飞书有(规则自动着色) +- **G19.** 无命令面板(⌘K)——Twenty 有全局快捷操作 +- **G20.** 协作单用户——飞书实时 + 评论 + 通知;Twenty 有 Notes/Tasks/@mention +- **G21.** 无移动端原生——飞书有原生 App +- **G22.** 权限粗粒度——仅文件/表所有权,飞书/Twenty 有字段级/记录级 + SSO + 审计 +- **G23.** 前端无采集入口 UI——后端三类采集就位但前端无触发入口(2026-06-29 阶段二未做) + +### 差异化优势(非差距,应加倍投入) + +- **D1.** Agent-as-data-author:三类 agent 驱动采集 + upsert 保留用户列——飞书/Twenty 均无 agent 原生模型 +- **D2.** 字段所有权模型:agent/user 列分离,用户列跨采集保留——独一无二 +- **D3.** 内部服务令牌:agent 用 `X-Internal-Token` 认证——agent 原生设计 +- **D4.** 公式引擎安全:AST 白名单 + DAG + 异步重算——比 Twenty 的 Workflow 变通方案更原生 + +--- + +## Key Decisions + +**KD1. 优先级策略:C 先行(UX 打磨)→ A 跟进(功能广度),B(Agent 差异化)作为贯穿主线。** 理由:后端已齐,最大感知差距是已存在的 grid 体验未达飞书/Twenty 水准(G14-G19);先打磨已有功能到竞品水准,再补广度。B 不阻塞任何一条,作为持续投入线。 + +**KD2. 飞书为主标杆,Twenty 为辅。** 飞书是更接近的纯多维表格形态(通用结构化数据载体),agentkit bitable 不是 CRM,Twenty 的 CRM 领域特性不适用。Twenty 贡献 UX 模式(⌘K 命令面板、记录详情抽屉、代码优先 `defineObject`)而非功能广度。 + +**KD3. Agent-as-data-author 是差异化强项,评估中不当差距处理。** 飞书的 AI 全家桶(搭建/问数/公式生成)与 agentkit 的 agent 采集是不同范式——飞书 AI 辅助人类建表,agentkit agent 自主建表写入。后者是 agentkit 身份主线,应加倍投入而非追赶飞书的 AI 辅助形态。 + +**KD4. 评估覆盖样式/功能/UX + 邻近能力(仪表盘/自动化/协作/移动/AI),但以 agentkit 产品身份过滤。** 飞书生态绑定(IM/文档/日历深度集成)与 Twenty 的 CRM 特性(管线/邮件同步)标记为本产品身份之外,不纳入优先级。 + +**KD5. 视觉决策用文字探讨,不开浏览器探针。** 沿用 2026-06-29 KD5,本次评估为分析交付,形状决策留待 ce-plan。 + +**KD6. P0/P1 关键范围决议(Resolve Before Planning 解决)。** +- R3 视图切换器:P0 暴露全部 5 种类型(grid/kanban/gallery/gantt/form),未实现的禁用态展示并标注"规划中",预告路线图而非隐藏。 +- R5 视觉对齐:P0 引入统一设计 token 系统(CSS 变量层:颜色/间距/圆角/字号),以 token 驱动重写列头/chip/密度等,避免后续 P1/P2 视觉不一致。 +- R8 字段类型扩展:P1 一次性补全 16 字段类型 + 1 双向关联 = 17 项,与飞书对齐全面;schema V3 迁移复杂度接受。 + +--- + +## Agent 对等评估方法 + +bitable 后端有 28 个 REST 端点,但 `BitableTool`(`src/agentkit/tools/bitable_tool.py`)仅暴露 6 个动作(`create_table`/`list_tables`/`query_records`/`create_record`/`update_record`/`delete_record`),存在系统性 agent 孤儿风险。下表为每个 R-ID 标注复用端点、需新增的 BitableTool 动作、以及 agent 孤儿风险等级。 + +| R-ID | 复用/新增端点 | BitableTool 需新动作 | Agent 孤儿风险 | +|------|--------------|---------------------|---------------| +| R1 | 复用 PATCH /fields | `update_field` | 高 — agent 无法内联改字段 | +| R2 | 复用 GET /records/{id} | (无需新动作,复用 `query_records`) | 低 | +| R3 | 复用 POST /views(需扩展 schema 接受 type) | `create_view` | 高 — agent 无法建非 grid 视图 | +| R4 | 复用 PATCH /views(需扩展 group_by/conditional_formatting) | `update_view` | 高 — agent 无法配置分组/条件格式 | +| R5 | 纯前端 token 系统 | (无需新动作) | 无 | +| R6/R7 | 复用 POST /views + PATCH /views | `create_view`/`update_view`(同 R3/R4) | 中 — 与 R3/R4 共享动作 | +| R8 | 复用 POST /fields(需扩展类型枚举) | `create_field`(可选,R1 的 `update_field` 已覆盖部分) | 中 | +| R9/R10 | 复用公式引擎(`formula/parser.py`)+ POST /fields(双向关联 schema) | (无需新动作,复用 `create_field`/`update_field`) | 低 | +| R11 | 复用 POST /views | `create_view`(同 R3) | 中 | +| R15a | 复用 DELETE /views(需新增后端端点) | `delete_view` | 高 — 后端 + agent 双侧缺口 | +| R15b | 复用 `create_table` + `create_field` + `create_view` 编排 | (无新动作,编排层) | 低 | +| R15c | 路径 (a) 新增 /collections 端点;路径 (b) 复用 BitableTool | 路径 (b) 需 `create_collection`/`update_collection` | 中 — 取决于路径决策 | + +**评估结论**:R15a 是 agent 对等的最高优先级子项,应与 R3/R4 同步推进(共享 `create_view`/`update_view` 动作)。其余 R-ID 的 agent 孤儿风险可通过 R1/R3/R4/R15a 四个新动作一并闭合。 + +--- + +## Priority Recommendations + +以下 R-ID 为分优先级的差距闭合建议,P0 交接 ce-plan。 + +### P0 — UX 打磨(C 先行,立即交接 ce-plan) + +- R1. 列内联字段配置:列头菜单直接编辑字段(重命名/改类型/选项管理),不跳右侧抽屉。闭合 G14。 +- R2. 记录详情侧边抽屉:行点击展开详情面板,含所有字段类型的可视化展示与编辑。闭合 G15。 +- R3. 视图类型切换与创建:`ViewSwitcher` 支持选 grid/kanban/gallery/gantt/form,新建视图选类型(不再硬编码 grid)。P0 暴露全部 5 种类型,未实现的以禁用态展示并标注"规划中"(预告路线图)。闭合 G16,为 P1 视图实现铺路。 +- R4. grid 视图内分组与条件格式:多字段分组 + 规则自动着色。**后端扩展**:View.config 新增 `group_by`/`conditional_formatting` schema,通过 PATCH /views 暴露。**Agent 对等**:BitableTool 的 `create_view`/`update_view` 动作需支持传入这些配置(见 Agent 对等评估方法)。闭合 G17、G18。 +- R5. 视觉风格对齐:P0 引入统一设计 token 系统(CSS 变量层:颜色/间距/圆角/字号),以 token 驱动重写列头/chip/密度等,配套字段类型图标与彩色 chip 标签达飞书水准。闭合 G1、G2、G3。 + +### P1 — 功能广度(A 跟进,下一轮 ce-plan) + +- R6. 看板视图实现:按 select 字段分列 + 拖拽卡片改值。闭合 G4(部分)。 +- R7. 画廊视图实现:以附件/图片为主视觉的卡片网格。闭合 G4(部分)。 +- R8. 字段类型扩展:P1 一次性补全 16 字段类型 + 1 双向关联 = 17 项(`user`/`checkbox`/`url`/`email`/`phone`/`auto-number`/`datetime`/`modified-by`/`location`/`barcode`/`rating`/`progress`/`currency`/`rich-text`/`date-range`/`json` + 双向关联),与飞书对齐全面,修正默认字段的 `user`/`datetime` 缺口。注:`datetime` 为通用日期时间类型,默认字段「创建时间」使用 `datetime` 类型;`modified-by` 为自动管理的用户类型。闭合 G5、G6。 +- R9. 公式库扩展:补 `LOOKUP`/`ROLLUP`/日期/条件/字符串函数,对标飞书函数库。闭合 G7。 +- R10. 跨表关联增强:双向关联 + rollup 聚合。闭合 G8。 + +### P2 — 后续轮次 + +- R11. 甘特视图 + 表单视图。闭合 G4(完整)、G10、G12。 +- R12. 自动化触发器:事件(记录新增/字段变更/定时)→ 动作(写字段/发通知/调 API)。闭合 G11。 +- R13. 仪表盘:图表组件库 + 关联多表。闭合 G9。 +- R14. 细粒度权限:字段级/记录级 + 角色自定义。闭合 G22。 + +### B 线 — Agent 差异化(贯穿,不阻塞 P0/P1) + +- R15a. BitableTool 动作补全:从 6 个动作扩展到 10 个(新增 `create_view`/`update_view`/`update_field`/`delete_view`),消除与 28 个 REST 端点的 agent 孤儿风险。强化 D1、D2,闭合 Agent 对等缺口(见 Agent 对等评估方法)。 +- R15b. 自然语言→表结构 agent 技能:agent 接收自然语言描述,调用 BitableTool 完成「建表 + 建字段 + 建视图」一站式编排,作为 agentkit 差异化主线。强化 D1、D2。 +- R15c. 定时采集 + 前端采集入口 UI:在现有 Excel/DB/API 三类采集基础上,新增定时调度能力(cron 表达式驱动)+ 前端采集入口 UI(闭合 G23)。**路径决策**:(a) 新增 REST 端点 `/api/v1/bitable/collections`(CRUD + 调度器管理);(b) 由 agent 通过 BitableTool 触发(依赖 R15a 完成)。ce-plan 阶段二选一。 + +--- + +## Acceptance Criteria (P0) + +每个 P0 R-ID 的可测试验收标准(Given/When/Then),供 ce-plan 估算测试工作量。 + +**R1. 列内联字段配置** +- Given select 字段有 3 选项,When 用户点击列头菜单,Then 重命名/改类型/选项管理均内联完成(不跳右侧抽屉) +- Given 字段名变更提交,When 后端 PATCH /fields/{id},Then 返回 200 + grid 在 1 帧内重渲染新标签 +- Given 字段类型从 text 改为 number,When 现有记录值不可转换,Then 显示警告 + 阻止提交 + +**R2. 记录详情侧边抽屉** +- Given grid 行,When 用户点击行,Then 右侧抽屉展开显示所有字段(含 attachment/formula/lookup) +- Given 抽屉打开且字段为 attachment/image,When 渲染,Then 显示缩略图/预览(非原始 URL) +- Given 抽屉打开且字段为 formula,When 渲染,Then 显示计算结果(只读,不可编辑) +- Given 抽屉中编辑 user-owned 字段并提交,When 后端 upsert,Then agent 列不被覆盖 + +**R3. 视图类型切换与创建** +- Given ViewSwitcher,When 用户点击新建视图,Then 显示 5 种类型选择(grid/kanban/gallery/gantt/form) +- Given kanban/gallery/gantt/form 未实现,When 渲染,Then 以禁用态展示 + 标注"规划中" + tooltip 说明 +- Given 新建视图选 grid,When 创建,Then 调用 POST /views 传 type=grid(不再硬编码) +- Given 禁用态类型,When 点击,Then 不触发创建 + 显示路线图提示 + +**R4. grid 视图内分组与条件格式** +- Given grid 视图,When 用户开启分组,Then 支持最多 3 字段分组(对标飞书/Twenty) +- Given 条件格式规则,When 运算符为 equals/not-equals/contains/is-empty/greater-than/less-than/between,Then 匹配单元格自动着色 +- Given 两条规则匹配同一单元格,When 冲突,Then 首条规则优先(按用户排序) +- Given 着色,When 颜色来源审计,Then 全部来自 design token 调色板(8 色预设,无硬编码 hex) + +**R5. 视觉风格对齐** +- Given 任何 bitable 组件,When 审计 CSS,Then 所有颜色来自 design token(grep 无硬编码 hex) +- Given 字段类型渲染,When 列头图标,Then 9 种(P0)类型各有 Ant Design Outlined icon(Component 类型) +- Given select 选项 chip,When 渲染,Then 对比度 ≥4.5:1(WCAG AA,axe-core 可测试) +- Given chip 配色,When 审计,Then 全部来自 design token 调色板 + +--- + +## Test Strategy + +**测试约定**:后端 pytest(asyncio_mode=auto,标记 integration/redis/postgres);前端 vitest(纯函数抽 helpers/,不用 @vue/test-utils);e2e Playwright。 + +**Test File Mapping**(R-ID → 测试文件映射): + +| R-ID | 后端单元测试 | 前端单元测试 | e2e 测试 | 集成标记 | +|---|---|---|---|---| +| R1 | tests/unit/bitable/test_routes.py(PATCH /fields/{id}) | helpers/fieldRenderUtils.ts | bitable-field-ops.spec.ts(extend) | - | +| R2 | tests/unit/bitable/test_service.py(upsert 保留 user 列) | helpers/recordDrawerUtils.ts | new: bitable-record-drawer.spec.ts | - | +| R3 | tests/unit/bitable/test_routes.py(POST /views type 参数) | helpers/viewSwitcherUtils.ts | bitable-view.spec.ts(extend) | - | +| R4 | new: test_grouping.py + test_conditional_formatting.py | helpers/groupingRulesUtils.ts | new: bitable-grouping.spec.ts | - | +| R5 | - | helpers/designTokenAudit.ts(grep token 使用) | bitable-view.spec.ts(visual regression) | - | +| R8 | test_models.py + test_default_fields.py + test_service.py | helpers/fieldTypeUtils.ts | bitable-field-ops.spec.ts(extend) | integration(schema V3 迁移) | +| R15 | test_bitable_tool.py(new actions) | - | new: bitable-agent-parity.spec.ts | redis(notify_callback) | + +**Agent 对等测试契约**:每个 P0/P1 R-ID 交付时,附 BitableTool 调用契约测试——验证 agent 能完成等价操作(人类可做的 agent 也能做)。BitableTool 当前 6 actions vs 28 REST 端点,新增端点的 R-ID 需同步补 BitableTool 动作 + 契约测试。 + +--- + +## R8 Field Type Acceptance Matrix + +R8 扩展的 16 字段类型 + 1 双向关联的逐类型验收标准。每行映射约 4-6 个单元测试(test_models.py / test_service.py / test_default_fields.py)。 + +| 类型 | 有效输入 | 无效输入(422) | 所有权 | 公式参与 | V2→V3 迁移 | +|---|---|---|---|---|---| +| `user` | user_id 字符串 | 非存在 user_id | both | 不支持 | 创建人 `text` → `user` | +| `checkbox` | `true`/`false` | 非布尔值 | both | 支持(IF 条件) | - | +| `url` | `https://example.com` | 非合法 URL | both | 不支持 | - | +| `email` | `a@b.com` | 非合法邮箱 | both | 不支持 | - | +| `phone` | `+86-...` 字符串 | 空字符串 OK | both | 不支持 | - | +| `auto-number` | 自增整数(后端分配) | 不可手动写 | agent only | 不支持 | - | +| `datetime` | ISO 8601 `2026-07-03T12:00:00Z` | 非日期格式 | both | 支持(日期函数) | 创建时间 `date` → `datetime` | +| `modified-by` | user_id(自动管理) | 不可手动写 | agent only | 不支持 | - | +| `location` | `{lat: float, lng: float}` | 非合法坐标 | both | 不支持 | - | +| `barcode` | 字符串 | 空字符串 OK | both | 不支持 | - | +| `rating` | 1-5 整数 | 超范围 | both | 支持(AVG) | - | +| `progress` | 0-100 整数 | 超范围 | both | 支持(AVG) | - | +| `currency` | `{amount: number, code: str}` | 负数 OK | both | 支持(SUM) | - | +| `rich-text` | HTML 子集(无 ` + + diff --git a/src/agentkit/server/frontend/src/components/bitable/FieldTypeIcon.vue b/src/agentkit/server/frontend/src/components/bitable/FieldTypeIcon.vue new file mode 100644 index 0000000..ec7e593 --- /dev/null +++ b/src/agentkit/server/frontend/src/components/bitable/FieldTypeIcon.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/agentkit/server/frontend/src/components/bitable/FileCard.vue b/src/agentkit/server/frontend/src/components/bitable/FileCard.vue index 88bf9d7..0eab058 100644 --- a/src/agentkit/server/frontend/src/components/bitable/FileCard.vue +++ b/src/agentkit/server/frontend/src/components/bitable/FileCard.vue @@ -52,18 +52,18 @@ function formatDate(iso: string): string { diff --git a/src/agentkit/server/frontend/src/components/bitable/ImageCell.vue b/src/agentkit/server/frontend/src/components/bitable/ImageCell.vue index 5d8e425..beb23d6 100644 --- a/src/agentkit/server/frontend/src/components/bitable/ImageCell.vue +++ b/src/agentkit/server/frontend/src/components/bitable/ImageCell.vue @@ -83,21 +83,21 @@ onUnmounted(() => { .image-cell { display: flex; flex-wrap: wrap; - gap: 4px; + gap: var(--bitable-spacing-xs); padding: 2px 0; } .image-cell__thumb { width: 40px; height: 40px; - border-radius: 4px; + border-radius: var(--bitable-radius-sm); overflow: hidden; cursor: pointer; - background: var(--bg-secondary, #f5f5f5); + background: var(--bitable-color-bg-secondary); display: flex; align-items: center; justify-content: center; - border: 1px solid var(--border-color, #f0f0f0); + border: 1px solid var(--bitable-color-border); } .image-cell__img { @@ -107,11 +107,11 @@ onUnmounted(() => { } .image-cell__placeholder { - color: var(--text-placeholder, #bfbfbf); - font-size: 16px; + color: var(--bitable-color-text-placeholder); + font-size: var(--bitable-font-lg); } .image-cell__empty { - color: var(--text-placeholder, #bfbfbf); + color: var(--bitable-color-text-placeholder); } diff --git a/src/agentkit/server/frontend/src/components/bitable/LoadingState.vue b/src/agentkit/server/frontend/src/components/bitable/LoadingState.vue new file mode 100644 index 0000000..15700cb --- /dev/null +++ b/src/agentkit/server/frontend/src/components/bitable/LoadingState.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/agentkit/server/frontend/src/components/bitable/SelectDisplay.vue b/src/agentkit/server/frontend/src/components/bitable/SelectDisplay.vue index 5b95996..9871daa 100644 --- a/src/agentkit/server/frontend/src/components/bitable/SelectDisplay.vue +++ b/src/agentkit/server/frontend/src/components/bitable/SelectDisplay.vue @@ -1,28 +1,27 @@ + + diff --git a/src/agentkit/server/frontend/src/composables/useResponsiveBreakpoint.ts b/src/agentkit/server/frontend/src/composables/useResponsiveBreakpoint.ts new file mode 100644 index 0000000..7f56a8d --- /dev/null +++ b/src/agentkit/server/frontend/src/composables/useResponsiveBreakpoint.ts @@ -0,0 +1,87 @@ +/** + * useResponsiveBreakpoint — 响应式断点 composable + * + * 断点(移动优先,与 plan U1 Open Questions 一致): + * - isMobile: viewport < 768px + * - isTablet: 768px ≤ viewport < 1024px + * - isDesktop: viewport ≥ 1024px + * - isWide: viewport ≥ 1440px(供宽屏布局可选使用) + * + * 消费点:U3 RecordDetailDrawer(isMobile 时全屏覆盖)、 + * U5 ViewConfigPanel(isMobile 时改底部抽屉)。U1 仅暴露 API。 + * + * 实现:window.matchMedia + change 事件监听,onUnmounted 清理。 + * SSR 安全:未定义 window 时返回全 false 默认值( ponytail: 不引入 + * 专门的 SSR 检测库,matchMedia 不存在即视为非移动端默认布局)。 + */ +import { ref, onMounted, onUnmounted, type Ref } from 'vue' + +export interface ResponsiveBreakpoint { + isMobile: Ref + isTablet: Ref + isDesktop: Ref + isWide: Ref +} + +const MOBILE_MAX = 767 // < 768 +const TABLET_MAX = 1023 // < 1024 +const WIDE_MIN = 1440 // ≥ 1440 + +export function useResponsiveBreakpoint(): ResponsiveBreakpoint { + const isMobile = ref(false) + const isTablet = ref(false) + const isDesktop = ref(false) + const isWide = ref(false) + + // matchMedia 不支持 change 事件的旧浏览器回退(ponytail: 不做 + // resize 节流兜底——现代浏览器均支持 matchMedia change,避免额外 + // 依赖 lodash/throttle;已知 ceiling: IE11 不可用,本项目目标现代浏览器) + const mobileMql: MediaQueryList | null = + typeof window !== 'undefined' && window.matchMedia + ? window.matchMedia(`(max-width: ${MOBILE_MAX}px)`) + : null + const tabletMql: MediaQueryList | null = + typeof window !== 'undefined' && window.matchMedia + ? window.matchMedia(`(min-width: ${MOBILE_MAX + 1}px) and (max-width: ${TABLET_MAX}px)`) + : null + const wideMql: MediaQueryList | null = + typeof window !== 'undefined' && window.matchMedia + ? window.matchMedia(`(min-width: ${WIDE_MIN}px)`) + : null + + function sync(): void { + isMobile.value = mobileMql?.matches ?? false + isTablet.value = tabletMql?.matches ?? false + isWide.value = wideMql?.matches ?? false + // desktop = 既非 mobile 也非 tablet(≥ 1024) + isDesktop.value = !isMobile.value && !isTablet.value + } + + function createHandler(): () => void { + return () => sync() + } + + const mobileHandler = createHandler() + const tabletHandler = createHandler() + const wideHandler = createHandler() + + onMounted(() => { + sync() + // addEventListener('change', ...) 是现代标准 API + mobileMql?.addEventListener('change', mobileHandler) + tabletMql?.addEventListener('change', tabletHandler) + wideMql?.addEventListener('change', wideHandler) + }) + + onUnmounted(() => { + mobileMql?.removeEventListener('change', mobileHandler) + tabletMql?.removeEventListener('change', tabletHandler) + wideMql?.removeEventListener('change', wideHandler) + }) + + return { isMobile, isTablet, isDesktop, isWide } +} + +// ponytail self-check: 断点边界互斥性。运行时调用 sync() 后, +// isMobile/isTablet/isDesktop 三者有且仅有一个为 true(基于互斥的 +// matchMedia 查询)。trivial 互斥逻辑,不另立测试文件。 diff --git a/src/agentkit/server/frontend/src/main.ts b/src/agentkit/server/frontend/src/main.ts index 36f3be5..f1ce076 100644 --- a/src/agentkit/server/frontend/src/main.ts +++ b/src/agentkit/server/frontend/src/main.ts @@ -2,6 +2,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import 'ant-design-vue/dist/reset.css' import './styles' +import './styles/bitable-tokens.css' import App from './App.vue' import router from './router' import { useAuthStore } from './stores/auth' diff --git a/src/agentkit/server/frontend/src/styles/bitable-tokens.css b/src/agentkit/server/frontend/src/styles/bitable-tokens.css new file mode 100644 index 0000000..2a50902 --- /dev/null +++ b/src/agentkit/server/frontend/src/styles/bitable-tokens.css @@ -0,0 +1,77 @@ +/** + * Bitable Design Tokens — CSS Custom Properties + * + * 用 `--bitable-*` 前缀避免与 Ant Design Vue 4 全局 token 冲突。 + * 颜色/间距/圆角/字号 4 类 token + 抽屉宽度。Phase 2 (U2-U5) 所有 + * bitable 组件必须引用本文件的 var(),禁止硬编码 hex。 + * + * 颜色 token 映射到全局 tokens.css(已支持暗色主题),bitable 组件 + * 不直接维护暗色覆写,跟随全局主题切换。 + * + * 条件格式 8 色(--bitable-cf-*):每色提供 -bg(浅底)+ -fg(深字)配对。 + * -bg 用于单元格/chip 背景(配深色文本,对比度 ~12:1) + * -fg 用于文本/边框/图标(在白底上对比度 ≥4.5:1,WCAG AA) + * 颜色语义 key: red | orange | yellow | green | blue | purple | gray | neutral + */ +:root { + /* ── 颜色:基础语义(映射全局 token,跟随暗色主题) ── */ + --bitable-color-primary: var(--color-primary, #1a1a1a); + --bitable-color-bg: var(--bg-primary, #ffffff); + --bitable-color-bg-secondary: var(--bg-secondary, #fbfbfa); + --bitable-color-bg-tertiary: var(--bg-tertiary, #f7f7f5); + --bitable-color-text: var(--text-primary, #1a1a1a); + --bitable-color-text-secondary: var(--text-secondary, #4a4a4a); + --bitable-color-text-tertiary: var(--text-tertiary, #6b6b6a); + --bitable-color-text-placeholder: var(--text-placeholder, #9b9b9a); + --bitable-color-border: var(--border-color, #ededec); + --bitable-color-border-hover: var(--border-color-hover, #dfdfde); + --bitable-color-border-split: var(--border-color-split, #f2f2f0); + + /* ── 颜色:条件格式 8 色(语义 key → bg/fg 配对) ── */ + /* red */ + --bitable-cf-red-bg: #fee2e2; + --bitable-cf-red-fg: #b91c1c; + /* orange */ + --bitable-cf-orange-bg: #ffedd5; + --bitable-cf-orange-fg: #9a3412; + /* yellow(深金以保证 AA) */ + --bitable-cf-yellow-bg: #fef9c3; + --bitable-cf-yellow-fg: #a16207; + /* green */ + --bitable-cf-green-bg: #dcfce7; + --bitable-cf-green-fg: #15803d; + /* blue */ + --bitable-cf-blue-bg: #dbeafe; + --bitable-cf-blue-fg: #1d4ed8; + /* purple */ + --bitable-cf-purple-bg: #f3e8ff; + --bitable-cf-purple-fg: #7e22ce; + /* gray */ + --bitable-cf-gray-bg: #f3f4f6; + --bitable-cf-gray-fg: #374151; + /* neutral(与 gray 区分:更纯的中性灰) */ + --bitable-cf-neutral-bg: #f5f5f4; + --bitable-cf-neutral-fg: #404040; + + /* ── 间距 ── */ + --bitable-spacing-xs: 4px; + --bitable-spacing-sm: 8px; + --bitable-spacing-md: 12px; + --bitable-spacing-lg: 16px; + --bitable-spacing-xl: 24px; + + /* ── 圆角 ── */ + --bitable-radius-sm: 4px; + --bitable-radius-md: 6px; + --bitable-radius-lg: 8px; + + /* ── 字号 ── */ + --bitable-font-xs: 12px; + --bitable-font-sm: 13px; + --bitable-font-md: 14px; + --bitable-font-lg: 16px; + + /* ── 抽屉宽度(KTD4: ≤10 字段 480px,>10 字段 640px) ── */ + --bitable-drawer-width: 480px; + --bitable-drawer-width-wide: 640px; +} diff --git a/src/agentkit/server/frontend/src/views/BitableFileDetailView.vue b/src/agentkit/server/frontend/src/views/BitableFileDetailView.vue index 6c3b845..8185e96 100644 --- a/src/agentkit/server/frontend/src/views/BitableFileDetailView.vue +++ b/src/agentkit/server/frontend/src/views/BitableFileDetailView.vue @@ -45,7 +45,7 @@
- +

请选择左侧的数据表,或点击 + 新建数据表

@@ -280,40 +280,40 @@ async function handleDeleteField(field: IBitableField): Promise { height: 100vh; width: 100vw; overflow: hidden; - background: var(--bg-primary, #fff); + background: var(--bitable-color-bg); } .bitable-file-detail-view__topbar { display: flex; align-items: center; justify-content: space-between; - padding: 8px 16px; - border-bottom: 1px solid var(--border-color, #f0f0f0); + padding: var(--bitable-spacing-sm) var(--bitable-spacing-lg); + border-bottom: 1px solid var(--bitable-color-border); flex-shrink: 0; } .bitable-file-detail-view__topbar-left { display: flex; align-items: center; - gap: 8px; + gap: var(--bitable-spacing-sm); } .bitable-file-detail-view__topbar-right { display: flex; align-items: center; - gap: 8px; + gap: var(--bitable-spacing-sm); } .bitable-file-detail-view__icon { font-size: 20px; - color: var(--color-primary, #1a1a1a); + color: var(--bitable-color-primary); display: inline-flex; align-items: center; } .bitable-file-detail-view__title { font-weight: 600; - font-size: 16px; + font-size: var(--bitable-font-lg); } .bitable-file-detail-view__body { @@ -342,28 +342,28 @@ async function handleDeleteField(field: IBitableField): Promise { align-items: center; justify-content: center; height: 100%; - gap: 16px; - color: var(--text-placeholder, #bfbfbf); + gap: var(--bitable-spacing-lg); + color: var(--bitable-color-text-placeholder); } .bitable-file-detail-view__grid-header { display: flex; align-items: baseline; - gap: 12px; - padding: 12px 16px; - border-bottom: 1px solid var(--border-color, #f0f0f0); + gap: var(--bitable-spacing-md); + padding: var(--bitable-spacing-md) var(--bitable-spacing-lg); + border-bottom: 1px solid var(--bitable-color-border); flex-shrink: 0; } .bitable-file-detail-view__table-name { margin: 0; - font-size: 16px; + font-size: var(--bitable-font-lg); font-weight: 600; } .bitable-file-detail-view__field-count { - font-size: 12px; - color: var(--text-secondary, #8c8c8c); + font-size: var(--bitable-font-xs); + color: var(--bitable-color-text-secondary); } .bitable-file-detail-view__grid-container { @@ -375,8 +375,8 @@ async function handleDeleteField(field: IBitableField): Promise { .bitable-file-detail-view__load-more { display: flex; justify-content: center; - padding: 8px; - border-top: 1px solid var(--border-color, #f0f0f0); + padding: var(--bitable-spacing-sm); + border-top: 1px solid var(--bitable-color-border); flex-shrink: 0; } diff --git a/src/agentkit/server/frontend/src/views/BitableFileListView.vue b/src/agentkit/server/frontend/src/views/BitableFileListView.vue index 55bfd10..8151fe6 100644 --- a/src/agentkit/server/frontend/src/views/BitableFileListView.vue +++ b/src/agentkit/server/frontend/src/views/BitableFileListView.vue @@ -189,16 +189,16 @@ function handleDelete(file: IBitableFile): void { height: 100vh; width: 100vw; overflow: hidden; - background: var(--bg-secondary, #fafafa); + background: var(--bitable-color-bg-secondary); } .bitable-file-list-view__topbar { display: flex; align-items: center; justify-content: space-between; - padding: 8px 16px; - background: var(--bg-primary, #fff); - border-bottom: 1px solid var(--border-color, #f0f0f0); + padding: var(--bitable-spacing-sm) var(--bitable-spacing-lg); + background: var(--bitable-color-bg); + border-bottom: 1px solid var(--bitable-color-border); flex-shrink: 0; } @@ -248,7 +248,7 @@ function handleDelete(file: IBitableFile): void { } .bitable-file-list-view__option-icon { - font-size: 16px; - color: var(--color-primary, #1a1a1a); + font-size: var(--bitable-font-lg); + color: var(--bitable-color-primary); } -- 2.43.0 From f0c993a0d9ceef3e2d02883f8fe509851a6577e6 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Fri, 3 Jul 2026 15:12:17 +0800 Subject: [PATCH 05/12] feat(bitable): U2 inline field configuration in column header menu - Add InlineFieldConfigurator.vue (inline panel reusing FieldConfigForm logic) - Add fieldRenderUtils.ts (type conversion compatibility check) - Refactor ColumnHeaderMenu: edit -> inline expand, batch -> open FieldManagePanel - Integrate InlineFieldConfigurator in BitableGrid header slot - Add batch-management banner to FieldManagePanel - Add submitting loading state to prevent duplicate clicks - Extend e2e/bitable-field-ops.spec.ts with inline edit scenarios Closes R1 (P0): column header menu inline edit, no more drawer jump. Refs: docs/plans/2026-07-03-001-feat-bitable-p0-ux-and-agent-parity-plan.md U2 --- .../frontend/e2e/bitable-field-ops.spec.ts | 140 +++++++ .../src/components/bitable/BitableGrid.vue | 56 ++- .../components/bitable/ColumnHeaderMenu.vue | 43 +- .../components/bitable/FieldManagePanel.vue | 17 +- .../bitable/InlineFieldConfigurator.vue | 388 ++++++++++++++++++ .../frontend/src/helpers/fieldRenderUtils.ts | 150 +++++++ .../server/frontend/src/stores/bitable.ts | 4 +- 7 files changed, 781 insertions(+), 17 deletions(-) create mode 100644 src/agentkit/server/frontend/src/components/bitable/InlineFieldConfigurator.vue create mode 100644 src/agentkit/server/frontend/src/helpers/fieldRenderUtils.ts diff --git a/src/agentkit/server/frontend/e2e/bitable-field-ops.spec.ts b/src/agentkit/server/frontend/e2e/bitable-field-ops.spec.ts index 533b57a..5333f4c 100644 --- a/src/agentkit/server/frontend/e2e/bitable-field-ops.spec.ts +++ b/src/agentkit/server/frontend/e2e/bitable-field-ops.spec.ts @@ -145,4 +145,144 @@ test.describe('Bitable Field Operations E2E', () => { // the select editor renders when editing a select-type cell await page.waitForTimeout(500) }) + + // ── U2: inline field configuration in column header menu ────────────── + + test('C6: edit menu opens InlineFieldConfigurator inline (no drawer)', async ({ page }) => { + await setupBitableWithTable(page, 'E2E内联编辑', '测试表') + + // Open the first column header dropdown + const headerMenu = page.locator('.column-header-menu').first() + await headerMenu.click() + + // Click "编辑字段" — should open the inline configurator, NOT the drawer + await page.getByText('编辑字段').click() + + // Inline configurator renders inside a popover (role=dialog) + await expect(page.locator('.inline-field-configurator')).toBeVisible({ + timeout: 5_000, + }) + + // The right-side drawer must NOT have opened + await expect(page.locator('.ant-drawer-content')).toHaveCount(0) + }) + + test('C7: rename field via inline config updates the grid label', async ({ page }) => { + await setupBitableWithTable(page, 'E2E重命名', '测试表') + + const headerMenu = page.locator('.column-header-menu').first() + await headerMenu.click() + await page.getByText('编辑字段').click() + + // The first input in the inline configurator is the field name + const nameInput = page.locator('.inline-field-configurator input').first() + await expect(nameInput).toBeVisible({ timeout: 5_000 }) + await nameInput.fill('') + await nameInput.fill('重命名后字段') + + // Save + await page.locator('.inline-field-configurator').getByRole('button', { name: '保存' }).click() + + // The grid header should now show the new label + await expect( + page.locator('.column-header-menu__title', { hasText: '重命名后字段' }), + ).toBeVisible({ timeout: 10_000 }) + }) + + test('C8: incompatible type change (text -> number) blocks submit', async ({ page }) => { + await setupBitableWithTable(page, 'E2E类型转换', '测试表') + + // Pick a text column with non-numeric values (the default "名称" field). + const headerMenu = page + .locator('.column-header-menu') + .filter({ hasText: '名称' }) + .first() + await headerMenu.click() + await page.getByText('编辑字段').click() + + // Switch type to number + await page.locator('.inline-field-configurator .ant-select').first().click() + await page.getByRole('option', { name: '数字' }).click() + + // Compatibility warning must appear + await expect( + page.locator('.inline-field-configurator__warning, .inline-field-configurator .ant-alert-warning'), + ).toBeVisible({ timeout: 5_000 }) + + // Save button must be disabled + const saveBtn = page + .locator('.inline-field-configurator') + .getByRole('button', { name: '保存' }) + await expect(saveBtn).toBeDisabled() + }) + + test('C9: select option management inline updates chips after save', async ({ page }) => { + await setupBitableWithTable(page, 'E2E选项管理', '测试表') + + // The "状态" field is a select field. Open its inline editor. + const statusHeader = page + .locator('.column-header-menu') + .filter({ hasText: '状态' }) + .first() + await statusHeader.click() + await page.getByText('编辑字段').click() + + // Add a new option + await page + .locator('.inline-field-configurator') + .getByRole('button', { name: /添加选项/ }) + .click() + const optionInputs = page.locator('.inline-field-configurator__option-row input') + const newOption = optionInputs.last() + await newOption.fill('新选项值') + + // Save + await page.locator('.inline-field-configurator').getByRole('button', { name: '保存' }).click() + + // Inline configurator closes after save + await expect(page.locator('.inline-field-configurator')).toHaveCount(0, { timeout: 10_000 }) + }) + + test('C10: keyboard nav — Tab to header, Enter opens inline editor', async ({ page }) => { + await setupBitableWithTable(page, 'E2E键盘导航', '测试表') + + // Tab until focus reaches the first column header menu (role=button) + for (let i = 0; i < 30; i++) { + await page.keyboard.press('Tab') + const focused = await page.locator(':focus').evaluate((el) => ({ + cls: el.className, + tag: el.tagName, + })) + if (focused.cls.includes('column-header-menu')) break + } + + // Enter opens the dropdown menu + await page.keyboard.press('Enter') + await expect(page.getByText('编辑字段')).toBeVisible({ timeout: 5_000 }) + + // Activate "编辑字段" via keyboard (a-menu supports arrow + Enter) + await page.keyboard.press('Enter') + await expect(page.locator('.inline-field-configurator')).toBeVisible({ timeout: 5_000 }) + + // Focus should be inside the inline configurator's first field + await expect(page.locator('.inline-field-configurator input').first()).toBeFocused() + + // Esc closes the inline configurator + await page.keyboard.press('Escape') + await expect(page.locator('.inline-field-configurator')).toHaveCount(0, { timeout: 5_000 }) + }) + + test('C11: batch management entry still opens FieldManagePanel', async ({ page }) => { + await setupBitableWithTable(page, 'E2E批量管理', '测试表') + + const headerMenu = page.locator('.column-header-menu').first() + await headerMenu.click() + + // Click "批量管理" — should open the right-side drawer + await page.getByText('批量管理').click() + await expect(page.locator('.ant-drawer-content')).toBeVisible({ timeout: 5_000 }) + + // The batch-management hint banner must be present + await expect(page.locator('.field-manage-panel__hint')).toBeVisible() + }) }) diff --git a/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue b/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue index c72fb85..5a72d52 100644 --- a/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue +++ b/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue @@ -36,18 +36,35 @@ :images="(row[f.id] as IAttachmentMeta[] | null | undefined)" /> - + @@ -76,7 +98,14 @@ import { } from 'ant-design-vue' import type { IBitableField, IBitableView } from '@/api/bitable' import { useBitableStore } from '@/stores/bitable' +import { useResponsiveBreakpoint } from '@/composables/useResponsiveBreakpoint' import FilterBuilder from './FilterBuilder.vue' +import GroupingEditor from './GroupingEditor.vue' +import ConditionalFormatEditor from './ConditionalFormatEditor.vue' +import type { + GroupByItem, + ConditionalFormatRule, +} from '@/helpers/groupingRulesUtils' const props = defineProps<{ open: boolean @@ -90,6 +119,14 @@ const emit = defineEmits<{ const store = useBitableStore() +// U1 step 7: ViewConfigPanel becomes a bottom drawer on mobile. +// ponytail: only the placement + width differ; the rest of the UI is shared. +// Ceiling: the drawer height on mobile is unbounded — long rule lists may +// scroll past the fold. Acceptable for v1; upgrade path: a max-height + scroll. +const { isMobile } = useResponsiveBreakpoint() +const drawerPlacement = computed(() => (isMobile.value ? 'bottom' : 'right')) +const drawerWidth = computed(() => (isMobile.value ? '100%' : 520)) + const activeTab = ref('filter') const filterRef = ref | null>(null) @@ -110,13 +147,31 @@ const hiddenFieldIds = ref( (props.view?.config?.hidden_fields as string[]) ?? [], ) -// Reset when view changes +// U5: group_by + conditional_formatting local working copies. +const groupByItems = ref(extractGroupBy(props.view)) +const cfRules = ref(extractConditionalFormat(props.view)) + +function extractGroupBy(view: IBitableView | null): GroupByItem[] { + const raw = view?.config?.group_by + if (!Array.isArray(raw)) return [] + return raw as GroupByItem[] +} + +function extractConditionalFormat(view: IBitableView | null): ConditionalFormatRule[] { + const raw = view?.config?.conditional_formatting + if (!Array.isArray(raw)) return [] + return raw as ConditionalFormatRule[] +} + +// Reset when view changes (covers initial load + view-switch reloads) watch( () => props.view?.id, () => { sortFieldId.value = (props.view?.config?.sort as { field?: string })?.field ?? '' sortOrder.value = (props.view?.config?.sort as { order?: string })?.order ?? 'asc' hiddenFieldIds.value = (props.view?.config?.hidden_fields as string[]) ?? [] + groupByItems.value = extractGroupBy(props.view) + cfRules.value = extractConditionalFormat(props.view) }, ) @@ -151,6 +206,25 @@ async function saveHidden(): Promise { } await store.updateView(props.view.id, { config }) } + +// U5: save group_by + conditional_formatting through the new +// updateViewConfig action (which calls the same PATCH /views endpoint; +// the route layer validates the U5 sub-keys and 422s on invalid input). +async function saveGrouping(): Promise { + if (!props.view) return + await store.updateViewConfig(props.view.id, { + group_by: groupByItems.value, + conditional_formatting: cfRules.value, + }) +} + +async function saveConditionalFormat(): Promise { + if (!props.view) return + await store.updateViewConfig(props.view.id, { + group_by: groupByItems.value, + conditional_formatting: cfRules.value, + }) +}