Ver código fonte

✨ feat(admin): 添加日期备注管理功能

- 在管理菜单中添加日期备注管理选项及CalendarOutlined图标
- 创建日期备注管理页面,实现数据的增删改查功能
- 添加日期备注管理路由配置
- 实现表格展示、搜索、分页、筛选及状态标签显示功能
- 支持日期备注的创建、编辑和删除操作
yourname 5 meses atrás
pai
commit
e74a9dc7f4

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

@@ -11,6 +11,7 @@ import {
   FileTextOutlined,
   LineChartOutlined,
   CodeOutlined,
+  CalendarOutlined,
 } from '@ant-design/icons';
 
 export interface MenuItem {
@@ -116,6 +117,13 @@ export const useMenu = () => {
       icon: <CodeOutlined />,
       path: '/admin/stock-xunlian-codes',
       permission: 'stock:manage'
+    },
+    {
+      key: 'date-notes',
+      label: '日期备注管理',
+      icon: <CalendarOutlined />,
+      path: '/admin/date-notes',
+      permission: 'stock:manage'
     }
   ];
 

+ 302 - 0
src/client/admin/pages/DateNotesPage.tsx

@@ -0,0 +1,302 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, DatePicker, Space, Typography, message, Tag } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
+import { dateNotesClient } from '@/client/api';
+import type { InferResponseType, InferRequestType } from 'hono/client';
+import { App } from 'antd';
+
+const { Title } = Typography;
+
+// 定义类型
+type DateNotesListResponse = InferResponseType<typeof dateNotesClient.$get, 200>;
+type DateNotesItem = DateNotesListResponse['data'][0];
+type CreateDateNotesRequest = InferRequestType<typeof dateNotesClient.$post>['json'];
+type UpdateDateNotesRequest = InferRequestType<typeof dateNotesClient[':id']['$put']>['json'];
+
+export const DateNotesPage: React.FC = () => {
+  const [data, setData] = useState<DateNotesItem[]>([]);
+  const [loading, setLoading] = useState<boolean>(true);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  const [searchText, setSearchText] = useState('');
+  const [isModalVisible, setIsModalVisible] = useState(false);
+  const [isEditing, setIsEditing] = useState(false);
+  const [currentItem, setCurrentItem] = useState<DateNotesItem | null>(null);
+  const [form] = Form.useForm();
+  const { message: antMessage } = App.useApp();
+
+  // 获取数据列表
+  const fetchData = async () => {
+    try {
+      setLoading(true);
+      const res = await dateNotesClient.$get({
+        query: {
+          page: pagination.current,
+          pageSize: pagination.pageSize,
+          keyword: searchText,
+        },
+      });
+      
+      if (!res.ok) {
+        throw new Error('获取数据失败');
+      }
+      
+      const result = await res.json() as DateNotesListResponse;
+      setData(result.data);
+      setPagination(prev => ({
+        ...prev,
+        total: result.pagination.total,
+      }));
+    } catch (error) {
+      console.error('获取日期备注数据失败:', error);
+      antMessage.error('获取数据失败,请重试');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 初始加载和分页、搜索变化时重新获取数据
+  useEffect(() => {
+    fetchData();
+  }, [pagination.current, pagination.pageSize]);
+
+  // 搜索功能
+  const handleSearch = () => {
+    setPagination(prev => ({ ...prev, current: 1 }));
+    fetchData();
+  };
+
+  // 显示创建模态框
+  const showCreateModal = () => {
+    setIsEditing(false);
+    setCurrentItem(null);
+    form.resetFields();
+    setIsModalVisible(true);
+  };
+
+  // 显示编辑模态框
+  const showEditModal = (record: DateNotesItem) => {
+    setIsEditing(true);
+    setCurrentItem(record);
+    form.setFieldsValue({
+      code: record.code,
+      noteDate: record.noteDate ? new Date(record.noteDate) : null,
+      note: record.note,
+    });
+    setIsModalVisible(true);
+  };
+
+  // 处理表单提交
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      
+      if (isEditing && currentItem) {
+        // 更新数据
+        const res = await dateNotesClient[':id'].$put({
+          param: { id: currentItem.id },
+          json: values as UpdateDateNotesRequest,
+        });
+        
+        if (!res.ok) {
+          throw new Error('更新失败');
+        }
+        antMessage.success('更新成功');
+      } else {
+        // 创建新数据
+        const res = await dateNotesClient.$post({
+          json: values as CreateDateNotesRequest,
+        });
+        
+        if (!res.ok) {
+          throw new Error('创建失败');
+        }
+        antMessage.success('创建成功');
+      }
+      
+      setIsModalVisible(false);
+      fetchData();
+    } catch (error) {
+      console.error('提交表单失败:', error);
+      antMessage.error(isEditing ? '更新失败,请重试' : '创建失败,请重试');
+    }
+  };
+
+  // 删除数据
+  const handleDelete = async (id: number) => {
+    try {
+      const res = await dateNotesClient[':id'].$delete({
+        param: { id },
+      });
+      
+      if (!res.ok) {
+        throw new Error('删除失败');
+      }
+      
+      antMessage.success('删除成功');
+      fetchData();
+    } catch (error) {
+      console.error('删除数据失败:', error);
+      antMessage.error('删除失败,请重试');
+    }
+  };
+
+  // 表格列定义
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '股票代码',
+      dataIndex: 'code',
+      key: 'code',
+      filters: [
+        ...Array.from(new Set(data.map(item => item.code))).map(code => ({
+          text: code,
+          value: code,
+        }))
+      ],
+      onFilter: (value: string, record: DateNotesItem) => record.code === value,
+    },
+    {
+      title: '备注日期',
+      dataIndex: 'noteDate',
+      key: 'noteDate',
+      render: (date: string) => date ? new Date(date).toLocaleString() : '-',
+    },
+    {
+      title: '备注内容',
+      dataIndex: 'note',
+      key: 'note',
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      render: (date: string) => new Date(date).toLocaleString(),
+    },
+    {
+      title: '状态',
+      key: 'status',
+      render: (_, record: DateNotesItem) => (
+        <Tag color={new Date(record.updatedAt) > new Date(record.createdAt) ? 'blue' : 'green'}>
+          {new Date(record.updatedAt) > new Date(record.createdAt) ? '已更新' : '原始数据'}
+        </Tag>
+      ),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: DateNotesItem) => (
+        <Space size="small">
+          <Button 
+            type="text" 
+            icon={<EditOutlined />} 
+            onClick={() => showEditModal(record)}
+          >
+            编辑
+          </Button>
+          <Button 
+            type="text" 
+            danger 
+            icon={<DeleteOutlined />} 
+            onClick={() => handleDelete(record.id)}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div className="page-container">
+      <div className="page-header" style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+        <Title level={2}>日期备注管理</Title>
+        <Button type="primary" icon={<PlusOutlined />} onClick={showCreateModal}>
+          添加备注
+        </Button>
+      </div>
+      
+      <div className="search-container" style={{ marginBottom: 16 }}>
+        <Input
+          placeholder="搜索股票代码或备注内容"
+          value={searchText}
+          onChange={(e) => setSearchText(e.target.value)}
+          onPressEnter={handleSearch}
+          style={{ width: 300 }}
+          suffix={<SearchOutlined onClick={handleSearch} />}
+        />
+      </div>
+      
+      <Table
+        columns={columns}
+        dataSource={data.map(item => ({ ...item, key: item.id }))}
+        loading={loading}
+        pagination={{
+          current: pagination.current,
+          pageSize: pagination.pageSize,
+          total: pagination.total,
+          showSizeChanger: true,
+          showTotal: (total) => `共 ${total} 条记录`,
+        }}
+        onChange={(p) => setPagination({ ...pagination, current: p.current || 1, pageSize: p.pageSize || 10 })}
+        rowKey="id"
+      />
+      
+      <Modal
+        title={isEditing ? "编辑日期备注" : "添加日期备注"}
+        open={isModalVisible}
+        onOk={handleSubmit}
+        onCancel={() => setIsModalVisible(false)}
+        destroyOnClose
+        maskClosable={false}
+        width={600}
+      >
+        <Form
+          form={form}
+          layout="vertical"
+          name="date_notes_form"
+        >
+          <Form.Item
+            name="code"
+            label="股票代码"
+            rules={[
+              { required: true, message: '请输入股票代码' },
+              { max: 255, message: '股票代码不能超过255个字符' }
+            ]}
+          >
+            <Input placeholder="请输入股票代码" />
+          </Form.Item>
+          
+          <Form.Item
+            name="noteDate"
+            label="备注日期"
+            rules={[{ required: true, message: '请选择备注日期' }]}
+          >
+            <DatePicker showTime placeholder="请选择备注日期" style={{ width: '100%' }} />
+          </Form.Item>
+          
+          <Form.Item
+            name="note"
+            label="备注内容"
+            rules={[
+              { required: true, message: '请输入备注内容' },
+              { max: 255, message: '备注内容不能超过255个字符' }
+            ]}
+          >
+            <Input.TextArea rows={4} placeholder="请输入备注内容" />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default DateNotesPage;

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

@@ -10,6 +10,7 @@ import { ClassroomDataPage } from './pages/ClassroomDataPage';
 import { SubmissionRecordsPage } from './pages/SubmissionRecordsPage';
 import { StockDataPage } from './pages/StockDataPage';
 import { StockXunlianCodesPage } from './pages/StockXunlianCodesPage';
+import { DateNotesPage } from './pages/DateNotesPage';
 import { LoginPage } from './pages/Login';
 
 export const router = createBrowserRouter([
@@ -63,6 +64,11 @@ export const router = createBrowserRouter([
         element: <StockXunlianCodesPage />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'date-notes',
+        element: <DateNotesPage />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,