| 版本 | 日期 | 描述 | 作者 |
|---|---|---|---|
| 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包@tarojs/components的组件开发重要: 在Taro小程序中,<View> 组件内的子元素默认是横向布局(flex-row),这与Web开发的div默认垂直布局行为完全不同。
问题: View容器默认横向布局,导致子元素横向排列
解决方案: 必须显式添加 flex flex-col 类才能实现垂直布局
正确示例:
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>
来源: 史诗011.003经验总结
标准模式:
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>
)
}
关键点:
flex flex-col 实现垂直布局flex justify-between 实现标签和值的左右分布space-y-2 或 space-y-3 添加垂直间距flex flex-col问题: Text组件默认是内联显示(类似Web中的<span>),不会自动换行
影响: 导致多个Text组件在同一行显示,即使它们在不同的代码行上
解决方案: 使用flex flex-col强制Text组件垂直排列
实际案例 (来源: 史诗011.003):
// ❌ 错误: 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>
<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>
<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>
{/* 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>
项目使用的图标库: Heroicons (UnoCSS图标集)
图标类命名规范: i-heroicons-{icon-name}-{size}-{style}
重要: 不要使用emoji,必须使用Heroicons图标类。
正确示例:
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>
格式: 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 - 轮廓图标尺寸类:
<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文本尺寸
颜色类:
<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" /> // 紫色
导航栏返回按钮:
<View className="i-heroicons-chevron-left-20-solid w-5 h-5" />
功能入口图标:
<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>
状态指示器:
// 加载中
<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" />
输入框图标:
<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>
二维码按钮:
<View className="flex items-center">
<Text>张三</Text>
<View className="i-heroicons-qr-code-20-solid text-white text-lg ml-2" />
</View>
旋转动画:
<View className="i-heroicons-arrow-path-20-solid animate-spin w-5 h-5" />
脉冲动画:
<View className="i-heroicons-bell-20-solid animate-pulse w-6 h-6" />
返回按钮图标:
leftIcon = 'i-heroicons-chevron-left-20-solid'
示例:
<Navbar
title="页面标题"
leftIcon="i-heroicons-chevron-left-20-solid"
leftText="返回"
onClickLeft={() => Taro.navigateBack()}
/>
使用iconClass属性(推荐):
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'
}
]
Heroicons官方图标库: https://heroicons.com/
使用UnoCSS图标集: 项目使用UnoCSS的Heroicons图标集,所有图标名称遵循Heroicons命名规范。
查找图标的方法:
user, chevron-left)i-heroicons-{图标名称}-20-solid错误示例:
// ❌ 错误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" /> {/* ✅ 正确 */}
正确示例:
// ✅ 正确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>
import { View, Text, Image, Button, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
<Image
src={imageUrl}
mode="aspectFill" // 或 aspectFit, widthFix
className="w-full h-32 rounded-lg"
lazyLoad // 懒加载
onClick={handleClick}
/>
mode模式说明:
aspectFill: 保持纵横比缩放图片,确保图片填充整个容器(可能裁剪)aspectFit: 保持纵横比缩放图片,确保图片完全显示(可能有空白)widthFix: 宽度不变,高度自动变化,保持原图宽高比<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>
注意: Taro的Button组件有默认样式,如需自定义样式建议使用View
// ✅ 推荐: 使用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>
import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'
TabBar页面(一级,无返回按钮):
非TabBar页面(二级,带返回按钮):
TabBar页面(无返回按钮):
<Navbar
title="页面标题"
leftIcon=""
leftText=""
onClickLeft={() => {}}
placeholder
fixed
/>
非TabBar页面(带返回按钮):
import Taro from '@tarojs/taro'
<Navbar
title="页面标题"
leftIcon="i-heroicons-chevron-left-20-solid"
leftText="返回"
onClickLeft={() => Taro.navigateBack()}
placeholder
fixed
/>
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>
)
}
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>
)
}
多图片预览:
const handlePreview = (currentIndex: number) => {
Taro.previewImage({
current: images[currentIndex].url,
urls: images.map(img => img.url)
})
}
/**
* 脱敏银行卡号
* @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>
/**
* 脱敏身份证号
* @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>
/**
* 脱敏手机号
* @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-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
{
"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"
}
}
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'
}
}
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')
})
})
问题: 元素横向排列而不是垂直排列
flex flex-col类问题: Text组件在同一行显示
flex flex-col类问题: Tailwind样式不生效
问题: 样式在不同平台表现不一致
问题: RPC客户端类型错误
mini-ui-packages/yongren-dashboard-uimini-ui-packages/yongren-order-management-uimini-ui-packages/yongren-talent-management-uimini-ui-packages/rencai-dashboard-uimini-ui-packages/rencai-personal-info-uimini-ui-packages/rencai-auth-uimini-ui-packages/mini-shared-ui-components| 版本 | 日期 | 变更说明 | 作者 |
|---|---|---|---|
| 1.0 | 2025-12-26 | 初始版本,基于史诗011和017经验总结 | Bob (Scrum Master) |
重要提醒:
ui-package-standards.md)不同flex flex-col是Taro小程序中最常用的布局类,请务必牢记