瀏覽代碼

添加了训练按钮列表页

yourname 6 月之前
父節點
當前提交
49aa8b19e1

+ 1 - 9
client/admin/pages_xunlian_codes.tsx

@@ -51,15 +51,7 @@ export const XunlianCodePage = () => {
         pageSize: searchParams.limit,
         code: searchParams.code,
       });
-      // return response;
-      const codes = response?.data || [];
-      const pagination = {
-        current: response?.pagination?.current || 1,
-        pageSize: response?.pagination?.pageSize || 10,
-        total: response?.pagination?.total || 0,
-        totalPages: response?.pagination?.totalPages || 1
-      };
-      return { data: codes, pagination}
+      return response;
     }
   });
 

+ 86 - 0
client/mobile/api/xunlian_codes.ts

@@ -0,0 +1,86 @@
+import axios from 'axios';
+import type { XunlianCode } from '../../share/types_stock.ts';
+
+
+// 训练代码列表响应
+interface XunlianCodeListResponse {
+  data: XunlianCode[];
+  pagination: {
+    current: number;
+    pageSize: number;
+    total: number;
+    totalPages: number;
+  };
+}
+
+interface XunlianCodeResponse {
+  data: XunlianCode;
+  message?: string;
+}
+
+interface XunlianCodeCreateResponse {
+  message: string;
+  data: XunlianCode;
+}
+
+interface XunlianCodeUpdateResponse {
+  message: string;
+  data: XunlianCode;
+}
+
+interface XunlianCodeDeleteResponse {
+  message: string;
+  id: number;
+}
+
+export const XunlianCodeAPI = {
+  getXunlianCodes: async (params: {
+    page?: number;
+    pageSize?: number;
+    code?: string;
+    stock_name?: string;
+  }): Promise<XunlianCodeListResponse> => {
+    try {
+      const response = await axios.get('/xunlian-codes', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  getXunlianCode: async (id: number): Promise<XunlianCodeResponse> => {
+    try {
+      const response = await axios.get(`/xunlian-codes/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  createXunlianCode: async (data: Partial<XunlianCode>): Promise<XunlianCodeCreateResponse> => {
+    try {
+      const response = await axios.post('/xunlian-codes', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  updateXunlianCode: async (id: number, data: Partial<XunlianCode>): Promise<XunlianCodeUpdateResponse> => {
+    try {
+      const response = await axios.put(`/xunlian-codes/${id}`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  deleteXunlianCode: async (id: number): Promise<XunlianCodeDeleteResponse> => {
+    try {
+      const response = await axios.delete(`/xunlian-codes/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 6 - 0
client/mobile/mobile_app.tsx

@@ -30,6 +30,7 @@ import ExamAdmin from './components/Exam/ExamAdmin.tsx';
 import ExamCard from './components/Exam/ExamCard.tsx';
 import ProfilePage from './pages_profile.tsx'
 import SettingsPage from './pages_settings.tsx'
+import { XunlianPage } from "./pages_xunlian.tsx";
 
 
 // 设置中文语言
@@ -287,6 +288,11 @@ const App = () => {
       element: <ProtectedRoute><StockMain /></ProtectedRoute>,
       errorElement: <ErrorPage />
     },
+    {
+      path: '/mobile/xunlian',
+      element: <ProtectedRoute><XunlianPage /></ProtectedRoute>,
+      errorElement: <ErrorPage />
+    },
     {
       path: '/mobile/exam',
       element: <ProtectedRoute><ExamIndex /></ProtectedRoute>,

+ 110 - 0
client/mobile/pages_xunlian.tsx

@@ -0,0 +1,110 @@
+import React, { useState } from "react";
+import dayjs from "dayjs";
+import { useQuery } from "@tanstack/react-query";
+import type { XunlianCode } from "../share/types_stock.ts";
+import { useNavigate } from "react-router";
+import { XunlianCodeAPI } from "./api/xunlian_codes.ts";
+
+export function XunlianPage() {
+  const [visibleStocks, setVisibleStocks] = useState<Record<number, boolean>>({});
+  const navigate = useNavigate();
+
+  const { data: codes = [], isLoading } = useQuery({
+    queryKey: ["xunlian-codes"],
+    queryFn: async (): Promise<XunlianCode[]> => {
+      const response = await XunlianCodeAPI.getXunlianCodes({});
+      return response.data;
+    },
+  });
+
+  const toggleStockVisibility = (id: number, e: React.MouseEvent) => {
+    e.stopPropagation();
+    setVisibleStocks((prev: Record<number, boolean>) => ({
+      ...prev,
+      [id]: !prev[id]
+    }));
+  };
+
+  const handleCardClick = (code: XunlianCode) => {
+    navigate(`/mobile/stock?code=${code.code}`);
+  };
+
+  return (
+    <div className="p-4">
+      <div className="flex justify-between items-center mb-4">
+        <h1 className="text-2xl font-bold">训练案例</h1>
+      </div>
+
+      <div className="grid gap-4">
+        {isLoading ? (
+          <div className="text-center text-gray-500 py-8">
+            <div className="animate-spin inline-block w-6 h-6 border-[3px] border-current border-t-transparent text-blue-600 rounded-full" role="status" aria-label="loading">
+              <span className="sr-only">加载中...</span>
+            </div>
+            <div className="mt-2">加载中...</div>
+          </div>
+        ) : codes.length === 0 ? (
+          <div className="text-center text-gray-500 py-8">
+            暂无训练案例
+          </div>
+        ) : (
+          codes.map((code: XunlianCode) => (
+            <div
+              key={code.id}
+              className="border rounded-lg p-4 hover:shadow-lg transition-shadow cursor-pointer"
+              onClick={() => handleCardClick(code)}
+            >
+              <div className="flex justify-between items-start">
+                <div>
+                  <h2 className="text-lg font-semibold flex items-center gap-2">
+                    {visibleStocks[code.id] ? (
+                      <>
+                        {code.stock_name} {code.name} ({code.code})
+                        <button
+                          onClick={(e) => toggleStockVisibility(code.id, e)}
+                          className="text-gray-500 hover:text-gray-700"
+                          title="隐藏股票信息"
+                        >
+                          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
+                            <path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
+                            <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
+                          </svg>
+                        </button>
+                      </>
+                    ) : (
+                      <>
+                        {code.name}
+                        <button
+                          onClick={(e) => toggleStockVisibility(code.id, e)}
+                          className="text-gray-400 hover:text-gray-600"
+                          title="显示股票信息"
+                        >
+                          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
+                            <path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
+                          </svg>
+                        </button>
+                      </>
+                    )}
+                  </h2>
+                  {code.description && (
+                    <p className="text-gray-600 mt-1">{code.description}</p>
+                  )}
+                </div>
+                <div className="text-sm text-gray-500">
+                  <div>{dayjs(code.trade_date).format("YYYY-MM-DD")}</div>
+                </div>
+              </div>
+              {code.type && (
+                <div className="mt-2">
+                  <span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">
+                    {code.type}
+                  </span>
+                </div>
+              )}
+            </div>
+          ))
+        )}
+      </div>
+    </div>
+  );
+}

+ 1 - 1
server/app.tsx

@@ -122,7 +122,7 @@ export default function({ apiClient, app, moduleDir , auth}: ModuleParams) {
 
                 {/* 股票训练入口按钮 */}
                 <a
-                  href="/mobile/stock"
+                  href="/mobile/xunlian" 
                   className="w-full flex justify-center py-3 px-4 border border-blue-600 rounded-md shadow-sm text-lg font-medium text-blue-600 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
                 >
                   进入股票训练