├── src ├── vite-env.d.ts ├── App.css ├── assets │ ├── background.png │ └── logo.svg ├── pages │ ├── dashboard │ │ └── Dashboard.tsx │ ├── login │ │ ├── data.d.ts │ │ ├── service.tsx │ │ └── Login.tsx │ ├── settings │ │ ├── role │ │ │ ├── data.d.ts │ │ │ ├── service.tsx │ │ │ ├── components │ │ │ │ ├── CreateForm.tsx │ │ │ │ ├── UpdateForm.tsx │ │ │ │ └── PermissionForm.tsx │ │ │ └── Role.tsx │ │ ├── group │ │ │ ├── data.d.ts │ │ │ ├── service.tsx │ │ │ ├── components │ │ │ │ ├── UpdateForm.tsx │ │ │ │ └── CreateForm.tsx │ │ │ └── Group.tsx │ │ ├── user │ │ │ ├── data.d.ts │ │ │ ├── service.tsx │ │ │ ├── components │ │ │ │ ├── UpdateForm.tsx │ │ │ │ └── CreateForm.tsx │ │ │ └── User.tsx │ │ └── menu │ │ │ ├── data.d.ts │ │ │ ├── service.tsx │ │ │ ├── components │ │ │ ├── UpdateForm.tsx │ │ │ └── CreateForm.tsx │ │ │ └── Menu.tsx │ └── 404.tsx ├── layouts │ ├── service.tsx │ ├── components │ │ └── LogoutModal.tsx │ └── BaseLayout.tsx ├── main.tsx ├── App.tsx ├── utils │ ├── auth.tsx │ ├── icons.tsx │ └── request.tsx ├── index.css └── routers │ └── Routers.tsx ├── .env.development ├── .env.production ├── tsconfig.json ├── .dockerignore ├── vite.config.ts ├── index.html ├── Dockerfile ├── .gitignore ├── nginx.conf ├── tsconfig.node.json ├── tsconfig.app.json ├── package.json ├── LICENSE ├── .github └── workflows │ └── docker-publish.yml ├── public └── icon.svg └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | } -------------------------------------------------------------------------------- /src/assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basefas/react-antd-admin/HEAD/src/assets/background.png -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # use in dev mode 2 | VITE_API_HOST=http://localhost 3 | VITE_API_PORT=8088 4 | VITE_API_TIMEOUT=5000 5 | VITE_PLATFORM_NAME=React Antd Admin -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # use in build mode 2 | VITE_API_HOST=http://localhost 3 | VITE_API_PORT=8086 4 | VITE_API_TIMEOUT=5000 5 | VITE_PLATFORM_NAME=React Antd Admin -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | 4 | .DS_Store 5 | .idea 6 | .vscode 7 | 8 | .git 9 | .gitignore 10 | .dockerignore 11 | .editorconfig 12 | Dockerfile 13 | .local -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/pages/dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Card } from "antd"; 3 | 4 | const Dashboard: FC = () => ( 5 | 6 | Dashboard~ 7 | 8 | ); 9 | 10 | export default Dashboard; 11 | -------------------------------------------------------------------------------- /src/pages/login/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface UserLogIn { 2 | username: string; 3 | password: string; 4 | } 5 | 6 | export interface UserLogInInfo { 7 | id: number; 8 | username: string; 9 | token: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/settings/role/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface RoleListItem { 2 | id: number; 3 | name: string; 4 | } 5 | 6 | export interface RoleCreateInfo { 7 | name: string; 8 | } 9 | 10 | export interface RoleUpdateInfo { 11 | name: string; 12 | } 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Antd Admin 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /src/pages/login/service.tsx: -------------------------------------------------------------------------------- 1 | import { post, ResponseData } from "../../utils/request"; 2 | import { UserLogIn, UserLogInInfo } from "./data"; 3 | 4 | export async function login(user: UserLogIn): Promise> { 5 | return await post('/api/v1/login', user) 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from 'antd'; 2 | import { FC } from 'react'; 3 | 4 | const NoFoundPage: FC = () => ( 5 | 10 | ); 11 | export default NoFoundPage; 12 | -------------------------------------------------------------------------------- /src/layouts/service.tsx: -------------------------------------------------------------------------------- 1 | import { get, ResponseData } from "../utils/request"; 2 | import { MenuDataItem } from "@ant-design/pro-layout"; 3 | 4 | export async function systemMenuList(): Promise> { 5 | return await get('/api/v1/menus', {type: "system"}) 6 | } 7 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/pages/settings/group/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface GroupListItem { 2 | id: number; 3 | name: string; 4 | role_id?: number; 5 | role_name?: string; 6 | } 7 | 8 | export interface GroupCreateInfo { 9 | name: string; 10 | role_id: number; 11 | } 12 | 13 | export interface GroupUpdateInfo { 14 | name?: string; 15 | role_id?: number; 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine AS builder 2 | WORKDIR /app 3 | COPY package.json package-lock.json ./ 4 | RUN npm install 5 | COPY ./ ./ 6 | RUN npm run build 7 | 8 | FROM nginx:stable-alpine 9 | COPY --from=builder /app/dist/ /usr/share/nginx/html 10 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf 11 | EXPOSE 80 12 | ENTRYPOINT ["nginx", "-g", "daemon off;"] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import "./App.css"; 3 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 4 | import routers from "./routers/Routers"; 5 | 6 | const App: FC = () => { 7 | return ( 8 |
9 | 12 |
13 | ); 14 | }; 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | # gzip config 4 | gzip on; 5 | gzip_min_length 1k; 6 | gzip_comp_level 6; 7 | gzip_types text/plain text/css test/xml text/javascript application/javascript application/x-javascript application/xml application/json; 8 | gzip_vary on; 9 | gzip_proxied any; 10 | gzip_disable "MSIE [1-6]\."; 11 | 12 | root /usr/share/nginx/html; 13 | 14 | location / { 15 | try_files $uri $uri/ /index.html; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/settings/user/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface UserListItem { 2 | id: number; 3 | username: string; 4 | email: string; 5 | group_id: number; 6 | group_name: string; 7 | role_id: number; 8 | role_name: string; 9 | status: number; 10 | } 11 | 12 | export interface UserCreateInfo { 13 | username: string; 14 | email: string; 15 | group_id: number; 16 | role_id: number; 17 | } 18 | 19 | export interface UserUpdateInfo { 20 | id?: number; 21 | username?: string; 22 | email?: string; 23 | group_id?: number; 24 | role_id?: number; 25 | status?: number; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/auth.tsx: -------------------------------------------------------------------------------- 1 | export function getToken() { 2 | return localStorage.getItem('token') || '' 3 | } 4 | 5 | export function setToken(token: string) { 6 | localStorage.setItem('token', token) 7 | } 8 | 9 | export function loggedIn(): boolean { 10 | return !!localStorage.getItem('token'); 11 | } 12 | 13 | export function deleteToken() { 14 | localStorage.removeItem('token') 15 | localStorage.removeItem('username') 16 | } 17 | 18 | export function getCurrentUser() { 19 | return localStorage.getItem('username') 20 | } 21 | 22 | export function setCurrentUser(username: string) { 23 | localStorage.setItem('username', username) 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/icons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ApartmentOutlined, 3 | ContactsOutlined, 4 | ControlOutlined, 5 | HomeOutlined, 6 | LockOutlined, 7 | ProjectOutlined, 8 | SettingOutlined, 9 | TeamOutlined, 10 | UserOutlined, 11 | } from "@ant-design/icons"; 12 | 13 | 14 | export const menuIcons: any = { 15 | HomeOutlined: , 16 | ProjectOutlined: , 17 | SettingOutlined: , 18 | LockOutlined: , 19 | UserOutlined: , 20 | ControlOutlined: , 21 | ContactsOutlined: , 22 | ApartmentOutlined: , 23 | TeamOutlined: , 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/settings/menu/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface MenuListItem { 2 | id: number; 3 | name: string; 4 | path: string; 5 | type: number; 6 | method: string; 7 | icon: string; 8 | parent_id: number; 9 | order_id: number; 10 | children: MenuListItem[]; 11 | funs: MenuListItem[]; 12 | } 13 | 14 | export interface MenuCreateInfo { 15 | name: string; 16 | path: string; 17 | type: number; 18 | method: string; 19 | icon: string; 20 | parent_id: number; 21 | order_id: number; 22 | } 23 | 24 | export interface MenuUpdateInfo { 25 | name?: string; 26 | path?: string; 27 | type?: number; 28 | method?: string; 29 | icon?: string; 30 | parent_id?: number; 31 | order_id?: number; 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/settings/user/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { UserCreateInfo, UserListItem, UserUpdateInfo } from "./data"; 3 | 4 | export async function userList(): Promise> { 5 | return await get('/api/v1/users') 6 | } 7 | 8 | export async function createUser(user: UserCreateInfo): Promise> { 9 | return await post<{}>('/api/v1/users', user) 10 | } 11 | 12 | export async function updateUser(id: number, user: UserUpdateInfo): Promise> { 13 | return await put<{}>('/api/v1/users/' + id, user) 14 | } 15 | 16 | export async function deleteUser(id: number): Promise> { 17 | return await del<{}>('/api/v1/users/' + id) 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/settings/group/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { GroupCreateInfo, GroupListItem, GroupUpdateInfo } from "./data"; 3 | 4 | export async function groupList(): Promise> { 5 | return await get('/api/v1/groups') 6 | } 7 | 8 | export async function createGroup(group: GroupCreateInfo): Promise> { 9 | return await post<{}>('/api/v1/groups', group) 10 | } 11 | 12 | export async function updateGroup(id: number, group: GroupUpdateInfo): Promise> { 13 | return await put<{}>('/api/v1/groups/' + id, group) 14 | } 15 | 16 | export async function deleteGroup(id: number): Promise> { 17 | return await del<{}>('/api/v1/groups/' + id) 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/settings/menu/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { MenuCreateInfo, MenuListItem, MenuUpdateInfo } from "./data"; 3 | 4 | export async function menuList(): Promise> { 5 | return await get('/api/v1/menus?type=tree') 6 | } 7 | 8 | export async function createMenu(menu: MenuCreateInfo): Promise> { 9 | return await post<{}>('/api/v1/menus', menu) 10 | } 11 | 12 | export async function updateMenu(id: number, menu: MenuUpdateInfo): Promise> { 13 | return await put<{}>('/api/v1/menus/' + id, menu) 14 | } 15 | 16 | export async function deleteMenu(id: number): Promise> { 17 | return await del<{}>('/api/v1/menus/' + id + '?type=tree') 18 | } 19 | 20 | export async function menuGet(id: number): Promise> { 21 | return await get('/api/v1/menus/' + id + '?type=tree') 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-antd-admin", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@ant-design/pro-layout": "7.21.2", 14 | "@ant-design/use-emotion-css": "1.0.4", 15 | "antd": "5.22.7", 16 | "axios": "1.7.9", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1", 19 | "react-router-dom": "6.28.1" 20 | }, 21 | "devDependencies": { 22 | "@eslint/js": "^9.17.0", 23 | "@types/react": "^18.3.18", 24 | "@types/react-dom": "^18.3.5", 25 | "@vitejs/plugin-react": "^4.3.4", 26 | "eslint": "^9.17.0", 27 | "eslint-plugin-react-hooks": "^5.0.0", 28 | "eslint-plugin-react-refresh": "^0.4.16", 29 | "globals": "^15.14.0", 30 | "typescript": "~5.6.2", 31 | "typescript-eslint": "^8.18.2", 32 | "vite": "^6.0.9" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */ 2 | html, 3 | body, 4 | p, 5 | ol, 6 | ul, 7 | li, 8 | dl, 9 | dt, 10 | dd, 11 | blockquote, 12 | figure, 13 | fieldset, 14 | legend, 15 | textarea, 16 | pre, 17 | iframe, 18 | hr, 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6 { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | h1, 30 | h2, 31 | h3, 32 | h4, 33 | h5, 34 | h6 { 35 | font-size: 100%; 36 | font-weight: normal; 37 | } 38 | 39 | ul { 40 | list-style: none; 41 | } 42 | 43 | button, 44 | input, 45 | select { 46 | margin: 0; 47 | } 48 | 49 | html { 50 | box-sizing: border-box; 51 | } 52 | 53 | *, *::before, *::after { 54 | box-sizing: inherit; 55 | } 56 | 57 | img, 58 | video { 59 | height: auto; 60 | max-width: 100%; 61 | } 62 | 63 | iframe { 64 | border: 0; 65 | } 66 | 67 | table { 68 | border-collapse: collapse; 69 | border-spacing: 0; 70 | } 71 | 72 | td, 73 | th { 74 | padding: 0; 75 | } 76 | 77 | #root { 78 | height: 100%; 79 | } 80 | -------------------------------------------------------------------------------- /src/pages/settings/role/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { RoleCreateInfo, RoleListItem, RoleUpdateInfo } from "./data"; 3 | 4 | export async function roleList(): Promise> { 5 | return await get('/api/v1/roles') 6 | } 7 | 8 | export async function createRole(role: RoleCreateInfo): Promise> { 9 | return await post<{}>('/api/v1/roles', role) 10 | } 11 | 12 | export async function updateRole(id: number, role: RoleUpdateInfo): Promise> { 13 | return await put<{}>('/api/v1/roles/' + id, role) 14 | } 15 | 16 | export async function deleteRole(id: number): Promise> { 17 | return await del<{}>('/api/v1/roles/' + id) 18 | } 19 | 20 | export async function roleMenus(id: number): Promise> { 21 | return await get('/api/v1/roles/' + id + '/menus') 22 | } 23 | 24 | export async function updateRoleMenus(id: number, menus: number[]): Promise> { 25 | return await put<{}>('/api/v1/roles/' + id + '/menus', menus) 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 basefas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/layouts/components/LogoutModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useNavigate } from "react-router-dom"; 3 | import { message, Modal } from "antd"; 4 | import { deleteToken } from "../../utils/auth"; 5 | 6 | interface LogoutModalProps { 7 | open: boolean; 8 | changeLogoutModalVisible: Function; 9 | } 10 | 11 | const LogoutModal: React.FC = (props) => { 12 | const navigate = useNavigate(); 13 | const {open, changeLogoutModalVisible} = props 14 | 15 | useEffect(() => { 16 | changeLogoutModalVisible(open) 17 | }, [open, changeLogoutModalVisible]); 18 | 19 | const okHandle = () => { 20 | deleteToken(); 21 | message.success("退出登录成功").then(); 22 | changeLogoutModalVisible(false) 23 | navigate('/login') 24 | }; 25 | const cancelHandle = () => { 26 | changeLogoutModalVisible(false) 27 | }; 28 | 29 | 30 | return ( 31 | 39 |
确定退出登录?
40 |
41 | ) 42 | 43 | }; 44 | export default LogoutModal; 45 | -------------------------------------------------------------------------------- /src/pages/settings/role/components/CreateForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Form, Input, Modal } from 'antd'; 3 | import { RoleCreateInfo } from "../data"; 4 | 5 | const {Item} = Form; 6 | 7 | interface CreateFormProps { 8 | open: boolean; 9 | onOk: (role: RoleCreateInfo) => void; 10 | onCancel: () => void; 11 | } 12 | 13 | const CreateForm: FC = (props) => { 14 | const {open, onOk, onCancel} = props 15 | const [form] = Form.useForm(); 16 | 17 | const ok = () => { 18 | form 19 | .validateFields() 20 | .then(values => { 21 | form.resetFields(); 22 | onOk(values); 23 | }) 24 | .catch(info => { 25 | console.log('参数校验失败:', info); 26 | }); 27 | } 28 | 29 | const form_layout = { 30 | labelCol: {span: 4}, 31 | }; 32 | 33 | return ( 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ) 48 | } 49 | export default CreateForm; 50 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: { } 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | 24 | # Workaround: https://github.com/docker/build-push-action/issues/461 25 | - name: Setup Docker buildx 26 | uses: docker/setup-buildx-action@v2 27 | 28 | - name: Log into registry ${{ env.REGISTRY }} 29 | uses: docker/login-action@v2 30 | with: 31 | registry: ${{ env.REGISTRY }} 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GHCR_PAT }} 34 | 35 | - name: Extract Docker metadata 36 | id: meta 37 | uses: docker/metadata-action@v4 38 | with: 39 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 40 | 41 | - name: Build and push Docker image 42 | id: build-and-push 43 | uses: docker/build-push-action@v4 44 | with: 45 | context: . 46 | push: true 47 | tags: ${{ steps.meta.outputs.tags }} 48 | labels: ${{ steps.meta.outputs.labels }} 49 | -------------------------------------------------------------------------------- /src/pages/settings/role/components/UpdateForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Form, Input, Modal } from 'antd'; 3 | import { RoleListItem, RoleUpdateInfo } from "../data"; 4 | 5 | const {Item} = Form; 6 | 7 | interface UpdateFormProps { 8 | open: boolean; 9 | role: RoleListItem; 10 | onOk: (id: number, user: RoleUpdateInfo) => void; 11 | onCancel: () => void; 12 | } 13 | 14 | const UpdateForm: FC = (props) => { 15 | const {open, role, onOk, onCancel} = props 16 | const [form] = Form.useForm(); 17 | 18 | const ok = () => { 19 | form 20 | .validateFields() 21 | .then(values => { 22 | form.resetFields(); 23 | onOk(role.id, values); 24 | }) 25 | .catch(info => { 26 | console.log('参数校验失败:', info); 27 | }); 28 | } 29 | 30 | const form_layout = { 31 | labelCol: {span: 4}, 32 | }; 33 | 34 | return ( 35 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | export default UpdateForm; 57 | -------------------------------------------------------------------------------- /src/routers/Routers.tsx: -------------------------------------------------------------------------------- 1 | import {Navigate, redirect} from 'react-router-dom' 2 | import {loggedIn} from "../utils/auth"; 3 | import BaseLayout from "../layouts/BaseLayout"; 4 | import NoFoundPage from "../pages/404"; 5 | import Login from "../pages/login/Login"; 6 | import Menu from "../pages/settings/menu/Menu"; 7 | import Role from "../pages/settings/role/Role"; 8 | import Group from "../pages/settings/group/Group"; 9 | import User from "../pages/settings/user/User"; 10 | import Dashboard from "../pages/dashboard/Dashboard"; 11 | 12 | 13 | const loaderBase = async () => { 14 | if (!loggedIn()) { 15 | return redirect("/login"); 16 | } 17 | return null; 18 | }; 19 | 20 | const loaderLogin = async () => { 21 | if (loggedIn()) { 22 | return redirect("/dashboard"); 23 | } 24 | return null; 25 | }; 26 | 27 | export default [ 28 | { 29 | path: "/login", 30 | element: , 31 | loader: loaderLogin, 32 | }, 33 | { 34 | path: "/", 35 | element: , 36 | loader: loaderBase, 37 | children: [ 38 | { 39 | errorElement: , 40 | children: [ 41 | { 42 | index: true, 43 | element: 44 | }, 45 | {path: "/dashboard", element: }, 46 | {path: "/settings/users", element: }, 47 | {path: "/settings/groups", element: }, 48 | {path: "/settings/roles", element: }, 49 | {path: "/settings/menus", element: }, 50 | {path: "*", element: }, 51 | ], 52 | } 53 | ] 54 | }, 55 | ] -------------------------------------------------------------------------------- /src/utils/request.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { deleteToken, getToken } from "./auth"; 3 | import { message, Modal } from "antd"; 4 | 5 | const {confirm} = Modal; 6 | 7 | export interface ResponseData { 8 | code: number; 9 | data: T; 10 | message: string; 11 | } 12 | 13 | const instance = axios.create({ 14 | baseURL: import.meta.env.VITE_API_HOST + ':' + import.meta.env.VITE_API_PORT || 'http://localhost:8086', 15 | timeout: import.meta.env.VITE_API_TIMEOUT || 5000, 16 | }); 17 | 18 | instance.interceptors.request.use( 19 | function (config) { 20 | config.headers["token"] = getToken(); 21 | return config; 22 | }, 23 | function (error) { 24 | return Promise.reject(error); 25 | }); 26 | 27 | instance.interceptors.response.use( 28 | response => { 29 | if (response.data.code === -2) { 30 | confirm({ 31 | title: ' Token 失效, 请重新登录!', 32 | onOk() { 33 | window.location.href = '/login' 34 | deleteToken() 35 | }, 36 | }); 37 | } 38 | return Promise.resolve(response.data); 39 | }, 40 | error => { 41 | if (error.response) { 42 | message.error(error.response.status + ': ' + error.response.statusText).then() 43 | } else { 44 | message.error('服务器错误: ' + error.message).then() 45 | } 46 | return Promise.reject(error); 47 | }); 48 | 49 | export function get(url: string, params?: any): Promise> { 50 | return instance.get(url, {params}) 51 | } 52 | 53 | export function post(url: string, data?: any, params?: any): Promise> { 54 | return instance.post(url, data, {params}) 55 | } 56 | 57 | export function put(url: string, data?: any, params?: any): Promise> { 58 | return instance.put(url, data, {params}) 59 | } 60 | 61 | export function del(url: string, params?: any): Promise> { 62 | return instance.delete(url, {params}) 63 | } 64 | -------------------------------------------------------------------------------- /src/pages/settings/group/components/UpdateForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from 'react'; 2 | import { Form, Input, Modal, Select } from 'antd'; 3 | import { GroupListItem, GroupUpdateInfo } from "../data"; 4 | import { RoleListItem } from "../../role/data"; 5 | import { roleList } from "../../role/service"; 6 | 7 | const {Item} = Form; 8 | const {Option} = Select; 9 | 10 | interface UpdateFormProps { 11 | open: boolean; 12 | group: GroupListItem; 13 | onOk: (id: number, group: GroupUpdateInfo) => void; 14 | onCancel: () => void; 15 | } 16 | 17 | const UpdateForm: FC = (props) => { 18 | const {open, group, onOk, onCancel} = props 19 | const [roles, setRoles] = useState([]) 20 | const [form] = Form.useForm(); 21 | 22 | const getRoleList = async () => { 23 | const result = await roleList(); 24 | setRoles(result.data); 25 | }; 26 | 27 | useEffect(() => { 28 | getRoleList().then() 29 | }, []); 30 | 31 | const ok = () => { 32 | form 33 | .validateFields() 34 | .then(values => { 35 | form.resetFields(); 36 | onOk(group.id, values); 37 | }) 38 | .catch(info => { 39 | console.log('参数校验失败:', info); 40 | }); 41 | } 42 | 43 | const form_layout = { 44 | labelCol: {span: 4}, 45 | }; 46 | 47 | return ( 48 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | ) 69 | } 70 | export default UpdateForm; 71 | -------------------------------------------------------------------------------- /src/pages/settings/group/components/CreateForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from 'react'; 2 | import { Form, Input, Modal, Select } from 'antd'; 3 | import { roleList } from "../../role/service"; 4 | import { GroupCreateInfo } from "../data"; 5 | import { RoleListItem } from "../../role/data"; 6 | 7 | const {Item} = Form; 8 | const {Option} = Select; 9 | 10 | interface CreateFormProps { 11 | open: boolean; 12 | onOk: (user: GroupCreateInfo) => void; 13 | onCancel: () => void; 14 | } 15 | 16 | const CreateForm: FC = (props) => { 17 | const {open, onOk, onCancel} = props 18 | const [roles, setRoles] = useState([]) 19 | const [form] = Form.useForm(); 20 | 21 | const getRoleList = async () => { 22 | const result = await roleList(); 23 | setRoles(result.data); 24 | }; 25 | 26 | useEffect(() => { 27 | getRoleList().then() 28 | }, []); 29 | 30 | const ok = () => { 31 | form 32 | .validateFields() 33 | .then(values => { 34 | form.resetFields(); 35 | onOk(values); 36 | }) 37 | .catch(info => { 38 | console.log('参数校验失败:', info); 39 | }); 40 | } 41 | 42 | const form_layout = { 43 | labelCol: {span: 4}, 44 | }; 45 | 46 | return ( 47 | 54 | 55 | 60 | 61 | 62 | 63 | 66 | 71 | 72 | 73 | 74 | ) 75 | } 76 | export default CreateForm; 77 | -------------------------------------------------------------------------------- /src/pages/login/Login.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { useEmotionCss } from "@ant-design/use-emotion-css"; 3 | 4 | import { Button, Card, Form, Input, message } from 'antd'; 5 | import { LockOutlined, UserOutlined } from '@ant-design/icons'; 6 | import { useNavigate } from "react-router-dom"; 7 | import { setCurrentUser, setToken } from "../../utils/auth"; 8 | import { UserLogIn } from "./data"; 9 | import { login } from "./service"; 10 | import bg from "../../assets/background.png" 11 | 12 | const {Item} = Form; 13 | 14 | const Login: FC = () => { 15 | const navigate = useNavigate(); 16 | 17 | const containerClassName = useEmotionCss(() => { 18 | return { 19 | display: 'flex', 20 | flexDirection: 'column', 21 | height: '100vh', 22 | overflow: 'auto', 23 | backgroundImage: `url(${bg})`, 24 | backgroundSize: '100% 100%', 25 | }; 26 | }); 27 | 28 | const onFinish = (user: UserLogIn) => { 29 | Login(user).then() 30 | }; 31 | 32 | const Login = async (user: UserLogIn) => { 33 | const result = await login(user) 34 | if (result.code === 0) { 35 | message.success("登录成功") 36 | setToken(result.data.token); 37 | setCurrentUser(result.data.username); 38 | navigate("/dashboard"); 39 | } else { 40 | console.log("登录失败") 41 | message.error("登录失败: " + result.message); 42 | } 43 | }; 44 | 45 | return ( 46 |
47 | 50 |
56 | 63 | } placeholder="用户名" /> 64 | 65 | 70 | } 72 | type="password" 73 | placeholder="密码" 74 | /> 75 | 76 | 77 | 80 | 81 |
82 |
83 |
84 | ); 85 | }; 86 | export default Login; 87 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![LICENSE](https://img.shields.io/github/license/basefas/react-antd-admin.svg?style=flat-square)](/LICENSE) 3 | [![Releases](https://img.shields.io/github/release/basefas/react-antd-admin/all.svg?style=flat-square)](https://github.com/basefas/react-antd-admin/releases) 4 | ![GitHub Repo stars](https://img.shields.io/github/stars/basefas/react-antd-admin?style=social) 5 | 6 | 7 |
8 |
9 | 10 | Logo 11 | 12 | 13 |

react-antd-admin

14 | 15 |

16 | 一个使用 React 和 Antd 开发管理系统 17 |
18 |

19 |
20 | 21 | 22 | 23 | ## 简介 24 | 25 | react-antd-admin 使用 vite 与 antd v5 开发,包含常用后台使用的基本模块,依赖项少,结构简单,同时提供完整功能的后端程序,可快速用于二次开发及功能扩展。 26 | 27 | 28 | | | url | introduction | 29 | |-----------|---------------------------------------------|--------------------------------------------| 30 | | backend | https://github.com/basefas/admin-go | 使用 Go & Gin 开发的后台管理系统后端 | 31 | | frontend | https://github.com/basefas/react-antd-admin | 使用 react & vite & antd 开发的后台管理系统前端| 32 | 33 | 34 | ## 页面截图 35 | 36 | ### 登录页面 37 | 38 | ![Screen Shot](https://github.com/basefas/files/blob/main/login.png) 39 | 40 | ### 用户管理 41 | 42 | ![Screen Shot](https://github.com/basefas/files/blob/main/user.png) 43 | 44 | ### 分组管理 45 | 46 | ![Screen Shot](https://github.com/basefas/files/blob/main/group.png) 47 | 48 | ### 菜单管理 49 | 50 | ![Screen Shot](https://github.com/basefas/files/blob/main/menu.png) 51 | 52 | ### 角色及权限管理 53 | 54 | ![Screen Shot](https://github.com/basefas/files/blob/main/permission.png) 55 | 56 | 57 | 58 | 59 | ## 快速开始 60 | 61 | 1. 克隆项目到本地 62 | 63 | ``` 64 | git clone https://github.com/basefas/react-antd-admin 65 | ``` 66 | 67 | 2. 安装依赖 68 | 69 | ``` 70 | npm install 71 | ``` 72 | 73 | 3. 运行 74 | 75 | ``` 76 | npm run dev 77 | ``` 78 | 79 | 80 | 81 | ## Build 82 | 83 | 1. 本地编译 84 | 85 | ``` 86 | npm run build 87 | ``` 88 | 89 | 2. 本地查看编译结果 90 | 91 | ``` 92 | npm run preview 93 | ``` 94 | 95 | 96 | 2. 使用 docker 编译并打包 docker 镜像 97 | 98 | ``` 99 | docker build -f Dockerfile -t react-antd-admin: . 100 | ``` 101 | 102 | > 注:将 `` 替换为你需要的版本号 103 | 104 | 3. 修改配置 105 | 106 | 本地开发修改 `env.development` 文件修改配置 107 | 打包需在编译前修改 `env.production` 文件修改配置 108 | 可配置项如下 109 | 110 | ``` 111 | # API 的 URL 112 | VITE_API_HOST=http://localhost 113 | # API 的 PORT 114 | VITE_API_PORT=8086 115 | # API 的 超时时间 116 | VITE_API_TIMEOUT=5000 117 | # 用于显示的平台名称 118 | VITE_PLATFORM_NAME=React Antd Admin 119 | ``` 120 | 121 | 122 | 123 | ## 版权声明 124 | 125 | react-antd-admin 基于 MIT 协议, 详情请参考 [license](LICENSE)。 126 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | 21 | -------------------------------------------------------------------------------- /src/pages/settings/user/components/UpdateForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from 'react'; 2 | import { Form, Input, Modal, Select } from 'antd'; 3 | import { groupList } from "../../group/service"; 4 | import { roleList } from "../../role/service"; 5 | import { GroupListItem } from "../../group/data"; 6 | import { RoleListItem } from "../../role/data"; 7 | import { UserListItem, UserUpdateInfo } from "../data"; 8 | 9 | const {Item} = Form; 10 | const {Option} = Select; 11 | 12 | interface UpdateFormProps { 13 | open: boolean; 14 | user: UserListItem; 15 | onOk: (id: number, user: UserUpdateInfo) => void; 16 | onCancel: () => void; 17 | } 18 | 19 | const UpdateForm: FC = (props) => { 20 | const {open, user, onOk, onCancel} = props 21 | const [groups, setGroups] = useState([]) 22 | const [roles, setRoles] = useState([]) 23 | const [form] = Form.useForm(); 24 | 25 | const getGroupList = async () => { 26 | const result = await groupList(); 27 | setGroups(result.data); 28 | }; 29 | 30 | const getRoleList = async () => { 31 | const result = await roleList(); 32 | setRoles(result.data); 33 | }; 34 | 35 | useEffect(() => { 36 | getGroupList().then() 37 | getRoleList().then() 38 | }, []); 39 | 40 | const ok = () => { 41 | form 42 | .validateFields() 43 | .then(values => { 44 | form.resetFields(); 45 | onOk(user.id, values); 46 | }) 47 | .catch(info => { 48 | console.log('参数校验失败:', info); 49 | }); 50 | } 51 | 52 | const form_layout = { 53 | labelCol: {span: 4}, 54 | }; 55 | 56 | return ( 57 | 64 | 65 | 71 | 72 | 73 | 74 | 80 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | 96 | 97 | 98 | 99 | ) 100 | } 101 | export default UpdateForm; 102 | -------------------------------------------------------------------------------- /src/pages/settings/user/components/CreateForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from 'react'; 2 | import { Form, Input, Modal, Select } from 'antd'; 3 | import { groupList } from "../../group/service"; 4 | import { roleList } from "../../role/service"; 5 | import { GroupListItem } from "../../group/data"; 6 | import { RoleListItem } from "../../role/data"; 7 | import { UserCreateInfo } from "../data"; 8 | 9 | const {Item} = Form; 10 | const {Option} = Select; 11 | 12 | interface CreateFormProps { 13 | open: boolean; 14 | onOk: (user: UserCreateInfo) => void; 15 | onCancel: () => void; 16 | } 17 | 18 | const CreateForm: FC = (props) => { 19 | const {open, onOk, onCancel} = props 20 | const [groups, setGroups] = useState([]) 21 | const [roles, setRoles] = useState([]) 22 | const [form] = Form.useForm(); 23 | 24 | const getGroupList = async () => { 25 | const result = await groupList(); 26 | setGroups(result.data); 27 | }; 28 | 29 | const getRoleList = async () => { 30 | const result = await roleList(); 31 | setRoles(result.data); 32 | }; 33 | 34 | useEffect(() => { 35 | getGroupList().then() 36 | getRoleList().then() 37 | }, []); 38 | 39 | const ok = () => { 40 | form 41 | .validateFields() 42 | .then(values => { 43 | form.resetFields(); 44 | onOk(values); 45 | }) 46 | .catch(info => { 47 | console.log('参数校验失败:', info); 48 | }); 49 | } 50 | 51 | const form_layout = { 52 | labelCol: {span: 4}, 53 | }; 54 | 55 | return ( 56 | 63 | 64 | 68 | 69 | 70 | 74 | 75 | 76 | 80 | 81 | 82 | 83 | 88 | 89 | 90 | 95 | 96 | 97 | 98 | ) 99 | } 100 | export default CreateForm; 101 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from "react"; 2 | import ProLayout, { MenuDataItem } from "@ant-design/pro-layout"; 3 | import { Outlet, useLocation, useNavigate } from "react-router-dom"; 4 | import logo from "../assets/logo.svg"; 5 | import { menuIcons } from "../utils/icons"; 6 | import { systemMenuList } from "./service"; 7 | import { Dropdown, MenuProps } from "antd"; 8 | import { LogoutOutlined } from "@ant-design/icons"; 9 | import { getCurrentUser } from "../utils/auth"; 10 | import LogoutModal from "./components/LogoutModal"; 11 | 12 | const BaseLayout: FC = () => { 13 | const navigate = useNavigate(); 14 | const location = useLocation(); 15 | const [menuData, setMenuData] = useState([]); 16 | const [pathname, setPathname] = useState(location.pathname); 17 | const [loading, setLoading] = useState(true); 18 | 19 | const [logoutModalVisible, setLogoutModalVisible] = useState(false); 20 | const [user, setUser] = useState("User"); 21 | 22 | const changeLogoutModalVisible = (status: boolean) => { 23 | setLogoutModalVisible(status) 24 | } 25 | 26 | const loopMenuItem = (menus: MenuDataItem[]): MenuDataItem[] => { 27 | if (menus != null && menus.length > 0) { 28 | return menus.map(({icon, children, locale, ...item}) => ({ 29 | ...item, 30 | icon: icon && menuIcons[icon as string], 31 | children: children && loopMenuItem(children), 32 | })); 33 | } else { 34 | return []; 35 | } 36 | }; 37 | 38 | const fetchData = async () => { 39 | const result = await systemMenuList(); 40 | setMenuData(result.data); 41 | setLoading(false); 42 | }; 43 | 44 | useEffect(() => { 45 | setMenuData([]); 46 | setLoading(true); 47 | fetchData().then(); 48 | }, []); 49 | 50 | useEffect(() => { 51 | let currentUser = getCurrentUser(); 52 | if (currentUser) { 53 | setUser(currentUser); 54 | } 55 | 56 | }, [getCurrentUser()]); 57 | 58 | const items: MenuProps['items'] = [ 59 | { 60 | key: '1', 61 | label: ( 62 |
63 | 64 | 退出登录 65 |
66 | ), 67 | onClick: () => { 68 | setLogoutModalVisible(true) 69 | } 70 | }, 71 | ]; 72 | return ( 73 | loopMenuItem(menuData)} 81 | menuItemRender={(item, dom) => ( 82 |
{ 83 | setPathname(item.path || "/dashboard"); 84 | navigate(item.path || "/dashboard"); 85 | }}> 86 | {dom} 87 |
88 | )} 89 | avatarProps={{ 90 | icon: user.slice(0, 1).toUpperCase(), 91 | size: 'small', 92 | title: user, 93 | render: (_props, dom) => { 94 | return ( 95 | 98 | {dom} 99 | 100 | ); 101 | }, 102 | }} 103 | actionsRender={() => { 104 | return []; 105 | }} 106 | token={{ 107 | sider: { 108 | colorBgMenuItemSelected: '#e6f7ff', 109 | colorTextMenuSelected: '#1890ff', 110 | }, 111 | }} 112 | > 113 | 114 | {logoutModalVisible ? 115 | : null} 118 |
119 | ); 120 | }; 121 | 122 | export default BaseLayout; 123 | -------------------------------------------------------------------------------- /src/pages/settings/menu/components/UpdateForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Form, Input, InputNumber, Modal, Select, TreeSelect } from 'antd'; 3 | import { menuIcons } from "../../../../utils/icons"; 4 | import { MenuListItem, MenuUpdateInfo } from "../data"; 5 | import { DataNode } from "antd/lib/tree"; 6 | import { DownOutlined } from "@ant-design/icons"; 7 | 8 | const {Item} = Form; 9 | const {Option} = Select; 10 | 11 | interface UpdateFormProps { 12 | open: boolean; 13 | menusSelect: DataNode[]; 14 | menu: MenuListItem; 15 | formType: number; 16 | onOk: (id: number, menu: MenuUpdateInfo) => void; 17 | onCancel: () => void; 18 | } 19 | 20 | const UpdateForm: FC = (props) => { 21 | const {open, formType, menu, menusSelect, onOk, onCancel} = props 22 | const [form] = Form.useForm(); 23 | 24 | const ok = () => { 25 | form 26 | .validateFields() 27 | .then(values => { 28 | form.resetFields(); 29 | onOk(menu.id, values); 30 | }) 31 | .catch(info => { 32 | console.log('参数校验失败:', info); 33 | }); 34 | } 35 | 36 | const form_layout = { 37 | labelCol: {span: 4}, 38 | }; 39 | 40 | return ( 41 | 48 | 49 | ({ 54 | validator( value) { 55 | if (!value || getFieldValue('parent_id') !== menu.id) { 56 | return Promise.resolve(); 57 | } 58 | return Promise.reject('上级不能选择自己'); 59 | }, 60 | }),]}> 61 | } 63 | placeholder={"请选择父节点"} 64 | treeData={menusSelect} 65 | /> 66 | 67 | {formType === 1 ? ( 68 | 69 | 73 | 74 | ) : null} 75 | 76 | 77 | 78 | 79 | 80 | 81 | {formType === 1 ? ( 82 | 83 | 88 | 89 | ) : null} 90 | {formType === 1 ? ( 91 | 93 | 94 | 95 | ) : null} 96 | {formType === 2 ? ( 97 | 98 | 105 | 106 | ) : null} 107 | 108 | 109 | ) 110 | } 111 | export default UpdateForm; 112 | -------------------------------------------------------------------------------- /src/pages/settings/menu/components/CreateForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Form, Input, InputNumber, Modal, Select, TreeSelect } from 'antd'; 3 | import { MenuCreateInfo } from "../data"; 4 | import { menuIcons } from "../../../../utils/icons"; 5 | import { DataNode } from "antd/lib/tree"; 6 | import { DownOutlined } from "@ant-design/icons"; 7 | 8 | const {Item} = Form; 9 | const {Option} = Select; 10 | 11 | interface CreateFormProps { 12 | open: boolean; 13 | menusSelect: DataNode[]; 14 | menu: DataNode; 15 | formType: number; 16 | onOk: (menu: MenuCreateInfo) => void; 17 | onCancel: () => void; 18 | } 19 | 20 | const CreateForm: FC = (props) => { 21 | const {open, menusSelect, menu, formType, onOk, onCancel} = props 22 | const [form] = Form.useForm(); 23 | const ok = () => { 24 | form 25 | .validateFields() 26 | .then(values => { 27 | form.resetFields(); 28 | if (formType === 1) { 29 | if (values.menu_type === 1) { 30 | values.method = '-' 31 | } 32 | if (values.menu_type === 2) { 33 | values.method = 'GET' 34 | } 35 | if (values.parent_id === 0) { 36 | values.menu_type = 1 37 | } 38 | } 39 | if (formType === 2) { 40 | values.menu_type = 3 41 | values.icon = '' 42 | } 43 | onOk(values); 44 | }) 45 | .catch(info => { 46 | console.log('参数校验失败:', info); 47 | }); 48 | } 49 | 50 | const form_layout = { 51 | labelCol: {span: 4}, 52 | }; 53 | 54 | return ( 55 | 61 | 64 | 65 | } 67 | placeholder={"请选择父节点"} 68 | treeData={menusSelect} /> 69 | 70 | {formType === 1 ? ( 71 | 72 | 76 | 77 | ) : null} 78 | 79 | 80 | 81 | 82 | 83 | 84 | {formType === 1 ? ( 85 | 86 | 91 | 92 | ) : null} 93 | {formType === 1 ? ( 94 | 96 | 97 | 98 | ) : null} 99 | {formType === 2 ? ( 100 | 101 | 108 | 109 | ) : null} 110 | 111 | 112 | ) 113 | } 114 | export default CreateForm; 115 | -------------------------------------------------------------------------------- /src/pages/settings/group/Group.tsx: -------------------------------------------------------------------------------- 1 | import { FC, Fragment, useEffect, useState } from 'react'; 2 | import { Button, Card, Divider, message, Modal, Table } from "antd"; 3 | import { GroupCreateInfo, GroupListItem, GroupUpdateInfo } from "./data"; 4 | import { createGroup, deleteGroup, groupList, updateGroup } from "./service"; 5 | import { ColumnsType } from "antd/es/table"; 6 | import dayjs from 'dayjs'; 7 | import { ExclamationCircleOutlined, PlusOutlined } from "@ant-design/icons"; 8 | import CreateForm from "./components/CreateForm"; 9 | import UpdateForm from "./components/UpdateForm"; 10 | 11 | const {confirm} = Modal; 12 | 13 | const Group: FC = () => { 14 | const [groups, setGroups] = useState([]) 15 | const [groupUpdate, setGroupUpdate] = useState() 16 | const [loading, setLoading] = useState(false) 17 | const [createFormVisible, setCreateFormVisible] = useState(false) 18 | const [updateFormVisible, setUpdateFormVisible] = useState(false) 19 | const title = '分组'; 20 | 21 | const getGroupList = async () => { 22 | const result = await groupList(); 23 | setGroups(result.data); 24 | setLoading(false); 25 | }; 26 | 27 | useEffect(() => { 28 | setLoading(true); 29 | getGroupList().then(); 30 | }, []); 31 | 32 | const formatTime = (date: any) => { 33 | return date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '' 34 | }; 35 | 36 | const handleCreateGroup = async (group: GroupCreateInfo) => { 37 | const result = await createGroup(group); 38 | if (result.code === 0) { 39 | setCreateFormVisible(false) 40 | getGroupList().then() 41 | } else { 42 | message.error("创建分组失败"); 43 | } 44 | }; 45 | 46 | const handleUpdateGroup = async (id: number, group: GroupUpdateInfo) => { 47 | const result = await updateGroup(id, group); 48 | if (result.code === 0) { 49 | setUpdateFormVisible(false); 50 | getGroupList().then() 51 | } else { 52 | message.error("修改分组失败"); 53 | } 54 | }; 55 | 56 | const handleDeleteGroup = async (id: number) => { 57 | const result = await deleteGroup(id); 58 | if (result.code === 0) { 59 | getGroupList().then() 60 | } else { 61 | message.error("删除分组失败"); 62 | } 63 | }; 64 | 65 | const addGroup = ( 66 | 71 | ) 72 | 73 | function deleteModal(group: GroupListItem) { 74 | confirm({ 75 | title: '确定删除分组 ' + group.name + ' ?', 76 | icon: , 77 | onOk() { 78 | handleDeleteGroup(group.id).then() 79 | }, 80 | }); 81 | } 82 | 83 | const columns: ColumnsType = [ 84 | { 85 | title: 'ID', 86 | dataIndex: 'id', 87 | key: 'id', 88 | align: 'center', 89 | }, 90 | { 91 | title: '分组', 92 | dataIndex: 'name', 93 | key: 'name', 94 | align: 'center', 95 | }, 96 | { 97 | title: '角色', 98 | dataIndex: 'role_name', 99 | key: 'role_name', 100 | align: 'center', 101 | }, 102 | { 103 | title: '创建时间', 104 | dataIndex: 'create_time', 105 | key: 'create_time', 106 | align: 'center', 107 | render: formatTime 108 | }, 109 | { 110 | title: '修改时间', 111 | dataIndex: 'update_time', 112 | key: 'update_time', 113 | align: 'center', 114 | render: formatTime 115 | }, 116 | { 117 | title: '操作', 118 | key: 'action', 119 | align: 'center', 120 | render: (group) => ( 121 | 122 | 128 | 129 | 133 | 134 | ), 135 | }, 136 | ]; 137 | 138 | return ( 139 |
140 | 141 | groups.id} 145 | loading={loading} 146 | pagination={{ 147 | hideOnSinglePage: true, 148 | pageSize: 10 149 | }} 150 | /> 151 | 152 | 153 | {createFormVisible ? 154 | { 158 | setCreateFormVisible(false) 159 | }} 160 | /> : null} 161 | 162 | {updateFormVisible ? 163 | { 168 | setUpdateFormVisible(false) 169 | }} 170 | /> : null} 171 | 172 | ); 173 | } 174 | export default Group; 175 | -------------------------------------------------------------------------------- /src/pages/settings/user/User.tsx: -------------------------------------------------------------------------------- 1 | import { FC, Fragment, useEffect, useState } from 'react'; 2 | import { Badge, Button, Card, Divider, message, Modal, Table } from "antd"; 3 | import { createUser, deleteUser, updateUser, userList } from "./service"; 4 | import { ExclamationCircleOutlined, PlusOutlined } from "@ant-design/icons"; 5 | import { UserCreateInfo, UserListItem, UserUpdateInfo } from "./data"; 6 | import { ColumnsType } from "antd/es/table"; 7 | import CreateForm from "./components/CreateForm"; 8 | import UpdateForm from "./components/UpdateForm"; 9 | 10 | const {confirm} = Modal; 11 | 12 | const User: FC = () => { 13 | const [users, setUsers] = useState([]) 14 | const [userUpdate, setUserUpdate] = useState() 15 | const [loading, setLoading] = useState(false) 16 | const [createFormVisible, setCreateFormVisible] = useState(false) 17 | const [updateFormVisible, setUpdateFormVisible] = useState(false) 18 | const title = '用户列表'; 19 | 20 | const getUserList = async () => { 21 | const result = await userList(); 22 | setUsers(result.data); 23 | setLoading(false); 24 | }; 25 | 26 | useEffect(() => { 27 | setLoading(true); 28 | getUserList().then(); 29 | }, []); 30 | 31 | const handleSwitchUserState = async (user: UserListItem) => { 32 | const result = await updateUser(user.id, { 33 | status: user.status === 1 ? 2 : 1 34 | } as UserUpdateInfo); 35 | if (result.code === 0) { 36 | getUserList().then() 37 | } else { 38 | message.error("修改用户状态失败"); 39 | } 40 | }; 41 | 42 | const handleCreateUser = async (user: UserCreateInfo) => { 43 | const result = await createUser(user); 44 | if (result.code === 0) { 45 | setCreateFormVisible(false) 46 | getUserList().then() 47 | message.success("创建成功"); 48 | } else { 49 | message.error("创建用户失败"); 50 | } 51 | }; 52 | 53 | const handleUpdateUser = async (id: number, user: UserUpdateInfo) => { 54 | const result = await updateUser(id, user); 55 | if (result.code === 0) { 56 | setUpdateFormVisible(false); 57 | getUserList().then() 58 | } else { 59 | message.error("修改用户信息失败"); 60 | } 61 | }; 62 | 63 | const handleDeleteUser = async (id: number) => { 64 | const result = await deleteUser(id); 65 | if (result.code === 0) { 66 | getUserList().then() 67 | } else { 68 | message.error("删除用户失败"); 69 | } 70 | }; 71 | 72 | const addUser = ( 73 | 76 | ) 77 | 78 | function deleteModal(user: UserListItem) { 79 | confirm({ 80 | title: '确定删除用户 ' + user.username + ' ?', 81 | icon: , 82 | onOk() { 83 | handleDeleteUser(user.id).then() 84 | }, 85 | }); 86 | } 87 | 88 | const columns: ColumnsType = [ 89 | { 90 | title: '用户 ID', 91 | dataIndex: 'id', 92 | key: 'id', 93 | align: 'center', 94 | }, 95 | { 96 | title: '用户名', 97 | dataIndex: 'username', 98 | key: 'username', 99 | align: 'center', 100 | }, 101 | { 102 | title: '邮箱', 103 | dataIndex: 'email', 104 | key: 'email', 105 | align: 'center', 106 | }, 107 | { 108 | title: '分组', 109 | dataIndex: 'group_name', 110 | key: 'group_name', 111 | align: 'center', 112 | }, 113 | { 114 | title: '角色', 115 | dataIndex: 'role_name', 116 | key: 'role_name', 117 | align: 'center', 118 | }, 119 | { 120 | title: '状态', 121 | dataIndex: 'status', 122 | key: 'status', 123 | align: 'center', 124 | render: (status: number) => { 125 | if (status === 1) { 126 | return 127 | } else if (status === 2) { 128 | return 129 | } else { 130 | return 131 | } 132 | } 133 | }, 134 | { 135 | title: '操作', 136 | key: 'action', 137 | align: 'center', 138 | render: (user: UserListItem) => ( 139 | 140 | 143 | 144 | 151 | 152 | 156 | 157 | ), 158 | }, 159 | ]; 160 | 161 | return ( 162 |
163 | 164 |
users.id} 168 | loading={loading} 169 | pagination={{ 170 | hideOnSinglePage: true, 171 | pageSize: 10 172 | }} 173 | /> 174 | 175 | {createFormVisible ? 176 | { 180 | setCreateFormVisible(false) 181 | }} 182 | /> : null} 183 | {updateFormVisible ? 184 | { 189 | setUpdateFormVisible(false) 190 | }} 191 | /> : null} 192 | 193 | ); 194 | } 195 | export default User; 196 | -------------------------------------------------------------------------------- /src/pages/settings/role/Role.tsx: -------------------------------------------------------------------------------- 1 | import { FC, Fragment, useEffect, useState } from 'react'; 2 | import dayjs from 'dayjs'; 3 | import { Button, Card, Divider, message, Modal, Table } from 'antd'; 4 | import { RoleCreateInfo, RoleListItem, RoleUpdateInfo } from "./data"; 5 | import { ColumnsType } from "antd/es/table"; 6 | import { createRole, deleteRole, roleList, updateRole, updateRoleMenus } from "./service"; 7 | import { ExclamationCircleOutlined, PlusOutlined } from "@ant-design/icons"; 8 | import { menuList } from "../menu/service"; 9 | import { MenuListItem } from "../menu/data"; 10 | import CreateForm from "./components/CreateForm"; 11 | import UpdateForm from "./components/UpdateForm"; 12 | import PermissionForm from "./components/PermissionForm"; 13 | 14 | const {confirm} = Modal; 15 | 16 | const Role: FC = () => { 17 | const [roles, setRoles] = useState([]) 18 | const [menus, setMenus] = useState([]) 19 | const [roleUpdate, setRoleUpdate] = useState() 20 | const [rolePermission, setRolePermission] = useState() 21 | const [loading, setLoading] = useState(false) 22 | const [createFormVisible, setCreateFormVisible] = useState(false) 23 | const [updateFormVisible, setUpdateFormVisible] = useState(false) 24 | const [permissionFormVisible, setPermissionFormVisible] = useState(false) 25 | const title = '角色'; 26 | 27 | const getRoleList = async () => { 28 | const result = await roleList(); 29 | setRoles(result.data); 30 | setLoading(false); 31 | }; 32 | 33 | const getMenuList = async () => { 34 | const result = await menuList(); 35 | setMenus(result.data); 36 | }; 37 | 38 | useEffect(() => { 39 | setLoading(true) 40 | getRoleList().then() 41 | getMenuList().then() 42 | }, []); 43 | 44 | const formatTime = (date: any) => { 45 | return date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '' 46 | }; 47 | 48 | const handleCreateRole = async (role: RoleCreateInfo) => { 49 | const result = await createRole(role); 50 | if (result.code === 0) { 51 | setCreateFormVisible(false) 52 | getRoleList().then() 53 | } else { 54 | message.error("创建角色失败"); 55 | } 56 | }; 57 | 58 | const handleUpdateRole = async (id: number, role: RoleUpdateInfo) => { 59 | const result = await updateRole(id, role); 60 | if (result.code === 0) { 61 | setUpdateFormVisible(false); 62 | getRoleList().then() 63 | } else { 64 | message.error("更新角色失败"); 65 | } 66 | }; 67 | 68 | const handleDeleteRole = async (id: number) => { 69 | const result = await deleteRole(id); 70 | if (result.code === 0) { 71 | getRoleList().then() 72 | } else { 73 | message.error("删除角色失败"); 74 | } 75 | }; 76 | 77 | const handleUpdatePermission = async (id: number, checkedList: number[]) => { 78 | const res = await updateRoleMenus(id, checkedList); 79 | if (res.code === 0) { 80 | setPermissionFormVisible(false); 81 | message.success("修改成功"); 82 | } else { 83 | message.success("修改失败"); 84 | } 85 | }; 86 | 87 | const addRole = ( 88 | 92 | ); 93 | 94 | function deleteModal(role: RoleListItem) { 95 | confirm({ 96 | title: '删除角色', 97 | content: '确定删除角色<' + role.name + '>?', 98 | icon: , 99 | onOk() { 100 | handleDeleteRole(role.id).then() 101 | }, 102 | }); 103 | } 104 | 105 | const columns: ColumnsType = [ 106 | { 107 | title: 'ID', 108 | dataIndex: 'id', 109 | key: 'id', 110 | align: 'center', 111 | }, 112 | { 113 | title: '角色', 114 | dataIndex: 'name', 115 | key: 'name', 116 | align: 'center', 117 | }, 118 | { 119 | title: '创建时间', 120 | dataIndex: 'create_time', 121 | key: 'create_time', 122 | align: 'center', 123 | render: formatTime 124 | }, 125 | { 126 | title: '修改时间', 127 | dataIndex: 'update_time', 128 | key: 'update_time', 129 | align: 'center', 130 | render: formatTime 131 | }, 132 | { 133 | title: '操作', 134 | key: 'action', 135 | align: 'center', 136 | render: (role: RoleListItem) => ( 137 | 138 | 144 | 145 | 152 | 153 | 157 | 158 | ), 159 | }, 160 | ]; 161 | 162 | return ( 163 |
164 | 165 |
roles.id} 169 | loading={loading} 170 | pagination={{ 171 | hideOnSinglePage: true, 172 | pageSize: 10 173 | }} 174 | /> 175 | 176 | {createFormVisible ? 177 | { 181 | setCreateFormVisible(false) 182 | }} 183 | /> : null} 184 | {updateFormVisible ? 185 | { 190 | setUpdateFormVisible(false) 191 | }} 192 | /> : null} 193 | {permissionFormVisible ? 194 | { 200 | setPermissionFormVisible(false) 201 | }} 202 | /> : null} 203 | 204 | ); 205 | } 206 | export default Role; 207 | -------------------------------------------------------------------------------- /src/pages/settings/role/components/PermissionForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from 'react'; 2 | import { Checkbox, Modal, Table } from 'antd'; 3 | import { MenuListItem } from "../../menu/data"; 4 | import { RoleListItem } from "../data"; 5 | import { CheckboxChangeEvent } from "antd/es/checkbox"; 6 | import { roleMenus } from "../service"; 7 | 8 | interface PermissionFormProps { 9 | open: boolean; 10 | role: RoleListItem; 11 | menus: MenuListItem[]; 12 | onOk: (id: number, checkedList: number[]) => void; 13 | onCancel: () => void; 14 | } 15 | 16 | const PermissionForm: FC = (props) => { 17 | const {open, role, menus, onOk, onCancel} = props 18 | 19 | const [menusTree, setMenusTree] = useState() 20 | const [loading, setLoading] = useState(false) 21 | const [firstIn, setFirstIn] = useState(true) 22 | const [checkedList, setCheckedList] = useState([]); 23 | const [indeterminateList, setIndeterminateList] = useState([]); 24 | const [fMap, setFMap] = useState>>(new Map()) 25 | const [sMap, setSMap] = useState>>(new Map()) 26 | let fatherMap = new Map(); 27 | let sonMap = new Map(); 28 | 29 | const getRoleMenus = async (id: number) => { 30 | const result = await roleMenus(id); 31 | setLoading(false); 32 | if (result.code === 0) { 33 | setCheckedList(result.data) 34 | } 35 | }; 36 | 37 | useEffect(() => { 38 | setMenusTree(makeMenus(menus)) 39 | makeList([], menus) 40 | setFMap(fatherMap) 41 | setSMap(sonMap) 42 | // eslint-disable-next-line 43 | }, [menus]) 44 | 45 | useEffect(() => { 46 | setLoading(true) 47 | getRoleMenus(role.id).then() 48 | }, [role]) 49 | 50 | useEffect(() => { 51 | updateIndeterminate() 52 | // eslint-disable-next-line 53 | }, [checkedList]); 54 | 55 | 56 | const ok = () => { 57 | onOk(role.id, checkedList); 58 | } 59 | 60 | const onChangeCheckbox = (e: CheckboxChangeEvent) => { 61 | updateChecked(e.target.value.id, e.target.checked, e.target.value.parent_id) 62 | }; 63 | 64 | const makeMenus = (menus: MenuListItem[]): any => { 65 | return menus.map(({children, ...item}) => ({ 66 | ...item, 67 | children: children.length === 0 ? null : makeMenus(children), 68 | })); 69 | } 70 | 71 | const makeList = (fids: number[], menus: MenuListItem[]) => { 72 | for (const menu of menus) { //遍历所有 menu 73 | if (menu.children.length !== 0) { //如果有 children 74 | const cg: number[] = [] 75 | for (const child of menu.children) { 76 | cg.push(child.id) //把 children ID 添加进集合 77 | } 78 | fatherMap.set(menu.id, cg) // menu 的 ID 和 children ID 的集合 79 | makeList([menu.id].concat(fids), menu.children) // 把当前层有 children 的 menu ID 传给下一层 80 | } 81 | if (menu.parent_id !== 0) { 82 | sonMap.set(menu.id, fids) //非根目录时,记录当前 menu ID 的父节点集合 83 | } 84 | if (menu.funs.length !== 0) { //如果有 funs 85 | const cg: number[] = [] 86 | for (const fun of menu.funs) { 87 | cg.push(fun.id) //把 fun ID 添加进集合 88 | sonMap.set(fun.id, [menu.id].concat(fids)) //记录当前 fun ID 的父节点集合 89 | } 90 | fatherMap.set(menu.id, cg) // menu 的 ID 和 fun ID 的集合 91 | } 92 | 93 | } 94 | } 95 | 96 | const updateChecked = (id: number, checked: boolean, fid: number) => { 97 | if (!checked) { 98 | checked = !checked && indeterminateList.includes(id); 99 | } 100 | let gcl = new Set(checkedList); 101 | let gil = new Set(indeterminateList); 102 | checked ? gcl.add(id) : gcl.delete(id) 103 | gil.delete(id) 104 | if (fMap.has(id)) { 105 | updateChildren(gcl, gil, id, checked) 106 | } 107 | if (fid !== 0) { 108 | updateFather(gcl, gil, id, checked) 109 | } 110 | setCheckedList(Array.from(gcl)) 111 | setIndeterminateList(Array.from(gil)) 112 | } 113 | 114 | const updateChildren = (gcl: Set, gil: Set, id: number, checked: boolean) => { 115 | if (fMap.has(id)) { 116 | // todo remove ignore 117 | // @ts-ignore 118 | for (let child of fMap.get(id)) { 119 | checked ? gcl.add(child) : gcl.delete(child) 120 | gil.delete(child) 121 | updateChildren(gcl, gil, child, checked) 122 | } 123 | } 124 | 125 | } 126 | 127 | const updateFather = (gcl: Set, gil: Set, id: number, checked: boolean) => { 128 | if (sMap.has(id)) { 129 | // todo remove ignore 130 | // @ts-ignore 131 | for (let fid of sMap.get(id)) { //遍历点击节点的所有父节点,按层级倒序排列 132 | const fcl = intersection(new Set(fMap.get(fid)), gcl) //此父节点的子节点选中的集合 133 | const fil = intersection(new Set(fMap.get(fid)), gil) //此父节点的子节点半选的集合 134 | 135 | if (checked) { 136 | gcl.add(fid) //设置此父节点为选中状态 137 | // todo remove ignore 138 | // @ts-ignore 139 | if (fcl.size === fMap.get(fid).length && fil.size === 0) { //此父节点选中的集合大小等于总集合大小时,且此父节点的子节点没有半选时取消半选状态 140 | gil.delete(fid) 141 | } 142 | } else { 143 | gil.add(fid) //设置此父节点为半选状态 144 | if (fcl.size === 0) { //此父节点选中的集合大小等于0时,设置父节点为取消状态,取消半选状态 145 | gcl.delete(fid) 146 | gil.delete(fid) 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | const updateIndeterminate = () => { 154 | const i: number[] = [] 155 | for (let [id, sonIds] of Array.from(fMap)) { //遍历所有有子节点的集合 156 | const ccl = intersection(new Set(sonIds), new Set(checkedList)) //此节点的子节点选中的集合 157 | if (firstIn) { //首次进入时半选集合为空 158 | if (sonIds.length !== ccl.size && ccl.size > 0) { //此节点子节点选中的集合大小不等于此节点子节点总集合大小时,且有选中的子节点 159 | i.push(id) 160 | setFirstIn(false) 161 | } 162 | } else { //非首次进入时需要校验半选集合 163 | const cil = intersection(new Set(sonIds), new Set(indeterminateList)) //此节点的子节点半选的集合 164 | if ((sonIds.length === ccl.size && cil.size > 0) || (sonIds.length !== ccl.size && ccl.size > 0)) { 165 | //此节点子节点选中的集合大小等于此节点子节点总集合大小时,且有半选的子节点 OR 166 | //此节点子节点选中的集合大小不等于此节点子节点总集合大小时,且有选中的子节点 167 | i.push(id) 168 | } 169 | } 170 | } 171 | setIndeterminateList(i) 172 | } 173 | 174 | function intersection(setA: Set, setB: Set) { 175 | let _intersection = new Set(); 176 | for (let elem of Array.from(setB)) { 177 | if (setA.has(elem)) { 178 | _intersection.add(elem); 179 | } 180 | } 181 | return _intersection; 182 | } 183 | 184 | const columns: any = [ 185 | { 186 | title: '菜单', 187 | dataIndex: 'id', 188 | align: 'left', 189 | width: 200, 190 | render: (_text: any, record: MenuListItem) => { 191 | return ( 198 | {record.name} 199 | ) 200 | } 201 | }, 202 | { 203 | title: '功能', 204 | dataIndex: 'id', 205 | align: 'left', 206 | render: (_text: any, record: MenuListItem) => { 207 | return ( 208 |
{ 209 | record.funs.map(value => { 210 | return 216 | {value.name} 217 | 218 | })} 219 |
220 | ) 221 | } 222 | }, 223 | ]; 224 | 225 | return ( 226 | 233 |
data.id} 237 | pagination={false} 238 | loading={loading} 239 | size={'small'} 240 | bordered 241 | scroll={{y: 360}} 242 | /> 243 | 244 | ) 245 | } 246 | export default PermissionForm; 247 | -------------------------------------------------------------------------------- /src/pages/settings/menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, Fragment, useEffect, useState } from 'react'; 2 | import { Button, Card, Col, Divider, message, Modal, Row, Table, Tree } from "antd"; 3 | import { MenuCreateInfo, MenuListItem, MenuUpdateInfo } from "./data"; 4 | import { DataNode } from 'antd/lib/tree'; 5 | import { createMenu, deleteMenu, menuGet, menuList, updateMenu } from "./service"; 6 | import { menuIcons } from "../../../utils/icons"; 7 | import { ColumnsType } from "antd/es/table"; 8 | import { DownOutlined, ExclamationCircleOutlined, PlusOutlined } from "@ant-design/icons"; 9 | import CreateForm from "./components/CreateForm"; 10 | import UpdateForm from "./components/UpdateForm"; 11 | 12 | 13 | const {DirectoryTree} = Tree; 14 | const {confirm} = Modal; 15 | 16 | const Menu: FC = () => { 17 | const [menusSelect, setMenusSelect] = useState([]) 18 | const [menusTree, setMenusTree] = useState([]) 19 | const [selectedMenu, setSelectedMenu] = useState([]) 20 | const [selectedFuns, setSelectedFuns] = useState([]) 21 | const [menuCreate, setMenuCreate] = useState({key: 0}) 22 | const [menuUpdate, setMenuUpdate] = useState() 23 | const [loading, setLoading] = useState(false) 24 | const [createFormVisible, setCreateFormVisible] = useState(false) 25 | const [updateFormVisible, setUpdateFormVisible] = useState(false) 26 | const [formType, setFormType] = useState(0) 27 | const [menuType, setMenuType] = useState(0) 28 | const funTitle = '功能'; 29 | const treeTitle = '菜单管理'; 30 | 31 | const getMenuList = async () => { 32 | setLoading(true) 33 | const result = await menuList(); 34 | if (result.code === 0) { 35 | const mt = convertMenusTree(result.data) 36 | setMenusTree(mt) 37 | let ms = setSelectable(mt) 38 | ms.push({key: 0, value: 0, title: "根目录", disabled: false}) 39 | setMenusSelect(ms) 40 | setLoading(false); 41 | } 42 | }; 43 | 44 | const getMenu = async (id: number) => { 45 | const result = await menuGet(id); 46 | if (result.code === 0) { 47 | let menu = [] 48 | menu.push(result.data) 49 | setSelectedMenu(menu); 50 | setSelectedFuns(result.data.funs); 51 | } 52 | }; 53 | 54 | useEffect(() => { 55 | getMenuList().then() 56 | // eslint-disable-next-line 57 | }, []); 58 | 59 | const convertMenusTree = (menus: MenuListItem[]): DataNode[] => { 60 | return menus.map((menu) => ({ 61 | key: menu.id, 62 | title: menu.name, 63 | isLeaf: menu.type === 2, 64 | children: menu.children && convertMenusTree(menu.children), 65 | } as DataNode)); 66 | } 67 | 68 | const setSelectable = (menus: DataNode[]): any[] => { 69 | return menus.map(({isLeaf, key, children, ...item}) => ({ 70 | ...item, 71 | disabled: isLeaf, 72 | value: key, 73 | key: key, 74 | children: children && setSelectable(children), 75 | })); 76 | } 77 | 78 | const handleCreateMenu = async (menu: MenuCreateInfo) => { 79 | const result = await createMenu(menu); 80 | if (result.code === 0) { 81 | setCreateFormVisible(false) 82 | getMenuList().then() 83 | getMenu(selectedMenu[0].id).then() 84 | message.success("创建成功"); 85 | } else { 86 | message.error("创建菜单失败"); 87 | } 88 | }; 89 | 90 | const handleUpdateMenu = async (id: number, menu: MenuUpdateInfo) => { 91 | const result = await updateMenu(id, menu); 92 | if (result.code === 0) { 93 | setUpdateFormVisible(false); 94 | getMenuList().then() 95 | getMenu(selectedMenu[0].id).then() 96 | } else { 97 | message.error("修改菜单信息失败"); 98 | } 99 | }; 100 | 101 | const handleDeleteMenu = async (id: number) => { 102 | const result = await deleteMenu(id); 103 | if (result.code === 0) { 104 | getMenuList().then() 105 | getMenu(selectedMenu[0].id).then() 106 | } else { 107 | message.error("删除菜单失败"); 108 | } 109 | }; 110 | 111 | const onSelect = (_selectedKeys: React.Key[], info: any) => { 112 | setMenuType(info.node.isLeaf ? 2 : 1) 113 | setMenuCreate({key: info.node.key, title: info.node.title}) 114 | getMenu(info.node.key).then() 115 | }; 116 | 117 | const addMenu = ( 118 | ); 123 | 124 | const addFun = ( 125 | ); 130 | 131 | function deleteModal(menu: MenuListItem) { 132 | confirm({ 133 | title: '删除菜单', 134 | content: '确定删除菜单<' + menu.name + '>?', 135 | icon: , 136 | onOk() { 137 | handleDeleteMenu(menu.id).then() 138 | }, 139 | }); 140 | } 141 | 142 | const menuColumns: ColumnsType = [ 143 | { 144 | title: 'ID', 145 | dataIndex: 'id', 146 | key: 'id', 147 | align: 'center', 148 | width: 80, 149 | }, 150 | { 151 | title: '名称', 152 | dataIndex: 'name', 153 | key: 'name', 154 | align: 'center', 155 | width: 120, 156 | }, 157 | { 158 | title: '路径', 159 | dataIndex: 'path', 160 | key: 'path', 161 | align: 'center', 162 | width: 240, 163 | }, 164 | { 165 | title: '图标', 166 | dataIndex: 'icon', 167 | key: 'icon', 168 | align: 'center', 169 | width: 100, 170 | render: (icon) => { 171 | return menuIcons[icon as string] 172 | } 173 | }, 174 | { 175 | title: '排序', 176 | dataIndex: 'order_id', 177 | key: 'order_id', 178 | align: 'center', 179 | width: 100, 180 | }, 181 | { 182 | title: '操作', 183 | key: 'action', 184 | align: 'center', 185 | render: (menu) => ( 186 | 187 | 193 | 194 | 197 | 198 | ), 199 | }, 200 | ]; 201 | 202 | const funColumns: ColumnsType = [ 203 | { 204 | title: 'ID', 205 | dataIndex: 'id', 206 | key: 'id', 207 | align: 'center', 208 | width: 80, 209 | }, 210 | { 211 | title: '名称', 212 | dataIndex: 'name', 213 | key: 'name', 214 | align: 'center', 215 | width: 120, 216 | }, 217 | { 218 | title: '路径', 219 | dataIndex: 'path', 220 | key: 'path', 221 | align: 'center', 222 | width: 240, 223 | }, 224 | { 225 | title: '方法', 226 | dataIndex: 'method', 227 | key: 'method', 228 | align: 'center', 229 | width: 100, 230 | }, 231 | { 232 | title: '操作', 233 | key: 'action', 234 | align: 'center', 235 | render: (menu: MenuListItem) => ( 236 | 237 | 243 | 244 | 248 | 249 | ), 250 | }, 251 | ]; 252 | 253 | return ( 254 |
255 | 256 |
257 | 258 | } 260 | showIcon={false} 261 | onSelect={onSelect} 262 | treeData={menusTree} /> 263 | 264 | 265 | 266 |
267 | {menuType !== 0 ? 268 | 269 |
selectedMenu.id} 273 | loading={loading} 274 | size={'middle'} 275 | pagination={{ 276 | hideOnSinglePage: true, 277 | pageSize: 10 278 | }} 279 | /> 280 | : null} 281 | {menuType === 2 ? 282 | 283 |
selectedFuns.id} 287 | loading={loading} 288 | size={'small'} 289 | pagination={{ 290 | hideOnSinglePage: true, 291 | pageSize: 10 292 | }} 293 | /> 294 | : null} 295 | 296 | 297 | 298 | {createFormVisible ? 299 | { 306 | setCreateFormVisible(false) 307 | }} 308 | /> : null} 309 | {updateFormVisible ? 310 | { 317 | setUpdateFormVisible(false) 318 | }} 319 | /> : null} 320 | 321 | ); 322 | } 323 | export default Menu; 324 | --------------------------------------------------------------------------------