Sfoglia il codice sorgente

✨ feat(order-detail): 重构订单详情页数据流并集成关联人才数据

- 使用React Query管理所有子状态(订单详情、关联人才、视频、统计数据),实现并行数据获取
- 从企业订单详情API正确获取并显示关联人才数据,解决人才显示为空的问题
- 优化日期格式转换和数据类型映射,确保UI数据一致性
- 实现组合加载状态和错误处理,提升用户体验
- 更新故事文档状态为"Ready for Review"并添加重构记录

📝 docs(story): 更新故事状态和开发记录

- 将故事011.004状态从"In Progress"更新为"Ready for Review"
- 添加重构订单详情页数据流的开发记录(版本1.11)
- 更新完成笔记列表,包含React Query数据流重构的详细说明
- 更新数据模拟状态说明:关联人才数据已集成真实API,视频和统计数据仍使用模拟数据

✅ test(taro-mock): 增强Taro模拟器功能

- 为Taro.useRouter添加模拟实现,返回包含订单ID的params对象
- 确保订单详情页测试能够正确获取路由参数
yourname 3 settimane fa
parent
commit
d61a53908e

+ 10 - 2
docs/stories/011.004.story.md

@@ -1,7 +1,7 @@
 # 故事 011.004:订单管理功能实现
 
 ## 状态
-In Progress
+Ready for Review
 
 ## 故事
 **作为**企业用户,
@@ -415,6 +415,7 @@ In Progress
 | 2025-12-22 | 1.8 | 验证订单详情页API调用与企业专用订单API实现的匹配性,优化订单ID获取方式(Taro.useRouter),更新调试日志和发现的问题部分 | Claude Code |
 | 2025-12-22 | 1.9 | 优化订单详情页样式和Taro适配:统一按钮样式(Button→View)、修复Text组件垂直排列(flex flex-col)、修复API类型错误、通过类型检查 | James (Developer) |
 | 2025-12-22 | 1.10 | 更新故事:由于用人小程序仅用于查看,明确订单详情页只用于查看,写操作只在管理后台执行 | James (Developer) |
+| 2025-12-22 | 1.11 | 重构订单详情页数据流:使用React Query管理所有子状态,实现并行数据获取和更好的错误处理 | James (Developer) |
 ## 开发代理记录
 
 ### 使用的代理模型
@@ -433,6 +434,7 @@ claude-sonnet
 - 优化订单详情页样式和Taro适配:将所有Button组件替换为View组件实现纯文本按钮样式,为包含多个Text组件的View容器添加flex flex-col确保垂直排列,修复API数据类型映射问题,通过类型检查
 - 移除订单详情页写操作功能:根据故事更新,移除状态变更、添加备注、编辑订单等写操作功能,订单详情页现在仅用于查看,所有写操作只在管理后台执行
 - 移除订单详情页操作日志卡片:检查数据库实体结构,发现没有订单操作日志表,已移除订单详情页中的操作日志卡片
+- 重构订单详情页数据流:使用React Query管理所有子状态(人才、视频、统计数据),实现并行数据获取和更好的错误处理,解决关联人才显示为空的问题
 
 ### 完成笔记列表
 - ✅ 检查故事011.004代码实现完成情况:
@@ -524,12 +526,18 @@ claude-sonnet
   - 修复API数据类型映射问题,使用可选链操作符和空值合并运算符确保类型安全
   - 更新`getStatusLabel`和`getStatusClass`函数支持`undefined`参数
   - 验证类型检查:`pnpm typecheck`通过,无TypeScript错误
+- ✅ 重构订单详情页数据流:
+  - 使用React Query管理所有子状态(订单详情、关联人才、视频、统计数据),实现并行数据获取
+  - 创建独立的查询函数和接口定义,提供更好的类型安全
+  - 实现组合加载状态和错误处理,优化用户体验
+  - 解决关联人才显示为空的问题,从企业订单详情API正确获取并显示人才数据
+  - 验证类型检查通过,组件逻辑完整
 
 ### 发现的问题
 1. **JSX语法错误**:OrderList.tsx中存在括号不匹配错误,导致TypeScript类型检查失败,需要修复JSX结构
    - ⏳ 类型检查已通过,但仍建议检查第265-350行JSX结构以确保代码质量
 2. **API集成缺失**:✅ 已实现企业专用订单API客户端`enterpriseOrderClient.ts`,使用`enterpriseOrderRoutes`类型,订单列表和详情组件已集成使用
-3. **数据模拟状态**:⏳ 部分完成 - 订单列表和详情组件已连接企业专用订单API获取核心订单数据,但关联人才、视频、统计数据仍使用模拟数据
+3. **数据模拟状态**:⏳ 部分完成 - 订单列表和详情组件已连接企业专用订单API获取核心订单数据和关联人才,但视频、统计数据仍使用模拟数据
 4. **原型对照检查**:需要对照原型文件 `docs/小程序原型/yongren.html` 第1114-1301行进行详细UI验证
    - ✅ 订单列表页对照检查已完成:修复订单信息网格字段数量问题(6字段→4字段),与原型设计完全一致
    - ⏳ 订单详情页对照检查待完成:原型文件中没有单独的订单详情页面,需要基于人才详情页面设计风格创建

+ 124 - 37
mini-ui-packages/yongren-order-management-ui/src/pages/OrderDetail/OrderDetail.tsx

@@ -24,6 +24,17 @@ interface OrderDetailData {
   actualStartDate: string
   expectedEndDate: string
   actualEndDate: string | null
+  orderPersons?: Array<{
+    id: number
+    workStatus: string
+    joinDate: string
+    person?: {
+      name: string
+      gender: string
+      disabilityType: string
+      phone: string
+    }
+  }>
 }
 
 const OrderDetail: React.FC = () => {
@@ -31,11 +42,6 @@ const OrderDetail: React.FC = () => {
   const orderIdParam = router.params.id ? parseInt(router.params.id) : null
   const orderId = orderIdParam && !Number.isNaN(orderIdParam) ? orderIdParam : null
 
-  const [persons, setPersons] = useState<any[]>([])
-  const [videos, setVideos] = useState<any[]>([])
-  const [checkinStats, setCheckinStats] = useState({ current: 0, total: 0, percentage: 0 })
-  const [salaryVideoStats, setSalaryVideoStats] = useState({ current: 0, total: 0, percentage: 0 })
-  const [taxVideoStats, setTaxVideoStats] = useState({ current: 0, total: 0, percentage: 0 })
 
   // 获取订单详情查询函数
   const fetchOrderDetailQuery = async (id: number) => {
@@ -52,6 +58,7 @@ const OrderDetail: React.FC = () => {
       throw new Error('API返回数据为空')
     }
     // 转换API数据到UI格式
+    const orderPersons = data?.orderPersons || []
     return {
       id: data?.id ?? 0,
       orderNumber: data?.orderName ? `ORDER-${data.id}` : `ORDER-${data?.id ?? 0}`,
@@ -65,11 +72,44 @@ const OrderDetail: React.FC = () => {
       platform: data?.platformId ? `平台${data.platformId}` : '未知平台',
       channel: '未知渠道', // data?.channelName 不存在
       expectedPeople: 0, // data?.expectedPersonCount 不存在
-      actualPeople: 0, // data?.actualPersonCount 不存在
-      expectedStartDate: '未设置', // data?.expectedStartDate 不存在
-      actualStartDate: '未设置', // data?.actualStartDate 不存在
+      actualPeople: orderPersons.length,
+      expectedStartDate: data?.expectedStartDate ? new Date(data.expectedStartDate).toISOString().split('T')[0] : '未设置',
+      actualStartDate: data?.actualStartDate ? new Date(data.actualStartDate).toISOString().split('T')[0] : '未设置',
       expectedEndDate: '未设置', // data?.expectedEndDate 不存在
-      actualEndDate: null,
+      actualEndDate: data?.actualEndDate ? new Date(data.actualEndDate).toISOString().split('T')[0] : null,
+      orderPersons: orderPersons.map((op: any) => ({
+        id: op.id,
+        workStatus: op.workStatus || 'unknown',
+        joinDate: op.joinDate ? new Date(op.joinDate).toISOString().split('T')[0] : '未知日期',
+        person: op.person ? {
+          name: op.person.name || '未知姓名',
+          gender: op.person.gender || '未知',
+          disabilityType: op.person.disabilityType || '未知类型',
+          phone: op.person.phone || '未知电话'
+        } : undefined
+      }))
+    }
+  }
+
+  // 获取订单视频查询函数(模拟数据)
+  const fetchOrderVideosQuery = async (orderId: number) => {
+    // TODO: 集成真实API,使用 enterpriseOrderClient['company-videos'].$get
+    // 暂时返回模拟数据
+    return [
+      { id: 1, name: '2024-01月打卡视频', type: 'checkin_video' as const, size: '15MB', uploadTime: '2024-01-15' },
+      { id: 2, name: '1月工资发放视频', type: 'salary_video' as const, size: '20MB', uploadTime: '2024-01-20' },
+      { id: 3, name: '个税申报视频', type: 'tax_video' as const, size: '12MB', uploadTime: '2024-01-25' },
+    ] as VideoItem[]
+  }
+
+  // 获取订单统计数据查询函数(模拟数据)
+  const fetchOrderStatisticsQuery = async (orderId: number) => {
+    // TODO: 集成真实API,使用 enterpriseOrderClient['checkin-statistics'].$get 和 ['video-statistics'].$get
+    // 暂时返回模拟数据
+    return {
+      checkinStats: { current: 24, total: 30, percentage: 80 },
+      salaryVideoStats: { current: 18, total: 30, percentage: 60 },
+      taxVideoStats: { current: 15, total: 30, percentage: 50 },
     }
   }
 
@@ -84,6 +124,44 @@ const OrderDetail: React.FC = () => {
     enabled: !!orderId,
   })
 
+  // 视频查询接口
+  interface VideoItem {
+    id: number
+    name: string
+    type: 'checkin_video' | 'salary_video' | 'tax_video'
+    size: string
+    uploadTime: string
+  }
+
+  // 统计数据接口
+  interface StatisticsData {
+    checkinStats: { current: number, total: number, percentage: number }
+    salaryVideoStats: { current: number, total: number, percentage: number }
+    taxVideoStats: { current: number, total: number, percentage: number }
+  }
+
+  // 使用React Query获取订单视频
+  const {
+    data: videos = [],
+    isLoading: videosLoading,
+    error: videosError,
+  } = useQuery<VideoItem[], Error>({
+    queryKey: ['order-videos', orderId],
+    queryFn: () => orderId ? fetchOrderVideosQuery(orderId) : Promise.reject(new Error('未找到订单ID')),
+    enabled: !!orderId,
+  })
+
+  // 使用React Query获取订单统计数据
+  const {
+    data: statistics,
+    isLoading: statisticsLoading,
+    error: statisticsError,
+  } = useQuery<StatisticsData, Error>({
+    queryKey: ['order-statistics', orderId],
+    queryFn: () => orderId ? fetchOrderStatisticsQuery(orderId) : Promise.reject(new Error('未找到订单ID')),
+    enabled: !!orderId,
+  })
+
   // 处理查询错误
   useEffect(() => {
     if (queryError) {
@@ -93,17 +171,26 @@ const OrderDetail: React.FC = () => {
         icon: 'none'
       })
     }
-  }, [queryError])
-
-  // 处理查询成功
-  useEffect(() => {
-    if (order) {
-      // TODO: 获取关联人才、视频、统计数据
-      // 暂时使用空数组
-      setPersons([])
-      setVideos([])
+    if (videosError) {
+      console.error('获取订单视频错误:', videosError)
+      Taro.showToast({
+        title: '获取视频数据失败',
+        icon: 'none'
+      })
     }
-  }, [order])
+    if (statisticsError) {
+      console.error('获取订单统计错误:', statisticsError)
+      Taro.showToast({
+        title: '获取统计数据失败',
+        icon: 'none'
+      })
+    }
+  }, [queryError, videosError, statisticsError])
+
+  // 组合加载状态和错误状态
+  const isLoadingAll = isLoading || videosLoading || statisticsLoading
+  const hasError = queryError || videosError || statisticsError
+  const errorMessage = queryError?.message || videosError?.message || statisticsError?.message || '加载数据失败'
 
   // 状态标签和样式辅助函数
   const getStatusLabel = (status: string | undefined) => {
@@ -157,13 +244,13 @@ const OrderDetail: React.FC = () => {
         className="h-screen overflow-y-auto px-4 pb-4 pt-0"
         scrollY
       >
-        {isLoading ? (
+        {isLoadingAll ? (
           <View className="flex items-center justify-center h-64">
             <Text className="text-gray-500">加载中...</Text>
           </View>
-        ) : queryError ? (
+        ) : hasError ? (
           <View className="flex items-center justify-center h-64">
-            <Text className="text-red-500">{queryError.message}</Text>
+            <Text className="text-red-500">{errorMessage}</Text>
           </View>
         ) : !order ? (
           <View className="flex items-center justify-center h-64">
@@ -245,23 +332,23 @@ const OrderDetail: React.FC = () => {
             <View className="bg-blue-50 rounded-lg p-2 text-center flex flex-col">
               <Text className="text-xs text-gray-600">本月打卡</Text>
               <Text className="text-sm font-bold text-gray-800">
-                {checkinStats.current}/{checkinStats.total}
+                {statistics?.checkinStats.current || 0}/{statistics?.checkinStats.total || 0}
               </Text>
-              <Text className="text-xs text-gray-500">{checkinStats.percentage}%</Text>
+              <Text className="text-xs text-gray-500">{statistics?.checkinStats.percentage || 0}%</Text>
             </View>
             <View className="bg-green-50 rounded-lg p-2 text-center flex flex-col">
               <Text className="text-xs text-gray-600">工资视频</Text>
               <Text className="text-sm font-bold text-gray-800">
-                {salaryVideoStats.current}/{salaryVideoStats.total}
+                {statistics?.salaryVideoStats.current || 0}/{statistics?.salaryVideoStats.total || 0}
               </Text>
-              <Text className="text-xs text-gray-500">{salaryVideoStats.percentage}%</Text>
+              <Text className="text-xs text-gray-500">{statistics?.salaryVideoStats.percentage || 0}%</Text>
             </View>
             <View className="bg-purple-50 rounded-lg p-2 text-center flex flex-col">
               <Text className="text-xs text-gray-600">个税视频</Text>
               <Text className="text-sm font-bold text-gray-800">
-                {taxVideoStats.current}/{taxVideoStats.total}
+                {statistics?.taxVideoStats.current || 0}/{statistics?.taxVideoStats.total || 0}
               </Text>
-              <Text className="text-xs text-gray-500">{taxVideoStats.percentage}%</Text>
+              <Text className="text-xs text-gray-500">{statistics?.taxVideoStats.percentage || 0}%</Text>
             </View>
           </View>
           <View className="flex items-center text-blue-500 text-sm" onClick={() => console.log('查看详细打卡记录')}>
@@ -275,21 +362,21 @@ const OrderDetail: React.FC = () => {
             <Text className="font-semibold text-gray-700">关联人才</Text>
           </View>
           <View className="space-y-3">
-            {persons.map((person) => (
-              <View key={person.id} className="flex justify-between items-center border-b border-gray-100 pb-2">
+            {order?.orderPersons?.map((orderPerson) => (
+              <View key={orderPerson.id} className="flex justify-between items-center border-b border-gray-100 pb-2">
                 <View className="flex flex-col">
-                  <Text className="font-medium text-gray-800">{person.name}</Text>
+                  <Text className="font-medium text-gray-800">{orderPerson.person?.name || '未知姓名'}</Text>
                   <Text className="text-xs text-gray-500">
-                    {person.gender} · {person.disabilityType} · 入职: {person.joinDate}
+                    {orderPerson.person?.gender || '未知'} · {orderPerson.person?.disabilityType || '未知类型'} · 入职: {orderPerson.joinDate}
                   </Text>
                 </View>
                 <Text className={`text-xs px-2 py-1 rounded-full ${
-                  person.workStatus === 'working' ? 'bg-green-100 text-green-800' :
-                  person.workStatus === 'pre_working' ? 'bg-yellow-100 text-yellow-800' :
+                  orderPerson.workStatus === 'working' ? 'bg-green-100 text-green-800' :
+                  orderPerson.workStatus === 'pre_working' ? 'bg-yellow-100 text-yellow-800' :
                   'bg-gray-100 text-gray-800'
                 }`}>
-                  {person.workStatus === 'working' ? '已就业' :
-                   person.workStatus === 'pre_working' ? '待就业' : '未就业'}
+                  {orderPerson.workStatus === 'working' ? '已就业' :
+                   orderPerson.workStatus === 'pre_working' ? '待就业' : '未就业'}
                 </Text>
               </View>
             ))}
@@ -305,7 +392,7 @@ const OrderDetail: React.FC = () => {
             </View>
           </View>
           <View className="space-y-3">
-            {videos.map((video) => (
+            {(videos || []).map((video: VideoItem) => (
               <View key={video.id} className="flex justify-between items-center border-b border-gray-100 pb-2">
                 <View className="flex flex-col">
                   <Text className="font-medium text-gray-800">{video.name}</Text>

+ 1 - 0
mini-ui-packages/yongren-order-management-ui/tests/__mocks__/taroMock.ts

@@ -17,6 +17,7 @@ const Taro = {
   reLaunch: jest.fn(),
   getCurrentPages: jest.fn(() => []),
   request: jest.fn(),
+  useRouter: jest.fn(() => ({ params: { id: '1' } })),
 };
 
 export default Taro