Compare commits
1 Commits
feat/backs
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
2905c63fab |
108
.drone.yml
108
.drone.yml
|
@ -1,108 +0,0 @@
|
||||||
---
|
|
||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: admin 资源发布
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- name: node_modules
|
|
||||||
host:
|
|
||||||
path: /home/drone/cache/node_modules
|
|
||||||
|
|
||||||
when:
|
|
||||||
branch:
|
|
||||||
- release/**
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# admin管理
|
|
||||||
- name: build-admin
|
|
||||||
image: node:16.19-alpine
|
|
||||||
volumes:
|
|
||||||
- name: node_modules
|
|
||||||
path: /drone/src/node_modules
|
|
||||||
commands:
|
|
||||||
- pwd
|
|
||||||
- node -v
|
|
||||||
- npm -v
|
|
||||||
- npm install -g pnpm
|
|
||||||
- pnpm i
|
|
||||||
- pnpm build:admin
|
|
||||||
|
|
||||||
- name: deploy-admin
|
|
||||||
image: appleboy/drone-scp:1.6
|
|
||||||
settings:
|
|
||||||
host:
|
|
||||||
- backset.cn
|
|
||||||
username: root
|
|
||||||
password: cr654654.
|
|
||||||
port: 22
|
|
||||||
overwrite: true
|
|
||||||
command_timeout: 2m
|
|
||||||
target: /www/wwwroot/nginx/html/backset.cn/
|
|
||||||
source: ./apps/admin/dist/*
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
branch:
|
|
||||||
- release/**
|
|
||||||
|
|
||||||
---
|
|
||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: web 资源发布
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- name: node_modules
|
|
||||||
host:
|
|
||||||
path: /home/drone/cache/node_modules
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# web端
|
|
||||||
- name: build-web
|
|
||||||
image: node:16.19-alpine
|
|
||||||
# 容器内挂载点
|
|
||||||
volumes:
|
|
||||||
- name: node_modules
|
|
||||||
path: /drone/src/node_modules
|
|
||||||
commands:
|
|
||||||
- pwd
|
|
||||||
- node -v
|
|
||||||
- npm -v
|
|
||||||
- npm install -g pnpm
|
|
||||||
- pnpm i
|
|
||||||
- pnpm build:web
|
|
||||||
|
|
||||||
- name: deploy-web
|
|
||||||
image: appleboy/drone-scp:1.6
|
|
||||||
settings:
|
|
||||||
host:
|
|
||||||
- backset.cn
|
|
||||||
username: root
|
|
||||||
password: cr654654.
|
|
||||||
port: 22
|
|
||||||
overwrite: true
|
|
||||||
command_timeout: 2m
|
|
||||||
target: /www/wwwroot/nginx/html/backset.cn/
|
|
||||||
source: ./apps/web/dist/*
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
branch:
|
|
||||||
- release/**
|
|
||||||
|
|
||||||
---
|
|
||||||
kind: pipeline
|
|
||||||
type: exec
|
|
||||||
name: server 镜像生产&容器发布
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: docker image
|
|
||||||
commands:
|
|
||||||
- docker-compose build
|
|
||||||
|
|
||||||
- name: docker container
|
|
||||||
commands:
|
|
||||||
- docker-compose up -d
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
branch:
|
|
||||||
- release/**
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
**/node_modules
|
**/node_modules
|
||||||
**/dist
|
**/dist
|
||||||
|
**/.DS_Store
|
|
@ -1,40 +0,0 @@
|
||||||
FROM node:16.19 AS build
|
|
||||||
|
|
||||||
# 创建一个项目文件夹,可自定义
|
|
||||||
WORKDIR /app
|
|
||||||
# 将本地文件复制到项目文件夹下
|
|
||||||
COPY ./apps/server/ .
|
|
||||||
|
|
||||||
RUN ls -a
|
|
||||||
RUN npm set registry https://registry.npm.taobao.org
|
|
||||||
RUN npm install -g pnpm
|
|
||||||
RUN pnpm i
|
|
||||||
RUN pnpm build
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 切换镜像文件, alpine镜像打包后更小
|
|
||||||
FROM node:16.19-alpine
|
|
||||||
# 切换工作目录
|
|
||||||
WORKDIR /app
|
|
||||||
# 将打包后的的文件复制到docker镜像里
|
|
||||||
COPY --from=build /app/dist ./dist
|
|
||||||
|
|
||||||
RUN ls -a
|
|
||||||
|
|
||||||
# 把源代码复制过去, 以便报错能报对行
|
|
||||||
COPY --from=build /app/src ./src
|
|
||||||
COPY --from=build /app/bootstrap.js ./
|
|
||||||
COPY --from=build /app/package.json ./
|
|
||||||
COPY --from=build /app/public ./public
|
|
||||||
COPY --from=build /app/.env ./
|
|
||||||
|
|
||||||
ENV TZ="Asia/Shanghai"
|
|
||||||
|
|
||||||
RUN npm set registry https://registry.npm.taobao.org
|
|
||||||
# 安装工程依赖
|
|
||||||
RUN npm install --production
|
|
||||||
# 设置暴露端口
|
|
||||||
EXPOSE 7001
|
|
||||||
# 启动
|
|
||||||
CMD ["npm", "run", "start"]
|
|
|
@ -14,9 +14,7 @@
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "6.8.0",
|
"react-router-dom": "6.8.0"
|
||||||
"@ant-design/icons": "5.0.1",
|
|
||||||
"nanoid": "4.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.27",
|
"@types/react": "^18.0.27",
|
||||||
|
|
|
@ -1,19 +1,52 @@
|
||||||
import { Route, Routes } from "react-router-dom";
|
|
||||||
import "./assets/less/common.less";
|
import "./assets/less/common.less";
|
||||||
import Layout from "./layout";
|
import { Route, Routes, useNavigate } from "react-router-dom";
|
||||||
import Login from "./view/Login";
|
import User from "./view/User";
|
||||||
import { ConfigProvider as AntDesignConfigProvider } from "antd";
|
import Home from "./view/Home";
|
||||||
import zhCN from "antd/locale/zh_CN";
|
import { Guard } from "./router/Guard";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const routerList = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <Home />,
|
||||||
|
name: "首页",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "user",
|
||||||
|
element: <User />,
|
||||||
|
name: "用户",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AntDesignConfigProvider locale={zhCN}>
|
<>
|
||||||
|
<header>header</header>
|
||||||
|
<div>
|
||||||
|
<aside>
|
||||||
|
<ul>
|
||||||
|
{routerList.map((router) => (
|
||||||
|
<li key={router.path}>
|
||||||
|
<a onClick={() => navigate(router.path)}>{router.name}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
<main>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index key={"login"} path={"/"} element={<Login />} />
|
{routerList.map((router) => (
|
||||||
<Route key={"dash"} path={"/*"} element={<Layout />} />
|
<Route
|
||||||
|
key={router.path}
|
||||||
|
path={router.path}
|
||||||
|
element={<Guard>{router.element}</Guard>}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<Route path="*" element={<span>404</span>} />
|
<Route path="*" element={<span>404</span>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</AntDesignConfigProvider>
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
export interface IgetVodRequest {
|
|
||||||
offset: number;
|
|
||||||
limit: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IGetVodeResponse {
|
|
||||||
MediaInfoSet: any[];
|
|
||||||
TotalCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICourseBasic {
|
|
||||||
course_id?: string;
|
|
||||||
course_title?: string;
|
|
||||||
course_cover_url?: string;
|
|
||||||
course_summary?: string;
|
|
||||||
course_createtime?: string;
|
|
||||||
valid?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICreateCourseRequest extends ICourseBasic {
|
|
||||||
course_chapterList: [];
|
|
||||||
course_guide: {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAdminLogin {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IXcode {
|
|
||||||
expiretime: string;
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISelectCourse {
|
|
||||||
all?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IChapter {
|
|
||||||
chapter_course_id?: string;
|
|
||||||
chapter_file_id?: string;
|
|
||||||
chapter_id?: string;
|
|
||||||
chapter_level?: string;
|
|
||||||
chapter_title?: string;
|
|
||||||
media_cover_url?: string;
|
|
||||||
media_time?: string;
|
|
||||||
media_url?: string;
|
|
||||||
order?: number;
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
import R from "./request";
|
|
||||||
import P from "./process";
|
|
||||||
import {
|
|
||||||
IAdminLogin,
|
|
||||||
IChapter,
|
|
||||||
ICourseBasic,
|
|
||||||
ICreateCourseRequest,
|
|
||||||
IgetVodRequest,
|
|
||||||
ISelectCourse,
|
|
||||||
IXcode,
|
|
||||||
} from "./dto";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 腾讯vod媒资
|
|
||||||
*/
|
|
||||||
export const getVod = (p: IgetVodRequest) =>
|
|
||||||
R.post("/api/vod/media/select", { ...p }).then((d: any) => P.getVod(d.data));
|
|
||||||
|
|
||||||
export const createCourse = (p: ICreateCourseRequest) =>
|
|
||||||
R.post("/api/course/create", { ...p });
|
|
||||||
|
|
||||||
export const adminLogin = (p: IAdminLogin) =>
|
|
||||||
R.post("/api/user/admin/auth", { ...p });
|
|
||||||
|
|
||||||
export const createXCode = (codeList: IXcode[]) =>
|
|
||||||
R.post("/api/xcode/admin/create", codeList);
|
|
||||||
|
|
||||||
export const selectXCodeList = () => R.post("/api/xcode/admin/select/all");
|
|
||||||
|
|
||||||
export const selectCourseList = (p: ISelectCourse) =>
|
|
||||||
R.post("/api/course/select/all", { ...p });
|
|
||||||
|
|
||||||
export const updateCourse = (course: ICourseBasic) =>
|
|
||||||
R.post("/api/course/update", course);
|
|
||||||
|
|
||||||
export const selectChapterList = ({
|
|
||||||
chapter_course_id,
|
|
||||||
}: {
|
|
||||||
chapter_course_id: string;
|
|
||||||
}) => R.post("/api/course/chapter/select", { chapter_course_id });
|
|
||||||
|
|
||||||
export const updateChapter = (chapter: IChapter) =>
|
|
||||||
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);
|
|
||||||
|
|
||||||
export const selectUserList = () => R.post("/api/user/admin/select/all");
|
|
||||||
|
|
||||||
export const updateUser = (user: any) => R.post("/api/user/admin/update", user);
|
|
||||||
|
|
||||||
export const selectGuide = (p: { guide_course_id: string }) =>
|
|
||||||
R.post("/api/course/guide/select", p);
|
|
||||||
|
|
||||||
export const updateGuide = (p: any) => R.post("/api/course/guide/update", p);
|
|
|
@ -1,23 +0,0 @@
|
||||||
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 };
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,47 +0,0 @@
|
||||||
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) => {
|
|
||||||
const { msg, code } = response.data;
|
|
||||||
switch (code) {
|
|
||||||
case 10000:
|
|
||||||
message.success(`接口: ${response.config.url}, 请求成功`);
|
|
||||||
break;
|
|
||||||
case 20000:
|
|
||||||
message.error(`接口: ${response.config.url}, 遇到错误`);
|
|
||||||
break;
|
|
||||||
case 40000:
|
|
||||||
message.error(msg);
|
|
||||||
// window.location.href = "/";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// TODO ...
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return response?.data;
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default instance;
|
|
|
@ -3,7 +3,6 @@ html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -15,25 +14,3 @@ ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "bs";
|
|
||||||
src: url("./backset.woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-scrollbar {
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 14px;
|
|
||||||
height: 4px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
border: 4px solid transparent;
|
|
||||||
background-clip: padding-box;
|
|
||||||
border-radius: 7px;
|
|
||||||
background-color: #d2d2d2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
.container {
|
|
||||||
height: 100%;
|
|
||||||
background: #f1f1f1;
|
|
||||||
> header {
|
|
||||||
padding: 0 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 46px;
|
|
||||||
background: #001529;
|
|
||||||
z-index: 19;
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 176px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #fff;
|
|
||||||
svg {
|
|
||||||
width: 22px;
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
padding-left: 10px;
|
|
||||||
font-family: "bs";
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> aside {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 46px;
|
|
||||||
bottom: 0;
|
|
||||||
width: 200px;
|
|
||||||
> aside {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> main {
|
|
||||||
position: absolute;
|
|
||||||
left: 200px;
|
|
||||||
right: 0;
|
|
||||||
top: 46px;
|
|
||||||
bottom: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
.view {
|
|
||||||
padding: 0 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
import React, { Suspense } from "react";
|
|
||||||
import "./index.less";
|
|
||||||
import {
|
|
||||||
VideoCameraAddOutlined,
|
|
||||||
UserSwitchOutlined,
|
|
||||||
AuditOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { Layout, Menu, MenuProps, Spin, theme } from "antd";
|
|
||||||
import { Route, Routes, useNavigate } from "react-router-dom";
|
|
||||||
import { Guard } from "../router/Guard";
|
|
||||||
import { navMenuList, navRoutes, sideMenuRoutes } from "../router";
|
|
||||||
|
|
||||||
const navMenus: MenuProps["items"] = navMenuList;
|
|
||||||
|
|
||||||
const sideMenus: MenuProps["items"] = [
|
|
||||||
{
|
|
||||||
key: "course",
|
|
||||||
icon: <VideoCameraAddOutlined />,
|
|
||||||
label: "课程",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: "create",
|
|
||||||
label: "创建",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "list",
|
|
||||||
label: "课程列表",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "library",
|
|
||||||
label: "视频库",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "user",
|
|
||||||
icon: <UserSwitchOutlined />,
|
|
||||||
label: "用户",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "xcode",
|
|
||||||
icon: <AuditOutlined />,
|
|
||||||
label: "神秘代码",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
|
||||||
|
|
||||||
const Index: React.FC = () => {
|
|
||||||
const {
|
|
||||||
token: { colorBgContainer },
|
|
||||||
} = theme.useToken();
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const onClickNavMenuItem = (p: any) => {
|
|
||||||
navigate(p.key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClickSideMenuItem = (p: any) => {
|
|
||||||
const path = p.keyPath.reverse().join("/");
|
|
||||||
navigate(path.startsWith("/") ? path : "/" + path);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container">
|
|
||||||
<header>
|
|
||||||
<div className="logo">
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
className="icon"
|
|
||||||
viewBox="0 0 1024 1024"
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path d="M158.165333 499.498667A42.496 42.496 0 0 0 170.666667 469.333333V256a42.666667 42.666667 0 0 1 42.666666-42.666667 42.666667 42.666667 0 0 0 0-85.333333C142.762667 128 85.333333 185.429333 85.333333 256v195.669333l-30.165333 30.165334a42.666667 42.666667 0 0 0 0 60.330666l30.165333 30.165334V768c0 70.570667 57.429333 128 128 128a42.666667 42.666667 0 0 0 0-85.333333 42.666667 42.666667 0 0 1-42.666666-42.666667v-213.333333a42.496 42.496 0 0 0-12.501334-30.165334L145.664 512l12.501333-12.501333zM978.090667 495.658667a42.709333 42.709333 0 0 0-9.258667-13.824L938.666667 451.669333V256c0-70.570667-57.429333-128-128-128a42.666667 42.666667 0 1 0 0 85.333333 42.666667 42.666667 0 0 1 42.666666 42.666667v213.333333a42.581333 42.581333 0 0 0 12.501334 30.165334l12.501333 12.501333-12.501333 12.501333A42.496 42.496 0 0 0 853.333333 554.666667v213.333333a42.666667 42.666667 0 0 1-42.666666 42.666667 42.666667 42.666667 0 1 0 0 85.333333c70.570667 0 128-57.429333 128-128v-195.669333l30.165333-30.165334a42.709333 42.709333 0 0 0 9.258667-46.506666zM669.738667 225.450667a42.752 42.752 0 0 0-69.546667 14.762666l-255.829333 512a42.624 42.624 0 0 0 23.893333 55.424 42.922667 42.922667 0 0 0 55.552-23.765333l255.786667-512a42.538667 42.538667 0 0 0-9.813334-46.421333z"></path>
|
|
||||||
</svg>
|
|
||||||
<span>Backset</span>
|
|
||||||
</div>
|
|
||||||
<Menu
|
|
||||||
theme="dark"
|
|
||||||
mode="horizontal"
|
|
||||||
defaultSelectedKeys={["/"]}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
items={navMenus}
|
|
||||||
onClick={onClickNavMenuItem}
|
|
||||||
/>
|
|
||||||
</header>
|
|
||||||
<aside>
|
|
||||||
<Sider width={200} style={{ background: colorBgContainer }}>
|
|
||||||
<Menu
|
|
||||||
mode="inline"
|
|
||||||
defaultSelectedKeys={["1"]}
|
|
||||||
defaultOpenKeys={["course"]}
|
|
||||||
style={{ height: "100%", borderRight: 0 }}
|
|
||||||
items={sideMenus}
|
|
||||||
onClick={onClickSideMenuItem}
|
|
||||||
/>
|
|
||||||
</Sider>
|
|
||||||
</aside>
|
|
||||||
<main className="bs-scrollbar">
|
|
||||||
<div className="view">
|
|
||||||
<Routes>
|
|
||||||
{[...navRoutes, ...sideMenuRoutes].map((router) => (
|
|
||||||
<Route
|
|
||||||
key={router.path}
|
|
||||||
path={router.path}
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<Spin />}>
|
|
||||||
<Guard>{<router.element />}</Guard>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Route path="*" element={<span>404</span>} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Index;
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import Cookie from "js-cookie";
|
|
||||||
|
|
||||||
interface IGuardProps {
|
interface IGuardProps {
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
|
@ -8,11 +7,9 @@ interface IGuardProps {
|
||||||
|
|
||||||
export const Guard = (props: IGuardProps) => {
|
export const Guard = (props: IGuardProps) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sign = Cookie.get("_sign_admin");
|
console.log("location.pathname changed 拦截", location.pathname);
|
||||||
if (!sign) navigate("/");
|
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
return props.children;
|
return props.children;
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { lazy } from "react";
|
|
||||||
|
|
||||||
export interface IRouteMenuItem {
|
|
||||||
path: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IRoute extends IRouteMenuItem {
|
|
||||||
element: React.LazyExoticComponent<() => JSX.Element>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const navRoutes: IRoute[] = [
|
|
||||||
{
|
|
||||||
path: "/overview",
|
|
||||||
element: lazy(() => import("../view/Overview")),
|
|
||||||
name: "总览",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/payment",
|
|
||||||
element: lazy(() => import("../view/Payment")),
|
|
||||||
name: "支付",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const sideMenuRoutes: IRoute[] = [
|
|
||||||
{
|
|
||||||
path: "/course/create",
|
|
||||||
element: lazy(() => import("../view/Course/Create")),
|
|
||||||
name: "创建课程",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/course/list",
|
|
||||||
element: lazy(() => import("../view/Course/List")),
|
|
||||||
name: "课程列表",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/course/library",
|
|
||||||
element: lazy(() => import("../view/Course/Library")),
|
|
||||||
name: "视频库",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/user",
|
|
||||||
element: lazy(() => import("../view/User")),
|
|
||||||
name: "用户",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/xcode",
|
|
||||||
element: lazy(() => import("../view/XCode")),
|
|
||||||
name: "邀请码",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const navMenuList = navRoutes.map((route) => {
|
|
||||||
const { path: key, name: label } = route;
|
|
||||||
return { key, label };
|
|
||||||
});
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { create } from "zustand";
|
|
||||||
import { getVod } from "../api";
|
|
||||||
|
|
||||||
export const useMediaStore = create((set) => ({
|
|
||||||
list: [],
|
|
||||||
listFilter: [],
|
|
||||||
setList: (newState: any) =>
|
|
||||||
set(() => ({ list: newState, listFilter: newState })),
|
|
||||||
useFilter: (keyword: string) =>
|
|
||||||
set((state: any) => ({
|
|
||||||
listFilter: !keyword
|
|
||||||
? state.list
|
|
||||||
: state.list.filter(
|
|
||||||
(i: any) => i.name.toUpperCase().indexOf(keyword.toUpperCase()) > -1
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
}));
|
|
3
apps/admin/src/test.less
Normal file
3
apps/admin/src/test.less
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
body {
|
||||||
|
background: grey;
|
||||||
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
.preview-course {
|
|
||||||
position: relative;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
height: 360px;
|
|
||||||
.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;
|
|
||||||
border-radius: 6px 6px 0 0;
|
|
||||||
}
|
|
||||||
&.summary {
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
padding: 6px;
|
|
||||||
line-height: 1.4;
|
|
||||||
border-radius: 0 0 6px 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
import { InboxOutlined } from "@ant-design/icons";
|
|
||||||
import { Col, Form, Input, message, Row, Upload, UploadProps } from "antd";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import "./index.less";
|
|
||||||
const { Dragger } = Upload;
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
onChange: Function;
|
|
||||||
styles?: React.CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BasicForm = (props: IProps) => {
|
|
||||||
const [preview, setPreivew] = useState({
|
|
||||||
course_cover_url: "",
|
|
||||||
course_title: "",
|
|
||||||
course_summary: "",
|
|
||||||
});
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
const onValuesChange = (_: any, all: any) =>
|
|
||||||
setPreivew((p) => ({ ...p, ...all }));
|
|
||||||
|
|
||||||
const coverDragger: UploadProps = {
|
|
||||||
name: "file",
|
|
||||||
multiple: true,
|
|
||||||
action: "/api/vod/course/cover/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, course_cover_url: MediaUrl }));
|
|
||||||
}
|
|
||||||
} else if (status === "error") {
|
|
||||||
message.error(`${info.file.name} 文件上传失败`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDrop(e) {
|
|
||||||
console.log("Dropped files", e.dataTransfer.files);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => props.onChange(preview), [preview]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ ...props.styles }}>
|
|
||||||
<Row style={{ marginBottom: 24 }}>
|
|
||||||
<Col span={24}>
|
|
||||||
<div
|
|
||||||
className="preview-course"
|
|
||||||
style={{
|
|
||||||
backgroundImage: !preview.course_cover_url
|
|
||||||
? `linear-gradient(
|
|
||||||
to right,
|
|
||||||
#e95659,
|
|
||||||
#e15084,
|
|
||||||
#c55aaa,
|
|
||||||
#976bc4,
|
|
||||||
#5678ce
|
|
||||||
)`
|
|
||||||
: `url(${preview.course_cover_url})`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="mask">
|
|
||||||
<p className="title">
|
|
||||||
{!preview.course_title ? "标题" : preview.course_title}
|
|
||||||
</p>
|
|
||||||
<p className="summary">
|
|
||||||
{!preview.course_summary ? "摘要" : preview.course_summary}{" "}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row gutter={24}>
|
|
||||||
<Col span={12}>
|
|
||||||
<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>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form form={form} onValuesChange={onValuesChange}>
|
|
||||||
<Form.Item name="course_title" rules={[{ required: true }]}>
|
|
||||||
<Input size="large" type="text" placeholder="标题" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="course_summary"
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
|
||||||
placeholder="摘要"
|
|
||||||
style={{ height: 130, resize: "none" }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BasicForm;
|
|
|
@ -1,12 +0,0 @@
|
||||||
.chapter-list {
|
|
||||||
li {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
&.l1 {
|
|
||||||
margin-top: 20px;
|
|
||||||
&:first-of-type {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Col,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Radio,
|
|
||||||
Row,
|
|
||||||
Space,
|
|
||||||
Typography,
|
|
||||||
} from "antd";
|
|
||||||
const { Text } = Typography;
|
|
||||||
import {} from "antd";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import "./index.less";
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
onChange?: Function;
|
|
||||||
styles?: React.CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IChapter {
|
|
||||||
chapter_level: string;
|
|
||||||
chapter_title: string;
|
|
||||||
chapter_file_id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Chatpter = (props: IProps) => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [chapterList, setChapterList] = useState<IChapter[]>([]);
|
|
||||||
|
|
||||||
const onTocChange = () => {
|
|
||||||
const { toc } = form.getFieldsValue();
|
|
||||||
const process = toc
|
|
||||||
.split("\n")
|
|
||||||
.filter((i: string) => i.replace(/\s/, "").length > 0)
|
|
||||||
.map((row: string, index: number) => {
|
|
||||||
const [chapter_level, chapter_title, chapter_file_id] = row.split("|");
|
|
||||||
return !chapter_file_id
|
|
||||||
? { order: index, chapter_level, chapter_title }
|
|
||||||
: { order: index, chapter_level, chapter_title, chapter_file_id };
|
|
||||||
});
|
|
||||||
setChapterList(process);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.onChange) props.onChange(chapterList);
|
|
||||||
}, [chapterList]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ ...props.styles }}>
|
|
||||||
<Row gutter={24}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form form={form} onChange={onTocChange}>
|
|
||||||
<Form.Item name="toc">
|
|
||||||
<Input.TextArea
|
|
||||||
placeholder="级别 | 标题 | FileId"
|
|
||||||
style={{ minHeight: 600, lineHeight: 2, fontSize: 16 }}
|
|
||||||
></Input.TextArea>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<ul className="chapter-list">
|
|
||||||
{chapterList.map((c, index) => (
|
|
||||||
<li
|
|
||||||
key={index}
|
|
||||||
className={+c.chapter_level === 1 ? "l1" : ""}
|
|
||||||
style={{ paddingLeft: +c.chapter_level === 1 ? 0 : 20 }}
|
|
||||||
>
|
|
||||||
<Text style={{ fontSize: +c.chapter_level === 1 ? 18 : 16 }}>
|
|
||||||
{c.chapter_title}
|
|
||||||
</Text>
|
|
||||||
{c.chapter_file_id && (
|
|
||||||
<Text type="secondary">{c.chapter_file_id}</Text>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Chatpter;
|
|
|
@ -1,2 +0,0 @@
|
||||||
.vditor {
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
import "vditor/dist/index.css";
|
|
||||||
import Vditor from "vditor";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import "./index.less";
|
|
||||||
import { message } from "antd";
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
onChange?: Function;
|
|
||||||
styles?: React.CSSProperties;
|
|
||||||
id: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Guide(props: IProps) {
|
|
||||||
const vditorRef = useRef<Vditor | null>(null);
|
|
||||||
|
|
||||||
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 });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderEditor = () => {
|
|
||||||
vditorRef.current = new Vditor("vditor", {
|
|
||||||
height: "100%",
|
|
||||||
cache: {
|
|
||||||
id: props.id,
|
|
||||||
enable: false,
|
|
||||||
},
|
|
||||||
toolbar: [...toolbar, "|", submitTool],
|
|
||||||
value: props.defaultValue ?? "",
|
|
||||||
hint: { delay: 200, emoji },
|
|
||||||
counter: { enable: true },
|
|
||||||
preview: { actions: ["desktop", "mobile"] },
|
|
||||||
after: () => console.log("[info] vditor init success..."),
|
|
||||||
upload: {
|
|
||||||
accept: "image/*",
|
|
||||||
url: "/api/vod/oss/image/upload",
|
|
||||||
multiple: false,
|
|
||||||
success(_, res) {
|
|
||||||
const { code, data, msg } = JSON.parse(res);
|
|
||||||
if (code === 10000) {
|
|
||||||
message.success("上传成功");
|
|
||||||
const { name, url } = data;
|
|
||||||
vditorRef.current?.insertValue(`![${name}](${url})`);
|
|
||||||
} else {
|
|
||||||
message.error(msg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const destroyEditor = () => {
|
|
||||||
vditorRef.current = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
renderEditor();
|
|
||||||
return () => destroyEditor();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
renderEditor();
|
|
||||||
return () => destroyEditor();
|
|
||||||
}, [props.defaultValue]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ ...props.styles }}>
|
|
||||||
<div id="vditor" className="vditor" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
.create-course {
|
|
||||||
width: 1120px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 24px 0;
|
|
||||||
.content {
|
|
||||||
padding: 20px 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-media-item {
|
|
||||||
border-bottom: 1px solid rgba(5, 5, 5, 0.06);
|
|
||||||
padding: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
|
@ -1,160 +0,0 @@
|
||||||
import { Button, Card, Drawer, Input, message, Steps, Typography } from "antd";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import Guide from "./Guide";
|
|
||||||
import BasicForm from "./BasicForm";
|
|
||||||
import Chatpter from "./Chatpter";
|
|
||||||
import "./index.less";
|
|
||||||
import { useMediaStore } from "../../../store/media";
|
|
||||||
import { createCourse } from "../../../api";
|
|
||||||
import { ICourseBasic } from "../../../api/dto";
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
interface ICourse {
|
|
||||||
basicInfo: ICourseBasic;
|
|
||||||
course_chapterList: [];
|
|
||||||
course_guide: {
|
|
||||||
guide_value: string;
|
|
||||||
guide_html: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const CourseCreate = () => {
|
|
||||||
const [current, setCurrent] = useState(0);
|
|
||||||
const [course, setCourse] = useState<ICourse>({
|
|
||||||
basicInfo: {
|
|
||||||
course_cover_url: "",
|
|
||||||
course_title: "",
|
|
||||||
course_summary: "",
|
|
||||||
},
|
|
||||||
course_chapterList: [],
|
|
||||||
course_guide: {
|
|
||||||
guide_value: "",
|
|
||||||
guide_html: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onBasicFormChange = (form: any) =>
|
|
||||||
setCourse((p) => ({ ...p, basicInfo: form }));
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
title: "基本信息",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "章节",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "导读",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const items = steps.map((item) => ({ key: item.title, title: item.title }));
|
|
||||||
|
|
||||||
const onChapterChange = (chapters: any) =>
|
|
||||||
setCourse((p) => ({ ...p, course_chapterList: chapters }));
|
|
||||||
|
|
||||||
const onGuideChange = ({ value, html }: { value: string; html: string }) => {
|
|
||||||
setCourse((p) => ({
|
|
||||||
...p,
|
|
||||||
course_guide: { guide_value: value, guide_html: html },
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建课程
|
|
||||||
*/
|
|
||||||
const onClickCreate = () => {
|
|
||||||
const { basicInfo, ...rest } = course;
|
|
||||||
createCourse({ ...basicInfo, ...rest }).then((res) => {
|
|
||||||
console.log(res);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const mediaList = useMediaStore((s: any) => s.listFilter);
|
|
||||||
|
|
||||||
const useFilter = useMediaStore((s: any) => s.useFilter);
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const showDrawer = () => {
|
|
||||||
setOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSearchChange = (e: any) => {
|
|
||||||
useFilter(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="create-course">
|
|
||||||
<Card>
|
|
||||||
<Steps current={current} items={items} />
|
|
||||||
<div className="content">
|
|
||||||
<BasicForm
|
|
||||||
onChange={onBasicFormChange}
|
|
||||||
styles={{ display: current === 0 ? "block" : "none" }}
|
|
||||||
/>
|
|
||||||
<Chatpter
|
|
||||||
onChange={onChapterChange}
|
|
||||||
styles={{ display: current === 1 ? "block" : "none" }}
|
|
||||||
/>
|
|
||||||
<Guide
|
|
||||||
id="createCourseEditor"
|
|
||||||
onChange={onGuideChange}
|
|
||||||
styles={{
|
|
||||||
display: current === 2 ? "block" : "none",
|
|
||||||
height: "600px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ textAlign: "right", marginTop: "40px" }}>
|
|
||||||
{current === 1 && (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={showDrawer}
|
|
||||||
style={{ marginRight: "12px" }}
|
|
||||||
>
|
|
||||||
视频库
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{current > 0 && (
|
|
||||||
<Button
|
|
||||||
style={{ marginRight: "12px" }}
|
|
||||||
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={onClickCreate}>
|
|
||||||
创建
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CourseCreate;
|
|
|
@ -1,200 +0,0 @@
|
||||||
import { CloudSyncOutlined } from "@ant-design/icons";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Col,
|
|
||||||
Row,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
Input,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
Tag,
|
|
||||||
} from "antd";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useContext, useState } from "react";
|
|
||||||
import { getVod } from "../../../api";
|
|
||||||
import { useMount } from "../../../hooks";
|
|
||||||
import { useMediaStore } from "../../../store/media";
|
|
||||||
const { Paragraph, Text } = Typography;
|
|
||||||
const { Search } = Input;
|
|
||||||
|
|
||||||
const Library = () => {
|
|
||||||
const [dataSource, setDataSource] = useState<any>([]);
|
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
||||||
|
|
||||||
const colors = [
|
|
||||||
"magenta",
|
|
||||||
"red",
|
|
||||||
"volcano",
|
|
||||||
"orange",
|
|
||||||
"gold",
|
|
||||||
"green",
|
|
||||||
"cyan",
|
|
||||||
].reverse();
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: "媒体文件名称",
|
|
||||||
dataIndex: "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 key={item} color={colors[index]}>
|
|
||||||
{item}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
<Row style={{ paddingTop: "5px" }}>
|
|
||||||
<Text type="secondary">时长: {record.duration}s</Text>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "FileID",
|
|
||||||
dataIndex: "key",
|
|
||||||
key: "key",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "hls",
|
|
||||||
dataIndex: "m3u8Size",
|
|
||||||
key: "m3u8Size",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "状态",
|
|
||||||
dataIndex: "status",
|
|
||||||
key: "status",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "创建时间",
|
|
||||||
dataIndex: "createtime",
|
|
||||||
key: "createtime",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
title: "m3u8",
|
|
||||||
dataIndex: "m3u8",
|
|
||||||
key: "m3u8",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "封面图",
|
|
||||||
dataIndex: "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 computeMediaList = (mediaList: any) => {
|
|
||||||
return mediaList.map((m: any) => ({
|
|
||||||
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"),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setMediaList = useMediaStore((s: any) => s.setList);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 最大每次5000条数据,估计这辈子也不可能了
|
|
||||||
*/
|
|
||||||
const fetchVod = () => {
|
|
||||||
getVod({ offset: 0, limit: 5000 }).then((process: any) => {
|
|
||||||
const { mediaList, total } = process;
|
|
||||||
setDataSource(computeMediaList(mediaList));
|
|
||||||
setMediaList(computeMediaList(mediaList));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useMount(() => {
|
|
||||||
fetchVod();
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card style={{ marginTop: 24 }}>
|
|
||||||
<Row>
|
|
||||||
<Col span={14}>
|
|
||||||
<Space>
|
|
||||||
<Search
|
|
||||||
placeholder="根据名称搜索视频"
|
|
||||||
onSearch={onSearch}
|
|
||||||
style={{ width: 200 }}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
<Col span={10} style={{ textAlign: "right" }}>
|
|
||||||
<Space>
|
|
||||||
<Text type="secondary">共计 {dataSource.length} 条媒体资源</Text>
|
|
||||||
<Tooltip title="从腾讯云VOD同步全部视频" placement="left">
|
|
||||||
<Button
|
|
||||||
onClick={() => fetchVod()}
|
|
||||||
type="primary"
|
|
||||||
icon={<CloudSyncOutlined />}
|
|
||||||
>
|
|
||||||
同步
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row style={{ marginTop: "16px" }}>
|
|
||||||
<Col span={24}>
|
|
||||||
<Table
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
dataSource={dataSource}
|
|
||||||
columns={columns}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Library;
|
|
|
@ -1,3 +0,0 @@
|
||||||
.list {
|
|
||||||
padding: 24px 0;
|
|
||||||
}
|
|
|
@ -1,570 +0,0 @@
|
||||||
import {
|
|
||||||
CompassOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
EditFilled,
|
|
||||||
EditOutlined,
|
|
||||||
FieldTimeOutlined,
|
|
||||||
FileAddOutlined,
|
|
||||||
InfoCircleOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
DatePicker,
|
|
||||||
Image,
|
|
||||||
Input,
|
|
||||||
Popconfirm,
|
|
||||||
Space,
|
|
||||||
Switch,
|
|
||||||
Table,
|
|
||||||
Modal,
|
|
||||||
Tooltip,
|
|
||||||
Form,
|
|
||||||
Drawer,
|
|
||||||
} from "antd";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
removeCourse,
|
|
||||||
selectChapterList,
|
|
||||||
selectCourseList,
|
|
||||||
updateChapter,
|
|
||||||
updateCourse,
|
|
||||||
createChapter,
|
|
||||||
removeChapter,
|
|
||||||
selectGuide,
|
|
||||||
updateGuide,
|
|
||||||
} from "../../../api";
|
|
||||||
import Guide from "../Create/Guide";
|
|
||||||
import { useMount } from "../../../hooks";
|
|
||||||
import "./index.less";
|
|
||||||
|
|
||||||
interface IEditItem {
|
|
||||||
key: string;
|
|
||||||
value: string | boolean | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultEditItem = { key: "", value: "" };
|
|
||||||
|
|
||||||
const defaultEditGuide = {
|
|
||||||
drawerVisible: false,
|
|
||||||
course_title: "",
|
|
||||||
guide_id: "",
|
|
||||||
guide_value: "",
|
|
||||||
guide_html: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
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 [editGuide, setEditGuide] = useState(defaultEditGuide);
|
|
||||||
|
|
||||||
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 onEditGuide = (record: any) => {
|
|
||||||
const { course_title, course_id: guide_course_id } = record;
|
|
||||||
selectGuide({ guide_course_id }).then((res: any) => {
|
|
||||||
const { data } = res;
|
|
||||||
const { guide_value = "", guide_html = "", guide_id } = data;
|
|
||||||
setEditGuide({
|
|
||||||
drawerVisible: true,
|
|
||||||
course_title,
|
|
||||||
guide_id,
|
|
||||||
guide_value,
|
|
||||||
guide_html,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onGuideChange = ({ value, html }: { value: string; html: string }) => {
|
|
||||||
setEditGuide((p) => ({ ...p, guide_value: value, guide_html: html }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onConfirmEditGuide = () => {
|
|
||||||
const { course_title, drawerVisible, ...rest } = editGuide;
|
|
||||||
console.log(editGuide);
|
|
||||||
updateGuide(rest).then((res: any) => {
|
|
||||||
if (res?.code === 10000) {
|
|
||||||
setEditGuide(defaultEditGuide);
|
|
||||||
renderCourseTable();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
<Tooltip title="修改导读">
|
|
||||||
<Button
|
|
||||||
type="default"
|
|
||||||
onClick={() => onEditGuide(record)}
|
|
||||||
icon={<CompassOutlined />}
|
|
||||||
></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>
|
|
||||||
<Drawer
|
|
||||||
title={editGuide.course_title}
|
|
||||||
placement="bottom"
|
|
||||||
height="95%"
|
|
||||||
open={editGuide.drawerVisible}
|
|
||||||
onClose={() => setEditGuide((p) => ({ ...p, drawerVisible: false }))}
|
|
||||||
extra={
|
|
||||||
<Space>
|
|
||||||
<Button type="primary" onClick={onConfirmEditGuide}>
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Guide
|
|
||||||
onChange={onGuideChange}
|
|
||||||
styles={{ display: "block", height: "100%" }}
|
|
||||||
id="editGuideEditor"
|
|
||||||
defaultValue={editGuide.guide_value}
|
|
||||||
/>
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,8 +1,11 @@
|
||||||
import { Button, message } from "antd";
|
import { Button, message } from "antd";
|
||||||
|
import { useArticle } from "@backset/ui";
|
||||||
import { useMount } from "../../hooks";
|
import { useMount } from "../../hooks";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
useMount(() => {});
|
useMount(() => {
|
||||||
|
console.log(useArticle());
|
||||||
|
});
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
message.info(`hi, 很惆怅啊, vite 哪里又有坑哦`);
|
message.info(`hi, 很惆怅啊, vite 哪里又有坑哦`);
|
|
@ -1,127 +0,0 @@
|
||||||
#root {
|
|
||||||
background: #f5f5f5;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.frosted {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 290px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: rgba(255, 255, 255, 1);
|
|
||||||
|
|
||||||
.bird-one {
|
|
||||||
width: 250px;
|
|
||||||
height: 290px;
|
|
||||||
position: relative;
|
|
||||||
background-color: #b4dfff;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.beak {
|
|
||||||
width: 90px;
|
|
||||||
height: 90px;
|
|
||||||
position: absolute;
|
|
||||||
top: 65px;
|
|
||||||
left: 85px;
|
|
||||||
transform: rotate(55deg);
|
|
||||||
background-color: #ff4658;
|
|
||||||
}
|
|
||||||
.neck {
|
|
||||||
width: 200px;
|
|
||||||
height: 260px;
|
|
||||||
border-top-left-radius: 15% 10%;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: -30px;
|
|
||||||
transform: skewX(-20deg);
|
|
||||||
background: linear-gradient(to top, #4c77d3 86%, #ff4658 16%);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.neck::before {
|
|
||||||
content: "";
|
|
||||||
width: 200px;
|
|
||||||
height: 20px;
|
|
||||||
border-top-left-radius: 18% 80%;
|
|
||||||
position: absolute;
|
|
||||||
top: 17px;
|
|
||||||
background: linear-gradient(to top, #102f97, #194fe6);
|
|
||||||
box-shadow: 0px -1px 0px 0.8px #417ef8, 0px -5px 8px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
.cir-blue {
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
border: 20px solid #2d4899;
|
|
||||||
border-top-left-radius: 65% 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 120px;
|
|
||||||
left: -20px;
|
|
||||||
background-color: #ff4658;
|
|
||||||
}
|
|
||||||
.feathers {
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 170px;
|
|
||||||
left: 135px;
|
|
||||||
background: linear-gradient(to left, #ccedff 75%, #4dabfd);
|
|
||||||
box-shadow: -6px 5px 8px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
.feather {
|
|
||||||
width: 30px;
|
|
||||||
height: 150px;
|
|
||||||
position: absolute;
|
|
||||||
border-top-left-radius: 100% 50%;
|
|
||||||
border-bottom-left-radius: 100% 50%;
|
|
||||||
}
|
|
||||||
.fe1 {
|
|
||||||
top: 12px;
|
|
||||||
left: 25px;
|
|
||||||
background-color: #84c7fc;
|
|
||||||
box-shadow: -8px 12px 8px rgba(0, 0, 0, 0.5), -1.5px 0px 0 #76c1ff;
|
|
||||||
}
|
|
||||||
.fe2 {
|
|
||||||
top: 5px;
|
|
||||||
left: 45px;
|
|
||||||
background-color: #99d7fe;
|
|
||||||
box-shadow: -5px 8px 8px rgba(0, 0, 0, 0.5), -1.5px 0 0 #52bafa;
|
|
||||||
}
|
|
||||||
.fe3 {
|
|
||||||
left: 65px;
|
|
||||||
background-color: #c4e9fb;
|
|
||||||
box-shadow: -5px 5px 8px rgba(0, 0, 0, 0.5), -0.8px 0 0 #60a2fd;
|
|
||||||
}
|
|
||||||
.eye {
|
|
||||||
width: 55px;
|
|
||||||
height: 55px;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 80px;
|
|
||||||
right: 75px;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
.eye::before {
|
|
||||||
content: "";
|
|
||||||
width: 16px;
|
|
||||||
height: 25px;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 15px;
|
|
||||||
left: 20px;
|
|
||||||
background: linear-gradient(to left, #2f4ba1 50%, #4b71d6 50%);
|
|
||||||
box-shadow: -0.6px 0px 0px 0.7px #364e9e, -4px 0px 4px #737e92,
|
|
||||||
2px 0px 3px #96a6bd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
import { Button, Checkbox, Form, Input, message } from "antd";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { adminLogin } from "../../api";
|
|
||||||
import "./index.less";
|
|
||||||
|
|
||||||
const Login = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const onFinish = (values: any) => {
|
|
||||||
adminLogin(values).then((res: any) => {
|
|
||||||
const { code } = res;
|
|
||||||
if (code == 10000) {
|
|
||||||
message.success("登录成功");
|
|
||||||
navigate("/overview");
|
|
||||||
} else {
|
|
||||||
message.error(res.msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="login">
|
|
||||||
<div className="frosted">
|
|
||||||
<div className="bird-one">
|
|
||||||
<div className="beak"></div>
|
|
||||||
<div className="neck">
|
|
||||||
<div className="cir-blue"></div>
|
|
||||||
</div>
|
|
||||||
<div className="feathers">
|
|
||||||
<div className="feather fe1"></div>
|
|
||||||
<div className="feather fe2"></div>
|
|
||||||
<div className="feather fe3"></div>
|
|
||||||
</div>
|
|
||||||
<div className="eye"></div>
|
|
||||||
</div>
|
|
||||||
<div className="content">
|
|
||||||
<Form
|
|
||||||
name="basic"
|
|
||||||
labelCol={{ span: 8 }}
|
|
||||||
wrapperCol={{ span: 16 }}
|
|
||||||
style={{ maxWidth: 600 }}
|
|
||||||
initialValues={{ remember: true }}
|
|
||||||
onFinish={onFinish}
|
|
||||||
autoComplete="off"
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="username"
|
|
||||||
wrapperCol={{ span: 24 }}
|
|
||||||
rules={[{ required: true, message: "请输入用户名" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="用户名" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
rules={[{ required: true, message: "请输入密码" }]}
|
|
||||||
wrapperCol={{ span: 24 }}
|
|
||||||
>
|
|
||||||
<Input.Password placeholder="密码" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item wrapperCol={{ span: 24 }}>
|
|
||||||
<Button type="primary" htmlType="submit" block>
|
|
||||||
登录
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login;
|
|
|
@ -1,5 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
return <div>UserIndex</div>;
|
|
||||||
}
|
|
|
@ -1,192 +1,5 @@
|
||||||
import {
|
import React from "react";
|
||||||
CheckOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
SketchOutlined,
|
|
||||||
StopOutlined,
|
|
||||||
UserSwitchOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
DatePicker,
|
|
||||||
Popconfirm,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
Tooltip,
|
|
||||||
} from "antd";
|
|
||||||
import { RangePickerProps } from "antd/es/date-picker";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { selectUserList, updateUser } from "../../api";
|
|
||||||
import { useMount } from "../../hooks";
|
|
||||||
|
|
||||||
interface IEditUser {
|
export default function Index() {
|
||||||
key: string;
|
return <div>UserIndex</div>;
|
||||||
value: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultEditUser: IEditUser = {
|
|
||||||
key: "",
|
|
||||||
value: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const User = () => {
|
|
||||||
const [userList, setUserList] = useState([]);
|
|
||||||
const [editUser, setEditUser] = useState<IEditUser>(defaultEditUser);
|
|
||||||
|
|
||||||
const onConfirmEditUser = (user: any) => {
|
|
||||||
updateUser(user).then((res: any) => {
|
|
||||||
if (res?.code === 10000) renderUserList();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const disabledDate: RangePickerProps["disabledDate"] = (current) => {
|
|
||||||
return current && current < dayjs().endOf("day");
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: "id",
|
|
||||||
dataIndex: "id",
|
|
||||||
key: "id",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "用户",
|
|
||||||
dataIndex: "user_login",
|
|
||||||
key: "user_login",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "创建时间",
|
|
||||||
dataIndex: "user_create_time",
|
|
||||||
key: "user_create_time",
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{dayjs(+record.user_create_time).format("YYYY-MM-DD HH:mm:ss")}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "状态",
|
|
||||||
dataIndex: "user_status",
|
|
||||||
key: "user_status",
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
return record.user_status ? (
|
|
||||||
<CheckOutlined style={{ color: "#52c41a" }} />
|
|
||||||
) : (
|
|
||||||
"-"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "订阅",
|
|
||||||
dataIndex: "user_sub",
|
|
||||||
key: "user_sub",
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
return record.user_sub ? (
|
|
||||||
<CheckOutlined style={{ color: "#52c41a" }} />
|
|
||||||
) : (
|
|
||||||
"-"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "订阅到期",
|
|
||||||
dataIndex: "user_sub_expired",
|
|
||||||
key: "user_sub_expired",
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
return record.user_sub ? (
|
|
||||||
<Space>
|
|
||||||
<span>
|
|
||||||
{dayjs(+record.user_sub_expired).format("YYYY-MM-DD HH:mm:ss")}
|
|
||||||
</span>
|
|
||||||
<Popconfirm
|
|
||||||
placement="top"
|
|
||||||
title="订阅到期时间"
|
|
||||||
description={
|
|
||||||
<DatePicker
|
|
||||||
onChange={(date: any) => {
|
|
||||||
setEditUser({
|
|
||||||
key: "user_sub_expired",
|
|
||||||
value: "" + new Date(date).getTime(),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
presets={[
|
|
||||||
{ label: "订阅 - 年", value: dayjs().add(1, "year") },
|
|
||||||
{ label: "订阅 - 季度", value: dayjs().add(3, "month") },
|
|
||||||
{ label: "订阅 - 月", value: dayjs().add(1, "month") },
|
|
||||||
]}
|
|
||||||
disabledDate={disabledDate}
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
showTime={{ defaultValue: dayjs("00:00:00", "HH:mm:ss") }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onConfirm={() =>
|
|
||||||
onConfirmEditUser({ ...record, [editUser.key]: editUser.value })
|
|
||||||
}
|
|
||||||
okText="确定"
|
|
||||||
cancelText="取消"
|
|
||||||
>
|
|
||||||
<Button type="text" icon={<EditOutlined />} />
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
) : (
|
|
||||||
"-"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "操作",
|
|
||||||
dataIndex: "operation",
|
|
||||||
key: "operation",
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
return (
|
|
||||||
<Space>
|
|
||||||
<Tooltip title="封号">
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
danger
|
|
||||||
icon={<StopOutlined />}
|
|
||||||
onClick={() =>
|
|
||||||
onConfirmEditUser({
|
|
||||||
...record,
|
|
||||||
user_status: !record.user_status,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="订阅">
|
|
||||||
<Button
|
|
||||||
type="default"
|
|
||||||
icon={<UserSwitchOutlined />}
|
|
||||||
onClick={() =>
|
|
||||||
onConfirmEditUser({ ...record, user_sub: !record.user_sub })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const renderUserList = () => {
|
|
||||||
selectUserList().then((res: any) => {
|
|
||||||
const { data = [] } = res;
|
|
||||||
setUserList(data.map((u: any) => ({ ...u, key: u.id })));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useMount(() => renderUserList());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ marginTop: 24 }}>
|
|
||||||
<Card>
|
|
||||||
<Table dataSource={userList} columns={columns} />
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default User;
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
.generate-invite-code {
|
|
||||||
padding: 24px 0;
|
|
||||||
}
|
|
|
@ -1,169 +0,0 @@
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Col,
|
|
||||||
Input,
|
|
||||||
message,
|
|
||||||
Row,
|
|
||||||
Segmented,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
} from "antd";
|
|
||||||
import "./index.less";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { createXCode, selectXCodeList } from "../../api";
|
|
||||||
import { useMount } from "../../hooks";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
export default function InviteCode() {
|
|
||||||
const [dataSource, setDataSource] = useState([]);
|
|
||||||
|
|
||||||
const defaultColumns = [
|
|
||||||
{
|
|
||||||
title: "神秘代码",
|
|
||||||
dataIndex: "code",
|
|
||||||
key: "code",
|
|
||||||
filters: [],
|
|
||||||
filterSearch: true,
|
|
||||||
onFilter: (value: string, record: any) => record.code.startsWith(value),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "生成时间",
|
|
||||||
dataIndex: "createtime",
|
|
||||||
key: "createtime",
|
|
||||||
defaultSortOrder: "descend",
|
|
||||||
sorter: (a: any, b: any) => a.createtime - b.createtime,
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
return dayjs(+record.createtime).format("YYYY-MM-DD hh:mm:ss");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "失效时间",
|
|
||||||
dataIndex: "expiretime",
|
|
||||||
key: "expiretime",
|
|
||||||
defaultSortOrder: "descend",
|
|
||||||
sorter: (a: any, b: any) => a.expiretime - b.expiretime,
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
return dayjs(+record.expiretime).format("YYYY-MM-DD hh:mm:ss");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "绑定时间",
|
|
||||||
dataIndex: "user_usetime",
|
|
||||||
key: "user_usetime",
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
if (!!record.user_usetime)
|
|
||||||
return dayjs(+record.user_usetime).format("YYYY-MM-DD hh:mm:ss");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "用户",
|
|
||||||
dataIndex: "user_id",
|
|
||||||
key: "user_id",
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
return record.user_id > 0
|
|
||||||
? `${record.user_login ?? ""}(${record.user_id})`
|
|
||||||
: "";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "操作",
|
|
||||||
dataIndex: "operation",
|
|
||||||
key: "operation",
|
|
||||||
render: () => {
|
|
||||||
return <span>123</span>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const [columns, setColumns] = useState<any>(defaultColumns);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (dataSource) {
|
|
||||||
defaultColumns[0].filters = dataSource.map((i: any) => ({
|
|
||||||
text: i.code.substring(0, 6) + "...",
|
|
||||||
value: i.code,
|
|
||||||
})) as any;
|
|
||||||
setColumns(defaultColumns);
|
|
||||||
}
|
|
||||||
}, [dataSource]);
|
|
||||||
|
|
||||||
const onClickGen = () => {
|
|
||||||
if (!genRule.amount || !genRule.day) return message.error("生成规则错误");
|
|
||||||
const params = [];
|
|
||||||
for (let i = 0; i < genRule.amount; i++)
|
|
||||||
params.push({
|
|
||||||
code: nanoid(),
|
|
||||||
expiretime: "" + (Date.now() + +genRule.day * 24 * 60 * 60 * 1000),
|
|
||||||
});
|
|
||||||
createXCode(params).then((res) => {
|
|
||||||
console.log(res);
|
|
||||||
setGenRule(defaultGenRule);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultGenRule = { day: 30, amount: 10 };
|
|
||||||
|
|
||||||
const [genRule, setGenRule] = useState(defaultGenRule);
|
|
||||||
|
|
||||||
const onSearch = (value: string) => console.log(value);
|
|
||||||
|
|
||||||
useMount(() => {
|
|
||||||
selectXCodeList().then((res: any) => {
|
|
||||||
const { code, data } = res;
|
|
||||||
if (code === 10000)
|
|
||||||
setDataSource(data.map((i: any) => ({ ...i, key: i.code })));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="generate-invite-code">
|
|
||||||
<Card>
|
|
||||||
<Row>
|
|
||||||
<Col span={12}>
|
|
||||||
<Space>
|
|
||||||
<Segmented options={["生", "熟"]} />
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
<Col span={12} style={{ textAlign: "right" }}>
|
|
||||||
<Space>
|
|
||||||
<Input
|
|
||||||
style={{ width: 80 }}
|
|
||||||
placeholder="数量"
|
|
||||||
suffix="个"
|
|
||||||
value={genRule.amount}
|
|
||||||
onChange={(e) =>
|
|
||||||
setGenRule((p: any) => ({
|
|
||||||
...p,
|
|
||||||
amount: +e.target.value.replace(/[^\-?\d.]/g, ""),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
style={{ width: 100 }}
|
|
||||||
placeholder="有效天数"
|
|
||||||
suffix="天"
|
|
||||||
value={genRule.day}
|
|
||||||
onChange={(e) =>
|
|
||||||
setGenRule((p: any) => ({
|
|
||||||
...p,
|
|
||||||
day: +e.target.value.replace(/[^\-?\d.]/g, ""),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Button type="primary" onClick={onClickGen}>
|
|
||||||
生成
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Table
|
|
||||||
style={{ marginTop: 24 }}
|
|
||||||
dataSource={dataSource}
|
|
||||||
columns={columns}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -5,14 +5,4 @@ import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tsconfigPaths()],
|
plugins: [react(), tsconfigPaths()],
|
||||||
server: {
|
|
||||||
port: 5174,
|
|
||||||
proxy: {
|
|
||||||
"/api": {
|
|
||||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
|
||||||
target: "http://127.0.0.1:7001/api/v1",
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,2 @@
|
||||||
# 代码中使用 process.env.OSS_SECRET
|
# 代码中使用 process.env.OSS_SECRET
|
||||||
OSS_SECRET=12345
|
OSS_SECRET=12345
|
||||||
|
|
||||||
# 腾讯vod
|
|
||||||
SECRET_ID=AKID534tZ7OvYzb2KQMwLYaVEl5FBwUtQWbU
|
|
||||||
SECRET_KEY=q9HD6lQimeLp9IH5h7NRJzUpNjwxmPq5
|
|
||||||
SUBAPPID=1500018521
|
|
||||||
SUBAPPID_OSS=1500018944
|
|
||||||
|
|
||||||
# 阿里sms
|
|
||||||
ACCESSKEY_ID=LTAIlHbKxMELdAM0
|
|
||||||
ACCESSKEY_SECRET=xLG8tBAMdUG8hVITgZEgl6lAgKamzC
|
|
1
apps/server/.gitignore
vendored
1
apps/server/.gitignore
vendored
|
@ -13,3 +13,4 @@ run/
|
||||||
*.un~
|
*.un~
|
||||||
.tsbuildinfo
|
.tsbuildinfo
|
||||||
.tsbuildinfo.*
|
.tsbuildinfo.*
|
||||||
|
public
|
|
@ -4,6 +4,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@backset/ui": "workspace:^1.0.0",
|
||||||
|
"@backset/util": "workspace:^1.0.0",
|
||||||
"@midwayjs/bootstrap": "^3.0.0",
|
"@midwayjs/bootstrap": "^3.0.0",
|
||||||
"@midwayjs/core": "^3.0.0",
|
"@midwayjs/core": "^3.0.0",
|
||||||
"@midwayjs/decorator": "^3.0.0",
|
"@midwayjs/decorator": "^3.0.0",
|
||||||
|
@ -11,10 +13,10 @@
|
||||||
"@midwayjs/koa": "^3.0.0",
|
"@midwayjs/koa": "^3.0.0",
|
||||||
"@midwayjs/logger": "^2.14.0",
|
"@midwayjs/logger": "^2.14.0",
|
||||||
"@midwayjs/validate": "^3.0.0",
|
"@midwayjs/validate": "^3.0.0",
|
||||||
|
"@midwayjs/view-ejs": "^3.0.0",
|
||||||
"@midwayjs/static-file": "^3.0.0",
|
"@midwayjs/static-file": "^3.0.0",
|
||||||
"@midwayjs/redis": "3.10.13",
|
"@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",
|
||||||
|
@ -22,15 +24,7 @@
|
||||||
"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",
|
"jquery": "3.6.3"
|
||||||
"vod-node-sdk": "1.1.0",
|
|
||||||
"@alicloud/dysmsapi20170525": "2.0.23",
|
|
||||||
"@alicloud/openapi-client": "0.4.5",
|
|
||||||
"@alicloud/tea-util": "1.4.5",
|
|
||||||
"@alicloud/tea-typescript": "1.8.0",
|
|
||||||
"object-hash": "3.0.0",
|
|
||||||
"nanoid": "3.3.4",
|
|
||||||
"crypto-js": "4.1.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@midwayjs/cli": "^2.0.0",
|
"@midwayjs/cli": "^2.0.0",
|
||||||
|
@ -43,14 +37,16 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
"@types/jsonwebtoken": "9.0.1",
|
"@types/jsonwebtoken": "9.0.1",
|
||||||
"@types/crypto-js": "4.1.1"
|
"@types/jquery": "3.5.16"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:be": "cross-env NODE_ENV=local midway-bin dev --ts",
|
|
||||||
"start": "NODE_ENV=production node ./bootstrap.js",
|
"start": "NODE_ENV=production node ./bootstrap.js",
|
||||||
|
"dev:be": "cross-env NODE_ENV=local midway-bin dev --ts",
|
||||||
|
"dev:fe": "cross-env RUNNING_ENV=dev webpack -w",
|
||||||
|
"build:fe": "cross-env RUNNING_ENV=prod webpack build",
|
||||||
"cov": "midway-bin cov --ts",
|
"cov": "midway-bin cov --ts",
|
||||||
"lint": "mwts check",
|
"lint": "mwts check",
|
||||||
"lint:fix": "mwts fix",
|
"lint:fix": "mwts fix",
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
/**
|
|
||||||
* 业务状态码
|
|
||||||
*/
|
|
||||||
export enum BizCode {
|
|
||||||
OK = 10000,
|
|
||||||
ERROR = 20000,
|
|
||||||
AUTH = 40000,
|
|
||||||
FORBID = 50000,
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
export const globalPrefix = '/api/v1';
|
|
||||||
|
|
||||||
const hour = 60 * 60 * 1000;
|
|
||||||
|
|
||||||
export const ADMIN = {
|
|
||||||
SIGN: '_sign_admin',
|
|
||||||
EXPIRED: 24 * hour,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WEB = {
|
|
||||||
SIGN: '_sign_web',
|
|
||||||
EXPIRED: 72 * hour,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 最后1小时续签
|
|
||||||
*/
|
|
||||||
export const SIGN_DEADLINE = 1 * hour;
|
|
|
@ -1,29 +1,16 @@
|
||||||
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';
|
|
||||||
import { globalPrefix } from './base.config';
|
|
||||||
|
|
||||||
export default (appInfo: MidwayAppInfo): MidwayConfig => {
|
export default (appInfo: MidwayAppInfo): MidwayConfig => {
|
||||||
return {
|
return {
|
||||||
keys: '1676532942172_2248',
|
keys: '1676532942172_2248',
|
||||||
koa: {
|
koa: {
|
||||||
port: 7001,
|
port: 7001,
|
||||||
globalPrefix,
|
|
||||||
},
|
},
|
||||||
upload: {
|
// ...
|
||||||
// mode: UploadMode, 默认为file,即上传到服务器临时目录,可以配置为 stream
|
view: {
|
||||||
mode: 'file',
|
mapping: {
|
||||||
// fileSize: string, 最大上传文件大小,默认为 10mb
|
'.ejs': 'ejs',
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,6 @@ export default (appInfo: MidwayAppInfo): MidwayConfig => {
|
||||||
database: 'backset',
|
database: 'backset',
|
||||||
synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
|
synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
|
||||||
logging: false,
|
logging: false,
|
||||||
connectTimeout: 15 * 1000,
|
|
||||||
|
|
||||||
// 扫描形式, 配置实体模型 entities: [Photo]
|
// 扫描形式, 配置实体模型 entities: [Photo]
|
||||||
entities: ['**/entity/*.entity{.ts,.js}'],
|
entities: ['**/entity/*.entity{.ts,.js}'],
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';
|
|
||||||
|
|
||||||
export default (appInfo: MidwayAppInfo): MidwayConfig => {
|
|
||||||
return {
|
|
||||||
typeorm: {
|
|
||||||
dataSource: {
|
|
||||||
default: {
|
|
||||||
/**
|
|
||||||
* 单数据库实例
|
|
||||||
*/
|
|
||||||
type: 'mysql',
|
|
||||||
host: 'backset-mysql',
|
|
||||||
port: 3307,
|
|
||||||
username: 'root',
|
|
||||||
password: 'root',
|
|
||||||
database: 'backset',
|
|
||||||
synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
|
|
||||||
logging: false,
|
|
||||||
connectTimeout: 15 * 1000,
|
|
||||||
|
|
||||||
// 扫描形式, 配置实体模型 entities: [Photo]
|
|
||||||
entities: ['**/entity/*.entity{.ts,.js}'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
redis: {
|
|
||||||
client: {
|
|
||||||
host: 'backset-redis', // Redis host
|
|
||||||
port: 26379, // Redis port
|
|
||||||
password: 'cr654654.',
|
|
||||||
db: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as MidwayConfig;
|
|
||||||
};
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { globalPrefix } from './base.config';
|
|
||||||
|
|
||||||
export const whiteApis = [
|
|
||||||
'/user/admin/auth',
|
|
||||||
'/user/web/auth',
|
|
||||||
'/user/web/sms',
|
|
||||||
'/course/select/all',
|
|
||||||
].map(api => globalPrefix + api);
|
|
|
@ -1,32 +1,28 @@
|
||||||
import {
|
import { Configuration, App } from '@midwayjs/core';
|
||||||
Configuration,
|
|
||||||
App,
|
|
||||||
Inject,
|
|
||||||
MidwayDecoratorService,
|
|
||||||
} from '@midwayjs/core';
|
|
||||||
import * as koa from '@midwayjs/koa';
|
import * as koa from '@midwayjs/koa';
|
||||||
import * as validate from '@midwayjs/validate';
|
import * as validate from '@midwayjs/validate';
|
||||||
import * as info from '@midwayjs/info';
|
import * as info from '@midwayjs/info';
|
||||||
|
import * as view from '@midwayjs/view-ejs';
|
||||||
import * as staticFile from '@midwayjs/static-file';
|
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';
|
||||||
import { ReportMiddleware } from './middleware/report.middleware';
|
import { ReportMiddleware } from './middleware/report.middleware';
|
||||||
import { AuthMiddleware } from './middleware/auth.middleware';
|
import { LocalMiddleware } from './middleware/local.middleware';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@Configuration({
|
@Configuration({
|
||||||
imports: [
|
imports: [
|
||||||
koa,
|
koa,
|
||||||
validate,
|
validate,
|
||||||
staticFile,
|
staticFile,
|
||||||
|
view,
|
||||||
orm,
|
orm,
|
||||||
redis,
|
redis,
|
||||||
upload,
|
|
||||||
{
|
{
|
||||||
component: info,
|
component: info,
|
||||||
enabledEnvironment: ['local'],
|
enabledEnvironment: ['local'],
|
||||||
|
@ -38,12 +34,9 @@ export class ContainerLifeCycle {
|
||||||
@App()
|
@App()
|
||||||
app: koa.Application;
|
app: koa.Application;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
decoratorService: MidwayDecoratorService;
|
|
||||||
|
|
||||||
async onReady() {
|
async onReady() {
|
||||||
// add middleware
|
// add middleware
|
||||||
this.app.useMiddleware([ReportMiddleware, AuthMiddleware]);
|
this.app.useMiddleware([ReportMiddleware, LocalMiddleware]);
|
||||||
// add filter
|
// add filter
|
||||||
this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
|
this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
|
||||||
}
|
}
|
||||||
|
|
18
apps/server/src/controller/api.controller.ts
Normal file
18
apps/server/src/controller/api.controller.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { Inject, Controller, Post, Body } from '@midwayjs/core';
|
||||||
|
import { Context } from '@midwayjs/koa';
|
||||||
|
import { Validate } from '@midwayjs/validate';
|
||||||
|
import { UserDTO } from '../dto/user.dto';
|
||||||
|
|
||||||
|
@Controller('/api')
|
||||||
|
export class APIController {
|
||||||
|
@Inject()
|
||||||
|
ctx: Context;
|
||||||
|
|
||||||
|
@Post('/get_user')
|
||||||
|
@Validate({
|
||||||
|
errorStatus: 422,
|
||||||
|
})
|
||||||
|
async getUser(@Body() user: UserDTO) {
|
||||||
|
return { success: true, message: 'OK', data: user };
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,170 +0,0 @@
|
||||||
import { Body, Controller, Inject, Post } from '@midwayjs/core';
|
|
||||||
import { Context } from '@midwayjs/koa';
|
|
||||||
import { BizCode } from '../biz/code';
|
|
||||||
import { WEB } from '../config/base.config';
|
|
||||||
import { CourseCreateDTO } from '../dto/course.dto';
|
|
||||||
import { Chapter } from '../entity/chapter.entity';
|
|
||||||
import { Course } from '../entity/course.entity';
|
|
||||||
import { Guide } from '../entity/guide.entity';
|
|
||||||
import { ChapterService } from '../service/chapter.service';
|
|
||||||
import { CourseService } from '../service/course.service';
|
|
||||||
import { GuideService } from '../service/guide.service';
|
|
||||||
import { UserService } from '../service/user.service';
|
|
||||||
import { decodeToken } from '../util/encrypt';
|
|
||||||
|
|
||||||
@Controller('/course')
|
|
||||||
export class CourseController {
|
|
||||||
@Inject()
|
|
||||||
ctx: Context;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
courseService: CourseService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
chapterService: ChapterService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
guideService: GuideService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
userService: UserService;
|
|
||||||
|
|
||||||
@Post('/create')
|
|
||||||
async create(@Body() param: CourseCreateDTO) {
|
|
||||||
try {
|
|
||||||
const { course_chapterList, course_guide, ...rest } = param;
|
|
||||||
const courseId = await this.courseService.create({ ...rest });
|
|
||||||
await this.chapterService.create(
|
|
||||||
course_chapterList.map((i: any) => ({
|
|
||||||
...i,
|
|
||||||
chapter_course_id: courseId,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
await this.guideService.create({
|
|
||||||
...course_guide,
|
|
||||||
guide_course_id: courseId,
|
|
||||||
});
|
|
||||||
return { code: BizCode.OK };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return {
|
|
||||||
code: BizCode.ERROR,
|
|
||||||
msg: `[error] course/create error happened`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/select/all')
|
|
||||||
async selectAll(@Body() params) {
|
|
||||||
const { all = false } = params;
|
|
||||||
const courseList = await this.courseService.selectAll(all);
|
|
||||||
return { code: BizCode.OK, data: courseList };
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/detail/select')
|
|
||||||
async selectDetailByCourseId(@Body() params) {
|
|
||||||
const { course_id } = params;
|
|
||||||
try {
|
|
||||||
const token = this.ctx.cookies.get(WEB.SIGN);
|
|
||||||
const { user_login } = decodeToken(token);
|
|
||||||
const user = await this.userService.select({ user_login });
|
|
||||||
// 用户订阅鉴权
|
|
||||||
if (!user.user_sub)
|
|
||||||
return { code: BizCode.AUTH, msg: '无权访问订阅课程' };
|
|
||||||
if (+user.user_sub_expired < Date.now())
|
|
||||||
return { code: BizCode.AUTH, msg: '订阅已过期' };
|
|
||||||
const course = await this.courseService.select({ course_id });
|
|
||||||
const chapterList = await this.chapterService.select(course_id);
|
|
||||||
const guide = await this.guideService.select(course_id);
|
|
||||||
return { code: BizCode.OK, data: { chapterList, guide, course } };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: '[error] /chapter/select error' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/update')
|
|
||||||
async updateCourse(@Body() course: Course) {
|
|
||||||
try {
|
|
||||||
await this.courseService.update(course);
|
|
||||||
return { code: BizCode.OK };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/chapter/select')
|
|
||||||
async selectChapterList(@Body() chapter: Chapter) {
|
|
||||||
const { chapter_course_id } = chapter;
|
|
||||||
const chapterList = await this.chapterService.select(chapter_course_id);
|
|
||||||
return { code: BizCode.OK, data: chapterList };
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/chapter/update')
|
|
||||||
async updateChapter(@Body() chapter: Chapter) {
|
|
||||||
try {
|
|
||||||
await this.chapterService.update(chapter);
|
|
||||||
return { code: BizCode.OK };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/remove')
|
|
||||||
async remove(@Body() course: Course) {
|
|
||||||
const { course_id } = course;
|
|
||||||
try {
|
|
||||||
await this.courseService.remove(course_id);
|
|
||||||
await this.chapterService.removeByCourseId(course_id);
|
|
||||||
await this.guideService.removeByCourseId(course_id);
|
|
||||||
return { code: BizCode.OK };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/chapter/create')
|
|
||||||
async createChapter(@Body() chapterList: Chapter[]) {
|
|
||||||
try {
|
|
||||||
// order默认为-1
|
|
||||||
await this.chapterService.create(chapterList);
|
|
||||||
return { code: BizCode.OK };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/chapter/remove')
|
|
||||||
async removeChapter(@Body() chapter: Chapter) {
|
|
||||||
try {
|
|
||||||
const { chapter_id } = chapter;
|
|
||||||
await this.chapterService.remove(chapter_id);
|
|
||||||
return { code: BizCode.OK };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/guide/select')
|
|
||||||
async selectGuide(@Body() guide: Guide) {
|
|
||||||
const { guide_course_id: course_id } = guide;
|
|
||||||
const data = await this.guideService.select(course_id);
|
|
||||||
return { code: BizCode.OK, data };
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/guide/update')
|
|
||||||
async updateGuide(@Body() guide: Guide) {
|
|
||||||
try {
|
|
||||||
await this.guideService.update(guide);
|
|
||||||
return { code: BizCode.OK };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
47
apps/server/src/controller/home.controller.ts
Normal file
47
apps/server/src/controller/home.controller.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { Controller, Get, Inject } from '@midwayjs/core';
|
||||||
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
|
import { Context } from '@midwayjs/koa';
|
||||||
|
import { Photo } from '../entity/photo.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Controller('/')
|
||||||
|
export class HomeController {
|
||||||
|
@Inject()
|
||||||
|
ctx: Context;
|
||||||
|
|
||||||
|
@InjectEntityModel(Photo)
|
||||||
|
photoModel: Repository<Photo>;
|
||||||
|
|
||||||
|
@Get('/')
|
||||||
|
async home() {
|
||||||
|
await this.ctx.render('page/home/index.ejs', {
|
||||||
|
name: 'home',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/signup')
|
||||||
|
async signup() {
|
||||||
|
await this.ctx.render('page/signup/index.ejs', {
|
||||||
|
name: 'signup',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/testMysql')
|
||||||
|
async testMysql() {
|
||||||
|
// create a entity object
|
||||||
|
let photo = new Photo();
|
||||||
|
photo.name = 'Me and Bears';
|
||||||
|
photo.description = 'I am near polar bears';
|
||||||
|
photo.filename = 'photo-with-bears.jpg';
|
||||||
|
photo.views = 1;
|
||||||
|
photo.isPublished = true;
|
||||||
|
|
||||||
|
// save entity
|
||||||
|
const photoResult = await this.photoModel.save(photo);
|
||||||
|
|
||||||
|
// save success
|
||||||
|
console.log('photo id = ', photoResult.id);
|
||||||
|
|
||||||
|
this.ctx.body = photoResult.id;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,154 +0,0 @@
|
||||||
import { Body, Controller, Get, Inject, Post } from '@midwayjs/core';
|
|
||||||
import { Context } from '@midwayjs/koa';
|
|
||||||
import { BizCode } from '../biz/code';
|
|
||||||
import { UserAdminAuthDTO, UserWebAuthDTO } from '../dto/user.dto';
|
|
||||||
import { XCodeService } from '../service/xcode.service';
|
|
||||||
import { UserService } from '../service/user.service';
|
|
||||||
import { createToken, decodeToken } from '../util/encrypt';
|
|
||||||
import { SmsService } from '../service/sms.service';
|
|
||||||
import { SmsDTO } from '../dto/sms.dto';
|
|
||||||
import { RedisService } from '@midwayjs/redis';
|
|
||||||
import * as CryptoJS from 'crypto-js';
|
|
||||||
import { ADMIN, WEB } from '../config/base.config';
|
|
||||||
import { User } from '../entity/user.entity';
|
|
||||||
@Controller('/user')
|
|
||||||
export class UserController {
|
|
||||||
@Inject()
|
|
||||||
ctx: Context;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
userService: UserService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
xcodeService: XCodeService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
smsService: SmsService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
redisService: RedisService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户前台登录
|
|
||||||
*/
|
|
||||||
@Post('/web/auth')
|
|
||||||
async webAuth(@Body() params: UserWebAuthDTO) {
|
|
||||||
try {
|
|
||||||
const verifyCode = await this.redisService.get('' + params.user_login);
|
|
||||||
if (!verifyCode) return { code: BizCode.ERROR, msg: '验证码无效' };
|
|
||||||
// 查询用户是否存在
|
|
||||||
const userExist = await this.userService.select(params);
|
|
||||||
// 用户是否被封号
|
|
||||||
if (!userExist?.user_status)
|
|
||||||
return { code: BizCode.FORBID, msg: '您的账号被封禁' };
|
|
||||||
const payload = userExist?.id
|
|
||||||
? userExist
|
|
||||||
: await this.userService.createUser(params);
|
|
||||||
const expiredIn = new Date(Date.now() + WEB.EXPIRED);
|
|
||||||
const token = createToken({
|
|
||||||
...payload,
|
|
||||||
hasLogin: true,
|
|
||||||
expiredIn,
|
|
||||||
platform: 'web',
|
|
||||||
});
|
|
||||||
this.ctx.cookies.set(WEB.SIGN, token, {
|
|
||||||
expires: expiredIn,
|
|
||||||
httpOnly: false,
|
|
||||||
});
|
|
||||||
await this.redisService.del('' + params.user_login);
|
|
||||||
return {
|
|
||||||
code: BizCode.OK,
|
|
||||||
msg: '欢迎来到 backset.cn',
|
|
||||||
data: { ...payload },
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: '[error] web/auth error' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理员登录
|
|
||||||
*/
|
|
||||||
@Post('/admin/auth')
|
|
||||||
async AdminAuth(@Body() params: UserAdminAuthDTO) {
|
|
||||||
try {
|
|
||||||
const { username, password } = params;
|
|
||||||
const expiredIn = new Date(Date.now() + ADMIN.EXPIRED);
|
|
||||||
const token = createToken({
|
|
||||||
hasLogin: true,
|
|
||||||
expiredIn,
|
|
||||||
platform: 'admin',
|
|
||||||
});
|
|
||||||
if (username === 'admin' && password === '123123') {
|
|
||||||
this.ctx.cookies.set(ADMIN.SIGN, token, {
|
|
||||||
expires: expiredIn,
|
|
||||||
httpOnly: false,
|
|
||||||
});
|
|
||||||
return { code: BizCode.OK };
|
|
||||||
} else {
|
|
||||||
return { code: BizCode.ERROR, msg: '用户名密码错误' };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('/web/state')
|
|
||||||
async state() {
|
|
||||||
try {
|
|
||||||
const token = this.ctx.cookies.get(WEB.SIGN);
|
|
||||||
const user = decodeToken(token);
|
|
||||||
const { user_login } = user;
|
|
||||||
// 查询用户是否被封号
|
|
||||||
const user_current = await this.userService.select({ user_login });
|
|
||||||
const { user_status } = user_current;
|
|
||||||
if (!user_status) return { code: BizCode.FORBID, msg: '您的账号被封禁' };
|
|
||||||
return { code: BizCode.OK, data: user };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: '[error] /web/state error' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/web/sms')
|
|
||||||
async verifyCode(@Body() params: SmsDTO) {
|
|
||||||
try {
|
|
||||||
const { phoneNumber: phoneNumbers, sign } = params;
|
|
||||||
// 查询手机号是否被封禁
|
|
||||||
const user = await this.userService.select({ user_login: phoneNumbers });
|
|
||||||
if (user && !user.user_status)
|
|
||||||
return { code: BizCode.FORBID, msg: '您的账号被封禁' };
|
|
||||||
// 防止接口调用 start
|
|
||||||
const decrypted = CryptoJS.AES.decrypt(sign, phoneNumbers);
|
|
||||||
const hackAction = decrypted.toString(CryptoJS.enc.Utf8) !== phoneNumbers;
|
|
||||||
if (hackAction) return { code: BizCode.ERROR, msg: 'fuck u' };
|
|
||||||
// 防止接口调用 end
|
|
||||||
const code = Math.floor(Math.random() * 9000 + 1000);
|
|
||||||
await this.redisService.set('' + phoneNumbers, code, 'EX', 60);
|
|
||||||
await this.smsService.send({ code, phoneNumbers });
|
|
||||||
return { code: BizCode.OK };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: '[error] /web/sms error' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/admin/select/all')
|
|
||||||
async selectUser() {
|
|
||||||
const data = await this.userService.selectAll();
|
|
||||||
return { code: BizCode.OK, data };
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/admin/update')
|
|
||||||
async updateUser(@Body() user: User) {
|
|
||||||
try {
|
|
||||||
await this.userService.update(user);
|
|
||||||
return { code: BizCode.OK };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
import { Inject, Post, Body, Files, Controller, Context } from '@midwayjs/core';
|
|
||||||
import { BizCode } from '../biz/code';
|
|
||||||
import { VodSearchDTO } from '../dto/vod.dto';
|
|
||||||
import { uploadImagePromise } from '../util/vod';
|
|
||||||
import { IVodResponse } from '../interface';
|
|
||||||
import { VodService } from '../service/vod.service';
|
|
||||||
|
|
||||||
@Controller('/vod')
|
|
||||||
export class VodController {
|
|
||||||
@Inject()
|
|
||||||
ctx: Context;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
vodService: VodService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 腾讯媒资管理查询
|
|
||||||
* 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, fileId } = param;
|
|
||||||
try {
|
|
||||||
// 单个媒体查询
|
|
||||||
if (fileId) {
|
|
||||||
const { data } = await this.vodService.selectMediaByFileId(fileId);
|
|
||||||
if (data) return { code: BizCode.OK, data };
|
|
||||||
}
|
|
||||||
const { data } = await this.vodService.selectMediaList({ Offset, Limit });
|
|
||||||
if (data) return { code: BizCode.OK, data };
|
|
||||||
} catch (err) {
|
|
||||||
this.ctx.logger.error(err);
|
|
||||||
return { code: BizCode.ERROR, msg: '[error] vod create error' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 后台管理:上传课程封面图
|
|
||||||
*/
|
|
||||||
@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,29 +0,0 @@
|
||||||
import { Body, Context, Controller, Inject, Post } from '@midwayjs/core';
|
|
||||||
import { BizCode } from '../biz/code';
|
|
||||||
import { XCodeService } from '../service/xcode.service';
|
|
||||||
|
|
||||||
@Controller('/xcode')
|
|
||||||
export class XCodeController {
|
|
||||||
@Inject()
|
|
||||||
xcodeService: XCodeService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
ctx: Context;
|
|
||||||
|
|
||||||
@Post('/admin/create')
|
|
||||||
async create(@Body() codeList) {
|
|
||||||
try {
|
|
||||||
await this.xcodeService.create(codeList);
|
|
||||||
return { code: BizCode.OK };
|
|
||||||
} catch (error) {
|
|
||||||
this.ctx.logger.error(error);
|
|
||||||
return { code: BizCode.ERROR, msg: '[error] xcode/create error' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/admin/select/all')
|
|
||||||
async selectAll() {
|
|
||||||
const data = await this.xcodeService.selectAll();
|
|
||||||
return { code: BizCode.OK, data };
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Rule, RuleType } from '@midwayjs/validate';
|
|
||||||
|
|
||||||
export class CourseCreateDTO {
|
|
||||||
@Rule(RuleType.string().required())
|
|
||||||
course_title: string;
|
|
||||||
|
|
||||||
@Rule(RuleType.string().required())
|
|
||||||
course_cover_url: string;
|
|
||||||
|
|
||||||
@Rule(RuleType.string().required())
|
|
||||||
course_summary: string;
|
|
||||||
|
|
||||||
@Rule(RuleType.required())
|
|
||||||
course_chapterList: [];
|
|
||||||
|
|
||||||
@Rule(RuleType.required())
|
|
||||||
course_guide: { guide_value: string; guide_html: string };
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { Rule, RuleType } from '@midwayjs/validate';
|
|
||||||
|
|
||||||
export class SmsDTO {
|
|
||||||
@Rule(
|
|
||||||
RuleType.string().required().length(11).error(new Error('手机号格式错误'))
|
|
||||||
)
|
|
||||||
phoneNumber?: string;
|
|
||||||
@Rule(RuleType.string().required())
|
|
||||||
sign?: string;
|
|
||||||
}
|
|
|
@ -1,23 +1,16 @@
|
||||||
// src/dto/user.ts
|
// src/dto/user.ts
|
||||||
import { Rule, RuleType } from '@midwayjs/validate';
|
import { Rule, RuleType } from '@midwayjs/validate';
|
||||||
|
|
||||||
export class UserWebAuthDTO {
|
export class UserDTO {
|
||||||
@Rule(
|
@Rule(RuleType.number().required())
|
||||||
RuleType.string().required().length(11).error(new Error('手机号为11位数字'))
|
id: number;
|
||||||
)
|
|
||||||
user_login: string;
|
|
||||||
|
|
||||||
@Rule(RuleType.string().required().min(4).error(new Error('验证码4位数字')))
|
|
||||||
user_pass: string;
|
|
||||||
|
|
||||||
@Rule(RuleType.string().allow(''))
|
|
||||||
xcode: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserAdminAuthDTO {
|
|
||||||
@Rule(RuleType.string().required())
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@Rule(RuleType.string().required())
|
@Rule(RuleType.string().required())
|
||||||
password: string;
|
firstName: string;
|
||||||
|
|
||||||
|
@Rule(RuleType.string().max(10))
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@Rule(RuleType.number().max(60))
|
||||||
|
age: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
// src/dto/user.ts
|
|
||||||
import { Rule, RuleType } from '@midwayjs/validate';
|
|
||||||
|
|
||||||
export class VodSearchDTO {
|
|
||||||
@Rule(RuleType.number().required())
|
|
||||||
offset: number;
|
|
||||||
|
|
||||||
@Rule(RuleType.number().required())
|
|
||||||
limit: number;
|
|
||||||
|
|
||||||
@Rule(RuleType.string())
|
|
||||||
fileId?: string;
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity('chapter')
|
|
||||||
export class Chapter {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
chapter_id?: string;
|
|
||||||
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
chapter_title?: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
chapter_level?: '1' | '2';
|
|
||||||
|
|
||||||
@Column({ default: '' })
|
|
||||||
chapter_file_id?: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
chapter_course_id?: string;
|
|
||||||
|
|
||||||
@Column({ default: -1 })
|
|
||||||
order?: number;
|
|
||||||
|
|
||||||
@Column({ default: '' })
|
|
||||||
media_time?: string;
|
|
||||||
|
|
||||||
@Column({ default: '' })
|
|
||||||
media_url?: string;
|
|
||||||
|
|
||||||
@Column({ default: '' })
|
|
||||||
media_cover_url?: string;
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity('course')
|
|
||||||
export class Course {
|
|
||||||
@PrimaryColumn()
|
|
||||||
course_id?: string;
|
|
||||||
|
|
||||||
@Column({ type: 'varchar' })
|
|
||||||
course_title?: string;
|
|
||||||
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
course_summary?: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
course_cover_url?: string;
|
|
||||||
|
|
||||||
@Column({ default: 1 })
|
|
||||||
valid?: boolean;
|
|
||||||
|
|
||||||
@Column({ default: Date.now() })
|
|
||||||
course_createtime?: string;
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity('guide')
|
|
||||||
export class Guide {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
guide_id?: string;
|
|
||||||
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
guide_value: string;
|
|
||||||
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
guide_html: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
guide_course_id: string;
|
|
||||||
}
|
|
|
@ -1,31 +1,3 @@
|
||||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
export class User{
|
||||||
|
|
||||||
@Entity('user')
|
|
||||||
export class User {
|
|
||||||
@PrimaryGeneratedColumn('increment')
|
|
||||||
id?: number;
|
|
||||||
|
|
||||||
@Column({ unique: true })
|
|
||||||
user_login?: string;
|
|
||||||
|
|
||||||
@Column({ default: '' })
|
|
||||||
user_email?: string;
|
|
||||||
|
|
||||||
@Column({ default: Date.now() })
|
|
||||||
user_create_time?: string;
|
|
||||||
|
|
||||||
@Column({ default: true })
|
|
||||||
user_status?: boolean;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
display_name?: string;
|
|
||||||
|
|
||||||
@Column({ default: '' })
|
|
||||||
user_avatar?: string;
|
|
||||||
|
|
||||||
@Column({ default: false })
|
|
||||||
user_sub?: boolean;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
user_sub_expired?: string;
|
|
||||||
}
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity('xcode')
|
|
||||||
export class XCode {
|
|
||||||
@PrimaryColumn()
|
|
||||||
code?: string;
|
|
||||||
|
|
||||||
@Column({ default: Date.now() })
|
|
||||||
createtime?: string;
|
|
||||||
|
|
||||||
@Column({ default: '' })
|
|
||||||
expiretime?: string;
|
|
||||||
|
|
||||||
@Column({ default: -1 })
|
|
||||||
user_id?: number;
|
|
||||||
|
|
||||||
@Column({ default: '' })
|
|
||||||
user_usetime?: string;
|
|
||||||
}
|
|
|
@ -1,12 +1,11 @@
|
||||||
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 = { code: BizCode.ERROR, msg: err };
|
ctx.body = '迷路了'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,3 @@
|
||||||
export interface IUserOptions {
|
export interface IUserOptions {
|
||||||
uid: number;
|
uid: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVodResponse {
|
|
||||||
CoverUrl: string;
|
|
||||||
FileId: string;
|
|
||||||
MediaUrl: string;
|
|
||||||
RequestId: string;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
import {
|
|
||||||
Middleware,
|
|
||||||
IMiddleware,
|
|
||||||
App,
|
|
||||||
IMidwayApplication,
|
|
||||||
} from '@midwayjs/core';
|
|
||||||
import { NextFunction, Context } from '@midwayjs/koa';
|
|
||||||
import { BizCode } from '../biz/code';
|
|
||||||
import { ADMIN, SIGN_DEADLINE, WEB } from '../config/base.config';
|
|
||||||
import { whiteApis } from '../config/white.api';
|
|
||||||
import { createToken, decodeToken } from '../util/encrypt';
|
|
||||||
|
|
||||||
@Middleware()
|
|
||||||
export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
|
|
||||||
@App()
|
|
||||||
app: IMidwayApplication;
|
|
||||||
|
|
||||||
resolve() {
|
|
||||||
return async (ctx: Context, next: NextFunction) => {
|
|
||||||
const isWhiteApi = whiteApis.some(api => ctx.url.indexOf(api) > -1);
|
|
||||||
if (!isWhiteApi) {
|
|
||||||
const token = ctx.cookies.get(ADMIN.SIGN) ?? ctx.cookies.get(WEB.SIGN);
|
|
||||||
try {
|
|
||||||
const { hasLogin, expiredIn, platform, ...rest } = decodeToken(token);
|
|
||||||
// token缺少hasLogin
|
|
||||||
if (!hasLogin) return { code: BizCode.AUTH, msg: '身份验证错误' };
|
|
||||||
// 续签
|
|
||||||
const sign = platform === 'web' ? WEB.SIGN : ADMIN.SIGN;
|
|
||||||
const signExpired = platform === 'web' ? WEB.EXPIRED : ADMIN.EXPIRED;
|
|
||||||
const timeLeft = new Date(expiredIn).getTime() - Date.now();
|
|
||||||
if (timeLeft < SIGN_DEADLINE) {
|
|
||||||
const expiredIn = new Date(Date.now() + signExpired);
|
|
||||||
const token = createToken({
|
|
||||||
hasLogin: true,
|
|
||||||
platform,
|
|
||||||
expiredIn,
|
|
||||||
...rest,
|
|
||||||
});
|
|
||||||
ctx.cookies.set(sign, token, {
|
|
||||||
expires: expiredIn,
|
|
||||||
httpOnly: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await next();
|
|
||||||
} catch (error) {
|
|
||||||
ctx.logger.error(error);
|
|
||||||
return { code: BizCode.AUTH, msg: '身份验证错误' };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getName(): string {
|
|
||||||
return 'auth';
|
|
||||||
}
|
|
||||||
}
|
|
21
apps/server/src/middleware/local.middleware.ts
Normal file
21
apps/server/src/middleware/local.middleware.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Middleware, IMiddleware } from '@midwayjs/core';
|
||||||
|
import { NextFunction, Context } from '@midwayjs/koa';
|
||||||
|
|
||||||
|
const locals = {
|
||||||
|
assets: 'public/',
|
||||||
|
version: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
@Middleware()
|
||||||
|
export class LocalMiddleware implements IMiddleware<Context, NextFunction> {
|
||||||
|
resolve() {
|
||||||
|
return async (ctx: Context, next: NextFunction) => {
|
||||||
|
ctx.locals = locals;
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getName(): string {
|
||||||
|
return 'local';
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,51 +0,0 @@
|
||||||
import { Context, Inject, Provide } from '@midwayjs/core';
|
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { Chapter } from '../entity/chapter.entity';
|
|
||||||
import { VodService } from './vod.service';
|
|
||||||
|
|
||||||
@Provide()
|
|
||||||
export class ChapterService {
|
|
||||||
@Inject()
|
|
||||||
ctx: Context;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
vodService: VodService;
|
|
||||||
|
|
||||||
@InjectEntityModel(Chapter)
|
|
||||||
chapterModel: Repository<Chapter>;
|
|
||||||
|
|
||||||
async create(chapterList: Chapter[]) {
|
|
||||||
for (const chapter of chapterList) {
|
|
||||||
const { chapter_file_id: fileID } = chapter;
|
|
||||||
if (fileID) {
|
|
||||||
const { data } = await this.vodService.selectMediaByFileId(fileID);
|
|
||||||
chapter.media_cover_url = data.MediaInfoSet[0].BasicInfo.CoverUrl;
|
|
||||||
chapter.media_time = '' + data.MediaInfoSet[0].MetaData.Duration;
|
|
||||||
chapter.media_url =
|
|
||||||
data.MediaInfoSet[0].AdaptiveDynamicStreamingInfo.AdaptiveDynamicStreamingSet[0].Url;
|
|
||||||
}
|
|
||||||
await this.chapterModel.save(chapter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async select(course_id: string) {
|
|
||||||
const result = await this.chapterModel.find({
|
|
||||||
where: { chapter_course_id: course_id },
|
|
||||||
order: { order: 'asc' },
|
|
||||||
});
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { Context, Inject, Provide } from '@midwayjs/core';
|
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { Course } from '../entity/course.entity';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
|
|
||||||
export interface ICourseCreate {
|
|
||||||
course_title: string;
|
|
||||||
course_summary: string;
|
|
||||||
course_cover_url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provide()
|
|
||||||
export class CourseService {
|
|
||||||
@Inject()
|
|
||||||
ctx: Context;
|
|
||||||
|
|
||||||
@InjectEntityModel(Course)
|
|
||||||
courseModel: Repository<Course>;
|
|
||||||
|
|
||||||
async create(course: Course) {
|
|
||||||
course.course_id = nanoid(13);
|
|
||||||
const courseCreateRes = await this.courseModel.save(course);
|
|
||||||
return courseCreateRes.course_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectAll(all) {
|
|
||||||
if (!all) return await this.courseModel.find({ where: { valid: true } });
|
|
||||||
else return await this.courseModel.find();
|
|
||||||
}
|
|
||||||
|
|
||||||
async select(course: Course) {
|
|
||||||
const { course_id } = course;
|
|
||||||
return await this.courseModel.findOne({ where: { course_id } });
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(course: Course) {
|
|
||||||
return await this.courseModel.save(course);
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove(course_id: string) {
|
|
||||||
await this.courseModel.delete({ course_id });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { Context, Inject, Provide } from '@midwayjs/core';
|
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { Guide } from '../entity/guide.entity';
|
|
||||||
|
|
||||||
@Provide()
|
|
||||||
export class GuideService {
|
|
||||||
@Inject()
|
|
||||||
ctx: Context;
|
|
||||||
|
|
||||||
@InjectEntityModel(Guide)
|
|
||||||
guideModel: Repository<Guide>;
|
|
||||||
|
|
||||||
async create(guide: Guide) {
|
|
||||||
const guideCreateRes = await this.guideModel.save(guide);
|
|
||||||
return guideCreateRes.guide_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async select(course_id: string) {
|
|
||||||
return await this.guideModel.findOne({
|
|
||||||
where: { guide_course_id: course_id },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeByCourseId(course_id: string) {
|
|
||||||
await this.guideModel.delete({ guide_course_id: course_id });
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(guide: Guide) {
|
|
||||||
await this.guideModel.save(guide);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
// This file is auto-generated, don't edit it
|
|
||||||
import Dysmsapi20170525, * as $Dysmsapi20170525 from '@alicloud/dysmsapi20170525';
|
|
||||||
// 依赖的模块可通过下载工程中的模块依赖文件或右上角的获取 SDK 依赖信息查看
|
|
||||||
import * as $OpenApi from '@alicloud/openapi-client';
|
|
||||||
import Util, * as $Util from '@alicloud/tea-util';
|
|
||||||
import { Context, Inject, Provide } from '@midwayjs/core';
|
|
||||||
|
|
||||||
@Provide()
|
|
||||||
export class SmsService {
|
|
||||||
@Inject()
|
|
||||||
ctx: Context;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用AK&SK初始化账号Client
|
|
||||||
* @param accessKeyId
|
|
||||||
* @param accessKeySecret
|
|
||||||
* @return Client
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
createClient(accessKeyId: string, accessKeySecret: string): Dysmsapi20170525 {
|
|
||||||
const config = new $OpenApi.Config({ accessKeyId, accessKeySecret });
|
|
||||||
// 访问的域名
|
|
||||||
config.endpoint = `dysmsapi.aliyuncs.com`;
|
|
||||||
return new Dysmsapi20170525(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async send({ code, phoneNumbers }): Promise<void> {
|
|
||||||
// 工程代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378664.html
|
|
||||||
const client = this.createClient(
|
|
||||||
process.env['ACCESSKEY_ID'],
|
|
||||||
process.env['ACCESSKEY_SECRET']
|
|
||||||
);
|
|
||||||
const sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({
|
|
||||||
phoneNumbers: phoneNumbers,
|
|
||||||
signName: '寻鹿网',
|
|
||||||
templateCode: 'SMS_186510297',
|
|
||||||
templateParam: `{"code":"${code}"}`,
|
|
||||||
});
|
|
||||||
const runtime = new $Util.RuntimeOptions({});
|
|
||||||
try {
|
|
||||||
// 复制代码运行请自行打印 API 的返回值
|
|
||||||
await client.sendSmsWithOptions(sendSmsRequest, runtime);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(Util.assertAsString(error.message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +1,14 @@
|
||||||
import { Provide } from '@midwayjs/core';
|
import { Provide } from '@midwayjs/core';
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
import { IUserOptions } from '../interface';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { User } from '../entity/user.entity';
|
|
||||||
import hash from 'object-hash';
|
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
@InjectEntityModel(User)
|
async getUser(options: IUserOptions) {
|
||||||
userModel: Repository<User>;
|
return {
|
||||||
|
uid: options.uid,
|
||||||
async select(p: User): Promise<User> {
|
username: 'mockedName',
|
||||||
const { user_login } = p;
|
phone: '12345678901',
|
||||||
const user = await this.userModel.findOne({
|
email: 'xxx.xxx@xxx.com',
|
||||||
where: { user_login },
|
};
|
||||||
});
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectAll(): Promise<User[]> {
|
|
||||||
return await this.userModel.find();
|
|
||||||
}
|
|
||||||
|
|
||||||
async createUser(user: User) {
|
|
||||||
const h = hash('' + user.user_login);
|
|
||||||
user.display_name = h.substring(0, 8);
|
|
||||||
user.user_avatar = h;
|
|
||||||
const result = await this.userModel.save(user);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(user: User) {
|
|
||||||
this.userModel.save(user);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { Provide } from '@midwayjs/core';
|
|
||||||
import * as tencentcloud from 'tencentcloud-sdk-nodejs';
|
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
@Provide()
|
|
||||||
export class VodService {
|
|
||||||
/**
|
|
||||||
* 根据FileId查询单个媒体资源
|
|
||||||
*/
|
|
||||||
async selectMediaByFileId(fileId: string) {
|
|
||||||
const client = new VodClient(clientConfig);
|
|
||||||
const params = {
|
|
||||||
SubAppId: +process.env.SUBAPPID,
|
|
||||||
FileIds: [fileId],
|
|
||||||
};
|
|
||||||
return await client.SearchMedia(params).then(
|
|
||||||
data => ({ data }),
|
|
||||||
err => {
|
|
||||||
throw new Error(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询全部媒体列表
|
|
||||||
*/
|
|
||||||
async selectMediaList({ Offset = 0, Limit = 5000 }) {
|
|
||||||
const client = new VodClient(clientConfig);
|
|
||||||
const params = {
|
|
||||||
SubAppId: +process.env.SUBAPPID,
|
|
||||||
Categories: ['Video'],
|
|
||||||
Offset,
|
|
||||||
Limit,
|
|
||||||
};
|
|
||||||
return await client.SearchMedia(params).then(
|
|
||||||
data => ({ data }),
|
|
||||||
err => {
|
|
||||||
throw new Error(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { Provide } from '@midwayjs/core';
|
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { XCode } from '../entity/xcode.entity';
|
|
||||||
|
|
||||||
@Provide()
|
|
||||||
export class XCodeService {
|
|
||||||
@InjectEntityModel(XCode)
|
|
||||||
xcodeModel: Repository<XCode>;
|
|
||||||
|
|
||||||
async valid(code: string) {
|
|
||||||
const row = await this.xcodeModel.findOne({ where: { code } });
|
|
||||||
return row && row.user_id === -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
async use(code: string, user_id: number) {
|
|
||||||
await this.xcodeModel.update(
|
|
||||||
{ code },
|
|
||||||
{ user_id, user_usetime: '' + Date.now() }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(codeList: XCode[]) {
|
|
||||||
await this.xcodeModel.save(codeList);
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectAll() {
|
|
||||||
return await this.xcodeModel.query(
|
|
||||||
'SELECT xcode.*,`user`.user_login FROM `xcode` LEFT JOIN `user` ON xcode.user_id = `user`.id'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { VodUploadClient, VodUploadRequest } from 'vod-node-sdk';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传课程封面图片
|
|
||||||
* @param {string} tmpPath 图片上传midwayjs缓存的地址
|
|
||||||
*/
|
|
||||||
export const uploadImagePromise = (tmpPath: string, subAppId: number) => {
|
|
||||||
const { SECRET_ID, SECRET_KEY } = process.env;
|
|
||||||
const client = new VodUploadClient(SECRET_ID, SECRET_KEY);
|
|
||||||
const req = new VodUploadRequest();
|
|
||||||
req.MediaFilePath = tmpPath;
|
|
||||||
req.SubAppId = subAppId;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
client.upload('ap-shanghai', req, (err, data) => {
|
|
||||||
err ? reject(err) : resolve(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
1
apps/server/view/assets/img/annotation.svg
Normal file
1
apps/server/view/assets/img/annotation.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg t="1667271285418" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="34809" width="200" height="200"><path d="M158.165333 499.498667A42.496 42.496 0 0 0 170.666667 469.333333V256a42.666667 42.666667 0 0 1 42.666666-42.666667 42.666667 42.666667 0 0 0 0-85.333333C142.762667 128 85.333333 185.429333 85.333333 256v195.669333l-30.165333 30.165334a42.666667 42.666667 0 0 0 0 60.330666l30.165333 30.165334V768c0 70.570667 57.429333 128 128 128a42.666667 42.666667 0 0 0 0-85.333333 42.666667 42.666667 0 0 1-42.666666-42.666667v-213.333333a42.496 42.496 0 0 0-12.501334-30.165334L145.664 512l12.501333-12.501333zM978.090667 495.658667a42.709333 42.709333 0 0 0-9.258667-13.824L938.666667 451.669333V256c0-70.570667-57.429333-128-128-128a42.666667 42.666667 0 1 0 0 85.333333 42.666667 42.666667 0 0 1 42.666666 42.666667v213.333333a42.581333 42.581333 0 0 0 12.501334 30.165334l12.501333 12.501333-12.501333 12.501333A42.496 42.496 0 0 0 853.333333 554.666667v213.333333a42.666667 42.666667 0 0 1-42.666666 42.666667 42.666667 42.666667 0 1 0 0 85.333333c70.570667 0 128-57.429333 128-128v-195.669333l30.165333-30.165334a42.709333 42.709333 0 0 0 9.258667-46.506666zM669.738667 225.450667a42.752 42.752 0 0 0-69.546667 14.762666l-255.829333 512a42.624 42.624 0 0 0 23.893333 55.424 42.922667 42.922667 0 0 0 55.552-23.765333l255.786667-512a42.538667 42.538667 0 0 0-9.813334-46.421333z" p-id="34810" fill="#2c2c2c"></path></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
apps/server/view/assets/img/icon-earth.svg
Normal file
1
apps/server/view/assets/img/icon-earth.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><g fill="none"><path d="M1 7.998a6.998 6.998 0 1 1 13.996 0A6.998 6.998 0 0 1 1 7.998zM7.998 2h-.05c.092.18.192.395.282.634c.243.646.464 1.592.071 2.428c-.362.77-.985.97-1.45 1.09l-.068.017c-.452.117-.64.165-.775.37c-.126.192-.104.433.04.9l.032.105c.057.182.125.398.16.602c.045.254.057.572-.103.88a1.51 1.51 0 0 1-.622.651c-.235.128-.48.177-.664.21l-.069.012c-.358.063-.54.095-.714.282c-.137.147-.222.402-.272.772c-.02.151-.032.301-.045.456l-.006.082c-.014.17-.032.364-.07.53l-.024.11a5.981 5.981 0 0 0 4.347 1.866a5.97 5.97 0 0 0 3.054-.835a3.16 3.16 0 0 1-.258-.286c-.237-.298-.544-.807-.438-1.406c.051-.287.205-.529.356-.716c.154-.19.34-.366.503-.517a40.6 40.6 0 0 1 .111-.101c.125-.115.233-.213.324-.309a1.32 1.32 0 0 0 .125-.146c.023-.033.03-.05.031-.053c.05-.167-.01-.29-.084-.347c-.055-.042-.195-.105-.446.053a8.542 8.542 0 0 1-.253.158a.985.985 0 0 1-.275.117a.534.534 0 0 1-.634-.36a.637.637 0 0 1-.028-.2a1.153 1.153 0 0 1 .016-.189c.025-.21.063-.52-.057-.982c-.097-.371-.238-.654-.382-.942a9.148 9.148 0 0 1-.196-.412c-.088-.2-.184-.46-.167-.736c.02-.32.181-.58.442-.776c.317-.238.716-.783 1.061-1.334c.165-.263.307-.51.407-.69l.023-.042A5.97 5.97 0 0 0 7.998 2zm4.037 1.561c-.102.18-.233.407-.384.648c-.339.54-.824 1.24-1.309 1.603a.244.244 0 0 0-.044.04c0 .027.01.101.084.27c.033.073.078.165.13.27c.157.314.376.755.5 1.234c.067.256.099.484.11.681c.436-.144.874-.078 1.212.183c.418.323.593.885.43 1.428c-.076.248-.25.453-.39.6c-.118.123-.26.252-.388.37l-.094.085a4.033 4.033 0 0 0-.401.41c-.106.13-.142.215-.15.262c-.03.165.051.377.236.61a2.221 2.221 0 0 0 .307.313a5.985 5.985 0 0 0 2.112-4.57c0-1.758-.756-3.34-1.961-4.437zM2 7.998a5.97 5.97 0 0 0 .787 2.973l.018-.15c.054-.405.168-.933.532-1.322c.414-.444.923-.528 1.248-.581c.032-.005.063-.01.091-.016c.174-.03.279-.058.36-.103a.515.515 0 0 0 .214-.236c.014-.027.033-.09.006-.245c-.023-.133-.067-.272-.123-.451a28.642 28.642 0 0 1-.041-.134c-.13-.425-.334-1.115.08-1.744c.361-.547.94-.686 1.314-.777l.114-.028c.398-.103.633-.2.796-.547c.212-.452.12-1.062-.102-1.65a5.381 5.381 0 0 0-.43-.88A6 6 0 0 0 2 7.998z" fill="currentColor"></path></g></svg>
|
After Width: | Height: | Size: 2.2 KiB |
BIN
apps/server/view/assets/img/logo.png
Normal file
BIN
apps/server/view/assets/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
235
apps/server/view/assets/img/signup-banner.svg
Normal file
235
apps/server/view/assets/img/signup-banner.svg
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="865.76" height="682.89" viewBox="0 0 865.76 682.89">
|
||||||
|
<defs>
|
||||||
|
<style xmlns="http://www.w3.org/1999/xhtml">*, ::after, ::before { box-sizing: border-box; }
|
||||||
|
img, svg { vertical-align: middle; }
|
||||||
|
</style>
|
||||||
|
<style xmlns="http://www.w3.org/1999/xhtml">*, body, html { -webkit-font-smoothing: antialiased; }
|
||||||
|
img, svg { max-width: 100%; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<rect x="167.21" y="379.13" width="39.09" height="16.52" fill="#ebebeb"/>
|
||||||
|
<path d="M189,358.3l19.37-.42,9.83-.16c3.29-.07,6.6,0,9.89-.07h.65v.66l.09,16.52v.74h-.71l-19.55-.09c-6.51,0-13-.18-19.54-.25h-.42v-.4c0-2.8.12-5.59.2-8.34Zm0,0,.21,8.34c0,2.76.16,5.47.19,8.18l-.4-.4c6.51-.08,13-.24,19.54-.25l19.58-.09-.74.74V358.3l.74.72c-3.22,0-6.44,0-9.67-.09l-9.71-.16Z" fill="#ebebeb"/>
|
||||||
|
<path d="M145.48,358.3l19.37-.42,9.83-.16c3.29-.07,6.58,0,9.89-.07h.65v.66l.09,16.52v.74h-.74L165,375.48c-6.53,0-13-.18-19.55-.25h-.41v-.4c0-2.8.13-5.59.19-8.34Zm0,0,.21,8.34c0,2.76.16,5.47.19,8.18l-.4-.4c6.51-.08,13-.24,19.55-.25l19.54-.09-.74.74V358.3l.67.66c-3.24,0-6.44,0-9.67-.09l-9.72-.16Z" fill="#ebebeb"/>
|
||||||
|
<path d="M746.68,51.89l-19.36.41-9.83.16c-3.3.08-6.6,0-9.89.09H707v-.66l-.09-16.52v-.74h.74l19.54.1c6.52,0,13,.17,19.54.24h.42v.4c0,2.8-.13,5.59-.19,8.35Zm0,0-.2-8.34c0-2.75-.16-5.47-.19-8.18l.39.41c-6.51,0-13,.24-19.54.24l-19.54.09.73-.74-.07,16.52-.66-.65h9.66l9.72.17Z" fill="#ebebeb"/>
|
||||||
|
<rect x="729.4" y="56.2" width="39.09" height="16.52" fill="#ebebeb"/>
|
||||||
|
<path d="M184.51,52l-19.38.39-9.83.18H144.76V34.7h.72l19.55.09c6.51,0,13,.16,19.54.24h.4v.4c0,2.79-.13,5.58-.19,8.34Zm0,0-.22-8.36c0-2.75-.16-5.46-.19-8.16l.41.4c-6.52.07-13,.23-19.54.23l-19.49,0,.72-.74L146.07,52l-.59-.73c3.22,0,6.42,0,9.66.09l9.71.16Z" fill="#ebebeb"/>
|
||||||
|
<rect x="420.15" y="139.6" width="39.09" height="16.52" fill="#ebebeb"/>
|
||||||
|
<path d="M729.4,364.47l19.38-.42,9.83-.16c3.29-.07,6.59,0,9.88-.09h.66v.67l.08,16.52v.73h-.8l-19.55-.08c-6.51,0-13-.18-19.54-.26h-.4V381c0-2.81.13-5.6.19-8.34Zm0,0,.22,8.34c0,2.76.16,5.47.19,8.18l-.41-.4c6.52-.08,13-.24,19.54-.25l19.55-.09-.73.74.08-16.52.65.66c-3.22,0-6.42,0-9.66-.09l-9.71-.16Z" fill="#ebebeb"/>
|
||||||
|
<rect x="473.81" y="501.71" width="39.09" height="16.52" fill="#ebebeb"/>
|
||||||
|
<path d="M441.94,118.77l19.44-.37,9.83-.16h10.54v.67l.08,16.51v.74h-.74l-19.54-.09c-6.53,0-13-.16-19.55-.25h-.41v-.4c0-2.8.13-5.59.19-8.34Zm0,0,.22,8.36c0,2.74.17,5.46.2,8.16l-.4-.4c6.51,0,13-.23,19.54-.23l19.55-.11-.74.74.07-16.52.67.67c-3.24,0-6.44,0-9.67-.09l-9.72-.16Z" fill="#ebebeb"/>
|
||||||
|
<polygon points="246.63 502.21 197.09 502.21 168.75 360.87 214.09 340.02 246.63 502.21" fill="#dbdbdb"/>
|
||||||
|
<polygon points="388.43 502.21 246.63 502.21 214.09 340.02 355.9 340.02 388.43 502.21" fill="#ebebeb"/>
|
||||||
|
<rect x="145.27" y="426.38" width="75.92" height="155.78" fill="#dbdbdb"/>
|
||||||
|
<polygon points="424.86 582.16 404.12 582.16 373.21 426.38 424.86 426.38 424.86 582.16" fill="#dbdbdb"/>
|
||||||
|
<rect x="221.19" y="426.38" width="41.72" height="155.78" fill="#ebebeb"/>
|
||||||
|
<rect x="424.86" y="426.38" width="41.72" height="155.78" fill="#ebebeb"/>
|
||||||
|
<rect x="262.91" y="502.21" width="161.95" height="79.94" fill="#ebebeb"/>
|
||||||
|
<rect x="262.91" y="467.65" width="161.95" height="34.88" fill="#ebebeb"/>
|
||||||
|
<path d="M231.42,426.38l15.76-.28,15.73-.21h.47v.49l.2,18.26v18.23c0,12.15.08,24.3,0,36.46s0,24.3-.21,36.46-.27,24.3-.53,36.46q-.39-18.24-.52-36.46c-.16-12.16-.18-24.31-.22-36.46s0-24.31,0-36.46V444.64l.18-18.23.47.47L247,426.69Z" fill="#dbdbdb"/>
|
||||||
|
<path d="M424.86,426.38c.33,12.19.48,24.38.61,36.59l.18,36.6-.18,36.59-.22,18.31-.39,18.29-.38-18.29-.22-18.31-.18-36.59.18-36.6C424.39,450.76,424.48,438.57,424.86,426.38Z" fill="#dbdbdb"/>
|
||||||
|
<path d="M424.86,502.21c-13.49.32-27,.47-40.49.6l-40.48.18-40.46-.18-20.24-.22-20.28-.38,20.25-.38,20.24-.23,40.49-.17,40.48.17C397.91,501.74,411.37,501.88,424.86,502.21Z" fill="#dbdbdb"/>
|
||||||
|
<path d="M424.86,467.65c-13.49.33-27,.47-40.49.61l-40.48.17-40.46-.17L283.19,468l-20.25-.39,20.25-.38,20.24-.22,40.49-.18,40.48.18C397.91,467.18,411.37,467.33,424.86,467.65Z" fill="#dbdbdb"/>
|
||||||
|
<polygon points="174.13 582.14 173.1 594.77 170.71 623.93 159.71 623.93 157.32 594.77 156.29 582.14 174.13 582.14" fill="#c7c7c7"/>
|
||||||
|
<polygon points="156.29 582.14 174.13 582.14 173.1 594.77 157.32 594.77 156.29 582.14" fill="#a6a6a6"/>
|
||||||
|
<polygon points="250.98 582.14 249.95 594.77 247.56 623.93 236.56 623.93 234.17 594.77 233.14 582.14 250.98 582.14" fill="#c7c7c7"/>
|
||||||
|
<polygon points="233.14 582.14 250.98 582.14 249.95 594.77 234.17 594.77 233.14 582.14" fill="#a6a6a6"/>
|
||||||
|
<polygon points="378.44 582.14 377.42 594.77 375.03 623.93 364.03 623.93 361.64 594.77 360.61 582.14 378.44 582.14" fill="#c7c7c7"/>
|
||||||
|
<polygon points="360.61 582.14 378.44 582.14 377.42 594.77 361.64 594.77 360.61 582.14" fill="#a6a6a6"/>
|
||||||
|
<polygon points="455.32 582.14 454.28 594.77 451.89 623.93 440.89 623.93 438.5 594.77 437.47 582.14 455.32 582.14" fill="#c7c7c7"/>
|
||||||
|
<polygon points="455.32 582.14 454.28 594.77 438.5 594.77 437.47 582.14 455.32 582.14" fill="#a6a6a6"/>
|
||||||
|
<rect x="175.4" y="127.2" width="253.39" height="10.88" fill="#ebebeb"/>
|
||||||
|
<polygon points="212.25 180.91 202.37 180.91 198.42 138.08 212.25 138.08 212.25 180.91" fill="#ebebeb"/>
|
||||||
|
<polygon points="391.94 180.91 401.82 180.91 405.78 138.08 391.94 138.08 391.94 180.91" fill="#ebebeb"/>
|
||||||
|
<polygon points="198.42 138.08 212.23 138.08 212.23 152.83 199.78 152.83 198.42 138.08" fill="#dbdbdb"/>
|
||||||
|
<polygon points="405.78 138.08 404.42 152.83 391.94 152.83 391.94 138.08 405.78 138.08" fill="#dbdbdb"/>
|
||||||
|
<path d="M267.16,61.94c-11.25,15.22-23,32.84-25.82,52a.26.26,0,0,0,.25.26.25.25,0,0,0,.24-.17,114,114,0,0,1,5-15.28c7.38-2,14.64-3.7,18.52-11s3.46-17.37,2.7-25.59C268,61.66,267.38,61.64,267.16,61.94Z" fill="#ebebeb"/>
|
||||||
|
<path d="M246.92,98.59a125.92,125.92,0,0,1,19.77-32m.09.09a136.1,136.1,0,0,0-13.54,21.23C257,84.1,261,80.7,264.56,76.7c0,0,.18,0,.12.12-3.5,4.19-7.38,8.75-11.81,11.81-1.22,2.26-2.46,4.53-3.72,6.79.31-.18.64-.33.95-.49m0,.09-1,.52c-.59,1.06-1.18,2.14-1.79,3.2-.11.3-.45.09-.34-.15Z" fill="#a6a6a6"/>
|
||||||
|
<path d="M250.67,94.64a67.6,67.6,0,0,0,8.64-5.76s.18,0,.1.1a46.21,46.21,0,0,1-8.62,5.91.14.14,0,0,1-.18-.07.12.12,0,0,1,.06-.18Z" fill="#a6a6a6"/>
|
||||||
|
<path d="M240.1,113.75c0,.24.42.22.42,0,0-6.29,0-12.6.1-18.92,1.61-3.25,5.06-5,7.47-7.66a33.37,33.37,0,0,0,4.43-6.25,45,45,0,0,0,5.2-17.34c.66-6.73-1.89-28.92-4.73-28.15-1.47.4-6.23,11.39-7.48,15a103.3,103.3,0,0,0-4.43,18.43C238.79,83.67,239,98.86,240.1,113.75Z" fill="#ebebeb"/>
|
||||||
|
<path d="M240.06,86.62h0a141.83,141.83,0,0,1,3.82-23.71c1.8-8.32,3.92-16.15,8.2-23.62,0-.14.28,0,.2.12A87.62,87.62,0,0,0,245.7,57.5c.29-.45.63-.87.93-1.3.12-.17.4,0,.28.16a11.29,11.29,0,0,1-1.33,1.89h0c-1.48,5.9-2.54,12-3.44,18.08l.21-.23h.09l-.36.6c-.46,3.07-.88,6.14-1.31,9.18a43,43,0,0,0,12.8-17h0a35.28,35.28,0,0,1-12.95,17.61c-.1.82-.22,1.64-.34,2.46,0,.17-.29.12-.29,0V86.77A.15.15,0,0,1,240.06,86.62Z" fill="#a6a6a6"/>
|
||||||
|
<path d="M242.64,75.09A124.48,124.48,0,0,0,253.8,55.9H254a61.86,61.86,0,0,1-11,19.35.16.16,0,0,1-.22.06A.17.17,0,0,1,242.64,75.09Z" fill="#a6a6a6"/>
|
||||||
|
<path d="M248.21,54.32c1.74-3.07,3.56-6,5.19-9.18,0-.09.17,0,.14.07a47.39,47.39,0,0,1-5.07,9.27.16.16,0,0,1-.23,0A.16.16,0,0,1,248.21,54.32Z" fill="#a6a6a6"/>
|
||||||
|
<path d="M237,97.63a63,63,0,0,1-1.47,12.4,11.1,11.1,0,0,0,.19,1.36A59.87,59.87,0,0,0,238,97.57a.45.45,0,0,0,0-.42h0a91.63,91.63,0,0,0-1.39-18.71c-1.95-11.56-6.2-22.66-14.26-31.31a.59.59,0,0,0-.83.08.36.36,0,0,0-.07.1c-3.25,8.27-2.11,18.14.67,26.39a47.73,47.73,0,0,0,6.39,12.54C231.26,90.1,234.79,93.4,237,97.63Z" fill="#ebebeb"/>
|
||||||
|
<path d="M223.29,51c7.65,12.4,13,27.08,13.61,41.73,0,.26-.43.34-.47.07-.18-1.31-.37-2.63-.59-3.93h0a50.43,50.43,0,0,1-8.67-10s0-.15.12,0A108.77,108.77,0,0,0,235.71,88c-.56-3.23-1.21-6.42-2-9.57-.2-.22-.4-.47-.6-.71s.09-.38.22-.22l.19.21a108.42,108.42,0,0,0-4-12.68A38.88,38.88,0,0,1,224,57.88s0-.13.1,0a57.55,57.55,0,0,0,3.55,4.68c.5.59,1.07,1.15,1.6,1.73a98.15,98.15,0,0,0-6.25-13,.19.19,0,1,1,.29-.26Z" fill="#a6a6a6"/>
|
||||||
|
<path d="M224.84,66.28c2.47,3.32,5.29,6.49,7.57,9.92,0,.1,0,.21-.16.13a51.34,51.34,0,0,1-7.57-10C224.6,66.26,224.77,66.18,224.84,66.28Z" fill="#a6a6a6"/>
|
||||||
|
<path d="M232.77,77l.1.11s0,.18-.12.11l-.1-.12C232.58,77.07,232.69,76.91,232.77,77Z" fill="#a6a6a6"/>
|
||||||
|
<path d="M210.15,71.68a37.83,37.83,0,0,0,8.06,18.91,20.07,20.07,0,0,0,6.53,5.11c2.83,1.4,5.8,3,8.72,4.16a58.11,58.11,0,0,1,3.1,11c.09-.32.18-.65.28-1-1.89-16.15-12.32-30-25.7-38.81a.65.65,0,0,0-.89.2A.66.66,0,0,0,210.15,71.68Z" fill="#ebebeb"/>
|
||||||
|
<path d="M214.76,76.45c-.09,0,0-.25.15-.18a55,55,0,0,1,17,21c.1.23-.21.42-.33.2-.62-1.08-1.25-2.14-1.9-3.19h0a18.34,18.34,0,0,1-8.21-4.77m.09-.09a18.87,18.87,0,0,0,3.85,2.72,33.17,33.17,0,0,0,3.88,1.47c-1.13-1.8-2.3-3.52-3.54-5.19a35.38,35.38,0,0,1-9.17-5.15.1.1,0,0,1,0-.13.1.1,0,0,1,.09,0,91.92,91.92,0,0,0,8.5,4.63,78.41,78.41,0,0,0-6-7c-1-.55-2.1-1.11-3.1-1.68a.09.09,0,0,1-.05-.12.07.07,0,0,1,.05,0,11,11,0,0,1,2.36,1.16,46.83,46.83,0,0,0-3.69-3.6Z" fill="#a6a6a6"/>
|
||||||
|
<path d="M215.05,82a4.87,4.87,0,0,1,.52.37v.09a4.07,4.07,0,0,1-.53-.31.09.09,0,0,1,0-.13Z" fill="#a6a6a6"/>
|
||||||
|
<polygon points="253.27 127.2 225.74 127.2 227.1 109.16 227.62 103.27 251.15 103.27 251.68 109.16 253.27 127.2" fill="#dbdbdb"/>
|
||||||
|
<polygon points="251.68 109.16 227.1 109.16 227.62 103.27 251.15 103.27 251.68 109.16" fill="#c7c7c7"/>
|
||||||
|
<rect x="225.74" y="98.45" width="27.28" height="6.04" fill="#dbdbdb"/>
|
||||||
|
<rect x="303.64" y="61.66" width="16.5" height="65.51" fill="#c7c7c7"/>
|
||||||
|
<rect x="303.65" y="58.15" width="16.5" height="7.2" fill="#dbdbdb"/>
|
||||||
|
<rect x="303.64" y="74.56" width="16.5" height="2.67" fill="#dbdbdb"/>
|
||||||
|
<rect x="303.64" y="111.36" width="16.5" height="7.2" fill="#dbdbdb"/>
|
||||||
|
<rect x="320.14" y="50.75" width="11.53" height="76.42" fill="#dbdbdb"/>
|
||||||
|
<rect x="322.4" y="57.92" width="6.92" height="29.39" fill="#ebebeb"/>
|
||||||
|
<rect x="376.78" y="57.92" width="11.53" height="76.42" transform="translate(45.24 300.6) rotate(-45.33)" fill="#c7c7c7"/>
|
||||||
|
<rect x="383.09" y="85.34" width="6.92" height="29.39" transform="translate(43.66 304.61) rotate(-45.33)" fill="#dbdbdb"/>
|
||||||
|
<rect x="331.69" y="69.13" width="19.57" height="58.04" fill="#a6a6a6"/>
|
||||||
|
<rect x="331.67" y="115.61" width="19.57" height="5.4" fill="#c7c7c7"/>
|
||||||
|
<rect x="331.69" y="109.96" width="19.57" height="2.1" fill="#c7c7c7"/>
|
||||||
|
<rect x="260.27" y="93.54" width="54.5" height="13.51" transform="translate(82.96 325.87) rotate(-67.03)" fill="#a6a6a6"/>
|
||||||
|
<rect x="276.77" y="110.14" width="5.08" height="13.52" transform="translate(61.72 327.46) rotate(-66.77)" fill="#c7c7c7"/>
|
||||||
|
<rect x="289.57" y="80.33" width="5.08" height="13.52" transform="translate(96.86 321.17) rotate(-66.77)" fill="#c7c7c7"/>
|
||||||
|
<rect x="288.36" y="86.77" width="1.98" height="13.52" transform="translate(89.28 322.54) rotate(-66.77)" fill="#c7c7c7"/>
|
||||||
|
<rect x="569.6" y="445.51" width="144.36" height="178.43" fill="#ebebeb"/>
|
||||||
|
<rect x="562.61" y="438.1" width="158.33" height="7.41" fill="#dbdbdb"/>
|
||||||
|
<rect x="720.95" y="438.1" width="49.79" height="7.41" fill="#ebebeb"/>
|
||||||
|
<rect x="713.96" y="445.51" width="48.91" height="178.43" fill="#dbdbdb"/>
|
||||||
|
<path d="M698.41,528.35H585.15v-64H698.41ZM585.52,528H698V464.69H585.52Z" fill="#c7c7c7"/>
|
||||||
|
<rect x="625.98" y="483.03" width="22.05" height="12.68" fill="#c7c7c7"/>
|
||||||
|
<path d="M654.76,494.93H632.31V483.8h22.45Zm-22.14-.37h21.69V484.23H632.62Z" fill="#c7c7c7"/>
|
||||||
|
<path d="M698.41,608.78H585.15v-64H698.41Zm-112.89-.37H698v-63.3H585.52Z" fill="#c7c7c7"/>
|
||||||
|
<rect x="625.98" y="563.46" width="22.05" height="12.68" fill="#c7c7c7"/>
|
||||||
|
<path d="M654.76,575.35H632.31V564.21h22.45ZM632.62,575h21.69V564.65H632.62Z" fill="#c7c7c7"/>
|
||||||
|
<polygon points="724.13 624.56 731.53 624.56 679.67 350.48 661.22 350.48 724.13 624.56" fill="#2f2e41" data-secondary="true"/>
|
||||||
|
<polygon points="661.22 350.48 670.53 391.01 687.33 391.01 679.68 350.52 679.67 350.48 661.22 350.48" opacity="0.2"/>
|
||||||
|
<polygon points="648.47 624.56 655.87 624.56 654.22 350.48 635.79 350.48 648.47 624.56" fill="#2f2e41" data-secondary="true"/>
|
||||||
|
<polygon points="635.79 350.48 637.66 391.01 654.47 391.01 654.22 350.52 654.22 350.48 635.79 350.48" opacity="0.2"/>
|
||||||
|
<polygon points="580.21 624.56 572.82 624.56 613.65 350.48 632.09 350.48 580.21 624.56" fill="#2f2e41" data-secondary="true"/>
|
||||||
|
<polygon points="607.61 391.01 624.41 391.01 632.09 350.48 613.65 350.48 613.64 350.52 607.61 391.01" opacity="0.2"/>
|
||||||
|
<rect x="595.4" y="507.14" width="107.62" height="5.35" fill="#2f2e41" data-secondary="true"/>
|
||||||
|
<ellipse cx="657.73" cy="244.11" rx="138.63" ry="98.46" transform="translate(381.44 888.99) rotate(-87.17)" fill="#0071f2" data-primary="true"/>
|
||||||
|
<polygon points="643.09 104.28 666.2 105.71 648.6 382.44 625.48 381.01 643.09 104.28" fill="#0071f2" data-primary="true"/>
|
||||||
|
<g opacity="0.1">
|
||||||
|
<polygon points="643.09 104.28 666.2 105.71 648.6 382.44 625.48 381.01 643.09 104.28"/>
|
||||||
|
</g>
|
||||||
|
<ellipse cx="636.45" cy="242.79" rx="138.63" ry="98.46" transform="translate(362.53 866.48) rotate(-87.17)" fill="#0071f2" data-primary="true"/>
|
||||||
|
<ellipse cx="636.45" cy="242.79" rx="108.14" ry="76.81" transform="translate(362.53 866.48) rotate(-87.17)" fill="#fff"/>
|
||||||
|
<ellipse cx="636.45" cy="242.79" rx="77.97" ry="55.37" transform="translate(362.53 866.48) rotate(-87.17)" fill="#0071f2" data-primary="true"/>
|
||||||
|
<ellipse cx="636.45" cy="242.79" rx="47.47" ry="33.71" transform="translate(362.53 866.48) rotate(-87.17)" fill="#fff"/>
|
||||||
|
<path d="M651.4,240.11c.8,11.63-5.25,22.16-13.52,23.67s-15.6-6.61-16.39-18.18,5.25-22.17,13.51-23.69S650.6,228.47,651.4,240.11Z" fill="#0071f2" data-primary="true"/>
|
||||||
|
<ellipse cx="636.45" cy="242.79" rx="138.63" ry="98.46" transform="translate(362.53 866.48) rotate(-87.17)" fill="#fff" opacity="0.4" style="isolation:isolate"/>
|
||||||
|
<path d="M528.17,536.16a361.87,361.87,0,0,0-.54-38.69,491.73,491.73,0,0,0-12.12-78.83c-3.38-14.58-7.54-28.93-11.88-43.25-.13-.44-.81-.31-.69.13,13.4,50.5,24.41,102.63,23.62,155.12-.21,14.76-1.91,29.13-3.31,43.73,0,.39.61.54.69.14C526.75,562.08,527.65,549,528.17,536.16Z" fill="#0071f2" data-primary="true"/>
|
||||||
|
<path d="M502.94,375.58s-20.33,51.58-15.78,63.7S501.09,454,501.09,454s-12.43,17.72-3.75,34.4,28.05,15.87,29.28,34.73C526.62,523.11,527.69,444.2,502.94,375.58Z" fill="#0071f2" data-primary="true"/>
|
||||||
|
<path d="M524.42,490.53a.66.66,0,0,0,0-.28,473.14,473.14,0,0,0-19.71-103.74c0-.09-.19,0-.16,0q6,22.62,10.67,45.49a22.46,22.46,0,0,0-9.7-5.4c-.07,0-.1.07,0,.1a25.64,25.64,0,0,1,10,6.69c.61,2.95,1.18,5.9,1.73,9-4.53-5.08-10.84-7.59-17.14-10-.09,0-.13.1,0,.13,6.57,2.67,13.09,6.11,17.55,11.81q2.33,12.84,4.07,25.76a24.51,24.51,0,0,0-12.11-6.94c-.14,0-.23.18-.08.22a25.59,25.59,0,0,1,12.28,7.87h.09c.4,3,.78,6.07,1.14,9.09a31.67,31.67,0,0,0-8.49-5.12c-.07,0-.12.1,0,.13a29.24,29.24,0,0,1,8.5,5.64.13.13,0,0,0,.12,0c.33,2.95.67,5.79,1,8.68a41.34,41.34,0,0,0-17.23-12.31.11.11,0,1,0-.07.21,45,45,0,0,1,17,13.08.4.4,0,0,0,.42.13c.45,4.43.85,8.86,1.17,13.28,0,0,.1.08.1,0C525.1,499.64,524.86,495.08,524.42,490.53Z" fill="#3f4347"/>
|
||||||
|
<path d="M508.09,421.52a25.43,25.43,0,0,0-12.21-3.81.08.08,0,0,0-.07.08.07.07,0,0,0,.07.07A49.39,49.39,0,0,1,508,421.8C508.1,421.88,508.24,421.63,508.09,421.52Z" fill="#3f4347"/>
|
||||||
|
<path d="M510.56,476.13a25.58,25.58,0,0,0-9.17-3.15c-.12,0-.12.16,0,.18a34.73,34.73,0,0,1,9.06,3.17C510.57,476.39,510.67,476.18,510.56,476.13Z" fill="#3f4347"/>
|
||||||
|
<path d="M581.57,440.12a139,139,0,0,0-23.51,20.55,84.62,84.62,0,0,0-16.49,28,208.9,208.9,0,0,0-12.28,66.9,161.76,161.76,0,0,0,3.61,37.62.37.37,0,0,0,.43.28.35.35,0,0,0,.28-.42,210.38,210.38,0,0,1,.37-65.74c3.49-21.35,9-44.56,22.53-61.9,7.38-9.43,16.59-16.86,25.7-24.54C582.68,440.43,582.08,439.77,581.57,440.12Z" fill="#0071f2" data-primary="true"/>
|
||||||
|
<path d="M532.24,540.18a126.61,126.61,0,0,1,17.15-21.91c10.16-10.17,18.72-17.39,20.05-24.11s-10.19-11.67-10.19-11.67,14.07,2.52,17.91-3.38,5.71-40.1,5.71-40.1-25.53,18.28-37.48,43.89S532.24,540.18,532.24,540.18Z" fill="#0071f2" data-primary="true"/>
|
||||||
|
<path d="M574.05,447.78c-10.77,10.59-21.6,21.85-27.95,35.74a139.33,139.33,0,0,0-5.65,14.76h0c-.33,1-.67,2-1,3a.49.49,0,0,0,0,.17,289,289,0,0,0-7.27,31.59s.1.1.11,0c1.1-5.33,2.32-10.72,3.68-16.09h.15a22.26,22.26,0,0,1,6.71-1.72c.15,0,.14-.28,0-.26a24.61,24.61,0,0,0-6.64,1.21c1.24-4.84,2.58-9.67,4.1-14.44a72,72,0,0,1,15.18-2v-.09A57.89,57.89,0,0,0,540.58,501c.25-.81.53-1.62.81-2.43a75.27,75.27,0,0,1,10.1-1.71m0-.12a49.17,49.17,0,0,0-9.89,1.21c1.4-4.12,2.95-8.18,4.58-12.14a70.35,70.35,0,0,1,4.82-9.38,61.53,61.53,0,0,1,8.2-1.48c.07,0,.07-.13,0-.13a22.17,22.17,0,0,0-7.77.84q2-3.25,4.27-6.36h0c5.74-1.27,11.56-2.45,17.43-1.35.09,0,.13-.16,0-.17-5.58-1.48-11.13-.62-16.7.47.6-.81,1.22-1.61,1.86-2.39A37.56,37.56,0,0,1,569,464.15a.06.06,0,1,0,0-.11,27.6,27.6,0,0,0-10,.88c4.74-5.9,10-11.44,15.16-16.92.31-.32.13-.41-.13-.22Z" fill="#3f4347"/>
|
||||||
|
<path d="M573.77,471.09a24.17,24.17,0,0,0-5.1-.18.2.2,0,0,0-.13.24.17.17,0,0,0,.13.13c1.69,0,3.38-.08,5.07,0a.1.1,0,0,0,.08-.12A.18.18,0,0,0,573.77,471.09Z" fill="#3f4347"/>
|
||||||
|
<path d="M555.34,504.26a23.38,23.38,0,0,0-7.88,1.58V506c2.61-.62,5.24-.87,7.88-1.31a.2.2,0,0,0,.19-.2A.19.19,0,0,0,555.34,504.26Z" fill="#3f4347"/>
|
||||||
|
<path d="M549.82,492.81a16.37,16.37,0,0,0-3.35.29c-.1,0-.1.21,0,.21a17.07,17.07,0,0,0,3.35-.16C550,493.12,550,492.81,549.82,492.81Z" fill="#3f4347"/>
|
||||||
|
<path d="M457.26,476.41a116.15,116.15,0,0,1,23.79,11.2,71,71,0,0,1,19.81,18.82,176.35,176.35,0,0,1,25.48,51.3A138,138,0,0,1,532.11,589a.31.31,0,0,1-.37.23.3.3,0,0,1-.24-.23A177.66,177.66,0,0,0,516,535.87c-7.76-16.46-17.55-34-32.47-44.88-8.17-5.9-17.33-9.82-26.47-13.92C456.44,476.91,456.78,476.24,457.26,476.41Z" fill="#0071f2" data-primary="true"/>
|
||||||
|
<path d="M520.3,546a106.15,106.15,0,0,0-18.94-13.76c-10.59-5.9-19.19-9.74-21.8-14.88s5.55-11.81,5.55-11.81-10.81,5.29-15.28,1.4-13.89-31.13-13.89-31.13,24.93,8.86,40.5,26.85S520.3,546,520.3,546Z" fill="#0071f2" data-primary="true"/>
|
||||||
|
<path d="M519,540.37C506.48,517,489.84,495.79,467,481.81h0a115,115,0,0,1,16.68,13.06,26.52,26.52,0,0,0-13.41,2.82.09.09,0,0,0,0,.13l.07,0a36.42,36.42,0,0,1,13.79-2.33h.14c.82.77,1.61,1.55,2.41,2.33a41.08,41.08,0,0,0-7.16,1m0,.09a36.82,36.82,0,0,1,7.56-.62,154.46,154.46,0,0,1,11.81,13.36,12,12,0,0,0-5,.35.1.1,0,0,0-.05.14l.05,0a25.86,25.86,0,0,1,5.35-.07c.69.87,1.37,1.73,2.05,2.61a48.21,48.21,0,0,0-15.13,1.48v.12a60,60,0,0,1,15.54-1c.64.84,1.24,1.71,1.86,2.57a32.54,32.54,0,0,0-9,1c-.12,0-.09.21,0,.18a34.79,34.79,0,0,1,9.33-.73c2.68,3.69,5.25,7.47,7.72,11.3a8.56,8.56,0,0,0-4,.23m0,.11a18,18,0,0,1,4.27.2c2.23,3.46,4.43,7,6.43,10.47.28.25.77,0,.56-.28Z" fill="#3f4347"/>
|
||||||
|
<path d="M499.6,521a19,19,0,0,0-7.25.88m0,.11a56.28,56.28,0,0,1,7.21-.74c.2,0,.2-.24,0-.25Z" fill="#3f4347"/>
|
||||||
|
<path d="M477.84,492.42a13.78,13.78,0,0,0-3.33.19.11.11,0,0,0-.11.11.11.11,0,0,0,.11.1c1.09-.12,2.17-.07,3.27-.1a.16.16,0,0,0,.18-.12A.16.16,0,0,0,477.84,492.42Z" fill="#3f4347"/>
|
||||||
|
<polygon points="552.45 623.37 496.18 623.37 504.19 564.59 506.25 549.54 542.37 549.54 544.42 564.59 552.45 623.37" fill="#3f4347"/>
|
||||||
|
<polygon points="544.42 564.59 504.19 564.59 506.25 549.54 542.37 549.54 544.42 564.59" fill="#2e3135"/>
|
||||||
|
<rect x="501.08" y="544.3" width="46.46" height="12.77" fill="#3f4347"/>
|
||||||
|
<path d="M223.65,476.08l3.1,18.14s1.31,8.66-4.74,13.88c0,0-16.17,14.38-15.35,35.13l-.49,58.48,14.37,2,34.64-91.48a32.86,32.86,0,0,0,5.71-17.65c0-.81-2.77-24.83-2.77-24.83Z" fill="#f9b499"/>
|
||||||
|
<path d="M230.51,646.31s11.92,1.3,7.68-12.91L222.5,602.2s-6.37-10.13-12.25-1.47c0,0-4.48,2.45-3.55-5,0,0-3-9.29-9.72-1.57a18.42,18.42,0,0,0-3.73,17.09l4.81,17a21.93,21.93,0,0,0,16.06,15.35A106.41,106.41,0,0,0,230.51,646.31Z" fill="#0071f2" data-primary="true"/>
|
||||||
|
<path d="M230.51,646.31s11.92,1.3,7.68-12.91L222.5,602.2s-6.37-10.13-12.25-1.47c0,0-4.48,2.45-3.55-5,0,0-3-9.29-9.72-1.57a18.42,18.42,0,0,0-3.73,17.09l4.81,17a21.93,21.93,0,0,0,16.06,15.35A106.41,106.41,0,0,0,230.51,646.31Z" opacity="0.2"/>
|
||||||
|
<path d="M173.33,613.47s-1-19.93-3.1-30.55c0,0-10-34.79,1-62.4,0,0,3.59-4.41,3.43-16.5l-2.78-23.2,41.33.49s-2.94,19.77-8.33,29.73c0,0-2.12,3.43-2,10.62,0,0-3.76,20.91-5.72,31.53,0,0-5.39,40.51-3.43,51.46L188.53,623l-9.81,1.63Z" fill="#f9b499"/>
|
||||||
|
<path d="M342.58,250.81l-1.8,12.74c0,.07,0,.13,0,.2.41,6-2.63,6.5-2.63,6.5l-8.65,2.12c-24.51,8.26-38.08,6.21-42.49,5.08a19,19,0,0,0-8.28-.33A37.65,37.65,0,0,0,266.61,282a17.51,17.51,0,0,1-4.51,2.82,23,23,0,0,1-9.24,2c-4.49.05-5.52,1.08-5.52,1.08-9.15,4.57-1.31,21.73-1.31,21.73,5.88,13.07,6.7,40.35,6.7,40.35a40.38,40.38,0,0,0,1.14,11.11c2.12,12.25.82,16.17.33,26.63,0,.26,0,.51,0,.76-.37,9.72,1.34,10.34,1.34,10.34,4.09,11.11,1.31,12.26,1.31,12.26-1.65,1.25-4,1.6-6.49,1.47a40.68,40.68,0,0,1-11.81-2.94c-7.18-3.11-7.35-1.47-7.35-1.47-1,4.34-9.46,1.59-14.1-.29A53.12,53.12,0,0,0,208,405c-21.3-4.53-34.34,0-34.34,0-6.15,2.22-10.09,2-11.42,1.88a5.08,5.08,0,0,1-.51-.08c-4.43-1-3.85-7-3.29-9.89a11,11,0,0,1,.6-2.1,9.23,9.23,0,0,0,.65-2.7,10.8,10.8,0,0,0-.72-4.8c-2-5.34,1.13-13.67,1.13-13.67,1.32-6.39,1.16-10.1.91-11.84a7.29,7.29,0,0,0-.46-1.72,6.69,6.69,0,0,1,.21-6.18,7.53,7.53,0,0,0,1.11-4.39c0-.34-.06-.66-.11-.95a7.11,7.11,0,0,0-.78-2.27,8.33,8.33,0,0,1-1-3.53l-.66-6.86c-.81-8.5-2.94-9.48-2.94-9.48,0,9-18.13,26.47-18.13,26.47-3.27,4.41-12.74,1.47-12.74,1.47a8.75,8.75,0,0,1-7.19-3.76c-3.76-4.08-2.78-16.82-2.78-16.82C115.67,321.87,118,322,118,322l10.78-37.57c12.58-18.3,23.86-16.66,23.86-16.66.65-2.45,4.24-2.78,4.24-2.78,2.78.49,3.6-1.14,3.6-1.14,4.08-8.33,13.48-10.9,13.48-10.9l49.61-8.28a8.41,8.41,0,0,1,8.79-1.24l9.47,2.28c35.94,3.6,91.16.49,91.16.49,5.79-1,8.12.52,9.05,2.07A4.28,4.28,0,0,1,342.58,250.81Z" fill="#0071f2" data-primary="true"/>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M180.51,289.43h0a3.67,3.67,0,0,1-2.54-1.44l0,0c-2.07-4-6.59-6.77-6.63-6.8a30.14,30.14,0,0,1-10.21-10.29,15.63,15.63,0,0,1-2-5,.16.16,0,0,1,.14-.18.15.15,0,0,1,.18.13c0,.09,1.27,8.18,12,15.08,0,0,4.62,2.84,6.75,6.91a3.44,3.44,0,0,0,2.28,1.31h0a2.65,2.65,0,0,0,2-1.19,6.11,6.11,0,0,0,1.23-2.87c.39-2.49,1.33-6.7,3.26-7a.18.18,0,0,1,.19.14.16.16,0,0,1-.14.18c-1.24.17-2.36,2.67-3,6.69a6.4,6.4,0,0,1-1.3,3A3,3,0,0,1,180.51,289.43Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M202.09,280.38c-3.25,0-6.85-6-7-6.29a.18.18,0,0,1,.06-.23.16.16,0,0,1,.22.06c0,.07,3.83,6.41,6.93,6.13,1.13-.11,2.06-1.12,2.74-3l5.4-12.1a.16.16,0,0,1,.22-.08.17.17,0,0,1,.08.22l-5.39,12.09c-.73,2-1.75,3.07-3,3.19Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M234.43,279.23a.16.16,0,0,1-.14-.07l-4.73-7.51c-3.27-4.1-2-11.88-2-12.21a.18.18,0,0,1,.19-.14.16.16,0,0,1,.13.19c0,.08-1.25,8,1.93,12v0l4.74,7.51a.17.17,0,0,1,0,.23Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M240.31,258.16a.16.16,0,0,1-.12-.06.17.17,0,0,1,0-.23c.16-.14,4-3.45,14.38-3.35a28,28,0,0,0,4.58-.3c3.19-.49,8.92-1.74,11.41-4.66a.16.16,0,1,1,.25.21c-2.56,3-8.37,4.28-11.61,4.77a28.69,28.69,0,0,1-4.64.31c-10.2-.1-14.11,3.23-14.15,3.27A.15.15,0,0,1,240.31,258.16Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M246.85,261.75a.2.2,0,0,1-.14-.06.17.17,0,0,1,0-.23s4.38-3.14,12-2.81a.15.15,0,0,1,.15.17.17.17,0,0,1-.17.16c-7.53-.33-11.78,2.71-11.82,2.74Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M157.32,273.51a.15.15,0,0,1-.16-.13c-.47-3.15-4.61-5.42-4.65-5.44a.16.16,0,0,1,.15-.29c.18.1,4.33,2.37,4.82,5.68a.15.15,0,0,1-.13.18Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M228.39,409.11a.18.18,0,0,1-.17-.14c-3.2-17.47-10.6-41.87-10.77-42.45-3.88-9.06-4.41-14-4.42-14.08-1.45-16.61-11.16-37.48-11.26-37.69-6.15-13.76-6.22-24.47-6.22-24.57a.17.17,0,0,1,.16-.17.18.18,0,0,1,.17.17c0,.1.07,10.75,6.19,24.44.1.2,9.83,21.12,11.29,37.78,0,.05.53,5,4.39,14v0c.07.25,7.55,24.88,10.79,42.49a.17.17,0,0,1-.14.19Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M239.82,383.78h0a.16.16,0,0,1-.14-.18c0-.12,1.68-11.34,6.05-16.71.21-1,3-13.8,5.29-15.26a.16.16,0,0,1,.23.05.15.15,0,0,1,0,.22c-2.2,1.42-5.13,15-5.16,15.09a.09.09,0,0,1,0,.07c-4.33,5.3-6,16.47-6,16.58A.15.15,0,0,1,239.82,383.78Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M228.22,317a.17.17,0,0,1-.14-.08.18.18,0,0,1,.06-.23s4-2.33,2.86-9.47c0-.06-.95-6.35,3.52-8.66a.16.16,0,1,1,.15.29c-4.26,2.2-3.36,8.26-3.35,8.32,1.15,7.38-3,9.79-3,9.81Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M235.25,292a.16.16,0,0,1-.15-.09.16.16,0,0,1,.08-.22l4.93-2.35a14.32,14.32,0,0,1,3.88-1.22A15.81,15.81,0,0,0,249.7,286a.18.18,0,0,1,.23.06.16.16,0,0,1-.06.22,16.27,16.27,0,0,1-5.83,2.19,14,14,0,0,0-3.79,1.18L235.32,292Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M156.5,318.77l-.09,0c-.13-.1-1.18-1.06,1.92-6.1v0a5.74,5.74,0,0,0,.79-4.92.15.15,0,0,1,0-.13.19.19,0,0,1,.11-.07s1.52-.68,1.83-10.63a.17.17,0,0,1,.17-.16.17.17,0,0,1,.16.17c-.28,8.81-1.48,10.53-1.94,10.86a6,6,0,0,1-.87,5.08c-2.92,4.76-2,5.65-2,5.65a.18.18,0,0,1,0,.23A.17.17,0,0,1,156.5,318.77Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M202.41,358.14h0c-10.25-2.62-20.43-11.43-27.17-18.36a136.18,136.18,0,0,1-12.29-14.54.16.16,0,0,1,0-.23.16.16,0,0,1,.22,0,135.68,135.68,0,0,0,12.26,14.51c6.71,6.89,16.85,15.66,27,18.26a.17.17,0,0,1,.12.2A.17.17,0,0,1,202.41,358.14Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M192.12,360.26h0c-6.12-.43-9.16-2.07-10.64-3.37a5.06,5.06,0,0,1-1.75-2.72c-6.62-2.77-7.8-6.61-7.85-6.77a.16.16,0,1,1,.31-.09s1.21,3.88,7.75,6.59a.17.17,0,0,1,.1.14,4.54,4.54,0,0,0,1.69,2.63c1.43,1.26,4.41,2.85,10.41,3.26a.17.17,0,0,1,.15.18A.16.16,0,0,1,192.12,360.26Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M124.94,348.39a40.06,40.06,0,0,1-5.54-.38.17.17,0,1,1,0-.33c.1,0,10,1.59,16.23-1.78a.2.2,0,0,1,.15,0,5.35,5.35,0,0,0,4.18-.09c2.57-1.12,4.6-4.08,6.05-8.8a.16.16,0,0,1,.2-.1.15.15,0,0,1,.11.2c-1.48,4.82-3.57,7.85-6.24,9a5.59,5.59,0,0,1-4.36.12C132.49,348,128.27,348.39,124.94,348.39Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M145.72,332a.16.16,0,0,1-.16-.15c-.32-3.33-5.28-7.69-5.33-7.73a.16.16,0,1,1,.21-.25c.21.18,5.12,4.49,5.45,7.95a.17.17,0,0,1-.15.18Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M145.23,319.58a.15.15,0,0,1-.11,0,14.67,14.67,0,0,0-6-3.06.17.17,0,0,1-.12-.2.16.16,0,0,1,.2-.12,15.14,15.14,0,0,1,6.12,3.14.16.16,0,0,1,0,.23A.13.13,0,0,1,145.23,319.58Z"/>
|
||||||
|
</g>
|
||||||
|
<path d="M139.52,258.32l-30.91,11.36a2.36,2.36,0,0,1-.78.16,2.52,2.52,0,0,1-2.46-1.72.14.14,0,0,0,0-.06,2.64,2.64,0,0,1-.11-.73,2.61,2.61,0,0,1,.05-.48,2.53,2.53,0,0,1,1.74-1.92l1.2-.36,30.14-9Z" fill="#0071f2" data-primary="true"/>
|
||||||
|
<polygon points="138.37 255.54 148.66 253.75 139.52 258.32 138.37 255.54" fill="#2f2e41" data-secondary="true"/>
|
||||||
|
<path d="M108.23,264.57l-1.2.36a2.53,2.53,0,0,0-1.74,1.92h0l-8.39-6.09s-2-1.33-1.85-3a1.86,1.86,0,0,1,1.64-1.59C98.69,255.91,103.64,256.31,108.23,264.57Z" fill="#2f2e41" data-secondary="true"/>
|
||||||
|
<path d="M107.83,269.84c-2.78,5.76-5.49,7.23-7,7.56A1.48,1.48,0,0,1,99,276c-.15-2.94,4.43-6.58,6.36-8a.14.14,0,0,1,0,.06A2.52,2.52,0,0,0,107.83,269.84Z" fill="#2f2e41" data-secondary="true"/>
|
||||||
|
<path d="M393,257.65l-4,1.65s6.19,2,5.07,5a.47.47,0,0,1-.53.3c-1.83-.37-10.21-1.94-14.31-.87a16.05,16.05,0,0,1-3.22.54,42,42,0,0,1-13.54-1.54,14.08,14.08,0,0,0-4.92-.44l-16.73,1.44c0-.07,0-.13,0-.2l1.8-12.74a4.28,4.28,0,0,0-.59-2.51c.74-.13,4.56-.63,13,.38,0,0,6.05,1.15,10.78-2l7.52-5.55s7-4.57,11.92.16c0,0,5.88-2.61,6.86,3.76,0,0,6.7-1.63,5.56,6,0,0,9.31-2.45,10,.82,0,0,2.65,2.5-10,4.43A19.42,19.42,0,0,0,393,257.65Z" fill="#f9b499"/>
|
||||||
|
<line x1="385.21" y1="241.33" x2="379.66" y2="242.48" fill="none" stroke="#f7a48b" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||||
|
<line x1="392.07" y1="245.09" x2="387.5" y2="246.4" fill="none" stroke="#f7a48b" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||||
|
<line x1="397.63" y1="251.13" x2="389.95" y2="252.28" fill="none" stroke="#f7a48b" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||||
|
<path d="M382.76,258.65a14,14,0,0,1,6.21.65" fill="none" stroke="#f7a48b" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||||
|
<path d="M262.53,468.9c-1,6.2-14.21,8.82-14.21,8.82-15.2,3.43-24.84.32-24.84.32-2.61,0-3.43-5.06-3.43-5.06-2.45-.49-2,5.55-2,5.55,0,7-6.2,5.72-6.2,5.72-20.56,4.9-36.69.57-40.91-.77A1.92,1.92,0,0,1,169.7,482l-3.07-16.39c-2.28-8.17-1.47-12.25-1.47-12.25A104.37,104.37,0,0,0,163,424.79c-1.21-9.94-1.14-15.17-.8-17.89,1.33.16,5.27.34,11.42-1.88,0,0,13-4.53,34.34,0a53.12,53.12,0,0,1,9.06,2.82c4.64,1.88,13.05,4.63,14.1.29,0,0,.17-1.64,7.35,1.47a40.68,40.68,0,0,0,11.81,2.94c1.21,10.47,7,25.81,7,25.81,2.61,8.33,1.63,10.13,1.63,10.13-.49,7.51,1,11.92,1,11.92A16.29,16.29,0,0,1,262.53,468.9Z" fill="#2f2e41" data-secondary="true"/>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M177.74,477.72a.16.16,0,0,1-.15-.12c-3.56-11.65-2.8-25.4-2.79-25.54.64-9.77-2.38-13.74-2.41-13.77a.09.09,0,0,1,0-.06c-4.05-15.07.28-31,.32-31.13a.17.17,0,0,1,.32.09c0,.16-4.35,16-.34,30.92.3.39,3.1,4.38,2.47,14,0,.14-.77,13.83,2.77,25.43a.16.16,0,0,1-.11.2Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M171.13,420.41a.17.17,0,0,1-.17-.16.18.18,0,0,1,.17-.17,24.58,24.58,0,0,0,9-2.15A21.86,21.86,0,0,0,192.46,403a.16.16,0,0,1,.2-.11.16.16,0,0,1,.12.2,22.16,22.16,0,0,1-12.55,15.13,25.13,25.13,0,0,1-9.1,2.18Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M220.05,473.14a.16.16,0,0,1-.16-.16l-.54-36.35a15.5,15.5,0,0,1,5.48-12.1L227,422.7a.16.16,0,0,1,.21.25L225,424.78a15.2,15.2,0,0,0-5.37,11.85l.55,36.35a.16.16,0,0,1-.16.16Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M245.37,472.65l-.09,0a.16.16,0,0,1,0-.22l5.53-7.8c0-1.23.06-5,.74-5.33.4-.22,2.19-4.32,3.68-8.08a.17.17,0,0,1,.21-.09.16.16,0,0,1,.09.21c-.52,1.32-3.16,7.92-3.83,8.26-.35.17-.57,2.77-.57,5.08a.14.14,0,0,1,0,.09l-5.55,7.84A.15.15,0,0,1,245.37,472.65Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M249.62,473.14l-.09,0a.16.16,0,0,1,0-.22l5.39-7.84a.16.16,0,0,1,.23-.05.17.17,0,0,1,0,.23l-5.39,7.84A.16.16,0,0,1,249.62,473.14Z"/>
|
||||||
|
</g>
|
||||||
|
<path d="M180.85,617.88s2.77,1.31.49-5.39c0,0-4.58-8.82,4.9-11.27,0,0,6.86-2.29,10,3.76,0,0,36.27,34.79,26,41.33,0,0-2.45,2.61-17.81,1.79,0,0-14.05-2.12-21.73-1.3,0,0-14.21,2.61-14.21-5.39l.49-21.57s-1-10.45,5.07-8.66C174,611.18,178.4,612.33,180.85,617.88Z" fill="#0071f2" data-primary="true"/>
|
||||||
|
<path d="M175.46,235.45s2.94,13.56-3.11,19.61l-4.9,6.53s2.13,4.74,10.78,8.82c0,0,13.24,4.25,15.2,17.15,0,0,2,.82-.66-8.82,0,0-2.28-6,3.76-7.68,0,0,9.26-4.9,4.3-12.41L195.71,249l-11.6-18.3-7-.07Z" fill="#f9b499"/>
|
||||||
|
<path d="M199.32,255.72l2.33,4.57a6.84,6.84,0,0,1-1,7.29s-12.27-.88-15.13-18.46Z" fill="#f7a48b"/>
|
||||||
|
<rect x="181.83" y="193.47" width="43.13" height="65.02" rx="21.56" fill="#f9b499"/>
|
||||||
|
<path d="M196.53,218.79s-2.91,12.52-8.8,6l-9.23,9.69s-.92,6.35-6.15,3.41c0,0-14.54-19.11-10.62-32.67,0,0,.49-1.31-1-3.1,0,0-2.94-3.27.17-5.89a6,6,0,0,0,.81-4.24s-1.63-7.68,6.21-13.4c0,0,5.72-6.37,13.56-2,0,0,2.29-5.39,7.84-.33,0,0,4.41-5.88,10.62-2.45,0,0,3.43.33,3.43,5.56,0,0,6.05-.66,6.7,3.1,0,0,2.94-2.12,8.49.82,0,0,2.62.49,3.43,6.37,0,0-.65,2.78,3.27,3.27,0,0,6.05,1,4.9,7.84,0,0-1.47,8.66-11.11,3.76,0,0-12.25-1.31-15,6.37,0,0,.33,5.55-6.37,6C197.67,217,196.37,216.5,196.53,218.79Z" fill="#2f2e41" data-secondary="true"/>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M204.38,205.74a4,4,0,0,1-2.95-1.48,2.41,2.41,0,0,0-.4-.35c-.24-.13-.49-.24-.72-.34a5,5,0,0,0-4.25.12c-4.1,2.05-6.79.89-8.34-.45-1.22-1.07-1.91-2.47-1.8-2.8a.16.16,0,0,1,.21-.1.15.15,0,0,1,.1.19,5.64,5.64,0,0,0,2.29,2.91c1.41,1,3.84,1.74,7.4,0a5.34,5.34,0,0,1,4.52-.13c.24.1.49.22.74.34a2.28,2.28,0,0,1,.48.42c.88.9,2.52,2.57,6.05.09a.16.16,0,0,1,.23,0,.16.16,0,0,1,0,.22A6.24,6.24,0,0,1,204.38,205.74Zm-18.15-5.2Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M170.72,186.77h-.05a.17.17,0,0,1-.11-.2,9.24,9.24,0,0,1,4.38-5.16,8.74,8.74,0,0,1,7.11.15.17.17,0,0,1,.09.22.17.17,0,0,1-.21.09,8.37,8.37,0,0,0-6.85-.16,9.12,9.12,0,0,0-4.21,5A.16.16,0,0,1,170.72,186.77Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M164.47,203a6.23,6.23,0,0,1-2.48-.52.17.17,0,1,1,.14-.3,5.71,5.71,0,0,0,4.07.19,5.94,5.94,0,0,0,3.22-3.53.16.16,0,0,1,.22-.09.17.17,0,0,1,.09.21,6.26,6.26,0,0,1-3.42,3.71A5.23,5.23,0,0,1,164.47,203Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M212.54,187.75a.15.15,0,0,1-.15-.1c-.41-1-.93-1.61-1.56-1.76a2,2,0,0,0-1.61.51.17.17,0,0,1-.12,0,.14.14,0,0,1-.12-.06c-.83-1-1.63-1.47-2.39-1.37-1.29.19-2.06,2-2.07,2a.15.15,0,0,1-.18.09.15.15,0,0,1-.13-.16c0-3.2-3.75-5-4-5.07-6.3-1.73-8.59,4.41-8.61,4.47a.16.16,0,0,1-.21.1.17.17,0,0,1-.1-.21c0-.06,2.42-6.49,9-4.67h0c.16.07,3.73,1.67,4.15,4.78a3.15,3.15,0,0,1,2-1.65,3,3,0,0,1,2.59,1.36,2.28,2.28,0,0,1,1.77-.48c.74.17,1.34.83,1.79,2a.17.17,0,0,1-.09.21Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M217.44,201.64l-.06,0c-2.08-.83-3.34-1.93-3.75-3.27a3.51,3.51,0,0,1,.41-2.85.15.15,0,0,1,.22,0,.15.15,0,0,1,.05.22,3.28,3.28,0,0,0-.37,2.58c.38,1.24,1.58,2.27,3.56,3.06a.17.17,0,0,1-.06.32Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M223.65,202.06a4.13,4.13,0,0,1-2.56-.89,6.45,6.45,0,0,1-2.34-4.76.17.17,0,0,1,.16-.17h0a.17.17,0,0,1,.16.17,6.28,6.28,0,0,0,2.22,4.51,3.84,3.84,0,0,0,3.07.75,3.66,3.66,0,0,0,2.48-1.74l.09-.17a.18.18,0,0,1,.23-.07.17.17,0,0,1,.06.23l-.1.18a4,4,0,0,1-2.7,1.89A4.08,4.08,0,0,1,223.65,202.06Z"/>
|
||||||
|
</g>
|
||||||
|
<g opacity="0.2">
|
||||||
|
<path d="M192.47,216.58a7.25,7.25,0,0,1-4.68-1.63,8.32,8.32,0,0,1-2.36-3.13.16.16,0,0,1,.1-.21.17.17,0,0,1,.21.1,8,8,0,0,0,2.27,3c2.14,1.74,4.88,2,8.14.82a.16.16,0,0,1,.21.1.17.17,0,0,1-.1.21A11.13,11.13,0,0,1,192.47,216.58Z"/>
|
||||||
|
</g>
|
||||||
|
<ellipse cx="181.83" cy="227.04" rx="5.88" ry="8.74" fill="#f9b499"/>
|
||||||
|
<path d="M120.4,289.36l-2.94,41s2.62,7.36,12.58,6.05c0,0,11.76-1.14,8.82-13.07s-7-32.67-7-32.67,3.1-5.72,1.31-14.54a8.58,8.58,0,0,1,0-3.43s.16-2.13-1.64-2.62c0,0,8.17-9,1.31-8.16,0,0,3.43-4.58-2.45-4.09,0,0-.66-4.9-8-2,0,0-15.22,6.53-12.64,20.33a3.31,3.31,0,0,0,1,1.77C112.85,280.09,119.32,286.52,120.4,289.36Z" fill="#f9b499"/>
|
||||||
|
<path d="M115.34,267.47l-.08,0a.15.15,0,0,1-.06-.22,20.65,20.65,0,0,1,15.15-9.56.16.16,0,1,1,0,.32,20.36,20.36,0,0,0-14.91,9.4A.16.16,0,0,1,115.34,267.47Z" fill="#f7a48b"/>
|
||||||
|
<path d="M119.75,274.82a.18.18,0,0,1-.13-.06.17.17,0,0,1,0-.23s4.77-3.95,8.16-7c0,0,2.95-3.12,4.88-5.69a.16.16,0,0,1,.23,0,.16.16,0,0,1,0,.22c-1.94,2.6-4.88,5.71-4.91,5.74-3.41,3.08-8.14,7-8.19,7A.13.13,0,0,1,119.75,274.82Z" fill="#f7a48b"/>
|
||||||
|
<path d="M120.89,270.9a.19.19,0,0,1-.1,0,.16.16,0,0,1,0-.23c0-.06,4.75-5.93,11.14-7.73a.16.16,0,0,1,.2.11.15.15,0,0,1-.11.2c-6.29,1.77-10.93,7.56-11,7.62A.16.16,0,0,1,120.89,270.9Z" fill="#f7a48b"/>
|
||||||
|
<path d="M129.71,285.6a.16.16,0,0,1-.16-.16c0-3.19-2.57-8.37-2.6-8.42-1.51-3.7,4.24-6.94,4.48-7.08a.17.17,0,0,1,.22.06.17.17,0,0,1-.06.23c-.06,0-5.74,3.24-4.34,6.65.1.21,2.63,5.29,2.63,8.56A.17.17,0,0,1,129.71,285.6Z" fill="#f7a48b"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 33 KiB |
49
apps/server/view/less/common.less
Normal file
49
apps/server/view/less/common.less
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
@import '~normalize.css';
|
||||||
|
@import './var.less';
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: backset;
|
||||||
|
src: url('../assets/font/backset.woff');
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 1120px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
height: 2.4rem;
|
||||||
|
.navbar-section a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
img {
|
||||||
|
width: 1.2rem;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-family: backset;
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
color: #121212;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
apps/server/view/less/var.less
Normal file
12
apps/server/view/less/var.less
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// 重写bootstrap5 css variable
|
||||||
|
:root {
|
||||||
|
--color-blue-primary: rgb(45, 111, 247);
|
||||||
|
--color-black-primary: rgb(39, 41, 48);
|
||||||
|
--color-yellow-light: rgb(255, 191, 0);
|
||||||
|
--color-yellow-dark: rgb(217, 163, 74);
|
||||||
|
--color-font-primary: rgb(38, 51, 59);
|
||||||
|
--color-font-light: rgb(124, 127, 133);
|
||||||
|
--color-border: rgb(225, 227, 227);
|
||||||
|
--padding-btn: 11px 12px 12px;
|
||||||
|
--radius-primary: 2px;
|
||||||
|
}
|
3
apps/server/view/page/_layout/base.ejs
Normal file
3
apps/server/view/page/_layout/base.ejs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
9
apps/server/view/page/_layout/footer.ejs
Normal file
9
apps/server/view/page/_layout/footer.ejs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<footer class="container" style="text-align: center; margin-top: 40px">
|
||||||
|
<small
|
||||||
|
>Developed by
|
||||||
|
<a href="https://backset.cn" class="secondary">Backset.cn</a> •
|
||||||
|
<a target="_blank" href="http://beian.miit.gov.cn" class="secondary"
|
||||||
|
>苏ICP备19008833号-4</a
|
||||||
|
></small
|
||||||
|
>
|
||||||
|
</footer>
|
26
apps/server/view/page/_layout/nav.ejs
Normal file
26
apps/server/view/page/_layout/nav.ejs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<header class="navbar container">
|
||||||
|
<section class="navbar-section">
|
||||||
|
<a href="/">
|
||||||
|
<img src="/public/assets/img/annotation.svg" />
|
||||||
|
<span>Backset</span>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
<section class="navbar-center">
|
||||||
|
<!-- logo here -->
|
||||||
|
</section>
|
||||||
|
<section class="navbar-section">
|
||||||
|
<a href="/" class="btn btn-link">主页</a>
|
||||||
|
<!-- basic dropdown button -->
|
||||||
|
<div class="dropdown dropdown-right">
|
||||||
|
<a class="btn btn-link dropdown-toggle" tabindex="0">
|
||||||
|
下拉<i class="icon icon-caret"></i>
|
||||||
|
</a>
|
||||||
|
<!-- menu component -->
|
||||||
|
<ul class="menu">
|
||||||
|
<li class="menu-item"><a href="#dropdowns">Slack</a></li>
|
||||||
|
<li class="menu-item"><a href="#dropdowns">Slack</a></li>
|
||||||
|
<li class="menu-item"><a href="#dropdowns">Slack</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</header>
|
8
apps/server/view/page/_layout/scripts.ejs
Normal file
8
apps/server/view/page/_layout/scripts.ejs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<script src="public/vendors.js" crossorigin="anonymous"></script>
|
||||||
|
<!-- <script src="public/chunk-lib.js" crossorigin="anonymous"></script> -->
|
||||||
|
<!-- <script src="https://code.jquery.com/jquery-3.6.3.min.js"></script> -->
|
||||||
|
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="<%= locals.assets %><%= name %>.js?v=<%= locals.version %>"
|
||||||
|
></script>
|
7
apps/server/view/page/_layout/styles.ejs
Normal file
7
apps/server/view/page/_layout/styles.ejs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/public/assets/img/annotation.svg" />
|
||||||
|
<!-- <link rel="stylesheet" type="text/css" href="public/assets/css/ui.min.css" /> -->
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="<%= locals.assets %><%= name %>.css?v=<%= locals.version %>"
|
||||||
|
/>
|
28
apps/server/view/page/home/index.ejs
Normal file
28
apps/server/view/page/home/index.ejs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<%- include('../_layout/base') -%> <%- include('../_layout/styles') -%>
|
||||||
|
<title>home页面</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>home</p>
|
||||||
|
<a href="/signup">去 注册 页面</a>
|
||||||
|
|
||||||
|
<span>当前assets: <%= assets %></span>
|
||||||
|
|
||||||
|
<span class="bs-tooltip fade-right" data-bs-text="Fade right">
|
||||||
|
Hover me
|
||||||
|
</span>
|
||||||
|
<span class="bs-tooltip zoom-up" data-bs-text="Zoom up">Hover me</span>
|
||||||
|
|
||||||
|
<h1>1</h1>
|
||||||
|
|
||||||
|
<select class="ttt">
|
||||||
|
<option>最近的</option>
|
||||||
|
<option selected>最热门</option>
|
||||||
|
<option>最多喜欢</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<%- include('../_layout/footer') -%> <%- include('../_layout/scripts') -%>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
apps/server/view/page/home/index.less
Normal file
1
apps/server/view/page/home/index.less
Normal file
|
@ -0,0 +1 @@
|
||||||
|
@import '../../less/common.less';
|
12
apps/server/view/page/home/index.ts
Normal file
12
apps/server/view/page/home/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import $ from 'jquery';
|
||||||
|
import '@backset/ui/dist/ui.css';
|
||||||
|
import { Dropdown } from '@backset/ui';
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
new Dropdown({
|
||||||
|
selector: '.ttt',
|
||||||
|
onChange: () => {
|
||||||
|
console.log('change');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
106
apps/server/view/page/login/index.ejs
Normal file
106
apps/server/view/page/login/index.ejs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<%- include('../_layout/base') -%> <%- include('../_layout/styles') -%>
|
||||||
|
<title>new page: <%= name %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- include('../_layout/nav') -%>
|
||||||
|
<main class="container" id="page-signup">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column col-5 col-mx-auto">
|
||||||
|
<div class="box-container">
|
||||||
|
<img src="/assets/img/logo.png" />
|
||||||
|
</div>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="switch-login">
|
||||||
|
<button
|
||||||
|
class="btn btn-link"
|
||||||
|
id="btn-switch-login-type"
|
||||||
|
data-login-type="password"
|
||||||
|
>
|
||||||
|
验证码登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form onsubmit="return false;" id="password-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="username">用户名</label>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
placeholder="用户名/手机号"
|
||||||
|
autocomplete="username"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="password">密码</label>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
placeholder="密码"
|
||||||
|
autocomplete="current-password"
|
||||||
|
/>
|
||||||
|
<p class="form-input-hint">密码长度至少6位</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-checkbox">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<i class="form-icon"></i>记住密码
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button
|
||||||
|
id="btn-signup-password"
|
||||||
|
class="btn btn-primary btn-block"
|
||||||
|
>
|
||||||
|
登入
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form onsubmit="return false;" id="verify-form" class="d-none">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="tel">手机号</label>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
type="tel"
|
||||||
|
id="tel"
|
||||||
|
placeholder="手机号"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="input-example-1">验证码</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
id="verify-code"
|
||||||
|
placeholder="输入验证码"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="btn-verify-code"
|
||||||
|
class="btn btn-primary input-group-btn"
|
||||||
|
>
|
||||||
|
获取验证码
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 2em">
|
||||||
|
<button
|
||||||
|
id="btn-signup-verify"
|
||||||
|
class="btn btn-primary btn-block"
|
||||||
|
>
|
||||||
|
登入
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<%- include('../_layout/footer') -%> <%- include('../_layout/scripts') -%>
|
||||||
|
</body>
|
||||||
|
</html>
|
23
apps/server/view/page/login/index.less
Normal file
23
apps/server/view/page/login/index.less
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
@import '../../less/common.less';
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
.hero {
|
||||||
|
padding-bottom: 0;
|
||||||
|
.hero-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.switch-login {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-container {
|
||||||
|
padding-top: 4rem;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
> img {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
|
}
|
71
apps/server/view/page/login/index.ts
Normal file
71
apps/server/view/page/login/index.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import './index.less';
|
||||||
|
import { RegUtil } from '@backset/util';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
$('#page-signup')
|
||||||
|
.on('click', '#btn-switch-login-type', handleSwitchLoginType)
|
||||||
|
.on('click', '#btn-signup-password', handlePasswordLogin)
|
||||||
|
.on('click', '#btn-signup-verify', handleVerifyLogin)
|
||||||
|
.on('click', '#btn-verify-code', handleGetVerifyCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取验证码
|
||||||
|
*/
|
||||||
|
function handleGetVerifyCode() {
|
||||||
|
const params = { phone: '' + $('#tel').val() };
|
||||||
|
if (!RegUtil.PHONE.test(params.phone)) return;
|
||||||
|
// return message.error({ text: '手机号格式错误' });
|
||||||
|
$('#btn-verify-code').addClass('loading');
|
||||||
|
$.post('/sms/verify', params, res => {
|
||||||
|
console.log(res);
|
||||||
|
if (res) {
|
||||||
|
$('#btn-verify-code').removeClass('loading');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码登录
|
||||||
|
*/
|
||||||
|
function handleVerifyLogin() {
|
||||||
|
const params = {
|
||||||
|
login_type: 'verifycode',
|
||||||
|
user_login: '' + $('#tel').val(),
|
||||||
|
verify_code: '' + $('#verify-code').val(),
|
||||||
|
};
|
||||||
|
$.post('/auth/user/login', params, res => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码登录
|
||||||
|
*/
|
||||||
|
function handlePasswordLogin() {
|
||||||
|
const params = {
|
||||||
|
login_type: 'password',
|
||||||
|
user_login: '' + $('#username').val(),
|
||||||
|
user_pass: '' + $('#password').val(),
|
||||||
|
};
|
||||||
|
$.post('/auth/user/login', params, res => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击登录方式切换
|
||||||
|
*/
|
||||||
|
function handleSwitchLoginType(this: any) {
|
||||||
|
const switchVerify = $(this).attr('data-login-type') === 'password';
|
||||||
|
if (switchVerify) {
|
||||||
|
$('#verify-form').removeClass('d-none');
|
||||||
|
$('#password-form').addClass('d-none');
|
||||||
|
$(this).attr('data-login-type', 'verify').html('密码登陆');
|
||||||
|
} else {
|
||||||
|
$('#verify-form').addClass('d-none');
|
||||||
|
$('#password-form').removeClass('d-none');
|
||||||
|
$(this).attr('data-login-type', 'password').html('验证码登陆');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
20
apps/server/view/page/notFound/index.ejs
Normal file
20
apps/server/view/page/notFound/index.ejs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>404</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="<%= assets %><%= name %>.css?v=<%= version %>"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>404</p>
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="<%= assets %><%= name %>.js?v=<%= version %>"
|
||||||
|
></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
2
apps/server/view/page/notFound/index.less
Normal file
2
apps/server/view/page/notFound/index.less
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
@import '../../less/common.less';
|
||||||
|
@import '../../less/var.less';
|
1
apps/server/view/page/notFound/index.ts
Normal file
1
apps/server/view/page/notFound/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import './index.less';
|
70
apps/server/view/page/signup/index.ejs
Normal file
70
apps/server/view/page/signup/index.ejs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<%- include('../_layout/base') -%>
|
||||||
|
<%- include('../_layout/styles') -%>
|
||||||
|
<title>new page: <%= name %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- include('../_layout/nav') -%>
|
||||||
|
<div class="container" id="signup-module">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column col-5 col-mx-auto">
|
||||||
|
<div class="box-container">
|
||||||
|
<div class="slogan">
|
||||||
|
<img src="/public/dev/assets/img/logo.png" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="hero">
|
||||||
|
<div class="hero-body">
|
||||||
|
<h2>你好啊!</h2>
|
||||||
|
<p>欢迎加入韭菜们的数字化空间</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form onsubmit="return false;" id="password-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="username">用户名</label>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
placeholder="用户名"
|
||||||
|
autocomplete="off"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="username">手机号</label>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
type="tel"
|
||||||
|
id="tel"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="手机号"
|
||||||
|
minlength="11"
|
||||||
|
maxlength="11"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="password">密码</label>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
placeholder="密码"
|
||||||
|
minlength="6"
|
||||||
|
/>
|
||||||
|
<p class="form-input-hint">密码长度至少6位</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button id="btn-signup" class="btn btn-primary btn-block">
|
||||||
|
注册
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%- include('../_layout/footer') -%> <%- include('../_layout/scripts') -%>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
apps/server/view/page/signup/index.less
Normal file
10
apps/server/view/page/signup/index.less
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
@import '../../less/common.less';
|
||||||
|
|
||||||
|
.slogan {
|
||||||
|
padding-top: 4rem;
|
||||||
|
text-align: center;
|
||||||
|
img {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
|
}
|
25
apps/server/view/page/signup/index.ts
Normal file
25
apps/server/view/page/signup/index.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import './index.less';
|
||||||
|
import { RegUtil, ValidateUtil } from '@backset/util';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
$('#signup-module').on('click', '#btn-signup', handleCreateUser);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册
|
||||||
|
*/
|
||||||
|
function handleCreateUser() {
|
||||||
|
const params = {
|
||||||
|
user_login: '' + $('#username').val(),
|
||||||
|
user_pass: '' + $('#password').val(),
|
||||||
|
user_phone: '' + $('#tel').val(),
|
||||||
|
};
|
||||||
|
if (ValidateUtil.withEmpty(params)) return;
|
||||||
|
// return message.error({ text: '请补全表单' });
|
||||||
|
if (!RegUtil.PHONE.test(params.user_phone)) return;
|
||||||
|
// return message.error({ text: '手机号格式错误' });
|
||||||
|
$.post('/user/create', params, res => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
141
apps/server/webpack.config.js
Normal file
141
apps/server/webpack.config.js
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
/*eslint-disable*/
|
||||||
|
const { join } = require('path');
|
||||||
|
const path = require('path');
|
||||||
|
const { readdirSync, statSync } = require('fs');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
const isDev = process.env.RUNNING_ENV === 'dev';
|
||||||
|
const isProd = process.env.RUNNING_ENV === 'prod';
|
||||||
|
/**
|
||||||
|
* 引入 src/view/pages下的页面文件,排除 _ 开头的文件夹
|
||||||
|
*/
|
||||||
|
const importEntry = () => {
|
||||||
|
const entries = {};
|
||||||
|
const rootDir = join(process.cwd(), '/view/page');
|
||||||
|
readdirSync(rootDir)
|
||||||
|
.filter(i => !i.startsWith('_'))
|
||||||
|
.forEach(file => {
|
||||||
|
if (statSync(join(rootDir, file)).isDirectory())
|
||||||
|
entries[file] = `/view/page/${file}/index.ts`;
|
||||||
|
});
|
||||||
|
return entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
...importEntry(),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'public/'),
|
||||||
|
publicPath: '',
|
||||||
|
filename: '[name].js',
|
||||||
|
chunkFilename: '[id].chunk.js?[hash:8]',
|
||||||
|
clean: true
|
||||||
|
},
|
||||||
|
mode: isDev ? 'development' : 'production',
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
minimizer: [new TerserPlugin({
|
||||||
|
terserOptions: {
|
||||||
|
compress: true,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
cacheGroups: {
|
||||||
|
vendor: {
|
||||||
|
name: 'vendors',
|
||||||
|
test: /[\\/]node_modules[\\/]/,
|
||||||
|
priority: 10,
|
||||||
|
chunks: 'initial'
|
||||||
|
},
|
||||||
|
// lib: {
|
||||||
|
// test(module) {
|
||||||
|
// return (
|
||||||
|
// module.size() > 50 * 1024 &&
|
||||||
|
// /node_modules[/\\]/.test(module.nameForCondition() || '')
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
// name: 'chunk-lib',
|
||||||
|
// priority: 15,
|
||||||
|
// minChunks: 1,
|
||||||
|
// reuseExistingChunk: true,
|
||||||
|
// },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [MiniCssExtractPlugin.loader, 'css-loader'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.less$/,
|
||||||
|
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(scss)$/,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
{
|
||||||
|
loader: 'css-loader'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: {
|
||||||
|
plugins: () => [
|
||||||
|
require('autoprefixer')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'sass-loader'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|gif)$/i,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'url-loader',
|
||||||
|
options: {
|
||||||
|
limit: 8192,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: '[name].css',
|
||||||
|
chunkFilename: '[id].css?[hash:8]',
|
||||||
|
}),
|
||||||
|
new CopyPlugin([
|
||||||
|
{ from: join(process.cwd(), 'view/assets'), to: 'assets' },
|
||||||
|
]),
|
||||||
|
isProd && new BundleAnalyzerPlugin({
|
||||||
|
analyzerHost: "0.0.0.0",
|
||||||
|
analyzerPort: 8088
|
||||||
|
})
|
||||||
|
].filter(Boolean),
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js', '.ejs'],
|
||||||
|
},
|
||||||
|
externals: {
|
||||||
|
// require("jquery") 是外部的,并且可用
|
||||||
|
// 在全局变量 jQuery 上
|
||||||
|
// jquery: 'jQuery',
|
||||||
|
// $: 'jQuery'
|
||||||
|
},
|
||||||
|
};
|
24
apps/web/.gitignore
vendored
24
apps/web/.gitignore
vendored
|
@ -1,24 +0,0 @@
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
|
@ -1,14 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>回溯 - Backset.cn</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="bs-app"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
<script src="/hls.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,40 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@backset/web",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc && vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"less": "^4.1.3",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-router-dom": "6.8.0",
|
|
||||||
"@ricons/fluent": "0.12.0",
|
|
||||||
"@ricons/utils": "0.1.6",
|
|
||||||
"dplayer": "1.27.1",
|
|
||||||
"identicon.js": "2.3.3",
|
|
||||||
"react-hot-toast": "2.4.0",
|
|
||||||
"react-spinners": "0.13.8",
|
|
||||||
"react-markdown": "8.0.6",
|
|
||||||
"remark-gfm": "3.0.1",
|
|
||||||
"crypto-js": "4.1.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^18.0.27",
|
|
||||||
"@types/react-dom": "^18.0.10",
|
|
||||||
"@types/react-router-dom": "5.3.3",
|
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
|
||||||
"vite-tsconfig-paths": "4.0.5",
|
|
||||||
"typescript": "^4.9.3",
|
|
||||||
"vite": "^4.1.0",
|
|
||||||
"@types/dplayer": "1.25.2",
|
|
||||||
"@types/identicon.js": "2.3.1",
|
|
||||||
"rollup-plugin-visualizer": "5.9.0",
|
|
||||||
"vite-plugin-compression": "0.5.1",
|
|
||||||
"@types/crypto-js": "4.1.1"
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user