feat: 课程创建
This commit is contained in:
parent
d649a14244
commit
220b46091d
9
apps/admin/src/api/dto.ts
Normal file
9
apps/admin/src/api/dto.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export interface IgetVodRequest {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetVodeResponse {
|
||||||
|
MediaInfoSet: any[];
|
||||||
|
TotalCount: number;
|
||||||
|
}
|
|
@ -1,3 +1,9 @@
|
||||||
import axios from "axios";
|
import R from "./request";
|
||||||
|
import P from "./process";
|
||||||
|
import { IgetVodRequest } from "./dto";
|
||||||
|
|
||||||
export const getVod = () => axios.post("/api/vod", { offset: 0, limit: 5000 });
|
/**
|
||||||
|
* 腾讯vod媒资
|
||||||
|
*/
|
||||||
|
export const getVod = (p: IgetVodRequest) =>
|
||||||
|
R.post("/api/vod", { ...p }).then((d: any) => P.getVod(d.data));
|
||||||
|
|
23
apps/admin/src/api/process.ts
Normal file
23
apps/admin/src/api/process.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { IGetVodeResponse } from "./dto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清洗数据
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* 腾讯vod媒资
|
||||||
|
*/
|
||||||
|
getVod: (p: IGetVodeResponse) => {
|
||||||
|
const { TotalCount: total, MediaInfoSet } = p;
|
||||||
|
const mediaList = MediaInfoSet.map((item) => {
|
||||||
|
return {
|
||||||
|
FileId: item.FileId,
|
||||||
|
AdaptStream:
|
||||||
|
item.AdaptiveDynamicStreamingInfo.AdaptiveDynamicStreamingSet[0],
|
||||||
|
BasicInfo: item.BasicInfo,
|
||||||
|
MetaData: item.MetaData,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { total, mediaList };
|
||||||
|
},
|
||||||
|
};
|
34
apps/admin/src/api/request.ts
Normal file
34
apps/admin/src/api/request.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { message } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
baseURL: "",
|
||||||
|
timeout: 1000 * 15,
|
||||||
|
headers: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = axios.create(config);
|
||||||
|
|
||||||
|
instance.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add a response interceptor
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
if (response.data.code === 10000)
|
||||||
|
message.success(`接口: ${response.config.url}, 请求成功`);
|
||||||
|
return response?.data;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
message.error(error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default instance;
|
5
apps/admin/src/view/Course/Create/Appendix.tsx
Normal file
5
apps/admin/src/view/Course/Create/Appendix.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const Appendix = () => {
|
||||||
|
return <div>附件</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Appendix;
|
30
apps/admin/src/view/Course/Create/BasicForm/index.less
Normal file
30
apps/admin/src/view/Course/Create/BasicForm/index.less
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
.preview-course {
|
||||||
|
position: relative;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 100%;
|
||||||
|
.mask {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 60%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
p {
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
&.title {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
font-size: 22px;
|
||||||
|
padding: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
&.summary {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 6px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
apps/admin/src/view/Course/Create/BasicForm/index.tsx
Normal file
112
apps/admin/src/view/Course/Create/BasicForm/index.tsx
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { InboxOutlined } from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Row,
|
||||||
|
Upload,
|
||||||
|
UploadProps,
|
||||||
|
} from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
|
import "./index.less";
|
||||||
|
const { Dragger } = Upload;
|
||||||
|
const { Meta } = Card;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onChange: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BasicForm = (props: IProps) => {
|
||||||
|
const [preview, setPreivew] = useState({
|
||||||
|
coverUrl: "",
|
||||||
|
title: "标题",
|
||||||
|
summary: "摘要",
|
||||||
|
});
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const onValuesChange = (_: any, all: any) => {
|
||||||
|
setPreivew((p) => ({ ...p, ...all }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const coverDragger: UploadProps = {
|
||||||
|
name: "file",
|
||||||
|
multiple: true,
|
||||||
|
action: "/api/upload",
|
||||||
|
onChange(info) {
|
||||||
|
const { status } = info.file;
|
||||||
|
if (status !== "uploading") console.log(info.file, info.fileList);
|
||||||
|
if (status === "done") {
|
||||||
|
const { code, data } = info.file.response;
|
||||||
|
if (code === 10000) {
|
||||||
|
message.success(`${info.file.name} 文件上传成功`);
|
||||||
|
const { MediaUrl } = data;
|
||||||
|
setPreivew((p) => ({ ...p, coverUrl: MediaUrl }));
|
||||||
|
}
|
||||||
|
} else if (status === "error") {
|
||||||
|
message.error(`${info.file.name} 文件上传失败`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrop(e) {
|
||||||
|
console.log("Dropped files", e.dataTransfer.files);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row gutter={24}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form form={form} onValuesChange={onValuesChange}>
|
||||||
|
<Form.Item>
|
||||||
|
<Dragger {...coverDragger} maxCount={1}>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<InboxOutlined />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
||||||
|
<p className="ant-upload-hint">支持单个或批量上传</p>
|
||||||
|
</Dragger>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="title" rules={[{ required: true }]}>
|
||||||
|
<Input type="text" placeholder="标题" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="summary"
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="摘要"
|
||||||
|
style={{ height: 120, resize: "none" }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<div
|
||||||
|
className="preview-course"
|
||||||
|
style={{
|
||||||
|
backgroundImage: !preview.coverUrl
|
||||||
|
? `linear-gradient(
|
||||||
|
to right,
|
||||||
|
#e95659,
|
||||||
|
#e15084,
|
||||||
|
#c55aaa,
|
||||||
|
#976bc4,
|
||||||
|
#5678ce
|
||||||
|
)`
|
||||||
|
: `url(${preview.coverUrl})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="mask">
|
||||||
|
<p className="title">{preview.title}</p>
|
||||||
|
<p className="summary">{preview.summary} </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BasicForm;
|
5
apps/admin/src/view/Course/Create/MediaBind.tsx
Normal file
5
apps/admin/src/view/Course/Create/MediaBind.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const MediaBind = () => {
|
||||||
|
return <div>123</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MediaBind;
|
8
apps/admin/src/view/Course/Create/index.less
Normal file
8
apps/admin/src/view/Course/Create/index.less
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.create-course {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.content {
|
||||||
|
padding: 20px 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,67 @@
|
||||||
import { Button, Form, Input, InputNumber, Row, Space } from "antd";
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Row,
|
||||||
|
Space,
|
||||||
|
Steps,
|
||||||
|
} from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Appendix from "./Appendix";
|
||||||
|
import BasicForm from "./BasicForm";
|
||||||
|
import "./index.less";
|
||||||
|
import MediaBind from "./MediaBind";
|
||||||
|
|
||||||
const CourseCreate = () => {
|
const CourseCreate = () => {
|
||||||
|
const [current, setCurrent] = useState(0);
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
title: "基本信息",
|
||||||
|
content: <BasicForm />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "媒体资源",
|
||||||
|
content: <MediaBind />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "附件",
|
||||||
|
content: <Appendix />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const items = steps.map((item) => ({ key: item.title, title: item.title }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="create-course">
|
||||||
<Form>
|
<Steps current={current} items={items} />
|
||||||
|
<div className="content">{steps[current].content}</div>
|
||||||
|
<div style={{ textAlign: "right" }}>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
margin: "0 8px",
|
||||||
|
visibility: current > 0 ? "visible" : "hidden",
|
||||||
|
}}
|
||||||
|
onClick={() => setCurrent(current - 1)}
|
||||||
|
>
|
||||||
|
上一步
|
||||||
|
</Button>
|
||||||
|
{current < steps.length - 1 && (
|
||||||
|
<Button type="primary" onClick={() => setCurrent(current + 1)}>
|
||||||
|
下一步
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{current === steps.length - 1 && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => message.success("Processing complete!")}
|
||||||
|
>
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* <Form>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
wrapperCol={{ span: 24 }}
|
wrapperCol={{ span: 24 }}
|
||||||
name="title"
|
name="title"
|
||||||
|
@ -11,13 +69,11 @@ const CourseCreate = () => {
|
||||||
>
|
>
|
||||||
<Input placeholder="标题" />
|
<Input placeholder="标题" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item><InputNumber placeholder="售价" /></Form.Item>
|
||||||
<InputNumber placeholder="售价" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary">提交</Button>
|
<Button type="primary">提交</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,69 +10,159 @@ import {
|
||||||
Segmented,
|
Segmented,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Image,
|
Image,
|
||||||
|
Typography,
|
||||||
|
Modal,
|
||||||
|
Tag,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { useEffect } from "react";
|
import dayjs from "dayjs";
|
||||||
|
import { useState } from "react";
|
||||||
import { getVod } from "../../../api";
|
import { getVod } from "../../../api";
|
||||||
import { useMount } from "../../../hooks";
|
import { useMount } from "../../../hooks";
|
||||||
|
const { Paragraph, Text } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
||||||
const Library = () => {
|
const Library = () => {
|
||||||
const dataSource = [
|
const [dataSource, setDataSource] = useState<any>([]);
|
||||||
{
|
const [total, setTotal] = useState<number | string>("");
|
||||||
key: "1",
|
const [loading, setLoading] = useState(false);
|
||||||
name: "K线篇1--特殊K线的量化描述.mp4",
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
duration: 845,
|
|
||||||
|
|
||||||
cover: (
|
const colors = [
|
||||||
<Image
|
"magenta",
|
||||||
height={60}
|
"red",
|
||||||
src="https://1500018521.vod2.myqcloud.com/a28b6648vodtranssh1500018521/8a1352da243791580308966554/coverBySnapshot_10_0.jpg"
|
"volcano",
|
||||||
/>
|
"orange",
|
||||||
),
|
"gold",
|
||||||
},
|
"green",
|
||||||
{
|
"cyan",
|
||||||
key: "2",
|
].reverse();
|
||||||
name: "Vite + React + TS - Google Chrome 2023-02-15 09-55-08.mp4",
|
|
||||||
duration: 16,
|
|
||||||
cover: (
|
|
||||||
<Image
|
|
||||||
height={60}
|
|
||||||
src="https://1500018521.vod2.myqcloud.com/a28b6648vodtranssh1500018521/29226db4243791580097740418/coverBySnapshot/coverBySnapshot_10_0.jpg"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: "媒体文件名称",
|
title: "媒体文件名称",
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
key: "name",
|
key: "name",
|
||||||
|
render: (_: any, record: any) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row>{record.name}</Row>
|
||||||
|
<Row style={{ paddingTop: "5px" }}>
|
||||||
|
{record.m3u8SubStreamList.map((item: any, index: number) => (
|
||||||
|
<Tag color={colors[index]}>{item}</Tag>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
<Row style={{ paddingTop: "5px" }}>
|
||||||
|
<Text type="secondary">时长: {record.duration}s</Text>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "时长",
|
title: "FileID",
|
||||||
dataIndex: "duration",
|
dataIndex: "key",
|
||||||
key: "duration",
|
key: "key",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "封面图片地址",
|
title: "m3u8 大小",
|
||||||
|
dataIndex: "m3u8Size",
|
||||||
|
key: "m3u8Size",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "创建时间",
|
||||||
|
dataIndex: "createtime",
|
||||||
|
key: "createtime",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: "m3u8",
|
||||||
|
dataIndex: "m3u8",
|
||||||
|
key: "m3u8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "视频封面图",
|
||||||
dataIndex: "cover",
|
dataIndex: "cover",
|
||||||
key: "cover",
|
key: "cover",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
dataIndex: "operation",
|
||||||
|
key: "operation",
|
||||||
|
render: (_: any, record: any) => {
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<a href={record.mp4} target="_blank">
|
||||||
|
预览
|
||||||
|
</a>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
|
||||||
|
console.log("selectedRowKeys changed: ", newSelectedRowKeys);
|
||||||
|
setSelectedRowKeys(newSelectedRowKeys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowSelection = { selectedRowKeys, onChange: onSelectChange };
|
||||||
|
|
||||||
const onSearch = (value: string) => console.log(value);
|
const onSearch = (value: string) => console.log(value);
|
||||||
|
|
||||||
useMount(() => {
|
const computeMediaList = (mediaList: any) => {
|
||||||
getVod().then((res) => {
|
return mediaList.map((m: any) => ({
|
||||||
console.log(res);
|
key: m.FileId,
|
||||||
|
mp4: m.BasicInfo.MediaUrl,
|
||||||
|
m3u8: (
|
||||||
|
<Paragraph style={{ margin: 0 }} copyable={{ text: m.AdaptStream.Url }}>
|
||||||
|
复制
|
||||||
|
</Paragraph>
|
||||||
|
),
|
||||||
|
m3u8Size: (m.AdaptStream.Size / 1024 / 1024).toFixed(2) + " MB",
|
||||||
|
m3u8SubStreamList: m.AdaptStream.SubStreamSet.map(
|
||||||
|
(i: any) => i.Height + "p"
|
||||||
|
),
|
||||||
|
name: m.BasicInfo.Name,
|
||||||
|
duration: m.MetaData.Duration,
|
||||||
|
cover: (
|
||||||
|
<Paragraph
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
copyable={{ text: m.BasicInfo.CoverUrl }}
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</Paragraph>
|
||||||
|
),
|
||||||
|
status: m.BasicInfo.Status,
|
||||||
|
createtime: dayjs(m.BasicInfo.CreateTime).format("YYYY-MM-DD HH:mm:ss"),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大每次5000条数据,估计这辈子也不可能了
|
||||||
|
*/
|
||||||
|
const fetchVod = () => {
|
||||||
|
setLoading(true);
|
||||||
|
getVod({ offset: 0, limit: 5000 }).then((process: any) => {
|
||||||
|
const { mediaList, total } = process;
|
||||||
|
setDataSource(computeMediaList(mediaList));
|
||||||
|
setTotal(total);
|
||||||
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useMount(() => {
|
||||||
|
fetchVod();
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={20}>
|
<Col span={14}>
|
||||||
<Space>
|
<Space>
|
||||||
<Segmented options={["生", "熟"]} />
|
<Segmented options={["生", "熟"]} />
|
||||||
<Search
|
<Search
|
||||||
|
@ -82,19 +172,29 @@ const Library = () => {
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4} style={{ textAlign: "right" }}>
|
<Col span={10} style={{ textAlign: "right" }}>
|
||||||
|
<Space>
|
||||||
|
<Text type="secondary">共计 {total} 条媒体资源</Text>
|
||||||
<Tooltip title="从腾讯云VOD同步全部视频" placement="left">
|
<Tooltip title="从腾讯云VOD同步全部视频" placement="left">
|
||||||
<Button type="primary" icon={<CloudSyncOutlined />}>
|
<Button
|
||||||
|
onClick={() => fetchVod()}
|
||||||
|
type="primary"
|
||||||
|
icon={<CloudSyncOutlined />}
|
||||||
|
>
|
||||||
同步
|
同步
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{ marginTop: "16px" }}>
|
<Row style={{ marginTop: "16px" }}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Card>
|
<Table
|
||||||
<Table dataSource={dataSource} columns={columns} />
|
rowSelection={rowSelection}
|
||||||
</Card>
|
loading={loading}
|
||||||
|
dataSource={dataSource}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default defineConfig({
|
||||||
port: 5174,
|
port: 5174,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://172.20.160.221:7001/api",
|
target: "http://127.0.0.1:7001/api",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,3 +4,4 @@ OSS_SECRET=12345
|
||||||
|
|
||||||
SECRET_ID=AKID534tZ7OvYzb2KQMwLYaVEl5FBwUtQWbU
|
SECRET_ID=AKID534tZ7OvYzb2KQMwLYaVEl5FBwUtQWbU
|
||||||
SECRET_KEY=q9HD6lQimeLp9IH5h7NRJzUpNjwxmPq5
|
SECRET_KEY=q9HD6lQimeLp9IH5h7NRJzUpNjwxmPq5
|
||||||
|
SUBAPPID=1500018521
|
|
@ -16,6 +16,7 @@
|
||||||
"@midwayjs/static-file": "^3.0.0",
|
"@midwayjs/static-file": "^3.0.0",
|
||||||
"@midwayjs/redis": "^3.0.0",
|
"@midwayjs/redis": "^3.0.0",
|
||||||
"@midwayjs/typeorm": "^3.0.0",
|
"@midwayjs/typeorm": "^3.0.0",
|
||||||
|
"@midwayjs/upload": "3.10.14",
|
||||||
"mongoose": "^6.0.7",
|
"mongoose": "^6.0.7",
|
||||||
"@midwayjs/typegoose": "3.0.0",
|
"@midwayjs/typegoose": "3.0.0",
|
||||||
"@typegoose/typegoose": "10.1.1",
|
"@typegoose/typegoose": "10.1.1",
|
||||||
|
@ -23,7 +24,8 @@
|
||||||
"mysql2": "3.0.1",
|
"mysql2": "3.0.1",
|
||||||
"dotenv": "16.0.3",
|
"dotenv": "16.0.3",
|
||||||
"jsonwebtoken": "9.0.0",
|
"jsonwebtoken": "9.0.0",
|
||||||
"tencentcloud-sdk-nodejs": "4.0.552"
|
"tencentcloud-sdk-nodejs": "4.0.552",
|
||||||
|
"vod-node-sdk": "1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@midwayjs/cli": "^2.0.0",
|
"@midwayjs/cli": "^2.0.0",
|
||||||
|
|
7
apps/server/src/biz/code.ts
Normal file
7
apps/server/src/biz/code.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* 业务状态码
|
||||||
|
*/
|
||||||
|
export enum BizCode {
|
||||||
|
OK = 10000,
|
||||||
|
ERROR = 20000,
|
||||||
|
}
|
|
@ -1,4 +1,34 @@
|
||||||
import { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';
|
import { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';
|
||||||
|
import { uploadWhiteList } from '@midwayjs/upload';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
// '.jpg',
|
||||||
|
// '.jpeg',
|
||||||
|
// '.png',
|
||||||
|
// '.gif',
|
||||||
|
// '.bmp',
|
||||||
|
// '.wbmp',
|
||||||
|
// '.webp',
|
||||||
|
// '.tif',
|
||||||
|
// '.psd',
|
||||||
|
// '.svg',
|
||||||
|
// '.js',
|
||||||
|
// '.jsx',
|
||||||
|
// '.json',
|
||||||
|
// '.css',
|
||||||
|
// '.less',
|
||||||
|
// '.html',
|
||||||
|
// '.htm',
|
||||||
|
// '.xml',
|
||||||
|
// '.pdf',
|
||||||
|
// '.zip',
|
||||||
|
// '.gz',
|
||||||
|
// '.tgz',
|
||||||
|
// '.gzip',
|
||||||
|
// '.mp3',
|
||||||
|
// '.mp4',
|
||||||
|
// '.avi',
|
||||||
|
|
||||||
export default (appInfo: MidwayAppInfo): MidwayConfig => {
|
export default (appInfo: MidwayAppInfo): MidwayConfig => {
|
||||||
return {
|
return {
|
||||||
|
@ -6,5 +36,19 @@ export default (appInfo: MidwayAppInfo): MidwayConfig => {
|
||||||
koa: {
|
koa: {
|
||||||
port: 7001,
|
port: 7001,
|
||||||
},
|
},
|
||||||
|
upload: {
|
||||||
|
// mode: UploadMode, 默认为file,即上传到服务器临时目录,可以配置为 stream
|
||||||
|
mode: 'file',
|
||||||
|
// fileSize: string, 最大上传文件大小,默认为 10mb
|
||||||
|
fileSize: '10mb',
|
||||||
|
// whitelist: string[],文件扩展名白名单
|
||||||
|
whitelist: uploadWhiteList,
|
||||||
|
// tmpdir: string,上传的文件临时存储路径
|
||||||
|
tmpdir: join(tmpdir(), 'midway-upload-files'),
|
||||||
|
// cleanTimeout: number,上传的文件在临时目录中多久之后自动删除,默认为 5 分钟
|
||||||
|
cleanTimeout: 5 * 60 * 1000,
|
||||||
|
// base64: boolean,设置原始body是否是base64格式,默认为false,一般用于腾讯云的兼容
|
||||||
|
base64: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import * as staticFile from '@midwayjs/static-file';
|
||||||
import * as orm from '@midwayjs/typeorm';
|
import * as orm from '@midwayjs/typeorm';
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
import * as redis from '@midwayjs/redis';
|
import * as redis from '@midwayjs/redis';
|
||||||
|
import * as upload from '@midwayjs/upload';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { DefaultErrorFilter } from './filter/default.filter';
|
import { DefaultErrorFilter } from './filter/default.filter';
|
||||||
import { NotFoundFilter } from './filter/notfound.filter';
|
import { NotFoundFilter } from './filter/notfound.filter';
|
||||||
|
@ -21,6 +22,7 @@ dotenv.config();
|
||||||
staticFile,
|
staticFile,
|
||||||
orm,
|
orm,
|
||||||
redis,
|
redis,
|
||||||
|
upload,
|
||||||
{
|
{
|
||||||
component: info,
|
component: info,
|
||||||
enabledEnvironment: ['local'],
|
enabledEnvironment: ['local'],
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { Inject, Controller, Post, Body } from '@midwayjs/core';
|
import { Inject, Controller, Post, Body, Files, Fields } 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';
|
import * as tencentcloud from 'tencentcloud-sdk-nodejs';
|
||||||
import { VodSearchDTO } from '../dto/vod.dto';
|
import { VodSearchDTO } from '../dto/vod.dto';
|
||||||
|
import { BizCode } from '../biz/code';
|
||||||
|
import { uploadImagePromise } from '../util/vod';
|
||||||
|
|
||||||
@Controller('/api')
|
@Controller('/api')
|
||||||
export class APIController {
|
export class APIController {
|
||||||
|
@ -39,10 +41,29 @@ export class APIController {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const client = new VodClient(clientConfig);
|
const client = new VodClient(clientConfig);
|
||||||
const params = { SubAppId: 1500018521, Offset, Limit };
|
const params = {
|
||||||
|
SubAppId: +process.env.SUBAPPID,
|
||||||
|
Categories: ['Video'],
|
||||||
|
Offset,
|
||||||
|
Limit,
|
||||||
|
};
|
||||||
return await client.SearchMedia(params).then(
|
return await client.SearchMedia(params).then(
|
||||||
data => data,
|
data => ({ code: BizCode.OK, data }),
|
||||||
err => this.ctx.logger.error(err)
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
18
apps/server/src/util/vod.ts
Normal file
18
apps/server/src/util/vod.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { VodUploadClient, VodUploadRequest } from 'vod-node-sdk';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传课程封面图片
|
||||||
|
* @param {string} tmpPath 图片上传midwayjs缓存的地址
|
||||||
|
*/
|
||||||
|
export const uploadImagePromise = (tmpPath: string) => {
|
||||||
|
const { SECRET_ID, SECRET_KEY } = process.env;
|
||||||
|
const client = new VodUploadClient(SECRET_ID, SECRET_KEY);
|
||||||
|
const req = new VodUploadRequest();
|
||||||
|
req.MediaFilePath = tmpPath;
|
||||||
|
req.SubAppId = +process.env.SUBAPPID;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
client.upload('ap-shanghai', req, (err, data) => {
|
||||||
|
err ? reject(err) : resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -16,7 +16,6 @@
|
||||||
"@arco-design/web-react": "2.45.0",
|
"@arco-design/web-react": "2.45.0",
|
||||||
"@ricons/fluent": "0.12.0",
|
"@ricons/fluent": "0.12.0",
|
||||||
"@ricons/utils": "0.1.6",
|
"@ricons/utils": "0.1.6",
|
||||||
"dayjs": "1.11.7",
|
|
||||||
"dplayer": "1.27.1"
|
"dplayer": "1.27.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -48,7 +48,8 @@
|
||||||
"rollup-plugin-babel": "4.4.0",
|
"rollup-plugin-babel": "4.4.0",
|
||||||
"@babel/core": "7.21.0",
|
"@babel/core": "7.21.0",
|
||||||
"rollup-plugin-dts": "5.2.0",
|
"rollup-plugin-dts": "5.2.0",
|
||||||
"axios": "1.3.4"
|
"axios": "1.3.4",
|
||||||
|
"dayjs": "1.11.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"object-hash": "^3.0.0"
|
"object-hash": "^3.0.0"
|
||||||
|
|
1468
pnpm-lock.yaml
1468
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user