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/openapi-client": "0.4.5",
"@alicloud/tea-util": "1.4.5", "@alicloud/tea-util": "1.4.5",
"@alicloud/tea-typescript": "1.8.0", "@alicloud/tea-typescript": "1.8.0",
"object-hash": "3.0.0" "object-hash": "3.0.0",
"nanoid": "3.3.4"
}, },
"devDependencies": { "devDependencies": {
"@midwayjs/cli": "^2.0.0", "@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 { BizCode } from '../biz/code';
import { webSign } from '../config/base.config';
import { CourseCreateDTO } from '../dto/course.dto'; import { CourseCreateDTO } from '../dto/course.dto';
import { ChapterService } from '../service/chapter.service'; import { ChapterService } from '../service/chapter.service';
import { CourseService } from '../service/course.service'; import { CourseService } from '../service/course.service';
import { GuideService } from '../service/guide.service'; import { GuideService } from '../service/guide.service';
import { UserService } from '../service/user.service';
import { decodeToken } from '../util/encrypt';
@Controller('/course') @Controller('/course')
export class CourseController { export class CourseController {
@ -19,6 +23,9 @@ export class CourseController {
@Inject() @Inject()
guideService: GuideService; guideService: GuideService;
@Inject()
userService: UserService;
@Post('/create') @Post('/create')
async create(@Body() param: CourseCreateDTO) { async create(@Body() param: CourseCreateDTO) {
try { try {
@ -50,11 +57,22 @@ export class CourseController {
return { code: BizCode.OK, data: courseList }; return { code: BizCode.OK, data: courseList };
} }
@Post('/chapter/select') @Post('/detail/select')
async selectChapterByCourseId(@Body() params) { async selectDetailByCourseId(@Body() params) {
const { course_id } = params; const { course_id } = params;
const chapterList = await this.chapterService.select(course_id); try {
const guide = await this.guideService.select(course_id); const token = this.ctx.cookies.get(webSign);
return { code: BizCode.OK, data: { chapterList, guide } }; 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, 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: '验证码无效' }; if (!verifyCode) return { code: BizCode.ERROR, msg: '验证码无效' };
// 查询用户是否存在 // 查询用户是否存在
const userExist = await this.userService.select(params); const userExist = await this.userService.select(params);
let payload = {}; const payload = userExist?.id
if (userExist?.id) { ? userExist
const { user_pass, ...rest } = userExist; : await this.userService.createUser(params);
payload = rest;
} else {
// 新用户注册
const { user_pass, ...rest } = await this.userService.createUser(
params
);
payload = rest;
}
const token = createToken({ ...payload, hasLogin: true }); const token = createToken({ ...payload, hasLogin: true });
this.ctx.cookies.set(webSign, token, { this.ctx.cookies.set(webSign, token, {
expires: new Date(Date.now() + webSignExpired), expires: new Date(Date.now() + webSignExpired),
@ -112,7 +104,7 @@ export class UserController {
60 60
); );
console.log('redis here', res); console.log('redis here', res);
// await this.smsService.send({ code, phoneNumbers }); await this.smsService.send({ code, phoneNumbers });
return { code: BizCode.OK }; return { code: BizCode.OK };
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@ -1,8 +1,8 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity('course') @Entity('course')
export class Course { export class Course {
@PrimaryGeneratedColumn('uuid') @PrimaryColumn()
course_id?: string; course_id?: string;
@Column({ unique: true }) @Column({ unique: true })

View File

@ -8,9 +8,6 @@ export class User {
@Column({ unique: true }) @Column({ unique: true })
user_login?: string; user_login?: string;
@Column()
user_pass?: string;
@Column({ default: '' }) @Column({ default: '' })
user_email?: string; user_email?: string;
@ -25,4 +22,7 @@ export class User {
@Column({ default: '' }) @Column({ default: '' })
user_avatar?: string; 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 { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { Course } from '../entity/course.entity'; import { Course } from '../entity/course.entity';
import { nanoid } from 'nanoid';
export interface ICourseCreate { export interface ICourseCreate {
course_title: string; course_title: string;
@ -18,6 +19,7 @@ export class CourseService {
courseModel: Repository<Course>; courseModel: Repository<Course>;
async create(course: Course) { async create(course: Course) {
course.course_id = nanoid(13);
const courseCreateRes = await this.courseModel.save(course); const courseCreateRes = await this.courseModel.save(course);
return courseCreateRes.course_id; return courseCreateRes.course_id;
} }
@ -25,4 +27,9 @@ export class CourseService {
async selectAll() { async selectAll() {
return await this.courseModel.find({ where: { valid: true } }); 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 { Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm'; import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { UserWebAuthDTO } from '../dto/user.dto';
import { User } from '../entity/user.entity'; import { User } from '../entity/user.entity';
import hash from 'object-hash'; import hash from 'object-hash';
@ -10,7 +9,7 @@ export class UserService {
@InjectEntityModel(User) @InjectEntityModel(User)
userModel: Repository<User>; userModel: Repository<User>;
async select(p: UserWebAuthDTO): Promise<any> { async select(p: User): Promise<User> {
const { user_login } = p; const { user_login } = p;
const user = await this.userModel.findOne({ const user = await this.userModel.findOne({
where: { user_login }, where: { user_login },

View File

@ -17,7 +17,9 @@
"@ricons/utils": "0.1.6", "@ricons/utils": "0.1.6",
"dplayer": "1.27.1", "dplayer": "1.27.1",
"highlight.js": "11.7.0", "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": { "devDependencies": {
"@types/react": "^18.0.27", "@types/react": "^18.0.27",

View File

@ -4,6 +4,8 @@ import "./assets/base.less";
import Nav from "./components/Nav"; import Nav from "./components/Nav";
import { commonRouters, lazyRouters } from "./router"; import { commonRouters, lazyRouters } from "./router";
import { Guard } from "./router/Guard"; import { Guard } from "./router/Guard";
import { Toaster } from "react-hot-toast";
import Loading from "./components/Loading";
function App() { function App() {
return ( return (
@ -23,7 +25,7 @@ function App() {
key={router.path} key={router.path}
path={router.path} path={router.path}
element={ element={
<Suspense fallback={"loading"}> <Suspense fallback={<Loading />}>
<Guard>{<router.element />}</Guard> <Guard>{<router.element />}</Guard>
</Suspense> </Suspense>
} }
@ -32,6 +34,7 @@ function App() {
<Route path="*" element={<span>404</span>} /> <Route path="*" element={<span>404</span>} />
</Routes> </Routes>
</main> </main>
<Toaster />
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
// import { message } from "antd";
import axios from "axios"; import axios from "axios";
import toast from "react-hot-toast";
const config = { const config = {
baseURL: "", baseURL: "",
@ -7,7 +7,7 @@ const config = {
headers: {}, headers: {},
}; };
const instance = axios.create(config); export const instance = axios.create(config);
instance.interceptors.request.use( instance.interceptors.request.use(
(config) => { (config) => {
@ -36,8 +36,7 @@ instance.interceptors.response.use(
// Message.error(`接口: ${response.config.url}, 遇到错误`); // Message.error(`接口: ${response.config.url}, 遇到错误`);
break; break;
case 40000: case 40000:
console.error(msg); toast.error(msg);
// console.log('登录')
break; break;
default: default:
// TODO ... // TODO ...

View File

@ -1,10 +1,10 @@
import { ILoginRequest } from "./dto"; import { ILoginRequest } from "./dto";
import R from "./request"; import R from "./axios";
export const getCourseList = () => R.post("/api/course/select/all"); export const getCourseList = () => R.post("/api/course/select/all");
export const getChapterGuideById = (course_id: string) => export const getCourseDetailById = (course_id: string) =>
R.post("/api/course/chapter/select", { course_id }); R.post("/api/course/detail/select", { course_id });
export const userLogin = (p: ILoginRequest) => export const userLogin = (p: ILoginRequest) =>
R.post("/api/user/web/auth", { ...p }); 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> </div>
<div className="main"> <div className="main">
<div {/* <div
className="bs fc sb" className="bs fc sb"
onClick={() => onClickProfileItem("sub")} onClick={() => onClickProfileItem("sub")}
> >
@ -111,7 +111,7 @@ function Nav() {
<Icon color="var(--color-text-3)"> <Icon color="var(--color-text-3)">
<PremiumPerson20Regular /> <PremiumPerson20Regular />
</Icon> </Icon>
</div> </div> */}
<div className="bs fc sb"> <div className="bs fc sb">
<span></span> <span></span>
<Icon color="var(--color-text-3)"> <Icon color="var(--color-text-3)">

View File

@ -3,21 +3,50 @@ import FlashOff24Regular from "@ricons/fluent/FlashOff24Regular";
import { Icon } from "@ricons/utils"; import { Icon } from "@ricons/utils";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export default function Result() { interface IProps {
const navigate = useNavigate(); code: 403 | 404 | 405;
}
return (
<div className="container result"> export default function Result(props: IProps) {
<Icon size={48} color="var(--color-text-3)"> const navigate = useNavigate();
<FlashOff24Regular /> const table = {
</Icon> 403: (
<div className="mt12">访</div> <div className="container result">
<button <Icon size={48} color="var(--color-text-3)">
className="bs btn br3 mt24" <FlashOff24Regular />
onClick={() => navigate("/subscribe")} </Icon>
> <div className="mt12">访</div>
<button
</button> className="bs btn br3 mt24"
</div> onClick={() => navigate("/subscribe")}
); >
</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"; 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(); if (!user) fetchUser();
}, [location.pathname]); }, [location.pathname]);
if (!sign && needAuth) return <Result />; if (!sign && needAuth) return <Result code={405} />;
return props.children; return props.children;
}; };

View File

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

View File

@ -5,6 +5,7 @@
bottom: 0; bottom: 0;
width: 300px; width: 300px;
padding: 20px; padding: 20px;
overflow-y: auto;
border-right: 1px solid var(--color-border-2); border-right: 1px solid var(--color-border-2);
> h2 { > h2 {
@ -44,7 +45,6 @@
} }
.content { .content {
position: fixed; position: fixed;
left: 300px;
right: 0; right: 0;
top: 60px; top: 60px;
bottom: 0; bottom: 0;

View File

@ -4,57 +4,70 @@ import Guide from "./components/Guide";
import { useMount } from "../../hook"; import { useMount } from "../../hook";
import Player from "./components/DPlayer"; import Player from "./components/DPlayer";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { getChapterGuideById } from "../../api"; import { getCourseDetailById } from "../../api";
import { ms2Time } from "./util"; import { ms2Time } from "./util";
import Result from "../../components/Result"; import Result from "../../components/Result";
import PlayCircle20Regular from "@ricons/fluent/PlayCircle20Regular"; import PlayCircle20Regular from "@ricons/fluent/PlayCircle20Regular";
import BookLetter20Regular from "@ricons/fluent/BookLetter20Regular";
import { Icon } from "@ricons/utils"; import { Icon } from "@ricons/utils";
import dayjs from "dayjs";
function CourseDetail() { function CourseDetail() {
const { id: course_id = "" } = useParams(); const { id: course_id = "" } = useParams();
const [toc, setToc] = useState([]); const [toc, setToc] = useState([]);
const [course, setCourse] = useState<any>({});
const [view, setView] = useState<any>(null); const [view, setView] = useState<any>(null);
useMount(() => { useMount(() => {
if (!!course_id) if (!!course_id)
getChapterGuideById(course_id).then((res) => { getCourseDetailById(course_id).then((res: any) => {
const { data } = res; const { data, code } = res;
const processToc = data?.chapterList.map((item: any) => { if (!data) setToc([]);
return { if (code === 10000) {
title: item.chapter_title, const processToc = data?.chapterList.map((item: any) => {
level: +item.chapter_level, return {
time: ms2Time(+item.media_time), title: item.chapter_title,
icon: !!item.media_url ? ( level: +item.chapter_level,
<Icon size={20}> time: ms2Time(+item.media_time),
<PlayCircle20Regular /> icon: !!item.media_url ? (
</Icon> <Icon size={20}>
) : null, <PlayCircle20Regular />
active: false, </Icon>
view: ( ) : null,
<Player active: false,
video={{ url: item.media_url, pic: item.media_cover_url }} view: (
/> <Player
video={{ url: item.media_url, pic: item.media_cover_url }}
/>
),
};
});
const composeToc = [
{
title: "导读",
level: 1,
time: "",
},
{
title: "介绍 / 下载",
level: 2,
time: "",
active: true,
view: <Guide html={data?.guide.guide_html} />,
},
...processToc,
] as any;
const { course } = data;
setCourse({
...course,
course_createtime: dayjs(+course.course_createtime).format(
"YYYY-MM-DD"
), ),
}; });
}); setToc(composeToc);
const append = [ setView(<Guide html={data?.guide.guide_html} />);
{ } else if (code === 40000) {
title: "导读", setView(<Result code={403} />);
level: 1, }
time: "",
},
{
title: "介绍 / 下载",
level: 2,
time: "",
active: true,
view: <Guide html={data?.guide.guide_html} />,
},
...processToc,
];
setToc(append as any);
setView(<Guide html={data?.guide.guide_html} />);
}); });
}); });
@ -62,45 +75,49 @@ function CourseDetail() {
setToc((t: any) => setToc((t: any) =>
t.map((p: any) => ({ ...p, active: i.title === p.title })) t.map((p: any) => ({ ...p, active: i.title === p.title }))
); );
setView(i.view ?? <Result />); setView(i.view ?? <Result code={404} />);
}; };
return ( return (
<div className="course-detail"> <div className="course-detail">
<aside className="table-of-content"> {toc.length > 0 && (
<h2>K线</h2> <aside className="table-of-content">
<div> <h2>{course.course_title}</h2>
<div style={{ color: "var(--color-text-3)" }}> <div>
<span>202332</span> <div style={{ color: "var(--color-text-3)" }}>
<span>{course.course_createtime}</span>
</div>
</div> </div>
</div> <div className="toc">
<div className="toc"> {toc.map((i: any) => {
{toc.map((i: any) => { if (i.level === 1) {
if (i.level === 1) { return (
return ( <div className="level-1" key={i.title}>
<div className="level-1" key={i.title}> {i.title}
{i.title}
</div>
);
} else if (i.level === 2) {
return (
<div
className={`level-2 ${i.active ? "active" : ""}`}
key={i.title}
onClick={() => onclickItem(i)}
>
<div className="bs ellipsis fc">
{i.icon}
<span>{i.title}</span>
</div> </div>
<span className="time">{i.time}</span> );
</div> } else if (i.level === 2) {
); return (
} <div
})} className={`level-2 ${i.active ? "active" : ""}`}
</div> key={i.title}
</aside> onClick={() => onclickItem(i)}
<div className="content">{view}</div> >
<div className="bs ellipsis fc">
{i.icon}
<span>{i.title}</span>
</div>
<span className="time">{i.time}</span>
</div>
);
}
})}
</div>
</aside>
)}
<div className="content" style={{ left: toc.length > 0 ? "300px" : 0 }}>
{view}
</div>
</div> </div>
); );
} }

File diff suppressed because it is too large Load Diff