← API | 列表 | 同伴App_VelocityX速查手册
提示信息
# 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 与组件展示页说明*