refactor: course detail
This commit is contained in:
parent
630c8ca1b5
commit
5b398e9c7e
|
@ -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);
|
||||
|
|
|
@ -46,8 +46,7 @@
|
|||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
.view {
|
||||
width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,37 +85,40 @@ const Guide = (props: IProps) => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
vditorRef.current = new Vditor("vditor", {
|
||||
height: 600,
|
||||
toolbar: [...toolbar, "|", submitTool],
|
||||
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);
|
||||
console.log(code, data, msg);
|
||||
if (code === 10000) {
|
||||
message.success("上传成功");
|
||||
const { name, url } = data;
|
||||
vditorRef.current?.insertValue(``);
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
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"] },
|
||||
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(``);
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [props.defaultValue]);
|
||||
|
||||
return (
|
||||
<div style={{ ...props.styles }}>
|
||||
<div id="vditor" className="vditor" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Guide;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
.create-course {
|
||||
width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 0;
|
||||
.content {
|
||||
padding: 20px 0;
|
||||
|
|
|
@ -102,6 +102,7 @@ const CourseCreate = () => {
|
|||
styles={{ display: current === 1 ? "block" : "none" }}
|
||||
/>
|
||||
<Guide
|
||||
id="createCourseEditor"
|
||||
onChange={onGuideChange}
|
||||
styles={{ display: current === 2 ? "block" : "none" }}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}</>;
|
||||
}
|
||||
|
|
|
@ -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,90 +11,113 @@ 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);
|
||||
|
||||
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(() => {
|
||||
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) => {
|
||||
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 composeToc = [
|
||||
{
|
||||
title: "导读",
|
||||
level: 1,
|
||||
time: "",
|
||||
},
|
||||
{
|
||||
title: "介绍 / 下载",
|
||||
level: 2,
|
||||
time: "",
|
||||
active: true,
|
||||
icon: (
|
||||
<Icon size={20}>
|
||||
<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 { 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,
|
||||
time: "",
|
||||
},
|
||||
{
|
||||
title: "介绍 / 下载",
|
||||
level: 2,
|
||||
time: "",
|
||||
active: true,
|
||||
icon: (
|
||||
<Icon size={20}>
|
||||
<DocumentChevronDouble20Regular />
|
||||
</Icon>
|
||||
),
|
||||
view: <Guide mdText={guide.guide_html} />,
|
||||
},
|
||||
...chapters2toc(chapterList),
|
||||
],
|
||||
view: <Guide mdText={data?.guide.guide_value} />,
|
||||
});
|
||||
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>
|
||||
</>
|
||||
) : (
|
||||
|
|
Loading…
Reference in New Issue
Block a user