├── .env.dev ├── .env.pre ├── .env.prod ├── .env.test ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.js ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── config ├── useCompressPlugin.js ├── useEslintPlugin.js ├── useProgressPlugin.js ├── useServer.js ├── useVisualizerPlugin.js └── useVuePlugin.js ├── index.html ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── images │ └── logo.svg └── libs │ └── tinymce │ ├── icons │ └── default │ │ └── icons.min.js │ ├── langs │ ├── README.md │ └── zh-Hans.js │ ├── license.txt │ ├── models │ └── dom │ │ └── model.min.js │ ├── plugins │ ├── advlist │ │ └── plugin.min.js │ ├── anchor │ │ └── plugin.min.js │ ├── autolink │ │ └── plugin.min.js │ ├── autoresize │ │ └── plugin.min.js │ ├── autosave │ │ └── plugin.min.js │ ├── charmap │ │ └── plugin.min.js │ ├── code │ │ └── plugin.min.js │ ├── codesample │ │ └── plugin.min.js │ ├── directionality │ │ └── plugin.min.js │ ├── emoticons │ │ ├── js │ │ │ ├── emojiimages.js │ │ │ ├── emojiimages.min.js │ │ │ ├── emojis.js │ │ │ └── emojis.min.js │ │ └── plugin.min.js │ ├── fullscreen │ │ └── plugin.min.js │ ├── help │ │ └── plugin.min.js │ ├── image │ │ └── plugin.min.js │ ├── importcss │ │ └── plugin.min.js │ ├── insertdatetime │ │ └── plugin.min.js │ ├── link │ │ └── plugin.min.js │ ├── lists │ │ └── plugin.min.js │ ├── media │ │ └── plugin.min.js │ ├── nonbreaking │ │ └── plugin.min.js │ ├── pagebreak │ │ └── plugin.min.js │ ├── preview │ │ └── plugin.min.js │ ├── quickbars │ │ └── plugin.min.js │ ├── save │ │ └── plugin.min.js │ ├── searchreplace │ │ └── plugin.min.js │ ├── table │ │ └── plugin.min.js │ ├── template │ │ └── plugin.min.js │ ├── visualblocks │ │ └── plugin.min.js │ ├── visualchars │ │ └── plugin.min.js │ └── wordcount │ │ └── plugin.min.js │ ├── skins │ ├── content │ │ ├── dark │ │ │ └── content.min.css │ │ ├── default │ │ │ └── content.min.css │ │ ├── document │ │ │ └── content.min.css │ │ ├── tinymce-5-dark │ │ │ └── content.min.css │ │ ├── tinymce-5 │ │ │ └── content.min.css │ │ └── writer │ │ │ └── content.min.css │ └── ui │ │ ├── oxide-dark │ │ ├── content.inline.min.css │ │ ├── content.min.css │ │ ├── skin.min.css │ │ └── skin.shadowdom.min.css │ │ ├── oxide │ │ ├── content.inline.min.css │ │ ├── content.min.css │ │ ├── skin.min.css │ │ └── skin.shadowdom.min.css │ │ ├── tinymce-5-dark │ │ ├── content.inline.min.css │ │ ├── content.min.css │ │ ├── skin.min.css │ │ └── skin.shadowdom.min.css │ │ └── tinymce-5 │ │ ├── content.inline.min.css │ │ ├── content.min.css │ │ ├── skin.min.css │ │ └── skin.shadowdom.min.css │ ├── themes │ └── silver │ │ └── theme.min.js │ ├── tinymce.d.ts │ └── tinymce.min.js ├── src ├── App.vue ├── apis │ ├── index.js │ └── modules │ │ ├── common.js │ │ ├── menu.js │ │ ├── role.js │ │ ├── system.js │ │ ├── user.js │ │ └── users.js ├── assets │ ├── avatar.jpg │ ├── cropper.png │ ├── login_aside_bg.jpg │ ├── login_welcome.svg │ ├── logo.svg │ ├── logos.png │ └── upgrade.svg ├── components │ ├── ActionBar │ │ └── ActionBar.vue │ ├── ActionButton │ │ └── ActionButton.vue │ ├── Breadcrumb │ │ └── Breadcrumb.vue │ ├── Cascader │ │ └── Cascader.vue │ ├── Chart │ │ └── Chart.vue │ ├── Cropper │ │ ├── Cropper.vue │ │ └── CropperDialog.vue │ ├── Editor │ │ ├── Editor.vue │ │ └── index.less │ ├── Filter │ │ ├── Filter.vue │ │ ├── FilterItem.vue │ │ ├── FilterTag.vue │ │ ├── FilterTagItem.vue │ │ ├── config.js │ │ └── context.js │ ├── FormTable │ │ └── FormTable.vue │ ├── Loading │ │ ├── Loading.vue │ │ ├── directive.js │ │ └── index.js │ ├── Preview │ │ ├── Preview.vue │ │ ├── config.js │ │ └── index.js │ ├── QrCode │ │ └── QrCode.vue │ ├── ResizeBox │ │ ├── ResizeBox.vue │ │ └── config.js │ ├── Scrollbar │ │ └── Scrollbar.vue │ ├── SearchBar │ │ └── SearchBar.vue │ ├── Upload │ │ ├── UploadImage.vue │ │ ├── UploadInput.vue │ │ └── config.js │ └── index.js ├── config │ ├── app.js │ ├── http.js │ ├── index.js │ ├── router.js │ └── storage.js ├── core │ ├── exception.js │ ├── index.js │ └── permission.js ├── directives │ ├── action.js │ └── index.js ├── enums │ └── system.js ├── hooks │ ├── index.js │ ├── useColors.js │ ├── useForm.js │ ├── useMenu.js │ ├── useModal.js │ ├── useMultiTab.js │ └── usePagination.js ├── layouts │ ├── BasicLayout.vue │ ├── CustomLayout.vue │ ├── RouteViewLayout.vue │ ├── UserLayout.vue │ ├── components │ │ ├── ActionButton.vue │ │ ├── BasicContent.vue │ │ ├── BasicHeader.vue │ │ ├── BasicMenu.vue │ │ ├── BasicSide.vue │ │ ├── Brand.vue │ │ ├── ConfigDialog.vue │ │ ├── IframeView.vue │ │ └── MultiTab.vue │ ├── hooks │ │ ├── useMenu.js │ │ └── useMultiTab.js │ └── index.js ├── locales │ ├── index.js │ └── lang │ │ ├── en-US.js │ │ ├── en-US │ │ ├── button.js │ │ ├── component.js │ │ ├── globalHeader.js │ │ ├── menu.js │ │ ├── pages.js │ │ ├── pwa.js │ │ ├── settingDrawer.js │ │ └── settings.js │ │ ├── zh-CN.js │ │ └── zh-CN │ │ ├── button.js │ │ ├── component.js │ │ ├── globalHeader.js │ │ ├── menu.js │ │ ├── pages.js │ │ ├── pwa.js │ │ ├── settingDrawer.js │ │ └── settings.js ├── main.js ├── mock │ ├── index.js │ ├── modules │ │ ├── common.js │ │ ├── system.js │ │ └── user.js │ └── util.js ├── plugins │ └── progress │ │ ├── index.js │ │ └── index.less ├── router │ ├── config.js │ ├── index.js │ ├── notMenuPage.js │ ├── routes │ │ ├── admin.js │ │ ├── exception.js │ │ ├── form.js │ │ ├── home.js │ │ ├── iframe.js │ │ ├── index.js │ │ ├── link.js │ │ ├── list.js │ │ ├── other.js │ │ ├── profile.js │ │ ├── result.js │ │ └── system.js │ └── util.js ├── store │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── multiTab.js │ │ ├── router.js │ │ └── user.js ├── styles │ ├── antd.less │ ├── index.less │ ├── mixins │ │ ├── color │ │ │ ├── bezierEasing.less │ │ │ ├── colorPalette.less │ │ │ ├── colors.less │ │ │ └── tinyColor.less │ │ ├── ellipsis.less │ │ ├── index.less │ │ └── scrollbar.less │ ├── reset.less │ ├── utils.less │ └── variables.less ├── utils │ ├── request.js │ ├── storage.js │ └── util.js └── views │ ├── exception │ ├── 403.vue │ ├── 404.vue │ └── 500.vue │ ├── form │ ├── advanced │ │ └── index.vue │ ├── basic │ │ └── index.vue │ └── step │ │ ├── components │ │ ├── Step1.vue │ │ ├── Step2.vue │ │ └── Step3.vue │ │ └── index.vue │ ├── home │ └── index.vue │ ├── iframe │ └── index.vue │ ├── list │ ├── basic │ │ ├── components │ │ │ └── EditDialog.vue │ │ └── index.vue │ ├── card │ │ └── index.vue │ ├── search │ │ ├── applications │ │ │ └── index.vue │ │ ├── articles │ │ │ └── index.vue │ │ ├── components │ │ │ └── PageHeader.vue │ │ └── projects │ │ │ └── index.vue │ └── table │ │ ├── components │ │ └── EditDialog.vue │ │ └── index.vue │ ├── login │ └── index.vue │ ├── other │ ├── badge │ │ └── index.vue │ └── multi-tab │ │ └── index.vue │ ├── profile │ ├── advanced │ │ └── index.vue │ └── basic │ │ └── index.vue │ ├── result │ ├── fail │ │ └── index.vue │ └── success │ │ └── index.vue │ └── system │ ├── dict │ ├── components │ │ ├── Dict.vue │ │ ├── EditDialog.vue │ │ └── EditDictDialog.vue │ └── index.vue │ ├── logger │ └── index.vue │ ├── menu │ ├── components │ │ └── EditDialog.vue │ └── index.vue │ ├── new-menu │ ├── components │ │ └── Menu.vue │ └── index.vue │ ├── role │ ├── components │ │ ├── EditDialog.vue │ │ ├── EditRoleDialog.vue │ │ └── Role.vue │ └── index.vue │ └── user │ ├── components │ ├── Department.vue │ ├── EditDepartmentDialog.vue │ └── EditDialog.vue │ └── index.vue ├── vite.config.js └── yarn.lock /.env.dev: -------------------------------------------------------------------------------- 1 | # 本地开发环境 2 | NODE_ENV=development 3 | 4 | # app 5 | VITE_TITLE=GIN-Admin 6 | VITE_PUBLIC_PATH=/ 7 | VITE_OUT_DIR=dist 8 | VITE_PERMISSION=false 9 | 10 | # router 11 | VITE_ROUTER_BASE=/ 12 | VITE_ROUTER_HISTORY=hash 13 | 14 | # api 15 | VITE_API_BASIC=/ 16 | VITE_API_HTTP=/api/v1/ 17 | # storage 18 | VITE_STORAGE_NAMESPACE = gin-admin_local_ -------------------------------------------------------------------------------- /.env.pre: -------------------------------------------------------------------------------- 1 | # 预发环境 2 | NODE_ENV=production 3 | 4 | # app 5 | VITE_TITLE=Admin 6 | VITE_PUBLIC_PATH=/ 7 | VITE_OUT_DIR=dist 8 | VITE_PERMISSION=false 9 | 10 | # router 11 | VITE_ROUTER_HISTORY=hash 12 | 13 | # api 14 | VITE_API_BASIC=/ 15 | VITE_API_HTTP=/api/v1/ 16 | 17 | # storage 18 | VITE_STORAGE_NAMESPACE=admin_pre_ 19 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | # 生产环境 2 | NODE_ENV=production 3 | 4 | # app 5 | VITE_TITLE=Admin 6 | VITE_PUBLIC_PATH=/ 7 | VITE_OUT_DIR=dist 8 | VITE_PERMISSION=true 9 | 10 | # router 11 | VITE_ROUTER_HISTORY=hash 12 | 13 | # api 14 | VITE_API_BASIC=/ 15 | VITE_API_HTTP=/api/v1/ 16 | 17 | # storage 18 | VITE_STORAGE_NAMESPACE=ginadmin_ 19 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # 测试环境 2 | NODE_ENV=production 3 | 4 | # app 5 | VITE_TITLE=Admin 6 | VITE_PUBLIC_PATH=/ 7 | VITE_OUT_DIR=dist 8 | VITE_PERMISSION=false 9 | 10 | # router 11 | VITE_ROUTER_HISTORY=hash 12 | 13 | # api 14 | VITE_API_BASIC=https://mock.apifox.cn/m1/3156808-0-default 15 | 16 | # storage 17 | VITE_STORAGE_NAMESPACE=admin_test_ 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin-admin/gin-admin-vue/c3cc921e708e8e7e5282deb3550ffe426276b94a/.eslintignore -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:vue/vue3-essential', 10 | 'plugin:prettier/recommended', 11 | 'eslint-config-prettier', 12 | ], 13 | overrides: [], 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | sourceType: 'module', 17 | }, 18 | plugins: ['vue'], 19 | globals: { 20 | __APP_INFO__: true, 21 | tinymce: true, 22 | }, 23 | rules: { 24 | 'vue/multi-word-component-names': 'off', 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /.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 | */.vitepress/cache/**/* 15 | */.vitepress/.temp/** 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'src/*.{js,vue}': (filenames) => `eslint --no-cache --ext ${filenames.join(' ')} --fix`, 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "bracketSameLine": true, 4 | "semi": false, 5 | "trailingComma": "es5", 6 | "singleQuote": true, 7 | "printWidth": 120, 8 | "endOfLine": "auto", 9 | "singleAttributePerLine": true 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 mengxianghan 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GIN-Admin-Frontend 2 | 3 | > GIN-Admin-vue is a frontend project for [gin-admin](https://github.com/LyricTian/gin-admin) base on Ant Design React. 4 | 5 | ![gin-admin-frontend](./demo.png) 6 | 7 | - [Preview](http://101.42.232.163:8040) 8 | - Username: admin 9 | - Password: abc-123 10 | 11 | ## Features 12 | 13 | - :gem: **Neat Design**: Follow Ant Design specification 14 | - :triangular_ruler: **Common Templates**: Typical templates for enterprise applications 15 | - :rocket: **State of The Art Development**: Newest development stack of React/umi/dva/antd 16 | - :cn: **International**: Built-in i18n solution 17 | - :closed_lock_with_key: **RBAC**: Support rbac permission management 18 | 19 | ## Environment Prepare 20 | 21 | > You can use [nvm](https://github.com/nvm-sh/nvm) to manage node version. 22 | 23 | - Node.js v16.20.2 24 | 25 | ## Quick Start 26 | 27 | ### Clone project 28 | 29 | ```bash 30 | git clone https://github.com/gin-admin/gin-admin-frontend.git 31 | ``` 32 | 33 | ### Install dependencies 34 | 35 | ```bash 36 | npm install or yarn add 37 | ``` 38 | 39 | ### Start project 40 | 41 | ```bash 42 | vite --mode dev 43 | ``` 44 | 45 | ### Build project 46 | 47 | ```bash 48 | vite build --mode prod 49 | ``` 50 | 51 | ### Check code style 52 | 53 | ```bash 54 | npm run lint 55 | ``` 56 | 57 | ## MIT License 58 | 59 | ```text 60 | Copyright (c) 2023 Muyu 61 | ``` -------------------------------------------------------------------------------- /config/useCompressPlugin.js: -------------------------------------------------------------------------------- 1 | import compressPlugin from 'vite-plugin-compression' 2 | 3 | export default () => { 4 | return compressPlugin({ 5 | ext: '.gz', 6 | deleteOriginFile: false, 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /config/useEslintPlugin.js: -------------------------------------------------------------------------------- 1 | import eslintPlugin from 'vite-plugin-eslint' 2 | 3 | export default () => { 4 | return eslintPlugin() 5 | } 6 | -------------------------------------------------------------------------------- /config/useProgressPlugin.js: -------------------------------------------------------------------------------- 1 | import progress from 'vite-plugin-progress' 2 | 3 | export default () => { 4 | return progress() 5 | } 6 | -------------------------------------------------------------------------------- /config/useServer.js: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | host: true, 3 | port: 9211, 4 | proxy: { 5 | '/api': { 6 | target: 'http://101.42.232.163:8080/api', 7 | // target: 'http://127.0.0.1:8045/api', 8 | changeOrigin: true, 9 | rewrite: (path) => path.replace('/api', ''), 10 | }, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /config/useVisualizerPlugin.js: -------------------------------------------------------------------------------- 1 | import visualizer from 'rollup-plugin-visualizer' 2 | 3 | const lifecycle = process.env.npm_lifecycle_event 4 | 5 | export default () => { 6 | if ('report' === lifecycle) { 7 | return visualizer({ 8 | filename: './node_modules/.cache/visualizer/stats.html', 9 | open: true, 10 | gzipSize: true, 11 | brotliSize: true, 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /config/useVuePlugin.js: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | 3 | export default () => { 4 | return vue() 5 | } 6 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
正在加载资源
18 |
初次加载需要较长时间 请耐心等待
19 |
20 | 81 |
82 | 83 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gin-admin", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "vite --mode dev", 7 | "build:test": "vite build --mode test", 8 | "build:pre": "vite build --mode pre", 9 | "build:prod": "vite build --mode prod", 10 | "preview": "npm run build:prod && vite preview --mode prod", 11 | "report": "vite build --mode prod", 12 | "lint": "eslint --ext .js,.vue --ignore-path .eslintignore --fix src", 13 | "prepare": "husky install", 14 | "prettier": "prettier --config .prettierrc --write ./src/**/*.{js,vue}", 15 | "docs:dev": "vitepress dev docs", 16 | "docs:build": "vitepress build docs", 17 | "docs:preview": "vitepress build docs && vitepress preview docs" 18 | }, 19 | "lint-staged": { 20 | "src/**/*.{js,vue}": "eslint --ext .js,.vue .eslintignore --no-cache --fix" 21 | }, 22 | "dependencies": { 23 | "@ant-design/colors": "^7.0.0", 24 | "@ant-design/icons-vue": "^6.1.0", 25 | "@icon-park/vue-next": "^1.4.2", 26 | "@tinymce/tinymce-vue": "^5.1.0", 27 | "ant-design-vue": "^4.0.1", 28 | "axios": "^1.4.0", 29 | "clipboard": "^2.0.11", 30 | "cropperjs": "^1.5.13", 31 | "dayjs": "^1.11.9", 32 | "echarts": "^5.4.3", 33 | "filesize": "^10.0.12", 34 | "filesize-parser": "^1.5.0", 35 | "js-md5": "^0.8.3", 36 | "jschardet": "^3.0.0", 37 | "json-bigint": "^1.0.0", 38 | "lodash-es": "^4.17.21", 39 | "nanoid": "^4.0.2", 40 | "nprogress": "^0.2.0", 41 | "overlayscrollbars": "^2.2.1", 42 | "overlayscrollbars-vue": "^0.5.2", 43 | "pinia": "^2.1.6", 44 | "prettier": "^3.0.3", 45 | "qrcode": "^1.5.3", 46 | "sortablejs": "^1.15.0", 47 | "tinymce": "^6.6.2", 48 | "vue": "^3.3.4", 49 | "vue-i18n": "^9.6.5", 50 | "vue-router": "^4.2.4", 51 | "xy-enum": "^1.4.4", 52 | "xy-http": "^1.0.0", 53 | "xy-storage": "^3.1.0" 54 | }, 55 | "devDependencies": { 56 | "@vitejs/plugin-vue": "^4.3.1", 57 | "eslint": "^8.47.0", 58 | "eslint-config-prettier": "^9.0.0", 59 | "eslint-plugin-prettier": "^5.0.0", 60 | "eslint-plugin-vue": "^9.17.0", 61 | "husky": "^8.0.3", 62 | "less": "^4.2.0", 63 | "lint-staged": "^14.0.0", 64 | "rollup-plugin-visualizer": "^5.9.2", 65 | "vite": "^4.4.9", 66 | "vite-plugin-compression": "^0.5.1", 67 | "vite-plugin-eslint": "^1.8.1", 68 | "vite-plugin-progress": "^0.0.7", 69 | "vitepress": "^1.0.0-beta.7" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin-admin/gin-admin-vue/c3cc921e708e8e7e5282deb3550ffe426276b94a/public/favicon.ico -------------------------------------------------------------------------------- /public/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/libs/tinymce/langs/README.md: -------------------------------------------------------------------------------- 1 | This is where language files should be placed. 2 | 3 | Please DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/ 4 | -------------------------------------------------------------------------------- /public/libs/tinymce/license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc. 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 | -------------------------------------------------------------------------------- /public/libs/tinymce/plugins/advlist/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.4.1 (2023-03-29) 3 | */ 4 | !function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=(t,e,s)=>{const r="UL"===e?"InsertUnorderedList":"InsertOrderedList";t.execCommand(r,!1,!1===s?null:{"list-style-type":s})},s=t=>e=>e.options.get(t),r=s("advlist_number_styles"),n=s("advlist_bullet_styles"),l=t=>null==t,i=t=>!l(t);var o=tinymce.util.Tools.resolve("tinymce.util.Tools");class a{constructor(t,e){this.tag=t,this.value=e}static some(t){return new a(!0,t)}static none(){return a.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?a.some(t(this.value)):a.none()}bind(t){return this.tag?t(this.value):a.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:a.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return i(t)?a.some(t):a.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);const u=t=>e=>i(e)&&t.test(e.nodeName),d=u(/^(OL|UL|DL)$/),g=u(/^(TH|TD)$/),h=t=>l(t)||"default"===t?"":t,c=(t,e)=>s=>{const r=r=>{s.setActive(((t,e,s)=>((t,e,s)=>{for(let e=0,n=t.length;ee.nodeName===s&&((t,e)=>t.dom.isChildOf(e,t.getBody()))(t,e))))(t,r.parents,e)),s.setEnabled(!((t,e)=>{const s=t.dom.getParent(e,"ol,ul,dl");return((t,e)=>null!==e&&!t.dom.isEditable(e))(t,s)})(t,r.element))};return t.on("NodeChange",r),()=>t.off("NodeChange",r)},m=(t,s,r,n,l,i)=>{i.length>1?((t,s,r,n,l,i)=>{t.ui.registry.addSplitButton(s,{tooltip:r,icon:"OL"===l?"ordered-list":"unordered-list",presets:"listpreview",columns:3,fetch:t=>{t(o.map(i,(t=>{const e="OL"===l?"num":"bull",s="disc"===t||"decimal"===t?"default":t,r=h(t),n=(t=>t.replace(/\-/g," ").replace(/\b\w/g,(t=>t.toUpperCase())))(t);return{type:"choiceitem",value:r,icon:"list-"+e+"-"+s,text:n}})))},onAction:()=>t.execCommand(n),onItemAction:(s,r)=>{e(t,l,r)},select:e=>{const s=(t=>{const e=t.dom.getParent(t.selection.getNode(),"ol,ul"),s=t.dom.getStyle(e,"listStyleType");return a.from(s)})(t);return s.map((t=>e===t)).getOr(!1)},onSetup:c(t,l)})})(t,s,r,n,l,i):((t,s,r,n,l,i)=>{t.ui.registry.addToggleButton(s,{active:!1,tooltip:r,icon:"OL"===l?"ordered-list":"unordered-list",onSetup:c(t,l),onAction:()=>t.queryCommandState(n)||""===i?t.execCommand(n):e(t,l,i)})})(t,s,r,n,l,h(i[0]))};t.add("advlist",(t=>{t.hasPlugin("lists")?((t=>{const e=t.options.register;e("advlist_number_styles",{processor:"string[]",default:"default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman".split(",")}),e("advlist_bullet_styles",{processor:"string[]",default:"default,circle,square".split(",")})})(t),(t=>{m(t,"numlist","Numbered list","InsertOrderedList","OL",r(t)),m(t,"bullist","Bullet list","InsertUnorderedList","UL",n(t))})(t),(t=>{t.addCommand("ApplyUnorderedListStyle",((s,r)=>{e(t,"UL",r["list-style-type"])})),t.addCommand("ApplyOrderedListStyle",((s,r)=>{e(t,"OL",r["list-style-type"])}))})(t)):console.error("Please use the Lists plugin together with the Advanced List plugin.")}))}(); -------------------------------------------------------------------------------- /public/libs/tinymce/plugins/anchor/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.4.1 (2023-03-29) 3 | */ 4 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),o=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=("allow_html_in_named_anchor",e=>e.options.get("allow_html_in_named_anchor"));const a="a:not([href])",r=e=>!e,i=e=>e.getAttribute("id")||e.getAttribute("name")||"",l=e=>(e=>"a"===e.nodeName.toLowerCase())(e)&&!e.getAttribute("href")&&""!==i(e),s=e=>e.dom.getParent(e.selection.getStart(),a),d=(e,a)=>{const r=s(e);r?((e,t,o)=>{o.removeAttribute("name"),o.id=t,e.addVisual(),e.undoManager.add()})(e,a,r):((e,a)=>{e.undoManager.transact((()=>{n(e)||e.selection.collapse(!0),e.selection.isCollapsed()?e.insertContent(e.dom.createHTML("a",{id:a})):((e=>{const n=e.dom;t(n).walk(e.selection.getRng(),(e=>{o.each(e,(e=>{var t;l(t=e)&&!t.firstChild&&n.remove(e,!1)}))}))})(e),e.formatter.remove("namedAnchor",void 0,void 0,!0),e.formatter.apply("namedAnchor",{value:a}),e.addVisual())}))})(e,a),e.focus()},c=e=>(e=>r(e.attr("href"))&&!r(e.attr("id")||e.attr("name")))(e)&&!e.firstChild,m=e=>t=>{for(let o=0;o{(e=>{(0,e.options.register)("allow_html_in_named_anchor",{processor:"boolean",default:!1})})(e),(e=>{e.on("PreInit",(()=>{e.parser.addNodeFilter("a",m("false")),e.serializer.addNodeFilter("a",m(null))}))})(e),(e=>{e.addCommand("mceAnchor",(()=>{(e=>{const t=(e=>{const t=s(e);return t?i(t):""})(e);e.windowManager.open({title:"Anchor",size:"normal",body:{type:"panel",items:[{name:"id",type:"input",label:"ID",placeholder:"example"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{id:t},onSubmit:t=>{((e,t)=>/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(t)?(d(e,t),!0):(e.windowManager.alert("ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),!1))(e,t.getData().id)&&t.close()}})})(e)}))})(e),(e=>{const t=()=>e.execCommand("mceAnchor");e.ui.registry.addToggleButton("anchor",{icon:"bookmark",tooltip:"Anchor",onAction:t,onSetup:t=>e.selection.selectorChangedWithUnbind("a:not([href])",t.setActive).unbind}),e.ui.registry.addMenuItem("anchor",{icon:"bookmark",text:"Anchor...",onAction:t})})(e),e.on("PreInit",(()=>{(e=>{e.formatter.register("namedAnchor",{inline:"a",selector:a,remove:"all",split:!0,deep:!0,attributes:{id:"%value"},onmatch:(e,t,o)=>l(e)})})(e)}))}))}(); -------------------------------------------------------------------------------- /public/libs/tinymce/plugins/autolink/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.4.1 (2023-03-29) 3 | */ 4 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>t.options.get(e),n=t("autolink_pattern"),o=t("link_default_target"),r=t("link_default_protocol"),a=t("allow_unsafe_link_target"),s=("string",e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=o=e,(r=String).prototype.isPrototypeOf(n)||(null===(a=o.constructor)||void 0===a?void 0:a.name)===r.name)?"string":t;var n,o,r,a})(e));const l=(void 0,e=>undefined===e);const i=e=>!(e=>null==e)(e),c=Object.hasOwnProperty,d=e=>"\ufeff"===e;var u=tinymce.util.Tools.resolve("tinymce.dom.TextSeeker");const f=e=>/^[(\[{ \u00a0]$/.test(e),g=(e,t,n)=>{for(let o=t-1;o>=0;o--){const t=e.charAt(o);if(!d(t)&&n(t))return o}return-1},m=(e,t)=>{var o;const a=e.schema.getVoidElements(),s=n(e),{dom:i,selection:d}=e;if(null!==i.getParent(d.getNode(),"a[href]"))return null;const m=d.getRng(),k=u(i,(e=>{return i.isBlock(e)||(t=a,n=e.nodeName.toLowerCase(),c.call(t,n))||"false"===i.getContentEditable(e);var t,n})),{container:p,offset:y}=((e,t)=>{let n=e,o=t;for(;1===n.nodeType&&n.childNodes[o];)n=n.childNodes[o],o=3===n.nodeType?n.data.length:n.childNodes.length;return{container:n,offset:o}})(m.endContainer,m.endOffset),h=null!==(o=i.getParent(p,i.isBlock))&&void 0!==o?o:i.getRoot(),w=k.backwards(p,y+t,((e,t)=>{const n=e.data,o=g(n,t,(r=f,e=>!r(e)));var r,a;return-1===o||(a=n[o],/[?!,.;:]/.test(a))?o:o+1}),h);if(!w)return null;let v=w.container;const _=k.backwards(w.container,w.offset,((e,t)=>{v=e;const n=g(e.data,t,f);return-1===n?n:n+1}),h),A=i.createRng();_?A.setStart(_.container,_.offset):A.setStart(v,0),A.setEnd(w.container,w.offset);const C=A.toString().replace(/\uFEFF/g,"").match(s);if(C){let t=C[0];return $="www.",(b=t).length>=$.length&&b.substr(0,0+$.length)===$?t=r(e)+"://"+t:((e,t,n=0,o)=>{const r=e.indexOf(t,n);return-1!==r&&(!!l(o)||r+t.length<=o)})(t,"@")&&!(e=>/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(e))(t)&&(t="mailto:"+t),{rng:A,url:t}}var b,$;return null},k=(e,t)=>{const{dom:n,selection:r}=e,{rng:l,url:i}=t,c=r.getBookmark();r.setRng(l);const d="createlink",u={command:d,ui:!1,value:i};if(!e.dispatch("BeforeExecCommand",u).isDefaultPrevented()){e.getDoc().execCommand(d,!1,i),e.dispatch("ExecCommand",u);const t=o(e);if(s(t)){const o=r.getNode();n.setAttrib(o,"target",t),"_blank"!==t||a(e)||n.setAttrib(o,"rel","noopener")}}r.moveToBookmark(c),e.nodeChanged()},p=e=>{const t=m(e,-1);i(t)&&k(e,t)},y=p;e.add("autolink",(e=>{(e=>{const t=e.options.register;t("autolink_pattern",{processor:"regexp",default:new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i")}),t("link_default_target",{processor:"string"}),t("link_default_protocol",{processor:"string",default:"https"})})(e),(e=>{e.on("keydown",(t=>{13!==t.keyCode||t.isDefaultPrevented()||(e=>{const t=m(e,0);i(t)&&k(e,t)})(e)})),e.on("keyup",(t=>{32===t.keyCode?p(e):(48===t.keyCode&&t.shiftKey||221===t.keyCode)&&y(e)}))})(e)}))}(); -------------------------------------------------------------------------------- /public/libs/tinymce/plugins/autoresize/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.4.1 (2023-03-29) 3 | */ 4 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.Env");const o=e=>t=>t.options.get(e),s=o("min_height"),i=o("max_height"),n=o("autoresize_overflow_padding"),r=o("autoresize_bottom_margin"),l=(e,t)=>{const o=e.getBody();o&&(o.style.overflowY=t?"":"hidden",t||(o.scrollTop=0))},g=(e,t,o,s)=>{var i;const n=parseInt(null!==(i=e.getStyle(t,o,s))&&void 0!==i?i:"",10);return isNaN(n)?0:n},a=(e,o,r,c)=>{var d;const f=e.dom,u=e.getDoc();if(!u)return;if((e=>e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen())(e))return void l(e,!0);const m=u.documentElement,h=c?c():n(e),p=null!==(d=s(e))&&void 0!==d?d:e.getElement().offsetHeight;let y=p;const S=g(f,m,"margin-top",!0),v=g(f,m,"margin-bottom",!0);let C=m.offsetHeight+S+v+h;C<0&&(C=0);const b=e.getContainer().offsetHeight-e.getContentAreaContainer().offsetHeight;C+b>p&&(y=C+b);const w=i(e);if(w&&y>w?(y=w,l(e,!0)):l(e,!1),y!==o.get()){const s=y-o.get();if(f.setStyle(e.getContainer(),"height",y+"px"),o.set(y),(e=>{e.dispatch("ResizeEditor")})(e),t.browser.isSafari()&&(t.os.isMacOS()||t.os.isiOS())){const t=e.getWin();t.scrollTo(t.pageXOffset,t.pageYOffset)}e.hasFocus()&&(e=>{if("setcontent"===(null==e?void 0:e.type.toLowerCase())){const t=e;return!0===t.selection||!0===t.paste}return!1})(r)&&e.selection.scrollIntoView(),(t.browser.isSafari()||t.browser.isChromium())&&s<0&&a(e,o,r,c)}};e.add("autoresize",(e=>{if((e=>{const t=e.options.register;t("autoresize_overflow_padding",{processor:"number",default:1}),t("autoresize_bottom_margin",{processor:"number",default:50})})(e),e.options.isSet("resize")||e.options.set("resize",!1),!e.inline){const o=(e=>{let t=0;return{get:()=>t,set:e=>{t=e}}})();((e,t)=>{e.addCommand("mceAutoResize",(()=>{a(e,t)}))})(e,o),((e,o)=>{let s,i,l=()=>r(e);e.on("init",(i=>{s=0;const r=n(e),g=e.dom;g.setStyles(e.getDoc().documentElement,{height:"auto"}),t.browser.isEdge()||t.browser.isIE()?g.setStyles(e.getBody(),{paddingLeft:r,paddingRight:r,"min-height":0}):g.setStyles(e.getBody(),{paddingLeft:r,paddingRight:r}),a(e,o,i,l),s+=1})),e.on("NodeChange SetContent keyup FullscreenStateChanged ResizeContent",(t=>{if(1===s)i=e.getContainer().offsetHeight,a(e,o,t,l),s+=1;else if(2===s){const t=i0):l,s+=1}else a(e,o,t,l)}))})(e,o)}}))}(); -------------------------------------------------------------------------------- /public/libs/tinymce/plugins/autosave/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.4.1 (2023-03-29) 3 | */ 4 | !function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=("string",t=>"string"===(t=>{const e=typeof t;return null===t?"null":"object"===e&&Array.isArray(t)?"array":"object"===e&&(r=o=t,(a=String).prototype.isPrototypeOf(r)||(null===(s=o.constructor)||void 0===s?void 0:s.name)===a.name)?"string":e;var r,o,a,s})(t));const r=(void 0,t=>undefined===t);var o=tinymce.util.Tools.resolve("tinymce.util.Delay"),a=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),s=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=t=>{const e=/^(\d+)([ms]?)$/.exec(t);return(e&&e[2]?{s:1e3,m:6e4}[e[2]]:1)*parseInt(t,10)},i=t=>e=>e.options.get(t),u=i("autosave_ask_before_unload"),l=i("autosave_restore_when_empty"),c=i("autosave_interval"),d=i("autosave_retention"),m=t=>{const e=document.location;return t.options.get("autosave_prefix").replace(/{path}/g,e.pathname).replace(/{query}/g,e.search).replace(/{hash}/g,e.hash).replace(/{id}/g,t.id)},v=(t,e)=>{if(r(e))return t.dom.isEmpty(t.getBody());{const r=s.trim(e);if(""===r)return!0;{const e=(new DOMParser).parseFromString(r,"text/html");return t.dom.isEmpty(e)}}},f=t=>{var e;const r=parseInt(null!==(e=a.getItem(m(t)+"time"))&&void 0!==e?e:"0",10)||0;return!((new Date).getTime()-r>d(t)&&(p(t,!1),1))},p=(t,e)=>{const r=m(t);a.removeItem(r+"draft"),a.removeItem(r+"time"),!1!==e&&(t=>{t.dispatch("RemoveDraft")})(t)},g=t=>{const e=m(t);!v(t)&&t.isDirty()&&(a.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),a.setItem(e+"time",(new Date).getTime().toString()),(t=>{t.dispatch("StoreDraft")})(t))},y=t=>{var e;const r=m(t);f(t)&&(t.setContent(null!==(e=a.getItem(r+"draft"))&&void 0!==e?e:"",{format:"raw"}),(t=>{t.dispatch("RestoreDraft")})(t))};var D=tinymce.util.Tools.resolve("tinymce.EditorManager");const h=t=>e=>{e.setEnabled(f(t));const r=()=>e.setEnabled(f(t));return t.on("StoreDraft RestoreDraft RemoveDraft",r),()=>t.off("StoreDraft RestoreDraft RemoveDraft",r)};t.add("autosave",(t=>((t=>{const r=t.options.register,o=t=>{const r=e(t);return r?{value:n(t),valid:r}:{valid:!1,message:"Must be a string."}};r("autosave_ask_before_unload",{processor:"boolean",default:!0}),r("autosave_prefix",{processor:"string",default:"tinymce-autosave-{path}{query}{hash}-{id}-"}),r("autosave_restore_when_empty",{processor:"boolean",default:!1}),r("autosave_interval",{processor:o,default:"30s"}),r("autosave_retention",{processor:o,default:"20m"})})(t),(t=>{t.editorManager.on("BeforeUnload",(t=>{let e;s.each(D.get(),(t=>{t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&u(t)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))})),e&&(t.preventDefault(),t.returnValue=e)}))})(t),(t=>{(t=>{const e=c(t);o.setEditorInterval(t,(()=>{g(t)}),e)})(t);const e=()=>{(t=>{t.undoManager.transact((()=>{y(t),p(t)})),t.focus()})(t)};t.ui.registry.addButton("restoredraft",{tooltip:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)}),t.ui.registry.addMenuItem("restoredraft",{text:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)})})(t),t.on("init",(()=>{l(t)&&t.dom.isEmpty(t.getBody())&&y(t)})),(t=>({hasDraft:()=>f(t),storeDraft:()=>g(t),restoreDraft:()=>y(t),removeDraft:e=>p(t,e),isEmpty:e=>v(t,e)}))(t))))}(); -------------------------------------------------------------------------------- /public/libs/tinymce/plugins/code/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.4.1 (2023-03-29) 3 | */ 4 | !function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",(e=>((e=>{e.addCommand("mceCodeEditor",(()=>{(e=>{const o=(e=>e.getContent({source_view:!0}))(e);e.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:o},onSubmit:o=>{((e,o)=>{e.focus(),e.undoManager.transact((()=>{e.setContent(o)})),e.selection.setCursorLocation(),e.nodeChanged()})(e,o.getData().code),o.close()}})})(e)}))})(e),(e=>{const o=()=>e.execCommand("mceCodeEditor");e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:o}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:o})})(e),{})))}(); -------------------------------------------------------------------------------- /public/libs/tinymce/plugins/insertdatetime/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.4.1 (2023-03-29) 3 | */ 4 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>t.options.get(e),a=t("insertdatetime_dateformat"),r=t("insertdatetime_timeformat"),n=t("insertdatetime_formats"),s=t("insertdatetime_element"),i="Sun Mon Tue Wed Thu Fri Sat Sun".split(" "),o="Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(" "),l="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),m="January February March April May June July August September October November December".split(" "),c=(e,t)=>{if((e=""+e).length(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=t.replace("%D","%m/%d/%Y")).replace("%r","%I:%M:%S %p")).replace("%Y",""+a.getFullYear())).replace("%y",""+a.getYear())).replace("%m",c(a.getMonth()+1,2))).replace("%d",c(a.getDate(),2))).replace("%H",""+c(a.getHours(),2))).replace("%M",""+c(a.getMinutes(),2))).replace("%S",""+c(a.getSeconds(),2))).replace("%I",""+((a.getHours()+11)%12+1))).replace("%p",a.getHours()<12?"AM":"PM")).replace("%B",""+e.translate(m[a.getMonth()]))).replace("%b",""+e.translate(l[a.getMonth()]))).replace("%A",""+e.translate(o[a.getDay()]))).replace("%a",""+e.translate(i[a.getDay()]))).replace("%%","%"),u=(e,t)=>{if(s(e)){const a=d(e,t);let r;r=/%[HMSIp]/.test(t)?d(e,"%Y-%m-%dT%H:%M"):d(e,"%Y-%m-%d");const n=e.dom.getParent(e.selection.getStart(),"time");n?((e,t,a,r)=>{const n=e.dom.create("time",{datetime:a},r);e.dom.replace(n,t),e.selection.select(n,!0),e.selection.collapse(!1)})(e,n,r,a):e.insertContent('")}else e.insertContent(d(e,t))};var p=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("insertdatetime",(e=>{(e=>{const t=e.options.register;t("insertdatetime_dateformat",{processor:"string",default:e.translate("%Y-%m-%d")}),t("insertdatetime_timeformat",{processor:"string",default:e.translate("%H:%M:%S")}),t("insertdatetime_formats",{processor:"string[]",default:["%H:%M:%S","%Y-%m-%d","%I:%M:%S %p","%D"]}),t("insertdatetime_element",{processor:"boolean",default:!1})})(e),(e=>{e.addCommand("mceInsertDate",((t,r)=>{u(e,null!=r?r:a(e))})),e.addCommand("mceInsertTime",((t,a)=>{u(e,null!=a?a:r(e))}))})(e),(e=>{const t=n(e),a=(e=>{let t=e;return{get:()=>t,set:e=>{t=e}}})((e=>{const t=n(e);return t.length>0?t[0]:r(e)})(e)),s=t=>e.execCommand("mceInsertDate",!1,t);e.ui.registry.addSplitButton("insertdatetime",{icon:"insert-time",tooltip:"Insert date/time",select:e=>e===a.get(),fetch:a=>{a(p.map(t,(t=>({type:"choiceitem",text:d(e,t),value:t}))))},onAction:e=>{s(a.get())},onItemAction:(e,t)=>{a.set(t),s(t)}});const i=e=>()=>{a.set(e),s(e)};e.ui.registry.addNestedMenuItem("insertdatetime",{icon:"insert-time",text:"Date/time",getSubmenuItems:()=>p.map(t,(t=>({type:"menuitem",text:d(e,t),onAction:i(t)})))})})(e)}))}(); -------------------------------------------------------------------------------- /public/libs/tinymce/plugins/nonbreaking/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.4.1 (2023-03-29) 3 | */ 4 | !function(){"use strict";var n=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=n=>e=>typeof e===n,a=e("boolean"),o=e("number"),t=n=>e=>e.options.get(n),i=t("nonbreaking_force_tab"),r=t("nonbreaking_wrap"),s=(n,e)=>{let a="";for(let o=0;o{const a=r(n)||n.plugins.visualchars?`${s(" ",e)}`:s(" ",e);n.undoManager.transact((()=>n.insertContent(a)))};var l=tinymce.util.Tools.resolve("tinymce.util.VK");n.add("nonbreaking",(n=>{(n=>{const e=n.options.register;e("nonbreaking_force_tab",{processor:n=>a(n)?{value:n?3:0,valid:!0}:o(n)?{value:n,valid:!0}:{valid:!1,message:"Must be a boolean or number."},default:!1}),e("nonbreaking_wrap",{processor:"boolean",default:!0})})(n),(n=>{n.addCommand("mceNonBreaking",(()=>{c(n,1)}))})(n),(n=>{const e=()=>n.execCommand("mceNonBreaking");n.ui.registry.addButton("nonbreaking",{icon:"non-breaking",tooltip:"Nonbreaking space",onAction:e}),n.ui.registry.addMenuItem("nonbreaking",{icon:"non-breaking",text:"Nonbreaking space",onAction:e})})(n),(n=>{const e=i(n);e>0&&n.on("keydown",(a=>{if(a.keyCode===l.TAB&&!a.isDefaultPrevented()){if(a.shiftKey)return;a.preventDefault(),a.stopImmediatePropagation(),c(n,e)}}))})(n)}))}(); -------------------------------------------------------------------------------- /public/libs/tinymce/plugins/pagebreak/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.4.1 (2023-03-29) 3 | */ 4 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.Env");const t=e=>a=>a.options.get(e),r=t("pagebreak_separator"),n=t("pagebreak_split_block"),o="mce-pagebreak",s=e=>{const t=``;return e?`

${t}

`:t};e.add("pagebreak",(e=>{(e=>{const a=e.options.register;a("pagebreak_separator",{processor:"string",default:"\x3c!-- pagebreak --\x3e"}),a("pagebreak_split_block",{processor:"boolean",default:!1})})(e),(e=>{e.addCommand("mcePageBreak",(()=>{e.insertContent(s(n(e)))}))})(e),(e=>{const a=()=>e.execCommand("mcePageBreak");e.ui.registry.addButton("pagebreak",{icon:"page-break",tooltip:"Page break",onAction:a}),e.ui.registry.addMenuItem("pagebreak",{text:"Page break",icon:"page-break",onAction:a})})(e),(e=>{const a=r(e),t=()=>n(e),c=new RegExp(a.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,(e=>"\\"+e)),"gi");e.on("BeforeSetContent",(e=>{e.content=e.content.replace(c,s(t()))})),e.on("PreInit",(()=>{e.serializer.addNodeFilter("img",(r=>{let n,s,c=r.length;for(;c--;)if(n=r[c],s=n.attr("class"),s&&-1!==s.indexOf(o)){const r=n.parent;if(r&&e.schema.getBlockElements()[r.name]&&t()){r.type=3,r.value=a,r.raw=!0,n.remove();continue}n.type=3,n.value=a,n.raw=!0}}))}))})(e),(e=>{e.on("ResolveName",(a=>{"IMG"===a.target.nodeName&&e.dom.hasClass(a.target,o)&&(a.name="pagebreak")}))})(e)}))}(); -------------------------------------------------------------------------------- /public/libs/tinymce/plugins/preview/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.4.1 (2023-03-29) 3 | */ 4 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.Env"),o=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=e=>t=>t.options.get(e),i=n("content_style"),s=n("content_css_cors"),c=n("body_class"),r=n("body_id");e.add("preview",(e=>{(e=>{e.addCommand("mcePreview",(()=>{(e=>{const n=(e=>{var n;let l="";const a=e.dom.encode,d=null!==(n=i(e))&&void 0!==n?n:"";l+='';const m=s(e)?' crossorigin="anonymous"':"";o.each(e.contentCSS,(t=>{l+='"})),d&&(l+='");const y=r(e),u=c(e),v=' 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/apis/index.js: -------------------------------------------------------------------------------- 1 | const modules = import.meta.glob('./modules/*.js', { eager: true }) 2 | 3 | const api = {} 4 | 5 | Object.keys(modules).forEach((key) => { 6 | const name = key.slice(key.lastIndexOf('/') + 1, key.lastIndexOf('.')) 7 | api[name] = { ...modules[key] } 8 | }) 9 | 10 | export default api 11 | -------------------------------------------------------------------------------- /src/apis/modules/common.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取地区 4 | export const getRegion = (params) => request.basic.get('/region', params) 5 | 6 | // 获取 验证码ID 7 | export const getCaptcha = (params) => request.basic.get('/api/v1/captcha/id', params) 8 | -------------------------------------------------------------------------------- /src/apis/modules/menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 菜单接口 3 | */ 4 | import request from '@/utils/request' 5 | // 获取菜单列表 6 | export const getMenuList = (params) => request.basic.get('/api/v1/menus', params) 7 | // 获取菜单条数据 8 | export const getMenu = (id) => request.basic.get(`/api/v1/menus/${id}`) 9 | // 添加菜单 10 | export const createMenu = (params) => request.basic.post('/api/v1/menus', params) 11 | // 更新菜单 12 | export const updateMenu = (id, params) => request.basic.put(`/api/v1/menus/${id}`, params) 13 | // 删除菜单 14 | export const delMenu = (id) => request.basic.delete(`/api/v1/menus/${id}`) 15 | -------------------------------------------------------------------------------- /src/apis/modules/role.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 权限接口 3 | */ 4 | import request from '@/utils/request' 5 | // 获取role列表 6 | export const getRoleList = (params) => request.basic.get('/api/v1/roles', params) 7 | // 获取role条数据 8 | export const getRole = (id) => request.basic.get(`/api/v1/roles/${id}`) 9 | // 添加role 10 | export const createRole = (params) => request.basic.post('/api/v1/roles', params) 11 | // 更新role 12 | export const updateRole = (id, params) => request.basic.put(`/api/v1/roles/${id}`, params) 13 | // 删除role 14 | export const delRole = (id) => request.basic.delete(`/api/v1/roles/${id}`) 15 | -------------------------------------------------------------------------------- /src/apis/modules/system.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取日志列表 4 | export const getLoggers = (params) => request.basic.get('/api/v1/loggers', params) 5 | -------------------------------------------------------------------------------- /src/apis/modules/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 登录 4 | export const login = (params) => request.basic.post('/api/v1/login', params) 5 | // 获取用户详情 6 | export const getUserDetail = () => request.basic.get('/api/v1/current/user') 7 | // 更新用户信息 8 | export const updateUser = (_, params) => request.basic.put(`/api/v1/current/user`, params) 9 | 10 | // 更新用户密码 11 | export const updatePassword = (_, params) => request.basic.put(`/api/v1/current/password`, params) 12 | // 用户权限菜单 13 | export const getUserMenu = () => request.basic.get('/api/v1/current/menus') 14 | -------------------------------------------------------------------------------- /src/apis/modules/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 管理员角色 3 | */ 4 | 5 | import request from '@/utils/request' 6 | // 获取管理员列表 7 | export const getUsersList = (params) => request.basic.get('/api/v1/users', params) 8 | // 获取管理员条数据 9 | export const getUsers = (id) => request.basic.get(`/api/v1/users/${id}`) 10 | // 添加管理员 11 | export const createUsers = (params) => request.basic.post('/api/v1/users', params) 12 | // 更新管理员 13 | export const updateUsers = (id, params) => request.basic.put(`/api/v1/users/${id}`, params) 14 | // 删除管理员 15 | export const delUsers = (id) => request.basic.delete(`/api/v1/users/${id}`) 16 | -------------------------------------------------------------------------------- /src/assets/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin-admin/gin-admin-vue/c3cc921e708e8e7e5282deb3550ffe426276b94a/src/assets/avatar.jpg -------------------------------------------------------------------------------- /src/assets/cropper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin-admin/gin-admin-vue/c3cc921e708e8e7e5282deb3550ffe426276b94a/src/assets/cropper.png -------------------------------------------------------------------------------- /src/assets/login_aside_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin-admin/gin-admin-vue/c3cc921e708e8e7e5282deb3550ffe426276b94a/src/assets/login_aside_bg.jpg -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/logos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin-admin/gin-admin-vue/c3cc921e708e8e7e5282deb3550ffe426276b94a/src/assets/logos.png -------------------------------------------------------------------------------- /src/components/ActionBar/ActionBar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/ActionButton/ActionButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 40 | 41 | 62 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 33 | 34 | 43 | -------------------------------------------------------------------------------- /src/components/Chart/Chart.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 91 | 92 | 98 | -------------------------------------------------------------------------------- /src/components/Cropper/CropperDialog.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/components/Editor/Editor.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 111 | 112 | 115 | 116 | 127 | -------------------------------------------------------------------------------- /src/components/Filter/FilterTagItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 56 | 57 | 76 | -------------------------------------------------------------------------------- /src/components/Filter/config.js: -------------------------------------------------------------------------------- 1 | export const FILTER_KEY = Symbol('filter') 2 | 3 | export const FILTER_ITEM_DATA_SOURCE_KEY = Symbol('filterItemDataSource') 4 | 5 | export const FILTER_TAG_KEY = Symbol('filterTag') 6 | 7 | export const FILTER_TAG_SELECTED_VALUE_KEY = Symbol('filterTagSelectedValue') 8 | -------------------------------------------------------------------------------- /src/components/Filter/context.js: -------------------------------------------------------------------------------- 1 | import { inject, provide } from 'vue' 2 | 3 | import { FILTER_ITEM_DATA_SOURCE_KEY, FILTER_KEY, FILTER_TAG_KEY, FILTER_TAG_SELECTED_VALUE_KEY } from './config' 4 | 5 | export const useFilterCtx = (props) => { 6 | provide(FILTER_KEY, props) 7 | } 8 | 9 | export const useInjectFilterCtx = () => { 10 | return inject(FILTER_KEY, { 11 | onChange: () => {}, 12 | }) 13 | } 14 | 15 | export const useFilterItemDataSourceCtx = (props) => { 16 | provide(FILTER_ITEM_DATA_SOURCE_KEY, props) 17 | } 18 | 19 | export const useInjectFilterItemDataSourceCtx = () => { 20 | return inject(FILTER_ITEM_DATA_SOURCE_KEY) 21 | } 22 | 23 | export const useFilterTagCtx = (props) => { 24 | provide(FILTER_TAG_KEY, props) 25 | } 26 | 27 | export const useInjectFilterTagCtx = () => { 28 | return inject(FILTER_TAG_KEY, { 29 | multiple: false, 30 | onTagClick: () => {}, 31 | }) 32 | } 33 | 34 | export const useFilterTagSelectedValueCtx = (props) => { 35 | provide(FILTER_TAG_SELECTED_VALUE_KEY, props) 36 | } 37 | 38 | export const useInjectFilterTagSelectedValueCtx = () => { 39 | return inject(FILTER_TAG_SELECTED_VALUE_KEY, []) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/FormTable/FormTable.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 93 | 94 | 129 | -------------------------------------------------------------------------------- /src/components/Loading/directive.js: -------------------------------------------------------------------------------- 1 | import { createVNode, render } from 'vue' 2 | 3 | import LoadingConstructor from './Loading.vue' 4 | 5 | function show(el) { 6 | hide(el) 7 | const container = document.createElement('div') 8 | const props = { 9 | type: 'directive', 10 | } 11 | const title = el.getAttribute('x-loading-title') 12 | if (title) { 13 | props.title = title 14 | } 15 | const vnode = createVNode(LoadingConstructor, props) 16 | render(vnode, container) 17 | container.classList.add('x-loading-container') 18 | el.classList.add('x-loading-wrap') 19 | el.style.position = 'relative' 20 | el.style.overflow = 'hidden' 21 | el.appendChild(container) 22 | } 23 | 24 | function hide(el) { 25 | el.classList.remove('x-loading-wrap') 26 | el.querySelector('.x-loading-container')?.remove() 27 | el.style.position = '' 28 | el.style.overflow = '' 29 | } 30 | 31 | const loadingDirective = { 32 | mounted: (el, binding) => { 33 | if (!binding?.value) return 34 | show(el) 35 | }, 36 | updated: (el, binding) => { 37 | if (binding?.value === binding?.oldValue) return 38 | binding?.value ? show(el) : hide(el) 39 | }, 40 | beforeUnmount: (el) => { 41 | hide(el) 42 | }, 43 | } 44 | 45 | export const setupLoadingDirective = (app) => { 46 | app.directive('loading', loadingDirective) 47 | return app 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Loading/index.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import LoadingConstructor from './Loading.vue' 4 | 5 | let container = null 6 | let app = null 7 | 8 | /** 9 | * 返回 10 | */ 11 | function popstateListener() { 12 | hide() 13 | } 14 | 15 | /** 16 | * 打开 17 | * @param {object} props 18 | */ 19 | function show(props) { 20 | hide() 21 | container = document.createElement('div') 22 | app = createApp(LoadingConstructor, props) 23 | const vm = app.mount(container) 24 | document.body.appendChild(container) 25 | 26 | window.addEventListener('popstate', popstateListener) 27 | 28 | return vm 29 | } 30 | 31 | /** 32 | * 隐藏 33 | */ 34 | function hide() { 35 | if (app) { 36 | app.unmount(container) 37 | } 38 | if (container) { 39 | container.remove() 40 | } 41 | container = null 42 | app = null 43 | 44 | window.removeEventListener('popstate', popstateListener) 45 | } 46 | 47 | const Loading = (props) => { 48 | const vm = show(props) 49 | return { 50 | ...vm, 51 | hide, 52 | } 53 | } 54 | 55 | Loading.hide = hide 56 | 57 | export default Loading 58 | -------------------------------------------------------------------------------- /src/components/Preview/config.js: -------------------------------------------------------------------------------- 1 | import Enum from 'xy-enum' 2 | 3 | export const ACTION_ENUM = new Enum([ 4 | { label: 'zoomOut', value: 'zoomOut', desc: '缩小' }, 5 | { label: 'zoomIn', value: 'zoomIn', desc: '放大' }, 6 | { label: 'fullscreen', value: 'fullscreen', desc: '全屏' }, 7 | { label: 'rotateLeft', value: 'rotateLeft', desc: '向左旋转' }, 8 | { label: 'rotateRight', value: 'rotateRight', desc: '向右旋转' }, 9 | { label: 'prev', value: 'prev', desc: '上一个' }, 10 | { label: 'next', value: 'next', desc: '下一个' }, 11 | ]) 12 | -------------------------------------------------------------------------------- /src/components/Preview/index.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import PreviewConstructor from './Preview.vue' 4 | 5 | let container = null 6 | let app = null 7 | let vm = null 8 | 9 | /** 10 | * 返回 11 | */ 12 | function popstateListener() { 13 | close() 14 | } 15 | 16 | function open(payload, index) { 17 | close() 18 | let props = { 19 | index: typeof index === 'number' ? index : 0, 20 | } 21 | container = document.createElement('div') 22 | if (typeof payload === 'string') { 23 | props.urls = [payload] 24 | } 25 | if (Array.isArray(payload)) { 26 | props.urls = payload 27 | } 28 | if (Object.prototype.toString.call(payload) === '[object Object]') { 29 | props = payload 30 | } 31 | app = createApp(PreviewConstructor, { 32 | ...props, 33 | afterClose: close, 34 | }) 35 | vm = app.mount(container) 36 | document.body.appendChild(container) 37 | vm.open = true 38 | window.addEventListener('popstate', popstateListener) 39 | } 40 | 41 | function close() { 42 | if (app) { 43 | app.unmount() 44 | vm.open = false 45 | } 46 | if (container) { 47 | container.remove() 48 | } 49 | container = null 50 | app = null 51 | vm = null 52 | 53 | window.removeEventListener('popstate', popstateListener) 54 | } 55 | 56 | const Preview = open 57 | 58 | Preview.close = close 59 | 60 | export default Preview 61 | -------------------------------------------------------------------------------- /src/components/ResizeBox/config.js: -------------------------------------------------------------------------------- 1 | import Enum from 'xy-enum' 2 | 3 | export const DIRECTION_LEFT = 'left' 4 | export const DIRECTION_RIGHT = 'right' 5 | export const DIRECTION_TOP = 'top' 6 | export const DIRECTION_BOTTOM = 'bottom' 7 | 8 | // 方向 9 | export const directionEnum = new Enum([ 10 | { key: 'top', value: 'top', desc: '上' }, 11 | { key: 'bottom', value: 'bottom', desc: '下' }, 12 | { key: 'left', value: 'left', desc: '左' }, 13 | { key: 'right', value: 'right', desc: '右' }, 14 | ]) 15 | -------------------------------------------------------------------------------- /src/components/Scrollbar/Scrollbar.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/components/SearchBar/SearchBar.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | 33 | 53 | -------------------------------------------------------------------------------- /src/components/Upload/UploadInput.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 120 | 121 | 135 | -------------------------------------------------------------------------------- /src/components/Upload/config.js: -------------------------------------------------------------------------------- 1 | import Enum from 'xy-enum' 2 | 3 | // 状态 4 | export const STATUS_ENUM = new Enum([ 5 | { key: 'wait', value: 1, desc: '等待上传' }, 6 | { key: 'uploading', value: 2, desc: '上传中' }, 7 | { key: 'done', value: 3, desc: '上传完成' }, 8 | { key: 'error', value: 4, desc: '上传错误' }, 9 | { key: 'removed', value: 5, desc: '已移除' }, 10 | ]) 11 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import ActionBar from './ActionBar/ActionBar.vue' 2 | import ActionButton from './ActionButton/ActionButton.vue' 3 | import Breadcrumb from './Breadcrumb/Breadcrumb.vue' 4 | import Chart from './Chart/Chart.vue' 5 | import Cropper from './Cropper/Cropper.vue' 6 | import CropperDialog from './Cropper/CropperDialog.vue' 7 | import Editor from './Editor/Editor.vue' 8 | import Filter from './Filter/Filter.vue' 9 | import FilterItem from './Filter/FilterItem.vue' 10 | import FilterTag from './Filter/FilterTag.vue' 11 | import FilterTagItem from './Filter/FilterTagItem.vue' 12 | import FormTable from './FormTable/FormTable.vue' 13 | import Loading from './Loading' 14 | import Preview from './Preview' 15 | import QrCode from './QrCode/QrCode.vue' 16 | import ResizeBox from './ResizeBox/ResizeBox.vue' 17 | import SearchBar from './SearchBar/SearchBar.vue' 18 | import UploadImage from './Upload/UploadImage.vue' 19 | import UploadInput from './Upload/UploadInput.vue' 20 | import Scrollbar from './Scrollbar/Scrollbar.vue' 21 | import Cascader from './Cascader/Cascader.vue' 22 | import { setupLoadingDirective } from './Loading/directive' 23 | 24 | const componentList = [ 25 | ActionBar, 26 | ActionButton, 27 | Breadcrumb, 28 | Chart, 29 | Cropper, 30 | CropperDialog, 31 | Editor, 32 | Filter, 33 | FilterItem, 34 | FilterTag, 35 | FilterTagItem, 36 | FormTable, 37 | QrCode, 38 | ResizeBox, 39 | SearchBar, 40 | UploadImage, 41 | UploadInput, 42 | Scrollbar, 43 | Cascader, 44 | ] 45 | 46 | export const loading = Loading 47 | export const preview = Preview 48 | 49 | export default { 50 | install(app) { 51 | componentList.forEach((component) => { 52 | app.component(component.name, component) 53 | }) 54 | 55 | app.config.globalProperties.$loading = Loading 56 | app.config.globalProperties.$preview = Preview 57 | 58 | setupLoadingDirective(app) 59 | 60 | return app 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /src/config/app.js: -------------------------------------------------------------------------------- 1 | import { env } from '@/utils/util' 2 | 3 | export default { 4 | title: env('title'), 5 | logo: `${import.meta.env.BASE_URL}images/logo.svg`, 6 | mock: env('mock'), 7 | permission: env('permission'), 8 | } 9 | -------------------------------------------------------------------------------- /src/config/http.js: -------------------------------------------------------------------------------- 1 | import { env } from '@/utils/util' 2 | 3 | export default { 4 | apiBasic: env('apiBasic'), 5 | code: { 6 | success: true, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash-es' 2 | 3 | const files = import.meta.glob('./*.js', { eager: true }) 4 | 5 | const configs = {} 6 | 7 | Object.keys(files).forEach((key) => { 8 | const name = key.slice(key.lastIndexOf('/') + 1, key.lastIndexOf('.')) 9 | if ('index' !== name) { 10 | configs[name] = { ...(files[key]?.default || {}) } 11 | } 12 | }) 13 | 14 | export const config = (key, def) => get(configs, key, def) 15 | -------------------------------------------------------------------------------- /src/config/router.js: -------------------------------------------------------------------------------- 1 | import { env } from '@/utils/util' 2 | 3 | export default { 4 | base: env('router_base'), 5 | history: env('router_history'), 6 | } 7 | -------------------------------------------------------------------------------- /src/config/storage.js: -------------------------------------------------------------------------------- 1 | import { env } from '@/utils/util' 2 | 3 | export default { 4 | namespace: env('storageNamespace'), 5 | isLogin: 'is_login', 6 | token: 'token', 7 | userInfo: 'user_info', 8 | permission: 'permission', 9 | config: 'config', 10 | lang: 'lang', 11 | } 12 | -------------------------------------------------------------------------------- /src/core/exception.js: -------------------------------------------------------------------------------- 1 | export const setupException = (app) => { 2 | app.config.errorHandler = (err) => { 3 | console.error(err) 4 | } 5 | return app 6 | } 7 | -------------------------------------------------------------------------------- /src/core/index.js: -------------------------------------------------------------------------------- 1 | import antd from 'ant-design-vue' 2 | import component from '@/components' 3 | import i18n from '@/locales' 4 | import { setupRouter } from '@/router' 5 | import { setupStore } from '@/store' 6 | import { setupException } from './exception' 7 | import { setupDirective } from '@/directives' 8 | 9 | import './permission' 10 | 11 | import 'ant-design-vue/dist/reset.css' 12 | import '@/styles/index.less' 13 | 14 | export const useCore = (app) => { 15 | app.use(antd) 16 | app.use(component) 17 | app.use(i18n) 18 | setupException(app) 19 | setupStore(app) 20 | setupRouter(app) 21 | setupDirective(app) 22 | } 23 | -------------------------------------------------------------------------------- /src/core/permission.js: -------------------------------------------------------------------------------- 1 | import { createProgress } from '@/plugins/progress' 2 | import router from '@/router' 3 | import { whiteList } from '@/router/config' 4 | import { useAppStore, useUserStore } from '@/store' 5 | 6 | const progress = createProgress() 7 | 8 | router.beforeEach((to, from, next) => { 9 | const { meta } = to 10 | const { title } = meta 11 | const appStore = useAppStore() 12 | const userStore = useUserStore() 13 | const isLogin = userStore.isLogin 14 | const complete = appStore.complete 15 | 16 | progress.start() 17 | 18 | // 设置标题 19 | document.title = title ? `${title} - ${import.meta.env.VITE_TITLE}` : import.meta.env.VITE_TITLE 20 | 21 | if (whiteList.includes(to.name)) { 22 | // 在白名单 23 | next() 24 | } else { 25 | // 判断当前登录状态 26 | if (isLogin) { 27 | // 已登录 28 | if (complete) { 29 | // 初始化完成 30 | next() 31 | } else { 32 | // 初始化未加载完成 33 | appStore.init().then(() => { 34 | next({ ...to, replace: true }) 35 | }) 36 | } 37 | } else { 38 | // 未登录 39 | next({ 40 | name: 'login', 41 | replace: true, 42 | query: { redirect: encodeURIComponent(location.href) }, 43 | }) 44 | } 45 | } 46 | }) 47 | 48 | router.afterEach(() => { 49 | progress.done() 50 | }) 51 | -------------------------------------------------------------------------------- /src/directives/action.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Action 3 | * @description 权限 4 | * @example v-action:action || v-action="'action'" || v-action="['action1', 'action2']" 5 | * @type {{mounted: actionDirective.mounted}} 6 | */ 7 | 8 | import router from '@/router' 9 | 10 | const action = { 11 | mounted: (el, binding) => { 12 | // const route = router.currentRoute.value 13 | // const actions = route?.meta?.actions ?? [] 14 | // const actionName = binding.arg || binding.value 15 | // 16 | // if (route?.meta?.actions.includes('*')) return 17 | // 18 | // if (!actionName) return 19 | // if (Array.isArray(actionName)) { 20 | // // 多个权限 21 | // if (!actions.some((value) => actionName.includes(value))) { 22 | // ;(el.parentNode && el.parentNode.removeChild(el)) || (el.style.display = 'none') 23 | // } 24 | // } else { 25 | // // 一个权限,完全匹配 26 | // if (!actions.includes(actionName)) { 27 | // ;(el.parentNode && el.parentNode.removeChild(el)) || (el.style.display = 'none') 28 | // } 29 | // } 30 | const { value: elActions } = binding 31 | const route = router.currentRoute.value 32 | const currentActions = route?.meta?.actions ?? [] 33 | const actions = typeof value === 'string' ? elActions.split() : elActions 34 | 35 | if (currentActions.includes('*')) return 36 | 37 | if (!currentActions.some((action) => actions.includes(action))) { 38 | el.remove() || (el.style.display = 'none') 39 | } 40 | }, 41 | } 42 | /** 43 | * 校验权限 44 | * @param {string | array} actions 45 | */ 46 | const checkAction = (actions = '') => { 47 | const route = router.currentRoute.value 48 | const currentActions = route?.meta?.actions ?? [] 49 | actions = typeof actions === 'string' ? actions.split() : actions 50 | 51 | if (currentActions.includes('*')) { 52 | return true 53 | } 54 | 55 | if (!currentActions.some((action) => actions.includes(action))) { 56 | return false 57 | } 58 | 59 | return true 60 | } 61 | 62 | export const setupActionDirective = (app) => { 63 | app.directive('action', action) 64 | app.config.globalProperties.$checkAction = checkAction 65 | return app 66 | } 67 | export default setupActionDirective 68 | -------------------------------------------------------------------------------- /src/directives/index.js: -------------------------------------------------------------------------------- 1 | import { setupActionDirective } from './action' 2 | 3 | export const setupDirective = (app) => { 4 | setupActionDirective(app) 5 | return app 6 | } 7 | -------------------------------------------------------------------------------- /src/enums/system.js: -------------------------------------------------------------------------------- 1 | import Enum from 'xy-enum' 2 | import i18n from '@/locales' 3 | // 菜单类型 4 | export const menuTypeEnum = new Enum([ 5 | { key: 'page', value: 'page', desc: i18n.global.t('button.menu') }, 6 | { key: 'button', value: 'button', desc: i18n.global.t('button.button') }, 7 | ]) 8 | // 启用状态 9 | export const statusTypeEnum = new Enum([ 10 | { key: 'enabled', value: 'enabled', desc: i18n.global.t('pages.system.menu.form.status.enabled') }, 11 | { key: 'disabled', value: 'disabled', desc: i18n.global.t('pages.system.menu.form.status.disabled') }, 12 | ]) 13 | 14 | // 角色开启状态 15 | export const statusUserTypeEnum = new Enum([ 16 | { key: 'activated', value: 'activated', desc: i18n.global.t('pages.system.user.form.status.activated') }, 17 | { key: 'freezed', value: 'freezed', desc: i18n.global.t('pages.system.user.form.status.freezed') }, 18 | ]) 19 | -------------------------------------------------------------------------------- /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | export { default as useColors } from './useColors' 2 | export { default as useForm } from './useForm' 3 | export { default as useMenu } from './useMenu' 4 | export { default as useModal } from './useModal' 5 | export { default as useMultiTab } from './useMultiTab' 6 | export { default as usePagination } from './usePagination' 7 | -------------------------------------------------------------------------------- /src/hooks/useColors.js: -------------------------------------------------------------------------------- 1 | import * as colors from '@ant-design/colors' 2 | 3 | const useColors = () => ({ ...colors }) 4 | 5 | export default useColors 6 | -------------------------------------------------------------------------------- /src/hooks/useForm.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export default () => { 4 | const formRef = ref() 5 | const formRecord = ref({}) 6 | const formRules = ref(null) 7 | const formData = ref({}) 8 | 9 | const formLayout = { 10 | labelCol: { 11 | span: 6, 12 | }, 13 | wrapperCol: { 14 | span: 18, 15 | }, 16 | } 17 | 18 | const formButtonLayout = { 19 | wrapperCol: { 20 | span: 18, 21 | offset: 6, 22 | }, 23 | } 24 | 25 | /** 26 | * 重置表单 27 | */ 28 | function resetForm() { 29 | formRecord.value = null 30 | formData.value = {} 31 | formRef.value.resetFields() 32 | formRef.value.clearValidate() 33 | } 34 | 35 | /** 36 | * 筛选输入项 37 | * @param input 38 | * @param option 39 | * @returns {boolean} 40 | */ 41 | function filterOption(input, option) { 42 | return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0 43 | } 44 | 45 | return { 46 | formRef, 47 | formRules, 48 | formRecord, 49 | formData, 50 | formLayout, 51 | formButtonLayout, 52 | resetForm, 53 | filterOption, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/hooks/useMenu.js: -------------------------------------------------------------------------------- 1 | import { useRouterStore } from '@/store' 2 | 3 | export default () => { 4 | const routerStore = useRouterStore() 5 | 6 | /** 7 | * 设置徽标 8 | * @param {string} name 路由名称 9 | * @param {number} count 数量 10 | */ 11 | function setBadge(name, count) { 12 | routerStore.setBadge({ name, count }) 13 | } 14 | 15 | return { 16 | setBadge, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useModal.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export default () => { 4 | const modal = ref({ 5 | type: '', 6 | title: '', 7 | open: false, 8 | confirmLoading: false, 9 | }) 10 | 11 | /** 12 | * 设置弹窗 13 | * @param options 14 | */ 15 | function setModal(options = {}) { 16 | modal.value = { 17 | ...modal.value, 18 | ...options, 19 | } 20 | } 21 | 22 | /** 23 | * 显示弹窗 24 | * @param options 25 | */ 26 | function showModal(options = {}) { 27 | setModal({ 28 | open: true, 29 | ...options, 30 | }) 31 | } 32 | 33 | /** 34 | * 隐藏弹窗 35 | */ 36 | function hideModal() { 37 | setModal({ 38 | type: '', 39 | open: false, 40 | confirmLoading: false, 41 | }) 42 | } 43 | 44 | /** 45 | * 显示 loading 46 | */ 47 | function showLoading() { 48 | setModal({ 49 | confirmLoading: true, 50 | }) 51 | } 52 | 53 | /** 54 | * 隐藏 loading 55 | */ 56 | function hideLoading() { 57 | setModal({ 58 | confirmLoading: false, 59 | }) 60 | } 61 | 62 | return { 63 | modal, 64 | showModal, 65 | hideModal, 66 | showLoading, 67 | hideLoading, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/hooks/usePagination.js: -------------------------------------------------------------------------------- 1 | import { reactive, ref } from 'vue' 2 | 3 | export default (options = {}) => { 4 | const loading = ref(false) 5 | const listData = ref([]) 6 | const searchFormData = ref({}) 7 | const paginationState = reactive({ 8 | total: 0, 9 | current: 1, 10 | pageSize: 10, 11 | showSizeChanger: true, 12 | showQuickJumper: true, 13 | showTotal: (total) => `总 ${total} 条数据`, 14 | pageSizeOptions: ['10', '20', '30', '40'], 15 | ...(options ?? {}), 16 | }) 17 | 18 | /** 19 | * 重置分页 20 | */ 21 | function resetPagination() { 22 | paginationState.total = 0 23 | paginationState.current = 1 24 | } 25 | 26 | /** 27 | * 刷新分页 28 | * 场景:删除 29 | * @param {number} count 受影响数量 30 | */ 31 | function refreshPagination(count = 1) { 32 | const { total, current, pageSize } = paginationState 33 | const totalPage = Math.ceil((total - count) / pageSize) 34 | paginationState.current = current > totalPage ? totalPage : current 35 | } 36 | 37 | /** 38 | * 显示 loading 39 | */ 40 | function showLoading() { 41 | loading.value = true 42 | } 43 | 44 | /** 45 | * 隐藏 loading 46 | */ 47 | function hideLoading() { 48 | loading.value = false 49 | } 50 | 51 | return { 52 | loading, 53 | listData, 54 | searchFormData, 55 | paginationState, 56 | resetPagination, 57 | refreshPagination, 58 | showLoading, 59 | hideLoading, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/layouts/CustomLayout.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /src/layouts/RouteViewLayout.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/layouts/components/ActionButton.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | 32 | 56 | -------------------------------------------------------------------------------- /src/layouts/components/BasicContent.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 34 | 35 | 41 | -------------------------------------------------------------------------------- /src/layouts/components/BasicSide.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 77 | 78 | 136 | -------------------------------------------------------------------------------- /src/layouts/components/Brand.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 53 | 54 | 93 | -------------------------------------------------------------------------------- /src/layouts/components/IframeView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 45 | -------------------------------------------------------------------------------- /src/layouts/hooks/useMenu.js: -------------------------------------------------------------------------------- 1 | import { find, get, head, omit } from 'lodash-es' 2 | import { ref, watch } from 'vue' 3 | import { useRoute, useRouter } from 'vue-router' 4 | 5 | import { useAppStore, useRouterStore } from '@/store' 6 | import { storeToRefs } from 'pinia' 7 | import { getFirstValidRoute } from '../../router/util' 8 | 9 | export default () => { 10 | const appStore = useAppStore() 11 | const routerStore = useRouterStore() 12 | const route = useRoute() 13 | const router = useRouter() 14 | 15 | const { menuList } = storeToRefs(routerStore) 16 | const sideMenuList = ref([]) 17 | const topMenuList = ref([]) 18 | 19 | watch( 20 | () => appStore.config.menuMode, 21 | (val) => { 22 | // 顶部菜单 23 | if ('top' === val) { 24 | topMenuList.value = menuList.value 25 | } 26 | // 侧边菜单 27 | if ('side' === val) { 28 | sideMenuList.value = menuList.value 29 | } 30 | // 混合菜单 31 | if ('mix' === val) { 32 | topMenuList.value = menuList.value.map((item) => { 33 | return { 34 | ...omit(item, ['children']), 35 | path: item.children ? '' : item.path, 36 | props: { 37 | children: item.children, 38 | click: (res) => { 39 | sideMenuList.value = res?.props?.children || [] 40 | // 获取侧边栏第一个有效路由 41 | const firstRoute = getFirstValidRoute(sideMenuList.value) 42 | if (firstRoute) { 43 | // 如果第一个路由是外部链接,则不跳转 44 | if (firstRoute?.meta?.isLink) return 45 | // 跳转到符合条件的路由中 46 | router.push({ 47 | path: firstRoute.path, 48 | query: firstRoute?.meta?.query || {}, 49 | }) 50 | } 51 | }, 52 | }, 53 | } 54 | }) 55 | const parentName = get(head(route?.meta?.breadcrumb), 'name', '') 56 | sideMenuList.value = get(find(menuList.value, { name: parentName }), 'children', []) 57 | } 58 | }, 59 | { immediate: true } 60 | ) 61 | 62 | return { 63 | sideMenuList, 64 | topMenuList, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/layouts/hooks/useMultiTab.js: -------------------------------------------------------------------------------- 1 | import { useMultiTab } from '@/hooks' 2 | import { onMounted } from 'vue' 3 | import { useRouter, onBeforeRouteUpdate } from 'vue-router' 4 | 5 | export default () => { 6 | const { getSimpleRoute, open } = useMultiTab() 7 | const router = useRouter() 8 | 9 | onBeforeRouteUpdate((to) => { 10 | open(getSimpleRoute(to)) 11 | }) 12 | 13 | onMounted(() => { 14 | open(getSimpleRoute(router.currentRoute.value)) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import BasicLayout from './BasicLayout.vue' 2 | import CustomLayout from './CustomLayout.vue' 3 | import RouteViewLayout from './RouteViewLayout.vue' 4 | import UserLayout from './UserLayout.vue' 5 | 6 | export { BasicLayout, CustomLayout, UserLayout, RouteViewLayout } 7 | -------------------------------------------------------------------------------- /src/locales/index.js: -------------------------------------------------------------------------------- 1 | import enUS from './lang/en-US' 2 | import zhCN from './lang/zh-CN' 3 | 4 | import { createI18n } from 'vue-i18n' 5 | import storage from '@/utils/storage' 6 | import { config } from '@/config' 7 | 8 | const messages = { 9 | 'en-us': { 10 | ...enUS, 11 | }, 12 | 'zh-ch': { 13 | ...zhCN, 14 | }, 15 | } 16 | const i18n = createI18n({ 17 | locale: storage.local.getItem(config('storage.lang')) || 'zh-ch', 18 | legacy: false, // composition API 19 | messages, 20 | }) 21 | 22 | export default i18n 23 | -------------------------------------------------------------------------------- /src/locales/lang/en-US.js: -------------------------------------------------------------------------------- 1 | import component from './en-US/component' 2 | import globalHeader from './en-US/globalHeader' 3 | import menu from './en-US/menu' 4 | import pages from './en-US/pages' 5 | import pwa from './en-US/pwa' 6 | import settingDrawer from './en-US/settingDrawer' 7 | import settings from './en-US/settings' 8 | import buttons from './en-US/button' 9 | 10 | export default { 11 | 'navBar.lang': 'Languages', 12 | 'layout.user.link.help': 'Help', 13 | 'layout.user.link.privacy': 'Privacy', 14 | 'layout.user.link.terms': 'Terms', 15 | 'app.copyright.produced': 'LyricTian', 16 | 'app.preview.down.block': 'Download this page to your local project', 17 | 'app.welcome.link.fetch-blocks': 'Get all block', 18 | 'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development', 19 | ...globalHeader, 20 | ...menu, 21 | ...settingDrawer, 22 | ...settings, 23 | ...pwa, 24 | ...component, 25 | ...pages, 26 | ...buttons, 27 | } 28 | -------------------------------------------------------------------------------- /src/locales/lang/en-US/button.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'button.add': 'Add', 3 | 'button.edit': 'Edit', 4 | 'button.delete': 'Delete', 5 | 'button.search': 'Search', 6 | 'button.reset': 'Reset', 7 | 'button.confirm': 'Confirm', 8 | 'button.cancel': 'Cancel', 9 | 'button.back': 'Back', 10 | 'button.save': 'Save', 11 | 'button.view': 'View', 12 | 'button.export': 'Export', 13 | 'button.import': 'Import', 14 | 'button.action': 'Action', 15 | 'button.menu': 'menu', 16 | 'button.button': 'button', 17 | } 18 | -------------------------------------------------------------------------------- /src/locales/lang/en-US/component.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expand', 3 | 'component.tagSelect.collapse': 'Collapse', 4 | 'component.tagSelect.all': 'All', 5 | 'component.RightContent.profile': 'Profile', 6 | 'component.RightContent.logout': 'Logout', 7 | 'component.message.success.save': 'Save successfully', 8 | 'component.message.error.save': 'Failed to save', 9 | 'component.message.success.delete': 'Delete successfully', 10 | } 11 | -------------------------------------------------------------------------------- /src/locales/lang/en-US/globalHeader.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Search', 3 | 'component.globalHeader.search.example1': 'Search example 1', 4 | 'component.globalHeader.search.example2': 'Search example 2', 5 | 'component.globalHeader.search.example3': 'Search example 3', 6 | 'component.globalHeader.help': 'Help', 7 | 'component.globalHeader.notification': 'Notification', 8 | 'component.globalHeader.notification.empty': 'You have viewed all notifications.', 9 | 'component.globalHeader.message': 'Message', 10 | 'component.globalHeader.message.empty': 'You have viewed all messsages.', 11 | 'component.globalHeader.event': 'Event', 12 | 'component.globalHeader.event.empty': 'You have viewed all events.', 13 | 'component.noticeIcon.clear': 'Clear', 14 | 'component.noticeIcon.cleared': 'Cleared', 15 | 'component.noticeIcon.empty': 'No notifications', 16 | 'component.noticeIcon.view-more': 'View more', 17 | } 18 | -------------------------------------------------------------------------------- /src/locales/lang/en-US/menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | welcome: 'Welcome', 3 | home: 'Home', 4 | system: 'System', 5 | menu: 'Menu', 6 | user: 'User', 7 | setting: 'InfoSetting', 8 | role: 'Role', 9 | logger: 'Logger', 10 | add: 'add', 11 | edit: 'edit', 12 | search: 'search', 13 | delete: 'delete', 14 | 'menu.account.settings': 'Account Settings', 15 | 'menu.login': 'Login', 16 | 'menu.register': 'Register', 17 | 'menu.dashboard': 'Dashboard', 18 | 'menu.dashboard.analysis': 'Analysis', 19 | 'menu.dashboard.monitor': 'Monitor', 20 | 'menu.dashboard.workplace': 'Workplace', 21 | 'menu.exception.403': '403', 22 | 'menu.exception.404': '404', 23 | 'menu.exception.500': '500', 24 | 'menu.result': 'Result', 25 | 'menu.result.success': 'Success', 26 | 'menu.result.fail': 'Fail', 27 | 'menu.exception': 'Exception', 28 | 'menu.exception.not-permission': '403', 29 | 'menu.exception.not-find': '404', 30 | 'menu.exception.server-error': '500', 31 | 'menu.exception.trigger': 'Trigger', 32 | 'menu.account': 'Account', 33 | 'menu.account.trigger': 'Trigger Error', 34 | 'menu.account.logout': 'Logout', 35 | } 36 | -------------------------------------------------------------------------------- /src/locales/lang/en-US/pwa.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'You are offline now', 3 | 'app.pwa.serviceworker.updated': 'New content is available', 4 | 'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page', 5 | 'app.pwa.serviceworker.updated.ok': 'Refresh', 6 | } 7 | -------------------------------------------------------------------------------- /src/locales/lang/en-US/settingDrawer.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.topBottom': 'topBottom', 3 | 'app.setting.leftRight': 'leftRight', 4 | 'app.setting.pagestyle': 'Page style setting', 5 | 'app.setting.pagestyle.dark': 'Dark style', 6 | 'app.setting.pagestyle.light': 'Light style', 7 | 'app.setting.content-width': 'Content Width', 8 | 'app.setting.content-width.fixed': 'Fixed', 9 | 'app.setting.content-width.fluid': 'Fluid', 10 | 'app.setting.themecolor': 'Theme Color', 11 | 'app.setting.themecolor.dust': 'Dust Red', 12 | 'app.setting.themecolor.volcano': 'Volcano', 13 | 'app.setting.themecolor.sunset': 'Sunset Orange', 14 | 'app.setting.themecolor.cyan': 'Cyan', 15 | 'app.setting.themecolor.green': 'Polar Green', 16 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 17 | 'app.setting.themecolor.geekblue': 'Geek Glue', 18 | 'app.setting.themecolor.purple': 'Golden Purple', 19 | 'app.setting.navigationmode': 'Navigation Mode', 20 | 'app.setting.sidemenu': 'Side Menu Layout', 21 | 'app.setting.topmenu': 'Top Menu Layout', 22 | 'app.setting.fixedheader': 'Fixed Header', 23 | 'app.setting.fixedsidebar': 'Fixed Sidebar', 24 | 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout', 25 | 'app.setting.hideheader': 'Hidden Header when scrolling', 26 | 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled', 27 | 'app.setting.othersettings': 'Other Settings', 28 | 'app.setting.weakmode': 'Weak Mode', 29 | 'app.setting.copy': 'Copy Setting', 30 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js', 31 | 'app.setting.production.hint': 'Setting panel shows in development environment only, please manually modify', 32 | } 33 | -------------------------------------------------------------------------------- /src/locales/lang/en-US/settings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': 'Basic Settings', 3 | 'app.settings.menuMap.security': 'Security Settings', 4 | 'app.settings.menuMap.binding': 'Account Binding', 5 | 'app.settings.menuMap.notification': 'New Message Notification', 6 | 'app.settings.basic.avatar': 'Avatar', 7 | 'app.settings.basic.change-avatar': 'Change avatar', 8 | 'app.settings.basic.email': 'Email', 9 | 'app.settings.basic.email-message': 'Please input your email!', 10 | 'app.settings.basic.nickname': 'Nickname', 11 | 'app.settings.basic.nickname-message': 'Please input your Nickname!', 12 | 'app.settings.basic.profile': 'Personal profile', 13 | 'app.settings.basic.profile-message': 'Please input your personal profile!', 14 | 'app.settings.basic.profile-placeholder': 'Brief introduction to yourself', 15 | 'app.settings.basic.country': 'Country/Region', 16 | 'app.settings.basic.country-message': 'Please input your country!', 17 | 'app.settings.basic.geographic': 'Province or city', 18 | 'app.settings.basic.geographic-message': 'Please input your geographic info!', 19 | 'app.settings.basic.address': 'Street Address', 20 | 'app.settings.basic.address-message': 'Please input your address!', 21 | 'app.settings.basic.phone': 'Phone Number', 22 | 'app.settings.basic.phone-message': 'Please input your phone!', 23 | 'app.settings.basic.update': 'Update Information', 24 | 'app.settings.security.strong': 'Strong', 25 | 'app.settings.security.medium': 'Medium', 26 | 'app.settings.security.weak': 'Weak', 27 | 'app.settings.security.password': 'Account Password', 28 | 'app.settings.security.password-description': 'Current password strength', 29 | 'app.settings.security.phone': 'Security Phone', 30 | 'app.settings.security.phone-description': 'Bound phone', 31 | 'app.settings.security.question': 'Security Question', 32 | 'app.settings.security.question-description': 33 | 'The security question is not set, and the security policy can effectively protect the account security', 34 | 'app.settings.security.email': 'Backup Email', 35 | 'app.settings.security.email-description': 'Bound Email', 36 | 'app.settings.security.mfa': 'MFA Device', 37 | 'app.settings.security.mfa-description': 'Unbound MFA device, after binding, can be confirmed twice', 38 | 'app.settings.security.modify': 'Modify', 39 | 'app.settings.security.set': 'Set', 40 | 'app.settings.security.bind': 'Bind', 41 | 'app.settings.binding.taobao': 'Binding Taobao', 42 | 'app.settings.binding.taobao-description': 'Currently unbound Taobao account', 43 | 'app.settings.binding.alipay': 'Binding Alipay', 44 | 'app.settings.binding.alipay-description': 'Currently unbound Alipay account', 45 | 'app.settings.binding.dingding': 'Binding DingTalk', 46 | 'app.settings.binding.dingding-description': 'Currently unbound DingTalk account', 47 | 'app.settings.binding.bind': 'Bind', 48 | 'app.settings.notification.password': 'Account Password', 49 | 'app.settings.notification.password-description': 50 | 'Messages from other users will be notified in the form of a station letter', 51 | 'app.settings.notification.messages': 'System Messages', 52 | 'app.settings.notification.messages-description': 53 | 'System messages will be notified in the form of a station letter', 54 | 'app.settings.notification.todo': 'To-do Notification', 55 | 'app.settings.notification.todo-description': 56 | 'The to-do list will be notified in the form of a letter from the station', 57 | 'app.settings.open': 'Open', 58 | 'app.settings.close': 'Close', 59 | } 60 | -------------------------------------------------------------------------------- /src/locales/lang/zh-CN.js: -------------------------------------------------------------------------------- 1 | import component from './zh-CN/component' 2 | import globalHeader from './zh-CN/globalHeader' 3 | import menu from './zh-CN/menu' 4 | import pages from './zh-CN/pages' 5 | import pwa from './zh-CN/pwa' 6 | import settingDrawer from './zh-CN/settingDrawer' 7 | import settings from './zh-CN/settings' 8 | import buttons from './zh-CN/button' 9 | 10 | export default { 11 | 'navBar.lang': '语言', 12 | 'layout.user.link.help': '帮助', 13 | 'layout.user.link.privacy': '隐私', 14 | 'layout.user.link.terms': '条款', 15 | 'app.copyright.produced': 'LyricTian', 16 | 'app.preview.down.block': '下载此页面到本地项目', 17 | 'app.welcome.link.fetch-blocks': '获取全部区块', 18 | 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面', 19 | ...pages, 20 | ...globalHeader, 21 | ...menu, 22 | ...settingDrawer, 23 | ...settings, 24 | ...pwa, 25 | ...component, 26 | ...buttons, 27 | } 28 | -------------------------------------------------------------------------------- /src/locales/lang/zh-CN/button.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'button.add': '添加', 3 | 'button.menu': '菜单', 4 | 'button.button': '按钮', 5 | 'button.edit': '编辑', 6 | 'button.delete': '删除', 7 | 'button.search': '查询', 8 | 'button.reset': '重置', 9 | 'button.confirm': '确认', 10 | 'button.cancel': '取消', 11 | 'button.back': '返回', 12 | 'button.save': '保存', 13 | 'button.view': '查看', 14 | 'button.export': '导出', 15 | 'button.import': '导入', 16 | 'button.action': '操作', 17 | } 18 | -------------------------------------------------------------------------------- /src/locales/lang/zh-CN/component.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展开', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | 'component.RightContent.profile': '个人设置', 6 | 'component.RightContent.logout': '退出登录', 7 | 'component.message.success.save': '保存成功', 8 | 'component.message.error.save': '保存失败', 9 | 'component.message.success.delete': '删除成功', 10 | } 11 | -------------------------------------------------------------------------------- /src/locales/lang/zh-CN/globalHeader.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站内搜索', 3 | 'component.globalHeader.search.example1': '搜索提示一', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用文档', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '你已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已读完所有消息', 11 | 'component.globalHeader.event': '待办', 12 | 'component.globalHeader.event.empty': '你已完成所有待办', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暂无数据', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | } 18 | -------------------------------------------------------------------------------- /src/locales/lang/zh-CN/menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | welcome: '欢迎', 3 | home: '首页', 4 | system: '系统设置', 5 | menu: '菜单管理', 6 | user: '用户管理', 7 | setting: '信息设置', 8 | role: '角色管理', 9 | logger: '日志管理', 10 | 'menu.account.settings': '个人设置', 11 | add: '添加', 12 | edit: '修改', 13 | delete: '删除', 14 | search: '搜索', 15 | login: '登录', 16 | register: '注册', 17 | dashboard: 'Dashboard', 18 | 'dashboard.analysis': '分析页', 19 | 'dashboard.monitor': '监控页', 20 | 'dashboard.workplace': '工作台', 21 | 'exception.403': '403', 22 | 'exception.404': '404', 23 | 'exception.500': '500', 24 | result: '结果页', 25 | 'result.success': '成功页', 26 | 'result.fail': '失败页', 27 | exception: '异常页', 28 | 'exception.not-permission': '403', 29 | 'exception.not-find': '404', 30 | 'exception.server-error': '500', 31 | 'exception.trigger': '触发错误', 32 | account: '个人页', 33 | 'account.trigger': '触发报错', 34 | 'account.logout': '退出登录', 35 | } 36 | -------------------------------------------------------------------------------- /src/locales/lang/zh-CN/pwa.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '当前处于离线状态', 3 | 'app.pwa.serviceworker.updated': '有新内容', 4 | 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | } 7 | -------------------------------------------------------------------------------- /src/locales/lang/zh-CN/settingDrawer.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整体风格设置', 3 | 'app.setting.pagestyle.dark': '暗色菜单风格', 4 | 'app.setting.pagestyle.light': '亮色菜单风格', 5 | 'app.setting.content-width': '内容区域宽度', 6 | 'app.setting.content-width.fixed': '定宽', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主题色', 9 | 'app.setting.topBottom': '上下布局', 10 | 'app.setting.leftRight': '左右布局', 11 | 'app.setting.themecolor.dust': '薄暮', 12 | 'app.setting.themecolor.volcano': '火山', 13 | 'app.setting.themecolor.sunset': '日暮', 14 | 'app.setting.themecolor.cyan': '明青', 15 | 'app.setting.themecolor.green': '极光绿', 16 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)', 17 | 'app.setting.themecolor.geekblue': '极客蓝', 18 | 'app.setting.themecolor.purple': '酱紫', 19 | 'app.setting.navigationmode': '导航模式', 20 | 'app.setting.sidemenu': '侧边菜单布局', 21 | 'app.setting.topmenu': '顶部菜单布局', 22 | 'app.setting.fixedheader': '固定 Header', 23 | 'app.setting.fixedsidebar': '固定侧边菜单', 24 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置', 25 | 'app.setting.hideheader': '下滑时隐藏 Header', 26 | 'app.setting.hideheader.hint': '固定 Header 时可配置', 27 | 'app.setting.othersettings': '其他设置', 28 | 'app.setting.weakmode': '色弱模式', 29 | 'app.setting.copy': '拷贝设置', 30 | 'app.setting.copyinfo': '拷贝成功,请到 config/defaultSettings.js 中替换默认配置', 31 | 'app.setting.production.hint': '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', 32 | } 33 | -------------------------------------------------------------------------------- /src/locales/lang/zh-CN/settings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本设置', 3 | 'app.settings.menuMap.security': '安全设置', 4 | 'app.settings.menuMap.binding': '账号绑定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '头像', 7 | 'app.settings.basic.change-avatar': '更换头像', 8 | 'app.settings.basic.email': '邮箱', 9 | 'app.settings.basic.email-message': '请输入您的邮箱!', 10 | 'app.settings.basic.nickname': '昵称', 11 | 'app.settings.basic.nickname-message': '请输入您的昵称!', 12 | 'app.settings.basic.profile': '个人简介', 13 | 'app.settings.basic.profile-message': '请输入个人简介!', 14 | 'app.settings.basic.profile-placeholder': '个人简介', 15 | 'app.settings.basic.country': '国家/地区', 16 | 'app.settings.basic.country-message': '请输入您的国家或地区!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '请输入您的街道地址!', 21 | 'app.settings.basic.phone': '联系电话', 22 | 'app.settings.basic.phone-message': '请输入您的联系电话!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '强', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '账户密码', 28 | 'app.settings.security.password-description': '当前密码强度', 29 | 'app.settings.security.phone': '密保手机', 30 | 'app.settings.security.phone-description': '已绑定手机', 31 | 'app.settings.security.question': '密保问题', 32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全', 33 | 'app.settings.security.email': '备用邮箱', 34 | 'app.settings.security.email-description': '已绑定邮箱', 35 | 'app.settings.security.mfa': 'MFA 设备', 36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '设置', 39 | 'app.settings.security.bind': '绑定', 40 | 'app.settings.binding.taobao': '绑定淘宝', 41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号', 42 | 'app.settings.binding.alipay': '绑定支付宝', 43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号', 44 | 'app.settings.binding.dingding': '绑定钉钉', 45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号', 46 | 'app.settings.binding.bind': '绑定', 47 | 'app.settings.notification.password': '账户密码', 48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知', 49 | 'app.settings.notification.messages': '系统消息', 50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知', 51 | 'app.settings.notification.todo': '待办任务', 52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知', 53 | 'app.settings.open': '开', 54 | 'app.settings.close': '关', 55 | } 56 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import App from '@/App.vue' 4 | import { useCore } from '@/core' 5 | 6 | const app = createApp(App) 7 | useCore(app) 8 | app.mount('#app') 9 | -------------------------------------------------------------------------------- /src/mock/index.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | import './modules/common.js' 4 | import './modules/system.js' 5 | import './modules/user.js' 6 | 7 | export const setupMock = () => { 8 | Mock.setup({ 9 | timeout: 200, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/mock/modules/user.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | import { builder, getBody } from '../util' 4 | 5 | // 登录 6 | Mock.mock(new RegExp('/user/login'), 'post', (options) => { 7 | const { username, password } = getBody(options) 8 | 9 | if ('admin' === username && '123456' === password) { 10 | return builder( 11 | Mock.mock({ 12 | id: '@increment', 13 | username: username, 14 | avatar: '@dataImage', 15 | token: '@guid', 16 | email: '@email', 17 | }) 18 | ) 19 | } else { 20 | return builder({}, '301', '用户名或密码错误') 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /src/mock/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础数据结构 3 | * @type {{msg: string, code: string, data: null, timestamp: number}} 4 | */ 5 | const responseBody = { 6 | code: 200, 7 | msg: 'success', 8 | timestamp: 0, 9 | data: null, 10 | } 11 | 12 | /** 13 | * 构建返回的数据结构 14 | * @param data 15 | * @param code 16 | * @param message 17 | * @returns {{msg: string, code: string, data: null, timestamp: number}} 18 | */ 19 | export const builder = (data = {}, code = 200, message = 'success') => { 20 | responseBody.data = data 21 | 22 | if (code !== undefined && code !== 0) { 23 | responseBody.code = code 24 | } 25 | 26 | if (message !== undefined && message !== null) { 27 | responseBody.msg = message 28 | } 29 | 30 | responseBody.timestamp = new Date().getTime() 31 | return responseBody 32 | } 33 | 34 | /** 35 | * 获取地址栏参数 36 | * @param options 37 | * @returns {{}|any} 38 | */ 39 | export const getQueryParams = (options) => { 40 | const url = options.url 41 | const search = url.split('?')[1] 42 | if (!search) { 43 | return {} 44 | } 45 | return JSON.parse( 46 | '{"' + decodeURIComponent(search).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') + '"}' 47 | ) 48 | } 49 | 50 | /** 51 | * 获取body参数 52 | * @param options 53 | * @returns {*} 54 | */ 55 | export const getBody = (options) => { 56 | return options.body && JSON.parse(options.body) 57 | } 58 | -------------------------------------------------------------------------------- /src/plugins/progress/index.js: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress' 2 | 3 | import './index.less' 4 | 5 | export const createProgress = (options = {}) => { 6 | NProgress.configure(options) 7 | return NProgress 8 | } 9 | -------------------------------------------------------------------------------- /src/plugins/progress/index.less: -------------------------------------------------------------------------------- 1 | @import url('nprogress/nprogress.css'); 2 | 3 | #nprogress { 4 | .bar { 5 | background: @color-primary; 6 | } 7 | .peg { 8 | box-shadow: 0 0 10px @color-primary, 0 0 5px @color-primary; 9 | } 10 | .spinner-icon { 11 | border-top-color: @color-primary; 12 | border-left-color: @color-primary; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/router/config.js: -------------------------------------------------------------------------------- 1 | import * as layouts from '@/layouts' 2 | /** 3 | * 白名单 4 | * @type {string[]} 5 | */ 6 | export const whiteList = ['login', 'logout', '404', 'users'] 7 | 8 | /** 9 | * 未找到页面路由 10 | * @type {{redirect: string, path: string, hidden: boolean}} 11 | */ 12 | export const notFoundRoute = { 13 | path: '/:pathMatch(.*)*', 14 | redirect: '/exception/404.vue', 15 | meta: { 16 | isLogin: false, 17 | isMenu: false, 18 | }, 19 | } 20 | 21 | /** 22 | * 基础路由 23 | * 关键字:index,login,exception,404,redirect 24 | * @type {*[]} 25 | */ 26 | export const constantRoutes = [ 27 | { 28 | path: '/', 29 | name: 'index', 30 | redirect: '/login', 31 | }, 32 | { 33 | path: '/base', 34 | component: layouts.UserLayout, 35 | children: [ 36 | { 37 | path: '/login', 38 | name: 'login', 39 | component: () => import('@/views/login/index.vue'), 40 | meta: { 41 | title: '登录', 42 | }, 43 | }, 44 | ], 45 | }, 46 | { 47 | path: '/404', 48 | component: () => import('@/views/exception/404.vue'), 49 | }, 50 | ] 51 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router' 2 | 3 | import { constantRoutes } from './config' 4 | import { config } from '@/config' 5 | 6 | const router = createRouter({ 7 | history: 8 | 'history' === config('router.history') 9 | ? createWebHistory(config('router.base')) 10 | : createWebHashHistory(config('router.base')), 11 | routes: [...constantRoutes], 12 | }) 13 | 14 | export const setupRouter = (app) => { 15 | app.use(router) 16 | return app 17 | } 18 | 19 | export default router 20 | -------------------------------------------------------------------------------- /src/router/notMenuPage.js: -------------------------------------------------------------------------------- 1 | const notMenuPage = ['setting'] 2 | export default notMenuPage 3 | -------------------------------------------------------------------------------- /src/router/routes/admin.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/setting', 4 | name: 'setting', 5 | component: 'admin/setting/index.vue', 6 | meta: { 7 | title: '个人设置', 8 | isMenu: false, 9 | keepAlive: false, 10 | permission: '*', 11 | active: 'setting', 12 | openKeys: 'setting', 13 | }, 14 | }, 15 | ] -------------------------------------------------------------------------------- /src/router/routes/exception.js: -------------------------------------------------------------------------------- 1 | import { WarningOutlined } from '@ant-design/icons-vue' 2 | 3 | export default [ 4 | { 5 | path: 'exception', 6 | name: 'exception', 7 | component: 'RouteViewLayout', 8 | meta: { 9 | icon: WarningOutlined, 10 | title: '异常页', 11 | isMenu: true, 12 | keepAlive: true, 13 | permission: '*', 14 | }, 15 | children: [ 16 | { 17 | path: '403', 18 | name: '403', 19 | component: 'exception/403.vue', 20 | meta: { 21 | title: '403', 22 | isMenu: true, 23 | keepAlive: true, 24 | permission: '*', 25 | }, 26 | }, 27 | { 28 | path: '404', 29 | name: '404', 30 | component: 'exception/404.vue', 31 | meta: { 32 | title: '404', 33 | isMenu: true, 34 | keepAlive: true, 35 | permission: '*', 36 | }, 37 | }, 38 | { 39 | path: '500', 40 | name: '500', 41 | component: 'exception/500.vue', 42 | meta: { 43 | title: '500', 44 | isMenu: true, 45 | keepAlive: true, 46 | permission: '*', 47 | }, 48 | }, 49 | ], 50 | }, 51 | ] 52 | -------------------------------------------------------------------------------- /src/router/routes/form.js: -------------------------------------------------------------------------------- 1 | import { FormOutlined } from '@ant-design/icons-vue' 2 | 3 | export default [ 4 | { 5 | path: 'form', 6 | name: 'form', 7 | component: 'RouteViewLayout', 8 | meta: { 9 | icon: FormOutlined, 10 | title: '表单页', 11 | isMenu: true, 12 | keepAlive: true, 13 | permission: '*', 14 | }, 15 | children: [ 16 | { 17 | path: 'basic', 18 | name: 'formBasic', 19 | component: 'form/basic/index.vue', 20 | meta: { 21 | title: '基础表单', 22 | isMenu: true, 23 | keepAlive: true, 24 | permission: '*', 25 | }, 26 | }, 27 | { 28 | path: 'step', 29 | name: 'formStep', 30 | component: 'form/step/index.vue', 31 | meta: { 32 | title: '分步表单', 33 | isMenu: true, 34 | keepAlive: true, 35 | permission: '*', 36 | }, 37 | }, 38 | { 39 | path: 'advanced', 40 | name: 'formAdvanced', 41 | component: 'form/advanced/index.vue', 42 | meta: { 43 | title: '高级表单', 44 | isMenu: true, 45 | keepAlive: true, 46 | permission: '*', 47 | }, 48 | }, 49 | ], 50 | }, 51 | ] 52 | -------------------------------------------------------------------------------- /src/router/routes/home.js: -------------------------------------------------------------------------------- 1 | import { SmileOutlined } from '@ant-design/icons-vue' 2 | 3 | export default [ 4 | { 5 | path: 'home', 6 | name: 'home', 7 | component: 'home/index.vue', 8 | meta: { 9 | icon: SmileOutlined, 10 | title: '欢迎页', 11 | isMenu: true, 12 | keepAlive: true, 13 | permission: '*', 14 | }, 15 | }, 16 | ] 17 | -------------------------------------------------------------------------------- /src/router/routes/iframe.js: -------------------------------------------------------------------------------- 1 | import { LayoutOutlined } from '@ant-design/icons-vue' 2 | 3 | export default [ 4 | { 5 | path: 'iframe', 6 | name: 'iframePage', 7 | component: 'RouteViewLayout', 8 | meta: { 9 | icon: LayoutOutlined, 10 | title: 'Iframe', 11 | isMenu: true, 12 | keepAlive: true, 13 | permission: '*', 14 | }, 15 | children: [ 16 | { 17 | path: 'vue', 18 | name: 'iframeVue', 19 | component: 'RouteViewLayout', 20 | meta: { 21 | type: 'iframe', 22 | url: 'https://cn.vuejs.org', 23 | title: 'Vue', 24 | isMenu: true, 25 | keepAlive: true, 26 | permission: '*', 27 | }, 28 | }, 29 | { 30 | path: 'antd', 31 | name: 'iframeAntd', 32 | component: 'RouteViewLayout', 33 | meta: { 34 | type: 'iframe', 35 | url: 'https://www.antdv.com/docs/vue/introduce-cn', 36 | title: 'Ant Design Vue', 37 | isMenu: true, 38 | keepAlive: true, 39 | permission: '*', 40 | }, 41 | }, 42 | ], 43 | }, 44 | ] 45 | -------------------------------------------------------------------------------- /src/router/routes/index.js: -------------------------------------------------------------------------------- 1 | import home from './home' 2 | import form from './form' 3 | import list from './list' 4 | import profile from './profile' 5 | import result from './result' 6 | import exception from './exception' 7 | import admin from './admin' 8 | import system from './system' 9 | import link from './link' 10 | import iframe from './iframe' 11 | import other from './other' 12 | 13 | export default [ 14 | ...home, 15 | ...form, 16 | ...list, 17 | ...profile, 18 | ...result, 19 | ...exception, 20 | ...admin, 21 | ...system, 22 | ...link, 23 | ...iframe, 24 | ...other, 25 | ] 26 | -------------------------------------------------------------------------------- /src/router/routes/link.js: -------------------------------------------------------------------------------- 1 | import { LinkOutlined } from '@ant-design/icons-vue' 2 | 3 | export default [ 4 | { 5 | path: '', 6 | name: 'link', 7 | component: 'RouteViewLayout', 8 | meta: { 9 | icon: LinkOutlined, 10 | title: '外部链接', 11 | isMenu: true, 12 | keepAlive: false, 13 | permission: '*', 14 | }, 15 | children: [ 16 | { 17 | path: 'https://github.com/mengxianghan/xy-admin', 18 | name: 'linkBaidu', 19 | meta: { 20 | type: 'link', 21 | title: 'Github', 22 | target: '_blank', 23 | isMenu: true, 24 | permission: '*', 25 | }, 26 | }, 27 | ], 28 | }, 29 | ] 30 | -------------------------------------------------------------------------------- /src/router/routes/list.js: -------------------------------------------------------------------------------- 1 | import { TableOutlined } from '@ant-design/icons-vue' 2 | 3 | export default [ 4 | { 5 | path: 'list', 6 | name: 'list', 7 | component: 'RouteViewLayout', 8 | meta: { 9 | icon: TableOutlined, 10 | title: '列表页', 11 | isMenu: true, 12 | keepAlive: true, 13 | permission: '*', 14 | }, 15 | children: [ 16 | { 17 | path: 'search', 18 | name: 'listSearch', 19 | meta: { 20 | title: '搜索列表', 21 | isMenu: true, 22 | keepAlive: true, 23 | permission: '*', 24 | }, 25 | children: [ 26 | { 27 | path: 'articles', 28 | name: 'listSearchArticles', 29 | component: 'list/search/articles/index.vue', 30 | meta: { 31 | title: '搜索列表(文章)', 32 | isMenu: true, 33 | keepAlive: true, 34 | permission: '*', 35 | }, 36 | }, 37 | { 38 | path: 'projects', 39 | name: 'listSearchProjects', 40 | component: 'list/search/projects/index.vue', 41 | meta: { 42 | title: '搜索列表(项目)', 43 | isMenu: true, 44 | keepAlive: true, 45 | permission: '*', 46 | }, 47 | }, 48 | { 49 | path: 'applications', 50 | name: 'listSearchApplications', 51 | component: 'list/search/applications/index.vue', 52 | meta: { 53 | title: '搜索列表(应用)', 54 | isMenu: true, 55 | keepAlive: true, 56 | permission: '*', 57 | }, 58 | }, 59 | ], 60 | }, 61 | { 62 | path: 'table', 63 | name: 'listTable', 64 | component: 'list/table/index.vue', 65 | meta: { 66 | title: '查询表格', 67 | isMenu: true, 68 | keepAlive: true, 69 | permission: '*', 70 | }, 71 | }, 72 | { 73 | path: 'basic', 74 | name: 'listBasic', 75 | component: 'list/basic/index.vue', 76 | meta: { 77 | title: '标准列表', 78 | isMenu: true, 79 | keepAlive: true, 80 | permission: '*', 81 | }, 82 | }, 83 | { 84 | path: 'card', 85 | name: 'listCard', 86 | component: 'list/card/index.vue', 87 | meta: { 88 | title: '卡片列表', 89 | isMenu: true, 90 | keepAlive: true, 91 | permission: '*', 92 | }, 93 | }, 94 | ], 95 | }, 96 | ] 97 | -------------------------------------------------------------------------------- /src/router/routes/other.js: -------------------------------------------------------------------------------- 1 | import { EllipsisOutlined } from '@ant-design/icons-vue' 2 | 3 | export default [ 4 | { 5 | path: 'other', 6 | name: 'other', 7 | component: 'RouteViewLayout', 8 | meta: { 9 | icon: EllipsisOutlined, 10 | title: '其他', 11 | isMenu: true, 12 | keepAlive: true, 13 | permission: '*', 14 | }, 15 | children: [ 16 | { 17 | path: 'custom-layout', 18 | name: 'otherCustomLayout', 19 | component: '/list/basic/index.vue', 20 | meta: { 21 | layout: 'CustomLayout', 22 | title: '自定义框架', 23 | isMenu: true, 24 | target: '_blank', 25 | keepAlive: true, 26 | permission: '*', 27 | }, 28 | }, 29 | { 30 | path: 'multi-tab', 31 | name: 'otherMultiTab', 32 | component: '/other/multi-tab/index.vue', 33 | meta: { 34 | title: '多标签操作', 35 | isMenu: true, 36 | keepAlive: true, 37 | permission: '*', 38 | }, 39 | }, 40 | { 41 | path: 'badge', 42 | name: 'otherBadge', 43 | component: 'other/badge/index.vue', 44 | meta: { 45 | title: '菜单徽标', 46 | isMenu: true, 47 | keepAlive: true, 48 | permission: '*', 49 | }, 50 | }, 51 | ], 52 | }, 53 | ] 54 | -------------------------------------------------------------------------------- /src/router/routes/profile.js: -------------------------------------------------------------------------------- 1 | import { ProfileOutlined } from '@ant-design/icons-vue' 2 | 3 | export default [ 4 | { 5 | path: 'profile', 6 | name: 'profile', 7 | component: 'RouteViewLayout', 8 | meta: { 9 | icon: ProfileOutlined, 10 | title: '详情页', 11 | isMenu: true, 12 | keepAlive: true, 13 | permission: '*', 14 | }, 15 | children: [ 16 | { 17 | path: 'basic', 18 | name: 'profileBasic', 19 | component: 'profile/basic/index.vue', 20 | meta: { 21 | title: '基础详情页', 22 | isMenu: true, 23 | keepAlive: true, 24 | permission: '*', 25 | }, 26 | }, 27 | { 28 | path: 'advanced', 29 | name: 'profileAdvanced', 30 | component: 'profile/advanced/index.vue', 31 | meta: { 32 | title: '高级详情页', 33 | isMenu: true, 34 | keepAlive: true, 35 | permission: '*', 36 | }, 37 | }, 38 | ], 39 | }, 40 | ] 41 | -------------------------------------------------------------------------------- /src/router/routes/result.js: -------------------------------------------------------------------------------- 1 | import { CheckCircleOutlined } from '@ant-design/icons-vue' 2 | 3 | export default [ 4 | { 5 | path: 'result', 6 | name: 'result', 7 | component: 'RouteViewLayout', 8 | meta: { 9 | icon: CheckCircleOutlined, 10 | title: '结果页', 11 | isMenu: true, 12 | keepAlive: true, 13 | permission: '*', 14 | }, 15 | children: [ 16 | { 17 | path: 'success', 18 | name: 'resultSuccess', 19 | component: 'result/success/index.vue', 20 | meta: { 21 | title: '成功页', 22 | isMenu: true, 23 | keepAlive: true, 24 | permission: '*', 25 | }, 26 | }, 27 | { 28 | path: 'fail', 29 | name: 'resultFail', 30 | component: 'result/fail/index.vue', 31 | meta: { 32 | title: '失败页', 33 | isMenu: true, 34 | keepAlive: true, 35 | permission: '*', 36 | }, 37 | }, 38 | ], 39 | }, 40 | ] 41 | -------------------------------------------------------------------------------- /src/router/routes/system.js: -------------------------------------------------------------------------------- 1 | import { SettingOutlined } from '@ant-design/icons-vue' 2 | 3 | export default [ 4 | { 5 | path: 'system', 6 | name: 'system', 7 | component: 'RouteViewLayout', 8 | meta: { 9 | icon: SettingOutlined, 10 | title: '系统管理', 11 | isMenu: true, 12 | keepAlive: true, 13 | permission: '*', 14 | }, 15 | children: [ 16 | { 17 | path: 'user', 18 | name: 'user', 19 | component: 'system/user/index.vue', 20 | meta: { 21 | title: '成员与部门', 22 | isMenu: true, 23 | keepAlive: true, 24 | permission: '*', 25 | }, 26 | }, 27 | { 28 | path: 'role', 29 | name: 'role', 30 | component: 'system/role/index.vue', 31 | meta: { 32 | title: '角色管理', 33 | isMenu: true, 34 | keepAlive: true, 35 | permission: '*', 36 | }, 37 | }, 38 | { 39 | path: 'menu', 40 | name: 'menu', 41 | component: 'system/menu/index.vue', 42 | meta: { 43 | title: '菜单管理', 44 | isMenu: true, 45 | keepAlive: true, 46 | permission: '*', 47 | }, 48 | }, 49 | { 50 | path: 'logger', 51 | name: 'logger', 52 | component: 'system/logger/index.vue', 53 | meta: { 54 | title: '日志管理', 55 | isMenu: true, 56 | keepAlive: true, 57 | permission: '*', 58 | }, 59 | }, 60 | ], 61 | }, 62 | ] 63 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | import useAppStore from './modules/app' 4 | import useMultiTabStore from './modules/multiTab' 5 | import useRouterStore from './modules/router' 6 | import useUserStore from './modules/user' 7 | 8 | const store = createPinia() 9 | const setupStore = (app) => { 10 | app.use(store) 11 | return app 12 | } 13 | 14 | export { setupStore, useAppStore, useMultiTabStore, useRouterStore, useUserStore } 15 | 16 | export default store 17 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import storage from '@/utils/storage' 3 | import useRouterStore from './router' 4 | import { config } from '@/config' 5 | 6 | const defaultConfig = { 7 | layout: 'leftRight', // 页面布局【topBottom=上下布局,leftRight=左右布局】 8 | menuMode: 'side', // 菜单模式【side=侧边菜单,top=顶部菜单,mix=混合菜单】 9 | sideCollapsedWidth: 60, 10 | sideWidth: 220, 11 | headerHeight: 60, 12 | sideTheme: 'dark', // 侧边菜单主题【dark=暗色,light=亮色】 13 | headerTheme: 'light', // 侧边菜单主题【dark=暗色,light=亮色】 14 | multiTab: true, 15 | multiTabHeight: 48, 16 | mainMargin: 16, 17 | } 18 | 19 | const useAppStore = defineStore('app', { 20 | name: 'useAppStore', 21 | state: () => ({ 22 | complete: false, 23 | config: storage.session.getItem(config('storage.config'), defaultConfig), 24 | }), 25 | getters: { 26 | mainOffsetTop: (state) => { 27 | const multiTabHeight = state.config?.multiTab ? `${state.config.multiTabHeight}px` : '0px' 28 | return `calc(${state.config.headerHeight}px + ${multiTabHeight} + ${state.config.mainMargin}px)` 29 | }, 30 | mainHeight: (state) => { 31 | const multiTabHeight = state.config?.multiTab ? `${state.config.multiTabHeight}px` : '0px' 32 | return `calc(100vh - ${state.config.headerHeight}px - ${multiTabHeight} - ${state.config.mainMargin * 2}px)` 33 | }, 34 | }, 35 | actions: { 36 | /** 37 | * 初始化 38 | * @returns {Promise} 39 | */ 40 | init() { 41 | const routerStore = useRouterStore() 42 | return new Promise((resolve) => { 43 | Promise.all([routerStore.getRouterList()]) 44 | .then(() => { 45 | this.complete = true 46 | resolve() 47 | }) 48 | .catch(() => {}) 49 | }) 50 | }, 51 | /** 52 | * 更新 config 53 | */ 54 | updateConfig() { 55 | storage.session.setItem(config('storage.config'), this.config) 56 | }, 57 | }, 58 | }) 59 | 60 | export default useAppStore 61 | -------------------------------------------------------------------------------- /src/store/modules/router.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | import router from '@/router' 4 | import { notFoundRoute } from '@/router/config' 5 | import { addWebPage, formatRoutes, generateMenuList, generateRoutes, getFirstValidRoute } from '@/router/util' 6 | import { findTree } from '@/utils/util' 7 | import { config } from '@/config' 8 | import apis from '@/apis' 9 | import { formatApiData } from '../../router/util' 10 | 11 | const useRouterStore = defineStore('router', { 12 | state: () => ({ 13 | routes: [], 14 | menuList: [], 15 | indexRoute: null, 16 | }), 17 | getters: {}, 18 | actions: { 19 | /** 20 | * 获取路由列表 21 | * @returns {Promise} 22 | */ 23 | getRouterList() { 24 | return new Promise((resolve, reject) => { 25 | ;(async () => { 26 | try { 27 | const { success, data } = await apis.user.getUserMenu().catch(() => { 28 | throw new Error() 29 | }) 30 | if (config('http.code.success') === success) { 31 | const list = formatApiData(data) 32 | 33 | list.push(...addWebPage()) 34 | 35 | const validRoutes = formatRoutes(list) 36 | 37 | const menuList = generateMenuList(validRoutes) 38 | const routes = [...generateRoutes(validRoutes), notFoundRoute] 39 | const indexRoute = getFirstValidRoute(menuList) 40 | routes.forEach((route) => { 41 | router.addRoute(route) 42 | }) 43 | this.routes = routes 44 | this.menuList = menuList 45 | this.indexRoute = indexRoute 46 | resolve() 47 | } 48 | } catch (error) { 49 | console.log(error) 50 | reject() 51 | } 52 | })() 53 | }) 54 | }, 55 | /** 56 | * 设置徽标 57 | * @param {string} name 名称 58 | * @param {number} count 数量 59 | */ 60 | setBadge({ name, count } = {}) { 61 | let menuInfo = null 62 | findTree( 63 | this.menuList, 64 | name, 65 | (item) => { 66 | menuInfo = item 67 | }, 68 | { key: 'name', children: 'children' } 69 | ) 70 | if (menuInfo) { 71 | menuInfo.meta.badge = count 72 | } 73 | }, 74 | }, 75 | }) 76 | 77 | export default useRouterStore 78 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { config } from '@/config' 3 | import storage from '@/utils/storage' 4 | import apis from '@/apis' 5 | 6 | import useAppStore from './app' 7 | import useMultiTab from './multiTab' 8 | import useRouter from './router' 9 | 10 | const useUserStore = defineStore('user', { 11 | state: () => ({ 12 | userInfo: storage.local.getItem(config('storage.userInfo'), null), 13 | token: storage.local.getItem(config('storage.token'), ''), 14 | permission: storage.local.getItem(config('storage.permission'), []), 15 | }), 16 | getters: { 17 | isLogin: (state) => !!state.token, 18 | }, 19 | actions: { 20 | /** 21 | * 登录 22 | * @param {object} params 23 | * @returns {Promise} 24 | */ 25 | login(params) { 26 | return new Promise((resolve, reject) => { 27 | ;(async () => { 28 | try { 29 | const result = await apis.user.login(params).catch(() => { 30 | throw new Error() 31 | }) 32 | const { success, data } = result || {} 33 | if (config('http.code.success') === success) { 34 | const { access_token } = data 35 | this.token = access_token 36 | storage.local.setItem(config('storage.token'), access_token) 37 | await this.getUserInfo() 38 | } 39 | resolve(result) 40 | } catch (error) { 41 | reject() 42 | } 43 | })() 44 | }) 45 | }, 46 | /** 47 | * 退出登录 48 | */ 49 | logout() { 50 | return new Promise((resolve) => { 51 | const appStore = useAppStore() 52 | const multiTab = useMultiTab() 53 | const router = useRouter() 54 | storage.local.removeItem(config('storage.token')) 55 | storage.local.removeItem(config('storage.userInfo')) 56 | this.$reset() 57 | appStore.$reset() 58 | multiTab.$reset() 59 | router.$reset() 60 | resolve() 61 | }) 62 | }, 63 | /** 64 | * 获取用户详情 65 | */ 66 | getUserInfo() { 67 | return new Promise((resolve, reject) => { 68 | ;(async () => { 69 | try { 70 | const result = await apis.user.getUserDetail().catch(() => { 71 | throw new Error() 72 | }) 73 | const { success, data } = result || {} 74 | if (config('http.code.success') === success) { 75 | this.userInfo = data 76 | storage.local.setItem(config('storage.userInfo'), this.userInfo) 77 | resolve(result) 78 | } else { 79 | throw new Error() 80 | } 81 | } catch (error) { 82 | reject() 83 | } 84 | })() 85 | }) 86 | }, 87 | }, 88 | }) 89 | 90 | export default useUserStore 91 | -------------------------------------------------------------------------------- /src/styles/antd.less: -------------------------------------------------------------------------------- 1 | // PageHeader 2 | .ant-page-header { 3 | &[main] { 4 | margin: -16px -16px 16px; 5 | } 6 | &[round] { 7 | border-radius: @border-radius-lg; 8 | } 9 | &[bordered] { 10 | border: @color-split solid 1px; 11 | } 12 | } 13 | 14 | .anticon { 15 | display: inline-flex; 16 | align-items: center; 17 | } 18 | 19 | // Tabs 20 | .ant-tabs { 21 | &[no-margin-bottom] { 22 | .ant-tabs-nav { 23 | margin-bottom: 0; 24 | } 25 | } 26 | } 27 | 28 | .ant-tabs-dropdown { 29 | .ant-tabs-dropdown-menu { 30 | padding-inline: 4px; 31 | } 32 | 33 | .ant-tabs-dropdown-menu-item { 34 | border-radius: 4px; 35 | } 36 | 37 | .ant-dropdown-trigger { 38 | display: flex; 39 | 40 | .multi-tab__icon { 41 | margin: 0 0 0 auto; 42 | } 43 | } 44 | } 45 | 46 | // Dropdown 47 | .ant-dropdown-menu-submenu-title { 48 | border-radius: 4px; 49 | } 50 | 51 | // Button 52 | .ant-btn { 53 | display: inline-flex; 54 | align-items: center; 55 | justify-content: center; 56 | } 57 | 58 | // Tree 59 | .ant-tree { 60 | &.ant-tree-block-node { 61 | .ant-tree-treenode { 62 | padding-block: 4px; 63 | margin-block: 0 4px; 64 | border-radius: @border-radius; 65 | transition: all 0.2s; 66 | 67 | &:hover { 68 | background: @color-bg-text-hover; 69 | } 70 | 71 | .x-action-btn { 72 | &:hover { 73 | background: @color-bg-text-hover; 74 | } 75 | } 76 | 77 | &.ant-tree-treenode-selected { 78 | background: color(~`colorPalette('@{color-primary}', 1) `); 79 | 80 | .x-action-btn { 81 | &:hover { 82 | background: color(~`colorPalette('@{color-primary}', 2) `); 83 | } 84 | } 85 | } 86 | } 87 | 88 | .ant-tree-node-content-wrapper { 89 | &:hover { 90 | background: transparent; 91 | } 92 | 93 | &.ant-tree-node-selected { 94 | background: transparent; 95 | } 96 | } 97 | } 98 | 99 | .ant-tree-title { 100 | display: flex; 101 | 102 | &__name { 103 | flex: 1; 104 | } 105 | 106 | &__actions { 107 | flex-shrink: 0; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import 'reset'; 2 | @import 'utils'; 3 | @import 'antd'; 4 | -------------------------------------------------------------------------------- /src/styles/mixins/color/colorPalette.less: -------------------------------------------------------------------------------- 1 | /* stylelint-disable no-duplicate-selectors */ 2 | @import "bezierEasing"; 3 | @import "tinyColor"; 4 | 5 | // We create a very complex algorithm which take the place of original tint/shade color system 6 | // to make sure no one can understand it 👻 7 | // and create an entire color palette magicly by inputing just a single primary color. 8 | // We are using bezier-curve easing function and some color manipulations like tint/shade/darken/spin 9 | .colorPaletteMixin() { 10 | @functions: ~`(function() { 11 | var hueStep = 2; 12 | var saturationStep = 0.16; 13 | var saturationStep2 = 0.05; 14 | var brightnessStep1 = 0.05; 15 | var brightnessStep2 = 0.15; 16 | var lightColorCount = 5; 17 | var darkColorCount = 4; 18 | 19 | var getHue = function(hsv, i, isLight) { 20 | var hue; 21 | if (hsv.h >= 60 && hsv.h <= 240) { 22 | hue = isLight ? hsv.h - hueStep * i : hsv.h + hueStep * i; 23 | } else { 24 | hue = isLight ? hsv.h + hueStep * i : hsv.h - hueStep * i; 25 | } 26 | if (hue < 0) { 27 | hue += 360; 28 | } else if (hue >= 360) { 29 | hue -= 360; 30 | } 31 | return Math.round(hue); 32 | }; 33 | var getSaturation = function(hsv, i, isLight) { 34 | var saturation; 35 | if (isLight) { 36 | saturation = hsv.s - saturationStep * i; 37 | } else if (i === darkColorCount) { 38 | saturation = hsv.s + saturationStep; 39 | } else { 40 | saturation = hsv.s + saturationStep2 * i; 41 | } 42 | if (saturation > 1) { 43 | saturation = 1; 44 | } 45 | if (isLight && i === lightColorCount && saturation > 0.1) { 46 | saturation = 0.1; 47 | } 48 | if (saturation < 0.06) { 49 | saturation = 0.06; 50 | } 51 | return Number(saturation.toFixed(2)); 52 | }; 53 | var getValue = function(hsv, i, isLight) { 54 | var value; 55 | if (isLight) { 56 | value = hsv.v + brightnessStep1 * i; 57 | }else{ 58 | value = hsv.v - brightnessStep2 * i 59 | } 60 | if (value > 1) { 61 | value = 1; 62 | } 63 | return Number(value.toFixed(2)) 64 | }; 65 | 66 | this.colorPalette = function(color, index) { 67 | var isLight = index <= 6; 68 | var hsv = tinycolor(color).toHsv(); 69 | var i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1; 70 | return tinycolor({ 71 | h: getHue(hsv, i, isLight), 72 | s: getSaturation(hsv, i, isLight), 73 | v: getValue(hsv, i, isLight), 74 | }).toHexString(); 75 | }; 76 | })()`; 77 | } 78 | // It is hacky way to make this function will be compiled preferentially by less 79 | // resolve error: `ReferenceError: colorPalette is not defined` 80 | // https://github.com/ant-design/ant-motion/issues/44 81 | .colorPaletteMixin(); 82 | -------------------------------------------------------------------------------- /src/styles/mixins/ellipsis.less: -------------------------------------------------------------------------------- 1 | .multi-ellipsis(@lines) { 2 | display: -webkit-box; 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | -webkit-line-clamp: @lines; 6 | line-break: anywhere; 7 | 8 | /* autoprefixer: ignore next */ 9 | -webkit-box-orient: vertical; 10 | } 11 | 12 | .ellipsis() { 13 | overflow: hidden; 14 | white-space: nowrap; 15 | text-overflow: ellipsis; 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/mixins/index.less: -------------------------------------------------------------------------------- 1 | @import './color/colors.less'; 2 | @import './ellipsis.less'; 3 | @import './scrollbar.less'; 4 | -------------------------------------------------------------------------------- /src/styles/mixins/scrollbar.less: -------------------------------------------------------------------------------- 1 | .scrollbar(@color: rgba(0, 0, 0, 0.15), @size: 8px, @border-radius: 10em) { 2 | &::-webkit-scrollbar { 3 | width: @size; 4 | height: 8px; 5 | background: transparent; 6 | } 7 | 8 | &::-webkit-scrollbar-thumb { 9 | background: @color; 10 | border-radius: @border-radius; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/reset.less: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | html, 7 | body { 8 | height: auto; 9 | min-height: 100%; 10 | word-break: break-word; 11 | hyphens: auto; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | overscroll-behavior: none; 15 | } 16 | 17 | #app { 18 | position: relative; 19 | } 20 | 21 | body { 22 | font: normal normal normal 14px/1.5 @font-family; 23 | color: @color-text; 24 | } 25 | 26 | img, 27 | video, 28 | audio { 29 | max-width: 100%; 30 | vertical-align: middle; 31 | } 32 | 33 | iframe { 34 | vertical-align: middle; 35 | } 36 | 37 | input:-internal-autofill-previewed, 38 | input:-internal-autofill-selected { 39 | transition: background-color 5000s ease-in-out 0s !important; 40 | } 41 | 42 | textarea { 43 | resize: none; 44 | } 45 | 46 | ul, 47 | ol, 48 | li { 49 | list-style: none; 50 | } 51 | -------------------------------------------------------------------------------- /src/styles/utils.less: -------------------------------------------------------------------------------- 1 | // Align 2 | each(left center right, { 3 | .align-@{value} { 4 | text-align: @value !important; 5 | } 6 | }) 7 | 8 | // Color 9 | each({ 10 | primary: @color-primary; 11 | error: @color-error; 12 | success: @color-success; 13 | warning: @color-warning; 14 | heading: @color-text-heading; 15 | secondary: @color-text-secondary; 16 | caption: @color-text-label; 17 | disabled: @color-text-disabled; 18 | placeholder: @color-text-placeholder; 19 | }, { 20 | .color-@{key} { 21 | color: @value !important; 22 | } 23 | }) 24 | 25 | // FontSize 26 | each(12 14 16 18 24, { 27 | .fs-@{value} { 28 | font-size: @value * 1px !important; 29 | } 30 | }) 31 | 32 | // FontWeight 33 | each(400 500 600, { 34 | .fw-@{value} { 35 | font-weight: @value !important; 36 | } 37 | }) 38 | 39 | // Spacing 40 | each({ 41 | mt: margin-top; 42 | mb: margin-bottom; 43 | ml: margin-left; 44 | mr: margin-right; 45 | mx: margin-left, margin-right; 46 | my: margin-top, margin-bottom; 47 | ma: margin; 48 | pt: padding-top; 49 | pb: padding-bottom; 50 | pl: padding-left; 51 | pr: padding-right; 52 | px: padding-left, padding-right; 53 | py: padding-top, padding-bottom; 54 | pa: padding; 55 | }, .(@property, @class) { 56 | 57 | each(0 auto, .(@value){ 58 | .@{class}-@{value} { 59 | each(@property, .(@prop) { 60 | @{prop}: @value !important; 61 | }) 62 | } 63 | }) 64 | 65 | each(4 5 8, .(@value) { 66 | each(1 2 3 4 5, .(@multi) { 67 | .@{class}-@{value}-@{multi} { 68 | each(@property, .(@prop) { 69 | @{prop}: @value * @multi * 1px !important; 70 | }) 71 | } 72 | }) 73 | }) 74 | }) 75 | 76 | // Display 77 | each({ 78 | display: block; 79 | display: flex; 80 | display: inline-block; 81 | display: inline-flex; 82 | flex-direction: column; 83 | flex-direction: row; 84 | flex-wrap: wrap; 85 | align-items: center; 86 | align-items: flex-end; 87 | align-items: flex-start; 88 | align-items: stretch; 89 | justify-content: center; 90 | justify-content: start; 91 | justify-content: end; 92 | justify-content: flex-start; 93 | justify-content: flex-end; 94 | justify-content: left; 95 | justify-content: right; 96 | }, { 97 | .@{key}-@{value} { 98 | @{key}: @value 99 | } 100 | }); 101 | 102 | // Percentage 103 | each(1 2 3 4 5 6 7 8 9 10, { 104 | @prop: @value * 10; 105 | @v: @prop * 1%; 106 | .wp-@{prop} { 107 | width: @v; 108 | } 109 | .hp-@{prop} { 110 | height: @v; 111 | } 112 | }); 113 | 114 | // Overflow 115 | each({ 116 | overflow: hidden; 117 | overflow: auto; 118 | overflow: scroll; 119 | overflow-x: hidden; 120 | overflow-x: auto; 121 | overflow-x: scroll; 122 | overflow-y: hidden; 123 | overflow-y: auto; 124 | overflow-y: scroll; 125 | }, .(@value, @prop) { 126 | .@{prop}-@{value} { 127 | @{prop}: @value; 128 | } 129 | }); 130 | 131 | // Cursor 132 | each(pointer not-allowed, { 133 | .cursor-@{value}{ 134 | cursor: @value; 135 | } 136 | }) -------------------------------------------------------------------------------- /src/styles/variables.less: -------------------------------------------------------------------------------- 1 | @font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 2 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 3 | 4 | @color-primary: #1677ff; 5 | 6 | @color-success: #52c41a; 7 | 8 | @color-warning: #faad14; 9 | 10 | @color-error: #ff4d4f; 11 | 12 | @color-text: rgba(0, 0, 0, 0.88); 13 | @color-text-secondary: rgba(0, 0, 0, 0.65); 14 | @color-text-tertiary: rgba(0, 0, 0, 0.45); 15 | @color-text-quaternary: rgba(0, 0, 0, 0.25); 16 | 17 | @color-text-heading: @color-text; 18 | @color-text-label: @color-text-secondary; 19 | @color-text-description: @color-text-tertiary; 20 | @color-text-disabled: @color-text-quaternary; 21 | @color-text-placeholder: @color-text-quaternary; 22 | 23 | @color-bg-mask: rgba(0, 0, 0, 0.45); 24 | @color-bg-text-hover: rgba(0, 0, 0, 0.06); 25 | 26 | @color-border: #d9d9d9; 27 | @color-border-secondary: #f0f0f0; 28 | @color-split: @color-border-secondary; 29 | 30 | @border-radius: 6px; 31 | @border-radius-lg: 8px; 32 | @border-radius-sm: 4px; 33 | @border-radius-xs: 2px; 34 | 35 | @box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02); 36 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import { message } from 'ant-design-vue' 2 | import jschardet from 'jschardet' 3 | import XYHttp from 'xy-http' 4 | import { config } from '@/config' 5 | 6 | import { useUserStore } from '@/store' 7 | 8 | const MSG_ERROR_KEY = Symbol('GLOBAL_ERROR') 9 | 10 | const options = { 11 | enableAbortController: true, 12 | interceptorRequest: (request) => { 13 | const userStore = useUserStore() 14 | const isLogin = userStore.isLogin 15 | const token = userStore.token 16 | 17 | if (isLogin) { 18 | request.headers['Authorization'] = token 19 | } 20 | }, 21 | interceptorRequestCatch: () => {}, 22 | interceptorResponse: (response) => { 23 | // 错误处理 24 | const { success, msg = 'Network Error' } = response.data || {} 25 | if (![true].includes(success)) { 26 | message.error({ 27 | content: msg, 28 | key: MSG_ERROR_KEY, 29 | }) 30 | } 31 | }, 32 | interceptorResponseCatch: (err) => { 33 | const { success, error } = err.response.data || {} 34 | if ([false].includes(success)) { 35 | if (error.code === 401) { 36 | return useUserStore().logout() 37 | } 38 | message.error({ 39 | content: error.detail, 40 | key: MSG_ERROR_KEY, 41 | }) 42 | } 43 | }, 44 | } 45 | 46 | /** 47 | * 读取文件 48 | */ 49 | class ReadFile extends XYHttp { 50 | constructor() { 51 | super({ 52 | baseURL: '', 53 | responseType: 'blob', 54 | transformResponse: [ 55 | async (data) => { 56 | const encoding = await this._encoding(data) 57 | return new Promise((resolve) => { 58 | let reader = new FileReader() 59 | reader.readAsText(data, encoding) 60 | reader.onload = function () { 61 | resolve(reader.result) 62 | } 63 | }) 64 | }, 65 | ], 66 | }) 67 | } 68 | 69 | /** 70 | * 文本编码 71 | * @param data 72 | * @returns {Promise} 73 | * @private 74 | */ 75 | _encoding(data) { 76 | return new Promise((resolve) => { 77 | let reader = new FileReader() 78 | reader.readAsBinaryString(data) 79 | reader.onload = function () { 80 | resolve(jschardet.detect(reader?.result).encoding) 81 | } 82 | }) 83 | } 84 | } 85 | 86 | const basic = new XYHttp({ 87 | ...options, 88 | baseURL: config('http.apiBasic'), 89 | }) 90 | 91 | const readFile = new ReadFile() 92 | 93 | export default { 94 | basic, 95 | readFile, 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | import Storage from 'xy-storage' 2 | import { config } from '@/config' 3 | 4 | const options = { 5 | namespace: config('storage.namespace'), 6 | } 7 | 8 | export const local = new Storage({ 9 | ...options, 10 | name: 'local', 11 | }) 12 | 13 | export const session = new Storage({ 14 | ...options, 15 | name: 'session', 16 | }) 17 | 18 | export const cookie = new Storage({ 19 | ...options, 20 | name: 'cookie', 21 | }) 22 | 23 | export default { 24 | local, 25 | session, 26 | cookie, 27 | } 28 | -------------------------------------------------------------------------------- /src/views/exception/403.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/views/exception/404.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/views/exception/500.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/views/form/step/components/Step2.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/views/form/step/components/Step3.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/form/step/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 60 | 61 | 72 | -------------------------------------------------------------------------------- /src/views/iframe/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | 31 | 47 | -------------------------------------------------------------------------------- /src/views/list/search/components/PageHeader.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/views/list/table/components/EditDialog.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/views/other/badge/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/views/other/multi-tab/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/views/result/fail/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/views/result/success/index.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/views/system/dict/components/EditDictDialog.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/views/system/role/components/EditRoleDialog.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import path from 'path' 3 | import pkg from './package.json' 4 | 5 | import useCompressPlugin from './config/useCompressPlugin' 6 | import useProgressPlugin from './config/useProgressPlugin' 7 | import useVuePlugin from './config/useVuePlugin' 8 | import useVisualizerPlugin from './config/useVisualizerPlugin' 9 | import useServer from './config/useServer' 10 | import useEslintPlugin from './config/useEslintPlugin' 11 | 12 | export default ({ mode }) => { 13 | const env = loadEnv(mode, process.cwd(), '') 14 | return defineConfig({ 15 | base: env.VITE_PUBLIC_PATH, 16 | build: { 17 | outDir: env.VITE_OUT_DIR, 18 | target: 'es2015', 19 | cssTarget: 'chrome80', 20 | brotliSize: false, 21 | chunkSizeWarningLimit: 2000, 22 | rollupOptions: { 23 | output: { 24 | manualChunks: { 25 | tinymce: ['tinymce'], 26 | echarts: ['echarts'], 27 | 'lodash-es': ['lodash-es'], 28 | 'ant-design-vue': ['ant-design-vue'], 29 | jschardet: ['jschardet'], 30 | qrcode: ['qrcode'], 31 | cropper: ['cropperjs'], 32 | }, 33 | }, 34 | }, 35 | }, 36 | css: { 37 | preprocessorOptions: { 38 | less: { 39 | modifyVars: { 40 | hack: ` 41 | true; 42 | @import '${path.resolve(__dirname, 'src/styles/variables.less')}'; 43 | @import '${path.resolve(__dirname, 'src/styles/mixins/index.less')}'; 44 | `, 45 | }, 46 | javascriptEnabled: true, 47 | }, 48 | }, 49 | devSourcemap: true, 50 | }, 51 | define: { 52 | __APP_INFO__: JSON.stringify({ 53 | version: pkg.version, 54 | }), 55 | }, 56 | plugins: [useVuePlugin(), useProgressPlugin(), useCompressPlugin(), useVisualizerPlugin(), useEslintPlugin()], 57 | server: useServer(), 58 | resolve: { 59 | alias: { 60 | '@': path.resolve(__dirname, 'src'), 61 | }, 62 | }, 63 | }) 64 | } 65 | --------------------------------------------------------------------------------