feat: 管理后台
This commit is contained in:
parent
3c1e8c2247
commit
031be2925f
|
@ -14,7 +14,8 @@
|
|||
"less": "^4.1.3",
|
||||
"react": "^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": {
|
||||
"@types/react": "^18.0.27",
|
||||
|
|
|
@ -1,52 +1,15 @@
|
|||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import "./assets/less/common.less";
|
||||
import { Route, Routes, useNavigate } from "react-router-dom";
|
||||
import User from "./view/User";
|
||||
import Home from "./view/Home";
|
||||
import { Guard } from "./router/Guard";
|
||||
import Layout from "./layout";
|
||||
import Login from "./view/Login";
|
||||
|
||||
function App() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const routerList = [
|
||||
{
|
||||
path: "/",
|
||||
element: <Home />,
|
||||
name: "首页",
|
||||
},
|
||||
{
|
||||
path: "user",
|
||||
element: <User />,
|
||||
name: "用户",
|
||||
},
|
||||
];
|
||||
|
||||
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>
|
||||
{routerList.map((router) => (
|
||||
<Route
|
||||
key={router.path}
|
||||
path={router.path}
|
||||
element={<Guard>{router.element}</Guard>}
|
||||
/>
|
||||
))}
|
||||
<Route path="*" element={<span>404</span>} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
<Routes>
|
||||
<Route key={"login"} path={"/login"} element={<Login />} />
|
||||
<Route key={"dash"} path={"/*"} element={<Layout />} />
|
||||
<Route path="*" element={<span>404</span>} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
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;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
|
@ -14,3 +15,12 @@ ul {
|
|||
margin: 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 { useArticle } from "@backset/ui";
|
||||
import { useMount } from "../../hooks";
|
||||
|
||||
export default function Index() {
|
||||
useMount(() => {
|
||||
console.log(useArticle());
|
||||
});
|
||||
useMount(() => {});
|
||||
|
||||
const onClick = () => {
|
||||
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() {
|
||||
return <div>UserIndex</div>;
|
||||
}
|
||||
export default User;
|
||||
|
|
|
@ -5,4 +5,7 @@ import tsconfigPaths from "vite-tsconfig-paths";
|
|||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
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