diff --git a/specs/030-steps-design-alignment/acceptance.md b/specs/030-steps-design-alignment/acceptance.md new file mode 100644 index 000000000..619063911 --- /dev/null +++ b/specs/030-steps-design-alignment/acceptance.md @@ -0,0 +1,103 @@ +# 验收记录 + +## 2026-09-09 可选择 Demo 动态标题 + +- 修改仅限公开 Demo、对应测试、生成片段与 Spec;`TSteps` 生产实现、公开 API 和 Theme 未变。 +- 垂直可选择示例继续固定四项,根据唯一受控状态 `_selectedStep` 派生“已完成步骤 / 当前步骤 / 未完成步骤”;点击后节点视觉与标题语义同步更新,不复制小程序通过 `count` 动态增删步骤的 Demo 数据技巧。 +- Steps Demo 结构/交互测试 3/3 通过,点击第一项后断言 1 个“当前步骤”和 3 个“未完成步骤”;生成片段 `--check` 与三个修改文件的严格 analyze 通过。 +- macOS Flutter 3.32.0 严格复跑 Steps 整页 Golden 出现 light 4.73% / dark 4.70% 差异,与当前分支已记录的 Linux 基线跨平台字体渲染差异一致;本次初始状态的文案、结构和布局不变,未在 macOS 更新 Linux 权威基线。 + +## 2026-09-09 命名构造收敛 + +- 移除未命名 `TSteps(...)` 与同时混合指示器/使用模式的 `TStepsVariant`。 +- `TSteps.progress` 可选 `onChange` 只启用点击,横纵 `dot` 保持相同进度视觉;`TSteps.selectable` 固定垂直点状、必填回调并显示右箭头;`TSteps.display` 不接收进度、状态或交互参数。 +- 公开 Demo、站点文档、生成 API、组件和交互测试同步到新契约。 +- Flutter 3.32.0 与 3.47.0 组件测试均为 28/28、Demo 测试均为 3/3,组件和 Example 严格 analyze 零问题;3.32.0 生产源码覆盖率 245/246 = 99.59%,生成片段 `--check` 通过。 +- 已合并 `origin/develop@335b30bc`;合并后的 Flutter 3.32.0 analyze、Steps 组件测试 28/28、Demo 测试 3/3 通过。macOS 本地共享导航 Golden 差异 light 2.71% / dark 2.67%,未覆盖 Linux 权威基线。 +- Xiaomi Android 16 真机集成测试 1/1 通过,普通 Example 再次安装并核对浅色、深色、滚动、选择与 Toast;选择后受控 `value` 正确更新。当前 Demo 的步骤标题为静态数据,点击后不会像小程序 Demo 一样随索引切换“已完成 / 当前 / 未完成”文案,留给本轮针对性 Review 判断是否应作为 Demo 对齐问题修正。 + +## 2026-09-08 最新 develop 同步与公开契约复审 + +- 已合并 `origin/develop@d2ff7a0a`,保留 Cascader、SideBar 与 Popover 的新增 + Demo、主题及回归登记;Steps 分支不再落后 develop。 +- 修复垂直可选择步骤使用 `customTitle` 或仅提供 `content` 时缺少右箭头的 + 问题;字符串标题与自定义标题现在共用标题行和间距,`onChange` 仍是唯一 + 可选择来源。 +- 重写 Steps 站点文档,删除不可编译的 `activeIndex`、`successIcon`、`simple`、 + `readOnly`、`verticalSelect`、`TStepsStatus.success` 旧用法,补充受控、 + 自定义内容、错误态、纯展示及 breaking change 迁移说明。 +- Flutter 3.32.0:Steps/Text 组件测试 38/38、Steps Demo 3/3 通过,严格 + analyze 零问题;组件路由、Example 与站点文档契约检查通过。 +- 合并后的共享导航 Golden 同时包含 Steps 与 SideBar 变更,不能选择任一旧 + 二进制基线冒充组合结果。当前先保留 develop 基线;固定 Linux + Flutter + 3.32.0 的组合基线仍需在允许挂载仓库的可信环境中重新生成并严格复跑。 + +## 2026-09-08 补充复审与修复 + +本节为本轮结果;下方真机、构建及首轮检查是历史记录,不替代本轮证据。 + +- 基于 PR head `79eff997` 修复;重新核对 `develop@3d5ed773` 未变化。 +- 修复横纵标题、内容和序号文字将默认值作为实例 style 传入的问题。内置 defaults 低于显式 TextTheme、DefaultTextStyle、TTextThemeData;只改字体族/字号不覆盖状态色,实例自定义 Widget 仍优先。 +- 新字段级解析仅对组合 defaults 生效;内部 `TTextThemeSource` 只读取原有投影快照,不导出、不持有新状态。无 defaults 的 TText 及其他组件维持原路径;试验性全局主题和共享消费者改动已撤回。 +- display 横纵均为全实心节点,忽略 value/status,交互仍只看 onChange。 +- 10 个公开代码入口逐个实际打开并与生成片段匹配;受控示例补充 State 初始字段和接入说明。10 份实际生成片段在最小宿主中编译/渲染,受控选择与反馈操作通过;临时拼接测试不作为独立示例实现提交。 +- Flutter 3.32.0 全部非视觉组件测试 2074/2074;3.47.0 Steps/Text/工具测试 73/73,Demo 与片段编译 13/13;两版本严格 analyze 零问题。 +- 生产覆盖率:Steps 238/239 = 99.58%,Text 219/222 = 98.65%。 +- Linux 3.32.0:Steps 整页与共享导航明暗 4 张基线更新后严格复跑。自动 Material 行高不再覆盖默认值,整页 375×3067 → 375×3055;共享导航仅 Steps 区域减少 2px,后续内容顺移。对照实际图/基线图,未见缺字、裁切或状态缺失。未设像素容差,复跑 diff 为 0。 +- 保留原路径的 Text/Button/ActionSheet 18 项结构与视觉测试通过,无需修改其基线。 +- 本轮未重新进行 Android/iOS 真机逐像素核对;不能将代码/Golden 验证等同于与设计稿逐像素完全一致。最新远端 CI 与 CNB Review 以推送后的结果为准。 + +## 2026-09-08 develop 同步复审 + +- 已合并 `origin/develop@3d5ed773`;组件 API、状态所有权与 Token 路径无新增冲突, + 未发现实例默认样式向 Theme 或其他组件泄漏。 +- Flutter 3.32.0 `flutter analyze --fatal-infos` 通过;组件测试 23/23、集中回归 + 清单自测 13/13 通过;生产源码覆盖率 226/233 = 97.00%。 +- macOS 上公开 Demo 结构测试通过;整页 Golden 因 develop 的公共导航标题样式变更 + 出现 light 4.72%、dark 4.69% 的预期差异。权威基线只在 Flutter 3.32.0 Linux + 更新,等待本轮 CI 产出 Linux failure artifact 后核对并提交。 + +## 环境 + +- 分支:`rss1102/breaking/steps-design-alignment` +- 基线:`origin/develop` (`f3e14c43`) +- Figma:页面 `24386:5241`,移动端画板 `28591:34552` +- 真机:Xiaomi Android 16,ADB `40302eeb` + +## 当前已完成 + +- [x] 新版 Figma / 小程序 / Flutter 三方差异与跨端取舍已记录。 +- [x] 组件公开契约、Flutter 状态所有权与 Theme Review 已完成首轮收敛。 +- [x] 真机首次安装运行并执行 uppercase `R`;浅色逐段滚动、点击,切换深色主题。 +- [x] 修复垂直可选择与纯展示节点状态后再次 uppercase `R`,重新进入页面并点击复验。 +- [x] Flutter 3.32.0 全包 analyze 为 0 issue;Steps focused tests 通过。 +- [x] 组件测试 23/23、Demo 结构/交互 2/2、生产源码覆盖率 226/233(97.00%)通过。 +- [x] Flutter 3.32.0 与 3.47.0 analyze、组件测试、Demo 测试、Web release 和 Android debug 构建通过。 +- [x] Flutter 3.32 Linux light/dark Golden 2/2 生成后无更新参数严格复跑 2/2;逐张检查无缺字、无裁切。 +- [x] 示例代码片段生成器完成 11 个旧片段清理、10 个新片段生成,`--check` 通过。 +- [x] Android 16 真机 integration 1/1 通过;普通 APK 安装返回 `Success`,强停后冷启动到 TDesign 首页。 + +## API / Theme Review + +- `value` 是唯一受控值;组件不内部回写,越界值仅在渲染时收敛。 +- `onChange` 是唯一交互/只读开关;垂直回调同时启用点击和右箭头,不再由 Theme 或第二个布尔值控制。 +- `variant` 只负责 `standard`、`dot`、`display` 视觉结构,`status` 只负责当前步骤的 `process/error` 业务状态。 +- `customTitle/customContent` 明确覆盖字符串便利字段;`icon/errorIcon` 保持强类型 `IconData`。 +- 删除持有业务状态的 `TStepsThemeData`;颜色与字体使用 `context.tTheme` 语义 Token,固定节点/连线尺寸记录为组件设计常量。 + +## Golden 人工检查 + +- light/dark 均为 375×3067 完整长页,三组顺序与新版 Figma 一致。 +- 水平/垂直默认、图标、点状及自定义内容完整;错误态包含默认、图标、点状三种。 +- 垂直可选择默认前三项实心、当前项空心;纯展示四项均实心且无箭头。 +- 独立 Steps CJK 子集消除 Linux 缺字方框;深色仅改变语义颜色,不改变结构。 +- 合并含 TabBar #1085 的 `develop@1de424cb` 后,GitHub Actions 在本分支 + `dac8ca16` 的 Flutter 3.32.0 Linux 任务仅产出共享导航 light/dark 两张差异图; + 人工检查确认 TabBar 新 API 渲染保持不变,差异来自 Steps 标题/内容布局向上收敛 + 2px 及其后续内容等量上移。基线采用该任务 artifact 的 `testImage`,未使用 macOS + 抗锯齿结果;更新后仍需由下一轮 Linux CI 严格复跑确认差异为零。 + +## 待完成(当前) + +- GitHub #1084 / CNB #151 已存在;`dac8ca16` 的双版本 analyze 已通过,Linux + Golden 按最新 artifact 修正;完整 CI 与 CodeBuddy Review 待下一次推送后核验。 diff --git a/specs/030-steps-design-alignment/plan.md b/specs/030-steps-design-alignment/plan.md new file mode 100644 index 000000000..595b1a9b6 --- /dev/null +++ b/specs/030-steps-design-alignment/plan.md @@ -0,0 +1,16 @@ +# 实施计划 + +## 本轮补充修复 + +- 以内部只读投影视图为共享解析器提供字段来源,只对组合 defaults 使用字段级解析,保留其他消费者原路径。 +- 标题、内容与数字节点使用低优先级 defaults;display 横纵状态一致。 +- 验证真实代码面板与核心片段接入,复用页面 State;补充组件、Demo、双版本与共享视觉回归。 + +## 首轮计划 + +1. 记录新版 Figma、小程序公开 Demo 与 Flutter 当前实现的结构、视觉和交互差异。 +2. 用 `progress`、`selectable`、`display` 命名构造分离使用模式,收敛 `value`、`status`、`indicator`、`onChange` 的职责及 Theme 所有权。 +3. 重构公开 Demo 为新版 Figma 的三组顺序,并保留小程序受控、只读和自定义内容的操作模式。 +4. 先完成组件、Demo、API/Theme Review 与真机明暗主题实际操作,再生成并严格复跑 light/dark Golden。 +5. 完成双 SDK analyze、测试、覆盖率、构建、生成产物和持久安装。 +6. 独立创建 GitHub/CNB PR,关联 #1027 Steps 条目并请求 CodeBuddy Review。 diff --git a/specs/030-steps-design-alignment/spec.md b/specs/030-steps-design-alignment/spec.md new file mode 100644 index 000000000..98423345e --- /dev/null +++ b/specs/030-steps-design-alignment/spec.md @@ -0,0 +1,40 @@ +# Steps 设计与公开契约收敛 + +## 背景 + +Flutter Steps 的公开 Demo、状态命名及只读/可选择所有权与新版 Figma、小程序公开用法不一致。旧 Demo 分为六组,缺少完整垂直图标和三种错误状态;组件同时由实例与 Theme 持有 `simple`、`readOnly`、`verticalSelect` 业务状态。 + +## 行为契约 + +- `TSteps.progress` 表达普通受控进度:`value` 由调用方持有,越界值只在渲染时收敛;可选 `onChange` 只报告点击索引,不改变指示器的进度语义。 +- `TSteps.selectable` 表达垂直可选择步骤:固定为点状指示器,`onChange` 必填,并显示右箭头。 +- `TSteps.display` 表达纯展示:不接收 `value`、`status` 或 `onChange`,横向与纵向均显示全实心节点及完成态连线。 +- `indicator` 只表达进度步骤条的指示器样式:`standard` 或 `dot`。 +- `status` 是当前 `value` 的业务状态:`process` 或 `error`。 +- `icon` 替换默认数字/完成图标;`customTitle`、`customContent` 分别优先于字符串便利字段。 +- `progress + dot` 在横向与纵向保持相同进度语义;`selectable` 为已完成节点实心、当前节点空心。 + +## Demo 契约 + +- 公开 Demo 按新版 Figma 收敛为“组件类型 / 组件状态 / 特殊类型”三组。 +- 组件类型依次展示水平默认/图标/点状、垂直默认/图标/点状和自定义内容。 +- 错误状态同屏展示默认、图标、点状三种样式。 +- 特殊类型依次展示垂直可选择步骤与纯展示时间线;前者真实更新受控值并反馈选择结果,后者无点击回调。 +- 垂直可选择示例保持固定四项,并根据受控 `value` 派生“已完成步骤 / 当前步骤 / 未完成步骤”标题;不复制小程序通过 `count` 动态增删步骤的 Demo 数据技巧。 +- 可选择示例的核心片段标明 State 宿主、初始字段与 build 接入方式,展示实际运行的回调重建;不声称省略应用壳的片段可以直接运行。 +- 小程序公开 Demo 仍是交互参考:保留受控/只读、横纵方向、默认/点状和自定义内容能力,但不复制动态事件对象或非受控双状态源。 + +## Theme 与尺寸 + +- Steps 不再注册持有业务状态的 ThemeExtension;默认颜色与字体读取 `context.tTheme` 语义 Token。内置文字样式通过共享解析器的低优先级 defaults 入口提供,显式 TTextThemeData、DefaultTextStyle、TextTheme 按字段覆盖,不将默认值伪装成实例覆盖。 +- 22dp 默认节点、8dp 点节点、16/22dp 图标和 1dp 连线属于组件内固定设计尺寸,不作为业务状态或主题模式开放。 +- 明暗主题使用同一结构,由语义 Token 驱动颜色变化。 +- 组合文字解析通过内部只读投影视图辨认 Material 自动补全;不移动共享主题类、不改变无 defaults 的既有 TText 路径。只有 Steps 与 TabBar 使用该低优先级入口。 + +## Breaking change + +- `TStepsStatus.success` 改为 `process`。 +- `TStepsItemData.successIcon` 改为 `icon`。 +- 移除未命名 `TSteps(...)` 构造和 `TStepsVariant`,分别改用 `TSteps.progress`、`TSteps.selectable`、`TSteps.display` 与 `TStepsIndicator`。 +- 移除 `TSteps.simple`、`readOnly`、`verticalSelect`;交互由各命名构造的 `onChange` 契约表达。 +- 删除只持有上述业务状态的 `TStepsThemeData`。 diff --git a/specs/030-steps-design-alignment/tasks.md b/specs/030-steps-design-alignment/tasks.md new file mode 100644 index 000000000..7a3739ace --- /dev/null +++ b/specs/030-steps-design-alignment/tasks.md @@ -0,0 +1,15 @@ +# 任务清单 + +- [x] 完成新版 Figma / 小程序 / Flutter 三方差异输出 +- [x] 收敛组件公开契约与 Theme 所有权 +- [x] 收敛公开 Demo 与代码片段源文件 +- [x] 完成首轮与修复后真机 uppercase `R`、明暗主题及交互操作 +- [x] 完成组件、Demo 与覆盖率本地验证 +- [x] 生成并严格复跑 light/dark Golden,逐张人工检查 +- [x] 完成 Flutter 3.32.0 / latest analyze、测试与构建 +- [x] 完成 Android 真机集成测试与持久安装 +- [x] 已创建 GitHub #1084 / CNB #151;PR 描述关联 GitHub #1027 +- [x] 处理上一轮 CodeBuddy Review 反馈 +- [x] 补充逐字段主题、双轴 display、实际代码面板和核心片段编译回归 +- [x] 用命名构造分离 progress/selectable/display,移除无效参数组合与 onChange 视觉耦合 +- [ ] 本轮修复推送后,检查新 head CI 并完成新一轮 CNB Review diff --git a/specs/030-steps-design-alignment/visual-comparison.md b/specs/030-steps-design-alignment/visual-comparison.md new file mode 100644 index 000000000..eb77a451b --- /dev/null +++ b/specs/030-steps-design-alignment/visual-comparison.md @@ -0,0 +1,25 @@ +# 三方视觉与交互对照 + +## 参考 + +- 新版 Figma:页面节点 `24386:5241`;移动端画板 `28591:34552`;组件类型 `28591:34556`、错误状态 `28591:34615`、特殊类型 `28591:34637`。 +- 小程序公开 Demo:TDesign 小程序 Steps 页面。 +- Flutter:`lib/src/components/steps/` 与 `example/lib/page/t_steps_page.dart`。 + +## 差异 + +| 项目 | 新版 Figma | 小程序公开 Demo | 修改前 Flutter | 收敛目标 | +| --- | --- | --- | --- | --- | +| 分组 | 组件类型、组件状态、特殊类型 | 基础、布局、类型、状态、只读、自定义 | 六个模块 | 三组及 Figma 顺序 | +| 组件类型 | 水平/垂直默认、图标、点状及自定义内容 | 水平/垂直、默认/点状、自定义内容 | 缺完整垂直图标 | 补齐七个例子 | +| 错误状态 | 默认、图标、点状 | 错误态示例 | 仅图标错误态 | 同屏三种错误态 | +| 垂直可选择 | 已完成实心、当前空心、右箭头 | 点击事件更新 current,标题由 current 派生 | 独立 `verticalSelect` 状态 | `TSteps.selectable` 固定垂直点状结构与必填回调;Demo 固定四项并动态更新状态标题 | +| 纯展示 | 四个蓝色实心节点与连线 | readonly 禁止点击 | `readOnly` 与 Theme 重复持有 | `TSteps.display` 不公开进度和交互参数 | +| 状态所有权 | 结构与交互分离 | props/event | Theme 与实例重复持有业务状态 | 命名构造分离 progress/selectable/display,`indicator` 只管指示器 | + +## 已记录的平台差异 + +- 小程序支持 `defaultCurrent` 非受控模式;Flutter 保持受控 `value/onChange`,避免双状态源。 +- 小程序 change 事件是动态事件对象;Flutter 保持 `ValueChanged` 强类型回调。 +- 小程序公开分组和新版 Figma 三组布局差异较大;Flutter 视觉与 Demo 顺序优先新版 Figma,操作模式参考小程序。 +- 新版 Figma 仅提供浅色画板;深色由 TDesign 语义 Token 和严格 Golden 独立验证。 diff --git a/tdesign-component/example/assets/api/steps_api.md b/tdesign-component/example/assets/api/steps_api.md index 0fb385c3c..da47e71db 100644 --- a/tdesign-component/example/assets/api/steps_api.md +++ b/tdesign-component/example/assets/api/steps_api.md @@ -1,18 +1,59 @@ ## API ### TSteps -#### 默认构造方法 + +#### 工厂构造方法 + +##### TSteps.display + +纯展示步骤条。 +所有节点和连线均使用完成态,不接收进度、状态或交互参数。 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| steps | List | - | 步骤条数据 | +| direction | TStepsDirection | TStepsDirection.vertical | 步骤条方向 | + + +##### TSteps.progress + +普通进度步骤条。 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| steps | List | - | 步骤条数据 | +| value | int | 0 | 进度或可选择步骤条当前激活的索引;越界值会收敛到有效范围。 | | direction | TStepsDirection | TStepsDirection.horizontal | 步骤条方向 | +| status | TStepsStatus | TStepsStatus.process | 进度步骤条当前 `value` 对应步骤的状态。 | +| indicator | TStepsIndicator | TStepsIndicator.standard | 进度步骤条的指示器样式。 | +| onChange | ValueChanged? | - | 为空时只读;非空时只报告用户点击的索引;当前进度仍由调用方更新 `value` 控制。 | + + +##### TSteps.selectable + +垂直可选择步骤条。 +固定使用点状指示器并显示右侧箭头:已完成节点实心, +当前与未完成节点空心。`onChange` 只报告用户选择的索引, +调用方需要更新 `value` 完成受控重建。 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | | key | Key? | - | 组件标识,用于区分或保留组件状态。 | -| onChange | ValueChanged? | - | 用户选择步骤时触发;通过更新 `value` 实现受控模式。 | -| readOnly | bool? | - | 步骤条readOnly模式(优先级高于 ThemeData) | -| simple | bool? | - | 步骤条simple模式(优先级高于 ThemeData) | -| status | TStepsStatus | TStepsStatus.success | 步骤条状态。 | | steps | List | - | 步骤条数据 | -| value | int | 0 | 步骤条当前激活的索引 | -| verticalSelect | bool? | - | 步骤条垂直自定义步骤条选择模式(优先级高于 ThemeData) | +| value | int | - | 进度或可选择步骤条当前激活的索引;越界值会收敛到有效范围。 | +| onChange | ValueChanged | - | 用户选择步骤时触发;调用方通过更新 `value` 实现受控模式。 `TSteps.progress` 中为空时只读,非空时不改变指示器视觉; `TSteps.selectable` 中必填。`TSteps.display` 不接收此参数。 | + +#### 公开属性 + +| 属性 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| direction | TStepsDirection | - | 步骤条方向 | +| indicator | TStepsIndicator | - | 进度步骤条的指示器样式。 | +| onChange | ValueChanged? | - | 用户选择步骤时触发;调用方通过更新 `value` 实现受控模式。 `TSteps.progress` 中为空时只读,非空时不改变指示器视觉; `TSteps.selectable` 中必填。`TSteps.display` 不接收此参数。 | +| status | TStepsStatus | - | 进度步骤条当前 `value` 对应步骤的状态。 | +| steps | List | - | 步骤条数据 | +| value | int | - | 进度或可选择步骤条当前激活的索引;越界值会收敛到有效范围。 | ### TStepsItemData @@ -24,7 +65,7 @@ | customContent | Widget? | - | 自定义内容 | | customTitle | Widget? | - | 自定义标题 | | errorIcon | IconData? | - | 失败图标 | -| successIcon | IconData? | - | 成功图标 | +| icon | IconData? | - | 步骤图标;未设置时使用数字或状态图标。 | | title | String? | - | 标题 | @@ -38,11 +79,21 @@ | vertical | 垂直方向 | +### TStepsIndicator +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| standard | 标准的数字或图标步骤条。 | +| dot | 点状进度指示器;横向与纵向均以当前节点实心表达进度。 在 `TSteps.progress` 中是否传入 `TSteps.onChange` 不改变该视觉语义。 | + + ### TStepsStatus #### 枚举值 | 名称 | 说明 | | --- | --- | -| success | 成功状态 | +| process | 当前步骤进行中。 | | error | 错误状态 | diff --git a/tdesign-component/example/assets/code/steps._buildBasicHSteps2.txt b/tdesign-component/example/assets/code/steps._buildBasicHSteps2.txt deleted file mode 100644 index d6bb314df..000000000 --- a/tdesign-component/example/assets/code/steps._buildBasicHSteps2.txt +++ /dev/null @@ -1,13 +0,0 @@ -/// 基本步骤2 - Widget _buildBasicHSteps2(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData(title: 'Steps1', content: 'Content1'), - TStepsItemData(title: 'Steps2', content: 'Content2'), - TStepsItemData(title: 'Steps3', content: 'Content3'), - ], - // 水平方向 - direction: TStepsDirection.horizontal, - value: 1, - ); - } diff --git a/tdesign-component/example/assets/code/steps._buildCustomContent.txt b/tdesign-component/example/assets/code/steps._buildCustomContent.txt new file mode 100644 index 000000000..2a077fa7f --- /dev/null +++ b/tdesign-component/example/assets/code/steps._buildCustomContent.txt @@ -0,0 +1,23 @@ +/// Customize Steps Content 自定义步骤条内容 + Widget _buildCustomContent(BuildContext context) { + return const TSteps.progress( + steps: [ + TStepsItemData(title: 'Finish', content: 'Customize content'), + TStepsItemData( + title: 'Process', + customContent: Padding( + padding: EdgeInsets.only(top: 4, bottom: 16), + child: TImage( + src: 'assets/img/image.png', + width: 280, + height: 100, + fit: BoxFit.cover, + ), + ), + ), + TStepsItemData(title: 'Default', content: 'Customize content'), + ], + value: 1, + direction: TStepsDirection.vertical, + ); + } diff --git a/tdesign-component/example/assets/code/steps._buildDisplaySteps.txt b/tdesign-component/example/assets/code/steps._buildDisplaySteps.txt new file mode 100644 index 000000000..1b333573f --- /dev/null +++ b/tdesign-component/example/assets/code/steps._buildDisplaySteps.txt @@ -0,0 +1,12 @@ +/// Read-only Steps 纯展示步骤条 + Widget _buildDisplaySteps(BuildContext context) { + return const TSteps.display( + steps: [ + TStepsItemData(title: '步骤展示', content: '可自定义此处内容'), + TStepsItemData(title: '步骤展示', content: '可自定义此处内容'), + TStepsItemData(title: '步骤展示', content: '可自定义此处内容'), + TStepsItemData(title: '步骤展示', content: '可自定义此处内容'), + ], + direction: TStepsDirection.vertical, + ); + } diff --git a/tdesign-component/example/assets/code/steps._buildErrorStates.txt b/tdesign-component/example/assets/code/steps._buildErrorStates.txt new file mode 100644 index 000000000..441082a48 --- /dev/null +++ b/tdesign-component/example/assets/code/steps._buildErrorStates.txt @@ -0,0 +1,57 @@ +/// Error 错误状态 + Widget _buildErrorStates(BuildContext context) { + return const Column( + children: [ + TSteps.progress( + steps: [ + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Error', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + ], + value: 1, + status: TStepsStatus.error, + ), + SizedBox(height: 32), + TSteps.progress( + steps: [ + TStepsItemData( + title: 'Finish', + content: 'Content', + icon: TIcons.cart, + ), + TStepsItemData( + title: 'Error', + content: 'Content', + icon: TIcons.cart, + errorIcon: TIcons.close_circle, + ), + TStepsItemData( + title: 'Default', + content: 'Content', + icon: TIcons.cart, + ), + TStepsItemData( + title: 'Default', + content: 'Content', + icon: TIcons.cart, + ), + ], + value: 1, + status: TStepsStatus.error, + ), + SizedBox(height: 32), + TSteps.progress( + steps: [ + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Error', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + ], + value: 1, + status: TStepsStatus.error, + indicator: TStepsIndicator.dot, + ), + ], + ); + } diff --git a/tdesign-component/example/assets/code/steps._buildHErrorSteps2.txt b/tdesign-component/example/assets/code/steps._buildHErrorSteps2.txt deleted file mode 100644 index e0ac4791f..000000000 --- a/tdesign-component/example/assets/code/steps._buildHErrorSteps2.txt +++ /dev/null @@ -1,33 +0,0 @@ -/// 水平错误状态图标步骤条 - Widget _buildHErrorSteps2(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData( - title: 'Steps1', - content: 'Content1', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Error', - content: 'Content2', - successIcon: TIcons.cart, - errorIcon: TIcons.close_circle, - ), - TStepsItemData( - title: 'Steps3', - content: 'Content3', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Steps4', - content: 'Content4', - successIcon: TIcons.cart, - ), - ], - // 水平方向 - direction: TStepsDirection.horizontal, - value: 1, - // 错误状态 - status: TStepsStatus.error, - ); - } diff --git a/tdesign-component/example/assets/code/steps._buildHIconSteps1.txt b/tdesign-component/example/assets/code/steps._buildHIconSteps1.txt deleted file mode 100644 index 9a2424b07..000000000 --- a/tdesign-component/example/assets/code/steps._buildHIconSteps1.txt +++ /dev/null @@ -1,20 +0,0 @@ -/// 水平图标步骤条1 - Widget _buildHIconSteps1(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData( - title: 'Steps1', - content: 'Content1', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Steps2', - content: 'Content2', - successIcon: TIcons.cart, - ), - ], - // 水平方向 - direction: TStepsDirection.horizontal, - value: 0, - ); - } diff --git a/tdesign-component/example/assets/code/steps._buildHReadOnlySteps.txt b/tdesign-component/example/assets/code/steps._buildHReadOnlySteps.txt deleted file mode 100644 index 19a2c226f..000000000 --- a/tdesign-component/example/assets/code/steps._buildHReadOnlySteps.txt +++ /dev/null @@ -1,13 +0,0 @@ -/// Read-only Steps 纯展示水平步骤条 - Widget _buildHReadOnlySteps(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData(title: 'Finish', content: 'content'), - TStepsItemData(title: 'Process', content: 'content'), - TStepsItemData(title: 'Default', content: 'content'), - TStepsItemData(title: 'Default', content: 'content'), - ], - // 只读模式 - readOnly: true, - ); - } diff --git a/tdesign-component/example/assets/code/steps._buildHorizontalDefault.txt b/tdesign-component/example/assets/code/steps._buildHorizontalDefault.txt new file mode 100644 index 000000000..4ff575f22 --- /dev/null +++ b/tdesign-component/example/assets/code/steps._buildHorizontalDefault.txt @@ -0,0 +1,12 @@ +/// Horizontal Default Steps 水平默认步骤条 + Widget _buildHorizontalDefault(BuildContext context) { + return const TSteps.progress( + steps: [ + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Process', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + ], + value: 1, + ); + } diff --git a/tdesign-component/example/assets/code/steps._buildHorizontalDot.txt b/tdesign-component/example/assets/code/steps._buildHorizontalDot.txt new file mode 100644 index 000000000..9e83d21ae --- /dev/null +++ b/tdesign-component/example/assets/code/steps._buildHorizontalDot.txt @@ -0,0 +1,13 @@ +/// Horizontal Dot Steps 水平简略步骤条 + Widget _buildHorizontalDot(BuildContext context) { + return const TSteps.progress( + steps: [ + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Process', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + ], + value: 1, + indicator: TStepsIndicator.dot, + ); + } diff --git a/tdesign-component/example/assets/code/steps._buildHorizontalIcon.txt b/tdesign-component/example/assets/code/steps._buildHorizontalIcon.txt new file mode 100644 index 000000000..8381c71b2 --- /dev/null +++ b/tdesign-component/example/assets/code/steps._buildHorizontalIcon.txt @@ -0,0 +1,12 @@ +/// Horizontal Icon Steps 水平图标步骤条 + Widget _buildHorizontalIcon(BuildContext context) { + return const TSteps.progress( + steps: [ + TStepsItemData(title: 'Finish', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Process', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Default', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Default', content: 'Content', icon: TIcons.cart), + ], + value: 1, + ); + } diff --git a/tdesign-component/example/assets/code/steps._buildSimpleHSteps1.txt b/tdesign-component/example/assets/code/steps._buildSimpleHSteps1.txt deleted file mode 100644 index 712f8aa3e..000000000 --- a/tdesign-component/example/assets/code/steps._buildSimpleHSteps1.txt +++ /dev/null @@ -1,14 +0,0 @@ -/// 水平简略步骤条1 - Widget _buildSimpleHSteps1(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData(title: 'Steps1', content: 'Content1'), - TStepsItemData(title: 'Steps2', content: 'Content2'), - ], - // 水平方向 - direction: TStepsDirection.horizontal, - value: 0, - // 简略模式 - simple: true, - ); - } diff --git a/tdesign-component/example/assets/code/steps._buildVBasicSteps.txt b/tdesign-component/example/assets/code/steps._buildVBasicSteps.txt deleted file mode 100644 index 33dd12cfb..000000000 --- a/tdesign-component/example/assets/code/steps._buildVBasicSteps.txt +++ /dev/null @@ -1,14 +0,0 @@ -/// 垂直默认步骤条 - Widget _buildVBasicSteps(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData(title: 'Finish', content: 'Customize content'), - TStepsItemData(title: 'Process', content: 'Customize content'), - TStepsItemData(title: 'Default', content: 'Customize content'), - TStepsItemData(title: 'Default', content: 'Customize content'), - ], - // 垂直方向 - direction: TStepsDirection.vertical, - value: 1, - ); - } diff --git a/tdesign-component/example/assets/code/steps._buildVCustomContentBaseSteps.txt b/tdesign-component/example/assets/code/steps._buildVCustomContentBaseSteps.txt deleted file mode 100644 index b7e893e40..000000000 --- a/tdesign-component/example/assets/code/steps._buildVCustomContentBaseSteps.txt +++ /dev/null @@ -1,24 +0,0 @@ -/// 垂直自定义内容基本步骤条 - Widget _buildVCustomContentBaseSteps(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData(title: 'Finish', content: 'Customize content'), - TStepsItemData( - title: '这是一个很长很长很长很长的文字,他是用来展示这个步骤的标题', - content: 'Customize content', - customContent: Container( - margin: const EdgeInsets.only(bottom: 16, top: 4), - child: const TImage( - src: 'assets/img/image.png', - variant: TImageVariant.roundedSquare, - ), - ), - ), - TStepsItemData(title: 'Default', content: 'Customize content'), - TStepsItemData(title: 'Default', content: 'Customize content'), - ], - // 垂直方向 - direction: TStepsDirection.vertical, - value: 1, - ); - } diff --git a/tdesign-component/example/assets/code/steps._buildVCustomTitleBaseSteps.txt b/tdesign-component/example/assets/code/steps._buildVCustomTitleBaseSteps.txt deleted file mode 100644 index 23827d4bb..000000000 --- a/tdesign-component/example/assets/code/steps._buildVCustomTitleBaseSteps.txt +++ /dev/null @@ -1,23 +0,0 @@ -/// 垂直自定义标题基本步骤条 - Widget _buildVCustomTitleBaseSteps(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData(title: 'Finish', content: 'Customize content'), - TStepsItemData( - title: 'Process', - content: 'Customize content', - customTitle: const TText( - '这是一个很长很长的自定义标题,可以自动换行的一个标题内容', - softWrap: true, - maxLines: 2, - overflow: TextOverflow.visible, - ), - ), - TStepsItemData(title: 'Default', content: 'Customize content'), - TStepsItemData(title: 'Default', content: 'Customize content'), - ], - // 垂直方向 - direction: TStepsDirection.vertical, - value: 1, - ); - } diff --git a/tdesign-component/example/assets/code/steps._buildVCustomizeSteps.txt b/tdesign-component/example/assets/code/steps._buildVCustomizeSteps.txt deleted file mode 100644 index ae0958d7a..000000000 --- a/tdesign-component/example/assets/code/steps._buildVCustomizeSteps.txt +++ /dev/null @@ -1,22 +0,0 @@ -/// Vertical Customize Steps 垂直自定义步骤条 - Widget _buildVCustomizeSteps(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData(title: 'Selected'), - TStepsItemData(title: 'Selected'), - TStepsItemData(title: 'Selected'), - TStepsItemData(title: 'Please Selected'), - ], - // 垂直方向 - direction: TStepsDirection.vertical, - // 简略模式 - simple: true, - value: _selectedStep, - // 步骤条垂直自定义步骤条选择模式 - verticalSelect: true, - onChange: (index) { - setState(() => _selectedStep = index); - TToast.showText('选择了步骤 ${index + 1}', context: context); - }, - ); - } diff --git a/tdesign-component/example/assets/code/steps._buildVErrorIconSteps.txt b/tdesign-component/example/assets/code/steps._buildVErrorIconSteps.txt deleted file mode 100644 index c96796cac..000000000 --- a/tdesign-component/example/assets/code/steps._buildVErrorIconSteps.txt +++ /dev/null @@ -1,33 +0,0 @@ -/// 垂直错误状态图标步骤条 - Widget _buildVErrorIconSteps(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData( - title: 'Finish', - content: 'Customize content', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Process', - content: 'Customize content', - successIcon: TIcons.cart, - errorIcon: TIcons.close_circle, - ), - TStepsItemData( - title: 'Default', - content: 'Customize content', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Default', - content: 'Customize content', - successIcon: TIcons.cart, - ), - ], - // 垂直方向 - direction: TStepsDirection.vertical, - value: 1, - // 错误状态 - status: TStepsStatus.error, - ); - } diff --git a/tdesign-component/example/assets/code/steps._buildVSimpleSteps.txt b/tdesign-component/example/assets/code/steps._buildVSimpleSteps.txt deleted file mode 100644 index 7aa81c63e..000000000 --- a/tdesign-component/example/assets/code/steps._buildVSimpleSteps.txt +++ /dev/null @@ -1,31 +0,0 @@ -/// 垂直简略步骤条 - Widget _buildVSimpleSteps(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData( - title: 'Finish', - content: 'Customize content', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Process', - content: 'Customize content', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Default', - content: 'Customize content', - successIcon: TIcons.cart), - TStepsItemData( - title: 'Default', - content: 'Customize content', - successIcon: TIcons.cart, - ), - ], - // 垂直方向 - direction: TStepsDirection.vertical, - value: 1, - // 简略模式 - simple: true, - ); - } diff --git a/tdesign-component/example/assets/code/steps._buildVerticalDefault.txt b/tdesign-component/example/assets/code/steps._buildVerticalDefault.txt new file mode 100644 index 000000000..d97911576 --- /dev/null +++ b/tdesign-component/example/assets/code/steps._buildVerticalDefault.txt @@ -0,0 +1,13 @@ +/// Vertical Default Steps 垂直默认步骤条 + Widget _buildVerticalDefault(BuildContext context) { + return const TSteps.progress( + steps: [ + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Process', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + ], + value: 1, + direction: TStepsDirection.vertical, + ); + } diff --git a/tdesign-component/example/assets/code/steps._buildVerticalDot.txt b/tdesign-component/example/assets/code/steps._buildVerticalDot.txt new file mode 100644 index 000000000..1921a0841 --- /dev/null +++ b/tdesign-component/example/assets/code/steps._buildVerticalDot.txt @@ -0,0 +1,14 @@ +/// Vertical Dot Steps 垂直简略步骤条 + Widget _buildVerticalDot(BuildContext context) { + return const TSteps.progress( + steps: [ + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Process', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + ], + value: 1, + direction: TStepsDirection.vertical, + indicator: TStepsIndicator.dot, + ); + } diff --git a/tdesign-component/example/assets/code/steps._buildVerticalIcon.txt b/tdesign-component/example/assets/code/steps._buildVerticalIcon.txt new file mode 100644 index 000000000..ef0a9b776 --- /dev/null +++ b/tdesign-component/example/assets/code/steps._buildVerticalIcon.txt @@ -0,0 +1,13 @@ +/// Vertical Icon Steps 垂直图标步骤条 + Widget _buildVerticalIcon(BuildContext context) { + return const TSteps.progress( + steps: [ + TStepsItemData(title: 'Finish', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Process', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Default', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Default', content: 'Content', icon: TIcons.cart), + ], + value: 1, + direction: TStepsDirection.vertical, + ); + } diff --git a/tdesign-component/example/assets/code/steps._buildVerticalSelectable.txt b/tdesign-component/example/assets/code/steps._buildVerticalSelectable.txt new file mode 100644 index 000000000..f54bda61a --- /dev/null +++ b/tdesign-component/example/assets/code/steps._buildVerticalSelectable.txt @@ -0,0 +1,25 @@ +/// Vertical Customize Steps 垂直自定义步骤条 + /// + /// 核心片段:导入 Flutter material.dart 和 tdesign_flutter.dart, + /// 将此方法放在 StatefulWidget 的 State 中,由 build 调用。 + /// State 声明 `int _selectedStep = 3;` 保存当前步骤; + /// 不在 build 中重新初始化,回调通过 setState 更新受控值。 + Widget _buildVerticalSelectable(BuildContext context) { + return TSteps.selectable( + steps: List.generate( + 4, + (index) => TStepsItemData( + title: index < _selectedStep + ? '已完成步骤' + : index == _selectedStep + ? '当前步骤' + : '未完成步骤', + ), + ), + value: _selectedStep, + onChange: (index) { + setState(() => _selectedStep = index); + TToast.showText('选择了步骤 ${index + 1}', context: context); + }, + ); + } diff --git a/tdesign-component/example/integration_test/steps_example_test.dart b/tdesign-component/example/integration_test/steps_example_test.dart new file mode 100644 index 000000000..73d64fbe2 --- /dev/null +++ b/tdesign-component/example/integration_test/steps_example_test.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:provider/provider.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; +import 'package:tdesign_flutter_example/main.dart' as app; +import 'package:tdesign_flutter_example/provider/theme_mode_provider.dart'; + +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Steps real app navigation, interaction and themes', ( + tester, + ) async { + await app.main(); + await tester.pumpAndSettle(); + final navigator = tester.state( + find.byType(Navigator).first, + ); + + unawaited(navigator.pushNamed('steps')); + await tester.pumpAndSettle(); + await binding.convertFlutterSurfaceToImage(); + await tester.pumpAndSettle(); + await binding.takeScreenshot('steps-light'); + + expect(find.text('01 组件类型'), findsOneWidget); + expect(find.byType(TSteps), findsWidgets); + await tester.scrollUntilVisible( + find.text('Vertical Customize Steps 垂直自定义步骤条'), + 600, + scrollable: find.byType(Scrollable).last, + ); + await tester.pumpAndSettle(); + + var steps = tester.widgetList(find.byType(TSteps)).toList(); + final selectable = steps.firstWhere((item) => item.onChange != null); + expect(selectable.value, 3); + expect(find.text('已完成步骤'), findsNWidgets(3)); + expect(find.text('当前步骤'), findsOneWidget); + expect(find.text('未完成步骤'), findsNothing); + await tester.tap(find.text('已完成步骤').first); + await tester.pumpAndSettle(); + steps = tester.widgetList(find.byType(TSteps)).toList(); + expect(steps.firstWhere((item) => item.onChange != null).value, 0); + expect(find.text('已完成步骤'), findsNothing); + expect(find.text('当前步骤'), findsOneWidget); + expect(find.text('未完成步骤'), findsNWidgets(3)); + expect(steps.last.onChange, isNull); + expect(steps.last.indicator, TStepsIndicator.dot); + + final provider = tester + .element(find.byType(TSteps).first) + .read(); + provider.themeMode = ThemeMode.dark; + await tester.pumpAndSettle(); + await binding.takeScreenshot('steps-dark'); + expect(tester.takeException(), isNull); + }); +} diff --git a/tdesign-component/example/lib/component_test/step_test.dart b/tdesign-component/example/lib/component_test/step_test.dart index b101a3f5e..f4ddb7775 100644 --- a/tdesign-component/example/lib/component_test/step_test.dart +++ b/tdesign-component/example/lib/component_test/step_test.dart @@ -12,9 +12,7 @@ class StepTestApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Step Test', - theme: ThemeData( - primarySwatch: Colors.blue, - ), + theme: ThemeData(primarySwatch: Colors.blue), home: TestPage(), ); } @@ -29,9 +27,9 @@ class TestPage extends StatelessWidget { Widget build(BuildContext context) { // 创建水平步骤条的数据 var horizontalSteps = [ - TStepsItemData(title: 'Step 1', content: 'Horizontal Step 1'), - TStepsItemData(title: 'Step 2', content: 'Horizontal Step 2'), - TStepsItemData(title: 'Step 3', content: 'Horizontal Step 3'), + const TStepsItemData(title: 'Step 1', content: 'Horizontal Step 1'), + const TStepsItemData(title: 'Step 2', content: 'Horizontal Step 2'), + const TStepsItemData(title: 'Step 3', content: 'Horizontal Step 3'), ]; // 创建垂直步骤条的数据 @@ -54,14 +52,12 @@ class TestPage extends StatelessWidget { ], ), ), - TStepsItemData(title: '2025-01-12', content: '今天是星期天'), - TStepsItemData(content: '今天是星期一'), + const TStepsItemData(title: '2025-01-12', content: '今天是星期天'), + const TStepsItemData(content: '今天是星期一'), ]; return Scaffold( - appBar: AppBar( - title: const TText('TSteps Test Page'), - ), + appBar: AppBar(title: const TText('TSteps Test Page')), body: Form( key: _formKey, child: Column( @@ -70,11 +66,11 @@ class TestPage extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.all(16.0), - child: TSteps( + child: TSteps.progress( steps: horizontalSteps, value: 1, // 设置当前激活的步骤索引 direction: TStepsDirection.horizontal, // 设置步骤条方向为水平 - status: TStepsStatus.success, // 设置步骤条状态 + status: TStepsStatus.process, // 设置步骤条状态 ), ), ), @@ -82,11 +78,11 @@ class TestPage extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.all(16.0), - child: TSteps( + child: TSteps.progress( steps: verticalSteps, value: 1, // 设置当前激活的步骤索引 direction: TStepsDirection.vertical, // 设置步骤条方向为垂直 - status: TStepsStatus.success, // 设置步骤条状态 + status: TStepsStatus.process, // 设置步骤条状态 ), ), ), diff --git a/tdesign-component/example/lib/page/t_steps_page.dart b/tdesign-component/example/lib/page/t_steps_page.dart index dbf14bbcc..c4a64686e 100644 --- a/tdesign-component/example/lib/page/t_steps_page.dart +++ b/tdesign-component/example/lib/page/t_steps_page.dart @@ -5,14 +5,14 @@ import '../../base/example_widget.dart'; import '../annotation/example_code.dart'; class TStepsPage extends StatefulWidget { - const TStepsPage({Key? key}) : super(key: key); + const TStepsPage({super.key}); @override - State createState() => _TStepsPageState(); + State createState() => _TStepsPageState(); } class _TStepsPageState extends State { - EdgeInsets exampleItemPadding = const EdgeInsets.symmetric(horizontal: 32); + static const _itemPadding = EdgeInsets.symmetric(horizontal: 16); int _selectedStep = 3; @override @@ -21,337 +21,299 @@ class _TStepsPageState extends State { title: tTitle(), exampleCodeGroup: 'steps', desc: '用于任务步骤展示或任务进度展示。', + compactDemo: true, + showTestModule: false, children: [ - ExampleModule(title: '水平默认步骤条', children: [ - ExampleItem( - desc: '水平默认步骤条', - padding: exampleItemPadding, - builder: _buildBasicHSteps2, - ), - ]), - ExampleModule(title: '水平图标步骤条', children: [ - ExampleItem( - desc: '水平图标步骤条', - padding: exampleItemPadding, - builder: _buildHIconSteps1, - ), - ]), - ExampleModule(title: '水平简略步骤条', children: [ - ExampleItem( - desc: '水平简略步骤条', - padding: exampleItemPadding, - builder: _buildSimpleHSteps1, - ), - ]), - ExampleModule(title: '水平错误状态步骤条', children: [ - ExampleItem( - desc: '水平错误状态图标步骤条', - padding: exampleItemPadding, - builder: _buildHErrorSteps2, - ), - ]), - ExampleModule(title: '垂直步骤条', children: [ - ExampleItem( - desc: '垂直默认步骤条', - padding: exampleItemPadding, - builder: _buildVBasicSteps, - ), - ExampleItem( - desc: '垂直简略步骤条', - padding: exampleItemPadding, - builder: _buildVSimpleSteps, - ), - ExampleItem( - desc: '垂直错误状态图标步骤条', - padding: exampleItemPadding, - builder: _buildVErrorIconSteps, - ), - ExampleItem( - desc: '垂直自定义标题基本步骤条', - padding: exampleItemPadding, - builder: _buildVCustomTitleBaseSteps, - ), - ExampleItem( - desc: '垂直自定义内容基本步骤条', - padding: exampleItemPadding, - builder: _buildVCustomContentBaseSteps, - ), - ]), - ExampleModule(title: 'Extension 步骤条', children: [ - ExampleItem( - desc: 'Read-only Steps 纯展示水平步骤条', - padding: exampleItemPadding, - builder: _buildHReadOnlySteps, - ), - ExampleItem( - desc: 'Controlled Vertical Customize Steps 受控垂直自定义步骤条', - padding: exampleItemPadding, - builder: _buildVCustomizeSteps, - ), - ]), - ], - ); - } - - /// 基本步骤2 - @ExampleCode(group: 'steps') - Widget _buildBasicHSteps2(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData(title: 'Steps1', content: 'Content1'), - TStepsItemData(title: 'Steps2', content: 'Content2'), - TStepsItemData(title: 'Steps3', content: 'Content3'), - ], - // 水平方向 - direction: TStepsDirection.horizontal, - value: 1, - ); - } - - /// 水平图标步骤条1 - @ExampleCode(group: 'steps') - Widget _buildHIconSteps1(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData( - title: 'Steps1', - content: 'Content1', - successIcon: TIcons.cart, + ExampleModule( + title: '组件类型', + children: [ + ExampleItem( + desc: 'Horizontal Default Steps 水平默认步骤条', + padding: _itemPadding, + builder: _buildHorizontalDefault, + methodName: '_buildHorizontalDefault', + ), + ExampleItem( + desc: 'Horizontal Icon Steps 水平图标步骤条', + padding: _itemPadding, + builder: _buildHorizontalIcon, + methodName: '_buildHorizontalIcon', + ), + ExampleItem( + desc: 'Horizontal Dot Steps 水平简略步骤条', + padding: _itemPadding, + builder: _buildHorizontalDot, + methodName: '_buildHorizontalDot', + ), + ExampleItem( + desc: 'Vertical Default Steps 垂直默认步骤条', + padding: _itemPadding, + builder: _buildVerticalDefault, + methodName: '_buildVerticalDefault', + ), + ExampleItem( + desc: 'Vertical Icon Steps 垂直图标步骤条', + padding: _itemPadding, + builder: _buildVerticalIcon, + methodName: '_buildVerticalIcon', + ), + ExampleItem( + desc: 'Vertical Dot Steps 垂直简略步骤条', + padding: _itemPadding, + builder: _buildVerticalDot, + methodName: '_buildVerticalDot', + ), + ExampleItem( + desc: 'Customize Steps Content 自定义步骤条内容', + padding: _itemPadding, + builder: _buildCustomContent, + methodName: '_buildCustomContent', + ), + ], ), - TStepsItemData( - title: 'Steps2', - content: 'Content2', - successIcon: TIcons.cart, + ExampleModule( + title: '组件状态', + children: [ + ExampleItem( + desc: 'Error 错误状态', + padding: _itemPadding, + builder: _buildErrorStates, + methodName: '_buildErrorStates', + ), + ], + ), + ExampleModule( + title: '特殊类型', + children: [ + ExampleItem( + desc: 'Vertical Customize Steps 垂直自定义步骤条', + padding: _itemPadding, + builder: _buildVerticalSelectable, + methodName: '_buildVerticalSelectable', + ), + ExampleItem( + desc: 'Read-only Steps 纯展示步骤条', + padding: _itemPadding, + builder: _buildDisplaySteps, + methodName: '_buildDisplaySteps', + ), + ], ), ], - // 水平方向 - direction: TStepsDirection.horizontal, - value: 0, ); } - /// 水平简略步骤条1 + /// Horizontal Default Steps 水平默认步骤条 @ExampleCode(group: 'steps') - Widget _buildSimpleHSteps1(BuildContext context) { - return TSteps( + Widget _buildHorizontalDefault(BuildContext context) { + return const TSteps.progress( steps: [ - TStepsItemData(title: 'Steps1', content: 'Content1'), - TStepsItemData(title: 'Steps2', content: 'Content2'), + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Process', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), ], - // 水平方向 - direction: TStepsDirection.horizontal, - value: 0, - // 简略模式 - simple: true, + value: 1, ); } - /// 水平错误状态图标步骤条 + /// Horizontal Icon Steps 水平图标步骤条 @ExampleCode(group: 'steps') - Widget _buildHErrorSteps2(BuildContext context) { - return TSteps( + Widget _buildHorizontalIcon(BuildContext context) { + return const TSteps.progress( steps: [ - TStepsItemData( - title: 'Steps1', - content: 'Content1', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Error', - content: 'Content2', - successIcon: TIcons.cart, - errorIcon: TIcons.close_circle, - ), - TStepsItemData( - title: 'Steps3', - content: 'Content3', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Steps4', - content: 'Content4', - successIcon: TIcons.cart, - ), + TStepsItemData(title: 'Finish', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Process', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Default', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Default', content: 'Content', icon: TIcons.cart), ], - // 水平方向 - direction: TStepsDirection.horizontal, value: 1, - // 错误状态 - status: TStepsStatus.error, ); } - /// 垂直默认步骤条 + /// Horizontal Dot Steps 水平简略步骤条 @ExampleCode(group: 'steps') - Widget _buildVBasicSteps(BuildContext context) { - return TSteps( + Widget _buildHorizontalDot(BuildContext context) { + return const TSteps.progress( steps: [ - TStepsItemData(title: 'Finish', content: 'Customize content'), - TStepsItemData(title: 'Process', content: 'Customize content'), - TStepsItemData(title: 'Default', content: 'Customize content'), - TStepsItemData(title: 'Default', content: 'Customize content'), + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Process', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), ], - // 垂直方向 - direction: TStepsDirection.vertical, value: 1, + indicator: TStepsIndicator.dot, ); } - /// 垂直简略步骤条 + /// Vertical Default Steps 垂直默认步骤条 @ExampleCode(group: 'steps') - Widget _buildVSimpleSteps(BuildContext context) { - return TSteps( + Widget _buildVerticalDefault(BuildContext context) { + return const TSteps.progress( steps: [ - TStepsItemData( - title: 'Finish', - content: 'Customize content', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Process', - content: 'Customize content', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Default', - content: 'Customize content', - successIcon: TIcons.cart), - TStepsItemData( - title: 'Default', - content: 'Customize content', - successIcon: TIcons.cart, - ), + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Process', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), ], - // 垂直方向 - direction: TStepsDirection.vertical, value: 1, - // 简略模式 - simple: true, + direction: TStepsDirection.vertical, ); } - /// 垂直错误状态图标步骤条 + /// Vertical Icon Steps 垂直图标步骤条 @ExampleCode(group: 'steps') - Widget _buildVErrorIconSteps(BuildContext context) { - return TSteps( + Widget _buildVerticalIcon(BuildContext context) { + return const TSteps.progress( steps: [ - TStepsItemData( - title: 'Finish', - content: 'Customize content', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Process', - content: 'Customize content', - successIcon: TIcons.cart, - errorIcon: TIcons.close_circle, - ), - TStepsItemData( - title: 'Default', - content: 'Customize content', - successIcon: TIcons.cart, - ), - TStepsItemData( - title: 'Default', - content: 'Customize content', - successIcon: TIcons.cart, - ), + TStepsItemData(title: 'Finish', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Process', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Default', content: 'Content', icon: TIcons.cart), + TStepsItemData(title: 'Default', content: 'Content', icon: TIcons.cart), ], - // 垂直方向 - direction: TStepsDirection.vertical, value: 1, - // 错误状态 - status: TStepsStatus.error, + direction: TStepsDirection.vertical, ); } - /// 垂直自定义标题基本步骤条 + /// Vertical Dot Steps 垂直简略步骤条 @ExampleCode(group: 'steps') - Widget _buildVCustomTitleBaseSteps(BuildContext context) { - return TSteps( + Widget _buildVerticalDot(BuildContext context) { + return const TSteps.progress( steps: [ - TStepsItemData(title: 'Finish', content: 'Customize content'), - TStepsItemData( - title: 'Process', - content: 'Customize content', - customTitle: const TText( - '这是一个很长很长的自定义标题,可以自动换行的一个标题内容', - softWrap: true, - maxLines: 2, - overflow: TextOverflow.visible, - ), - ), - TStepsItemData(title: 'Default', content: 'Customize content'), - TStepsItemData(title: 'Default', content: 'Customize content'), + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Process', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), ], - // 垂直方向 - direction: TStepsDirection.vertical, value: 1, + direction: TStepsDirection.vertical, + indicator: TStepsIndicator.dot, ); } - /// 垂直自定义内容基本步骤条 + /// Customize Steps Content 自定义步骤条内容 @ExampleCode(group: 'steps') - Widget _buildVCustomContentBaseSteps(BuildContext context) { - return TSteps( + Widget _buildCustomContent(BuildContext context) { + return const TSteps.progress( steps: [ TStepsItemData(title: 'Finish', content: 'Customize content'), TStepsItemData( - title: '这是一个很长很长很长很长的文字,他是用来展示这个步骤的标题', - content: 'Customize content', - customContent: Container( - margin: const EdgeInsets.only(bottom: 16, top: 4), - child: const TImage( + title: 'Process', + customContent: Padding( + padding: EdgeInsets.only(top: 4, bottom: 16), + child: TImage( src: 'assets/img/image.png', - variant: TImageVariant.roundedSquare, + width: 280, + height: 100, + fit: BoxFit.cover, ), ), ), TStepsItemData(title: 'Default', content: 'Customize content'), - TStepsItemData(title: 'Default', content: 'Customize content'), ], - // 垂直方向 - direction: TStepsDirection.vertical, value: 1, + direction: TStepsDirection.vertical, ); } - /// Read-only Steps 纯展示水平步骤条 + /// Error 错误状态 @ExampleCode(group: 'steps') - Widget _buildHReadOnlySteps(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData(title: 'Finish', content: 'content'), - TStepsItemData(title: 'Process', content: 'content'), - TStepsItemData(title: 'Default', content: 'content'), - TStepsItemData(title: 'Default', content: 'content'), + Widget _buildErrorStates(BuildContext context) { + return const Column( + children: [ + TSteps.progress( + steps: [ + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Error', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + ], + value: 1, + status: TStepsStatus.error, + ), + SizedBox(height: 32), + TSteps.progress( + steps: [ + TStepsItemData( + title: 'Finish', + content: 'Content', + icon: TIcons.cart, + ), + TStepsItemData( + title: 'Error', + content: 'Content', + icon: TIcons.cart, + errorIcon: TIcons.close_circle, + ), + TStepsItemData( + title: 'Default', + content: 'Content', + icon: TIcons.cart, + ), + TStepsItemData( + title: 'Default', + content: 'Content', + icon: TIcons.cart, + ), + ], + value: 1, + status: TStepsStatus.error, + ), + SizedBox(height: 32), + TSteps.progress( + steps: [ + TStepsItemData(title: 'Finish', content: 'Content'), + TStepsItemData(title: 'Error', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + TStepsItemData(title: 'Default', content: 'Content'), + ], + value: 1, + status: TStepsStatus.error, + indicator: TStepsIndicator.dot, + ), ], - // 只读模式 - readOnly: true, ); } /// Vertical Customize Steps 垂直自定义步骤条 + /// + /// 核心片段:导入 Flutter material.dart 和 tdesign_flutter.dart, + /// 将此方法放在 StatefulWidget 的 State 中,由 build 调用。 + /// State 声明 `int _selectedStep = 3;` 保存当前步骤; + /// 不在 build 中重新初始化,回调通过 setState 更新受控值。 @ExampleCode(group: 'steps') - Widget _buildVCustomizeSteps(BuildContext context) { - return TSteps( - steps: [ - TStepsItemData(title: 'Selected'), - TStepsItemData(title: 'Selected'), - TStepsItemData(title: 'Selected'), - TStepsItemData(title: 'Please Selected'), - ], - // 垂直方向 - direction: TStepsDirection.vertical, - // 简略模式 - simple: true, + Widget _buildVerticalSelectable(BuildContext context) { + return TSteps.selectable( + steps: List.generate( + 4, + (index) => TStepsItemData( + title: index < _selectedStep + ? '已完成步骤' + : index == _selectedStep + ? '当前步骤' + : '未完成步骤', + ), + ), value: _selectedStep, - // 步骤条垂直自定义步骤条选择模式 - verticalSelect: true, onChange: (index) { setState(() => _selectedStep = index); TToast.showText('选择了步骤 ${index + 1}', context: context); }, ); } + + /// Read-only Steps 纯展示步骤条 + @ExampleCode(group: 'steps') + Widget _buildDisplaySteps(BuildContext context) { + return const TSteps.display( + steps: [ + TStepsItemData(title: '步骤展示', content: '可自定义此处内容'), + TStepsItemData(title: '步骤展示', content: '可自定义此处内容'), + TStepsItemData(title: '步骤展示', content: '可自定义此处内容'), + TStepsItemData(title: '步骤展示', content: '可自定义此处内容'), + ], + direction: TStepsDirection.vertical, + ); + } } diff --git a/tdesign-component/example/test/fonts/README.md b/tdesign-component/example/test/fonts/README.md index fa69149e3..1e6897e52 100644 --- a/tdesign-component/example/test/fonts/README.md +++ b/tdesign-component/example/test/fonts/README.md @@ -16,6 +16,12 @@ NoticeBar 新增整页 Golden 的独立字体子集,字符清单见 新增文案的补充 fallback。它排在原字体之后,避免扩充原字体改变 Button、Checkbox、 Upload 等既有 Golden 的字形选择与像素基线。 +`StepsGoldenCJK-Regular.otf` 仅补充 Steps 整页 Golden 所需字形,字符清单见 +`steps_demo_glyphs.txt`。子集 SHA-256 为 +`1c4a9391deda7834d5f3357e20d8bfcdd97be685e021ed31d2bde4166cdf9524`;源文件为 +Android 16 系统的开源 `NotoSansCJK-Regular.ttc` SC 字体面,SHA-256 为 +`3e7e5afaac2c6d872592d76abedac03a51c6f0fc42d11e311ff2816a6c368afe`。 + `SliderGoldenCJK-Regular.otf` 仅补充 Slider 整页 Golden 新增且既有子集未覆盖的 字形,字符清单见 `slider_demo_glyphs.txt`,不会改变既有组件的字形选择。子集 SHA-256 为 `59c5bebba9bf720005fb977a94b9c150b0b8e8c2698c1dca3a66d9f0d7cd9722`。 @@ -89,8 +95,9 @@ ActionSheet、Dialog、DropdownMenu 或 NoticeBar 页面文案时,更新 feedb 更新 Slider 页面文案时,更新 Slider 字符清单。 更新 Picker 页面文案时,更新 Picker 字符清单。 更新 Calendar 页面文案时,更新 Calendar 字符清单。 - 更新 TabBar 页面文案时,更新 TabBar 字符清单。 - 更新 SideBar 页面文案时,更新 SideBar 字符清单。 +更新 Steps 页面文案时,更新 Steps 字符清单。 +更新 SideBar 页面文案时,更新 SideBar 字符清单。 +更新 TabBar 页面文案时,更新 TabBar 字符清单。 更新 Cascader 页面文案时,更新 Cascader 字符清单。 更新 Indexes 页面或城市数据时,更新 Indexes 字符清单。 更新 BackTop 页面文案时,更新 BackTop 字符清单。 diff --git a/tdesign-component/example/test/fonts/StepsGoldenCJK-Regular.otf b/tdesign-component/example/test/fonts/StepsGoldenCJK-Regular.otf new file mode 100644 index 000000000..603f9c7ac Binary files /dev/null and b/tdesign-component/example/test/fonts/StepsGoldenCJK-Regular.otf differ diff --git a/tdesign-component/example/test/fonts/steps_demo_glyphs.txt b/tdesign-component/example/test/fonts/steps_demo_glyphs.txt new file mode 100644 index 000000000..e61db8e84 --- /dev/null +++ b/tdesign-component/example/test/fonts/steps_demo_glyphs.txt @@ -0,0 +1 @@ +Steps 步骤条用于任务步骤展示或任务进度展示。组件类型水平默认图标简略垂直自定义内容状态错误特殊已完成当前纯展示可自定义此处内容选择了步骤一二三四 diff --git a/tdesign-component/example/test/goldens/steps_page_dark.png b/tdesign-component/example/test/goldens/steps_page_dark.png new file mode 100644 index 000000000..042d5de60 Binary files /dev/null and b/tdesign-component/example/test/goldens/steps_page_dark.png differ diff --git a/tdesign-component/example/test/goldens/steps_page_light.png b/tdesign-component/example/test/goldens/steps_page_light.png new file mode 100644 index 000000000..5931ae6c1 Binary files /dev/null and b/tdesign-component/example/test/goldens/steps_page_light.png differ diff --git a/tdesign-component/example/test/steps_demo_golden_test.dart b/tdesign-component/example/test/steps_demo_golden_test.dart new file mode 100644 index 000000000..ff4bb0a4f --- /dev/null +++ b/tdesign-component/example/test/steps_demo_golden_test.dart @@ -0,0 +1,6 @@ +import 'demo_page_test_utils.dart'; +import 'steps_demo_test_spec.dart'; + +void main() { + registerDemoGoldenTests(stepsDemoPageTestSpec); +} diff --git a/tdesign-component/example/test/steps_demo_test_spec.dart b/tdesign-component/example/test/steps_demo_test_spec.dart new file mode 100644 index 000000000..2b8f86c45 --- /dev/null +++ b/tdesign-component/example/test/steps_demo_test_spec.dart @@ -0,0 +1,28 @@ +import 'package:tdesign_flutter/tdesign_flutter.dart'; +import 'package:tdesign_flutter_example/page/t_steps_page.dart'; + +import 'demo_page_test_utils.dart'; + +const stepsDemoPageTestSpec = DemoPageTestSpec( + name: 'steps', + title: 'Steps 步骤条', + page: TStepsPage(), + componentType: TSteps, + expectedComponentCount: 12, + useAlignmentCjkFont: true, + supplementalCjkFontFamily: 'TDesign Steps Golden CJK', + supplementalCjkFontPath: 'test/fonts/StepsGoldenCJK-Regular.otf', + precacheAssetImages: ['assets/img/image.png'], + expectedTexts: [ + '用于任务步骤展示或任务进度展示。', + '01 组件类型', + 'Horizontal Default Steps 水平默认步骤条', + 'Vertical Icon Steps 垂直图标步骤条', + 'Customize Steps Content 自定义步骤条内容', + '02 组件状态', + 'Error 错误状态', + '03 特殊类型', + 'Vertical Customize Steps 垂直自定义步骤条', + 'Read-only Steps 纯展示步骤条', + ], +); diff --git a/tdesign-component/example/test/steps_page_test.dart b/tdesign-component/example/test/steps_page_test.dart new file mode 100644 index 000000000..9e7318c59 --- /dev/null +++ b/tdesign-component/example/test/steps_page_test.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; +import 'package:tdesign_flutter_example/base/example_widget.dart'; +import 'package:tdesign_flutter_example/base/notification_center.dart'; + +import 'demo_page_test_utils.dart'; +import 'steps_demo_test_spec.dart'; + +void main() { + registerDemoStructureTests(stepsDemoPageTestSpec); + + testWidgets('Steps 公开 Demo 的类型、状态与交互契约对齐', (tester) async { + await pumpFullDemoPage(tester, stepsDemoPageTestSpec, ThemeMode.light); + + final steps = tester.widgetList(find.byType(TSteps)).toList(); + expect(steps, hasLength(12)); + expect(steps[0].direction, TStepsDirection.horizontal); + expect(steps[2].indicator, TStepsIndicator.dot); + expect(steps[4].steps.every((item) => item.icon != null), isTrue); + expect(steps[6].steps[1].customContent, isNotNull); + expect( + steps.sublist(7, 10).every((item) => item.status == TStepsStatus.error), + isTrue, + ); + + expect(steps[10].direction, TStepsDirection.vertical); + expect(steps[10].indicator, TStepsIndicator.dot); + expect(steps[10].onChange, isNotNull); + expect(steps[10].value, 3); + expect(find.text('已完成步骤'), findsNWidgets(3)); + expect(find.text('当前步骤'), findsOneWidget); + expect(find.text('未完成步骤'), findsNothing); + expect(steps[11].indicator, TStepsIndicator.dot); + expect(steps[11].onChange, isNull); + expect(steps[11].direction, TStepsDirection.vertical); + + final selectable = find.byType(TSteps).at(10); + await tester.tap( + find.descendant(of: selectable, matching: find.text('已完成步骤')).first, + ); + await tester.pumpAndSettle(); + expect(tester.widget(find.byType(TSteps).at(10)).value, 0); + expect(find.text('已完成步骤'), findsNothing); + expect(find.text('当前步骤'), findsOneWidget); + expect(find.text('未完成步骤'), findsNWidgets(3)); + expect(find.text('选择了步骤 1'), findsOneWidget); + await tester.pump(const Duration(seconds: 3)); + expect(tester.takeException(), isNull); + await disposeDemoPage(tester); + }); + + testWidgets('Steps 所有代码入口展示对应的实际生成片段', (tester) async { + await pumpFullDemoPage(tester, stepsDemoPageTestSpec, ThemeMode.light); + const names = [ + '_buildHorizontalDefault', + '_buildHorizontalIcon', + '_buildHorizontalDot', + '_buildVerticalDefault', + '_buildVerticalIcon', + '_buildVerticalDot', + '_buildCustomContent', + '_buildErrorStates', + '_buildVerticalSelectable', + '_buildDisplaySteps', + ]; + TNotification.postNotification('onApiVisibleChange', {'apiVisible': true}); + await tester.pumpAndSettle(); + expect(find.byType(CodeWrapper), findsNWidgets(names.length)); + for (var index = 0; index < names.length; index++) { + final name = names[index]; + final source = await rootBundle.loadString('assets/code/steps.$name.txt'); + final wrapper = find.byType(CodeWrapper).at(index); + await tester.tap( + find.descendant(of: wrapper, matching: find.text('code')), + ); + await tester.pumpAndSettle(); + final panel = find.byType(Markdown); + expect(tester.widget(panel).data, contains(source)); + Navigator.of(tester.element(panel)).pop(); + await tester.pumpAndSettle(); + } + + await disposeDemoPage(tester); + }); +} diff --git a/tdesign-component/lib/src/components/steps/t_steps.dart b/tdesign-component/lib/src/components/steps/t_steps.dart index 76ffe1cf4..f10f6e468 100644 --- a/tdesign-component/lib/src/components/steps/t_steps.dart +++ b/tdesign-component/lib/src/components/steps/t_steps.dart @@ -1,25 +1,25 @@ import 'package:flutter/material.dart'; import 't_steps_horizontal.dart'; -import 't_steps_theme_data.dart'; +import 't_steps_mode.dart'; import 't_steps_vertical.dart'; /// Steps步骤条数据类型 class TStepsItemData { - TStepsItemData({ + const TStepsItemData({ this.title, this.content, - this.successIcon, + this.icon, this.errorIcon, this.customContent, this.customTitle, }) : assert( - title != null || - customTitle != null || - content != null || - customContent != null, - 'title, content, customContent needs at least one non-empty value', - ); + title != null || + customTitle != null || + content != null || + customContent != null, + 'title, customTitle, content, or customContent must be provided', + ); /// 标题 final String? title; @@ -27,8 +27,8 @@ class TStepsItemData { /// 内容 final String? content; - /// 成功图标 - final IconData? successIcon; + /// 步骤图标;未设置时使用数字或状态图标。 + final IconData? icon; /// 失败图标 final IconData? errorIcon; @@ -49,28 +49,69 @@ enum TStepsDirection { vertical, } +/// 步骤条指示器样式。 +enum TStepsIndicator { + /// 标准的数字或图标步骤条。 + standard, + + /// 点状进度指示器;横向与纵向均以当前节点实心表达进度。 + /// + /// 在 [TSteps.progress] 中是否传入 [TSteps.onChange] 不改变该视觉语义。 + dot, +} + /// steps步骤条状态 enum TStepsStatus { - /// 成功状态 - success, + /// 当前步骤进行中。 + process, /// 错误状态 error, } /// Steps步骤条 -class TSteps extends StatefulWidget { - const TSteps({ +class TSteps extends StatelessWidget { + /// 普通进度步骤条。 + /// + /// [onChange] 为空时只读;非空时只报告用户点击的索引;当前进度仍由调用方更新 [value] 控制。 + const TSteps.progress({ super.key, required this.steps, this.value = 0, this.direction = TStepsDirection.horizontal, - this.status = TStepsStatus.success, - this.simple, - this.readOnly, - this.verticalSelect, + this.status = TStepsStatus.process, + this.indicator = TStepsIndicator.standard, this.onChange, - }); + }) : _mode = TStepsMode.progress; + + /// 垂直可选择步骤条。 + /// + /// 固定使用点状指示器并显示右侧箭头:已完成节点实心, + /// 当前与未完成节点空心。[onChange] 只报告用户选择的索引, + /// 调用方需要更新 [value] 完成受控重建。 + const TSteps.selectable({ + super.key, + required this.steps, + required this.value, + required ValueChanged onChange, + }) : direction = TStepsDirection.vertical, + status = TStepsStatus.process, + indicator = TStepsIndicator.dot, + onChange = onChange, + _mode = TStepsMode.selectable; + + /// 纯展示步骤条。 + /// + /// 所有节点和连线均使用完成态,不接收进度、状态或交互参数。 + const TSteps.display({ + super.key, + required this.steps, + this.direction = TStepsDirection.vertical, + }) : value = 0, + status = TStepsStatus.process, + indicator = TStepsIndicator.dot, + onChange = null, + _mode = TStepsMode.display; /// 步骤条数据 final List steps; @@ -78,31 +119,23 @@ class TSteps extends StatefulWidget { /// 步骤条方向 final TStepsDirection direction; - /// 步骤条当前激活的索引 + /// 进度或可选择步骤条当前激活的索引;越界值会收敛到有效范围。 final int value; - /// 步骤条状态。 + /// 进度步骤条当前 [value] 对应步骤的状态。 final TStepsStatus status; - /// 步骤条simple模式(优先级高于 ThemeData) - final bool? simple; + /// 进度步骤条的指示器样式。 + final TStepsIndicator indicator; - /// 步骤条readOnly模式(优先级高于 ThemeData) - final bool? readOnly; - - /// 步骤条垂直自定义步骤条选择模式(优先级高于 ThemeData) - final bool? verticalSelect; - - /// 用户选择步骤时触发;通过更新 [value] 实现受控模式。 + /// 用户选择步骤时触发;调用方通过更新 [value] 实现受控模式。 + /// + /// [TSteps.progress] 中为空时只读,非空时不改变指示器视觉; + /// [TSteps.selectable] 中必填。[TSteps.display] 不接收此参数。 final ValueChanged? onChange; - /// 子树级主题数据 - - @override - _TStepsState createState() => _TStepsState(); -} + final TStepsMode _mode; -class _TStepsState extends State { int _clampActiveIndex(int index, int length) { if (index < 0) { return 0; @@ -115,35 +148,25 @@ class _TStepsState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context).extension(); - final effectiveIndex = widget.value; - final effectiveSimple = widget.simple ?? theme?.simple ?? false; - final effectiveReadOnly = widget.readOnly ?? theme?.readOnly ?? false; - final effectiveVerticalSelect = - widget.verticalSelect ?? theme?.verticalSelect ?? false; - /// 当前激活的step索引 - final currentActiveIndex = _clampActiveIndex( - effectiveIndex, - widget.steps.length, - ); + final currentActiveIndex = _clampActiveIndex(value, steps.length); - return widget.direction == TStepsDirection.horizontal + return direction == TStepsDirection.horizontal ? TStepsHorizontal( - steps: widget.steps, + steps: steps, activeIndex: currentActiveIndex, - status: widget.status, - simple: effectiveSimple, - readOnly: effectiveReadOnly, - onChange: widget.onChange) + status: status, + indicator: indicator, + mode: _mode, + onChange: onChange, + ) : TStepsVertical( - steps: widget.steps, + steps: steps, activeIndex: currentActiveIndex, - status: widget.status, - simple: effectiveSimple, - readOnly: effectiveReadOnly, - verticalSelect: effectiveVerticalSelect, - onChange: widget.onChange, + status: status, + indicator: indicator, + mode: _mode, + onChange: onChange, ); } } diff --git a/tdesign-component/lib/src/components/steps/t_steps_horizontal.dart b/tdesign-component/lib/src/components/steps/t_steps_horizontal.dart index f3deb5e1c..cd0e27341 100644 --- a/tdesign-component/lib/src/components/steps/t_steps_horizontal.dart +++ b/tdesign-component/lib/src/components/steps/t_steps_horizontal.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 't_steps.dart'; import 't_steps_horizontal_item.dart'; +import 't_steps_mode.dart'; /// Steps步骤条,水平步骤 class TStepsHorizontal extends StatelessWidget { @@ -14,11 +15,11 @@ class TStepsHorizontal extends StatelessWidget { /// 步骤条状态 final TStepsStatus status; - /// 是否为简略模式 - final bool simple; + /// 步骤条指示器样式。 + final TStepsIndicator indicator; - /// 是否为只读模式(纯展示) - final bool readOnly; + /// 根组件已解析的使用模式。 + final TStepsMode mode; /// 选择步骤回调。 final ValueChanged? onChange; @@ -28,8 +29,8 @@ class TStepsHorizontal extends StatelessWidget { required this.steps, required this.activeIndex, required this.status, - required this.simple, - required this.readOnly, + required this.indicator, + required this.mode, this.onChange, }); @@ -46,9 +47,9 @@ class TStepsHorizontal extends StatelessWidget { stepsCount: stepsCount, activeIndex: activeIndex, status: status, - simple: simple, - readOnly: readOnly, - onTap: readOnly ? null : () => onChange?.call(item.key), + indicator: indicator, + mode: mode, + onTap: onChange == null ? null : () => onChange?.call(item.key), ), ); }).toList(); diff --git a/tdesign-component/lib/src/components/steps/t_steps_horizontal_item.dart b/tdesign-component/lib/src/components/steps/t_steps_horizontal_item.dart index 3cf26950e..85df89958 100644 --- a/tdesign-component/lib/src/components/steps/t_steps_horizontal_item.dart +++ b/tdesign-component/lib/src/components/steps/t_steps_horizontal_item.dart @@ -5,7 +5,9 @@ import '../../theme/t_colors.dart'; import '../../theme/t_fonts.dart'; import '../../theme/t_theme.dart'; import '../text/t_text.dart'; +import '../text/t_text_resolve.dart'; import 't_steps.dart'; +import 't_steps_mode.dart'; /// Steps步骤条,水平步骤item class TStepsHorizontalItem extends StatelessWidget { @@ -24,11 +26,11 @@ class TStepsHorizontalItem extends StatelessWidget { /// 步骤条状态 final TStepsStatus status; - /// 是否为简略模式 - final bool simple; + /// 步骤条指示器样式。 + final TStepsIndicator indicator; - /// 是否为只读模式(纯展示) - final bool readOnly; + /// 根组件已解析的使用模式。 + final TStepsMode mode; /// 点击回调。 final VoidCallback? onTap; @@ -40,8 +42,8 @@ class TStepsHorizontalItem extends StatelessWidget { required this.stepsCount, required this.activeIndex, required this.status, - required this.simple, - required this.readOnly, + required this.indicator, + required this.mode, this.onTap, }); @@ -92,10 +94,13 @@ class TStepsHorizontalItem extends StatelessWidget { /// 步骤条icon图标组件,默认为索引文字 Widget? stepsIconWidget = Text( (index + 1).toString(), - style: TextStyle( - color: stepsNumberTextColor, - fontWeight: FontWeight.w400, - fontSize: theme.fontBodyMedium?.size ?? 14, + style: TTextResolve.resolve( + context: context, + defaults: TextStyle( + color: stepsNumberTextColor, + fontWeight: FontWeight.w400, + fontSize: theme.fontBodyMedium?.size ?? 14, + ), ), ); @@ -105,12 +110,8 @@ class TStepsHorizontalItem extends StatelessWidget { } /// 传递了成功的icon图标, 已完成的step都需要显示 - if (data.successIcon != null) { - stepsIconWidget = Icon( - data.successIcon, - color: stepsIconColor, - size: 22, - ); + if (data.icon != null) { + stepsIconWidget = Icon(data.icon, color: stepsIconColor, size: 22); /// 传了图标则不用设置背景色 shouldSetIconWidgetDecoration = false; @@ -122,7 +123,7 @@ class TStepsHorizontalItem extends StatelessWidget { stepsNumberBgColor = theme.errorLightColor; stepsTitleColor = theme.errorNormalColor; - if (simple) { + if (indicator != TStepsIndicator.standard) { simpleStepsIconColor = theme.errorNormalColor; } else { shouldSetIconWidgetDecoration = data.errorIcon == null; @@ -143,9 +144,9 @@ class TStepsHorizontalItem extends StatelessWidget { double iconContainerSize = 22; /// 简略步骤条 - if (simple || readOnly) { - /// readOnly纯展示 - if (readOnly) { + if (indicator != TStepsIndicator.standard) { + /// display 纯展示 + if (mode == TStepsMode.display) { simpleStepsIconColor = theme.brandNormalColor; stepsTitleColor = theme.textColorPrimary; } @@ -156,12 +157,9 @@ class TStepsHorizontalItem extends StatelessWidget { var simpleDecoration = BoxDecoration( color: Colors.transparent, shape: BoxShape.circle, - border: Border.all( - color: simpleStepsIconColor, - width: 1, - ), + border: Border.all(color: simpleStepsIconColor, width: 1), ); - if (activeIndex == index && !readOnly) { + if (mode == TStepsMode.display || activeIndex == index) { simpleDecoration = BoxDecoration( color: simpleStepsIconColor, shape: BoxShape.circle, @@ -170,10 +168,10 @@ class TStepsHorizontalItem extends StatelessWidget { iconWidgetDecoration = simpleDecoration; } - var leftLineColor = (activeIndex >= index || readOnly) + var leftLineColor = (activeIndex >= index || mode == TStepsMode.display) ? theme.brandNormalColor : theme.componentBorderColor; - var rightLineColor = (activeIndex > index || readOnly) + var rightLineColor = (activeIndex > index || mode == TStepsMode.display) ? theme.brandNormalColor : theme.componentBorderColor; @@ -184,8 +182,11 @@ class TStepsHorizontalItem extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - _buildLineWidget(context, - visible: index != 0, color: leftLineColor), + _buildLineWidget( + context, + visible: index != 0, + color: leftLineColor, + ), Container( width: iconContainerSize, height: iconContainerSize, @@ -194,12 +195,15 @@ class TStepsHorizontalItem extends StatelessWidget { decoration: iconWidgetDecoration, child: stepsIconWidget, ), - _buildLineWidget(context, - visible: index != stepsCount - 1, color: rightLineColor), + _buildLineWidget( + context, + visible: index != stepsCount - 1, + color: rightLineColor, + ), ], ), _buildTitleWidget(context, stepsTitleColor), - _buildContentWidget(context) + _buildContentWidget(context), ], ); return onTap == null @@ -228,30 +232,31 @@ class TStepsHorizontalItem extends StatelessWidget { /// 构建标题组件 Widget _buildTitleWidget(BuildContext context, Color stepsTitleColor) { - if (data.customTitle != null) { - return data.customTitle!; - } - final title = data.title ?? ''; - if (title.isEmpty) { + if (data.customTitle == null && title.isEmpty) { return const SizedBox.shrink(); } return Container( margin: const EdgeInsets.only(top: 8), alignment: Alignment.center, - child: TText( - title, - style: TextStyle( - fontWeight: (activeIndex == index && !readOnly) - ? FontWeight.w600 - : FontWeight.w400, - color: stepsTitleColor, - fontSize: context.tTheme.fontBodyMedium?.size ?? 14, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + child: + data.customTitle ?? + TText( + title, + style: TTextResolve.resolve( + context: context, + defaults: TextStyle( + fontWeight: (activeIndex == index && mode != TStepsMode.display) + ? FontWeight.w600 + : FontWeight.w400, + color: stepsTitleColor, + fontSize: context.tTheme.fontBodyMedium?.size ?? 14, + ), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ); } @@ -260,13 +265,17 @@ class TStepsHorizontalItem extends StatelessWidget { return Container( margin: const EdgeInsets.only(top: 4), alignment: Alignment.center, - child: data.customContent ?? + child: + data.customContent ?? TText( data.content ?? '', - style: TextStyle( - fontWeight: FontWeight.w400, - color: context.tTheme.textColorPlaceholder, - fontSize: context.tTheme.fontBodySmall?.size ?? 12, + style: TTextResolve.resolve( + context: context, + defaults: TextStyle( + fontWeight: FontWeight.w400, + color: context.tTheme.textColorPlaceholder, + fontSize: context.tTheme.fontBodySmall?.size ?? 12, + ), ), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/tdesign-component/lib/src/components/steps/t_steps_mode.dart b/tdesign-component/lib/src/components/steps/t_steps_mode.dart new file mode 100644 index 000000000..75b82675d --- /dev/null +++ b/tdesign-component/lib/src/components/steps/t_steps_mode.dart @@ -0,0 +1,5 @@ +/// Steps 内部使用模式。 +/// +/// 该类型不从包入口导出,只用于在根组件与渲染层之间传递 +/// 已由命名构造确定的单一模式。 +enum TStepsMode { progress, selectable, display } diff --git a/tdesign-component/lib/src/components/steps/t_steps_theme_data.dart b/tdesign-component/lib/src/components/steps/t_steps_theme_data.dart deleted file mode 100644 index 33ba52c74..000000000 --- a/tdesign-component/lib/src/components/steps/t_steps_theme_data.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; - -/// 步骤条组件 ThemeExtension -/// -/// 管理 TSteps 的子树级默认布局和交互模式。 -/// 构造器参数优先级高于 ThemeData。 -class TStepsThemeData extends ThemeExtension { - /// 默认 simple 模式 - final bool? simple; - - /// 默认 readOnly 模式 - final bool? readOnly; - - /// 默认垂直自定义步骤条选择模式 - final bool? verticalSelect; - - const TStepsThemeData({this.simple, this.readOnly, this.verticalSelect}); - - @override - TStepsThemeData copyWith({ - bool? simple, - bool? readOnly, - bool? verticalSelect, - }) { - return TStepsThemeData( - simple: simple ?? this.simple, - readOnly: readOnly ?? this.readOnly, - verticalSelect: verticalSelect ?? this.verticalSelect, - ); - } - - @override - TStepsThemeData lerp(ThemeExtension? other, double t) { - if (other is! TStepsThemeData) { - return this; - } - return TStepsThemeData( - simple: t < 0.5 ? simple : other.simple, - readOnly: t < 0.5 ? readOnly : other.readOnly, - verticalSelect: t < 0.5 ? verticalSelect : other.verticalSelect, - ); - } -} diff --git a/tdesign-component/lib/src/components/steps/t_steps_vertical.dart b/tdesign-component/lib/src/components/steps/t_steps_vertical.dart index a6984fe38..aae19c6b2 100644 --- a/tdesign-component/lib/src/components/steps/t_steps_vertical.dart +++ b/tdesign-component/lib/src/components/steps/t_steps_vertical.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 't_steps.dart'; +import 't_steps_mode.dart'; import 't_steps_vertical_item.dart'; /// Steps步骤条,垂直步骤 @@ -14,14 +15,11 @@ class TStepsVertical extends StatelessWidget { /// 步骤条状态 final TStepsStatus status; - /// 是否为简略模式 - final bool simple; + /// 步骤条指示器样式。 + final TStepsIndicator indicator; - /// 是否为只读模式(纯展示) - final bool readOnly; - - /// 垂直模式下是否可点击选择 - final bool verticalSelect; + /// 根组件已解析的使用模式。 + final TStepsMode mode; /// 选择步骤回调。 final ValueChanged? onChange; @@ -31,9 +29,8 @@ class TStepsVertical extends StatelessWidget { required this.steps, required this.activeIndex, required this.status, - required this.simple, - required this.readOnly, - required this.verticalSelect, + required this.indicator, + required this.mode, this.onChange, }); @@ -47,11 +44,9 @@ class TStepsVertical extends StatelessWidget { stepsCount: stepsCount, activeIndex: activeIndex, status: status, - simple: simple, - readOnly: readOnly, - verticalSelect: verticalSelect, - onTap: - readOnly || !verticalSelect ? null : () => onChange?.call(item.key), + indicator: indicator, + mode: mode, + onTap: onChange == null ? null : () => onChange?.call(item.key), ); }).toList(); diff --git a/tdesign-component/lib/src/components/steps/t_steps_vertical_item.dart b/tdesign-component/lib/src/components/steps/t_steps_vertical_item.dart index e6fe14ce5..407c93b9d 100644 --- a/tdesign-component/lib/src/components/steps/t_steps_vertical_item.dart +++ b/tdesign-component/lib/src/components/steps/t_steps_vertical_item.dart @@ -5,7 +5,9 @@ import '../../theme/t_colors.dart'; import '../../theme/t_fonts.dart'; import '../../theme/t_theme.dart'; import '../text/t_text.dart'; +import '../text/t_text_resolve.dart'; import 't_steps.dart'; +import 't_steps_mode.dart'; /// Steps步骤条,垂直步骤item class TStepsVerticalItem extends StatelessWidget { @@ -24,17 +26,11 @@ class TStepsVerticalItem extends StatelessWidget { /// 步骤条状态 final TStepsStatus status; - /// 是否为简略模式 - final bool simple; + /// 步骤条指示器样式。 + final TStepsIndicator indicator; - /// 是否为只读模式(纯展示) - final bool readOnly; - - /// 垂直模式下是否可点击选择 - final bool verticalSelect; - - /// item 标题组件插槽 - final Widget? titleWidget; + /// 根组件已解析的使用模式。 + final TStepsMode mode; /// 点击回调。 final VoidCallback? onTap; @@ -46,10 +42,8 @@ class TStepsVerticalItem extends StatelessWidget { required this.stepsCount, required this.activeIndex, required this.status, - required this.simple, - required this.readOnly, - required this.verticalSelect, - this.titleWidget, + required this.indicator, + required this.mode, this.onTap, }); @@ -100,10 +94,13 @@ class TStepsVerticalItem extends StatelessWidget { /// 步骤条icon图标组件,默认为索引文字 Widget? stepsIconWidget = Text( (index + 1).toString(), - style: TextStyle( - color: stepsNumberTextColor, - fontWeight: FontWeight.w400, - fontSize: theme.fontBodyMedium?.size ?? 14, + style: TTextResolve.resolve( + context: context, + defaults: TextStyle( + color: stepsNumberTextColor, + fontWeight: FontWeight.w400, + fontSize: theme.fontBodyMedium?.size ?? 14, + ), ), ); @@ -113,12 +110,8 @@ class TStepsVerticalItem extends StatelessWidget { } /// 传递了成功的 icon 图标, 已完成的 step 都需要显示 - if (data.successIcon != null) { - stepsIconWidget = Icon( - data.successIcon, - color: stepsIconColor, - size: 22, - ); + if (data.icon != null) { + stepsIconWidget = Icon(data.icon, color: stepsIconColor, size: 22); /// 传了图标则不用设置背景色 shouldSetIconWidgetDecoration = false; @@ -130,7 +123,7 @@ class TStepsVerticalItem extends StatelessWidget { stepsNumberBgColor = theme.errorLightColor; stepsTitleColor = theme.errorNormalColor; - if (simple) { + if (indicator != TStepsIndicator.standard) { simpleStepsIconColor = theme.errorNormalColor; } else { shouldSetIconWidgetDecoration = data.errorIcon == null; @@ -154,9 +147,9 @@ class TStepsVerticalItem extends StatelessWidget { double iconMarginBottom = 8; /// 简略步骤条 - if (simple || readOnly) { - /// readOnly纯展示 - if (readOnly) { + if (indicator != TStepsIndicator.standard) { + /// display 纯展示 + if (mode == TStepsMode.display) { simpleStepsIconColor = theme.brandNormalColor; stepsTitleColor = theme.textColorPrimary; } @@ -168,12 +161,14 @@ class TStepsVerticalItem extends StatelessWidget { var simpleDecoration = BoxDecoration( color: Colors.transparent, shape: BoxShape.circle, - border: Border.all( - color: simpleStepsIconColor, - width: 1, - ), + border: Border.all(color: simpleStepsIconColor, width: 1), ); - if (activeIndex == index && !readOnly) { + final shouldFillDot = + mode == TStepsMode.display || + (mode == TStepsMode.selectable + ? activeIndex > index + : activeIndex == index); + if (shouldFillDot) { simpleDecoration = BoxDecoration( color: simpleStepsIconColor, shape: BoxShape.circle, @@ -206,7 +201,7 @@ class TStepsVerticalItem extends StatelessWidget { decoration: iconWidgetDecoration, child: stepsIconWidget, ), - _buildLineWidget(context) + _buildLineWidget(context), ], ), ), @@ -218,43 +213,53 @@ class TStepsVerticalItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (data.customTitle != null) - data.customTitle! - else if (data.title != null && data.title!.isNotEmpty) + if (data.customTitle != null || + (data.title != null && data.title!.isNotEmpty) || + mode == TStepsMode.selectable) Container( margin: const EdgeInsets.only(bottom: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: TText( - data.title!, - style: TextStyle( - fontWeight: (activeIndex == index && !readOnly) - ? FontWeight.w600 - : FontWeight.w400, - color: stepsTitleColor, - fontSize: theme.fontBodyMedium?.size ?? 14, - height: 1.2, - ), - softWrap: true, - overflow: TextOverflow.visible, - ), + child: + data.customTitle ?? + (data.title != null && data.title!.isNotEmpty + ? TText( + data.title!, + style: TTextResolve.resolve( + context: context, + defaults: TextStyle( + fontWeight: + (activeIndex == index && + mode != TStepsMode.display) + ? FontWeight.w600 + : FontWeight.w400, + color: stepsTitleColor, + fontSize: + theme.fontBodyMedium?.size ?? + 14, + height: 1.2, + ), + ), + softWrap: true, + overflow: TextOverflow.visible, + ) + : const SizedBox.shrink()), ), - verticalSelect - ? Icon( - TIcons.chevron_right, - color: theme.textColorPrimary, - size: 16, - ) - : Container(), + if (mode == TStepsMode.selectable) + Icon( + TIcons.chevron_right, + color: theme.textColorPrimary, + size: 16, + ), ], ), ), - _buildContentWidget(context) + _buildContentWidget(context), ], ), - ) + ), ], ), ), @@ -276,7 +281,7 @@ class TStepsVerticalItem extends StatelessWidget { child: Container( width: 1, height: double.infinity, - color: (activeIndex > index || readOnly) + color: (activeIndex > index || mode == TStepsMode.display) ? context.tTheme.brandNormalColor : context.tTheme.componentBorderColor, ), @@ -294,10 +299,13 @@ class TStepsVerticalItem extends StatelessWidget { else if (data.content != null && data.content!.isNotEmpty) TText( data.content!, - style: TextStyle( - fontWeight: FontWeight.w400, - color: context.tTheme.textColorPlaceholder, - fontSize: context.tTheme.fontBodySmall?.size ?? 12, + style: TTextResolve.resolve( + context: context, + defaults: TextStyle( + fontWeight: FontWeight.w400, + color: context.tTheme.textColorPlaceholder, + fontSize: context.tTheme.fontBodySmall?.size ?? 12, + ), ), ), ], diff --git a/tdesign-component/lib/src/theme/t_component_theme_data.dart b/tdesign-component/lib/src/theme/t_component_theme_data.dart index 2b8a7c078..5eed425de 100644 --- a/tdesign-component/lib/src/theme/t_component_theme_data.dart +++ b/tdesign-component/lib/src/theme/t_component_theme_data.dart @@ -50,7 +50,6 @@ export '../components/skeleton/t_skeleton_theme_data.dart' show TSkeletonThemeData; export '../components/slider/t_slider_theme.dart' show TSliderThemeData; export '../components/stepper/t_stepper_theme_data.dart' show TStepperThemeData; -export '../components/steps/t_steps_theme_data.dart' show TStepsThemeData; export '../components/swipe_cell/t_swipe_cell_theme_data.dart' show TSwipeCellThemeData; export '../components/swiper/t_swiper_theme_data.dart' show TSwiperThemeData; diff --git a/tdesign-component/lib/src/theme/t_theme.dart b/tdesign-component/lib/src/theme/t_theme.dart index ec8e764ce..6ff5997b7 100644 --- a/tdesign-component/lib/src/theme/t_theme.dart +++ b/tdesign-component/lib/src/theme/t_theme.dart @@ -589,7 +589,6 @@ class TMaterialThemeBuilder { const TSideBarThemeData(), const TSliderThemeData(), const TStepperThemeData(), - const TStepsThemeData(), const TSwipeCellThemeData(), const TSwiperThemeData(), const TSwitchThemeData(), diff --git a/tdesign-component/lib/tdesign_flutter.dart b/tdesign-component/lib/tdesign_flutter.dart index c8c97bfa8..d01abd6ea 100644 --- a/tdesign-component/lib/tdesign_flutter.dart +++ b/tdesign-component/lib/tdesign_flutter.dart @@ -87,7 +87,8 @@ export 'src/components/loading/t_loading_controller.dart'; export 'src/components/loading/t_loading_theme_data.dart' show TLoadingThemeData; export 'src/components/message/t_message.dart'; -export 'src/components/message/t_message_theme_data.dart' show TMessageThemeData; +export 'src/components/message/t_message_theme_data.dart' + show TMessageThemeData; export 'src/components/message/t_message_types.dart' show TMessageStatus; export 'src/components/navbar/t_nav_bar.dart'; export 'src/components/navbar/t_nav_bar_theme_data.dart' @@ -152,7 +153,6 @@ export 'src/components/stepper/t_stepper.dart'; export 'src/components/stepper/t_stepper_theme_data.dart' show TStepperThemeData; export 'src/components/steps/t_steps.dart'; -export 'src/components/steps/t_steps_theme_data.dart' show TStepsThemeData; export 'src/components/swipe_cell/t_swipe_cell.dart'; export 'src/components/swipe_cell/t_swipe_cell_action.dart'; export 'src/components/swipe_cell/t_swipe_cell_panel.dart'; diff --git a/tdesign-component/test/components/goldens/navigation_components_dark.png b/tdesign-component/test/components/goldens/navigation_components_dark.png index a706c6473..70aa3bcc0 100644 Binary files a/tdesign-component/test/components/goldens/navigation_components_dark.png and b/tdesign-component/test/components/goldens/navigation_components_dark.png differ diff --git a/tdesign-component/test/components/goldens/navigation_components_light.png b/tdesign-component/test/components/goldens/navigation_components_light.png index 96749959e..b5ae29d62 100644 Binary files a/tdesign-component/test/components/goldens/navigation_components_light.png and b/tdesign-component/test/components/goldens/navigation_components_light.png differ diff --git a/tdesign-component/test/components/navigation_components_golden_test.dart b/tdesign-component/test/components/navigation_components_golden_test.dart index d844a6926..348b8da4a 100644 --- a/tdesign-component/test/components/navigation_components_golden_test.dart +++ b/tdesign-component/test/components/navigation_components_golden_test.dart @@ -125,7 +125,7 @@ class _NavigationComponentsScene extends StatelessWidget { ), const SizedBox(height: 18), const _SectionLabel('Steps'), - TSteps( + const TSteps.progress( value: 1, steps: [ TStepsItemData(title: 'Done', content: 'Complete'), diff --git a/tdesign-component/test/components/steps/t_steps_test.dart b/tdesign-component/test/components/steps/t_steps_test.dart index 1d253fb04..6fe139690 100644 --- a/tdesign-component/test/components/steps/t_steps_test.dart +++ b/tdesign-component/test/components/steps/t_steps_test.dart @@ -1,322 +1,382 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:tdesign_flutter/src/components/steps/t_steps_horizontal_item.dart'; +import 'package:tdesign_flutter/src/components/steps/t_steps_mode.dart'; import 'package:tdesign_flutter/src/components/steps/t_steps_vertical_item.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; void main() { - Widget wrapWithTheme(Widget child, {TStepsThemeData? stepsTheme}) { - final themeExtensions = [ - if (stepsTheme != null) stepsTheme, - ]; - return Theme( - data: ThemeData(extensions: [TThemeData.defaultData()]), - child: MaterialApp( - theme: ThemeData(extensions: themeExtensions), - home: Scaffold(body: child), - ), + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeData(extensions: [TThemeData.defaultData()]), + home: Scaffold(body: child), ); } List buildSteps(int count) { - return List.generate(count, - (i) => TStepsItemData(title: '步骤${i + 1}', content: '内容${i + 1}')); + return List.generate( + count, + (index) => + TStepsItemData(title: '步骤${index + 1}', content: '内容${index + 1}'), + ); } - group('TStepsItemData', () { - test('带参数构造', () { - final data = TStepsItemData(title: '标题', content: '内容'); - expect(data.title, '标题'); - expect(data.content, '内容'); - }); - - test('至少需要一个非空值断言', () { - expect(TStepsItemData.new, throwsA(isA())); - }); + testWidgets('横纵文字继承显式主题,局部字号不改写状态颜色', (tester) async { + final token = TThemeData.defaultData(); + for (final direction in TStepsDirection.values) { + for (final materialTheme in [false, true]) { + final theme = TThemeBuilder.light(token); + await tester.pumpWidget( + MaterialApp( + theme: materialTheme + ? theme.copyWith( + textTheme: const TextTheme( + bodyLarge: TextStyle(fontSize: 21), + ), + ) + : theme.mergeExtension( + const TTextThemeData(textStyle: TextStyle(fontSize: 21)), + ), + home: Scaffold( + body: TSteps.progress( + direction: direction, + steps: buildSteps(3), + value: 1, + status: TStepsStatus.error, + ), + ), + ), + ); + expect(tester.widget(find.text('步骤1')).style?.fontSize, 21); + expect(tester.widget(find.text('步骤2')).style?.fontSize, 21); + expect( + tester.widget(find.text('步骤1')).style?.color, + token.textColorPrimary, + ); + expect( + tester.widget(find.text('步骤2')).style?.color, + token.errorNormalColor, + ); + expect( + tester.widget(find.text('步骤3')).style?.color, + token.textColorPlaceholder, + ); + expect(tester.widget(find.text('内容1')).style?.fontSize, 21); + expect(tester.takeException(), isNull); + } + } + }); - test('自定义内容构造', () { - const custom = Text('自定义'); - final data = TStepsItemData(customContent: custom); - expect(data.customContent, custom); - }); + testWidgets('显式文字颜色生效且 customTitle 保持实例优先', (tester) async { + await tester.pumpWidget( + MaterialApp( + theme: TThemeBuilder.light(TThemeData.defaultData()).mergeExtension( + const TTextThemeData(textStyle: TextStyle(color: Colors.purple)), + ), + home: const Scaffold( + body: TSteps.progress( + steps: [ + TStepsItemData(title: '普通标题'), + TStepsItemData( + customTitle: TText('自定义标题', textColor: Colors.orange), + ), + ], + ), + ), + ), + ); + expect(tester.widget(find.text('普通标题')).style?.color, Colors.purple); + expect(tester.widget(find.text('自定义标题')).style?.color, Colors.orange); }); - group('枚举', () { - test('TStepsDirection 枚举值', () { - expect(TStepsDirection.values.length, 2); - expect(TStepsDirection.values, contains(TStepsDirection.horizontal)); - expect(TStepsDirection.values, contains(TStepsDirection.vertical)); - }); + testWidgets('垂直可选择步骤的自定义标题与纯内容项保留右箭头', (tester) async { + int? selected; + await tester.pumpWidget( + wrap( + TSteps.selectable( + steps: const [ + TStepsItemData(customTitle: Text('自定义标题')), + TStepsItemData(content: '仅内容'), + ], + value: 0, + onChange: (value) => selected = value, + ), + ), + ); - test('TStepsStatus 枚举值', () { - expect(TStepsStatus.values.length, 2); - expect(TStepsStatus.values, contains(TStepsStatus.success)); - expect(TStepsStatus.values, contains(TStepsStatus.error)); - }); + expect(find.byIcon(TIcons.chevron_right), findsNWidgets(2)); + await tester.tap(find.text('自定义标题')); + expect(selected, 0); }); - group('TStepsThemeData', () { - test('默认构造', () { - const data = TStepsThemeData(); - expect(data.simple, null); - }); - - test('带参数构造', () { - const data = TStepsThemeData( - simple: true, - readOnly: true, - verticalSelect: true, + testWidgets('display 横纵节点都为实心且不受 value/status 影响', (tester) async { + for (final direction in TStepsDirection.values) { + await tester.pumpWidget( + wrap(TSteps.display(direction: direction, steps: buildSteps(3))), ); - expect(data.simple, true); - }); - - test('copyWith', () { - const data = TStepsThemeData(simple: false); - final copied = data.copyWith(simple: true, readOnly: true); - expect(copied.simple, true); - expect(copied.readOnly, true); - }); - - test('lerp', () { - const data1 = TStepsThemeData(simple: false); - const data2 = TStepsThemeData(simple: true); - final lerped = data1.lerp(data2, 0.5); - expect(lerped.simple, true); - }); - - test('lerp 非 TStepsThemeData 返回自身', () { - const data = TStepsThemeData(simple: false); - final lerped = data.lerp(null, 0.5); - expect(lerped, same(data)); - }); + final dots = tester + .widgetList(find.byType(Container)) + .where( + (item) => + item.decoration is BoxDecoration && + (item.decoration! as BoxDecoration).shape == BoxShape.circle, + ) + .toList(); + expect(dots, hasLength(3)); + for (final dot in dots) { + final decoration = dot.decoration! as BoxDecoration; + expect(decoration.color, TThemeData.defaultData().brandNormalColor); + expect(decoration.border, isNull); + } + } }); - group('TSteps 基础渲染', () { - testWidgets('horizontal 默认渲染', (tester) async { - await tester.pumpWidget(wrapWithTheme(TSteps(steps: buildSteps(3)))); - expect(find.byType(TSteps), findsOneWidget); - expect(find.text('步骤1'), findsOneWidget); - }); - - testWidgets('vertical 方向渲染', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps(steps: buildSteps(3), direction: TStepsDirection.vertical), - )); - expect(find.byType(TSteps), findsOneWidget); - }); - - testWidgets('使用 value 指定激活索引', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps(steps: buildSteps(3), value: 1), - )); - expect(find.byType(TSteps), findsOneWidget); - }); + group('TStepsItemData', () { + test('文本、图标与自定义内容构造', () { + const customTitle = Text('自定义标题'); + const customContent = Text('自定义内容'); + const data = TStepsItemData( + title: '标题', + content: '内容', + icon: Icons.shopping_cart, + errorIcon: Icons.close, + customTitle: customTitle, + customContent: customContent, + ); - testWidgets('父级更新 value 同步激活索引', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps(steps: buildSteps(3), value: 0), - )); - await tester.pumpWidget(wrapWithTheme( - TSteps(steps: buildSteps(3), value: 2), - )); - expect(find.byType(TSteps), findsOneWidget); + expect(data.title, '标题'); + expect(data.content, '内容'); + expect(data.icon, Icons.shopping_cart); + expect(data.errorIcon, Icons.close); + expect(data.customTitle, customTitle); + expect(data.customContent, customContent); }); - testWidgets('horizontal 通过 onChange 通知受控父级', (tester) async { - int? selectedIndex; - await tester.pumpWidget(wrapWithTheme( - TSteps( - steps: buildSteps(3), - onChange: (index) => selectedIndex = index, - ), - )); - - await tester.tap(find.text('步骤2')); - expect(selectedIndex, 1); + test('至少需要一个非空值', () { + expect(TStepsItemData.new, throwsA(isA())); }); + }); - testWidgets('verticalSelect 通过 onChange 通知受控父级', (tester) async { - int? selectedIndex; - await tester.pumpWidget(wrapWithTheme( - TSteps( - steps: buildSteps(3), - direction: TStepsDirection.vertical, - verticalSelect: true, - onChange: (index) => selectedIndex = index, - ), - )); - - await tester.tap(find.text('步骤3')); - expect(selectedIndex, 2); + group('公开枚举', () { + test('方向、形态和状态语义固定', () { + expect(TStepsDirection.values, [ + TStepsDirection.horizontal, + TStepsDirection.vertical, + ]); + expect(TStepsIndicator.values, [ + TStepsIndicator.standard, + TStepsIndicator.dot, + ]); + expect(TStepsStatus.values, [TStepsStatus.process, TStepsStatus.error]); }); }); - group('TSteps 状态', () { - testWidgets('success 状态', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps(steps: buildSteps(3), status: TStepsStatus.success), - )); - expect(find.byType(TSteps), findsOneWidget); + group('TSteps 渲染与状态', () { + testWidgets('水平与垂直方向渲染', (tester) async { + await tester.pumpWidget(wrap(TSteps.progress(steps: buildSteps(3)))); + expect(find.byType(TStepsHorizontalItem), findsNWidgets(3)); + + await tester.pumpWidget( + wrap( + TSteps.progress( + steps: buildSteps(3), + direction: TStepsDirection.vertical, + ), + ), + ); + expect(find.byType(TStepsVerticalItem), findsNWidgets(3)); }); - testWidgets('error 状态', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps(steps: buildSteps(3), status: TStepsStatus.error), - )); - expect(find.byType(TSteps), findsOneWidget); - }); + testWidgets('value 更新同步当前步骤并收敛越界值', (tester) async { + await tester.pumpWidget( + wrap(TSteps.progress(steps: buildSteps(3), value: -1)), + ); + expect( + tester + .widget( + find.byType(TStepsHorizontalItem).first, + ) + .activeIndex, + 0, + ); - testWidgets('simple 模式', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps(steps: buildSteps(3), simple: true), - )); - expect(find.byType(TSteps), findsOneWidget); + await tester.pumpWidget( + wrap(TSteps.progress(steps: buildSteps(3), value: 9)), + ); + expect( + tester + .widget( + find.byType(TStepsHorizontalItem).first, + ) + .activeIndex, + 2, + ); }); - testWidgets('readOnly 模式', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps(steps: buildSteps(3), readOnly: true), - )); + testWidgets('空数据安全渲染', (tester) async { + await tester.pumpWidget(wrap(const TSteps.progress(steps: []))); expect(find.byType(TSteps), findsOneWidget); + expect(find.byType(TStepsHorizontalItem), findsNothing); }); - testWidgets('verticalSelect 模式', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps( - steps: buildSteps(3), - direction: TStepsDirection.vertical, - verticalSelect: true, + testWidgets('进度状态、指示器与 display 模式透传给 item', (tester) async { + await tester.pumpWidget( + wrap( + TSteps.progress( + steps: buildSteps(2), + status: TStepsStatus.error, + indicator: TStepsIndicator.dot, + ), ), - )); - expect(find.byType(TSteps), findsOneWidget); - }); - }); - - group('TSteps 边界', () { - testWidgets('value 超出上限自动 clamp', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps(steps: buildSteps(3), value: 10), - )); - expect(find.byType(TSteps), findsOneWidget); - }); - - testWidgets('value 为负数自动 clamp', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps(steps: buildSteps(3), value: -1), - )); - expect(find.byType(TSteps), findsOneWidget); - }); - - testWidgets('使用 mergeExtension 子树覆盖', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps( - steps: buildSteps(3), - direction: TStepsDirection.vertical, + ); + var item = tester.widget( + find.byType(TStepsHorizontalItem).first, + ); + expect(item.status, TStepsStatus.error); + expect(item.indicator, TStepsIndicator.dot); + + await tester.pumpWidget( + wrap( + TSteps.display( + steps: buildSteps(2), + direction: TStepsDirection.vertical, + ), ), - stepsTheme: const TStepsThemeData(simple: true), - )); - final item = tester.widget( + ); + final verticalItem = tester.widget( find.byType(TStepsVerticalItem).first, ); - expect(item.simple, isTrue); + expect(verticalItem.status, TStepsStatus.process); + expect(verticalItem.mode, TStepsMode.display); }); - testWidgets('构造器参数覆盖 ThemeData', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps( - steps: buildSteps(3), - direction: TStepsDirection.vertical, - simple: false, - readOnly: true, + testWidgets('图标、错误图标和自定义区域可渲染', (tester) async { + await tester.pumpWidget( + wrap( + const TSteps.progress( + steps: [ + TStepsItemData(icon: Icons.shopping_cart, title: '图标'), + TStepsItemData( + errorIcon: Icons.cancel, + customTitle: Text('自定义标题'), + customContent: Text('自定义内容'), + ), + ], + value: 1, + status: TStepsStatus.error, + direction: TStepsDirection.vertical, + ), ), - stepsTheme: const TStepsThemeData(simple: true, readOnly: false), - )); - final item = tester.widget( - find.byType(TStepsVerticalItem).first, ); - expect(item.simple, isFalse); - expect(item.readOnly, isTrue); + + expect(find.byIcon(Icons.shopping_cart), findsOneWidget); + expect(find.byIcon(Icons.cancel), findsOneWidget); + expect(find.text('自定义标题'), findsOneWidget); + expect(find.text('自定义内容'), findsOneWidget); }); }); - // ============================================================ - // 覆盖率补充 - // ============================================================ - group('TSteps 覆盖率补充', () { - // 覆盖 successIcon 分支(107, 108 行) - testWidgets('successIcon 渲染成功图标', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps( - steps: [ - TStepsItemData(title: '步骤1', successIcon: TIcons.check_circle), - TStepsItemData(title: '步骤2'), - ], - value: 1, + group('受控交互', () { + testWidgets('水平点击只通知父级且不自行改变 value', (tester) async { + int? selected; + await tester.pumpWidget( + wrap( + TSteps.progress( + steps: buildSteps(3), + onChange: (value) => selected = value, + ), ), - )); - expect(find.byType(TSteps), findsOneWidget); - }); + ); - // 覆盖 errorIcon + error status 分支(126-129 行) - testWidgets('error 状态渲染错误图标', (tester) async { - // TStepsStatus 只有 success/error,通过 TStepsItemData 触发 - await tester.pumpWidget(wrapWithTheme( - TSteps( - steps: [ - TStepsItemData(title: '步骤1', errorIcon: TIcons.close_circle), - TStepsItemData(title: '步骤2'), - ], - value: 0, - direction: TStepsDirection.vertical, - ), - )); - expect(find.byType(TSteps), findsOneWidget); + await tester.tap(find.text('步骤2')); + expect(selected, 1); + expect( + tester + .widget( + find.byType(TStepsHorizontalItem).first, + ) + .activeIndex, + 0, + ); }); - // 覆盖 simple + active + !readOnly 分支(167 行) - testWidgets('simple 模式激活索引渲染', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps( - steps: buildSteps(3), - value: 1, - direction: TStepsDirection.vertical, - ), - stepsTheme: const TStepsThemeData(simple: true), - )); - expect(find.byType(TSteps), findsOneWidget); + testWidgets('无 onChange 时步骤不可点击', (tester) async { + await tester.pumpWidget(wrap(TSteps.progress(steps: buildSteps(2)))); + expect( + tester + .widget( + find.byType(TStepsHorizontalItem).first, + ) + .onTap, + isNull, + ); }); - // 覆盖 customTitle 分支(212 行) - testWidgets('customTitle 渲染自定义标题', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps( - steps: [ - TStepsItemData( - title: '步骤1', - customTitle: const Text('自定义标题'), - ), - ], - direction: TStepsDirection.vertical, + testWidgets('垂直 onChange 同时提供选择与箭头语义', (tester) async { + int? selected; + await tester.pumpWidget( + wrap( + TSteps.selectable( + steps: buildSteps(3), + value: 0, + onChange: (value) => selected = value, + ), ), - )); - expect(find.byType(TSteps), findsOneWidget); - expect(find.text('自定义标题'), findsOneWidget); + ); + + final firstItem = tester.widget( + find.byType(TStepsVerticalItem).first, + ); + expect(firstItem.mode, TStepsMode.selectable); + await tester.tap(find.text('步骤3')); + expect(selected, 2); }); - // 覆盖 customContent 分支(276 行) - testWidgets('customContent 渲染自定义内容', (tester) async { - await tester.pumpWidget(wrapWithTheme( - TSteps( - steps: [ - TStepsItemData( - title: '步骤1', - customContent: const Text('自定义内容'), + testWidgets('progress onChange 只启用点击,横纵 dot 保持进度视觉', (tester) async { + for (final direction in TStepsDirection.values) { + int? selected; + await tester.pumpWidget( + wrap( + TSteps.progress( + steps: buildSteps(3), + value: 1, + direction: direction, + indicator: TStepsIndicator.dot, + onChange: (value) => selected = value, ), - ], - direction: TStepsDirection.vertical, - ), - )); - expect(find.byType(TSteps), findsOneWidget); - expect(find.text('自定义内容'), findsOneWidget); + ), + ); + + if (direction == TStepsDirection.vertical) { + final items = tester + .widgetList(find.byType(TStepsVerticalItem)) + .toList(); + expect( + items.every((item) => item.mode == TStepsMode.progress), + isTrue, + ); + expect(find.byIcon(TIcons.chevron_right), findsNothing); + } + + await tester.tap(find.text('步骤3')); + expect(selected, 2); + + final dotDecorations = tester + .widgetList(find.byType(Container)) + .map((item) => item.decoration) + .whereType() + .where((decoration) => decoration.shape == BoxShape.circle) + .toList(); + expect(dotDecorations, hasLength(3)); + expect(dotDecorations[0].color, Colors.transparent); + expect(dotDecorations[0].border, isNotNull); + expect( + dotDecorations[1].color, + TThemeData.defaultData().brandNormalColor, + ); + expect(dotDecorations[2].color, Colors.transparent); + expect(dotDecorations[2].border, isNotNull); + } }); }); } diff --git a/tdesign-component/test/components/steps/t_steps_theme_test.dart b/tdesign-component/test/components/steps/t_steps_theme_test.dart deleted file mode 100644 index 2f104481c..000000000 --- a/tdesign-component/test/components/steps/t_steps_theme_test.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:tdesign_flutter/src/components/steps/t_steps_theme_data.dart'; - -/// TStepsThemeData 纯函数覆盖(copyWith / lerp),用于提升覆盖率。 -void main() { - group('TStepsThemeData 纯函数', () { - const theme = TStepsThemeData( - simple: false, - readOnly: false, - verticalSelect: false, - ); - - test('copyWith 覆盖字段', () { - final copied = theme.copyWith(simple: true, readOnly: true); - expect(copied, isA()); - expect(copied.simple, true); - expect(copied.readOnly, true); - expect(copied.verticalSelect, false); - }); - - test('copyWith cover verticalSelect', () { - final copied = theme.copyWith(verticalSelect: true); - expect(copied.verticalSelect, true); - }); - - test('lerp 在 t=0 / 0.5 / 1 返回 TStepsThemeData', () { - const other = TStepsThemeData(simple: true, readOnly: true, verticalSelect: true); - final at0 = theme.lerp(other, 0); - final atHalf = theme.lerp(other, 0.5); - final at1 = theme.lerp(other, 1); - expect(at0, isA()); - expect(atHalf, isA()); - expect(at1, isA()); - expect(atHalf.simple, true); - expect(at1.simple, true); - }); - - test('lerp other 非同类型时返回 this', () { - expect(theme.lerp(null, 0.5), theme); - }); - - test('lerp cover remaining fields', () { - const other = TStepsThemeData( - simple: true, - readOnly: true, - verticalSelect: true, - ); - final lerped = theme.lerp(other, 0.5); - expect(lerped.simple, true); - expect(lerped.readOnly, true); - expect(lerped.verticalSelect, true); - }); - }); -} diff --git a/tdesign-component/test/components/steps/t_steps_vertical_item_test.dart b/tdesign-component/test/components/steps/t_steps_vertical_item_test.dart index b3804e41a..d7942f9c0 100644 --- a/tdesign-component/test/components/steps/t_steps_vertical_item_test.dart +++ b/tdesign-component/test/components/steps/t_steps_vertical_item_test.dart @@ -1,43 +1,38 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:tdesign_flutter/src/components/steps/t_steps.dart'; +import 'package:tdesign_flutter/src/components/steps/t_steps_mode.dart'; import 'package:tdesign_flutter/src/components/steps/t_steps_vertical_item.dart'; import 'package:tdesign_flutter/src/theme/t_theme.dart'; // TStepsVerticalItem 覆盖率补充 // // 覆盖 build 中以下未覆盖分支: -// - data.successIcon != null:已完成步骤用成功图标替换序号(107-111) -// - status == error && activeIndex == index(非 simple):错误图标分支(126-131) -// - simple && activeIndex == index && !readOnly:简略模式激活项装饰(167) +// - data.icon != null:使用自定义步骤图标 +// - status == error && activeIndex == index:错误图标分支 +// - dot:普通、可选择与纯展示三种节点装饰 void main() { Widget wrapWithTheme(Widget child) { return MaterialApp( - theme: ThemeData( - extensions: [TThemeData.defaultData()], - ), + theme: ThemeData(extensions: [TThemeData.defaultData()]), home: Scaffold(body: child), ); } - final baseData = TStepsItemData(title: '步骤'); + const baseData = TStepsItemData(title: '步骤'); group('TStepsVerticalItem 分支覆盖', () { - testWidgets('successIcon 替换序号图标', (tester) async { + testWidgets('icon 替换序号图标', (tester) async { await tester.pumpWidget( wrapWithTheme( - TStepsVerticalItem( - data: TStepsItemData( - title: '步骤', - successIcon: Icons.check, - ), + const TStepsVerticalItem( + data: TStepsItemData(title: '步骤', icon: Icons.check), index: 1, stepsCount: 2, activeIndex: 0, - status: TStepsStatus.success, - simple: false, - readOnly: false, - verticalSelect: false, + status: TStepsStatus.process, + indicator: TStepsIndicator.standard, + mode: TStepsMode.progress, ), ), ); @@ -47,18 +42,14 @@ void main() { testWidgets('error 状态且为当前激活项时显示错误图标', (tester) async { await tester.pumpWidget( wrapWithTheme( - TStepsVerticalItem( - data: TStepsItemData( - title: '步骤', - errorIcon: Icons.close, - ), + const TStepsVerticalItem( + data: TStepsItemData(title: '步骤', errorIcon: Icons.close), index: 0, stepsCount: 1, activeIndex: 0, status: TStepsStatus.error, - simple: false, - readOnly: false, - verticalSelect: false, + indicator: TStepsIndicator.standard, + mode: TStepsMode.progress, ), ), ); @@ -68,54 +59,123 @@ void main() { testWidgets('error 状态且无 errorIcon 时回落默认关闭图标', (tester) async { await tester.pumpWidget( wrapWithTheme( - TStepsVerticalItem( + const TStepsVerticalItem( data: baseData, index: 0, stepsCount: 1, activeIndex: 0, status: TStepsStatus.error, - simple: false, - readOnly: false, - verticalSelect: false, + indicator: TStepsIndicator.standard, + mode: TStepsMode.progress, ), ), ); expect(find.byType(TStepsVerticalItem), findsOneWidget); }); - testWidgets('simple 模式且为当前激活项时应用激活装饰', (tester) async { + testWidgets('dot 模式且为当前激活项时应用实心装饰', (tester) async { await tester.pumpWidget( wrapWithTheme( - TStepsVerticalItem( + const TStepsVerticalItem( data: baseData, index: 0, stepsCount: 1, activeIndex: 0, - status: TStepsStatus.success, - simple: true, - readOnly: false, - verticalSelect: false, + status: TStepsStatus.process, + indicator: TStepsIndicator.dot, + mode: TStepsMode.progress, ), ), ); expect(find.byType(TStepsVerticalItem), findsOneWidget); }); + testWidgets('可选择 dot 的已完成项为实心、当前项为空心', (tester) async { + await tester.pumpWidget( + wrapWithTheme( + const Column( + children: [ + TStepsVerticalItem( + data: baseData, + index: 0, + stepsCount: 2, + activeIndex: 1, + status: TStepsStatus.process, + indicator: TStepsIndicator.dot, + mode: TStepsMode.selectable, + ), + TStepsVerticalItem( + data: baseData, + index: 1, + stepsCount: 2, + activeIndex: 1, + status: TStepsStatus.process, + indicator: TStepsIndicator.dot, + mode: TStepsMode.selectable, + ), + ], + ), + ), + ); + + final dotDecorations = tester + .widgetList( + find.byWidgetPredicate( + (widget) => + widget is Container && + widget.constraints?.maxWidth == 8 && + widget.decoration is BoxDecoration, + ), + ) + .map((container) => container.decoration! as BoxDecoration) + .toList(); + expect(dotDecorations[0].color, isNot(Colors.transparent)); + expect(dotDecorations[1].color, Colors.transparent); + }); + + testWidgets('display 模式的所有节点均为实心', (tester) async { + await tester.pumpWidget( + wrapWithTheme( + const TStepsVerticalItem( + data: baseData, + index: 1, + stepsCount: 2, + activeIndex: 0, + status: TStepsStatus.process, + indicator: TStepsIndicator.dot, + mode: TStepsMode.display, + ), + ), + ); + + final dot = tester.widget( + find.byWidgetPredicate( + (widget) => + widget is Container && + widget.constraints?.maxWidth == 8 && + widget.decoration is BoxDecoration, + ), + ); + expect( + (dot.decoration! as BoxDecoration).color, + isNot(Colors.transparent), + ); + }); + testWidgets('长标题在可用宽度内自动换行', (tester) async { const longTitle = '这是一个非常非常非常长的步骤标题用于验证垂直步骤可以展示完整内容'; await tester.pumpWidget( wrapWithTheme( - SizedBox( + const SizedBox( width: 260, child: TStepsVerticalItem( data: TStepsItemData(title: longTitle), index: 0, stepsCount: 2, activeIndex: 0, - status: TStepsStatus.success, - simple: false, - readOnly: false, - verticalSelect: true, + status: TStepsStatus.process, + indicator: TStepsIndicator.standard, + mode: TStepsMode.selectable, ), ), ), diff --git a/tdesign-component/test/components/steps/t_steps_widget_test.dart b/tdesign-component/test/components/steps/t_steps_widget_test.dart index 4d04a756d..d18e28d39 100644 --- a/tdesign-component/test/components/steps/t_steps_widget_test.dart +++ b/tdesign-component/test/components/steps/t_steps_widget_test.dart @@ -6,17 +6,16 @@ void main() { Widget wrap(Widget child) => MaterialApp(home: Scaffold(body: child)); final baseSteps = [ - TStepsItemData(title: '步骤一', content: '描述一'), - TStepsItemData(title: '步骤二', content: '描述二'), - TStepsItemData(title: '步骤三', content: '描述三'), + const TStepsItemData(title: '步骤一', content: '描述一'), + const TStepsItemData(title: '步骤二', content: '描述二'), + const TStepsItemData(title: '步骤三', content: '描述三'), ]; group('TSteps widget 级用例', () { - testWidgets('横向 success 可构建', (tester) async { - await tester.pumpWidget(wrap(TSteps( - steps: baseSteps, - value: 1, - ))); + testWidgets('横向 process 可构建', (tester) async { + await tester.pumpWidget( + wrap(TSteps.progress(steps: baseSteps, value: 1)), + ); expect(find.byType(TSteps), findsOneWidget); expect(find.text('步骤一'), findsOneWidget); }); @@ -24,14 +23,18 @@ void main() { testWidgets('横向长标题和内容收口为单行省略', (tester) async { const longTitle = '这是一个非常非常非常长的步骤标题用于验证不会换行'; const longContent = '这是一个非常非常非常长的步骤内容用于验证不会撑高布局'; - await tester.pumpWidget(wrap(TSteps( - steps: [ - TStepsItemData(title: longTitle, content: longContent), - TStepsItemData(title: '步骤二', content: '描述二'), - TStepsItemData(title: '步骤三', content: '描述三'), - ], - value: 1, - ))); + await tester.pumpWidget( + wrap( + const TSteps.progress( + steps: [ + TStepsItemData(title: longTitle, content: longContent), + TStepsItemData(title: '步骤二', content: '描述二'), + TStepsItemData(title: '步骤三', content: '描述三'), + ], + value: 1, + ), + ), + ); final title = tester.widget(find.text(longTitle)); final content = tester.widget(find.text(longContent)); @@ -41,37 +44,41 @@ void main() { expect(content.overflow, TextOverflow.ellipsis); }); - testWidgets('纵向 / error / simple / readOnly / verticalSelect', - (tester) async { - await tester.pumpWidget(wrap(TSteps( - steps: baseSteps, - direction: TStepsDirection.vertical, - status: TStepsStatus.error, - simple: true, - readOnly: true, - verticalSelect: true, - value: 2, - ))); + testWidgets('纵向 error dot 可构建', (tester) async { + await tester.pumpWidget( + wrap( + TSteps.progress( + steps: baseSteps, + direction: TStepsDirection.vertical, + status: TStepsStatus.error, + indicator: TStepsIndicator.dot, + value: 2, + ), + ), + ); expect(find.byType(TSteps), findsOneWidget); }); testWidgets('customTitle / customContent 可构建', (tester) async { - await tester.pumpWidget(wrap(TSteps( - steps: [ - TStepsItemData(customTitle: const Text('自定义标题')), - TStepsItemData(customContent: const Text('自定义内容')), - ], - value: 0, - ))); + await tester.pumpWidget( + wrap( + const TSteps.progress( + steps: [ + TStepsItemData(customTitle: Text('自定义标题')), + TStepsItemData(customContent: Text('自定义内容')), + ], + value: 0, + ), + ), + ); expect(find.text('自定义标题'), findsOneWidget); expect(find.text('自定义内容'), findsOneWidget); }); testWidgets('value 越界被 clamp', (tester) async { - await tester.pumpWidget(wrap(TSteps( - steps: baseSteps, - value: 99, - ))); + await tester.pumpWidget( + wrap(TSteps.progress(steps: baseSteps, value: 99)), + ); expect(find.byType(TSteps), findsOneWidget); }); }); diff --git a/tdesign-component/test/components/theme/t_theme_test.dart b/tdesign-component/test/components/theme/t_theme_test.dart index f47cf1846..d022a887f 100644 --- a/tdesign-component/test/components/theme/t_theme_test.dart +++ b/tdesign-component/test/components/theme/t_theme_test.dart @@ -167,7 +167,6 @@ void main() { expect(theme.extension(), isNull); expect(theme.extension(), isNotNull); expect(theme.extension(), isNotNull); - expect(theme.extension(), isNotNull); expect(theme.extension(), isNotNull); expect(theme.extension(), isNotNull); expect(theme.extension(), isNotNull); diff --git a/tdesign-component/tool/component_test_manifest.dart b/tdesign-component/tool/component_test_manifest.dart index 2d6494246..8bc241062 100644 --- a/tdesign-component/tool/component_test_manifest.dart +++ b/tdesign-component/tool/component_test_manifest.dart @@ -567,6 +567,28 @@ const componentTestManifests = [ ), ], ), + ComponentTestManifest( + name: 'steps', + coverageTargets: ['lib/src/components/steps/'], + componentTests: [ + 'test/components/steps/t_steps_test.dart', + 'test/components/steps/t_steps_vertical_item_test.dart', + 'test/components/steps/t_steps_widget_test.dart', + ], + exampleTests: ['test/steps_page_test.dart'], + visualTests: [ + VisualTestManifest( + name: 'Steps Demo', + workingDirectory: 'example', + testFiles: ['test/steps_demo_golden_test.dart'], + ), + VisualTestManifest( + name: 'Steps shared navigation', + workingDirectory: '.', + testFiles: ['test/components/navigation_components_golden_test.dart'], + ), + ], + ), ComponentTestManifest( name: 'sidebar', coverageTargets: ['lib/src/components/sidebar/'], diff --git a/tdesign-site/docs/components/steps/README.md b/tdesign-site/docs/components/steps/README.md index 4f4ce5e05..7c488db07 100644 --- a/tdesign-site/docs/components/steps/README.md +++ b/tdesign-site/docs/components/steps/README.md @@ -1,754 +1,235 @@ --- title: Steps 步骤条 description: 用于任务步骤展示或任务进度展示。 -spline: base +spline: navigation isComponent: true --- - ## 引入 -在tdesign_flutter/tdesign_flutter.dart中有所有组件的路径。 - ```dart import 'package:tdesign_flutter/tdesign_flutter.dart'; ``` ## 代码演示 -[td_steps_page.dart](https://github.com/Tencent/tdesign-flutter/blob/main/tdesign-component/example/lib/page/td_steps_page.dart) - -### 1 水平默认步骤条 - -水平默认步骤条1 - - - -
-  Widget _buildBasicHSteps1(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Steps1', content: 'Content1'),
-        TStepsItemData(title: 'Steps2', content: 'Content2'),
-      ],
-    );
-  }
- -
- - -水平默认步骤条2 - - - -
-  Widget _buildBasicHSteps2(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Steps1', content: 'Content1'),
-        TStepsItemData(title: 'Steps2', content: 'Content2'),
-        TStepsItemData(title: 'Steps3', content: 'Content3'),
-      ],
-      // 水平方向
-      direction: TStepsDirection.horizontal,
-      activeIndex: 1,
-    );
-  }
- -
- - -水平默认步骤条3 - - - -
-  Widget _buildBasicHSteps3(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Steps1', content: 'Content1'),
-        TStepsItemData(title: 'Steps2', content: 'Content2'),
-        TStepsItemData(title: 'Steps3', content: 'Content3'),
-        TStepsItemData(title: 'Steps4', content: 'Content4'),
-      ],
-      // 水平方向
-      direction: TStepsDirection.horizontal,
-      activeIndex: 1,
-    );
-  }
- -
- -### 1 水平图标步骤条 - -水平图标步骤条1 - - - -
-  Widget _buildHIconSteps1(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(
-          title: 'Steps1',
-          content: 'Content1',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Steps2',
-          content: 'Content2',
-          successIcon: TIcons.cart,
-        ),
-      ],
-      // 水平方向
-      direction: TStepsDirection.horizontal,
-      activeIndex: 0,
-    );
-  }
- -
- - -水平图标步骤条2 - - - -
-  Widget _buildHIconSteps2(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(
-          title: 'Steps1',
-          content: 'Content1',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Steps2',
-          content: 'Content2',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Steps3',
-          content: 'Content3',
-          successIcon: TIcons.cart,
-        ),
-      ],
-      // 水平方向
-      direction: TStepsDirection.horizontal,
-      activeIndex: 1,
-    );
-  }
- -
- +完整示例见 [t_steps_page.dart](https://github.com/Tencent/tdesign-flutter/blob/develop/tdesign-component/example/lib/page/t_steps_page.dart)。 -水平图标步骤条3 - - - -
-  Widget _buildHIconSteps3(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(
-          title: 'Steps1',
-          content: 'Content1',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Steps2',
-          content: 'Content2',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Steps3',
-          content: 'Content3',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Steps4',
-          content: 'Content4',
-          successIcon: TIcons.cart,
-        ),
-      ],
-      // 水平方向
-      direction: TStepsDirection.horizontal,
-      activeIndex: 1,
-    );
-  }
+### 基础步骤条 -
- -### 1 水平简略步骤条 +`direction` 控制水平或垂直布局,默认使用水平布局。 -水平简略步骤条1 - -
-  Widget _buildSimpleHSteps1(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Steps1', content: 'Content1'),
-        TStepsItemData(title: 'Steps2', content: 'Content2'),
-      ],
-      // 水平方向
-      direction: TStepsDirection.horizontal,
-      activeIndex: 0,
-      // 简略模式
-      simple: true,
-    );
-  }
+
+const TSteps.progress(
+  value: 1,
+  steps: [
+    TStepsItemData(title: '已完成', content: '辅助信息'),
+    TStepsItemData(title: '进行中', content: '辅助信息'),
+    TStepsItemData(title: '未完成', content: '辅助信息'),
+  ],
+)
- -水平简略步骤条2 - -
-  Widget _buildSimpleHSteps2(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Steps1', content: 'Content1'),
-        TStepsItemData(title: 'Steps2', content: 'Content2'),
-        TStepsItemData(title: 'Steps3', content: 'Content3'),
-      ],
-      // 水平方向
-      direction: TStepsDirection.horizontal,
-      activeIndex: 1,
-      // 简略模式
-      simple: true,
-    );
-  }
+
+const TSteps.progress(
+  value: 1,
+  direction: TStepsDirection.vertical,
+  steps: [
+    TStepsItemData(title: '已完成', content: '辅助信息'),
+    TStepsItemData(title: '进行中', content: '辅助信息'),
+    TStepsItemData(title: '未完成', content: '辅助信息'),
+  ],
+)
- - -水平简略步骤条3 - - - -
-  Widget _buildSimpleHSteps3(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Steps1', content: 'Content1'),
-        TStepsItemData(title: 'Steps2', content: 'Content2'),
-        TStepsItemData(title: 'Steps3', content: 'Content3'),
-        TStepsItemData(title: 'Steps4', content: 'Content4'),
-      ],
-      // 水平方向
-      direction: TStepsDirection.horizontal,
-      activeIndex: 1,
-      // 简略模式
-      simple: true,
-    );
-  }
- -
- -### 1 水平错误状态步骤条 - -水平错误状态基本步骤条 - - -
-  Widget _buildHErrorSteps1(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Steps1', content: 'Content1'),
-        TStepsItemData(title: 'Error', content: 'Content2'),
-        TStepsItemData(title: 'Steps3', content: 'Content3'),
-        TStepsItemData(title: 'Steps4', content: 'Content4'),
-      ],
-      // 水平方向
-      direction: TStepsDirection.horizontal,
-      activeIndex: 1,
-      // 错误状态
-      status: TStepsStatus.error,
-    );
-  }
+### 图标与点状步骤条 -
- +通过 `icon` 自定义步骤图标;`indicator: TStepsIndicator.dot` 显示点状步骤条。 -水平错误状态图标步骤条 - -
-  Widget _buildHErrorSteps2(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(
-          title: 'Steps1',
-          content: 'Content1',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Error',
-          content: 'Content2',
-          successIcon: TIcons.cart,
-          errorIcon: TIcons.close_circle,
-        ),
-        TStepsItemData(
-          title: 'Steps3',
-          content: 'Content3',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Steps4',
-          content: 'Content4',
-          successIcon: TIcons.cart,
-        ),
-      ],
-      // 水平方向
-      direction: TStepsDirection.horizontal,
-      activeIndex: 1,
-      // 错误状态
-      status: TStepsStatus.error,
-    );
-  }
+
+const TSteps.progress(
+  value: 1,
+  steps: [
+    TStepsItemData(title: '已完成', icon: TIcons.cart),
+    TStepsItemData(title: '进行中', icon: TIcons.cart),
+    TStepsItemData(title: '未完成', icon: TIcons.cart),
+  ],
+)
- -水平错误状态简略步骤条 - -
-  Widget _buildHErrorSteps3(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(
-          title: 'Steps1',
-          content: 'Content1',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Error',
-          content: 'Content2',
-          successIcon: TIcons.cart,
-          errorIcon: TIcons.close_circle,
-        ),
-        TStepsItemData(
-          title: 'Steps3',
-          content: 'Content3',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Steps4',
-          content: 'Content4',
-          successIcon: TIcons.cart,
-        ),
-      ],
-      // 水平方向
-      direction: TStepsDirection.horizontal,
-      activeIndex: 1,
-      // 错误状态
-      status: TStepsStatus.error,
-      // 简略模式
-      simple: true,
-    );
-  }
+
+const TSteps.progress(
+  value: 1,
+  indicator: TStepsIndicator.dot,
+  steps: [
+    TStepsItemData(title: '已完成'),
+    TStepsItemData(title: '进行中'),
+    TStepsItemData(title: '未完成'),
+  ],
+)
- -### 1 垂直步骤条 -垂直默认步骤条 - - +### 错误状态 -
-  Widget _buildVBasicSteps(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Finish', content: 'Customize content'),
-        TStepsItemData(title: 'Process', content: 'Customize content'),
-        TStepsItemData(title: 'Default', content: 'Customize content'),
-        TStepsItemData(title: 'Default', content: 'Customize content'),
-      ],
-      // 垂直方向
-      direction: TStepsDirection.vertical,
-      activeIndex: 1,
-    );
-  }
- -
- +`status` 描述当前 `value` 对应步骤的状态。 -垂直图标步骤条 - -
-  Widget _buildVIconSteps(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(
-          title: 'Finish',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Process',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Default',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Default',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-      ],
-      // 垂直方向
-      direction: TStepsDirection.vertical,
-      activeIndex: 1,
-    );
-  }
+
+const TSteps.progress(
+  value: 1,
+  status: TStepsStatus.error,
+  steps: [
+    TStepsItemData(title: '已完成'),
+    TStepsItemData(title: '错误', errorIcon: TIcons.close_circle),
+    TStepsItemData(title: '未完成'),
+  ],
+)
- -垂直简略步骤条 - - - -
-  Widget _buildVSimpleSteps(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(
-          title: 'Finish',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Process',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-            title: 'Default',
-            content: 'Customize content',
-            successIcon: TIcons.cart),
-        TStepsItemData(
-          title: 'Default',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-      ],
-      // 垂直方向
-      direction: TStepsDirection.vertical,
-      activeIndex: 1,
-      // 简略模式
-      simple: true,
-    );
-  }
+### 受控选择 -
- +`TSteps.selectable` 是垂直受控选择组件。调用方持有 `value`,并在 +`onChange` 中更新它。它固定使用点状指示器和右侧箭头,已完成节点实心, +当前节点空心。 -垂直错误状态基本步骤条 - -
-  Widget _buildVErrorBasicSteps(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Finish', content: 'Customize content'),
-        TStepsItemData(title: 'Process', content: 'Customize content'),
-        TStepsItemData(title: 'Default', content: 'Customize content'),
-        TStepsItemData(title: 'Default', content: 'Customize content'),
-      ],
-      // 垂直方向
-      direction: TStepsDirection.vertical,
-      activeIndex: 1,
-      // 错误状态
-      status: TStepsStatus.error,
-    );
-  }
- -
- - -垂直错误状态图标步骤条 - - +
+int _selectedStep = 2;
 
-  
-  Widget _buildVErrorIconSteps(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(
-          title: 'Finish',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Process',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-          errorIcon: TIcons.close_circle,
-        ),
-        TStepsItemData(
-          title: 'Default',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Default',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-      ],
-      // 垂直方向
-      direction: TStepsDirection.vertical,
-      activeIndex: 1,
-      // 错误状态
-      status: TStepsStatus.error,
-    );
-  }
+Widget build(BuildContext context) { + return TSteps.selectable( + value: _selectedStep, + steps: const [ + TStepsItemData(title: '已完成步骤'), + TStepsItemData(title: '已完成步骤'), + TStepsItemData(title: '当前步骤'), + ], + onChange: (index) { + setState(() => _selectedStep = index); + }, + ); +}
- -垂直错误状态简略步骤条 - - +### 自定义标题与内容 -
-  Widget _buildVErrorSimpleSteps(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(
-          title: 'Finish',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Process',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Default',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-        TStepsItemData(
-          title: 'Default',
-          content: 'Customize content',
-          successIcon: TIcons.cart,
-        ),
-      ],
-      // 垂直方向
-      direction: TStepsDirection.vertical,
-      activeIndex: 1,
-      // 简略模式
-      simple: true,
-      // 错误状态
-      status: TStepsStatus.error,
-    );
-  }
+`customTitle` 和 `customContent` 分别优先于 `title` 和 `content`。 -
- - -垂直自定义标题基本步骤条 - -
-  Widget _buildVCustomTitleBaseSteps(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Finish', content: 'Customize content'),
-        TStepsItemData(
-          title: 'Process',
-          content: 'Customize content',
-          customTitle: const TText(
-            '这是一个很长很长的自定义标题,可以自动换行的一个标题内容',
-            softWrap: true,
-            maxLines: 2,
-            overflow: TextOverflow.visible,
-          ),
-        ),
-        TStepsItemData(title: 'Default', content: 'Customize content'),
-        TStepsItemData(title: 'Default', content: 'Customize content'),
-      ],
-      // 垂直方向
-      direction: TStepsDirection.vertical,
-      activeIndex: 1,
-    );
-  }
+
+const TSteps.progress(
+  value: 1,
+  direction: TStepsDirection.vertical,
+  steps: [
+    TStepsItemData(title: '已完成', content: '辅助信息'),
+    TStepsItemData(
+      customTitle: Text('自定义标题'),
+      customContent: Padding(
+        padding: EdgeInsets.only(top: 4),
+        child: Text('自定义内容'),
+      ),
+    ),
+  ],
+)
- - -垂直自定义内容基本步骤条 - - -
-  Widget _buildVCustomContentBaseSteps(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Finish', content: 'Customize content'),
-        TStepsItemData(
-          title: '这是一个很长很长很长很长的文字,他是用来展示这个步骤的标题',
-          content: 'Customize content',
-          customContent: Container(
-            margin: const EdgeInsets.only(bottom: 16, top: 4),
-            child: const TImage(
-              assetUrl: 'assets/img/image.png',
-              type: TImageType.roundedSquare,
-            ),
-          ),
-        ),
-        TStepsItemData(title: 'Default', content: 'Customize content'),
-        TStepsItemData(title: 'Default', content: 'Customize content'),
-      ],
-      // 垂直方向
-      direction: TStepsDirection.vertical,
-      activeIndex: 1,
-    );
-  }
+### 纯展示步骤条 -
- -### 1 Extension 步骤条 +`TSteps.display` 的节点和连线始终使用完成态品牌色,不接收 `value`、 +`status` 或 `onChange`。 -Read-only Steps 纯展示水平步骤条 - -
-  Widget _buildHReadOnlySteps(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Finish', content: 'content'),
-        TStepsItemData(title: 'Process', content: 'content'),
-        TStepsItemData(title: 'Default', content: 'content'),
-        TStepsItemData(title: 'Default', content: 'content'),
-      ],
-      // 只读模式
-      readOnly: true,
-    );
-  }
+
+const TSteps.display(
+  direction: TStepsDirection.vertical,
+  steps: [
+    TStepsItemData(title: '步骤展示', content: '可自定义此处内容'),
+    TStepsItemData(title: '步骤展示', content: '可自定义此处内容'),
+    TStepsItemData(title: '步骤展示', content: '可自定义此处内容'),
+  ],
+)
- -Read-only Steps 纯展示垂直步骤条 - - +## API -
-  Widget _buildVReadOnlySteps(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Finish', content: 'Customize content'),
-        TStepsItemData(title: 'Process', content: 'Customize content'),
-        TStepsItemData(title: 'Default', content: 'Customize content'),
-        TStepsItemData(title: 'Default', content: 'Customize content'),
-      ],
-      // 垂直方向
-      direction: TStepsDirection.vertical,
-      activeIndex: 0,
-      // 只读模式
-      readOnly: true,
-    );
-  }
+### TSteps.progress -
- - -Vertical Customize Steps 垂直自定义步骤条 - - - -
-  Widget _buildVCustomizeSteps(BuildContext context) {
-    return TSteps(
-      steps: [
-        TStepsItemData(title: 'Selected'),
-        TStepsItemData(title: 'Selected'),
-        TStepsItemData(title: 'Selected'),
-        TStepsItemData(title: 'Please Selected'),
-      ],
-      // 垂直方向
-      direction: TStepsDirection.vertical,
-      // 简略模式
-      simple: true,
-      activeIndex: 3,
-      // 步骤条垂直自定义步骤条选择模式
-      verticalSelect: true,
-    );
-  }
+| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| steps | `List` | - | 步骤数据,必填 | +| value | `int` | `0` | 当前激活索引;越界值仅在渲染时收敛到有效范围 | +| direction | `TStepsDirection` | `horizontal` | 水平或垂直方向 | +| status | `TStepsStatus` | `process` | 当前步骤的进行中或错误状态 | +| indicator | `TStepsIndicator` | `standard` | 标准或点状指示器 | +| onChange | `ValueChanged<int>?` | - | 选择步骤时触发;为空时只读,非空时不改变指示器视觉 | -
- +### TSteps.selectable +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| steps | `List` | - | 步骤数据,必填 | +| value | `int` | - | 当前选中索引,必填 | +| onChange | `ValueChanged<int>` | - | 选择步骤时触发,必填 | -## API -### TSteps -#### 默认构造方法 +### TSteps.display | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| activeIndex | int | 0 | 步骤条当前激活的索引 | -| direction | TStepsDirection | TStepsDirection.horizontal | 步骤条方向 | -| key | Key? | - | 组件标识,用于区分或保留组件状态。 | -| readOnly | bool | false | 步骤条readOnly模式 | -| simple | bool | false | 步骤条simple模式 | -| status | TStepsStatus | TStepsStatus.success | 步骤条状态 | -| steps | List | - | 步骤条数据 | -| verticalSelect | bool | false | 步骤条垂直自定义步骤条选择模式 | - +| steps | `List` | - | 步骤数据,必填 | +| direction | `TStepsDirection` | `vertical` | 水平或垂直方向 | ### TStepsItemData -#### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| content | String? | - | 内容 | -| customContent | Widget? | - | 自定义内容 | -| customTitle | Widget? | - | 自定义标题 | -| errorIcon | IconData? | - | 失败图标 | -| successIcon | IconData? | - | 成功图标 | -| title | String? | - | 标题 | - +| title | `String?` | - | 标题 | +| content | `String?` | - | 辅助内容 | +| icon | `IconData?` | - | 自定义步骤图标 | +| errorIcon | `IconData?` | - | 当前步骤处于错误状态时使用的图标 | +| customTitle | `Widget?` | - | 自定义标题,优先于 `title` | +| customContent | `Widget?` | - | 自定义内容,优先于 `content` | -### TStepsDirection -#### 枚举值 +`title`、`customTitle`、`content`、`customContent` 至少提供一个。 +### 枚举 -| 名称 | 说明 | +| 枚举 | 可选值 | | --- | --- | -| horizontal | - | -| vertical | - | +| `TStepsDirection` | `horizontal`、`vertical` | +| `TStepsStatus` | `process`、`error` | +| `TStepsIndicator` | `standard`、`dot` | +## Breaking Change 迁移 -### TStepsStatus -#### 枚举值 - - -| 名称 | 说明 | +| 旧 API | 新 API | | --- | --- | -| success | - | -| error | - | - - - \ No newline at end of file +| `activeIndex` | `value` | +| `TStepsStatus.success` | `TStepsStatus.process` | +| `successIcon` | `icon` | +| `TSteps(...)` | 进度用 `TSteps.progress(...)`,垂直选择用 `TSteps.selectable(...)`,纯展示用 `TSteps.display(...)` | +| `simple: true` | `TSteps.progress(indicator: TStepsIndicator.dot)` | +| `readOnly` | 使用 `TSteps.progress` 并省略 `onChange` | +| `verticalSelect: true` | `TSteps.selectable(...)` | +| `TStepsVariant.defaultTheme` / `TStepsVariant.dot` | `TStepsIndicator.standard` / `TStepsIndicator.dot` | +| `TStepsVariant.display` | `TSteps.display(...)` | +| `TStepsThemeData` 中的业务开关 | 改用对应的命名构造 |