Ver Fonte

✨ feat(stock): 重构训练案例日期系统,支持日期范围设置

- 将"交易日期"字段重构为"训练开始日期"和"训练结束日期"范围选择
- 更新实体模型,添加trainingStartDate和trainingEndDate字段
- 调整前端表单和表格,支持日期范围的显示与编辑
- 使用Hono类型系统优化API请求和响应类型定义

🔧 chore: 删除过时的UI样式规范文档

- 移除.roo/rules/13-ui-style.md文件
- 更新CLAUDE.md文档引用,移除对UI样式规范的引用
yourname há 6 meses atrás
pai
commit
130b7bc824

+ 0 - 169
.roo/rules/13-ui-style.md

@@ -1,169 +0,0 @@
-# 管理后台界面开发规范
-
-## 1. 布局规范
-
-### 1.1 整体布局结构
-- 采用三栏布局:侧边导航栏 + 顶部操作栏 + 主内容区
-- 侧边栏固定宽度240px,支持折叠功能
-- 顶部导航栏高度固定为64px
-- 主内容区边距统一为24px
-
-### 1.2 响应式设计
-- 桌面端:完整三栏布局
-- 平板端:可折叠侧边栏
-- 移动端:侧边栏转为抽屉式导航
-
-### 1.3 容器样式
-- 卡片容器使用白色背景(#ffffff)
-- 卡片阴影使用 `shadow-sm transition-all duration-300 hover:shadow-md`
-- 卡片边框使用 `border: none`
-- 卡片圆角统一为 `border-radius: 6px`
-
-## 2. 色彩规范
-
-### 2.1 主色调
-- 主色:蓝色(#1890ff),用于主要按钮、选中状态和关键交互元素
-- 辅助色:绿色(#52c41a)用于成功状态,红色(#ff4d4f)用于错误状态,黄色(#faad14)用于警告状态
-
-### 2.2 中性色
-- 背景色:浅灰(#f5f5f5)用于页面背景,白色(#ffffff)用于卡片背景
-- 文本色:深灰(#1f2937)用于主要文本,中灰(#6b7280)用于次要文本,浅灰(#9ca3af)用于提示文本
-
-## 3. 组件样式规范
-
-### 3.1 按钮样式
-- 主要按钮:使用主色调背景,白色文字
-- 按钮高度统一为40px,大型按钮使用48px
-- 按钮圆角统一为4px
-- 按钮文本使用14px字体
-- 按钮添加悬停效果:`hover:shadow-lg transition-all duration-200`
-
-### 3.2 表单元素
-- 输入框高度统一为40px
-- 输入框前缀图标颜色使用主色调
-- 表单标签宽度统一为80px
-- 表单布局使用垂直布局,标签在上,输入框在下
-- 输入框聚焦状态:`focus:border-primary focus:ring-1 focus:ring-primary`
-
-### 3.5 日期表单组件
-- 日期选择器使用 `DatePicker` 组件,时间选择使用 `TimePicker` 组件
-- 日期选择器大小与输入框保持一致:`size="middle"`
-- 日期格式统一为 `YYYY-MM-DD`,时间格式为 `HH:mm:ss`
-- 日期范围选择使用 `RangePicker` 组件,格式为 `[YYYY-MM-DD, YYYY-MM-DD]`
-- 日期选择器添加清除按钮:`allowClear`
-- 日期选择器添加占位提示:`placeholder="请选择日期"`
-- 日期选择器禁用未来日期:`disabledDate={(current) => current && current > dayjs().endOf('day')}`(根据业务需求调整)
-- 日期对象规范:始终使用dayjs对象而非原生Date对象,避免出现"isValid is not a function"错误
-  ```typescript
-  // 错误示例 - 使用原生Date对象
-  form.setFieldsValue({
-    noteDate: new Date(record.noteDate) // 导致验证失败
-  });
-  
-  // 正确示例 - 使用dayjs对象
-  form.setFieldsValue({
-    noteDate: dayjs(record.noteDate) // 正常支持验证方法
-  });
-  ```
-- 日期时间转换规范:
-  ```typescript
-  // 日期对象转字符串(提交给后端)
-  const formatDate = (date: Dayjs | null) => {
-    return date ? date.format('YYYY-MM-DD') : '';
-  };
-  
-  // 字符串转日期对象(从后端接收)
-  const parseDate = (str: string) => {
-    return str ? dayjs(str) : null;
-  };
-  
-  // 日期时间对象转字符串
-  const formatDateTime = (date: Dayjs | null) => {
-    return date ? date.format('YYYY-MM-DD HH:mm:ss') : '';
-  };
-  
-  // 日期范围转换
-  const formatDateRange = (range: [Dayjs | null, Dayjs | null]) => {
-    return range && range[0] && range[1]
-      ? [range[0].format('YYYY-MM-DD'), range[1].format('YYYY-MM-DD')]
-      : [];
-  };
-  ```
-
-### 3.3 表格样式
-- 表格添加边框:`bordered`
-- 表头背景色使用浅灰(#f9fafb)
-- 表格行添加交替背景色:`rowClassName={(record, index) => index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`
-- 支持横向滚动:`scroll={{ x: 'max-content' }}`
-
-### 3.4 卡片组件
-- 卡片标题区使用 `flex items-center justify-between` 布局
-- 统计数字使用28px字体大小
-- 添加卡片图标时使用24px大小,颜色与统计项主题匹配
-- 卡片底部添加辅助信息,使用12px浅灰色字体
-
-## 4. 页面规范
-
-### 4.1 页面标题
-- 页面标题使用 `Title level={2}` 组件
-- 标题区添加 `mb-6 flex justify-between items-center` 样式
-- 标题右侧可放置操作按钮组
-
-### 4.2 登录页面
-- 使用渐变背景:`bg-gradient-to-br from-blue-50 to-indigo-100`
-- 登录卡片居中显示,添加阴影效果:`shadow-lg`
-- 登录表单添加图标前缀增强可读性
-- 底部添加版权信息和测试账号提示
-
-### 4.3 数据展示页面
-- 数据卡片使用响应式布局,在不同屏幕尺寸下自动调整列数
-- 关键数据使用 `Statistic` 组件展示
-- 添加数据趋势指示和环比增长信息
-- 数据加载状态使用 `loading` 属性
-
-## 5. 交互规范
-
-### 5.1 悬停效果
-- 可交互元素添加悬停效果
-- 卡片悬停效果:`hover:shadow-md transition-all duration-300`
-- 按钮悬停效果:`hover:shadow-lg transition-all duration-200`
-
-### 5.2 模态框
-- 模态框使用 `destroyOnClose` 属性确保每次打开都是新实例
-- 模态框居中显示:`centered`
-- 禁止点击遮罩关闭:`maskClosable={false}`
-- 表单模态框使用垂直布局
-
-### 5.3 反馈机制
-- 操作成功/失败使用 `message` 组件提供反馈
-- 加载状态使用 `loading` 属性显示加载指示器
-- 删除等危险操作使用 `Popconfirm` 组件二次确认
-
-## 5.4 消息提示规范
-- 统一使用App.useApp()获取message实例
-  ```typescript
-  import { App } from 'antd';
-  const { message } = App.useApp();
-  ```
-- 消息提示使用明确的类型区分:
-  ```typescript
-  message.success('操作成功');
-  message.error('操作失败');
-  message.warning('警告信息');
-  message.info('提示信息');
-  ```
-- 消息显示时长统一使用默认值,重要操作可适当延长:`message.success('操作成功', 3);`
-
-## 6. 图标规范
-
-### 6.1 图标选择
-- 用户相关:UserOutlined
-- 密码相关:LockOutlined
-- 搜索相关:SearchOutlined
-- 消息相关:BellOutlined
-- 眼睛相关:EyeOutlined/EyeInvisibleOutlined
-
-### 6.2 图标样式
-- 功能图标大小统一为24px
-- 前缀图标颜色与主题匹配
-- 操作图标使用 `Button type="link"` 样式

+ 0 - 1
CLAUDE.md

@@ -65,7 +65,6 @@ This is a full-stack TypeScript application using:
 - @.roo/rules/11-home-frontend.md - 首页前端规范
 - @.roo/rules/11-standard-crud.md - 标准CRUD规范
 - @.roo/rules/12-generic-crud.md - 通用CRUD规范
-- @.roo/rules/13-ui-style.md - UI样式规范
 - @.roo/rules/14-crud-filtering.md - CRUD筛选规范
 - @.roo/rules/15-user-tracking.md - 用户追踪规范
 

+ 104 - 24
src/client/admin/pages/StockXunlianCodesPage.tsx

@@ -6,6 +6,7 @@ import { Plus, Search, Edit, Trash2, CalendarIcon } from 'lucide-react';
 import { useForm } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { z } from 'zod';
+import type { InferResponseType, InferRequestType } from 'hono/client';
 
 import { Button } from '@/client/components/ui/button';
 import { Input } from '@/client/components/ui/input';
@@ -24,19 +25,10 @@ import { CreateStockXunlianCodesDto, UpdateStockXunlianCodesDto } from '@/server
 import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
 
 // 类型定义
-type CreateRequest = z.infer<typeof CreateStockXunlianCodesDto>;
-type UpdateRequest = z.infer<typeof UpdateStockXunlianCodesDto>;
-type StockXunlianCode = {
-  id: number;
-  code: string;
-  stockName: string;
-  name: string;
-  type: string | null;
-  description: string | null;
-  tradeDate: string;
-  createdAt: string;
-  updatedAt: string;
-};
+type CreateRequest = InferRequestType<typeof stockXunlianCodesClient.$post>['json'];
+type UpdateRequest = InferRequestType<typeof stockXunlianCodesClient[':id']['$put']>['json'];
+type StockXunlianCode = InferResponseType<typeof stockXunlianCodesClient.$get, 200>['data'][0];
+type StockXunlianCodeListResponse = InferResponseType<typeof stockXunlianCodesClient.$get, 200>;
 
 // 表单Schema
 const createFormSchema = CreateStockXunlianCodesDto;
@@ -66,7 +58,8 @@ export const StockXunlianCodesPage = () => {
       name: '',
       type: '',
       description: '',
-      tradeDate: new Date(),
+      trainingStartDate: new Date(),
+      trainingEndDate: new Date(),
     },
   });
 
@@ -75,7 +68,7 @@ export const StockXunlianCodesPage = () => {
   });
 
   // 数据查询
-  const { data, isLoading, refetch } = useQuery({
+  const { data, isLoading, refetch } = useQuery<StockXunlianCodeListResponse>({
     queryKey: ['stock-xunlian-codes', searchParams],
     queryFn: async () => {
       const res = await stockXunlianCodesClient.$get({
@@ -170,7 +163,8 @@ export const StockXunlianCodesPage = () => {
       name: entity.name,
       type: entity.type || '',
       description: entity.description || '',
-      tradeDate: new Date(entity.tradeDate),
+      trainingStartDate: new Date(entity.trainingStartDate),
+      trainingEndDate: new Date(entity.trainingEndDate),
     });
     setIsModalOpen(true);
   };
@@ -267,7 +261,8 @@ export const StockXunlianCodesPage = () => {
                   <TableHead>股票名称</TableHead>
                   <TableHead>案例名称</TableHead>
                   <TableHead>案例类型</TableHead>
-                  <TableHead>交易日期</TableHead>
+                  <TableHead>训练开始日期</TableHead>
+                  <TableHead>训练结束日期</TableHead>
                   <TableHead>创建时间</TableHead>
                   <TableHead className="text-right">操作</TableHead>
                 </TableRow>
@@ -279,7 +274,8 @@ export const StockXunlianCodesPage = () => {
                     <TableCell>{item.stockName}</TableCell>
                     <TableCell>{item.name}</TableCell>
                     <TableCell>{item.type || '-'}</TableCell>
-                    <TableCell>{format(new Date(item.tradeDate), 'yyyy-MM-dd')}</TableCell>
+                    <TableCell>{format(new Date(item.trainingStartDate), 'yyyy-MM-dd')}</TableCell>
+                    <TableCell>{format(new Date(item.trainingEndDate), 'yyyy-MM-dd')}</TableCell>
                     <TableCell>{format(new Date(item.createdAt), 'yyyy-MM-dd HH:mm')}</TableCell>
                     <TableCell className="text-right">
                       <div className="flex justify-end gap-2">
@@ -405,10 +401,10 @@ export const StockXunlianCodesPage = () => {
 
                 <FormField
                   control={createForm.control}
-                  name="tradeDate"
+                  name="trainingStartDate"
                   render={({ field }) => (
                     <FormItem className="flex flex-col">
-                      <FormLabel>交易日期</FormLabel>
+                      <FormLabel>训练开始日期</FormLabel>
                       <Popover>
                         <PopoverTrigger asChild>
                           <FormControl>
@@ -422,7 +418,49 @@ export const StockXunlianCodesPage = () => {
                               {field.value ? (
                                 format(field.value, 'yyyy-MM-dd')
                               ) : (
-                                <span>选择日期</span>
+                                <span>选择开始日期</span>
+                              )}
+                              <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+                            </Button>
+                          </FormControl>
+                        </PopoverTrigger>
+                        <PopoverContent className="w-auto p-0" align="start">
+                          <Calendar
+                            mode="single"
+                            selected={field.value}
+                            onSelect={field.onChange}
+                            disabled={(date) =>
+                              date > new Date() || date < new Date('1900-01-01')
+                            }
+                            initialFocus
+                          />
+                        </PopoverContent>
+                      </Popover>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="trainingEndDate"
+                  render={({ field }) => (
+                    <FormItem className="flex flex-col">
+                      <FormLabel>训练结束日期</FormLabel>
+                      <Popover>
+                        <PopoverTrigger asChild>
+                          <FormControl>
+                            <Button
+                              variant="outline"
+                              className={cn(
+                                'w-full pl-3 text-left font-normal',
+                                !field.value && 'text-muted-foreground'
+                              )}
+                            >
+                              {field.value ? (
+                                format(field.value, 'yyyy-MM-dd')
+                              ) : (
+                                <span>选择结束日期</span>
                               )}
                               <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
                             </Button>
@@ -530,10 +568,52 @@ export const StockXunlianCodesPage = () => {
 
                 <FormField
                   control={updateForm.control}
-                  name="tradeDate"
+                  name="trainingStartDate"
+                  render={({ field }) => (
+                    <FormItem className="flex flex-col">
+                      <FormLabel>训练开始日期</FormLabel>
+                      <Popover>
+                        <PopoverTrigger asChild>
+                          <FormControl>
+                            <Button
+                              variant="outline"
+                              className={cn(
+                                'w-full pl-3 text-left font-normal',
+                                !field.value && 'text-muted-foreground'
+                              )}
+                            >
+                              {field.value ? (
+                                format(field.value, 'yyyy-MM-dd')
+                              ) : (
+                                <span>选择开始日期</span>
+                              )}
+                              <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+                            </Button>
+                          </FormControl>
+                        </PopoverTrigger>
+                        <PopoverContent className="w-auto p-0" align="start">
+                          <Calendar
+                            mode="single"
+                            selected={field.value}
+                            onSelect={field.onChange}
+                            disabled={(date) =>
+                              date > new Date() || date < new Date('1900-01-01')
+                            }
+                            initialFocus
+                          />
+                        </PopoverContent>
+                      </Popover>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="trainingEndDate"
                   render={({ field }) => (
                     <FormItem className="flex flex-col">
-                      <FormLabel>交易日期</FormLabel>
+                      <FormLabel>训练结束日期</FormLabel>
                       <Popover>
                         <PopoverTrigger asChild>
                           <FormControl>
@@ -547,7 +627,7 @@ export const StockXunlianCodesPage = () => {
                               {field.value ? (
                                 format(field.value, 'yyyy-MM-dd')
                               ) : (
-                                <span>选择日期</span>
+                                <span>选择结束日期</span>
                               )}
                               <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
                             </Button>

+ 5 - 2
src/server/modules/stock/stock-xunlian-codes.entity.ts

@@ -20,8 +20,11 @@ export class StockXunlianCodes {
   @Column({ name: 'description', type: 'varchar', length: 255, nullable: true, comment: '案例描述' })
   description!: string | null;
 
-  @Column({ name: 'trade_date', type: 'timestamp', nullable: false, comment: '交易日期' })
-  tradeDate!: Date;
+  @Column({ name: 'training_start_date', type: 'timestamp', nullable: false, comment: '训练开始日期' })
+  trainingStartDate!: Date;
+
+  @Column({ name: 'training_end_date', type: 'timestamp', nullable: false, comment: '训练结束日期' })
+  trainingEndDate!: Date;
 
   @Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
   createdAt!: Date;

+ 28 - 9
src/server/modules/stock/stock-xunlian-codes.schema.ts

@@ -52,10 +52,16 @@ export const StockXunlianCodesSchema = z.object({
       example: '基于MACD指标的技术分析案例'
     }),
     
-  tradeDate: z.coerce.date<Date>()
+  trainingStartDate: z.coerce.date<Date>()
     .openapi({
-      description: '交易日期',
-      example: '2024-01-15'
+      description: '训练开始日期',
+      example: '2024-01-01'
+    }),
+
+  trainingEndDate: z.coerce.date<Date>()
+    .openapi({
+      description: '训练结束日期',
+      example: '2024-12-31'
     }),
     
   createdAt: z.coerce.date<Date>()
@@ -115,10 +121,16 @@ export const CreateStockXunlianCodesDto = z.object({
       example: '基于MACD指标的技术分析案例'
     }),
     
-  tradeDate: z.coerce.date<Date>()
+  trainingStartDate: z.coerce.date<Date>()
+    .openapi({
+      description: '训练开始日期',
+      example: '2024-01-01'
+    }),
+
+  trainingEndDate: z.coerce.date<Date>()
     .openapi({
-      description: '交易日期',
-      example: '2024-01-15'
+      description: '训练结束日期',
+      example: '2024-12-31'
     })
 });
 
@@ -169,10 +181,17 @@ export const UpdateStockXunlianCodesDto = z.object({
       example: '基于MACD指标的技术分析案例'
     }),
     
-  tradeDate: z.coerce.date<Date>()
+  trainingStartDate: z.coerce.date<Date>()
+    .optional()
+    .openapi({
+      description: '训练开始日期',
+      example: '2024-01-01'
+    }),
+
+  trainingEndDate: z.coerce.date<Date>()
     .optional()
     .openapi({
-      description: '交易日期',
-      example: '2024-01-15'
+      description: '训练结束日期',
+      example: '2024-12-31'
     })
 });