feat: admin course list

This commit is contained in:
mozzie 2023-03-22 17:05:08 +08:00
parent 9ffd6abac7
commit aab88ebc0f
15 changed files with 670 additions and 18 deletions

View File

@ -9,9 +9,12 @@ export interface IGetVodeResponse {
} }
export interface ICourseBasic { export interface ICourseBasic {
course_title: string; course_id?: string;
course_cover_url: string; course_title?: string;
course_summary: string; course_cover_url?: string;
course_summary?: string;
course_createtime?: string;
valid?: boolean;
} }
export interface ICreateCourseRequest extends ICourseBasic { export interface ICreateCourseRequest extends ICourseBasic {
@ -28,3 +31,19 @@ export interface IXcode {
expiretime: string; expiretime: string;
code: 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;
}

View File

@ -2,8 +2,11 @@ import R from "./request";
import P from "./process"; import P from "./process";
import { import {
IAdminLogin, IAdminLogin,
IChapter,
ICourseBasic,
ICreateCourseRequest, ICreateCourseRequest,
IgetVodRequest, IgetVodRequest,
ISelectCourse,
IXcode, IXcode,
} from "./dto"; } from "./dto";
@ -23,3 +26,27 @@ export const createXCode = (codeList: IXcode[]) =>
R.post("/api/xcode/admin/create", codeList); R.post("/api/xcode/admin/create", codeList);
export const selectXCodeList = () => R.post("/api/xcode/admin/select/all"); 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);

View File

@ -31,7 +31,7 @@ instance.interceptors.response.use(
break; break;
case 40000: case 40000:
message.error(msg); message.error(msg);
window.location.href = "/"; // window.location.href = "/";
break; break;
default: default:
// TODO ... // TODO ...

View File

@ -22,6 +22,10 @@ const sideMenus: MenuProps["items"] = [
key: "create", key: "create",
label: "创建", label: "创建",
}, },
{
key: "list",
label: "课程列表",
},
{ {
key: "library", key: "library",
label: "视频库", label: "视频库",

View File

@ -28,6 +28,11 @@ export const sideMenuRoutes: IRoute[] = [
element: lazy(() => import("../view/Course/Create")), element: lazy(() => import("../view/Course/Create")),
name: "创建课程", name: "创建课程",
}, },
{
path: "/course/list",
element: lazy(() => import("../view/Course/List")),
name: "课程列表",
},
{ {
path: "/course/library", path: "/course/library",
element: lazy(() => import("../view/Course/Library")), element: lazy(() => import("../view/Course/Library")),

View File

@ -7,11 +7,8 @@ import {
Space, Space,
Table, Table,
Input, Input,
Segmented,
Tooltip, Tooltip,
Image,
Typography, Typography,
Modal,
Tag, Tag,
} from "antd"; } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -65,7 +62,7 @@ const Library = () => {
key: "key", key: "key",
}, },
{ {
title: "m3u8 大小", title: "hls",
dataIndex: "m3u8Size", dataIndex: "m3u8Size",
key: "m3u8Size", key: "m3u8Size",
}, },
@ -86,7 +83,7 @@ const Library = () => {
key: "m3u8", key: "m3u8",
}, },
{ {
title: "视频封面图", title: "封面图",
dataIndex: "cover", dataIndex: "cover",
key: "cover", key: "cover",
}, },

View File

@ -0,0 +1,3 @@
.list {
padding: 24px 0;
}

View File

@ -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<any>([]);
const [chapterList, setChapterList] = useState([]);
const [editChapterItem, setEditChapterItem] =
useState<IEditItem>(defaultEditItem);
const [editCourseItem, setEditCourseItem] =
useState<IEditItem>(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: <InfoCircleOutlined />,
content: (
<Form form={addChapterForm}>
<Form.Item name="chapterList">
<Input.TextArea
placeholder="级别 | 章节名 | 文件FileID"
autoSize={{ minRows: 12, maxRows: 20 }}
/>
</Form.Item>
</Form>
),
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 (
<Space>
<span>{record.course_title}</span>
<Popconfirm
placement="top"
title="修改课程标题"
description={
<Input
defaultValue={record.course_title}
onChange={(e: any) =>
setEditCourseItem({
key: "course_title",
value: e.target.value,
})
}
/>
}
onConfirm={() => onConfirmEditCourseItem(record)}
okText="确定"
cancelText="取消"
>
<Button type="text" icon={<EditFilled />} />
</Popconfirm>
</Space>
);
},
},
{
title: "摘要",
dataIndex: "course_summary",
key: "course_summary",
render: (_: any, record: any) => {
return (
<Space>
<span>{record.course_summary}</span>
<Popconfirm
placement="top"
title="修改摘要"
description={
<Input
defaultValue={record.course_summary}
onChange={(e: any) =>
setEditCourseItem({
key: "course_summary",
value: e.target.value,
})
}
/>
}
onConfirm={() => onConfirmEditCourseItem(record)}
okText="确定"
cancelText="取消"
>
<Button type="text" icon={<EditFilled />} />
</Popconfirm>
</Space>
);
},
},
{
title: "创建时间",
dataIndex: "course_createtime",
key: "course_createtime",
render: (_: any, record: any) => {
return (
<Space>
<span>
{dayjs(+record.course_createtime).format("YYYY-MM-DD HH:mm:ss")}
</span>
<Popconfirm
placement="top"
title="时间"
description={
<DatePicker
onChange={(date: any) => {
setEditCourseItem({
key: "course_createtime",
value: "" + new Date(date).getTime(),
});
}}
format="YYYY-MM-DD HH:mm:ss"
showTime={{ defaultValue: dayjs("00:00:00", "HH:mm:ss") }}
/>
}
onConfirm={() => onConfirmEditCourseItem(record)}
okText="确定"
cancelText="取消"
>
<Button type="text" icon={<FieldTimeOutlined />} />
</Popconfirm>
</Space>
);
},
},
{
title: "封面",
dataIndex: "course_cover_url",
key: "course_cover_url",
render: (_: any, record: any) => {
return <Image width={120} src={record.course_cover_url} />;
},
},
{
title: "可见",
dataIndex: "visible",
key: "visible",
render: (_: any, record: any) => {
return (
<Switch
checkedChildren="开启"
unCheckedChildren="关闭"
defaultChecked={record.valid}
onChange={(value: boolean) =>
updateCourse({ ...record, valid: value }).then((res: any) => {
if (res?.code === 10000) {
renderCourseTable();
setEditCourseItem(defaultEditItem);
}
})
}
/>
);
},
},
{
title: "操作",
dataIndex: "operation",
key: "operation",
render: (_: any, record: any) => {
return (
<Space>
<Popconfirm
placement="top"
title="确定删除课程嘛"
onConfirm={() => onConfirmDeleteCourseItem(record)}
okText="确定"
cancelText="取消"
>
<Button type="dashed" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
<Tooltip title="添加章节">
<Button
type="default"
onClick={() => onAddChapter(record)}
icon={<FileAddOutlined />}
></Button>
</Tooltip>
</Space>
);
},
},
];
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 (
<Space>
<span>{record.chapter_level}</span>
<Popconfirm
placement="top"
title="修改级别"
description={
<Input
defaultValue={record.chapter_level}
onChange={(e: any) =>
setEditChapterItem({
key: "chapter_level",
value: e.target.value,
})
}
/>
}
onConfirm={() => onConfirmEditChapterItem(record)}
okText="确定"
cancelText="取消"
>
<Button type="text" icon={<EditOutlined />} />
</Popconfirm>
</Space>
);
},
},
{
title: "章节",
dataIndex: "chapter_title",
key: "chapter_title",
render: (_: any, record: any) => {
return (
<Space>
<span>{record.chapter_title}</span>
<Popconfirm
placement="top"
title="修改章节标题名"
description={
<Input
defaultValue={record.chapter_title}
onChange={(e: any) =>
setEditChapterItem({
key: "chapter_title",
value: e.target.value,
})
}
/>
}
onConfirm={() => onConfirmEditChapterItem(record)}
okText="确定"
cancelText="取消"
>
<Button type="text" icon={<EditOutlined />} />
</Popconfirm>
</Space>
);
},
},
{
title: "FileID",
dataIndex: "chapter_file_id",
key: "chapter_file_id",
render: (_: any, record: any) => {
return +record.chapter_level === 2 ? (
<Space>
<span>{record.chapter_file_id}</span>
<Popconfirm
placement="top"
title="修改FileID"
description={
<Input
defaultValue={record.chapter_file_id}
onChange={(e: any) =>
setEditChapterItem({
key: "chapter_file_id",
value: e.target.value,
})
}
/>
}
onConfirm={() => onConfirmEditChapterItem(record)}
okText="确定"
cancelText="取消"
>
<Button type="text" icon={<EditOutlined />} />
</Popconfirm>
</Space>
) : (
"-"
);
},
},
{
title: "顺序",
dataIndex: "order",
key: "order",
render: (_: any, record: any) => {
return (
<Space>
<span>{record.order}</span>
<Popconfirm
placement="top"
title="修改顺序"
description={
<Input
defaultValue={record.order}
onChange={(e: any) =>
setEditChapterItem({
key: "order",
value: +e.target.value,
})
}
/>
}
onConfirm={() => onConfirmEditChapterItem(record)}
okText="确定"
cancelText="取消"
>
<Button type="text" icon={<EditOutlined />} />
</Popconfirm>
</Space>
);
},
},
{
title: "时长",
dataIndex: "media_time",
key: "media_time",
render: (_: any, record: any) => {
return +record.chapter_level === 2 ? (
<Space>
<span>{record.media_time}</span>
</Space>
) : (
"-"
);
},
},
{
title: "操作",
dataIndex: "operation_chapter",
key: "operation_chapter",
render: (_: any, record: any) => {
return (
<Space>
<Popconfirm
placement="top"
title="确定删除章节嘛"
onConfirm={() => onConfirmDeleteChapterItem(record)}
okText="确定"
cancelText="取消"
>
<Button type="dashed" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space>
);
},
},
];
return <Table columns={col} dataSource={chapterList} pagination={false} />;
};
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 (
<div className="list">
<Card>
<Table
dataSource={courseList}
columns={columns}
expandable={{ expandedRowRender }}
onExpand={onExpand}
/>
</Card>
</div>
);
}

View File

@ -3,6 +3,8 @@ import { Context } from '@midwayjs/koa';
import { BizCode } from '../biz/code'; import { BizCode } from '../biz/code';
import { WEB } from '../config/base.config'; 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 { Course } from '../entity/course.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';
@ -52,8 +54,9 @@ export class CourseController {
} }
@Post('/select/all') @Post('/select/all')
async selectAll() { async selectAll(@Body() params) {
const courseList = await this.courseService.selectAll(); const { all = false } = params;
const courseList = await this.courseService.selectAll(all);
return { code: BizCode.OK, data: courseList }; return { code: BizCode.OK, data: courseList };
} }
@ -72,7 +75,75 @@ export class CourseController {
const guide = await this.guideService.select(course_id); const guide = await this.guideService.select(course_id);
return { code: BizCode.OK, data: { chapterList, guide, course } }; return { code: BizCode.OK, data: { chapterList, guide, course } };
} catch (error) { } catch (error) {
this.ctx.logger.error(error);
return { code: BizCode.ERROR, msg: '[error] /chapter/select 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 };
}
}
} }

View File

@ -6,18 +6,18 @@ export class Chapter {
chapter_id?: string; chapter_id?: string;
@Column({ type: 'text' }) @Column({ type: 'text' })
chapter_title: string; chapter_title?: string;
@Column() @Column()
chapter_level: '1' | '2'; chapter_level?: '1' | '2';
@Column({ default: '' }) @Column({ default: '' })
chapter_file_id?: string; chapter_file_id?: string;
@Column() @Column()
chapter_course_id: string; chapter_course_id?: string;
@Column() @Column({ default: -1 })
order?: number; order?: number;
@Column({ default: '' }) @Column({ default: '' })
@ -26,6 +26,6 @@ export class Chapter {
@Column({ default: '' }) @Column({ default: '' })
media_url?: string; media_url?: string;
@Column({ default: ''}) @Column({ default: '' })
media_cover_url?: string; media_cover_url?: string;
} }

View File

@ -43,6 +43,7 @@ export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
} }
await next(); await next();
} catch (error) { } catch (error) {
ctx.logger.error(error);
return { code: BizCode.AUTH, msg: '身份验证错误' }; return { code: BizCode.AUTH, msg: '身份验证错误' };
} }
} else { } else {

View File

@ -36,4 +36,16 @@ export class ChapterService {
}); });
return result; 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 });
}
} }

View File

@ -24,12 +24,21 @@ export class CourseService {
return courseCreateRes.course_id; return courseCreateRes.course_id;
} }
async selectAll() { async selectAll(all) {
return await this.courseModel.find({ where: { valid: true } }); if (!all) return await this.courseModel.find({ where: { valid: true } });
else return await this.courseModel.find();
} }
async select(course: Course) { async select(course: Course) {
const { course_id } = course; const { course_id } = course;
return await this.courseModel.findOne({ where: { course_id } }); 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 });
}
} }

View File

@ -21,4 +21,8 @@ export class GuideService {
where: { guide_course_id: course_id }, where: { guide_course_id: course_id },
}); });
} }
async removeByCourseId(course_id: string) {
await this.guideModel.delete({ guide_course_id: course_id });
}
} }

View File

@ -45,6 +45,7 @@
grid-row-gap: 10px; grid-row-gap: 10px;
.course-card { .course-card {
cursor: pointer;
border-radius: 3px; border-radius: 3px;
.cover { .cover {
height: 120px; height: 120px;