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 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;
overflow-y: auto;
.view {
width: 1120px;
margin: 0 auto;
padding: 0 24px;
}
}
}

View File

@ -1,12 +1,14 @@
import "vditor/dist/index.css";
import Vditor from "vditor";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
import "./index.less";
import { message } from "antd";
interface IProps {
onChange?: Function;
styles?: React.CSSProperties;
id: string;
defaultValue?: string;
}
const emoji = {
@ -66,7 +68,7 @@ const toolbar = [
},
];
const Guide = (props: IProps) => {
export default function Guide(props: IProps) {
const vditorRef = useRef<Vditor>();
const submitTool = {
@ -83,9 +85,15 @@ const Guide = (props: IProps) => {
};
useEffect(() => {
if (props.defaultValue)
vditorRef.current = new Vditor("vditor", {
height: 600,
cache: {
id: props.id,
enable: false,
},
toolbar: [...toolbar, "|", submitTool],
value: props.defaultValue ?? "",
hint: { delay: 200, emoji },
counter: { enable: true },
preview: { actions: ["desktop", "mobile"] },
@ -96,7 +104,6 @@ const Guide = (props: IProps) => {
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;
@ -107,13 +114,11 @@ const Guide = (props: IProps) => {
},
},
});
}, []);
}, [props.defaultValue]);
return (
<div style={{ ...props.styles }}>
<div id="vditor" className="vditor" />
</div>
);
};
export default Guide;
}

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import {
CompassOutlined,
DeleteOutlined,
EditFilled,
EditOutlined,
@ -19,8 +20,9 @@ import {
Modal,
Tooltip,
Form,
Drawer,
} from "antd";
import dayjs, { Dayjs } from "dayjs";
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import {
removeCourse,
@ -30,8 +32,10 @@ import {
updateCourse,
createChapter,
removeChapter,
selectGuide,
updateGuide,
} from "../../../api";
import { IChapter } from "../../../api/dto";
import Guide from "../Create/Guide";
import { useMount } from "../../../hooks";
import "./index.less";
@ -42,6 +46,14 @@ interface IEditItem {
const defaultEditItem = { key: "", value: "" };
const defaultEditGuide = {
drawerVisible: false,
course_title: "",
guide_id: "",
guide_value: "",
guide_html: "",
};
export default function List() {
const [courseList, setCourseList] = useState<any>([]);
const [chapterList, setChapterList] = useState([]);
@ -50,6 +62,7 @@ export default function List() {
const [editCourseItem, setEditCourseItem] =
useState<IEditItem>(defaultEditItem);
const [addChapterForm] = Form.useForm();
const [editGuide, setEditGuide] = useState(defaultEditGuide);
const onConfirmEditCourseItem = (record: any) => {
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 = [
{
title: "课程",
@ -280,6 +323,13 @@ export default function List() {
icon={<FileAddOutlined />}
></Button>
</Tooltip>
<Tooltip title="修改导读">
<Button
type="default"
onClick={() => onEditGuide(record)}
icon={<CompassOutlined />}
></Button>
</Tooltip>
</Space>
);
},
@ -494,6 +544,27 @@ export default function List() {
onExpand={onExpand}
/>
</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>
);
}

View File

@ -5,6 +5,7 @@ import { WEB } from '../config/base.config';
import { CourseCreateDTO } from '../dto/course.dto';
import { Chapter } from '../entity/chapter.entity';
import { Course } from '../entity/course.entity';
import { Guide } from '../entity/guide.entity';
import { ChapterService } from '../service/chapter.service';
import { CourseService } from '../service/course.service';
import { GuideService } from '../service/guide.service';
@ -148,4 +149,22 @@ export class CourseController {
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) {
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;
// 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 { Icon } from "@ricons/utils";
import { useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
interface IProps {
code: 403 | 404 | 405;
@ -9,6 +10,7 @@ interface IProps {
export default function Result(props: IProps) {
const navigate = useNavigate();
const [view, setView] = useState<JSX.Element>(<span></span>);
const table = {
403: (
<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 Guide from "./components/Guide";
import { useMount } from "../../hook";
@ -11,24 +11,36 @@ import PlayCircle20Regular from "@ricons/fluent/PlayCircle20Regular";
import DocumentChevronDouble20Regular from "@ricons/fluent/DocumentChevronDouble20Regular";
import { Icon } from "@ricons/utils";
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() {
const { id: course_id = "" } = useParams();
const [toc, setToc] = useState([]);
const [course, setCourse] = useState<any>({});
const [view, setView] = useState<any>(null);
const [state, setState] = useState<IState>(defaultState);
useMount(() => {
if (!!course_id)
getCourseDetailById(course_id).then((res: any) => {
const { data, code } = res;
if (!data) setToc([]);
if (code === 10000) {
const processToc = data?.chapterList.map((item: any) => {
const chapters2toc = (chapterList: IChapter[]) => {
return chapterList.map((item) => {
return {
title: item.chapter_title,
level: +item.chapter_level,
time: ms2Time(+item.media_time),
level: +item.chapter_level!,
time: ms2Time(+item.media_time!),
icon: !!item.media_url ? (
<Icon size={20}>
<PlayCircle20Regular />
@ -37,12 +49,29 @@ function CourseDetail() {
active: false,
view: (
<Player
video={{ url: item.media_url, pic: item.media_cover_url }}
video={{ url: item.media_url!, pic: item.media_cover_url! }}
/>
),
};
});
const composeToc = [
};
const computeYMD = (ts: number) => dayjs(ts).format("YYYY-MM-DD");
useMount(() => {
if (!!course_id)
getCourseDetailById(course_id).then((res: any) => {
const { data = [], code } = res;
switch (code) {
case 50000:
setState((p) => ({ ...p, view: <Result code={403} /> }));
break;
case 10000:
const { chapterList = [], course, guide } = data;
const course_createtime = computeYMD(+course.course_createtime);
setState({
course: { ...course, course_createtime },
toc: [
{
title: "导读",
level: 1,
@ -58,43 +87,37 @@ function CourseDetail() {
<DocumentChevronDouble20Regular />
</Icon>
),
view: <Guide mdText={data?.guide.guide_html} />,
view: <Guide mdText={guide.guide_html} />,
},
...processToc,
] as any;
const { course } = data;
setCourse({
...course,
course_createtime: dayjs(+course.course_createtime).format(
"YYYY-MM-DD"
),
...chapters2toc(chapterList),
],
view: <Guide mdText={data?.guide.guide_value} />,
});
setToc(composeToc);
setView(<Guide mdText={data?.guide.guide_value} />);
} else if (code === 40000) {
setView(<Result code={403} />);
default:
break;
}
});
});
const onclickItem = (i: any) => {
setToc((t: any) =>
t.map((p: any) => ({ ...p, active: i.title === p.title }))
);
setView(i.view ?? <Result code={404} />);
setState((p) => ({
...p,
toc: p.toc.map((p: any) => ({ ...p, active: i.title === p.title })),
view: i.view ?? <Result code={404} />,
}));
};
return (
<div className="course-detail">
{toc.length > 0 ? (
{state.toc.length > 0 ? (
<>
<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 }}>
<span>{course.course_createtime}</span>
<span>{state.course.course_createtime}</span>
</div>
<div className="toc">
{toc.map((i: any) => {
{state.toc.map((i: any) => {
if (i.level === 1) {
return (
<div className="level-1" key={i.title}>
@ -121,8 +144,11 @@ function CourseDetail() {
})}
</div>
</aside>
<div className="content" style={{ left: 300 }}>
{view}
<div
className="content"
style={{ left: state.toc.length > 0 ? 300 : "0" }}
>
{state.view}
</div>
</>
) : (