feat: 管理后台
This commit is contained in:
parent
3c1e8c2247
commit
031be2925f
|
@ -14,7 +14,8 @@
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.27",
|
"@types/react": "^18.0.27",
|
||||||
|
|
|
@ -1,52 +1,15 @@
|
||||||
|
import { Navigate, Route, Routes } from "react-router-dom";
|
||||||
import "./assets/less/common.less";
|
import "./assets/less/common.less";
|
||||||
import { Route, Routes, useNavigate } from "react-router-dom";
|
import Layout from "./layout";
|
||||||
import User from "./view/User";
|
import Login from "./view/Login";
|
||||||
import Home from "./view/Home";
|
|
||||||
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 (
|
||||||
<>
|
|
||||||
<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>
|
||||||
{routerList.map((router) => (
|
<Route key={"login"} path={"/login"} element={<Login />} />
|
||||||
<Route
|
<Route key={"dash"} path={"/*"} element={<Layout />} />
|
||||||
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>
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
apps/admin/src/assets/less/backset.woff
Normal file
BIN
apps/admin/src/assets/less/backset.woff
Normal file
Binary file not shown.
|
@ -3,6 +3,7 @@ html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -14,3 +15,12 @@ ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "bs";
|
||||||
|
src: url("./backset.woff");
|
||||||
|
}
|
||||||
|
|
17
apps/admin/src/layout/index.less
Normal file
17
apps/admin/src/layout/index.less
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.logo {
|
||||||
|
float: left;
|
||||||
|
width: 120px;
|
||||||
|
height: 31px;
|
||||||
|
margin: 16px 24px 16px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
svg {
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
padding-left: 10px;
|
||||||
|
font-family: "bs";
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
112
apps/admin/src/layout/index.tsx
Normal file
112
apps/admin/src/layout/index.tsx
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import React, { Suspense } from "react";
|
||||||
|
import "./index.less";
|
||||||
|
import { VideoCameraAddOutlined, UserSwitchOutlined } 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: "user",
|
||||||
|
icon: <UserSwitchOutlined />,
|
||||||
|
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 (
|
||||||
|
<Layout style={{ height: "100%" }}>
|
||||||
|
<Header className="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={["/"]}
|
||||||
|
items={navMenus}
|
||||||
|
onClick={onClickNavMenuItem}
|
||||||
|
/>
|
||||||
|
</Header>
|
||||||
|
<Layout>
|
||||||
|
<Sider width={200} style={{ background: colorBgContainer }}>
|
||||||
|
<Menu
|
||||||
|
mode="inline"
|
||||||
|
defaultSelectedKeys={["1"]}
|
||||||
|
defaultOpenKeys={["course"]}
|
||||||
|
style={{ height: "100%", borderRight: 0 }}
|
||||||
|
items={sideMenus}
|
||||||
|
onClick={onClickSideMenuItem}
|
||||||
|
/>
|
||||||
|
</Sider>
|
||||||
|
<Layout className="site-layout">
|
||||||
|
<Content
|
||||||
|
style={{
|
||||||
|
margin: "24px 16px",
|
||||||
|
padding: 24,
|
||||||
|
minHeight: 280,
|
||||||
|
background: colorBgContainer,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
41
apps/admin/src/router/index.tsx
Normal file
41
apps/admin/src/router/index.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
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: "/",
|
||||||
|
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: "/user",
|
||||||
|
element: lazy(() => import("../view/User")),
|
||||||
|
name: "用户",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const navMenuList = navRoutes.map((route) => {
|
||||||
|
const { path: key, name: label } = route;
|
||||||
|
return { key, label };
|
||||||
|
});
|
|
@ -1,3 +0,0 @@
|
||||||
body {
|
|
||||||
background: grey;
|
|
||||||
}
|
|
5
apps/admin/src/view/Course/Create/index.tsx
Normal file
5
apps/admin/src/view/Course/Create/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const CourseCreate = () => {
|
||||||
|
return <div>课程</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CourseCreate;
|
134
apps/admin/src/view/Login/index.less
Normal file
134
apps/admin/src/view/Login/index.less
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
#root {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
#e95659,
|
||||||
|
#e15084,
|
||||||
|
#c55aaa,
|
||||||
|
#976bc4,
|
||||||
|
#5678ce
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
66
apps/admin/src/view/Login/index.tsx
Normal file
66
apps/admin/src/view/Login/index.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { Button, Checkbox, Form, Input } from "antd";
|
||||||
|
import "./index.less";
|
||||||
|
|
||||||
|
const onFinish = (values: any) => {
|
||||||
|
console.log("Success:", values);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
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 name="remember" valuePropName="checked">
|
||||||
|
<Checkbox>记住我</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item wrapperCol={{ span: 24 }}>
|
||||||
|
<Button type="primary" htmlType="submit" block>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
|
@ -1,11 +1,8 @@
|
||||||
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 哪里又有坑哦`);
|
5
apps/admin/src/view/Payment/index.tsx
Normal file
5
apps/admin/src/view/Payment/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
return <div>UserIndex</div>;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
const User = () => {
|
||||||
|
return <div>用户据</div>;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Index() {
|
export default User;
|
||||||
return <div>UserIndex</div>;
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,4 +5,7 @@ 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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
826
pnpm-lock.yaml
826
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user