diff --git a/apps/admin/src/api/index.ts b/apps/admin/src/api/index.ts index cc85475..1cc25b3 100644 --- a/apps/admin/src/api/index.ts +++ b/apps/admin/src/api/index.ts @@ -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); diff --git a/apps/admin/src/layout/index.less b/apps/admin/src/layout/index.less index 0854f3f..5b66d04 100644 --- a/apps/admin/src/layout/index.less +++ b/apps/admin/src/layout/index.less @@ -46,8 +46,7 @@ bottom: 0; overflow-y: auto; .view { - width: 1120px; - margin: 0 auto; + padding: 0 24px; } } } diff --git a/apps/admin/src/view/Course/Create/Guide/index.tsx b/apps/admin/src/view/Course/Create/Guide/index.tsx index 0a3a256..cc84c27 100644 --- a/apps/admin/src/view/Course/Create/Guide/index.tsx +++ b/apps/admin/src/view/Course/Create/Guide/index.tsx @@ -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(); 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(`![${name}](${url})`); - } 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(`![${name}](${url})`); + } else { + message.error(msg); + } + }, + }, + }); + }, [props.defaultValue]); return (
); -}; - -export default Guide; +} diff --git a/apps/admin/src/view/Course/Create/index.less b/apps/admin/src/view/Course/Create/index.less index f84c7c7..478316f 100644 --- a/apps/admin/src/view/Course/Create/index.less +++ b/apps/admin/src/view/Course/Create/index.less @@ -1,4 +1,6 @@ .create-course { + width: 1120px; + margin: 0 auto; padding: 24px 0; .content { padding: 20px 0; diff --git a/apps/admin/src/view/Course/Create/index.tsx b/apps/admin/src/view/Course/Create/index.tsx index a4977b2..227d3e0 100644 --- a/apps/admin/src/view/Course/Create/index.tsx +++ b/apps/admin/src/view/Course/Create/index.tsx @@ -102,6 +102,7 @@ const CourseCreate = () => { styles={{ display: current === 1 ? "block" : "none" }} /> diff --git a/apps/admin/src/view/Course/List/index.tsx b/apps/admin/src/view/Course/List/index.tsx index 03357dc..9e12d13 100644 --- a/apps/admin/src/view/Course/List/index.tsx +++ b/apps/admin/src/view/Course/List/index.tsx @@ -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([]); const [chapterList, setChapterList] = useState([]); @@ -50,6 +62,7 @@ export default function List() { const [editCourseItem, setEditCourseItem] = useState(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={} > + + + ); }, @@ -494,6 +544,27 @@ export default function List() { onExpand={onExpand} /> + setEditGuide((p) => ({ ...p, drawerVisible: false }))} + extra={ + + + + } + > + +
); } diff --git a/apps/server/src/controller/course.controller.ts b/apps/server/src/controller/course.controller.ts index 0572ec3..6db4e62 100644 --- a/apps/server/src/controller/course.controller.ts +++ b/apps/server/src/controller/course.controller.ts @@ -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 }; + } + } } diff --git a/apps/server/src/service/guide.service.ts b/apps/server/src/service/guide.service.ts index 4987571..f494055 100644 --- a/apps/server/src/service/guide.service.ts +++ b/apps/server/src/service/guide.service.ts @@ -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); + } } diff --git a/apps/web/src/api/dto.ts b/apps/web/src/api/dto.ts index c8cf5b3..78e03a4 100644 --- a/apps/web/src/api/dto.ts +++ b/apps/web/src/api/dto.ts @@ -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; +} diff --git a/apps/web/src/components/Result/index.tsx b/apps/web/src/components/Result/index.tsx index 0a861fd..80b87b6 100644 --- a/apps/web/src/components/Result/index.tsx +++ b/apps/web/src/components/Result/index.tsx @@ -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(); const table = { 403: (
@@ -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}; } diff --git a/apps/web/src/view/CourseDetail/index.tsx b/apps/web/src/view/CourseDetail/index.tsx index 0793e8f..7dcc7b5 100644 --- a/apps/web/src/view/CourseDetail/index.tsx +++ b/apps/web/src/view/CourseDetail/index.tsx @@ -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({}); - const [view, setView] = useState(null); + const [state, setState] = useState(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 ? ( + + + + ) : null, + active: false, + view: ( + + ), + }; + }); + }; + + 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 ? ( - - - - ) : null, - active: false, - view: ( - - ), - }; - }); - const composeToc = [ - { - title: "导读", - level: 1, - time: "", - }, - { - title: "介绍 / 下载", - level: 2, - time: "", - active: true, - icon: ( - - - - ), - view: , - }, - ...processToc, - ] as any; - const { course } = data; - setCourse({ - ...course, - course_createtime: dayjs(+course.course_createtime).format( - "YYYY-MM-DD" - ), - }); - setToc(composeToc); - setView(); - } else if (code === 40000) { - setView(); + const { data = [], code } = res; + switch (code) { + case 50000: + setState((p) => ({ ...p, view: })); + 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: ( + + + + ), + view: , + }, + ...chapters2toc(chapterList), + ], + view: , + }); + default: + break; } }); }); const onclickItem = (i: any) => { - setToc((t: any) => - t.map((p: any) => ({ ...p, active: i.title === p.title })) - ); - setView(i.view ?? ); + setState((p) => ({ + ...p, + toc: p.toc.map((p: any) => ({ ...p, active: i.title === p.title })), + view: i.view ?? , + })); }; return (
- {toc.length > 0 ? ( + {state.toc.length > 0 ? ( <> -
- {view} +
0 ? 300 : "0" }} + > + {state.view}
) : (