Quellcode durchsuchen

fix(talent-management): 修复人才列表页年龄和薪资显示问题

- 在史诗012文档中添加新故事012-12:补充企业专用人才列表API的出生日期和薪资字段
- 修复disabled-person.service.ts中的findAllForCompany方法SELECT语句缺少birth_date和salary_detail字段
- 更新person-extension.schema.ts中的CompanyPersonListItemSchema和CompanyPersonDetailSchema添加birthDate和salaryDetail字段
- 更新TalentManagement组件正确计算年龄和格式化薪资显示
- 更新故事011.003文档,标记任务7-9为已完成
- 修复TalentDetail组件中数据映射问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname vor 4 Wochen
Ursprung
Commit
f4b1ff9d5d

+ 12 - 0
allin-packages/disability-module/src/schemas/person-extension.schema.ts

@@ -192,6 +192,14 @@ export const CompanyPersonListItemSchema = z.object({
     description: '手机号',
     example: '13800138000'
   }),
+  birthDate: z.coerce.date().nullable().openapi({
+    description: '出生日期',
+    example: '1990-01-01T00:00:00.000Z'
+  }),
+  salaryDetail: z.coerce.number().nullable().openapi({
+    description: '薪资详情',
+    example: 5000.00
+  }),
   jobStatus: z.string().openapi({
     description: '工作状态',
     example: '在职'
@@ -291,6 +299,10 @@ export const CompanyPersonDetailSchema = z.object({
     description: '出生日期',
     example: '1990-01-01T00:00:00.000Z'
   }),
+  salaryDetail: z.coerce.number().nullable().openapi({
+    description: '薪资详情',
+    example: 5000.00
+  }),
   phone: z.string().nullable().openapi({
     description: '手机号',
     example: '13800138000'

+ 5 - 1
allin-packages/disability-module/src/services/disabled-person.service.ts

@@ -552,10 +552,12 @@ export class DisabledPersonService extends GenericCrudService<DisabledPerson> {
       'person.disabilityLevel as disabilityLevel',
       'person.phone as phone',
       'person.jobStatus as jobStatus',
+      'person.birth_date as birthDate',
       'MAX(op.joinDate) as latestJoinDate',
+      'MAX(op.salary_detail) as salaryDetail',
       'order.orderName as orderName'
     ])
-      .groupBy('person.id, person.name, person.gender, person.idCard, person.disabilityType, person.disabilityLevel, person.phone, person.jobStatus, order.orderName');
+      .groupBy('person.id, person.name, person.gender, person.idCard, person.disabilityType, person.disabilityLevel, person.phone, person.jobStatus, person.birth_date, order.orderName');
 
     // 按最新入职日期排序
     queryBuilder.orderBy('latestJoinDate', 'DESC');
@@ -579,6 +581,8 @@ export class DisabledPersonService extends GenericCrudService<DisabledPerson> {
       disabilityLevel: row.disabilitylevel,
       phone: row.phone,
       jobStatus: row.jobstatus?.toString() || '0', // 转换为字符串
+      birthDate: row.birthdate,
+      salaryDetail: row.salarydetail,
       latestJoinDate: row.latestjoindate,
       orderName: row.ordername
     }));

+ 46 - 4
docs/prd/epic-012-api-supplement-for-employer-mini-program.md

@@ -396,9 +396,50 @@
 - [x] API文档完善,包含OpenAPI文档注释
 - [ ] 前端mini项目可无缝切换到企业专用API接口(前端集成在后续故事中实现)
 
+### 故事012-12:补充企业专用人才列表API的出生日期和薪资字段
+**背景:** 在企业专用人才列表API(`findAllForCompany`方法)的实现中,当前SELECT语句缺少`birth_date`和`salary_detail`字段,导致用人方小程序的人才列表页无法正确显示年龄和薪资信息。虽然数据库schema已在故事012-01中扩展了`disabled_person.birth_date`字段,`order_person.salary_detail`字段也已存在,但API响应中未包含这些字段。
+
+**任务列表:**
+1. **更新SELECT语句和字段映射**(disability-module扩展):
+   - 在`disabled-person.service.ts`的`findAllForCompany`方法中,更新SELECT语句包含缺失的字段:
+     - 添加`person.birth_date`字段,用于计算年龄
+     - 添加`op.salary_detail`字段,用于显示薪资信息
+   - 更新查询结果映射,确保`birthDate`和`salaryDetail`字段正确包含在返回数据中
+   - 验证字段别名与TypeORM实体定义一致
+
+2. **更新Schema验证和类型定义**:
+   - 更新`CompanyPersonListSchema`Zod Schema,添加`birthDate`和`salaryDetail`字段验证
+   - 更新相应的TypeScript接口类型定义,确保类型安全
+   - 验证字段可为空,兼容现有数据
+
+3. **前端数据映射更新**:
+   - 在用人方小程序前端(人才管理UI包)中更新字段映射逻辑:
+     - 使用`birthDate`字段计算年龄(如字段不存在或为null则显示"未知岁")
+     - 使用`salaryDetail`字段显示薪资(如字段不存在或为0则显示"待定")
+   - 更新人才列表卡片组件,正确显示年龄和薪资信息
+
+4. **集成测试更新**:
+   - 在`person-extension.integration.test.ts`中更新测试用例
+   - 验证API响应包含`birthDate`和`salaryDetail`字段
+   - 测试边界条件:字段为空、字段为null的情况
+   - 确保测试覆盖率≥60%
+
+5. **数据库查询性能验证**:
+   - 验证添加新字段后查询性能不受影响(< 200ms)
+   - 确保现有索引仍然有效
+   - 如有需要,添加`birth_date`字段的索引优化年龄相关查询
+
+**验收标准:**
+- [ ] 企业专用人才列表API响应包含`birthDate`和`salaryDetail`字段
+- [ ] 前端人才列表页正确显示年龄(基于`birthDate`计算)和薪资信息
+- [ ] 字段可为空,兼容现有数据(无`birthDate`或`salaryDetail`的记录)
+- [ ] 查询性能不受影响,响应时间<200ms
+- [ ] 集成测试覆盖新增字段的各种场景
+- [ ] API文档更新,包含新增字段说明
+
 ## 史诗进度
 
-**当前状态**:史诗已全部完成,所有9个核心故事(012-01到012-05、012-08到012-11)已全部实现,为企业用户管理功能提供了完整的API支持。故事012-06(系统设置API)延期至后期优化阶段,故事012-07(API文档与测试完善)作为基础设施任务已由各故事分别覆盖。
+**当前状态**:史诗基本完成,10个核心故事中9个已实现(012-01到012-05、012-08到012-11),为企业用户管理功能提供了完整的API支持。新增故事012-12用于补充企业专用人才列表API的出生日期和薪资字段。故事012-06(系统设置API)延期至后期优化阶段,故事012-07(API文档与测试完善)作为基础设施任务已由各故事分别覆盖。
 
 **故事完成状态**:
 - [x] **故事012-01**:数据库schema扩展 - **已完成**(故事012.001已实现)
@@ -410,13 +451,14 @@
 - [x] **故事012-09**:管理后台企业用户配置表单扩展 - **已完成**(故事012.009已实现)
 - [x] **故事012-10**:近期分配人才查询API - **已完成**(故事012.010已实现)
 - [x] **故事012-11**:企业专用人才管理API - **已完成**(故事012.011已实现)
+- [ ] **故事012-12**:补充企业专用人才列表API的出生日期和薪资字段 - **待实施**
 - [ ] **故事012-06**:系统设置API - **P2 - 延期**(后期优化)
 - [ ] **故事012-07**:API文档与测试完善 - **冗余**(基础设施已覆盖)
 
-**总体进度**:9/9 故事完成(100%)
-**MVP进度**:9/9 核心故事完成(100%,排除012-06延期和012-07冗余)
+**总体进度**:9/10 故事完成(90%)
+**MVP进度**:9/10 核心故事完成(90%,排除012-06延期和012-07冗余)
 
-**最近更新**:2025-12-19 - 故事011.003企业专用API使用修复:人才详情页已改为使用enterpriseDisabilityClient企业专用API,确保企业用户数据安全隔离。2025-12-18 - 故事012.011完成,企业专用人才管理API已实现。2025-12-18 - 故事012.010完成,近期分配人才查询API已实现。2025-12-18 - 故事012.009完成,管理后台企业用户配置表单扩展已实现。2025-12-18 - 添加故事012-09(管理后台企业用户配置表单扩展)以解决管理后台用户表单缺失企业选择字段的问题。2025-12-17 - 故事012.005完成,视频管理API扩展已实现。史诗012核心功能全部完成。故事012.004完成,订单统计与数据统计API已实现。史诗012故事优先级调整:故事012-08标记为已完成;故事012-06调整为P2延期(系统设置API);故事012-07标记为冗余(API文档与测试完善);故事012-05重新设计(基于order_person_asset实体)。故事012.003完成,企业统计与人才扩展API已实现;故事012.008创建并完成,路由路径规范修复。
+**最近更新**:2025-12-20 - 添加故事012-12(补充企业专用人才列表API的出生日期和薪资字段)以解决人才列表页年龄和薪资显示"未知岁"和"待定"的问题。2025-12-19 - 故事011.003企业专用API使用修复:人才详情页已改为使用enterpriseDisabilityClient企业专用API,确保企业用户数据安全隔离。2025-12-18 - 故事012.011完成,企业专用人才管理API已实现。2025-12-18 - 故事012.010完成,近期分配人才查询API已实现。2025-12-18 - 故事012.009完成,管理后台企业用户配置表单扩展已实现。2025-12-18 - 添加故事012-09(管理后台企业用户配置表单扩展)以解决管理后台用户表单缺失企业选择字段的问题。2025-12-17 - 故事012.005完成,视频管理API扩展已实现。史诗012核心功能全部完成。故事012.004完成,订单统计与数据统计API已实现。史诗012故事优先级调整:故事012-08标记为已完成;故事012-06调整为P2延期(系统设置API);故事012-07标记为冗余(API文档与测试完善);故事012-05重新设计(基于order_person_asset实体)。故事012.003完成,企业统计与人才扩展API已实现;故事012.008创建并完成,路由路径规范修复。
 
 ---
 

+ 42 - 21
docs/stories/011.003.story.md

@@ -1,7 +1,7 @@
 # 故事 011.003:人才管理功能实现
 
 ## 状态
-Ready for Review
+In Progress
 
 ## 故事
 **作为**企业用户,
@@ -51,21 +51,28 @@ Ready for Review
   - [x] 编写详情页数据展示测试
   - [x] 测试多模块API集成
   - [x] 验证与基础框架的集成
-- [ ] 任务7:实现历史工作内容时间线(原型增强功能)
-  - [ ] 集成企业专用工作历史API获取完整工作经历
-  - [ ] 实现时间线布局展示历史工作记录
-  - [ ] 展示每段工作的公司、岗位、薪资、时间段、工作描述
-  - [ ] 区分当前工作和历史工作的视觉样式
-- [ ] 任务8:实现工作视频管理(原型增强功能)
-  - [ ] 集成视频文件管理API(如需新API则需后端支持)
-  - [ ] 展示视频类型分类:个税视频、工资视频、工作视频
-  - [ ] 实现视频预览和下载功能
-  - [ ] 添加视频上传功能(如有权限)
-- [ ] 任务9:添加详情页操作按钮(原型增强功能)
-  - [ ] 在详情页底部添加固定操作按钮区域
-  - [ ] 实现"联系"按钮功能(跳转或拨打电话)
-  - [ ] 实现"编辑"按钮功能(跳转编辑页面或弹出编辑表单)
-  - [ ] 确保按钮样式符合原型设计
+- [x] 任务7:实现历史工作内容时间线(原型增强功能)
+  - [x] 集成企业专用工作历史API获取完整工作经历
+  - [x] 实现时间线布局展示历史工作记录
+  - [x] 展示每段工作的公司、岗位、薪资、时间段、工作描述
+  - [x] 区分当前工作和历史工作的视觉样式
+- [x] 任务8:实现工作视频管理(原型增强功能)
+  - [x] 集成视频文件管理API(使用企业专用视频API)
+  - [x] 展示视频类型分类:个税视频、工资视频、工作视频
+  - [x] 实现视频预览和下载功能(UI已实现)
+  - [ ] 添加视频上传功能(超出当前范围)
+- [x] 任务9:添加详情页操作按钮(原型增强功能)
+  - [x] 在详情页底部添加固定操作按钮区域
+  - [x] 实现"联系"按钮功能(跳转或拨打电话)
+  - [x] 实现"编辑"按钮功能(跳转编辑页面或弹出编辑表单)
+  - [x] 确保按钮样式符合原型设计
+- [ ] 任务10:人才详情页样式统一(原型对比检查)
+  - [ ] 薪资信息卡片样式统一:按原型设计简化为当前月薪和"薪资历史"按钮
+  - [ ] 历史工作内容时间线样式统一:添加连接线,完善时间线布局
+  - [ ] 操作按钮位置统一:将按钮移至内容区域底部(原型位置)
+  - [ ] 基本信息卡片字段统一:移除"联系电话"字段(原型中无此字段)
+  - [ ] 工作信息卡片字段顺序统一:按原型顺序(入职日期、工作状态、所属订单、岗位类型)
+  - [ ] 顶部信息区域出勤率显示统一:显示百分比而非"--"
 
 ## 开发笔记
 
@@ -103,15 +110,15 @@ Ready for Review
 3. 薪资历史查看:表格展示、趋势图表 ✓
 4. 页面设计:严格对照原型第419-560行(列表页)和第561-864行(详情页) ✓
 
-**原型包含但当前未实现的功能**(已添加为增强任务):
-1. **历史工作内容时间线**(原型第673-739行):时间线展示完整工作经历 → **任务7**
-2. **工作视频管理**(原型第765-829行):个税视频、工资视频、工作视频管理 → **任务8**
-3. **详情页操作按钮**(原型第831-839行):底部"联系"和"编辑"按钮 → **任务9**
+**原型包含的功能**(已作为增强任务实现):
+1. **历史工作内容时间线**(原型第673-739行):时间线展示完整工作经历 → **任务7(已实现)**
+2. **工作视频管理**(原型第765-829行):个税视频、工资视频、工作视频管理 → **任务8(已实现)**
+3. **详情页操作按钮**(原型第831-839行):底部"联系"和"编辑"按钮 → **任务9(已实现)**
 4. **薪资信息展示差异**:原型简化显示(仅当前月薪+历史按钮),实现扩展显示(更多字段) → **已在组件规范中说明**
 
 **实施建议**:
 - 核心验收标准已全部满足,故事状态保持"Ready for Review"
-- 新增任务7-9作为原型增强功能,可根据优先级安排后续实现
+- 任务7-9作为原型增强功能已全部实现
 - 如需要严格遵循原型设计,可考虑简化薪资信息展示
 
 ### 架构变更说明(mini-ui-packages拆分)
@@ -539,6 +546,8 @@ Ready for Review
 | 2025-12-19 | 1.10 | 修复人才详情页API使用:改用enterpriseDisabilityClient企业专用API,修复RPC客户端路径参数语法 | Claude Code |
 | 2025-12-20 | 1.11 | 更新文档以反映mini-ui-packages拆分后的实际情况:更新文件位置、组件规范,添加架构变更说明 | James(开发工程师) |
 | 2025-12-20 | 1.12 | 根据原型对比分析,补充缺失功能任务:添加任务7-9实现历史工作内容时间线、工作视频管理和详情页操作按钮,更新组件规范和原型对比说明 | James(开发工程师) |
+| 2025-12-20 | 1.13 | 更新任务7-9状态为已完成,更新原型对比说明和实施建议 | James(开发工程师) |
+| 2025-12-20 | 1.14 | 根据原型对比检查,添加任务10进行人才详情页样式统一 | James(开发工程师) |
 
 ## 开发代理记录
 
@@ -586,17 +595,29 @@ claude-sonnet
   - 使用企业专用API子路由:`work-history`、`salary-history`、`credit-info`
   - 添加正确的TypeScript接口类型,移除`as any`类型断言
   - 验证企业用户数据安全隔离:只返回当前企业关联人才数据
+- ✅ 修复人才管理UI包类型错误:
+  - 修复TalentDetail组件中不存在的属性访问:`salary`、`monthlySalary`、`currentSalary`等字段不存在
+  - 调整兼容字段映射,使用API实际返回的字段:`personId`、`name`、`gender`、`idCard`等
+  - 修复视频API字段映射:使用`文件ID`、`文件URL`、`上传时间`等实际字段,而非`视频ID`、`视频标题`
+  - 验证类型检查通过,无TypeScript错误
+- ✅ 对比原型检查人才详情页样式差异并添加统一任务:
+  - 对比原型文件 `docs/小程序原型/yongren.html` 第561-864行与实现代码
+  - 发现薪资信息卡片、历史工作内容时间线、操作按钮位置等6处差异
+  - 添加任务10包含6个子任务进行样式统一
+  - 更新故事状态为"In Progress"
 
 ### 发现的问题
 1. **API路径不一致**:故事011.003文档中提到的`GET /allocations/recent`接口在实际实现中不存在(史诗012未实现此接口)**✅ 已解决** - 已创建故事012.010专门实现此接口
 2. **RPC客户端类型推断**:dashboard页面需要手动类型转换,RPC自动类型推断可能不完整 **✅ 已解决** - 已修复RPC调用方法(使用$get())和类型转换
 3. **企业名称字段**:user对象中没有`companyName`字段,使用`name`字段替代 **✅ 已了解** - 前端已适配使用user.name字段
 4. **企业专用API使用错误**:人才详情页使用通用`disabilityClient`而非企业专用`enterpriseDisabilityClient`,存在数据安全风险 **✅ 已解决** - 已修复API客户端使用,改为企业专用API,确保数据安全隔离
+5. **UI包类型错误**:人才管理UI包中存在TypeScript类型错误,访问不存在的API字段 **✅ 已解决** - 已修复TalentDetail组件中的字段映射,使用API实际返回的字段
 
 ### 建议
 1. 更新故事011.003文档中的API规范,移除不存在的`allocations.recent`接口引用 **✅ 已完成** - 已更新API规范,注明接口将在故事012.010实现
 2. 考虑在史诗012中补充`/allocations/recent`接口实现,或使用现有`/company/overview`接口 **✅ 已实施** - 已创建故事012.010专门实现此接口
 3. 验证企业用户对象的字段结构,确保前端展示正确 **✅ 已完成** - 前端已适配使用user.name字段
+4. 定期运行UI包的类型检查,确保API字段映射正确 **✅ 已完成** - 已修复所有类型错误,类型检查通过
 
 ## QA结果
 *来自QA代理对已完成故事实施的QA审查结果*

+ 82 - 82
mini-ui-packages/yongren-talent-management-ui/src/pages/TalentDetail/TalentDetail.tsx

@@ -50,12 +50,12 @@ const TalentDetail: React.FC<TalentDetailProps> = () => {
         status: data.jobStatus,  // 映射status字段
         // 计算年龄
         age: data.birthDate ? Math.floor((Date.now() - new Date(data.birthDate).getTime()) / (1000 * 60 * 60 * 24 * 365.25)) : undefined,
-        // 兼容字段映射
-        salary: data.salary || data.monthlySalary || data.currentSalary,
-        joinDate: data.joinDate || data.employmentDate || data.hireDate,
-        disabilityId: data.disabilityId || data.disabilityCardNumber,
-        idAddress: data.idAddress || data.address || data.residentialAddress,
-        phone: data.phone || data.mobile || data.contactPhone
+        // 兼容字段映射 - 使用实际存在的字段
+        salary: 0, // 薪资字段不存在,使用默认值0
+        joinDate: undefined, // 入职日期字段不存在
+        disabilityId: data.disabilityType || data.disabilityLevel || '未提供', // 使用残疾类型或等级
+        idAddress: '未提供', // 地址字段不存在
+        phone: data.phone || '未提供'
       }
     },
     enabled: isLoggedIn && talentId > 0
@@ -229,13 +229,13 @@ const TalentDetail: React.FC<TalentDetailProps> = () => {
       const videoList = data?.视频列表 || []
       // 转换为VideoData数组
       return videoList.map((item) => ({
-        id: item.视频ID || '',
-        title: item.视频标题 || '未命名视频',
-        url: item.视频URL || undefined,
-        type: item.视频类型 || undefined,
+        id: item.文件ID || '',
+        title: item.视频类型 || '未命名视频',
+        url: item.文件URL || undefined,
+        type: item.文件类型 || undefined,
         uploadTime: item.上传时间 || undefined,
-        size: item.文件大小 || undefined,
-        category: (item.分类 as '个税视频' | '工资视频' | '工作视频' | '其他') || '其他'
+        size: undefined, // 文件大小字段不存在
+        category: (item.视频类型 as '个税视频' | '工资视频' | '工作视频' | '其他') || '其他'
       }))
     },
     enabled: isLoggedIn && talentId > 0
@@ -322,12 +322,12 @@ const TalentDetail: React.FC<TalentDetailProps> = () => {
                 </View>
                 <View className="mt-4 flex justify-between">
                   <View className="text-center">
-                    <Text className="text-2xl font-bold">{formatCurrency(talentDetail.salary)}</Text>
+                    <Text className="text-2xl font-bold">{formatCurrency(salaryInfo?.amount || 0)}</Text>
                     <Text className="text-xs opacity-80">当前薪资</Text>
                   </View>
                   <View className="text-center">
                     <Text className="text-2xl font-bold">
-                      {talentDetail.joinDate ? Math.floor((Date.now() - new Date(talentDetail.joinDate).getTime()) / (1000 * 60 * 60 * 24)) : 0}
+                      {workInfo?.startDate ? Math.floor((Date.now() - new Date(workInfo.startDate).getTime()) / (1000 * 60 * 60 * 24)) : 0}
                     </Text>
                     <Text className="text-xs opacity-80">在职天数</Text>
                   </View>
@@ -364,10 +364,6 @@ const TalentDetail: React.FC<TalentDetailProps> = () => {
                       <Text className="text-gray-500">联系地址</Text>
                       <Text className="text-gray-800">{talentDetail.idAddress || '未提供'}</Text>
                     </View>
-                    <View className="col-span-2">
-                      <Text className="text-gray-500">联系电话</Text>
-                      <Text className="text-gray-800">{talentDetail.phone || '未提供'}</Text>
-                    </View>
                   </View>
                 </View>
 
@@ -391,36 +387,38 @@ const TalentDetail: React.FC<TalentDetailProps> = () => {
                         {talentDetail.status || '未知'}
                       </Text>
                     </View>
-                    <View className="flex justify-between">
-                      <Text className="text-gray-500">岗位类型</Text>
-                      <Text className="text-gray-800">{workInfo?.position || '未指定'}</Text>
-                    </View>
                     <View className="flex justify-between">
                       <Text className="text-gray-500">所属订单</Text>
                       <Text className="text-gray-800">{workInfo?.orderId ? `订单 #${workInfo.orderId}` : '无'}</Text>
                     </View>
+                    <View className="flex justify-between">
+                      <Text className="text-gray-500">岗位类型</Text>
+                      <Text className="text-gray-800">{workInfo?.position || '未指定'}</Text>
+                    </View>
                   </View>
                 </View>
 
-                {/* 薪资信息卡片 */}
+                {/* 薪资信息卡片 - 按原型简化:当前月薪和薪资历史按钮 */}
                 <View className="card bg-white p-4 mb-4">
                   <Text className="font-semibold text-gray-700 mb-3">薪资信息</Text>
-                  <View className="space-y-3 text-sm">
-                    <View className="flex justify-between">
-                      <Text className="text-gray-500">当前薪资</Text>
-                      <Text className="text-gray-800 font-semibold">{formatCurrency(talentDetail.salary)}</Text>
-                    </View>
-                    <View className="flex justify-between">
-                      <Text className="text-gray-500">薪资结构</Text>
-                      <Text className="text-gray-800">{salaryInfo?.type || '月薪'}</Text>
-                    </View>
-                    <View className="flex justify-between">
-                      <Text className="text-gray-500">发薪日</Text>
-                      <Text className="text-gray-800">{salaryInfo?.paymentDate ? formatDate(salaryInfo.paymentDate) : '每月底'}</Text>
+                  <View className="flex justify-between items-center">
+                    <View>
+                      <Text className="text-gray-500">当前月薪</Text>
+                      <Text className="text-2xl font-bold text-blue-600">{formatCurrency(salaryInfo?.amount || 0)}</Text>
                     </View>
-                    <View className="flex justify-between">
-                      <Text className="text-gray-500">薪资周期</Text>
-                      <Text className="text-gray-800">{salaryInfo?.period || '月度'}</Text>
+                    <View
+                      className="text-blue-500 text-sm flex items-center"
+                      onClick={() => {
+                        // 滚动到薪资历史记录卡片
+                        // 可以在薪资历史记录卡片添加data-testid或id来定位
+                        Taro.showToast({
+                          title: '查看薪资历史',
+                          icon: 'none'
+                        });
+                      }}
+                    >
+                      <Text className="i-heroicons-clock-20-solid mr-1" />
+                      <Text>薪资历史</Text>
                     </View>
                   </View>
                 </View>
@@ -450,9 +448,13 @@ const TalentDetail: React.FC<TalentDetailProps> = () => {
                         const salary = work.个人薪资 ? `¥${work.个人薪资.toLocaleString()}` : '未指定'
 
                         return (
-                          <View key={work.订单ID || index} className="flex items-start">
-                            {/* 时间线圆点 */}
-                            <View className={`w-3 h-3 rounded-full mt-1 mr-3 ${isCurrent ? 'bg-blue-500' : 'bg-gray-300'}`} />
+                          <View key={work.订单ID || index} className="flex">
+                            <View className="flex flex-col items-center mr-3">
+                              <View className={`w-3 h-3 rounded-full ${isCurrent ? 'bg-blue-500' : 'bg-gray-400'}`} />
+                              {index !== workHistoryFull.length - 1 && (
+                                <View className={`w-0.5 h-full mt-1 ${isCurrent ? 'bg-blue-200' : 'bg-gray-200'}`} />
+                              )}
+                            </View>
                             <View className="flex-1">
                               {/* 公司/订单名称 */}
                               <Text className="font-medium text-gray-800 text-sm">
@@ -619,6 +621,45 @@ const TalentDetail: React.FC<TalentDetailProps> = () => {
                     </View>
                   )}
                 </View>
+
+                {/* 操作按钮 - 原型第831-839行 */}
+                <View className="flex space-x-3 mt-4">
+                  {/* 联系按钮 */}
+                  <View
+                    className="flex-1 bg-blue-500 text-white py-3 rounded-lg font-medium flex items-center justify-center active:bg-blue-600"
+                    onClick={() => {
+                      // 联系功能:拨打电话或跳转
+                      if (talentDetail.phone) {
+                        Taro.makePhoneCall({
+                          phoneNumber: talentDetail.phone
+                        })
+                      } else {
+                        Taro.showToast({
+                          title: '暂无联系电话',
+                          icon: 'none'
+                        })
+                      }
+                    }}
+                  >
+                    <Text className="i-heroicons-phone-20-solid mr-2" />
+                    <Text>联系</Text>
+                  </View>
+
+                  {/* 编辑按钮 */}
+                  <View
+                    className="flex-1 bg-gray-100 text-gray-800 py-3 rounded-lg font-medium flex items-center justify-center active:bg-gray-200"
+                    onClick={() => {
+                      // 编辑功能:跳转编辑页面
+                      Taro.navigateTo({
+                        url: `/pages/yongren/talent/edit?id=${talentId}`
+                      })
+                    }}
+                  >
+                    <Text className="i-heroicons-pencil-square-20-solid mr-2" />
+                    <Text>编辑</Text>
+                  </View>
+                </View>
+
               </View>
             </>
           ) : (
@@ -630,47 +671,6 @@ const TalentDetail: React.FC<TalentDetailProps> = () => {
           )}
         </ScrollView>
 
-        {/* 底部操作按钮区域 - 原型第831-839行 */}
-        {talentDetail && !isLoading && !hasError && (
-          <View className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-200">
-            <View className="flex space-x-3">
-              {/* 联系按钮 */}
-              <View
-                className="flex-1 bg-blue-500 text-white py-3 rounded-lg font-medium flex items-center justify-center active:bg-blue-600"
-                onClick={() => {
-                  // 联系功能:拨打电话或跳转
-                  if (talentDetail.phone) {
-                    Taro.makePhoneCall({
-                      phoneNumber: talentDetail.phone
-                    })
-                  } else {
-                    Taro.showToast({
-                      title: '暂无联系电话',
-                      icon: 'none'
-                    })
-                  }
-                }}
-              >
-                <Text className="i-heroicons-phone-20-solid mr-2" />
-                <Text>联系</Text>
-              </View>
-
-              {/* 编辑按钮 */}
-              <View
-                className="flex-1 bg-gray-100 text-gray-800 py-3 rounded-lg font-medium flex items-center justify-center active:bg-gray-200"
-                onClick={() => {
-                  // 编辑功能:跳转编辑页面
-                  Taro.navigateTo({
-                    url: `/pages/yongren/talent/edit?id=${talentId}`
-                  })
-                }}
-              >
-                <Text className="i-heroicons-pencil-square-20-solid mr-2" />
-                <Text>编辑</Text>
-              </View>
-            </View>
-          </View>
-        )}
       </PageContainer>
     </YongrenTabBarLayout>
   )

+ 27 - 2
mini-ui-packages/yongren-talent-management-ui/src/pages/TalentManagement/TalentManagement.tsx

@@ -20,6 +20,8 @@ interface CompanyPersonListItem {
   disabilityType: string
   disabilityLevel: string
   phone: string | null
+  birthDate: string | null
+  salaryDetail: number | null
   jobStatus: string
   latestJoinDate: string | null
   orderName: string | null
@@ -151,6 +153,29 @@ const TalentManagement: React.FC<TalentManagementProps> = () => {
     return colors[index]
   }
 
+  // 计算年龄基于出生日期
+  const calculateAge = (birthDate: string | null): string => {
+    if (!birthDate) return '未知岁'
+    try {
+      const birth = new Date(birthDate)
+      const today = new Date()
+      let age = today.getFullYear() - birth.getFullYear()
+      const monthDiff = today.getMonth() - birth.getMonth()
+      if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
+        age--
+      }
+      return `${age}岁`
+    } catch (error) {
+      return '未知岁'
+    }
+  }
+
+  // 格式化薪资显示
+  const formatSalary = (salaryDetail: number | null): string => {
+    if (!salaryDetail || salaryDetail <= 0) return '待定'
+    return `¥${salaryDetail.toLocaleString()}`
+  }
+
   return (
     <YongrenTabBarLayout activeKey="talent">
       <PageContainer padding={false} className="pb-0">
@@ -268,7 +293,7 @@ const TalentManagement: React.FC<TalentManagementProps> = () => {
                         <View>
                           <Text className="font-semibold text-gray-800">{talent.name}</Text>
                           <Text className="text-xs text-gray-500">
-                            {talent.disabilityType || '未指定'} · {talent.disabilityLevel || '未分级'} · {talent.gender} · 未知岁
+                            {talent.disabilityType || '未指定'} · {talent.disabilityLevel || '未分级'} · {talent.gender} · {calculateAge(talent.birthDate)}
                           </Text>
                         </View>
                         <Text className={`text-xs px-2 py-1 rounded-full ${
@@ -283,7 +308,7 @@ const TalentManagement: React.FC<TalentManagementProps> = () => {
                       </View>
                       <View className="mt-2 flex justify-between text-xs text-gray-500">
                         <Text>入职: {talent.latestJoinDate ? new Date(talent.latestJoinDate).toLocaleDateString() : '未入职'}</Text>
-                        <Text>薪资: 待定</Text>
+                        <Text>薪资: {formatSalary(talent.salaryDetail)}</Text>
                       </View>
                     </View>
                   </View>