feat: 创建课程

This commit is contained in:
mozzie 2023-03-10 17:55:01 +08:00
parent 12be9a9180
commit 475b2932ef
28 changed files with 425 additions and 120 deletions

View File

@ -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 (

View File

@ -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));

View File

@ -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) => {

View File

@ -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 { > main {
position: fixed; position: absolute;
left: 200px; left: 200px;
right: 0; right: 0;
top: 46px; top: 46px;

View File

@ -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}
/> />

View 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
),
})),
}));

View File

@ -1,10 +0,0 @@
interface IProps {
onChange?: Function;
styles?: React.CSSProperties;
}
const Appendix = (props: IProps) => {
return <div style={{ ...props.styles }}></div>;
};
export default Appendix;

View File

@ -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;

View File

@ -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);

View File

@ -40,7 +40,7 @@ const Chatpter = (props: IProps) => {
}; };
useEffect(() => { useEffect(() => {
console.log(chapterList); if (props.onChange) props.onChange(chapterList);
}, [chapterList]); }, [chapterList]);
return ( return (

View File

@ -0,0 +1,2 @@
.vditor {
}

View 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;

View File

@ -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;
}

View File

@ -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>
); );
}; };

View File

@ -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}
/> />

View File

@ -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,
}, },
}, },
}, },

View File

@ -4,4 +4,5 @@ OSS_SECRET=12345
SECRET_ID=AKID534tZ7OvYzb2KQMwLYaVEl5FBwUtQWbU SECRET_ID=AKID534tZ7OvYzb2KQMwLYaVEl5FBwUtQWbU
SECRET_KEY=q9HD6lQimeLp9IH5h7NRJzUpNjwxmPq5 SECRET_KEY=q9HD6lQimeLp9IH5h7NRJzUpNjwxmPq5
SUBAPPID=1500018521 SUBAPPID=1500018521
SUBAPPID_OSS=1500018944

View File

@ -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

View File

@ -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);
});
}
} }

View 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 };
});
}
}

View File

@ -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 };
} }
} }

View File

@ -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;
}

View File

@ -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);

View File

@ -0,0 +1,9 @@
import { useLocation } from "react-router-dom";
function Material() {
const location = useLocation();
return <div>{location.pathname}</div>;
}
export default Material;

View File

@ -1,5 +0,0 @@
function Material() {
return <div> </div>;
}
export default Material;

View File

@ -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线的量化描述",

View File

@ -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"
} }
} }

View File

@ -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