Procházet zdrojové kódy

✨ feat(admin): add contact management feature

- add contact management menu item and route
- add LinkmanSelect component for contact selection
- update OrderRecords page to include client and contact selection

♻️ refactor(components): upgrade LinkmanSelect to use React Query

- replace ahooks useRequest with React Query useQuery
- add type definition for API response
- implement proper error handling for API requests
- add staleTime and gcTime for better caching strategy
- optimize loading state handling

🐛 fix(pages): resolve potential null reference in OrderRecords

- add null check for orderDate when setting form values
- ensure proper date formatting only when date exists
yourname před 8 měsíci
rodič
revize
76a29d7cd9

+ 27 - 19
src/client/admin/components/LinkmanSelect.tsx

@@ -1,7 +1,8 @@
 import React from 'react';
-import { Select } from 'antd';
-import { useRequest } from 'ahooks';
-import { contactsClient } from '@/client/api';
+import { Select, Spin } from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import { linkmanClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
 
 const { Option } = Select;
 
@@ -24,6 +25,9 @@ interface LinkmanData {
   clientId: number;
 }
 
+// 定义联系人列表响应类型
+type ContactsResponse = InferResponseType<typeof linkmanClient.$get, 200>;
+
 export const LinkmanSelect: React.FC<LinkmanSelectProps> = ({
   value,
   onChange,
@@ -34,10 +38,11 @@ export const LinkmanSelect: React.FC<LinkmanSelectProps> = ({
   mode,
   showSearch = true,
 }) => {
-  // 获取联系人列表
-  const { data: linkmanData, loading } = useRequest(
-    async () => {
-      const response = await contactsClient.$get({
+  // 使用React Query V5获取联系人列表
+  const { data: contactsData, isLoading } = useQuery({
+    queryKey: ['linkman', clientId],
+    queryFn: async (): Promise<ContactsResponse> => {
+      const response = await linkmanClient.$get({
         query: {
           page: 1,
           pageSize: 1000,
@@ -45,16 +50,18 @@ export const LinkmanSelect: React.FC<LinkmanSelectProps> = ({
         },
       });
       
-      if (response.status === 200) {
-        const result = await response.json();
-        return result.data as LinkmanData[];
+      if (!response.ok) {
+        throw new Error('获取联系人列表失败');
       }
-      return [];
+      
+      return response.json();
     },
-    {
-      refreshDeps: [clientId],
-    }
-  );
+    staleTime: 5 * 60 * 1000, // 5分钟
+    gcTime: 10 * 60 * 1000, // 10分钟
+    enabled: !disabled, // 禁用时不加载
+  });
+
+  const linkmanData = contactsData?.data || [];
 
   const handleChange = (newValue: string) => {
     onChange?.(newValue);
@@ -62,7 +69,7 @@ export const LinkmanSelect: React.FC<LinkmanSelectProps> = ({
 
   const filterOption = (input: string, option?: any) => {
     const text = option?.children || '';
-    return String(text).toLowerCase().indexOf(input.toLowerCase()) >= 0;
+    return String(text).toLowerCase().includes(input.toLowerCase());
   };
 
   return (
@@ -71,14 +78,15 @@ export const LinkmanSelect: React.FC<LinkmanSelectProps> = ({
       onChange={handleChange}
       placeholder={placeholder}
       allowClear={allowClear}
-      disabled={disabled || loading}
-      loading={loading}
+      disabled={disabled || isLoading}
+      loading={isLoading}
       mode={mode}
       showSearch={showSearch}
       filterOption={filterOption}
       style={{ width: '100%' }}
+      notFoundContent={isLoading ? <Spin size="small" /> : '未找到联系人'}
     >
-      {linkmanData?.map((linkman: LinkmanData) => (
+      {linkmanData.map((linkman: LinkmanData) => (
         <Option key={linkman.id} value={linkman.id}>
           {linkman.name}
           {linkman.mobile && ` (${linkman.mobile})`}

+ 6 - 0
src/client/admin/menu.tsx

@@ -106,6 +106,12 @@ export const useMenu = () => {
           path: '/admin/clients',
           permission: 'client:manage'
         },
+        {
+          key: 'contacts',
+          label: '联系人管理',
+          path: '/admin/contacts',
+          permission: 'contact:manage'
+        },
         {
           key: 'order-records',
           label: '订单记录',

+ 45 - 11
src/client/admin/pages/OrderRecords.tsx

@@ -2,9 +2,12 @@ import React, { useState, useEffect } from 'react';
 import { Table, Button, Modal, Form, Input, DatePicker, Select, message, Space, Popconfirm } from 'antd';
 import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
 import type { ColumnsType } from 'antd/es/table';
+import dayjs from 'dayjs';
 import { InferRequestType, InferResponseType } from 'hono/client';
 import { orderRecordClient } from '@/client/api';
 import { ClientSelect } from '@/client/admin/components/ClientSelect';
+import { UserSelect } from '@/client/admin/components/UserSelect';
+import { LinkmanSelect } from '@/client/admin/components/LinkmanSelect';
 
 const { RangePicker } = DatePicker;
 const { Option } = Select;
@@ -211,7 +214,7 @@ const OrderRecords: React.FC = () => {
     setEditingRecord(record);
     form.setFieldsValue({
       ...record,
-      orderDate: dayjs(record.orderDate),
+      orderDate: record.orderDate ? dayjs(record.orderDate) : null,
       deliveryDate: record.deliveryDate ? dayjs(record.deliveryDate) : null,
     });
     setModalVisible(true);
@@ -247,7 +250,7 @@ const OrderRecords: React.FC = () => {
       const values = await form.validateFields();
       const formData = {
         ...values,
-        orderDate: values.orderDate.format('YYYY-MM-DD'),
+        orderDate: values.orderDate ? values.orderDate.format('YYYY-MM-DD') : null,
         deliveryDate: values.deliveryDate ? values.deliveryDate.format('YYYY-MM-DD') : null,
       };
 
@@ -355,6 +358,15 @@ const OrderRecords: React.FC = () => {
           >
             <Input placeholder="请输入公司名称" />
           </Form.Item>
+          
+          <Form.Item
+            name="clientId"
+            label="选择客户"
+            rules={[{ required: true, message: '请选择客户' }]}
+          >
+            <ClientSelect placeholder="请选择客户" />
+          </Form.Item>
+          
           <Form.Item
             name="orderNumber"
             label="订单编号"
@@ -362,13 +374,38 @@ const OrderRecords: React.FC = () => {
           >
             <Input placeholder="请输入订单编号" />
           </Form.Item>
+          
+          <Form.Item
+            name="linkmanId"
+            label="选择联系人"
+          >
+            <LinkmanSelect placeholder="请选择联系人" />
+          </Form.Item>
+          
           <Form.Item
             name="contactPerson"
             label="联系人"
-            rules={[{ required: true, message: '请输入联系人' }]}
+            rules={[{ required: true, message: '请输入联系人姓名' }]}
+          >
+            <Input placeholder="请输入联系人姓名" />
+          </Form.Item>
+          
+          <Form.Item
+            name="userId"
+            label="选择业务员"
+            rules={[{ required: true, message: '请选择业务员' }]}
           >
-            <Input placeholder="请输入联系人" />
+            <UserSelect placeholder="请选择业务员" />
           </Form.Item>
+          
+          <Form.Item
+            name="salesperson"
+            label="业务员姓名"
+            rules={[{ required: true, message: '请输入业务员姓名' }]}
+          >
+            <Input placeholder="请输入业务员姓名" />
+          </Form.Item>
+          
           <Form.Item
             name="orderDate"
             label="下单日期"
@@ -376,9 +413,11 @@ const OrderRecords: React.FC = () => {
           >
             <DatePicker style={{ width: '100%' }} />
           </Form.Item>
+          
           <Form.Item name="deliveryDate" label="交单日期">
             <DatePicker style={{ width: '100%' }} />
           </Form.Item>
+          
           <Form.Item
             name="advancePayment"
             label="预付款"
@@ -386,6 +425,7 @@ const OrderRecords: React.FC = () => {
           >
             <Input type="number" placeholder="请输入预付款" />
           </Form.Item>
+          
           <Form.Item
             name="orderAmount"
             label="订单金额"
@@ -393,6 +433,7 @@ const OrderRecords: React.FC = () => {
           >
             <Input type="number" placeholder="请输入订单金额" />
           </Form.Item>
+          
           <Form.Item
             name="orderStatus"
             label="订单状态"
@@ -403,13 +444,6 @@ const OrderRecords: React.FC = () => {
               <Option value={1}>已完成</Option>
             </Select>
           </Form.Item>
-          <Form.Item
-            name="salesperson"
-            label="业务员"
-            rules={[{ required: true, message: '请输入业务员' }]}
-          >
-            <Input placeholder="请输入业务员" />
-          </Form.Item>
         </Form>
       </Modal>
     </div>