From aab88ebc0fcd812408d79a28ad45557094b80186 Mon Sep 17 00:00:00 2001 From: mozzie Date: Wed, 22 Mar 2023 17:05:08 +0800 Subject: [PATCH] feat: admin course list --- apps/admin/src/api/dto.ts | 25 +- apps/admin/src/api/index.ts | 27 + apps/admin/src/api/request.ts | 2 +- apps/admin/src/layout/index.tsx | 4 + apps/admin/src/router/index.tsx | 5 + apps/admin/src/view/Course/Library/index.tsx | 7 +- apps/admin/src/view/Course/List/index.less | 3 + apps/admin/src/view/Course/List/index.tsx | 499 ++++++++++++++++++ .../src/controller/course.controller.ts | 75 ++- apps/server/src/entity/chapter.entity.ts | 10 +- apps/server/src/middleware/auth.middleware.ts | 1 + apps/server/src/service/chapter.service.ts | 12 + apps/server/src/service/course.service.ts | 13 +- apps/server/src/service/guide.service.ts | 4 + apps/web/src/view/Course/index.less | 1 + 15 files changed, 670 insertions(+), 18 deletions(-) create mode 100644 apps/admin/src/view/Course/List/index.less create mode 100644 apps/admin/src/view/Course/List/index.tsx diff --git a/apps/admin/src/api/dto.ts b/apps/admin/src/api/dto.ts index 53d2b6f..f48f3e0 100644 --- a/apps/admin/src/api/dto.ts +++ b/apps/admin/src/api/dto.ts @@ -9,9 +9,12 @@ export interface IGetVodeResponse { } export interface ICourseBasic { - course_title: string; - course_cover_url: string; - course_summary: string; + course_id?: string; + course_title?: string; + course_cover_url?: string; + course_summary?: string; + course_createtime?: string; + valid?: boolean; } export interface ICreateCourseRequest extends ICourseBasic { @@ -28,3 +31,19 @@ export interface IXcode { expiretime: string; code: string; } + +export interface ISelectCourse { + all?: boolean; +} + +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/admin/src/api/index.ts b/apps/admin/src/api/index.ts index ac01ffa..e16ff82 100644 --- a/apps/admin/src/api/index.ts +++ b/apps/admin/src/api/index.ts @@ -2,8 +2,11 @@ import R from "./request"; import P from "./process"; import { IAdminLogin, + IChapter, + ICourseBasic, ICreateCourseRequest, IgetVodRequest, + ISelectCourse, IXcode, } from "./dto"; @@ -23,3 +26,27 @@ export const createXCode = (codeList: IXcode[]) => R.post("/api/xcode/admin/create", codeList); export const selectXCodeList = () => R.post("/api/xcode/admin/select/all"); + +export const selectCourseList = (p: ISelectCourse) => + R.post("/api/course/select/all", { ...p }); + +export const updateCourse = (course: ICourseBasic) => + R.post("/api/course/update", course); + +export const selectChapterList = ({ + chapter_course_id, +}: { + chapter_course_id: string; +}) => R.post("/api/course/chapter/select", { chapter_course_id }); + +export const updateChapter = (chapter: any) => + R.post("/api/course/chapter/update", chapter); + +export const removeCourse = (course: ICourseBasic) => + R.post("/api/course/remove", course); + +export const createChapter = (chapterList: IChapter[]) => + R.post("/api/course/chapter/create", chapterList); + +export const removeChapter = (chapter: IChapter) => + R.post("/api/course/chapter/remove", chapter); diff --git a/apps/admin/src/api/request.ts b/apps/admin/src/api/request.ts index 793f608..03c46f8 100644 --- a/apps/admin/src/api/request.ts +++ b/apps/admin/src/api/request.ts @@ -31,7 +31,7 @@ instance.interceptors.response.use( break; case 40000: message.error(msg); - window.location.href = "/"; + // window.location.href = "/"; break; default: // TODO ... diff --git a/apps/admin/src/layout/index.tsx b/apps/admin/src/layout/index.tsx index e54d119..af35d3e 100644 --- a/apps/admin/src/layout/index.tsx +++ b/apps/admin/src/layout/index.tsx @@ -22,6 +22,10 @@ const sideMenus: MenuProps["items"] = [ key: "create", label: "创建", }, + { + key: "list", + label: "课程列表", + }, { key: "library", label: "视频库", diff --git a/apps/admin/src/router/index.tsx b/apps/admin/src/router/index.tsx index 015414d..1a9beb7 100644 --- a/apps/admin/src/router/index.tsx +++ b/apps/admin/src/router/index.tsx @@ -28,6 +28,11 @@ export const sideMenuRoutes: IRoute[] = [ element: lazy(() => import("../view/Course/Create")), name: "创建课程", }, + { + path: "/course/list", + element: lazy(() => import("../view/Course/List")), + name: "课程列表", + }, { path: "/course/library", element: lazy(() => import("../view/Course/Library")), diff --git a/apps/admin/src/view/Course/Library/index.tsx b/apps/admin/src/view/Course/Library/index.tsx index 8fdb0e8..ae38c37 100644 --- a/apps/admin/src/view/Course/Library/index.tsx +++ b/apps/admin/src/view/Course/Library/index.tsx @@ -7,11 +7,8 @@ import { Space, Table, Input, - Segmented, Tooltip, - Image, Typography, - Modal, Tag, } from "antd"; import dayjs from "dayjs"; @@ -65,7 +62,7 @@ const Library = () => { key: "key", }, { - title: "m3u8 大小", + title: "hls", dataIndex: "m3u8Size", key: "m3u8Size", }, @@ -86,7 +83,7 @@ const Library = () => { key: "m3u8", }, { - title: "视频封面图", + title: "封面图", dataIndex: "cover", key: "cover", }, diff --git a/apps/admin/src/view/Course/List/index.less b/apps/admin/src/view/Course/List/index.less new file mode 100644 index 0000000..037c791 --- /dev/null +++ b/apps/admin/src/view/Course/List/index.less @@ -0,0 +1,3 @@ +.list { + padding: 24px 0; +} diff --git a/apps/admin/src/view/Course/List/index.tsx b/apps/admin/src/view/Course/List/index.tsx new file mode 100644 index 0000000..03357dc --- /dev/null +++ b/apps/admin/src/view/Course/List/index.tsx @@ -0,0 +1,499 @@ +import { + DeleteOutlined, + EditFilled, + EditOutlined, + FieldTimeOutlined, + FileAddOutlined, + InfoCircleOutlined, +} from "@ant-design/icons"; +import { + Button, + Card, + DatePicker, + Image, + Input, + Popconfirm, + Space, + Switch, + Table, + Modal, + Tooltip, + Form, +} from "antd"; +import dayjs, { Dayjs } from "dayjs"; +import { useEffect, useState } from "react"; +import { + removeCourse, + selectChapterList, + selectCourseList, + updateChapter, + updateCourse, + createChapter, + removeChapter, +} from "../../../api"; +import { IChapter } from "../../../api/dto"; +import { useMount } from "../../../hooks"; +import "./index.less"; + +interface IEditItem { + key: string; + value: string | boolean | number; +} + +const defaultEditItem = { key: "", value: "" }; + +export default function List() { + const [courseList, setCourseList] = useState([]); + const [chapterList, setChapterList] = useState([]); + const [editChapterItem, setEditChapterItem] = + useState(defaultEditItem); + const [editCourseItem, setEditCourseItem] = + useState(defaultEditItem); + const [addChapterForm] = Form.useForm(); + + const onConfirmEditCourseItem = (record: any) => { + const { key, value } = editCourseItem; + updateCourse({ ...record, [key]: value }).then((res: any) => { + if (res?.code === 10000) { + renderCourseTable(); + setEditCourseItem(defaultEditItem); + } + }); + }; + + const onConfirmEditChapterItem = (record: any) => { + const { key, value } = editChapterItem; + updateChapter({ ...record, [key]: value }).then((res: any) => { + if (res?.code === 10000) { + const { chapter_course_id } = record; + renderChapterList(chapter_course_id); + setEditChapterItem(defaultEditItem); + } + }); + }; + + const onConfirmDeleteCourseItem = (record: any) => { + const { course_id } = record; + removeCourse({ course_id }).then((res: any) => { + if (res?.code === 10000) renderCourseTable(); + }); + }; + + const onAddChapter = (record: any) => { + const { course_id: chapter_course_id } = record; + Modal.info({ + title: "添加章节", + icon: , + content: ( +
+ + + +
+ ), + onOk: async () => { + const origin = addChapterForm.getFieldValue("chapterList"); + const chapterList = origin + .split("\n") + .filter((i: string) => i.replace(/\s/, "").length > 0) + .map((row: string) => { + const [chapter_level, chapter_title, chapter_file_id] = + row.split("|"); + return !chapter_file_id + ? { chapter_level, chapter_title, chapter_course_id } + : { + chapter_level, + chapter_title, + chapter_file_id, + chapter_course_id, + }; + }); + createChapter(chapterList).then((res: any) => { + if (res?.code === 10000) renderChapterList(chapter_course_id); + }); + }, + okText: "确认", + cancelText: "取消", + }); + }; + + const onConfirmDeleteChapterItem = (record: any) => { + const { chapter_id, chapter_course_id } = record; + removeChapter({ chapter_id }).then((res: any) => { + if (res?.code === 10000) renderChapterList(chapter_course_id); + }); + }; + + const columns = [ + { + title: "课程", + dataIndex: "course_title", + key: "course_title", + render: (_: any, record: any) => { + return ( + + {record.course_title} + + setEditCourseItem({ + key: "course_title", + value: e.target.value, + }) + } + /> + } + onConfirm={() => onConfirmEditCourseItem(record)} + okText="确定" + cancelText="取消" + > + + + + + + + ); + }, + }, + ]; + + useEffect(() => {}, [courseList]); + + const renderCourseTable = () => { + selectCourseList({ all: true }).then((res) => { + const { data } = res; + setCourseList(data.map((i: any) => ({ ...i, key: i.course_id }))); + }); + }; + + useMount(() => { + renderCourseTable(); + }); + + const expandedRowRender = () => { + const col: any = [ + { + title: "级别", + dataIndex: "chapter_level", + key: "chapter_level", + render: (_: any, record: any) => { + return ( + + {record.chapter_level} + + setEditChapterItem({ + key: "chapter_level", + value: e.target.value, + }) + } + /> + } + onConfirm={() => onConfirmEditChapterItem(record)} + okText="确定" + cancelText="取消" + > + + + + ); + }, + }, + ]; + return ; + }; + + const renderChapterList = (chapter_course_id: string) => { + selectChapterList({ chapter_course_id }).then((res: any) => { + const { data = [] } = res; + setChapterList(data.map((i: any) => ({ ...i, key: i.chapter_id }))); + }); + }; + + const onExpand = (expand: boolean, record: any) => { + if (expand) { + const { course_id: chapter_course_id } = record; + renderChapterList(chapter_course_id); + } + }; + + return ( +
+ +
+ + + ); +} diff --git a/apps/server/src/controller/course.controller.ts b/apps/server/src/controller/course.controller.ts index 5efada4..742c9c9 100644 --- a/apps/server/src/controller/course.controller.ts +++ b/apps/server/src/controller/course.controller.ts @@ -3,6 +3,8 @@ import { Context } from '@midwayjs/koa'; import { BizCode } from '../biz/code'; 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 { ChapterService } from '../service/chapter.service'; import { CourseService } from '../service/course.service'; import { GuideService } from '../service/guide.service'; @@ -52,8 +54,9 @@ export class CourseController { } @Post('/select/all') - async selectAll() { - const courseList = await this.courseService.selectAll(); + async selectAll(@Body() params) { + const { all = false } = params; + const courseList = await this.courseService.selectAll(all); return { code: BizCode.OK, data: courseList }; } @@ -72,7 +75,75 @@ export class CourseController { const guide = await this.guideService.select(course_id); return { code: BizCode.OK, data: { chapterList, guide, course } }; } catch (error) { + this.ctx.logger.error(error); return { code: BizCode.ERROR, msg: '[error] /chapter/select error' }; } } + + @Post('/update') + async updateCourse(@Body() course: Course) { + try { + await this.courseService.update(course); + return { code: BizCode.OK }; + } catch (error) { + this.ctx.logger.error(error); + return { code: BizCode.ERROR, msg: error }; + } + } + + @Post('/chapter/select') + async selectChapterList(@Body() chapter: Chapter) { + const { chapter_course_id } = chapter; + const chapterList = await this.chapterService.select(chapter_course_id); + return { code: BizCode.OK, data: chapterList }; + } + + @Post('/chapter/update') + async updateChapter(@Body() chapter: Chapter) { + try { + await this.chapterService.update(chapter); + return { code: BizCode.OK }; + } catch (error) { + this.ctx.logger.error(error); + return { code: BizCode.ERROR, msg: error }; + } + } + + @Post('/remove') + async remove(@Body() course: Course) { + const { course_id } = course; + try { + await this.courseService.remove(course_id); + await this.chapterService.removeByCourseId(course_id); + await this.guideService.removeByCourseId(course_id); + return { code: BizCode.OK }; + } catch (error) { + this.ctx.logger.error(error); + return { code: BizCode.ERROR, msg: error }; + } + } + + @Post('/chapter/create') + async createChapter(@Body() chapterList: Chapter[]) { + try { + // order默认为-1 + await this.chapterService.create(chapterList); + return { code: BizCode.OK }; + } catch (error) { + this.ctx.logger.error(error); + return { code: BizCode.ERROR, msg: error }; + } + } + + @Post('/chapter/remove') + async removeChapter(@Body() chapter: Chapter) { + try { + const { chapter_id } = chapter; + await this.chapterService.remove(chapter_id); + return { code: BizCode.OK }; + } catch (error) { + this.ctx.logger.error(error); + return { code: BizCode.ERROR, msg: error }; + } + } } diff --git a/apps/server/src/entity/chapter.entity.ts b/apps/server/src/entity/chapter.entity.ts index 2a3c797..33a23d8 100644 --- a/apps/server/src/entity/chapter.entity.ts +++ b/apps/server/src/entity/chapter.entity.ts @@ -6,18 +6,18 @@ export class Chapter { chapter_id?: string; @Column({ type: 'text' }) - chapter_title: string; + chapter_title?: string; @Column() - chapter_level: '1' | '2'; + chapter_level?: '1' | '2'; @Column({ default: '' }) chapter_file_id?: string; @Column() - chapter_course_id: string; + chapter_course_id?: string; - @Column() + @Column({ default: -1 }) order?: number; @Column({ default: '' }) @@ -26,6 +26,6 @@ export class Chapter { @Column({ default: '' }) media_url?: string; - @Column({ default: ''}) + @Column({ default: '' }) media_cover_url?: string; } diff --git a/apps/server/src/middleware/auth.middleware.ts b/apps/server/src/middleware/auth.middleware.ts index d2e5819..f12d593 100644 --- a/apps/server/src/middleware/auth.middleware.ts +++ b/apps/server/src/middleware/auth.middleware.ts @@ -43,6 +43,7 @@ export class AuthMiddleware implements IMiddleware { } await next(); } catch (error) { + ctx.logger.error(error); return { code: BizCode.AUTH, msg: '身份验证错误' }; } } else { diff --git a/apps/server/src/service/chapter.service.ts b/apps/server/src/service/chapter.service.ts index 6ba0b8c..e7333d5 100644 --- a/apps/server/src/service/chapter.service.ts +++ b/apps/server/src/service/chapter.service.ts @@ -36,4 +36,16 @@ export class ChapterService { }); return result; } + + async update(chapter: Chapter) { + await this.chapterModel.save(chapter); + } + + async removeByCourseId(course_id: string) { + await this.chapterModel.delete({ chapter_course_id: course_id }); + } + + async remove(chapter_id: string) { + await this.chapterModel.delete({ chapter_id }); + } } diff --git a/apps/server/src/service/course.service.ts b/apps/server/src/service/course.service.ts index 7211334..8f492ef 100644 --- a/apps/server/src/service/course.service.ts +++ b/apps/server/src/service/course.service.ts @@ -24,12 +24,21 @@ export class CourseService { return courseCreateRes.course_id; } - async selectAll() { - return await this.courseModel.find({ where: { valid: true } }); + async selectAll(all) { + if (!all) return await this.courseModel.find({ where: { valid: true } }); + else return await this.courseModel.find(); } async select(course: Course) { const { course_id } = course; return await this.courseModel.findOne({ where: { course_id } }); } + + async update(course: Course) { + return await this.courseModel.save(course); + } + + async remove(course_id: string) { + await this.courseModel.delete({ course_id }); + } } diff --git a/apps/server/src/service/guide.service.ts b/apps/server/src/service/guide.service.ts index 0c6f6c3..4987571 100644 --- a/apps/server/src/service/guide.service.ts +++ b/apps/server/src/service/guide.service.ts @@ -21,4 +21,8 @@ export class GuideService { where: { guide_course_id: course_id }, }); } + + async removeByCourseId(course_id: string) { + await this.guideModel.delete({ guide_course_id: course_id }); + } } diff --git a/apps/web/src/view/Course/index.less b/apps/web/src/view/Course/index.less index 4b191d4..7531436 100644 --- a/apps/web/src/view/Course/index.less +++ b/apps/web/src/view/Course/index.less @@ -45,6 +45,7 @@ grid-row-gap: 10px; .course-card { + cursor: pointer; border-radius: 3px; .cover { height: 120px;