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

✨ feat(admin): add multiple management modules

- add system management menu with areas, files and logs submodules
- add finance management menu with expenses submodule
- add contract management menu with contracts and contract renewals submodules
- implement CRUD operations for areas, clients, contacts, contracts, contract renewals, expenses, files and logs
- add corresponding API clients for new modules
- update routing configuration for new pages
yourname 9 месяцев назад
Родитель
Сommit
8a5a0d4aee

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

@@ -11,6 +11,13 @@ import {
   DollarOutlined,
   CalendarOutlined
 } from '@ant-design/icons';
+import {
+  FileTextOutlined,
+  DatabaseOutlined,
+  FileProtectOutlined,
+  HistoryOutlined,
+  AuditOutlined
+} from '@ant-design/icons';
 
 export interface MenuItem {
   key: string;
@@ -114,6 +121,65 @@ export const useMenu = () => {
           permission: 'followUp:manage'
         }
       ]
+    },
+    {
+      key: 'system',
+      label: '系统管理',
+      icon: <DatabaseOutlined />,
+      children: [
+        {
+          key: 'areas',
+          label: '区域管理',
+          path: '/admin/areas',
+          permission: 'area:manage'
+        },
+        {
+          key: 'files',
+          label: '文件管理',
+          icon: <FileTextOutlined />,
+          path: '/admin/files',
+          permission: 'file:manage'
+        },
+        {
+          key: 'logs',
+          label: '系统日志',
+          icon: <HistoryOutlined />,
+          path: '/admin/logs',
+          permission: 'log:view'
+        }
+      ]
+    },
+    {
+      key: 'finance',
+      label: '财务管理',
+      icon: <FileProtectOutlined />,
+      children: [
+        {
+          key: 'expenses',
+          label: '费用管理',
+          path: '/admin/expenses',
+          permission: 'expense:manage'
+        }
+      ]
+    },
+    {
+      key: 'contract',
+      label: '合同管理',
+      icon: <AuditOutlined />,
+      children: [
+        {
+          key: 'contracts',
+          label: '合同列表',
+          path: '/admin/contracts',
+          permission: 'contract:manage'
+        },
+        {
+          key: 'contract-renews',
+          label: '合同续签',
+          path: '/admin/contract-renews',
+          permission: 'contract:renew'
+        }
+      ]
     }
   ];
 

+ 234 - 0
src/client/admin/pages/Areas.tsx

@@ -0,0 +1,234 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Space, Input, Modal, Form, message } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
+import { useRequest } from 'ahooks';
+import { areaClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+
+// 定义类型
+type AreaItem = InferResponseType<typeof areaClient.$get, 200>['data'][0];
+type AreaListResponse = InferResponseType<typeof areaClient.$get, 200>;
+
+const Areas: React.FC = () => {
+  const [form] = Form.useForm();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [editingKey, setEditingKey] = useState<string | null>(null);
+  const [searchText, setSearchText] = useState('');
+  
+  // 获取区域列表数据
+  const { data, loading, run } = useRequest<AreaListResponse>(
+    ({ page, pageSize }: { page: number; pageSize: number }) => areaClient.$get({
+      query: { page, pageSize, keyword: searchText } 
+    }),
+    {
+      refreshDeps: [searchText],
+      defaultParams: [{ page: 1, pageSize: 10 }],
+      onSuccess: (result: AreaListResponse) => {
+        setDataSource(result.data);
+        setPagination({
+          ...pagination,
+          total: result.pagination.total,
+        });
+      },
+    }
+  );
+  
+  const [dataSource, setDataSource] = useState<AreaItem[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  
+  // 搜索
+  const handleSearch = () => {
+    run({ page: 1, pageSize: pagination.pageSize });
+  };
+  
+  // 分页变化
+  const handleTableChange = (pagination: any) => {
+    setPagination(pagination);
+    run({ page: pagination.current, pageSize: pagination.pageSize });
+  };
+  
+  // 显示添加/编辑弹窗
+  const showModal = (record?: AreaItem) => {
+    setModalVisible(true);
+    if (record) {
+      setEditingKey(record.id.toString());
+      form.setFieldsValue({
+        name: record.name,
+        parentId: record.parentId,
+      });
+    } else {
+      setEditingKey(null);
+      form.resetFields();
+    }
+  };
+  
+  // 关闭弹窗
+  const handleCancel = () => {
+    setModalVisible(false);
+    form.resetFields();
+  };
+  
+  // 提交表单
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      
+      if (editingKey) {
+        // 更新操作
+        await areaClient[':id'].$put({
+          param: { id: editingKey },
+          json: values,
+        });
+        message.success('更新成功');
+      } else {
+        // 创建操作
+        await areaClient.$post({ json: values });
+        message.success('创建成功');
+      }
+      
+      setModalVisible(false);
+      run({ page: pagination.current, pageSize: pagination.pageSize });
+    } catch (error) {
+      message.error('操作失败,请重试');
+    }
+  };
+  
+  // 删除操作
+  const handleDelete = async (id: string) => {
+    try {
+      await areaClient[':id'].$delete({ param: { id } });
+      message.success('删除成功');
+      run({ page: pagination.current, pageSize: pagination.pageSize });
+    } catch (error) {
+      message.error('删除失败,请重试');
+    }
+  };
+  
+  // 表格列定义
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '区域名称',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '父区域ID',
+      dataIndex: 'parentId',
+      key: 'parentId',
+      width: 120,
+      render: (parentId: number | undefined) => parentId || '-',
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 180,
+      render: (date: string) => new Date(date).toLocaleString(),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 180,
+      render: (_: any, record: AreaItem) => (
+        <Space size="middle">
+          <Button 
+            type="text" 
+            icon={<EditOutlined />} 
+            onClick={() => showModal(record)}
+          >
+            编辑
+          </Button>
+          <Button 
+            type="text" 
+            danger 
+            icon={<DeleteOutlined />} 
+            onClick={() => handleDelete(record.id.toString())}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+  
+  return (
+    <div className="p-4">
+      <div className="flex justify-between items-center mb-4">
+        <h2 className="text-xl font-bold">区域管理</h2>
+        <Button 
+          type="primary" 
+          icon={<PlusOutlined />} 
+          onClick={() => showModal()}
+        >
+          添加区域
+        </Button>
+      </div>
+      
+      <div className="mb-4">
+        <Input
+          placeholder="搜索区域名称"
+          prefix={<SearchOutlined />}
+          value={searchText}
+          onChange={(e) => setSearchText(e.target.value)}
+          onPressEnter={handleSearch}
+          style={{ width: 300 }}
+        />
+        <Button type="default" onClick={handleSearch} style={{ marginLeft: 8 }}>
+          搜索
+        </Button>
+      </div>
+      
+      <Table
+        columns={columns}
+        dataSource={dataSource}
+        rowKey="id"
+        loading={loading}
+        pagination={pagination}
+        onChange={handleTableChange}
+        bordered
+      />
+      
+      <Modal
+        title={editingKey ? "编辑区域" : "添加区域"}
+        open={modalVisible}
+        onCancel={handleCancel}
+        footer={[
+          <Button key="cancel" onClick={handleCancel}>
+            取消
+          </Button>,
+          <Button key="submit" type="primary" onClick={handleSubmit}>
+            确定
+          </Button>,
+        ]}
+      >
+        <Form form={form} layout="vertical">
+          <Form.Item
+            name="name"
+            label="区域名称"
+            rules={[{ required: true, message: '请输入区域名称' }]}
+          >
+            <Input placeholder="请输入区域名称" />
+          </Form.Item>
+          <Form.Item
+            name="parentId"
+            label="父区域ID"
+          >
+            <Input placeholder="请输入父区域ID,顶级区域留空" />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default Areas;

+ 453 - 0
src/client/admin/pages/Clients.tsx

@@ -0,0 +1,453 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Space, Input, Modal, Form, message, Select } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
+import { useRequest } from 'ahooks';
+import { clientClient, areaClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+
+// 定义类型
+type ClientItem = InferResponseType<typeof clientClient.$get, 200>['data'][0];
+type ClientListResponse = InferResponseType<typeof clientClient.$get, 200>;
+type AreaItem = InferResponseType<typeof areaClient.$get, 200>['data'][0];
+
+const Clients: React.FC = () => {
+  const [form] = Form.useForm();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [editingKey, setEditingKey] = useState<string | null>(null);
+  const [searchText, setSearchText] = useState('');
+  const [areas, setAreas] = useState<AreaItem[]>([]);
+  
+  // 获取区域列表
+  const { run: fetchAreas } = useRequest(
+    () => areaClient.$get({ query: { page: 1, pageSize: 1000 } }),
+    {
+      onSuccess: (result) => {
+        setAreas(result.data);
+      },
+    }
+  );
+  
+  // 获取客户列表数据
+  const { data, loading, run } = useRequest<ClientListResponse>(
+    ({ page, pageSize }) => clientClient.$get({ 
+      query: { page, pageSize, keyword: searchText } 
+    }),
+    {
+      refreshDeps: [searchText],
+      defaultParams: [{ page: 1, pageSize: 10 }],
+      onSuccess: (result) => {
+        setDataSource(result.data);
+        setPagination({
+          ...pagination,
+          total: result.pagination.total,
+        });
+      },
+    }
+  );
+  
+  // 初始化获取区域数据
+  useEffect(() => {
+    fetchAreas();
+  }, [fetchAreas]);
+  
+  const [dataSource, setDataSource] = useState<ClientItem[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  
+  // 搜索
+  const handleSearch = () => {
+    run({ page: 1, pageSize: pagination.pageSize });
+  };
+  
+  // 分页变化
+  const handleTableChange = (pagination: any) => {
+    setPagination(pagination);
+    run({ page: pagination.current, pageSize: pagination.pageSize });
+  };
+  
+  // 显示添加/编辑弹窗
+  const showModal = (record?: ClientItem) => {
+    setModalVisible(true);
+    if (record) {
+      setEditingKey(record.id.toString());
+      form.setFieldsValue({
+        companyName: record.companyName,
+        areaId: record.areaId,
+        square: record.square,
+        address: record.address,
+        contactPerson: record.contactPerson,
+        position: record.position,
+        mobile: record.mobile,
+        zipCode: record.zipCode,
+        telephone: record.telephone,
+        fax: record.fax,
+        homepage: record.homepage,
+        email: record.email,
+        industry: record.industry,
+        subIndustry: record.subIndustry,
+        customerType: record.customerType,
+        startDate: record.startDate ? new Date(record.startDate) : null,
+        source: record.source,
+        description: record.description,
+        responsibleUserId: record.responsibleUserId,
+        groupId: record.groupId,
+        remarks: record.remarks,
+        status: record.status,
+        auditStatus: record.auditStatus,
+      });
+    } else {
+      setEditingKey(null);
+      form.resetFields();
+    }
+  };
+  
+  // 关闭弹窗
+  const handleCancel = () => {
+    setModalVisible(false);
+    form.resetFields();
+  };
+  
+  // 提交表单
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      
+      // 处理日期字段
+      if (values.startDate) {
+        values.startDate = values.startDate.format('YYYY-MM-DD');
+      }
+      
+      if (editingKey) {
+        // 更新操作
+        await clientClient[':id'].$put({
+          param: { id: editingKey },
+          json: values,
+        });
+        message.success('客户更新成功');
+      } else {
+        // 创建操作
+        await clientClient.$post({ json: values });
+        message.success('客户创建成功');
+      }
+      
+      setModalVisible(false);
+      run({ page: pagination.current, pageSize: pagination.pageSize });
+    } catch (error) {
+      message.error('操作失败,请重试');
+    }
+  };
+  
+  // 删除操作
+  const handleDelete = async (id: number) => {
+    try {
+      await clientClient[':id'].$delete({ param: { id: id.toString() } });
+      message.success('客户删除成功');
+      run({ page: pagination.current, pageSize: pagination.pageSize });
+    } catch (error) {
+      message.error('删除失败,请重试');
+    }
+  };
+  
+  // 表格列定义
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '公司名称',
+      dataIndex: 'companyName',
+      key: 'companyName',
+    },
+    {
+      title: '所在区域',
+      dataIndex: 'areaId',
+      key: 'areaId',
+      render: (areaId: number) => {
+        const area = areas.find(a => a.id === areaId);
+        return area ? area.name : '-';
+      },
+    },
+    {
+      title: '联系人',
+      dataIndex: 'contactPerson',
+      key: 'contactPerson',
+    },
+    {
+      title: '联系电话',
+      dataIndex: 'mobile',
+      key: 'mobile',
+    },
+    {
+      title: '客户状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (status: number) => (
+        <span>{status === 1 ? '有效' : '无效'}</span>
+      ),
+    },
+    {
+      title: '审核状态',
+      dataIndex: 'auditStatus',
+      key: 'auditStatus',
+      render: (status: number) => {
+        switch (status) {
+          case 0: return <span>待审核</span>;
+          case 1: return <span>已审核</span>;
+          case 2: return <span>已拒绝</span>;
+          default: return <span>-</span>;
+        }
+      },
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 180,
+      render: (date: string) => new Date(date).toLocaleString(),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 180,
+      render: (_: any, record: ClientItem) => (
+        <Space size="middle">
+          <Button 
+            type="text" 
+            icon={<EditOutlined />} 
+            onClick={() => showModal(record)}
+          >
+            编辑
+          </Button>
+          <Button 
+            type="text" 
+            danger 
+            icon={<DeleteOutlined />} 
+            onClick={() => handleDelete(record.id)}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+  
+  return (
+    <div className="p-4">
+      <div className="flex justify-between items-center mb-4">
+        <h2 className="text-xl font-bold">客户管理</h2>
+        <Button 
+          type="primary" 
+          icon={<PlusOutlined />} 
+          onClick={() => showModal()}
+        >
+          添加客户
+        </Button>
+      </div>
+      
+      <div className="mb-4">
+        <Input
+          placeholder="搜索公司名称或联系人"
+          prefix={<SearchOutlined />}
+          value={searchText}
+          onChange={(e) => setSearchText(e.target.value)}
+          onPressEnter={handleSearch}
+          style={{ width: 300 }}
+        />
+        <Button type="default" onClick={handleSearch} style={{ marginLeft: 8 }}>
+          搜索
+        </Button>
+      </div>
+      
+      <Table
+        columns={columns}
+        dataSource={dataSource}
+        rowKey="id"
+        loading={loading}
+        pagination={pagination}
+        onChange={handleTableChange}
+        bordered
+      />
+      
+      <Modal
+        title={editingKey ? "编辑客户" : "添加客户"}
+        open={modalVisible}
+        onCancel={handleCancel}
+        footer={[
+          <Button key="cancel" onClick={handleCancel}>
+            取消
+          </Button>,
+          <Button key="submit" type="primary" onClick={handleSubmit}>
+            确定
+          </Button>,
+        ]}
+        width={800}
+      >
+        <Form form={form} layout="vertical">
+          <Form.Item
+            name="companyName"
+            label="公司名称"
+            rules={[{ required: true, message: '请输入公司名称' }]}
+          >
+            <Input placeholder="请输入公司名称" />
+          </Form.Item>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="areaId"
+              label="所在区域"
+            >
+              <Select placeholder="请选择区域">
+                {areas.map(area => (
+                  <Select.Option key={area.id} value={area.id}>
+                    {area.name}
+                  </Select.Option>
+                ))}
+              </Select>
+            </Form.Item>
+            
+            <Form.Item
+              name="square"
+              label="场地面积"
+            >
+              <Input placeholder="请输入场地面积" />
+            </Form.Item>
+          </div>
+          
+          <Form.Item
+            name="address"
+            label="客户地址"
+          >
+            <Input placeholder="请输入客户地址" />
+          </Form.Item>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="contactPerson"
+              label="联系人姓名"
+            >
+              <Input placeholder="请输入联系人姓名" />
+            </Form.Item>
+            
+            <Form.Item
+              name="position"
+              label="联系人职位"
+            >
+              <Input placeholder="请输入联系人职位" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="mobile"
+              label="手机号码"
+            >
+              <Input placeholder="请输入手机号码" />
+            </Form.Item>
+            
+            <Form.Item
+              name="telephone"
+              label="联系电话"
+            >
+              <Input placeholder="请输入联系电话" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="email"
+              label="电子邮箱"
+            >
+              <Input placeholder="请输入电子邮箱" />
+            </Form.Item>
+            
+            <Form.Item
+              name="zipCode"
+              label="邮政编码"
+            >
+              <Input placeholder="请输入邮政编码" />  
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="industry"
+              label="所属行业"
+            >
+              <Input placeholder="请输入所属行业" />
+            </Form.Item>
+            
+            <Form.Item
+              name="subIndustry"
+              label="所属子行业"
+            >
+              <Input placeholder="请输入所属子行业" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="customerType"
+              label="客户类型"
+            >
+              <Input placeholder="请输入客户类型" />
+            </Form.Item>
+            
+            <Form.Item
+              name="source"
+              label="客户来源"
+            >
+              <Input placeholder="请输入客户来源" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="status"
+              label="客户状态"
+              initialValue={1}
+            >
+              <Select>
+                <Select.Option value={0}>无效</Select.Option>
+                <Select.Option value={1}>有效</Select.Option>
+              </Select>
+            </Form.Item>
+            
+            <Form.Item
+              name="auditStatus"
+              label="审核状态"
+              initialValue={0}
+            >
+              <Select>
+                <Select.Option value={0}>待审核</Select.Option>
+                <Select.Option value={1}>已审核</Select.Option>
+                <Select.Option value={2}>已拒绝</Select.Option>
+              </Select>
+            </Form.Item>
+          </div>
+          
+          <Form.Item
+            name="description"
+            label="客户详细信息"
+          >
+            <Input.TextArea rows={4} placeholder="请输入客户详细信息" />
+          </Form.Item>
+          
+          <Form.Item
+            name="remarks"
+            label="备注信息"
+          >
+            <Input.TextArea rows={2} placeholder="请输入备注信息" />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default Clients;

+ 439 - 0
src/client/admin/pages/Contacts.tsx

@@ -0,0 +1,439 @@
+import React, { useState } from 'react';
+import { Table, Button, Space, Input, Modal, Form, message, Select, DatePicker } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { linkmanClient, clientClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+import dayjs from 'dayjs';
+
+// 定义类型
+type LinkmanItem = InferResponseType<typeof linkmanClient.$get, 200>['data'][0];
+type LinkmanListResponse = InferResponseType<typeof linkmanClient.$get, 200>;
+type ClientItem = InferResponseType<typeof clientClient.$get, 200>['data'][0];
+
+const Contacts: React.FC = () => {
+  const [form] = Form.useForm();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [editingKey, setEditingKey] = useState<string | null>(null);
+  const [searchText, setSearchText] = useState('');
+  const [clients, setClients] = useState<ClientItem[]>([]);
+  const queryClient = useQueryClient();
+  
+  // 获取客户列表
+  const { data: clientsData } = useQuery(
+    ['clientsForContacts'],
+    async () => {
+      const response = await clientClient.$get({ query: { page: 1, pageSize: 1000 } });
+      if (response.status !== 200) throw new Error('Failed to fetch clients');
+      return response.json() as Promise<InferResponseType<typeof clientClient.$get, 200>>;
+    },
+    {
+      onSuccess: (result) => {
+        setClients(result.data);
+      },
+    }
+  );
+  
+  // 获取联系人列表数据
+  const fetchContacts = async ({ page, pageSize }: { page: number; pageSize: number }): Promise<LinkmanListResponse> => {
+    const response = await linkmanClient.$get({ query: { page, pageSize, keyword: searchText } });
+    if (response.status !== 200) throw new Error('Failed to fetch contacts');
+    return response.json() as Promise<LinkmanListResponse>;
+  };
+  
+  const { data, isLoading: loading, refetch } = useQuery(
+    ['contacts', pagination.current, pagination.pageSize, searchText],
+    () => fetchContacts({ page: pagination.current, pageSize: pagination.pageSize }),
+    {
+      onSuccess: (result) => {
+        setDataSource(result.data);
+        setPagination({
+          ...pagination,
+          total: result.pagination.total,
+        });
+      },
+    }
+  );
+  
+  const [dataSource, setDataSource] = useState<LinkmanItem[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  
+  // 搜索
+  const handleSearch = () => {
+    setPagination({ ...pagination, current: 1 });
+    refetch();
+  };
+  
+  // 分页变化
+  const handleTableChange = (pagination: any) => {
+    setPagination(pagination);
+    refetch();
+  };
+  
+  // 显示添加/编辑弹窗
+  const showModal = (record?: LinkmanItem) => {
+    setModalVisible(true);
+    if (record) {
+      setEditingKey(record.id);
+      form.setFieldsValue({
+        id: record.id,
+        clientId: record.clientId,
+        name: record.name,
+        gender: record.gender,
+        position: record.position,
+        mobile: record.mobile,
+        qq: record.qq,
+        telephone: record.telephone,
+        email: record.email,
+        msn: record.msn,
+        alww: record.alww,
+        birthday: record.birthday ? dayjs(record.birthday) : null,
+        description: record.description,
+        createdUserId: record.createdUserId,
+        createdTime: record.createdTime ? dayjs(record.createdTime) : null,
+      });
+    } else {
+      setEditingKey(null);
+      form.resetFields();
+    }
+  };
+  
+  // 关闭弹窗
+  const handleCancel = () => {
+    setModalVisible(false);
+    form.resetFields();
+  };
+  
+  // 创建联系人记录
+  const createContact = useMutation(
+    async (data: any) => {
+      const response = await linkmanClient.$post({ json: data });
+      if (!response.ok) throw new Error('Failed to create contact');
+      return response.json();
+    },
+    {
+      onSuccess: () => {
+        message.success('联系人记录创建成功');
+        queryClient.invalidateQueries({ queryKey: ['contacts'] });
+        setModalVisible(false);
+      },
+      onError: () => {
+        message.error('操作失败,请重试');
+      }
+    }
+  );
+  
+  // 更新联系人记录
+  const updateContact = useMutation(
+    async ({ id, data }: { id: string; data: any }) => {
+      const response = await linkmanClient[':id'].$put({ param: { id }, json: data });
+      if (!response.ok) throw new Error('Failed to update contact');
+      return response.json();
+    },
+    {
+      onSuccess: () => {
+        message.success('联系人记录更新成功');
+        queryClient.invalidateQueries({ queryKey: ['contacts'] });
+        setModalVisible(false);
+      },
+      onError: () => {
+        message.error('操作失败,请重试');
+      }
+    }
+  );
+  
+  // 删除联系人记录
+  const deleteContact = useMutation(
+    async (id: string) => {
+      const response = await linkmanClient[':id'].$delete({ param: { id } });
+      if (!response.ok) throw new Error('Failed to delete contact');
+      return response.json();
+    },
+    {
+      onSuccess: () => {
+        message.success('联系人记录删除成功');
+        queryClient.invalidateQueries({ queryKey: ['contacts'] });
+      },
+      onError: () => {
+        message.error('删除失败,请重试');
+      }
+    }
+  );
+  
+  // 提交表单
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      
+      // 处理日期字段
+      if (values.birthday) values.birthday = values.birthday.format('YYYY-MM-DD');
+      if (values.createdTime) values.createdTime = values.createdTime.format('YYYY-MM-DD HH:mm:ss');
+      
+      if (editingKey) {
+        // 更新操作
+        await updateContact.mutateAsync({ id: editingKey, data: values });
+      } else {
+        // 创建操作
+        await createContact.mutateAsync(values);
+      }
+    } catch (error) {
+      message.error('操作失败,请重试');
+    }
+  };
+  
+  // 表格列定义
+  const columns = [
+    {
+      title: '联系人ID',
+      dataIndex: 'id',
+      key: 'id',
+    },
+    {
+      title: '客户',
+      dataIndex: 'clientId',
+      key: 'clientId',
+      render: (clientId: string) => {
+        const client = clients.find(c => c.id.toString() === clientId);
+        return client ? client.companyName : clientId;
+      },
+    },
+    {
+      title: '姓名',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '职位',
+      dataIndex: 'position',
+      key: 'position',
+    },
+    {
+      title: '手机号码',
+      dataIndex: 'mobile',
+      key: 'mobile',
+    },
+    {
+      title: '电子邮箱',
+      dataIndex: 'email',
+      key: 'email',
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: LinkmanItem) => (
+        <Space size="middle">
+          <Button 
+            type="text" 
+            icon={<EditOutlined />} 
+            onClick={() => showModal(record)}
+          >
+            编辑
+          </Button>
+          <Button 
+            type="text" 
+            danger 
+            icon={<DeleteOutlined />} 
+            onClick={() => deleteContact.mutate(record.id)}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+  
+  return (
+    <div className="p-4">
+      <div className="flex justify-between items-center mb-4">
+        <h2 className="text-xl font-bold">联系人管理</h2>
+        <Button 
+          type="primary" 
+          icon={<PlusOutlined />} 
+          onClick={() => showModal()}
+        >
+          添加联系人
+        </Button>
+      </div>
+      
+      <div className="mb-4">
+        <Input
+          placeholder="搜索联系人姓名或客户名称"
+          prefix={<SearchOutlined />}
+          value={searchText}
+          onChange={(e) => setSearchText(e.target.value)}
+          onPressEnter={handleSearch}
+          style={{ width: 300 }}
+        />
+        <Button type="default" onClick={handleSearch} style={{ marginLeft: 8 }}>
+          搜索
+        </Button>
+      </div>
+      
+      <Table
+        columns={columns}
+        dataSource={dataSource}
+        rowKey="id"
+        loading={loading}
+        pagination={pagination}
+        onChange={handleTableChange}
+        bordered
+      />
+      
+      <Modal
+        title={editingKey ? "编辑联系人记录" : "添加联系人记录"}
+        open={modalVisible}
+        onCancel={handleCancel}
+        footer={[
+          <Button key="cancel" onClick={handleCancel}>
+            取消
+          </Button>,
+          <Button key="submit" type="primary" onClick={handleSubmit}>
+            确定
+          </Button>,
+        ]}
+        width={800}
+      >
+        <Form form={form} layout="vertical">
+          {!editingKey && (
+            <Form.Item
+              name="id"
+              label="联系人ID"
+              rules={[{ required: true, message: '请输入联系人ID' }]}
+            >
+              <Input placeholder="请输入联系人ID" />
+            </Form.Item>
+          )}
+          
+          <Form.Item
+            name="clientId"
+            label="客户"
+            rules={[{ required: true, message: '请选择客户' }]}
+          >
+            <Select placeholder="请选择客户">
+              {clients.map(client => (
+                <Select.Option key={client.id} value={client.id.toString()}>
+                  {client.companyName}
+                </Select.Option>
+              ))}
+            </Select>
+          </Form.Item>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="name"
+              label="姓名"
+              rules={[{ required: true, message: '请输入姓名' }]}
+            >
+              <Input placeholder="请输入姓名" />
+            </Form.Item>
+            
+            <Form.Item
+              name="gender"
+              label="性别"
+            >
+              <Select placeholder="请选择性别">
+                <Select.Option value="男">男</Select.Option>
+                <Select.Option value="女">女</Select.Option>
+                <Select.Option value="其他">其他</Select.Option>
+              </Select>
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="position"
+              label="职位"
+            >
+              <Input placeholder="请输入职位" />
+            </Form.Item>
+            
+            <Form.Item
+              name="mobile"
+              label="手机号码"
+            >
+              <Input placeholder="请输入手机号码" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="telephone"
+              label="联系电话"
+            >
+              <Input placeholder="请输入联系电话" />
+            </Form.Item>
+            
+            <Form.Item
+              name="email"
+              label="电子邮箱"
+            >
+              <Input placeholder="请输入电子邮箱" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-3 gap-4">
+            <Form.Item
+              name="qq"
+              label="QQ号码"
+            >
+              <Input placeholder="请输入QQ号码" />
+            </Form.Item>
+            
+            <Form.Item
+              name="msn"
+              label="MSN账号"
+            >
+              <Input placeholder="请输入MSN账号" />
+            </Form.Item>
+            
+            <Form.Item
+              name="alww"
+              label="其他网络账号"
+            >
+              <Input placeholder="请输入其他网络账号" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="birthday"
+              label="出生日期"
+            >
+              <DatePicker format="YYYY-MM-DD" />
+            </Form.Item>
+            
+            <Form.Item
+              name="createdUserId"
+              label="创建用户ID"
+            >
+              <Input placeholder="请输入创建用户ID" />
+            </Form.Item>
+          </div>
+          
+          <Form.Item
+            name="description"
+            label="详细信息"
+          >
+            <Input.TextArea rows={4} placeholder="请输入详细信息" />
+          </Form.Item>
+          
+          {!editingKey && (
+            <Form.Item
+              name="createdTime"
+              label="创建时间"
+              rules={[{ required: true, message: '请选择创建时间' }]}
+            >
+              <DatePicker showTime format="YYYY-MM-DD HH:mm:ss" />
+            </Form.Item>
+          )}
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default Contacts;

+ 430 - 0
src/client/admin/pages/ContractRenews.tsx

@@ -0,0 +1,430 @@
+import React, { useState } from 'react';
+import { Table, Button, Space, Input, Modal, Form, message, Select, DatePicker } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { hetongRenewClient, hetongClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+import dayjs from 'dayjs';
+
+// 定义类型
+type HetongRenewItem = InferResponseType<typeof hetongRenewClient.$get, 200>['data'][0];
+type HetongRenewListResponse = InferResponseType<typeof hetongRenewClient.$get, 200>;
+type HetongItem = InferResponseType<typeof hetongClient.$get, 200>['data'][0];
+
+const ContractRenews: React.FC = () => {
+  const [form] = Form.useForm();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [editingKey, setEditingKey] = useState<string | null>(null);
+  const [searchText, setSearchText] = useState('');
+  const [contracts, setContracts] = useState<HetongItem[]>([]);
+  const queryClient = useQueryClient();
+  
+  // 获取合同列表
+  const { data: contractsData } = useQuery(
+    ['contractsForRenew'],
+    async () => {
+      const response = await hetongClient.$get({ query: { page: 1, pageSize: 1000 } });
+      if (response.status !== 200) throw new Error('Failed to fetch contracts');
+      return response.json() as Promise<InferResponseType<typeof hetongClient.$get, 200>>;
+    },
+    {
+      onSuccess: (result) => {
+        setContracts(result.data);
+      },
+    }
+  );
+  
+  // 获取合同续签列表数据
+  const fetchContractRenews = async ({ page, pageSize }: { page: number; pageSize: number }): Promise<HetongRenewListResponse> => {
+    const response = await hetongRenewClient.$get({ query: { page, pageSize, keyword: searchText } });
+    if (response.status !== 200) throw new Error('Failed to fetch contract renews');
+    return response.json() as Promise<HetongRenewListResponse>;
+  };
+  
+  const { data, isLoading: loading, refetch } = useQuery(
+    ['contractRenews', pagination.current, pagination.pageSize, searchText],
+    () => fetchContractRenews({ page: pagination.current, pageSize: pagination.pageSize }),
+    {
+      onSuccess: (result) => {
+        setDataSource(result.data);
+        setPagination({
+          ...pagination,
+          total: result.pagination.total,
+        });
+      },
+    }
+  );
+  
+  const [dataSource, setDataSource] = useState<HetongRenewItem[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  
+  // 搜索
+  const handleSearch = () => {
+    setPagination({ ...pagination, current: 1 });
+    refetch();
+  };
+  
+  // 分页变化
+  const handleTableChange = (pagination: any) => {
+    setPagination(pagination);
+    refetch();
+  };
+  
+  // 显示添加/编辑弹窗
+  const showModal = (record?: HetongRenewItem) => {
+    setModalVisible(true);
+    if (record) {
+      setEditingKey(record.id);
+      form.setFieldsValue({
+        id: record.id,
+        contractId: record.contractId,
+        amount: record.amount,
+        revenue: record.revenue,
+        endDate: record.endDate ? dayjs(record.endDate) : null,
+        state: record.state,
+        content: record.content,
+        auditStatus: record.auditStatus,
+        auditTime: record.auditTime ? dayjs(record.auditTime) : null,
+        auditReasons: record.auditReasons,
+        userId: record.userId,
+        createdTime: record.createdTime ? dayjs(record.createdTime) : null,
+      });
+    } else {
+      setEditingKey(null);
+      form.resetFields();
+    }
+  };
+  
+  // 关闭弹窗
+  const handleCancel = () => {
+    setModalVisible(false);
+    form.resetFields();
+  };
+  
+  // 创建合同续签记录
+  const createContractRenew = useMutation(
+    async (data: any) => {
+      const response = await hetongRenewClient.$post({ json: data });
+      if (!response.ok) throw new Error('Failed to create contract renew');
+      return response.json();
+    },
+    {
+      onSuccess: () => {
+        message.success('合同续签记录创建成功');
+        queryClient.invalidateQueries({ queryKey: ['contractRenews'] });
+        setModalVisible(false);
+      },
+      onError: () => {
+        message.error('操作失败,请重试');
+      }
+    }
+  );
+  
+  // 更新合同续签记录
+  const updateContractRenew = useMutation(
+    async ({ id, data }: { id: string; data: any }) => {
+      const response = await hetongRenewClient[':id'].$put({ param: { id: parseInt(id, 10) }, json: data });
+      if (!response.ok) throw new Error('Failed to update contract renew');
+      return response.json();
+    },
+    {
+      onSuccess: () => {
+        message.success('合同续签记录更新成功');
+        queryClient.invalidateQueries({ queryKey: ['contractRenews'] });
+        setModalVisible(false);
+      },
+      onError: () => {
+        message.error('操作失败,请重试');
+      }
+    }
+  );
+  
+  // 删除合同续签记录
+  const deleteContractRenew = useMutation(
+    async (id: string) => {
+      const response = await hetongRenewClient[':id'].$delete({ param: { id: parseInt(id, 10) } });
+      if (!response.ok) throw new Error('Failed to delete contract renew');
+      return response.json();
+    },
+    {
+      onSuccess: () => {
+        message.success('合同续签记录删除成功');
+        queryClient.invalidateQueries({ queryKey: ['contractRenews'] });
+      },
+      onError: () => {
+        message.error('删除失败,请重试');
+      }
+    }
+  );
+  
+  // 提交表单
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      
+      // 处理日期字段
+      if (values.endDate) values.endDate = values.endDate.format('YYYY-MM-DD');
+      if (values.auditTime) values.auditTime = values.auditTime.format('YYYY-MM-DD HH:mm:ss');
+      if (values.createdTime) values.createdTime = values.createdTime.format('YYYY-MM-DD HH:mm:ss');
+      
+      if (editingKey) {
+        // 更新操作
+        await updateContractRenew.mutateAsync({ id: editingKey, data: values });
+      } else {
+        // 创建操作
+        await createContractRenew.mutateAsync(values);
+      }
+    } catch (error) {
+      message.error('操作失败,请重试');
+    }
+  };
+  
+  // 表格列定义
+  const columns = [
+    {
+      title: '续签ID',
+      dataIndex: 'id',
+      key: 'id',
+    },
+    {
+      title: '合同ID',
+      dataIndex: 'contractId',
+      key: 'contractId',
+      render: (contractId: string) => {
+        const contract = contracts.find(c => c.id === contractId);
+        return contract ? `${contract.id} (${contract.contractNumber})` : contractId;
+      },
+    },
+    {
+      title: '续签金额',
+      dataIndex: 'amount',
+      key: 'amount',
+    },
+    {
+      title: '续签状态',
+      dataIndex: 'state',
+      key: 'state',
+    },
+    {
+      title: '结束日期',
+      dataIndex: 'endDate',
+      key: 'endDate',
+      render: (date: string) => date ? new Date(date).toLocaleDateString() : '-',
+    },
+    {
+      title: '审批状态',
+      dataIndex: 'auditStatus',
+      key: 'auditStatus',
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: HetongRenewItem) => (
+        <Space size="middle">
+          <Button 
+            type="text" 
+            icon={<EditOutlined />} 
+            onClick={() => showModal(record)}
+          >
+            编辑
+          </Button>
+          <Button 
+            type="text" 
+            danger 
+            icon={<DeleteOutlined />} 
+            onClick={() => deleteContractRenew.mutate(record.id)}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+  
+  return (
+    <div className="p-4">
+      <div className="flex justify-between items-center mb-4">
+        <h2 className="text-xl font-bold">合同续签管理</h2>
+        <Button 
+          type="primary" 
+          icon={<PlusOutlined />} 
+          onClick={() => showModal()}
+        >
+          添加续签记录
+        </Button>
+      </div>
+      
+      <div className="mb-4">
+        <Input
+          placeholder="搜索合同ID或续签状态"
+          prefix={<SearchOutlined />}
+          value={searchText}
+          onChange={(e) => setSearchText(e.target.value)}
+          onPressEnter={handleSearch}
+          style={{ width: 300 }}
+        />
+        <Button type="default" onClick={handleSearch} style={{ marginLeft: 8 }}>
+          搜索
+        </Button>
+      </div>
+      
+      <Table
+        columns={columns}
+        dataSource={dataSource}
+        rowKey="id"
+        loading={loading}
+        pagination={pagination}
+        onChange={handleTableChange}
+        bordered
+      />
+      
+      <Modal
+        title={editingKey ? "编辑合同续签记录" : "添加合同续签记录"}
+        open={modalVisible}
+        onCancel={handleCancel}
+        footer={[
+          <Button key="cancel" onClick={handleCancel}>
+            取消
+          </Button>,
+          <Button key="submit" type="primary" onClick={handleSubmit}>
+            确定
+          </Button>,
+        ]}
+        width={700}
+      >
+        <Form form={form} layout="vertical">
+          {!editingKey && (
+            <Form.Item
+              name="id"
+              label="续签记录ID"
+              rules={[{ required: true, message: '请输入续签记录ID' }]}
+            >
+              <Input placeholder="请输入续签记录ID" />
+            </Form.Item>
+          )}
+          
+          <Form.Item
+            name="contractId"
+            label="原合同"
+            rules={[{ required: true, message: '请选择原合同' }]}
+          >
+            <Select placeholder="请选择原合同">
+              {contracts.map(contract => (
+                <Select.Option key={contract.id} value={contract.id}>
+                  {`${contract.id} - ${contract.contractNumber} (${contract.clientId})`}
+                </Select.Option>
+              ))}
+            </Select>
+          </Form.Item>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="amount"
+              label="续签金额"
+            >
+              <Input placeholder="请输入续签金额" />
+            </Form.Item>
+            
+            <Form.Item
+              name="revenue"
+              label="续签收入"
+            >
+              <Input placeholder="请输入续签收入" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="endDate"
+              label="续签结束日期"
+            >
+              <DatePicker format="YYYY-MM-DD" />
+            </Form.Item>
+            
+            <Form.Item
+              name="state"
+              label="续签状态"
+            >
+              <Input placeholder="请输入续签状态:如待审批、已生效等" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="auditStatus"
+              label="审批情况"
+            >
+              <Input placeholder="请输入审批情况" />
+            </Form.Item>
+            
+            <Form.Item
+              name="userId"
+              label="处理用户ID"
+            >
+              <Input placeholder="请输入处理用户ID" />
+            </Form.Item>
+          </div>
+          
+          <Form.Item
+            name="content"
+            label="续签内容描述"
+          >
+            <Input.TextArea rows={4} placeholder="请输入续签内容描述" />
+          </Form.Item>
+          
+          <Form.Item
+            name="auditReasons"
+            label="审批原因"
+          >
+            <Input.TextArea rows={2} placeholder="请输入审批原因" />
+          </Form.Item>
+          
+          {!editingKey && (
+            <Form.Item
+              name="createdTime"
+              label="记录创建时间"
+              rules={[{ required: true, message: '请选择记录创建时间' }]}
+            >
+              <DatePicker showTime format="YYYY-MM-DD HH:mm:ss" />
+            </Form.Item>
+          )}
+        </Form>
+      </Modal>
+    </div>
+            </Form.Item>
+          </div>
+          
+          <Form.Item
+            name="content"
+            label="续签内容描述"
+          >
+            <Input.TextArea rows={4} placeholder="请输入续签内容描述" />
+          </Form.Item>
+          
+          <Form.Item
+            name="auditReasons"
+            label="审批原因"
+          >
+            <Input.TextArea rows={2} placeholder="请输入审批原因" />
+          </Form.Item>
+          
+          {!editingKey && (
+            <Form.Item
+              name="createdTime"
+              label="记录创建时间"
+              rules={[{ required: true, message: '请选择记录创建时间' }]}
+            >
+              <DatePicker showTime format="YYYY-MM-DD HH:mm:ss" />
+            </Form.Item>
+          )}
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default ContractRenews;

+ 443 - 0
src/client/admin/pages/Contracts.tsx

@@ -0,0 +1,443 @@
+import React, { useState } from 'react';
+import { Table, Button, Space, Input, Modal, Form, message, Select, DatePicker, InputNumber } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, FileTextOutlined } from '@ant-design/icons';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { hetongClient, clientClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+import dayjs from 'dayjs';
+
+// 定义类型
+type HetongItem = InferResponseType<typeof hetongClient.$get, 200>['data'][0];
+type HetongListResponse = InferResponseType<typeof hetongClient.$get, 200>;
+type ClientItem = InferResponseType<typeof clientClient.$get, 200>['data'][0];
+
+const Contracts: React.FC = () => {
+  const [form] = Form.useForm();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [editingKey, setEditingKey] = useState<string | null>(null);
+  const [searchText, setSearchText] = useState('');
+  const [clients, setClients] = useState<ClientItem[]>([]);
+  const queryClient = useQueryClient();
+  
+  // 获取客户列表
+  const { data: clientsData } = useQuery(
+    ['clients'],
+    async () => {
+      const response = await clientClient.$get({ query: { page: 1, pageSize: 1000 } });
+      if (response.status !== 200) throw new Error('Failed to fetch clients');
+      return response.json() as Promise<InferResponseType<typeof clientClient.$get, 200>>;
+    },
+    {
+      onSuccess: (result) => {
+        setClients(result.data);
+      },
+    }
+  );
+  
+  // 获取合同列表数据
+  const fetchContracts = ({ page, pageSize }: { page: number; pageSize: number }): Promise<HetongListResponse> => 
+    async () => {
+      const response = await hetongClient.$get({ query: { page, pageSize, keyword: searchText } });
+      if (response.status !== 200) throw new Error('Failed to fetch contracts');
+      return response.json() as Promise<HetongListResponse>;
+    };
+  
+  const { data, isLoading: loading, refetch } = useQuery(
+    ['contracts', pagination.current, pagination.pageSize, searchText],
+    () => fetchContracts({ page: pagination.current, pageSize: pagination.pageSize }),
+    {
+      onSuccess: (result: HetongListResponse) => {
+        setDataSource(result.data);
+        setPagination({
+          ...pagination,
+          total: result.pagination.total,
+        });
+      },
+    }
+  );
+  
+  const [dataSource, setDataSource] = useState<HetongItem[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  
+  // 搜索
+  const handleSearch = () => {
+    setPagination({ ...pagination, current: 1 });
+    refetch();
+  };
+  
+  // 分页变化
+  const handleTableChange = (pagination: any) => {
+    setPagination(pagination);
+    refetch();
+  };
+  
+  // 显示添加/编辑弹窗
+  const showModal = (record?: HetongItem) => {
+    setModalVisible(true);
+    if (record) {
+      setEditingKey(record.id);
+      form.setFieldsValue({
+        id: record.id,
+        contractDate: record.contractDate ? dayjs(record.contractDate) : null,
+        userId: record.userId,
+        clientId: record.clientId,
+        projectId: record.projectId,
+        amount: record.amount,
+        type: record.type,
+        status: record.status,
+        startDate: record.startDate ? dayjs(record.startDate) : null,
+        endDate: record.endDate ? dayjs(record.endDate) : null,
+        description: record.description,
+        contractNumber: record.contractNumber,
+        currency: record.currency,
+        exchangeRate: record.exchangeRate,
+        foreignAmount: record.foreignAmount,
+        filePath: record.filePath,
+      });
+    } else {
+      setEditingKey(null);
+      form.resetFields();
+    }
+  };
+  
+  // 关闭弹窗
+  const handleCancel = () => {
+    setModalVisible(false);
+    form.resetFields();
+  };
+  
+  // 创建合同记录
+  const createContract = useMutation(
+    async (data: any) => {
+      const response = await hetongClient.$post({ json: data });
+      if (!response.ok) throw new Error('Failed to create contract');
+      return response.json();
+    },
+    {
+      onSuccess: () => {
+        message.success('合同记录创建成功');
+        queryClient.invalidateQueries({ queryKey: ['contracts'] });
+        setModalVisible(false);
+      },
+      onError: () => {
+        message.error('操作失败,请重试');
+      }
+    }
+  );
+  
+  // 更新合同记录
+  const updateContract = useMutation(
+    async ({ id, data }: { id: string; data: any }) => {
+      const response = await hetongClient[':id'].$put({ param: { id: parseInt(id, 10) }, json: data });
+      if (!response.ok) throw new Error('Failed to update contract');
+      return response.json();
+    },
+    {
+      onSuccess: () => {
+        message.success('合同记录更新成功');
+        queryClient.invalidateQueries({ queryKey: ['contracts'] });
+        setModalVisible(false);
+      },
+      onError: () => {
+        message.error('操作失败,请重试');
+      }
+    }
+  );
+  
+  // 删除合同记录
+  const deleteContract = useMutation(
+    async (id: string) => {
+      const response = await hetongClient[':id'].$delete({ param: { id: parseInt(id, 10) } });
+      if (!response.ok) throw new Error('Failed to delete contract');
+      return response.json();
+    },
+    {
+      onSuccess: () => {
+        message.success('合同记录删除成功');
+        queryClient.invalidateQueries({ queryKey: ['contracts'] });
+      },
+      onError: () => {
+        message.error('删除失败,请重试');
+      }
+    }
+  );
+  
+  // 提交表单
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      
+      // 处理日期字段
+      if (values.contractDate) values.contractDate = values.contractDate.format('YYYY-MM-DD');
+      if (values.startDate) values.startDate = values.startDate.format('YYYY-MM-DD');
+      if (values.endDate) values.endDate = values.endDate.format('YYYY-MM-DD');
+      
+      if (editingKey) {
+        // 更新操作
+        await updateContract.mutateAsync({ id: editingKey, data: values });
+      } else {
+        // 创建操作
+        await createContract.mutateAsync(values);
+      }
+    } catch (error) {
+      message.error('操作失败,请重试');
+    }
+  };
+  
+  // 表格列定义
+  const columns = [
+    {
+      title: '合同ID',
+      dataIndex: 'id',
+      key: 'id',
+    },
+    {
+      title: '合同编号',
+      dataIndex: 'contractNumber',
+      key: 'contractNumber',
+    },
+    {
+      title: '客户',
+      dataIndex: 'clientId',
+      key: 'clientId',
+      render: (clientId: string) => {
+        const client = clients.find(c => c.id.toString() === clientId);
+        return client ? client.companyName : '-';
+      },
+    },
+    {
+      title: '合同类型',
+      dataIndex: 'type',
+      key: 'type',
+    },
+    {
+      title: '合同金额',
+      dataIndex: 'amount',
+      key: 'amount',
+      render: (amount: number, record: HetongItem) => 
+        `${record.currency || 'CNY'} ${amount.toFixed(2)}`,
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+    },
+    {
+      title: '有效期',
+      key: 'dateRange',
+      render: (_: any, record: HetongItem) =>
+        `${record.startDate ? new Date(record.startDate).toLocaleDateString() : '-'} ~ ${record.endDate ? new Date(record.endDate).toLocaleDateString() : '-'}`
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: HetongItem) => (
+        <Space size="middle">
+          <Button 
+            type="text" 
+            icon={<EditOutlined />} 
+            onClick={() => showModal(record)}
+          >
+            编辑
+          </Button>
+          <Button 
+            type="text" 
+            danger 
+            icon={<DeleteOutlined />} 
+            onClick={() => deleteContract.mutate(record.id.toString())}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+  
+  return (
+    <div className="p-4">
+      <div className="flex justify-between items-center mb-4">
+        <h2 className="text-xl font-bold">合同管理</h2>
+        <Button 
+          type="primary" 
+          icon={<PlusOutlined />} 
+          onClick={() => showModal()}
+        >
+          添加合同
+        </Button>
+      </div>
+      
+      <div className="mb-4">
+        <Input
+          placeholder="搜索合同编号或客户名称"
+          prefix={<SearchOutlined />}
+          value={searchText}
+          onChange={(e) => setSearchText(e.target.value)}
+          onPressEnter={handleSearch}
+          style={{ width: 300 }}
+        />
+        <Button type="default" onClick={handleSearch} style={{ marginLeft: 8 }}>
+          搜索
+        </Button>
+      </div>
+      
+      <Table
+        columns={columns}
+        dataSource={dataSource}
+        rowKey="id"
+        loading={loading}
+        pagination={pagination}
+        onChange={handleTableChange}
+        bordered
+      />
+      
+      <Modal
+        title={editingKey ? "编辑合同记录" : "添加合同记录"}
+        open={modalVisible}
+        onCancel={handleCancel}
+        footer={[
+          <Button key="cancel" onClick={handleCancel}>
+            取消
+          </Button>,
+          <Button key="submit" type="primary" onClick={handleSubmit}>
+            确定
+          </Button>,
+        ]}
+        width={800}
+      >
+        <Form form={form} layout="vertical">
+          {!editingKey && (
+            <Form.Item
+              name="id"
+              label="合同ID"
+              rules={[{ required: true, message: '请输入合同ID' }]}
+            >
+              <Input placeholder="请输入合同ID" />
+            </Form.Item>
+          )}
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="contractNumber"
+              label="合同编号"
+              rules={[{ required: true, message: '请输入合同编号' }]}
+            >
+              <Input placeholder="请输入合同编号" />
+            </Form.Item>
+            
+            <Form.Item
+              name="contractDate"
+              label="合同日期"
+              rules={[{ required: true, message: '请选择合同日期' }]}
+            >
+              <DatePicker format="YYYY-MM-DD" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="clientId"
+              label="客户"
+              rules={[{ required: true, message: '请选择客户' }]}
+            >
+              <Select placeholder="请选择客户">
+                {clients.map(client => (
+                  <Select.Option key={client.id} value={client.id.toString()}>
+                    {client.companyName}
+                  </Select.Option>
+                ))}
+              </Select>
+            </Form.Item>
+            
+            <Form.Item
+              name="type"
+              label="合同类型"
+              rules={[{ required: true, message: '请输入合同类型' }]}
+            >
+              <Input placeholder="请输入合同类型" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="amount"
+              label="合同金额"
+              rules={[{ required: true, message: '请输入合同金额' }]}
+            >
+              <InputNumber 
+                style={{ width: '100%' }} 
+                formatter={value => `¥ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
+                parser={value => value!.replace(/\¥\s?|(,*)/g, '')}
+                placeholder="请输入合同金额"
+              />
+            </Form.Item>
+            
+            <Form.Item
+              name="currency"
+              label="货币类型"
+              initialValue="CNY"
+              rules={[{ required: true, message: '请输入货币类型' }]}
+            >
+              <Input placeholder="请输入货币类型" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="startDate"
+              label="开始日期"
+              rules={[{ required: true, message: '请选择开始日期' }]}
+            >
+              <DatePicker format="YYYY-MM-DD" />
+            </Form.Item>
+            
+            <Form.Item
+              name="endDate"
+              label="结束日期"
+              rules={[{ required: true, message: '请选择结束日期' }]}
+            >
+              <DatePicker format="YYYY-MM-DD" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="status"
+              label="合同状态"
+              rules={[{ required: true, message: '请输入合同状态' }]}
+            >
+              <Input placeholder="请输入合同状态:如生效中、已结束等" />
+            </Form.Item>
+            
+            <Form.Item
+              name="userId"
+              label="关联用户ID"
+              rules={[{ required: true, message: '请输入关联用户ID' }]}
+            >
+              <Input placeholder="请输入关联用户ID" />
+            </Form.Item>
+          </div>
+          
+          <Form.Item
+            name="description"
+            label="合同描述"
+          >
+            <Input.TextArea rows={4} placeholder="请输入合同描述" />
+          </Form.Item>
+          
+          <Form.Item
+            name="filePath"
+            label="合同文件路径"
+          >
+            <Input placeholder="请输入合同文件路径" prefix={<FileTextOutlined />} />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default Contracts;

+ 449 - 0
src/client/admin/pages/Expenses.tsx

@@ -0,0 +1,449 @@
+import React, { useState } from 'react';
+import { Table, Button, Space, Input, Modal, Form, message, Select, DatePicker } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import type { InferResponseType } from 'hono/client';
+import { expenseClient, clientClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+import dayjs from 'dayjs';
+
+// 定义类型
+type ExpenseItem = InferResponseType<typeof expenseClient.$get, 200>['data'][0];
+type ExpenseListResponse = InferResponseType<typeof expenseClient.$get, 200>;
+type ClientItem = InferResponseType<typeof clientClient.$get, 200>['data'][0];
+
+const Expenses: React.FC = () => {
+  const [form] = Form.useForm();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [editingKey, setEditingKey] = useState<string | null>(null);
+  const [searchText, setSearchText] = useState('');
+  const [clients, setClients] = useState<ClientItem[]>([]);
+  
+  // 获取客户列表
+  const queryClient = useQueryClient();
+  
+  // 获取客户列表
+  const { data: clientsData } = useQuery(
+    ['clients'],
+    () => clientClient.$get({ query: { page: 1, pageSize: 1000 } }) as Promise<InferResponseType<typeof clientClient.$get, 200>>,
+    {
+      onSuccess: (result) => {
+        setClients(result.data);
+      },
+    }
+  );
+  
+  // 获取费用列表数据
+  // 获取费用列表数据
+  const fetchExpenses = ({ page, pageSize }: { page: number; pageSize: number }): Promise<ExpenseListResponse> =>
+    expenseClient.$get({ query: { page, pageSize, keyword: searchText } });
+  
+  const { data, isLoading: loading, refetch } = useQuery(
+    ['expenses', pagination.current, pagination.pageSize, searchText],
+    () => fetchExpenses({ page: pagination.current, pageSize: pagination.pageSize }) as Promise<ExpenseListResponse>,
+    {
+      onSuccess: (result) => {
+        setDataSource(result.data);
+        setPagination({
+          ...pagination,
+          total: result.pagination.total,
+        });
+      },
+    }
+  );
+  
+  // 创建费用记录
+  const createExpense = useMutation(
+    (data: any) => expenseClient.$post({ json: data }),
+    {
+      onSuccess: () => {
+        message.success('费用记录创建成功');
+        queryClient.invalidateQueries(['expenses']);
+      },
+      onError: () => {
+        message.error('操作失败,请重试');
+      }
+    }
+  );
+  
+  // 更新费用记录
+  const updateExpense = useMutation(
+    ({ id, data }: { id: string; data: any }) => expenseClient[':id'].$put({ param: { id }, json: data }),
+    {
+      onSuccess: () => {
+        message.success('费用记录更新成功');
+        queryClient.invalidateQueries(['expenses']);
+      },
+      onError: () => {
+        message.error('操作失败,请重试');
+      }
+    }
+  );
+  
+  // 删除费用记录
+  const deleteExpense = useMutation(
+    (id: string) => expenseClient[':id'].$delete({ param: { id } }),
+    {
+      onSuccess: () => {
+        message.success('费用记录删除成功');
+        queryClient.invalidateQueries(['expenses']);
+      },
+      onError: () => {
+        message.error('删除失败,请重试');
+      }
+    }
+  );
+  
+  // 初始化获取客户数据
+  React.useEffect(() => {
+    fetchClients();
+  }, [fetchClients]);
+  
+  const [dataSource, setDataSource] = useState<ExpenseItem[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  
+  // 搜索
+  const handleSearch = () => {
+    run({ page: 1, pageSize: pagination.pageSize });
+  };
+  
+  // 分页变化
+  const handleTableChange = (pagination: any) => {
+    setPagination(pagination);
+    run({ page: pagination.current, pageSize: pagination.pageSize });
+  };
+  
+  // 显示添加/编辑弹窗
+  const showModal = (record?: ExpenseItem) => {
+    setModalVisible(true);
+    if (record) {
+      setEditingKey(record.id.toString());
+      form.setFieldsValue({
+        id: record.id,
+        expenseDate: record.expenseDate ? dayjs(record.expenseDate) : null,
+        userId: record.userId,
+        type: record.type,
+        amount: record.amount,
+        clientId: record.clientId,
+        projectId: record.projectId,
+        department: record.department,
+        description: record.description,
+        status: record.status,
+        approver: record.approver,
+        approveDate: record.approveDate ? dayjs(record.approveDate) : null,
+        reimbursementDate: record.reimbursementDate ? dayjs(record.reimbursementDate) : null,
+        paymentMethod: record.paymentMethod,
+        invoiceNumber: record.invoiceNumber,
+        currency: record.currency,
+        exchangeRate: record.exchangeRate,
+        foreignAmount: record.foreignAmount ? parseFloat(record.foreignAmount) : undefined,
+      });
+    } else {
+      setEditingKey(null);
+      form.resetFields();
+    }
+  };
+  
+  // 关闭弹窗
+  const handleCancel = () => {
+    setModalVisible(false);
+    form.resetFields();
+  };
+  
+  // 提交表单
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      
+      // 处理日期字段
+      if (values.expenseDate) values.expenseDate = values.expenseDate.format('YYYY-MM-DD');
+      if (values.approveDate) values.approveDate = values.approveDate.format('YYYY-MM-DD');
+      if (values.reimbursementDate) values.reimbursementDate = values.reimbursementDate.format('YYYY-MM-DD');
+      
+      if (editingKey) {
+        // 更新操作
+        await expenseClient[':id'].$put({
+          param: { id: editingKey },
+          json: values,
+        });
+        message.success('费用记录更新成功');
+      } else {
+        // 创建操作
+        await expenseClient.$post({ json: values });
+        message.success('费用记录创建成功');
+      }
+      
+      setModalVisible(false);
+      run({ page: pagination.current, pageSize: pagination.pageSize });
+    } catch (error) {
+      message.error('操作失败,请重试');
+    }
+  };
+  
+  // 删除操作
+  const handleDelete = async (id: string) => {
+    try {
+      await expenseClient[':id'].$delete({ param: { id } });
+      message.success('费用记录删除成功');
+      run({ page: pagination.current, pageSize: pagination.pageSize });
+    } catch (error) {
+      message.error('删除失败,请重试');
+    }
+  };
+  
+  // 表格列定义
+  const columns = [
+    {
+      title: '费用ID',
+      dataIndex: 'id',
+      key: 'id',
+    },
+    {
+      title: '费用日期',
+      dataIndex: 'expenseDate',
+      key: 'expenseDate',
+      render: (date: string) => date ? new Date(date).toLocaleDateString() : '-',
+    },
+    {
+      title: '费用类型',
+      dataIndex: 'type',
+      key: 'type',
+    },
+    {
+      title: '金额',
+      dataIndex: 'amount',
+      key: 'amount',
+      render: (amount: number) => `¥${amount.toFixed(2)}`,
+    },
+    {
+      title: '客户',
+      dataIndex: 'clientId',
+      key: 'clientId',
+      render: (clientId: string) => {
+        const client = clients.find(c => c.id.toString() === clientId);
+        return client ? client.companyName : '-';
+      },
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+    },
+    {
+      title: '操作用户',
+      dataIndex: 'userId',
+      key: 'userId',
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: ExpenseItem) => (
+        <Space size="middle">
+          <Button 
+            type="text" 
+            icon={<EditOutlined />} 
+            onClick={() => showModal(record)}
+          >
+            编辑
+          </Button>
+          <Button 
+            type="text" 
+            danger 
+            icon={<DeleteOutlined />} 
+            onClick={() => handleDelete(record.id)}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+  
+  return (
+    <div className="p-4">
+      <div className="flex justify-between items-center mb-4">
+        <h2 className="text-xl font-bold">费用管理</h2>
+        <Button 
+          type="primary" 
+          icon={<PlusOutlined />} 
+          onClick={() => showModal()}
+        >
+          添加费用
+        </Button>
+      </div>
+      
+      <div className="mb-4">
+        <Input
+          placeholder="搜索费用类型或描述"
+          prefix={<SearchOutlined />}
+          value={searchText}
+          onChange={(e) => setSearchText(e.target.value)}
+          onPressEnter={handleSearch}
+          style={{ width: 300 }}
+        />
+        <Button type="default" onClick={handleSearch} style={{ marginLeft: 8 }}>
+          搜索
+        </Button>
+      </div>
+      
+      <Table
+        columns={columns}
+        dataSource={dataSource}
+        rowKey="id"
+        loading={loading}
+        pagination={pagination}
+        onChange={handleTableChange}
+        bordered
+      />
+      
+      <Modal
+        title={editingKey ? "编辑费用记录" : "添加费用记录"}
+        open={modalVisible}
+        onCancel={handleCancel}
+        footer={[
+          <Button key="cancel" onClick={handleCancel}>
+            取消
+          </Button>,
+          <Button key="submit" type="primary" onClick={handleSubmit}>
+            确定
+          </Button>,
+        ]}
+        width={800}
+      >
+        <Form form={form} layout="vertical">
+          {!editingKey && (
+            <Form.Item
+              name="id"
+              label="费用记录ID"
+              rules={[{ required: true, message: '请输入费用记录ID' }]}
+            >
+              <Input placeholder="请输入费用记录ID" />
+            </Form.Item>
+          )}
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="expenseDate"
+              label="费用发生日期"
+              rules={[{ required: true, message: '请选择费用发生日期' }]}
+            >
+              <DatePicker format="YYYY-MM-DD" />
+            </Form.Item>
+            
+            <Form.Item
+              name="type"
+              label="费用类型"
+              rules={[{ required: true, message: '请输入费用类型' }]}
+            >
+              <Input placeholder="请输入费用类型" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="amount"
+              label="费用金额"
+              rules={[{ required: true, message: '请输入费用金额' }]}
+            >
+              <Input type="number" placeholder="请输入费用金额" />
+            </Form.Item>
+            
+            <Form.Item
+              name="clientId"
+              label="关联客户"
+            >
+              <Select placeholder="请选择客户">
+                {clients.map(client => (
+                  <Select.Option key={client.id} value={client.id.toString()}>
+                    {client.companyName}
+                  </Select.Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="status"
+              label="费用状态"
+              rules={[{ required: true, message: '请输入费用状态' }]}
+            >
+              <Input placeholder="请输入费用状态:如审批中、已报销等" />
+            </Form.Item>
+            
+            <Form.Item
+              name="userId"
+              label="操作用户ID"
+              rules={[{ required: true, message: '请输入操作用户ID' }]}
+            >
+              <Input placeholder="请输入操作用户ID" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="paymentMethod"
+              label="支付方式"
+            >
+              <Input placeholder="请输入支付方式" />
+            </Form.Item>
+            
+            <Form.Item
+              name="invoiceNumber"
+              label="发票号码"
+            >
+              <Input placeholder="请输入发票号码" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="currency"
+              label="货币类型"
+              initialValue="CNY"
+            >
+              <Input placeholder="请输入货币类型" />
+            </Form.Item>
+            
+            <Form.Item
+              name="exchangeRate"
+              label="汇率"
+              initialValue={1}
+            >
+              <Input type="number" step="0.0001" placeholder="请输入汇率" />
+            </Form.Item>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="approveDate"
+              label="审批日期"
+            >
+              <DatePicker format="YYYY-MM-DD" />
+            </Form.Item>
+            
+            <Form.Item
+              name="reimbursementDate"
+              label="报销日期"
+            >
+              <DatePicker format="YYYY-MM-DD" />
+            </Form.Item>
+          </div>
+          
+          <Form.Item
+            name="description"
+            label="费用描述"
+          >
+            <Input.TextArea rows={4} placeholder="请输入费用描述" />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default Expenses;

+ 369 - 0
src/client/admin/pages/Files.tsx

@@ -0,0 +1,369 @@
+import React, { useState } from 'react';
+import { Table, Button, Space, Input, Modal, Form, message, Upload, Select } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined } from '@ant-design/icons';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { fileClient, clientClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+import dayjs from 'dayjs';
+
+// 定义类型
+type FileItem = InferResponseType<typeof fileClient.$get, 200>['data'][0];
+type FileListResponse = InferResponseType<typeof fileClient.$get, 200>;
+type ClientItem = InferResponseType<typeof clientClient.$get, 200>['data'][0];
+
+const Files: React.FC = () => {
+  const [form] = Form.useForm();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [editingKey, setEditingKey] = useState<string | null>(null);
+  const [searchText, setSearchText] = useState('');
+  const [clients, setClients] = useState<ClientItem[]>([]);
+  const queryClient = useQueryClient();
+  
+  // 获取客户列表
+  const { data: clientsData } = useQuery(
+    ['clients'],
+    () => clientClient.$get({ query: { page: 1, pageSize: 1000 } }) as Promise<InferResponseType<typeof clientClient.$get, 200>>,
+    {
+      onSuccess: (result) => {
+        setClients(result.data);
+      },
+    }
+  );
+  
+  // 获取文件列表数据
+  const fetchFiles = ({ page, pageSize }: { page: number; pageSize: number }): Promise<FileListResponse> => 
+    fileClient.$get({ query: { page, pageSize, keyword: searchText } }) as Promise<FileListResponse>;
+  
+  const { data, isLoading: loading, refetch } = useQuery(
+    ['files', pagination.current, pagination.pageSize, searchText],
+    () => fetchFiles({ page: pagination.current, pageSize: pagination.pageSize }),
+    {
+      onSuccess: (result) => {
+        setDataSource(result.data);
+        setPagination({
+          ...pagination,
+          total: result.pagination.total,
+        });
+      },
+    }
+  );
+  
+  const [dataSource, setDataSource] = useState<FileItem[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  
+  // 搜索
+  const handleSearch = () => {
+    setPagination({ ...pagination, current: 1 });
+    refetch();
+  };
+  
+  // 分页变化
+  const handleTableChange = (pagination: any) => {
+    setPagination(pagination);
+    refetch();
+  };
+  
+  // 显示添加/编辑弹窗
+  const showModal = (record?: FileItem) => {
+    setModalVisible(true);
+    if (record) {
+      setEditingKey(record.id);
+      form.setFieldsValue({
+        id: record.id,
+        name: record.name,
+        type: record.type,
+        size: record.size,
+        path: record.path,
+        description: record.description,
+        uploadUserId: record.uploadUserId,
+        uploadTime: record.uploadTime ? dayjs(record.uploadTime) : null,
+        lastUpdated: record.lastUpdated ? dayjs(record.lastUpdated) : null,
+      });
+    } else {
+      setEditingKey(null);
+      form.resetFields();
+    }
+  };
+  
+  // 关闭弹窗
+  const handleCancel = () => {
+    setModalVisible(false);
+    form.resetFields();
+  };
+  
+  // 创建文件记录
+  const createFile = useMutation(
+    (data: any) => fileClient.$post({ json: data }),
+    {
+      onSuccess: () => {
+        message.success('文件记录创建成功');
+        queryClient.invalidateQueries(['files']);
+        setModalVisible(false);
+      },
+      onError: () => {
+        message.error('操作失败,请重试');
+      }
+    }
+  );
+  
+  // 更新文件记录
+  const updateFile = useMutation(
+    ({ id, data }: { id: string; data: any }) => fileClient[':id'].$put({ param: { id }, json: data }),
+    {
+      onSuccess: () => {
+        message.success('文件记录更新成功');
+        queryClient.invalidateQueries(['files']);
+        setModalVisible(false);
+      },
+      onError: () => {
+        message.error('操作失败,请重试');
+      }
+    }
+  );
+  
+  // 删除文件记录
+  const deleteFile = useMutation(
+    (id: string) => fileClient[':id'].$delete({ param: { id } }),
+    {
+      onSuccess: () => {
+        message.success('文件记录删除成功');
+        queryClient.invalidateQueries(['files']);
+      },
+      onError: () => {
+        message.error('删除失败,请重试');
+      }
+    }
+  );
+  
+  // 提交表单
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      
+      // 处理日期字段
+      if (values.uploadTime) values.uploadTime = values.uploadTime.format('YYYY-MM-DD HH:mm:ss');
+      if (values.lastUpdated) values.lastUpdated = values.lastUpdated.format('YYYY-MM-DD HH:mm:ss');
+      
+      if (editingKey) {
+        // 更新操作
+        await updateFile.mutateAsync({ id: editingKey, data: values });
+      } else {
+        // 创建操作
+        await createFile.mutateAsync(values);
+      }
+    } catch (error) {
+      message.error('操作失败,请重试');
+    }
+  };
+  
+  // 处理文件上传
+  const handleUpload = async (file: any) => {
+    // 这里实现文件上传逻辑
+    message.success('文件上传成功');
+    return { status: 'done', response: { data: { path: `/uploads/${file.name}` } } };
+  };
+  
+  // 表格列定义
+  const columns = [
+    {
+      title: '文件ID',
+      dataIndex: 'id',
+      key: 'id',
+    },
+    {
+      title: '文件名称',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '文件类型',
+      dataIndex: 'type',
+      key: 'type',
+    },
+    {
+      title: '文件大小',
+      dataIndex: 'size',
+      key: 'size',
+      render: (size: number) => size ? `${(size / 1024).toFixed(2)} KB` : '-',
+    },
+    {
+      title: '上传时间',
+      dataIndex: 'uploadTime',
+      key: 'uploadTime',
+      render: (time: string) => time ? new Date(time).toLocaleString() : '-',
+    },
+    {
+      title: '上传用户',
+      dataIndex: 'uploadUserId',
+      key: 'uploadUserId',
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: FileItem) => (
+        <Space size="middle">
+          <Button 
+            type="text" 
+            icon={<EditOutlined />} 
+            onClick={() => showModal(record)}
+          >
+            编辑
+          </Button>
+          <Button 
+            type="text" 
+            danger 
+            icon={<DeleteOutlined />} 
+            onClick={() => deleteFile.mutate(record.id)}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+  
+  return (
+    <div className="p-4">
+      <div className="flex justify-between items-center mb-4">
+        <h2 className="text-xl font-bold">文件管理</h2>
+        <Button 
+          type="primary" 
+          icon={<PlusOutlined />} 
+          onClick={() => showModal()}
+        >
+          添加文件
+        </Button>
+      </div>
+      
+      <div className="mb-4">
+        <Input
+          placeholder="搜索文件名称或类型"
+          prefix={<SearchOutlined />}
+          value={searchText}
+          onChange={(e) => setSearchText(e.target.value)}
+          onPressEnter={handleSearch}
+          style={{ width: 300 }}
+        />
+        <Button type="default" onClick={handleSearch} style={{ marginLeft: 8 }}>
+          搜索
+        </Button>
+      </div>
+      
+      <Table
+        columns={columns}
+        dataSource={dataSource}
+        rowKey="id"
+        loading={loading}
+        pagination={pagination}
+        onChange={handleTableChange}
+        bordered
+      />
+      
+      <Modal
+        title={editingKey ? "编辑文件记录" : "添加文件记录"}
+        open={modalVisible}
+        onCancel={handleCancel}
+        footer={[
+          <Button key="cancel" onClick={handleCancel}>
+            取消
+          </Button>,
+          <Button key="submit" type="primary" onClick={handleSubmit}>
+            确定
+          </Button>,
+        ]}
+        width={600}
+      >
+        <Form form={form} layout="vertical">
+          {!editingKey && (
+            <Form.Item
+              name="id"
+              label="文件ID"
+              rules={[{ required: true, message: '请输入文件ID' }]}
+            >
+              <Input placeholder="请输入文件ID" />
+            </Form.Item>
+          )}
+          
+          <Form.Item
+            name="name"
+            label="文件名称"
+            rules={[{ required: true, message: '请输入文件名称' }]}
+          >
+            <Input placeholder="请输入文件名称" />
+          </Form.Item>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="type"
+              label="文件类型"
+            >
+              <Input placeholder="请输入文件类型" />
+            </Form.Item>
+            
+            <Form.Item
+              name="size"
+              label="文件大小(字节)"
+            >
+              <Input type="number" placeholder="请输入文件大小" />
+            </Form.Item>
+          </div>
+          
+          <Form.Item
+            name="path"
+            label="文件路径"
+            rules={[{ required: true, message: '请输入文件路径' }]}
+          >
+            <Input placeholder="请输入文件路径" />
+          </Form.Item>
+          
+          <Form.Item
+            name="upload"
+            label="文件上传"
+            valuePropName="fileList"
+            extra="请上传文件"
+          >
+            <Upload
+              name="file"
+              customRequest={handleUpload}
+              showUploadList={{ showRemoveIcon: true }}
+              maxCount={1}
+            >
+              <Button icon={<UploadOutlined />}>点击上传</Button>
+            </Upload>
+          </Form.Item>
+          
+          <div className="grid grid-cols-2 gap-4">
+            <Form.Item
+              name="uploadUserId"
+              label="上传用户ID"
+              rules={[{ required: true, message: '请输入上传用户ID' }]}
+            >
+              <Input placeholder="请输入上传用户ID" />
+            </Form.Item>
+            
+            <Form.Item
+              name="uploadTime"
+              label="上传时间"
+              rules={[{ required: true, message: '请选择上传时间' }]}
+            >
+              <DatePicker showTime format="YYYY-MM-DD HH:mm:ss" />
+            </Form.Item>
+          </div>
+          
+          <Form.Item
+            name="description"
+            label="文件描述"
+          >
+            <Input.TextArea rows={4} placeholder="请输入文件描述" />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default Files;

+ 200 - 0
src/client/admin/pages/Logs.tsx

@@ -0,0 +1,200 @@
+import React, { useState } from 'react';
+import { Table, Button, Space, Input, Select, DatePicker, Form } from 'antd';
+import { SearchOutlined, FilterOutlined } from '@ant-design/icons';
+import { useQuery } from '@tanstack/react-query';
+import { logfileClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+import dayjs from 'dayjs';
+
+// 定义类型
+type LogfileItem = InferResponseType<typeof logfileClient.$get, 200>['data'][0];
+type LogfileListResponse = InferResponseType<typeof logfileClient.$get, 200>;
+
+const { RangePicker } = DatePicker;
+
+const Logs: React.FC = () => {
+  const [form] = Form.useForm();
+  const [searchText, setSearchText] = useState('');
+  const [filters, setFilters] = useState({
+    class: '',
+    action: '',
+    userId: '',
+    dateRange: [] as [dayjs.Dayjs | null, dayjs.Dayjs | null]
+  });
+  
+  // 获取日志列表数据
+  const fetchLogs = async ({ page, pageSize }: { page: number; pageSize: number }): Promise<LogfileListResponse> => {
+    const queryParams: Record<string, any> = { page, pageSize };
+    
+    if (searchText) queryParams.keyword = searchText;
+    if (filters.class) queryParams.class = filters.class;
+    if (filters.action) queryParams.action = filters.action;
+    if (filters.userId) queryParams.userId = filters.userId;
+    if (filters.dateRange?.[0]) queryParams.startDate = filters.dateRange[0].format('YYYY-MM-DD');
+    if (filters.dateRange?.[1]) queryParams.endDate = filters.dateRange[1].format('YYYY-MM-DD');
+    
+    const response = await logfileClient.$get({ query: queryParams });
+    if (response.status !== 200) throw new Error('Failed to fetch logs');
+    return response.json() as Promise<LogfileListResponse>;
+  };
+  
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  
+  const { data, isLoading: loading, refetch } = useQuery(
+    ['logs', pagination.current, pagination.pageSize, searchText, filters],
+    () => fetchLogs({ page: pagination.current, pageSize: pagination.pageSize }),
+    {
+      onSuccess: (result) => {
+        setPagination({
+          ...pagination,
+          total: result.pagination.total,
+        });
+      },
+    }
+  );
+  
+  // 搜索
+  const handleSearch = () => {
+    setPagination({ ...pagination, current: 1 });
+    refetch();
+  };
+  
+  // 过滤条件变化
+  const handleFilterChange = (values: any) => {
+    setFilters({
+      ...filters,
+      class: values.class || '',
+      action: values.action || '',
+      userId: values.userId || '',
+      dateRange: values.dateRange || []
+    });
+    setPagination({ ...pagination, current: 1 });
+    refetch();
+  };
+  
+  // 分页变化
+  const handleTableChange = (pagination: any) => {
+    setPagination(pagination);
+    refetch();
+  };
+  
+  // 重置过滤条件
+  const handleResetFilters = () => {
+    form.resetFields();
+    setFilters({
+      class: '',
+      action: '',
+      userId: '',
+      dateRange: []
+    });
+    setSearchText('');
+    setPagination({ ...pagination, current: 1 });
+    refetch();
+  };
+  
+  // 表格列定义
+  const columns = [
+    {
+      title: '日志ID',
+      dataIndex: 'id',
+      key: 'id',
+    },
+    {
+      title: '关联记录ID',
+      dataIndex: 'relatedId',
+      key: 'relatedId',
+    },
+    {
+      title: '类别',
+      dataIndex: 'class',
+      key: 'class',
+    },
+    {
+      title: '操作',
+      dataIndex: 'action',
+      key: 'action',
+    },
+    {
+      title: '操作用户',
+      dataIndex: 'userId',
+      key: 'userId',
+    },
+    {
+      title: '操作时间',
+      dataIndex: 'logTime',
+      key: 'logTime',
+      render: (time: string) => time ? new Date(time).toLocaleString() : '-',
+    },
+    {
+      title: '操作原因',
+      dataIndex: 'reason',
+      key: 'reason',
+    },
+  ];
+  
+  return (
+    <div className="p-4">
+      <div className="flex justify-between items-center mb-4">
+        <h2 className="text-xl font-bold">系统日志管理</h2>
+      </div>
+      
+      <div className="bg-white p-4 mb-4 rounded-lg shadow-sm">
+        <Form form={form} layout="inline" onFinish={handleFilterChange}>
+          <Form.Item name="class" label="日志类别">
+            <Input placeholder="请输入日志类别" style={{ width: 150 }} />
+          </Form.Item>
+          <Form.Item name="action" label="操作动作">
+            <Input placeholder="请输入操作动作" style={{ width: 150 }} />
+          </Form.Item>
+          <Form.Item name="userId" label="操作用户ID">
+            <Input placeholder="请输入用户ID" style={{ width: 150 }} />
+          </Form.Item>
+          <Form.Item name="dateRange" label="操作时间">
+            <RangePicker format="YYYY-MM-DD" />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit" icon={<FilterOutlined />}>
+              筛选
+            </Button>
+          </Form.Item>
+          <Form.Item>
+            <Button onClick={handleResetFilters}>
+              重置
+            </Button>
+          </Form.Item>
+        </Form>
+        
+        <div className="mt-4">
+          <Input
+            placeholder="搜索日志内容"
+            prefix={<SearchOutlined />}
+            value={searchText}
+            onChange={(e) => setSearchText(e.target.value)}
+            onPressEnter={handleSearch}
+            style={{ width: 300 }}
+          />
+          <Button type="default" onClick={handleSearch} style={{ marginLeft: 8 }}>
+            搜索
+          </Button>
+        </div>
+      </div>
+      
+      <Table
+        columns={columns}
+        dataSource={data?.data || []}
+        rowKey="id"
+        loading={loading}
+        pagination={pagination}
+        onChange={handleTableChange}
+        bordered
+        size="middle"
+      />
+    </div>
+  );
+};
+
+export default Logs;

+ 48 - 0
src/client/admin/routes.tsx

@@ -9,6 +9,14 @@ import { UsersPage } from './pages/Users';
 import { CustomersPage } from './pages/Customers';
 import { OpportunitiesPage } from './pages/Opportunities';
 import { FollowUpsPage } from './pages/FollowUps';
+import AreasPage from './pages/Areas';
+import ClientsPage from './pages/Clients';
+import ExpensesPage from './pages/Expenses';
+import FilesPage from './pages/Files';
+import ContractsPage from './pages/Contracts';
+import ContractRenewsPage from './pages/ContractRenews';
+import ContactsPage from './pages/Contacts';
+import LogsPage from './pages/Logs';
 import { LoginPage } from './pages/Login';
 
 export const router = createBrowserRouter([
@@ -57,6 +65,46 @@ export const router = createBrowserRouter([
         element: <FollowUpsPage />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'areas',
+        element: <AreasPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'clients',
+        element: <ClientsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'expenses',
+        element: <ExpensesPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'files',
+        element: <FilesPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'contracts',
+        element: <ContractsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'contract-renews',
+        element: <ContractRenewsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'contacts',
+        element: <ContactsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'logs',
+        element: <LogsPage />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,

+ 34 - 1
src/client/api.ts

@@ -1,7 +1,8 @@
 import axios, { isAxiosError } from 'axios';
 import { hc } from 'hono/client'
 import type {
-  AuthRoutes, UserRoutes, RoleRoutes, CustomerRoutes, OpportunityRoutes, FollowUpRoutes
+  AuthRoutes, UserRoutes, RoleRoutes, CustomerRoutes, OpportunityRoutes, FollowUpRoutes,
+  AreaRoutes, ClientRoutes, ExpenseRoutes, FileRoutes, HetongRoutes, HetongRenewRoutes, LinkmanRoutes, LogfileRoutes
 } from '@/server/api';
 
 // 创建 axios 适配器
@@ -82,3 +83,35 @@ export const opportunityClient = hc<OpportunityRoutes>('/', {
 export const followUpClient = hc<FollowUpRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1['follow-ups'];
+
+export const areaClient = hc<AreaRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.areas;
+
+export const clientClient = hc<ClientRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.clients;
+
+export const expenseClient = hc<ExpenseRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.expenses;
+
+export const fileClient = hc<FileRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.files;
+
+export const hetongClient = hc<HetongRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.contracts;
+
+export const hetongRenewClient = hc<HetongRenewRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['contracts-renew'];
+
+export const linkmanClient = hc<LinkmanRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.contacts;
+
+export const logfileClient = hc<LogfileRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.logs;