ソースを参照

✨ feat(follow-up): 重构跟进记录功能,关联客户信息

- 移除serialNumber字段,新增clientId关联客户
- 表格展示客户名称,替换原编号列
- 搜索框提示文本更新为"收录资源/备注"
- 添加ClientSelect组件用于客户选择
- 接口添加client关联查询和用户跟踪字段

✨ feat(order): 优化订单金额输入体验

- 将预付款和订单金额输入框替换为InputNumber组件
- 添加金额前缀、最小值限制和两位小数精度控制
- 接口添加用户跟踪字段记录创建者和更新者

♻️ refactor(api): 优化API配置

- 跟进记录接口移除serialNumber搜索字段,添加client关联
- 订单记录接口添加client、linkman和user关联查询
- 为两个接口添加用户跟踪配置,自动记录创建和更新用户ID

🐛 fix(api): 修复响应状态判断逻辑

- 将response.status === 200判断统一修改为response.ok
- 确保正确处理所有成功的HTTP响应状态码
yourname 8 ヶ月 前
コミット
47d0b78657

+ 15 - 13
src/client/admin/pages/FollowUpRecords.tsx

@@ -4,6 +4,7 @@ import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant
 import type { ColumnsType } from 'antd/es/table';
 import { InferRequestType, InferResponseType } from 'hono/client';
 import { followUpRecordClient } from '@/client/api';
+import ClientSelect from '@/client/admin/components/ClientSelect';
 import dayjs from 'dayjs';
 
 const { TextArea } = Input;
@@ -31,10 +32,11 @@ const FollowUpRecords: React.FC = () => {
       width: 80,
     },
     {
-      title: '编号',
-      dataIndex: 'serialNumber',
-      key: 'serialNumber',
-      width: 150,
+      title: '客户名称',
+      dataIndex: ['client', 'companyName'],
+      key: 'client.companyName',
+      width: 200,
+      render: (text: string) => text || '-',
     },
     {
       title: '收录资源',
@@ -113,7 +115,7 @@ const FollowUpRecords: React.FC = () => {
         }
       });
       
-      if (response.status === 200) {
+      if (response.ok) {
         const result = await response.json();
         setData(result.data);
         setPagination({
@@ -188,7 +190,7 @@ const FollowUpRecords: React.FC = () => {
         param: { id }
       });
       
-      if (response.status === 200) {
+      if (response.ok) {
         message.success('删除成功');
         fetchData(pagination.current, pagination.pageSize, searchParams);
       }
@@ -214,7 +216,7 @@ const FollowUpRecords: React.FC = () => {
           json: formData as UpdateRequest
         });
         
-        if (response.status === 200) {
+        if (response.ok) {
           message.success('更新成功');
           setModalVisible(false);
           fetchData(pagination.current, pagination.pageSize, searchParams);
@@ -225,7 +227,7 @@ const FollowUpRecords: React.FC = () => {
           json: formData as CreateRequest
         });
         
-        if (response.status === 200) {
+        if (response.ok) {
           message.success('新增成功');
           setModalVisible(false);
           fetchData(1, pagination.pageSize, searchParams);
@@ -255,7 +257,7 @@ const FollowUpRecords: React.FC = () => {
       <div className="mb-6 p-4 bg-gray-50 rounded-lg">
         <Form form={searchForm} layout="inline">
           <Form.Item name="keyword" label="关键词">
-            <Input placeholder="编号/收录资源" />
+            <Input placeholder="收录资源/备注" />
           </Form.Item>
           <Form.Item name="nextContactTime" label="下次联系时间">
             <DatePicker.RangePicker showTime />
@@ -299,11 +301,11 @@ const FollowUpRecords: React.FC = () => {
       >
         <Form form={form} layout="vertical">
           <Form.Item
-            name="serialNumber"
-            label="编号"
-            rules={[{ required: true, message: '请输入编号' }]}
+            name="clientId"
+            label="选择客户"
+            rules={[{ required: true, message: '请选择客户' }]}
           >
-            <Input placeholder="请输入编号" />
+            <ClientSelect placeholder="请选择客户" />
           </Form.Item>
           <Form.Item
             name="resourceName"

+ 17 - 3
src/client/admin/pages/OrderRecords.tsx

@@ -1,5 +1,5 @@
 import React, { useState, useEffect } from 'react';
-import { Table, Button, Modal, Form, Input, DatePicker, Select, message, Space, Popconfirm } from 'antd';
+import { Table, Button, Modal, Form, Input, InputNumber, DatePicker, Select, message, Space, Popconfirm } from 'antd';
 import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
 import type { ColumnsType } from 'antd/es/table';
 import dayjs from 'dayjs';
@@ -402,7 +402,14 @@ const OrderRecords: React.FC = () => {
             label="预付款"
             rules={[{ required: true, message: '请输入预付款' }]}
           >
-            <Input type="number" placeholder="请输入预付款" />
+            <InputNumber
+              style={{ width: '100%' }}
+              placeholder="请输入预付款"
+              min={0}
+              step={0.01}
+              precision={2}
+              prefix="¥"
+            />
           </Form.Item>
           
           <Form.Item
@@ -410,7 +417,14 @@ const OrderRecords: React.FC = () => {
             label="订单金额"
             rules={[{ required: true, message: '请输入订单金额' }]}
           >
-            <Input type="number" placeholder="请输入订单金额" />
+            <InputNumber
+              style={{ width: '100%' }}
+              placeholder="请输入订单金额"
+              min={0}
+              step={0.01}
+              precision={2}
+              prefix="¥"
+            />
           </Form.Item>
           
           <Form.Item

+ 7 - 2
src/server/api/follow-up-records/index.ts

@@ -9,8 +9,13 @@ const followUpRecordRoutes = createCrudRoutes({
   updateSchema: UpdateFollowUpRecordDto,
   getSchema: FollowUpRecordSchema,
   listSchema: FollowUpRecordSchema,
-  searchFields: ['serialNumber', 'resourceName', 'details'],
-  middleware: [authMiddleware]
+  searchFields: ['resourceName', 'details'],
+  relations: ['client'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
 });
 
 export default followUpRecordRoutes;

+ 6 - 1
src/server/api/order-records/index.ts

@@ -10,7 +10,12 @@ const orderRecordRoutes = createCrudRoutes({
   getSchema: OrderRecordSchema,
   listSchema: OrderRecordSchema,
   searchFields: ['companyName', 'orderNumber', 'contactPerson', 'salesperson'],
-  middleware: [authMiddleware]
+  middleware: [authMiddleware],
+  relations: ['client', 'linkman', 'user'],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
 });
 
 export default orderRecordRoutes;

+ 0 - 0
src/server/migrations/UpdateFollowUpRecordMigration.ts


+ 27 - 7
src/server/modules/follow-ups/follow-up-record.entity.ts

@@ -1,13 +1,18 @@
-import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
 import { z } from '@hono/zod-openapi';
+import { Client } from '@/server/modules/clients/client.entity';
 
 @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: 'client_id', type: 'int', unsigned: true, nullable: false, comment: '客户ID' })
+  clientId!: number;
+
+  @ManyToOne(() => Client, { nullable: false })
+  @JoinColumn({ name: 'client_id', referencedColumnName: 'id' })
+  client!: Client;
 
   @Column({ name: 'resource_name', type: 'varchar', length: 255, comment: '收录资源' })
   resourceName!: string;
@@ -26,23 +31,38 @@ export class FollowUpRecord {
 
   @Column({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', comment: '更新时间' })
   updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy?: number;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy?: number;
 }
 
 // 基础Schema
 export const FollowUpRecordSchema = z.object({
   id: z.number().int().positive().openapi({ description: '主键ID' }),
-  serialNumber: z.string().max(50).openapi({ description: '编号', example: 'FUP202407150001' }),
+  clientId: z.number().int().positive().openapi({ description: '客户ID', example: 1 }),
   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' })
+  updatedAt: z.string().datetime().openapi({ description: '更新时间', example: '2024-07-15T12:00:00Z' }),
+  createdBy: z.number().int().positive().nullable().openapi({ description: '创建用户ID', example: 1 }),
+  updatedBy: z.number().int().positive().nullable().openapi({ description: '更新用户ID', example: 1 }),
+  client: z.object({
+    id: z.number(),
+    companyName: z.string(),
+    contactPerson: z.string().nullable()
+  }).nullable().optional().openapi({
+    description: '关联客户信息'
+  })
 });
 
 // 创建DTO
 export const CreateFollowUpRecordDto = z.object({
-  serialNumber: z.string().max(50).openapi({ description: '编号', example: 'FUP202407150001' }),
+  clientId: z.coerce.number().int().positive().openapi({ description: '客户ID', example: 1 }),
   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: '客户对产品功能有特殊要求,需要单独沟通' })
@@ -50,7 +70,7 @@ export const CreateFollowUpRecordDto = z.object({
 
 // 更新DTO
 export const UpdateFollowUpRecordDto = z.object({
-  serialNumber: z.string().max(50).optional().openapi({ description: '编号', example: 'FUP202407150001' }),
+  clientId: z.coerce.number().int().positive().optional().openapi({ description: '客户ID', example: 1 }),
   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: '客户对产品功能有特殊要求,需要单独沟通' })

+ 42 - 34
src/server/modules/orders/order-record.entity.ts

@@ -57,44 +57,52 @@ export class OrderRecord {
 
   @Column({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', comment: '更新时间' })
   updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy?: number;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy?: number;
 }
 
 // 基础Schema
 export const OrderRecordSchema = z.object({
-  id: z.number().int().positive().openapi({ description: '记录ID' }),
-  orderNumber: z.string().max(50).openapi({ description: '订单编号', example: 'ORD202407150001' }),
-  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 }),
-  clientId: z.number().int().positive().nullable().openapi({ description: '客户ID', example: 1 }),
-  linkmanId: z.number().int().positive().nullable().openapi({ description: '联系人ID', example: 1 }),
-  userId: z.number().int().positive().nullable().openapi({ description: '业务员用户ID', example: 1 }),
-  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' }),
-  client: z.object({
-    id: z.number(),
-    companyName: z.string(),
-    contactPerson: z.string().nullable()
-  }).nullable().optional().openapi({
-    description: '关联客户信息'
-  }),
-  linkman: z.object({
-    id: z.number(),
-    name: z.string(),
-    mobile: z.string().nullable()
-  }).nullable().optional().openapi({
-    description: '关联联系人信息'
-  }),
-  user: z.object({
-    id: z.number(),
-    username: z.string(),
-    name: z.string().nullable()
-  }).nullable().optional().openapi({
-    description: '关联业务员信息'
-  })
+id: z.number().int().positive().openapi({ description: '记录ID' }),
+orderNumber: z.string().max(50).openapi({ description: '订单编号', example: 'ORD202407150001' }),
+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 }),
+clientId: z.number().int().positive().nullable().openapi({ description: '客户ID', example: 1 }),
+linkmanId: z.number().int().positive().nullable().openapi({ description: '联系人ID', example: 1 }),
+userId: z.number().int().positive().nullable().openapi({ description: '业务员用户ID', example: 1 }),
+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' }),
+createdBy: z.number().int().positive().nullable().openapi({ description: '创建用户ID', example: 1 }),
+updatedBy: z.number().int().positive().nullable().openapi({ description: '更新用户ID', example: 1 }),
+client: z.object({
+  id: z.number(),
+  companyName: z.string(),
+  contactPerson: z.string().nullable()
+}).nullable().optional().openapi({
+  description: '关联客户信息'
+}),
+linkman: z.object({
+  id: z.number(),
+  name: z.string(),
+  mobile: z.string().nullable()
+}).nullable().optional().openapi({
+  description: '关联联系人信息'
+}),
+user: z.object({
+  id: z.number(),
+  username: z.string(),
+  name: z.string().nullable()
+}).nullable().optional().openapi({
+  description: '关联业务员信息'
+})
 });
 
 // 创建DTO