提示信息
# VelocityX · Flutter AI 速查手册
> 本文档供 AI 生成代码时快速检索 VelocityX API。
> 项目规范:**所有颜色用 `AppColors`,间距用 `AppSpacing`,字体用 `AppTextStyles`,禁止字面量。**
> VelocityX 版本:`^4.0.0`
---
## 目录
1. [文本 Text](#一文本-text)
2. [容器 Box](#二容器-box)
3. [内边距 Padding](#三内边距-padding)
4. [布局 Stack / Row / Column](#四布局-stack--row--column)
5. [点击与手势](#五点击与手势)
6. [尺寸与全宽](#六尺寸与全宽)
7. [颜色快捷引用规范](#七颜色快捷引用规范)
8. [业务组件写法](#八业务组件写法)
9. [导航栏组件](#九导航栏组件)
10. [完整页面级组合示例](#十完整页面级组合示例)
11. [禁止写法对照表](#十一禁止写法对照表)
---
## 一、文本 Text
### 基础链式调用
```dart
// 基础
'Hello'.text.make()
// 字重
'标题'.text.bold.make()
'副标题'.text.semiBold.make()
'正文'.text.normal.make()
'细体'.text.thin.make()
'中等'.text.medium.make()
// 字号(对应 Material TextTheme 规格)
'大标题'.text.xl4.make() // 36px
'标题'.text.xl3.make() // 30px
'标题'.text.xl2.make() // 24px
'标题'.text.xl.make() // 20px
'正文'.text.base.make() // 16px(默认)
'小字'.text.sm.make() // 14px
'极小'.text.xs.make() // 12px
// 颜色(必须用 AppColors,禁止 .red500 等 VelocityX 内置色)
'文字'.text.color(AppColors.textPrimary).make()
'次要文字'.text.color(AppColors.textSecondary).make()
'提示文字'.text.color(AppColors.textHint).make()
'主色文字'.text.color(AppColors.primary).make()
// 对齐
'居中'.text.center.make()
'居右'.text.end.make()
'居左'.text.start.make()
// 行数与溢出
'长文本'.text.maxLines(2).ellipsis.make()
'不换行'.text.maxLines(1).ellipsis.make()
// 装饰
'下划线'.text.underline.make()
'删除线'.text.lineThrough.make()
// 行高与字间距
'文字'.text.lineHeight(1.5).make()
'文字'.text.letterSpacing(1.2).make()
// 组合示例(标题)
'页面标题'
.text
.bold
.xl2
.color(AppColors.textPrimary)
.make()
// 组合示例(副标题)
'这是一段描述文字'
.text
.sm
.color(AppColors.textSecondary)
.maxLines(2)
.ellipsis
.make()
```
### 使用 AppTextStyles(推荐方式)
```dart
// 直接使用预定义样式,比链式更简洁
Text('标题', style: AppTextStyles.heading1)
Text('正文', style: AppTextStyles.body)
Text('小字', style: AppTextStyles.caption)
// VelocityX 与 AppTextStyles 结合
'标题'.text.style(AppTextStyles.heading1).make()
```
---
## 二、容器 Box
### 基础容器
```dart
// 最简容器
child.box.make()
// 背景色(必须用 AppColors)
child.box.color(AppColors.bgPrimary).make()
child.box.color(AppColors.primary).make()
// 圆角
child.box.rounded.make() // 全圆角(圆形)
child.box.roundedSM.make() // 小圆角 4px
child.box.roundedLg.make() // 大圆角 8px
child.box.roundedXl.make() // 12px
child.box.rounded3xl.make() // 24px
child.box.withRounded(value: AppRadius.card).make() // 使用 AppRadius 常量
// 阴影
child.box.shadow.make() // 默认阴影
child.box.shadowSm.make() // 小阴影
child.box.shadowLg.make() // 大阴影
child.box.shadowXl.make() // 超大阴影
child.box.shadowNone.make() // 无阴影
// 边框
child.box.border(color: AppColors.divider).make()
child.box.border(color: AppColors.primary, width: 2).make()
// 固定尺寸
child.box.width(120).make()
child.box.height(48).make()
child.box.size(120, 48).make() // width, height
child.box.square(48).make() // 正方形
// 组合示例(卡片)
child
.box
.color(AppColors.bgPrimary)
.roundedXl
.shadow
.make()
// 组合示例(主色圆角按钮背景)
child
.box
.color(AppColors.primary)
.withRounded(value: AppRadius.button)
.make()
// 组合示例(带边框的输入框背景)
child
.box
.color(AppColors.bgSecondary)
.border(color: AppColors.divider)
.roundedLg
.make()
```
---
## 三、内边距 Padding
### 统一边距
```dart
child.p1().make() // 4px
child.p2().make() // 8px
child.p4().make() // 16px
child.p6().make() // 24px
child.p8().make() // 32px
child.p12().make() // 48px
child.p16().make() // 64px(注意:p16 = 64px,不是 16px)
// 使用 AppSpacing 常量(推荐,符合规范)
child.p(AppSpacing.md).make()
child.p(AppSpacing.lg).make()
```
### 方向边距
```dart
// 水平
child.px4().make() // 左右各 16px
child.px8().make() // 左右各 32px
// 垂直
child.py2().make() // 上下各 8px
child.py4().make() // 上下各 16px
// 单方向
child.pt4().make() // top 16px
child.pb4().make() // bottom 16px
child.pl4().make() // left 16px
child.pr4().make() // right 16px
// 组合
child.px4().py2().make() // 水平16px + 垂直8px
// 使用 AppSpacing 精确控制(推荐)
Padding(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
child: child,
)
```
> **注意**:VelocityX 的 `pN()` 中 N 是 4 的倍数索引(p1=4px, p2=8px, p4=16px),与 Tailwind 不完全一致。生产代码推荐直接用 `AppSpacing` 常量配合 `Padding` widget,避免混淆。
---
## 四、布局 Stack / Row / Column
### hStack(水平排列,等同 Row)
```dart
// 基础
[widget1, widget2, widget3].hStack()
// 带间距
[widget1, widget2].hStack(spacing: AppSpacing.sm)
[widget1, widget2].hStack(spacing: 8)
// 对齐(crossAlignment = 垂直方向对齐)
[widget1, widget2].hStack(
crossAlignment: CrossAxisAlignment.center,
)
[widget1, widget2].hStack(
crossAlignment: CrossAxisAlignment.start,
)
[widget1, widget2].hStack(
crossAlignment: CrossAxisAlignment.end,
)
// 主轴对齐(alignment = 水平方向)
[widget1, widget2].hStack(
alignment: MainAxisAlignment.spaceBetween,
)
[widget1, widget2].hStack(
alignment: MainAxisAlignment.center,
)
[widget1, widget2].hStack(
alignment: MainAxisAlignment.end,
)
// 组合示例(头像 + 用户名 + 时间)
[
avatarWidget,
[nameText, timeText].vStack(crossAlignment: CrossAxisAlignment.start),
].hStack(
spacing: AppSpacing.sm,
crossAlignment: CrossAxisAlignment.center,
)
```
### vStack(垂直排列,等同 Column)
```dart
// 基础
[widget1, widget2, widget3].vStack()
// 带间距
[widget1, widget2].vStack(spacing: AppSpacing.sm)
// 对齐(crossAlignment = 水平方向对齐)
[widget1, widget2].vStack(
crossAlignment: CrossAxisAlignment.start,
)
[widget1, widget2].vStack(
crossAlignment: CrossAxisAlignment.center,
)
[widget1, widget2].vStack(
crossAlignment: CrossAxisAlignment.stretch, // 子项填满宽度
)
// 组合示例(标题 + 副标题)
[
'标题'.text.bold.xl2.color(AppColors.textPrimary).make(),
'这是副标题描述'.text.sm.color(AppColors.textSecondary).make(),
].vStack(
crossAlignment: CrossAxisAlignment.start,
spacing: AppSpacing.xs,
)
```
### zStack(层叠,等同 Stack)
```dart
[backgroundWidget, foregroundWidget].zStack()
[backgroundWidget, foregroundWidget].zStack(
alignment: Alignment.bottomRight,
)
```
---
## 五、点击与手势
```dart
// 单击
widget.onTap(() => context.push(AppRoutes.detail))
widget.onTap(() => ref.read(controller.notifier).doSomething())
// 双击
widget.onDoubleTap(() => handleDoubleTap())
// 长按
widget.onLongPress(() => showContextMenu())
// 点击带水波纹效果(InkWell)
widget.onInkTap(() => handleTap())
// 组合示例(可点击卡片)
[
titleWidget,
subtitleWidget,
].vStack(crossAlignment: CrossAxisAlignment.start)
.box
.color(AppColors.bgPrimary)
.roundedXl
.shadow
.make()
.p4()
.onTap(() => context.push(AppRoutes.detail, extra: item))
```
---
## 六、尺寸与全宽
```dart
// 全宽(撑满父容器宽度)
widget.wFull(context)
// 全高
widget.hFull(context)
// 固定宽高
widget.w(120)
widget.h(48)
// 屏幕宽度百分比
widget.wPCT(context, widthPCT: 50) // 屏幕宽度 50%
// 组合示例(全宽按钮)
ElevatedButton(
onPressed: handleTap,
child: '确认'.text.bold.make(),
).wFull(context)
// 组合示例(全宽卡片)
cardWidget
.box
.color(AppColors.bgPrimary)
.roundedXl
.shadow
.make()
.wFull(context)
```
---
## 七、颜色快捷引用规范
### ⚠️ 强制规则
```dart
// ❌ 禁止使用 VelocityX 内置颜色
'文字'.text.red500.make()
child.box.red500.make()
child.box.gray100.make()
// ❌ 禁止使用 Flutter 内置颜色字面量
Container(color: Colors.blue)
Container(color: Color(0xFF6366F1))
// ✅ 必须使用 AppColors
'文字'.text.color(AppColors.error).make()
child.box.color(AppColors.bgSecondary).make()
child.box.color(AppColors.primary).make()
```
### AppColors 常用对照
| 语义 | 代码 |
|------|------|
| 主色 | `AppColors.primary` |
| 主色浅背景 | `AppColors.primaryLight` |
| 错误/危险 | `AppColors.error` |
| 成功 | `AppColors.success` |
| 警告 | `AppColors.warning` |
| 主要文字 | `AppColors.textPrimary` |
| 次要文字 | `AppColors.textSecondary` |
| 提示文字 | `AppColors.textHint` |
| 页面背景 | `AppColors.bgPrimary` |
| 卡片/区块背景 | `AppColors.bgSecondary` |
| 分割线 | `AppColors.divider` |
| 语音消息 | `AppColors.voiceMessage` |
---
## 八、业务组件写法
> 以下组件分两类:
> **封装组件**(`lib/widgets/` 下已有实现,直接使用)和
> **VelocityX 组合写法**(用链式 API 临时拼装的 UI 片段)。
> AI 生成代码时,优先使用封装组件,封装组件不覆盖的场景再用 VelocityX 组合。
---
### 8.1 CustomAvatar · 智能头像(封装组件)
> 路径:`lib/widgets/custom_avatar.dart`
> **所有头像场景必须用此组件,禁止直接用 `CircleAvatar` + `CachedNetworkImage` 手拼。**
```dart
// ── 变体说明 ──────────────────────────────────────────
// AvatarSize.df → 列表用小图,OSS 裁剪为 128x128
// AvatarSize.xxl → 主页/大图查看,OSS 裁剪为 750px
// ── 基础用法(列表头像)─────────────────────────────
CustomAvatar(
url: user.avatarUrl,
name: user.name, // 用于加载失败时哈希生成占位色 + 首字母
size: AvatarSize.df,
)
// ── 主页大头像 ───────────────────────────────────────
CustomAvatar(
url: user.avatarUrl,
name: user.name,
size: AvatarSize.xxl,
)
// ── 带在线状态绿点 ───────────────────────────────────
Stack(
clipBehavior: Clip.none,
children: [
CustomAvatar(url: user.avatarUrl, name: user.name, size: AvatarSize.df),
if (user.isOnline)
Positioned(
right: 0, bottom: 0,
child: Container(
width: 10, height: 10,
decoration: BoxDecoration(
color: AppColors.onlineGreen,
shape: BoxShape.circle,
border: Border.all(color: AppColors.bgPrimary, width: 2),
),
),
),
],
)
// ── 带 VIP 角标 ──────────────────────────────────────
Stack(
clipBehavior: Clip.none,
children: [
CustomAvatar(url: user.avatarUrl, name: user.name, size: AvatarSize.df),
Positioned(
right: -4, bottom: -4,
child: 'VIP'
.text.bold.xs.color(AppColors.bgPrimary).make()
.box.color(AppColors.vipGold)
.withRounded(value: AppRadius.tag).make()
.px(4).py(2),
),
],
)
```
**特殊逻辑说明(AI 生成时需知道):**
- OSS 样式参数自动注入,无需手动拼接 URL
- 内存缓存同步加载,滚动列表无白色瞬闪
- 加载失败时根据 `name` 哈希生成护眼色块 + 首字母占位,不需要额外 errorWidget
---
### 8.2 AppCapsuleButton · 社交胶囊按钮(封装组件)
> 路径:`lib/widgets/app_capsule_button.dart`
> 专为社交关注场景设计,高度固定 28px,4 种变体对应关注状态流转。
```dart
// ── 变体对照 ─────────────────────────────────────────
// primaryOutline → 「+ 关注」 未关注状态,主色描边
// primaryFilled → 「回关」 对方已关注我,我未关注,主色填充
// secondaryOutline→ 「已关注」 我已关注对方,灰色描边
// surfaceVariant → 「互相关注」 双方互关,背景色填充
// ── 用法(配合关注状态枚举)─────────────────────────
AppCapsuleButton(
variant: switch (user.followState) {
FollowState.none => AppCapsuleVariant.primaryOutline,
FollowState.following => AppCapsuleVariant.secondaryOutline,
FollowState.followBack=> AppCapsuleVariant.primaryFilled,
FollowState.mutual => AppCapsuleVariant.surfaceVariant,
},
onTap: () => ref.read(followController.notifier).toggle(user.id),
)
// ── 列表项中的典型用法 ───────────────────────────────
[
CustomAvatar(url: user.avatarUrl, name: user.name, size: AvatarSize.df),
[
user.name.text.bold.base.color(AppColors.textPrimary).make(),
user.bio.text.sm.color(AppColors.textSecondary).maxLines(1).ellipsis.make(),
].vStack(crossAlignment: CrossAxisAlignment.start, spacing: AppSpacing.xs).expand(),
AppCapsuleButton(
variant: user.followVariant,
onTap: () => ref.read(followController.notifier).toggle(user.id),
),
].hStack(spacing: AppSpacing.sm, crossAlignment: CrossAxisAlignment.center)
.p(AppSpacing.md)
.onTap(() => context.push(AppRoutes.userProfile, extra: user.id))
```
---
### 8.3 PrimaryActionButton · 任务主按钮(封装组件)
> 路径:`lib/widgets/primary_action_button.dart`
> 全宽主操作按钮,内置异步 Loading 支持,禁止连点。
```dart
// ── 基础用法 ─────────────────────────────────────────
PrimaryActionButton(
label: '立即发送',
onPressed: () async {
await ref.read(chatController.notifier).sendMessage(content);
// Future 执行期间自动显示 Loading,完成后自动恢复
},
)
// ── 危险操作(传 isDestructive 变红)────────────────
PrimaryActionButton(
label: '注销账号',
isDestructive: true,
onPressed: () async => await ref.read(accountController.notifier).deleteAccount(),
)
// ── 置灰禁用 ─────────────────────────────────────────
PrimaryActionButton(
label: '确认',
onPressed: isFormValid ? handleSubmit : null, // null 自动置灰
)
```
**特殊逻辑说明:**
- `onPressed` 传入返回 `Future` 的函数时,点击后自动 Loading 并锁定,任务结束自动恢复
- 不需要手动管理 `isLoading` 状态,不需要手动 `setState`
---
### 8.4 PagedListView · 分页列表(封装组件)
> 路径:`lib/widgets/paged_list_view.dart`
> 一站式解决:下拉刷新 + 上拉加载更多 + 骨架屏 + 空状态 + "没有更多了"页脚。
```dart
// ── 基础用法 ─────────────────────────────────────────
PagedListView<UserModel>(
state: ref.watch(userListControllerProvider), // AsyncValue<PagedState<UserModel>>
itemBuilder: (context, user, index) => [
CustomAvatar(url: user.avatarUrl, name: user.name, size: AvatarSize.df),
[
user.name.text.bold.base.color(AppColors.textPrimary).make(),
user.bio.text.sm.color(AppColors.textSecondary).maxLines(1).ellipsis.make(),
].vStack(crossAlignment: CrossAxisAlignment.start, spacing: AppSpacing.xs).expand(),
AppCapsuleButton(variant: user.followVariant, onTap: () => handleFollow(user.id)),
].hStack(spacing: AppSpacing.sm, crossAlignment: CrossAxisAlignment.center)
.p(AppSpacing.md)
.onTap(() => context.push(AppRoutes.userProfile, extra: user.id)),
skeletonBuilder: () => AppSkeleton.listTile(), // 加载时展示的骨架
onRefresh: () => ref.refresh(userListControllerProvider),
onLoadMore: () => ref.read(userListControllerProvider.notifier).loadMore(),
)
// ── 带 SliverAppBar 的复杂页面 ───────────────────────
// PagedListView 支持 sliver 模式,可直接嵌入 CustomScrollView
CustomScrollView(
slivers: [
SliverAppBar(...),
PagedListView<PostModel>.sliver(
state: ref.watch(postListControllerProvider),
itemBuilder: (context, post, index) => PostCard(post: post),
skeletonBuilder: () => AppSkeleton.card(),
onRefresh: () => ref.refresh(postListControllerProvider),
onLoadMore: () => ref.read(postListControllerProvider.notifier).loadMore(),
),
],
)
```
---
### 8.5 AppEmptyState · 动态空状态(封装组件)
> 路径:`lib/widgets/app_empty_state.dart`
> 提供多种预设空状态类型,支持标题、副标题与操作按钮。
```dart
// ── 基础空状态 ───────────────────────────────────────
AppEmptyState(
type: AppEmptyType.noData,
title: '暂时没有内容',
subtitle: '快去发现有趣的人吧',
)
// ── 网络错误场景 ─────────────────────────────────────
AppEmptyState(
type: AppEmptyType.networkError,
actionLabel: '重新加载',
onAction: () => ref.refresh(xxxProvider),
)
```
> 注意:`PagedListView` 内部已自动使用 `AppEmpty`,不需要在 `itemBuilder` 外层再手动包一个。
---
### 8.6 AppSkeleton · 骨架屏(封装组件)
> 路径:`lib/widgets/app_skeleton.dart`
> 提供预设形状,内置 Shimmer 扫光,直接调用工厂方法。
```dart
// ── 预设形状 ─────────────────────────────────────────
AppSkeleton.circle() // 圆形占位(头像)
AppSkeleton.listTile() // 列表项占位(头像 + 两行文字 + 按钮)
AppSkeleton.card() // 卡片占位(图片 + 标题 + 副标题)
// ── 典型用法:Controller build() 返回前的 loading 态 ──
// 通常配合 PagedListView 的 skeletonBuilder 使用
// 单页非列表场景,在 state.when 的 loading 里调用:
state.when(
data: (data) => _buildContent(data),
loading: () => ListView.builder(
itemCount: 6,
itemBuilder: (_, __) => AppSkeleton.listTile(),
),
error: (e, _) => AppEmpty(
icon: AppIcons.networkError,
title: '加载失败',
actionLabel: '重试',
onAction: () => ref.refresh(xxxControllerProvider),
),
)
```
---
### 8.7 AppLoading · 加载指示器(封装组件)
> 路径:`lib/widgets/app_loading.dart`
> 统一颜色的 `CircularProgressIndicator`,避免各处颜色不一致。
```dart
// ── 内联加载(嵌入页面内容中)──────────────────────
AppLoading()
// ── 全屏加载(覆盖整个页面)─────────────────────────
AppLoading.fullScreen()
// ── 典型用法:提交按钮旁边的小 loading ──────────────
// 注意:如果用 PrimaryActionButton,不需要手动用 AppLoading
// 以下用于非 PrimaryActionButton 的自定义场景:
if (isSubmitting.value)
const AppLoading()
else
Icon(AppIcons.send, color: AppColors.primary)
```
---
### 8.8 AppRefresh · 下拉刷新(封装组件)
> 路径:`lib/widgets/app_refresh.dart`
> 全站统一刷新动画,基于 `EasyRefresh` 封装。
```dart
// ── 用法(包裹需要下拉刷新的内容)──────────────────
// 注意:PagedListView 内部已集成,以下用于非列表场景:
AppRefresh(
onRefresh: () async {
await ref.refresh(xxxControllerProvider.future);
},
child: SingleChildScrollView(
child: _buildPageContent(),
),
)
```
---
### 8.9 VelocityX 组合写法(非封装组件场景)
以下是封装组件不覆盖时,用 VelocityX 手动组合的常见 UI 片段。
#### 输入框
```dart
// ── 搜索框 ───────────────────────────────────────────
TextField(
controller: searchController,
decoration: InputDecoration(
hintText: '搜索',
hintStyle: AppTextStyles.body.copyWith(color: AppColors.textHint),
prefixIcon: Icon(AppIcons.search, color: AppColors.textHint, size: 20),
filled: true,
fillColor: AppColors.bgSecondary,
contentPadding: EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
),
),
)
// ── 表单输入框(带标签 + 错误态)────────────────────
[
'手机号'.text.sm.bold.color(AppColors.textPrimary).make(),
SizedBox(height: AppSpacing.xs),
TextField(
controller: phoneController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: '请输入手机号',
hintStyle: AppTextStyles.body.copyWith(color: AppColors.textHint),
filled: true,
fillColor: AppColors.bgSecondary,
contentPadding: EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.md),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: BorderSide(color: AppColors.error, width: 1.5),
),
),
),
].vStack(crossAlignment: CrossAxisAlignment.start)
// ── 验证码框(右侧发送按钮)──────────────────────────
[
TextField(
controller: codeController,
keyboardType: TextInputType.number,
maxLength: 6,
decoration: InputDecoration(
hintText: '验证码',
counterText: '',
filled: true,
fillColor: AppColors.bgSecondary,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: BorderSide.none,
),
),
).expand(),
SizedBox(width: AppSpacing.sm),
_SendCodeButton(phone: phoneController.text),
].hStack(crossAlignment: CrossAxisAlignment.center)
```
#### 标签与徽章
```dart
// ── 普通标签 ─────────────────────────────────────────
'Flutter'.text.sm.color(AppColors.primary).make()
.box.color(AppColors.primaryLight)
.withRounded(value: AppRadius.tag).make()
.px(AppSpacing.sm).py(4)
// ── 状态标签(成功 / 警告 / 错误)───────────────────
'已认证'.text.xs.color(AppColors.success).make()
.box.color(AppColors.success.withOpacity(0.1))
.withRounded(value: AppRadius.tag).make()
.px(AppSpacing.sm).py(4)
// ── 未读数字徽章 ─────────────────────────────────────
(count > 99 ? '99+' : count.toString())
.text.bold.xs.white.make()
.box.color(AppColors.error).rounded.make()
.px(6).py(2)
// ── 小红点(无数字)──────────────────────────────────
''.text.make().box.color(AppColors.error).rounded.make().square(8)
// ── 横向滚动标签组 ───────────────────────────────────
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: [
for (final tag in tags)
tag.text.sm.color(
selectedTag == tag ? AppColors.bgPrimary : AppColors.primary,
).make()
.box.color(selectedTag == tag ? AppColors.primary : AppColors.primaryLight)
.withRounded(value: AppRadius.tag).make()
.px(AppSpacing.sm).py(6)
.onTap(() => onTagSelected(tag)),
].hStack(spacing: AppSpacing.xs),
)
```
#### 图片展示
```dart
// ── 单张网络图片(圆角 + 骨架占位)────────────────
CachedNetworkImage(
imageUrl: imageUrl,
width: double.infinity, height: 200, fit: BoxFit.cover,
placeholder: (context, url) => AppSkeleton.card(),
errorWidget: (context, url, error) =>
Icon(AppIcons.image, color: AppColors.textHint, size: 40)
.box.color(AppColors.bgSecondary).make().h(200).wFull(context),
).box.roundedLg.make()
.clipRRect(borderRadius: BorderRadius.circular(AppRadius.card))
// ── 九宫格图片 ───────────────────────────────────────
GridView.count(
crossAxisCount: images.length == 1 ? 1 : 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 2, crossAxisSpacing: 2,
children: images.map((url) =>
CachedNetworkImage(imageUrl: url, fit: BoxFit.cover)
.onTap(() => context.push(AppRoutes.imagePreview, extra: url)),
).toList(),
)
```
#### 分割线与节标题
```dart
// ── 分割线 ───────────────────────────────────────────
Divider(height: 1, color: AppColors.divider)
// ── 节标题(带查看全部)──────────────────────────────
[
'推荐好友'.text.bold.base.color(AppColors.textPrimary).make().expand(),
'查看全部'.text.sm.color(AppColors.primary).make()
.onTap(() => context.push(AppRoutes.discover)),
].hStack(crossAlignment: CrossAxisAlignment.center)
.px(AppSpacing.md).py(AppSpacing.sm)
```
---
## 九、导航栏组件
> 路径:`lib/widgets/nav/`
> **所有页面的 AppBar 必须使用封装组件,禁止直接使用 Flutter 原生 `AppBar`。**
---
### 9.1 AppNavBar · 标准导航栏
> 适用场景:绝大多数普通页面(列表页、设置页、编辑页等)。
```dart
// ── 组件签名 ─────────────────────────────────────────
AppNavBar({
String? title, // 页面标题,null 则不显示
Widget? titleWidget, // 自定义标题 Widget,与 title 二选一
List<Widget>? actions, // 右侧操作区,可放多个图标
bool showBack = true, // 是否显示返回按钮,根页面传 false
VoidCallback? onBack, // 自定义返回逻辑,null 时默认 context.pop()
Color? backgroundColor, // 背景色,默认 AppColors.bgPrimary
})
// ── 基础用法(标题 + 默认返回)──────────────────────
Scaffold(
appBar: AppNavBar(title: '设置'),
body: ...,
)
// ── 带右侧单个操作按钮 ───────────────────────────────
Scaffold(
appBar: AppNavBar(
title: '编辑资料',
actions: [
Icon(AppIcons.check, color: AppColors.primary, size: 22)
.onTap(handleSave)
.p(AppSpacing.sm),
],
),
body: ...,
)
// ── 带右侧多个操作按钮 ───────────────────────────────
Scaffold(
appBar: AppNavBar(
title: '动态详情',
actions: [
Icon(AppIcons.share, color: AppColors.textSecondary, size: 22)
.onTap(handleShare)
.p(AppSpacing.sm),
Icon(AppIcons.more, color: AppColors.textSecondary, size: 22)
.onTap(handleMore)
.p(AppSpacing.sm),
],
),
body: ...,
)
// ── 无返回按钮(Tab 根页面)──────────────────────────
Scaffold(
appBar: AppNavBar(title: '首页', showBack: false),
body: ...,
)
// ── 自定义返回逻辑(表单页防误退)───────────────────
Scaffold(
appBar: AppNavBar(
title: '发布动态',
onBack: () {
// 有草稿时弹确认弹窗
if (hasContent.value) {
showDiscardDialog(context);
} else {
context.pop();
}
},
),
body: ...,
)
```
---
### 9.2 AppTransparentNavBar · 透明/渐变导航栏
> 适用场景:个人主页、用户详情页等顶部有封面大图的页面。
> 随页面滚动从透明渐变为实色,返回按钮始终可见。
```dart
// ── 组件签名 ─────────────────────────────────────────
AppTransparentNavBar({
String? title, // 滚动到一定位置后淡入显示的标题
List<Widget>? actions, // 右侧操作区(始终显示)
ScrollController? scrollController, // 传入以监听滚动位置
double fadeStartOffset = 100, // 开始渐变的滚动距离(px)
double fadeEndOffset = 200, // 完全变为实色的滚动距离(px)
})
// ── 典型用法(个人主页)─────────────────────────────
class UserProfilePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final scrollCtrl = useScrollController();
return Scaffold(
// extendBodyBehindAppBar 让 body 延伸到 AppBar 后面
extendBodyBehindAppBar: true,
appBar: AppTransparentNavBar(
title: user.name, // 滚动后淡入显示用户名
scrollController: scrollCtrl,
actions: [
Icon(AppIcons.share, color: Colors.white, size: 22)
.onTap(handleShare)
.p(AppSpacing.sm),
Icon(AppIcons.more, color: Colors.white, size: 22)
.onTap(handleMore)
.p(AppSpacing.sm),
],
),
body: CustomScrollView(
controller: scrollCtrl,
slivers: [
// 封面图(撑到 AppBar 后面)
SliverToBoxAdapter(
child: CachedNetworkImage(
imageUrl: user.coverUrl,
height: 280, width: double.infinity, fit: BoxFit.cover,
),
),
// 用户信息 + 内容列表
SliverToBoxAdapter(child: _UserInfoSection(user: user)),
PagedListView<PostModel>.sliver(
state: ref.watch(userPostsProvider(user.id)),
itemBuilder: (context, post, index) => PostCard(post: post),
skeletonBuilder: () => AppSkeleton.card(),
onRefresh: () => ref.refresh(userPostsProvider(user.id)),
onLoadMore: () => ref.read(userPostsProvider(user.id).notifier).loadMore(),
),
],
),
);
}
}
```
**内部渐变实现原理(供 AI 了解,不需要手写):**
```dart
// AppTransparentNavBar 内部通过监听 scrollController 计算透明度
// opacity = ((offset - fadeStartOffset) / (fadeEndOffset - fadeStartOffset)).clamp(0.0, 1.0)
// 返回按钮背景始终有半透明圆形背景保证可见性:
// Icon 外层套 .box.color(Colors.black26).rounded.make()
```
---
### 9.3 AppSearchNavBar · 搜索导航栏
> 适用场景:发现页、搜索结果页,导航栏内嵌搜索框。
```dart
// ── 组件签名 ─────────────────────────────────────────
AppSearchNavBar({
String hintText = '搜索',
TextEditingController? controller,
ValueChanged<String>? onChanged, // 实时搜索
ValueChanged<String>? onSubmitted, // 键盘确认搜索
VoidCallback? onClear, // 清空按钮
bool showBack = true, // 发现页 Tab 根页面传 false
bool autofocus = false, // 进入页面自动弹键盘
})
// ── 发现页(根 Tab,无返回)──────────────────────────
Scaffold(
appBar: AppSearchNavBar(
hintText: '搜索用户、话题',
controller: searchCtrl,
showBack: false,
onChanged: (query) =>
ref.read(discoverController.notifier).search(query),
),
body: ...,
)
// ── 搜索结果页(有返回,自动聚焦)──────────────────
Scaffold(
appBar: AppSearchNavBar(
hintText: '搜索',
controller: searchCtrl,
autofocus: true,
onSubmitted: (query) =>
ref.read(searchController.notifier).submit(query),
onClear: () =>
ref.read(searchController.notifier).clear(),
),
body: ...,
)
```
---
### 9.4 AppChatNavBar · 聊天页导航栏
> 适用场景:一对一聊天页,显示对方头像 + 昵称 + 在线状态。
```dart
// ── 组件签名 ─────────────────────────────────────────
AppChatNavBar({
required String avatarUrl,
required String name,
bool isOnline = false,
String? onlineText, // 在线时显示的文字,默认「在线」
String? offlineText, // 离线时显示的文字,默认「离线」
List<Widget>? actions,
})
// ── 标准用法 ─────────────────────────────────────────
Scaffold(
appBar: AppChatNavBar(
avatarUrl: conversation.targetUser.avatarUrl,
name: conversation.targetUser.name,
isOnline: conversation.targetUser.isOnline,
actions: [
Icon(AppIcons.more, color: AppColors.textSecondary, size: 22)
.onTap(() => showChatMenu(context))
.p(AppSpacing.sm),
],
),
body: ...,
)
// ── 渲染效果(供 AI 理解内部结构)───────────────────
// [返回按钮] [头像] [名字(加粗)] [操作区]
// (带绿点) [在线 / 离线(小字)]
//
// 具体实现:
[
// 返回
Icon(AppIcons.back, color: AppColors.textPrimary, size: 22)
.onTap(() => context.pop())
.p(AppSpacing.sm),
// 头像(带在线绿点)
Stack(
clipBehavior: Clip.none,
children: [
CustomAvatar(url: avatarUrl, name: name, size: AvatarSize.df),
if (isOnline)
Positioned(
right: 0, bottom: 0,
child: Container(
width: 10, height: 10,
decoration: BoxDecoration(
color: AppColors.onlineGreen,
shape: BoxShape.circle,
border: Border.all(color: AppColors.bgPrimary, width: 2),
),
),
),
],
),
SizedBox(width: AppSpacing.xs),
// 名字 + 在线状态文字
[
name.text.bold.base.color(AppColors.textPrimary).make(),
(isOnline ? '在线' : '离线')
.text.xs.color(isOnline ? AppColors.onlineGreen : AppColors.textHint).make(),
].vStack(crossAlignment: CrossAxisAlignment.start, spacing: 2).expand(),
// 右侧操作区
...?actions,
].hStack(crossAlignment: CrossAxisAlignment.center)
.box.color(AppColors.bgPrimary).make()
.h(56)
.px(AppSpacing.sm)
```
---
### 9.5 AppBottomNavBar · 底部主导航
> 适用场景:App 主框架的三个 Tab(首页 / 消息 / 我的)。
> 通过 `go_router` 的 `StatefulShellRoute.indexedStack` 管理 Tab 状态。
```dart
// ── 组件签名 ─────────────────────────────────────────
AppBottomNavBar({
required int currentIndex,
required ValueChanged<int> onTap,
int unreadMessageCount = 0, // 消息 Tab 的未读角标数
})
// ── 主框架用法(在 ShellRoute 的 builder 中)────────
class MainShell extends StatelessWidget {
final StatefulNavigationShell navigationShell;
const MainShell({required this.navigationShell, super.key});
@override
Widget build(BuildContext context) {
// 从 IM 状态中读取未读总数
final unreadCount = ref.watch(imUnreadCountProvider);
return Scaffold(
body: navigationShell,
bottomNavigationBar: AppBottomNavBar(
currentIndex: navigationShell.currentIndex,
unreadMessageCount: unreadCount,
onTap: (index) => navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
),
),
);
}
}
// ── Tab 定义(供 AI 了解三个 Tab 的 icon 和 label)──
// index 0 → 首页 AppIcons.home / AppIcons.homeFilled
// index 1 → 消息 AppIcons.message / AppIcons.messageFilled (带未读角标)
// index 2 → 我的 AppIcons.profile / AppIcons.profileFilled
// ── 消息 Tab 未读角标渲染逻辑 ────────────────────────
// unreadMessageCount == 0 → 不显示角标
// 1 ~ 99 → 显示数字
// > 99 → 显示 "99+"
Stack(
clipBehavior: Clip.none,
children: [
Icon(AppIcons.message, color: tabColor),
if (unreadMessageCount > 0)
Positioned(
right: -6, top: -4,
child: (unreadMessageCount > 99 ? '99+' : unreadMessageCount.toString())
.text.bold.xs.white.make()
.box.color(AppColors.error).rounded.make()
.px(4).py(2),
),
],
)
```
---
### 9.6 导航栏选择速查表
| 场景 | 使用组件 | 关键参数 |
|------|----------|----------|
| 普通页面(设置、列表、详情) | `AppNavBar` | `title`, `actions` |
| 有表单、需要防误退 | `AppNavBar` | `onBack` 自定义返回逻辑 |
| Tab 根页面(首页/消息/我的) | `AppNavBar` | `showBack: false` |
| 个人主页、封面大图页 | `AppTransparentNavBar` | `scrollController`, `fadeStartOffset` |
| 发现页搜索 Tab | `AppSearchNavBar` | `showBack: false` |
| 搜索结果页 | `AppSearchNavBar` | `autofocus: true` |
| 一对一聊天页 | `AppChatNavBar` | `isOnline`, `avatarUrl`, `name` |
| App 主框架 | `AppBottomNavBar` | `unreadMessageCount` |
---
### 9.7 导航栏通用禁止写法
```dart
// ❌ 禁止直接使用原生 AppBar
Scaffold(
appBar: AppBar(title: Text('设置')),
)
// ✅ 使用封装组件
Scaffold(
appBar: AppNavBar(title: '设置'),
)
// ❌ 禁止在普通页面用透明导航栏(无 scrollController)
AppTransparentNavBar(title: '设置') // 没有传 scrollController,透明度永远不变
// ❌ 禁止在聊天页手拼头像 + 在线状态
AppBar(
title: Row(children: [CircleAvatar(...), Column(children: [Text(name), Text('在线')])]),
)
// ✅ 使用 AppChatNavBar
AppChatNavBar(avatarUrl: ..., name: ..., isOnline: ...)
// ❌ 禁止在 BottomNavigationBar 里手写未读角标逻辑
BottomNavigationBar(items: [...])
// ✅ 使用 AppBottomNavBar
AppBottomNavBar(currentIndex: ..., unreadMessageCount: ..., onTap: ...)
```
---
## 十、完整页面级组合示例
### 示例一:用户列表页(PagedListView + CustomAvatar + AppCapsuleButton)
```dart
class UserListPage extends HookConsumerWidget {
const UserListPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(userListControllerProvider);
return Scaffold(
appBar: AppBar(title: '推荐好友'.text.bold.make()),
body: PagedListView<UserModel>(
state: state,
skeletonBuilder: () => AppSkeleton.listTile(),
onRefresh: () => ref.refresh(userListControllerProvider),
onLoadMore: () => ref.read(userListControllerProvider.notifier).loadMore(),
itemBuilder: (context, user, index) => [
// 头像
CustomAvatar(url: user.avatarUrl, name: user.name, size: AvatarSize.df),
// 名字 + 简介
[
user.name.text.bold.base.color(AppColors.textPrimary).make(),
user.bio.text.sm.color(AppColors.textSecondary).maxLines(1).ellipsis.make(),
].vStack(crossAlignment: CrossAxisAlignment.start, spacing: AppSpacing.xs).expand(),
// 关注胶囊按钮
AppCapsuleButton(
variant: user.followVariant,
onTap: () => ref.read(userListControllerProvider.notifier).toggleFollow(user.id),
),
].hStack(spacing: AppSpacing.sm, crossAlignment: CrossAxisAlignment.center)
.p(AppSpacing.md)
.onTap(() => context.push(AppRoutes.userProfile, extra: user.id)),
),
);
}
}
```
### 示例二:聊天会话列表项(CustomAvatar + 未读徽章)
```dart
// 会话列表项(在 PagedListView 的 itemBuilder 中使用)
(context, conv, index) => [
// 头像 + 未读角标
Stack(
clipBehavior: Clip.none,
children: [
CustomAvatar(url: conv.avatar, name: conv.name, size: AvatarSize.df),
if (conv.unreadCount > 0)
Positioned(
right: -4, top: -4,
child: (conv.unreadCount > 99 ? '99+' : conv.unreadCount.toString())
.text.bold.xs.white.make()
.box.color(AppColors.error).rounded.make()
.px(6).py(2),
),
],
),
// 名称 + 最新消息 + 时间
[
[
conv.name.text.bold.base.color(AppColors.textPrimary).make().expand(),
conv.lastTime.text.xs.color(AppColors.textHint).make(),
].hStack(crossAlignment: CrossAxisAlignment.center),
conv.lastMessage.text.sm.color(AppColors.textSecondary).maxLines(1).ellipsis.make(),
].vStack(crossAlignment: CrossAxisAlignment.start, spacing: AppSpacing.xs).expand(),
].hStack(spacing: AppSpacing.sm, crossAlignment: CrossAxisAlignment.center)
.p(AppSpacing.md)
.onTap(() => context.push(AppRoutes.chat, extra: conv.id))
```
### 示例三:表单提交页(输入框 + PrimaryActionButton)
```dart
class LoginPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final phoneCtrl = useTextEditingController();
final codeCtrl = useTextEditingController();
return Scaffold(
body: [
// 标题
'欢迎回来'.text.bold.xl3.color(AppColors.textPrimary).make(),
SizedBox(height: AppSpacing.xs),
'请登录你的账号'.text.base.color(AppColors.textSecondary).make(),
SizedBox(height: AppSpacing.xl),
// 手机号
[
'手机号'.text.sm.bold.color(AppColors.textPrimary).make(),
SizedBox(height: AppSpacing.xs),
TextField(
controller: phoneCtrl,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: '请输入手机号',
filled: true, fillColor: AppColors.bgSecondary,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
),
),
),
].vStack(crossAlignment: CrossAxisAlignment.start),
SizedBox(height: AppSpacing.md),
// 验证码
[
'验证码'.text.sm.bold.color(AppColors.textPrimary).make(),
SizedBox(height: AppSpacing.xs),
[
TextField(
controller: codeCtrl,
keyboardType: TextInputType.number,
maxLength: 6,
decoration: InputDecoration(
hintText: '请输入验证码',
counterText: '',
filled: true, fillColor: AppColors.bgSecondary,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: BorderSide.none,
),
),
).expand(),
SizedBox(width: AppSpacing.sm),
_SendCodeButton(phone: phoneCtrl.text),
].hStack(crossAlignment: CrossAxisAlignment.center),
].vStack(crossAlignment: CrossAxisAlignment.start),
SizedBox(height: AppSpacing.xl),
// 主按钮(自动 Loading)
PrimaryActionButton(
label: '登录',
onPressed: () async {
await ref.read(authController.notifier).login(
phone: phoneCtrl.text,
code: codeCtrl.text,
);
},
),
SizedBox(height: AppSpacing.md),
// 隐私协议
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: AppTextStyles.caption.copyWith(color: AppColors.textHint),
children: [
const TextSpan(text: '登录即代表同意'),
TextSpan(
text: '《用户协议》',
style: TextStyle(color: AppColors.primary),
recognizer: TapGestureRecognizer()..onTap = () => context.push(AppRoutes.agreement),
),
const TextSpan(text: '和'),
TextSpan(
text: '《隐私政策》',
style: TextStyle(color: AppColors.primary),
recognizer: TapGestureRecognizer()..onTap = () => context.push(AppRoutes.privacy),
),
],
),
).wFull(context),
].vStack(crossAlignment: CrossAxisAlignment.start)
.p(AppSpacing.xl)
.wFull(context),
);
}
}
```
### 示例四:空状态页(AppEmpty)
```dart
// 在 state.when 的 error 分支,或无数据时使用
AppEmpty(
icon: AppIcons.emptyMessage,
title: '还没有消息',
subtitle: '去认识新朋友吧',
actionLabel: '去探索',
onAction: () => context.push(AppRoutes.discover),
)
// 网络错误
AppEmpty(
icon: AppIcons.networkError,
title: '网络开小差了',
subtitle: '请检查网络后重试',
actionLabel: '重新加载',
onAction: () => ref.refresh(xxxControllerProvider),
)
```
---
## 十一、禁止写法对照表
| ❌ 禁止 | ✅ 替代 | 原因 |
|---------|---------|------|
| `CircleAvatar(backgroundImage: ...)` | `CustomAvatar(url: ..., name: ...)` | 无 OSS 裁剪、无防闪烁缓存、无兜底占位 |
| `CachedNetworkImage` 手拼头像 | `CustomAvatar(url: ..., name: ...)` | 同上 |
| 手动写关注按钮样式 | `AppCapsuleButton(variant: ...)` | 样式不统一,状态流转容易出错 |
| 手动管理 `isLoading` + `ElevatedButton` | `PrimaryActionButton(onPressed: () async {...})` | 重复造轮子,容易漏处理连点 |
| `Shimmer.fromColors(...)` 手拼骨架 | `AppSkeleton.listTile()` / `.card()` / `.circle()` | 骨架样式不统一 |
| `CircularProgressIndicator()` 裸用 | `AppLoading()` | 颜色不统一 |
| `EasyRefresh(...)` 手拼刷新 | `AppRefresh(onRefresh: ...)` 或 `PagedListView` | 刷新动画不统一 |
| 手动拼分页逻辑(下拉/上拉/空态) | `PagedListView<T>(state: ..., itemBuilder: ...)` | 大量重复代码 |
| 手写空状态 `Column(Lottie + Text + Button)` | `AppEmpty(icon: ..., title: ...)` | 缺少"呼吸+微浮动"动画,视觉不一致 |
| `'文字'.text.red500.make()` | `'文字'.text.color(AppColors.error).make()` | VelocityX 内置色绕过了 AppColors 规范 |
| `Container(color: Colors.blue)` | `child.box.color(AppColors.primary).make()` | 同上 |
| `Padding(padding: EdgeInsets.all(16))` | `child.p(AppSpacing.md).make()` | 间距不走 AppSpacing 常量 |
| `SizedBox(width: double.infinity)` | `widget.wFull(context)` | 语义不清晰 |
| `Row(children: [a, b])` | `[a, b].hStack()` | VelocityX 项目统一风格 |
| `Column(children: [a, b])` | `[a, b].vStack()` | 同上 |
| `GestureDetector(onTap: fn, child: w)` | `w.onTap(fn)` | 同上 |
| `AppBar(title: Text('xxx'))` 原生 AppBar | `AppNavBar(title: 'xxx')` | 返回按钮图标不统一,缺自定义返回逻辑 |
| 聊天页手拼头像 + 在线状态 AppBar | `AppChatNavBar(avatarUrl:..., name:..., isOnline:...)` | 重复实现,在线绿点逻辑容易遗漏 |
| `BottomNavigationBar` 手写未读角标 | `AppBottomNavBar(unreadMessageCount: ...)` | 99+ 截断逻辑容易写错 |
| `AppTransparentNavBar` 不传 `scrollController` | 必须传入页面的 `scrollController` | 不传则渐变永远不触发 |
---
---
## 十二、AppTabBar · 选项卡
> 路径:`lib/widgets/app_tab_bar.dart`
> 标准化的选项卡组件,支持滚动与自定义交互。
```dart
AppTabBar(
tabs: const ['热门', '推荐', '关注'],
controller: _tabController,
onTap: (index) => print('Selected $index'),
)
```
---
## 十三、组件自动化展示
开发环境下可以通过路由 `/showcase` 访问所有组件的视觉样例。
```dart
context.push(AppRoutes.showcase);
```
---
*文档版本:v2.2 · VelocityX ^4.0.0 · 新增 AppTabBar 与组件展示页说明*