From 535b11bc7e65253d3ecebbb7bc0fcd2a98bc6311 Mon Sep 17 00:00:00 2001 From: zflyluo Date: Wed, 13 May 2026 14:28:24 +0800 Subject: [PATCH 01/10] feat(harness): add harness tooling and issue workflow scripts --- .gitignore | 4 + .harness/README.md | 76 ++++ .harness/cursor/AGENTS.md | 9 + .harness/cursor/agents/acceptance-writer.md | 19 + .harness/cursor/agents/docs-reviewer.md | 16 + .../cursor/agents/flutter-issue-reviewer.md | 18 + .harness/cursor/agents/flutter-verifier.md | 16 + .harness/cursor/agents/issue-analyst.md | 17 + .harness/cursor/rules/core/core-project.mdc | 12 + .../cursor/rules/core/issue-fix-process.mdc | 12 + .../core/requirements-acceptance-docs.mdc | 12 + .../rules/flutter/flutter-issue-review.mdc | 13 + .../cursor/rules/flutter/flutter-package.mdc | 13 + .harness/cursor/rules/site/site-docs.mdc | 13 + .../cursor/skills/component-workflow/SKILL.md | 27 ++ .harness/cursor/skills/doc-sync/SKILL.md | 25 ++ .../cursor/skills/issue-fix-entry/SKILL.md | 95 +++++ .../cursor/skills/issue-fix-workflow/SKILL.md | 57 +++ .../skills/issue-fix-workflow/reference.md | 40 ++ .../cursor/skills/release-checklist/SKILL.md | 24 ++ .../templates/issue-fix/TaskContract.md.tpl | 36 ++ .../issue-fix/acceptance-report.md.tpl | 33 ++ .../issue-fix/code-review-report.md.tpl | 39 ++ .harness/templates/issue-fix/pr-body.md.tpl | 25 ++ .../templates/issue-fix/test-cases.md.tpl | 19 + scripts/init-cursor-harness.mjs | 354 ++++++++++++++++++ scripts/issue-workflow/check-issue-fix.mjs | 234 ++++++++++++ scripts/issue-workflow/init-issue-fix.mjs | 165 ++++++++ 28 files changed, 1423 insertions(+) create mode 100644 .harness/README.md create mode 100644 .harness/cursor/AGENTS.md create mode 100644 .harness/cursor/agents/acceptance-writer.md create mode 100644 .harness/cursor/agents/docs-reviewer.md create mode 100644 .harness/cursor/agents/flutter-issue-reviewer.md create mode 100644 .harness/cursor/agents/flutter-verifier.md create mode 100644 .harness/cursor/agents/issue-analyst.md create mode 100644 .harness/cursor/rules/core/core-project.mdc create mode 100644 .harness/cursor/rules/core/issue-fix-process.mdc create mode 100644 .harness/cursor/rules/core/requirements-acceptance-docs.mdc create mode 100644 .harness/cursor/rules/flutter/flutter-issue-review.mdc create mode 100644 .harness/cursor/rules/flutter/flutter-package.mdc create mode 100644 .harness/cursor/rules/site/site-docs.mdc create mode 100644 .harness/cursor/skills/component-workflow/SKILL.md create mode 100644 .harness/cursor/skills/doc-sync/SKILL.md create mode 100644 .harness/cursor/skills/issue-fix-entry/SKILL.md create mode 100644 .harness/cursor/skills/issue-fix-workflow/SKILL.md create mode 100644 .harness/cursor/skills/issue-fix-workflow/reference.md create mode 100644 .harness/cursor/skills/release-checklist/SKILL.md create mode 100644 .harness/templates/issue-fix/TaskContract.md.tpl create mode 100644 .harness/templates/issue-fix/acceptance-report.md.tpl create mode 100644 .harness/templates/issue-fix/code-review-report.md.tpl create mode 100644 .harness/templates/issue-fix/pr-body.md.tpl create mode 100644 .harness/templates/issue-fix/test-cases.md.tpl create mode 100644 scripts/init-cursor-harness.mjs create mode 100644 scripts/issue-workflow/check-issue-fix.mjs create mode 100644 scripts/issue-workflow/init-issue-fix.mjs diff --git a/.gitignore b/.gitignore index 17a2e1078..50409679d 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,7 @@ build/ /tdesign-component/example/pubspec.lock /tdesign-site/package-lock.json + +# Cursor harness generated artifacts +/.cursor/ +/AGENTS.md diff --git a/.harness/README.md b/.harness/README.md new file mode 100644 index 000000000..372337a7e --- /dev/null +++ b/.harness/README.md @@ -0,0 +1,76 @@ +# Cursor Harness 说明 + +这个目录是当前仓库 Cursor 项目级规则、技能和子代理配置的唯一维护源。 + +## 目录结构 + +```text +.harness/ + cursor/ + AGENTS.md + rules/ + skills/ + agents/ + templates/ + issue-fix/ +``` + +## 映射关系 + +- `.harness/cursor/rules/**` -> `.cursor/rules/**` +- `.harness/cursor/skills/**` -> `.cursor/skills/**` +- `.harness/cursor/agents/**` -> `.cursor/agents/**` +- `.harness/cursor/AGENTS.md` -> `AGENTS.md` + +生成产物由 `scripts/init-cursor-harness.mjs` 统一管理。 +不要直接修改 `.cursor/**` 或仓库根目录下的 `AGENTS.md`;请在这里更新源文件后重新执行初始化脚本。 + +## 常用命令 + +```bash +node scripts/init-cursor-harness.mjs +node scripts/init-cursor-harness.mjs --check +node scripts/init-cursor-harness.mjs --clean +node scripts/init-cursor-harness.mjs --force +node scripts/issue-workflow/init-issue-fix.mjs --help +node scripts/issue-workflow/check-issue-fix.mjs --help +``` + +## 维护方式 + +1. 在 `.harness/cursor/` 下新增或修改 rule、skill、subagent,或更新 `AGENTS.md`。 +2. 执行 `node scripts/init-cursor-harness.mjs`。 +3. 等生成后的 `.cursor` 文件刷新完成后,再在仓库中使用 Cursor。 + +## Issue Workflow + +这套 harness 额外沉淀了一条面向 issue 修复的标准流程,覆盖: + +1. 读取 issue 与贡献指南 +2. 创建分支 +3. 分析根因与修复方案 +4. 编写测试与必要检查 +5. 按规范实现代码 +6. 生成 `requirements/` 下的验收文档 +7. 执行强制检查 +8. 整理 PR + +相关资产分布: + +- `.harness/cursor/skills/issue-fix-entry/`:**一键入口** skill(从 issue 链接到 init、检查、PR 的最短路径) +- `.harness/cursor/skills/issue-fix-workflow/`:主流程 skill(详细步骤与注意事项) +- `.harness/cursor/rules/`:issue 修复与 Flutter 代码规范规则 +- `.harness/cursor/agents/`:问题分析、Review、验收文档编写等子代理 +- `.harness/templates/issue-fix/`:`requirements/` 文档模板 +- `scripts/issue-workflow/`:初始化模板与强制检查脚本 + +示例参考: + +- `requirements/issue-924-fab-on-long-press/` + +## 说明 + +- 同步脚本会写入 `.cursor/.harness-manifest.json`,用于记录受管理的生成文件。 +- 默认只会覆盖或清理 manifest 管理的文件。 +- 仓库文档同步仍然沿用现有的 `scripts/sync-readme.mjs` 流程。 +- issue workflow 的脚本面向“给定 issue 链接后由 AI 协助执行”的场景,强制检查只负责能被机器稳定判定的部分,仍需结合 code review 清单做人工复核。 diff --git a/.harness/cursor/AGENTS.md b/.harness/cursor/AGENTS.md new file mode 100644 index 000000000..0d33d78d9 --- /dev/null +++ b/.harness/cursor/AGENTS.md @@ -0,0 +1,9 @@ +# TDesign Flutter 工作区 + +- 仓库级 Cursor 资产统一维护在 `.harness/cursor/` 下。修改后请通过 `node scripts/init-cursor-harness.mjs` 重新生成 `.cursor/**` 和当前文件。 +- 请把改动控制在正确的目录范围内:`tdesign-component/` 负责 Flutter 组件库与示例,`tdesign-site/` 负责文档站点,`tdesign-adaptation/` 负责兼容适配相关内容。 +- 遵循现有仓库自动化边界。仓库级 Cursor 初始化逻辑放在 `scripts/` 下,Flutter 环境初始化仍放在 `tdesign-component/init.sh` 中。 +- 修改仓库根目录的引导文档时,请将 `README.md` 与 `README_zh_CN.md` 视为源文件,并使用 `node scripts/sync-readme.mjs` 进行下游同步。 +- 当用户提供 GitHub issue 链接并要求修复时,优先使用 skill **`issue-fix-entry`** 作为一键入口,再按 **`issue-fix-workflow`** 展开细节;并配合 issue workflow 相关 rules、agents、模板与 `scripts/issue-workflow/` 下的辅助脚本。 +- issue 修复的分析、测试计划、验收报告与 PR 摘要应优先沉淀到 `requirements/issue-*/` 目录中,便于人类验收与后续追踪。 +- 优先保持小而聚焦的改动,遵循既有约定,并对变更影响范围执行针对性验证。 diff --git a/.harness/cursor/agents/acceptance-writer.md b/.harness/cursor/agents/acceptance-writer.md new file mode 100644 index 000000000..518f5f992 --- /dev/null +++ b/.harness/cursor/agents/acceptance-writer.md @@ -0,0 +1,19 @@ +--- +name: acceptance-writer +description: 按 issue workflow 模板编写 requirements 目录下的任务契约、测试用例、代码审查报告、验收报告和 PR 摘要。适用于 issue 修复完成后的文档沉淀与人工验收准备。 +readonly: true +--- + +请根据当前 issue 内容、代码改动和验证结果,按模板补齐以下文档: + +1. `TaskContract.md` +2. `test-cases.md` +3. `code-review-report.md` +4. `acceptance-report.md` +5. `pr-body.md` + +要求: + +- 文档统一归档在 `requirements/issue-*/` +- 内容面向人类验收,强调问题、方案、检查结果和人工验收步骤 +- 若存在环境阻塞或未执行项,必须明确写出 diff --git a/.harness/cursor/agents/docs-reviewer.md b/.harness/cursor/agents/docs-reviewer.md new file mode 100644 index 000000000..54a3353be --- /dev/null +++ b/.harness/cursor/agents/docs-reviewer.md @@ -0,0 +1,16 @@ +--- +name: docs-reviewer +description: 审查 README 与文档改动在根文档、组件库文档和站点之间是否一致。适用于 onboarding 文档、README 同步和纯文档审查场景。 +readonly: true +--- + +请从一致性和用户可读性角度审查当前文档改动。 + +重点检查: + +1. 根 README 源文件与下游生成文档之间是否存在漂移 +2. 是否遗漏了 `scripts/sync-readme.mjs` 相关同步步骤 +3. 是否存在术语不一致或过时命令 +4. 纯文档改动是否缺少必要的验证说明 + +请先输出具体问题,再列出假设前提或尚未验证的区域。 diff --git a/.harness/cursor/agents/flutter-issue-reviewer.md b/.harness/cursor/agents/flutter-issue-reviewer.md new file mode 100644 index 000000000..7168e3ff1 --- /dev/null +++ b/.harness/cursor/agents/flutter-issue-reviewer.md @@ -0,0 +1,18 @@ +--- +name: flutter-issue-reviewer +description: 审查 Flutter issue 修复代码是否符合 TDesign Flutter 贡献规范。重点检查构造方法顺序、注释风格、all_build 配置、TTheme 使用和 TResourceDelegate 使用。 +readonly: true +--- + +请对当前 Flutter 相关改动做 issue 修复视角的 Review。 + +重点检查: + +1. 类声明后是否先写构造方法,字段是否在构造方法下方 +2. API 注释是否统一使用 `///` +3. 若新增组件类或 API 入口,`tdesign-component/demo_tool/all_build.sh` 是否已配置 +4. 组件内部颜色、圆角、阴影、字体等样式是否通过 `TTheme.of(context)` 获取 +5. 组件内部固定文案是否通过 `TResourceDelegate` 管理 +6. 是否满足贡献指南 `5.2` 与 `5.3` 的自检要求 + +请先输出 findings,再补充未验证项和残余风险。 diff --git a/.harness/cursor/agents/flutter-verifier.md b/.harness/cursor/agents/flutter-verifier.md new file mode 100644 index 000000000..9fa773775 --- /dev/null +++ b/.harness/cursor/agents/flutter-verifier.md @@ -0,0 +1,16 @@ +--- +name: flutter-verifier +description: 校验 `tdesign-component/` 下的 Flutter 改动,包括组件行为、示例、主题与资源相关问题,以及针对性的验证建议。 +readonly: true +--- + +请从 Flutter 组件库视角审查当前改动。 + +重点检查: + +1. `tdesign-component/lib/` 中是否存在公开 API 漂移 +2. 受影响组件是否遗漏了 example 或 demo 更新 +3. 是否引入了主题 token 或 `TResourceDelegate` 相关回归 +4. 当前改动区域最小且相关的验证命令是什么 + +请先按严重程度输出发现的问题,再补充剩余风险或缺失的验证项。 diff --git a/.harness/cursor/agents/issue-analyst.md b/.harness/cursor/agents/issue-analyst.md new file mode 100644 index 000000000..d9390e3fe --- /dev/null +++ b/.harness/cursor/agents/issue-analyst.md @@ -0,0 +1,17 @@ +--- +name: issue-analyst +description: 分析 Tencent/tdesign-flutter 的 GitHub issue,输出根因、修复方案、测试计划、分支命名建议和 requirements 目录命名建议。适用于用户给出 issue 链接或要求修复 issue 时。 +readonly: true +--- + +请先阅读 issue 内容,再结合 `CONTRIBUTING.md` 与贡献指南章节 `4`、`5.2`、`5.3` 做分析。 + +输出格式: + +1. 问题描述 +2. 根因分析 +3. 修复方案 +4. 测试计划 +5. 风险点 +6. 建议分支名 +7. 建议 requirements 目录名 diff --git a/.harness/cursor/rules/core/core-project.mdc b/.harness/cursor/rules/core/core-project.mdc new file mode 100644 index 000000000..dba60b48f --- /dev/null +++ b/.harness/cursor/rules/core/core-project.mdc @@ -0,0 +1,12 @@ +--- +description: TDesign Flutter 仓库级核心协作规则 +alwaysApply: true +--- + +# TDesign Flutter 核心协作规则 + +- 将 `.harness/cursor/**` 视为仓库级 Cursor 资产的唯一维护源。 +- 不要直接修改生成后的 `.cursor/**` 或根目录 `AGENTS.md`;应更新 `.harness/cursor/**` 后重新执行 `node scripts/init-cursor-harness.mjs`。 +- 改动范围应控制在对应工作区内:`tdesign-component/`、`tdesign-site/` 或 `tdesign-adaptation/`。 +- 保持现有自动化边界,不要把仓库级初始化逻辑塞进 `tdesign-component/init.sh` 这类包级脚本中。 +- 优先提交最小必要改动;除非任务明确要求重新生成,否则避免改动生成物或派生产物。 diff --git a/.harness/cursor/rules/core/issue-fix-process.mdc b/.harness/cursor/rules/core/issue-fix-process.mdc new file mode 100644 index 000000000..47bbf0f99 --- /dev/null +++ b/.harness/cursor/rules/core/issue-fix-process.mdc @@ -0,0 +1,12 @@ +--- +description: 用户提供 issue 链接时的标准修复流程 +alwaysApply: true +--- + +# Issue 修复流程 + +- 当用户提供 GitHub issue 链接、issue 编号,或明确要求“修复 issue”时,优先使用 issue workflow 相关 skill、agents、模板与脚本。 +- 开始编码前,先读取 `CONTRIBUTING.md`,并对照 https://tdesign.tencent.com/flutter/develop 的 `4. 开发规范`、`5.2 代码 Review 自检`、`5.3 文档自检`。 +- 先给出问题原因、修复方案和测试计划,再开始改代码。 +- 修复流程必须覆盖:创建分支、编写用例或必要检查、实现代码、Review、自检、生成 `requirements/issue-*/` 验收文档、提交 PR。 +- 结束前必须运行 `scripts/issue-workflow/check-issue-fix.mjs` 对可自动判定的必做项做强制检查。 diff --git a/.harness/cursor/rules/core/requirements-acceptance-docs.mdc b/.harness/cursor/rules/core/requirements-acceptance-docs.mdc new file mode 100644 index 000000000..c264a8b4d --- /dev/null +++ b/.harness/cursor/rules/core/requirements-acceptance-docs.mdc @@ -0,0 +1,12 @@ +--- +description: issue 修复验收文档在 requirements 目录下的归档规范 +globs: requirements/**/*.md +alwaysApply: false +--- + +# 验收文档归档规范 + +- issue 修复相关文档统一归档到 `requirements/issue--/`。 +- 最少包含 `TaskContract.md`、`test-cases.md`、`code-review-report.md`、`acceptance-report.md`、`pr-body.md`。 +- `acceptance-report.md` 必须覆盖:验收结论、需求对照、执行检查、人工验收指引、阻塞项或环境说明。 +- PR 摘要优先复用 `requirements/issue-*/pr-body.md`,确保 issue 链接、变更摘要和测试计划完整。 diff --git a/.harness/cursor/rules/flutter/flutter-issue-review.mdc b/.harness/cursor/rules/flutter/flutter-issue-review.mdc new file mode 100644 index 000000000..a93480150 --- /dev/null +++ b/.harness/cursor/rules/flutter/flutter-issue-review.mdc @@ -0,0 +1,13 @@ +--- +description: Flutter issue 修复时必须执行的代码规范与 Review 检查 +globs: tdesign-component/lib/src/components/**/*.dart +alwaysApply: false +--- + +# Flutter Issue Review 检查项 + +- 类声明后应先写构造方法,再写字段;不要把字段放在类声明与构造方法之间。 +- 组件 API 注释使用 `///`,不要在公开类、字段、枚举说明上使用 `//`。 +- 如果新增了组件类或需要新增 API 生成入口,检查 `tdesign-component/demo_tool/all_build.sh` 是否已配置。 +- 组件内部颜色、圆角、阴影、字体等样式优先从 `TTheme.of(context)` 获取,不要硬编码。 +- 组件内部固定文案应统一走 `TResourceDelegate`,不要直接硬编码到组件实现里。 diff --git a/.harness/cursor/rules/flutter/flutter-package.mdc b/.harness/cursor/rules/flutter/flutter-package.mdc new file mode 100644 index 000000000..f132c4d26 --- /dev/null +++ b/.harness/cursor/rules/flutter/flutter-package.mdc @@ -0,0 +1,13 @@ +--- +description: TDesign Flutter 组件库的 Flutter 开发约束 +globs: tdesign-component/**/* +alwaysApply: false +--- + +# Flutter 组件库协作规则 + +- 遵循仓库贡献规范:组件命名使用 `TD` 前缀,固定样式值应沉淀到主题定义中,固定文案应统一放入 `TResourceDelegate`。 +- 除非任务明确要求引入破坏性变更,否则应扩展原生 Flutter 组件能力,而不是删减系统组件既有行为。 +- 当组件行为或公开 API 发生变化时,要同步检查相关 demo 与 example,用需要时一并更新。 +- 遵循 `tdesign-component/init.sh` 与 `tdesign-component/example/shell/flutter_versions/` 下的版本适配逻辑;不要手工修改本地初始化过程中生成的 override 或 lock 文件。 +- 优先做针对性验证,例如聚焦的 `flutter analyze`、相关测试,或最小范围的示例运行。 diff --git a/.harness/cursor/rules/site/site-docs.mdc b/.harness/cursor/rules/site/site-docs.mdc new file mode 100644 index 000000000..5a8b93bff --- /dev/null +++ b/.harness/cursor/rules/site/site-docs.mdc @@ -0,0 +1,13 @@ +--- +description: tdesign-site 文档内容与构建脚本约束 +globs: tdesign-site/**/* +alwaysApply: false +--- + +# 站点文档协作规则 + +- `tdesign-site/` 下的改动要和现有站点结构、包脚本以及 `site/site.config.mjs` 的路由方式保持一致。 +- 修改 onboarding 或 getting started 相关内容时,请记住根目录 `README.md` 与 `README_zh_CN.md` 才是源文件,下游同步由 `scripts/sync-readme.mjs` 负责。 +- 对于 `tdesign-site/src/**/README.md` 下的组件文档页面,保持现有页面结构,并确保示例内容与组件真实行为一致。 +- 仓库级自动化放在根目录 `scripts/` 下,站点专属自动化放在 `tdesign-site/script/` 下。 +- 文档变更优先执行最小必要验证,例如对应的同步步骤或受影响范围内的站点构建命令。 diff --git a/.harness/cursor/skills/component-workflow/SKILL.md b/.harness/cursor/skills/component-workflow/SKILL.md new file mode 100644 index 000000000..12bd3f98a --- /dev/null +++ b/.harness/cursor/skills/component-workflow/SKILL.md @@ -0,0 +1,27 @@ +--- +name: component-workflow +description: 指导 TDesign Flutter 组件、示例、demo 和相关 API 的改动。适用于处理 `tdesign-component/` 下内容、修改组件行为,或更新 Flutter 组件库示例时。 +--- + +# 组件开发流程 + +## 关注范围 + +只在必要的最小文件集合内开展改动: + +1. `tdesign-component/lib/` 中的组件实现 +2. `tdesign-component/demo_tool/` 中的 demo 或 API 配置 +3. `tdesign-component/example/` 中的示例页面 +4. 当公开 API 或行为变化时需要同步的相关文档 + +## 评审检查项 + +- 固定样式 token 应沉淀到主题数据中,而不是以内联值散落在代码里。 +- 当新增或修改用户可见文案时,应通过 `TResourceDelegate` 统一管理。 +- 没有充分理由时,不要删减系统组件能力,应优先基于原生 Flutter 行为做扩展。 +- 如果改动影响公开组件契约,要同步检查该组件对应的 example 和 demo。 + +## 验证建议 + +- 优先针对受影响的组件区域做定向验证。 +- 除非改动是跨模块的,否则应优先使用聚焦的 `flutter analyze`、相关测试或最小示例运行,而不是做过宽的全仓校验。 diff --git a/.harness/cursor/skills/doc-sync/SKILL.md b/.harness/cursor/skills/doc-sync/SKILL.md new file mode 100644 index 000000000..bb2847173 --- /dev/null +++ b/.harness/cursor/skills/doc-sync/SKILL.md @@ -0,0 +1,25 @@ +--- +name: doc-sync +description: 保持仓库 README 源文件与下游文档同步。适用于编辑 `README.md`、`README_zh_CN.md`、`tdesign-component/README*`、`tdesign-site/site/docs/getting-started.md` 或相关引导文档时。 +--- + +# 文档同步流程 + +## 源文件定义 + +- 将根目录 `README.md` 与 `README_zh_CN.md` 视为唯一源文件。 +- `scripts/sync-readme.mjs` 会把根目录 README 同步到以下位置: + - `tdesign-component/README.md` + - `tdesign-component/README_zh_CN.md` + - `tdesign-site/site/docs/getting-started.md` + +## 操作流程 + +1. 优先修改根目录 README 源文件。 +2. 执行 `node scripts/sync-readme.mjs`。 +3. 检查被同步更新的下游文档。 + +## 适用边界 + +- 如果任务只涉及 `tdesign-site/src/**/README.md` 下的组件专属文档,则不需要触发根 README 同步。 +- 更新共享 onboarding 内容时,优先使用根目录 `scripts/sync-readme.mjs`,而不是依赖旧的站点单点复制脚本。 diff --git a/.harness/cursor/skills/issue-fix-entry/SKILL.md b/.harness/cursor/skills/issue-fix-entry/SKILL.md new file mode 100644 index 000000000..88935d659 --- /dev/null +++ b/.harness/cursor/skills/issue-fix-entry/SKILL.md @@ -0,0 +1,95 @@ +--- +name: issue-fix-entry +description: 一键启动 Tencent/tdesign-flutter 的 GitHub issue 修复流程。适用于用户提供 issue 链接或编号并说「按 harness 修 issue」「一键 issue」「开始修 issue」等场景;串联贡献指南、requirements 模板初始化、强制检查与 PR 收尾。 +--- + +# Issue 修复一键入口 + +收到 issue 链接或编号后,**不要先写代码**。按下面顺序执行;详细步骤与规范说明见同目录下的 [issue-fix-workflow/SKILL.md](../issue-fix-workflow/SKILL.md)。 + +## 0. 前置检查(必做) + +- 若工作区有与当前 issue **无关**的未提交改动,先向用户确认是暂存、另分支还是继续。 +- 确认可执行:`gh`、`git`、`node`;能访问 GitHub(`gh issue view` 成功)。 + +## 1. 加载贡献指南(必做) + +- 阅读仓库根目录 [CONTRIBUTING.md](../../../../CONTRIBUTING.md)。 +- 对照 [TDesign Flutter 贡献指南](https://tdesign.tencent.com/flutter/develop):**4. 开发规范**、**5.2 代码 Review 自检**、**5.3 文档自检**。 + +## 2. 拉取 issue 并定分支名(必做) + +```bash +gh issue view --repo Tencent/tdesign-flutter +``` + +从标题提炼 `slug`(小写、连字符),分支名建议: + +```text +fix/issue-- +``` + +创建并切换分支: + +```bash +git fetch origin develop +git checkout -b fix/issue-- origin/develop +``` + +(若团队约定从其他分支拉取,以用户或仓库约定为准。) + +## 3. 初始化 requirements 骨架(必做) + +将占位符换成实际值后执行: + +```bash +node scripts/issue-workflow/init-issue-fix.mjs \ + --issue-number \ + --issue-url "https://github.com/Tencent/tdesign-flutter/issues/" \ + --issue-title "<从 gh issue view 复制的标题>" \ + --slug "" \ + --component "<组件名或主题,如 TFab>" +``` + +若目录已存在且需覆盖,加 `--force`。 + +## 4. 执行完整修复流程(必做) + +从这一步起,**严格按** [issue-fix-workflow/SKILL.md](../issue-fix-workflow/SKILL.md) 执行,包括: + +- 在 `TaskContract.md`、`test-cases.md` 中写清根因、方案与用例后再改代码。 +- 实现、测试、`ExamplePage.test`(如适用)、站点/API 文档(如适用)。 +- 按需委托子代理:`issue-analyst`、`flutter-issue-reviewer`、`acceptance-writer`(见 `.harness/cursor/agents/`)。 + +## 5. 强制检查(必做,未通过不得收尾) + +按实际路径传入 `--requirements-dir` 与 `--component-file` 等: + +```bash +node scripts/issue-workflow/check-issue-fix.mjs \ + --requirements-dir requirements/issue-- \ + --component-file tdesign-component/lib/src/components//.dart \ + --class-name \ + --all-build tdesign-component/demo_tool/all_build.sh \ + --require-all-build-class +``` + +说明:若本次**未新增**需在 `all_build.sh` 中登记的类,可去掉 `--require-all-build-class`(以贡献指南 5.3 为准)。 + +检查失败则修代码或文档后**重新运行**,直至通过。 + +## 6. 同步 harness 到 Cursor(若刚改过 `.harness`) + +```bash +node scripts/init-cursor-harness.mjs +``` + +## 7. 提交与 PR(必做) + +- 提交信息建议:`fix(): <简述> (fixes #)` 或团队约定格式。 +- PR 目标分支默认 **`develop`**;正文可基于 `requirements/issue-*/pr-body.md` 粘贴并补全。 +- PR 中关联 issue(链接或 `fixes #xxx`)。 + +## 一键记忆口诀 + +**读 issue → 开分支 → init requirements → 按 workflow 改 → check 通过 → 提交 PR。** diff --git a/.harness/cursor/skills/issue-fix-workflow/SKILL.md b/.harness/cursor/skills/issue-fix-workflow/SKILL.md new file mode 100644 index 000000000..4d34403e1 --- /dev/null +++ b/.harness/cursor/skills/issue-fix-workflow/SKILL.md @@ -0,0 +1,57 @@ +--- +name: issue-fix-workflow +description: 处理 Tencent/tdesign-flutter 仓库中的 GitHub issue 修复流程。适用于用户提供 issue 链接、issue 编号、要求按贡献指南修复 issue、生成 requirements 验收文档、提交 PR 等场景。 +--- + +# Issue 修复工作流 + +用户给出 issue 链接后,按下面这条固定流程执行: + +> 分析需求、编写用例(必要检查)、加载贡献指南、生成代码、检查必要操作、循环优化代码,知道检查通过、生成验收文档(按指定输出模版,方便人类验收)、提交pr + +## 固定参考 + +开始前先读取并对照: + +- `CONTRIBUTING.md` +- https://tdesign.tencent.com/flutter/develop 的 `4. 开发规范` +- https://tdesign.tencent.com/flutter/develop 的 `5.2 代码 Review 自检` +- https://tdesign.tencent.com/flutter/develop 的 `5.3 文档自检` + +补充参考: + +- `requirements/issue-924-fab-on-long-press/` +- `requirements/issue-900-tab-bar/` +- [reference.md](reference.md) + +## 标准步骤 + +1. 用 `gh issue view` 读取 issue,输出问题描述、根因假设、修复方案和风险点。 +2. 创建专用分支,默认格式:`fix/issue--`。 +3. 运行 `node scripts/issue-workflow/init-issue-fix.mjs ...` 初始化 `requirements/issue-*/` 文档骨架。 +4. 先补 `TaskContract.md` 与 `test-cases.md`,再开始实现代码。 +5. 按贡献指南实现修复,并补充必要测试或 `ExamplePage.test` 用例。 +6. 按下列规则做 Review: + - 类声明后先写构造方法,字段在构造方法下方 + - API 注释统一用 `///` + - 新增组件类或 API 生成入口时检查 `tdesign-component/demo_tool/all_build.sh` + - 组件内部样式 token 优先使用 `TTheme.of(context)` + - 组件内部固定文案优先使用 `TResourceDelegate` +7. 跑最小必要验证,再运行 `node scripts/issue-workflow/check-issue-fix.mjs ...` 做强制检查。 +8. 通过后补全 `code-review-report.md`、`acceptance-report.md` 与 `pr-body.md`。 +9. 最后提交 commit 并创建 PR,目标分支默认是 `develop`。 + +## 输出要求 + +执行结束时,至少应交付: + +- issue 修复分支 +- 代码改动与测试 +- `requirements/issue-*/` 下的完整文档 +- PR 链接 + +## 注意事项 + +- 如果工作区里有与当前 issue 无关的未提交改动,先让用户决定如何处理。 +- 若强制检查失败,不要跳过;修复后重新执行。 +- 如果自动化无法稳定判定某一项,必须在 `code-review-report.md` 或 `acceptance-report.md` 中明确写出人工复核结论。 diff --git a/.harness/cursor/skills/issue-fix-workflow/reference.md b/.harness/cursor/skills/issue-fix-workflow/reference.md new file mode 100644 index 000000000..75380eaff --- /dev/null +++ b/.harness/cursor/skills/issue-fix-workflow/reference.md @@ -0,0 +1,40 @@ +# Issue 修复参考 + +## 贡献指南关注点 + +### 4. 开发规范 + +- 组件命名规范与 API 命名应遵循 TDesign 现有约定。 +- 样式属性如色值、圆角、字体字号等应沉淀到主题中。 +- 对系统原有组件应做能力扩展,而不是删减既有能力。 +- 固定文案应抽离到 `TResourceDelegate`。 +- 组件 API 和 demo 写法参考 `tdesign-component/demo_tool/README.md`。 + +### 5.2 代码 Review 自检 + +- 尽量使用 TD 已有组件而不是系统组件。 +- 检查空值与边界条件。 +- 是否补了验收用例,且需要的示例放在 `ExamplePage.test`。 +- 是否提供了文档。 + +### 5.3 文档自检 + +- 检查 `tdesign-component/demo_tool/all_build.sh` 中是否已有对应组件 API 生成配置。 +- 检查组件属性注释是否完整。 +- 若组件有系统对应组件,检查是否遗漏系统组件已有能力。 +- `all_build.sh` 中的名称与 `tdesign-component/example/lib/config.dart` 保持一致。 + +## issue #924 示例 + +已沉淀的示例材料: + +- `requirements/issue-924-fab-on-long-press/TaskContract.md` +- `requirements/issue-924-fab-on-long-press/test-cases.md` +- `requirements/issue-924-fab-on-long-press/code-review-report.md` +- `requirements/issue-924-fab-on-long-press/acceptance-report.md` + +可复用模式: + +1. 先写任务契约与测试用例 +2. 再实现代码和测试 +3. 最后补 Review、验收与 PR 摘要 diff --git a/.harness/cursor/skills/release-checklist/SKILL.md b/.harness/cursor/skills/release-checklist/SKILL.md new file mode 100644 index 000000000..a457367d4 --- /dev/null +++ b/.harness/cursor/skills/release-checklist/SKILL.md @@ -0,0 +1,24 @@ +--- +name: release-checklist +description: 检查 Flutter 组件库、文档和适配层的发版准备情况。适用于准备发版、整理 changelog,或在合并与发布前做最终核查时。 +--- + +# 发版检查清单 + +## 核对发版范围 + +- 确认预期中的组件库、示例和文档改动都已包含在本次交付中。 +- 检查 README 或 onboarding 更新是否需要执行 `node scripts/sync-readme.mjs`。 +- 检查 Flutter 版本适配文件或初始化说明是否仍与 `tdesign-component/init.sh` 保持一致。 + +## 验证步骤 + +1. 对改动区域执行最小必要验证。 +2. 检查文档、example 和 demo 是否存在用户可见层面的漂移。 +3. 明确标出尚未执行的手工发布、changelog 或后续跟进步骤。 + +## 输出要求 + +- 总结已经就绪的部分。 +- 列出剩余风险和未验证路径。 +- 将发版阻塞项与可选清理项分开描述。 diff --git a/.harness/templates/issue-fix/TaskContract.md.tpl b/.harness/templates/issue-fix/TaskContract.md.tpl new file mode 100644 index 000000000..95851c29b --- /dev/null +++ b/.harness/templates/issue-fix/TaskContract.md.tpl @@ -0,0 +1,36 @@ +# TaskContract — issue #{{ISSUE_NUMBER}} {{ISSUE_TITLE}} + +## 基本信息 + +- issue: {{ISSUE_URL}} +- 组件:`{{COMPONENT_NAME}}` +- 分支:`{{BRANCH_NAME}}` +- 目录:`{{REQUIREMENTS_DIR}}` +- 类型:待补充 + +## 问题描述 + +- 待补充 + +## 根因分析 + +- 待补充 + +## 修复方案 + +1. 待补充 + +## 贡献指南对照 + +- 开发规范:待补充 +- 代码 Review 自检:待补充 +- 文档自检:待补充 + +## 交付物清单 + +| 序号 | 交付物 | 说明 | +|------|--------|------| +| 1 | `{{REQUIREMENTS_DIR}}/test-cases.md` | 验收用例 | +| 2 | `{{REQUIREMENTS_DIR}}/code-review-report.md` | 代码审查结论 | +| 3 | `{{REQUIREMENTS_DIR}}/acceptance-report.md` | 验收报告 | +| 4 | `{{REQUIREMENTS_DIR}}/pr-body.md` | PR 摘要 | diff --git a/.harness/templates/issue-fix/acceptance-report.md.tpl b/.harness/templates/issue-fix/acceptance-report.md.tpl new file mode 100644 index 000000000..7bdef6096 --- /dev/null +++ b/.harness/templates/issue-fix/acceptance-report.md.tpl @@ -0,0 +1,33 @@ +# Acceptance Report — issue #{{ISSUE_NUMBER}} {{ISSUE_TITLE}} + +## 验收结论 + +状态:待补充 + +## 需求对照 + +| 验收项 | 结果 | 说明 | +|---|---|---| +| 待补充 | 待补充 | 待补充 | + +## 执行检查 + +### 通过项 + +```bash +# 待补充 +``` + +### 未通过项或阻塞项 + +- 待补充 + +## 人工验收指引 + +1. 待补充 +2. 待补充 +3. 待补充 + +## 环境说明 + +- 待补充 diff --git a/.harness/templates/issue-fix/code-review-report.md.tpl b/.harness/templates/issue-fix/code-review-report.md.tpl new file mode 100644 index 000000000..61226214d --- /dev/null +++ b/.harness/templates/issue-fix/code-review-report.md.tpl @@ -0,0 +1,39 @@ +# Code Review Report — issue #{{ISSUE_NUMBER}} + +## 审查结论 + +状态:待补充 + +## 修改范围 + +- 待补充 + +## 规范检查 + +### 1. 构造方法与字段顺序 + +- 待补充 + +### 2. 注释风格 + +- 待补充 + +### 3. all_build 配置 + +- 待补充 + +### 4. TTheme 使用 + +- 待补充 + +### 5. TResourceDelegate 使用 + +- 待补充 + +## 正确性评审 + +- 待补充 + +## 风险与未验证项 + +- 待补充 diff --git a/.harness/templates/issue-fix/pr-body.md.tpl b/.harness/templates/issue-fix/pr-body.md.tpl new file mode 100644 index 000000000..737d99a1e --- /dev/null +++ b/.harness/templates/issue-fix/pr-body.md.tpl @@ -0,0 +1,25 @@ +## Summary + +- issue: {{ISSUE_URL}} +- 组件:`{{COMPONENT_NAME}}` +- requirements:`{{REQUIREMENTS_DIR}}` +- 分支:`{{BRANCH_NAME}}` + +## Root Cause + +- 待补充 + +## Fix Plan + +- 待补充 + +## Test Plan + +- [ ] 待补充 + +## Acceptance Docs + +- `{{REQUIREMENTS_DIR}}/TaskContract.md` +- `{{REQUIREMENTS_DIR}}/test-cases.md` +- `{{REQUIREMENTS_DIR}}/code-review-report.md` +- `{{REQUIREMENTS_DIR}}/acceptance-report.md` diff --git a/.harness/templates/issue-fix/test-cases.md.tpl b/.harness/templates/issue-fix/test-cases.md.tpl new file mode 100644 index 000000000..6018a21d0 --- /dev/null +++ b/.harness/templates/issue-fix/test-cases.md.tpl @@ -0,0 +1,19 @@ +# 测试用例 — issue #{{ISSUE_NUMBER}} {{ISSUE_TITLE}} + +## TC-01 + +- **前置条件**:待补充 +- **操作**:待补充 +- **期望**:待补充 + +## TC-02 + +- **前置条件**:待补充 +- **操作**:待补充 +- **期望**:待补充 + +## TC-03 + +- **前置条件**:待补充 +- **操作**:待补充 +- **期望**:待补充 diff --git a/scripts/init-cursor-harness.mjs b/scripts/init-cursor-harness.mjs new file mode 100644 index 000000000..c77d11ca2 --- /dev/null +++ b/scripts/init-cursor-harness.mjs @@ -0,0 +1,354 @@ +import { access, mkdir, readFile, readdir, rm, writeFile } from 'fs/promises'; +import { dirname, join, relative, resolve, sep } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = resolve(__dirname, '..'); +const sourceRoot = join(rootDir, '.harness', 'cursor'); +const cursorRoot = join(rootDir, '.cursor'); +const manifestPath = join(cursorRoot, '.harness-manifest.json'); + +const args = new Set(process.argv.slice(2)); +const options = { + check: args.has('--check'), + clean: args.has('--clean'), + force: args.has('--force'), + help: args.has('--help'), +}; + +const targetSpecs = [ + { + kind: 'dir', + source: join(sourceRoot, 'rules'), + target: join(cursorRoot, 'rules'), + }, + { + kind: 'dir', + source: join(sourceRoot, 'skills'), + target: join(cursorRoot, 'skills'), + }, + { + kind: 'dir', + source: join(sourceRoot, 'agents'), + target: join(cursorRoot, 'agents'), + }, + { + kind: 'file', + source: join(sourceRoot, 'AGENTS.md'), + target: join(rootDir, 'AGENTS.md'), + }, +]; + +function printUsage() { + console.log(`Usage: node scripts/init-cursor-harness.mjs [--check] [--clean] [--force] + +Options: + --check Validate generated files without writing changes + --clean Remove files tracked by the harness manifest + --force Overwrite unmanaged target files during sync + --help Show this help message`); +} + +function toPortablePath(filePath) { + return filePath.split(sep).join('/'); +} + +function fromPortablePath(filePath) { + return filePath.split('/').join(sep); +} + +async function pathExists(filePath) { + try { + await access(filePath); + return true; + } catch { + return false; + } +} + +function isWithin(baseDir, targetDir) { + const rel = relative(baseDir, targetDir); + return rel === '' || (!rel.startsWith('..') && rel !== ''); +} + +async function collectFiles(dirPath) { + if (!(await pathExists(dirPath))) { + return []; + } + + const entries = await readdir(dirPath, { withFileTypes: true }); + entries.sort((left, right) => left.name.localeCompare(right.name)); + + const files = []; + for (const entry of entries) { + const fullPath = join(dirPath, entry.name); + if (entry.isDirectory()) { + files.push(...(await collectFiles(fullPath))); + continue; + } + + if (entry.isFile()) { + files.push(fullPath); + } + } + + return files; +} + +async function loadManifest() { + if (!(await pathExists(manifestPath))) { + return { files: new Set() }; + } + + const raw = await readFile(manifestPath, 'utf8'); + const parsed = JSON.parse(raw); + const files = Array.isArray(parsed.files) ? parsed.files : []; + return { files: new Set(files) }; +} + +async function buildDesiredFiles() { + if (!(await pathExists(sourceRoot))) { + throw new Error(`Harness source directory not found: ${toPortablePath(relative(rootDir, sourceRoot))}`); + } + + const desiredFiles = []; + for (const spec of targetSpecs) { + if (spec.kind === 'dir') { + const sourceFiles = await collectFiles(spec.source); + for (const sourceFile of sourceFiles) { + const rel = relative(spec.source, sourceFile); + const targetPath = join(spec.target, rel); + desiredFiles.push({ + sourcePath: sourceFile, + targetPath, + relativeTarget: toPortablePath(relative(rootDir, targetPath)), + }); + } + continue; + } + + if (await pathExists(spec.source)) { + desiredFiles.push({ + sourcePath: spec.source, + targetPath: spec.target, + relativeTarget: toPortablePath(relative(rootDir, spec.target)), + }); + } + } + + desiredFiles.sort((left, right) => left.relativeTarget.localeCompare(right.relativeTarget)); + return desiredFiles; +} + +async function inspectFile(record, previousFiles) { + const sourceContent = await readFile(record.sourcePath, 'utf8'); + const targetExists = await pathExists(record.targetPath); + + if (!targetExists) { + return { status: 'missing', sourceContent }; + } + + const targetContent = await readFile(record.targetPath, 'utf8'); + if (targetContent === sourceContent) { + return { status: 'in-sync', sourceContent }; + } + + if (previousFiles.has(record.relativeTarget)) { + return { status: 'managed-drift', sourceContent }; + } + + return { status: 'unmanaged-conflict', sourceContent }; +} + +async function writeManagedFile(record, content) { + await mkdir(dirname(record.targetPath), { recursive: true }); + await writeFile(record.targetPath, content, 'utf8'); +} + +async function pruneEmptyDirsUpwards(startDir, stopDir) { + let currentDir = startDir; + + while (isWithin(stopDir, currentDir)) { + if (!(await pathExists(currentDir))) { + break; + } + + const entries = await readdir(currentDir); + if (entries.length > 0) { + break; + } + + await rm(currentDir, { recursive: false, force: true }); + if (currentDir === stopDir) { + break; + } + + currentDir = dirname(currentDir); + } +} + +async function removeManagedTargets(relativeTargets) { + let removed = 0; + + for (const relativeTarget of relativeTargets) { + const targetPath = join(rootDir, fromPortablePath(relativeTarget)); + if (!(await pathExists(targetPath))) { + continue; + } + + await rm(targetPath, { force: true }); + removed += 1; + + if (isWithin(cursorRoot, dirname(targetPath))) { + await pruneEmptyDirsUpwards(dirname(targetPath), cursorRoot); + } + } + + return removed; +} + +async function writeManifest(relativeTargets) { + await mkdir(cursorRoot, { recursive: true }); + + const manifest = { + version: 1, + sourceRoot: '.harness/cursor', + generatedAt: new Date().toISOString(), + files: [...relativeTargets].sort(), + }; + + await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); +} + +async function cleanManifestFiles() { + const manifest = await loadManifest(); + + if (manifest.files.size === 0) { + console.log('No manifest-managed Cursor harness files to clean.'); + return; + } + + const removed = await removeManagedTargets(manifest.files); + + if (await pathExists(manifestPath)) { + await rm(manifestPath, { force: true }); + } + + await pruneEmptyDirsUpwards(cursorRoot, cursorRoot); + console.log(`Removed ${removed} managed file(s).`); +} + +async function checkHarness() { + const desiredFiles = await buildDesiredFiles(); + const previousManifest = await loadManifest(); + const desiredTargets = new Set(desiredFiles.map((file) => file.relativeTarget)); + const issues = []; + + for (const record of desiredFiles) { + const inspection = await inspectFile(record, previousManifest.files); + if (inspection.status === 'missing') { + issues.push(`Missing generated file: ${record.relativeTarget}`); + } else if (inspection.status === 'managed-drift') { + issues.push(`Managed file is out of sync: ${record.relativeTarget}`); + } else if (inspection.status === 'unmanaged-conflict') { + issues.push(`Unmanaged file conflicts with harness output: ${record.relativeTarget}`); + } + } + + for (const previousTarget of previousManifest.files) { + if (!desiredTargets.has(previousTarget) && (await pathExists(join(rootDir, fromPortablePath(previousTarget))))) { + issues.push(`Stale managed file still exists: ${previousTarget}`); + } + } + + if (issues.length > 0) { + console.error('Cursor harness check failed:'); + for (const issue of issues) { + console.error(`- ${issue}`); + } + process.exitCode = 1; + return; + } + + console.log('Cursor harness is in sync.'); +} + +async function syncHarness() { + const desiredFiles = await buildDesiredFiles(); + const previousManifest = await loadManifest(); + const desiredTargets = new Set(desiredFiles.map((file) => file.relativeTarget)); + const conflicts = []; + + const inspections = new Map(); + for (const record of desiredFiles) { + const inspection = await inspectFile(record, previousManifest.files); + inspections.set(record.relativeTarget, inspection); + if (inspection.status === 'unmanaged-conflict' && !options.force) { + conflicts.push(record.relativeTarget); + } + } + + if (conflicts.length > 0) { + throw new Error( + `Refusing to overwrite unmanaged file(s):\n- ${conflicts.join('\n- ')}\nRe-run with --force to overwrite them.` + ); + } + + let created = 0; + let updated = 0; + let unchanged = 0; + + for (const record of desiredFiles) { + const inspection = inspections.get(record.relativeTarget); + if (inspection.status === 'in-sync') { + unchanged += 1; + continue; + } + + await writeManagedFile(record, inspection.sourceContent); + if (inspection.status === 'missing') { + created += 1; + } else { + updated += 1; + } + } + + const staleTargets = [...previousManifest.files].filter((file) => !desiredTargets.has(file)); + const removed = await removeManagedTargets(staleTargets); + + await writeManifest(desiredTargets); + + console.log('Cursor harness synced successfully.'); + console.log(`Created: ${created}`); + console.log(`Updated: ${updated}`); + console.log(`Unchanged: ${unchanged}`); + console.log(`Removed stale: ${removed}`); +} + +async function main() { + if (options.help) { + printUsage(); + return; + } + + if (options.check && options.clean) { + throw new Error('Choose either --check or --clean, not both.'); + } + + if (options.clean) { + await cleanManifestFiles(); + return; + } + + if (options.check) { + await checkHarness(); + return; + } + + await syncHarness(); +} + +main().catch((error) => { + console.error(error.message); + process.exitCode = 1; +}); diff --git a/scripts/issue-workflow/check-issue-fix.mjs b/scripts/issue-workflow/check-issue-fix.mjs new file mode 100644 index 000000000..2b4b15972 --- /dev/null +++ b/scripts/issue-workflow/check-issue-fix.mjs @@ -0,0 +1,234 @@ +import { readFile } from 'fs/promises'; +import { dirname, join, relative, resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = resolve(__dirname, '../..'); + +function parseArgs(argv) { + const args = {}; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token.startsWith('--')) { + continue; + } + const key = token.slice(2); + const next = argv[index + 1]; + if (!next || next.startsWith('--')) { + args[key] = true; + continue; + } + args[key] = next; + index += 1; + } + return args; +} + +function printUsage() { + console.log(`Usage: + node scripts/issue-workflow/check-issue-fix.mjs \\ + --requirements-dir requirements/issue-924-fab-on-long-press \\ + --component-file tdesign-component/lib/src/components/fab/t_fab.dart \\ + --class-name TFab \\ + --all-build tdesign-component/demo_tool/all_build.sh \\ + --require-all-build-class + +Options: + --requirements-dir 必填,requirements 目录 + --component-file 选填,待检查的组件文件 + --class-name 选填,组件类名 + --all-build 选填,all_build.sh 路径 + --require-all-build-class 选填,强制检查类名是否出现在 all_build.sh 中 + --help 显示帮助`); +} + +async function readText(relativePath) { + return readFile(join(rootDir, relativePath), 'utf8'); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function collectLineMatches(content, pattern) { + return content + .split('\n') + .map((line, index) => ({ line, lineNumber: index + 1 })) + .filter(({ line }) => pattern.test(line)) + .map(({ lineNumber }) => lineNumber); +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + if (args.help) { + printUsage(); + return; + } + + if (!args['requirements-dir']) { + printUsage(); + process.exitCode = 1; + return; + } + + const failures = []; + const warnings = []; + const requirementsDir = String(args['requirements-dir']); + const requiredDocs = { + 'TaskContract.md': [ + '## 基本信息', + '## 问题描述', + '## 根因分析', + '## 修复方案', + '## 贡献指南对照', + '## 交付物清单', + ], + 'test-cases.md': ['# 测试用例', '## TC-01'], + 'code-review-report.md': ['## 审查结论', '## 修改范围', '## 规范检查'], + 'acceptance-report.md': ['## 验收结论', '## 需求对照', '## 执行检查', '## 人工验收指引'], + 'pr-body.md': ['## Summary', '## Root Cause', '## Fix Plan', '## Test Plan'], + }; + + for (const [fileName, headings] of Object.entries(requiredDocs)) { + const relativePath = join(requirementsDir, fileName); + let content = ''; + try { + content = await readText(relativePath); + } catch { + failures.push(`缺少必需文档:${relativePath}`); + continue; + } + + for (const heading of headings) { + if (!content.includes(heading)) { + failures.push(`${relativePath} 缺少章节:${heading}`); + } + } + + if (content.includes('待补充')) { + failures.push(`${relativePath} 仍包含“待补充”占位内容`); + } + } + + if (args['component-file']) { + const componentPath = String(args['component-file']); + let componentContent = ''; + try { + componentContent = await readText(componentPath); + } catch { + failures.push(`无法读取组件文件:${componentPath}`); + componentContent = ''; + } + + if (componentContent) { + const lineCommentLines = collectLineMatches(componentContent, /^\s*\/\/(?!\/)/); + if (lineCommentLines.length > 0) { + failures.push( + `${componentPath} 存在 \`//\` 注释,请改用 \`///\`。行号:${lineCommentLines.join(', ')}` + ); + } + + const hardcodedColorLines = collectLineMatches( + componentContent, + /\bColors\.[A-Za-z_]+|(? 0) { + failures.push( + `${componentPath} 存在疑似硬编码颜色,请改用 TTheme 字段。行号:${hardcodedColorLines.join(', ')}` + ); + } + + const hardcodedChineseCopyLines = collectLineMatches( + componentContent, + /['"`][^'"`]*[\u4e00-\u9fa5]+[^'"`]*['"`]/ + ); + if (hardcodedChineseCopyLines.length > 0) { + failures.push( + `${componentPath} 存在疑似硬编码中文文案,请评估是否应抽离到 TResourceDelegate。行号:${hardcodedChineseCopyLines.join(', ')}` + ); + } + + if (args['class-name']) { + const className = String(args['class-name']); + const lines = componentContent.split('\n'); + const classPattern = new RegExp(`^\\s*class\\s+${escapeRegExp(className)}(?:\\s|<|\\{)`); + const constructorPattern = new RegExp(`^\\s*(const\\s+)?${escapeRegExp(className)}\\(`); + const fieldPattern = /^\s*(final|late|var|static)\b/; + + const classIndex = lines.findIndex((line) => classPattern.test(line)); + if (classIndex === -1) { + failures.push(`${componentPath} 中未找到类 ${className}`); + } else { + let constructorIndex = -1; + let firstFieldIndex = -1; + for (let index = classIndex + 1; index < lines.length; index += 1) { + const line = lines[index]; + if (constructorIndex === -1 && constructorPattern.test(line)) { + constructorIndex = index; + } + if (firstFieldIndex === -1 && fieldPattern.test(line)) { + firstFieldIndex = index; + } + if (constructorIndex !== -1 && firstFieldIndex !== -1) { + break; + } + } + + if (constructorIndex === -1) { + failures.push(`${componentPath} 中未找到 ${className} 构造方法`); + } + if ( + constructorIndex !== -1 && + firstFieldIndex !== -1 && + firstFieldIndex < constructorIndex + ) { + failures.push( + `${componentPath} 中字段出现在构造方法之前,请先写构造方法再声明字段` + ); + } + } + } + } + } + + if (args['require-all-build-class']) { + if (!args['all-build'] || !args['class-name']) { + failures.push('启用 --require-all-build-class 时,必须同时提供 --all-build 和 --class-name'); + } else { + const allBuildPath = String(args['all-build']); + const className = String(args['class-name']); + try { + const content = await readText(allBuildPath); + if (!content.includes(className)) { + failures.push(`${allBuildPath} 中未找到类名 ${className} 的配置`); + } + } catch { + failures.push(`无法读取 all_build 配置文件:${allBuildPath}`); + } + } + } + + if (warnings.length > 0) { + console.log('Warnings:'); + for (const warning of warnings) { + console.log(`- ${warning}`); + } + } + + if (failures.length > 0) { + console.error('Issue workflow check failed:'); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exitCode = 1; + return; + } + + console.log(`Issue workflow check passed for ${relative(rootDir, join(rootDir, requirementsDir))}`); +} + +main().catch((error) => { + console.error(error.message); + process.exitCode = 1; +}); diff --git a/scripts/issue-workflow/init-issue-fix.mjs b/scripts/issue-workflow/init-issue-fix.mjs new file mode 100644 index 000000000..68ff7ba64 --- /dev/null +++ b/scripts/issue-workflow/init-issue-fix.mjs @@ -0,0 +1,165 @@ +import { mkdir, readdir, readFile, writeFile } from 'fs/promises'; +import { dirname, join, relative, resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = resolve(__dirname, '../..'); +const templateDir = join(rootDir, '.harness', 'templates', 'issue-fix'); + +function parseArgs(argv) { + const args = {}; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token.startsWith('--')) { + continue; + } + const key = token.slice(2); + const next = argv[index + 1]; + if (!next || next.startsWith('--')) { + args[key] = true; + continue; + } + args[key] = next; + index += 1; + } + return args; +} + +function printUsage() { + console.log(`Usage: + node scripts/issue-workflow/init-issue-fix.mjs \\ + --issue-number 924 \\ + --issue-url https://github.com/Tencent/tdesign-flutter/issues/924 \\ + --issue-title "TDFab 暴露 onLongPress 方法" \\ + --slug fab-on-long-press \\ + --component TFab \\ + --branch fix/issue-924-fab-on-long-press + +Options: + --issue-number 必填,issue 编号 + --issue-url issue 链接,未提供时会按 GitHub 默认格式生成 + --issue-title issue 标题,未提供时默认"待补充标题" + --slug 必填,requirements 目录 slug + --component 必填,组件名或问题主体 + --branch 选填,默认 fix/issue-- + --requirements-dir 选填,默认 requirements/issue-- + --force 覆盖已存在文件 + --dry-run 只输出将要生成的文件,不落盘 + --help 显示帮助`); +} + +function sanitizeSlug(input) { + return input + .trim() + .toLowerCase() + .replace(/[^a-z0-9-]+/g, '-') + .replace(/-{2,}/g, '-') + .replace(/^-|-$/g, ''); +} + +async function loadTemplates() { + const entries = await readdir(templateDir, { withFileTypes: true }); + const templates = []; + + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith('.tpl')) { + continue; + } + + const fullPath = join(templateDir, entry.name); + const content = await readFile(fullPath, 'utf8'); + templates.push({ + sourcePath: fullPath, + fileName: entry.name.replace(/\.tpl$/, ''), + content, + }); + } + + return templates.sort((left, right) => left.fileName.localeCompare(right.fileName)); +} + +async function fileExists(targetPath) { + try { + await readFile(targetPath, 'utf8'); + return true; + } catch { + return false; + } +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + if (args.help) { + printUsage(); + return; + } + + if (!args['issue-number'] || !args.slug || !args.component) { + printUsage(); + process.exitCode = 1; + return; + } + + const issueNumber = String(args['issue-number']).trim(); + const slug = sanitizeSlug(String(args.slug)); + const branchName = args.branch || `fix/issue-${issueNumber}-${slug}`; + const issueUrl = + args['issue-url'] || + `https://github.com/Tencent/tdesign-flutter/issues/${issueNumber}`; + const issueTitle = args['issue-title'] || '待补充标题'; + const requirementsDir = + args['requirements-dir'] || `requirements/issue-${issueNumber}-${slug}`; + const requirementsAbsDir = join(rootDir, requirementsDir); + + const replacements = { + '{{ISSUE_NUMBER}}': issueNumber, + '{{ISSUE_URL}}': issueUrl, + '{{ISSUE_TITLE}}': issueTitle, + '{{COMPONENT_NAME}}': args.component, + '{{BRANCH_NAME}}': branchName, + '{{REQUIREMENTS_DIR}}': requirementsDir, + }; + + const templates = await loadTemplates(); + if (templates.length === 0) { + throw new Error(`No issue-fix templates found in ${templateDir}`); + } + + if (args['dry-run']) { + console.log(`Requirements 目录:${requirementsDir}`); + console.log(`建议分支:${branchName}`); + console.log('将生成以下文件:'); + for (const template of templates) { + console.log(`- ${join(requirementsDir, template.fileName)}`); + } + return; + } + + await mkdir(requirementsAbsDir, { recursive: true }); + + for (const template of templates) { + const targetPath = join(requirementsAbsDir, template.fileName); + if (!args.force && (await fileExists(targetPath))) { + throw new Error( + `Target already exists: ${relative(rootDir, targetPath)}. Re-run with --force to overwrite.` + ); + } + + let rendered = template.content; + for (const [placeholder, value] of Object.entries(replacements)) { + rendered = rendered.replaceAll(placeholder, value); + } + + await writeFile(targetPath, rendered, 'utf8'); + console.log(`Created ${relative(rootDir, targetPath)}`); + } + + console.log(`Done. Requirements 目录:${requirementsDir}`); + console.log(`建议分支:${branchName}`); +} + +main().catch((error) => { + console.error(error.message); + process.exitCode = 1; +}); From 750332b327e6c130a7349a3a3e86fb211c5e7eed Mon Sep 17 00:00:00 2001 From: zflyluo Date: Wed, 13 May 2026 14:28:34 +0800 Subject: [PATCH 02/10] docs(harness): site README policy and GitHub PR body template Aligned with 57096391; .harness only (no regenerated .cursor). --- .harness/cursor/AGENTS.md | 2 + .harness/cursor/agents/docs-reviewer.md | 1 + .harness/cursor/rules/core/core-project.mdc | 1 + .harness/cursor/rules/core/github-pr.mdc | 74 +++++++++++++++++++ .../cursor/rules/core/issue-fix-process.mdc | 2 + .../core/requirements-acceptance-docs.mdc | 2 +- .harness/cursor/rules/site/site-docs.mdc | 2 +- .../cursor/skills/component-workflow/SKILL.md | 2 +- .harness/cursor/skills/doc-sync/SKILL.md | 2 +- .../cursor/skills/issue-fix-entry/SKILL.md | 5 +- .../cursor/skills/issue-fix-workflow/SKILL.md | 6 +- .../skills/issue-fix-workflow/reference.md | 2 + .harness/templates/issue-fix/pr-body.md.tpl | 59 +++++++++++---- 13 files changed, 135 insertions(+), 25 deletions(-) create mode 100644 .harness/cursor/rules/core/github-pr.mdc diff --git a/.harness/cursor/AGENTS.md b/.harness/cursor/AGENTS.md index 0d33d78d9..2b99ece43 100644 --- a/.harness/cursor/AGENTS.md +++ b/.harness/cursor/AGENTS.md @@ -6,4 +6,6 @@ - 修改仓库根目录的引导文档时,请将 `README.md` 与 `README_zh_CN.md` 视为源文件,并使用 `node scripts/sync-readme.mjs` 进行下游同步。 - 当用户提供 GitHub issue 链接并要求修复时,优先使用 skill **`issue-fix-entry`** 作为一键入口,再按 **`issue-fix-workflow`** 展开细节;并配合 issue workflow 相关 rules、agents、模板与 `scripts/issue-workflow/` 下的辅助脚本。 - issue 修复的分析、测试计划、验收报告与 PR 摘要应优先沉淀到 `requirements/issue-*/` 目录中,便于人类验收与后续追踪。 +- **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物);见 `rules/site/site-docs.mdc`。 +- 提交 GitHub PR 时,正文须符合团队模版与注意事项,见 `rules/core/github-pr.mdc`(与 `.harness/templates/issue-fix/pr-body.md.tpl` 一致)。 - 优先保持小而聚焦的改动,遵循既有约定,并对变更影响范围执行针对性验证。 diff --git a/.harness/cursor/agents/docs-reviewer.md b/.harness/cursor/agents/docs-reviewer.md index 54a3353be..bc17c0711 100644 --- a/.harness/cursor/agents/docs-reviewer.md +++ b/.harness/cursor/agents/docs-reviewer.md @@ -12,5 +12,6 @@ readonly: true 2. 是否遗漏了 `scripts/sync-readme.mjs` 相关同步步骤 3. 是否存在术语不一致或过时命令 4. 纯文档改动是否缺少必要的验证说明 +5. 是否误将 `tdesign-site/src/**/README.md` 当作手写维护入口(该类文件为站点打包生成物,见 `rules/site/site-docs.mdc`) 请先输出具体问题,再列出假设前提或尚未验证的区域。 diff --git a/.harness/cursor/rules/core/core-project.mdc b/.harness/cursor/rules/core/core-project.mdc index dba60b48f..2f90c7035 100644 --- a/.harness/cursor/rules/core/core-project.mdc +++ b/.harness/cursor/rules/core/core-project.mdc @@ -10,3 +10,4 @@ alwaysApply: true - 改动范围应控制在对应工作区内:`tdesign-component/`、`tdesign-site/` 或 `tdesign-adaptation/`。 - 保持现有自动化边界,不要把仓库级初始化逻辑塞进 `tdesign-component/init.sh` 这类包级脚本中。 - 优先提交最小必要改动;除非任务明确要求重新生成,否则避免改动生成物或派生产物。 +- **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物);维护入口见 `rules/site/site-docs.mdc`。 diff --git a/.harness/cursor/rules/core/github-pr.mdc b/.harness/cursor/rules/core/github-pr.mdc new file mode 100644 index 000000000..393f99a7d --- /dev/null +++ b/.harness/cursor/rules/core/github-pr.mdc @@ -0,0 +1,74 @@ +--- +description: GitHub PR 正文模版与提交前自检(Tencent/tdesign-flutter) +globs: requirements/**/pr-body.md +alwaysApply: false +--- + +# GitHub PR 描述规范 + +创建或更新 Pull Request 正文时(含使用 `gh pr create --body-file`、或从 `requirements/issue-*/pr-body.md` 粘贴到 GitHub),须使用与 `.harness/templates/issue-fix/pr-body.md.tpl` **一致的结构**(`init-issue-fix.mjs` 会由该 tpl 生成工作区 `pr-body.md`)。 + +## 注意事项(必遵守) + +1. **「🤔 这个 PR 的性质是?」**:按实际改动勾选;只要有**新增公开参数 / 新 API**,应勾选「新特性提交」;仅修内部缺陷且未新增参数时,才勾选「日常 bug 修复」;其余选项按改动性质判断。 +2. **「🔗 相关 Issue」「💡 需求背景和解决方案」等带说明性注释的区块**:按注释提示补全正文后,**删除 HTML 说明注释**(``),再提交 PR,避免把模板说明留给评审。 +3. **「☑️ 请求合并前的自查清单」**:提 PR 前逐项自检并**全部勾选**。 + +## PR 正文模版 + +将下列内容作为 PR 描述骨架(占位符在 `init-issue-fix` 生成时会被替换;手工编写时自行替换 `{{ISSUE_URL}}` 等)。 + +```markdown +### 🤔 这个 PR 的性质是? +> 勾选规则: +> 1.只要有新增参数,就勾选”新特性提交“ +> 2.只修改内部bug,未新增参数,才勾选”日常 bug 修复“ +> 3.其他选项视具体改动判断 + +- [ ] 日常 bug 修复 +- [ ] 新特性提交 +- [ ] 文档改进 +- [ ] 演示代码改进 +- [ ] 组件样式/交互改进 +- [ ] CI/CD 改进 +- [ ] 重构 +- [ ] 代码风格优化 +- [ ] 测试用例 +- [ ] 分支合并 +- [ ] 其他 + +### 🔗 相关 Issue + + + +{{ISSUE_URL}} + +### 💡 需求背景和解决方案 + + + +### 📝 更新日志 + + + +- fix({{COMPONENT_NAME}}): 处理问题或特性描述 ... + +- [ ] 本条 PR 不需要纳入 Changelog + +### ☑️ 请求合并前的自查清单 + +⚠️ 请自检并全部**勾选全部选项**。⚠️ + +- [ ] pr目标分支为develop分支,请勿直接往main分支合并 +- [ ] 标题格式为:`组件类名`: 修改描述(示例:`TBottomTabBar`: 修复iconText模式,底部溢出2.5像素) +- [ ] ”相关issue“处带上修复的issue链接 +- [ ] 相关文档已补充或无须补充 +``` diff --git a/.harness/cursor/rules/core/issue-fix-process.mdc b/.harness/cursor/rules/core/issue-fix-process.mdc index 47bbf0f99..e355d1e6a 100644 --- a/.harness/cursor/rules/core/issue-fix-process.mdc +++ b/.harness/cursor/rules/core/issue-fix-process.mdc @@ -9,4 +9,6 @@ alwaysApply: true - 开始编码前,先读取 `CONTRIBUTING.md`,并对照 https://tdesign.tencent.com/flutter/develop 的 `4. 开发规范`、`5.2 代码 Review 自检`、`5.3 文档自检`。 - 先给出问题原因、修复方案和测试计划,再开始改代码。 - 修复流程必须覆盖:创建分支、编写用例或必要检查、实现代码、Review、自检、生成 `requirements/issue-*/` 验收文档、提交 PR。 +- **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物),见 `rules/site/site-docs.mdc`。 +- 提交 PR 时,正文须符合团队模版与注意事项,见 `rules/core/github-pr.mdc`(与 `.harness/templates/issue-fix/pr-body.md.tpl` 结构一致)。 - 结束前必须运行 `scripts/issue-workflow/check-issue-fix.mjs` 对可自动判定的必做项做强制检查。 diff --git a/.harness/cursor/rules/core/requirements-acceptance-docs.mdc b/.harness/cursor/rules/core/requirements-acceptance-docs.mdc index c264a8b4d..0068d280f 100644 --- a/.harness/cursor/rules/core/requirements-acceptance-docs.mdc +++ b/.harness/cursor/rules/core/requirements-acceptance-docs.mdc @@ -9,4 +9,4 @@ alwaysApply: false - issue 修复相关文档统一归档到 `requirements/issue--/`。 - 最少包含 `TaskContract.md`、`test-cases.md`、`code-review-report.md`、`acceptance-report.md`、`pr-body.md`。 - `acceptance-report.md` 必须覆盖:验收结论、需求对照、执行检查、人工验收指引、阻塞项或环境说明。 -- PR 摘要优先复用 `requirements/issue-*/pr-body.md`,确保 issue 链接、变更摘要和测试计划完整。 +- PR 正文优先以 `requirements/issue-*/pr-body.md` 为底稿,再按 `rules/core/github-pr.mdc` 的团队模版与注意事项整理后用于 GitHub(填完删除说明性注释、完成自查清单勾选)。 diff --git a/.harness/cursor/rules/site/site-docs.mdc b/.harness/cursor/rules/site/site-docs.mdc index 5a8b93bff..a2b2ab9e4 100644 --- a/.harness/cursor/rules/site/site-docs.mdc +++ b/.harness/cursor/rules/site/site-docs.mdc @@ -8,6 +8,6 @@ alwaysApply: false - `tdesign-site/` 下的改动要和现有站点结构、包脚本以及 `site/site.config.mjs` 的路由方式保持一致。 - 修改 onboarding 或 getting started 相关内容时,请记住根目录 `README.md` 与 `README_zh_CN.md` 才是源文件,下游同步由 `scripts/sync-readme.mjs` 负责。 -- 对于 `tdesign-site/src/**/README.md` 下的组件文档页面,保持现有页面结构,并确保示例内容与组件真实行为一致。 +- **禁止**手工修改 `tdesign-site/src/<组件名>/README.md`(即 `tdesign-site/src/**/README.md`):该类文件在站点**打包 / 同步**流程中自动生成,人工改动会在下次构建时被覆盖或产生无意义 diff。组件说明与 API 表应在对应**源**维护(例如 `tdesign-component` 示例、`demo_tool` 生成物、`example/assets/api` 等团队约定入口),由构建链路反映到站点。 - 仓库级自动化放在根目录 `scripts/` 下,站点专属自动化放在 `tdesign-site/script/` 下。 - 文档变更优先执行最小必要验证,例如对应的同步步骤或受影响范围内的站点构建命令。 diff --git a/.harness/cursor/skills/component-workflow/SKILL.md b/.harness/cursor/skills/component-workflow/SKILL.md index 12bd3f98a..8de684532 100644 --- a/.harness/cursor/skills/component-workflow/SKILL.md +++ b/.harness/cursor/skills/component-workflow/SKILL.md @@ -12,7 +12,7 @@ description: 指导 TDesign Flutter 组件、示例、demo 和相关 API 的改 1. `tdesign-component/lib/` 中的组件实现 2. `tdesign-component/demo_tool/` 中的 demo 或 API 配置 3. `tdesign-component/example/` 中的示例页面 -4. 当公开 API 或行为变化时需要同步的相关文档 +4. 当公开 API 或行为变化时需要同步的相关文档(**不要**手改 `tdesign-site/src/**/README.md`,该路径为站点打包生成物;见 `rules/site/site-docs.mdc`)。 ## 评审检查项 diff --git a/.harness/cursor/skills/doc-sync/SKILL.md b/.harness/cursor/skills/doc-sync/SKILL.md index bb2847173..6582c2d62 100644 --- a/.harness/cursor/skills/doc-sync/SKILL.md +++ b/.harness/cursor/skills/doc-sync/SKILL.md @@ -21,5 +21,5 @@ description: 保持仓库 README 源文件与下游文档同步。适用于编 ## 适用边界 -- 如果任务只涉及 `tdesign-site/src/**/README.md` 下的组件专属文档,则不需要触发根 README 同步。 +- 如果任务只涉及 `tdesign-site/src/**/README.md`:该路径为站点**打包 / 同步**生成物,**不应**作为手写文档任务单独维护;请改对应源并由构建链路更新站点,见 `rules/site/site-docs.mdc`。 - 更新共享 onboarding 内容时,优先使用根目录 `scripts/sync-readme.mjs`,而不是依赖旧的站点单点复制脚本。 diff --git a/.harness/cursor/skills/issue-fix-entry/SKILL.md b/.harness/cursor/skills/issue-fix-entry/SKILL.md index 88935d659..e452f692f 100644 --- a/.harness/cursor/skills/issue-fix-entry/SKILL.md +++ b/.harness/cursor/skills/issue-fix-entry/SKILL.md @@ -58,7 +58,7 @@ node scripts/issue-workflow/init-issue-fix.mjs \ 从这一步起,**严格按** [issue-fix-workflow/SKILL.md](../issue-fix-workflow/SKILL.md) 执行,包括: - 在 `TaskContract.md`、`test-cases.md` 中写清根因、方案与用例后再改代码。 -- 实现、测试、`ExamplePage.test`(如适用)、站点/API 文档(如适用)。 +- 实现、测试、`ExamplePage.test`(如适用)、API 与文档源(如适用);**不要**手改 `tdesign-site/src/**/README.md`。 - 按需委托子代理:`issue-analyst`、`flutter-issue-reviewer`、`acceptance-writer`(见 `.harness/cursor/agents/`)。 ## 5. 强制检查(必做,未通过不得收尾) @@ -87,8 +87,9 @@ node scripts/init-cursor-harness.mjs ## 7. 提交与 PR(必做) - 提交信息建议:`fix(): <简述> (fixes #)` 或团队约定格式。 -- PR 目标分支默认 **`develop`**;正文可基于 `requirements/issue-*/pr-body.md` 粘贴并补全。 +- PR 目标分支默认 **`develop`**;正文使用 `requirements/issue-*/pr-body.md`,结构与 `.harness/templates/issue-fix/pr-body.md.tpl` 一致,并遵守 [rules/core/github-pr.mdc](../rules/core/github-pr.mdc):**按实际勾选 PR 性质**、**补全后删除 `` 说明注释**、**提 PR 前完成自查清单全部勾选**。 - PR 中关联 issue(链接或 `fixes #xxx`)。 +- **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物),见 [rules/site/site-docs.mdc](../rules/site/site-docs.mdc)。 ## 一键记忆口诀 diff --git a/.harness/cursor/skills/issue-fix-workflow/SKILL.md b/.harness/cursor/skills/issue-fix-workflow/SKILL.md index 4d34403e1..545b976a7 100644 --- a/.harness/cursor/skills/issue-fix-workflow/SKILL.md +++ b/.harness/cursor/skills/issue-fix-workflow/SKILL.md @@ -30,7 +30,7 @@ description: 处理 Tencent/tdesign-flutter 仓库中的 GitHub issue 修复流 2. 创建专用分支,默认格式:`fix/issue--`。 3. 运行 `node scripts/issue-workflow/init-issue-fix.mjs ...` 初始化 `requirements/issue-*/` 文档骨架。 4. 先补 `TaskContract.md` 与 `test-cases.md`,再开始实现代码。 -5. 按贡献指南实现修复,并补充必要测试或 `ExamplePage.test` 用例。 +5. 按贡献指南实现修复,并补充必要测试或 `ExamplePage.test` 用例;**不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物,见 `rules/site/site-docs.mdc`)。 6. 按下列规则做 Review: - 类声明后先写构造方法,字段在构造方法下方 - API 注释统一用 `///` @@ -38,8 +38,8 @@ description: 处理 Tencent/tdesign-flutter 仓库中的 GitHub issue 修复流 - 组件内部样式 token 优先使用 `TTheme.of(context)` - 组件内部固定文案优先使用 `TResourceDelegate` 7. 跑最小必要验证,再运行 `node scripts/issue-workflow/check-issue-fix.mjs ...` 做强制检查。 -8. 通过后补全 `code-review-report.md`、`acceptance-report.md` 与 `pr-body.md`。 -9. 最后提交 commit 并创建 PR,目标分支默认是 `develop`。 +8. 通过后补全 `code-review-report.md`、`acceptance-report.md` 与 `pr-body.md`(`pr-body.md` 须符合 `.harness/templates/issue-fix/pr-body.md.tpl` 结构,并遵守 `rules/core/github-pr.mdc` 的注意事项)。 +9. 最后提交 commit 并创建 PR,目标分支默认是 `develop`;PR 正文以 `pr-body.md` 为底稿,提交前按 `github-pr` 规则删除说明注释并完成自查清单勾选。 ## 输出要求 diff --git a/.harness/cursor/skills/issue-fix-workflow/reference.md b/.harness/cursor/skills/issue-fix-workflow/reference.md index 75380eaff..8a02d9fbc 100644 --- a/.harness/cursor/skills/issue-fix-workflow/reference.md +++ b/.harness/cursor/skills/issue-fix-workflow/reference.md @@ -23,6 +23,8 @@ - 检查组件属性注释是否完整。 - 若组件有系统对应组件,检查是否遗漏系统组件已有能力。 - `all_build.sh` 中的名称与 `tdesign-component/example/lib/config.dart` 保持一致。 +- **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物);见 `rules/site/site-docs.mdc`。 +- 创建 PR 时正文按 `rules/core/github-pr.mdc` 与 `.harness/templates/issue-fix/pr-body.md.tpl`。 ## issue #924 示例 diff --git a/.harness/templates/issue-fix/pr-body.md.tpl b/.harness/templates/issue-fix/pr-body.md.tpl index 737d99a1e..1546a06c8 100644 --- a/.harness/templates/issue-fix/pr-body.md.tpl +++ b/.harness/templates/issue-fix/pr-body.md.tpl @@ -1,25 +1,52 @@ -## Summary +### 🤔 这个 PR 的性质是? +> 勾选规则: +> 1.只要有新增参数,就勾选”新特性提交“ +> 2.只修改内部bug,未新增参数,才勾选”日常 bug 修复“ +> 3.其他选项视具体改动判断 -- issue: {{ISSUE_URL}} -- 组件:`{{COMPONENT_NAME}}` -- requirements:`{{REQUIREMENTS_DIR}}` -- 分支:`{{BRANCH_NAME}}` +- [ ] 日常 bug 修复 +- [ ] 新特性提交 +- [ ] 文档改进 +- [ ] 演示代码改进 +- [ ] 组件样式/交互改进 +- [ ] CI/CD 改进 +- [ ] 重构 +- [ ] 代码风格优化 +- [ ] 测试用例 +- [ ] 分支合并 +- [ ] 其他 -## Root Cause +### 🔗 相关 Issue -- 待补充 + -## Fix Plan +{{ISSUE_URL}} -- 待补充 +### 💡 需求背景和解决方案 -## Test Plan + -- [ ] 待补充 +### 📝 更新日志 -## Acceptance Docs + -- `{{REQUIREMENTS_DIR}}/TaskContract.md` -- `{{REQUIREMENTS_DIR}}/test-cases.md` -- `{{REQUIREMENTS_DIR}}/code-review-report.md` -- `{{REQUIREMENTS_DIR}}/acceptance-report.md` +- fix({{COMPONENT_NAME}}): 处理问题或特性描述 ... + +- [ ] 本条 PR 不需要纳入 Changelog + +### ☑️ 请求合并前的自查清单 + +⚠️ 请自检并全部**勾选全部选项**。⚠️ + +- [ ] pr目标分支为develop分支,请勿直接往main分支合并 +- [ ] 标题格式为:`组件类名`: 修改描述(示例:`TBottomTabBar`: 修复iconText模式,底部溢出2.5像素) +- [ ] ”相关issue“处带上修复的issue链接 +- [ ] 相关文档已补充或无须补充 From 964bfaa6dada412c7a8292999c57d48f69663670 Mon Sep 17 00:00:00 2001 From: zflyluo Date: Wed, 13 May 2026 14:37:00 +0800 Subject: [PATCH 03/10] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=86=97=E4=BD=99?= =?UTF-8?q?=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .harness/README.md | 4 +-- .harness/cursor/agents/docs-reviewer.md | 17 ------------ .harness/cursor/agents/flutter-verifier.md | 16 ----------- .../cursor/skills/component-workflow/SKILL.md | 27 ------------------- .harness/cursor/skills/doc-sync/SKILL.md | 25 ----------------- .../cursor/skills/release-checklist/SKILL.md | 24 ----------------- 6 files changed, 2 insertions(+), 111 deletions(-) delete mode 100644 .harness/cursor/agents/docs-reviewer.md delete mode 100644 .harness/cursor/agents/flutter-verifier.md delete mode 100644 .harness/cursor/skills/component-workflow/SKILL.md delete mode 100644 .harness/cursor/skills/doc-sync/SKILL.md delete mode 100644 .harness/cursor/skills/release-checklist/SKILL.md diff --git a/.harness/README.md b/.harness/README.md index 372337a7e..14f3b517c 100644 --- a/.harness/README.md +++ b/.harness/README.md @@ -60,7 +60,7 @@ node scripts/issue-workflow/check-issue-fix.mjs --help - `.harness/cursor/skills/issue-fix-entry/`:**一键入口** skill(从 issue 链接到 init、检查、PR 的最短路径) - `.harness/cursor/skills/issue-fix-workflow/`:主流程 skill(详细步骤与注意事项) - `.harness/cursor/rules/`:issue 修复与 Flutter 代码规范规则 -- `.harness/cursor/agents/`:问题分析、Review、验收文档编写等子代理 +- `.harness/cursor/agents/`:仅保留 issue 修复时会委托的三类子代理——`issue-analyst`、`flutter-issue-reviewer`、`acceptance-writer` - `.harness/templates/issue-fix/`:`requirements/` 文档模板 - `scripts/issue-workflow/`:初始化模板与强制检查脚本 @@ -72,5 +72,5 @@ node scripts/issue-workflow/check-issue-fix.mjs --help - 同步脚本会写入 `.cursor/.harness-manifest.json`,用于记录受管理的生成文件。 - 默认只会覆盖或清理 manifest 管理的文件。 -- 仓库文档同步仍然沿用现有的 `scripts/sync-readme.mjs` 流程。 +- 仓库文档同步沿用现有的 `scripts/sync-readme.mjs` 流程(不再单独维护 `doc-sync` skill,避免与 issue 流程无关的上下文)。 - issue workflow 的脚本面向“给定 issue 链接后由 AI 协助执行”的场景,强制检查只负责能被机器稳定判定的部分,仍需结合 code review 清单做人工复核。 diff --git a/.harness/cursor/agents/docs-reviewer.md b/.harness/cursor/agents/docs-reviewer.md deleted file mode 100644 index bc17c0711..000000000 --- a/.harness/cursor/agents/docs-reviewer.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: docs-reviewer -description: 审查 README 与文档改动在根文档、组件库文档和站点之间是否一致。适用于 onboarding 文档、README 同步和纯文档审查场景。 -readonly: true ---- - -请从一致性和用户可读性角度审查当前文档改动。 - -重点检查: - -1. 根 README 源文件与下游生成文档之间是否存在漂移 -2. 是否遗漏了 `scripts/sync-readme.mjs` 相关同步步骤 -3. 是否存在术语不一致或过时命令 -4. 纯文档改动是否缺少必要的验证说明 -5. 是否误将 `tdesign-site/src/**/README.md` 当作手写维护入口(该类文件为站点打包生成物,见 `rules/site/site-docs.mdc`) - -请先输出具体问题,再列出假设前提或尚未验证的区域。 diff --git a/.harness/cursor/agents/flutter-verifier.md b/.harness/cursor/agents/flutter-verifier.md deleted file mode 100644 index 9fa773775..000000000 --- a/.harness/cursor/agents/flutter-verifier.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: flutter-verifier -description: 校验 `tdesign-component/` 下的 Flutter 改动,包括组件行为、示例、主题与资源相关问题,以及针对性的验证建议。 -readonly: true ---- - -请从 Flutter 组件库视角审查当前改动。 - -重点检查: - -1. `tdesign-component/lib/` 中是否存在公开 API 漂移 -2. 受影响组件是否遗漏了 example 或 demo 更新 -3. 是否引入了主题 token 或 `TResourceDelegate` 相关回归 -4. 当前改动区域最小且相关的验证命令是什么 - -请先按严重程度输出发现的问题,再补充剩余风险或缺失的验证项。 diff --git a/.harness/cursor/skills/component-workflow/SKILL.md b/.harness/cursor/skills/component-workflow/SKILL.md deleted file mode 100644 index 8de684532..000000000 --- a/.harness/cursor/skills/component-workflow/SKILL.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: component-workflow -description: 指导 TDesign Flutter 组件、示例、demo 和相关 API 的改动。适用于处理 `tdesign-component/` 下内容、修改组件行为,或更新 Flutter 组件库示例时。 ---- - -# 组件开发流程 - -## 关注范围 - -只在必要的最小文件集合内开展改动: - -1. `tdesign-component/lib/` 中的组件实现 -2. `tdesign-component/demo_tool/` 中的 demo 或 API 配置 -3. `tdesign-component/example/` 中的示例页面 -4. 当公开 API 或行为变化时需要同步的相关文档(**不要**手改 `tdesign-site/src/**/README.md`,该路径为站点打包生成物;见 `rules/site/site-docs.mdc`)。 - -## 评审检查项 - -- 固定样式 token 应沉淀到主题数据中,而不是以内联值散落在代码里。 -- 当新增或修改用户可见文案时,应通过 `TResourceDelegate` 统一管理。 -- 没有充分理由时,不要删减系统组件能力,应优先基于原生 Flutter 行为做扩展。 -- 如果改动影响公开组件契约,要同步检查该组件对应的 example 和 demo。 - -## 验证建议 - -- 优先针对受影响的组件区域做定向验证。 -- 除非改动是跨模块的,否则应优先使用聚焦的 `flutter analyze`、相关测试或最小示例运行,而不是做过宽的全仓校验。 diff --git a/.harness/cursor/skills/doc-sync/SKILL.md b/.harness/cursor/skills/doc-sync/SKILL.md deleted file mode 100644 index 6582c2d62..000000000 --- a/.harness/cursor/skills/doc-sync/SKILL.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: doc-sync -description: 保持仓库 README 源文件与下游文档同步。适用于编辑 `README.md`、`README_zh_CN.md`、`tdesign-component/README*`、`tdesign-site/site/docs/getting-started.md` 或相关引导文档时。 ---- - -# 文档同步流程 - -## 源文件定义 - -- 将根目录 `README.md` 与 `README_zh_CN.md` 视为唯一源文件。 -- `scripts/sync-readme.mjs` 会把根目录 README 同步到以下位置: - - `tdesign-component/README.md` - - `tdesign-component/README_zh_CN.md` - - `tdesign-site/site/docs/getting-started.md` - -## 操作流程 - -1. 优先修改根目录 README 源文件。 -2. 执行 `node scripts/sync-readme.mjs`。 -3. 检查被同步更新的下游文档。 - -## 适用边界 - -- 如果任务只涉及 `tdesign-site/src/**/README.md`:该路径为站点**打包 / 同步**生成物,**不应**作为手写文档任务单独维护;请改对应源并由构建链路更新站点,见 `rules/site/site-docs.mdc`。 -- 更新共享 onboarding 内容时,优先使用根目录 `scripts/sync-readme.mjs`,而不是依赖旧的站点单点复制脚本。 diff --git a/.harness/cursor/skills/release-checklist/SKILL.md b/.harness/cursor/skills/release-checklist/SKILL.md deleted file mode 100644 index a457367d4..000000000 --- a/.harness/cursor/skills/release-checklist/SKILL.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: release-checklist -description: 检查 Flutter 组件库、文档和适配层的发版准备情况。适用于准备发版、整理 changelog,或在合并与发布前做最终核查时。 ---- - -# 发版检查清单 - -## 核对发版范围 - -- 确认预期中的组件库、示例和文档改动都已包含在本次交付中。 -- 检查 README 或 onboarding 更新是否需要执行 `node scripts/sync-readme.mjs`。 -- 检查 Flutter 版本适配文件或初始化说明是否仍与 `tdesign-component/init.sh` 保持一致。 - -## 验证步骤 - -1. 对改动区域执行最小必要验证。 -2. 检查文档、example 和 demo 是否存在用户可见层面的漂移。 -3. 明确标出尚未执行的手工发布、changelog 或后续跟进步骤。 - -## 输出要求 - -- 总结已经就绪的部分。 -- 列出剩余风险和未验证路径。 -- 将发版阻塞项与可选清理项分开描述。 From 138c4807e85d33073931819374ae6b67b58208a1 Mon Sep 17 00:00:00 2001 From: zflyluo Date: Wed, 13 May 2026 15:09:52 +0800 Subject: [PATCH 04/10] =?UTF-8?q?=E8=A1=A5=E5=85=85pr=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=A8=A1=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .harness/cursor/rules/core/github-pr.mdc | 4 ++ scripts/issue-workflow/check-issue-fix.mjs | 63 +++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/.harness/cursor/rules/core/github-pr.mdc b/.harness/cursor/rules/core/github-pr.mdc index 393f99a7d..0b6a55912 100644 --- a/.harness/cursor/rules/core/github-pr.mdc +++ b/.harness/cursor/rules/core/github-pr.mdc @@ -13,6 +13,10 @@ alwaysApply: false 1. **「🤔 这个 PR 的性质是?」**:按实际改动勾选;只要有**新增公开参数 / 新 API**,应勾选「新特性提交」;仅修内部缺陷且未新增参数时,才勾选「日常 bug 修复」;其余选项按改动性质判断。 2. **「🔗 相关 Issue」「💡 需求背景和解决方案」等带说明性注释的区块**:按注释提示补全正文后,**删除 HTML 说明注释**(``),再提交 PR,避免把模板说明留给评审。 3. **「☑️ 请求合并前的自查清单」**:提 PR 前逐项自检并**全部勾选**。 +4. **禁止“自由发挥”的章节结构**:`pr-body.md` 必须完全沿用模板里的标题与顺序,不允许新增/替换自定义标题(例如 `## Summary / ## Root Cause / ## Fix Plan / ## Test Plan`),也不允许删改模板标题文本。允许的改动仅包括: + - 在模板既有栏目下补充必要内容; + - 勾选/取消勾选清单项; + - 删除模板中的 HTML 说明注释。 ## PR 正文模版 diff --git a/scripts/issue-workflow/check-issue-fix.mjs b/scripts/issue-workflow/check-issue-fix.mjs index 2b4b15972..55d6603a9 100644 --- a/scripts/issue-workflow/check-issue-fix.mjs +++ b/scripts/issue-workflow/check-issue-fix.mjs @@ -46,6 +46,50 @@ async function readText(relativePath) { return readFile(join(rootDir, relativePath), 'utf8'); } +function extractHeadings(markdown) { + return markdown + .split('\n') + .map((line) => line.trimEnd()) + .filter((line) => /^#{2,6}\s+/.test(line)); +} + +function validatePrBodyTemplate({ prBodyContent, templateContent, failures, relativePath }) { + const allowedHeadings = new Set(extractHeadings(templateContent)); + const prHeadings = extractHeadings(prBodyContent); + + // 1) 必须包含模板里的全部标题(且标题文本需一致) + for (const requiredHeading of allowedHeadings) { + if (!prBodyContent.includes(requiredHeading)) { + failures.push(`${relativePath} 缺少模板标题:${requiredHeading}`); + } + } + + // 2) 禁止出现模板之外的自定义标题(如 ## Summary / ## Root Cause 等) + const extraHeadings = prHeadings.filter((heading) => !allowedHeadings.has(heading)); + if (extraHeadings.length > 0) { + failures.push( + `${relativePath} 出现模板之外的标题,请严格按 pr-body.md.tpl 结构填写:${extraHeadings.join( + ', ' + )}` + ); + } + + // 3) 标题顺序必须与模板一致(允许在栏目内补充内容,但不允许打乱栏目顺序) + const orderedTemplateHeadings = extractHeadings(templateContent); + let lastIndex = -1; + for (const heading of orderedTemplateHeadings) { + const index = prBodyContent.indexOf(heading); + if (index === -1) { + continue; + } + if (index < lastIndex) { + failures.push(`${relativePath} 标题顺序与模板不一致,请不要调整栏目顺序`); + break; + } + lastIndex = index; + } +} + function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } @@ -75,6 +119,7 @@ async function main() { const failures = []; const warnings = []; const requirementsDir = String(args['requirements-dir']); + const prBodyTemplate = await readText('.harness/templates/issue-fix/pr-body.md.tpl'); const requiredDocs = { 'TaskContract.md': [ '## 基本信息', @@ -87,7 +132,14 @@ async function main() { 'test-cases.md': ['# 测试用例', '## TC-01'], 'code-review-report.md': ['## 审查结论', '## 修改范围', '## 规范检查'], 'acceptance-report.md': ['## 验收结论', '## 需求对照', '## 执行检查', '## 人工验收指引'], - 'pr-body.md': ['## Summary', '## Root Cause', '## Fix Plan', '## Test Plan'], + // PR body 必须严格遵循模板结构(见 .harness/templates/issue-fix/pr-body.md.tpl) + 'pr-body.md': [ + '### 🤔 这个 PR 的性质是?', + '### 🔗 相关 Issue', + '### 💡 需求背景和解决方案', + '### 📝 更新日志', + '### ☑️ 请求合并前的自查清单', + ], }; for (const [fileName, headings] of Object.entries(requiredDocs)) { @@ -106,6 +158,15 @@ async function main() { } } + if (fileName === 'pr-body.md') { + validatePrBodyTemplate({ + prBodyContent: content, + templateContent: prBodyTemplate, + failures, + relativePath, + }); + } + if (content.includes('待补充')) { failures.push(`${relativePath} 仍包含“待补充”占位内容`); } From 37c4904d0399588445fdec790d8ceaaef945eac2 Mon Sep 17 00:00:00 2001 From: zflyluo Date: Wed, 13 May 2026 16:23:48 +0800 Subject: [PATCH 05/10] =?UTF-8?q?=E6=B7=BB=E5=8A=A0code=5Freviewer?= =?UTF-8?q?=E5=AD=90Agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .harness/README.md | 9 ++-- .harness/cursor/AGENTS.md | 2 +- .harness/cursor/agents/code-review.md | 41 +++++++++++++++++++ .../cursor/rules/core/issue-fix-process.mdc | 3 +- .../cursor/skills/issue-fix-entry/SKILL.md | 10 +++-- .../cursor/skills/issue-fix-workflow/SKILL.md | 15 ++++--- .../skills/issue-fix-workflow/reference.md | 3 +- 7 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 .harness/cursor/agents/code-review.md diff --git a/.harness/README.md b/.harness/README.md index 14f3b517c..52fea0287 100644 --- a/.harness/README.md +++ b/.harness/README.md @@ -52,15 +52,16 @@ node scripts/issue-workflow/check-issue-fix.mjs --help 4. 编写测试与必要检查 5. 按规范实现代码 6. 生成 `requirements/` 下的验收文档 -7. 执行强制检查 -8. 整理 PR +7. 执行强制检查(`check-issue-fix.mjs`) +8. **Checklist 复审**:委托 `code-review` 子代理对照贡献指南与脚本未覆盖项(如 `ExamplePage.test`)复核,并更新 `code-review-report.md` +9. 整理 PR 相关资产分布: - `.harness/cursor/skills/issue-fix-entry/`:**一键入口** skill(从 issue 链接到 init、检查、PR 的最短路径) - `.harness/cursor/skills/issue-fix-workflow/`:主流程 skill(详细步骤与注意事项) - `.harness/cursor/rules/`:issue 修复与 Flutter 代码规范规则 -- `.harness/cursor/agents/`:仅保留 issue 修复时会委托的三类子代理——`issue-analyst`、`flutter-issue-reviewer`、`acceptance-writer` +- `.harness/cursor/agents/`:issue 修复常用子代理——`issue-analyst`、`flutter-issue-reviewer`、`code-review`(checklist 复审,建议在强制检查通过后委托)、`acceptance-writer` - `.harness/templates/issue-fix/`:`requirements/` 文档模板 - `scripts/issue-workflow/`:初始化模板与强制检查脚本 @@ -73,4 +74,4 @@ node scripts/issue-workflow/check-issue-fix.mjs --help - 同步脚本会写入 `.cursor/.harness-manifest.json`,用于记录受管理的生成文件。 - 默认只会覆盖或清理 manifest 管理的文件。 - 仓库文档同步沿用现有的 `scripts/sync-readme.mjs` 流程(不再单独维护 `doc-sync` skill,避免与 issue 流程无关的上下文)。 -- issue workflow 的脚本面向“给定 issue 链接后由 AI 协助执行”的场景,强制检查只负责能被机器稳定判定的部分,仍需结合 code review 清单做人工复核。 +- issue workflow 的脚本面向“给定 issue 链接后由 AI 协助执行”的场景;**强制检查**只负责能被机器稳定判定的部分。**`code-review` 子代理**负责按 checklist 复核脚本未覆盖项(如 `ExamplePage.test`、requirements 与 PR 一致性);二者串联使用。 diff --git a/.harness/cursor/AGENTS.md b/.harness/cursor/AGENTS.md index 2b99ece43..162b7534a 100644 --- a/.harness/cursor/AGENTS.md +++ b/.harness/cursor/AGENTS.md @@ -4,7 +4,7 @@ - 请把改动控制在正确的目录范围内:`tdesign-component/` 负责 Flutter 组件库与示例,`tdesign-site/` 负责文档站点,`tdesign-adaptation/` 负责兼容适配相关内容。 - 遵循现有仓库自动化边界。仓库级 Cursor 初始化逻辑放在 `scripts/` 下,Flutter 环境初始化仍放在 `tdesign-component/init.sh` 中。 - 修改仓库根目录的引导文档时,请将 `README.md` 与 `README_zh_CN.md` 视为源文件,并使用 `node scripts/sync-readme.mjs` 进行下游同步。 -- 当用户提供 GitHub issue 链接并要求修复时,优先使用 skill **`issue-fix-entry`** 作为一键入口,再按 **`issue-fix-workflow`** 展开细节;并配合 issue workflow 相关 rules、agents、模板与 `scripts/issue-workflow/` 下的辅助脚本。 +- 当用户提供 GitHub issue 链接并要求修复时,优先使用 skill **`issue-fix-entry`** 作为一键入口,再按 **`issue-fix-workflow`** 展开细节;并配合 issue workflow 相关 rules、agents、模板与 `scripts/issue-workflow/` 下的辅助脚本。强制检查通过后建议委托 **`code-review`** 子代理做 checklist 复审(见 `.harness/cursor/agents/code-review.md`)。 - issue 修复的分析、测试计划、验收报告与 PR 摘要应优先沉淀到 `requirements/issue-*/` 目录中,便于人类验收与后续追踪。 - **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物);见 `rules/site/site-docs.mdc`。 - 提交 GitHub PR 时,正文须符合团队模版与注意事项,见 `rules/core/github-pr.mdc`(与 `.harness/templates/issue-fix/pr-body.md.tpl` 一致)。 diff --git a/.harness/cursor/agents/code-review.md b/.harness/cursor/agents/code-review.md new file mode 100644 index 000000000..f1c5a726d --- /dev/null +++ b/.harness/cursor/agents/code-review.md @@ -0,0 +1,41 @@ +--- +name: code-review +description: 在强制检查脚本通过后,对照 issue 修复 checklist(贡献指南 5.2/5.3、单元测试、ExamplePage.test、requirements 与 PR)做复审;覆盖脚本无法稳定判定的项。与 flutter-issue-reviewer(偏 Dart 组件实现细节)互补。 +readonly: true +--- + +## 角色与触发时机 + +你在 `node scripts/issue-workflow/check-issue-fix.mjs ...` **通过之后**执行(或与其输出交叉验证)。 +强制检查只负责机器可稳定判定的子集;本代理按 **checklist** 做流程与规范向复审,并把缺口写回 `requirements/issue-*/code-review-report.md`(由执行者落盘)。 + +## Checklist(逐项给出:通过 / 缺口 / 未验证 + 依据) + +### A. 自动化边界(与脚本对齐) + +1. 是否已实际运行 `check-issue-fix.mjs` 且零失败退出。 +2. 若传了 `--component-file`,脚本覆盖的项是否可信:非 `///` 的 `//` 行注释、疑似硬编码颜色、疑似硬编码中文文案、类构造与字段顺序等;是否存在「删参数逃避检查」等绕过行为。 + +### B. 测试分层(两条独立要求,不得混为一谈) + +1. **单元 / 集成测试**(例如 `tdesign-component/test/`):是否针对本次行为补充或更新用例,用于**锁定代码逻辑与回归**;能否在 CI/本地 `flutter test` 中稳定复现断言。 +2. **示例验收(Example)**:若需要**人工走查** UI/交互、或与站点「单元测试」栏目对齐,是否在对应示例页的 `ExamplePage(..., test: [...])` 中增加 `ExampleItem`;**目的**是方便人类验收与对照示例稿——**不替代**上一条的自动化测试。 + +### C. 贡献指南与仓库约定 + +1. 对照根目录 `CONTRIBUTING.md` 与 develop **4. 开发规范**、**5.2 代码 Review 自检**、**5.3 文档自检**。 +2. 新增组件类或 API 生成入口时,`tdesign-component/demo_tool/all_build.sh` 是否已配置;命名是否与 `example/lib/config.dart` 等约定一致。 +3. **不要**手工修改 `tdesign-site/src/**/README.md`(站点生成物);文档源与同步按 `rules/site/site-docs.mdc` 与团队流程处理。 + +### D. requirements 与 PR 收尾 + +1. `TaskContract.md`、`test-cases.md`、`code-review-report.md`、`acceptance-report.md`、`pr-body.md` 是否与当前 diff 一致。 +2. `pr-body.md` 是否遵守 `.harness/templates/issue-fix/pr-body.md.tpl` 与 `rules/core/github-pr.mdc`(含删除说明性 ``、自查清单按实际勾选)。 +3. 凡脚本或自动化无法裁定的项,是否在 `code-review-report.md` 或 `acceptance-report.md` 中写明**人工复核结论**。 + +## 输出格式 + +1. **总评**:通过 / 有条件通过 / 需返工(一句话)。 +2. **分项结论**:按 A→D 顺序,每条标注状态;能定位到文件路径或 requirements 章节则写出。 +3. **行动项**:执行者应修改的最小列表(可勾选风格)。 +4. **与 `flutter-issue-reviewer` 的分工**:若仅需对单个 Dart 组件文件做深度风格审读,可建议并行或追加委托 `flutter-issue-reviewer`。 diff --git a/.harness/cursor/rules/core/issue-fix-process.mdc b/.harness/cursor/rules/core/issue-fix-process.mdc index e355d1e6a..e3b453d58 100644 --- a/.harness/cursor/rules/core/issue-fix-process.mdc +++ b/.harness/cursor/rules/core/issue-fix-process.mdc @@ -8,7 +8,8 @@ alwaysApply: true - 当用户提供 GitHub issue 链接、issue 编号,或明确要求“修复 issue”时,优先使用 issue workflow 相关 skill、agents、模板与脚本。 - 开始编码前,先读取 `CONTRIBUTING.md`,并对照 https://tdesign.tencent.com/flutter/develop 的 `4. 开发规范`、`5.2 代码 Review 自检`、`5.3 文档自检`。 - 先给出问题原因、修复方案和测试计划,再开始改代码。 +- 验证分两条:**单元/集成测试**(`tdesign-component/test/` 等,锁定逻辑与回归)与 **示例验收**(`ExamplePage(..., test: [...])` 中的 `ExampleItem`,便于人工走查;需要时二者都做,后者不替代前者)。 - 修复流程必须覆盖:创建分支、编写用例或必要检查、实现代码、Review、自检、生成 `requirements/issue-*/` 验收文档、提交 PR。 - **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物),见 `rules/site/site-docs.mdc`。 - 提交 PR 时,正文须符合团队模版与注意事项,见 `rules/core/github-pr.mdc`(与 `.harness/templates/issue-fix/pr-body.md.tpl` 结构一致)。 -- 结束前必须运行 `scripts/issue-workflow/check-issue-fix.mjs` 对可自动判定的必做项做强制检查。 +- 结束前必须运行 `scripts/issue-workflow/check-issue-fix.mjs` 对可自动判定的必做项做强制检查;**通过后**建议委托 `.harness/cursor/agents/code-review.md` 子代理按 checklist 复审脚本未覆盖项(如 Example `test`、requirements 与 PR 一致性),并更新 `code-review-report.md`。 diff --git a/.harness/cursor/skills/issue-fix-entry/SKILL.md b/.harness/cursor/skills/issue-fix-entry/SKILL.md index e452f692f..4d68fa4cc 100644 --- a/.harness/cursor/skills/issue-fix-entry/SKILL.md +++ b/.harness/cursor/skills/issue-fix-entry/SKILL.md @@ -58,8 +58,8 @@ node scripts/issue-workflow/init-issue-fix.mjs \ 从这一步起,**严格按** [issue-fix-workflow/SKILL.md](../issue-fix-workflow/SKILL.md) 执行,包括: - 在 `TaskContract.md`、`test-cases.md` 中写清根因、方案与用例后再改代码。 -- 实现、测试、`ExamplePage.test`(如适用)、API 与文档源(如适用);**不要**手改 `tdesign-site/src/**/README.md`。 -- 按需委托子代理:`issue-analyst`、`flutter-issue-reviewer`、`acceptance-writer`(见 `.harness/cursor/agents/`)。 +- 实现与验证分两条:**单元/集成测试**(`tdesign-component/test/` 等,保证逻辑与回归)与 **示例验收**(`ExamplePage(..., test: [...])` 中的 `ExampleItem`,便于人工走查;后者不替代前者);另有 API 与文档源时一并处理;**不要**手改 `tdesign-site/src/**/README.md`。 +- 按需委托子代理:`issue-analyst`、`flutter-issue-reviewer`、`code-review`、`acceptance-writer`(见 `.harness/cursor/agents/`)。**建议**:`check-issue-fix.mjs` 通过后再委托 `code-review` 做 checklist 复审。 ## 5. 强制检查(必做,未通过不得收尾) @@ -78,6 +78,10 @@ node scripts/issue-workflow/check-issue-fix.mjs \ 检查失败则修代码或文档后**重新运行**,直至通过。 +## 5.1 Checklist 复审(强烈建议) + +强制检查通过后,委托 **`code-review`** 子代理(`.harness/cursor/agents/code-review.md`)对照贡献指南与 workflow 全量 checklist 复审,并把结论写入 `requirements/issue-*/code-review-report.md`。脚本**不会**校验 `ExamplePage.test` 等项。 + ## 6. 同步 harness 到 Cursor(若刚改过 `.harness`) ```bash @@ -93,4 +97,4 @@ node scripts/init-cursor-harness.mjs ## 一键记忆口诀 -**读 issue → 开分支 → init requirements → 按 workflow 改 → check 通过 → 提交 PR。** +**读 issue → 开分支 → init requirements → 按 workflow 改 → check 通过 → code-review checklist → 提交 PR。** diff --git a/.harness/cursor/skills/issue-fix-workflow/SKILL.md b/.harness/cursor/skills/issue-fix-workflow/SKILL.md index 545b976a7..c2373cade 100644 --- a/.harness/cursor/skills/issue-fix-workflow/SKILL.md +++ b/.harness/cursor/skills/issue-fix-workflow/SKILL.md @@ -7,7 +7,7 @@ description: 处理 Tencent/tdesign-flutter 仓库中的 GitHub issue 修复流 用户给出 issue 链接后,按下面这条固定流程执行: -> 分析需求、编写用例(必要检查)、加载贡献指南、生成代码、检查必要操作、循环优化代码,知道检查通过、生成验收文档(按指定输出模版,方便人类验收)、提交pr +> 分析需求、编写用例(必要检查)、加载贡献指南、生成代码、跑强制检查、**checklist 复审**、循环优化代码,直到检查与复审结论收敛、生成验收文档(按指定输出模版,方便人类验收)、提交 PR ## 固定参考 @@ -30,23 +30,26 @@ description: 处理 Tencent/tdesign-flutter 仓库中的 GitHub issue 修复流 2. 创建专用分支,默认格式:`fix/issue--`。 3. 运行 `node scripts/issue-workflow/init-issue-fix.mjs ...` 初始化 `requirements/issue-*/` 文档骨架。 4. 先补 `TaskContract.md` 与 `test-cases.md`,再开始实现代码。 -5. 按贡献指南实现修复,并补充必要测试或 `ExamplePage.test` 用例;**不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物,见 `rules/site/site-docs.mdc`)。 -6. 按下列规则做 Review: +5. 按贡献指南实现修复,测试与示例分两条落实(**不要**手工修改 `tdesign-site/src/**/README.md`,站点打包生成物见 `rules/site/site-docs.mdc`): + - **单元 / 集成测试**:在 `tdesign-component/test/`(等)补充或更新用例,用于锁定逻辑与回归,保证 `flutter test` 可重复执行。 + - **示例验收(Example)**:若需人工走查 UI/交互或与示例稿对齐,在对应组件示例页的 `ExamplePage(..., test: [...])` 中增加 `ExampleItem`;用于人类验收,**不替代**上一条。 +6. 按下列规则做 Self Review: - 类声明后先写构造方法,字段在构造方法下方 - API 注释统一用 `///` - 新增组件类或 API 生成入口时检查 `tdesign-component/demo_tool/all_build.sh` - 组件内部样式 token 优先使用 `TTheme.of(context)` - 组件内部固定文案优先使用 `TResourceDelegate` 7. 跑最小必要验证,再运行 `node scripts/issue-workflow/check-issue-fix.mjs ...` 做强制检查。 -8. 通过后补全 `code-review-report.md`、`acceptance-report.md` 与 `pr-body.md`(`pr-body.md` 须符合 `.harness/templates/issue-fix/pr-body.md.tpl` 结构,并遵守 `rules/core/github-pr.mdc` 的注意事项)。 -9. 最后提交 commit 并创建 PR,目标分支默认是 `develop`;PR 正文以 `pr-body.md` 为底稿,提交前按 `github-pr` 规则删除说明注释并完成自查清单勾选。 +8. **委托 `code-review` 子代理**(见 `.harness/cursor/agents/code-review.md`):在脚本通过后对照 checklist 复审(含脚本未覆盖项:Example.test、文档与 requirements 一致性等);将结论同步进 `code-review-report.md`,必要时返工后再跑步骤 7。 +9. 通过后补全 `code-review-report.md`、`acceptance-report.md` 与 `pr-body.md`(`pr-body.md` 须符合 `.harness/templates/issue-fix/pr-body.md.tpl` 结构,并遵守 `rules/core/github-pr.mdc` 的注意事项)。 +10. 最后提交 commit 并创建 PR,目标分支默认是 `develop`;PR 正文以 `pr-body.md` 为底稿,提交前按 `github-pr` 规则删除说明注释并完成自查清单勾选。 ## 输出要求 执行结束时,至少应交付: - issue 修复分支 -- 代码改动与测试 +- 代码改动与测试(含单元/集成测试;按需含 Example `test` 区块) - `requirements/issue-*/` 下的完整文档 - PR 链接 diff --git a/.harness/cursor/skills/issue-fix-workflow/reference.md b/.harness/cursor/skills/issue-fix-workflow/reference.md index 8a02d9fbc..5d68e6560 100644 --- a/.harness/cursor/skills/issue-fix-workflow/reference.md +++ b/.harness/cursor/skills/issue-fix-workflow/reference.md @@ -14,7 +14,8 @@ - 尽量使用 TD 已有组件而不是系统组件。 - 检查空值与边界条件。 -- 是否补了验收用例,且需要的示例放在 `ExamplePage.test`。 +- **单元 / 集成测试**:是否在 `tdesign-component/test/`(等)补充或更新用例,用于锁定逻辑与回归。 +- **示例验收(Example)**:若需人工走查 UI/交互,是否在对应 `ExamplePage(..., test: [...])` 中补充 `ExampleItem`(与上一条目的不同,二者都需时不可互相替代)。 - 是否提供了文档。 ### 5.3 文档自检 From cd723b65cee60dc3d2377e5dd97baa749b185e3c Mon Sep 17 00:00:00 2001 From: zflyluo Date: Thu, 14 May 2026 19:14:02 +0800 Subject: [PATCH 06/10] =?UTF-8?q?feat(TFab):=20=E6=96=B0=E5=A2=9E=20onLong?= =?UTF-8?q?Press=20=E6=94=AF=E6=8C=81=E9=95=BF=E6=8C=89=E5=9B=9E=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 onLongPress 透传至 InkWell,满足 issue #924 - 图标反色与投影改走 TTheme,移除硬编码 Colors - 补充单测、示例与 API 表,完善 requirements 验收文档 --- .../TaskContract.md | 43 +++++++++++++ .../acceptance-report.md | 43 +++++++++++++ .../code-review-report.md | 44 +++++++++++++ .../issue-924-fab-on-long-press/pr-body.md | 58 +++++++++++++++++ .../issue-924-fab-on-long-press/test-cases.md | 19 ++++++ .../example/assets/api/fab_api.md | 1 + .../example/lib/page/t_fab_page.dart | 31 +++++++++- .../lib/src/components/fab/t_fab.dart | 31 ++++------ tdesign-component/test/t_fab_test.dart | 62 +++++++++++++++++++ 9 files changed, 311 insertions(+), 21 deletions(-) create mode 100644 requirements/issue-924-fab-on-long-press/TaskContract.md create mode 100644 requirements/issue-924-fab-on-long-press/acceptance-report.md create mode 100644 requirements/issue-924-fab-on-long-press/code-review-report.md create mode 100644 requirements/issue-924-fab-on-long-press/pr-body.md create mode 100644 requirements/issue-924-fab-on-long-press/test-cases.md create mode 100644 tdesign-component/test/t_fab_test.dart diff --git a/requirements/issue-924-fab-on-long-press/TaskContract.md b/requirements/issue-924-fab-on-long-press/TaskContract.md new file mode 100644 index 000000000..c4a3788a8 --- /dev/null +++ b/requirements/issue-924-fab-on-long-press/TaskContract.md @@ -0,0 +1,43 @@ +# TaskContract — issue #924 [TDFab] 暴露 onLongPress 方法 + +## 基本信息 + +- issue: https://github.com/Tencent/tdesign-flutter/issues/924 +- 组件:`TFab` +- 分支:`fix/issue-924-fab-on-long-press` +- 目录:`requirements/issue-924-fab-on-long-press` +- 类型:新特性(新增可选回调参数 `onLongPress`) + +## 问题描述 + +业务需要在悬浮按钮上实现长按逻辑(例如长按展开菜单、快捷操作等)。`TFab` 内部使用 `InkWell` 承载点击,但未将长按能力透出,调用方无法接入 `onLongPress`。 + +## 根因分析 + +`TFab` 的 `build` 中仅向 `InkWell` 传入了 `onTap: onClick`,缺少 `onLongPress` 参数及对应构造字段,导致 Material 长按手势链路无法由外部配置。 + +## 修复方案 + +1. 为 `TFab` 增加可选参数 `onLongPress`(`VoidCallback?`),文档注释为「长按回调」。 +2. 在 `InkWell` 上设置 `onLongPress: onLongPress`,与 `onTap` 并存,行为与 Flutter 原生一致。 +3. 为满足仓库 `check-issue-fix` 对组件文件的色值检查,将原先硬编码的 `Colors.white` 与手写阴影改为 `TTheme.of(context).textColorAnti` 与主题投影 `shadowsMiddle` / `shadowsBase` 回退链。 +4. 补充 `tdesign-component/test/t_fab_test.dart` 覆盖长按与单击共存;示例页增加「交互」模块演示长按弹出 `SnackBar`;同步 `example/assets/api/fab_api.md`。 + +## 贡献指南对照 + +- 开发规范:新增 API 使用 `///` 注释;样式与色值从 `TTheme` 获取;保持 `TFab` 命名与现有导出一致。 +- 代码 Review 自检:构造方法在字段之前;未引入与需求无关的重构;测试可重复执行。 +- 文档自检:示例与 API 表已更新;未手工改动 `tdesign-site/src/**/README.md`。 + +## 交付物清单 + +| 序号 | 交付物 | 说明 | +|------|--------|------| +| 1 | `requirements/issue-924-fab-on-long-press/test-cases.md` | 验收用例 | +| 2 | `requirements/issue-924-fab-on-long-press/code-review-report.md` | 代码审查结论 | +| 3 | `requirements/issue-924-fab-on-long-press/acceptance-report.md` | 验收报告 | +| 4 | `requirements/issue-924-fab-on-long-press/pr-body.md` | PR 摘要 | +| 5 | `tdesign-component/lib/src/components/fab/t_fab.dart` | 组件实现 | +| 6 | `tdesign-component/test/t_fab_test.dart` | 单元测试 | +| 7 | `tdesign-component/example/lib/page/t_fab_page.dart` | 示例演示 | +| 8 | `tdesign-component/example/assets/api/fab_api.md` | API 文档 | diff --git a/requirements/issue-924-fab-on-long-press/acceptance-report.md b/requirements/issue-924-fab-on-long-press/acceptance-report.md new file mode 100644 index 000000000..f6af07d99 --- /dev/null +++ b/requirements/issue-924-fab-on-long-press/acceptance-report.md @@ -0,0 +1,43 @@ +# Acceptance Report — issue #924 [TDFab] 暴露 onLongPress 方法 + +## 验收结论 + +状态:通过(自动化测试与文档自检已完成;UI 光感变更建议设计走查) + +## 需求对照 + +| 验收项 | 结果 | 说明 | +|--------|------|------| +| issue 要求透出长按能力 | 通过 | 新增 `onLongPress` 并绑定 `InkWell.onLongPress` | +| 与单击互不干扰 | 通过 | 见 `t_fab_test.dart` TC-02 | +| 示例可人工验证 | 通过 | `TFabPage`「交互」模块长按出 `SnackBar` | +| API 文档 | 通过 | `fab_api.md` 已增加参数行 | + +## 执行检查 + +### 通过项 + +```bash +cd tdesign-component && flutter test test/t_fab_test.dart +cd tdesign-component && flutter analyze lib/src/components/fab/t_fab.dart +node scripts/issue-workflow/check-issue-fix.mjs \ + --requirements-dir requirements/issue-924-fab-on-long-press \ + --component-file tdesign-component/lib/src/components/fab/t_fab.dart \ + --class-name TFab \ + --all-build tdesign-component/demo_tool/all_build.sh \ + --require-all-build-class +``` + +### 未通过项或阻塞项 + +- 无。 + +## 人工验收指引 + +1. 打开示例应用,进入 Fab 示例页,找到「交互」中的「Fab onLongPress 长按回调」。 +2. 长按按钮,确认出现「已长按」提示。 +3. 在业务工程中引用新版本 `TFab`,传入 `onLongPress` 验证自定义逻辑。 + +## 环境说明 + +- Flutter 测试在 macOS 本机执行;`tdesign_flutter_tools` 本地为 path stub,未运行全量 `all_build.sh` 生成 API,已手工维护 `fab_api.md` 与代码一致。 diff --git a/requirements/issue-924-fab-on-long-press/code-review-report.md b/requirements/issue-924-fab-on-long-press/code-review-report.md new file mode 100644 index 000000000..c0dcb13ae --- /dev/null +++ b/requirements/issue-924-fab-on-long-press/code-review-report.md @@ -0,0 +1,44 @@ +# Code Review Report — issue #924 + +## 审查结论 + +状态:通过(自检) + +## 修改范围 + +- `tdesign-component/lib/src/components/fab/t_fab.dart`:新增 `onLongPress`,`InkWell` 透传;图标反色与阴影改走主题。 +- `tdesign-component/test/t_fab_test.dart`:新增用例。 +- `tdesign-component/example/lib/page/t_fab_page.dart`:新增交互示例模块;`ExamplePage.test` 增加同构项便于站点「单元测试」区块生成。 +- `tdesign-component/example/assets/api/fab_api.md`:API 表增加 `onLongPress` 行。 + +## 规范检查 + +### 1. 构造方法与字段顺序 + +- `TFab` 保持「构造方法在前、字段在后」;新增参数置于 `onClick` 之后,与 `InkWell` 参数语义相邻。 + +### 2. 注释风格 + +- 新增公开字段使用 `/// 长按回调`。 + +### 3. all_build 配置 + +- `TFab` 已在 `tdesign-component/demo_tool/all_build.sh` 中配置,本次未新增组件类名。 + +### 4. TTheme 使用 + +- 主色 / 危险态上图标与文字反色使用 `textColorAnti`;投影使用 `shadowsMiddle` 与 `shadowsBase` 回退。 + +### 5. TResourceDelegate 使用 + +- 本次无新增用户可见组件内固定文案;示例页 `SnackBar` 文案为演示用途,沿用示例页既有写法。 + +## 正确性评审 + +- `onLongPress` 为可选,默认 `null` 时行为与升级前一致(无长按回调)。 +- 单击与长按手势由 `InkWell` 区分,与 Flutter 默认语义一致;已由 TC-01、TC-02 锁定。 + +## 风险与未验证项 + +- 投影由自定义三层阴影改为主题 `shadowsMiddle` / `shadowsBase`,FAB 外观光感可能与旧版略有差异,但更符合设计 token;若设计侧有专门 FAB 投影 token,可后续单独收敛。 +- 本地 `dart run tdesign_flutter_tools` 生成命令不可用(stub 包),`fab_api.md` 已手工同步;若 CI 会跑全量 `all_build.sh`,应以 CI 产物为准再核对一行差异。 diff --git a/requirements/issue-924-fab-on-long-press/pr-body.md b/requirements/issue-924-fab-on-long-press/pr-body.md new file mode 100644 index 000000000..ea3261424 --- /dev/null +++ b/requirements/issue-924-fab-on-long-press/pr-body.md @@ -0,0 +1,58 @@ +### 🤔 这个 PR 的性质是? +> 勾选规则: +> 1.只要有新增参数,就勾选”新特性提交“ +> 2.只修改内部bug,未新增参数,才勾选”日常 bug 修复“ +> 3.其他选项视具体改动判断 + +- [ ] 日常 bug 修复 +- [x] 新特性提交 +- [ ] 文档改进 +- [x] 演示代码改进 +- [x] 组件样式/交互改进 +- [ ] CI/CD 改进 +- [ ] 重构 +- [ ] 代码风格优化 +- [x] 测试用例 +- [ ] 分支合并 +- [ ] 其他 + +### 🔗 相关 Issue + +需求来源:悬浮按钮需要长按扩展能力。 + +https://github.com/Tencent/tdesign-flutter/issues/924 + +### 💡 需求背景和解决方案 + +**问题**:`TFab` 仅支持单击 `onClick`,业务无法实现长按菜单等交互。 + +**方案**: + +- 新增可选参数 `onLongPress`(`VoidCallback?`),并传给内部 `InkWell.onLongPress`。 +- 将原先硬编码的反色与阴影改为 `TTheme` 中的 `textColorAnti` 与 `shadowsMiddle` / `shadowsBase`,满足主题规范与强制检查。 + +**用法示例**: + +```dart +TFab( + theme: TFabTheme.primary, + text: '操作', + onClick: () {}, + onLongPress: () {}, +) +``` + +### 📝 更新日志 + +- feat(TFab): 新增 `onLongPress`,支持长按回调;图标反色与投影改为主题 token。 + +- [ ] 本条 PR 不需要纳入 Changelog + +### ☑️ 请求合并前的自查清单 + +⚠️ 请自检并全部**勾选全部选项**。⚠️ + +- [x] pr目标分支为develop分支,请勿直接往main分支合并 +- [x] 标题格式为:`组件类名`: 修改描述(示例:`TBottomTabBar`: 修复iconText模式,底部溢出2.5像素) +- [x] ”相关issue“处带上修复的issue链接 +- [x] 相关文档已补充或无须补充 diff --git a/requirements/issue-924-fab-on-long-press/test-cases.md b/requirements/issue-924-fab-on-long-press/test-cases.md new file mode 100644 index 000000000..cede8041a --- /dev/null +++ b/requirements/issue-924-fab-on-long-press/test-cases.md @@ -0,0 +1,19 @@ +# 测试用例 — issue #924 [TDFab] 暴露 onLongPress 方法 + +## TC-01 + +- **前置条件**:使用 `TTheme` + `MaterialApp` 包裹 `TFab`,并设置非空的 `onLongPress` 回调。 +- **操作**:对 `TFab` 执行 `longPress` 手势。 +- **期望**:`onLongPress` 被调用一次。 + +## TC-02 + +- **前置条件**:同一 `TFab` 上同时设置 `onClick` 与 `onLongPress`。 +- **操作**:先 `tap`,再 `longPress`。 +- **期望**:单击只触发 `onClick`,不触发 `onLongPress`;长按触发 `onLongPress`。 + +## TC-03 + +- **前置条件**:运行示例应用,进入「Fab」示例页;打开「单元测试」折叠区,或进入主内容区「交互」模块。 +- **操作**:长按展示「长按」文案的悬浮按钮。 +- **期望**:出现内容为「已长按」的 `SnackBar`(人工走查)。 diff --git a/tdesign-component/example/assets/api/fab_api.md b/tdesign-component/example/assets/api/fab_api.md index 641de8127..9d88c4f8a 100644 --- a/tdesign-component/example/assets/api/fab_api.md +++ b/tdesign-component/example/assets/api/fab_api.md @@ -7,6 +7,7 @@ | icon | Icon? | - | 图标 | | key | | - | | | onClick | VoidCallback? | - | 点击事件 | +| onLongPress | VoidCallback? | - | 长按回调 | | shape | TFabShape | TFabShape.circle | 形状 | | size | TFabSize | TFabSize.large | 大小 | | text | String? | - | 文本 | diff --git a/tdesign-component/example/lib/page/t_fab_page.dart b/tdesign-component/example/lib/page/t_fab_page.dart index 1f3030b84..6269b5639 100644 --- a/tdesign-component/example/lib/page/t_fab_page.dart +++ b/tdesign-component/example/lib/page/t_fab_page.dart @@ -16,7 +16,17 @@ class _TFabPageState extends State { @override Widget build(BuildContext context) { - return ExamplePage(title: tTitle(), exampleCodeGroup: 'fab', children: [ + return ExamplePage( + title: tTitle(), + exampleCodeGroup: 'fab', + test: [ + ExampleItem( + ignoreCode: true, + desc: 'TFab onLongPress 长按回调', + builder: _buildLongPressFab, + ), + ], + children: [ ExampleModule(title: '组件类型', children: [ ExampleItem(desc: 'Icon Fab 纯图标悬浮按钮', builder: _buildPureIconFab), ExampleItem( @@ -26,6 +36,9 @@ class _TFabPageState extends State { ExampleItem(desc: 'Fab Theme 悬浮按钮主题', builder: _buildThemeFab), ExampleItem(desc: 'Fab Shape 悬浮按钮形状', builder: _buildShapeFab), ExampleItem(desc: 'Fab Size 悬浮按钮尺寸', builder: _buildSizeFab) + ]), + ExampleModule(title: '交互', children: [ + ExampleItem(desc: 'Fab onLongPress 长按回调', builder: _buildLongPressFab), ]) ]); } @@ -99,6 +112,22 @@ class _TFabPageState extends State { ]); } + @Demo(group: 'fab') + Widget _buildLongPressFab(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 16), + child: TFab( + theme: TFabTheme.primary, + text: '长按', + onLongPress: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('已长按')), + ); + }, + ), + ); + } + @Demo(group: 'fab') Widget _buildSizeFab(BuildContext context) { return _buildRowDemoWidthDescription([ diff --git a/tdesign-component/lib/src/components/fab/t_fab.dart b/tdesign-component/lib/src/components/fab/t_fab.dart index e2d05c668..a73f251a5 100644 --- a/tdesign-component/lib/src/components/fab/t_fab.dart +++ b/tdesign-component/lib/src/components/fab/t_fab.dart @@ -23,8 +23,9 @@ class TFab extends StatelessWidget { this.shape = TFabShape.circle, this.size = TFabSize.large, this.text, - this.onClick, this.icon, + this.onClick, + this.onLongPress, }) : super(key: key); /// 主题 @@ -45,6 +46,9 @@ class TFab extends StatelessWidget { /// 点击事件 final VoidCallback? onClick; + /// 长按回调 + final VoidCallback? onLongPress; + bool get showText => text?.isNotEmpty ?? false; EdgeInsets getPadding() { @@ -97,13 +101,13 @@ class TFab extends StatelessWidget { Color getIconColor(BuildContext context) { switch (theme) { case TFabTheme.primary: - return Colors.white; + return TTheme.of(context).textColorAnti; case TFabTheme.defaultTheme: return TTheme.of(context).fontGyColor1; case TFabTheme.light: return TTheme.of(context).brandNormalColor; case TFabTheme.danger: - return Colors.white; + return TTheme.of(context).textColorAnti; } } @@ -137,27 +141,14 @@ class TFab extends StatelessWidget { Widget build(BuildContext context) { return InkWell( onTap: onClick, + onLongPress: onLongPress, child: Container( padding: getPadding(), decoration: BoxDecoration( color: getBackgroundColor(context), - boxShadow: [ - BoxShadow( - offset: const Offset(0, 5), - blurRadius: 2.5, - spreadRadius: -1.5, - color: Colors.black.withOpacity(0.1)), - BoxShadow( - offset: const Offset(0, 8), - blurRadius: 5, - spreadRadius: 0.5, - color: Colors.black.withOpacity(0.06)), - BoxShadow( - offset: const Offset(0, 3), - blurRadius: 7, - spreadRadius: 1, - color: Colors.black.withOpacity(0.05)) - ], + boxShadow: TTheme.of(context).shadowsMiddle ?? + TTheme.of(context).shadowsBase ?? + const [], borderRadius: shape == TFabShape.circle ? BorderRadius.circular(TTheme.of(context).radiusCircle) : BorderRadius.circular(TTheme.of(context).radiusDefault)), diff --git a/tdesign-component/test/t_fab_test.dart b/tdesign-component/test/t_fab_test.dart new file mode 100644 index 000000000..da030a6e4 --- /dev/null +++ b/tdesign-component/test/t_fab_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +Widget _wrap(Widget child) { + return TTheme( + data: TThemeData.defaultData(), + child: MaterialApp( + home: Scaffold( + body: Center(child: child), + ), + ), + ); +} + +void main() { + group('TFab — onLongPress (issue #924)', () { + testWidgets('长按时应触发 onLongPress', (tester) async { + var longPressed = false; + await tester.pumpWidget(_wrap( + TFab( + theme: TFabTheme.primary, + onLongPress: () { + longPressed = true; + }, + ), + )); + await tester.pumpAndSettle(); + + await tester.longPress(find.byType(TFab)); + await tester.pumpAndSettle(); + + expect(longPressed, isTrue); + }); + + testWidgets('单击时应触发 onClick,且可与 onLongPress 共存', (tester) async { + var tapped = false; + var longPressed = false; + await tester.pumpWidget(_wrap( + TFab( + theme: TFabTheme.primary, + onClick: () { + tapped = true; + }, + onLongPress: () { + longPressed = true; + }, + ), + )); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(TFab)); + await tester.pumpAndSettle(); + expect(tapped, isTrue); + expect(longPressed, isFalse); + + await tester.longPress(find.byType(TFab)); + await tester.pumpAndSettle(); + expect(longPressed, isTrue); + }); + }); +} From b9bd654c1bbf2dd6ae7ecab7ff421d21e49696dc Mon Sep 17 00:00:00 2001 From: zflyluo Date: Thu, 14 May 2026 19:25:24 +0800 Subject: [PATCH 07/10] =?UTF-8?q?chore(harness):=20issue=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=B5=81=E7=A8=8B=E4=BC=98=E5=85=88=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8F=91=E8=B5=B7=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - workflow / entry skill:commit 后先 push + gh pr create,失败再提示本地步骤 - issue-fix-process 规则与 reference 对齐 --- .harness/cursor/rules/core/issue-fix-process.mdc | 1 + .harness/cursor/skills/issue-fix-entry/SKILL.md | 6 +++++- .harness/cursor/skills/issue-fix-workflow/SKILL.md | 12 +++++++++++- .../cursor/skills/issue-fix-workflow/reference.md | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.harness/cursor/rules/core/issue-fix-process.mdc b/.harness/cursor/rules/core/issue-fix-process.mdc index e3b453d58..0e28cb25d 100644 --- a/.harness/cursor/rules/core/issue-fix-process.mdc +++ b/.harness/cursor/rules/core/issue-fix-process.mdc @@ -10,6 +10,7 @@ alwaysApply: true - 先给出问题原因、修复方案和测试计划,再开始改代码。 - 验证分两条:**单元/集成测试**(`tdesign-component/test/` 等,锁定逻辑与回归)与 **示例验收**(`ExamplePage(..., test: [...])` 中的 `ExampleItem`,便于人工走查;需要时二者都做,后者不替代前者)。 - 修复流程必须覆盖:创建分支、编写用例或必要检查、实现代码、Review、自检、生成 `requirements/issue-*/` 验收文档、提交 PR。 +- **发起 PR**:在 `git commit` 完成后应**优先尝试自动** `git push` 与 `gh pr create`(目标分支默认 `develop`,正文用 `requirements/issue-*/pr-body.md`);**仅当推送或创建 PR 失败**(权限、`gh` 未登录、网络、需 fork 流程等)时,再向用户说明原因并给出本地补救步骤;成功时交付 PR 链接。 - **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物),见 `rules/site/site-docs.mdc`。 - 提交 PR 时,正文须符合团队模版与注意事项,见 `rules/core/github-pr.mdc`(与 `.harness/templates/issue-fix/pr-body.md.tpl` 结构一致)。 - 结束前必须运行 `scripts/issue-workflow/check-issue-fix.mjs` 对可自动判定的必做项做强制检查;**通过后**建议委托 `.harness/cursor/agents/code-review.md` 子代理按 checklist 复审脚本未覆盖项(如 Example `test`、requirements 与 PR 一致性),并更新 `code-review-report.md`。 diff --git a/.harness/cursor/skills/issue-fix-entry/SKILL.md b/.harness/cursor/skills/issue-fix-entry/SKILL.md index 4d68fa4cc..35ec04c69 100644 --- a/.harness/cursor/skills/issue-fix-entry/SKILL.md +++ b/.harness/cursor/skills/issue-fix-entry/SKILL.md @@ -95,6 +95,10 @@ node scripts/init-cursor-harness.mjs - PR 中关联 issue(链接或 `fixes #xxx`)。 - **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物),见 [rules/site/site-docs.mdc](../rules/site/site-docs.mdc)。 +### 7.1 优先自动发起 PR + +在 commit 已就绪的前提下,**应先尝试** `git push -u origin <修复分支>`,再 `gh pr create --base develop --head <修复分支> --title "..." --body-file requirements/issue-*/pr-body.md`(分支已有 PR 时用 `gh pr view` 取链接即可)。**仅当 push 或 `gh pr create` 失败**时,再在回复中说明原因并给出用户需在本地完成的步骤(登录 `gh`、配置 fork、网页创建 PR 等)。成功时必须向用户交付 **PR 链接**。 + ## 一键记忆口诀 -**读 issue → 开分支 → init requirements → 按 workflow 改 → check 通过 → code-review checklist → 提交 PR。** +**读 issue → 开分支 → init requirements → 按 workflow 改 → check 通过 → code-review checklist → 提交并尽量自动开 PR(失败再提示本地操作)。** diff --git a/.harness/cursor/skills/issue-fix-workflow/SKILL.md b/.harness/cursor/skills/issue-fix-workflow/SKILL.md index c2373cade..30b068b3c 100644 --- a/.harness/cursor/skills/issue-fix-workflow/SKILL.md +++ b/.harness/cursor/skills/issue-fix-workflow/SKILL.md @@ -42,7 +42,16 @@ description: 处理 Tencent/tdesign-flutter 仓库中的 GitHub issue 修复流 7. 跑最小必要验证,再运行 `node scripts/issue-workflow/check-issue-fix.mjs ...` 做强制检查。 8. **委托 `code-review` 子代理**(见 `.harness/cursor/agents/code-review.md`):在脚本通过后对照 checklist 复审(含脚本未覆盖项:Example.test、文档与 requirements 一致性等);将结论同步进 `code-review-report.md`,必要时返工后再跑步骤 7。 9. 通过后补全 `code-review-report.md`、`acceptance-report.md` 与 `pr-body.md`(`pr-body.md` 须符合 `.harness/templates/issue-fix/pr-body.md.tpl` 结构,并遵守 `rules/core/github-pr.mdc` 的注意事项)。 -10. 最后提交 commit 并创建 PR,目标分支默认是 `develop`;PR 正文以 `pr-body.md` 为底稿,提交前按 `github-pr` 规则删除说明注释并完成自查清单勾选。 +10. 最后提交 commit 并**优先自动创建 PR**(见下节「发起 PR」);目标分支默认 **`develop`**;PR 正文以 `pr-body.md` 为底稿,提交前按 `github-pr` 规则删除说明注释并完成自查清单勾选。 + +## 发起 PR(优先自动化) + +在本地 commit 完成后,**应先自行执行**(或由具备终端与 GitHub 权限的自动化执行),不要默认假定用户会手工开 PR: + +1. `git push -u origin <修复分支>` +2. 若该分支尚无 PR:`gh pr create --base develop --head <修复分支> --title "<符合团队格式的标题>" --body-file requirements/issue-*/pr-body.md`(已存在 PR 时可用 `gh pr view` 确认链接即可)。 + +**仅当 `git push` 或 `gh pr create` 失败**(无远端写权限、`gh` 未登录、网络错误、需走 fork 工作流等)时,才在回复中说明失败原因,并给出需用户本地执行的补救步骤(如 `gh auth login`、配置 fork remote、在网页端从分支创建 PR 等)。成功时应在收尾输出中给出 **PR 链接**。 ## 输出要求 @@ -58,3 +67,4 @@ description: 处理 Tencent/tdesign-flutter 仓库中的 GitHub issue 修复流 - 如果工作区里有与当前 issue 无关的未提交改动,先让用户决定如何处理。 - 若强制检查失败,不要跳过;修复后重新执行。 - 如果自动化无法稳定判定某一项,必须在 `code-review-report.md` 或 `acceptance-report.md` 中明确写出人工复核结论。 +- **发起 PR**:默认定序为「尝试自动 push + `gh pr create` → 失败再提示本地操作」,避免把本可由代理完成的步骤默认推给用户。 diff --git a/.harness/cursor/skills/issue-fix-workflow/reference.md b/.harness/cursor/skills/issue-fix-workflow/reference.md index 5d68e6560..a1693f273 100644 --- a/.harness/cursor/skills/issue-fix-workflow/reference.md +++ b/.harness/cursor/skills/issue-fix-workflow/reference.md @@ -25,7 +25,7 @@ - 若组件有系统对应组件,检查是否遗漏系统组件已有能力。 - `all_build.sh` 中的名称与 `tdesign-component/example/lib/config.dart` 保持一致。 - **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物);见 `rules/site/site-docs.mdc`。 -- 创建 PR 时正文按 `rules/core/github-pr.mdc` 与 `.harness/templates/issue-fix/pr-body.md.tpl`。 +- 创建 PR 时正文按 `rules/core/github-pr.mdc` 与 `.harness/templates/issue-fix/pr-body.md.tpl`;**优先**用 `git push` + `gh pr create` 自动发起,失败再提示本地操作(与 `issue-fix-workflow` 一致)。 ## issue #924 示例 From 1ab7c8c414c8a26f11d147bd6ce80a935b7b03a6 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 11:27:06 +0000 Subject: [PATCH 08/10] [autofix.ci] apply automated fixes --- .../assets/code/fab._buildLongPressFab.txt | 15 +++++++++++ tdesign-site/src/fab/README.md | 25 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 tdesign-component/example/assets/code/fab._buildLongPressFab.txt diff --git a/tdesign-component/example/assets/code/fab._buildLongPressFab.txt b/tdesign-component/example/assets/code/fab._buildLongPressFab.txt new file mode 100644 index 000000000..1749e663a --- /dev/null +++ b/tdesign-component/example/assets/code/fab._buildLongPressFab.txt @@ -0,0 +1,15 @@ + + Widget _buildLongPressFab(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 16), + child: TFab( + theme: TFabTheme.primary, + text: '长按', + onLongPress: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('已长按')), + ); + }, + ), + ); + } \ No newline at end of file diff --git a/tdesign-site/src/fab/README.md b/tdesign-site/src/fab/README.md index c24020665..e662337cf 100644 --- a/tdesign-site/src/fab/README.md +++ b/tdesign-site/src/fab/README.md @@ -158,6 +158,30 @@ Fab Size 悬浮按钮尺寸 +### 1 交互 + +Fab onLongPress 长按回调 + + + +
+  Widget _buildLongPressFab(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.only(left: 16),
+      child: TFab(
+        theme: TFabTheme.primary,
+        text: '长按',
+        onLongPress: () {
+          ScaffoldMessenger.of(context).showSnackBar(
+            const SnackBar(content: Text('已长按')),
+          );
+        },
+      ),
+    );
+  }
+ +
+ ## API @@ -169,6 +193,7 @@ Fab Size 悬浮按钮尺寸 | icon | Icon? | - | 图标 | | key | | - | | | onClick | VoidCallback? | - | 点击事件 | +| onLongPress | VoidCallback? | - | 长按回调 | | shape | TFabShape | TFabShape.circle | 形状 | | size | TFabSize | TFabSize.large | 大小 | | text | String? | - | 文本 | From 22042e3428675c203285290306c3670120151c65 Mon Sep 17 00:00:00 2001 From: zflyluo Date: Thu, 14 May 2026 19:44:49 +0800 Subject: [PATCH 09/10] =?UTF-8?q?docs(harness):=20issue=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20PR=20=E5=90=8E=E8=A1=A5=E5=85=85=20demo=20APK=20?= =?UTF-8?q?=E9=AA=8C=E6=94=B6=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .harness/cursor/rules/core/issue-fix-process.mdc | 1 + .harness/cursor/skills/issue-fix-entry/SKILL.md | 6 +++++- .harness/cursor/skills/issue-fix-workflow/SKILL.md | 9 +++++++++ .harness/cursor/skills/issue-fix-workflow/reference.md | 1 + 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.harness/cursor/rules/core/issue-fix-process.mdc b/.harness/cursor/rules/core/issue-fix-process.mdc index 0e28cb25d..50a167786 100644 --- a/.harness/cursor/rules/core/issue-fix-process.mdc +++ b/.harness/cursor/rules/core/issue-fix-process.mdc @@ -11,6 +11,7 @@ alwaysApply: true - 验证分两条:**单元/集成测试**(`tdesign-component/test/` 等,锁定逻辑与回归)与 **示例验收**(`ExamplePage(..., test: [...])` 中的 `ExampleItem`,便于人工走查;需要时二者都做,后者不替代前者)。 - 修复流程必须覆盖:创建分支、编写用例或必要检查、实现代码、Review、自检、生成 `requirements/issue-*/` 验收文档、提交 PR。 - **发起 PR**:在 `git commit` 完成后应**优先尝试自动** `git push` 与 `gh pr create`(目标分支默认 `develop`,正文用 `requirements/issue-*/pr-body.md`);**仅当推送或创建 PR 失败**(权限、`gh` 未登录、网络、需 fork 流程等)时,再向用户说明原因并给出本地补救步骤;成功时交付 PR 链接。 +- **PR 之后**:成功发起 PR 后,应向用户补充下一步建议:等待 PR 流水线构建 **demo APK** 完成,安装验收;若有问题在 PR / issue 反馈并继续修改(详见 `issue-fix-workflow` skill)。 - **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物),见 `rules/site/site-docs.mdc`。 - 提交 PR 时,正文须符合团队模版与注意事项,见 `rules/core/github-pr.mdc`(与 `.harness/templates/issue-fix/pr-body.md.tpl` 结构一致)。 - 结束前必须运行 `scripts/issue-workflow/check-issue-fix.mjs` 对可自动判定的必做项做强制检查;**通过后**建议委托 `.harness/cursor/agents/code-review.md` 子代理按 checklist 复审脚本未覆盖项(如 Example `test`、requirements 与 PR 一致性),并更新 `code-review-report.md`。 diff --git a/.harness/cursor/skills/issue-fix-entry/SKILL.md b/.harness/cursor/skills/issue-fix-entry/SKILL.md index 35ec04c69..e71e11668 100644 --- a/.harness/cursor/skills/issue-fix-entry/SKILL.md +++ b/.harness/cursor/skills/issue-fix-entry/SKILL.md @@ -99,6 +99,10 @@ node scripts/init-cursor-harness.mjs 在 commit 已就绪的前提下,**应先尝试** `git push -u origin <修复分支>`,再 `gh pr create --base develop --head <修复分支> --title "..." --body-file requirements/issue-*/pr-body.md`(分支已有 PR 时用 `gh pr view` 取链接即可)。**仅当 push 或 `gh pr create` 失败**时,再在回复中说明原因并给出用户需在本地完成的步骤(登录 `gh`、配置 fork、网页创建 PR 等)。成功时必须向用户交付 **PR 链接**。 +### 7.2 PR 提交后的下一步建议 + +成功发起 PR 并给出链接后,收尾时**务必**向用户补充:等待该 PR 流水线构建 **demo APK**(或仓库文档中的等价产物)完成 → 下载安装做走查验收 → 若有问题在 PR / issue 反馈并继续修改迭代。详细表述见 [issue-fix-workflow/SKILL.md](../issue-fix-workflow/SKILL.md) 中的「PR 提交后的下一步建议」。 + ## 一键记忆口诀 -**读 issue → 开分支 → init requirements → 按 workflow 改 → check 通过 → code-review checklist → 提交并尽量自动开 PR(失败再提示本地操作)。** +**读 issue → 开分支 → init requirements → 按 workflow 改 → check 通过 → code-review checklist → 提交并尽量自动开 PR(失败再提示本地操作)→ 提醒等待 demo APK 安装验收与反馈闭环。** diff --git a/.harness/cursor/skills/issue-fix-workflow/SKILL.md b/.harness/cursor/skills/issue-fix-workflow/SKILL.md index 30b068b3c..6ac047973 100644 --- a/.harness/cursor/skills/issue-fix-workflow/SKILL.md +++ b/.harness/cursor/skills/issue-fix-workflow/SKILL.md @@ -53,6 +53,14 @@ description: 处理 Tencent/tdesign-flutter 仓库中的 GitHub issue 修复流 **仅当 `git push` 或 `gh pr create` 失败**(无远端写权限、`gh` 未登录、网络错误、需走 fork 工作流等)时,才在回复中说明失败原因,并给出需用户本地执行的补救步骤(如 `gh auth login`、配置 fork remote、在网页端从分支创建 PR 等)。成功时应在收尾输出中给出 **PR 链接**。 +## PR 提交后的下一步建议 + +在已成功发起 PR 并向用户交付 **PR 链接** 后,收尾回复中应**补充一段下一步建议**(与团队 CI 实际产物表述一致即可),例如: + +1. **等待构建**:关注该 PR 在 GitHub Actions(或仓库约定流水线)中的检查结果,待 **demo 示例 APK**(或等价产物名称)构建成功并完成归档/上传。 +2. **安装验收**:下载并安装上述 APK,按 `requirements/issue-*/test-cases.md` 与示例页场景做人工走查验收。 +3. **闭环修改**:若验收中发现问题,在 PR 或关联 issue 中反馈现象与复现路径,再回到本 workflow 做小步修复、推送更新分支并等待新一轮构建,直至验收通过。 + ## 输出要求 执行结束时,至少应交付: @@ -61,6 +69,7 @@ description: 处理 Tencent/tdesign-flutter 仓库中的 GitHub issue 修复流 - 代码改动与测试(含单元/集成测试;按需含 Example `test` 区块) - `requirements/issue-*/` 下的完整文档 - PR 链接 +- **PR 提交后的下一步建议**(见上节「PR 提交后的下一步建议」,便于人类等待 demo APK 并安装验收、有问题再反馈迭代) ## 注意事项 diff --git a/.harness/cursor/skills/issue-fix-workflow/reference.md b/.harness/cursor/skills/issue-fix-workflow/reference.md index a1693f273..6085fd718 100644 --- a/.harness/cursor/skills/issue-fix-workflow/reference.md +++ b/.harness/cursor/skills/issue-fix-workflow/reference.md @@ -26,6 +26,7 @@ - `all_build.sh` 中的名称与 `tdesign-component/example/lib/config.dart` 保持一致。 - **不要**手工修改 `tdesign-site/src/**/README.md`(站点打包生成物);见 `rules/site/site-docs.mdc`。 - 创建 PR 时正文按 `rules/core/github-pr.mdc` 与 `.harness/templates/issue-fix/pr-body.md.tpl`;**优先**用 `git push` + `gh pr create` 自动发起,失败再提示本地操作(与 `issue-fix-workflow` 一致)。 +- PR 创建成功后:提醒用户等待 CI 构建 **demo APK**、安装验收,有问题再在 PR / issue 反馈并迭代(见 `issue-fix-workflow` 中「PR 提交后的下一步建议」)。 ## issue #924 示例 From 29d62790d1130e195769b03e35655fa9eee7103d Mon Sep 17 00:00:00 2001 From: zflyluo Date: Thu, 14 May 2026 20:00:36 +0800 Subject: [PATCH 10/10] =?UTF-8?q?fix(TFab):=20=E9=95=BF=E6=8C=89=E5=8F=8D?= =?UTF-8?q?=E9=A6=88=E6=94=B9=E4=B8=BA=E5=BA=95=E8=89=B2=E5=8E=8B=E6=9A=97?= =?UTF-8?q?=EF=BC=8C=E5=8E=BB=E9=99=A4=20Ink=20=E5=9B=9B=E8=A7=92=E7=9F=A9?= =?UTF-8?q?=E5=BD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/src/components/fab/t_fab.dart | 119 ++++++++++++------ 1 file changed, 78 insertions(+), 41 deletions(-) diff --git a/tdesign-component/lib/src/components/fab/t_fab.dart b/tdesign-component/lib/src/components/fab/t_fab.dart index a73f251a5..27b30e3e7 100644 --- a/tdesign-component/lib/src/components/fab/t_fab.dart +++ b/tdesign-component/lib/src/components/fab/t_fab.dart @@ -16,7 +16,13 @@ enum TFabSize { extraSmall // 特小 } -class TFab extends StatelessWidget { +/// 长按态下在底色上叠一层半透明压暗,避免使用 Ink 水波纹带来的矩形裁切问题。 +Color _fabLongPressOverlay(BuildContext context, Color base) { + final overlay = TTheme.of(context).fontGyColor1.withOpacity(0.15); + return Color.alphaBlend(overlay, base); +} + +class TFab extends StatefulWidget { const TFab({ Key? key, this.theme = TFabTheme.defaultTheme, @@ -49,10 +55,17 @@ class TFab extends StatelessWidget { /// 长按回调 final VoidCallback? onLongPress; - bool get showText => text?.isNotEmpty ?? false; + @override + State createState() => _TFabState(); +} + +class _TFabState extends State { + bool _longPressHeld = false; + + bool get showText => widget.text?.isNotEmpty ?? false; EdgeInsets getPadding() { - switch (size) { + switch (widget.size) { case TFabSize.large: return showText ? const EdgeInsets.symmetric(horizontal: 20, vertical: 12) @@ -73,7 +86,7 @@ class TFab extends StatelessWidget { } double getMinWidthOrHeight() { - switch (size) { + switch (widget.size) { case TFabSize.large: return 48.0; case TFabSize.medium: @@ -86,7 +99,7 @@ class TFab extends StatelessWidget { } Color getBackgroundColor(BuildContext context) { - switch (theme) { + switch (widget.theme) { case TFabTheme.primary: return TTheme.of(context).brandColor7; case TFabTheme.defaultTheme: @@ -99,7 +112,7 @@ class TFab extends StatelessWidget { } Color getIconColor(BuildContext context) { - switch (theme) { + switch (widget.theme) { case TFabTheme.primary: return TTheme.of(context).textColorAnti; case TFabTheme.defaultTheme: @@ -112,7 +125,7 @@ class TFab extends StatelessWidget { } double getIconSize() { - switch (size) { + switch (widget.size) { case TFabSize.large: return 24.0; case TFabSize.medium: @@ -125,7 +138,7 @@ class TFab extends StatelessWidget { } double getFontSize() { - switch (size) { + switch (widget.size) { case TFabSize.large: return 16.0; case TFabSize.medium: @@ -137,45 +150,69 @@ class TFab extends StatelessWidget { } } + BorderRadius _borderRadius(BuildContext context) { + return widget.shape == TFabShape.circle + ? BorderRadius.circular(TTheme.of(context).radiusCircle) + : BorderRadius.circular(TTheme.of(context).radiusDefault); + } + @override Widget build(BuildContext context) { - return InkWell( - onTap: onClick, - onLongPress: onLongPress, - child: Container( - padding: getPadding(), - decoration: BoxDecoration( - color: getBackgroundColor(context), + final baseBg = getBackgroundColor(context); + final displayBg = + _longPressHeld ? _fabLongPressOverlay(context, baseBg) : baseBg; + + return Semantics( + button: true, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: widget.onClick, + onLongPress: widget.onLongPress, + onLongPressStart: (_) { + if (widget.onLongPress != null) { + setState(() => _longPressHeld = true); + } + }, + onLongPressEnd: (_) { + setState(() => _longPressHeld = false); + }, + onLongPressCancel: () { + setState(() => _longPressHeld = false); + }, + child: Container( + padding: getPadding(), + decoration: BoxDecoration( + color: displayBg, boxShadow: TTheme.of(context).shadowsMiddle ?? TTheme.of(context).shadowsBase ?? const [], - borderRadius: shape == TFabShape.circle - ? BorderRadius.circular(TTheme.of(context).radiusCircle) - : BorderRadius.circular(TTheme.of(context).radiusDefault)), - height: getMinWidthOrHeight(), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - icon ?? - Icon( - TIcons.add, - size: getIconSize(), - color: getIconColor(context), - ), - if (showText) const SizedBox(width: 4), - if (showText) - TText( - text ?? '', - style: TextStyle( - height: 1.5, - fontWeight: FontWeight.w600, - fontSize: getFontSize(), - color: getIconColor(context), - leadingDistribution: TextLeadingDistribution.even, + borderRadius: _borderRadius(context), + ), + height: getMinWidthOrHeight(), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + widget.icon ?? + Icon( + TIcons.add, + size: getIconSize(), + color: getIconColor(context), + ), + if (showText) const SizedBox(width: 4), + if (showText) + TText( + widget.text ?? '', + style: TextStyle( + height: 1.5, + fontWeight: FontWeight.w600, + fontSize: getFontSize(), + color: getIconColor(context), + leadingDistribution: TextLeadingDistribution.even, + ), ), - ), - ], + ], + ), ), ), );