Просмотр исходного кода

✨ feat(admin): add contract selection component

- create ContractSelect component with search and loading states
- implement react-query for contract data fetching
- support keyword search functionality
- add loading indicator during data fetching
- replace hardcoded Select components in ContractRenews page with new ContractSelect component
yourname 8 месяцев назад
Родитель
Сommit
b0ea539912
2 измененных файлов с 91 добавлено и 14 удалено
  1. 88 0
      src/client/admin/components/ContractSelect.tsx
  2. 3 14
      src/client/admin/pages/ContractRenews.tsx

+ 88 - 0
src/client/admin/components/ContractSelect.tsx

@@ -0,0 +1,88 @@
+import React, { useState } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { Select, Spin } from 'antd';
+import type { SelectProps } from 'antd';
+import { InferResponseType } from 'hono/client';
+import { hetongClient } from '@/client/api';
+
+// 定义合同数据类型
+type ContractItem = InferResponseType<typeof hetongClient[':id']['$get'], 200>;
+type ContractListResponse = InferResponseType<typeof hetongClient.$get, 200>;
+
+// 定义组件属性接口
+interface ContractSelectProps {
+  value?: number;
+  onChange?: (value?: number) => void;
+  placeholder?: string;
+  disabled?: boolean;
+}
+
+const ContractSelect: React.FC<ContractSelectProps> = ({
+  value,
+  onChange,
+  placeholder = '请选择合同',
+  disabled = false
+}) => {
+  const [searchValue, setSearchValue] = useState('');
+
+  // 使用React Query获取合同列表数据
+  const { data: contractsData, isLoading, error } = useQuery<ContractListResponse>({
+    queryKey: ['contracts', searchValue],
+    queryFn: async () => {
+      const res = await hetongClient.$get({
+        query: { 
+          page: 1, 
+          pageSize: 1000,
+          keyword: searchValue 
+        }
+      });
+      
+      if (!res.ok) {
+        throw new Error('获取合同列表失败');
+      }
+      
+      return res.json();
+    },
+  });
+
+  // 处理搜索输入变化
+  const handleSearch = (value: string) => {
+    setSearchValue(value);
+  };
+
+  // 格式化合同数据为Select组件所需的选项格式
+  const options = contractsData?.data?.map(contract => ({
+    label: `${contract.contractNumber} - ${contract.client?.name || '未知客户'}`,
+    value: contract.id,
+  })) || [];
+
+  return (
+    <div style={{ position: 'relative' }}>
+      <Select
+        value={value}
+        onChange={onChange}
+        placeholder={placeholder}
+        disabled={disabled || isLoading}
+        showSearch
+        filterOption={false}
+        onSearch={handleSearch}
+        style={{ width: '100%' }}
+        options={options}
+        notFoundContent={isLoading ? <Spin size="small" /> : '未找到匹配合同'}
+      />
+      {isLoading && (
+        <div style={{ 
+          position: 'absolute', 
+          top: '50%', 
+          right: '16px', 
+          transform: 'translateY(-50%)',
+          pointerEvents: 'none'
+        }}>
+          <Spin size="small" />
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default ContractSelect;

+ 3 - 14
src/client/admin/pages/ContractRenews.tsx

@@ -1,5 +1,6 @@
 import React, { useState, useEffect } from 'react';
 import React, { useState, useEffect } from 'react';
 import { Table, Button, Space, Tag, Input, DatePicker, Select, Form, Modal, Typography, Divider, Card } from 'antd';
 import { Table, Button, Space, Tag, Input, DatePicker, Select, Form, Modal, Typography, Divider, Card } from 'antd';
+import ContractSelect from '@/client/admin/components/ContractSelect';
 import { App } from 'antd';
 import { App } from 'antd';
 import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons';
 import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons';
 import type { TableProps, FormProps } from 'antd';
 import type { TableProps, FormProps } from 'antd';
@@ -344,13 +345,7 @@ const ContractRenews: React.FC = () => {
           className="mb-4"
           className="mb-4"
         >
         >
           <Form.Item name="contractId" label="合同">
           <Form.Item name="contractId" label="合同">
-            <Select placeholder="选择合同" style={{ width: 200 }}>
-              {contracts.map(contract => (
-                <Option key={contract.id} value={contract.id}>
-                  {contract.contractNo} - {contract.customer?.name}
-                </Option>
-              ))}
-            </Select>
+            <ContractSelect placeholder="选择合同" style={{ width: 200 }} />
           </Form.Item>
           </Form.Item>
           
           
           <Form.Item name="status" label="状态">
           <Form.Item name="status" label="状态">
@@ -435,13 +430,7 @@ const ContractRenews: React.FC = () => {
             label="关联合同"
             label="关联合同"
             rules={[{ required: true, message: '请选择关联合同' }]}
             rules={[{ required: true, message: '请选择关联合同' }]}
           >
           >
-            <Select placeholder="选择合同" style={{ width: '100%' }}>
-              {contracts.map(contract => (
-                <Option key={contract.id} value={contract.id}>
-                  {contract.contractNo} - {contract.customer?.name}
-                </Option>
-              ))}
-            </Select>
+            <ContractSelect placeholder="选择合同" style={{ width: '100%' }} />
           </Form.Item>
           </Form.Item>
           
           
           <Form.Item
           <Form.Item