项目初始化

This commit is contained in:
禺狨
2023-06-25 11:25:39 +08:00
commit 87f82475aa
39 changed files with 893 additions and 0 deletions

6
src/App.scss Normal file
View File

@@ -0,0 +1,6 @@
#root {
width: 100%;
margin: 0 auto;
text-align: center;
background-color: #ffffff;
}

31
src/App.tsx Normal file
View 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
View 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
View 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
View 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";

View 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
View 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
View 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
View 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,
});
}

View File

@@ -0,0 +1,4 @@
.img-box {
width: 100%;
height: auto;
}

View 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
View File

@@ -0,0 +1 @@
export * from "./empty"

View 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
View 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
View File

@@ -0,0 +1,3 @@
export default {
app_url: import.meta.env.VITE_APP_URL || "",
};

30
src/main.scss Normal file
View 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
View 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
View File

@@ -0,0 +1,2 @@
export * from './login';
export * from './layout';

View File

28
src/pages/index/index.tsx Normal file
View 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
View 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>
)}
</>
);
};

View File

@@ -0,0 +1,11 @@
import { DotLoading } from 'antd-mobile'
const LoadingPage = () => {
return (
<>
<DotLoading color='primary' />
</>
);
};
export default LoadingPage;

View File

40
src/pages/login/index.tsx Normal file
View 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
View 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
View 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
View 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;

View 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 };

View 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
View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />