charles_c vor 8 Monaten
Ursprung
Commit
1f6a887bab

+ 1 - 1
.gitignore

@@ -38,4 +38,4 @@ screenshot
 .firebase
 .eslintcache
 
-build
+dist.zip

+ 0 - 4
.husky/pre-commit

@@ -1,4 +0,0 @@
-#!/bin/sh
-. "$(dirname "$0")/_/husky.sh"
-
-npx --no-install lint-staged

+ 14 - 0
config/config.ts

@@ -7,6 +7,15 @@ import routes from './routes';
 
 const { REACT_APP_ENV = 'dev' } = process.env;
 
+// 定义可能的环境类型
+type Env = 'dev' | 'prod';
+
+// 定义 API URL 配置
+const API_URL: Record<Env, string> = {
+  dev: 'http://localhost:3000',
+  prod: 'http://match.dzhhzy.com:8088',
+};
+
 /**
  * @name 使用公共路径
  * @description 部署时的路径,如果部署在非根目录下,需要配置这个变量
@@ -15,6 +24,11 @@ const { REACT_APP_ENV = 'dev' } = process.env;
 const PUBLIC_PATH: string = '/';
 
 export default defineConfig({
+  define: {
+    // 使用类型断言来确保 REACT_APP_ENV 是有效的键
+    API_URL: API_URL[REACT_APP_ENV as Env] || API_URL.dev,
+  },
+
   /**
    * @name 开启 hash 模式
    * @description 让 build 之后的产物包含 hash 后缀。通常用于增量发布和避免浏览器加载缓存。

+ 12 - 0
config/routes.ts

@@ -30,6 +30,18 @@ export default [
     component: './User',
   },
   {
+    name: '积分管理',
+    locale: false,
+    path: '/points-manager',
+    component: './Points',
+  },
+  {
+    name: '活动管理',
+    locale: false,
+    path: '/activity-manager',
+    component: './Activity',
+  },
+  {
     path: '/user',
     layout: false,
     routes: [

+ 4 - 12
package.json

@@ -6,7 +6,7 @@
   "repository": "git@github.com:ant-design/ant-design-pro.git",
   "scripts": {
     "analyze": "cross-env ANALYZE=1 max build",
-    "build": "max build",
+    "build": "cross-env REACT_APP_ENV=prod max build",
     "deploy": "npm run build && npm run gh-pages",
     "dev": "npm run start:dev",
     "gh-pages": "gh-pages -d dist",
@@ -14,8 +14,6 @@
     "postinstall": "max setup",
     "jest": "jest",
     "lint": "npm run lint:js && npm run lint:prettier && npm run tsc",
-    "lint-staged": "lint-staged",
-    "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
     "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ",
     "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
     "lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto",
@@ -29,18 +27,13 @@
     "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
     "start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
     "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
+    "start:prod": "cross-env REACT_APP_ENV=prod MOCK=none UMI_ENV=dev max dev",
     "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
     "test": "jest",
     "test:coverage": "npm run jest -- --coverage",
     "test:update": "npm run jest -- -u",
     "tsc": "tsc --noEmit"
   },
-  "lint-staged": {
-    "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
-    "**/*.{js,jsx,tsx,ts,less,md,json}": [
-      "prettier --write"
-    ]
-  },
   "browserslist": [
     "> 1%",
     "last 2 versions",
@@ -57,7 +50,8 @@
     "querystring": "^0.2.1",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
-    "react-error-overlay": "^6.0.11"
+    "react-error-overlay": "^6.0.11",
+    "react-quill": "^2.0.0"
   },
   "devDependencies": {
     "@ant-design/pro-cli": "^3.3.0",
@@ -80,8 +74,6 @@
     "husky": "^7.0.4",
     "jest": "^29.7.0",
     "jest-environment-jsdom": "^29.7.0",
-    "lint-staged": "^10.5.4",
-    "mockjs": "^1.1.0",
     "prettier": "^2.8.8",
     "react-dev-inspector": "^1.9.0",
     "swagger-ui-dist": "^4.19.1",

+ 4 - 0
src/global.tsx

@@ -5,6 +5,10 @@ import defaultSettings from '../config/defaultSettings';
 const { pwa } = defaultSettings;
 const isHttps = document.location.protocol === 'https:';
 
+declare global {
+  const API_URL: string;
+}
+
 const clearCache = () => {
   // remove all caches
   if (window.caches) {

+ 348 - 0
src/pages/Activity/index.tsx

@@ -0,0 +1,348 @@
+import { createActivity, deleteActivity, getActivity, updateActivity } from '@/services/api';
+import { PlusOutlined } from '@ant-design/icons';
+import {
+  ActionType,
+  ModalForm,
+  ProColumns,
+  ProForm,
+  ProFormSwitch,
+  ProFormUploadButton,
+  ProTable,
+} from '@ant-design/pro-components';
+import { Button, message, Popconfirm } from 'antd';
+import React, { useCallback, useMemo, useRef, useState } from 'react';
+import ReactQuill from 'react-quill';
+import 'react-quill/dist/quill.snow.css';
+
+const modules = {
+  toolbar: [
+    [{ header: [1, 2, false] }],
+    ['bold', 'italic', 'underline', 'strike', 'blockquote'],
+    [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
+    ['link', 'image'],
+    [{ align: [] }],
+    [{ color: [] }, { background: [] }],
+    // ['clean'],
+  ],
+};
+
+const ActivityManagement: React.FC = () => {
+  const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
+  const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
+  const [currentActivity, setCurrentActivity] = useState<API.ActivityItem | null>(null);
+  const actionRef = useRef<ActionType>();
+
+  const [backgroundImageUrl, setBackgroundImageUrl] = useState<string>('');
+
+  console.log('currentActivity', currentActivity);
+
+  const columns: ProColumns<API.ActivityItem>[] = [
+    {
+      title: '活动标题',
+      dataIndex: 'title',
+      copyable: true,
+      ellipsis: true,
+      tip: '标题过长会自动收缩',
+    },
+    {
+      title: '活动内容',
+      dataIndex: 'content',
+      ellipsis: true,
+      tip: '内容过长会自动收缩',
+    },
+    {
+      title: '是否激活',
+      dataIndex: 'isActive',
+      valueEnum: {
+        true: { text: '是', status: 'Success' },
+        false: { text: '否', status: 'Default' },
+      },
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      key: 'option',
+      render: (text, record) => [
+        <a
+          key="edit"
+          onClick={() => {
+            setCurrentActivity(record);
+            setBackgroundImageUrl(record.backgroundImage);
+            setEditModalVisible(true);
+          }}
+        >
+          编辑
+        </a>,
+        <Popconfirm
+          key="delete"
+          title="确认删除"
+          description={`您确定要删除活动 "${record.title}" 吗?`}
+          onConfirm={() => handleDelete(record)}
+          okText="确定"
+          cancelText="取消"
+        >
+          <a>删除</a>
+        </Popconfirm>,
+      ],
+    },
+  ];
+
+  const handleAdd = async (fields: API.ActivityItem) => {
+    try {
+      await createActivity({ ...fields });
+      message.success('创建成功');
+      return true;
+    } catch (error) {
+      message.error('创建失败,请重试!');
+      return false;
+    }
+  };
+
+  const handleEdit = async (fields: API.ActivityItem) => {
+    try {
+      const { _id, ...updateData } = fields;
+      const response = await updateActivity(currentActivity._id, updateData);
+
+      if (response.success) {
+        message.success('更新成功');
+        return true;
+      } else {
+        message.error(response.message || '更新失败,请重试!');
+        return false;
+      }
+    } catch (error) {
+      console.error('更新活动时出错:', error);
+      message.error('更新失败,请重试!');
+      return false;
+    }
+  };
+
+  const handleDelete = async (record: API.ActivityItem) => {
+    const res = await deleteActivity(record._id);
+    if (res.success) {
+      message.success('活动已成功删除');
+      actionRef.current?.reload();
+    } else {
+      message.error('删除失败,请重试');
+    }
+  };
+
+  const handleUpload = useCallback(async (file) => {
+    const formData = new FormData();
+    formData.append('file', file);
+
+    try {
+      const uploadResponse = await fetch(`${API_URL}/api/upload`, {
+        method: 'POST',
+        body: formData,
+      });
+
+      if (!uploadResponse.ok) {
+        throw new Error(`Upload failed with status ${uploadResponse.status}`);
+      }
+
+      const uploadData = await uploadResponse.json();
+
+      console.log('uploadData', uploadData);
+
+      if (uploadData.success) {
+        setBackgroundImageUrl(uploadData.url);
+        return {
+          status: 'done',
+          url: uploadData.url,
+        };
+      }
+    } catch (error) {
+      console.error('Upload error:', error);
+      message.error(`文件上传失败: ${error.message}`);
+      return {
+        status: 'error',
+        error: error.message,
+      };
+    }
+  }, []);
+
+  return (
+    <>
+      <ProTable<API.ActivityItem>
+        headerTitle="活动管理"
+        actionRef={actionRef}
+        rowKey="id"
+        search={false}
+        toolBarRender={() => [
+          <Button type="primary" key="primary" onClick={() => setCreateModalVisible(true)}>
+            <PlusOutlined /> 新建活动
+          </Button>,
+        ]}
+        request={getActivity}
+        columns={columns}
+      />
+      {/* 新建活动 */}
+      <ModalForm
+        title="新建活动"
+        width="400px"
+        open={createModalVisible}
+        onOpenChange={setCreateModalVisible}
+        onFinish={async (value) => {
+          console.log('value', value, backgroundImageUrl);
+          const formData = {
+            ...value,
+            backgroundImage: backgroundImageUrl,
+          };
+          console.log('formData', formData);
+          const success = await handleAdd(formData as API.ActivityItem);
+          if (success) {
+            setCreateModalVisible(false);
+            if (actionRef.current) {
+              actionRef.current.reload();
+            }
+          }
+        }}
+      >
+        <ProForm.Item
+          name="title"
+          label="活动标题"
+          rules={[
+            {
+              required: true,
+              message: '活动标题为必填项',
+            },
+          ]}
+        >
+          <ReactQuill
+            theme="snow"
+            modules={modules}
+            style={{
+              height: '150px', // 增加高度
+              paddingBottom: '60px',
+            }}
+          />
+        </ProForm.Item>
+        <ProForm.Item
+          name="content"
+          label="活动内容"
+          rules={[
+            {
+              required: true,
+              message: '活动内容为必填项',
+            },
+          ]}
+        >
+          <ReactQuill
+            theme="snow"
+            modules={modules}
+            style={{
+              height: '250px',
+              paddingBottom: '50px',
+            }}
+          />
+        </ProForm.Item>
+        <ProFormUploadButton
+          name="backgroundImage"
+          label="背景图"
+          max={1}
+          action={handleUpload}
+          fieldProps={{
+            name: 'file',
+            listType: 'picture-card',
+          }}
+          onChange={(info) => {
+            const { status } = info.file;
+            if (status === 'done') {
+              message.success(`文件上传成功`);
+            } else if (status === 'error') {
+              message.error(`文件上传失败`);
+            }
+          }}
+        />
+        <ProFormSwitch name="isActive" label="是否激活" />
+      </ModalForm>
+
+      {/* 编辑活动 */}
+      <ModalForm
+        key={currentActivity?._id}
+        title="编辑活动"
+        width="400px"
+        open={editModalVisible}
+        onOpenChange={setEditModalVisible}
+        onFinish={async (value) => {
+          console.log('edit value', value);
+          const formData = {
+            ...value,
+            backgroundImage: backgroundImageUrl,
+          };
+          const success = await handleEdit(formData as API.ActivityItem);
+          if (success) {
+            setEditModalVisible(false);
+            if (actionRef.current) {
+              actionRef.current.reload();
+            }
+          }
+        }}
+        initialValues={currentActivity}
+      >
+        <ProForm.Item
+          name="title"
+          label="活动标题"
+          rules={[
+            {
+              required: true,
+              message: '活动标题为必填项',
+            },
+          ]}
+        >
+          <ReactQuill
+            theme="snow"
+            modules={modules}
+            style={{
+              height: '150px', // 增加高度
+              paddingBottom: '60px',
+            }}
+          />
+        </ProForm.Item>
+
+        <ProForm.Item
+          name="content"
+          label="活动内容"
+          rules={[
+            {
+              required: true,
+              message: '活动内容为必填项',
+            },
+          ]}
+        >
+          <ReactQuill
+            theme="snow"
+            modules={modules}
+            style={{
+              height: '250px',
+              paddingBottom: '50px',
+            }}
+          />
+        </ProForm.Item>
+        <ProFormUploadButton
+          name="backgroundImage"
+          label="背景图"
+          max={1}
+          action={handleUpload}
+          fieldProps={{
+            name: 'file',
+            listType: 'picture-card',
+          }}
+          fileList={useMemo(() => {
+            return backgroundImageUrl ? [{ url: `${API_URL}${backgroundImageUrl}` }] : [];
+          }, [backgroundImageUrl])}
+          onChange={({ fileList }) => {
+            if (fileList.length === 0) {
+              setBackgroundImageUrl('');
+            }
+          }}
+        />
+
+        <ProFormSwitch name="isActive" label="是否激活" />
+      </ModalForm>
+    </>
+  );
+};
+
+export default ActivityManagement;

+ 39 - 38
src/pages/Match/components/UpdateForm.tsx

@@ -12,7 +12,6 @@ export type UpdateFormProps = {
   onSubmit: (values: API.MatchItem) => Promise<void>;
   updateModalOpen: boolean;
   values: Partial<API.MatchItem>;
-  handleUpload: (file: any) => Promise<string>;
 };
 
 const UpdateForm: React.FC<UpdateFormProps> = (props) => {
@@ -41,6 +40,18 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => {
             ...values.awayTeam,
             ...value.awayTeam,
           },
+          result: {
+            ...values.result,
+            ...value.result,
+            totalGoals:
+              value.homeTeamScore === null && value.awayTeamScore === null
+                ? null
+                : (value.homeTeamScore || 0) + (value.awayTeamScore || 0),
+          },
+          pointRewards: {
+            ...values.pointRewards,
+            ...value.pointRewards,
+          },
         };
         await onSubmit(formData as API.MatchItem);
       }}
@@ -57,21 +68,6 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => {
         name={['homeTeam', 'name']}
         label="主队名称"
       />
-      {/* <ProFormUploadButton
-        name={['homeTeam', 'logo']}
-        label="主队Logo"
-        max={1}
-        action={handleUpload}
-        fieldProps={{
-          onChange: (info) => {
-            if (info.file.status === 'done') {
-              message.success(`${info.file.name} 文件上传成功`);
-            } else if (info.file.status === 'error') {
-              message.error(`${info.file.name} 文件上传失败`);
-            }
-          },
-        }}
-      /> */}
       <ProFormText
         rules={[
           {
@@ -83,21 +79,6 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => {
         name={['awayTeam', 'name']}
         label="客队名称"
       />
-      {/* <ProFormUploadButton
-        name={['awayTeam', 'logo']}
-        label="客队Logo"
-        max={1}
-        action={handleUpload}
-        fieldProps={{
-          onChange: (info) => {
-            if (info.file.status === 'done') {
-              message.success(`${info.file.name} 文件上传成功`);
-            } else if (info.file.status === 'error') {
-              message.error(`${info.file.name} 文件上传失败`);
-            }
-          },
-        }}
-      /> */}
       <ProFormText
         rules={[
           {
@@ -134,14 +115,30 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => {
       <ProFormDigit width="md" name="homeTeamScore" label="主队得分" />
       <ProFormDigit width="md" name="awayTeamScore" label="客队得分" />
       <ProFormSelect
-        rules={[
-          {
-            required: true,
-            message: '请选择哪只球队先得分',
-          },
-        ]}
+        // rules={[
+        //   {
+        //     required: true,
+        //     message: '请选择胜负结果',
+        //   },
+        // ]}
+        width="md"
+        name={['result', 'whoWillWin']}
+        label="胜负结果"
+        valueEnum={{
+          home: '主队胜',
+          away: '客队胜',
+          draw: '平局',
+        }}
+      />
+      <ProFormSelect
+        // rules={[
+        //   {
+        //     required: true,
+        //     message: '请选择首个进球球队',
+        //   },
+        // ]}
         width="md"
-        name="firstTeamToScore"
+        name={['result', 'firstTeamToScore']}
         label="首个进球球队"
         valueEnum={{
           home: '主队',
@@ -149,6 +146,10 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => {
           no_goal: '无进球',
         }}
       />
+      {/* <ProFormDigit width="md" name={['result', 'totalGoals']} label="总进球数" disabled /> */}
+      <ProFormDigit width="md" name={['pointRewards', 'whoWillWin']} label="胜负预测积分" />
+      <ProFormDigit width="md" name={['pointRewards', 'firstTeamToScore']} label="首进球预测积分" />
+      <ProFormDigit width="md" name={['pointRewards', 'totalGoals']} label="总进球预测积分" />
       <ProFormSelect
         rules={[
           {

+ 212 - 28
src/pages/Match/index.tsx

@@ -1,4 +1,11 @@
-import { addMatch, batchDeleteMatches, deleteMatch, getMatches, updateMatch } from '@/services/api';
+import {
+  addMatch,
+  batchDeleteMatches,
+  deleteMatch,
+  getMatches,
+  updateMatch,
+  updatePredictionsForMatch,
+} from '@/services/api';
 import { PlusOutlined } from '@ant-design/icons';
 import type { ActionType, ProColumns } from '@ant-design/pro-components';
 import { debounce } from 'lodash';
@@ -14,6 +21,7 @@ import {
 
 import { FooterToolbar, PageContainer, ProTable } from '@ant-design/pro-components';
 import { Button, FormInstance, message, Popconfirm } from 'antd';
+import Modal from 'antd/lib/modal';
 import React, { useCallback, useMemo, useRef, useState } from 'react';
 import UpdateForm from './components/UpdateForm';
 
@@ -76,8 +84,35 @@ const MatchList: React.FC = () => {
       valueType: 'digit',
     },
     {
+      title: '总进球数',
+      dataIndex: 'totalGoals',
+      valueType: 'digit',
+      render: (_, record) => {
+        const homeScore = record.homeTeamScore;
+        const awayScore = record.awayTeamScore;
+
+        if (homeScore === null && awayScore === null) {
+          return '-';
+        }
+        const homeGoals = homeScore === null ? 0 : homeScore;
+        const awayGoals = awayScore === null ? 0 : awayScore;
+
+        return homeGoals + awayGoals;
+      },
+    },
+
+    {
+      title: '胜负结果',
+      dataIndex: ['result', 'whoWillWin'],
+      valueEnum: {
+        home: { text: '主队胜' },
+        away: { text: '客队胜' },
+        draw: { text: '平局' },
+      },
+    },
+    {
       title: '首个进球球队',
-      dataIndex: 'firstTeamToScore',
+      dataIndex: ['result', 'firstTeamToScore'],
       valueEnum: {
         home: { text: '主队' },
         away: { text: '客队' },
@@ -85,6 +120,21 @@ const MatchList: React.FC = () => {
       },
     },
     {
+      title: '胜负预测积分',
+      dataIndex: ['pointRewards', 'whoWillWin'],
+      valueType: 'digit',
+    },
+    {
+      title: '首进球预测积分',
+      dataIndex: ['pointRewards', 'firstTeamToScore'],
+      valueType: 'digit',
+    },
+    {
+      title: '总进球预测积分',
+      dataIndex: ['pointRewards', 'totalGoals'],
+      valueType: 'digit',
+    },
+    {
       title: '操作',
       dataIndex: 'option',
       valueType: 'option',
@@ -118,6 +168,8 @@ const MatchList: React.FC = () => {
     if (res.success) {
       message.success('比赛记录已成功删除');
       actionRef.current?.reload();
+    } else {
+      message.error('删除失败,请重试');
     }
   };
 
@@ -154,11 +206,9 @@ const MatchList: React.FC = () => {
     formData.append('teamName', teamType === 'home' ? homeTeamName : awayTeamName);
     formData.append('teamType', teamType);
 
-    console.log(111, file, teamType, homeTeamName, awayTeamName);
-
     try {
       // 首先上传文件
-      const uploadResponse = await fetch('http://admin.dzhhzy.com/api/upload', {
+      const uploadResponse = await fetch(`${API_URL}/api/upload`, {
         method: 'POST',
         body: formData,
       });
@@ -173,7 +223,7 @@ const MatchList: React.FC = () => {
 
       if (uploadData.success) {
         // 文件上传成功后,更新球队信息
-        const teamResponse = await fetch('http://admin.dzhhzy.com/api/team', {
+        const teamResponse = await fetch(`${API_URL}/api/team`, {
           method: 'POST',
           headers: {
             'Content-Type': 'application/json',
@@ -217,7 +267,7 @@ const MatchList: React.FC = () => {
 
   const debouncedFetchTeamLogo = useRef(
     debounce(async (teamName, setLogoFunc) => {
-      console.log('teamName111', teamName);
+      console.log('teamName', teamName);
 
       if (!teamName) {
         setLogoFunc('');
@@ -225,8 +275,8 @@ const MatchList: React.FC = () => {
       }
 
       try {
-        const response = await fetch(`http://admin.dzhhzy.com/api/team?name=${teamName}`);
-        console.log('response222', response);
+        const response = await fetch(`${API_URL}/api/team?name=${teamName}`);
+        console.log('response', response);
 
         if (response.ok) {
           const data = await response.json();
@@ -246,6 +296,95 @@ const MatchList: React.FC = () => {
     }, 300),
   ).current;
 
+  const handleUpdateSubmit = async (value: API.MatchItem) => {
+    console.log('value', value);
+    if (value.status === '已结束') {
+      // 检查必填字段
+      const missingFields = checkRequiredFields(value);
+      if (missingFields.length > 0) {
+        message.error(`以下字段不能为空: ${missingFields.join(', ')}`);
+        return;
+      }
+      Modal.confirm({
+        title: '确认结束比赛?',
+        content: '比赛结束后将会给预测对本次比赛的用户添加积分,是否确认结束比赛?',
+        onOk: async () => {
+          await updateMatchAndHandleResponse(value);
+        },
+        onCancel: () => {
+          message.info('取消了比赛结束操作');
+        },
+      });
+    } else {
+      await updateMatchAndHandleResponse(value);
+    }
+  };
+
+  const checkRequiredFields = (value: API.MatchItem): string[] => {
+    const missingFields: string[] = [];
+
+    // 检查 result 字段
+    if (!value.result?.whoWillWin) missingFields.push('胜负结果');
+    if (!value.result?.firstTeamToScore) missingFields.push('首先得分球队');
+
+    // 检查 pointRewards 字段
+    if (value.pointRewards?.whoWillWin === undefined || value.pointRewards.whoWillWin === null)
+      missingFields.push('胜负预测积分奖励');
+    if (
+      value.pointRewards?.firstTeamToScore === undefined ||
+      value.pointRewards.firstTeamToScore === null
+    )
+      missingFields.push('首先得分预测积分奖励');
+    if (value.pointRewards?.totalGoals === undefined || value.pointRewards.totalGoals === null)
+      missingFields.push('总进球数预测积分奖励');
+
+    // 检查比分
+    if (value.homeTeamScore === undefined || value.homeTeamScore === null)
+      missingFields.push('主队得分');
+    if (value.awayTeamScore === undefined || value.awayTeamScore === null)
+      missingFields.push('客队得分');
+
+    return missingFields;
+  };
+
+  const updateMatchAndHandleResponse = async (value: API.MatchItem) => {
+    try {
+      const res = await updateMatch(value._id, value);
+      console.log('updateMatch res', res);
+
+      if (res.success) {
+        message.success('修改成功');
+        if (value.status === '已结束') {
+          // message.info('系统正在处理用户积分,这可能需要一些时间');
+          await handleMatchEnd(value._id);
+        }
+        handleUpdateModalOpen(false);
+        setCurrentRow(undefined);
+        actionRef.current?.reload();
+      } else {
+        message.error(res.message || '修改失败,请重试');
+      }
+    } catch (error) {
+      console.error('更新比赛时出错:', error);
+      message.error('修改失败,请重试');
+    }
+  };
+
+  // 新增函数来处理比赛结束后的预测更新
+  const handleMatchEnd = async (matchId: string) => {
+    try {
+      const res = await updatePredictionsForMatch(matchId);
+      if (res.success) {
+        message.success('用户积分已更新');
+      } else {
+        message.error('更新用户积分时出错');
+      }
+    } catch (error) {
+      console.error('处理比赛结束时出错:', error);
+      message.error('更新用户积分失败');
+    }
+  };
+
   return (
     <PageContainer>
       <ProTable<API.MatchItem, API.PageParams>
@@ -279,6 +418,7 @@ const MatchList: React.FC = () => {
           };
         }}
         columns={columns}
+        scroll={{ x: 1380 }}
         pagination={{
           showSizeChanger: true,
           pageSize: pageSize,
@@ -381,7 +521,7 @@ const MatchList: React.FC = () => {
           max={1}
           action={handleUpload('home')}
           fileList={useMemo(() => {
-            return homeTeamLogoUrl ? [{ url: `http://admin.dzhhzy.com/${homeTeamLogoUrl}` }] : [];
+            return homeTeamLogoUrl ? [{ url: `${API_URL}${homeTeamLogoUrl}` }] : [];
           }, [homeTeamLogoUrl])}
           onChange={({ fileList }) => {
             if (fileList.length === 0) {
@@ -416,7 +556,7 @@ const MatchList: React.FC = () => {
           max={1}
           action={handleUpload('away')}
           fileList={useMemo(() => {
-            return awayTeamLogoUrl ? [{ url: `http://admin.dzhhzy.com/${awayTeamLogoUrl}` }] : [];
+            return awayTeamLogoUrl ? [{ url: `${API_URL}/${awayTeamLogoUrl}` }] : [];
           }, [awayTeamLogoUrl])}
           onChange={({ fileList }) => {
             if (fileList.length === 0) {
@@ -459,9 +599,9 @@ const MatchList: React.FC = () => {
           name="league"
           label="联赛"
         />
-        <ProFormDigit width="md" name={['odds', 'home']} label="主队进球" />
-        <ProFormDigit width="md" name={['odds', 'away']} label="客队进球" />
-        <ProFormSelect
+        {/* <ProFormDigit width="md" name={['odds', 'home']} label="主队进球" />
+        <ProFormDigit width="md" name={['odds', 'away']} label="客队进球" /> */}
+        {/* <ProFormSelect
           rules={[
             {
               required: true,
@@ -476,7 +616,64 @@ const MatchList: React.FC = () => {
             away: '客队',
             no_goal: '无进球',
           }}
+        /> */}
+        {/* <ProFormSelect
+          rules={[
+            {
+              required: true,
+              message: '请选择胜负结果',
+            },
+          ]}
+          width="md"
+          name={['result', 'whoWillWin']}
+          label="胜负结果"
+          valueEnum={{
+            home: '主队胜',
+            away: '客队胜',
+            draw: '平局',
+          }}
         />
+
+        <ProFormSelect
+          rules={[
+            {
+              required: true,
+              message: '请选择首个进球球队',
+            },
+          ]}
+          width="md"
+          name={['result', 'firstTeamToScore']}
+          label="首个进球球队"
+          valueEnum={{
+            home: '主队',
+            away: '客队',
+            no_goal: '无进球',
+          }}
+        /> */}
+
+        {/* <ProFormDigit width="md" name={['result', 'totalGoals']} label="总进球数" /> */}
+
+        <ProFormDigit
+          width="md"
+          name={['pointRewards', 'whoWillWin']}
+          label="胜负预测积分"
+          // initialValue={3}
+        />
+
+        <ProFormDigit
+          width="md"
+          name={['pointRewards', 'firstTeamToScore']}
+          label="首进球预测积分"
+          // initialValue={2}
+        />
+
+        <ProFormDigit
+          width="md"
+          name={['pointRewards', 'totalGoals']}
+          label="总进球预测积分"
+          // initialValue={5}
+        />
+
         <ProFormSelect
           rules={[
             {
@@ -497,20 +694,7 @@ const MatchList: React.FC = () => {
       {/* 新建弹框--END */}
 
       <UpdateForm
-        onSubmit={async (value) => {
-          const res = await updateMatch(value._id, value as API.MatchItem);
-
-          console.log('updateMatch res', res);
-
-          if (res.success) {
-            message.success('修改成功');
-            handleUpdateModalOpen(false);
-            setCurrentRow(undefined);
-            if (actionRef.current) {
-              actionRef.current.reload();
-            }
-          }
-        }}
+        onSubmit={handleUpdateSubmit}
         onCancel={() => {
           handleUpdateModalOpen(false);
         }}

+ 102 - 0
src/pages/Points/index.tsx

@@ -0,0 +1,102 @@
+import { deletePointHistory, getUserPointHistory } from '@/services/api';
+import type { ActionType, ProColumns } from '@ant-design/pro-components';
+import { PageContainer, ProTable } from '@ant-design/pro-components';
+import { message, Popconfirm } from 'antd';
+import React, { useRef, useState } from 'react';
+
+const PointHistoryList: React.FC = () => {
+  const actionRef = useRef<ActionType>();
+  const [pageSize, setPageSize] = useState<number>(10);
+
+  const columns: ProColumns<API.PointHistoryItem>[] = [
+    {
+      title: '用户名',
+      dataIndex: ['user', 'username'],
+      valueType: 'text',
+    },
+    {
+      title: '积分变更',
+      dataIndex: 'points',
+      valueType: 'digit',
+      render: (text, record) => {
+        const value = typeof text === 'number' ? text : Number(text);
+        const points = isNaN(value) ? record.points : value;
+        return (
+          <span style={{ color: points > 0 ? 'green' : points < 0 ? 'red' : 'inherit' }}>
+            {points > 0 ? `+${points}` : points}
+          </span>
+        );
+      },
+    },
+
+    {
+      title: '变更原因',
+      dataIndex: 'reason',
+      valueType: 'text',
+    },
+    {
+      title: '变更时间',
+      dataIndex: 'createdAt',
+      valueType: 'dateTime',
+    },
+    {
+      title: '操作',
+      dataIndex: 'option',
+      valueType: 'option',
+      render: (_, record) => [
+        <Popconfirm
+          key="delete"
+          title="确认删除"
+          description="您确定要删除这条积分历史记录吗?"
+          onConfirm={() => handleDelete(record)}
+          okText="确定"
+          cancelText="取消"
+        >
+          <a>删除</a>
+        </Popconfirm>,
+      ],
+    },
+  ];
+
+  const handleDelete = async (record: API.PointHistoryItem) => {
+    const res = await deletePointHistory(record._id);
+    if (res.success) {
+      message.success('积分历史记录已成功删除');
+      actionRef.current?.reload();
+    }
+  };
+
+  return (
+    <PageContainer>
+      <ProTable<API.PointHistoryItem, API.PageParams>
+        headerTitle="积分历史记录"
+        actionRef={actionRef}
+        rowKey="_id"
+        search={{
+          labelWidth: 120,
+        }}
+        request={async (params) => {
+          const response = await getUserPointHistory(params);
+          return {
+            data: response.data,
+            success: response.success,
+            total: response.total,
+          };
+        }}
+        columns={columns}
+        pagination={{
+          showSizeChanger: true,
+          pageSize: pageSize,
+          onChange: (page, pageSize) => {
+            setPageSize(pageSize);
+            if (actionRef.current) {
+              actionRef.current.reload();
+            }
+          },
+        }}
+      />
+    </PageContainer>
+  );
+};
+
+export default PointHistoryList;

+ 4 - 3
src/pages/Prediction/components/UpdateForm.tsx

@@ -1,4 +1,4 @@
-import { ModalForm, ProFormDigit, ProFormSelect, ProFormSwitch } from '@ant-design/pro-components';
+import { ModalForm, ProFormDigit, ProFormSelect } from '@ant-design/pro-components';
 import React from 'react';
 
 export type UpdatePredictionFormProps = {
@@ -23,6 +23,7 @@ const UpdatePredictionForm: React.FC<UpdatePredictionFormProps> = (props) => {
         }
       }}
       onFinish={async (value) => {
+        // values.isCorrect = values.isCorrect ? true : false;
         const formData = {
           ...values,
           ...value,
@@ -75,8 +76,8 @@ const UpdatePredictionForm: React.FC<UpdatePredictionFormProps> = (props) => {
         label="总进球数"
         min={0}
       />
-      <ProFormDigit width="md" name="pointsEarned" label="获得积分" min={0} />
-      <ProFormSwitch name="isCorrect" label="预测是否正确" />
+      {/* <ProFormDigit width="md" name="pointsEarned" label="获得积分" min={0} /> */}
+      {/* <ProFormSwitch name="isCorrect" label="预测是否正确" /> */}
     </ModalForm>
   );
 };

+ 42 - 26
src/pages/Prediction/index.tsx

@@ -1,22 +1,16 @@
-import {
-  batchDeletePredictions,
-  deletePrediction,
-  getPredictions,
-  updatePrediction,
-} from '@/services/api'; // 假设这些API已经实现
+import { batchDeletePredictions, deletePrediction, getPredictions } from '@/services/api'; // 假设这些API已经实现
 import type { ActionType, ProColumns } from '@ant-design/pro-components';
 import { FooterToolbar, PageContainer, ProTable } from '@ant-design/pro-components';
-import { Button, FormInstance, message, Popconfirm } from 'antd';
+import { Button, message, Popconfirm } from 'antd';
 import React, { useRef, useState } from 'react';
-import UpdateForm from './components/UpdateForm';
 
 const PredictionList: React.FC = () => {
   const actionRef = useRef<ActionType>();
-  const [currentRow, setCurrentRow] = useState<API.PredictionItem>();
+  // const [currentRow, setCurrentRow] = useState<API.PredictionItem>();
   const [selectedRows, setSelectedRows] = useState<API.PredictionItem[]>([]);
   const [pageSize, setPageSize] = useState<number>(10);
-  const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
-  const formRef = useRef<FormInstance>(null);
+  // const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
+  // const formRef = useRef<FormInstance>(null);
 
   const columns: ProColumns<API.PredictionItem>[] = [
     {
@@ -58,31 +52,53 @@ const PredictionList: React.FC = () => {
       valueType: 'digit',
     },
     {
-      title: '预测结果',
-      dataIndex: 'isCorrect',
+      title: '胜负预测结果',
+      dataIndex: 'whoWillWinResult',
       valueEnum: {
-        true: { text: '正确', status: 'Success' },
-        false: { text: '错误', status: 'Error' },
+        correct: { text: '正确', status: 'Success' },
+        incorrect: { text: '错误', status: 'Error' },
+        pending: { text: '-' },
+      },
+    },
+    {
+      title: '首先得分预测结果',
+      dataIndex: 'firstTeamToScoreResult',
+      valueEnum: {
+        correct: { text: '正确', status: 'Success' },
+        incorrect: { text: '错误', status: 'Error' },
+        // pending: { text: '待定', status: 'Warning' },
+        pending: { text: '-' },
+      },
+    },
+    {
+      title: '总进球数预测结果',
+      dataIndex: 'totalGoalsResult',
+      valueEnum: {
+        correct: { text: '正确', status: 'Success' },
+        incorrect: { text: '错误', status: 'Error' },
+        // pending: { text: '待定', status: 'Warning' },
+        pending: { text: '-' },
       },
     },
     {
       title: '获得积分',
       dataIndex: 'pointsEarned',
+      valueType: 'digit',
     },
     {
       title: '操作',
       dataIndex: 'option',
       valueType: 'option',
       render: (_, record) => [
-        <a
-          key="edit"
-          onClick={() => {
-            handleUpdateModalOpen(true);
-            setCurrentRow(record);
-          }}
-        >
-          编辑
-        </a>,
+        // <a
+        //   key="edit"
+        //   onClick={() => {
+        //     handleUpdateModalOpen(true);
+        //     setCurrentRow(record);
+        //   }}
+        // >
+        //   编辑
+        // </a>,
         <Popconfirm
           key="delete"
           title="确认删除"
@@ -162,7 +178,7 @@ const PredictionList: React.FC = () => {
         </FooterToolbar>
       )}
 
-      <UpdateForm
+      {/* <UpdateForm
         onSubmit={async (value) => {
           const res = await updatePrediction(value._id, value as API.PredictionItem);
           if (res.success) {
@@ -177,7 +193,7 @@ const PredictionList: React.FC = () => {
         }}
         updateModalOpen={updateModalOpen}
         values={currentRow || {}}
-      />
+      /> */}
 
       {/* <ModalForm
         title="编辑预测"

+ 1 - 13
src/pages/User/components/UpdateForm.tsx → src/pages/User/components/UpdateUser.tsx

@@ -1,4 +1,4 @@
-import { ModalForm, ProFormDigit, ProFormSelect, ProFormText } from '@ant-design/pro-components';
+import { ModalForm, ProFormSelect, ProFormText } from '@ant-design/pro-components';
 import React from 'react';
 
 export type UpdateUserProps = {
@@ -69,18 +69,6 @@ const UpdateUser: React.FC<UpdateUserProps> = (props) => {
           admin: '管理员',
         }}
       />
-      <ProFormDigit
-        rules={[
-          {
-            required: true,
-            message: '积分是必填项',
-          },
-        ]}
-        width="md"
-        name="points"
-        label="积分"
-        fieldProps={{ precision: 0 }}
-      />
     </ModalForm>
   );
 };

+ 86 - 0
src/pages/User/components/UpdateUserPoints.tsx

@@ -0,0 +1,86 @@
+import { ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
+import React, { useRef } from 'react';
+
+export type UpdateUserPointsProps = {
+  onCancel: () => void;
+  onSubmit: (values: API.UpdateUserPointsParams) => Promise<void>;
+  updatePointsModalOpen: boolean;
+  currentUser: Partial<API.UserItem>;
+};
+
+const UpdateUserPoints: React.FC<UpdateUserPointsProps> = (props) => {
+  const { onCancel, onSubmit, updatePointsModalOpen, currentUser } = props;
+  const formRef = useRef();
+
+  return (
+    <ModalForm
+      title="修改积分"
+      width="400px"
+      open={updatePointsModalOpen}
+      onOpenChange={(visible) => {
+        if (!visible) {
+          onCancel();
+        }
+      }}
+      formRef={formRef}
+      modalProps={{
+        destroyOnClose: true,
+        afterClose: () => {
+          formRef.current?.resetFields();
+        },
+      }}
+      onFinish={async (value) => {
+        const formData = {
+          userId: currentUser._id,
+          points: parseInt(value.points, 10),
+          reason: value.reason,
+        };
+        await onSubmit(formData as API.UpdateUserPointsParams);
+      }}
+    >
+      <div>当前积分:{currentUser.points}</div>
+      <ProFormText
+        rules={[
+          {
+            required: true,
+            message: '积分变更是必填项',
+          },
+          {
+            pattern: /^-?\d+$/,
+            message: '请输入整数',
+          },
+          ({ getFieldValue }) => ({
+            validator(_, value) {
+              const points = parseInt(value, 10);
+              //   if (isNaN(points)) {
+              //     return Promise.reject(new Error('请输入有效的数字'));
+              //   }
+              if (points < 0 && Math.abs(points) > currentUser.points) {
+                return Promise.reject(new Error('减少的积分不能超过当前积分'));
+              }
+              return Promise.resolve();
+            },
+          }),
+        ]}
+        width="md"
+        name="points"
+        label="积分变更"
+        placeholder="输入正数增加积分,负数减少积分"
+        // extra="输入正数增加积分,负数减少积分"
+      />
+      <ProFormTextArea
+        rules={[
+          {
+            required: true,
+            message: '修改原因是必填项',
+          },
+        ]}
+        width="md"
+        name="reason"
+        label="修改原因"
+      />
+    </ModalForm>
+  );
+};
+
+export default UpdateUserPoints;

+ 58 - 21
src/pages/User/index.tsx

@@ -1,4 +1,11 @@
-import { addUser, batchDeleteUsers, deleteUser, getUsers, updateUser } from '@/services/api';
+import {
+  addUser,
+  batchDeleteUsers,
+  deleteUser,
+  getUsers,
+  updateUser,
+  updateUserPoints,
+} from '@/services/api';
 import { PlusOutlined } from '@ant-design/icons';
 import type { ActionType, ProColumns } from '@ant-design/pro-components';
 import { ModalForm, ProFormDigit, ProFormSelect, ProFormText } from '@ant-design/pro-form';
@@ -6,7 +13,8 @@ import { ModalForm, ProFormDigit, ProFormSelect, ProFormText } from '@ant-design
 import { FooterToolbar, PageContainer, ProTable } from '@ant-design/pro-components';
 import { Button, FormInstance, message, Popconfirm } from 'antd';
 import React, { useRef, useState } from 'react';
-import UpdateForm from './components/UpdateForm';
+import UpdateUser from './components/UpdateUser';
+import UpdateUserPoints from './components/UpdateUserPoints';
 
 const UserList: React.FC = () => {
   const [createModalOpen, handleModalOpen] = useState<boolean>(false);
@@ -16,6 +24,24 @@ const UserList: React.FC = () => {
   const [selectedRows, setSelectedRows] = useState<API.UserItem[]>([]);
   const [pageSize, setPageSize] = useState<number>(10);
   const formRef = useRef<FormInstance>(null);
+  const [updatePointsModalOpen, setUpdatePointsModalOpen] = useState(false);
+
+  const handleDelete = async (record: API.UserItem) => {
+    const res = await deleteUser(record._id);
+    if (res.success) {
+      message.success('用户已成功删除');
+      actionRef.current?.reload();
+    }
+  };
+
+  const handleBatchDelete = async (selectedRowKeys: string[]) => {
+    if (selectedRowKeys.length === 0) return;
+    const res = await batchDeleteUsers(selectedRowKeys);
+    if (res.success) {
+      message.success(`${res.message}`);
+      actionRef.current?.reload();
+    }
+  };
 
   const columns: ProColumns<API.UserItem>[] = [
     {
@@ -49,7 +75,16 @@ const UserList: React.FC = () => {
             setCurrentRow(record);
           }}
         >
-          编辑
+          编辑用户
+        </a>,
+        <a
+          key="edit"
+          onClick={() => {
+            setUpdatePointsModalOpen(true);
+            setCurrentRow(record);
+          }}
+        >
+          修改积分
         </a>,
         <Popconfirm
           key="delete"
@@ -65,23 +100,6 @@ const UserList: React.FC = () => {
     },
   ];
 
-  const handleDelete = async (record: API.UserItem) => {
-    const res = await deleteUser(record._id);
-    if (res.success) {
-      message.success('用户已成功删除');
-      actionRef.current?.reload();
-    }
-  };
-
-  const handleBatchDelete = async (selectedRowKeys: string[]) => {
-    if (selectedRowKeys.length === 0) return;
-    const res = await batchDeleteUsers(selectedRowKeys);
-    if (res.success) {
-      message.success(`${res.message}`);
-      actionRef.current?.reload();
-    }
-  };
-
   return (
     <PageContainer>
       <ProTable<API.UserItem, API.PageParams>
@@ -206,7 +224,7 @@ const UserList: React.FC = () => {
         <ProFormDigit width="md" name="points" label="积分" initialValue={0} />
       </ModalForm>
 
-      <UpdateForm
+      <UpdateUser
         onSubmit={async (value) => {
           const res = await updateUser(value._id, value as API.UserItem);
           if (res.success) {
@@ -224,6 +242,25 @@ const UserList: React.FC = () => {
         updateUserModalOpen={updateModalOpen}
         values={currentRow || {}}
       />
+      {/* 修改积分 */}
+      <UpdateUserPoints
+        onSubmit={async (value: API.UpdateUserPointsParams) => {
+          const res = await updateUserPoints(value.userId, value.points, value.reason);
+          if (res.success) {
+            message.success('积分更新成功');
+            setUpdatePointsModalOpen(false);
+            setCurrentRow(undefined);
+            if (actionRef.current) {
+              actionRef.current.reload();
+            }
+          }
+        }}
+        onCancel={() => {
+          setUpdatePointsModalOpen(false);
+        }}
+        updatePointsModalOpen={updatePointsModalOpen}
+        currentUser={currentRow || {}}
+      />
     </PageContainer>
   );
 };

+ 3 - 1
src/requestErrorConfig.ts

@@ -69,9 +69,11 @@ export const errorConfig: RequestConfig = {
           }
         }
       } else if (error.response) {
+        console.error(error);
+
         // Axios 的错误
         // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
-        message.error(`${error.response.data.error}`);
+        message.error(`${error?.response?.data?.error}`);
       } else if (error.request) {
         // 请求已经成功发起,但没有收到响应
         // \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,

+ 114 - 60
src/services/api.ts

@@ -10,7 +10,7 @@ export async function getPredictions(
   },
   options?: { [key: string]: any },
 ) {
-  return request<API.PredictionList>('http://admin.dzhhzy.com/api/prediction', {
+  return request<API.PredictionList>(`${API_URL}/api/prediction`, {
     method: 'GET',
     params: {
       ...params,
@@ -29,11 +29,11 @@ export async function searchPredictions(
     firstTeamToScore?: 'home' | 'away' | 'no_goal';
     totalGoals?: number;
     pointsEarned?: number;
-    isCorrect?: boolean;
+    // isCorrect?: boolean;
   },
   options?: { [key: string]: any },
 ) {
-  return request<API.PredictionList>('http://admin.dzhhzy.com/api/prediction', {
+  return request<API.PredictionList>(`${API_URL}/api/prediction`, {
     method: 'GET',
     params: {
       action: 'searchPredictions',
@@ -44,7 +44,7 @@ export async function searchPredictions(
 }
 
 export async function addPrediction(options?: { [key: string]: any }) {
-  return request<API.PredictionList>('http://admin.dzhhzy.com/api/prediction', {
+  return request<API.PredictionList>(`${API_URL}/api/prediction`, {
     method: 'POST',
     data: {
       ...(options || {}),
@@ -55,7 +55,7 @@ export async function addPrediction(options?: { [key: string]: any }) {
 export async function updatePrediction(id: string, options?: { [key: string]: any }) {
   console.log('Sending update request for id:', id);
   console.log('Update data:', options);
-  return request<API.PredictionList>('http://admin.dzhhzy.com/api/prediction', {
+  return request<API.PredictionList>(`${API_URL}/api/prediction`, {
     method: 'PUT',
     headers: {
       'Content-Type': 'application/json',
@@ -69,7 +69,7 @@ export async function updatePrediction(id: string, options?: { [key: string]: an
 
 export async function deletePrediction(id: string) {
   console.log('Sending delete request for id:', id);
-  return request<API.PredictionList>(`http://admin.dzhhzy.com/api/prediction?id=${id}`, {
+  return request<API.PredictionList>(`${API_URL}/api/prediction?id=${id}`, {
     method: 'DELETE',
     headers: {
       'Content-Type': 'application/json',
@@ -77,19 +77,9 @@ export async function deletePrediction(id: string) {
   });
 }
 
-// export async function deleteMatch(id: string) {
-//   console.log('Sending delete request for id:', id);
-//   return request<API.MatchList>(`http://admin.dzhhzy.com/api/match?id=${id}`, {
-//     method: 'DELETE',
-//     headers: {
-//       'Content-Type': 'application/json',
-//     },
-//   });
-// }
-
 export async function batchDeletePredictions(ids: string[]) {
   console.log('Sending batch delete request for ids:', ids);
-  return request<API.PredictionList>(`http://admin.dzhhzy.com/api/prediction?ids=${ids}`, {
+  return request<API.PredictionList>(`${API_URL}/api/prediction?ids=${ids}`, {
     method: 'DELETE',
     headers: {
       'Content-Type': 'application/json',
@@ -107,7 +97,7 @@ export async function getMatches(
   },
   options?: { [key: string]: any },
 ) {
-  return request<API.MatchList>('http://admin.dzhhzy.com/api/match?action=getMatches', {
+  return request<API.MatchList>(`${API_URL}/api/match?action=getMatches`, {
     method: 'GET',
     params: {
       ...params,
@@ -117,7 +107,7 @@ export async function getMatches(
 }
 
 export async function addMatch(options?: { [key: string]: any }) {
-  return request<API.MatchList>('http://admin.dzhhzy.com/api/match', {
+  return request<API.MatchList>(`${API_URL}/api/match`, {
     method: 'POST',
     data: {
       method: 'update',
@@ -129,7 +119,7 @@ export async function addMatch(options?: { [key: string]: any }) {
 export async function updateMatch(id: string, options?: { [key: string]: any }) {
   console.log('Sending update request for id:', id);
   console.log('Update data:', options);
-  return request<API.MatchList>('http://admin.dzhhzy.com/api/match', {
+  return request<API.MatchList>(`${API_URL}/api/match`, {
     method: 'PUT',
     headers: {
       'Content-Type': 'application/json',
@@ -143,7 +133,7 @@ export async function updateMatch(id: string, options?: { [key: string]: any })
 
 export async function deleteMatch(id: string) {
   console.log('Sending delete request for id:', id);
-  return request<API.MatchList>(`http://admin.dzhhzy.com/api/match?id=${id}`, {
+  return request<API.MatchList>(`${API_URL}/api/match?id=${id}`, {
     method: 'DELETE',
     headers: {
       'Content-Type': 'application/json',
@@ -156,7 +146,7 @@ export async function batchDeleteMatches(ids: string[]) {
 
   const idsString = ids.join(',');
 
-  return request<API.MatchList>(`http://admin.dzhhzy.com/api/match?ids=${idsString}`, {
+  return request<API.MatchList>(`${API_URL}/api/match?ids=${idsString}`, {
     method: 'DELETE',
     headers: {
       'Content-Type': 'application/json',
@@ -174,7 +164,7 @@ export async function getUsers(
   },
   options?: { [key: string]: any },
 ) {
-  return request<API.UserList>('http://admin.dzhhzy.com/api/user', {
+  return request<API.UserList>(`${API_URL}/api/user`, {
     method: 'GET',
     params: {
       ...params,
@@ -184,7 +174,7 @@ export async function getUsers(
 }
 
 export async function addUser(options?: { [key: string]: any }) {
-  return request<API.UserList>('http://admin.dzhhzy.com/api/user', {
+  return request<API.UserList>(`${API_URL}/api/user`, {
     method: 'POST',
     data: {
       method: 'add',
@@ -196,7 +186,7 @@ export async function addUser(options?: { [key: string]: any }) {
 export async function updateUser(id: string, options?: { [key: string]: any }) {
   console.log('Sending update request for user id:', id);
   console.log('Update data:', options);
-  return request<API.UserList>('http://admin.dzhhzy.com/api/user', {
+  return request<API.UserList>(`${API_URL}/api/user`, {
     method: 'PUT',
     headers: {
       'Content-Type': 'application/json',
@@ -210,7 +200,7 @@ export async function updateUser(id: string, options?: { [key: string]: any }) {
 
 export async function deleteUser(id: string) {
   console.log('Sending delete request for user id:', id);
-  return request<API.UserList>(`http://admin.dzhhzy.com/api/user?id=${id}`, {
+  return request<API.UserList>(`${API_URL}/api/user?id=${id}`, {
     method: 'DELETE',
     headers: {
       'Content-Type': 'application/json',
@@ -223,7 +213,7 @@ export async function batchDeleteUsers(ids: string[]) {
 
   const idsString = ids.join(',');
 
-  return request<API.UserList>(`http://admin.dzhhzy.com/api/user?ids=${idsString}`, {
+  return request<API.UserList>(`${API_URL}/api/user?ids=${idsString}`, {
     method: 'DELETE',
     headers: {
       'Content-Type': 'application/json',
@@ -234,7 +224,7 @@ export async function batchDeleteUsers(ids: string[]) {
 
 /** 登录接口 POST /api/login/account */
 export async function login(body: API.LoginParams, options?: { [key: string]: any }) {
-  return request<API.LoginResult>('http://admin.dzhhzy.com/api/auth/login', {
+  return request<API.LoginResult>(`${API_URL}/api/auth/login`, {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
@@ -245,7 +235,7 @@ export async function login(body: API.LoginParams, options?: { [key: string]: an
 }
 
 export async function register(body: API.LoginParams, options?: { [key: string]: any }) {
-  return request<API.RegisterResult>('http://admin.dzhhzy.com/api/auth/register', {
+  return request<API.RegisterResult>(`${API_URL}/api/auth/register`, {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
@@ -265,34 +255,90 @@ export async function register(body: API.LoginParams, options?: { [key: string]:
 //   });
 // }
 
-/** 退出登录接口 POST /api/login/outLogin */
-export async function outLogin(options?: { [key: string]: any }) {
-  return request<Record<string, any>>('/api/login/outLogin', {
+// 修改积分历史
+export async function updateUserPoints(userId: string, points: number, reason: string) {
+  console.log('发送积分更新请求,用户ID:', userId);
+  console.log('积分变更:', points);
+  console.log('变更原因:', reason);
+
+  return request<API.PointHistoryResponse>(`${API_URL}/api/point-history`, {
     method: 'POST',
-    ...(options || {}),
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: JSON.stringify({
+      userId,
+      points,
+      reason,
+    }),
   });
 }
 
-/** 此处后端没有提供注释 GET /api/notices */
-export async function getNotices(options?: { [key: string]: any }) {
-  return request<API.NoticeIconList>('/api/notices', {
+// 获取积分历史
+export async function getUserPointHistory(
+  params: {
+    userId: string;
+    current?: number;
+    pageSize?: number;
+  },
+  options?: { [key: string]: any },
+) {
+  return request<API.PointHistoryList>(`${API_URL}/api/point-history`, {
     method: 'GET',
+    params: {
+      ...params,
+    },
+    ...(options || {}),
+  });
+}
+
+// 删除单个积分历史记录
+export async function deletePointHistory(id: string) {
+  console.log('发送删除请求,积分历史记录ID:', id);
+  return request<API.PointHistoryList>(`${API_URL}/api/point-history?id=${id}`, {
+    method: 'DELETE',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+  });
+}
+
+// 批量删除积分历史记录
+export async function batchDeletePointHistory(ids: string[]) {
+  console.log('发送批量删除请求,积分历史记录IDs:', ids);
+  const idsString = ids.join(',');
+  return request<API.PointHistoryList>(`${API_URL}/api/point-history?ids=${idsString}`, {
+    method: 'DELETE',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+  });
+}
+
+export async function updatePredictionsForMatch(matchId: string) {
+  return request<API.updatePredictionsForMatchRes>(`${API_URL}/api/updateForMatch`, {
+    method: 'POST',
+    data: { matchId },
+  });
+}
+
+/** 退出登录接口 POST /api/login/outLogin */
+export async function outLogin(options?: { [key: string]: any }) {
+  return request<Record<string, any>>('/api/login/outLogin', {
+    method: 'POST',
     ...(options || {}),
   });
 }
 
-/** 获取规则列表 GET /api/rule */
-export async function rule(
+//活动
+export async function getActivity(
   params: {
-    // query
-    /** 当前的页码 */
     current?: number;
-    /** 页面的容量 */
     pageSize?: number;
   },
   options?: { [key: string]: any },
 ) {
-  return request<API.RuleList>('/api/rule', {
+  return request<API.ActivityList>(`${API_URL}/api/activity`, {
     method: 'GET',
     params: {
       ...params,
@@ -301,35 +347,43 @@ export async function rule(
   });
 }
 
-/** 更新规则 PUT /api/rule */
-export async function updateRule(options?: { [key: string]: any }) {
-  return request<API.RuleListItem>('/api/rule', {
+// export async function getActivityById(id: string, options?: { [key: string]: any }) {
+//   return request<API.ActivityList>(`${API_URL}/api/activity/${id}`, {
+//     method: 'GET',
+//     ...(options || {}),
+//   });
+// }
+
+export async function createActivity(options?: { [key: string]: any }) {
+  return request<API.ActivityList>(`${API_URL}/api/activity`, {
     method: 'POST',
     data: {
-      method: 'update',
       ...(options || {}),
     },
   });
 }
 
-/** 新建规则 POST /api/rule */
-export async function addRule(options?: { [key: string]: any }) {
-  return request<API.RuleListItem>('/api/rule', {
-    method: 'POST',
-    data: {
-      method: 'post',
-      ...(options || {}),
+export async function updateActivity(id: string, options?: { [key: string]: any }) {
+  console.log('Sending update request for activity id:', id);
+  console.log('Update data:', options);
+  return request<API.ActivityList>(`${API_URL}/api/activity`, {
+    method: 'PUT',
+    headers: {
+      'Content-Type': 'application/json',
     },
+    data: JSON.stringify({
+      id,
+      ...(options || {}),
+    }),
   });
 }
 
-/** 删除规则 DELETE /api/rule */
-export async function removeRule(options?: { [key: string]: any }) {
-  return request<Record<string, any>>('/api/rule', {
-    method: 'POST',
-    data: {
-      method: 'delete',
-      ...(options || {}),
+export async function deleteActivity(id: string) {
+  console.log('Sending delete request for activity id:', id);
+  return request<{ success: boolean }>(`${API_URL}/api/activity?id=${id}`, {
+    method: 'DELETE',
+    headers: {
+      'Content-Type': 'application/json',
     },
   });
 }

+ 57 - 62
src/services/typings.d.ts

@@ -10,14 +10,19 @@ declare namespace API {
   };
 
   type PredictionItem = {
-    matchId: string;
     _id: string;
-    user: string;
-    whoWillWin: string;
-    pointsEarned: number;
-    isCorrect: boolean;
+
+    whoWillWin: 'home' | 'away' | 'draw';
+    whoWillWinResult: 'correct' | 'incorrect' | 'pending';
+
+    firstTeamToScore: 'home' | 'away' | 'no_goal';
+    firstTeamToScoreResult: 'correct' | 'incorrect' | 'pending';
+    firstTeamToScoreLogo?: string;
+
     totalGoals: number;
-    firstTeamToScore: string;
+    totalGoalsResult: 'correct' | 'incorrect' | 'pending';
+
+    pointsEarned: number | null;
   };
 
   type MatchList = {
@@ -85,7 +90,6 @@ declare namespace API {
   };
 
   type LoginResult = {
-    status?: string;
     type?: string;
     success?: boolean;
     error?: string;
@@ -99,13 +103,48 @@ declare namespace API {
   };
 
   type RegisterResult = {
-    status?: string;
     type?: string;
     success?: boolean;
     error?: string;
     message?: string;
   };
 
+  type PointHistoryItem = {
+    _id: string;
+    userId?: string;
+    points: number;
+    reason: string;
+    createdAt: string;
+  };
+
+  type PointHistoryList = {
+    data?: UserItem[];
+    total?: number;
+    success?: boolean;
+    error?: string;
+    message?: string;
+  };
+
+  type UpdateUserPointsParams = {
+    userId: string;
+    points: number;
+    reason: string;
+  };
+
+  type PointHistoryResponse = {
+    success?: boolean;
+    error?: string;
+    message?: string;
+    user?: object;
+  };
+
+  type updatePredictionsForMatchRes = {
+    success?: boolean;
+    error?: string;
+    message?: string;
+    user?: object;
+  };
+
   /**
    * 用户角色枚举
    */
@@ -129,61 +168,17 @@ declare namespace API {
     updatedAt?: Date;
   };
 
-  type RuleListItem = {
-    key?: number;
-    disabled?: boolean;
-    href?: string;
-    avatar?: string;
-    name?: string;
-    owner?: string;
-    desc?: string;
-    callNo?: number;
-    status?: number;
-    updatedAt?: string;
-    createdAt?: string;
-    progress?: number;
-  };
-
-  type RuleList = {
-    data?: RuleListItem[];
-    /** 列表的内容总数 */
-    total?: number;
-    success?: boolean;
-  };
-
-  type FakeCaptcha = {
-    code?: number;
-    status?: string;
-  };
-
-  type ErrorResponse = {
-    /** 业务约定的错误码 */
-    errorCode: string;
-    /** 业务上的错误信息 */
-    errorMessage?: string;
-    /** 业务上的请求是否成功 */
-    success?: boolean;
-  };
-
-  type NoticeIconList = {
-    data?: NoticeIconItem[];
-    /** 列表的内容总数 */
-    total?: number;
-    success?: boolean;
+  type ActivityItem = {
+    _id: string;
+    title: string;
+    content: string;
+    isActive: boolean;
+    backgroundImage: string;
   };
 
-  type NoticeIconItemType = 'notification' | 'message' | 'event';
-
-  type NoticeIconItem = {
-    id?: string;
-    extra?: string;
-    key?: string;
-    read?: boolean;
-    avatar?: string;
-    title?: string;
-    status?: string;
-    datetime?: string;
-    description?: string;
-    type?: NoticeIconItemType;
+  type ActivityList = {
+    data: ActivityItem[];
+    total: number;
+    success: boolean;
   };
 }