mirror of
https://github.com/PlayEdu/h5.git
synced 2025-02-05 18:18:29 +08:00
项目初始化
This commit is contained in:
commit
87f82475aa
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
||||
VITE_APP_URL=
|
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
.env.production
|
||||
.env.development
|
||||
.env
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"antd-mobile": "^5.31.1",
|
||||
"axios": "^1.3.4",
|
||||
"localforage": "^1.10.0",
|
||||
"match-sorter": "^6.3.1",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-ga": "^3.3.1",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"redux": "^4.2.1",
|
||||
"sort-by": "^1.2.0",
|
||||
"web-vitals": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||
"rollup-plugin-gzip": "^3.1.0",
|
||||
"sass": "^1.59.3",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.2.0"
|
||||
}
|
||||
}
|
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
6
src/App.scss
Normal file
6
src/App.scss
Normal file
@ -0,0 +1,6 @@
|
||||
#root {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
background-color: #ffffff;
|
||||
}
|
31
src/App.tsx
Normal file
31
src/App.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Suspense, useEffect } from "react";
|
||||
import ReactGA from "react-ga";
|
||||
import { useLocation, useRoutes } from "react-router-dom";
|
||||
import routes from "./routes";
|
||||
import "./App.scss";
|
||||
import LoadingPage from "./pages/loading";
|
||||
|
||||
const G_ID = import.meta.env.VITE_G_ID || "";
|
||||
if (G_ID) {
|
||||
ReactGA.initialize(G_ID);
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const Views = () => useRoutes(routes);
|
||||
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
if (!G_ID) {
|
||||
return;
|
||||
}
|
||||
ReactGA.pageview(location.pathname + location.search);
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingPage />}>
|
||||
<Views />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
13
src/AutoTop.ts
Normal file
13
src/AutoTop.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { useLayoutEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const AutoScorllTop: React.FC<{ children: any }> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
useLayoutEffect(() => {
|
||||
document.documentElement.scrollTo(0, 0);
|
||||
}, [location.pathname]);
|
||||
return children;
|
||||
};
|
||||
|
||||
export default AutoScorllTop;
|
33
src/api/course.ts
Normal file
33
src/api/course.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
// 线上课详情
|
||||
export function detail(id: number) {
|
||||
return client.get(`/api/v1/course/${id}`, {});
|
||||
}
|
||||
|
||||
// 线上课课时详情
|
||||
export function play(courseId: number, id: number) {
|
||||
return client.get(`/api/v1/course/${courseId}/hour/${id}`, {});
|
||||
}
|
||||
|
||||
// 获取播放地址
|
||||
export function playUrl(courseId: number, hourId: number) {
|
||||
return client.get(`/api/v1/course/${courseId}/hour/${hourId}/play`, {});
|
||||
}
|
||||
|
||||
// 记录学员观看时长
|
||||
export function record(courseId: number, hourId: number, duration: number) {
|
||||
return client.post(`/api/v1/course/${courseId}/hour/${hourId}/record`, {
|
||||
duration,
|
||||
});
|
||||
}
|
||||
|
||||
//观看ping
|
||||
export function playPing(courseId: number, hourId: number) {
|
||||
return client.post(`/api/v1/course/${courseId}/hour/${hourId}/ping`, {});
|
||||
}
|
||||
|
||||
//最近学习课程
|
||||
export function latestLearn() {
|
||||
return client.get(`/api/v1/user/latest-learn`, {});
|
||||
}
|
4
src/api/index.ts
Normal file
4
src/api/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * as login from "./login";
|
||||
export * as user from "./user";
|
||||
export * as course from "./course";
|
||||
export * as system from "./system";
|
143
src/api/internal/httpClient.ts
Normal file
143
src/api/internal/httpClient.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import axios, { Axios, AxiosResponse } from "axios";
|
||||
import { Toast } from "antd-mobile";
|
||||
import { getToken, clearToken } from "../../utils/index";
|
||||
|
||||
const GoLogin = () => {
|
||||
clearToken();
|
||||
window.location.href = "/login";
|
||||
};
|
||||
|
||||
export class HttpClient {
|
||||
axios: Axios;
|
||||
|
||||
constructor(url: string) {
|
||||
this.axios = axios.create({
|
||||
baseURL: url,
|
||||
timeout: 15000,
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
//拦截器注册
|
||||
this.axios.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = getToken();
|
||||
token && (config.headers.Authorization = "Bearer " + token);
|
||||
return config;
|
||||
},
|
||||
(err) => {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
this.axios.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
let code = response.data.code; //业务返回代码
|
||||
let msg = response.data.msg; //错误消息
|
||||
|
||||
if (code === 0) {
|
||||
return Promise.resolve(response);
|
||||
} else {
|
||||
Toast.show({
|
||||
icon: "fail",
|
||||
content: msg,
|
||||
});
|
||||
}
|
||||
return Promise.reject(response);
|
||||
},
|
||||
// 当http的状态码非0
|
||||
(error) => {
|
||||
let status = error.response.status;
|
||||
if (status === 401) {
|
||||
Toast.show({
|
||||
icon: "fail",
|
||||
content: "请重新登录",
|
||||
});
|
||||
GoLogin();
|
||||
} else if (status === 404) {
|
||||
// 跳转到404页面
|
||||
} else if (status === 403) {
|
||||
// 跳转到无权限页面
|
||||
} else if (status === 500) {
|
||||
// 跳转到500异常页面
|
||||
}
|
||||
return Promise.reject(error.response);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get(url: string, params: object) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axios
|
||||
.get(url, {
|
||||
params: params,
|
||||
})
|
||||
.then((res) => {
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
destroy(url: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axios
|
||||
.delete(url)
|
||||
.then((res) => {
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
post(url: string, params: object) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axios
|
||||
.post(url, params)
|
||||
.then((res) => {
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
put(url: string, params: object) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axios
|
||||
.put(url, params)
|
||||
.then((res) => {
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
request(config: object) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axios
|
||||
.request(config)
|
||||
.then((res) => {
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const APP_URL = import.meta.env.VITE_APP_URL || "";
|
||||
|
||||
const client = new HttpClient(APP_URL);
|
||||
|
||||
export default client;
|
19
src/api/login.ts
Normal file
19
src/api/login.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function login(
|
||||
email: string,
|
||||
password: string,
|
||||
captchaKey: string,
|
||||
captchaVal: string
|
||||
) {
|
||||
return client.post("/api/v1/auth/login/password", {
|
||||
email: email,
|
||||
password: password,
|
||||
captcha_key: captchaKey,
|
||||
captcha_val: captchaVal,
|
||||
});
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return client.post("/api/v1/auth/logout", {});
|
||||
}
|
9
src/api/system.ts
Normal file
9
src/api/system.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function config() {
|
||||
return client.get("/api/v1/system/config", {});
|
||||
}
|
||||
|
||||
export function imageCaptcha() {
|
||||
return client.get("/api/v1/system/image-captcha", {});
|
||||
}
|
31
src/api/user.ts
Normal file
31
src/api/user.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function detail() {
|
||||
return client.get("/api/v1/user/detail", {});
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
export function password(oldPassword: string, newPassword: string) {
|
||||
return client.put("/api/v1/user/password", {
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
});
|
||||
}
|
||||
|
||||
// 学员课程
|
||||
export function coursesCategories() {
|
||||
return client.get("/api/v1/category/all", {});
|
||||
}
|
||||
export function courses(depId: number, categoryId: number) {
|
||||
return client.get("/api/v1/user/courses", {
|
||||
dep_id: depId,
|
||||
category_id: categoryId,
|
||||
});
|
||||
}
|
||||
|
||||
// 修改头像
|
||||
export function avatar(file: any) {
|
||||
return client.put("/api/v1/user/avatar", {
|
||||
file: file,
|
||||
});
|
||||
}
|
4
src/components/empty/index.module.scss
Normal file
4
src/components/empty/index.module.scss
Normal file
@ -0,0 +1,4 @@
|
||||
.img-box {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
12
src/components/empty/index.tsx
Normal file
12
src/components/empty/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import styles from "./index.module.scss";
|
||||
import React from "react";
|
||||
import { Image } from "antd-mobile";
|
||||
import empty from "../../assets/images/commen/empty.png";
|
||||
|
||||
export const Empty: React.FC = () => {
|
||||
return (
|
||||
<div className={styles["img-box"]}>
|
||||
<Image src={empty} width={400} height={400} />
|
||||
</div>
|
||||
);
|
||||
};
|
1
src/components/index.ts
Normal file
1
src/components/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./empty"
|
12
src/components/private-route/index.tsx
Normal file
12
src/components/private-route/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import { getToken } from "../../utils/index";
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
interface PropInterface {
|
||||
Component: any;
|
||||
}
|
||||
|
||||
const PrivateRoute: React.FC<PropInterface> = ({ Component }) => {
|
||||
return getToken() ? Component : <Navigate to="/login" replace={true} />;
|
||||
};
|
||||
export default PrivateRoute;
|
17
src/index.tsx
Normal file
17
src/index.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "./store";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import "./index.scss";
|
||||
import App from "./App";
|
||||
import AutoScorllTop from "./AutoTop";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<AutoScorllTop>
|
||||
<App />
|
||||
</AutoScorllTop>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
3
src/js/config.ts
Normal file
3
src/js/config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default {
|
||||
app_url: import.meta.env.VITE_APP_URL || "",
|
||||
};
|
30
src/main.scss
Normal file
30
src/main.scss
Normal file
@ -0,0 +1,30 @@
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
:root {
|
||||
--adm-color-primary: #FF4D4F;
|
||||
--adm-color-success: #00b578;
|
||||
--adm-color-warning: #ff8f1f;
|
||||
--adm-color-danger: #ff3141;
|
||||
|
||||
--adm-color-white: #ffffff;
|
||||
--adm-color-text: #333333;
|
||||
--adm-color-text-secondary: #666666;
|
||||
--adm-color-weak: #999999;
|
||||
--adm-color-light: #cccccc;
|
||||
--adm-color-border: #eeeeee;
|
||||
--adm-color-box: #f5f5f5;
|
||||
--adm-color-background: #ffffff;
|
||||
|
||||
--adm-font-size-main: var(--adm-font-size-5);
|
||||
|
||||
--adm-font-family: -apple-system, blinkmacsystemfont, 'Helvetica Neue',
|
||||
helvetica, segoe ui, arial, roboto, 'PingFang SC', 'miui',
|
||||
'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
|
||||
}
|
17
src/main.tsx
Normal file
17
src/main.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "./store";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import "./main.scss";
|
||||
import App from "./App";
|
||||
import AutoScorllTop from "./AutoTop";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<AutoScorllTop>
|
||||
<App />
|
||||
</AutoScorllTop>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
2
src/pages/index.ts
Normal file
2
src/pages/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './login';
|
||||
export * from './layout';
|
0
src/pages/index/index.module.scss
Normal file
0
src/pages/index/index.module.scss
Normal file
28
src/pages/index/index.tsx
Normal file
28
src/pages/index/index.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { user } from "../../api/index";
|
||||
import styles from "./index.module.scss";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const IndexPage = () => {
|
||||
const systemConfig = useSelector((state: any) => state.systemConfig.value);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [tabKey, setTabKey] = useState(0);
|
||||
const departments = useSelector(
|
||||
(state: any) => state.loginUser.value.departments
|
||||
);
|
||||
const currentDepId = useSelector(
|
||||
(state: any) => state.loginUser.value.currentDepId
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = systemConfig.systemName || "首页";
|
||||
}, [systemConfig]);
|
||||
|
||||
return (
|
||||
<div className="main-body">
|
||||
<div className="content">我是首页</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndexPage;
|
66
src/pages/init/index.tsx
Normal file
66
src/pages/init/index.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import {
|
||||
SystemConfigStoreInterface,
|
||||
saveConfigAction,
|
||||
} from "../../store/system/systemConfigSlice";
|
||||
import { loginAction } from "../../store/user/loginUserSlice";
|
||||
import { useParams, useLocation } from "react-router-dom";
|
||||
|
||||
interface Props {
|
||||
loginData?: any;
|
||||
configData?: any;
|
||||
}
|
||||
|
||||
export const InitPage = (props: Props) => {
|
||||
const pathname = useLocation().pathname;
|
||||
const params = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const [init, setInit] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.loginData) {
|
||||
dispatch(loginAction(props.loginData));
|
||||
}
|
||||
if (props.configData) {
|
||||
let config: SystemConfigStoreInterface = {
|
||||
//系统配置
|
||||
systemApiUrl: props.configData["system-api-url"],
|
||||
systemH5Url: props.configData["system-h5-url"],
|
||||
systemLogo: props.configData["system-logo"],
|
||||
systemName: props.configData["system-name"],
|
||||
systemPcUrl: props.configData["system-pc-url"],
|
||||
pcIndexFooterMsg: props.configData["system-pc-index-footer-msg"],
|
||||
//播放器配置
|
||||
playerPoster: props.configData["player-poster"],
|
||||
playerIsEnabledBulletSecret:
|
||||
props.configData["player-is-enabled-bullet-secret"] &&
|
||||
props.configData["player-is-enabled-bullet-secret"] === "1"
|
||||
? true
|
||||
: false,
|
||||
playerIsDisabledDrag:
|
||||
props.configData["player-disabled-drag"] &&
|
||||
props.configData["player-disabled-drag"] === "1"
|
||||
? true
|
||||
: false,
|
||||
playerBulletSecretText: props.configData["player-bullet-secret-text"],
|
||||
playerBulletSecretColor: props.configData["player-bullet-secret-color"],
|
||||
playerBulletSecretOpacity:
|
||||
props.configData["player-bullet-secret-opacity"],
|
||||
};
|
||||
dispatch(saveConfigAction(config));
|
||||
}
|
||||
setInit(true);
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{init && (
|
||||
<div>
|
||||
<Outlet />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
11
src/pages/loading/index.tsx
Normal file
11
src/pages/loading/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { DotLoading } from 'antd-mobile'
|
||||
|
||||
const LoadingPage = () => {
|
||||
return (
|
||||
<>
|
||||
<DotLoading color='primary' />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingPage;
|
0
src/pages/login/index.module.scss
Normal file
0
src/pages/login/index.module.scss
Normal file
40
src/pages/login/index.tsx
Normal file
40
src/pages/login/index.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Button } from "antd-mobile";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { loginAction, logoutAction } from "../../store/user/loginUserSlice";
|
||||
|
||||
const LoginPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const loginState = useSelector((state: any) => {
|
||||
return state.loginUser.value;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
loginAction({
|
||||
user: {
|
||||
name: "霸王",
|
||||
},
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
登录吧
|
||||
</Button>
|
||||
|
||||
{loginState.isLogin && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch(logoutAction());
|
||||
}}
|
||||
>
|
||||
{loginState.user.name}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
15
src/reportWebVitals.ts
Normal file
15
src/reportWebVitals.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
49
src/routes/index.tsx
Normal file
49
src/routes/index.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { lazy } from "react";
|
||||
import { RouteObject } from "react-router-dom";
|
||||
import { system, user } from "../api";
|
||||
|
||||
import { getToken } from "../utils";
|
||||
import { InitPage } from "../pages/init";
|
||||
import IndexPage from "../pages/index/index";
|
||||
import LoginPage from "../pages/login";
|
||||
import PrivateRoute from "../components/private-route";
|
||||
|
||||
let RootPage: any = null;
|
||||
if (getToken()) {
|
||||
RootPage = lazy(async () => {
|
||||
return new Promise<any>(async (resolve) => {
|
||||
try {
|
||||
let configRes: any = await system.config();
|
||||
let userRes: any = await user.detail();
|
||||
resolve({
|
||||
default: (
|
||||
<InitPage configData={configRes.data} loginData={userRes.data} />
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("系统初始化失败", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
RootPage = <InitPage />;
|
||||
}
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
{
|
||||
path: "/",
|
||||
element: RootPage,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <IndexPage />,
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
12
src/store/index.ts
Normal file
12
src/store/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import systemConfigReducer from "./system/systemConfigSlice";
|
||||
import loginUserReducer from "./user/loginUserSlice";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
loginUser: loginUserReducer,
|
||||
systemConfig: systemConfigReducer,
|
||||
},
|
||||
});
|
||||
|
||||
export default store;
|
48
src/store/system/systemConfigSlice.ts
Normal file
48
src/store/system/systemConfigSlice.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
type SystemConfigStoreInterface = {
|
||||
systemApiUrl: string;
|
||||
systemPcUrl: string;
|
||||
systemH5Url: string;
|
||||
systemLogo: string;
|
||||
systemName: string;
|
||||
pcIndexFooterMsg: string;
|
||||
playerPoster: string;
|
||||
playerIsEnabledBulletSecret: boolean;
|
||||
playerIsDisabledDrag: boolean;
|
||||
playerBulletSecretText: string;
|
||||
playerBulletSecretColor: string;
|
||||
playerBulletSecretOpacity: string;
|
||||
};
|
||||
|
||||
let defaultValue: SystemConfigStoreInterface = {
|
||||
systemApiUrl: "",
|
||||
systemPcUrl: "",
|
||||
systemH5Url: "",
|
||||
systemLogo: "",
|
||||
systemName: "",
|
||||
pcIndexFooterMsg: "",
|
||||
playerPoster: "",
|
||||
playerIsEnabledBulletSecret: false,
|
||||
playerIsDisabledDrag: false,
|
||||
playerBulletSecretText: "",
|
||||
playerBulletSecretColor: "",
|
||||
playerBulletSecretOpacity: "",
|
||||
};
|
||||
|
||||
const systemConfigSlice = createSlice({
|
||||
name: "systemConfig",
|
||||
initialState: {
|
||||
value: defaultValue,
|
||||
},
|
||||
reducers: {
|
||||
saveConfigAction(stage, e) {
|
||||
stage.value = e.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default systemConfigSlice.reducer;
|
||||
export const { saveConfigAction } = systemConfigSlice.actions;
|
||||
|
||||
export type { SystemConfigStoreInterface };
|
58
src/store/user/loginUserSlice.ts
Normal file
58
src/store/user/loginUserSlice.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import {
|
||||
getDepKey,
|
||||
clearDepKey,
|
||||
clearDepName,
|
||||
setDepName,
|
||||
clearToken,
|
||||
} from "../../utils/index";
|
||||
|
||||
type UserStoreInterface = {
|
||||
user: null;
|
||||
departments: string[];
|
||||
currentDepId: number;
|
||||
isLogin: boolean;
|
||||
};
|
||||
|
||||
let defaultValue: UserStoreInterface = {
|
||||
user: null,
|
||||
departments: [],
|
||||
currentDepId: Number(getDepKey()) || 0,
|
||||
isLogin: false,
|
||||
};
|
||||
|
||||
const loginUserSlice = createSlice({
|
||||
name: "loginUser",
|
||||
initialState: {
|
||||
value: defaultValue,
|
||||
},
|
||||
reducers: {
|
||||
loginAction(stage, e) {
|
||||
stage.value.user = e.payload.user;
|
||||
stage.value.departments = e.payload.departments;
|
||||
stage.value.isLogin = true;
|
||||
if (e.payload.departments.length > 0 && stage.value.currentDepId === 0) {
|
||||
stage.value.currentDepId = e.payload.departments[0].id;
|
||||
setDepName(e.payload.departments[0].name);
|
||||
}
|
||||
},
|
||||
logoutAction(stage) {
|
||||
stage.value.user = null;
|
||||
stage.value.departments = [];
|
||||
stage.value.isLogin = false;
|
||||
stage.value.currentDepId = 0;
|
||||
clearToken();
|
||||
clearDepKey();
|
||||
clearDepName();
|
||||
},
|
||||
saveCurrentDepId(stage, e) {
|
||||
stage.value.currentDepId = e.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default loginUserSlice.reducer;
|
||||
export const { loginAction, logoutAction, saveCurrentDepId } =
|
||||
loginUserSlice.actions;
|
||||
|
||||
export type { UserStoreInterface };
|
52
src/utils/index.ts
Normal file
52
src/utils/index.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import moment from "moment";
|
||||
|
||||
export function getToken(): string {
|
||||
return window.localStorage.getItem("playedu-h5-token") || "";
|
||||
}
|
||||
|
||||
export function setToken(token: string) {
|
||||
window.localStorage.setItem("playedu-h5-token", token);
|
||||
}
|
||||
|
||||
export function clearToken() {
|
||||
window.localStorage.removeItem("playedu-h5-token");
|
||||
}
|
||||
|
||||
export function dateFormat(dateStr: string) {
|
||||
return moment(dateStr).format("YYYY-MM-DD HH:mm");
|
||||
}
|
||||
|
||||
export function getHost() {
|
||||
return window.location.protocol + "//" + window.location.host + "/";
|
||||
}
|
||||
export function getDepKey(): string {
|
||||
return window.localStorage.getItem("playedu-h5-depatmentKey") || "";
|
||||
}
|
||||
|
||||
export function setDepKey(token: string) {
|
||||
window.localStorage.setItem("playedu-h5-depatmentKey", token);
|
||||
}
|
||||
|
||||
export function clearDepKey() {
|
||||
window.localStorage.removeItem("playedu-h5-depatmentKey");
|
||||
}
|
||||
export function getDepName(): string {
|
||||
return window.localStorage.getItem("playedu-h5-depatmentName") || "";
|
||||
}
|
||||
|
||||
export function setDepName(token: string) {
|
||||
window.localStorage.setItem("playedu-h5-depatmentName", token);
|
||||
}
|
||||
|
||||
export function clearDepName() {
|
||||
window.localStorage.removeItem("playedu-frontend-depatmentName");
|
||||
}
|
||||
|
||||
export function changeAppUrl(str: string) {
|
||||
let key = str.slice(str.length - 1);
|
||||
if (key === "/") {
|
||||
return str;
|
||||
} else {
|
||||
return str + "/";
|
||||
}
|
||||
}
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
13
vite.config.ts
Normal file
13
vite.config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import gzipPlugin from "rollup-plugin-gzip";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
plugins: [gzipPlugin()],
|
||||
},
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user