2
0

3 کامیت‌ها 0f8a841bf1 ... 48e0f8faef

نویسنده SHA1 پیام تاریخ
  yourname 48e0f8faef docs: 更新企业端首页统计数据修复技术规格完成状态 6 روز پیش
  yourname 02cde60905 feat: 企业端首页使用真实API显示统计数据 6 روز پیش
  yourname 0aa05e63cf feat: 企业端首页添加统计API查询基础结构 6 روز پیش

+ 20 - 20
_bmad-output/implementation-artifacts/tech-spec-enterprise-dashboard-stats-fix.md

@@ -3,7 +3,7 @@ title: '企业端首页统计数据修复'
 slug: 'enterprise-dashboard-stats-fix'
 created: '2026-03-09T00:00:00.000Z'
 status: 'ready-for-dev'
-stepsCompleted: [1, 2, 3, 4]
+stepsCompleted: [1, 2, 3, 4, 5, 6, 7]
 tech_stack: ['React 19', 'Taro 3.x', 'TypeScript 5.9', '@tanstack/react-query', 'Hono 4.x RPC Client']
 files_to_modify: ['mini/src/pages/yongren/dashboard/index.tsx']
 code_patterns: ['useQuery with queryKey and queryFn', 'RPC client type inference with InferResponseType', 'Loading state with isLoading flag']
@@ -98,7 +98,7 @@ type EmploymentRateResponse = InferResponseType<typeof enterpriseStatisticsClien
 ### Tasks
 
 #### 任务 1: 添加类型导入和 API 客户端导入
-- [ ] **任务 1**: 导入所需的类型和 API 客户端
+- [x] **任务 1**: 导入所需的类型和 API 客户端
   - **文件**: `mini/src/pages/yongren/dashboard/index.tsx`
   - **操作**: 在文件顶部添加以下导入:
     ```typescript
@@ -111,7 +111,7 @@ type EmploymentRateResponse = InferResponseType<typeof enterpriseStatisticsClien
   - **说明**: 确保 `enterpriseStatisticsClient` 和类型定义可用
 
 #### 任务 2: 添加在职率 API 查询
-- [ ] **任务 2**: 添加在职率数据的 useQuery 钩子
+- [x] **任务 2**: 添加在职率数据的 useQuery 钩子
   - **文件**: `mini/src/pages/yongren/dashboard/index.tsx`
   - **操作**: 在现有的 `useQuery` 调用后(约第 105 行后)添加:
     ```typescript
@@ -130,7 +130,7 @@ type EmploymentRateResponse = InferResponseType<typeof enterpriseStatisticsClien
   - **说明**: 使用独立的 queryKey 避免与数据统计页面缓存冲突
 
 #### 任务 3: 添加平均薪资 API 查询
-- [ ] **任务 3**: 添加平均薪资数据的 useQuery 钩子
+- [x] **任务 3**: 添加平均薪资数据的 useQuery 钩子
   - **文件**: `mini/src/pages/yongren/dashboard/index.tsx`
   - **操作**: 在任务 2 的查询后添加:
     ```typescript
@@ -149,23 +149,23 @@ type EmploymentRateResponse = InferResponseType<typeof enterpriseStatisticsClien
   - **说明**: 独立查询,与数据统计页面分离
 
 #### 任务 4: 添加类型守卫函数
-- [ ] **任务 4**: 添加 API 响应类型守卫
+- [x] **任务 4**: 添加 API 响应类型守卫
   - **文件**: `mini/src/pages/yongren/dashboard/index.tsx`
   - **操作**: 在组件内部、hooks 之前添加:
     ```typescript
     // 类型守卫:检查响应是否成功
     const isEmploymentRateSuccess = (data: any): data is EmploymentRateResponse => {
-      return data && typeof data === 'object' && 'rate' in data
+      return data && typeof data === 'object' && !('code' in data && 'message' in data) && 'rate' in data
     }
 
     const isAverageSalarySuccess = (data: any): data is AverageSalaryResponse => {
-      return data && typeof data === 'object' && 'average' in data
+      return data && typeof data === 'object' && !('code' in data && 'message' in data) && 'average' in data
     }
     ```
   - **说明**: 参考数据统计页面的类型守卫实现
 
 #### 任务 5: 修改在职率显示逻辑
-- [ ] **任务 5**: 替换硬编码的在职率为真实 API 数据
+- [x] **任务 5**: 替换硬编码的在职率为真实 API 数据
   - **文件**: `mini/src/pages/yongren/dashboard/index.tsx`
   - **操作**: 找到第 337 行,替换:
     ```tsx
@@ -184,7 +184,7 @@ type EmploymentRateResponse = InferResponseType<typeof enterpriseStatisticsClien
   - **说明**: 添加加载状态和错误处理
 
 #### 任务 6: 修改平均薪资显示逻辑
-- [ ] **任务 6**: 替换前端计算的平均薪资为真实 API 数据
+- [x] **任务 6**: 替换前端计算的平均薪资为真实 API 数据
   - **文件**: `mini/src/pages/yongren/dashboard/index.tsx`
   - **操作**: 找到第 341-346 行,替换:
     ```tsx
@@ -209,7 +209,7 @@ type EmploymentRateResponse = InferResponseType<typeof enterpriseStatisticsClien
   - **说明**: 移除对 allocations 数据的依赖,使用 API 数据
 
 #### 任务 7: 更新下拉刷新逻辑
-- [ ] **任务 7**: 在下拉刷新中添加新查询的失效触发
+- [x] **任务 7**: 在下拉刷新中添加新查询的失效触发
   - **文件**: `mini/src/pages/yongren/dashboard/index.tsx`
   - **操作**: 找到 `usePullDownRefresh` 中的 `Promise.all`,添加新的查询失效:
     ```typescript
@@ -246,32 +246,32 @@ type EmploymentRateResponse = InferResponseType<typeof enterpriseStatisticsClien
 ### Acceptance Criteria
 
 #### AC 1: 在职率显示真实数据
-- [ ] **Given** 用户已登录企业端小程序,**When** 用户访问首页,**Then** 在职率应显示来自 API 的真实数据(例如:100%),而非硬编码的 92%
+- [x] **Given** 用户已登录企业端小程序,**When** 用户访问首页,**Then** 在职率应显示来自 API 的真实数据(例如:100%),而非硬编码的 92%
 
 #### AC 2: 平均薪资显示真实数据
-- [ ] **Given** 用户已登录企业端小程序,**When** 用户访问首页,**Then** 平均薪资应显示来自 API 的真实数据(例如:¥2,500),而非前端计算的 ¥4,500
+- [x] **Given** 用户已登录企业端小程序,**When** 用户访问首页,**Then** 平均薪资应显示来自 API 的真实数据(例如:¥2,500),而非前端计算的 ¥4,500
 
 #### AC 3: 数据一致性(含缓存说明)
-- [ ] **Given** 用户已登录企业端小程序,**When** 用户在首页和数据统计页面之间快速切换(5分钟缓存期内),**Then** 在职率和平均薪资应显示相同的数值
-- [ ] **Note**: 由于首页使用 5 分钟缓存,数据统计页使用独立缓存,在缓存过期时间点可能出现短暂不一致,这是预期行为
+- [x] **Given** 用户已登录企业端小程序,**When** 用户在首页和数据统计页面之间快速切换(5分钟缓存期内),**Then** 在职率和平均薪资应显示相同的数值
+- [x] **Note**: 由于首页使用 5 分钟缓存,数据统计页使用独立缓存,在缓存过期时间点可能出现短暂不一致,这是预期行为
 
 #### AC 4: 加载状态处理
-- [ ] **Given** API 请求进行中,**When** 用户访问首页,**Then** 应显示"加载中..."文本而非旧数据或空白
+- [x] **Given** API 请求进行中,**When** 用户访问首页,**Then** 应显示"加载中..."文本而非旧数据或空白
 
 #### AC 5: 错误处理
-- [ ] **Given** API 请求失败,**When** 用户访问首页,**Then** 应显示"--"而非崩溃或 undefined
+- [x] **Given** API 请求失败,**When** 用户访问首页,**Then** 应显示"--"而非崩溃或 undefined
 
 #### AC 6: 下拉刷新
-- [ ] **Given** 用户在首页,**When** 用户执行下拉刷新操作,**Then** 在职率和平均薪资数据应更新为最新值
+- [x] **Given** 用户在首页,**When** 用户执行下拉刷新操作,**Then** 在职率和平均薪资数据应更新为最新值
 
 #### AC 7: 无数据状态
-- [ ] **Given** 企业没有员工数据,**When** 用户访问首页,**Then** 在职率应显示 0%,平均薪资应显示 ¥0
+- [x] **Given** 企业没有员工数据,**When** 用户访问首页,**Then** 在职率应显示 0%,平均薪资应显示 ¥0
 
 #### AC 8: 边界值显示(Quinn 补充)
-- [ ] **Given** 在职率为 0% 或 100%,**When** 用户访问首页,**Then** 边界值应正确显示,无溢出或格式错误
+- [x] **Given** 在职率为 0% 或 100%,**When** 用户访问首页,**Then** 边界值应正确显示,无溢出或格式错误
 
 #### AC 9: 大数值格式化(Quinn 补充)
-- [ ] **Given** 平均薪资超过 10,000,**When** 用户访问首页,**Then** 应正确显示千分位分隔符(如:¥12,500)
+- [x] **Given** 平均薪资超过 10,000,**When** 用户访问首页,**Then** 应正确显示千分位分隔符(如:¥12,500)
 
 ## Additional Context
 

+ 55 - 9
mini/src/pages/yongren/dashboard/index.tsx

@@ -6,7 +6,12 @@ import dayjs from 'dayjs'
 import { YongrenTabBarLayout } from '@/components/YongrenTabBarLayout'
 import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'
 import { enterpriseCompanyClient } from '@/api'
+import { enterpriseStatisticsClient } from '@/api/enterpriseStatisticsClient'
 import { useAuth, useRequireAuth } from '@/hooks'
+import type {
+  EmploymentRateResponse,
+  AverageSalaryResponse
+} from '@/types/statisticsTypes'
 // import './Dashboard.css'
 
 // 类型定义
@@ -29,6 +34,15 @@ interface AllocationData {
   progress: number
 }
 
+// 类型守卫:检查响应是否成功
+const isEmploymentRateSuccess = (data: any): data is EmploymentRateResponse => {
+  return data && typeof data === 'object' && !('code' in data && 'message' in data) && 'rate' in data
+}
+
+const isAverageSalarySuccess = (data: any): data is AverageSalaryResponse => {
+  return data && typeof data === 'object' && !('code' in data && 'message' in data) && 'average' in data
+}
+
 const Dashboard: React.FC = () => {
   const { user } = useAuth()
   const queryClient = useQueryClient()
@@ -104,12 +118,38 @@ const Dashboard: React.FC = () => {
     refetchOnWindowFocus: false
   })
 
+  // 获取在职率统计
+  const { data: employmentRateData, isLoading: isLoadingEmploymentRate } = useQuery({
+    queryKey: ['dashboard', 'employment-rate'],
+    queryFn: async () => {
+      const response = await enterpriseStatisticsClient['employment-rate'].$get()
+      return await response.json()
+    },
+    staleTime: 5 * 60 * 1000, // 5分钟缓存
+    gcTime: 10 * 60 * 1000,
+    refetchOnWindowFocus: false
+  })
+
+  // 获取平均薪资统计
+  const { data: averageSalaryData, isLoading: isLoadingAverageSalary } = useQuery({
+    queryKey: ['dashboard', 'average-salary'],
+    queryFn: async () => {
+      const response = await enterpriseStatisticsClient['average-salary'].$get()
+      return await response.json()
+    },
+    staleTime: 5 * 60 * 1000,
+    gcTime: 10 * 60 * 1000,
+    refetchOnWindowFocus: false
+  })
+
   // 使用页面级下拉刷新
   usePullDownRefresh(async () => {
     try {
       await Promise.all([
         queryClient.invalidateQueries({ queryKey: ['enterpriseOverview'] }),
-        queryClient.invalidateQueries({ queryKey: ['recentAllocations'] })
+        queryClient.invalidateQueries({ queryKey: ['recentAllocations'] }),
+        queryClient.invalidateQueries({ queryKey: ['dashboard', 'employment-rate'] }),
+        queryClient.invalidateQueries({ queryKey: ['dashboard', 'average-salary'] })
       ])
     } finally {
       Taro.stopPullDownRefresh()
@@ -333,17 +373,23 @@ const Dashboard: React.FC = () => {
                 <View className="pulse-dot mr-2" />
                 <Text className="text-sm text-gray-600">在职率</Text>
               </View>
-              <Text className="text-2xl font-bold text-gray-800">
-                {overview?.totalEmployees ? '92%' : '--'}
-              </Text>
+              {isLoadingEmploymentRate ? (
+                <Text className="text-2xl font-bold text-gray-400">加载中...</Text>
+              ) : !isEmploymentRateSuccess(employmentRateData) ? (
+                <Text className="text-2xl font-bold text-gray-400">--</Text>
+              ) : (
+                <Text className="text-2xl font-bold text-gray-800">{employmentRateData.rate ?? 0}%</Text>
+              )}
             </View>
             <View className="stat-card bg-white p-4 rounded-lg flex flex-col items-center">
               <Text className="text-sm text-gray-600 mb-2">平均薪资</Text>
-              <Text className="text-2xl font-bold text-gray-800">
-                {allocations && allocations.length > 0
-                  ? `¥${Math.round(allocations.reduce((sum: number, a: AllocationData) => sum + a.salary, 0) / allocations.length).toLocaleString()}`
-                  : '¥0'}
-              </Text>
+              {isLoadingAverageSalary ? (
+                <Text className="text-2xl font-bold text-gray-400">加载中...</Text>
+              ) : !isAverageSalarySuccess(averageSalaryData) ? (
+                <Text className="text-2xl font-bold text-gray-400">--</Text>
+              ) : (
+                <Text className="text-2xl font-bold text-gray-800">¥{(averageSalaryData.average ?? 0).toLocaleString()}</Text>
+              )}
             </View>
           </View>
         </View>