feat: message loadin

This commit is contained in:
mozzie 2023-03-18 22:03:17 +08:00
parent 3514c1e395
commit a807f2c557
22 changed files with 686 additions and 513 deletions

View File

@ -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",

View File

@ -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' };
}
}
}

View File

@ -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);

View File

@ -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 })

View File

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

View File

@ -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 } });
}
}

View File

@ -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 },

View File

@ -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",

View File

@ -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 />
</>
);
}

View File

@ -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 ...

View File

@ -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 });

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

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

View File

@ -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)">

View File

@ -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]}</>;
}

View File

@ -1,5 +1,4 @@
import { useCallback, useEffect, useState } from "react";
/**
*
*/

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

View File

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

View File

@ -70,6 +70,9 @@ export default function Index() {
setTimeline({ top });
};
/**
*
*/
const onClickCourseItem = (d: any) => {
navigate(`/course/detail/${d.course_id}`);
};

View File

@ -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;

View File

@ -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>202332</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>
);
}

File diff suppressed because it is too large Load Diff