# 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 Jest配置

```javascript
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['@d8d/mini-testing-utils/setup'],
  moduleNameMapper: {
    '^@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'
  }
}
```

### 9.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.1 布局问题

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

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

### 10.2 样式问题

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

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

### 10.3 API问题

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

## 最佳实践

### 11.1 组件开发

1. **始终使用flex flex-col实现垂直布局**
2. **为每个View添加语义化的className**
3. **使用data-testid属性便于测试**
4. **组件props使用TypeScript接口定义**
5. **使用相对路径导入包内模块**

### 11.2 性能优化

1. **使用Image组件的lazyLoad属性**
2. **列表数据使用虚拟滚动**
3. **避免不必要的重渲染**
4. **使用React.memo优化组件性能**

### 11.3 代码质量

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

## 参考实现

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

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

### 12.2 人才小程序UI包

- `mini-ui-packages/rencai-dashboard-ui`
- `mini-ui-packages/rencai-personal-info-ui`
- `mini-ui-packages/rencai-auth-ui`

### 12.3 共享组件

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

## 版本历史

| 版本 | 日期 | 变更说明 | 作者 |
|------|------|----------|------|
| 1.0 | 2025-12-26 | 初始版本，基于史诗011和017经验总结 | Bob (Scrum Master) |

---

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