├── .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 | 
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 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/ActionButton/ActionButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
40 |
41 |
62 |
--------------------------------------------------------------------------------
/src/components/Breadcrumb/Breadcrumb.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 | {{ item.meta.title }}
7 |
8 |
9 |
10 |
11 |
33 |
34 |
43 |
--------------------------------------------------------------------------------
/src/components/Chart/Chart.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
91 |
92 |
98 |
--------------------------------------------------------------------------------
/src/components/Cropper/CropperDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
16 |
17 |
18 |
19 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/src/components/Editor/Editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
111 |
112 |
115 |
116 |
127 |
--------------------------------------------------------------------------------
/src/components/Filter/FilterTagItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
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 |
2 |
8 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
{{ index + 1 }}
25 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
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 |
2 |
7 |
8 |
9 |
10 |
11 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/components/SearchBar/SearchBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
10 |
11 |
32 |
33 |
53 |
--------------------------------------------------------------------------------
/src/components/Upload/UploadInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 | {{ loadingBtnText }}
12 |
13 |
17 | {{ btnText }}
18 |
19 |
20 |
21 |
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 |
2 |
3 |
null" />
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
26 |
--------------------------------------------------------------------------------
/src/layouts/RouteViewLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/layouts/components/ActionButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
31 |
32 |
56 |
--------------------------------------------------------------------------------
/src/layouts/components/BasicContent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
34 |
35 |
41 |
--------------------------------------------------------------------------------
/src/layouts/components/BasicSide.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
77 |
78 |
136 |
--------------------------------------------------------------------------------
/src/layouts/components/Brand.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
![]()
9 |
{{ config('app.title') }}
10 |
11 |
12 |
13 |
53 |
54 |
93 |
--------------------------------------------------------------------------------
/src/layouts/components/IframeView.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
11 |
12 |
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 |
2 |
6 |
7 | 返回
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/views/exception/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | 返回
12 |
13 |
14 |
15 |
16 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/views/exception/500.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | 返回
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/views/form/step/components/Step2.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
13 | ****@***.com
14 | test@example.com
15 | Alex
16 | ¥ 5,000.00
17 |
18 |
21 |
25 |
26 |
27 |
28 |
32 | 提交
33 |
34 | 上一步
35 |
36 |
37 |
38 |
39 |
40 |
41 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/views/form/step/components/Step3.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
58 |
59 |
60 | 再转一笔
64 |
65 | 查看账单
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/src/views/form/step/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | 将一个冗长或用户不熟悉的表单任务分成多个步骤,指导用户完成。
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
24 |
28 |
29 |
30 |
31 |
32 |
60 |
61 |
72 |
--------------------------------------------------------------------------------
/src/views/iframe/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
10 |
11 |
12 |
13 |
14 |
30 |
31 |
47 |
--------------------------------------------------------------------------------
/src/views/list/search/components/PageHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
16 |
17 |
21 |
24 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/views/list/table/components/EditDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/src/views/other/badge/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 | 设置
9 | 清除
10 |
11 |
12 |
13 |
14 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/views/other/multi-tab/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 打开欢迎页
6 |
7 |
8 |
11 |
12 | 刷新当前
13 | {{ date }}
14 |
15 |
16 |
19 |
20 | 关闭当前
21 | 关闭其他
22 |
23 |
24 |
27 |
28 |
29 | 设置标题
30 | 还原
31 |
32 |
33 |
34 |
35 |
36 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/src/views/result/fail/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 提交失败
5 | 请核对并修改以下信息后,再重新提交。
6 |
7 | 返回修改
8 |
9 |
10 |
31 |
32 |
33 |
34 |
35 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/views/result/success/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 提交成功
5 | 提交结果页用于反馈一系列操作任务的处理结果, 如果仅是简单操作,使用 Message 全局提示反馈即可。
7 | 本文字区域可以展示简单的补充说明,如果有类似展示
8 | “单据”的需求,下面这个灰色区域可以呈现比较复杂的内容。
10 |
11 |
12 | 返回列表
13 | 查看项目
14 | 打印
15 |
16 |
17 |
18 |
19 | 12345
20 | 张三
21 | 2016-12-12 ~ 2017-12-12
22 |
23 |
27 |
28 |
29 |
30 |
31 | 曲丽丽
32 |
33 |
34 |
2016-12-12 12:32
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 周毛毛
43 |
44 |
45 |
催一下
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/views/system/dict/components/EditDictDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
16 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/src/views/system/role/components/EditRoleDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
16 |
19 |
20 |
21 |
22 |
23 |
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 |
--------------------------------------------------------------------------------