Skip to content

11 · 组件与主题

目标:知道 88 个 He 组件里哪个能直接用(不再重复造轮子),以及所有样式取值都走 AppTokens

⚠️ 重要:项目里有两份主题文档(.agents/skills/theme-usagedocs/theme-guide.mdtoken 命名已过时。 本文的 token 表全部从 app_tokens.dart 当前代码核实,以本文为准。


开篇:前端概念对照

前端概念本项目
UI 组件库(antd / vant)he_components(自研,88 个)+ tdesign_flutter
import { Button } from 'antd'import 'package:haierenergy/he_components/he_components.dart';单一 barrel
CSS 变量 / design tokenAppTokens
var(--color-primary)tokens.brandPrimaryDefault
var(--spacing-md)AppTokens.spacingMd
font-size: 14pxAppTokens.bodyMdRegular
<img src="...">Assets.images.xxx.image()
图标字体 / SVGAppIcon(name: 'icon_xxx')

一、he_components 组件库

唯一导入入口

dart
import 'package:haierenergy/he_components/he_components.dart';

⚠️ 禁止按具体路径导入(import '.../src/widgets/he_button.dart')——一律走 barrel。

组件总览(88 个,barrel 导出)

lib/he_components/he_components.dart 共 105 行、91 条 export(2 个主题文件 + 88 个组件 + 1 个外部类型再导出)。

按钮类

组件用途前端类比
he_button通用按钮<Button>
he_loading_button带 loading 态<Button loading>
he_fab悬浮按钮Fab
he_back_top回到顶部BackTop

输入 / 表单类

组件用途
he_input输入框
he_textarea多行输入
he_form表单容器
he_search_bar搜索框
he_select_item选择项
he_dropdown_select下拉选择
he_dropdown_menu下拉菜单
he_picker选择器
he_date_picker / he_date_picker_sheet日期选择
he_calendar日历
he_cascader级联选择
he_tree_select树形选择
he_multi_select_picker多选
he_checkbox / he_radio复选 / 单选
he_switch开关
he_slider滑块
he_rate评分
he_stepper步进器
he_upload上传
he_time_counter倒计时(验证码)

导航类

组件用途
he_nav_bar导航栏
he_tab_barTab 栏
he_bottom_tab_bar底部导航
he_side_bar侧边栏
he_indexes索引栏(城市列表)
he_link链接
he_keep_alive_tabTab 保活

展示类

组件用途
he_text文本
he_icon / he_image / he_image_group图标 / 图片 / 图片组
he_image_preview图片预览
he_network_image网络图片(带缓存/占位)
he_avatar头像
he_tag / he_badge标签 / 徽标
he_cell / he_cell_group单元格 / 分组
he_kv_row键值对行
he_table表格
he_detail_card详情卡片
he_menu_list菜单列表
he_steps步骤条
he_progress进度条
he_notice_bar通知栏
he_result结果页
he_inline_leading_text行内前置文本

容器 / 布局类

组件用途
he_list_scaffold列表页脚手架(导航+搜索+Tab)
he_tabbed_detail_scaffoldTab 详情页脚手架
he_collapse折叠面板
he_swipe_cell滑动单元格(侧滑操作)
he_divider分割线
he_footer页脚
he_glass_effect毛玻璃效果

反馈 / 弹层类

组件用途
he_dialog / he_dialog_extra对话框
he_action_sheet动作面板
he_bottom_sheet_scaffold底部弹层脚手架
he_list_bottom_sheet列表型底部弹层
he_option_select_sheet选项选择弹层
he_search_select_sheet搜索选择弹层
he_popover气泡卡片
he_message消息提示
he_loadingToast / Loading 遮罩
he_loading_widget / he_loading_indicator加载指示
he_empty / he_empty_state空态
he_drawer抽屉
he_easy_refresh_indicators下拉刷新/上拉加载指示器
he_reusable_list复用列表
he_filter_scroll_bar筛选滚动条
he_locale_dialog / he_locale_switcher语言切换
he_theme_switcher主题切换

骨架屏类

he_loading_skeleton / he_skeleton_box / he_skeleton_list_view / he_sheet_skeleton / he_list_skeleton

图表类

he_bar_chart(柱状)/ he_line_chart(折线)/ he_donut_chart(环形)

高频组件用法

dart
// 按钮
HeButton(text: l10n.t('submit'), onPressed: _onSubmit)
HeLoadingButton(isLoading: state.isLoading, text: '...', onPressed: _onSubmit)

// 提示
HeLoading.showToast(l10n.t('tip'))
HeLoading.showSuccess(l10n.t('success'))
HeLoading.showError(l10n.t('failed'))

// 确认弹窗
final ok = await HeDialog.confirm(context, title: '...', content: '...');

// 单元格组
HeCellGroup(cells: [
  HeCell(title: '设备名', value: '逆变器01'),
  HeCell(title: '状态', value: '在线'),
])

// 图片
Assets.images.home.banner.image()      // 本地
HeNetworkImage(url: url)                // 网络

⚠️ 造轮子前先查这里

新增任何 UI 控件前,先确认 he_components 里有没有。重复造轮子是 code review 高频打回项。


二、AppTokens 体系

两种等价取法

dart
final tokens = AppTokens.of(context);    // 定义于 app_tokens.dart:22
final tokens = context.appTokens;        // extension,app_tokens.dart:545(推荐)

架构四层

Figma → L1 (app_colors.gen.dart)  → L2 (app_theme_data.dart) → L3 (app_tokens.dart) → 业务
文件说明
L1src/themes/app_colors.gen.dartFigma 镜像,自动生成,勿手改
L2src/themes/app_theme_data.dartlight/dark 快照
L3src/themes/app_tokens.dart业务唯一入口

业务代码只碰 L3


三、Token 速查表(以代码为准)

⚠️ 命名变更提示

项目早期用 spacerMdradiusMbodyMedium 这类命名,现已改为 spacingMdradiusLargebodyMdRegular

部分旧文档(含 .agents/skills/theme-usage)仍在写旧名,以本文为准

间距(9 档,静态 getter)

dart
AppTokens.spacingXxs     // 4
AppTokens.spacingXs      // 8
AppTokens.spacingSm      // 12
AppTokens.spacingMd      // 16
AppTokens.spacingLg      // 20
AppTokens.spacingXl      // 24
AppTokens.spacingXxl     // 32
AppTokens.spacingXxxl    // 40
AppTokens.spacingXxxxl   // 48

用法

dart
Padding(padding: EdgeInsets.all(AppTokens.spacingMd))
SizedBox(height: AppTokens.spacingSm)

圆角(静态 getter)

dart
AppTokens.radiusSmall                  // 4
AppTokens.radiusDefault                // 8
AppTokens.radiusLarge                  // 10
AppTokens.radiusExtraLarge             // 20
AppTokens.radiusExtraExtraLarge        // 更大
AppTokens.radiusExtraExtraExtraLarge   // 更大
AppTokens.radiusFull                   // 9999(胶囊/圆形)

用法

dart
BorderRadius.circular(AppTokens.radiusLarge)

项目实例 —— paged_list_body.dart:173-180

dart
decoration: BoxDecoration(
  color: tokens.white,
  borderRadius: BorderRadius.circular(AppTokens.radiusLarge),
),

字体(静态 getter,×3 字重)

命名规则:{title|body}{Xl|Lg|Md|Sm|Xs}{Regular|Medium|Bold}

dart
// Title 系列
AppTokens.titleXlRegular / titleXlMedium / titleXlBold
AppTokens.titleLgRegular / titleLgMedium / titleLgBold
AppTokens.titleMdRegular / titleMdMedium / titleMdBold
AppTokens.titleSmRegular / titleSmMedium / titleSmBold
AppTokens.titleXsRegular / titleXsMedium / titleXsBold

// Body 系列
AppTokens.bodyXlRegular / bodyXlMedium / bodyXlBold
AppTokens.bodyLgRegular / bodyLgMedium / bodyLgBold
AppTokens.bodyMdRegular / bodyMdMedium / bodyMdBold

用法

dart
Text('标题', style: AppTokens.titleMdMedium)
Text('正文', style: AppTokens.bodyMdRegular.copyWith(color: tokens.textSecondary))

项目实例 —— paged_list_body.dart:193-197

dart
Text(
  l10n.t('bTerminal.nomore'),
  style: AppTokens.bodyMdRegular.copyWith(color: tokens.textOnSurfaceHeading),
)

颜色(实例 getter,跟随亮/暗切换)

⚠️ 颜色必须先取 tokens 实例context.appTokens),不能像间距那样静态访问。

品牌色

dart
tokens.brandPrimaryDefault      // 主品牌色
tokens.brandSecondaryDefault    // 次品牌色
tokens.brandPrimaryDisable      // 禁用态

状态色

dart
tokens.statusDangerDefault / statusDangerActive / statusDangerSubtle
tokens.statusWarningDefault / statusWarningSubtle
tokens.statusSuccessDefault / statusSuccessSubtle
tokens.statusNeutralDefault / statusNeutralSubtle

文字色(语义化)

dart
tokens.textPrimary
tokens.textSecondary
tokens.textPlaceholder
tokens.textDisabled
tokens.textInverse
tokens.textOnSurfaceDefault
tokens.textOnSurfaceEmphasis
tokens.textOnSurfaceMutedAlt
tokens.textOnSurfaceHeading        // ⭐ 最常用
tokens.textOnSurfaceTertiary
tokens.textOnSurfaceInstruction
tokens.textOnSurfaceRemark

背景 / 表面色

dart
tokens.surfaceBackground
tokens.surfaceColor
tokens.surfacePageWarm
tokens.surfacePageNeutral
tokens.surfaceInputDefault
tokens.surfaceStatusCard
tokens.surfaceCardOverlay
tokens.surfaceScrim
tokens.white

边框色

dart
tokens.borderDefault
tokens.borderTertiary

标签色(成对使用)

dart
tokens.tagSuccessText / tagSuccessBackground
tokens.tagWarningText / tagWarningBackground
tokens.tagErrorText / tagErrorBackground
tokens.tagBrandText / tagBrandBackground

项目自定义色

dart
tokens.suggestion
tokens.barrier                  // 遮罩背景
tokens.navBarGradientEnd        // 导航栏渐变(配合 Start)
tokens.buttonPrimaryText
tokens.chartLabelBackground / chartLabelCard / chartLabelVariables

图标 / 组件尺寸(const,可用于 const 表达式)

dart
AppTokens.iconS    // 16
AppTokens.iconM    // 24
AppTokens.iconL    // 32
AppTokens.iconXL   // 48

AppTokens.buttonHeight              // 48
AppTokens.loadingIndicatorSize      // 32
AppTokens.dividerThickness          // 1
AppTokens.dialogMaxWidth            // 320

图表尺寸

chartLineWidth / chartDotRadius / chartLegendBarHeight / chartDefaultHeight 等。


四、const 兼容性(高频错误)

类别能否 const示例
图标/组件/图表尺寸✅ 可以AppTokens.iconMAppTokens.buttonHeight
间距 / 圆角❌ 不行AppTokens.spacingMdAppTokens.radiusLarge
字体 TextStyle❌ 不行AppTokens.bodyMdRegular
颜色(实例 getter)❌ 不行tokens.textPrimary
dart
// ❌ 编译错误
const EdgeInsets.all(AppTokens.spacingMd)
const TextStyle(fontSize: AppTokens.bodyMdRegular.fontSize)
const SizedBox(height: AppTokens.spacingSm)

// ✅ 正确
EdgeInsets.all(AppTokens.spacingMd)
AppTokens.bodyMdRegular.copyWith(color: tokens.textSecondary)
SizedBox(height: AppTokens.spacingSm)

// ✅ const 字段可以用在 const 表达式
const SizedBox(height: AppTokens.buttonHeight)

原因:间距/圆角/字体都是 getter(内部走 ScreenUtil 缩放),不是编译期常量。

构造参数默认值场景走初始化列表:

dart
class MyWidget extends StatelessWidget {
  MyWidget({EdgeInsets? padding})
      : padding = padding ?? EdgeInsets.all(AppTokens.spacingMd);

  final EdgeInsets padding;
}

五、屏幕适配

配置

lib/main.dart:370-371

dart
ScreenUtilInit(
  designSize: const Size(750, 1624),   // 设计稿基准
  minTextAdapt: true,
  ...
)

文字缩放固定

lib/main.dart:391-395

dart
MediaQuery(
  data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
  child: content,
)

App 字体不跟随系统字体大小

⚠️ 禁止在页面层用 .w / .h / .sp / .r

dart
SizedBox(height: 32.h)                    // ❌ 禁止
SizedBox(height: AppTokens.spacingXxl)    // ✅ 正确

ScreenUtil 双轨制

对象处理
业务 UIAppTokens.*(已含缩放)
TDesign 组件内部固定 dp,不要外套 .w/.h

TDesign 内部是固定 dp,外层再缩放会造成双重缩放——这是「TDesign 假溢出」的根因(见 02 章)。


六、Dark 模式

实现机制

lib/main.dart:217-231 注入两套主题:

dart
final tdLight = TDSemanticThemeData.light;
final tdDark = TDSemanticThemeData.dark;

const appTokensLight = AppTokens(appThemeData: AppThemeData.light);
final appTokensDark = AppTokens(appThemeData: AppThemeData.dark);

setState(() {
  _lightTheme = AppTheme.light(AppThemeData.light).copyWith(
    extensions: [tdLight, AppThemeData.light, appTokensLight],
  );
  _darkTheme = AppTheme.dark(AppThemeData.dark).copyWith(
    extensions: [tdDark, AppThemeData.dark, appTokensDark],
  );
  _themeLoaded = true;
});

颜色通过 ThemeExtension 机制注入——这就是 context.appTokens 能取到值的原因。

Dark 值从哪来

L2 的 _generateDark 按色阶语义从 light 推导:

语义算法
浅色档 / 页面背景明度反转
正常档(*Default 等)提亮 +15%
黑白半透明(文字/分割线)黑→白保留 alpha
白文字保持 light 值

也可通过 _kFigmaDarkOverrides 逐字段覆盖。

业务代码要注意什么

什么都不用做——只要用 tokens.xxx 取色,亮暗自动切换。

⚠️ 唯一禁忌:硬编码颜色。

dart
Container(color: Color(0xFF0073E5))    // ❌ Dark 模式下不会变
Container(color: tokens.brandPrimaryDefault)   // ✅

切换

dart
ref.read(themeModeNotifierProvider.notifier).setMode(ThemeMode.dark);

ThemeModeNotifierlib/config/providers/theme_mode_provider.dart:35)是 keepAlive,持久化到 SharedPreferences。


七、图片资源

强类型引用(唯一正确写法)

dart
import 'package:haierenergy/generated/assets.gen.dart';

Assets.images.home.banner.image()
Assets.images.auth.logo.image(width: 100)

⚠️ 禁止字面字符串

dart
Image.asset('assets/images/home/banner.png')    // ❌ 禁止
AssetImage('assets/images/home/banner.png')     // ❌ 禁止

原因flutter_gen 生成强类型访问层,改名/删除时编译期就能发现,且 IDE 可补全。

新增图片的四步闭环

  1. 放文件 → 放到 assets/images/<feature>/
  2. 注册目录 → 确认 pubspec.yamlassets: 包含该目录
  3. 跑生成fvm dart run build_runner build --delete-conflicting-outputs
  4. 引用Assets.images.<feature>.<name>.image()

生成的 lib/generated/assets.gen.dart 要提交到 git

网络图片

dart
HeNetworkImage(url: url)        // 带缓存与占位,优先用它

八、图标

dart
AppIcon(name: 'icon_search', size: AppTokens.iconM, color: tokens.textSecondary)

⚠️ 不用 Icon(Icons.search)——项目用远程加载的 iconfont 字体,统一走 AppIcon

AppIcon 定义于 lib/core/iconfont/app_icon_widget.dart:11,是个 ConsumerWidget(内部监听字体加载状态)。


九、禁止事项总表

禁止正确做法
Color(0xFF0073E5)tokens.brandPrimaryDefault
fontSize: 14AppTokens.bodyMdRegular
padding: EdgeInsets.all(16)EdgeInsets.all(AppTokens.spacingMd)
Image.asset('assets/...')Assets.images.xxx.image()
Icon(Icons.home)AppIcon(name: 'icon_home')
SizedBox(height: 32.h)AppTokens.spacingXxl
AppTokens.spacerMd(旧名)AppTokens.spacingMd
AppTokens.radiusM(旧名)AppTokens.radiusLarge
AppTokens.bodyMedium(旧名)AppTokens.bodyMdRegular
业务直接 import 'package:tdesign_flutter/...'he_components 封装
Text('中文')Text(l10n.t('key'))
print('...')AppLogger.I.d('...')
Navigator.push(...)context.push(...)

十、自检清单

  • [ ] 知道组件库唯一导入入口是 he_components.dart
  • [ ] 造 UI 前先在 88 个组件里找,不重复造轮子
  • [ ] 所有间距用 AppTokens.spacingXxx不是 spacerXxx
  • [ ] 所有圆角用 AppTokens.radiusXxx
  • [ ] 所有字体用 AppTokens.{title,body}Xx{Regular,Medium,Bold}
  • [ ] 所有颜色用 tokens.xxx,且先取实例
  • [ ] 知道间距/圆角/字体/颜色都不能放进 const
  • [ ] 图片用 Assets.images.xxx,不用字面串
  • [ ] 图标用 AppIcon,不用 Icon(Icons.xxx)
  • [ ] 不用 .w/.h/.sp/.r

下一步

12 · 分页与列表