feat: 创建课程
This commit is contained in:
parent
12be9a9180
commit
475b2932ef
|
@ -1,10 +1,9 @@
|
||||||
import { Navigate, Route, Routes } from "react-router-dom";
|
import { Route, Routes } from "react-router-dom";
|
||||||
import "./assets/less/common.less";
|
import "./assets/less/common.less";
|
||||||
import Layout from "./layout";
|
import Layout from "./layout";
|
||||||
import Login from "./view/Login";
|
import Login from "./view/Login";
|
||||||
import { ConfigProvider as AntDesignConfigProvider } from "antd";
|
import { ConfigProvider as AntDesignConfigProvider } from "antd";
|
||||||
import zhCN from 'antd/locale/zh_CN';
|
import zhCN from "antd/locale/zh_CN";
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -6,4 +6,4 @@ import { IgetVodRequest } from "./dto";
|
||||||
* 腾讯vod媒资
|
* 腾讯vod媒资
|
||||||
*/
|
*/
|
||||||
export const getVod = (p: IgetVodRequest) =>
|
export const getVod = (p: IgetVodRequest) =>
|
||||||
R.post("/api/vod", { ...p }).then((d: any) => P.getVod(d.data));
|
R.post("/api/vod/media/select", { ...p }).then((d: any) => P.getVod(d.data));
|
||||||
|
|
|
@ -11,6 +11,7 @@ const instance = axios.create(config);
|
||||||
|
|
||||||
instance.interceptors.request.use(
|
instance.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
|
console.log(config)
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
.container {
|
.container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #f1f1f1;
|
background: #f1f1f1;
|
||||||
header {
|
> header {
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
z-index: 19;
|
z-index: 19;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 320px;
|
width: 176px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
@ -28,15 +28,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
aside {
|
> aside {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 46px;
|
top: 46px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
> aside {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
main {
|
}
|
||||||
position: fixed;
|
> main {
|
||||||
|
position: absolute;
|
||||||
left: 200px;
|
left: 200px;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 46px;
|
top: 46px;
|
||||||
|
|
|
@ -68,6 +68,7 @@ const Index: React.FC = () => {
|
||||||
theme="dark"
|
theme="dark"
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
defaultSelectedKeys={["/"]}
|
defaultSelectedKeys={["/"]}
|
||||||
|
style={{ flex: 1 }}
|
||||||
items={navMenus}
|
items={navMenus}
|
||||||
onClick={onClickNavMenuItem}
|
onClick={onClickNavMenuItem}
|
||||||
/>
|
/>
|
||||||
|
|
24
apps/admin/src/store/media.tsx
Normal file
24
apps/admin/src/store/media.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { getVod } from "../api";
|
||||||
|
|
||||||
|
export const useMediaStore = create((set) => ({
|
||||||
|
list: [],
|
||||||
|
listFilter: [],
|
||||||
|
getListFilter: (state: any) => {
|
||||||
|
return state.list.length === 0
|
||||||
|
? getVod({ offset: 0, limit: 5000 }).then((res: any) =>
|
||||||
|
set({ list: res.mediaList, listFilter: res.mediaList })
|
||||||
|
)
|
||||||
|
: state.list;
|
||||||
|
},
|
||||||
|
setList: (newState: any) =>
|
||||||
|
set(() => ({ list: newState, listFilter: newState })),
|
||||||
|
filterList: (keyword: string) =>
|
||||||
|
set((state: any) => ({
|
||||||
|
listFilter: !keyword
|
||||||
|
? state.list
|
||||||
|
: state.list.filter(
|
||||||
|
(i: any) => i.name.toUpperCase().indexOf(keyword.toUpperCase()) > -1
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
}));
|
|
@ -1,10 +0,0 @@
|
||||||
interface IProps {
|
|
||||||
onChange?: Function;
|
|
||||||
styles?: React.CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Appendix = (props: IProps) => {
|
|
||||||
return <div style={{ ...props.styles }}>附件</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Appendix;
|
|
|
@ -14,7 +14,6 @@
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
letter-spacing: 2px;
|
|
||||||
&.title {
|
&.title {
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
|
|
|
@ -23,7 +23,7 @@ const BasicForm = (props: IProps) => {
|
||||||
const coverDragger: UploadProps = {
|
const coverDragger: UploadProps = {
|
||||||
name: "file",
|
name: "file",
|
||||||
multiple: true,
|
multiple: true,
|
||||||
action: "/api/upload",
|
action: "/api/vod/course/cover/upload",
|
||||||
onChange(info) {
|
onChange(info) {
|
||||||
const { status } = info.file;
|
const { status } = info.file;
|
||||||
if (status !== "uploading") console.log(info.file, info.fileList);
|
if (status !== "uploading") console.log(info.file, info.fileList);
|
||||||
|
|
|
@ -40,7 +40,7 @@ const Chatpter = (props: IProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(chapterList);
|
if (props.onChange) props.onChange(chapterList);
|
||||||
}, [chapterList]);
|
}, [chapterList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
2
apps/admin/src/view/Course/Create/Guide/index.less
Normal file
2
apps/admin/src/view/Course/Create/Guide/index.less
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.vditor {
|
||||||
|
}
|
134
apps/admin/src/view/Course/Create/Guide/index.tsx
Normal file
134
apps/admin/src/view/Course/Create/Guide/index.tsx
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import "vditor/dist/index.css";
|
||||||
|
import Vditor from "vditor";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import "./index.less";
|
||||||
|
import { message } from "antd";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onChange?: Function;
|
||||||
|
styles?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emoji = {
|
||||||
|
"+1": "👍",
|
||||||
|
"-1": "👎",
|
||||||
|
confused: "😕",
|
||||||
|
eyes: "👀️",
|
||||||
|
heart: "❤️",
|
||||||
|
rocket: "🚀️",
|
||||||
|
smile: "😄",
|
||||||
|
tada: "🎉️",
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolbar = [
|
||||||
|
"emoji",
|
||||||
|
"headings",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"strike",
|
||||||
|
"link",
|
||||||
|
"|",
|
||||||
|
"list",
|
||||||
|
"ordered-list",
|
||||||
|
"check",
|
||||||
|
"outdent",
|
||||||
|
"indent",
|
||||||
|
"|",
|
||||||
|
"quote",
|
||||||
|
"line",
|
||||||
|
"code",
|
||||||
|
"inline-code",
|
||||||
|
"insert-before",
|
||||||
|
"insert-after",
|
||||||
|
"|",
|
||||||
|
"upload",
|
||||||
|
// "record",
|
||||||
|
"table",
|
||||||
|
"|",
|
||||||
|
"undo",
|
||||||
|
"redo",
|
||||||
|
"|",
|
||||||
|
"fullscreen",
|
||||||
|
"edit-mode",
|
||||||
|
{
|
||||||
|
name: "more",
|
||||||
|
toolbar: [
|
||||||
|
"both",
|
||||||
|
"code-theme",
|
||||||
|
"content-theme",
|
||||||
|
"export",
|
||||||
|
"outline",
|
||||||
|
"preview",
|
||||||
|
"devtools",
|
||||||
|
"info",
|
||||||
|
"help",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Guide = (props: IProps) => {
|
||||||
|
const vditorRef = useRef<Vditor>();
|
||||||
|
|
||||||
|
const submitTool = {
|
||||||
|
name: "submit",
|
||||||
|
tipPosition: "s",
|
||||||
|
tip: "提交",
|
||||||
|
className: "right",
|
||||||
|
icon: '<svg viewBox="0 0 1024 1024"><path d="M385 840.5c-20.8 0-41.7-7.9-57.6-23.8L87.6 576.9c-31.8-31.8-31.8-83.3 0-115.1s83.3-31.8 115.1 0l239.8 239.8c31.8 31.8 31.8 83.3 0 115.1-15.9 15.9-36.7 23.8-57.5 23.8z" fill="#1296db" p-id="25510"></path><path d="M384.6 840.5c-20.8 0-41.7-7.9-57.6-23.8-31.8-31.8-31.8-83.3 0-115.1l494.2-494.2c31.8-31.8 83.3-31.8 115.1 0s31.8 83.3 0 115.1L442.2 816.7c-15.9 15.9-36.8 23.8-57.6 23.8z" fill="#1296db"></path></svg>',
|
||||||
|
click() {
|
||||||
|
const value = vditorRef.current?.getValue();
|
||||||
|
const html = vditorRef.current?.getHTML();
|
||||||
|
if (props.onChange) props.onChange({ value, html });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
vditorRef.current = new Vditor("vditor", {
|
||||||
|
height: 600,
|
||||||
|
toolbar: [...toolbar, "|", submitTool],
|
||||||
|
hint: {
|
||||||
|
delay: 200,
|
||||||
|
emoji,
|
||||||
|
},
|
||||||
|
preview: { actions: ["desktop", "mobile"] },
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
counter: {
|
||||||
|
enable: true,
|
||||||
|
},
|
||||||
|
after: () => {
|
||||||
|
console.log("[info] vditor init success...");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (props.onChange)
|
||||||
|
// props.onChange({
|
||||||
|
// value: vditorRef.current?.getValue(),
|
||||||
|
// html: vditorRef.current?.getHTML(),
|
||||||
|
// });
|
||||||
|
// }, [vditorRef.current?.getValue()]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ ...props.styles }}>
|
||||||
|
<div id="vditor" className="vditor" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Guide;
|
|
@ -5,3 +5,10 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drawer-media-item {
|
||||||
|
border: 1px solid rgba(5, 5, 5, 0.06);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,25 +1,18 @@
|
||||||
import {
|
import { Button, Card, Drawer, Input, message, Steps, Typography } from "antd";
|
||||||
Button,
|
import { useEffect, useState } from "react";
|
||||||
Card,
|
import Guide from "./Guide";
|
||||||
Col,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
InputNumber,
|
|
||||||
message,
|
|
||||||
Row,
|
|
||||||
Space,
|
|
||||||
Steps,
|
|
||||||
} from "antd";
|
|
||||||
import { useState } from "react";
|
|
||||||
import Appendix from "./Appendix";
|
|
||||||
import BasicForm from "./BasicForm";
|
import BasicForm from "./BasicForm";
|
||||||
import Chatpter from "./Chatpter";
|
import Chatpter from "./Chatpter";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
import { useMediaStore } from "../../../store/media";
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
const CourseCreate = () => {
|
const CourseCreate = () => {
|
||||||
const [current, setCurrent] = useState(0);
|
const [current, setCurrent] = useState(0);
|
||||||
const [course, setCourse] = useState({
|
const [course, setCourse] = useState({
|
||||||
basicInfo: {},
|
basicInfo: {},
|
||||||
|
chapters: [],
|
||||||
|
guide: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onBasicFormChange = (form: any) =>
|
const onBasicFormChange = (form: any) =>
|
||||||
|
@ -33,12 +26,51 @@ const CourseCreate = () => {
|
||||||
title: "章节",
|
title: "章节",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "附件",
|
title: "导读",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const [createBtnValid, setCreateBtnValid] = useState(true);
|
||||||
|
|
||||||
const items = steps.map((item) => ({ key: item.title, title: item.title }));
|
const items = steps.map((item) => ({ key: item.title, title: item.title }));
|
||||||
|
|
||||||
|
const onChapterChange = (chapters: any) =>
|
||||||
|
setCourse((p) => ({ ...p, chapters }));
|
||||||
|
|
||||||
|
const onGuideChange = ({ value, html }: { value: string; html: string }) => {
|
||||||
|
setCourse((p) => ({ ...p, guide: { value, html } }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickCreate = () => {
|
||||||
|
console.log(course);
|
||||||
|
message.info("撒打算");
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const basicValid = !Object.values(course.basicInfo).includes("");
|
||||||
|
const chaptersValid = course.chapters.length !== 0;
|
||||||
|
const guideValid = !Object.values(course.guide).includes("");
|
||||||
|
setCreateBtnValid(basicValid && chaptersValid && guideValid);
|
||||||
|
}, [course]);
|
||||||
|
|
||||||
|
const mediaList = useMediaStore((s: any) => s.listFilter);
|
||||||
|
|
||||||
|
const mediaListFilter = useMediaStore((s: any) => s.filterList);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const showDrawer = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearchChange = (e: any) => {
|
||||||
|
mediaListFilter(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="create-course">
|
<div className="create-course">
|
||||||
<Card>
|
<Card>
|
||||||
|
@ -48,10 +80,25 @@ const CourseCreate = () => {
|
||||||
onChange={onBasicFormChange}
|
onChange={onBasicFormChange}
|
||||||
styles={{ display: current === 0 ? "block" : "none" }}
|
styles={{ display: current === 0 ? "block" : "none" }}
|
||||||
/>
|
/>
|
||||||
<Chatpter styles={{ display: current === 1 ? "block" : "none" }} />
|
<Chatpter
|
||||||
<Appendix styles={{ display: current === 2 ? "block" : "none" }} />
|
onChange={onChapterChange}
|
||||||
|
styles={{ display: current === 1 ? "block" : "none" }}
|
||||||
|
/>
|
||||||
|
<Guide
|
||||||
|
onChange={onGuideChange}
|
||||||
|
styles={{ display: current === 2 ? "block" : "none" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: "right", marginTop: "40px" }}>
|
<div style={{ textAlign: "right", marginTop: "40px" }}>
|
||||||
|
{current === 1 && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={showDrawer}
|
||||||
|
style={{ marginRight: "12px" }}
|
||||||
|
>
|
||||||
|
视频库
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{current > 0 && (
|
{current > 0 && (
|
||||||
<Button
|
<Button
|
||||||
style={{ marginRight: "12px" }}
|
style={{ marginRight: "12px" }}
|
||||||
|
@ -68,13 +115,27 @@ const CourseCreate = () => {
|
||||||
{current === steps.length - 1 && (
|
{current === steps.length - 1 && (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => message.success("Processing complete!")}
|
onClick={onClickCreate}
|
||||||
|
disabled={!createBtnValid}
|
||||||
>
|
>
|
||||||
创建
|
创建
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Drawer title="媒体库" placement="right" onClose={onClose} open={open}>
|
||||||
|
<Input placeholder={`根据名称搜索`} onChange={onSearchChange} />
|
||||||
|
<section style={{ marginTop: 24 }}>
|
||||||
|
{mediaList.map((media: any) => {
|
||||||
|
return (
|
||||||
|
<div key={media.key} className="drawer-media-item">
|
||||||
|
<div>{media.name}</div>
|
||||||
|
<Text type="secondary">{media.key}</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,16 +15,15 @@ import {
|
||||||
Tag,
|
Tag,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { getVod } from "../../../api";
|
import { getVod } from "../../../api";
|
||||||
import { useMount } from "../../../hooks";
|
import { useMount } from "../../../hooks";
|
||||||
|
import { useMediaStore } from "../../../store/media";
|
||||||
const { Paragraph, Text } = Typography;
|
const { Paragraph, Text } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
||||||
const Library = () => {
|
const Library = () => {
|
||||||
const [dataSource, setDataSource] = useState<any>([]);
|
const [dataSource, setDataSource] = useState<any>([]);
|
||||||
const [total, setTotal] = useState<number | string>("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
|
@ -144,16 +143,16 @@ const Library = () => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setMediaList = useMediaStore((s: any) => s.setList);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 最大每次5000条数据,估计这辈子也不可能了
|
* 最大每次5000条数据,估计这辈子也不可能了
|
||||||
*/
|
*/
|
||||||
const fetchVod = () => {
|
const fetchVod = () => {
|
||||||
setLoading(true);
|
|
||||||
getVod({ offset: 0, limit: 5000 }).then((process: any) => {
|
getVod({ offset: 0, limit: 5000 }).then((process: any) => {
|
||||||
const { mediaList, total } = process;
|
const { mediaList, total } = process;
|
||||||
setDataSource(computeMediaList(mediaList));
|
setDataSource(computeMediaList(mediaList));
|
||||||
setTotal(total);
|
setMediaList(computeMediaList(mediaList));
|
||||||
setLoading(false);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -176,7 +175,7 @@ const Library = () => {
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={10} style={{ textAlign: "right" }}>
|
<Col span={10} style={{ textAlign: "right" }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Text type="secondary">共计 {total} 条媒体资源</Text>
|
<Text type="secondary">共计 {dataSource.length} 条媒体资源</Text>
|
||||||
<Tooltip title="从腾讯云VOD同步全部视频" placement="left">
|
<Tooltip title="从腾讯云VOD同步全部视频" placement="left">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => fetchVod()}
|
onClick={() => fetchVod()}
|
||||||
|
@ -193,7 +192,6 @@ const Library = () => {
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Table
|
<Table
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
loading={loading}
|
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,9 +9,9 @@ export default defineConfig({
|
||||||
port: 5174,
|
port: 5174,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://127.0.0.1:7001/api",
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||||
|
target: "http://127.0.0.1:7001/api/v1",
|
||||||
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,3 +5,4 @@ OSS_SECRET=12345
|
||||||
SECRET_ID=AKID534tZ7OvYzb2KQMwLYaVEl5FBwUtQWbU
|
SECRET_ID=AKID534tZ7OvYzb2KQMwLYaVEl5FBwUtQWbU
|
||||||
SECRET_KEY=q9HD6lQimeLp9IH5h7NRJzUpNjwxmPq5
|
SECRET_KEY=q9HD6lQimeLp9IH5h7NRJzUpNjwxmPq5
|
||||||
SUBAPPID=1500018521
|
SUBAPPID=1500018521
|
||||||
|
SUBAPPID_OSS=1500018944
|
|
@ -35,6 +35,7 @@ export default (appInfo: MidwayAppInfo): MidwayConfig => {
|
||||||
keys: '1676532942172_2248',
|
keys: '1676532942172_2248',
|
||||||
koa: {
|
koa: {
|
||||||
port: 7001,
|
port: 7001,
|
||||||
|
globalPrefix: '/api/v1',
|
||||||
},
|
},
|
||||||
upload: {
|
upload: {
|
||||||
// mode: UploadMode, 默认为file,即上传到服务器临时目录,可以配置为 stream
|
// mode: UploadMode, 默认为file,即上传到服务器临时目录,可以配置为 stream
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import { Inject, Controller, Post, Body, Files, Fields } from '@midwayjs/core';
|
import { Inject, Controller, Post, Body } from '@midwayjs/core';
|
||||||
import { Context } from '@midwayjs/koa';
|
import { Context } from '@midwayjs/koa';
|
||||||
import { Validate } from '@midwayjs/validate';
|
import { Validate } from '@midwayjs/validate';
|
||||||
import { UserDTO } from '../dto/user.dto';
|
import { UserDTO } from '../dto/user.dto';
|
||||||
import * as tencentcloud from 'tencentcloud-sdk-nodejs';
|
@Controller('/')
|
||||||
import { VodSearchDTO } from '../dto/vod.dto';
|
|
||||||
import { BizCode } from '../biz/code';
|
|
||||||
import { uploadImagePromise } from '../util/vod';
|
|
||||||
|
|
||||||
@Controller('/api')
|
|
||||||
export class APIController {
|
export class APIController {
|
||||||
@Inject()
|
@Inject()
|
||||||
ctx: Context;
|
ctx: Context;
|
||||||
|
@ -19,51 +14,4 @@ export class APIController {
|
||||||
async getUser(@Body() user: UserDTO) {
|
async getUser(@Body() user: UserDTO) {
|
||||||
return { success: true, message: 'OK', data: user };
|
return { success: true, message: 'OK', data: user };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 腾讯媒资管理查询
|
|
||||||
* API调用demo: https://console.cloud.tencent.com/api/explorer?Product=vod&Version=2018-07-17&Action=SearchMedia
|
|
||||||
* 最大返回5000条数据
|
|
||||||
*/
|
|
||||||
@Post('/vod')
|
|
||||||
async vod(@Body() param: VodSearchDTO) {
|
|
||||||
const { offset: Offset = 0, limit: Limit = 5000 } = param;
|
|
||||||
const VodClient = tencentcloud.vod.v20180717.Client;
|
|
||||||
const clientConfig = {
|
|
||||||
credential: {
|
|
||||||
secretId: process.env.SECRET_ID,
|
|
||||||
secretKey: process.env.SECRET_KEY,
|
|
||||||
},
|
|
||||||
profile: {
|
|
||||||
httpProfile: {
|
|
||||||
endpoint: 'vod.tencentcloudapi.com',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const client = new VodClient(clientConfig);
|
|
||||||
const params = {
|
|
||||||
SubAppId: +process.env.SUBAPPID,
|
|
||||||
Categories: ['Video'],
|
|
||||||
Offset,
|
|
||||||
Limit,
|
|
||||||
};
|
|
||||||
return await client.SearchMedia(params).then(
|
|
||||||
data => ({ code: BizCode.OK, data }),
|
|
||||||
err => {
|
|
||||||
this.ctx.logger.error(err);
|
|
||||||
throw new Error(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/upload')
|
|
||||||
async upload(@Files() files, @Fields() fields) {
|
|
||||||
const tmpPath = files[0].data;
|
|
||||||
return await uploadImagePromise(tmpPath)
|
|
||||||
.then(data => ({ code: BizCode.OK, data }))
|
|
||||||
.catch(err => {
|
|
||||||
this.ctx.logger.error(err);
|
|
||||||
throw new Error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
79
apps/server/src/controller/vod.controller.ts
Normal file
79
apps/server/src/controller/vod.controller.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { Inject, Post, Body, Files, Controller } from '@midwayjs/core';
|
||||||
|
import { Context } from 'koa';
|
||||||
|
import { BizCode } from '../biz/code';
|
||||||
|
import * as tencentcloud from 'tencentcloud-sdk-nodejs';
|
||||||
|
import { VodSearchDTO } from '../dto/vod.dto';
|
||||||
|
import { uploadImagePromise } from '../util/vod';
|
||||||
|
import { IVodResponse } from '../interface';
|
||||||
|
|
||||||
|
@Controller('/vod')
|
||||||
|
export class VodController {
|
||||||
|
@Inject()
|
||||||
|
ctx: Context;
|
||||||
|
/**
|
||||||
|
* 腾讯媒资管理查询
|
||||||
|
* API调用demo: https://console.cloud.tencent.com/api/explorer?Product=vod&Version=2018-07-17&Action=SearchMedia
|
||||||
|
* 最大返回5000条数据
|
||||||
|
*/
|
||||||
|
@Post('/media/select')
|
||||||
|
async getCourseMediaList(@Body() param: VodSearchDTO) {
|
||||||
|
const { offset: Offset = 0, limit: Limit = 5000 } = param;
|
||||||
|
const VodClient = tencentcloud.vod.v20180717.Client;
|
||||||
|
const clientConfig = {
|
||||||
|
credential: {
|
||||||
|
secretId: process.env.SECRET_ID,
|
||||||
|
secretKey: process.env.SECRET_KEY,
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
httpProfile: {
|
||||||
|
endpoint: 'vod.tencentcloudapi.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const client = new VodClient(clientConfig);
|
||||||
|
const params = {
|
||||||
|
SubAppId: +process.env.SUBAPPID,
|
||||||
|
Categories: ['Video'],
|
||||||
|
Offset,
|
||||||
|
Limit,
|
||||||
|
};
|
||||||
|
return await client.SearchMedia(params).then(
|
||||||
|
data => ({ code: BizCode.OK, data }),
|
||||||
|
err => {
|
||||||
|
this.ctx.logger.error(err);
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台管理:上传课程封面图
|
||||||
|
*/
|
||||||
|
@Post('/course/cover/upload')
|
||||||
|
async uploadCourseCoverImage(@Files() files) {
|
||||||
|
const tmpPath = files[0].data;
|
||||||
|
return await uploadImagePromise(tmpPath, +process.env.SUBAPPID)
|
||||||
|
.then((data: IVodResponse) => ({ code: BizCode.OK, data }))
|
||||||
|
.catch(err => {
|
||||||
|
this.ctx.logger.error(err);
|
||||||
|
throw new Error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片单个上传
|
||||||
|
*/
|
||||||
|
@Post('/oss/image/upload')
|
||||||
|
async ossUploadImage(@Files() files) {
|
||||||
|
const tmpPath = files[0].data;
|
||||||
|
return await uploadImagePromise(tmpPath, +process.env.SUBAPPID_OSS)
|
||||||
|
.then((data: IVodResponse) => ({
|
||||||
|
code: BizCode.OK,
|
||||||
|
data: { name: data.FileId, url: data.MediaUrl },
|
||||||
|
}))
|
||||||
|
.catch(err => {
|
||||||
|
this.ctx.logger.error(err);
|
||||||
|
return { code: BizCode.ERROR, msg: err };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
import { Catch, httpError, MidwayHttpError } from '@midwayjs/core';
|
import { Catch, httpError, MidwayHttpError } from '@midwayjs/core';
|
||||||
import { Context } from '@midwayjs/koa';
|
import { Context } from '@midwayjs/koa';
|
||||||
|
import { BizCode } from '../biz/code';
|
||||||
|
|
||||||
@Catch(httpError.NotFoundError)
|
@Catch(httpError.NotFoundError)
|
||||||
export class NotFoundFilter {
|
export class NotFoundFilter {
|
||||||
async catch(err: MidwayHttpError, ctx: Context) {
|
async catch(err: MidwayHttpError, ctx: Context) {
|
||||||
// 404 错误会到这里
|
// 404 错误会到这里
|
||||||
// ctx.redirect('/404.html');
|
// ctx.redirect('/404.html');
|
||||||
ctx.body = '迷路了'
|
ctx.body = { code: BizCode.ERROR, msg: err };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,10 @@
|
||||||
export interface IUserOptions {
|
export interface IUserOptions {
|
||||||
uid: number;
|
uid: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IVodResponse {
|
||||||
|
CoverUrl: string;
|
||||||
|
FileId: string;
|
||||||
|
MediaUrl: string;
|
||||||
|
RequestId: string;
|
||||||
|
}
|
||||||
|
|
|
@ -4,12 +4,12 @@ import { VodUploadClient, VodUploadRequest } from 'vod-node-sdk';
|
||||||
* 上传课程封面图片
|
* 上传课程封面图片
|
||||||
* @param {string} tmpPath 图片上传midwayjs缓存的地址
|
* @param {string} tmpPath 图片上传midwayjs缓存的地址
|
||||||
*/
|
*/
|
||||||
export const uploadImagePromise = (tmpPath: string) => {
|
export const uploadImagePromise = (tmpPath: string, subAppId: number) => {
|
||||||
const { SECRET_ID, SECRET_KEY } = process.env;
|
const { SECRET_ID, SECRET_KEY } = process.env;
|
||||||
const client = new VodUploadClient(SECRET_ID, SECRET_KEY);
|
const client = new VodUploadClient(SECRET_ID, SECRET_KEY);
|
||||||
const req = new VodUploadRequest();
|
const req = new VodUploadRequest();
|
||||||
req.MediaFilePath = tmpPath;
|
req.MediaFilePath = tmpPath;
|
||||||
req.SubAppId = +process.env.SUBAPPID;
|
req.SubAppId = subAppId;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
client.upload('ap-shanghai', req, (err, data) => {
|
client.upload('ap-shanghai', req, (err, data) => {
|
||||||
err ? reject(err) : resolve(data);
|
err ? reject(err) : resolve(data);
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
function Material() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return <div>{location.pathname}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Material;
|
|
@ -1,5 +0,0 @@
|
||||||
function Material() {
|
|
||||||
return <div>这里放课程资料。例如 指标 等文件下载</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Material;
|
|
|
@ -2,21 +2,21 @@ import { useEffect, useState } from "react";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
import { ResizeBox, Space, Result, Button } from "@arco-design/web-react";
|
import { ResizeBox, Space, Result, Button } from "@arco-design/web-react";
|
||||||
import { Icon } from "@ricons/utils";
|
import { Icon } from "@ricons/utils";
|
||||||
import Material from "./components/Material";
|
import Guide from "./components/Guide";
|
||||||
import { useMount } from "../../hook";
|
import { useMount } from "../../hook";
|
||||||
import Player from "./components/DPlayer";
|
import Player from "./components/DPlayer";
|
||||||
|
|
||||||
function CourseDetail() {
|
function CourseDetail() {
|
||||||
const [toc, setToc] = useState([
|
const [toc, setToc] = useState([
|
||||||
{
|
{
|
||||||
title: "准备",
|
title: "起步",
|
||||||
level: 1,
|
level: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "资料下载",
|
title: "导读",
|
||||||
level: 2,
|
level: 2,
|
||||||
active: true,
|
active: true,
|
||||||
view: <Material />,
|
view: <Guide />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "第一讲:特殊K线的量化描述",
|
title: "第一讲:特殊K线的量化描述",
|
||||||
|
|
|
@ -52,6 +52,8 @@
|
||||||
"dayjs": "1.11.7"
|
"dayjs": "1.11.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"object-hash": "^3.0.0"
|
"object-hash": "^3.0.0",
|
||||||
|
"vditor": "3.9.0",
|
||||||
|
"zustand": "4.3.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -40,11 +40,15 @@ importers:
|
||||||
ts-loader: 9.4.2
|
ts-loader: 9.4.2
|
||||||
typescript: ^4.9.5
|
typescript: ^4.9.5
|
||||||
url-loader: 4.1.1
|
url-loader: 4.1.1
|
||||||
|
vditor: 3.9.0
|
||||||
webpack: ^5.75.0
|
webpack: ^5.75.0
|
||||||
webpack-bundle-analyzer: 4.8.0
|
webpack-bundle-analyzer: 4.8.0
|
||||||
webpack-cli: ^5.0.1
|
webpack-cli: ^5.0.1
|
||||||
|
zustand: 4.3.6
|
||||||
dependencies:
|
dependencies:
|
||||||
object-hash: registry.npmmirror.com/object-hash/3.0.0
|
object-hash: registry.npmmirror.com/object-hash/3.0.0
|
||||||
|
vditor: registry.npmmirror.com/vditor/3.9.0
|
||||||
|
zustand: registry.npmmirror.com/zustand/4.3.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@babel/core': registry.npmmirror.com/@babel/core/7.21.0
|
'@babel/core': registry.npmmirror.com/@babel/core/7.21.0
|
||||||
'@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.21.0
|
'@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.21.0
|
||||||
|
@ -7206,6 +7210,12 @@ packages:
|
||||||
wrappy: registry.npmmirror.com/wrappy/1.0.2
|
wrappy: registry.npmmirror.com/wrappy/1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
registry.npmmirror.com/diff-match-patch/1.0.5:
|
||||||
|
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz}
|
||||||
|
name: diff-match-patch
|
||||||
|
version: 1.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/diff/4.0.2:
|
registry.npmmirror.com/diff/4.0.2:
|
||||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz}
|
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz}
|
||||||
name: diff
|
name: diff
|
||||||
|
@ -13605,6 +13615,14 @@ packages:
|
||||||
prepend-http: registry.npmmirror.com/prepend-http/2.0.0
|
prepend-http: registry.npmmirror.com/prepend-http/2.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
registry.npmmirror.com/use-sync-external-store/1.2.0:
|
||||||
|
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz}
|
||||||
|
name: use-sync-external-store
|
||||||
|
version: 1.2.0
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/util-deprecate/1.0.2:
|
registry.npmmirror.com/util-deprecate/1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz}
|
||||||
name: util-deprecate
|
name: util-deprecate
|
||||||
|
@ -13650,6 +13668,14 @@ packages:
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
registry.npmmirror.com/vditor/3.9.0:
|
||||||
|
resolution: {integrity: sha512-CLLtrexUY/LGN1Lp1iu242Uq9GuNP98UTXFRY9hjTNFkpVH9L4M3jrQ9yIZ711zYwsl78GxKeskuU7WieA96ow==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/vditor/-/vditor-3.9.0.tgz}
|
||||||
|
name: vditor
|
||||||
|
version: 3.9.0
|
||||||
|
dependencies:
|
||||||
|
diff-match-patch: registry.npmmirror.com/diff-match-patch/1.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/verror/1.10.0:
|
registry.npmmirror.com/verror/1.10.0:
|
||||||
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz}
|
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz}
|
||||||
name: verror
|
name: verror
|
||||||
|
@ -14118,3 +14144,20 @@ packages:
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
registry.npmmirror.com/zustand/4.3.6:
|
||||||
|
resolution: {integrity: sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/zustand/-/zustand-4.3.6.tgz}
|
||||||
|
name: zustand
|
||||||
|
version: 4.3.6
|
||||||
|
engines: {node: '>=12.7.0'}
|
||||||
|
peerDependencies:
|
||||||
|
immer: '>=9.0'
|
||||||
|
react: '>=16.8'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store: registry.npmmirror.com/use-sync-external-store/1.2.0
|
||||||
|
dev: false
|
||||||
|
|
Loading…
Reference in New Issue
Block a user