refactor: course detail

This commit is contained in:
mozzie 2023-03-23 18:00:49 +08:00
parent 630c8ca1b5
commit 5b398e9c7e
11 changed files with 252 additions and 100 deletions

View File

@ -54,3 +54,8 @@ export const removeChapter = (chapter: IChapter) =>
export const selectUserList = () => R.post("/api/user/admin/select/all"); export const selectUserList = () => R.post("/api/user/admin/select/all");
export const updateUser = (user: any) => R.post("/api/user/admin/update", user); export const updateUser = (user: any) => R.post("/api/user/admin/update", user);
export const selectGuide = (p: { guide_course_id: string }) =>
R.post("/api/course/guide/select", p);
export const updateGuide = (p: any) => R.post("/api/course/guide/update", p);

View File

@ -46,8 +46,7 @@
bottom: 0; bottom: 0;
overflow-y: auto; overflow-y: auto;
.view { .view {
width: 1120px; padding: 0 24px;
margin: 0 auto;
} }
} }
} }

View File

@ -1,12 +1,14 @@
import "vditor/dist/index.css"; import "vditor/dist/index.css";
import Vditor from "vditor"; import Vditor from "vditor";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef } from "react";
import "./index.less"; import "./index.less";
import { message } from "antd"; import { message } from "antd";
interface IProps { interface IProps {
onChange?: Function; onChange?: Function;
styles?: React.CSSProperties; styles?: React.CSSProperties;
id: string;
defaultValue?: string;
} }
const emoji = { const emoji = {
@ -66,7 +68,7 @@ const toolbar = [
}, },
]; ];
const Guide = (props: IProps) => { export default function Guide(props: IProps) {
const vditorRef = useRef<Vditor>(); const vditorRef = useRef<Vditor>();
const submitTool = { const submitTool = {
@ -83,37 +85,40 @@ const Guide = (props: IProps) => {
}; };
useEffect(() => { useEffect(() => {
vditorRef.current = new Vditor("vditor", { if (props.defaultValue)
height: 600, vditorRef.current = new Vditor("vditor", {
toolbar: [...toolbar, "|", submitTool], height: 600,
hint: { delay: 200, emoji }, cache: {
counter: { enable: true }, id: props.id,
preview: { actions: ["desktop", "mobile"] }, enable: false,
after: () => console.log("[info] vditor init success..."),
upload: {
accept: "image/*",
url: "/api/vod/oss/image/upload",
multiple: false,
success(_, res) {
const { code, data, msg } = JSON.parse(res);
console.log(code, data, msg);
if (code === 10000) {
message.success("上传成功");
const { name, url } = data;
vditorRef.current?.insertValue(`![${name}](${url})`);
} else {
message.error(msg);
}
}, },
}, toolbar: [...toolbar, "|", submitTool],
}); value: props.defaultValue ?? "",
}, []); hint: { delay: 200, emoji },
counter: { enable: true },
preview: { actions: ["desktop", "mobile"] },
after: () => console.log("[info] vditor init success..."),
upload: {
accept: "image/*",
url: "/api/vod/oss/image/upload",
multiple: false,
success(_, res) {
const { code, data, msg } = JSON.parse(res);
if (code === 10000) {
message.success("上传成功");
const { name, url } = data;
vditorRef.current?.insertValue(`![${name}](${url})`);
} else {
message.error(msg);
}
},
},
});
}, [props.defaultValue]);
return ( return (
<div style={{ ...props.styles }}> <div style={{ ...props.styles }}>
<div id="vditor" className="vditor" /> <div id="vditor" className="vditor" />
</div> </div>
); );
}; }
export default Guide;

View File

@ -1,4 +1,6 @@
.create-course { .create-course {
width: 1120px;
margin: 0 auto;
padding: 24px 0; padding: 24px 0;
.content { .content {
padding: 20px 0; padding: 20px 0;

View File

@ -102,6 +102,7 @@ const CourseCreate = () => {
styles={{ display: current === 1 ? "block" : "none" }} styles={{ display: current === 1 ? "block" : "none" }}
/> />
<Guide <Guide
id="createCourseEditor"
onChange={onGuideChange} onChange={onGuideChange}
styles={{ display: current === 2 ? "block" : "none" }} styles={{ display: current === 2 ? "block" : "none" }}
/> />

View File

@ -1,4 +1,5 @@
import { import {
CompassOutlined,
DeleteOutlined, DeleteOutlined,
EditFilled, EditFilled,
EditOutlined, EditOutlined,
@ -19,8 +20,9 @@ import {
Modal, Modal,
Tooltip, Tooltip,
Form, Form,
Drawer,
} from "antd"; } from "antd";
import dayjs, { Dayjs } from "dayjs"; import dayjs from "dayjs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
removeCourse, removeCourse,
@ -30,8 +32,10 @@ import {
updateCourse, updateCourse,
createChapter, createChapter,
removeChapter, removeChapter,
selectGuide,
updateGuide,
} from "../../../api"; } from "../../../api";
import { IChapter } from "../../../api/dto"; import Guide from "../Create/Guide";
import { useMount } from "../../../hooks"; import { useMount } from "../../../hooks";
import "./index.less"; import "./index.less";
@ -42,6 +46,14 @@ interface IEditItem {
const defaultEditItem = { key: "", value: "" }; const defaultEditItem = { key: "", value: "" };
const defaultEditGuide = {
drawerVisible: false,
course_title: "",
guide_id: "",
guide_value: "",
guide_html: "",
};
export default function List() { export default function List() {
const [courseList, setCourseList] = useState<any>([]); const [courseList, setCourseList] = useState<any>([]);
const [chapterList, setChapterList] = useState([]); const [chapterList, setChapterList] = useState([]);
@ -50,6 +62,7 @@ export default function List() {
const [editCourseItem, setEditCourseItem] = const [editCourseItem, setEditCourseItem] =
useState<IEditItem>(defaultEditItem); useState<IEditItem>(defaultEditItem);
const [addChapterForm] = Form.useForm(); const [addChapterForm] = Form.useForm();
const [editGuide, setEditGuide] = useState(defaultEditGuide);
const onConfirmEditCourseItem = (record: any) => { const onConfirmEditCourseItem = (record: any) => {
const { key, value } = editCourseItem; const { key, value } = editCourseItem;
@ -127,6 +140,36 @@ export default function List() {
}); });
}; };
const onEditGuide = (record: any) => {
const { course_title, course_id: guide_course_id } = record;
selectGuide({ guide_course_id }).then((res: any) => {
const { data } = res;
const { guide_value = "", guide_html = "", guide_id } = data;
setEditGuide({
drawerVisible: true,
course_title,
guide_id,
guide_value,
guide_html,
});
});
};
const onGuideChange = ({ value, html }: { value: string; html: string }) => {
setEditGuide((p) => ({ ...p, guide_value: value, guide_html: html }));
};
const onConfirmEditGuide = () => {
const { course_title, drawerVisible, ...rest } = editGuide;
console.log(editGuide);
updateGuide(rest).then((res: any) => {
if (res?.code === 10000) {
setEditGuide(defaultEditGuide);
renderCourseTable();
}
});
};
const columns = [ const columns = [
{ {
title: "课程", title: "课程",
@ -280,6 +323,13 @@ export default function List() {
icon={<FileAddOutlined />} icon={<FileAddOutlined />}
></Button> ></Button>
</Tooltip> </Tooltip>
<Tooltip title="修改导读">
<Button
type="default"
onClick={() => onEditGuide(record)}
icon={<CompassOutlined />}
></Button>
</Tooltip>
</Space> </Space>
); );
}, },
@ -494,6 +544,27 @@ export default function List() {
onExpand={onExpand} onExpand={onExpand}
/> />
</Card> </Card>
<Drawer
title={editGuide.course_title}
placement="bottom"
height="95%"
open={editGuide.drawerVisible}
onClose={() => setEditGuide((p) => ({ ...p, drawerVisible: false }))}
extra={
<Space>
<Button type="primary" onClick={onConfirmEditGuide}>
</Button>
</Space>
}
>
<Guide
onChange={onGuideChange}
styles={{ display: "block" }}
id="editGuideEditor"
defaultValue={editGuide.guide_value}
/>
</Drawer>
</div> </div>
); );
} }

View File

@ -5,6 +5,7 @@ import { WEB } from '../config/base.config';
import { CourseCreateDTO } from '../dto/course.dto'; import { CourseCreateDTO } from '../dto/course.dto';
import { Chapter } from '../entity/chapter.entity'; import { Chapter } from '../entity/chapter.entity';
import { Course } from '../entity/course.entity'; import { Course } from '../entity/course.entity';
import { Guide } from '../entity/guide.entity';
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';
@ -148,4 +149,22 @@ export class CourseController {
return { code: BizCode.ERROR, msg: error }; return { code: BizCode.ERROR, msg: error };
} }
} }
@Post('/guide/select')
async selectGuide(@Body() guide: Guide) {
const { guide_course_id: course_id } = guide;
const data = await this.guideService.select(course_id);
return { code: BizCode.OK, data };
}
@Post('/guide/update')
async updateGuide(@Body() guide: Guide) {
try {
await this.guideService.update(guide);
return { code: BizCode.OK };
} catch (error) {
this.ctx.logger.error(error);
return { code: BizCode.ERROR, msg: error };
}
}
} }

View File

@ -25,4 +25,8 @@ export class GuideService {
async removeByCourseId(course_id: string) { async removeByCourseId(course_id: string) {
await this.guideModel.delete({ guide_course_id: course_id }); await this.guideModel.delete({ guide_course_id: course_id });
} }
async update(guide: Guide) {
await this.guideModel.save(guide);
}
} }

View File

@ -3,3 +3,15 @@ export interface ILoginRequest {
user_pass: string; user_pass: string;
// xcode: string; // xcode: string;
} }
export interface IChapter {
chapter_course_id?: string;
chapter_file_id?: string;
chapter_id?: string;
chapter_level?: string;
chapter_title?: string;
media_cover_url?: string;
media_time?: string;
media_url?: string;
order?: number;
}

View File

@ -2,6 +2,7 @@ import "./index.less";
import FlashOff24Regular from "@ricons/fluent/FlashOff24Regular"; 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";
import { useEffect, useState } from "react";
interface IProps { interface IProps {
code: 403 | 404 | 405; code: 403 | 404 | 405;
@ -9,6 +10,7 @@ interface IProps {
export default function Result(props: IProps) { export default function Result(props: IProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [view, setView] = useState<JSX.Element>(<span></span>);
const table = { const table = {
403: ( 403: (
<div className="container result"> <div className="container result">
@ -48,5 +50,11 @@ export default function Result(props: IProps) {
), ),
}; };
return <>{table[props.code]}</>; useEffect(() => {
if (props.code) {
setView(table[props.code]);
}
}, [props.code]);
return <>{view}</>;
} }

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import "./index.less"; import "./index.less";
import Guide from "./components/Guide"; import Guide from "./components/Guide";
import { useMount } from "../../hook"; import { useMount } from "../../hook";
@ -11,90 +11,113 @@ import PlayCircle20Regular from "@ricons/fluent/PlayCircle20Regular";
import DocumentChevronDouble20Regular from "@ricons/fluent/DocumentChevronDouble20Regular"; import DocumentChevronDouble20Regular from "@ricons/fluent/DocumentChevronDouble20Regular";
import { Icon } from "@ricons/utils"; import { Icon } from "@ricons/utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { IChapter } from "../../api/dto";
interface IState {
toc: any[];
course: {
course_title: string;
course_createtime: string;
};
view: JSX.Element | null;
}
const defaultState = {
toc: [],
course: {
course_title: "",
course_createtime: "",
},
view: null,
};
function CourseDetail() { function CourseDetail() {
const { id: course_id = "" } = useParams(); const { id: course_id = "" } = useParams();
const [toc, setToc] = useState([]); const [state, setState] = useState<IState>(defaultState);
const [course, setCourse] = useState<any>({});
const [view, setView] = useState<any>(null); const chapters2toc = (chapterList: IChapter[]) => {
return chapterList.map((item) => {
return {
title: item.chapter_title,
level: +item.chapter_level!,
time: ms2Time(+item.media_time!),
icon: !!item.media_url ? (
<Icon size={20}>
<PlayCircle20Regular />
</Icon>
) : null,
active: false,
view: (
<Player
video={{ url: item.media_url!, pic: item.media_cover_url! }}
/>
),
};
});
};
const computeYMD = (ts: number) => dayjs(ts).format("YYYY-MM-DD");
useMount(() => { useMount(() => {
if (!!course_id) if (!!course_id)
getCourseDetailById(course_id).then((res: any) => { getCourseDetailById(course_id).then((res: any) => {
const { data, code } = res; const { data = [], code } = res;
if (!data) setToc([]); switch (code) {
if (code === 10000) { case 50000:
const processToc = data?.chapterList.map((item: any) => { setState((p) => ({ ...p, view: <Result code={403} /> }));
return { break;
title: item.chapter_title, case 10000:
level: +item.chapter_level, const { chapterList = [], course, guide } = data;
time: ms2Time(+item.media_time), const course_createtime = computeYMD(+course.course_createtime);
icon: !!item.media_url ? ( setState({
<Icon size={20}> course: { ...course, course_createtime },
<PlayCircle20Regular /> toc: [
</Icon> {
) : null, title: "导读",
active: false, level: 1,
view: ( time: "",
<Player },
video={{ url: item.media_url, pic: item.media_cover_url }} {
/> title: "介绍 / 下载",
), level: 2,
}; time: "",
}); active: true,
const composeToc = [ icon: (
{ <Icon size={20}>
title: "导读", <DocumentChevronDouble20Regular />
level: 1, </Icon>
time: "", ),
}, view: <Guide mdText={guide.guide_html} />,
{ },
title: "介绍 / 下载", ...chapters2toc(chapterList),
level: 2, ],
time: "", view: <Guide mdText={data?.guide.guide_value} />,
active: true, });
icon: ( default:
<Icon size={20}> break;
<DocumentChevronDouble20Regular />
</Icon>
),
view: <Guide mdText={data?.guide.guide_html} />,
},
...processToc,
] as any;
const { course } = data;
setCourse({
...course,
course_createtime: dayjs(+course.course_createtime).format(
"YYYY-MM-DD"
),
});
setToc(composeToc);
setView(<Guide mdText={data?.guide.guide_value} />);
} else if (code === 40000) {
setView(<Result code={403} />);
} }
}); });
}); });
const onclickItem = (i: any) => { const onclickItem = (i: any) => {
setToc((t: any) => setState((p) => ({
t.map((p: any) => ({ ...p, active: i.title === p.title })) ...p,
); toc: p.toc.map((p: any) => ({ ...p, active: i.title === p.title })),
setView(i.view ?? <Result code={404} />); view: i.view ?? <Result code={404} />,
}));
}; };
return ( return (
<div className="course-detail"> <div className="course-detail">
{toc.length > 0 ? ( {state.toc.length > 0 ? (
<> <>
<aside className="table-of-content"> <aside className="table-of-content">
<h3>{course.course_title}</h3> <h3>{state.course.course_title}</h3>
<div style={{ color: "var(--color-text-3)", fontSize: 13 }}> <div style={{ color: "var(--color-text-3)", fontSize: 13 }}>
<span>{course.course_createtime}</span> <span>{state.course.course_createtime}</span>
</div> </div>
<div className="toc"> <div className="toc">
{toc.map((i: any) => { {state.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}>
@ -121,8 +144,11 @@ function CourseDetail() {
})} })}
</div> </div>
</aside> </aside>
<div className="content" style={{ left: 300 }}> <div
{view} className="content"
style={{ left: state.toc.length > 0 ? 300 : "0" }}
>
{state.view}
</div> </div>
</> </>
) : ( ) : (