2
0
Эх сурвалжийг харах

refactor(yongren-order-management-ui): 更新故事检查结果并用React Query重构订单详情数据获取

- 更新故事011.004检查结果:已验证订单详情页API调用与企业专用订单API实现完全匹配
- 重构OrderDetail组件:使用React Query的useQuery替代useState+useEffect组合,提供更好的缓存和错误处理
- 修复null读取错误:添加加载状态和错误处理,避免Uncaught TypeError: Cannot read properties of null (reading 'name')

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

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 3 долоо хоног өмнө
parent
commit
c5e3e28eb7

+ 15 - 16
docs/stories/011.004.story.md

@@ -422,6 +422,9 @@ claude-sonnet
 - 企业专用订单详情API集成测试已添加,验证5个测试场景全部通过
 - 原型对照检查:订单列表页订单信息网格字段修复(6字段→4字段),与原型设计一致,类型检查通过
 - 按钮组件样式优化:将所有Button组件替换为View组件,实现纯文本按钮样式,与原型设计完全一致
+- 修复OrderDetail组件中的Uncaught TypeError: Cannot read properties of null (reading 'name')错误,添加加载状态和错误处理
+- 使用React Query重构订单详情数据获取,用useQuery替代useState+useEffect组合,提供更好的缓存和错误处理
+- 检查订单详情页API调用与企业专用订单API实现的匹配性:订单详情页正确使用`enterpriseOrderClient.detail[':id'].$get`调用企业专用订单详情路由`GET /detail/{id}`,路径、方法、参数和认证中间件均匹配正确(基于故事012.014的路由分离实现)
 
 ### 完成笔记列表
 - ✅ 检查故事011.004代码实现完成情况:
@@ -495,6 +498,13 @@ claude-sonnet
   - 验证类型检查:`pnpm typecheck`通过,无TypeScript错误
   - 样式一致性:所有按钮现在均为纯文本样式(操作按钮)或自定义样式(搜索/新建按钮),与原型视觉一致
   - 遗留问题:右侧按钮动态逻辑仍需实现(根据订单状态显示下载视频/数据报告/编辑)
+- ✅ 修复OrderDetail组件null读取错误并使用React Query重构:
+  - 修复Uncaught TypeError: Cannot read properties of null (reading 'name')错误,添加加载状态和错误处理
+  - 使用React Query的useQuery替代useState+useEffect数据获取模式,提供更好的缓存、错误处理和重试机制
+  - 创建OrderDetailData接口,确保类型安全
+  - 添加useEffect处理查询成功和错误状态,保持toast通知功能
+  - 验证类型检查:`pnpm typecheck`通过,无TypeScript错误
+  - 遗留问题:测试需要更新以支持QueryClientProvider
 
 ### 发现的问题
 1. **JSX语法错误**:OrderList.tsx中存在括号不匹配错误,导致TypeScript类型检查失败,需要修复JSX结构
@@ -505,25 +515,14 @@ claude-sonnet
    - ✅ 订单列表页对照检查已完成:修复订单信息网格字段数量问题(6字段→4字段),与原型设计完全一致
    - ⏳ 订单详情页对照检查待完成:原型文件中没有单独的订单详情页面,需要基于人才详情页面设计风格创建
    - ⏳ 右侧按钮动态逻辑缺失:根据订单状态显示不同右侧按钮(进行中→下载视频、已完成→数据报告、待开始→编辑),当前固定显示"下载视频"
-5. **企业专用API使用**:需要确认使用企业专用订单API(`/api/v1/yongren/order`)而非通用订单API,确保数据安全隔离
+5. **企业专用API使用**:✅ 已验证订单详情页正确使用企业专用订单API(`/api/v1/yongren/order`)的`GET /detail/{id}`路由,数据安全隔离已验证(基于故事012.014的路由分离实现)
 6. **Taro小程序Text组件垂直排列**:可能需要为包含多个Text组件的View容器添加`flex flex-col`类,确保垂直排列符合原型设计
 
 ### 建议
-1. **集成真实的企业专用订单API客户端**:
-   - 在`mini-ui-packages/yongren-order-management-ui/src/api/`目录创建`enterpriseOrderClient.ts`文件
-   - 参考`@d8d/mini-enterprise-auth-ui`包的模式创建客户端:
-     ```typescript
-     // 故事012.014已实现,使用enterpriseOrderRoutes类型(仅包含企业专用路由)
-     // 原orderRoutes类型包含混合路由,已不适用于企业专用API客户端
-     import type { enterpriseOrderRoutes } from '@d8d/order-module';
-     import { rpcClient } from '@d8d/mini-shared-ui-components/utils/rpc/rpc-client';
-
-     // 注意:企业专用订单API通过enterpriseAuthMiddleware中间件保护,确保仅限企业用户访问
-     // 路径前缀 /api/v1/yongren/order 在路由层配置
-     export const enterpriseOrderClient = rpcClient<typeof enterpriseOrderRoutes>('/api/v1/yongren/order');
-     ```
-   - **注意**:企业专用订单API已通过史诗012在order-module中实现。故事012.014已提供`enterpriseOrderRoutes`类型(仅包含企业专用路由),实现更安全的类型检查。所有企业专用API(包括基础CRUD接口和扩展API)均通过`enterpriseAuthMiddleware`中间件保护,确保数据安全隔离。
-   - 更新`src/api/index.ts`导出新的客户端
+1. **集成真实的企业专用订单API客户端**:✅ 已实现
+   - ✅ `enterpriseOrderClient.ts`文件已创建,使用`enterpriseOrderRoutes`类型
+   - ✅ 订单列表和详情组件已导入并使用`enterpriseOrderClient`
+   - ✅ 企业专用订单详情路由`GET /detail/{id}`已通过故事012.014实现并验证
 2. **更新订单列表和详情组件**:
    - 在`OrderList.tsx`和`OrderDetail.tsx`中导入并使用`enterpriseOrderClient`
    - 替换硬编码的模拟数据,连接后端API获取真实数据

+ 101 - 58
mini-ui-packages/yongren-order-management-ui/src/pages/OrderDetail/OrderDetail.tsx

@@ -1,17 +1,35 @@
 import React, { useState, useEffect } from 'react'
 import { View, Text, ScrollView, Button, Input, Textarea } from '@tarojs/components'
 import Taro from '@tarojs/taro'
+import { useQuery } from '@tanstack/react-query'
 import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'
 import { enterpriseOrderClient } from '../../api'
 
 type OrderStatus = 'draft' | 'confirmed' | 'in_progress' | 'completed' | 'cancelled'
 
+interface OrderDetailData {
+  id: any
+  orderNumber: any
+  name: any
+  createdAt: string
+  updatedAt: string
+  status: any
+  statusLabel: string
+  statusClass: string
+  company: any
+  platform: any
+  channel: any
+  expectedPeople: any
+  actualPeople: any
+  expectedStartDate: string
+  actualStartDate: string
+  expectedEndDate: string
+  actualEndDate: string | null
+}
+
 const OrderDetail: React.FC = () => {
   const [orderStatus, setOrderStatus] = useState<OrderStatus>('in_progress')
   const [note, setNote] = useState('')
-  const [order, setOrder] = useState<any>(null)
-  const [loading, setLoading] = useState(false)
-  const [error, setError] = useState<string | null>(null)
   const [orderId, setOrderId] = useState<number | null>(null)
   const [persons, setPersons] = useState<any[]>([])
   const [videos, setVideos] = useState<any[]>([])
@@ -37,58 +55,71 @@ const OrderDetail: React.FC = () => {
     }
   }
 
-  // 获取订单详情
-  const fetchOrderDetail = async (id: number) => {
-    setLoading(true)
-    setError(null)
-    try {
-      const response = await enterpriseOrderClient.detail[':id'].$get({
-        param: { id: id }
-      })
+  // 获取订单详情查询函数
+  const fetchOrderDetailQuery = async (id: number) => {
+    const response = await enterpriseOrderClient.detail[':id'].$get({
+      param: { id: id }
+    })
+
+    if (!response.ok) {
+      throw new Error(`获取订单详情失败: ${response.status}`)
+    }
+
+    const data = await response.json() as any
+    // 转换API数据到UI格式
+    return {
+      id: data.id,
+      orderNumber: data.orderNumber || `ORDER-${data.id}`,
+      name: data.orderName || '未命名订单',
+      createdAt: data.createTime ? new Date(data.createTime).toISOString().split('T')[0] : '未知日期',
+      updatedAt: data.updateTime ? new Date(data.updateTime).toISOString().split('T')[0] : '未知日期',
+      status: data.orderStatus || 'draft',
+      statusLabel: getStatusLabel(data.orderStatus),
+      statusClass: getStatusClass(data.orderStatus),
+      company: data.companyName || '未知公司',
+      platform: data.platformName || '未知平台',
+      channel: data.channelName || '未知渠道',
+      expectedPeople: data.expectedPersonCount || 0,
+      actualPeople: data.actualPersonCount || 0,
+      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 ? new Date(data.expectedEndDate).toISOString().split('T')[0] : '未设置',
+      actualEndDate: data.actualEndDate ? new Date(data.actualEndDate).toISOString().split('T')[0] : null,
+    }
+  }
 
-      if (response.ok) {
-        const data = await response.json() as any
-        // 转换API数据到UI格式
-        const transformedOrder = {
-          id: data.id,
-          orderNumber: data.orderNumber || `ORDER-${data.id}`,
-          name: data.orderName || '未命名订单',
-          createdAt: data.createTime ? new Date(data.createTime).toISOString().split('T')[0] : '未知日期',
-          updatedAt: data.updateTime ? new Date(data.updateTime).toISOString().split('T')[0] : '未知日期',
-          status: data.orderStatus || 'draft',
-          statusLabel: getStatusLabel(data.orderStatus),
-          statusClass: getStatusClass(data.orderStatus),
-          company: data.companyName || '未知公司',
-          platform: data.platformName || '未知平台',
-          channel: data.channelName || '未知渠道',
-          expectedPeople: data.expectedPersonCount || 0,
-          actualPeople: data.actualPersonCount || 0,
-          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 ? new Date(data.expectedEndDate).toISOString().split('T')[0] : '未设置',
-          actualEndDate: data.actualEndDate ? new Date(data.actualEndDate).toISOString().split('T')[0] : null,
-        }
-        setOrder(transformedOrder)
-        setOrderStatus(data.orderStatus || 'draft')
+  // 使用React Query获取订单详情
+  const {
+    data: order,
+    isLoading,
+    error: queryError,
+  } = useQuery<OrderDetailData, Error>({
+    queryKey: ['order-detail', orderId],
+    queryFn: () => orderId ? fetchOrderDetailQuery(orderId) : Promise.reject(new Error('未找到订单ID')),
+    enabled: !!orderId,
+  })
 
-        // TODO: 获取关联人才、视频、统计数据
-        // 暂时使用空数组
-        setPersons([])
-        setVideos([])
-      } else {
-        throw new Error(`获取订单详情失败: ${response.status}`)
-      }
-    } catch (err) {
-      console.error('获取订单详情错误:', err)
-      setError(err instanceof Error ? err.message : '获取订单详情失败')
+  // 处理查询错误
+  useEffect(() => {
+    if (queryError) {
+      console.error('获取订单详情错误:', queryError)
       Taro.showToast({
-        title: '获取订单详情失败',
+        title: queryError.message.includes('未找到订单ID') ? '未找到订单ID' : '获取订单详情失败',
         icon: 'none'
       })
-    } finally {
-      setLoading(false)
     }
-  }
+  }, [queryError])
+
+  // 处理查询成功
+  useEffect(() => {
+    if (order) {
+      setOrderStatus(order.status || 'draft')
+      // TODO: 获取关联人才、视频、统计数据
+      // 暂时使用空数组
+      setPersons([])
+      setVideos([])
+    }
+  }, [order])
 
   // 状态标签和样式辅助函数
   const getStatusLabel = (status: string) => {
@@ -123,9 +154,6 @@ const OrderDetail: React.FC = () => {
 
     if (id) {
       setOrderId(id)
-      fetchOrderDetail(id)
-    } else {
-      setError('未找到订单ID')
     }
   }, [])
 
@@ -168,6 +196,20 @@ const OrderDetail: React.FC = () => {
         className="h-screen overflow-y-auto px-4 pb-4 pt-0"
         scrollY
       >
+        {isLoading ? (
+          <View className="flex items-center justify-center h-64">
+            <Text className="text-gray-500">加载中...</Text>
+          </View>
+        ) : queryError ? (
+          <View className="flex items-center justify-center h-64">
+            <Text className="text-red-500">{queryError.message}</Text>
+          </View>
+        ) : !order ? (
+          <View className="flex items-center justify-center h-64">
+            <Text className="text-gray-500">未找到订单数据</Text>
+          </View>
+        ) : (
+          <>
         {/* 顶部信息卡片 */}
         <View className="card bg-white p-4 mb-4 mt-4">
           <View className="flex justify-between items-start mb-3">
@@ -242,23 +284,23 @@ const OrderDetail: React.FC = () => {
             <View className="bg-blue-50 rounded-lg p-2 text-center">
               <Text className="text-xs text-gray-600">本月打卡</Text>
               <Text className="text-sm font-bold text-gray-800">
-                {order.checkinStats.current}/{order.checkinStats.total}
+                {checkinStats.current}/{checkinStats.total}
               </Text>
-              <Text className="text-xs text-gray-500">{order.checkinStats.percentage}%</Text>
+              <Text className="text-xs text-gray-500">{checkinStats.percentage}%</Text>
             </View>
             <View className="bg-green-50 rounded-lg p-2 text-center">
               <Text className="text-xs text-gray-600">工资视频</Text>
               <Text className="text-sm font-bold text-gray-800">
-                {order.salaryVideoStats.current}/{order.salaryVideoStats.total}
+                {salaryVideoStats.current}/{salaryVideoStats.total}
               </Text>
-              <Text className="text-xs text-gray-500">{order.salaryVideoStats.percentage}%</Text>
+              <Text className="text-xs text-gray-500">{salaryVideoStats.percentage}%</Text>
             </View>
             <View className="bg-purple-50 rounded-lg p-2 text-center">
               <Text className="text-xs text-gray-600">个税视频</Text>
               <Text className="text-sm font-bold text-gray-800">
-                {order.taxVideoStats.current}/{order.taxVideoStats.total}
+                {taxVideoStats.current}/{taxVideoStats.total}
               </Text>
-              <Text className="text-xs text-gray-500">{order.taxVideoStats.percentage}%</Text>
+              <Text className="text-xs text-gray-500">{taxVideoStats.percentage}%</Text>
             </View>
           </View>
           <Button className="text-blue-500 text-sm">查看详细打卡记录</Button>
@@ -387,6 +429,7 @@ const OrderDetail: React.FC = () => {
             </Button>
           </View>
         </View>
+        </>)}
       </ScrollView>
     </>
   )