作为 人才小程序开发者, 我想要 实现个人信息页面功能, 以便 人才用户能够查看个人基本信息、银行卡信息和证件照片。
现有系统状态:
@d8d/rencai-personal-info-ui包基础框架已就绪src/api/index.ts)pages/personal-info/index在TabBar中)原型设计参考:
docs/小程序原型/rencai.html 提供了个人信息页面的完整原型设计技术集成模式:
yongren-personal-info-ui的实现模式(如果存在)@d8d/rencai-personal-info-ui包,集成个人信息查询API、银行卡信息查询API、证件照片查询API依赖API (史诗015):
依赖故事完成状态:
@d8d/rencai-personal-info-ui中实现PersonalInfoPage页面组件 (src/pages/PersonalInfoPage/PersonalInfoPage.tsx)talentPersonalInfoClient.personal.info.$get)src/components/BankCardInfo.tsx)src/components/BankCardItem.tsx)talentPersonalInfoClient.personal.bankCards.$get)src/components/DocumentPhotos.tsx)src/components/DocumentPhotoItem.tsx)talentPersonalInfoClient.personal.documents.$get)import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'leftIcon="" leftText="" onClickLeft={() => {}}placeholder属性占位,移除手动空白占位fixed=true)mini-talent/src/pages/personal-info/index.tsx:
@d8d/rencai-personal-info-ui/pages/PersonalInfoPage/PersonalInfoPage导入PersonalInfoPage组件tests/pages/PersonalInfoPage/PersonalInfoPage.test.tsx)
pnpm typecheck确保类型检查通过故事017.001完成状态:
src/api/index.ts)jest.config.cjs)故事017.002完成状态:
故事017.012完成状态:
关键实现经验:
@d8d/server@d8d/mini-shared-ui-components中的实现来源: architecture/tech-stack.md
运行时和框架:
测试框架:
重要: mini项目使用Jest测试框架,与web应用使用的Vitest不同。
来源:
关键规范要求:
重要: UI包内部导入必须使用相对路径,不要使用别名。
正确示例:
// ✅ 正确: 使用相对路径导入同一包内的模块
import { talentPersonalInfoClient } from '../../api'
import { BankCardItem } from '../components/BankCardItem'
错误示例:
// ❌ 错误: 不要使用别名导入UI包内部的模块
import { talentPersonalInfoClient } from '@/api'
import { BankCardItem } from '@/components/BankCardItem'
{
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js",
"require": "./dist/src/index.js"
},
"./api": {
"types": "./src/api/index.ts",
"import": "./src/api/index.ts",
"require": "./src/api/index.ts"
},
"./pages/PersonalInfoPage/PersonalInfoPage": {
"types": "./dist/src/pages/PersonalInfoPage/PersonalInfoPage.d.ts",
"import": "./dist/src/pages/PersonalInfoPage/PersonalInfoPage.js",
"require": "./dist/src/pages/PersonalInfoPage/PersonalInfoPage.js"
}
}
}
使用简单的RPC客户端导出模式 (参照yongren-statistics-ui/src/api/enterpriseStatisticsClient.ts):
// src/api/talentPersonalInfoClient.ts
import type { rencaiPersonalInfoRoutes } from '@d8d/core-module/allin-disability-module'
import { rpcClient } from '@d8d/mini-shared-ui-components/utils/rpc/rpc-client'
// 人才小程序API路径前缀: /api/v1/rencai
export const talentPersonalInfoClient = rpcClient<typeof rencaiPersonalInfoRoutes>('/api/v1/rencai')
src/api/index.ts导出:
export { talentPersonalInfoClient } from './talentPersonalInfoClient'
必须使用RPC推断类型,避免直接导入schema类型:
// ✅ 正确:使用RPC推断类型 + 相对路径导入
import type { InferResponseType } from 'hono'
import { talentPersonalInfoClient } from '../api/talentPersonalInfoClient'
export type PersonalInfoResponse = InferResponseType<typeof talentPersonalInfoClient.personal.info.$get, 200>
export type BankCardsResponse = InferResponseType<typeof talentPersonalInfoClient.personal.bankCards.$get, 200>
export type DocumentsResponse = InferResponseType<typeof talentPersonalInfoClient.personal.documents.$get, 200>
// ❌ 错误:直接导入schema类型 (可能导致Date/string不匹配)
import type { PersonalInfoSchema } from '@d8d/allin-disability-module/schemas'
每个UI包必须创建jest.config.cjs配置文件,参照yongren-dashboard-ui/jest.config.cjs:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@d8d/mini-testing-utils/setup'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^~/(.*)$': '<rootDir>/tests/$1',
'^@tarojs/taro$': '@d8d/mini-testing-utils/testing/taro-api-mock.ts',
'\\.(css|less|scss|sass)$': '@d8d/mini-testing-utils/testing/style-mock.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'@d8d/mini-testing-utils/testing/file-mock.js'
},
testMatch: [
'<rootDir>/tests/**/*.spec.{ts,tsx}',
'<rootDir>/tests/**/*.test.{ts,tsx}'
],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.{ts,tsx}',
'!src/**/*.stories.{ts,tsx}'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
testPathIgnorePatterns: [
'/node_modules/',
'/dist/',
'/coverage/'
],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest'
},
transformIgnorePatterns: [
'/node_modules/(?!(swiper|@tarojs)/)'
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json']
}
关键配置说明:
setupFilesAfterEnv: 使用@d8d/mini-testing-utils/setup进行测试环境初始化moduleNameMapper:
^@/(.*)$ 和 ^~/(.*)$: 仅用于测试文件的路径映射,不在源代码中使用^@tarojs/taro$: 映射Taro API到mock@/或~/别名,只使用相对路径testMatch: 支持.spec.{ts,tsx}和.test.{ts,tsx}两种测试文件格式来源: docs/prd/epic-015-talent-mini-program-api-support.md
API路径约定:
所有人才小程序API使用 api/v1/rencai 前缀:
GET /api/v1/rencai/personal/infoGET /api/v1/rencai/personal/bank-cardsGET /api/v1/rencai/personal/documents已完成的API (可直接集成):
@d8d/allin-disability-module/src/routes/talent-personal-info.route.tstalentPersonalInfoRoutes.personal.info{ name: string, gender: string, age: number, idCard: string, disabilityCard: string, disabilityType: string, phone: string, address: string, ... }@d8d/allin-disability-module/src/routes/talent-bank-cards.route.tstalentPersonalInfoRoutes.personal.bankCards{ bankCards: [{ bankName: string, cardNumber: string, cardType: string, isDefault: boolean, ... }] }@d8d/allin-disability-module/src/routes/talent-documents.route.tstalentPersonalInfoRoutes.personal.documents{ documents: [{ type: string, url: string, uploadDate: Date, ... }] }只读设计原则: 遵循与用人方小程序相同的设计原则,人才小程序API以查询功能为主。
个人信息页 (原型行483-628):
<!-- 个人基本信息卡片 -->
<div style="background: white; border-radius: 12px;">
<h3>个人基本信息</h3>
<div>姓名: 张三</div>
<div>性别: 男</div>
<div>年龄: 35</div>
<div>身份证号: 3301**********1234</div>
<div>残疾证号: 3301**********5678</div>
<div>残疾类型: 肢体残疾</div>
<div>联系电话: 138****5678</div>
<div>联系地址: 浙江省杭州市...</div>
</div>
<!-- 银行卡信息卡片 -->
<div style="background: white; border-radius: 12px;">
<h3>银行卡信息</h3>
<div style="border-bottom: 1px solid #eee;">
<div>中国建设银行</div>
<div>**** **** **** 1234</div>
<div>储蓄卡</div>
<div>默认</div>
</div>
<div>
<div>中国工商银行</div>
<div>**** **** **** 5678</div>
<div>储蓄卡</div>
</div>
</div>
<!-- 证件照片卡片 -->
<div style="background: white; border-radius: 12px;">
<h3>证件照片</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr;">
<div>
<img src="id-card-front.jpg" />
<div>身份证(正面)</div>
</div>
<div>
<img src="id-card-back.jpg" />
<div>身份证(反面)</div>
</div>
<div>
<img src="disability-card.jpg" />
<div>残疾证</div>
</div>
<div>
<img src="medical-report.jpg" />
<div>体检报告</div>
</div>
</div>
</div>
移动端设计规范:
来源: docs/stories/017.012.story.md
TabBar页面规范(个人信息页属于此类):
leftIcon=""和leftText=""隐藏返回按钮@d8d/mini-shared-ui-components/components/navbarNavbar集成示例:
import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'
import { View, ScrollView } from '@tarojs/components'
export function PersonalInfoPage() {
return (
<View className="h-screen bg-gray-100">
{/* Navbar导航栏 - TabBar页面无返回按钮 */}
<Navbar
title="个人信息"
leftIcon=""
leftText=""
onClickLeft={() => {}}
placeholder
fixed
/>
{/* 页面内容 */}
<ScrollView scrollY className="h-full">
{/* 页面内容 */}
</ScrollView>
</View>
)
}
关键配置:
leftIcon="": 隐藏返回按钮图标leftText="": 隐藏返回按钮文字onClickLeft={() => {}}: 空函数(TabBar页面不需要返回功能)placeholder: 添加占位空间,避免内容被Navbar遮挡fixed: 固定在顶部来源: architecture/source-tree.md
mini-talent项目结构:
mini-talent/ # 人才小程序项目
├── src/
│ ├── app.tsx # 小程序入口
│ ├── app.config.ts # 小程序配置 (已在故事017.001中更新)
│ ├── app.css # 全局样式
│ ├── pages/ # 页面目录
│ │ ├── login/ # 登录页 (从UI包导入)
│ │ │ └── index.tsx
│ │ ├── index/ # 首页/个人主页 (从UI包导入)
│ │ │ └── index.tsx
│ │ ├── personal-info/ # 个人信息页 (从UI包导入) - 本故事
│ │ │ └── index.tsx
│ │ ├── attendance/ # 考勤记录页 (从UI包导入) - 故事017.004
│ │ │ └── index.tsx
│ │ └── settings/ # 设置页 (从UI包导入) - 故事017.006
│ │ └── index.tsx
├── package.json
├── jest.config.js # Jest配置
└── tsconfig.json
mini-ui-packages目录结构:
mini-ui-packages/
├── rencai-personal-info-ui/ # 人才个人信息UI包
│ ├── src/
│ │ ├── api/
│ │ │ ├── talentPersonalInfoClient.ts
│ │ │ └── index.ts
│ │ ├── pages/
│ │ │ └── PersonalInfoPage/
│ │ │ ├── PersonalInfoPage.tsx
│ │ │ └── index.ts (可选)
│ │ ├── components/ # UI组件
│ │ │ ├── PersonalBasicInfo.tsx # 个人基本信息卡片
│ │ │ ├── BankCardInfo.tsx # 银行卡信息卡片
│ │ │ ├── BankCardItem.tsx # 银行卡列表项
│ │ │ ├── DocumentPhotos.tsx # 证件照片卡片
│ │ │ └── DocumentPhotoItem.tsx # 证件照片列表项
│ │ └── index.ts
│ ├── package.json # 包含exports配置
│ ├── jest.config.cjs
│ └── tsconfig.json
└── mini-shared-ui-components/ # 通用小程序UI组件
├── src/
│ └── components/
│ ├── status-bar.tsx
│ ├── page-container.tsx
│ ├── navbar.tsx
│ └── tab-bar.tsx
└── ...
mini-talent页面导入方式:
// mini-talent/src/pages/personal-info/index.tsx
import PersonalInfoPage from '@d8d/rencai-personal-info-ui/pages/PersonalInfoPage/PersonalInfoPage'
import { AuthContextProvider, useAuth } from '@d8d/rencai-auth-ui/utils'
function PersonalInfo() {
const { isLoggedIn } = useAuth()
// 未登录跳转到登录页
if (!isLoggedIn) {
Taro.navigateTo({ url: '/pages/login/index' })
return null
}
return <PersonalInfoPage />
}
export default function PersonalInfoIndex() {
return (
<AuthContextProvider>
<PersonalInfo />
</AuthContextProvider>
)
}
来源: architecture/testing-strategy.md
测试框架:
测试文件位置:
mini-ui-packages/<package-name>/
└── tests/
├── unit/ # 单元测试
│ └── components/
│ ├── PersonalBasicInfo.test.tsx
│ ├── BankCardItem.test.tsx
│ └── DocumentPhotoItem.test.tsx
└── pages/ # 页面组件测试
└── PersonalInfoPage/
└── PersonalInfoPage.test.tsx
测试要求:
pnpm typecheck确保类型检查通过Mock响应工具函数:
const createMockResponse = (status: number, data?: any) => ({
status,
ok: status >= 200 && status < 300,
body: null,
bodyUsed: false,
statusText: status === 200 ? 'OK' : 'Error',
headers: new Headers(),
url: '',
redirected: false,
type: 'basic' as ResponseType,
json: async () => data || {},
text: async () => '',
blob: async () => new Blob(),
arrayBuffer: async () => new ArrayBuffer(0),
formData: async () => new FormData(),
clone: function() { return this; }
})
来源: architecture/coding-standards.md
关键编码规范:
开发UI包时,必须参考并遵循UI包开发规范,该规范基于史诗008(AllIn UI模块移植)的经验总结。
data-testid属性getByText()查找可能重复的文本元素@/, ~/等),必须使用相对路径路径使用示例:
// ✅ 正确: UI包内部使用相对路径
import { apiClient } from '../../api'
import { MyComponent } from '../components'
// ✅ 正确: 跨包导入使用workspace包名
import { SharedComponent } from '@d8d/mini-shared-ui-components'
// ❌ 错误: UI包内部使用别名
import { apiClient } from '@/api'
import { MyComponent } from '@/pages/PersonalInfoPage/components'
mini-ui-packages/yongren-personal-info-ui (如果存在)mini-ui-packages/yongren-dashboard-ui
src/pages/Dashboard/Dashboard.tsxjest.config.cjs银行卡号脱敏:
/**
* 脱敏银行卡号
* @param cardNumber 完整银行卡号
* @returns 脱敏后的银行卡号(如:**** **** **** 1234)
*/
export function maskCardNumber(cardNumber: string): string {
if (!cardNumber || cardNumber.length < 4) {
return '****'
}
const last4 = cardNumber.slice(-4)
return `**** **** **** ${last4}`
}
// 使用示例
<BankCardItem>
<Text>{maskCardNumber(bankCard.cardNumber)}</Text>
</BankCardItem>
身份证号脱敏:
/**
* 脱敏身份证号
* @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}`
}
// 使用示例
<PersonalBasicInfo>
<Text>身份证号: {maskIdCard(personalInfo.idCard)}</Text>
</PersonalBasicInfo>
使用Taro.previewImage:
import Taro from '@tarojs/taro'
import { 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>
)
}
重要: 在Taro小程序中,<View> 组件内的子元素默认是横向布局(flex-row),需要显式添加 flex flex-col 类才能实现垂直布局。
正确示例:
// ✅ 正确: 使用 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>
个人信息卡片示例:
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>
{/* 垂直布局的信息列表 */}
<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主要风险:
缓解措施:
来源: architecture/testing-strategy.md
测试框架:
测试文件位置:
mini-ui-packages/<package-name>/
└── tests/
├── unit/ # 单元测试
│ └── components/
│ ├── PersonalBasicInfo.test.tsx
│ ├── BankCardItem.test.tsx
│ └── DocumentPhotoItem.test.tsx
└── pages/ # 页面组件测试
└── PersonalInfoPage/
└── PersonalInfoPage.test.tsx
组件测试:
脱敏逻辑测试:
集成测试:
回归测试:
pnpm typecheck确保类型检查通过# 运行所有测试
cd mini-ui-packages/rencai-personal-info-ui && pnpm test
# 运行特定测试
pnpm test --testNamePattern="PersonalInfoPage"
# 生成覆盖率报告
pnpm test:coverage
| 日期 | 版本 | 描述 | 作者 |
|---|---|---|---|
| 2025-12-26 | 1.0 | 创建故事文档 | Bob (Scrum Master) |
此部分由开发代理在实施过程中填写
Claude Sonnet (claude-sonnet-4-20250514)
无重大调试问题,开发过程顺利。
任务1: 创建个人信息页面组件 ✅
PersonalBasicInfo.tsx 组件,展示个人基本信息(姓名、性别、身份证号、残疾证号、残疾类型、残疾等级、联系电话、地址等)maskUtils.ts 工具函数,包含 maskIdCard 和 maskBankCard 脱敏函数talentPersonalInfoClient.personal.info.$get任务2: 实现银行卡信息模块 ✅
BankCardInfo.tsx 和 BankCardItem.tsx 组件talentPersonalInfoClient.personal['bank-cards'].$get**** **** **** 4567)任务3: 实现证件照片模块 ✅
DocumentPhotos.tsx 和 DocumentPhotoItem.tsx 组件talentPersonalInfoClient.personal.photos.$getTaro.previewImage)任务4: 集成Navbar导航栏组件 ✅
@d8d/mini-shared-ui-components/components/navbar 导入 Navbar 组件leftIcon="" leftText="",无返回按钮)placeholder 和 fixed 属性任务5: 更新mini-talent页面集成 ✅
mini-talent/src/pages/personal-info/index.tsx@d8d/rencai-personal-info-ui/pages/PersonalInfoPage/PersonalInfoPage 导入组件useRequireAuth hook)任务6: 实现页面样式和移动端适配 ✅
flex flex-col 实现垂直布局任务7: 编写测试 ✅
PersonalBasicInfo.test.tsx、BankCardItem.test.tsx、DocumentPhotoItem.test.tsxPersonalInfoPage.test.tsx@d8d/mini-testing-utils 提供的 Taro mock,不重写自定义 mock新增文件:
mini-ui-packages/rencai-personal-info-ui/src/components/PersonalBasicInfo.tsx - 个人基本信息卡片组件mini-ui-packages/rencai-personal-info-ui/src/components/BankCardInfo.tsx - 银行卡信息卡片容器mini-ui-packages/rencai-personal-info-ui/src/components/BankCardItem.tsx - 银行卡列表项组件mini-ui-packages/rencai-personal-info-ui/src/components/DocumentPhotos.tsx - 证件照片卡片容器mini-ui-packages/rencai-personal-info-ui/src/components/DocumentPhotoItem.tsx - 证件照片列表项组件mini-ui-packages/rencai-personal-info-ui/src/utils/maskUtils.ts - 数据脱敏工具函数mini-ui-packages/rencai-personal-info-ui/src/pages/PersonalInfoPage/PersonalInfoPage.tsx - 个人信息页面主组件mini-ui-packages/rencai-personal-info-ui/tests/unit/components/PersonalBasicInfo.test.tsx - 个人基本信息组件测试mini-ui-packages/rencai-personal-info-ui/tests/unit/components/BankCardItem.test.tsx - 银行卡组件测试mini-ui-packages/rencai-personal-info-ui/tests/unit/components/DocumentPhotoItem.test.tsx - 证件照片组件测试mini-ui-packages/rencai-personal-info-ui/tests/pages/PersonalInfoPage/PersonalInfoPage.test.tsx - 个人信息页面测试修改文件:
mini-ui-packages/rencai-personal-info-ui/package.json - 添加了 @d8d/rencai-auth-ui 依赖mini-ui-packages/rencai-auth-ui/src/hooks/index.ts - 导出 useRequireAuth hookmini-ui-packages/rencai-auth-ui/src/hooks/useAuth.tsx - 添加 useRequireAuth hook 实现mini-ui-packages/rencai-personal-info-ui/jest.config.cjs - 更新为使用 @d8d/mini-testing-utils/testing/setupmini-ui-packages/mini-testing-utils/testing/taro-api-mock.ts - 添加 previewImage 和 setNavigationBarTitle mockmini-ui-packages/mini-testing-utils/package.json - 添加 ./testing/setup 导出配置此部分由QA代理在审查完成后填写