017.004.story.md 44 KB

故事017.004: 考勤记录功能实现

元信息

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

故事描述

作为 人才小程序开发者, 我想要 实现考勤记录页面功能, 以便 人才用户能够查看自己的考勤日历、打卡明细和考勤统计数据。

背景

现有系统状态:

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

原型设计参考:

  • docs/小程序原型/rencai.html 提供了考勤记录页面的完整原型设计
  • 考勤记录页 (原型行303-481): 月份选择、考勤统计、日历视图、打卡明细

技术集成模式:

  • 参照yongren-attendance-ui的实现模式(如果存在)
  • 考勤记录页使用@d8d/rencai-attendance-ui
  • API调用逻辑封装在页面组件内部
  • TabBar页面使用Navbar无返回按钮(参照故事017.012规范)

依赖API (史诗015):

  • ⚠️ 考勤记录API为P2延期功能
  • ✅ 当前使用前端模拟数据实现
  • 📋 前端数据结构符合后续API接口规范,便于后续替换为真实API

依赖故事完成状态:

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

验收标准

考勤日历视图

  • 考勤日历视图功能完整,正确标记打卡状态(前端模拟数据)
  • 日历视图显示当前月份的完整日期网格(7列布局)
  • 已打卡日期使用绿色标记显示
  • 未打卡日期使用灰色显示
  • 周末日期使用不同颜色标识

月份切换功能

  • 支持月份切换,展示对应月份的考勤数据
  • 左右箭头按钮可切换到上个月/下个月
  • 月份选择器显示当前年月(如:2023年11月)
  • 切换月份后更新日历视图和统计数据

考勤统计展示

  • 考勤统计数据展示完整(出勤率、正常出勤天数、迟到次数、早退次数、缺勤次数)
  • 出勤率以百分比形式显示(如:100%)
  • 正常出勤天数以数字形式显示(如:28天)
  • 异常统计(迟到、早退、缺勤)以次数形式显示

打卡明细列表

  • 打卡明细列表功能完整,支持按日期倒序排列
  • 显示每日打卡详情(上班打卡时间、下班打卡时间、打卡状态)
  • 打卡状态使用彩色标签显示(正常-绿色、迟到-黄色、早退-橙色、缺勤-红色)
  • 显示日期和星期信息(如:11月25日 星期六)

页面设计与布局

  • 页面设计符合原型标准,移动端体验良好
  • 已集成Navbar导航栏组件(TabBar页面,无返回按钮)
  • TabBarLayout底部导航正常工作
  • 页面布局包含:月份选择器、考勤统计卡片、日历视图、打卡明细列表

前端模拟数据规范

  • 前端模拟数据结构符合后续API接口规范
  • 考勤统计数据结构包含:attendanceRate、normalDays、lateCount、earlyLeaveCount、absentCount
  • 打卡明细数据结构包含:date、weekday、checkInTime、checkOutTime、status
  • 数据结构便于后续替换为真实API

集成与兼容性

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

任务列表

任务1: 创建考勤记录页面组件 (AC: 考勤日历视图)

  • 1.1 在@d8d/rencai-attendance-ui中实现AttendancePage页面组件 (src/pages/AttendancePage/AttendancePage.tsx)
  • 1.2 创建考勤日历组件 (src/components/AttendanceCalendar.tsx)
    • 实现7列网格布局(周日到周六)
    • 显示当前月份的完整日期
    • 标记已打卡日期(绿色背景)
    • 标记未打卡日期(灰色背景)
    • 标记周末日期(不同颜色)
  • 1.3 创建前端模拟数据 (src/utils/mockAttendanceData.ts)
    • 考勤统计数据
    • 打卡明细数据(按日期倒序)
  • 1.4 实现月份选择器组件 (src/components/MonthSelector.tsx)
    • 显示当前年月
    • 左右箭头切换月份
    • 更新日历视图和统计数据
  • 1.5 实现数据加载状态(Loading状态)
  • 1.6 实现错误处理(数据加载失败时显示错误提示)

任务2: 实现考勤统计模块 (AC: 考勤统计展示)

  • 2.1 创建考勤统计卡片组件 (src/components/AttendanceStats.tsx)
    • 显示出勤率(百分比)
    • 显示正常出勤天数
    • 显示异常统计(迟到、早退、缺勤次数)
  • 2.2 创建统计数据类型定义 (src/types/attendance.ts)
    • AttendanceStats接口
    • 使用RPC推断类型预留(便于后续API集成)
  • 2.3 集成前端模拟统计数据
  • 2.4 实现统计数据的响应式布局

任务3: 实现打卡明细模块 (AC: 打卡明细列表)

  • 3.1 创建打卡明细列表组件 (src/components/AttendanceDetails.tsx)
    • 显示打卡记录列表(按日期倒序)
    • 显示日期和星期信息
    • 显示上班/下班打卡时间
    • 显示打卡状态标签
  • 3.2 创建打卡记录项组件 (src/components/AttendanceRecordItem.tsx)
    • 日期和星期显示
    • 打卡时间显示(上班/下班)
    • 打卡状态标签(正常-绿色、迟到-黄色、早退-橙色、缺勤-红色)
  • 3.3 创建打卡记录类型定义 (src/types/attendance.ts)
    • AttendanceRecord接口
    • AttendanceStatus枚举类型
  • 3.4 集成前端模拟打卡明细数据
  • 3.5 实现列表滚动加载(预留分页加载接口)

任务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/attendance/index.tsx
    • @d8d/rencai-attendance-ui/pages/AttendancePage/AttendancePage导入AttendancePage组件
    • 用AuthProvider包装页面
    • 添加认证检查(未登录跳转到登录页)
    • 导出AttendancePage组件
  • 5.2 验证页面路由配置(已在故事017.001中配置完成)
  • 5.3 验证底部TabBar导航正常工作(TabBar中"考勤"对应attendance页面)

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

  • 6.1 参照原型设计实现考勤记录页样式(原型行303-481)
    • 月份选择器样式(灰色背景、圆角、箭头图标)
    • 考勤统计卡片样式(白色卡片、两列布局)
    • 日历视图样式(7列网格、日期圆形、状态标记)
    • 打卡明细列表样式(白色卡片、列表项分隔、状态标签)
  • 6.2 确保页面设计符合移动端规范:
    • 宽度参考: 375px
    • 圆角规范: 12px (卡片)
    • 颜色主题: 蓝色渐变 (#3b82f6 → #1e40af)
    • 字体规范: 标题18-24px, 正文14px, 小字12px
    • 状态颜色: 正常-绿色、迟到-黄色、早退-橙色、缺勤-红色
  • 6.3 使用正确的组件:考勤记录页使用TabBarLayout(无返回按钮的Navbar)

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

  • 7.1 为AttendancePage编写组件测试 (tests/pages/AttendancePage/AttendancePage.test.tsx)
    • 测试组件渲染
    • 测试考勤日历展示(Mock数据)
    • 测试考勤统计展示(Mock数据)
    • 测试打卡明细列表展示(Mock数据)
    • 测试月份切换功能
  • 7.2 为子组件编写单元测试:
    • AttendanceCalendar.test.tsx
    • AttendanceStats.test.tsx
    • AttendanceDetails.test.tsx
    • AttendanceRecordItem.test.tsx
    • MonthSelector.test.tsx
  • 7.3 编写集成测试验证现有功能不受影响
  • 7.4 运行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.003完成状态:

  • ✅ 个人信息页面功能完整,包含个人基本信息、银行卡信息、证件照片
  • ✅ 数据脱敏工具函数实现(maskUtils.ts
  • ✅ 证件照片预览功能实现
  • ✅ 测试框架选择确认:mini项目使用Jest(不是Vitest)
  • ✅ 使用@d8d/mini-testing-utils提供的Taro mock

故事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)
  5. 前端模拟数据规范:数据结构必须符合后续API接口规范,便于后续替换为真实API

技术栈要求

来源: 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 { mockAttendanceData } from '../../utils/mockAttendanceData'
import { AttendanceCalendar } from '../components/AttendanceCalendar'

错误示例:

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

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/AttendancePage/AttendancePage": {
      "types": "./dist/src/pages/AttendancePage/AttendancePage.d.ts",
      "import": "./dist/src/pages/AttendancePage/AttendancePage.js",
      "require": "./dist/src/pages/AttendancePage/AttendancePage.js"
    }
  }
}

3. 前端模拟数据规范

重要: 本故事使用前端模拟数据,数据结构必须符合后续API接口规范。

模拟数据文件位置: src/utils/mockAttendanceData.ts

数据结构示例:

// 考勤统计数据(符合后续API接口规范)
export interface AttendanceStats {
  attendanceRate: number    // 出勤率(如:100)
  normalDays: number        // 正常出勤天数
  lateCount: number         // 迟到次数
  earlyLeaveCount: number   // 早退次数
  absentCount: number       // 缺勤次数
}

// 打卡记录数据(符合后续API接口规范)
export interface AttendanceRecord {
  date: string              // 日期(如:2023-11-25)
  weekday: string           // 星期(如:星期六)
  checkInTime: string       // 上班打卡时间(如:08:30)
  checkOutTime: string      // 下班打卡时间(如:17:30,未打卡为--:--)
  status: AttendanceStatus  // 打卡状态
}

export enum AttendanceStatus {
  NORMAL = 'normal',         // 正常
  LATE = 'late',             // 迟到
  EARLY_LEAVE = 'early',     // 早退
  ABSENT = 'absent'          // 缺勤
}

// 模拟数据导出
export const mockAttendanceStats: AttendanceStats = {
  attendanceRate: 100,
  normalDays: 28,
  lateCount: 0,
  earlyLeaveCount: 0,
  absentCount: 0
}

export const mockAttendanceRecords: AttendanceRecord[] = [
  {
    date: '2023-11-25',
    weekday: '星期六',
    checkInTime: '08:30',
    checkOutTime: '--:--',
    status: AttendanceStatus.NORMAL
  },
  // ... 更多记录
]

后续API集成预留:

  • 类型定义使用RPC推断类型预留接口
  • 模拟数据导出函数名称与API客户端方法保持一致
  • 便于后续替换为真实API调用

4. Jest配置规范

每个UI包必须创建jest.config.cjs配置文件,参照rencai-personal-info-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}两种测试文件格式

原型设计参考

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

考勤记录页 (原型行303-481):

1. 月份选择器

<!-- 月份选择 -->
<div class="flex justify-between items-center mb-4">
  <h3 class="font-semibold text-gray-700">考勤记录</h3>
  <div class="flex items-center bg-gray-100 rounded-lg px-3 py-1">
    <i class="fas fa-chevron-left text-gray-500 mr-2"></i>
    <span class="text-sm text-gray-700 mr-2">2023年11月</span>
    <i class="fas fa-chevron-right text-gray-500"></i>
  </div>
</div>

移动端设计规范:

  • 灰色背景:bg-gray-100
  • 圆角:12px
  • 箭头图标:Heroicons chevron-left-20-solidchevron-right-20-solid
  • 文字大小:14px (text-sm)

2. 考勤统计卡片

<!-- 考勤统计 -->
<div class="card bg-white p-4 mb-4">
  <div class="flex justify-between items-center mb-4">
    <div>
      <p class="text-gray-700">出勤率</p>
      <p class="text-2xl font-bold text-gray-800">100%</p>
    </div>
    <div class="text-right">
      <p class="text-gray-700">正常出勤</p>
      <p class="text-2xl font-bold text-gray-800">28天</p>
    </div>
  </div>
  <div class="flex justify-between text-sm text-gray-500">
    <span>迟到: 0次</span>
    <span>早退: 0次</span>
    <span>缺勤: 0次</span>
  </div>
</div>

移动端设计规范:

  • 白色卡片背景:bg-white
  • 圆角:12px
  • 内边距:16px (p-4)
  • 卡片间距:16px (mb-4)
  • 统计数字字体大小:24px (text-2xl)
  • 异常统计字体大小:14px (text-sm)

3. 考勤日历视图

<!-- 日历视图 -->
<div class="card bg-white p-4 mb-4">
  <h3 class="font-semibold text-gray-700 mb-3">考勤日历</h3>
  <div class="grid grid-cols-7 gap-1 mb-2">
    <div class="text-center text-xs text-gray-500">日</div>
    <div class="text-center text-xs text-gray-500">一</div>
    <div class="text-center text-xs text-gray-500">二</div>
    <div class="text-center text-xs text-gray-500">三</div>
    <div class="text-center text-xs text-gray-500">四</div>
    <div class="text-center text-xs text-gray-500">五</div>
    <div class="text-center text-xs text-gray-500">六</div>
  </div>
  <div class="grid grid-cols-7 gap-1">
    <!-- 日期单元格 -->
    <div class="calendar-day checked">1</div>
    <div class="calendar-day checked">2</div>
    <!-- ... 更多日期 -->
  </div>
</div>

移动端设计规范:

  • 7列网格布局:grid grid-cols-7
  • 网格间距:4px (gap-1)
  • 星期标题字体大小:12px (text-xs)
  • 日期圆形:rounded-full
  • 已打卡日期:绿色背景 (bg-green-500)
  • 未打卡日期:灰色背景 (bg-gray-200)
  • 周末日期:不同颜色标识

4. 打卡明细列表

<!-- 打卡明细 -->
<div class="card bg-white p-4">
  <h3 class="font-semibold text-gray-700 mb-3">打卡明细</h3>
  <div class="space-y-3">
    <!-- 打卡记录1 -->
    <div class="flex justify-between items-center p-3 border border-gray-100 rounded-lg">
      <div>
        <p class="text-sm font-medium text-gray-800">11月25日 星期六</p>
        <div class="flex text-xs text-gray-500 mt-1">
          <span class="mr-3">上班: 08:30</span>
          <span>下班: --:--</span>
        </div>
      </div>
      <span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full">正常</span>
    </div>
    <!-- ... 更多记录 -->
  </div>
</div>

移动端设计规范:

  • 白色卡片背景:bg-white
  • 圆角:12px (rounded-lg)
  • 列表项间距:12px (space-y-3)
  • 日期字体大小:14px (text-sm)
  • 时间字体大小:12px (text-xs)
  • 状态标签:圆角胶囊 (rounded-full)
  • 状态颜色:
    • 正常:绿色 (bg-green-100 text-green-800)
    • 迟到:黄色 (bg-yellow-100 text-yellow-800)
    • 早退:橙色 (bg-orange-100 text-orange-800)
    • 缺勤:红色 (bg-red-100 text-red-800)

整体移动端设计规范:

  • 宽度参考: 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 AttendancePage() {
  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
│   │   ├── attendance/        # 考勤记录页 (从UI包导入) - 本故事
│   │   │   └── index.tsx
│   │   ├── personal-info/     # 个人信息页 (从UI包导入)
│   │   │   └── index.tsx
│   │   └── settings/          # 设置页 (从UI包导入)
│   │       └── index.tsx
├── package.json
├── jest.config.js             # Jest配置
└── tsconfig.json

mini-ui-packages目录结构:

mini-ui-packages/
├── rencai-attendance-ui/      # 人才考勤记录UI包
│   ├── src/
│   │   ├── api/
│   │   │   ├── attendanceClient.ts
│   │   │   └── index.ts
│   │   ├── pages/
│   │   │   └── AttendancePage/
│   │   │       ├── AttendancePage.tsx
│   │   │       └── index.ts (可选)
│   │   ├── components/        # UI组件
│   │   │   ├── AttendanceCalendar.tsx     # 考勤日历组件
│   │   │   ├── AttendanceStats.tsx        # 考勤统计卡片
│   │   │   ├── MonthSelector.tsx          # 月份选择器
│   │   │   ├── AttendanceDetails.tsx      # 打卡明细列表
│   │   │   └── AttendanceRecordItem.tsx   # 打卡记录项
│   │   ├── types/
│   │   │   └── attendance.ts              # 类型定义
│   │   ├── utils/
│   │   │   └── mockAttendanceData.ts      # 前端模拟数据
│   │   └── 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/attendance/index.tsx
import AttendancePage from '@d8d/rencai-attendance-ui/pages/AttendancePage/AttendancePage'
import { AuthContextProvider, useAuth } from '@d8d/rencai-auth-ui/utils'

function Attendance() {
  const { isLoggedIn } = useAuth()

  // 未登录跳转到登录页
  if (!isLoggedIn) {
    Taro.navigateTo({ url: '/pages/login/index' })
    return null
  }

  return <AttendancePage />
}

export default function AttendanceIndex() {
  return (
    <AuthContextProvider>
      <Attendance />
    </AuthContextProvider>
  )
}

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 AttendanceStats({ stats }: { stats: AttendanceStats }) {
  return (
    <View className="bg-white rounded-lg p-4 mb-4">
      {/* 两列统计布局 - 水平排列 */}
      <View className="flex justify-between items-center mb-4">
        <View>
          <Text className="text-gray-700">出勤率</Text>
          <Text className="text-2xl font-bold text-gray-800">{stats.attendanceRate}%</Text>
        </View>
        <View className="text-right">
          <Text className="text-gray-700">正常出勤</Text>
          <Text className="text-2xl font-bold text-gray-800">{stats.normalDays}天</Text>
        </View>
      </View>

      {/* 异常统计 - 水平排列 */}
      <View className="flex justify-between text-sm text-gray-500">
        <Text>迟到: {stats.lateCount}次</Text>
        <Text>早退: {stats.earlyLeaveCount}次</Text>
        <Text>缺勤: {stats.absentCount}次</Text>
      </View>
    </View>
  )
}

关键点:

  1. 统计卡片使用水平布局(flex justify-between)显示两列统计
  2. 异常统计使用水平布局(flex justify-between)显示三项异常
  3. 打卡明细列表使用 flex flex-col 实现垂直布局
  4. 列表项使用水平布局显示日期、时间和状态
  5. 重要: 记住在所有需要垂直排列的 View 上添加 flex flex-col

图标使用规范

来源: architecture/mini-ui-package-standards.md

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

图标类命名格式: i-heroicons-{图标名称}-{尺寸}-{样式}

本故事需要的图标:

  • chevron-left-20-solid - 左箭头(上个月)
  • chevron-right-20-solid - 右箭头(下个月)
  • calendar-20-solid - 日历图标

正确示例:

// ✅ 正确: 使用Heroicons图标类
<View className="i-heroicons-chevron-left-20-solid w-5 h-5 text-gray-500" />
<View className="i-heroicons-chevron-right-20-solid w-5 h-5 text-gray-500" />

// ❌ 错误: 使用emoji
<Text>←</Text>
<Text>→</Text>

月份选择器图标示例:

import { View, Text } from '@tarojs/components'

export function MonthSelector({ currentMonth, onPreviousMonth, onNextMonth }: MonthSelectorProps) {
  return (
    <View className="flex justify-between items-center mb-4">
      <Text className="font-semibold text-gray-700">考勤记录</Text>
      <View className="flex items-center bg-gray-100 rounded-lg px-3 py-1">
        {/* 左箭头图标 */}
        <View
          className="i-heroicons-chevron-left-20-solid w-5 h-5 text-gray-500 mr-2"
          onClick={onPreviousMonth}
        />
        <Text className="text-sm text-gray-700 mr-2">{currentMonth}</Text>
        {/* 右箭头图标 */}
        <View
          className="i-heroicons-chevron-right-20-solid w-5 h-5 text-gray-500"
          onClick={onNextMonth}
        />
      </View>
    </View>
  )
}

日历组件实现规范

日历网格布局:

import { View, Text } from '@tarojs/components'

export function AttendanceCalendar({ attendanceRecords }: AttendanceCalendarProps) {
  // 计算当前月份的日期网格
  const dates = getCalendarDates(currentYear, currentMonth)

  return (
    <View className="bg-white rounded-lg p-4 mb-4">
      <Text className="font-semibold text-gray-700 mb-3">考勤日历</Text>

      {/* 星期标题 */}
      <View className="grid grid-cols-7 gap-1 mb-2">
        <View className="text-center text-xs text-gray-500"><Text>日</Text></View>
        <View className="text-center text-xs text-gray-500"><Text>一</Text></View>
        <View className="text-center text-xs text-gray-500"><Text>二</Text></View>
        <View className="text-center text-xs text-gray-500"><Text>三</Text></View>
        <View className="text-center text-xs text-gray-500"><Text>四</Text></View>
        <View className="text-center text-xs text-gray-500"><Text>五</Text></View>
        <View className="text-center text-xs text-gray-500"><Text>六</Text></View>
      </View>

      {/* 日期网格 */}
      <View className="grid grid-cols-7 gap-1">
        {dates.map((date) => {
          const attendance = getAttendanceForDate(date, attendanceRecords)
          return (
            <View
              key={date.toString()}
              className={`
                calendar-day
                text-center
                p-2
                rounded-full
                ${attendance ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-700'}
                ${isWeekend(date) ? 'text-gray-400' : ''}
              `}
            >
              <Text className="text-sm">{date.getDate()}</Text>
            </View>
          )
        })}
      </View>
    </View>
  )
}

关键点:

  1. 使用grid grid-cols-7实现7列网格布局
  2. 使用gap-1设置网格间距
  3. 日期使用圆形背景(rounded-full
  4. 已打卡日期使用绿色背景(bg-green-500
  5. 未打卡日期使用灰色背景(bg-gray-200
  6. 周末日期使用灰色文字(text-gray-400

打卡明细列表实现规范

列表组件结构:

import { View, Text } from '@tarojs/components'

export function AttendanceDetails({ records }: AttendanceDetailsProps) {
  return (
    <View className="bg-white rounded-lg p-4">
      <Text className="font-semibold text-gray-700 mb-3">打卡明细</Text>

      {/* 列表容器 - 垂直布局 */}
      <View className="flex flex-col space-y-3">
        {records.map((record) => (
          <AttendanceRecordItem key={record.date} record={record} />
        ))}
      </View>
    </View>
  )
}

export function AttendanceRecordItem({ record }: AttendanceRecordItemProps) {
  const statusLabel = getStatusLabel(record.status)
  const statusColor = getStatusColor(record.status)

  return (
    <View className="flex justify-between items-center p-3 border border-gray-100 rounded-lg">
      {/* 左侧:日期和时间 */}
      <View>
        <Text className="text-sm font-medium text-gray-800">{record.date} {record.weekday}</Text>
        <View className="flex text-xs text-gray-500 mt-1">
          <Text className="mr-3">上班: {record.checkInTime}</Text>
          <Text>下班: {record.checkOutTime}</Text>
        </View>
      </View>

      {/* 右侧:状态标签 */}
      <View className={`text-xs px-2 py-1 rounded-full ${statusColor}`}>
        <Text>{statusLabel}</Text>
      </View>
    </View>
  )
}

状态颜色映射:

function getStatusColor(status: AttendanceStatus): string {
  switch (status) {
    case AttendanceStatus.NORMAL:
      return 'bg-green-100 text-green-800'
    case AttendanceStatus.LATE:
      return 'bg-yellow-100 text-yellow-800'
    case AttendanceStatus.EARLY_LEAVE:
      return 'bg-orange-100 text-orange-800'
    case AttendanceStatus.ABSENT:
      return 'bg-red-100 text-red-800'
    default:
      return 'bg-gray-100 text-gray-800'
  }
}

function getStatusLabel(status: AttendanceStatus): string {
  switch (status) {
    case AttendanceStatus.NORMAL:
      return '正常'
    case AttendanceStatus.LATE:
      return '迟到'
    case AttendanceStatus.EARLY_LEAVE:
      return '早退'
    case AttendanceStatus.ABSENT:
      return '缺勤'
    default:
      return '未知'
  }
}

前端模拟数据最佳实践

数据工厂模式:

// src/utils/mockAttendanceData.ts

/**
 * 生成指定月份的模拟考勤数据
 * @param year 年份
 * @param month 月份 (1-12)
 * @returns 考勤统计数据和打卡记录
 */
export function generateMockAttendanceData(year: number, month: number): {
  stats: AttendanceStats
  records: AttendanceRecord[]
} {
  // 生成当前月份的所有日期
  const dates = getDatesInMonth(year, month)

  // 生成打卡记录
  const records: AttendanceRecord[] = dates.map((date) => {
    const weekday = getWeekday(date)
    const isWeekend = weekday === '星期六' || weekday === '星期日'

    // 周末不打卡
    if (isWeekend) {
      return {
        date: formatDate(date),
        weekday,
        checkInTime: '--:--',
        checkOutTime: '--:--',
        status: AttendanceStatus.NORMAL
      }
    }

    // 工作日随机生成打卡记录
    const random = Math.random()
    let status = AttendanceStatus.NORMAL
    let checkInTime = '08:30'
    let checkOutTime = '17:30'

    if (random < 0.1) {
      status = AttendanceStatus.LATE
      checkInTime = '09:15'
    } else if (random < 0.2) {
      status = AttendanceStatus.EARLY_LEAVE
      checkOutTime = '16:30'
    }

    return {
      date: formatDate(date),
      weekday,
      checkInTime,
      checkOutTime,
      status
    }
  })

  // 计算统计数据
  const stats = calculateAttendanceStats(records)

  return { stats, records }
}

/**
 * 计算考勤统计数据
 */
function calculateAttendanceStats(records: AttendanceRecord[]): AttendanceStats {
  const workDays = records.filter(r => {
    const weekday = r.weekday
    return weekday !== '星期六' && weekday !== '星期日'
  })

  const normalDays = records.filter(r => r.status === AttendanceStatus.NORMAL).length
  const lateCount = records.filter(r => r.status === AttendanceStatus.LATE).length
  const earlyLeaveCount = records.filter(r => r.status === AttendanceStatus.EARLY_LEAVE).length
  const absentCount = records.filter(r => r.status === AttendanceStatus.ABSENT).length

  const attendanceRate = workDays.length > 0
    ? Math.round((normalDays / workDays.length) * 100)
    : 0

  return {
    attendanceRate,
    normalDays,
    lateCount,
    earlyLeaveCount,
    absentCount
  }
}

测试策略

来源: 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/
    │       ├── AttendanceCalendar.test.tsx
    │       ├── AttendanceStats.test.tsx
    │       ├── AttendanceDetails.test.tsx
    │       ├── AttendanceRecordItem.test.tsx
    │       └── MonthSelector.test.tsx
    └── pages/                     # 页面组件测试
        └── AttendancePage/
            └── AttendancePage.test.tsx

测试要求:

  1. 为每个页面组件编写Jest测试
  2. 测试前端模拟数据的正确性
  3. 测试月份切换功能
  4. 测试日历视图的正确渲染
  5. 测试打卡明细列表的正确显示
  6. 验证mini-talent项目现有功能不受影响
  7. 运行pnpm typecheck确保类型检查通过

Mock响应示例:

const mockAttendanceStats: AttendanceStats = {
  attendanceRate: 100,
  normalDays: 28,
  lateCount: 0,
  earlyLeaveCount: 0,
  absentCount: 0
}

const mockAttendanceRecords: AttendanceRecord[] = [
  {
    date: '2023-11-25',
    weekday: '星期六',
    checkInTime: '08:30',
    checkOutTime: '--:--',
    status: AttendanceStatus.NORMAL
  },
  // ... 更多记录
]

编码标准

来源: architecture/coding-standards.md

关键编码规范:

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

开发Mini UI包时,必须参考并遵循Mini UI包开发规范,该规范基于史诗011和017的经验总结。

2. 关键检查点 (基于史诗011和017经验)

  • Taro布局规范: View容器默认横向布局,必须添加flex flex-col实现垂直布局
  • 图标使用规范: 必须使用Heroicons图标类,不要使用emoji
  • 测试框架选择: mini项目使用Jest(不是Vitest)
  • Navbar集成: TabBar页面使用Navbar无返回按钮

3. 常见错误避免

  • ❌ 不要忘记添加 flex flex-col 实现垂直布局
  • ❌ 不要使用emoji代替Heroicons图标
  • ❌ 不要忘记为图标添加尺寸类(w-5 h-5text-lg等)
  • ❌ 不要在Mini UI包内部导入中使用别名 (@/~/等),必须使用相对路径
  • ❌ 不要在TabBar页面添加返回按钮
  • ❌ 不要使用Vitest作为Mini项目的测试框架(应使用Jest)

路径使用示例:

// ✅ 正确: UI包内部使用相对路径
import { mockAttendanceData } from '../../utils/mockAttendanceData'
import { AttendanceCalendar } from '../components/AttendanceCalendar'

// ✅ 正确: 跨包导入使用workspace包名
import { SharedComponent } from '@d8d/mini-shared-ui-components'

// ❌ 错误: UI包内部使用别名
import { mockAttendanceData } from '@/utils/mockAttendanceData'
import { AttendanceCalendar } from '@/components/AttendanceCalendar'

4. 参考实现

  • 用人方考勤UI包: mini-ui-packages/yongren-attendance-ui (如果存在)
  • 用人方仪表板UI包: mini-ui-packages/yongren-dashboard-ui
    • 组件: src/pages/Dashboard/Dashboard.tsx
    • package.json exports配置
    • 目录结构参考
    • Jest配置: jest.config.cjs
  • 人才个人信息UI包: mini-ui-packages/rencai-personal-info-ui
    • 组件结构和测试参考
    • 前端模拟数据实现参考

技术约束

  1. 向后兼容: 不影响现有mini-talent项目功能
  2. 类型安全: 使用TypeScript严格模式,所有数据结构必须有类型定义
  3. 模块独立性: 每个UI包独立管理自己的类型定义和模拟数据
  4. 测试覆盖: 所有新增代码必须有测试覆盖
  5. 代码规范: 遵循项目编码标准和Mini UI包开发规范
  6. 数据规范: 前端模拟数据结构符合后续API接口规范,便于后续替换为真实API

风险和缓解措施

主要风险:

  1. API延期风险: 史诗015的考勤记录API为P2延期功能,使用前端模拟数据
  2. 日历组件复杂度: 日历视图实现可能较为复杂,需要处理月份切换和日期计算
  3. 数据结构变更: 前端模拟数据结构与后续API接口可能存在差异
  4. UI组件复用风险: rencai系列UI包可能与现有yongren系列UI包存在差异

缓解措施:

  1. 前端模拟数据规范: 严格按照后续API接口规范设计数据结构,便于后续替换
  2. 分阶段实现: 先实现考勤统计和打卡明细,再实现日历视图
  3. 参考现有模式: 参照yongren系列UI包的实现模式和架构
  4. 类型安全: 使用TypeScript接口定义数据结构,确保类型一致性
  5. 测试驱动: 编写完整的测试,确保功能正确性
  6. 数据工厂模式: 使用数据工厂模式生成模拟数据,便于后续替换为真实API

测试

测试框架和模式

来源: 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/
    │       ├── AttendanceCalendar.test.tsx
    │       ├── AttendanceStats.test.tsx
    │       ├── AttendanceDetails.test.tsx
    │       ├── AttendanceRecordItem.test.tsx
    │       └── MonthSelector.test.tsx
    └── pages/                     # 页面组件测试
        └── AttendancePage/
            └── AttendancePage.test.tsx

测试要求

  1. 组件测试:

    • 测试组件渲染正确
    • 测试用户交互(月份切换、点击日期等)
    • 测试前端模拟数据集成
    • 测试错误处理
  2. 日历组件测试:

    • 测试日历网格的正确渲染
    • 测试日期标记的正确显示
    • 测试周末日期的正确标识
    • 测试月份切换功能
  3. 统计组件测试:

    • 测试统计数据的正确显示
    • 测试数据格式化(百分比、天数、次数)
  4. 打卡明细测试:

    • 测试打卡记录列表的正确渲染
    • 测试打卡状态标签的颜色
    • 测试日期倒序排列
  5. 集成测试:

    • 测试考勤记录页面的三个模块数据加载
    • 测试Navbar导航栏的正确显示
    • 测试月份切换后数据和视图的更新
  6. 回归测试:

    • 验证mini-talent项目现有功能不受影响
    • 运行pnpm typecheck确保类型检查通过

测试执行

# 运行所有测试
cd mini-ui-packages/rencai-attendance-ui && pnpm test

# 运行特定测试
pnpm test --testNamePattern="AttendancePage"

# 生成覆盖率报告
pnpm test:coverage

变更日志

日期 版本 描述 作者
2025-12-26 1.0 创建故事文档 Bob (Scrum Master)

开发者记录

此部分由开发代理在实施过程中填写

使用的代理模型

Claude Sonnet (claude-sonnet-4-20250514)

调试日志引用

无特殊调试需求

完成说明列表

  1. 类型定义 (src/types/attendance.ts): 创建了AttendanceStats、AttendanceRecord接口和AttendanceStatus枚举
  2. 前端模拟数据 (src/utils/mockAttendanceData.ts): 实现数据工厂模式,生成符合后续API接口规范的模拟数据
  3. 月份选择器 (src/components/MonthSelector.tsx): 支持月份切换,使用Heroicons图标
  4. 考勤统计卡片 (src/components/AttendanceStats.tsx): 显示出勤率、正常出勤天数和异常统计
  5. 考勤日历 (src/components/AttendanceCalendar.tsx): 7列网格布局,标记已打卡日期(绿色)和未打卡日期(灰色)
  6. 打卡记录项 (src/components/AttendanceRecordItem.tsx): 显示单条打卡记录,支持状态颜色标签
  7. 打卡明细列表 (src/components/AttendanceDetails.tsx): 显示打卡记录列表,按日期倒序排列
  8. 主页面组件 (src/pages/AttendancePage/AttendancePage.tsx): 集成所有子组件,实现月份切换和数据加载
  9. 测试覆盖: 编写36个测试用例,覆盖所有组件和页面功能

文件列表

新增源代码文件:

  • mini-ui-packages/rencai-attendance-ui/src/types/attendance.ts - 类型定义
  • mini-ui-packages/rencai-attendance-ui/src/utils/mockAttendanceData.ts - 前端模拟数据
  • mini-ui-packages/rencai-attendance-ui/src/components/MonthSelector.tsx - 月份选择器
  • mini-ui-packages/rencai-attendance-ui/src/components/AttendanceStats.tsx - 考勤统计卡片
  • mini-ui-packages/rencai-attendance-ui/src/components/AttendanceCalendar.tsx - 考勤日历
  • mini-ui-packages/rencai-attendance-ui/src/components/AttendanceRecordItem.tsx - 打卡记录项
  • mini-ui-packages/rencai-attendance-ui/src/components/AttendanceDetails.tsx - 打卡明细列表

修改源代码文件:

  • mini-ui-packages/rencai-attendance-ui/src/pages/AttendancePage/AttendancePage.tsx - 更新主页面组件

新增测试文件:

  • tests/unit/components/MonthSelector.test.tsx - 月份选择器测试
  • tests/unit/components/AttendanceStats.test.tsx - 考勤统计测试
  • tests/unit/components/AttendanceCalendar.test.tsx - 考勤日历测试
  • tests/unit/components/AttendanceRecordItem.test.tsx - 打卡记录项测试
  • tests/unit/components/AttendanceDetails.test.tsx - 打卡明细列表测试
  • tests/pages/AttendancePage/AttendancePage.test.tsx - 主页面测试

测试结果: 36个测试全部通过

QA结果

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