017.003.story.md 35 KB

故事017.003: 个人信息功能实现

元信息

  • 史诗: 017 - 人才小程序功能实现
  • 优先级: P1 - 核心功能
  • 状态: Ready for Review
  • 创建日期: 2025-12-26
  • 负责人: 开发团队

故事描述

作为 人才小程序开发者, 我想要 实现个人信息页面功能, 以便 人才用户能够查看个人基本信息、银行卡信息和证件照片。

背景

现有系统状态:

  • 故事017.001已完成rencai mini ui包基础框架搭建
  • 故事017.002已完成登录与首页实现
  • rencai系列7个UI包基础目录结构已创建
  • @d8d/rencai-personal-info-ui包基础框架已就绪
  • API客户端文件已创建(src/api/index.ts
  • mini-talent项目路由结构已配置完成(pages/personal-info/index在TabBar中)

原型设计参考:

  • docs/小程序原型/rencai.html 提供了个人信息页面的完整原型设计
  • 个人信息页 (原型行483-628): 个人基本信息、银行卡信息、证件照片

技术集成模式:

  • 参照yongren-personal-info-ui的实现模式(如果存在)
  • 个人信息页使用@d8d/rencai-personal-info-ui包,集成个人信息查询API、银行卡信息查询API、证件照片查询API
  • API调用逻辑封装在页面组件内部
  • TabBar页面使用Navbar无返回按钮(参照故事017.012规范)

依赖API (史诗015):

  • ✅ 个人信息查询API (GET /api/v1/rencai/personal/info) - 已就绪
  • ✅ 银行卡信息查询API (GET /api/v1/rencai/personal/bank-cards) - 已就绪
  • ✅ 证件照片查询API (GET /api/v1/rencai/personal/documents) - 已就绪

依赖故事完成状态:

  • ✅ 故事017.001: rencai mini ui包基础框架搭建完成
  • ✅ 故事017.002: 登录与首页实现完成
  • ✅ 故事017.012: 统一Navbar导航栏组件规范完成

验收标准

个人基本信息展示

  • 个人信息页面展示完整的个人基本信息(姓名、性别、年龄、身份证号、残疾证号、残疾类型、联系电话、联系地址)

银行卡信息展示

  • 银行卡信息查询接口返回银行卡信息列表
  • 银行卡信息展示包含:银行名称、卡号脱敏、卡类型、是否默认
  • 支持多张银行卡的列表展示

证件照片展示与预览

  • 证件照片查询接口返回证件照片信息列表
  • 证件照片类型包含:身份证、残疾证、体检报告、征信报告
  • 支持证件照片预览功能(点击照片放大查看)

页面设计与布局

  • 页面设计符合原型标准,移动端体验良好
  • 已集成Navbar导航栏组件(TabBar页面,无返回按钮)
  • TabBarLayout底部导航正常工作

数据安全与权限

  • 所有查询接口验证用户权限,确保用户只能查询自己的数据
  • 银行卡号进行脱敏处理(如:**** **** **** 1234)

集成与兼容性

  • 与前置故事(017.001、017.002)无缝集成
  • 现有mini-talent项目功能不受影响

任务列表

任务1: 创建个人信息页面组件 (AC: 个人基本信息展示)

  • 1.1 在@d8d/rencai-personal-info-ui中实现PersonalInfoPage页面组件 (src/pages/PersonalInfoPage/PersonalInfoPage.tsx)
  • 1.2 创建个人基本信息卡片组件:
    • 显示姓名
    • 显示性别
    • 显示年龄
    • 显示身份证号(脱敏显示)
    • 显示残疾证号(脱敏显示)
    • 显示残疾类型
    • 显示联系电话
    • 显示联系地址
  • 1.3 集成个人信息查询API (talentPersonalInfoClient.personal.info.$get)
  • 1.4 实现数据加载状态(Loading状态)
  • 1.5 实现错误处理(API调用失败时显示错误提示)

任务2: 实现银行卡信息模块 (AC: 银行卡信息展示)

  • 2.1 创建银行卡信息卡片组件 (src/components/BankCardInfo.tsx)
  • 2.2 创建银行卡列表项组件 (src/components/BankCardItem.tsx)
  • 2.3 集成银行卡信息查询API (talentPersonalInfoClient.personal.bankCards.$get)
  • 2.4 实现银行卡号脱敏显示(如:**** **** **** 1234)
  • 2.5 显示银行卡信息:银行名称、卡号脱敏、卡类型(储蓄卡/信用卡)、是否默认标识
  • 2.6 支持多张银行卡的列表展示

任务3: 实现证件照片模块 (AC: 证件照片展示与预览)

  • 3.1 创建证件照片卡片组件 (src/components/DocumentPhotos.tsx)
  • 3.2 创建证件照片列表项组件 (src/components/DocumentPhotoItem.tsx)
  • 3.3 集成证件照片查询API (talentPersonalInfoClient.personal.documents.$get)
  • 3.4 支持证件照片类型展示:身份证、残疾证、体检报告、征信报告
  • 3.5 实现证件照片预览功能(点击照片放大查看)
  • 3.6 使用Taro的Image组件展示照片
  • 3.7 实现照片预览Modal(使用Taro.previewImage或自定义Modal)

任务4: 集成Navbar导航栏组件 (AC: 页面设计与布局)

  • 4.1 导入Navbar组件: import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'
  • 4.2 在页面顶部添加Navbar,配置为TabBar页面(无返回按钮)
  • 4.3 Navbar配置: leftIcon="" leftText="" onClickLeft={() => {}}
  • 4.4 使用Navbar的placeholder属性占位,移除手动空白占位
  • 4.5 确保Navbar固定在顶部 (fixed=true)

任务5: 更新mini-talent页面集成 (AC: 集成与兼容性)

  • 5.1 更新mini-talent/src/pages/personal-info/index.tsx
    • @d8d/rencai-personal-info-ui/pages/PersonalInfoPage/PersonalInfoPage导入PersonalInfoPage组件
    • 用AuthProvider包装页面
    • 添加认证检查(未登录跳转到登录页)
    • 导出PersonalInfoPage组件
  • 5.2 验证页面路由配置(已在故事017.001中配置完成)
  • 5.3 验证底部TabBar导航正常工作(TabBar中"我的"对应personal-info页面)

任务6: 实现页面样式和移动端适配 (AC: 页面设计与布局)

  • 6.1 参照原型设计实现个人信息页样式(原型行483-628)
    • 个人基本信息卡片样式(白色卡片,圆角)
    • 银行卡信息列表样式(白色卡片,列表项分隔)
    • 证件照片列表样式(白色卡片,网格布局)
  • 6.2 确保页面设计符合移动端规范:
    • 宽度参考: 375px
    • 圆角规范: 12px (卡片)
    • 颜色主题: 蓝色渐变 (#3b82f6 → #1e40af)
    • 字体规范: 标题18-24px, 正文14px, 小字12px
  • 6.3 使用正确的组件:个人信息页使用TabBarLayout(无返回按钮的Navbar)

任务7: 编写测试 (AC: 集成与兼容性)

  • 7.1 为PersonalInfoPage编写组件测试 (tests/pages/PersonalInfoPage/PersonalInfoPage.test.tsx)
    • 测试组件渲染
    • 测试个人基本信息展示(Mock API响应)
    • 测试银行卡信息展示(Mock API响应)
    • 测试证件照片展示(Mock API响应)
    • 测试照片预览功能
  • 7.2 编写集成测试验证现有功能不受影响
  • 7.3 运行pnpm typecheck确保类型检查通过

开发者笔记

前置故事见解

故事017.001完成状态:

  • ✅ rencai系列7个UI包基础结构已创建
  • ✅ API客户端文件已创建(src/api/index.ts
  • ✅ mini-talent项目路由结构已配置完成
  • ✅ 基础布局组件(TabBarLayout、Navbar等)已就绪
  • ✅ package.json的exports字段已配置
  • ✅ Jest测试框架已配置(jest.config.cjs

故事017.002完成状态:

  • ✅ 登录页面功能完整,支持人才用户身份证号/残疾证号密码登录
  • ✅ 首页/个人主页页面展示个人概览数据
  • ✅ 认证状态管理正常,token存储和验证可靠
  • ✅ AuthContext提供登录状态、用户信息和登出方法
  • ✅ 页面设计符合原型标准,移动端体验良好

故事017.012完成状态:

  • ✅ 统一Navbar导航栏组件规范
  • ✅ TabBar页面使用Navbar无返回按钮(leftIcon="" leftText="")
  • ✅ 非TabBar页面使用Navbar带返回按钮
  • ✅ Navbar样式与用人方小程序保持一致

关键实现经验:

  1. API客户端导入路径修正:从相应的后端模块包导入,而不是@d8d/server
  2. 页面文件简化设计:采用"薄包装层",仅导入并导出组件
  3. 复用现有共享组件:StatusBar、PageContainer、Navbar使用@d8d/mini-shared-ui-components中的实现
  4. 测试框架选择:mini项目使用Jest(不是Vitest)

技术栈要求

来源: architecture/tech-stack.md

运行时和框架:

  • Node.js: 20.18.3
  • Hono: 4.8.5 (RPC客户端)
  • React: 19.1.0 (UI组件)
  • Taro: 4.1.4 (小程序框架)
  • Tailwind CSS: 4.1.11 (样式)

测试框架:

  • Jest: 30.2.0 (mini项目使用Jest,不是Vitest!)
  • ts-jest: 29.4.5 (TypeScript预处理器)
  • @testing-library/react: 16.3.0 (React组件测试)
  • @d8d/mini-testing-utils: workspace包 (Taro小程序测试工具)

重要: mini项目使用Jest测试框架,与web应用使用的Vitest不同。

UI包开发规范

来源:

关键规范要求:

1. UI包内部导入规范

重要: UI包内部导入必须使用相对路径,不要使用别名。

正确示例:

// ✅ 正确: 使用相对路径导入同一包内的模块
import { talentPersonalInfoClient } from '../../api'
import { BankCardItem } from '../components/BankCardItem'

错误示例:

// ❌ 错误: 不要使用别名导入UI包内部的模块
import { talentPersonalInfoClient } from '@/api'
import { BankCardItem } from '@/components/BankCardItem'

2. package.json exports配置规范

{
  "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"
    }
  }
}

3. RPC客户端实现规范

使用简单的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'

4. 类型定义规范

必须使用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'

5. Jest配置规范

每个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
    • 样式和文件映射: 将样式文件和静态文件映射到mock
  • 重要: 源代码中不使用@/~/别名,只使用相对路径
  • testMatch: 支持.spec.{ts,tsx}.test.{ts,tsx}两种测试文件格式

史诗015 API集成要点

来源: docs/prd/epic-015-talent-mini-program-api-support.md

API路径约定: 所有人才小程序API使用 api/v1/rencai 前缀:

  • 人才个人信息: GET /api/v1/rencai/personal/info
  • 人才银行卡信息: GET /api/v1/rencai/personal/bank-cards
  • 人才证件照片: GET /api/v1/rencai/personal/documents

已完成的API (可直接集成):

  • ✅ 个人信息查询API (故事015-03)
    • 路由定义: @d8d/allin-disability-module/src/routes/talent-personal-info.route.ts
    • RPC路由: talentPersonalInfoRoutes.personal.info
    • 响应格式: { name: string, gender: string, age: number, idCard: string, disabilityCard: string, disabilityType: string, phone: string, address: string, ... }
  • ✅ 银行卡信息查询API (故事015-04)
    • 路由定义: @d8d/allin-disability-module/src/routes/talent-bank-cards.route.ts
    • RPC路由: talentPersonalInfoRoutes.personal.bankCards
    • 响应格式: { bankCards: [{ bankName: string, cardNumber: string, cardType: string, isDefault: boolean, ... }] }
  • ✅ 证件照片查询API (故事015-05)
    • 路由定义: @d8d/allin-disability-module/src/routes/talent-documents.route.ts
    • RPC路由: talentPersonalInfoRoutes.personal.documents
    • 响应格式: { documents: [{ type: string, url: string, uploadDate: Date, ... }] }

只读设计原则: 遵循与用人方小程序相同的设计原则,人才小程序API以查询功能为主。

原型设计参考

来源: docs/小程序原型/rencai.html

个人信息页 (原型行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>

移动端设计规范:

  • 宽度参考: 375px
  • 状态栏高度: 44px
  • 底部导航高度: 60px
  • 圆角规范: 12px (卡片)、40px (移动框架)
  • 颜色主题: 蓝色渐变 (#3b82f6 → #1e40af)
  • 字体规范: 标题18-24px, 正文14px, 小字12px

Navbar导航栏集成规范

来源: docs/stories/017.012.story.md

TabBar页面规范(个人信息页属于此类):

  • 使用leftIcon=""leftText=""隐藏返回按钮
  • 参照yongren-dashboard-ui:139-148
  • Navbar组件来源: @d8d/mini-shared-ui-components/components/navbar

Navbar集成示例:

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

测试框架:

  • Jest: 30.2.0 (mini项目使用Jest,不是Vitest!)
  • ts-jest: 29.4.5 (TypeScript预处理器)
  • @testing-library/react: 16.3.0 (React组件测试)
  • @d8d/mini-testing-utils: workspace包 (Taro小程序测试工具)

测试文件位置:

mini-ui-packages/<package-name>/
└── tests/
    ├── unit/                      # 单元测试
    │   └── components/
    │       ├── PersonalBasicInfo.test.tsx
    │       ├── BankCardItem.test.tsx
    │       └── DocumentPhotoItem.test.tsx
    └── pages/                     # 页面组件测试
        └── PersonalInfoPage/
            └── PersonalInfoPage.test.tsx

测试要求:

  1. 为每个页面组件编写Jest测试
  2. 测试API客户端的集成(使用Mock响应)
  3. 测试银行卡号脱敏逻辑
  4. 测试证件照片预览功能
  5. 验证mini-talent项目现有功能不受影响
  6. 运行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

关键编码规范:

1. 必须遵循UI包开发规范

开发UI包时,必须参考并遵循UI包开发规范,该规范基于史诗008(AllIn UI模块移植)的经验总结。

2. 关键检查点 (基于史诗008经验)

  • API路径映射验证: 开发前必须验证故事中的API路径映射与实际后端路由定义的一致性
  • 类型推断最佳实践: 必须使用RPC推断类型,而不是直接导入schema类型
  • 测试选择器优化: 必须为关键交互元素添加data-testid属性
  • API调用一致性: 必须根据实际路由名称修正API调用

3. 常见错误避免

  • ❌ 不要直接导入schema类型 (可能导致Date/string类型不匹配)
  • ❌ 不要使用getByText()查找可能重复的文本元素
  • ❌ 不要在UI包内部导入中使用别名 (@/, ~/等),必须使用相对路径
  • ❌ 不要忘记在TabBar页面集成Navbar导航栏组件
  • ❌ 不要在TabBar页面添加返回按钮

路径使用示例:

// ✅ 正确: 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'

4. 参考实现

  • 用人方个人信息UI包: mini-ui-packages/yongren-personal-info-ui (如果存在)
  • 用人方仪表板UI包: mini-ui-packages/yongren-dashboard-ui
    • 组件: src/pages/Dashboard/Dashboard.tsx
    • package.json exports配置
    • 目录结构参考
    • Jest配置: jest.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小程序布局规范

重要: 在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>
  )
}

关键点:

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

技术约束

  1. 向后兼容: 不影响现有mini-talent项目功能
  2. 类型安全: 使用TypeScript严格模式,所有API调用必须有类型定义
  3. 模块独立性: 每个UI包独立管理自己的API客户端和类型定义
  4. 测试覆盖: 所有新增代码必须有测试覆盖
  5. 代码规范: 遵循项目编码标准和UI包开发规范
  6. 数据安全: 银行卡号、身份证号等敏感信息必须脱敏显示

风险和缓解措施

主要风险:

  1. API集成风险: 史诗015的API可能存在接口变更
  2. 数据脱敏风险: 银行卡号、身份证号脱敏逻辑可能存在漏洞
  3. 照片预览风险: Taro.previewImage在不同平台可能有兼容性问题
  4. UI组件复用风险: rencai系列UI包可能与现有yongren系列UI包存在差异

缓解措施:

  1. API验证: 开发前验证API路径映射与实际后端路由定义的一致性
  2. 分阶段实现: 先实现个人基本信息,再实现银行卡信息,最后实现证件照片
  3. 参考现有模式: 参照yongren系列UI包的实现模式和架构
  4. 类型安全: 使用RPC推断类型,避免类型不匹配问题
  5. 测试驱动: 编写完整的测试,确保功能正确性
  6. 数据安全测试: 测试各种边界情况,确保脱敏逻辑正确

测试

测试框架和模式

来源: architecture/testing-strategy.md

测试框架:

  • Jest: 30.2.0 (mini项目使用Jest,不是Vitest!)
  • ts-jest: 29.4.5 (TypeScript预处理器)
  • @testing-library/react: 16.3.0 (React组件测试)
  • @d8d/mini-testing-utils: workspace包 (Taro小程序测试工具)

测试文件位置:

mini-ui-packages/<package-name>/
└── tests/
    ├── unit/                      # 单元测试
    │   └── components/
    │       ├── PersonalBasicInfo.test.tsx
    │       ├── BankCardItem.test.tsx
    │       └── DocumentPhotoItem.test.tsx
    └── pages/                     # 页面组件测试
        └── PersonalInfoPage/
            └── PersonalInfoPage.test.tsx

测试要求

  1. 组件测试:

    • 测试组件渲染正确
    • 测试用户交互(点击照片预览等)
    • 测试API集成(使用Mock响应)
    • 测试错误处理
  2. 脱敏逻辑测试:

    • 测试银行卡号脱敏功能
    • 测试身份证号脱敏功能
    • 测试各种边界情况(空值、短字符串等)
  3. 集成测试:

    • 测试个人信息页面的三个模块数据加载
    • 测试Navbar导航栏的正确显示
  4. 回归测试:

    • 验证mini-talent项目现有功能不受影响
    • 运行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 工具函数,包含 maskIdCardmaskBankCard 脱敏函数
  • 集成了个人信息查询API talentPersonalInfoClient.personal.info.$get
  • 实现了数据加载状态和错误处理

任务2: 实现银行卡信息模块

  • 创建了 BankCardInfo.tsxBankCardItem.tsx 组件
  • 集成了银行卡信息查询API talentPersonalInfoClient.personal['bank-cards'].$get
  • 实现了银行卡号脱敏显示(**** **** **** 4567
  • 支持多张银行卡的列表展示,显示银行名称、卡类型、是否默认标识

任务3: 实现证件照片模块

  • 创建了 DocumentPhotos.tsxDocumentPhotoItem.tsx 组件
  • 集成了证件照片查询API talentPersonalInfoClient.personal.photos.$get
  • 实现了证件照片预览功能(使用 Taro.previewImage
  • 支持多种证件照片类型展示,无照片时显示占位图标

任务4: 集成Navbar导航栏组件

  • @d8d/mini-shared-ui-components/components/navbar 导入 Navbar 组件
  • 配置为TabBar页面(leftIcon="" leftText="",无返回按钮)
  • 使用 placeholderfixed 属性

任务5: 更新mini-talent页面集成

  • 更新了 mini-talent/src/pages/personal-info/index.tsx
  • @d8d/rencai-personal-info-ui/pages/PersonalInfoPage/PersonalInfoPage 导入组件
  • 使用 AuthProvider 包装页面,添加认证检查(通过 useRequireAuth hook)
  • 路由配置已在故事017.001中完成

任务6: 实现页面样式和移动端适配

  • 使用白色卡片样式,圆角12px
  • 使用 flex flex-col 实现垂直布局
  • 使用 Tailwind CSS 实现响应式设计
  • 卡片间距、字体大小符合移动端规范

任务7: 编写测试

  • 编写了 20 个测试用例,全部通过
  • 组件测试:PersonalBasicInfo.test.tsxBankCardItem.test.tsxDocumentPhotoItem.test.tsx
  • 页面测试:PersonalInfoPage.test.tsx
  • 使用真实的 React Query 验证 RPC 类型推断
  • 使用 @d8d/mini-testing-utils 提供的 Taro mock,不重写自定义 mock
  • 类型检查通过

文件列表

新增文件:

  1. mini-ui-packages/rencai-personal-info-ui/src/components/PersonalBasicInfo.tsx - 个人基本信息卡片组件
  2. mini-ui-packages/rencai-personal-info-ui/src/components/BankCardInfo.tsx - 银行卡信息卡片容器
  3. mini-ui-packages/rencai-personal-info-ui/src/components/BankCardItem.tsx - 银行卡列表项组件
  4. mini-ui-packages/rencai-personal-info-ui/src/components/DocumentPhotos.tsx - 证件照片卡片容器
  5. mini-ui-packages/rencai-personal-info-ui/src/components/DocumentPhotoItem.tsx - 证件照片列表项组件
  6. mini-ui-packages/rencai-personal-info-ui/src/utils/maskUtils.ts - 数据脱敏工具函数
  7. mini-ui-packages/rencai-personal-info-ui/src/pages/PersonalInfoPage/PersonalInfoPage.tsx - 个人信息页面主组件
  8. mini-ui-packages/rencai-personal-info-ui/tests/unit/components/PersonalBasicInfo.test.tsx - 个人基本信息组件测试
  9. mini-ui-packages/rencai-personal-info-ui/tests/unit/components/BankCardItem.test.tsx - 银行卡组件测试
  10. mini-ui-packages/rencai-personal-info-ui/tests/unit/components/DocumentPhotoItem.test.tsx - 证件照片组件测试
  11. mini-ui-packages/rencai-personal-info-ui/tests/pages/PersonalInfoPage/PersonalInfoPage.test.tsx - 个人信息页面测试

修改文件:

  1. mini-ui-packages/rencai-personal-info-ui/package.json - 添加了 @d8d/rencai-auth-ui 依赖
  2. mini-ui-packages/rencai-auth-ui/src/hooks/index.ts - 导出 useRequireAuth hook
  3. mini-ui-packages/rencai-auth-ui/src/hooks/useAuth.tsx - 添加 useRequireAuth hook 实现
  4. mini-ui-packages/rencai-personal-info-ui/jest.config.cjs - 更新为使用 @d8d/mini-testing-utils/testing/setup
  5. mini-ui-packages/mini-testing-utils/testing/taro-api-mock.ts - 添加 previewImagesetNavigationBarTitle mock
  6. mini-ui-packages/mini-testing-utils/package.json - 添加 ./testing/setup 导出配置

QA结果

此部分由QA代理在审查完成后填写