瀏覽代碼

✨ feat(order): 新增订单记录和跟单记录管理功能

- 在后台管理菜单中添加订单记录和跟单记录入口
- 实现订单记录CRUD功能,包含公司、订单号、金额等字段
- 实现跟单记录CRUD功能,包含编号、资源、下次联系等字段
- 创建OrderRecord和FollowUpRecord实体及相关API路由
- 添加前端页面组件,支持搜索、分页、编辑、删除操作
yourname 8 月之前
父節點
當前提交
aedcc922da

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

@@ -106,6 +106,18 @@ export const useMenu = () => {
           path: '/admin/clients',
           permission: 'client:manage'
         },
+        {
+          key: 'order-records',
+          label: '订单记录',
+          path: '/admin/order-records',
+          permission: 'order:manage'
+        },
+        {
+          key: 'follow-up-records',
+          label: '跟单记录',
+          path: '/admin/follow-up-records',
+          permission: 'followup:manage'
+        },
       ]
     },
     {

+ 327 - 0
src/client/admin/pages/FollowUpRecords.tsx

@@ -0,0 +1,327 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, DatePicker, message, Space, Popconfirm } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
+import type { ColumnsType } from 'antd/es/table';
+import { InferRequestType, InferResponseType } from 'hono/client';
+import { followUpRecordClient } from '@/client/api';
+import dayjs from 'dayjs';
+
+const { TextArea } = Input;
+
+type FollowUpRecordType = InferResponseType<typeof followUpRecordClient.$get, 200>['data'][0];
+type CreateRequest = InferRequestType<typeof followUpRecordClient.$post>['json'];
+type UpdateRequest = InferRequestType<typeof followUpRecordClient[':id']['$put']>['json'];
+
+const FollowUpRecords: React.FC = () => {
+  const [loading, setLoading] = useState(false);
+  const [data, setData] = useState<FollowUpRecordType[]>([]);
+  const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
+  const [modalVisible, setModalVisible] = useState(false);
+  const [editingRecord, setEditingRecord] = useState<FollowUpRecordType | null>(null);
+  const [searchForm] = Form.useForm();
+  const [form] = Form.useForm();
+  const [searchParams, setSearchParams] = useState<any>({});
+
+  // 定义表格列
+  const columns: ColumnsType<FollowUpRecordType> = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '编号',
+      dataIndex: 'serialNumber',
+      key: 'serialNumber',
+      width: 150,
+    },
+    {
+      title: '收录资源',
+      dataIndex: 'resourceName',
+      key: 'resourceName',
+      width: 250,
+      ellipsis: true,
+    },
+    {
+      title: '下次联系时间',
+      dataIndex: 'nextContactTime',
+      key: 'nextContactTime',
+      width: 160,
+      render: (text?: string) => text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-',
+    },
+    {
+      title: '详细备注',
+      dataIndex: 'details',
+      key: 'details',
+      width: 300,
+      ellipsis: true,
+      render: (text?: string) => text || '-',
+    },
+    {
+      title: '录入时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 160,
+      render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm:ss'),
+    },
+    {
+      title: '更新时间',
+      dataIndex: 'updatedAt',
+      key: 'updatedAt',
+      width: 160,
+      render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm:ss'),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 150,
+      fixed: 'right',
+      render: (_, record) => (
+        <Space size="middle">
+          <Button
+            type="link"
+            icon={<EditOutlined />}
+            onClick={() => handleEdit(record)}
+          >
+            编辑
+          </Button>
+          <Popconfirm
+            title="确定要删除这条记录吗?"
+            onConfirm={() => handleDelete(record.id)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button type="link" danger icon={<DeleteOutlined />}>
+              删除
+            </Button>
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+
+  // 获取数据
+  const fetchData = async (page = 1, pageSize = 10, searchData?: any) => {
+    setLoading(true);
+    try {
+      const response = await followUpRecordClient.$get({
+        query: {
+          page,
+          pageSize,
+          ...searchData,
+        }
+      });
+      
+      if (response.status === 200) {
+        const result = await response.json();
+        setData(result.data);
+        setPagination({
+          current: page,
+          pageSize,
+          total: result.pagination.total,
+        });
+      }
+    } catch (error) {
+      message.error('获取跟单记录失败');
+      console.error(error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 处理搜索
+  const handleSearch = () => {
+    const values = searchForm.getFieldsValue();
+    const searchData: any = {};
+    
+    if (values.keyword) {
+      searchData.keyword = values.keyword;
+    }
+    if (values.nextContactTime && values.nextContactTime.length === 2) {
+      searchData.filters = JSON.stringify({
+        nextContactTime: {
+          between: [values.nextContactTime[0].format('YYYY-MM-DD HH:mm:ss'), values.nextContactTime[1].format('YYYY-MM-DD HH:mm:ss')]
+        }
+      });
+    }
+    
+    setSearchParams(searchData);
+    setPagination({ ...pagination, current: 1 });
+    fetchData(1, pagination.pageSize, searchData);
+  };
+
+  // 处理重置
+  const handleReset = () => {
+    searchForm.resetFields();
+    setSearchParams({});
+    fetchData(1, pagination.pageSize);
+  };
+
+  // 处理表格分页变化
+  const handleTableChange = (newPagination: any) => {
+    setPagination(newPagination);
+    fetchData(newPagination.current, newPagination.pageSize, searchParams);
+  };
+
+  // 处理编辑
+  const handleEdit = (record: FollowUpRecordType) => {
+    setEditingRecord(record);
+    form.setFieldsValue({
+      ...record,
+      nextContactTime: record.nextContactTime ? dayjs(record.nextContactTime) : null,
+    });
+    setModalVisible(true);
+  };
+
+  // 处理新增
+  const handleAdd = () => {
+    setEditingRecord(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+
+  // 处理删除
+  const handleDelete = async (id: number) => {
+    try {
+      const response = await followUpRecordClient[':id']['$delete']({
+        param: { id }
+      });
+      
+      if (response.status === 200) {
+        message.success('删除成功');
+        fetchData(pagination.current, pagination.pageSize, searchParams);
+      }
+    } catch (error) {
+      message.error('删除失败');
+      console.error(error);
+    }
+  };
+
+  // 处理提交表单
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      const formData = {
+        ...values,
+        nextContactTime: values.nextContactTime ? values.nextContactTime.format('YYYY-MM-DD HH:mm:ss') : null,
+      };
+
+      if (editingRecord) {
+        // 更新
+        const response = await followUpRecordClient[':id']['$put']({
+          param: { id: editingRecord.id },
+          json: formData as UpdateRequest
+        });
+        
+        if (response.status === 200) {
+          message.success('更新成功');
+          setModalVisible(false);
+          fetchData(pagination.current, pagination.pageSize, searchParams);
+        }
+      } else {
+        // 新增
+        const response = await followUpRecordClient.$post({
+          json: formData as CreateRequest
+        });
+        
+        if (response.status === 200) {
+          message.success('新增成功');
+          setModalVisible(false);
+          fetchData(1, pagination.pageSize, searchParams);
+        }
+      }
+    } catch (error) {
+      message.error('操作失败');
+      console.error(error);
+    }
+  };
+
+  // 初始化加载数据
+  useEffect(() => {
+    fetchData();
+  }, []);
+
+  return (
+    <div className="bg-white p-6 rounded-lg shadow-sm">
+      <div className="mb-6 flex justify-between items-center">
+        <h1 className="text-2xl font-bold">跟单记录管理</h1>
+        <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
+          新增跟单记录
+        </Button>
+      </div>
+
+      {/* 搜索表单 */}
+      <div className="mb-6 p-4 bg-gray-50 rounded-lg">
+        <Form form={searchForm} layout="inline">
+          <Form.Item name="keyword" label="关键词">
+            <Input placeholder="编号/收录资源" />
+          </Form.Item>
+          <Form.Item name="nextContactTime" label="下次联系时间">
+            <DatePicker.RangePicker showTime />
+          </Form.Item>
+          <Form.Item>
+            <Space>
+              <Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
+                搜索
+              </Button>
+              <Button onClick={handleReset}>重置</Button>
+            </Space>
+          </Form.Item>
+        </Form>
+      </div>
+
+      {/* 数据表格 */}
+      <Table
+        columns={columns}
+        dataSource={data}
+        rowKey="id"
+        loading={loading}
+        pagination={{
+          ...pagination,
+          showSizeChanger: true,
+          showQuickJumper: true,
+          showTotal: (total) => `共 ${total} 条记录`,
+        }}
+        onChange={handleTableChange}
+        scroll={{ x: 'max-content' }}
+        bordered
+      />
+
+      {/* 新增/编辑模态框 */}
+      <Modal
+        title={editingRecord ? '编辑跟单记录' : '新增跟单记录'}
+        open={modalVisible}
+        onOk={handleSubmit}
+        onCancel={() => setModalVisible(false)}
+        width={600}
+        destroyOnClose
+      >
+        <Form form={form} layout="vertical">
+          <Form.Item
+            name="serialNumber"
+            label="编号"
+            rules={[{ required: true, message: '请输入编号' }]}
+          >
+            <Input placeholder="请输入编号" />
+          </Form.Item>
+          <Form.Item
+            name="resourceName"
+            label="收录资源"
+            rules={[{ required: true, message: '请输入收录资源' }]}
+          >
+            <Input placeholder="请输入收录资源" />
+          </Form.Item>
+          <Form.Item name="nextContactTime" label="下次联系时间">
+            <DatePicker showTime style={{ width: '100%' }} />
+          </Form.Item>
+          <Form.Item name="details" label="详细备注">
+            <TextArea rows={4} placeholder="请输入详细备注信息" />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default FollowUpRecords;

+ 413 - 0
src/client/admin/pages/OrderRecords.tsx

@@ -0,0 +1,413 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, DatePicker, Select, message, Space, Popconfirm } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
+import type { ColumnsType } from 'antd/es/table';
+import { InferRequestType, InferResponseType } from 'hono/client';
+import { orderRecordClient } from '@/client/api';
+import dayjs from 'dayjs';
+
+const { RangePicker } = DatePicker;
+const { Option } = Select;
+
+type OrderRecordType = InferResponseType<typeof orderRecordClient.$get, 200>['data'][0];
+type CreateRequest = InferRequestType<typeof orderRecordClient.$post>['json'];
+type UpdateRequest = InferRequestType<typeof orderRecordClient[':id']['$put']>['json'];
+
+const OrderRecords: React.FC = () => {
+  const [loading, setLoading] = useState(false);
+  const [data, setData] = useState<OrderRecordType[]>([]);
+  const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
+  const [modalVisible, setModalVisible] = useState(false);
+  const [editingRecord, setEditingRecord] = useState<OrderRecordType | null>(null);
+  const [searchForm] = Form.useForm();
+  const [form] = Form.useForm();
+  const [searchParams, setSearchParams] = useState<any>({});
+
+  // 定义表格列
+  const columns: ColumnsType<OrderRecordType> = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '公司名称',
+      dataIndex: 'companyName',
+      key: 'companyName',
+      width: 200,
+    },
+    {
+      title: '订单编号',
+      dataIndex: 'orderNumber',
+      key: 'orderNumber',
+      width: 150,
+    },
+    {
+      title: '联系人',
+      dataIndex: 'contactPerson',
+      key: 'contactPerson',
+      width: 100,
+    },
+    {
+      title: '下单日期',
+      dataIndex: 'orderDate',
+      key: 'orderDate',
+      width: 120,
+      render: (text: string) => dayjs(text).format('YYYY-MM-DD'),
+    },
+    {
+      title: '交单日期',
+      dataIndex: 'deliveryDate',
+      key: 'deliveryDate',
+      width: 120,
+      render: (text?: string) => text ? dayjs(text).format('YYYY-MM-DD') : '-',
+    },
+    {
+      title: '预付款',
+      dataIndex: 'advancePayment',
+      key: 'advancePayment',
+      width: 120,
+      render: (amount: number) => `¥${amount.toFixed(2)}`,
+    },
+    {
+      title: '订单金额',
+      dataIndex: 'orderAmount',
+      key: 'orderAmount',
+      width: 120,
+      render: (amount: number) => `¥${amount.toFixed(2)}`,
+    },
+    {
+      title: '订单状态',
+      dataIndex: 'orderStatus',
+      key: 'orderStatus',
+      width: 100,
+      render: (status: number) => (
+        <Select value={status} style={{ width: 80 }} disabled>
+          <Option value={0}>未处理</Option>
+          <Option value={1}>已完成</Option>
+        </Select>
+      ),
+    },
+    {
+      title: '业务员',
+      dataIndex: 'salesperson',
+      key: 'salesperson',
+      width: 100,
+    },
+    {
+      title: '录入时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 160,
+      render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm:ss'),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 150,
+      fixed: 'right',
+      render: (_, record) => (
+        <Space size="middle">
+          <Button
+            type="link"
+            icon={<EditOutlined />}
+            onClick={() => handleEdit(record)}
+          >
+            编辑
+          </Button>
+          <Popconfirm
+            title="确定要删除这条记录吗?"
+            onConfirm={() => handleDelete(record.id)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button type="link" danger icon={<DeleteOutlined />}>
+              删除
+            </Button>
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+
+  // 获取数据
+  const fetchData = async (page = 1, pageSize = 10, searchData?: any) => {
+    setLoading(true);
+    try {
+      const response = await orderRecordClient.$get({
+        query: {
+          page,
+          pageSize,
+          ...searchData,
+        }
+      });
+      
+      if (response.status === 200) {
+        const result = await response.json();
+        setData(result.data);
+        setPagination({
+          current: page,
+          pageSize,
+          total: result.pagination.total,
+        });
+      }
+    } catch (error) {
+      message.error('获取订单记录失败');
+      console.error(error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 处理搜索
+  const handleSearch = () => {
+    const values = searchForm.getFieldsValue();
+    const searchData: any = {};
+    
+    if (values.keyword) {
+      searchData.keyword = values.keyword;
+    }
+    if (values.orderDate && values.orderDate.length === 2) {
+      searchData.filters = JSON.stringify({
+        orderDate: {
+          between: [values.orderDate[0].format('YYYY-MM-DD'), values.orderDate[1].format('YYYY-MM-DD')]
+        }
+      });
+    }
+    if (values.orderStatus !== undefined) {
+      searchData.filters = JSON.stringify({
+        ...(searchData.filters ? JSON.parse(searchData.filters) : {}),
+        orderStatus: values.orderStatus
+      });
+    }
+    
+    setSearchParams(searchData);
+    setPagination({ ...pagination, current: 1 });
+    fetchData(1, pagination.pageSize, searchData);
+  };
+
+  // 处理重置
+  const handleReset = () => {
+    searchForm.resetFields();
+    setSearchParams({});
+    fetchData(1, pagination.pageSize);
+  };
+
+  // 处理表格分页变化
+  const handleTableChange = (newPagination: any) => {
+    setPagination(newPagination);
+    fetchData(newPagination.current, newPagination.pageSize, searchParams);
+  };
+
+  // 处理编辑
+  const handleEdit = (record: OrderRecordType) => {
+    setEditingRecord(record);
+    form.setFieldsValue({
+      ...record,
+      orderDate: dayjs(record.orderDate),
+      deliveryDate: record.deliveryDate ? dayjs(record.deliveryDate) : null,
+    });
+    setModalVisible(true);
+  };
+
+  // 处理新增
+  const handleAdd = () => {
+    setEditingRecord(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+
+  // 处理删除
+  const handleDelete = async (id: number) => {
+    try {
+      const response = await orderRecordClient[':id']['$delete']({
+        param: { id }
+      });
+      
+      if (response.status === 200) {
+        message.success('删除成功');
+        fetchData(pagination.current, pagination.pageSize, searchParams);
+      }
+    } catch (error) {
+      message.error('删除失败');
+      console.error(error);
+    }
+  };
+
+  // 处理提交表单
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      const formData = {
+        ...values,
+        orderDate: values.orderDate.format('YYYY-MM-DD'),
+        deliveryDate: values.deliveryDate ? values.deliveryDate.format('YYYY-MM-DD') : null,
+      };
+
+      if (editingRecord) {
+        // 更新
+        const response = await orderRecordClient[':id']['$put']({
+          param: { id: editingRecord.id },
+          json: formData as UpdateRequest
+        });
+        
+        if (response.status === 200) {
+          message.success('更新成功');
+          setModalVisible(false);
+          fetchData(pagination.current, pagination.pageSize, searchParams);
+        }
+      } else {
+        // 新增
+        const response = await orderRecordClient.$post({
+          json: formData as CreateRequest
+        });
+        
+        if (response.status === 200) {
+          message.success('新增成功');
+          setModalVisible(false);
+          fetchData(1, pagination.pageSize, searchParams);
+        }
+      }
+    } catch (error) {
+      message.error('操作失败');
+      console.error(error);
+    }
+  };
+
+  // 初始化加载数据
+  useEffect(() => {
+    fetchData();
+  }, []);
+
+  return (
+    <div className="bg-white p-6 rounded-lg shadow-sm">
+      <div className="mb-6 flex justify-between items-center">
+        <h1 className="text-2xl font-bold">订单记录管理</h1>
+        <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
+          新增订单
+        </Button>
+      </div>
+
+      {/* 搜索表单 */}
+      <div className="mb-6 p-4 bg-gray-50 rounded-lg">
+        <Form form={searchForm} layout="inline">
+          <Form.Item name="keyword" label="关键词">
+            <Input placeholder="公司名称/订单编号/联系人" />
+          </Form.Item>
+          <Form.Item name="orderDate" label="下单日期">
+            <RangePicker />
+          </Form.Item>
+          <Form.Item name="orderStatus" label="订单状态">
+            <Select style={{ width: 120 }} allowClear>
+              <Option value={0}>未处理</Option>
+              <Option value={1}>已完成</Option>
+            </Select>
+          </Form.Item>
+          <Form.Item>
+            <Space>
+              <Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
+                搜索
+              </Button>
+              <Button onClick={handleReset}>重置</Button>
+            </Space>
+          </Form.Item>
+        </Form>
+      </div>
+
+      {/* 数据表格 */}
+      <Table
+        columns={columns}
+        dataSource={data}
+        rowKey="id"
+        loading={loading}
+        pagination={{
+          ...pagination,
+          showSizeChanger: true,
+          showQuickJumper: true,
+          showTotal: (total) => `共 ${total} 条记录`,
+        }}
+        onChange={handleTableChange}
+        scroll={{ x: 'max-content' }}
+        bordered
+      />
+
+      {/* 新增/编辑模态框 */}
+      <Modal
+        title={editingRecord ? '编辑订单记录' : '新增订单记录'}
+        open={modalVisible}
+        onOk={handleSubmit}
+        onCancel={() => setModalVisible(false)}
+        width={600}
+        destroyOnClose
+      >
+        <Form form={form} layout="vertical">
+          <Form.Item
+            name="companyName"
+            label="公司名称"
+            rules={[{ required: true, message: '请输入公司名称' }]}
+          >
+            <Input placeholder="请输入公司名称" />
+          </Form.Item>
+          <Form.Item
+            name="orderNumber"
+            label="订单编号"
+            rules={[{ required: true, message: '请输入订单编号' }]}
+          >
+            <Input placeholder="请输入订单编号" />
+          </Form.Item>
+          <Form.Item
+            name="contactPerson"
+            label="联系人"
+            rules={[{ required: true, message: '请输入联系人' }]}
+          >
+            <Input placeholder="请输入联系人" />
+          </Form.Item>
+          <Form.Item
+            name="orderDate"
+            label="下单日期"
+            rules={[{ required: true, message: '请选择下单日期' }]}
+          >
+            <DatePicker style={{ width: '100%' }} />
+          </Form.Item>
+          <Form.Item name="deliveryDate" label="交单日期">
+            <DatePicker style={{ width: '100%' }} />
+          </Form.Item>
+          <Form.Item
+            name="advancePayment"
+            label="预付款"
+            rules={[{ required: true, message: '请输入预付款' }]}
+          >
+            <Input type="number" placeholder="请输入预付款" />
+          </Form.Item>
+          <Form.Item
+            name="orderAmount"
+            label="订单金额"
+            rules={[{ required: true, message: '请输入订单金额' }]}
+          >
+            <Input type="number" placeholder="请输入订单金额" />
+          </Form.Item>
+          <Form.Item
+            name="orderStatus"
+            label="订单状态"
+            rules={[{ required: true, message: '请选择订单状态' }]}
+          >
+            <Select>
+              <Option value={0}>未处理</Option>
+              <Option value={1}>已完成</Option>
+            </Select>
+          </Form.Item>
+          <Form.Item
+            name="salesperson"
+            label="业务员"
+            rules={[{ required: true, message: '请输入业务员' }]}
+          >
+            <Input placeholder="请输入业务员" />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default OrderRecords;

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

@@ -14,6 +14,8 @@ import ContractsPage from './pages/Contracts';
 import ContractRenewsPage from './pages/ContractRenews';
 import ContactsPage from './pages/Contacts';
 import LogsPage from './pages/Logs';
+import OrderRecordsPage from './pages/OrderRecords';
+import FollowUpRecordsPage from './pages/FollowUpRecords';
 import { LoginPage } from './pages/Login';
 
 export const router = createBrowserRouter([
@@ -87,6 +89,16 @@ export const router = createBrowserRouter([
         element: <LogsPage />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'order-records',
+        element: <OrderRecordsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'follow-up-records',
+        element: <FollowUpRecordsPage />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,

+ 11 - 2
src/client/api.ts

@@ -1,8 +1,9 @@
 import axios, { isAxiosError } from 'axios';
 import { hc } from 'hono/client'
 import type {
-  AuthRoutes, UserRoutes, RoleRoutes, 
-  AreaRoutes, ClientRoutes, ExpenseRoutes, FileRoutes, HetongRoutes, HetongRenewRoutes, LinkmanRoutes, LogfileRoutes
+  AuthRoutes, UserRoutes, RoleRoutes,
+  AreaRoutes, ClientRoutes, ExpenseRoutes, FileRoutes, HetongRoutes, HetongRenewRoutes, LinkmanRoutes, LogfileRoutes,
+  OrderRecordRoutes, FollowUpRecordRoutes
 } from '@/server/api';
 
 // 创建 axios 适配器
@@ -103,3 +104,11 @@ export const linkmanClient = hc<LinkmanRoutes>('/', {
 export const logfileClient = hc<LogfileRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1.logs;
+
+export const orderRecordClient = hc<OrderRecordRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['order-records'];
+
+export const followUpRecordClient = hc<FollowUpRecordRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['follow-up-records'];

+ 6 - 0
src/server/api.ts

@@ -12,6 +12,8 @@ import hetongRoutes from './api/contracts/index'
 import hetongRenewRoutes from './api/contracts-renew/index'
 import linkmanRoutes from './api/contacts/index'
 import logfileRoutes from './api/logs/index'
+import orderRecordRoutes from './api/order-records/index'
+import followUpRecordRoutes from './api/follow-up-records/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 
@@ -71,6 +73,8 @@ const hetongApiRoutes = api.route('/api/v1/contracts', hetongRoutes)
 const hetongRenewApiRoutes = api.route('/api/v1/contracts-renew', hetongRenewRoutes)
 const linkmanApiRoutes = api.route('/api/v1/contacts', linkmanRoutes)
 const logfileApiRoutes = api.route('/api/v1/logs', logfileRoutes)
+const orderRecordApiRoutes = api.route('/api/v1/order-records', orderRecordRoutes)
+const followUpRecordApiRoutes = api.route('/api/v1/follow-up-records', followUpRecordRoutes)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
@@ -84,5 +88,7 @@ export type HetongRoutes = typeof hetongApiRoutes
 export type HetongRenewRoutes = typeof hetongRenewApiRoutes
 export type LinkmanRoutes = typeof linkmanApiRoutes
 export type LogfileRoutes = typeof logfileApiRoutes
+export type OrderRecordRoutes = typeof orderRecordApiRoutes
+export type FollowUpRecordRoutes = typeof followUpRecordApiRoutes
 
 export default api

+ 16 - 0
src/server/api/follow-up-records/index.ts

@@ -0,0 +1,16 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { FollowUpRecord } from '@/server/modules/follow-ups/follow-up-record.entity';
+import { FollowUpRecordSchema, CreateFollowUpRecordDto, UpdateFollowUpRecordDto } from '@/server/modules/follow-ups/follow-up-record.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const followUpRecordRoutes = createCrudRoutes({
+  entity: FollowUpRecord,
+  createSchema: CreateFollowUpRecordDto,
+  updateSchema: UpdateFollowUpRecordDto,
+  getSchema: FollowUpRecordSchema,
+  listSchema: FollowUpRecordSchema,
+  searchFields: ['serialNumber', 'resourceName', 'details'],
+  middleware: [authMiddleware]
+});
+
+export default followUpRecordRoutes;

+ 16 - 0
src/server/api/order-records/index.ts

@@ -0,0 +1,16 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { OrderRecord } from '@/server/modules/orders/order-record.entity';
+import { OrderRecordSchema, CreateOrderRecordDto, UpdateOrderRecordDto } from '@/server/modules/orders/order-record.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const orderRecordRoutes = createCrudRoutes({
+  entity: OrderRecord,
+  createSchema: CreateOrderRecordDto,
+  updateSchema: UpdateOrderRecordDto,
+  getSchema: OrderRecordSchema,
+  listSchema: OrderRecordSchema,
+  searchFields: ['companyName', 'orderNumber', 'contactPerson', 'salesperson'],
+  middleware: [authMiddleware]
+});
+
+export default orderRecordRoutes;

+ 5 - 2
src/server/data-source.ts

@@ -14,6 +14,8 @@ import { Hetong } from "./modules/contracts/hetong.entity"
 import { HetongRenew } from "./modules/contracts/hetong-renew.entity"
 import { Linkman } from "./modules/contacts/linkman.entity"
 import { Logfile } from "./modules/logs/logfile.entity"
+import { OrderRecord } from "./modules/orders/order-record.entity"
+import { FollowUpRecord } from "./modules/follow-ups/follow-up-record.entity"
 
 export const AppDataSource = new DataSource({
   type: "mysql",
@@ -23,8 +25,9 @@ export const AppDataSource = new DataSource({
   password: process.env.DB_PASSWORD || "",
   database: process.env.DB_DATABASE || "d8dai",
   entities: [
-    User, Role, 
-    AreaData, Client, Expense, File, Hetong, HetongRenew, Linkman, Logfile
+    User, Role,
+    AreaData, Client, Expense, File, Hetong, HetongRenew, Linkman, Logfile,
+    OrderRecord, FollowUpRecord
   ],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 57 - 0
src/server/modules/follow-ups/follow-up-record.entity.ts

@@ -0,0 +1,57 @@
+import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+
+@Entity('follow_up_record')
+export class FollowUpRecord {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'serial_number', type: 'varchar', length: 50, comment: '编号' })
+  serialNumber!: string;
+
+  @Column({ name: 'resource_name', type: 'varchar', length: 255, comment: '收录资源' })
+  resourceName!: string;
+
+  @Column({ name: 'next_contact_time', type: 'datetime', nullable: true, comment: '下次联系时间' })
+  nextContactTime?: Date;
+
+  @Column({ name: 'details', type: 'text', nullable: true, comment: '详细备注' })
+  details?: string;
+
+  @Column({ name: 'is_deleted', type: 'tinyint', default: 0, comment: '是否删除(0-未删除,1-已删除)' })
+  isDeleted!: number;
+
+  @Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', comment: '录入时间' })
+  createdAt!: Date;
+
+  @Column({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', comment: '更新时间' })
+  updatedAt!: Date;
+}
+
+// 基础Schema
+export const FollowUpRecordSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '主键ID' }),
+  serialNumber: z.string().max(50).openapi({ description: '编号', example: 'FUP202407150001' }),
+  resourceName: z.string().max(255).openapi({ description: '收录资源', example: '某科技公司合作项目' }),
+  nextContactTime: z.string().datetime().nullable().openapi({ description: '下次联系时间', example: '2024-07-20 14:30:00' }),
+  details: z.string().nullable().optional().openapi({ description: '详细备注', example: '客户对产品功能有特殊要求,需要单独沟通' }),
+  isDeleted: z.coerce.number().int().min(0).max(1).default(0).openapi({ description: '删除状态', example: 0 }),
+  createdAt: z.string().datetime().openapi({ description: '录入时间', example: '2024-07-15T12:00:00Z' }),
+  updatedAt: z.string().datetime().openapi({ description: '更新时间', example: '2024-07-15T12:00:00Z' })
+});
+
+// 创建DTO
+export const CreateFollowUpRecordDto = z.object({
+  serialNumber: z.string().max(50).openapi({ description: '编号', example: 'FUP202407150001' }),
+  resourceName: z.string().max(255).openapi({ description: '收录资源', example: '某科技公司合作项目' }),
+  nextContactTime: z.coerce.date().nullable().optional().openapi({ description: '下次联系时间', example: '2024-07-20 14:30:00' }),
+  details: z.string().nullable().optional().openapi({ description: '详细备注', example: '客户对产品功能有特殊要求,需要单独沟通' })
+});
+
+// 更新DTO
+export const UpdateFollowUpRecordDto = z.object({
+  serialNumber: z.string().max(50).optional().openapi({ description: '编号', example: 'FUP202407150001' }),
+  resourceName: z.string().max(255).optional().openapi({ description: '收录资源', example: '某科技公司合作项目' }),
+  nextContactTime: z.coerce.date().nullable().optional().openapi({ description: '下次联系时间', example: '2024-07-20 14:30:00' }),
+  details: z.string().nullable().optional().openapi({ description: '详细备注', example: '客户对产品功能有特殊要求,需要单独沟通' })
+});

+ 9 - 0
src/server/modules/follow-ups/follow-up-record.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { FollowUpRecord } from './follow-up-record.entity';
+
+export class FollowUpRecordService extends GenericCrudService<FollowUpRecord> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, FollowUpRecord);
+  }
+}

+ 87 - 0
src/server/modules/orders/order-record.entity.ts

@@ -0,0 +1,87 @@
+import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+
+@Entity('order_record')
+export class OrderRecord {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'company_name', type: 'varchar', length: 255, comment: '公司名称' })
+  companyName!: string;
+
+  @Column({ name: 'order_number', type: 'varchar', length: 50, comment: '订单编号' })
+  orderNumber!: string;
+
+  @Column({ name: 'contact_person', type: 'varchar', length: 50, comment: '联系人' })
+  contactPerson!: string;
+
+  @Column({ name: 'order_date', type: 'date', comment: '下单日期' })
+  orderDate!: Date;
+
+  @Column({ name: 'delivery_date', type: 'date', nullable: true, comment: '交单日期' })
+  deliveryDate?: Date;
+
+  @Column({ name: 'advance_payment', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '预付款' })
+  advancePayment!: number;
+
+  @Column({ name: 'order_amount', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '订单金额' })
+  orderAmount!: number;
+
+  @Column({ name: 'order_status', type: 'tinyint', default: 0, comment: '订单状态(0-未处理,1-已完成)' })
+  orderStatus!: number;
+
+  @Column({ name: 'salesperson', type: 'varchar', length: 50, comment: '业务员' })
+  salesperson!: string;
+
+  @Column({ name: 'is_deleted', type: 'tinyint', default: 0, comment: '删除状态(0-未删除,1-已删除)' })
+  isDeleted!: number;
+
+  @Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', comment: '录入时间' })
+  createdAt!: Date;
+
+  @Column({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', comment: '更新时间' })
+  updatedAt!: Date;
+}
+
+// 基础Schema
+export const OrderRecordSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '记录ID' }),
+  companyName: z.string().max(255).openapi({ description: '公司名称', example: '测试科技有限公司' }),
+  orderNumber: z.string().max(50).openapi({ description: '订单编号', example: 'ORD202407150001' }),
+  contactPerson: z.string().max(50).openapi({ description: '联系人', example: '张三' }),
+  orderDate: z.string().datetime().openapi({ description: '下单日期', example: '2024-07-15' }),
+  deliveryDate: z.string().datetime().nullable().openapi({ description: '交单日期', example: '2024-07-20' }),
+  advancePayment: z.coerce.number().multipleOf(0.01).openapi({ description: '预付款', example: 1000.00 }),
+  orderAmount: z.coerce.number().multipleOf(0.01).openapi({ description: '订单金额', example: 5000.00 }),
+  orderStatus: z.coerce.number().int().min(0).max(1).openapi({ description: '订单状态(0-未处理,1-已完成)', example: 0 }),
+  salesperson: z.string().max(50).openapi({ description: '业务员', example: '李四' }),
+  isDeleted: z.coerce.number().int().min(0).max(1).default(0).openapi({ description: '删除状态', example: 0 }),
+  createdAt: z.string().datetime().openapi({ description: '录入时间', example: '2024-07-15T12:00:00Z' }),
+  updatedAt: z.string().datetime().openapi({ description: '更新时间', example: '2024-07-15T12:00:00Z' })
+});
+
+// 创建DTO
+export const CreateOrderRecordDto = z.object({
+  companyName: z.string().max(255).openapi({ description: '公司名称', example: '测试科技有限公司' }),
+  orderNumber: z.string().max(50).openapi({ description: '订单编号', example: 'ORD202407150001' }),
+  contactPerson: z.string().max(50).openapi({ description: '联系人', example: '张三' }),
+  orderDate: z.coerce.date().openapi({ description: '下单日期', example: '2024-07-15' }),
+  deliveryDate: z.coerce.date().nullable().optional().openapi({ description: '交单日期', example: '2024-07-20' }),
+  advancePayment: z.coerce.number().multipleOf(0.01).default(0).openapi({ description: '预付款', example: 1000.00 }),
+  orderAmount: z.coerce.number().multipleOf(0.01).default(0).openapi({ description: '订单金额', example: 5000.00 }),
+  orderStatus: z.coerce.number().int().min(0).max(1).default(0).openapi({ description: '订单状态', example: 0 }),
+  salesperson: z.string().max(50).openapi({ description: '业务员', example: '李四' })
+});
+
+// 更新DTO
+export const UpdateOrderRecordDto = z.object({
+  companyName: z.string().max(255).optional().openapi({ description: '公司名称', example: '测试科技有限公司' }),
+  orderNumber: z.string().max(50).optional().openapi({ description: '订单编号', example: 'ORD202407150001' }),
+  contactPerson: z.string().max(50).optional().openapi({ description: '联系人', example: '张三' }),
+  orderDate: z.coerce.date().optional().openapi({ description: '下单日期', example: '2024-07-15' }),
+  deliveryDate: z.coerce.date().nullable().optional().openapi({ description: '交单日期', example: '2024-07-20' }),
+  advancePayment: z.coerce.number().multipleOf(0.01).optional().openapi({ description: '预付款', example: 1000.00 }),
+  orderAmount: z.coerce.number().multipleOf(0.01).optional().openapi({ description: '订单金额', example: 5000.00 }),
+  orderStatus: z.coerce.number().int().min(0).max(1).optional().openapi({ description: '订单状态', example: 0 }),
+  salesperson: z.string().max(50).optional().openapi({ description: '业务员', example: '李四' })
+});

+ 9 - 0
src/server/modules/orders/order-record.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { OrderRecord } from './order-record.entity';
+
+export class OrderRecordService extends GenericCrudService<OrderRecord> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, OrderRecord);
+  }
+}