一、背景

Playedu的前端是基于React18 + TypeScript开发。主要包括:PC学员界面、H5学员界面、后台管理界面三个界面程序。它们都是相同的技术架构,因此只要您懂得其中一个项目的开发,那么另外2个项目可以无缝上手。

二、开发运行快速上手

2.1环境要求

2.2 运行系统

下面我们将以后台管理界面为例,给您演示如何将系统跑起来。

2.2.1 安装依赖

首先,进入到后台管理界面程序的根目录,然后执下面命令安装依赖:
pnpm install

2.2.2 配置文件

依赖安装完成之后,执行下面命令创建本地开发配置文件:
cp .env.example .env.local
注意,上述命令仅适用于mac或者linux系统,如果您是window系统的话,这一步请手动复制 .env.example 文件并重命名为.env.local
修改.env.local中的VITE_APP_URL的值为您的PlayEdu API服务的地址,示例如下:
VITE_APP_URL=http://127.0.0.1:9898

2.2.3 程序跑起来

执行下面命令将程序跑起来:
pnpm dev
跑起来之后可以看到:
  VITE v4.4.9  ready in 515 ms

  ➜  Local:   http://localhost:4000/
  ➜  Network: http://192.168.3.14:4000/
  ➜  Network: http://192.168.64.1:4000/
  ➜  press h to show help
我们在浏览器就可以打开上述地址访问。

三、生产部署快速上手

当我们程序二开完成之后,需要将程序部署更新到生产服务器,在部署之前,我们需要编译程序。下面将是编译的流程。

3.1 生产环境配置文件

执行下面命令创建生产环境配置文件:
cp .env.example .env.production
注意,上述命令仅适用于mac或者linux系统,如果您是window系统的话,这一步请手动复制 .env.example 文件并重命名为.env.production
修改.env.production中的VITE_APP_URL的值为您的PlayEdu API服务生产环境的地址,示例如下:
VITE_APP_URL=https://demo-api.playedu.xyz

3.2 编译程序

执行下面命令编译程序:
pnpm build
上述命令执行成功之后,可以看到类似下面的输出信息:
dist/assets/index-c61b50c7.js                      100.60 kB │ gzip:  30.06 kB
dist/assets/xlsx-4f9172c7.js                       424.92 kB │ gzip: 140.79 kB
dist/assets/index-e0333ef0.js                    1,029.09 kB │ gzip: 341.34 kB
dist/assets/index-3c2cf1a6.js                    1,347.42 kB │ gzip: 442.01 kB

(!) Some chunks are larger than 500 kBs after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 7.46s
✨  Done in 14.22s.
这就是编译完成了。编译后的程序文件在 dist 目录。到这里,我们就可以将 dist 目录的程序文件部署到生产环境服务器了。

四、项目介绍

4.1后台管理界面

4.1.1文件目录结构

playedu 
├── public                                // 公共资源
│       └── js                            // 公共JS
│       └── template                      // 公共下载、导入模板
│       └── favicon.ico                   // 网站ICO
│
├── scripts                               // 脚本
├── src                                   // 资源
│       └── api                           // 各页面API
│           └── internal                  // 全局API配置
│           └──...                        // 各模块API配置
│       └── compenents                    // 全局组件
│       └── assets                        // 全局外部资源
│       └── i18n                          // 国际化配置
│           └── cn.json                   // 简中语言JSON
│           └── config.ts                 // 国际化配置
│           └── tc.json                   // 繁中语言JSON
│       └── js                            // 通用JS
│       └── pages                         // 页面资源
│           └── cert                      // 证书模块
│           └── change-password           // 修改密码模块
│           └── course                    // 线上课模块
│           └── dashboard                 // 主页模块
│           └── department                // 部门模块
│           └── error                     // 错误模块
│           └── exam                      // 考试模块
│                └── papers               // 试卷模块
│                └── questions            // 试题模块
│                └── task                 // 考试任务模块
│           └── init                      // 初始化
│           └── layouts                   // 页面布局
│           └── loading                   // 公用loading模块
│           └── login                     // 登录模块
│           └── member                    // 学员模块
│           └── paper-show                // 试卷阅览模块
│           └── resource                  // 资源模块
│                └── attachment           // 课件模块
│                └── audio                // 音频模块
│                └── courseware           // 文档模块
│                └── images               // 图片模块
│                └── resource-category    // 分类模块
│                └── videos               // 视频模块
│           └── study                     // 学习任务模块
│           └── system                    // 系统模块
│                └── administrator        // 管理人员模块
│                └── adminlog             // 管理日志模块
│                └── adminroles           // 管理角色模块
│                └── config               // 系统配置模块
│           └── teacher                   // 讲师模块
│           └── test                      // 测试页面(仅用于测试环境)
│       └── routes                        // 路由
│       └── store                         // 中间件及store
│           └── system                    // 系统store
│           └── user                      // 用户store
│           └── index.ts                  // store配置
│       └── types                         // 全局静态类型
│       └── utils                         // 工具类
│       └── playedu.d.ts                  // 全局类型
│       └── AutoTop.ts                    // 全局页面自动滚动到最上方
│       └── custom.d.ts                   // 全局css、less、scss配置
│       └── index.less                    // 全局样式
│       └── main.tsx                      // 主要模块
│       └── App.module.less               // 系统样式
│       └── App.tsx                       // 系统模块
├── .env.example                          // Env配置模版
├── .env.local                            // Env配置
├── .env(可用)                           // Env配置
├── vite.config.ts                        // VITE配置
├── ...                      

4.1.2 依赖库

依赖库地址说明
antdhttps://ant.designUI框架
axioshttps://axios-http.com/docs/introHTTP请求库
echartshttps://echarts.apache.org/zh/index.html图表渲染库
i18nexthttps://www.i18next.com国际化
momenthttps://momentjs.cn时间解析库
ahookshttps://ahooks.js.org/zh-CN/React Hooks
localforagehttps://localforage.docschina.orgLocalStorage的封装
match-sorterhttps://github.com/kentcdodds/match-sorter数组处理库
react-router-domhttps://reactrouter.com/en/main路由库
reduxhttps://cn.redux.js.org全局状态
sort-byhttps://github.com/kvnneff/sort-by数组加强库
xlsxhttps://github.com/SheetJS/sheetjsExcel操作

4.1.3 页面架构图

4.1.4 全局状态

4.1.4.1 全局状态一、登录用户
src/store/user/loginUserSlice.ts
type UserInterface = {
  id: number;
  name: string;
  email: string;
};

type UserStoreInterface = {
  user: UserInterface | null;
  isLogin: boolean;
  permissions: string[];
  uploadStatus: boolean;
  uploadCateIds: number[];
};
import { useSelector } from "react-redux";

// 读取登录用户信息
const user = useSelector((state: any) => state.loginUser.value.user);

// 读取登录用户的关联权限
const user = useSelector((state: any) => state.loginUser.value.permissions);
import { useDispatch } from "react-redux";
import { loginAction } from "./store/user/loginUserSlice";

const dispatch = useDispatch();

// 获取用户数据
let res: any = await loginApi.getUser();
// 将用户数据存储到全局状态
dispatch(loginAction(res.data));

4.1.4.2 全局状态二、全局配置

src/store/system/systemConfigSlice.ts
type SystemConfigStoreInterface = {
  synchEnabled?: boolean;
  synchScene?: number;
  systemLanguage?: string;
  systemApiUrl?: string;
  systemPcUrl?: string;
  systemH5Url?: string;
  systemLogo?: string;
  systemName?: string;
  memberDefaultAvatar?: string;
  resourceUrl?: {
    [key: number]: string;
  };
};
import { useSelector } from "react-redux";

const systemConfig = useSelector((state: any) => state.systemConfig.value);
import { useDispatch } from "react-redux";
import {
  SystemConfigStoreInterface,
  saveConfigAction,
} from "../../store/system/systemConfigSlice";

let config: SystemConfigStoreInterface = {...};
dispatch(saveConfigAction(config));

4.1.5 后台权限概述

4.1.5.1按钮权限组件
文件:src/components/permission-button
使用权限按钮组件(demo
import { PerButton } from "@/components/permission-button";
import { PlusOutlined } from "@ant-design/icons";

return (
  <>
    <PerButton
       type="primary"
       text={"测试按钮"}
       class="demo-button"
       icon={<PlusOutlined />}//或者null
       p="upload-word"  //该按钮权限 
       onClick={() => void()}
       disabled={true|false}//或者null
      />
    </>
 )
4.1.5.2 左侧菜单组件
文件:src/components/left-menu
import React, { useEffect, useState } from "react";
import { Menu } from "antd";
import { useSelector } from "react-redux";
import { useNavigate, useLocation } from "react-router-dom";

export const LeftMenu: React.FC = () => {
  const location = useLocation();
  // 全局权限
  const permissions = useSelector(
    (state: any) => state.loginUser.value.permissions
  );
  // 选中的菜单
  const [selectedKeys, setSelectedKeys] = useState<string[]>([
    location.pathname,
  ]);

  // 展开菜单
  const hit = (pathname: string): string[] => {
    for (let p in children2Parent) {
      if (pathname.search(p) >= 0) {
        return children2Parent[p];
      }
    }
    return [];
  };
  const [openKeys, setOpenKeys] = useState<string[]>(hit(location.pathname));
  const items = [
      getItem(
        "主页",
        "/",
        <i className={`iconfont icon-icon-home`} />,
        null,
        null,
        "menu-index"  //该菜单权限 
      ),
      getItem(
        "分类",
        "/resource-category",
        <i className="iconfont icon-icon-category" />,
        null,
        null,
        "menu-category"  //该菜单权限 
      )
      // ...
   ]
   
   // ...省略代码
    
     return (
       <div className={styles["menu-box"]}>
          <Menu
            onClick={onClick}
            style={{
              width: 200,
              background: "#ffffff",
            }}
            selectedKeys={selectedKeys}
            openKeys={openKeys}
            mode="inline"
            items={activeMenus}
            onSelect={(data: any) => {
              setSelectedKeys(data.selectedKeys);
            }}
            onOpenChange={(keys: any) => {
              setOpenKeys(keys);
            }}
          />
        </div>
       )
    
 }

4.2 PC学员界面

4.2.1文件目录结构

playedu 
├── public                                // 公共资源
│       └── js                            // 公共JS
│       └── favicon.ico                   // 网站ICO
│
├── src                                   // 资源
│       └── api                           // 各页面API
│           └── internal                  // 全局API配置
│           └──...                        // 各模块API配置
│       └── compenents                    // 全局组件
│       └── assets                        // 全局外部资源
│       └── i18n                          // 国际化配置
│           └── cn.json                   // 简中语言JSON
│           └── config.ts                 // 国际化配置
│           └── tc.json                   // 繁中语言JSON
│       └── js                            // 通用JS
│       └── pages                         // 页面资源
│           └── cert                      // 证书模块
│           └── course                    // 线上课模块
│           └── index                     // 首页模块
│           └── error                     // 错误模块
│           └── init                      // 初始化
│           └── layouts                   // 页面布局
│           └── loading                   // 公用loading模块
│           └── login                     // 登录模块
│           └── task                      // 任务模块
│                └── index.tsx            // 学习任务模块
│                └── exam.tsx             // 考试任务模块
│                └── ...                  // 任务相关模块
│           └── latest-learn              // 最近学习模块
│       └── routes                        // 路由
│       └── store                         // 中间件及store
│           └── system                    // 系统store
│           └── user                      // 用户store
│           └── index.ts                  // store配置
│       └── utils                         // 工具类
│       └── global.d.ts                   // 全局变量
│       └── playedu.d.ts                  // 全局类型
│       └── AutoTop.ts                    // 全局页面自动滚动到最上方
│       └── custom.d.ts                   // 全局css、less、scss配置
│       └── index.scss                    // 全局样式
│       └── main.tsx                      // 主要模块
│       └── App.scss                      // 系统样式
│       └── App.tsx                       // 系统模块
├── .env.example                          // Env配置模版
├── .env.local                            // Env配置
├── .env(可用)                           // Env配置
├── vite.config.ts                        // VITE配置
├── ...                      

4.2.2 依赖库

依赖库地址说明
antdhttps://ant.designUI框架
axioshttps://axios-http.com/docs/introHTTP请求库
jspdfhttps://github.com/parallax/jsPDF生成PDF文档
html2canvashttps://html2canvas.hertzen.com/html内容写入canvas生成图片
lodashhttps://www.lodashjs.com/Lodash
i18nexthttps://www.i18next.com国际化
momenthttps://momentjs.cn时间解析库
localforagehttps://localforage.docschina.orgLocalStorage的封装
match-sorterhttps://github.com/kentcdodds/match-sorter数组处理库
react-router-domhttps://reactrouter.com/en/main路由库
reduxhttps://cn.redux.js.org全局状态
sort-byhttps://github.com/kvnneff/sort-by数组加强库
react-gahttps://github.com/react-ga/react-ga反应谷歌分析模块

4.2.3 页面架构图

4.2.4 全局状态

4.2.4.1 全局状态一、登录用户
src/store/user/loginUserSlice.ts
type ResourceUrlModel = {
   [key: number]: string;
};

type UserModel = {
    id: number;
    name: string;
    avatar: string;
    credit1: number;
    email: string;
    create_city: string;
    create_ip: string;
    id_card: string;
    is_active: number;
    is_lock: number;
    from_scene: number;
    is_set_password: number;
    is_verify: number;
    created_at: string;
    updated_at: string;
    login_at?: string;
    verify_at?: string;
};

type UserStoreInterface = {
   user: UserModel | null;
   departments: string[];
   currentDepId: number;
   resourceUrl: ResourceUrlModel;
   isLogin: boolean;
};
import { useSelector } from "react-redux";

// 读取登录用户信息
const user = useSelector((state: any) => state.loginUser.value.user);

// 读取登录用户的部门
const departments = useSelector((state: any) => state.loginUser.value.departments);

// 读取登录用户的资源地址
const resourceUrl = useSelector((state: any) => state.loginUser.value.resourceUrl);

// 读取登录用户的登录状态
const isLogin = useSelector((state: any) => state.loginUser.value.isLogin);

// 读取登录用户的默认当前部门ID
const currentDepId = useSelector((state: any) => state.loginUser.value.currentDepId);
import { useDispatch } from "react-redux";
import { loginAction } from "./store/user/loginUserSlice";

const dispatch = useDispatch();

// 获取用户数据
let userRes: any = await user.detail();

// 将用户数据存储到全局状态
dispatch(loginAction(userRes));

4.2.4.2 全局状态二、全局配置

src/store/system/systemConfigSlice.ts
type SystemConfigStoreInterface = {
  synchEnabled: boolean;
  synchScene: number;
  synchWorkAgentId: string;
  synchWorkCorpId: string;
  synchWorkCorpSecret: string;
  synchWorkUrl: string;
  systemApiUrl: string;
  systemLanguage?: string;
  systemPcUrl: string;
  systemH5Url: string;
  systemLogo: string;
  systemName: string;
  pcIndexFooterMsg: string;
  playerPoster: string;
  playerIsEnabledBulletSecret: boolean;
  playerIsDisabledDrag: boolean;
  playerBulletSecretText: string;
  playerBulletSecretColor: string;
  playerBulletSecretOpacity: string;
  resourceUrl?: {
    [key: number]: string;
  };
};
import { useSelector } from "react-redux";

const systemConfig = useSelector((state: any) => state.systemConfig.value);
import { useDispatch } from "react-redux";
import {
  SystemConfigStoreInterface,
  saveConfigAction,
} from "../../store/system/systemConfigSlice";

let configRes: any = await system.config();

let config: SystemConfigStoreInterface = {
        //系统配置
        synchEnabled: configRes["synch.enabled"],
        synchScene: configRes["synch.scene"],
        synchWorkAgentId: configRes["synch.agentid"],
        synchWorkCorpId: configRes["synch.corpid"],
        synchWorkCorpSecret: configRes["synch.corpsecret"],
        synchWorkUrl: configRes["synch.url"],
        systemApiUrl: configRes["system-api-url"],
        systemLanguage: configRes["system.language"],
        systemH5Url: configRes["system-h5-url"],
        systemLogo: configRes["resource_url"][
          Number(configRes["system-logo"])
        ]
          ? configRe["resource_url"][
              Number(configRes["system-logo"])
            ]
          : "",
        systemName: configRes["system-name"],
        systemPcUrl: configRes["system-pc-url"],
        pcIndexFooterMsg: configRes["system-pc-index-footer-msg"],
        //播放器配置
        playerPoster: configRes["resource_url"][
          Number(configRes["player-poster"])
        ]
          ? configRes["resource_url"][
              Number(configRes["player-poster"])
            ]
          : "",
        playerIsEnabledBulletSecret:
          configRes["player-is-enabled-bullet-secret"] &&
          configRes["player-is-enabled-bullet-secret"] === "1"
            ? true
            : false,
        playerIsDisabledDrag:
          configRes["player-disabled-drag"] &&
          configRes["player-disabled-drag"] === "1"
            ? true
            : false,
        playerBulletSecretText: configRes["player-bullet-secret-text"],
        playerBulletSecretColor: configRes["player-bullet-secret-color"],
        playerBulletSecretOpacity:
          configRes["player-bullet-secret-opacity"],
        resourceUrl: configRes["resource_url"],
  };
 i18n.changeLanguage(configRes["system.language"]);
 // 将系统数据存储到全局状态
 dispatch(saveConfigAction(config));

4.3 H5学员界面

4.3.1文件目录结构

playedu 
├── public                                // 公共资源
│       └── js                            // 公共JS
│       └── favicon.ico                   // 网站ICO
│
├── src                                   // 资源
│       └── api                           // 各页面API
│           └── internal                  // 全局API配置
│           └──...                        // 各模块API配置
│       └── compenents                    // 全局组件
│       └── assets                        // 全局外部资源
│       └── i18n                          // 国际化配置
│           └── cn.json                   // 简中语言JSON
│           └── config.ts                 // 国际化配置
│           └── tc.json                   // 繁中语言JSON
│       └── js                            // 通用JS
│       └── pages                         // 页面资源
│           └── cert                      // 证书模块
│           └── change-department         // 切换部门模块
│           └── change-password           // 修改密码模块
│           └── course                    // 线上课模块
│           └── index                     // 首页模块
│           └── error                     // 错误模块
│           └── init                      // 初始化
│           └── layouts                   // 页面布局
│           └── loading                   // 公用loading模块
│           └── login                     // 登录模块
│           └── member                    // 学员中心模块
│           └── task                      // 任务模块
│                └── index.tsx            // 学习任务模块
│                └── exam.tsx             // 考试任务模块
│                └── ...                  // 任务相关模块
│           └── study                     // 最近学习模块
│           └── upload                    // PC扫码、H5的考试上传模块
│       └── routes                        // 路由
│       └── store                         // 中间件及store
│           └── system                    // 系统store
│           └── user                      // 用户store
│           └── index.ts                  // store配置
│       └── utils                         // 工具类
│       └── global.d.ts                   // 全局变量
│       └── playedu.d.ts                  // 全局类型
│       └── AutoTop.ts                    // 全局页面自动滚动到最上方
│       └── custom.d.ts                   // 全局css、less、scss配置
│       └── main.scss                     // 全局样式
│       └── main.tsx                      // 主要模块
│       └── App.scss                      // 系统样式
│       └── App.tsx                       // 系统模块
├── .env.example                          // Env配置模版
├── .env.local                            // Env配置
├── .env(可用)                           // Env配置
├── vite.config.ts                        // VITE配置
├── ...                      

4.3.2 依赖库

依赖库地址说明
antd-mobilehttps://ant-design-mobile.antgroup.com/zhUI框架
axioshttps://axios-http.com/docs/introHTTP请求库
jspdfhttps://github.com/parallax/jsPDF生成PDF文档
html2canvashttps://html2canvas.hertzen.com/html内容写入canvas生成图片
@types/lodashhttps://www.lodashjs.com/Lodash
i18nexthttps://www.i18next.com国际化
momenthttps://momentjs.cn时间解析库
xgplayerhttps://h5player.bytedance.com播放器
xgplayer-hls-播放器HLS
localforagehttps://localforage.docschina.orgLocalStorage的封装
match-sorterhttps://github.com/developit/mitt数组处理库
mitthttps://github.com/kentcdodds/match-sorterMitt事件库
react-router-domhttps://reactrouter.com/en/main路由库
reduxhttps://cn.redux.js.org全局状态
sort-byhttps://github.com/kvnneff/sort-by数组加强库
react-gahttps://github.com/react-ga/react-ga反应谷歌分析模块

4.3.3 页面架构图


4.3.4 全局状态

4.3.4.1 全局状态一、登录用户
src/store/user/loginUserSlice.ts
type ResourceUrlModel = {
   [key: number]: string;
};

type UserModel = {
    id: number;
    name: string;
    avatar: string;
    credit1: number;
    email: string;
    create_city: string;
    create_ip: string;
    id_card: string;
    is_active: number;
    is_lock: number;
    from_scene: number;
    is_set_password: number;
    is_verify: number;
    created_at: string;
    updated_at: string;
    login_at?: string;
    verify_at?: string;
};

type UserStoreInterface = {
   user: UserModel | null;
   departments: string[];
   currentDepId: number;
   resourceUrl: ResourceUrlModel;
   isLogin: boolean;
};
import { useSelector } from "react-redux";

// 读取登录用户信息
const user = useSelector((state: any) => state.loginUser.value.user);

// 读取登录用户的部门
const departments = useSelector((state: any) => state.loginUser.value.departments);

// 读取登录用户的资源地址
const resourceUrl = useSelector((state: any) => state.loginUser.value.resourceUrl);

// 读取登录用户的登录状态
const isLogin = useSelector((state: any) => state.loginUser.value.isLogin);

// 读取登录用户的默认当前部门ID
const currentDepId = useSelector((state: any) => state.loginUser.value.currentDepId);
import { useDispatch } from "react-redux";
import { loginAction } from "./store/user/loginUserSlice";

const dispatch = useDispatch();

// 获取用户数据
let userRes: any = await user.detail();

// 将用户数据存储到全局状态
dispatch(loginAction(userRes));

4.3.4.2 全局状态二、全局配置

src/store/system/systemConfigSlice.ts
type SystemConfigStoreInterface = {
  synchEnabled: boolean;
  synchScene: number;
  synchWorkAgentId: string;
  synchWorkCorpId: string;
  synchWorkCorpSecret: string;
  synchWorkUrl: string;
  systemApiUrl: string;
  systemLanguage?: string;
  systemPcUrl: string;
  systemH5Url: string;
  systemLogo: string;
  systemName: string;
  pcIndexFooterMsg: string;
  playerPoster: string;
  playerIsEnabledBulletSecret: boolean;
  playerIsDisabledDrag: boolean;
  playerBulletSecretText: string;
  playerBulletSecretColor: string;
  playerBulletSecretOpacity: string;
  resourceUrl?: {
    [key: number]: string;
  };
};
import { useSelector } from "react-redux";

const systemConfig = useSelector((state: any) => state.systemConfig.value);
import { useDispatch } from "react-redux";
import {
  SystemConfigStoreInterface,
  saveConfigAction,
} from "../../store/system/systemConfigSlice";

let configRes: any = await system.config();

let config: SystemConfigStoreInterface = {
        //系统配置
        synchEnabled: configRes["synch.enabled"],
        synchScene: configRes["synch.scene"],
        synchWorkAgentId: configRes["synch.agentid"],
        synchWorkCorpId: configRes["synch.corpid"],
        synchWorkCorpSecret: configRes["synch.corpsecret"],
        synchWorkUrl: configRes["synch.url"],
        systemApiUrl: configRes["system-api-url"],
        systemLanguage: configRes["system.language"],
        systemH5Url: configRes["system-h5-url"],
        systemLogo: configRes["resource_url"][
          Number(configRes["system-logo"])
        ]
          ? configRe["resource_url"][
              Number(configRes["system-logo"])
            ]
          : "",
        systemName: configRes["system-name"],
        systemPcUrl: configRes["system-pc-url"],
        pcIndexFooterMsg: configRes["system-pc-index-footer-msg"],
        //播放器配置
        playerPoster: configRes["resource_url"][
          Number(configRes["player-poster"])
        ]
          ? configRes["resource_url"][
              Number(configRes["player-poster"])
            ]
          : "",
        playerIsEnabledBulletSecret:
          configRes["player-is-enabled-bullet-secret"] &&
          configRes["player-is-enabled-bullet-secret"] === "1"
            ? true
            : false,
        playerIsDisabledDrag:
          configRes["player-disabled-drag"] &&
          configRes["player-disabled-drag"] === "1"
            ? true
            : false,
        playerBulletSecretText: configRes["player-bullet-secret-text"],
        playerBulletSecretColor: configRes["player-bullet-secret-color"],
        playerBulletSecretOpacity:
          configRes["player-bullet-secret-opacity"],
        resourceUrl: configRes["resource_url"],
  };
 i18n.changeLanguage(configRes["system.language"]);
 // 将系统数据存储到全局状态
 dispatch(saveConfigAction(config));