zt-admin-template 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -0
- package/template/.env.development +2 -0
- package/template/.env.production +2 -0
- package/template/.env.test +2 -0
- package/template/.kiro/specs/course-backend-integration/.config.kiro +1 -0
- package/template/.kiro/specs/course-backend-integration/design.md +234 -0
- package/template/.kiro/specs/course-backend-integration/requirements.md +116 -0
- package/template/.kiro/specs/course-backend-integration/tasks.md +0 -0
- package/template/COMPLETION_CHECKLIST.md +305 -0
- package/template/DEPLOYMENT_GUIDE.md +391 -0
- package/template/FINAL_SUMMARY.md +428 -0
- package/template/IMPLEMENTATION_SUMMARY.md +382 -0
- package/template/INTEGRATION_GUIDE.md +458 -0
- package/template/PROJECT_OVERVIEW.md +343 -0
- package/template/QUICK_START.md +273 -0
- package/template/RBAC_Tutorial.md +424 -0
- package/template/README.md +16 -0
- package/template/React_Antd_TS_Tutorial.md +279 -0
- package/template/START_ALL.md +163 -0
- package/template/SYSTEM_MANAGEMENT.md +247 -0
- package/template/eslint.config.js +29 -0
- package/template/index.html +13 -0
- package/template/koa-server/README.md +65 -0
- package/template/koa-server/app.js +625 -0
- package/template/koa-server/package-lock.json +1547 -0
- package/template/koa-server/package.json +26 -0
- package/template/koa-server/public/assets/index-B1Cj4mG9.css +1 -0
- package/template/koa-server/public/assets/index-Mgxg-xqT.js +503 -0
- package/template/koa-server/public/favicon.svg +1 -0
- package/template/koa-server/public/icons.svg +24 -0
- package/template/koa-server/public/index.html +14 -0
- package/template/koa-server/uploads/1774265088480-962006467.png +0 -0
- package/template/koa-server/uploads/file-1774346891704-610962013.png +0 -0
- package/template/koa-server/uploads/file-1774346898887-58636533.png +0 -0
- package/template/koa-server/uploads/file-1774346912676-771862547.png +0 -0
- package/template/koa-server/uploads/file-1774347025308-130037894.png +0 -0
- package/template/koa-server/uploads/file-1774347031104-766499773.png +0 -0
- package/template/koa-server/uploads/file-1774347094969-731402203.png +0 -0
- package/template/koa-server/uploads/file-1774347101948-330296656.png +0 -0
- package/template/koa-server/uploads/file-1774351682377-932868720.png +0 -0
- package/template/koa-server/uploads/file-1774352037654-877426905.png +0 -0
- package/template/koa-server/uploads/file-1774352175463-386248997.png +0 -0
- package/template/koa-server/uploads/file-1774361446433-405859961.png +0 -0
- package/template/koa-server/uploads/file-1774361512207-465806267.png +0 -0
- package/template/lianxi.html +15 -0
- package/template/package-lock.json +6307 -0
- package/template/package.json +36 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/icons.svg +24 -0
- package/template/src/App.css +184 -0
- package/template/src/App.tsx +44 -0
- package/template/src/api/course.ts +86 -0
- package/template/src/api/menu.ts +55 -0
- package/template/src/api/role.ts +58 -0
- package/template/src/api/user.ts +58 -0
- package/template/src/assets/hero.png +0 -0
- package/template/src/assets/react.svg +1 -0
- package/template/src/assets/vite.svg +1 -0
- package/template/src/components/Child.tsx +10 -0
- package/template/src/components/MainLayout.tsx +169 -0
- package/template/src/components/SunZi.tsx +13 -0
- package/template/src/contexts/ThemeContext.tsx +33 -0
- package/template/src/hooks/usePermission.tsx +62 -0
- package/template/src/index.css +111 -0
- package/template/src/main.tsx +13 -0
- package/template/src/pages/Dashboard.tsx +39 -0
- package/template/src/pages/Users.tsx +95 -0
- package/template/src/pages/banner/BannerList.tsx +182 -0
- package/template/src/pages/course/Course.tsx +586 -0
- package/template/src/pages/course/CourseList.tsx +168 -0
- package/template/src/pages/system/menu/Menu.tsx +501 -0
- package/template/src/pages/system/role/Role.tsx +458 -0
- package/template/src/pages/system/user/User.tsx +364 -0
- package/template/src/types/permission.ts +21 -0
- package/template/src/utils/request.tsx +94 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.app.json +32 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +13 -0
- package/template/vite.config.ts +30 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Layout, Menu, Breadcrumb, theme } from 'antd';
|
|
3
|
+
import {
|
|
4
|
+
PieChartOutlined,
|
|
5
|
+
UserOutlined,
|
|
6
|
+
BookOutlined,
|
|
7
|
+
PictureOutlined,
|
|
8
|
+
} from '@ant-design/icons';
|
|
9
|
+
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
|
10
|
+
import type { MenuProps } from 'antd';
|
|
11
|
+
import { usePermission } from '../hooks/usePermission';
|
|
12
|
+
|
|
13
|
+
const { Header, Content, Footer, Sider } = Layout;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const items: any[] = [
|
|
17
|
+
{
|
|
18
|
+
key: "/",
|
|
19
|
+
icon: <PieChartOutlined />,
|
|
20
|
+
label: "Dashboard",
|
|
21
|
+
permission: "admin"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: "system",
|
|
25
|
+
label: "系统管理",
|
|
26
|
+
icon: <UserOutlined />,
|
|
27
|
+
permission: "admin",
|
|
28
|
+
children: [
|
|
29
|
+
{
|
|
30
|
+
label: "用户管理",
|
|
31
|
+
key: "/system/user",
|
|
32
|
+
permission: "admin",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: "角色管理",
|
|
36
|
+
key: "/system/role",
|
|
37
|
+
permission: "admin",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: "菜单管理",
|
|
41
|
+
key: "/system/menu",
|
|
42
|
+
permission: "admin",
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "course",
|
|
48
|
+
label: "课程管理",
|
|
49
|
+
icon: <BookOutlined />,
|
|
50
|
+
permission: "admin",
|
|
51
|
+
children: [
|
|
52
|
+
{
|
|
53
|
+
label: "添加课程",
|
|
54
|
+
key: "/course/add",
|
|
55
|
+
permission: "admin",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: "课程列表",
|
|
59
|
+
key: "/course/list",
|
|
60
|
+
permission: "admin",
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: "banner",
|
|
66
|
+
label: "Banner管理",
|
|
67
|
+
icon: <PictureOutlined />,
|
|
68
|
+
permission: "admin",
|
|
69
|
+
children: [
|
|
70
|
+
{
|
|
71
|
+
label: "Banner列表",
|
|
72
|
+
key: "/banner/list",
|
|
73
|
+
permission: "admin",
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// 过滤用户拥有的菜单
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
const MainLayout: React.FC = () => {
|
|
83
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
84
|
+
const {
|
|
85
|
+
token: { colorBgContainer, borderRadiusLG },
|
|
86
|
+
} = theme.useToken();
|
|
87
|
+
const navigate = useNavigate();
|
|
88
|
+
const location = useLocation();
|
|
89
|
+
|
|
90
|
+
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
|
|
91
|
+
if (key.startsWith('/')) {
|
|
92
|
+
navigate(key);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
//2. 从所有菜单中过滤出当前用户能看到的
|
|
99
|
+
const { filterMenu } = usePermission()
|
|
100
|
+
const visibleMenus = filterMenu(items);
|
|
101
|
+
console.log(visibleMenus);
|
|
102
|
+
|
|
103
|
+
console.log("--------------");
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Layout
|
|
109
|
+
style={{
|
|
110
|
+
minWidth: "100vw",
|
|
111
|
+
minHeight: '100vh',
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<Sider collapsible collapsed={collapsed} onCollapse={(value) => setCollapsed(value)}>
|
|
115
|
+
<div className="demo-logo-vertical" style={{ height: 32, margin: 16, background: 'rgba(255, 255, 255, 0.2)' }} />
|
|
116
|
+
<Menu
|
|
117
|
+
theme="dark"
|
|
118
|
+
defaultSelectedKeys={['/']}
|
|
119
|
+
selectedKeys={[location.pathname]}
|
|
120
|
+
mode="inline"
|
|
121
|
+
items={visibleMenus}
|
|
122
|
+
onClick={handleMenuClick}
|
|
123
|
+
/>
|
|
124
|
+
</Sider>
|
|
125
|
+
<Layout>
|
|
126
|
+
<Header
|
|
127
|
+
style={{
|
|
128
|
+
padding: 0,
|
|
129
|
+
background: colorBgContainer,
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
<Content
|
|
133
|
+
style={{
|
|
134
|
+
margin: '0 16px',
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<Breadcrumb
|
|
138
|
+
style={{
|
|
139
|
+
margin: '16px 0',
|
|
140
|
+
}}
|
|
141
|
+
items={[
|
|
142
|
+
{ title: 'User' },
|
|
143
|
+
{ title: 'Bill' },
|
|
144
|
+
]}
|
|
145
|
+
/>
|
|
146
|
+
<div
|
|
147
|
+
style={{
|
|
148
|
+
padding: 24,
|
|
149
|
+
minHeight: 360,
|
|
150
|
+
background: colorBgContainer,
|
|
151
|
+
borderRadius: borderRadiusLG,
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<Outlet />
|
|
155
|
+
</div>
|
|
156
|
+
</Content>
|
|
157
|
+
<Footer
|
|
158
|
+
style={{
|
|
159
|
+
textAlign: 'center',
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
Ant Design ©{new Date().getFullYear()} Created by Ant UED
|
|
163
|
+
</Footer>
|
|
164
|
+
</Layout>
|
|
165
|
+
</Layout>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default MainLayout;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useTheme } from '../contexts/ThemeContext'
|
|
2
|
+
|
|
3
|
+
export default function SunZi() {
|
|
4
|
+
|
|
5
|
+
const {theme, toggleTheme} = useTheme()
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div>
|
|
9
|
+
孙子组件:{theme}
|
|
10
|
+
<button onClick={toggleTheme} style={{ marginLeft: '10px' }}>切换主题</button>
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createContext, useContext, useState, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
interface ThemeContextType {
|
|
4
|
+
theme: string;
|
|
5
|
+
toggleTheme: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const ThemeContext = createContext<ThemeContextType>({
|
|
9
|
+
theme: "light",
|
|
10
|
+
toggleTheme: () => { }
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
interface ThemeProviderProps {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ThemeProvider({ children }: ThemeProviderProps) {
|
|
18
|
+
const [theme, setTheme] = useState<string>("light");
|
|
19
|
+
|
|
20
|
+
const toggleTheme = () => {
|
|
21
|
+
setTheme(prev => prev === "light" ? "dark" : "light");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
|
26
|
+
{children}
|
|
27
|
+
</ThemeContext.Provider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useTheme(){
|
|
32
|
+
return useContext(ThemeContext)
|
|
33
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
2
|
+
import { AuthRouteConfig, PermissionCode, UserPermission } from "../types/permission";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const PermissionContext = createContext<UserPermission | null>(null);
|
|
6
|
+
|
|
7
|
+
// 导出一个权限Provider,通过这个Provider给后代组件传递数据
|
|
8
|
+
export const PermissionProvider: React.FC<{
|
|
9
|
+
children: React.ReactNode,
|
|
10
|
+
permission: UserPermission
|
|
11
|
+
}> = ({ children, permission }) => {
|
|
12
|
+
return (
|
|
13
|
+
<PermissionContext.Provider value={permission}>
|
|
14
|
+
{children}
|
|
15
|
+
</PermissionContext.Provider>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 封装一个权限校验的hook
|
|
20
|
+
export const usePermission = () => {
|
|
21
|
+
//1. 先取出用户拥有的权限列表
|
|
22
|
+
const context = useContext(PermissionContext);
|
|
23
|
+
|
|
24
|
+
//2. 封装权限校验的函数
|
|
25
|
+
const hasPermission = (code: PermissionCode) => {
|
|
26
|
+
return context?.permissions.includes(code)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//3. 过滤带权限的菜单
|
|
30
|
+
const filterMenu = useMemo(() => {
|
|
31
|
+
const filter = (menus: any[]) => {
|
|
32
|
+
// 遍历所有的菜单
|
|
33
|
+
return menus.filter((menu: AuthRouteConfig) => {
|
|
34
|
+
// 如果菜单是隐藏的,就不显示
|
|
35
|
+
if (menu.hidden) return false;
|
|
36
|
+
|
|
37
|
+
if (!menu.permission) return true;
|
|
38
|
+
|
|
39
|
+
// 执行到这里,说明有权限标识符,判断一下是否有权限
|
|
40
|
+
const hasMenuPermission = hasPermission(menu.permission)
|
|
41
|
+
|
|
42
|
+
// 还需要递归遍历子菜单
|
|
43
|
+
if (menu.children && menu.children.length > 0) {
|
|
44
|
+
menu.children = filter(menu.children)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return hasMenuPermission;
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return filter;
|
|
53
|
+
}, [hasPermission])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
hasPermission,
|
|
58
|
+
filterMenu,
|
|
59
|
+
context
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--text: #6b6375;
|
|
3
|
+
--text-h: #08060d;
|
|
4
|
+
--bg: #fff;
|
|
5
|
+
--border: #e5e4e7;
|
|
6
|
+
--code-bg: #f4f3ec;
|
|
7
|
+
--accent: #aa3bff;
|
|
8
|
+
--accent-bg: rgba(170, 59, 255, 0.1);
|
|
9
|
+
--accent-border: rgba(170, 59, 255, 0.5);
|
|
10
|
+
--social-bg: rgba(244, 243, 236, 0.5);
|
|
11
|
+
--shadow:
|
|
12
|
+
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
|
13
|
+
|
|
14
|
+
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
15
|
+
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
16
|
+
--mono: ui-monospace, Consolas, monospace;
|
|
17
|
+
|
|
18
|
+
font: 18px/145% var(--sans);
|
|
19
|
+
letter-spacing: 0.18px;
|
|
20
|
+
color-scheme: light dark;
|
|
21
|
+
color: var(--text);
|
|
22
|
+
background: var(--bg);
|
|
23
|
+
font-synthesis: none;
|
|
24
|
+
text-rendering: optimizeLegibility;
|
|
25
|
+
-webkit-font-smoothing: antialiased;
|
|
26
|
+
-moz-osx-font-smoothing: grayscale;
|
|
27
|
+
|
|
28
|
+
@media (max-width: 1024px) {
|
|
29
|
+
font-size: 16px;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@media (prefers-color-scheme: dark) {
|
|
34
|
+
:root {
|
|
35
|
+
--text: #9ca3af;
|
|
36
|
+
--text-h: #f3f4f6;
|
|
37
|
+
--bg: #16171d;
|
|
38
|
+
--border: #2e303a;
|
|
39
|
+
--code-bg: #1f2028;
|
|
40
|
+
--accent: #c084fc;
|
|
41
|
+
--accent-bg: rgba(192, 132, 252, 0.15);
|
|
42
|
+
--accent-border: rgba(192, 132, 252, 0.5);
|
|
43
|
+
--social-bg: rgba(47, 48, 58, 0.5);
|
|
44
|
+
--shadow:
|
|
45
|
+
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#social .button-icon {
|
|
49
|
+
filter: invert(1) brightness(2);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
body {
|
|
54
|
+
margin: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#root {
|
|
58
|
+
box-sizing: border-box;
|
|
59
|
+
max-width: 100%;
|
|
60
|
+
margin: 0 auto;
|
|
61
|
+
text-align: center;
|
|
62
|
+
border-inline: 1px solid var(--border);
|
|
63
|
+
min-height: 100svh;
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
h1,
|
|
70
|
+
h2 {
|
|
71
|
+
font-family: var(--heading);
|
|
72
|
+
font-weight: 500;
|
|
73
|
+
color: var(--text-h);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
h1 {
|
|
77
|
+
font-size: 56px;
|
|
78
|
+
letter-spacing: -1.68px;
|
|
79
|
+
margin: 32px 0;
|
|
80
|
+
@media (max-width: 1024px) {
|
|
81
|
+
font-size: 36px;
|
|
82
|
+
margin: 20px 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
h2 {
|
|
86
|
+
font-size: 24px;
|
|
87
|
+
line-height: 118%;
|
|
88
|
+
letter-spacing: -0.24px;
|
|
89
|
+
margin: 0 0 8px;
|
|
90
|
+
@media (max-width: 1024px) {
|
|
91
|
+
font-size: 20px;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
p {
|
|
95
|
+
margin: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
code,
|
|
99
|
+
.counter {
|
|
100
|
+
font-family: var(--mono);
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
border-radius: 4px;
|
|
103
|
+
color: var(--text-h);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
code {
|
|
107
|
+
font-size: 15px;
|
|
108
|
+
line-height: 135%;
|
|
109
|
+
padding: 4px 8px;
|
|
110
|
+
background: var(--code-bg);
|
|
111
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom'
|
|
4
|
+
import './index.css'
|
|
5
|
+
import App from './App'
|
|
6
|
+
|
|
7
|
+
createRoot(document.getElementById('root')!).render(
|
|
8
|
+
<StrictMode>
|
|
9
|
+
<BrowserRouter>
|
|
10
|
+
<App />
|
|
11
|
+
</BrowserRouter>
|
|
12
|
+
</StrictMode>
|
|
13
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Card, Row, Col, Statistic } from 'antd';
|
|
3
|
+
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
|
|
4
|
+
|
|
5
|
+
const Dashboard: React.FC = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div>
|
|
8
|
+
<h2>Dashboard</h2>
|
|
9
|
+
<Row gutter={16}>
|
|
10
|
+
<Col span={12}>
|
|
11
|
+
<Card>
|
|
12
|
+
<Statistic
|
|
13
|
+
title="Active Users"
|
|
14
|
+
value={112893}
|
|
15
|
+
precision={2}
|
|
16
|
+
valueStyle={{ color: '#3f8600' }}
|
|
17
|
+
prefix={<ArrowUpOutlined />}
|
|
18
|
+
suffix="%"
|
|
19
|
+
/>
|
|
20
|
+
</Card>
|
|
21
|
+
</Col>
|
|
22
|
+
<Col span={12}>
|
|
23
|
+
<Card>
|
|
24
|
+
<Statistic
|
|
25
|
+
title="Idle Users"
|
|
26
|
+
value={9.3}
|
|
27
|
+
precision={2}
|
|
28
|
+
valueStyle={{ color: '#cf1322' }}
|
|
29
|
+
prefix={<ArrowDownOutlined />}
|
|
30
|
+
suffix="%"
|
|
31
|
+
/>
|
|
32
|
+
</Card>
|
|
33
|
+
</Col>
|
|
34
|
+
</Row>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default Dashboard;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Table, Tag, Space } from 'antd';
|
|
3
|
+
import type { ColumnsType } from 'antd/es/table';
|
|
4
|
+
|
|
5
|
+
interface DataType {
|
|
6
|
+
key: string;
|
|
7
|
+
name: string;
|
|
8
|
+
age: number;
|
|
9
|
+
address: string;
|
|
10
|
+
tags: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const columns: ColumnsType<DataType> = [
|
|
14
|
+
{
|
|
15
|
+
title: 'Name',
|
|
16
|
+
dataIndex: 'name',
|
|
17
|
+
key: 'name',
|
|
18
|
+
render: (text) => <a>{text}</a>,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
title: 'Age',
|
|
22
|
+
dataIndex: 'age',
|
|
23
|
+
key: 'age',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: 'Address',
|
|
27
|
+
dataIndex: 'address',
|
|
28
|
+
key: 'address',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: 'Tags',
|
|
32
|
+
key: 'tags',
|
|
33
|
+
dataIndex: 'tags',
|
|
34
|
+
render: (_, { tags }) => (
|
|
35
|
+
<>
|
|
36
|
+
{tags.map((tag) => {
|
|
37
|
+
let color = tag.length > 5 ? 'geekblue' : 'green';
|
|
38
|
+
if (tag === 'loser') {
|
|
39
|
+
color = 'volcano';
|
|
40
|
+
}
|
|
41
|
+
return (
|
|
42
|
+
<Tag color={color} key={tag}>
|
|
43
|
+
{tag.toUpperCase()}
|
|
44
|
+
</Tag>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
</>
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
title: 'Action',
|
|
52
|
+
key: 'action',
|
|
53
|
+
render: (_, record) => (
|
|
54
|
+
<Space size="middle">
|
|
55
|
+
<a>Invite {record.name}</a>
|
|
56
|
+
<a>Delete</a>
|
|
57
|
+
</Space>
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const data: DataType[] = [
|
|
63
|
+
{
|
|
64
|
+
key: '1',
|
|
65
|
+
name: 'John Brown',
|
|
66
|
+
age: 32,
|
|
67
|
+
address: 'New York No. 1 Lake Park',
|
|
68
|
+
tags: ['nice', 'developer'],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: '2',
|
|
72
|
+
name: 'Jim Green',
|
|
73
|
+
age: 42,
|
|
74
|
+
address: 'London No. 1 Lake Park',
|
|
75
|
+
tags: ['loser'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: '3',
|
|
79
|
+
name: 'Joe Black',
|
|
80
|
+
age: 32,
|
|
81
|
+
address: 'Sidney No. 1 Lake Park',
|
|
82
|
+
tags: ['cool', 'teacher'],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const Users: React.FC = () => {
|
|
87
|
+
return (
|
|
88
|
+
<div>
|
|
89
|
+
<h2>Users Management</h2>
|
|
90
|
+
<Table columns={columns} dataSource={data} />
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default Users;
|