# Mini UI包开发规范

## 版本信息
| 版本 | 日期 | 描述 | 作者 |
|------|------|------|------|
| 1.1 | 2025-12-26 | 添加图标使用规范（Heroicons） | Bob (Scrum Master) |
| 1.0 | 2025-12-26 | 基于史诗011和017经验创建Mini UI包开发规范 | Bob (Scrum Master) |

## 概述

本文档专门针对Taro小程序UI包（mini-ui-packages）的开发规范，基于史诗011（用人方小程序）和史诗017（人才小程序）的实施经验总结。Mini UI包与Web UI包有显著的差异，特别是在布局、组件行为和平台特性方面。

**适用范围:**
- `mini-ui-packages/` 目录下的所有UI包
- 使用Taro框架的小程序项目
- 所有基于`@tarojs/components`的组件开发

## Taro小程序核心布局规范

### 1. View组件的默认布局行为

**重要**: 在Taro小程序中，`<View>` 组件内的子元素默认是**横向布局**（`flex-row`），这与Web开发的div默认垂直布局行为完全不同。

#### 1.1 垂直布局规范

**问题**: View容器默认横向布局，导致子元素横向排列

**解决方案**: 必须显式添加 `flex flex-col` 类才能实现垂直布局

**正确示例**:
```typescript
import { View, Text } from '@tarojs/components'

// ✅ 正确: 使用 flex flex-col 实现垂直布局
<View className="flex flex-col">
  <Text>姓名: 张三</Text>
  <Text>性别: 男</Text>
  <Text>年龄: 35</Text>
</View>

// ❌ 错误: 缺少 flex flex-col，子元素会横向排列
<View>
  <Text>姓名: 张三</Text>
  <Text>性别: 男</Text>
  <Text>年龄: 35</Text>
</View>
```

#### 1.2 信息卡片布局模式

**来源**: 史诗011.003经验总结

**标准模式**:
```typescript
import { View, Text } from '@tarojs/components'

export function PersonalBasicInfo({ personalInfo }: { personalInfo: PersonalInfoResponse }) {
  return (
    <View className="bg-white rounded-lg p-4">
      <Text className="text-lg font-semibold mb-4">个人基本信息</Text>

      {/* 垂直布局的信息列表 - 必须使用 flex flex-col */}
      <View className="flex flex-col space-y-2">
        <View className="flex justify-between">
          <Text className="text-gray-600">姓名</Text>
          <Text>{personalInfo.name}</Text>
        </View>

        <View className="flex justify-between">
          <Text className="text-gray-600">性别</Text>
          <Text>{personalInfo.gender}</Text>
        </View>

        <View className="flex justify-between">
          <Text className="text-gray-600">年龄</Text>
          <Text>{personalInfo.age}</Text>
        </View>

        {/* 更多字段... */}
      </View>
    </View>
  )
}
```

**关键点**:
1. **信息列表容器**必须使用 `flex flex-col` 实现垂直布局
2. **每个信息项**使用 `flex justify-between` 实现标签和值的左右分布
3. 使用 `space-y-2` 或 `space-y-3` 添加垂直间距
4. **重要**: 记住在所有需要垂直排列的 View 上添加 `flex flex-col`

### 2. Text组件的默认内联行为

**问题**: Text组件默认是内联显示（类似Web中的`<span>`），不会自动换行

**影响**: 导致多个Text组件在同一行显示，即使它们在不同的代码行上

**解决方案**: 使用`flex flex-col`强制Text组件垂直排列

**实际案例** (来源: 史诗011.003):
```typescript
// ❌ 错误: Text组件会内联显示在同一行
<View>
  <Text>统计1</Text>
  <Text>统计2</Text>
  <Text>统计3</Text>
</View>

// ✅ 正确: 使用 flex flex-col 强制垂直排列
<View className="flex flex-col">
  <Text>统计1</Text>
  <Text>统计2</Text>
  <Text>统计3</Text>
</View>
```

### 3. 常见布局模式

#### 3.1 卡片容器布局

```typescript
<View className="bg-white rounded-lg p-4 shadow-sm">
  <Text className="text-lg font-semibold mb-4">卡片标题</Text>

  {/* 内容区域 - 垂直布局 */}
  <View className="flex flex-col space-y-3">
    {items.map(item => (
      <View key={item.id} className="flex justify-between border-b pb-2">
        <Text className="text-gray-600">{item.label}</Text>
        <Text>{item.value}</Text>
      </View>
    ))}
  </View>
</View>
```

#### 3.2 列表项布局

```typescript
<View className="flex flex-col space-y-2">
  {list.map(item => (
    <View key={item.id} className="bg-white rounded-lg p-3">
      {/* 列表项标题 */}
      <Text className="font-semibold mb-2">{item.title}</Text>

      {/* 列表项内容 - 垂直布局 */}
      <View className="flex flex-col space-y-1">
        <Text className="text-sm text-gray-600">{item.description}</Text>
        <Text className="text-xs text-gray-400">{item.date}</Text>
      </View>
    </View>
  ))}
</View>
```

#### 3.3 网格布局

```typescript
{/* 2列网格 */}
<View className="grid grid-cols-2 gap-3">
  {items.map(item => (
    <View key={item.id} className="bg-white rounded-lg p-3">
      <View className="flex flex-col">
        <Text className="font-semibold">{item.title}</Text>
        <Text className="text-sm text-gray-600">{item.value}</Text>
      </View>
    </View>
  ))}
</View>
```

## 图标使用规范

### 3.1 图标系统概述

**项目使用的图标库**: Heroicons (UnoCSS图标集)

**图标类命名规范**: `i-heroicons-{icon-name}-{size}-{style}`

**重要**: **不要使用emoji**，必须使用Heroicons图标类。

### 3.2 图标类使用规范

#### 3.2.1 基础图标使用

**正确示例**:
```typescript
import { View } from '@tarojs/components'

// ✅ 正确: 使用Heroicons图标类
<View className="i-heroicons-chevron-left-20-solid w-5 h-5 text-gray-600" />
<View className="i-heroicons-user-20-solid w-6 h-6 text-blue-500" />
<View className="i-heroicons-bell-20-solid w-4 h-4 text-gray-400" />

// ❌ 错误: 使用emoji
<Text>🔔</Text>
<Text>👤</Text>
<View>←</View>
```

#### 3.2.2 图标类命名格式

**格式**: `i-heroicons-{图标名称}-{尺寸}-{样式}`

**常用图标名称**:
- `chevron-left` - 左箭头
- `chevron-right` - 右箭头
- `user` - 用户
- `bell` - 通知铃
- `document-text` - 文档
- `chart-bar` - 图表
- `building-office` - 建筑/企业
- `calendar` - 日历
- `phone` - 电话
- `lock-closed` - 锁
- `camera` - 相机
- `qr-code` - 二维码
- `device-phone-mobile` - 手机
- `arrow-right-on-rectangle` - 登出/外跳
- `arrow-left-on-rectangle` - 登入/内跳
- `exclamation-triangle` - 警告
- `exclamation-circle` - 提示
- `photo` - 图片
- `arrow-path` - 加载中

**尺寸选项**:
- `20` - 20x20 (推荐用于小程序)
- `24` - 24x24

**样式选项**:
- `solid` - 实心图标（推荐）
- `outline` - 轮廓图标

#### 3.2.3 图标尺寸和颜色

**尺寸类**:
```typescript
<View className="i-heroicons-user-20-solid w-4 h-4" />   // 16px
<View className="i-heroicons-user-20-solid w-5 h-5" />   // 20px
<View className="i-heroicons-user-20-solid w-6 h-6" />   // 24px
<View className="i-heroicons-user-20-solid w-8 h-8" />   // 32px
<View className="i-heroicons-user-20-solid text-xl" />  // 使用Tailwind文本尺寸
<View className="i-heroicons-user-20-solid text-2xl" /> // 使用Tailwind文本尺寸
```

**颜色类**:
```typescript
<View className="i-heroicons-user-20-solid text-gray-400" />  // 灰色
<View className="i-heroicons-user-20-solid text-blue-500" />  // 蓝色
<View className="i-heroicons-user-20-solid text-green-500" /> // 绿色
<View className="i-heroicons-user-20-solid text-red-500" />   // 红色
<View className="i-heroicons-user-20-solid text-white" />    // 白色
<View className="i-heroicons-user-20-solid text-yellow-600" /> // 黄色
<View className="i-heroicons-user-20-solid text-purple-500" /> // 紫色
```

#### 3.2.4 常见使用场景

**导航栏返回按钮**:
```typescript
<View className="i-heroicons-chevron-left-20-solid w-5 h-5" />
```

**功能入口图标**:
```typescript
<View className="flex flex-col items-center">
  <View className="i-heroicons-user-20-solid text-blue-500 text-lg mb-1" />
  <Text className="text-sm">个人信息</Text>
</View>

<View className="flex flex-col items-center">
  <View className="i-heroicons-document-text-20-solid text-green-500 text-lg mb-1" />
  <Text className="text-sm">考勤记录</Text>
</View>

<View className="flex flex-col items-center">
  <View className="i-heroicons-chart-bar-20-solid text-purple-500 text-lg mb-1" />
  <Text className="text-sm">薪资查询</Text>
</View>

<View className="flex flex-col items-center">
  <View className="i-heroicons-building-office-2-20-solid text-yellow-600 text-lg mb-1" />
  <Text className="text-sm">企业信息</Text>
</View>
```

**状态指示器**:
```typescript
// 加载中
<View className="i-heroicons-arrow-path-20-solid animate-spin w-5 h-5" />

// 成功/提示
<View className="i-heroicons-check-circle-20-solid text-green-500 w-6 h-6" />

// 警告
<View className="i-heroicons-exclamation-triangle-20-solid text-orange-500 w-6 h-6" />

// 错误
<View className="i-heroicons-x-circle-20-solid text-red-500 w-6 h-6" />

// 信息
<View className="i-heroicons-information-circle-20-solid text-blue-500 w-6 h-6" />
```

**输入框图标**:
```typescript
<View className="flex items-center">
  <View className="i-heroicons-phone-20-solid text-gray-400 mr-3 w-5 h-5" />
  <Input placeholder="请输入手机号" />
</View>

<View className="flex items-center">
  <View className="i-heroicons-lock-closed-20-solid text-gray-400 mr-3 w-5 h-5" />
  <Input placeholder="请输入密码" type="password" />
</View>
```

**二维码按钮**:
```typescript
<View className="flex items-center">
  <Text>张三</Text>
  <View className="i-heroicons-qr-code-20-solid text-white text-lg ml-2" />
</View>
```

#### 3.2.5 动画效果

**旋转动画**:
```typescript
<View className="i-heroicons-arrow-path-20-solid animate-spin w-5 h-5" />
```

**脉冲动画**:
```typescript
<View className="i-heroicons-bell-20-solid animate-pulse w-6 h-6" />
```

### 3.3 Navbar图标规范

**返回按钮图标**:
```typescript
leftIcon = 'i-heroicons-chevron-left-20-solid'
```

**示例**:
```typescript
<Navbar
  title="页面标题"
  leftIcon="i-heroicons-chevron-left-20-solid"
  leftText="返回"
  onClickLeft={() => Taro.navigateBack()}
/>
```

### 3.4 TabBar图标规范

**使用iconClass属性**（推荐）:
```typescript
const tabList = [
  {
    title: '首页',
    iconClass: 'i-heroicons-home-20-solid',
    selectedIconClass: 'i-heroicons-home-20-solid',
    pagePath: '/pages/index/index'
  },
  {
    title: '考勤',
    iconClass: 'i-heroicons-calendar-20-solid',
    selectedIconClass: 'i-heroicons-calendar-20-solid',
    pagePath: '/pages/attendance/index'
  }
]
```

### 3.5 图标查找参考

**Heroicons官方图标库**: https://heroicons.com/

**使用UnoCSS图标集**: 项目使用UnoCSS的Heroicons图标集，所有图标名称遵循Heroicons命名规范。

**查找图标的方法**:
1. 访问 Heroicons 官网查找所需图标
2. 记录图标名称（如 `user`, `chevron-left`）
3. 使用格式: `i-heroicons-{图标名称}-20-solid`
4. 添加尺寸和颜色类

### 3.6 常见错误避免

**错误示例**:
```typescript
// ❌ 错误1: 使用emoji
<Text>🔔 通知</Text>
<View>👤</View>

// ❌ 错误2: 使用文本符号
<Text>← 返回</Text>
<View>→</View>

// ❌ 错误3: 使用其他图标库
<View className="fa fa-user" />
<View className="material-icons">person</View>

// ❌ 错误4: 忘记添加尺寸类
<View className="i-heroicons-user-20-solid" />  {/* 没有尺寸，可能不显示 */}

// ❌ 错误5: 图标类名拼写错误
<View className="i-heroicon-user-20-solid" />   {/* 缺少s */}
<View className="i-heroicons-user-20-solid" />   {/* ✅ 正确 */}
```

**正确示例**:
```typescript
// ✅ 正确1: 使用Heroicons图标类
<View className="i-heroicons-bell-20-solid w-5 h-5" />
<View className="i-heroicons-user-20-solid w-6 h-6" />

// ✅ 正确2: 添加尺寸和颜色
<View className="i-heroicons-chevron-left-20-solid w-5 h-5 text-gray-600" />

// ✅ 正确3: 图标+文本组合
<View className="flex items-center">
  <View className="i-heroicons-phone-20-solid text-blue-500 w-5 h-5 mr-2" />
  <Text>联系电话</Text>
</View>
```

## Taro组件使用规范

### 4.1 基础组件导入

```typescript
import { View, Text, Image, Button, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
```

### 4.2 Image组件规范

```typescript
<Image
  src={imageUrl}
  mode="aspectFill"  // 或 aspectFit, widthFix
  className="w-full h-32 rounded-lg"
  lazyLoad          // 懒加载
  onClick={handleClick}
/>
```

**mode模式说明**:
- `aspectFill`: 保持纵横比缩放图片，确保图片填充整个容器（可能裁剪）
- `aspectFit`: 保持纵横比缩放图片，确保图片完全显示（可能有空白）
- `widthFix`: 宽度不变，高度自动变化，保持原图宽高比

### 4.3 ScrollView组件规范

```typescript
<ScrollView
  scrollY           // 垂直滚动
  className="h-full"
  onScrollToLower={handleLoadMore}
  lowerThreshold={100}
>
  <View className="flex flex-col">
    {items.map(item => (
      <View key={item.id}>{item.content}</View>
    ))}
  </View>
</ScrollView>
```

### 4.4 Button组件规范

**注意**: Taro的Button组件有默认样式，如需自定义样式建议使用View

```typescript
// ✅ 推荐: 使用View实现自定义按钮
<View
  className="bg-blue-500 text-white py-2 px-4 rounded text-center"
  onClick={handleClick}
>
  <Text>确定</Text>
</View>

// ⚠️ 谨慎使用: Taro Button组件有平台默认样式
<Button onClick={handleClick}>确定</Button>
```

## Navbar导航栏集成规范

### 5.1 Navbar组件来源

```typescript
import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'
```

### 5.2 页面层级划分

**TabBar页面（一级，无返回按钮）**:
- 首页/个人主页
- 列表页
- 个人信息页
- 设置页

**非TabBar页面（二级，带返回按钮）**:
- 详情页
- 编辑页
- 从其他页面跳转来的页面

### 5.3 Navbar配置规范

**TabBar页面（无返回按钮）**:
```typescript
<Navbar
  title="页面标题"
  leftIcon=""
  leftText=""
  onClickLeft={() => {}}
  placeholder
  fixed
/>
```

**非TabBar页面（带返回按钮）**:
```typescript
import Taro from '@tarojs/taro'

<Navbar
  title="页面标题"
  leftIcon="i-heroicons-chevron-left-20-solid"
  leftText="返回"
  onClickLeft={() => Taro.navigateBack()}
  placeholder
  fixed
/>
```

### 5.4 完整页面结构示例

```typescript
import { View, ScrollView } from '@tarojs/components'
import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'

export function MyPage() {
  return (
    <View className="h-screen bg-gray-100">
      {/* Navbar导航栏 */}
      <Navbar
        title="页面标题"
        leftIcon=""
        leftText=""
        onClickLeft={() => {}}
        placeholder
        fixed
      />

      {/* 页面内容 */}
      <ScrollView scrollY className="h-full">
        <View className="flex flex-col space-y-3 p-4">
          {/* 页面内容 */}
        </View>
      </ScrollView>
    </View>
  )
}
```

## 照片预览功能实现

### 6.1 使用Taro.previewImage

```typescript
import Taro from '@tarojs/taro'
import { View, Image, Text } from '@tarojs/components'

interface DocumentPhotoItemProps {
  type: string
  url: string
}

export function DocumentPhotoItem({ type, url }: DocumentPhotoItemProps) {
  const handlePreview = () => {
    Taro.previewImage({
      current: url,    // 当前显示图片的http链接
      urls: [url]      // 需要预览的图片http链接列表
    })
  }

  return (
    <View onClick={handlePreview} className="flex flex-col">
      <Image
        src={url}
        mode="aspectFill"
        className="w-full h-32 rounded-lg"
      />
      <Text>{type}</Text>
    </View>
  )
}
```

**多图片预览**:
```typescript
const handlePreview = (currentIndex: number) => {
  Taro.previewImage({
    current: images[currentIndex].url,
    urls: images.map(img => img.url)
  })
}
```

## 数据脱敏规范

### 7.1 银行卡号脱敏

```typescript
/**
 * 脱敏银行卡号
 * @param cardNumber 完整银行卡号
 * @returns 脱敏后的银行卡号（如：**** **** **** 1234）
 */
export function maskCardNumber(cardNumber: string): string {
  if (!cardNumber || cardNumber.length < 4) {
    return '****'
  }
  const last4 = cardNumber.slice(-4)
  return `**** **** **** ${last4}`
}

// 使用示例
<View className="flex justify-between">
  <Text className="text-gray-600">银行卡号</Text>
  <Text>{maskCardNumber(bankCard.cardNumber)}</Text>
</View>
```

### 7.2 身份证号脱敏

```typescript
/**
 * 脱敏身份证号
 * @param idCard 完整身份证号
 * @returns 脱敏后的身份证号（如：3301**********1234）
 */
export function maskIdCard(idCard: string): string {
  if (!idCard || idCard.length < 8) {
    return '********'
  }
  const prefix = idCard.slice(0, 4)
  const suffix = idCard.slice(-4)
  return `${prefix}**********${suffix}`
}

// 使用示例
<View className="flex justify-between">
  <Text className="text-gray-600">身份证号</Text>
  <Text>{maskIdCard(personalInfo.idCard)}</Text>
</View>
```

### 7.3 手机号脱敏

```typescript
/**
 * 脱敏手机号
 * @param phone 完整手机号
 * @returns 脱敏后的手机号（如：138****5678）
 */
export function maskPhone(phone: string): string {
  if (!phone || phone.length < 7) {
    return '****'
  }
  return `${phone.slice(0, 3)}****${phone.slice(-4)}`
}
```

## Mini UI包结构规范

### 8.1 标准目录结构

```text
mini-ui-packages/<package-name>/
├── src/
│   ├── pages/                    # 页面组件
│   │   └── PageName/
│   │       ├── PageName.tsx
│   │       └── index.ts
│   ├── components/               # UI组件
│   │   ├── ComponentName.tsx
│   │   └── index.ts
│   ├── api/                      # API客户端
│   │   ├── client.ts
│   │   └── index.ts
│   ├── utils/                    # 工具函数
│   │   ├── helpers.ts
│   │   └── index.ts
│   └── index.ts                  # 主入口
├── tests/                        # 测试文件
│   ├── pages/
│   │   └── PageName/
│   │       └── PageName.test.tsx
│   └── components/
│       └── ComponentName.test.tsx
├── package.json
├── jest.config.cjs               # Jest配置
└── tsconfig.json
```

### 8.2 package.json配置

```json
{
  "name": "@d8d/<package-name>",
  "version": "1.0.0",
  "type": "module",
  "main": "src/index.ts",
  "types": "src/index.ts",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts"
    },
    "./api": {
      "types": "./src/api/index.ts",
      "import": "./src/api/index.ts"
    },
    "./pages/<PageName>/<PageName>": {
      "types": "./src/pages/<PageName>/<PageName>.tsx",
      "import": "./src/pages/<PageName>/<PageName>.tsx"
    }
  },
  "dependencies": {
    "@d8d/mini-shared-ui-components": "workspace:*",
    "@tarojs/components": "^4.1.4",
    "@tarojs/taro": "^4.1.4",
    "react": "^19.1.0"
  },
  "devDependencies": {
    "@testing-library/react": "^16.3.0",
    "jest": "^30.2.0",
    "ts-jest": "^29.4.5"
  }
}
```

## 数据获取规范

### 9.1 使用React Query管理服务端状态

**重要**: Mini UI包必须使用React Query (`@tanstack/react-query`) 管理服务端状态，而不是手动使用`useState` + `useEffect`。

**原因**:
- 符合项目技术栈要求（见`component-architecture.md`）
- 自动处理加载状态、错误状态、缓存
- 更好的类型推断和RPC集成
- 统一的数据获取模式

#### 9.1.1 基本用法

```typescript
import { useQuery } from '@tanstack/react-query'
import { apiClient } from '../api'

const MyPage: React.FC = () => {
  // ✅ 正确: 使用React Query
  const { data, isLoading, error } = useQuery({
    queryKey: ['resource-name'],
    queryFn: async () => {
      const res = await apiClient.resource.$get()
      if (!res.ok) {
        throw new Error('获取数据失败')
      }
      return await res.json()
    }
  })

  // ❌ 错误: 不要使用useState + useEffect手动管理状态
  // const [data, setData] = useState(null)
  // const [loading, setLoading] = useState(true)
  // useEffect(() => {
  //   const fetchData = async () => {
  //     setLoading(true)
  //     const res = await apiClient.resource.$get()
  //     const data = await res.json()
  //     setData(data)
  //     setLoading(false)
  //   }
  //   fetchData()
  // }, [])

  if (isLoading) return <div>加载中...</div>
  if (error) return <div>加载失败</div>

  return <div>{/* 渲染数据 */}</div>
}
```

#### 9.1.2 多个独立查询

```typescript
const MyPage: React.FC = () => {
  // 多个独立的查询可以并行执行
  const { data: statusData, isLoading: statusLoading } = useQuery({
    queryKey: ['employment-status'],
    queryFn: async () => {
      const res = await apiClient.employment.status.$get()
      if (!res.ok) throw new Error('获取就业状态失败')
      return await res.json()
    }
  })

  const { data: recordsData, isLoading: recordsLoading } = useQuery({
    queryKey: ['salary-records'],
    queryFn: async () => {
      const res = await apiClient.employment['salary-records'].$get({
        query: { take: 3 }
      })
      if (!res.ok) throw new Error('获取薪资记录失败')
      const data = await res.json()
      return data.data || []
    }
  })

  const { data: historyData, isLoading: historyLoading } = useQuery({
    queryKey: ['employment-history'],
    queryFn: async () => {
      const res = await apiClient.employment.history.$get({
        query: { take: 20 }
      })
      if (!res.ok) throw new Error('获取就业历史失败')
      const data = await res.json()
      return data.data || []
    }
  })

  // 每个查询有独立的loading状态
  return (
    <View>
      <StatusCard data={statusData} loading={statusLoading} />
      <RecordsCard data={recordsData} loading={recordsLoading} />
      <HistoryCard data={historyData} loading={historyLoading} />
    </View>
  )
}
```

#### 9.1.3 错误处理

```typescript
const MyPage: React.FC = () => {
  const { data, isLoading, error } = useQuery({
    queryKey: ['resource'],
    queryFn: async () => {
      const res = await apiClient.resource.$get()
      if (!res.ok) {
        throw new Error('获取数据失败')
      }
      return await res.json()
    }
  })

  // 使用useEffect处理错误
  React.useEffect(() => {
    if (error) {
      Taro.showToast({
        title: error.message,
        icon: 'none'
      })
    }
  }, [error])

  if (isLoading) return <div>加载中...</div>

  return <div>{/* 渲染数据 */}</div>
}
```

#### 9.1.4 数据修改 (useMutation)

对于需要修改服务端数据的操作（POST、PUT、DELETE），使用`useMutation`：

```typescript
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { apiClient } from '../api'

const MyPage: React.FC = () => {
  const queryClient = useQueryClient()

  // 数据修改mutation
  const mutation = useMutation({
    mutationFn: async (formData: MyFormData) => {
      const res = await apiClient.resource.$post({
        json: formData
      })
      if (!res.ok) {
        throw new Error('操作失败')
      }
      return await res.json()
    },
    onSuccess: (data) => {
      // 成功后刷新相关查询
      queryClient.invalidateQueries({ queryKey: ['resource-list'] })
      Taro.showToast({
        title: '操作成功',
        icon: 'success'
      })
    },
    onError: (error) => {
      Taro.showToast({
        title: error.message,
        icon: 'none'
      })
    }
  })

  const handleSubmit = (formData: MyFormData) => {
    mutation.mutate(formData)
  }

  return (
    <View>
      <button onClick={() => handleSubmit(formData)}>
        {mutation.isPending ? '提交中...' : '提交'}
      </button>
    </View>
  )
}
```

**关键点**:
- 使用`mutationFn`定义异步操作
- 使用`onSuccess`处理成功逻辑，通常需要`invalidateQueries`刷新数据
- 使用`onError`处理错误
- 使用`isPending`判断加载状态

#### 9.1.5 无限滚动查询 (useInfiniteQuery)

对于分页列表数据，使用`useInfiniteQuery`实现无限滚动：

```typescript
import { useInfiniteQuery } from '@tanstack/react-query'
import { apiClient } from '../api'

const MyPage: React.FC = () => {
  // 无限滚动查询
  const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: ['infinite-list'],
    queryFn: async ({ pageParam = 0 }) => {
      const res = await apiClient.items.$get({
        query: { skip: pageParam * 20, take: 20 }
      })
      if (!res.ok) {
        throw new Error('获取数据失败')
      }
      const data = await res.json()
      return {
        items: data.data || [],
        nextCursor: pageParam + 1
      }
    },
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor
  })

  // 扁平化所有页的数据
  const allItems = data?.pages.flatMap(page => page.items) || []

  const handleLoadMore = () => {
    if (hasNextPage && !isFetchingNextPage) {
      fetchNextPage()
    }
  }

  return (
    <ScrollView
      scrollY
      onScrollToLower={handleLoadMore}
    >
      {allItems.map(item => (
        <View key={item.id}>{item.name}</View>
      ))}

      {isFetchingNextPage && <Text>加载更多...</Text>}
      {!hasNextPage && allItems.length > 0 && <Text>没有更多数据了</Text>}
    </ScrollView>
  )
}
```

**关键点**:
- `pageParam`用于传递分页参数
- `getNextPageParam`决定是否有下一页
- 使用`pages.flatMap`合并所有页数据
- 使用`fetchNextPage`加载下一页
- 使用`hasNextPage`和`isFetchingNextPage`控制加载状态

## 测试规范

### 10.1 Jest配置

```javascript
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['@d8d/mini-testing-utils/testing/setup'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^~/(.*)$': '<rootDir>/tests/$1',
    '^@tarojs/taro$': '@d8d/mini-testing-utils/testing/taro-api-mock.ts',
    '\\.(css|less|scss)$': '@d8d/mini-testing-utils/testing/style-mock.js'
  },
  testMatch: [
    '<rootDir>/tests/**/*.test.{ts,tsx}'
  ],
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest'
  }
}
```

### 10.2 组件测试示例

```typescript
import { render, screen } from '@testing-library/react'
import { View, Text } from '@tarojs/components'
import { MyComponent } from '../MyComponent'

describe('MyComponent', () => {
  it('渲染组件并验证垂直布局', () => {
    render(<MyComponent />)

    // 验证组件包含 flex flex-col 类
    const container = screen.getByTestId('my-container')
    expect(container.className).toContain('flex flex-col')
  })
})
```

### 10.3 页面集成测试（使用React Query）

**重要**: 页面集成测试必须使用真实的React Query，不要mock React Query。

```typescript
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import MyPage from '../pages/MyPage'

// Mock API client
jest.mock('../api', () => ({
  apiClient: {
    resource: {
      $get: jest.fn()
    }
  }
}))

const { apiClient } = require('../api')

const createTestQueryClient = () => new QueryClient({
  defaultOptions: {
    queries: { retry: false, staleTime: Infinity },
    mutations: { retry: false }
  }
})

const renderWithQueryClient = (component: React.ReactElement) => {
  const queryClient = createTestQueryClient()
  return render(
    <QueryClientProvider client={queryClient}>
      {component}
    </QueryClientProvider>
  )
}

describe('MyPage', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('应该显示加载状态', async () => {
    // Mock API为pending状态
    apiClient.resource.$get.mockImplementation(() => new Promise(() => {}))

    renderWithQueryClient(<MyPage />)

    expect(screen.getByText('加载中...')).toBeInTheDocument()
  })

  it('应该成功加载并显示数据', async () => {
    const mockData = { name: '测试数据' }
    apiClient.resource.$get.mockResolvedValue({
      ok: true,
      json: async () => mockData
    })

    renderWithQueryClient(<MyPage />)

    await waitFor(() => {
      expect(screen.getByText('测试数据')).toBeInTheDocument()
    })
  })
})
```

## 常见问题和解决方案

### 11.1 布局问题

**问题**: 元素横向排列而不是垂直排列
- **原因**: View容器默认是flex-row
- **解决**: 添加`flex flex-col`类

**问题**: Text组件在同一行显示
- **原因**: Text组件默认是内联显示
- **解决**: 父容器添加`flex flex-col`类

### 11.2 样式问题

**问题**: Tailwind样式不生效
- **原因**: 类名冲突或拼写错误
- **解决**: 检查类名拼写，确保使用正确的Tailwind类名

**问题**: 样式在不同平台表现不一致
- **原因**: 不同小程序平台的样式引擎差异
- **解决**: 使用Taro提供的跨平台样式方案，避免使用平台特有样式

### 11.3 API问题

**问题**: RPC客户端类型错误
- **原因**: API路径映射错误或类型推断不正确
- **解决**: 验证后端路由定义，使用RPC推断类型

### 11.4 React Query问题

**问题**: 测试中React Query不工作
- **原因**: 忘记使用QueryClientProvider包裹
- **解决**: 使用renderWithQueryClient包装组件

**问题**: queryKey冲突导致数据混乱
- **原因**: 不同查询使用了相同的queryKey
- **解决**: 为每个查询使用唯一的queryKey

**问题**: mutation后数据没有更新
- **原因**: 忘记调用invalidateQueries
- **解决**: 在onSuccess回调中刷新相关查询

**问题**: 无限滚动一直触发加载
- **原因**: getNextPageParam返回逻辑错误
- **解决**: 正确判断是否还有下一页，返回undefined或nextCursor

## 最佳实践

### 12.1 组件开发

1. **始终使用flex flex-col实现垂直布局**
2. **为每个View添加语义化的className**
3. **使用data-testid属性便于测试**
4. **组件props使用TypeScript接口定义**
5. **使用相对路径导入包内模块**
6. **使用React Query管理服务端状态**
7. **为每个query使用唯一的queryKey**

### 12.2 数据获取

1. **使用useQuery获取数据，不要使用useState + useEffect**
2. **在queryFn中检查response.ok，失败时throw Error**
3. **使用useEffect处理错误，显示Toast提示**
4. **多个独立查询使用不同的queryKey**
5. **测试中使用真实的QueryClientProvider**

### 12.3 数据修改

1. **使用useMutation处理POST/PUT/DELETE操作**
2. **在mutationFn中检查response.ok，失败时throw Error**
3. **使用onSuccess刷新相关查询（invalidateQueries）**
4. **使用onError显示错误提示**
5. **使用isPending显示加载状态，避免重复提交**

### 12.4 无限滚动

1. **使用useInfiniteQuery处理分页列表**
2. **使用pages.flatMap合并所有页数据**
3. **正确实现getNextPageParam判断是否有下一页**
4. **使用hasNextPage和isFetchingNextPage控制加载状态**
5. **在ScrollView的onScrollToLower中触发fetchNextPage**

### 12.5 性能优化

1. **使用Image组件的lazyLoad属性**
2. **列表数据使用虚拟滚动**
3. **避免不必要的重渲染**
4. **使用React.memo优化组件性能**
5. **利用React Query的缓存机制减少重复请求**

### 12.6 代码质量

1. **遵循项目编码标准**
2. **编写单元测试和集成测试**
3. **使用TypeScript严格模式**
4. **运行pnpm typecheck确保类型正确**
5. **使用ESLint进行代码检查**

## 参考实现

### 13.1 用人方小程序UI包

- `mini-ui-packages/yongren-dashboard-ui`
- `mini-ui-packages/yongren-order-management-ui`
- `mini-ui-packages/yongren-talent-management-ui`

### 13.2 人才小程序UI包

- `mini-ui-packages/rencai-dashboard-ui`
- `mini-ui-packages/rencai-personal-info-ui`
- `mini-ui-packages/rencai-employment-ui` - 使用React Query的参考实现
- `mini-ui-packages/rencai-auth-ui`

### 13.3 共享组件

- `mini-ui-packages/mini-shared-ui-components`

## 版本历史

| 版本 | 日期 | 变更说明 | 作者 |
|------|------|----------|------|
| 1.3 | 2025-12-28 | 添加useMutation和useInfiniteQuery规范，完善React Query最佳实践 | James (Claude Code) |
| 1.2 | 2025-12-28 | 添加React Query数据获取规范，更新测试规范章节 | James (Claude Code) |
| 1.1 | 2025-12-26 | 添加图标使用规范（Heroicons） | Bob (Scrum Master) |
| 1.0 | 2025-12-26 | 基于史诗011和017经验创建Mini UI包开发规范 | Bob (Scrum Master) |

---

**重要提醒**:
1. 本规范专门针对Taro小程序UI包开发，与Web UI包开发规范（`ui-package-standards.md`）不同
2. `flex flex-col`是Taro小程序中最常用的布局类，请务必牢记
3. **使用React Query管理服务端状态**，不要使用useState + useEffect手动管理
4. 所有Mini UI包的开发都应遵循本规范
