feat: message loadin
This commit is contained in:
parent
3514c1e395
commit
a807f2c557
|
@ -30,7 +30,8 @@
|
|||
"@alicloud/openapi-client": "0.4.5",
|
||||
"@alicloud/tea-util": "1.4.5",
|
||||
"@alicloud/tea-typescript": "1.8.0",
|
||||
"object-hash": "3.0.0"
|
||||
"object-hash": "3.0.0",
|
||||
"nanoid": "3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@midwayjs/cli": "^2.0.0",
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { Body, Context, Controller, Inject, Post } from '@midwayjs/core';
|
||||
import { Body, Controller, Inject, Post } from '@midwayjs/core';
|
||||
import { Context } from '@midwayjs/koa';
|
||||
import { BizCode } from '../biz/code';
|
||||
import { webSign } from '../config/base.config';
|
||||
import { CourseCreateDTO } from '../dto/course.dto';
|
||||
import { ChapterService } from '../service/chapter.service';
|
||||
import { CourseService } from '../service/course.service';
|
||||
import { GuideService } from '../service/guide.service';
|
||||
import { UserService } from '../service/user.service';
|
||||
import { decodeToken } from '../util/encrypt';
|
||||
|
||||
@Controller('/course')
|
||||
export class CourseController {
|
||||
|
@ -19,6 +23,9 @@ export class CourseController {
|
|||
@Inject()
|
||||
guideService: GuideService;
|
||||
|
||||
@Inject()
|
||||
userService: UserService;
|
||||
|
||||
@Post('/create')
|
||||
async create(@Body() param: CourseCreateDTO) {
|
||||
try {
|
||||
|
@ -50,11 +57,22 @@ export class CourseController {
|
|||
return { code: BizCode.OK, data: courseList };
|
||||
}
|
||||
|
||||
@Post('/chapter/select')
|
||||
async selectChapterByCourseId(@Body() params) {
|
||||
@Post('/detail/select')
|
||||
async selectDetailByCourseId(@Body() params) {
|
||||
const { course_id } = params;
|
||||
try {
|
||||
const token = this.ctx.cookies.get(webSign);
|
||||
const { user_login } = decodeToken(token);
|
||||
const user = await this.userService.select({ user_login });
|
||||
// 用户订阅鉴权
|
||||
if (!user.user_sub)
|
||||
return { code: BizCode.AUTH, msg: '无权访问订阅课程' };
|
||||
const course = await this.courseService.select({ course_id });
|
||||
const chapterList = await this.chapterService.select(course_id);
|
||||
const guide = await this.guideService.select(course_id);
|
||||
return { code: BizCode.OK, data: { chapterList, guide } };
|
||||
return { code: BizCode.OK, data: { chapterList, guide, course } };
|
||||
} catch (error) {
|
||||
return { code: BizCode.ERROR, msg: '[error] /chapter/select error' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,17 +42,9 @@ export class UserController {
|
|||
if (!verifyCode) return { code: BizCode.ERROR, msg: '验证码无效' };
|
||||
// 查询用户是否存在
|
||||
const userExist = await this.userService.select(params);
|
||||
let payload = {};
|
||||
if (userExist?.id) {
|
||||
const { user_pass, ...rest } = userExist;
|
||||
payload = rest;
|
||||
} else {
|
||||
// 新用户注册
|
||||
const { user_pass, ...rest } = await this.userService.createUser(
|
||||
params
|
||||
);
|
||||
payload = rest;
|
||||
}
|
||||
const payload = userExist?.id
|
||||
? userExist
|
||||
: await this.userService.createUser(params);
|
||||
const token = createToken({ ...payload, hasLogin: true });
|
||||
this.ctx.cookies.set(webSign, token, {
|
||||
expires: new Date(Date.now() + webSignExpired),
|
||||
|
@ -112,7 +104,7 @@ export class UserController {
|
|||
60
|
||||
);
|
||||
console.log('redis here', res);
|
||||
// await this.smsService.send({ code, phoneNumbers });
|
||||
await this.smsService.send({ code, phoneNumbers });
|
||||
return { code: BizCode.OK };
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('course')
|
||||
export class Course {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@PrimaryColumn()
|
||||
course_id?: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
|
|
|
@ -8,9 +8,6 @@ export class User {
|
|||
@Column({ unique: true })
|
||||
user_login?: string;
|
||||
|
||||
@Column()
|
||||
user_pass?: string;
|
||||
|
||||
@Column({ default: '' })
|
||||
user_email?: string;
|
||||
|
||||
|
@ -25,4 +22,7 @@ export class User {
|
|||
|
||||
@Column({ default: '' })
|
||||
user_avatar?: string;
|
||||
|
||||
@Column({ default: false })
|
||||
user_sub?: boolean;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Context, Inject, Provide } from '@midwayjs/core';
|
|||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Course } from '../entity/course.entity';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export interface ICourseCreate {
|
||||
course_title: string;
|
||||
|
@ -18,6 +19,7 @@ export class CourseService {
|
|||
courseModel: Repository<Course>;
|
||||
|
||||
async create(course: Course) {
|
||||
course.course_id = nanoid(13);
|
||||
const courseCreateRes = await this.courseModel.save(course);
|
||||
return courseCreateRes.course_id;
|
||||
}
|
||||
|
@ -25,4 +27,9 @@ export class CourseService {
|
|||
async selectAll() {
|
||||
return await this.courseModel.find({ where: { valid: true } });
|
||||
}
|
||||
|
||||
async select(course: Course) {
|
||||
const { course_id } = course;
|
||||
return await this.courseModel.findOne({ where: { course_id } });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Provide } from '@midwayjs/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UserWebAuthDTO } from '../dto/user.dto';
|
||||
import { User } from '../entity/user.entity';
|
||||
import hash from 'object-hash';
|
||||
|
||||
|
@ -10,7 +9,7 @@ export class UserService {
|
|||
@InjectEntityModel(User)
|
||||
userModel: Repository<User>;
|
||||
|
||||
async select(p: UserWebAuthDTO): Promise<any> {
|
||||
async select(p: User): Promise<User> {
|
||||
const { user_login } = p;
|
||||
const user = await this.userModel.findOne({
|
||||
where: { user_login },
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
"@ricons/utils": "0.1.6",
|
||||
"dplayer": "1.27.1",
|
||||
"highlight.js": "11.7.0",
|
||||
"identicon.js": "2.3.3"
|
||||
"identicon.js": "2.3.3",
|
||||
"react-hot-toast": "2.4.0",
|
||||
"react-spinners": "0.13.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.27",
|
||||
|
|
|
@ -4,6 +4,8 @@ import "./assets/base.less";
|
|||
import Nav from "./components/Nav";
|
||||
import { commonRouters, lazyRouters } from "./router";
|
||||
import { Guard } from "./router/Guard";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import Loading from "./components/Loading";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
|
@ -23,7 +25,7 @@ function App() {
|
|||
key={router.path}
|
||||
path={router.path}
|
||||
element={
|
||||
<Suspense fallback={"loading"}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Guard>{<router.element />}</Guard>
|
||||
</Suspense>
|
||||
}
|
||||
|
@ -32,6 +34,7 @@ function App() {
|
|||
<Route path="*" element={<span>404</span>} />
|
||||
</Routes>
|
||||
</main>
|
||||
<Toaster />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// import { message } from "antd";
|
||||
import axios from "axios";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const config = {
|
||||
baseURL: "",
|
||||
|
@ -7,7 +7,7 @@ const config = {
|
|||
headers: {},
|
||||
};
|
||||
|
||||
const instance = axios.create(config);
|
||||
export const instance = axios.create(config);
|
||||
|
||||
instance.interceptors.request.use(
|
||||
(config) => {
|
||||
|
@ -36,8 +36,7 @@ instance.interceptors.response.use(
|
|||
// Message.error(`接口: ${response.config.url}, 遇到错误`);
|
||||
break;
|
||||
case 40000:
|
||||
console.error(msg);
|
||||
// console.log('登录')
|
||||
toast.error(msg);
|
||||
break;
|
||||
default:
|
||||
// TODO ...
|
|
@ -1,10 +1,10 @@
|
|||
import { ILoginRequest } from "./dto";
|
||||
import R from "./request";
|
||||
import R from "./axios";
|
||||
|
||||
export const getCourseList = () => R.post("/api/course/select/all");
|
||||
|
||||
export const getChapterGuideById = (course_id: string) =>
|
||||
R.post("/api/course/chapter/select", { course_id });
|
||||
export const getCourseDetailById = (course_id: string) =>
|
||||
R.post("/api/course/detail/select", { course_id });
|
||||
|
||||
export const userLogin = (p: ILoginRequest) =>
|
||||
R.post("/api/user/web/auth", { ...p });
|
||||
|
|
11
apps/web/src/components/Loading/index.less
Normal file
11
apps/web/src/components/Loading/index.less
Normal file
|
@ -0,0 +1,11 @@
|
|||
.loading {
|
||||
position: fixed;
|
||||
z-index: 1999401021;
|
||||
background: #fff;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: #333;
|
||||
font-size: 40px;
|
||||
}
|
10
apps/web/src/components/Loading/index.tsx
Normal file
10
apps/web/src/components/Loading/index.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import "./index.less";
|
||||
import BarLoader from "react-spinners/BarLoader";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="bs loading fc c">
|
||||
<BarLoader color="#24292f" />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -103,7 +103,7 @@ function Nav() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="main">
|
||||
<div
|
||||
{/* <div
|
||||
className="bs fc sb"
|
||||
onClick={() => onClickProfileItem("sub")}
|
||||
>
|
||||
|
@ -111,7 +111,7 @@ function Nav() {
|
|||
<Icon color="var(--color-text-3)">
|
||||
<PremiumPerson20Regular />
|
||||
</Icon>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="bs fc sb">
|
||||
<span>设置</span>
|
||||
<Icon color="var(--color-text-3)">
|
||||
|
|
|
@ -3,10 +3,14 @@ import FlashOff24Regular from "@ricons/fluent/FlashOff24Regular";
|
|||
import { Icon } from "@ricons/utils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function Result() {
|
||||
const navigate = useNavigate();
|
||||
interface IProps {
|
||||
code: 403 | 404 | 405;
|
||||
}
|
||||
|
||||
return (
|
||||
export default function Result(props: IProps) {
|
||||
const navigate = useNavigate();
|
||||
const table = {
|
||||
403: (
|
||||
<div className="container result">
|
||||
<Icon size={48} color="var(--color-text-3)">
|
||||
<FlashOff24Regular />
|
||||
|
@ -19,5 +23,30 @@ export default function Result() {
|
|||
订阅课程
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
),
|
||||
404: (
|
||||
<div className="container result">
|
||||
<Icon size={48} color="var(--color-text-3)">
|
||||
<FlashOff24Regular />
|
||||
</Icon>
|
||||
<div className="mt12">Ooops! 资源被怪兽吃了~</div>
|
||||
<button className="bs btn br3 mt24" onClick={() => navigate(-1)}>
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
405: (
|
||||
<div className="container result">
|
||||
<Icon size={48} color="var(--color-text-3)">
|
||||
<FlashOff24Regular />
|
||||
</Icon>
|
||||
<div className="mt12">请先登录</div>
|
||||
<button className="bs btn br3 mt24" onClick={() => navigate("/login")}>
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
return <>{table[props.code]}</>;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* 挂载
|
||||
*/
|
||||
|
|
44
apps/web/src/hook/useAxios.tsx
Normal file
44
apps/web/src/hook/useAxios.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { useEffect, useState } from "react";
|
||||
import { instance } from "../api/axios";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface IProps {
|
||||
axiosInstance?: AxiosInstance;
|
||||
method?: "get" | "post" | "put" | "delete";
|
||||
url: string;
|
||||
requestConfig?: AxiosRequestConfig;
|
||||
}
|
||||
|
||||
export const useAxios = (props: IProps) => {
|
||||
const [response, setResponse] = useState<AxiosResponse<any, any>>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<unknown>();
|
||||
const {
|
||||
axiosInstance = instance,
|
||||
method = "post",
|
||||
url,
|
||||
requestConfig = {},
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await axiosInstance[method](url, {
|
||||
...requestConfig,
|
||||
signal: controller.signal,
|
||||
});
|
||||
setResponse(res);
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
return controller.abort();
|
||||
}, []);
|
||||
|
||||
return [response, loading, error];
|
||||
};
|
|
@ -23,6 +23,7 @@ export const Guard = (props: IGuardProps) => {
|
|||
if (!user) fetchUser();
|
||||
}, [location.pathname]);
|
||||
|
||||
if (!sign && needAuth) return <Result />;
|
||||
if (!sign && needAuth) return <Result code={405} />;
|
||||
|
||||
return props.children;
|
||||
};
|
||||
|
|
|
@ -70,6 +70,9 @@ export default function Index() {
|
|||
setTimeline({ top });
|
||||
};
|
||||
|
||||
/**
|
||||
* 点击课程
|
||||
*/
|
||||
const onClickCourseItem = (d: any) => {
|
||||
navigate(`/course/detail/${d.course_id}`);
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
bottom: 0;
|
||||
width: 300px;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid var(--color-border-2);
|
||||
|
||||
> h2 {
|
||||
|
@ -44,7 +45,6 @@
|
|||
}
|
||||
.content {
|
||||
position: fixed;
|
||||
left: 300px;
|
||||
right: 0;
|
||||
top: 60px;
|
||||
bottom: 0;
|
||||
|
|
|
@ -4,22 +4,25 @@ import Guide from "./components/Guide";
|
|||
import { useMount } from "../../hook";
|
||||
import Player from "./components/DPlayer";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { getChapterGuideById } from "../../api";
|
||||
import { getCourseDetailById } from "../../api";
|
||||
import { ms2Time } from "./util";
|
||||
import Result from "../../components/Result";
|
||||
import PlayCircle20Regular from "@ricons/fluent/PlayCircle20Regular";
|
||||
import BookLetter20Regular from "@ricons/fluent/BookLetter20Regular";
|
||||
import { Icon } from "@ricons/utils";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
function CourseDetail() {
|
||||
const { id: course_id = "" } = useParams();
|
||||
const [toc, setToc] = useState([]);
|
||||
const [course, setCourse] = useState<any>({});
|
||||
const [view, setView] = useState<any>(null);
|
||||
|
||||
useMount(() => {
|
||||
if (!!course_id)
|
||||
getChapterGuideById(course_id).then((res) => {
|
||||
const { data } = res;
|
||||
getCourseDetailById(course_id).then((res: any) => {
|
||||
const { data, code } = res;
|
||||
if (!data) setToc([]);
|
||||
if (code === 10000) {
|
||||
const processToc = data?.chapterList.map((item: any) => {
|
||||
return {
|
||||
title: item.chapter_title,
|
||||
|
@ -38,7 +41,7 @@ function CourseDetail() {
|
|||
),
|
||||
};
|
||||
});
|
||||
const append = [
|
||||
const composeToc = [
|
||||
{
|
||||
title: "导读",
|
||||
level: 1,
|
||||
|
@ -52,9 +55,19 @@ function CourseDetail() {
|
|||
view: <Guide html={data?.guide.guide_html} />,
|
||||
},
|
||||
...processToc,
|
||||
];
|
||||
setToc(append as any);
|
||||
] as any;
|
||||
const { course } = data;
|
||||
setCourse({
|
||||
...course,
|
||||
course_createtime: dayjs(+course.course_createtime).format(
|
||||
"YYYY-MM-DD"
|
||||
),
|
||||
});
|
||||
setToc(composeToc);
|
||||
setView(<Guide html={data?.guide.guide_html} />);
|
||||
} else if (code === 40000) {
|
||||
setView(<Result code={403} />);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -62,16 +75,17 @@ function CourseDetail() {
|
|||
setToc((t: any) =>
|
||||
t.map((p: any) => ({ ...p, active: i.title === p.title }))
|
||||
);
|
||||
setView(i.view ?? <Result />);
|
||||
setView(i.view ?? <Result code={404} />);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="course-detail">
|
||||
{toc.length > 0 && (
|
||||
<aside className="table-of-content">
|
||||
<h2>云顶计划:K线</h2>
|
||||
<h2>{course.course_title}</h2>
|
||||
<div>
|
||||
<div style={{ color: "var(--color-text-3)" }}>
|
||||
<span>2023年3月2日</span>
|
||||
<span>{course.course_createtime}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="toc">
|
||||
|
@ -100,7 +114,10 @@ function CourseDetail() {
|
|||
})}
|
||||
</div>
|
||||
</aside>
|
||||
<div className="content">{view}</div>
|
||||
)}
|
||||
<div className="content" style={{ left: toc.length > 0 ? "300px" : 0 }}>
|
||||
{view}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
808
pnpm-lock.yaml
808
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user