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

✨ feat(admin): 重构股票训练代码管理页面并新增移动端H5开发指令

- 将股票训练代码管理页面从Ant Design迁移到Shadcn UI组件库
- 新增React Query v5数据获取和状态管理
- 集成zod和react-hook-form进行表单验证
- 添加移动端小程序开发完整指令文档
- 优化用户体验:加载状态、错误处理、删除确认等
- 新增搜索、分页、创建、编辑、删除功能
yourname 6 месяцев назад
Родитель
Сommit
d278270352
2 измененных файлов с 1773 добавлено и 329 удалено
  1. 1193 0
      .roo/commands/mobile-h5-shadui-页面开发.md
  2. 580 329
      src/client/admin/pages/StockXunlianCodesPage.tsx

+ 1193 - 0
.roo/commands/mobile-h5-shadui-页面开发.md

@@ -0,0 +1,1193 @@
+---
+description: "小程序shadui页面的开发指令"
+---
+
+# 小程序页面开发指令
+
+## 概述
+本指令规范了基于Taro + React + Shadui + Tailwind CSS的小程序页面开发流程,包含tabbar页和非tabbar页的创建标准和最佳实践,涵盖了认证、RPC调用、React Query v5使用等核心功能。
+
+## 小程序Shadui路径
+mini/src/components/ui
+
+## 当前可用的Shadui组件
+基于项目实际文件,当前小程序可用的shadui组件如下:
+
+### 基础组件
+- **Button** - 按钮组件 (`button.tsx`)
+- **Card** - 卡片组件 (`card.tsx`)
+- **Input** - 输入框组件 (`input.tsx`)
+- **Label** - 标签组件 (`label.tsx`)
+- **Form** - 表单组件 (`form.tsx`)
+
+### 交互组件
+- **AvatarUpload** - 头像上传组件 (`avatar-upload.tsx`)
+- **Carousel** - 轮播图组件 (`carousel.tsx`)
+- **Image** - 图片组件 (`image.tsx`)
+
+### 导航组件
+- **Navbar** - 顶部导航栏组件 (`navbar.tsx`)
+- **TabBar** - 底部标签栏组件 (`tab-bar.tsx`)
+
+### 布局组件
+- **TabBarLayout**: 用于tabbar页面,包含底部导航
+
+- 根据需求可扩展更多业务组件
+
+## 组件使用示例
+
+### Button 组件
+```typescript
+import { Button } from '@/components/ui/button'
+
+// 基础用法
+<Button onClick={handleClick}>主要按钮</Button>
+
+// 不同尺寸
+<Button size="sm">小按钮</Button>
+<Button size="md">中按钮</Button>
+<Button size="lg">大按钮</Button>
+
+// 不同样式
+<Button variant="primary">主要按钮</Button>
+<Button variant="secondary">次要按钮</Button>
+<Button variant="outline">边框按钮</Button>
+<Button variant="ghost">幽灵按钮</Button>
+```
+
+### Input 组件
+```typescript
+import { Input } from '@/components/ui/input'
+
+// 基础用法
+<Input placeholder="请输入内容" />
+
+// 受控组件
+<Input value={value} onChange={handleChange} />
+
+// 不同类型
+<Input type="text" placeholder="文本输入" />
+<Input type="number" placeholder="数字输入" />
+<Input type="password" placeholder="密码输入" />
+```
+
+### Form 组件
+```typescript
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { z } from 'zod'
+
+const formSchema = z.object({
+  username: z.string().min(2, '用户名至少2个字符'),
+  email: z.string().email('请输入有效的邮箱地址')
+})
+
+const form = useForm({
+  resolver: zodResolver(formSchema),
+  defaultValues: { username: '', email: '' }
+})
+
+<Form {...form}>
+  <FormField
+    name="username"
+    render={({ field }) => (
+      <FormItem>
+        <FormLabel>用户名</FormLabel>
+        <FormControl>
+          <Input placeholder="请输入用户名" {...field} />
+        </FormControl>
+        <FormMessage />
+      </FormItem>
+    )}
+  />
+</Form>
+```
+
+### Card 组件
+```typescript
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+
+<Card>
+  <CardHeader>
+    <CardTitle>卡片标题</CardTitle>
+  </CardHeader>
+  <CardContent>
+    <Text>卡片内容</Text>
+  </CardContent>
+</Card>
+```
+
+### Navbar 组件
+```typescript
+import { Navbar } from '@/components/ui/navbar'
+
+// 基础用法
+<Navbar title="页面标题" />
+
+// 带返回按钮
+<Navbar 
+  title="页面标题" 
+  leftIcon="i-heroicons-chevron-left-20-solid"
+  onClickLeft={() => Taro.navigateBack()}
+/>
+
+// 带右侧操作
+<Navbar 
+  title="页面标题"
+  rightIcon="i-heroicons-share-20-solid"
+  onClickRight={handleShare}
+/>
+```
+
+### Carousel 组件
+```typescript
+// 实际页面使用示例
+export function HomeCarousel() {
+  const bannerItems: CarouselItem[] = [
+    {
+      src: 'https://via.placeholder.com/750x400/3B82F6/FFFFFF?text=Banner+1',
+      title: '新品上市',
+      description: '最新款式,限时优惠',
+      link: '/pages/goods/new-arrival'
+    },
+    {
+      src: 'https://via.placeholder.com/750x400/EF4444/FFFFFF?text=Banner+2',
+      title: '限时秒杀',
+      description: '每日特价,不容错过',
+      link: '/pages/goods/flash-sale'
+    },
+    {
+      src: 'https://via.placeholder.com/750x400/10B981/FFFFFF?text=Banner+3',
+      title: '会员专享',
+      description: '会员专享折扣和福利',
+      link: '/pages/member/benefits'
+    }
+  ]
+
+  const handleBannerClick = (item: CarouselItem, index: number) => {
+    if (item.link) {
+      // 使用Taro跳转
+      Taro.navigateTo({
+        url: item.link
+      })
+    }
+  }
+
+  return (
+    <View className="w-full">
+      <Carousel
+        items={bannerItems}
+        height={400}
+        autoplay={true}
+        interval={4000}
+        circular={true}
+        rounded="none"
+        onItemClick={handleBannerClick}
+      />
+    </View>
+  )
+}
+```
+
+## 页面类型分类
+
+### 1. TabBar页面(底部导航页)
+特点:
+- 使用 `TabBarLayout` 布局组件
+- 路径配置在 `mini/src/app.config.ts` 中的 `tabBar.list`
+- 包含底部导航栏,用户可直接切换
+- 通常包含 `Navbar` 顶部导航组件
+- 示例页面:首页、发现、个人中心
+
+### 2. 非TabBar页面(独立页面)
+特点:
+- 不使用 `TabBarLayout`,直接渲染内容
+- 使用 `Navbar` 组件作为顶部导航
+- 需要手动处理返回导航
+- 示例页面:登录、注册、详情页
+
+## 开发流程
+
+### 1. 创建页面目录
+```bash
+# TabBar页面
+mkdir -p mini/src/pages/[页面名称]
+
+# 非TabBar页面
+mkdir -p mini/src/pages/[页面名称]
+```
+
+### 2. 创建页面文件
+
+#### TabBar页面模板
+```typescript
+// mini/src/pages/[页面名称]/index.tsx
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import { TabBarLayout } from '@/layouts/tab-bar-layout'
+import { Navbar } from '@/components/ui/navbar'
+import { Button } from '@/components/ui/button'
+import { Card } from '@/components/ui/card'
+import './index.css'
+
+const [页面名称]Page: React.FC = () => {
+  return (
+    <TabBarLayout activeKey="[对应tabBar.key]">
+      <Navbar
+        title="页面标题"
+        rightIcon="i-heroicons-[图标名称]-20-solid"
+        onClickRight={() => console.log('点击右上角')}
+        leftIcon=""
+      />
+      <View className="px-4 py-4">
+        <Card>
+          <CardHeader>
+            <CardTitle>欢迎使用</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <Text>这是一个使用shadui组件的TabBar页面</Text>
+            <Button className="mt-4">开始使用</Button>
+          </CardContent>
+        </Card>
+      </View>
+    </TabBarLayout>
+  )
+}
+
+export default [页面名称]Page
+```
+
+#### 非TabBar页面模板
+```typescript
+// mini/src/pages/[页面名称]/index.tsx
+import { View } from '@tarojs/components'
+import { useEffect } from 'react'
+import Taro from '@tarojs/taro'
+import { Navbar } from '@/components/ui/navbar'
+import { Card } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import './index.css'
+
+export default function [页面名称]() {
+  useEffect(() => {
+    Taro.setNavigationBarTitle({
+      title: '页面标题'
+    })
+  }, [])
+
+  return (
+    <View className="min-h-screen bg-gray-50">
+      <Navbar
+        title="页面标题"
+        backgroundColor="bg-transparent"
+        textColor="text-gray-900"
+        border={false}
+      />
+      <View className="px-6 py-4">
+        <Card>
+          <CardContent>
+            <Text>这是一个使用shadui组件的非TabBar页面</Text>
+            <Button className="mt-4">返回</Button>
+          </CardContent>
+        </Card>
+      </View>
+    </View>
+  )
+}
+```
+
+### 3. 页面配置文件
+```typescript
+// mini/src/pages/[页面名称]/index.config.ts
+export default definePageConfig({
+  navigationBarTitleText: '页面标题',
+  enablePullDownRefresh: true,
+  backgroundTextStyle: 'dark',
+  navigationBarBackgroundColor: '#ffffff',
+  navigationBarTextStyle: 'black'
+})
+```
+
+### 4. 样式文件
+统一使用tailwindcss类,index.css为空即可
+```css
+/* mini/src/pages/[页面名称]/index.css */
+
+```
+
+## 高级功能模板
+
+### 1. 带认证的页面模板
+```typescript
+// mini/src/pages/[需要认证的页面]/index.tsx
+import { View, Text } from '@tarojs/components'
+import { useEffect } from 'react'
+import { useAuth } from '@/utils/auth'
+import Taro from '@tarojs/taro'
+import { Navbar } from '@/components/ui/navbar'
+import { Card } from '@/components/ui/card'
+
+export default function ProtectedPage() {
+  const { user, isLoading, isLoggedIn } = useAuth()
+
+  useEffect(() => {
+    if (!isLoading && !isLoggedIn) {
+      Taro.navigateTo({ url: '/pages/login/index' })
+    }
+  }, [isLoading, isLoggedIn])
+
+  if (isLoading) {
+    return (
+      <View className="flex-1 flex items-center justify-center">
+        <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
+      </View>
+    )
+  }
+
+  if (!user) return null
+
+  return (
+    <View className="min-h-screen bg-gray-50">
+      <Navbar title="受保护页面" leftIcon="" />
+      <View className="px-4 py-4">
+        <Text>欢迎, {user.username}</Text>
+      </View>
+    </View>
+  )
+}
+```
+
+### 2. 带API调用的页面模板
+```typescript
+// mini/src/pages/[数据展示页面]/index.tsx
+import { View, ScrollView } from '@tarojs/components'
+import { useQuery } from '@tanstack/react-query'
+import { userClient } from '@/api'
+import { InferResponseType } from 'hono'
+import Taro from '@tarojs/taro'
+
+type UserListResponse = InferResponseType<typeof userClient.$get, 200>
+
+export default function UserListPage() {
+  const { data, isLoading, error } = useQuery<UserListResponse>({
+    queryKey: ['users'],
+    queryFn: async () => {
+      const response = await userClient.$get({})
+      if (response.status !== 200) {
+        throw new Error('获取用户列表失败')
+      }
+      return response.json()
+    },
+    staleTime: 5 * 60 * 1000, // 5分钟
+  })
+
+  if (isLoading) {
+    return (
+      <View className="flex-1 flex items-center justify-center">
+        <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
+      </View>
+    )
+  }
+
+  if (error) {
+    return (
+      <View className="flex-1 flex items-center justify-center">
+        <Text className="text-red-500">{error.message}</Text>
+      </View>
+    )
+  }
+
+  return (
+    <ScrollView className="h-screen">
+      <Navbar title="用户列表" leftIcon="" />
+      <View className="px-4 py-4">
+        {data?.data.map(user => (
+          <View key={user.id} className="bg-white rounded-lg p-4 mb-3">
+            <Text>{user.username}</Text>
+          </View>
+        ))}
+      </View>
+    </ScrollView>
+  )
+}
+```
+
+### 3. 带表单提交的页面模板
+```typescript
+// mini/src/pages/[表单页面]/index.tsx
+import { View } from '@tarojs/components'
+import { useState } from 'react'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { z } from 'zod'
+import { useMutation } from '@tanstack/react-query'
+import { userClient } from '@/api'
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+import { Button } from '@/components/ui/button'
+import Taro from '@tarojs/taro'
+
+const formSchema = z.object({
+  username: z.string().min(3, '用户名至少3个字符'),
+  email: z.string().email('请输入有效的邮箱地址'),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号')
+})
+
+type FormData = z.infer<typeof formSchema>
+
+export default function CreateUserPage() {
+  const [loading, setLoading] = useState(false)
+  
+  const form = useForm<FormData>({
+    resolver: zodResolver(formSchema),
+    defaultValues: {
+      username: '',
+      email: '',
+      phone: ''
+    }
+  })
+
+  const mutation = useMutation({
+    mutationFn: async (data: FormData) => {
+      const response = await userClient.$post({ json: data })
+      if (response.status !== 201) {
+        throw new Error('创建用户失败')
+      }
+      return response.json()
+    },
+    onSuccess: () => {
+      Taro.showToast({
+        title: '创建成功',
+        icon: 'success'
+      })
+      Taro.navigateBack()
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '创建失败',
+        icon: 'none'
+      })
+    }
+  })
+
+  const onSubmit = async (data: FormData) => {
+    setLoading(true)
+    try {
+      await mutation.mutateAsync(data)
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  return (
+    <View className="min-h-screen bg-gray-50">
+      <Navbar title="创建用户" leftIcon="" />
+      <View className="px-4 py-4">
+        <Form {...form}>
+          <View className="space-y-4">
+            <FormField
+              control={form.control}
+              name="username"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>用户名</FormLabel>
+                  <FormControl>
+                    <Input placeholder="请输入用户名" {...field} />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+            <Button
+              className="w-full"
+              onClick={form.handleSubmit(onSubmit)}
+              disabled={loading}
+            >
+              {loading ? '创建中...' : '创建用户'}
+            </Button>
+          </View>
+        </Form>
+      </View>
+    </View>
+  )
+}
+```
+
+## 认证功能使用
+
+### 1. useAuth Hook 使用规范
+```typescript
+import { useAuth } from '@/utils/auth'
+
+// 在页面或组件中使用
+const { 
+  user,           // 当前用户信息
+  login,          // 登录函数
+  logout,         // 登出函数
+  register,       // 注册函数
+  updateUser,     // 更新用户信息
+  isLoading,      // 加载状态
+  isLoggedIn      // 是否已登录
+} = useAuth()
+
+// 使用示例
+const handleLogin = async (formData) => {
+  try {
+    await login(formData)
+    Taro.switchTab({ url: '/pages/index/index' })
+  } catch (error) {
+    console.error('登录失败:', error)
+  }
+}
+```
+
+### 2. 页面权限控制
+```typescript
+// 在需要认证的页面顶部
+const { user, isLoading, isLoggedIn } = useAuth()
+
+useEffect(() => {
+  if (!isLoading && !isLoggedIn) {
+    Taro.navigateTo({ url: '/pages/login/index' })
+  }
+}, [isLoading, isLoggedIn])
+
+// 或者使用路由守卫模式
+```
+
+## RPC客户端调用规范
+
+### 1. 客户端导入
+```typescript
+// 从api.ts导入对应的客户端
+import { authClient, userClient, fileClient } from '@/api'
+```
+
+### 2. 类型提取
+```typescript
+import { InferResponseType, InferRequestType } from 'hono'
+
+// 响应类型提取
+type UserResponse = InferResponseType<typeof userClient.$get, 200>
+type UserDetailResponse = InferResponseType<typeof userClient[':id']['$get'], 200>
+
+// 请求类型提取
+type CreateUserRequest = InferRequestType<typeof userClient.$post>['json']
+type UpdateUserRequest = InferRequestType<typeof userClient[':id']['$put']>['json']
+```
+
+### 3. 调用示例
+```typescript
+// GET请求 - 列表
+const response = await userClient.$get({
+  query: {
+    page: 1,
+    pageSize: 10,
+    keyword: 'search'
+  }
+})
+
+// GET请求 - 单条
+const response = await userClient[':id'].$get({
+  param: { id: userId }
+})
+
+// POST请求
+const response = await userClient.$post({
+  json: {
+    username: 'newuser',
+    email: 'user@example.com'
+  }
+})
+
+// PUT请求
+const response = await userClient[':id'].$put({
+  param: { id: userId },
+  json: { username: 'updated' }
+})
+
+// DELETE请求
+const response = await userClient[':id'].$delete({
+  param: { id: userId }
+})
+```
+
+## React Query v5使用规范
+
+### 1. 查询配置
+```typescript
+const { data, isLoading, error, refetch } = useQuery({
+  queryKey: ['users', page, keyword], // 唯一的查询键
+  queryFn: async () => {
+    const response = await userClient.$get({
+      query: { page, pageSize: 10, keyword }
+    })
+    if (response.status !== 200) {
+      throw new Error('获取数据失败')
+    }
+    return response.json()
+  },
+  staleTime: 5 * 60 * 1000, // 5分钟
+  cacheTime: 10 * 60 * 1000, // 10分钟
+  retry: 3, // 重试3次
+  enabled: !!keyword, // 条件查询
+})
+```
+
+### 2. 变更操作
+```typescript
+const queryClient = useQueryClient()
+
+const mutation = useMutation({
+  mutationFn: async (data: CreateUserRequest) => {
+    const response = await userClient.$post({ json: data })
+    if (response.status !== 201) {
+      throw new Error('创建失败')
+    }
+    return response.json()
+  },
+  onSuccess: () => {
+    // 成功后刷新相关查询
+    queryClient.invalidateQueries({ queryKey: ['users'] })
+    Taro.showToast({ title: '创建成功', icon: 'success' })
+  },
+  onError: (error) => {
+    Taro.showToast({ 
+      title: error.message || '操作失败', 
+      icon: 'none' 
+    })
+  }
+})
+```
+
+### 3. 删除操作
+```typescript
+const queryClient = useQueryClient()
+
+const mutation = useMutation({
+  mutationFn: async (id: number) => {
+    const response = await deliveryAddressClient[':id'].$delete({
+      param: { id }
+    })
+    if (response.status !== 204) {
+      throw new Error('删除地址失败')
+    }
+    return response.json()
+  },
+  onSuccess: () => {
+    queryClient.invalidateQueries({ queryKey: ['delivery-addresses'] })
+    Taro.showToast({
+      title: '删除成功',
+      icon: 'success'
+    })
+  },
+  onError: (error) => {
+    Taro.showToast({
+      title: error.message || '删除失败',
+      icon: 'none'
+    })
+  }
+})
+```
+
+### 4. 分页查询
+#### 标准分页(useQuery)
+```typescript
+const useUserList = (page: number, pageSize: number = 10) => {
+  return useQuery({
+    queryKey: ['users', page, pageSize],
+    queryFn: async () => {
+      const response = await userClient.$get({
+        query: { page, pageSize }
+      })
+      return response.json()
+    },
+    keepPreviousData: true, // 保持上一页数据
+  })
+}
+```
+
+#### 移动端无限滚动分页(useInfiniteQuery)
+```typescript
+import { useInfiniteQuery } from '@tanstack/react-query'
+
+const useInfiniteUserList = (keyword?: string) => {
+  return useInfiniteQuery({
+    queryKey: ['users-infinite', keyword],
+    queryFn: async ({ pageParam = 1 }) => {
+      const response = await userClient.$get({
+        query: {
+          page: pageParam,
+          pageSize: 10,
+          keyword
+        }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取用户列表失败')
+      }
+      return response.json()
+    },
+    getNextPageParam: (lastPage, allPages) => {
+      const totalPages = Math.ceil(lastPage.pagination.total / lastPage.pagination.pageSize)
+      const nextPage = allPages.length + 1
+      return nextPage <= totalPages ? nextPage : undefined
+    },
+    staleTime: 5 * 60 * 1000,
+  })
+}
+
+// 使用示例
+const {
+  data,
+  isLoading,
+  isFetchingNextPage,
+  fetchNextPage,
+  hasNextPage,
+  refetch
+} = useInfiniteUserList(searchKeyword)
+
+// 合并所有分页数据
+const allUsers = data?.pages.flatMap(page => page.data) || []
+
+// 触底加载更多处理
+const handleScrollToLower = () => {
+  if (hasNextPage && !isFetchingNextPage) {
+    fetchNextPage()
+  }
+}
+```
+
+#### 移动端分页页面模板
+```typescript
+// mini/src/pages/[无限滚动列表]/index.tsx
+import { View, ScrollView } from '@tarojs/components'
+import { useInfiniteQuery } from '@tanstack/react-query'
+import { goodsClient } from '@/api'
+import { InferResponseType } from 'hono'
+import Taro from '@tarojs/taro'
+
+type GoodsResponse = InferResponseType<typeof goodsClient.$get, 200>
+
+export default function InfiniteGoodsList() {
+  const [searchKeyword, setSearchKeyword] = useState('')
+
+  const {
+    data,
+    isLoading,
+    isFetchingNextPage,
+    fetchNextPage,
+    hasNextPage,
+    refetch
+  } = useInfiniteQuery({
+    queryKey: ['goods-infinite', searchKeyword],
+    queryFn: async ({ pageParam = 1 }) => {
+      const response = await goodsClient.$get({
+        query: {
+          page: pageParam,
+          pageSize: 10,
+          keyword: searchKeyword
+        }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取商品失败')
+      }
+      return response.json()
+    },
+    getNextPageParam: (lastPage) => {
+      const { pagination } = lastPage
+      const totalPages = Math.ceil(pagination.total / pagination.pageSize)
+      return pagination.current < totalPages ? pagination.current + 1 : undefined
+    },
+    staleTime: 5 * 60 * 1000,
+    initialPageParam: 1,
+  })
+
+  // 合并所有分页数据
+  const allGoods = data?.pages.flatMap(page => page.data) || []
+
+  // 触底加载更多
+  const handleScrollToLower = () => {
+    if (hasNextPage && !isFetchingNextPage) {
+      fetchNextPage()
+    }
+  }
+
+  // 下拉刷新
+  const onPullDownRefresh = () => {
+    refetch().finally(() => {
+      Taro.stopPullDownRefresh()
+    })
+  }
+
+  return (
+    <ScrollView
+      className="h-screen"
+      scrollY
+      onScrollToLower={handleScrollToLower}
+      refresherEnabled
+      refresherTriggered={false}
+      onRefresherRefresh={onPullDownRefresh}
+    >
+      <View className="px-4 py-4">
+        {isLoading ? (
+          <View className="flex justify-center py-10">
+            <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
+          </View>
+        ) : (
+          <>
+            {allGoods.map((item) => (
+              <View key={item.id} className="bg-white rounded-lg p-4 mb-3">
+                <Text>{item.name}</Text>
+              </View>
+            ))}
+            
+            {isFetchingNextPage && (
+              <View className="flex justify-center py-4">
+                <View className="i-heroicons-arrow-path-20-solid animate-spin w-6 h-6 text-blue-500" />
+                <Text className="ml-2 text-sm text-gray-500">加载更多...</Text>
+              </View>
+            )}
+            
+            {!hasNextPage && allGoods.length > 0 && (
+              <View className="text-center py-4 text-sm text-gray-400">
+                没有更多了
+              </View>
+            )}
+          </>
+        )}
+      </View>
+    </ScrollView>
+  )
+}
+```
+
+## 表单处理规范
+
+### 1. 表单Schema定义
+```typescript
+// 在schemas目录下定义
+import { z } from 'zod'
+
+export const userSchema = z.object({
+  username: z.string()
+    .min(3, '用户名至少3个字符')
+    .max(20, '用户名最多20个字符')
+    .regex(/^\S+$/, '用户名不能包含空格'),
+  email: z.string().email('请输入有效的邮箱地址'),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号')
+})
+
+export type UserFormData = z.infer<typeof userSchema>
+```
+
+### 2. 表单使用
+```typescript
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { userSchema, type UserFormData } from '@/schemas/user.schema'
+
+const form = useForm<UserFormData>({
+  resolver: zodResolver(userSchema),
+  defaultValues: {
+    username: '',
+    email: '',
+    phone: ''
+  }
+})
+
+// 表单提交
+const onSubmit = async (data: UserFormData) => {
+  try {
+    await mutation.mutateAsync(data)
+  } catch (error) {
+    console.error('表单提交失败:', error)
+  }
+}
+```
+
+## 错误处理规范
+
+### 1. 统一的错误处理
+```typescript
+const handleApiError = (error: any) => {
+  const message = error.response?.data?.message || error.message || '操作失败'
+  
+  if (error.response?.status === 401) {
+    Taro.showModal({
+      title: '未登录',
+      content: '请先登录',
+      success: () => {
+        Taro.navigateTo({ url: '/pages/login/index' })
+      }
+    })
+  } else if (error.response?.status === 403) {
+    Taro.showToast({ title: '权限不足', icon: 'none' })
+  } else if (error.response?.status === 404) {
+    Taro.showToast({ title: '资源不存在', icon: 'none' })
+  } else if (error.response?.status >= 500) {
+    Taro.showToast({ title: '服务器错误,请稍后重试', icon: 'none' })
+  } else {
+    Taro.showToast({ title: message, icon: 'none' })
+  }
+}
+```
+
+### 2. 页面级错误处理
+```typescript
+const { data, isLoading, error } = useQuery({
+  // ...查询配置
+})
+
+if (error) {
+  return (
+    <View className="flex-1 flex items-center justify-center">
+      <View className="text-center">
+        <View className="i-heroicons-exclamation-triangle-20-solid w-12 h-12 text-red-500 mx-auto mb-4" />
+        <Text className="text-gray-600 mb-4">{error.message}</Text>
+        <Button onClick={() => refetch()}>重新加载</Button>
+      </View>
+    </View>
+  )
+}
+```
+
+## 页面模板示例
+
+### 1. TabBar页面标准结构
+```typescript
+// 示例:首页
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import { TabBarLayout } from '@/layouts/tab-bar-layout'
+import { Navbar } from '@/components/ui/navbar'
+import { Button } from '@/components/ui/button'
+import './index.css'
+
+const HomePage: React.FC = () => {
+  return (
+    <TabBarLayout activeKey="home">
+      <Navbar
+        title="首页"
+        rightIcon="i-heroicons-bell-20-solid"
+        onClickRight={() => console.log('点击通知')}
+        leftIcon=""
+      />
+      <View className="px-4 py-4">
+        <Text className="text-2xl font-bold text-gray-900">欢迎使用</Text>
+        <View className="mt-4">
+          <Text className="text-gray-600">这是一个简洁优雅的小程序首页</Text>
+        </View>
+      </View>
+    </TabBarLayout>
+  )
+}
+
+export default HomePage
+```
+
+### 2. 非TabBar页面标准结构
+```typescript
+// 示例:登录页
+import { View } from '@tarojs/components'
+import { useEffect } from 'react'
+import Taro from '@tarojs/taro'
+import { Navbar } from '@/components/ui/navbar'
+import { Button } from '@/components/ui/button'
+import './index.css'
+
+export default function Login() {
+  useEffect(() => {
+    Taro.setNavigationBarTitle({
+      title: '用户登录'
+    })
+  }, [])
+
+  return (
+    <View className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
+      <Navbar
+        title="用户登录"
+        backgroundColor="bg-transparent"
+        textColor="text-gray-900"
+        border={false}
+      />
+      <View className="flex-1 px-6 py-12">
+        {/* Logo区域 */}
+        <View className="flex flex-col items-center mb-10">
+          <View className="w-20 h-20 mb-4 rounded-full bg-white shadow-lg flex items-center justify-center">
+            <View className="i-heroicons-user-circle-20-solid w-12 h-12 text-blue-500" />
+          </View>
+          <Text className="text-2xl font-bold text-gray-900 mb-1">欢迎回来</Text>
+        </View>
+
+        {/* 表单区域 */}
+        <View className="bg-white rounded-2xl shadow-sm p-6">
+          <View className="space-y-5">
+            {/* 表单内容 */}
+          </View>
+        </View>
+      </View>
+    </View>
+  )
+}
+```
+
+
+## 路由配置
+
+### 1. TabBar页面配置
+```typescript
+// mini/src/app.config.ts
+export default defineAppConfig({
+  pages: [
+    'pages/index/index',
+    'pages/explore/index',
+    'pages/profile/index',
+    // 其他页面
+  ],
+  tabBar: {
+    color: '#666666',
+    selectedColor: '#1976D2',
+    backgroundColor: '#ffffff',
+    borderStyle: 'black',
+    list: [
+      {
+        pagePath: 'pages/index/index',
+        text: '首页',
+        iconPath: 'assets/icons/home.png',
+        selectedIconPath: 'assets/icons/home-active.png'
+      },
+      {
+        pagePath: 'pages/explore/index',
+        text: '发现',
+        iconPath: 'assets/icons/explore.png',
+        selectedIconPath: 'assets/icons/explore-active.png'
+      },
+      {
+        pagePath: 'pages/profile/index',
+        text: '我的',
+        iconPath: 'assets/icons/profile.png',
+        selectedIconPath: 'assets/icons/profile-active.png'
+      }
+    ]
+  }
+})
+```
+
+### 2. 非TabBar页面路由
+非TabBar页面会自动添加到pages数组中,无需额外配置tabBar。
+
+## 最佳实践
+
+### 1. 命名规范
+- 页面目录:使用小写+中划线命名,如 `user-profile`
+- 组件名称:使用PascalCase,如 `UserProfilePage`
+- 文件名:使用小写+中划线命名,如 `user-profile.tsx`
+
+### 2. 样式规范
+- 使用Tailwind CSS原子类
+- 避免使用px,使用rpx单位
+- 页面背景色统一使用 `bg-gray-50` 或 `bg-white`
+
+### 3. 状态管理
+- 使用React hooks进行状态管理
+- 复杂状态使用Context API
+- 用户信息使用 `useAuth` hook
+
+### 4. 错误处理
+- 使用Taro.showToast显示错误信息
+- 网络请求使用try-catch包裹
+- 提供友好的用户反馈
+
+### 5. 性能优化
+- 使用懒加载组件
+- 避免不必要的重新渲染
+- 合理使用useMemo和useCallback
+
+## 常用工具函数
+
+### 1. 页面跳转
+```typescript
+// Tab页面跳转
+Taro.switchTab({ url: '/pages/index/index' })
+
+// 普通页面跳转
+Taro.navigateTo({ url: '/pages/login/index' })
+
+// 返回上一页
+Taro.navigateBack()
+
+// 重定向(清除当前页面历史)
+Taro.redirectTo({ url: '/pages/login/index' })
+
+// 重新启动应用
+Taro.reLaunch({ url: '/pages/index/index' })
+```
+
+### 2. 用户交互
+```typescript
+// 显示提示
+Taro.showToast({
+  title: '操作成功',
+  icon: 'success',
+  duration: 2000
+})
+
+// 显示加载
+Taro.showLoading({
+  title: '加载中...',
+  mask: true
+})
+Taro.hideLoading()
+
+// 显示确认对话框
+Taro.showModal({
+  title: '确认操作',
+  content: '确定要执行此操作吗?',
+  success: (res) => {
+    if (res.confirm) {
+      // 用户点击确认
+    }
+  }
+})
+
+// 显示操作菜单
+Taro.showActionSheet({
+  itemList: ['选项1', '选项2', '选项3'],
+  success: (res) => {
+    console.log('用户选择了', res.tapIndex)
+  }
+})
+```
+
+### 3. 本地存储
+```typescript
+// 存储数据
+Taro.setStorageSync('key', 'value')
+Taro.setStorageSync('user', JSON.stringify(user))
+
+// 获取数据
+const value = Taro.getStorageSync('key')
+const user = JSON.parse(Taro.getStorageSync('user') || '{}')
+
+// 移除数据
+Taro.removeStorageSync('key')
+
+// 清空所有数据
+Taro.clearStorageSync()
+```
+
+### 4. 设备信息
+```typescript
+// 获取系统信息
+const systemInfo = Taro.getSystemInfoSync()
+const { screenWidth, screenHeight, windowWidth, windowHeight, statusBarHeight } = systemInfo
+
+// 获取用户位置
+Taro.getLocation({
+  type: 'wgs84',
+  success: (res) => {
+    console.log('纬度:', res.latitude)
+    console.log('经度:', res.longitude)
+  }
+})

+ 580 - 329
src/client/admin/pages/StockXunlianCodesPage.tsx

@@ -1,357 +1,608 @@
-import React, { useState, useEffect } from 'react';
-import { Table, Button, Modal, Form, Input, DatePicker, Space, Typography, message, Tag } from 'antd';
-import dayjs from 'dayjs';
-import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
+import { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { format } from 'date-fns';
+import { zhCN } from 'date-fns/locale';
+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 { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Skeleton } from '@/client/components/ui/skeleton';
+import { toast } from 'sonner';
+import { Calendar } from '@/client/components/ui/calendar';
+import { Popover, PopoverContent, PopoverTrigger } from '@/client/components/ui/popover';
+import { cn } from '@/client/lib/utils';
+
 import { stockXunlianCodesClient } from '@/client/api';
-import type { InferResponseType, InferRequestType } from 'hono/client';
-import { App } from 'antd';
-
-const { Title } = Typography;
-
-// 定义类型
-type StockXunlianCodesListResponse = InferResponseType<typeof stockXunlianCodesClient.$get, 200>;
-type StockXunlianCodesItem = StockXunlianCodesListResponse['data'][0];
-type CreateStockXunlianCodesRequest = InferRequestType<typeof stockXunlianCodesClient.$post>['json'];
-type UpdateStockXunlianCodesRequest = InferRequestType<typeof stockXunlianCodesClient[':id']['$put']>['json'];
-
-export const StockXunlianCodesPage: React.FC = () => {
-  const [data, setData] = useState<StockXunlianCodesItem[]>([]);
-  const [loading, setLoading] = useState<boolean>(true);
-  const [pagination, setPagination] = useState({
-    current: 1,
-    pageSize: 10,
-    total: 0,
+import { CreateStockXunlianCodesDto, UpdateStockXunlianCodesDto } from '@/server/modules/stock/stock-xunlian-codes.schema';
+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;
+};
+
+// 表单Schema
+const createFormSchema = CreateStockXunlianCodesDto;
+const updateFormSchema = UpdateStockXunlianCodesDto;
+
+export const StockXunlianCodesPage = () => {
+  const queryClient = useQueryClient();
+  
+  // 状态管理
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    search: '',
+  });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [editingEntity, setEditingEntity] = useState<StockXunlianCode | null>(null);
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [entityToDelete, setEntityToDelete] = useState<number | null>(null);
+
+  // 表单实例
+  const createForm = useForm<CreateRequest>({
+    resolver: zodResolver(createFormSchema),
+    defaultValues: {
+      code: '',
+      stockName: '',
+      name: '',
+      type: '',
+      description: '',
+      tradeDate: new Date(),
+    },
   });
-  const [searchText, setSearchText] = useState('');
-  const [isModalVisible, setIsModalVisible] = useState(false);
-  const [isEditing, setIsEditing] = useState(false);
-  const [currentItem, setCurrentItem] = useState<StockXunlianCodesItem | null>(null);
-  const [form] = Form.useForm();
-  const { message: antMessage } = App.useApp();
-
-  // 获取数据列表
-  const fetchData = async () => {
-    try {
-      setLoading(true);
+
+  const updateForm = useForm<UpdateRequest>({
+    resolver: zodResolver(updateFormSchema),
+  });
+
+  // 数据查询
+  const { data, isLoading, refetch } = useQuery({
+    queryKey: ['stock-xunlian-codes', searchParams],
+    queryFn: async () => {
       const res = await stockXunlianCodesClient.$get({
         query: {
-          page: pagination.current,
-          pageSize: pagination.pageSize,
-          keyword: searchText,
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search,
         },
       });
-      
-      if (!res.ok) {
-        throw new Error('获取数据失败');
-      }
-      
-      const result = await res.json() as StockXunlianCodesListResponse;
-      setData(result.data);
-      setPagination(prev => ({
-        ...prev,
-        total: result.pagination.total,
-      }));
-    } catch (error) {
-      console.error('获取训练代码数据失败:', error);
-      antMessage.error('获取数据失败,请重试');
-    } finally {
-      setLoading(false);
-    }
-  };
+      if (res.status !== 200) throw new Error('获取列表失败');
+      return await res.json();
+    },
+  });
+
+  // 创建mutation
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateRequest) => {
+      const res = await stockXunlianCodesClient.$post({ json: data });
+      if (res.status !== 201) throw new Error('创建失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('创建成功');
+      setIsModalOpen(false);
+      createForm.reset();
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(`创建失败: ${error.message}`);
+    },
+  });
+
+  // 更新mutation
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateRequest }) => {
+      const res = await stockXunlianCodesClient[':id']['$put']({
+        param: { id: id.toString() },
+        json: data,
+      });
+      if (res.status !== 200) throw new Error('更新失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('更新成功');
+      setIsModalOpen(false);
+      setEditingEntity(null);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(`更新失败: ${error.message}`);
+    },
+  });
 
-  // 初始加载和分页、搜索变化时重新获取数据
-  useEffect(() => {
-    fetchData();
-  }, [pagination.current, pagination.pageSize]);
+  // 删除mutation
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const res = await stockXunlianCodesClient[':id']['$delete']({
+        param: { id: id.toString() },
+      });
+      if (res.status !== 204) throw new Error('删除失败');
+      return id;
+    },
+    onSuccess: () => {
+      toast.success('删除成功');
+      setDeleteDialogOpen(false);
+      setEntityToDelete(null);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(`删除失败: ${error.message}`);
+    },
+  });
 
-  // 搜索功能
+  // 业务逻辑函数
   const handleSearch = () => {
-    setPagination(prev => ({ ...prev, current: 1 }));
-    fetchData();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
   };
 
-  // 显示创建模态框
-  const showCreateModal = () => {
-    setIsEditing(false);
-    setCurrentItem(null);
-    form.resetFields();
-    setIsModalVisible(true);
+  const handleCreateEntity = () => {
+    setIsCreateForm(true);
+    setEditingEntity(null);
+    createForm.reset();
+    setIsModalOpen(true);
   };
 
-  // 显示编辑模态框
-  const showEditModal = (record: StockXunlianCodesItem) => {
-    setIsEditing(true);
-    setCurrentItem(record);
-    form.setFieldsValue({
-      code: record.code,
-      stockName: record.stockName,
-      name: record.name,
-      type: record.type || undefined,
-      description: record.description || undefined,
-      tradeDate: record.tradeDate ? dayjs(record.tradeDate) : null,
+  const handleEditEntity = (entity: StockXunlianCode) => {
+    setIsCreateForm(false);
+    setEditingEntity(entity);
+    updateForm.reset({
+      code: entity.code,
+      stockName: entity.stockName,
+      name: entity.name,
+      type: entity.type || '',
+      description: entity.description || '',
+      tradeDate: new Date(entity.tradeDate),
     });
-    setIsModalVisible(true);
+    setIsModalOpen(true);
   };
 
-  // 处理表单提交
-  const handleSubmit = async () => {
-    try {
-      const values = await form.validateFields();
-      
-      if (isEditing && currentItem) {
-        // 更新数据
-        const res = await stockXunlianCodesClient[':id'].$put({
-          param: { id: currentItem.id },
-          json: values as UpdateStockXunlianCodesRequest,
-        });
-        
-        if (!res.ok) {
-          throw new Error('更新失败');
-        }
-        antMessage.success('更新成功');
-      } else {
-        // 创建新数据
-        const res = await stockXunlianCodesClient.$post({
-          json: values as CreateStockXunlianCodesRequest,
-        });
-        
-        if (!res.ok) {
-          throw new Error('创建失败');
-        }
-        antMessage.success('创建成功');
-      }
-      
-      setIsModalVisible(false);
-      fetchData();
-    } catch (error) {
-      console.error('提交表单失败:', error);
-      antMessage.error(isEditing ? '更新失败,请重试' : '创建失败,请重试');
+  const handleDeleteEntity = (id: number) => {
+    setEntityToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  const handleCreateSubmit = (data: CreateRequest) => {
+    createMutation.mutate(data);
+  };
+
+  const handleUpdateSubmit = (data: UpdateRequest) => {
+    if (editingEntity) {
+      updateMutation.mutate({ id: editingEntity.id, data });
     }
   };
 
-  // 删除数据
-  const handleDelete = async (id: number) => {
-    try {
-      const res = await stockXunlianCodesClient[':id'].$delete({
-        param: { id },
-      });
-      
-      if (!res.ok) {
-        throw new Error('删除失败');
-      }
-      
-      antMessage.success('删除成功');
-      fetchData();
-    } catch (error) {
-      console.error('删除数据失败:', error);
-      antMessage.error('删除失败,请重试');
+  const confirmDelete = () => {
+    if (entityToDelete) {
+      deleteMutation.mutate(entityToDelete);
     }
   };
 
-  // 表格列定义
-  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: StockXunlianCodesItem) => record.code === value,
-    },
-    {
-      title: '股票名称',
-      dataIndex: 'stockName',
-      key: 'stockName',
-    },
-    {
-      title: '案例名称',
-      dataIndex: 'name',
-      key: 'name',
-    },
-    {
-      title: '案例类型',
-      dataIndex: 'type',
-      key: 'type',
-      render: (type: string | null) => type ? (
-        <Tag color="blue">{type}</Tag>
-      ) : (
-        <Tag color="default">未分类</Tag>
-      ),
-      filters: [
-        { text: '技术分析', value: '技术分析' },
-        { text: '基本面分析', value: '基本面分析' },
-        { text: '未分类', value: '未分类' },
-      ],
-      onFilter: (value: string, record: StockXunlianCodesItem) => {
-        if (value === '未分类') {
-          return !record.type;
-        }
-        return record.type === value;
-      },
-    },
-    {
-      title: '交易日期',
-      dataIndex: 'tradeDate',
-      key: 'tradeDate',
-      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-',
-    },
-    {
-      title: '创建时间',
-      dataIndex: 'createdAt',
-      key: 'createdAt',
-      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
-    },
-    {
-      title: '操作',
-      key: 'action',
-      render: (_: any, record: StockXunlianCodesItem) => (
-        <Space size="small">
-          <Button 
-            type="text" 
-            icon={<EditOutlined />} 
-            onClick={() => showEditModal(record)}
-          >
-            编辑
-          </Button>
-          <Button 
-            type="text" 
-            danger 
-            icon={<DeleteOutlined />} 
-            onClick={() => handleDelete(record.id)}
-          >
-            删除
-          </Button>
-        </Space>
-      ),
-    },
-  ];
+  // 加载骨架屏
+  if (isLoading) {
+    return (
+      <div className="space-y-4">
+        <div className="flex justify-between items-center">
+          <Skeleton className="h-8 w-48" />
+          <Skeleton className="h-10 w-32" />
+        </div>
+        
+        <Card>
+          <CardContent className="pt-6">
+            <div className="space-y-3">
+              {[...Array(5)].map((_, i) => (
+                <div key={i} className="flex gap-4">
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 w-20" />
+                </div>
+              ))}
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    );
+  }
 
   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}>
-          添加训练代码
+    <div className="space-y-4">
+      {/* 页面标题区域 */}
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">股票训练代码管理</h1>
+        <Button onClick={handleCreateEntity}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建训练代码
         </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"
-        bordered
-        scroll={{ x: 'max-content' }}
-        headerCellStyle={{ backgroundColor: '#f9fafb' }}
-        rowClassName={(record, index) => index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
-      />
-      
-      <Modal
-        title={isEditing ? "编辑训练代码" : "添加训练代码"}
-        open={isModalVisible}
-        onOk={handleSubmit}
-        onCancel={() => setIsModalVisible(false)}
-        destroyOnClose
-        maskClosable={false}
-        width={700}
-      >
-        <Form
-          form={form}
-          layout="vertical"
-          name="stock_xunlian_codes_form"
-        >
-          <Form.Item
-            name="code"
-            label="股票代码"
-            rules={[
-              { required: true, message: '请输入股票代码' },
-              { max: 255, message: '股票代码不能超过255个字符' }
-            ]}
-          >
-            <Input placeholder="请输入股票代码" />
-          </Form.Item>
-          
-          <Form.Item
-            name="stockName"
-            label="股票名称"
-            rules={[
-              { required: true, message: '请输入股票名称' },
-              { max: 255, message: '股票名称不能超过255个字符' }
-            ]}
-          >
-            <Input placeholder="请输入股票名称" />
-          </Form.Item>
-          
-          <Form.Item
-            name="name"
-            label="案例名称"
-            rules={[
-              { required: true, message: '请输入案例名称' },
-              { max: 255, message: '案例名称不能超过255个字符' }
-            ]}
-          >
-            <Input placeholder="请输入案例名称" />
-          </Form.Item>
-          
-          <Form.Item
-            name="type"
-            label="案例类型"
-            rules={[
-              { required: true, message: '请输入案例类型' },
-              { max: 255, message: '案例类型不能超过255个字符' }
-            ]}
-          >
-            <Input placeholder="请输入案例类型(如:技术分析、基本面分析)" />
-          </Form.Item>
-          
-          <Form.Item
-            name="description"
-            label="案例描述"
-            rules={[{ max: 255, message: '案例描述不能超过255个字符' }]}
-          >
-            <Input.TextArea rows={4} placeholder="请输入案例描述" />
-          </Form.Item>
-          
-          <Form.Item
-            name="tradeDate"
-            label="交易日期"
-            rules={[{ required: true, message: '请选择交易日期' }]}
-          >
-            <DatePicker showTime placeholder="请选择交易日期" style={{ width: '100%' }} />
-          </Form.Item>
-        </Form>
-      </Modal>
+
+      {/* 搜索区域 */}
+      <Card>
+        <CardHeader>
+          <CardTitle>训练代码列表</CardTitle>
+          <CardDescription>管理股票训练案例和代码</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="mb-4">
+            <form onSubmit={(e) => { e.preventDefault(); handleSearch(); }} className="flex gap-2">
+              <div className="relative flex-1 max-w-sm">
+                <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+                <Input
+                  placeholder="搜索股票代码、名称或案例..."
+                  value={searchParams.search}
+                  onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
+                  className="pl-8"
+                />
+              </div>
+              <Button type="submit" variant="outline">
+                搜索
+              </Button>
+            </form>
+          </div>
+
+          {/* 数据表格 */}
+          <div className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>股票代码</TableHead>
+                  <TableHead>股票名称</TableHead>
+                  <TableHead>案例名称</TableHead>
+                  <TableHead>案例类型</TableHead>
+                  <TableHead>交易日期</TableHead>
+                  <TableHead>创建时间</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {data?.data.map((item) => (
+                  <TableRow key={item.id}>
+                    <TableCell className="font-medium">{item.code}</TableCell>
+                    <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.createdAt), 'yyyy-MM-dd HH:mm')}</TableCell>
+                    <TableCell className="text-right">
+                      <div className="flex justify-end gap-2">
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleEditEntity(item)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleDeleteEntity(item.id)}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </div>
+
+          {data?.data.length === 0 && !isLoading && (
+            <div className="text-center py-8">
+              <p className="text-muted-foreground">暂无数据</p>
+            </div>
+          )}
+
+          <DataTablePagination
+            currentPage={searchParams.page}
+            pageSize={searchParams.limit}
+            totalCount={data?.pagination.total || 0}
+            onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
+          />
+        </CardContent>
+      </Card>
+
+      {/* 创建/编辑模态框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{isCreateForm ? '创建训练代码' : '编辑训练代码'}</DialogTitle>
+            <DialogDescription>
+              {isCreateForm ? '创建一个新的股票训练案例' : '编辑现有训练案例信息'}
+            </DialogDescription>
+          </DialogHeader>
+
+          {isCreateForm ? (
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
+                <FormField
+                  control={createForm.control}
+                  name="code"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>股票代码</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入股票代码,如000001" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="stockName"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>股票名称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入股票名称,如平安银行" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>案例名称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入案例名称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="type"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>案例类型</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入案例类型,如技术分析" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="description"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>案例描述</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入案例描述" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="tradeDate"
+                  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>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={createMutation.isPending}>
+                    {createMutation.isPending ? '创建中...' : '创建'}
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          ) : (
+            <Form {...updateForm}>
+              <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
+                <FormField
+                  control={updateForm.control}
+                  name="code"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>股票代码</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入股票代码" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="stockName"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>股票名称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入股票名称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>案例名称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入案例名称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="type"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>案例类型</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入案例类型" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="description"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>案例描述</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入案例描述" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="tradeDate"
+                  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>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={updateMutation.isPending}>
+                    {updateMutation.isPending ? '更新中...' : '更新'}
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>确认删除</DialogTitle>
+            <DialogDescription>
+              确定要删除这个训练代码吗?此操作无法撤销。
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+              取消
+            </Button>
+            <Button variant="destructive" onClick={confirmDelete} disabled={deleteMutation.isPending}>
+              {deleteMutation.isPending ? '删除中...' : '删除'}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
     </div>
   );
-};
-
-export default StockXunlianCodesPage;
+};