├── .editorconfig ├── .env ├── .env.preview ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── deploy-preview.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── config └── plugin.config.js ├── jest.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── public ├── avatar2.jpg ├── favicon.ico ├── index.html └── loading │ ├── loading.css │ ├── loading.html │ └── option2 │ ├── html_code_segment.html │ ├── loading.css │ └── loading.svg ├── src ├── App.vue ├── api │ ├── auth.js │ ├── captcha.js │ ├── i18n │ │ └── i18n-data.js │ ├── log │ │ ├── access-log.js │ │ ├── login-log.js │ │ └── operation-log.js │ ├── notify │ │ └── announcement.js │ ├── sample │ │ └── document.js │ └── system │ │ ├── config.js │ │ ├── dict-item.js │ │ ├── dict.js │ │ ├── lov.js │ │ ├── menu.js │ │ ├── organization.js │ │ ├── role.js │ │ └── user.js ├── assets │ ├── background.svg │ ├── captcha │ │ ├── captcha-slider.png │ │ ├── close.png │ │ └── refresh.png │ ├── icons │ │ ├── colorPicker.svg │ │ ├── compress.svg │ │ ├── draggable.svg │ │ ├── expend.svg │ │ └── i18n.svg │ └── logo.svg ├── components │ ├── Breadcrumb │ │ └── Breadcrumb.vue │ ├── CropperModal │ │ └── index.vue │ ├── Dict │ │ ├── dictMixin.js │ │ ├── dictPlugin.js │ │ ├── display │ │ │ ├── DictBadge.vue │ │ │ ├── DictTag.vue │ │ │ ├── DictText.vue │ │ │ └── dictDisplayMixin.js │ │ └── group │ │ │ ├── DictCheckBoxGroup.vue │ │ │ ├── DictRadioGroup.vue │ │ │ ├── DictSelect.vue │ │ │ └── dictGroupMixin.js │ ├── Editor │ │ └── WangEditor.vue │ ├── FooterToolbar │ │ ├── FooterToolBar.vue │ │ ├── index.js │ │ ├── index.less │ │ └── index.md │ ├── GlobalFooter │ │ └── index.vue │ ├── GlobalHeader │ │ ├── AvatarDropdown.vue │ │ ├── LangSelect.vue │ │ ├── LeftContent.vue │ │ ├── NoticeIcon.vue │ │ ├── RightContent.vue │ │ ├── index.less │ │ └── index.vue │ ├── IconSelector │ │ ├── IconSelector.vue │ │ ├── IconSelectorModal.vue │ │ ├── icons.js │ │ └── index.js │ ├── Lov │ │ ├── LovLocal.vue │ │ ├── LovModal.vue │ │ ├── lovOptions.js │ │ └── lovPlugin.js │ ├── Menu │ │ ├── SideMenu.less │ │ ├── SideMenu.vue │ │ ├── TopMenu.vue │ │ ├── index.js │ │ ├── menu.js │ │ └── menu.render.js │ ├── MultiTab │ │ ├── MultiTab.vue │ │ ├── index.js │ │ └── index.less │ ├── Notify │ │ ├── AnnouncementModal.vue │ │ └── AnnouncementRibbon.vue │ ├── PageLoading │ │ └── index.jsx │ ├── ProjectLogo │ │ ├── ProjectLogo.less │ │ ├── ProjectLogo.vue │ │ └── index.js │ ├── Result │ │ └── index.vue │ ├── Screenfull │ │ └── index.vue │ ├── SettingDrawer │ │ ├── SettingDrawer.vue │ │ ├── index.js │ │ ├── settingConfig.js │ │ └── themeColor.js │ ├── SideBar │ │ ├── Sider.vue │ │ └── index.vue │ ├── Table │ │ ├── ColumnSetting.vue │ │ ├── EditableCell.vue │ │ ├── ProTable.js │ │ ├── alert.less │ │ ├── columnSetting.less │ │ ├── listToolBar.less │ │ ├── proTable.less │ │ ├── searchForm.less │ │ └── toolbar.less │ ├── Verifition │ │ ├── Verify.vue │ │ └── Verify │ │ │ ├── VerifyPoints.vue │ │ │ └── VerifySlide.vue │ └── WebSocket │ │ ├── GlobalWebSocket.vue │ │ └── GlobalWebSocketListener.vue ├── config │ ├── defaultSettings.js │ └── projectConfig.js ├── core │ ├── bootstrap.js │ ├── icons.js │ ├── lazy_lib │ │ └── components_use.js │ ├── lazy_use.js │ └── use.js ├── layouts │ ├── BasicLayout.vue │ ├── BlankRouterView.vue │ ├── ContentView.vue │ ├── UserLayout.vue │ └── index.js ├── locales │ ├── index.js │ └── lang │ │ ├── en-US.js │ │ └── zh-CN.js ├── main.js ├── mixins │ ├── formMixin.js │ ├── index.js │ ├── pageFormMixin.js │ ├── popUpFormMixin.js │ └── tablePageMixin.js ├── permission.js ├── router │ ├── constantRouter.js │ ├── dynamicRouter.js │ └── index.js ├── store │ ├── index.js │ ├── modules │ │ ├── app.js │ │ ├── dict.js │ │ ├── i18n.js │ │ ├── lov.js │ │ └── user.js │ ├── mutation-types.js │ └── storage-types.js ├── styles │ ├── ballcat.less │ ├── global.less │ ├── index.less │ └── nprogress.less ├── utils │ ├── authorize.js │ ├── captchaUtil.js │ ├── device.js │ ├── domUtil.js │ ├── fileUtil.js │ ├── filter.js │ ├── mixin.js │ ├── password.js │ ├── request.js │ ├── strUtil.js │ ├── treeUtil.js │ ├── util.js │ └── utils.less └── views │ ├── account │ └── settings │ │ ├── AvatarModal.vue │ │ ├── BaseSetting.vue │ │ ├── Binding.vue │ │ ├── Index.vue │ │ ├── Notification.vue │ │ └── Security.vue │ ├── exception │ └── index.vue │ ├── i18n │ ├── I18nMessageModal.vue │ ├── LanguageText.vue │ └── i18n-data │ │ ├── I18nDataCreateModal.vue │ │ ├── I18nDataImportModal.vue │ │ ├── I18nDataPage.vue │ │ └── I18nDataUpdateModal.vue │ ├── iframe │ └── index.vue │ ├── log │ ├── access-log │ │ └── AccessLogPage.vue │ ├── login-log │ │ └── LoginLogPage.vue │ └── operation-log │ │ └── OperationLogPage.vue │ ├── notify │ └── announcement │ │ ├── AnnouncementPage.vue │ │ └── AnnouncementPageForm.vue │ ├── oauth2 │ └── OAuth2Authorize.vue │ ├── redirect │ └── index.vue │ ├── sample │ └── document │ │ ├── DocumentModalForm.vue │ │ └── DocumentPage.vue │ ├── system │ ├── config │ │ ├── SysConfigModalForm.vue │ │ └── SysConfigPage.vue │ ├── dict │ │ ├── SysDictItemModal.vue │ │ ├── SysDictItemPageForm.vue │ │ ├── SysDictModalForm.vue │ │ └── SysDictPage.vue │ ├── menu │ │ ├── SysMenuModalForm.vue │ │ └── SysMenuPage.vue │ ├── organization │ │ ├── SysOrganizationModalForm.vue │ │ ├── SysOrganizationPage.vue │ │ └── SysOrganizationTreeSelect.vue │ ├── role │ │ ├── SysRoleGrantDrawer.vue │ │ ├── SysRoleModalForm.vue │ │ ├── SysRolePage.vue │ │ ├── SysRoleSelect.vue │ │ └── SysRoleUserModal.vue │ └── user │ │ ├── PasswordModal.vue │ │ ├── ScopeModal.vue │ │ ├── SysUserModalForm.vue │ │ └── SysUserPage.vue │ └── user │ ├── Login.vue │ ├── Register.vue │ └── RegisterResult.vue ├── vue.config.js ├── webstorm.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | VUE_APP_API_BASE_URL=/api 3 | -------------------------------------------------------------------------------- /.env.preview: -------------------------------------------------------------------------------- 1 | NODE_ENV=preview 2 | VUE_APP_API_BASE_URL=/api 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.vuejs.org/user-guide 2 | module.exports = { 3 | root: true, 4 | env: { 5 | node: true, 6 | }, 7 | extends: [ 8 | // add more generic rulesets here, such as: 9 | // 'eslint:recommended', 10 | 'plugin:vue/base', 11 | // 'plugin:vue/vue3-recommended' 12 | 'plugin:vue/recommended' // Use this if you are using Vue.js 2.x. 13 | ], 14 | parserOptions: { 15 | parser: "@babel/eslint-parser", 16 | }, 17 | rules: { 18 | // override/add rules settings here, such as: 19 | // 'vue/no-unused-vars': 'error' 20 | "no-console": process.env.NODE_ENV === "production" ? 2 : 0, 21 | "no-debugger": process.env.NODE_ENV === "production" ? 2 : 0, 22 | 23 | // 'vue/require-default-prop': 'off', 24 | 25 | // 关闭使用 v-html 的检查 26 | 'vue/no-v-html': 'off', 27 | // 3个属性以上才进行换行 28 | 'vue/max-attributes-per-line': ['error', { 29 | 'singleline': 3, 30 | 'multiline': 1 31 | }], 32 | "vue/first-attribute-linebreak": ["error", { 33 | "singleline": "ignore", 34 | "multiline": "below" 35 | }], 36 | 'vue/singleline-html-element-content-newline': 'off', 37 | 'vue/multiline-html-element-content-newline': 'off', 38 | // 关闭组件名称必须多单词 39 | 'vue/multi-word-component-names': 'off', 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | public/* linguist-vendored -------------------------------------------------------------------------------- /.github/workflows/deploy-preview.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Deploy to preview server 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | # 手动触发事件 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [16.x] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | - name: Install npm dependencies 28 | run: yarn install 29 | - name: Run build task 30 | run: yarn build 31 | - name: Deploy to Server 32 | uses: easingthemes/ssh-deploy@main 33 | env: 34 | SSH_PRIVATE_KEY: ${{ secrets.PREVIEW_SERVER_SSH_PRIVATE_KEY }} 35 | ARGS: '-avz --delete' 36 | SOURCE: 'dist/' 37 | REMOTE_HOST: ${{ secrets.PREVIEW_SERVER_HOST }} 38 | REMOTE_USER: ${{ secrets.PREVIEW_SERVER_USER }} 39 | TARGET: ${{ secrets.VUE2_TARGET }} 40 | EXCLUDE: "/dist/, /node_modules/" 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | .history/ 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10.15.0 4 | cache: yarn 5 | script: 6 | - yarn 7 | - yarn run lint --no-fix && yarn run build 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hccake 4 | Copyright (c) 2018 Anan Yang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BallCat 2 | 3 | ## 前言 4 | 5 | `BallCat` 组织旨在为项目快速开发提供一系列的基础能力,方便使用者根据项目需求快速进行功能拓展。 6 | 7 | 在以前使用其他后台管理脚手架进行开发时,经常会遇到因为项目业务原因需要进行二开的问题,在长期的开发后,一旦源项目进行迭代升级,很难进行同步更新。 8 | 9 | 所以 BallCat 推荐开发项目时,以依赖的方式引入 BallCat 中提供的所有功能,包括用户及权限管理相关,这样后续跟随 BallCat 版本升级时只需要修改对应的依赖版本号即可完成同步,BallCat 也会为每个版本升级提供 SQL 改动文件。 10 | 11 | BallCat 中的所有 JAR 包都已推送至中央仓库,尝鲜使用快照版本也可以通过配置 sonatype 的快照仓库获取。 12 | 13 | 如果在使用中遇到了必须通过二开修改源码才能解决的问题或功能时,欢迎提 issuse,如果功能具有通用性,我们会为 BallCat 添加此能力,也欢迎直接 PR 你的改动。 14 | 15 | 16 | 17 | ## 相关仓库 18 | 19 | | 项目 | 简介 | github 地址 | 20 | | --------------- | ------------------------------------ | --------------------------------------------------- | 21 | | ballcat | 核心项目组件 | https://github.com/ballcat-projects/ballcat | 22 | | ballcat-ui-vue | 管理后台前端 | https://github.com/ballcat-projects/ballcat-ui-vue | 23 | | ballcat-codegen | 代码生成器 | https://github.com/ballcat-projects/ballcat-codegen | 24 | | ballcat-samples | 一些使用示例,例如权限管理模块的引入 | https://github.com/ballcat-projects/ballcat-samples | 25 | 26 | 27 | 28 | ## 地址链接 29 | 30 | **管理后台预览**:http://preview.ballcat.cn 31 | > admin / a123456 32 | 33 | **代码生成器预览**:http://codegen.ballcat.cn/ 34 | 35 | **文档地址**:http://www.ballcat.cn/ (目前文档只有少量内容,会陆续填坑) 36 | 37 | 38 | 39 | # ballcat-ui-vue 40 | 41 | 此仓库是 BallCat 项目中的后台管理的前端实现,基于 Vue + Ant-Design-Vue 实现。 42 | 43 | 项目对于基础表格页面和表单页面的增删改查等操作抽取了 mixin 混入,简化 CRUD 开发难度。 44 | 45 | 另外还提供了一些基本的业务组件,如字典选择,字典标签 和 弹窗选择器等常用功能组件。 46 | 47 | 48 | 49 | ## 项目结构 50 | 51 | ```s 52 | |-- config -- 切换主题色使用的插件 53 | |-- public -- 依赖的静态资源存放 54 | `-- src 55 | |-- api -- 和服务端交互的请求方法 56 | |-- assets -- 本地静态资源 57 | |-- components -- 通用组件 58 | |-- config -- 框架配置 59 | |-- core -- 项目引导, 全局配置初始化,依赖包引入等 60 | |-- layouts -- 布局 61 | |-- locales -- 国际化 62 | |-- mixins -- 增删改查页面的抽取模板 63 | |-- router -- 路由相关 64 | |-- store -- 数据存储相关 65 | |-- styles -- 项目中的一些全局样式 66 | |-- utils -- 工具类 67 | |-- views -- 页面 68 | |-- App.Vue -- Vue 模板入口 69 | |-- main.js -- Vue 入口js 70 | `-- permission.js -- 路由守卫 权限控制 71 | ``` 72 | 73 | 74 | 75 | ## 核心依赖 76 | 77 | | 依赖 | 版本 | 官网 | 78 | | -------------- | ------ | --------------------------------- | 79 | | Vue | 2.6.12 | https://cn.vuejs.org/ | 80 | | Vue Router | 3.5.1 | https://router.vuejs.org/zh/ | 81 | | Vuex | 3.6.2 | https://vuex.vuejs.org/zh/guide/ | 82 | | Axios | 0.21.1 | https://axios-http.com/docs/intro | 83 | | Ant Design Vue | 1.7.4 | https://www.antdv.com | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app', 4 | [ 5 | '@babel/preset-env', 6 | { 7 | 'useBuiltIns': 'entry', 8 | 'corejs': '3' // 声明corejs版本 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /config/plugin.config.js: -------------------------------------------------------------------------------- 1 | const ThemeColorReplacer = require('webpack-theme-color-replacer') 2 | const generate = require('@ant-design/colors').generate 3 | 4 | const getAntdSerials = (color) => { 5 | // 淡化(即less的tint) 6 | const lightens = new Array(9).fill().map((t, i) => { 7 | return ThemeColorReplacer.varyColor.lighten(color, i / 10) 8 | }) 9 | const colorPalettes = generate(color) 10 | return lightens.concat(colorPalettes) 11 | } 12 | 13 | const themePluginOption = { 14 | fileName: 'css/theme-colors-[contenthash:8].css', 15 | matchColors: getAntdSerials('#1890ff'), // 主色系列 16 | // 改变样式选择器,解决样式覆盖问题 17 | changeSelector (selector) { 18 | switch (selector) { 19 | case '.ant-calendar-today .ant-calendar-date': 20 | return ':not(.ant-calendar-selected-date):not(.ant-calendar-selected-day)' + selector 21 | case '.ant-btn:focus,.ant-btn:hover': 22 | return '.ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger)' 23 | case '.ant-btn.active,.ant-btn:active': 24 | return '.ant-btn.active:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:active:not(.ant-btn-primary):not(.ant-btn-danger)' 25 | case '.ant-steps-item-process .ant-steps-item-icon > .ant-steps-icon': 26 | return ':not(.ant-steps-item-process)' + selector 27 | case '.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover': 28 | case '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal > .ant-menu-submenu-selected,.ant-menu-horizontal > .ant-menu-submenu:hover': 29 | return '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu:hover' 30 | case '.ant-menu-horizontal > .ant-menu-item-selected > a': 31 | return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item-selected > a' 32 | case '.ant-menu-horizontal > .ant-menu-item > a:hover': 33 | return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item > a:hover' 34 | default : 35 | return selector 36 | } 37 | } 38 | } 39 | 40 | const createThemeColorReplacerPlugin = () => new ThemeColorReplacer(themePluginOption) 41 | 42 | module.exports = createThemeColorReplacerPlugin 43 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue' 7 | ], 8 | transform: { 9 | '^.+\\.vue$': 'vue-jest', 10 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 11 | '^.+\\.jsx?$': 'babel-jest' 12 | }, 13 | moduleNameMapper: { 14 | '^@/(.*)$': '/src/$1' 15 | }, 16 | snapshotSerializers: [ 17 | 'jest-serializer-vue' 18 | ], 19 | testMatch: [ 20 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 21 | ], 22 | testURL: 'http://localhost/' 23 | } 24 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | } 8 | }, 9 | "exclude": ["node_modules", "dist"], 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ballcat-ui-vue", 3 | "version": "1.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "build:preview": "vue-cli-service build --mode preview", 9 | "lint": "vue-cli-service lint", 10 | "lint:nofix": "vue-cli-service lint --no-fix", 11 | "test:unit": "vue-cli-service test:unit" 12 | }, 13 | "dependencies": { 14 | "@antv/data-set": "^0.11.8", 15 | "ant-design-vue": "^1.7.8", 16 | "axios": "^1.6.0", 17 | "core-js": "^3.8.3", 18 | "crypto-js": "^4.2.0", 19 | "enquire.js": "^2.1.6", 20 | "lodash.get": "^4.4.2", 21 | "lodash.pick": "^4.4.0", 22 | "moment": "^2.29.1", 23 | "nprogress": "^0.2.0", 24 | "screenfull": "^5.1.0", 25 | "vue": "^2.7.8", 26 | "vue-clipboard2": "^0.3.1", 27 | "vue-codemirror": "^4.0.6", 28 | "vue-color": "^2.8.1", 29 | "vue-cropper": "0.5.5", 30 | "vue-i18n": "^8.24.2", 31 | "vue-ls": "^3.2.2", 32 | "vue-router": "^3.5.4", 33 | "vuedraggable": "^2.24.3", 34 | "vuex": "^3.6.2", 35 | "wangeditor": "^4.6.11" 36 | }, 37 | "devDependencies": { 38 | "@ant-design/colors": "^5.1.1", 39 | "@babel/core": "^7.12.16", 40 | "@babel/eslint-parser": "^7.12.16", 41 | "@vue/cli-plugin-babel": "~5.0.0", 42 | "@vue/cli-plugin-eslint": "~5.0.0", 43 | "@vue/cli-plugin-router": "~5.0.0", 44 | "@vue/cli-plugin-vuex": "~5.0.0", 45 | "@vue/cli-service": "~5.0.0", 46 | "@vue/eslint-config-standard": "^7.0.0", 47 | "@vue/test-utils": "^1.1.3", 48 | "babel-jest": "^26.6.3", 49 | "babel-plugin-import": "^1.13.3", 50 | "eslint": "^8.21.0", 51 | "eslint-config-prettier": "^8.3.0", 52 | "eslint-plugin-prettier": "^4.0.0", 53 | "eslint-plugin-vue": "^8.0.3", 54 | "less": "^4.0.0", 55 | "less-loader": "^8.0.0", 56 | "vue-svg-loader": "^0.17.0-beta.2", 57 | "vue-template-compiler": "^2.6.12", 58 | "webpack-theme-color-replacer": "^1.4.1" 59 | }, 60 | "postcss": { 61 | "plugins": { 62 | "autoprefixer": {} 63 | } 64 | }, 65 | "browserslist": [ 66 | "> 1%", 67 | "last 2 versions", 68 | "not ie <= 10" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /public/avatar2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballcat-projects/ballcat-ui-vue/147f5c457770014723ee156a4eabfec6af582d2f/public/avatar2.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballcat-projects/ballcat-ui-vue/147f5c457770014723ee156a4eabfec6af582d2f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= webpackConfig.name %> 9 | 10 | 11 | <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %> 12 | 13 | <% } %> 14 | 15 | 16 | 19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 | 27 | <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %> 28 | 29 | <% } %> 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /public/loading/loading.css: -------------------------------------------------------------------------------- 1 | #preloadingAnimation{position:fixed;left:0;top:0;height:100%;width:100%;background:#ffffff;user-select:none;z-index: 9999;overflow: hidden}.lds-roller{display:inline-block;position:relative;left:50%;top:50%;transform:translate(-50%,-50%);width:64px;height:64px;}.lds-roller div{animation:lds-roller 1.2s cubic-bezier(0.5,0,0.5,1) infinite;transform-origin:32px 32px;}.lds-roller div:after{content:" ";display:block;position:absolute;width:6px;height:6px;border-radius:50%;background:#13c2c2;margin:-3px 0 0 -3px;}.lds-roller div:nth-child(1){animation-delay:-0.036s;}.lds-roller div:nth-child(1):after{top:50px;left:50px;}.lds-roller div:nth-child(2){animation-delay:-0.072s;}.lds-roller div:nth-child(2):after{top:54px;left:45px;}.lds-roller div:nth-child(3){animation-delay:-0.108s;}.lds-roller div:nth-child(3):after{top:57px;left:39px;}.lds-roller div:nth-child(4){animation-delay:-0.144s;}.lds-roller div:nth-child(4):after{top:58px;left:32px;}.lds-roller div:nth-child(5){animation-delay:-0.18s;}.lds-roller div:nth-child(5):after{top:57px;left:25px;}.lds-roller div:nth-child(6){animation-delay:-0.216s;}.lds-roller div:nth-child(6):after{top:54px;left:19px;}.lds-roller div:nth-child(7){animation-delay:-0.252s;}.lds-roller div:nth-child(7):after{top:50px;left:14px;}.lds-roller div:nth-child(8){animation-delay:-0.288s;}.lds-roller div:nth-child(8):after{top:45px;left:10px;}#preloadingAnimation .load-tips{color: #13c2c2;font-size:2rem;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin-top:80px;text-align:center;width:400px;height:64px;} @keyframes lds-roller{0%{transform:rotate(0deg);} 100%{transform:rotate(360deg);}} -------------------------------------------------------------------------------- /public/loading/loading.html: -------------------------------------------------------------------------------- 1 |
Loading
-------------------------------------------------------------------------------- /public/loading/option2/html_code_segment.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
-------------------------------------------------------------------------------- /public/loading/option2/loading.css: -------------------------------------------------------------------------------- 1 | .preloading-animate{background:#ffffff;width:100%;height:100%;position:fixed;left:0;top:0;z-index:299;}.preloading-animate .preloading-wrapper{position:absolute;width:5rem;height:5rem;left:50%;top:50%;transform:translate(-50%,-50%);}.preloading-animate .preloading-wrapper .preloading-balls{font-size:5rem;} -------------------------------------------------------------------------------- /public/loading/option2/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 35 | 40 | -------------------------------------------------------------------------------- /src/api/auth.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import Vue from 'vue' 3 | import { ACCESS_TOKEN } from '@/store/storage-types' 4 | 5 | // Base64(clientId:clientSecret) 6 | const BASIC_AUTHORIZATION = 'Basic dWk6dWk=' 7 | 8 | export function login (parameter) { 9 | return request({ 10 | headers: { 11 | 'Authorization': BASIC_AUTHORIZATION 12 | }, 13 | url: '/oauth2/token', 14 | method: 'post', 15 | params: Object.assign({'grant_type': 'password'}, parameter) 16 | }) 17 | } 18 | 19 | export function checkToken (token) { 20 | return request({ 21 | headers: { 22 | 'Authorization': BASIC_AUTHORIZATION 23 | }, 24 | url: '/oauth2/check_token', 25 | method: 'post', 26 | params: { token: token } 27 | }) 28 | } 29 | 30 | export function logout () { 31 | const accessToken = Vue.ls.get(ACCESS_TOKEN) 32 | return request({ 33 | url: '/oauth2/revoke', 34 | method: 'POST', 35 | headers: { 36 | Authorization: BASIC_AUTHORIZATION 37 | }, 38 | params: { token: accessToken } 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/api/captcha.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 此处可直接引用自己项目封装好的 axios 配合后端联调 3 | */ 4 | import request from '@/utils/request' //组件内部封装的axios 5 | // import request from "@/api/axios.js" //调用项目封装的axios 6 | 7 | //获取验证图片 以及token 8 | export function reqGet(params) { 9 | return request({ 10 | url: '/captcha/tianai/gen', 11 | method: 'get', 12 | params 13 | }) 14 | } 15 | 16 | //滑动或者点选验证 17 | export function reqCheck(params,data) { 18 | return request({ 19 | url: '/captcha/tianai/check', 20 | method: 'post', 21 | data, 22 | params 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/api/i18n/i18n-data.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getPage (query) { 4 | return request({ 5 | url: '/i18n/i18n-data/page', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function addObj (obj) { 12 | return request({ 13 | url: '/i18n/i18n-data', 14 | method: 'post', 15 | data: obj 16 | }) 17 | } 18 | 19 | export function delObj (code, languageTag) { 20 | return request({ 21 | url: '/i18n/i18n-data', 22 | method: 'delete', 23 | params: { 24 | code: code, 25 | languageTag: languageTag 26 | } 27 | }) 28 | } 29 | 30 | export function putObj (obj) { 31 | return request({ 32 | url: '/i18n/i18n-data', 33 | method: 'put', 34 | data: obj 35 | }) 36 | } 37 | 38 | export function exportExcel (query) { 39 | return request({ 40 | url: '/i18n/i18n-data/export', 41 | method: 'get', 42 | params: query, 43 | responseType: 'blob' 44 | }) 45 | } 46 | 47 | export function importExcel (formData) { 48 | return request.post('/i18n/i18n-data/import', formData, { contentType: false, processData: false }) 49 | } 50 | 51 | export function downloadTemplate () { 52 | return request({ 53 | url: '/i18n/i18n-data/excel-template', 54 | method: 'get', 55 | responseType: 'blob' 56 | }) 57 | } 58 | 59 | export function listByCode (code) { 60 | return request({ 61 | url: '/i18n/i18n-data/list', 62 | method: 'get', 63 | params: { code: code }, 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /src/api/log/access-log.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getPage(query) { 4 | return request({ 5 | url: '/log/access-log/page', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/api/log/login-log.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getPage(query) { 4 | return request({ 5 | url: '/log/login-log/page', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/api/log/operation-log.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getPage(query) { 4 | return request({ 5 | url: '/log/operation-log/page', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/api/notify/announcement.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getPage(query) { 4 | return request({ 5 | url: '/notify/announcement/page', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function addObj(obj) { 12 | return request({ 13 | url: '/notify/announcement', 14 | method: 'post', 15 | data: obj 16 | }) 17 | } 18 | 19 | export function delObj(id) { 20 | return request({ 21 | url: '/notify/announcement/' + id, 22 | method: 'delete' 23 | }) 24 | } 25 | 26 | export function putObj(obj) { 27 | return request({ 28 | url: '/notify/announcement', 29 | method: 'put', 30 | data: obj 31 | }) 32 | } 33 | 34 | 35 | export function publish(id) { 36 | return request({ 37 | url: '/notify/announcement/publish/' + id, 38 | method: 'patch' 39 | }) 40 | } 41 | 42 | 43 | export function close(id) { 44 | return request({ 45 | url: '/notify/announcement/close/' + id, 46 | method: 'patch' 47 | }) 48 | } 49 | 50 | export function uploadImage (resultFiles) { 51 | const formData = new FormData() 52 | resultFiles.forEach(file => { 53 | formData.append('files', file); 54 | }); 55 | return request.post('/notify/announcement/image', formData, { contentType: false, processData: false }) 56 | } 57 | 58 | 59 | export function getUserAnnouncements() { 60 | return request({ 61 | url: '/notify/announcement/user', 62 | method: 'get' 63 | }) 64 | } 65 | 66 | 67 | export function readAnnouncement(announcementId) { 68 | return request({ 69 | url: '/notify/user-announcement/read/' + announcementId, 70 | method: 'patch' 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /src/api/sample/document.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getPage(query) { 4 | return request({ 5 | url: '/sample/document/page', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function addObj(obj) { 12 | return request({ 13 | url: '/sample/document', 14 | method: 'post', 15 | data: obj 16 | }) 17 | } 18 | 19 | export function delObj(id) { 20 | return request({ 21 | url: '/sample/document/' + id, 22 | method: 'delete' 23 | }) 24 | } 25 | 26 | export function putObj(obj) { 27 | return request({ 28 | url: '/sample/document', 29 | method: 'put', 30 | data: obj 31 | }) 32 | } -------------------------------------------------------------------------------- /src/api/system/config.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getPage(query) { 4 | return request({ 5 | url: '/system/config/page', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function addObj(obj) { 12 | return request({ 13 | url: '/system/config', 14 | method: 'post', 15 | data: obj 16 | }) 17 | } 18 | 19 | export function delObj(confKey) { 20 | return request({ 21 | url: `/system/config?confKey=${confKey}`, 22 | method: 'delete' 23 | }) 24 | } 25 | 26 | export function putObj(obj) { 27 | return request({ 28 | url: '/system/config', 29 | method: 'put', 30 | data: obj 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/api/system/dict-item.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getPage(query) { 4 | return request({ 5 | url: '/system/dict/item/page', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function addObj(obj) { 12 | return request({ 13 | url: '/system/dict/item', 14 | method: 'post', 15 | data: obj 16 | }) 17 | } 18 | 19 | 20 | export function delObj(id) { 21 | return request({ 22 | url: '/system/dict/item/' + id, 23 | method: 'delete' 24 | }) 25 | } 26 | 27 | export function putObj(obj) { 28 | return request({ 29 | url: '/system/dict/item', 30 | method: 'put', 31 | data: obj 32 | }) 33 | } 34 | 35 | export function updateStatus(id, status) { 36 | return request({ 37 | url: `/system/dict/item/${id}?status=${status}`, 38 | method: 'patch' 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/api/system/dict.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getDictData (dictCodes) { 4 | return request({ 5 | url: '/system/dict/data?dictCodes=' + dictCodes.join(','), 6 | method: 'get' 7 | }) 8 | } 9 | 10 | export function invalidDictHash(map) { 11 | return request({ 12 | url: '/system/dict/invalid-hash', 13 | method: 'post', 14 | data: map 15 | }) 16 | } 17 | 18 | 19 | export function getPage(query) { 20 | return request({ 21 | url: '/system/dict/page', 22 | method: 'get', 23 | params: query 24 | }) 25 | } 26 | 27 | export function addObj(obj) { 28 | return request({ 29 | url: '/system/dict', 30 | method: 'post', 31 | data: obj 32 | }) 33 | } 34 | 35 | export function getObj(id) { 36 | return request({ 37 | url: '/system/dict/' + id, 38 | method: 'get' 39 | }) 40 | } 41 | 42 | export function delObj(id) { 43 | return request({ 44 | url: '/system/dict/' + id, 45 | method: 'delete' 46 | }) 47 | } 48 | 49 | export function putObj(obj) { 50 | return request({ 51 | url: '/system/dict', 52 | method: 'put', 53 | data: obj 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /src/api/system/lov.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getData(keyword){ 4 | return request.get(`/system/lov/data/${keyword}`) 5 | } 6 | 7 | export function getPage(query) { 8 | return request({ 9 | url: '/system/lov/page', 10 | method: 'get', 11 | params: query 12 | }) 13 | } 14 | 15 | export function update(obj) { 16 | return request({ 17 | url: '/system/lov', 18 | method: 'post', 19 | data: obj 20 | }) 21 | } 22 | 23 | export function delObj(id) { 24 | return request({ 25 | url: '/system/lov/' + id, 26 | method: 'delete' 27 | }) 28 | } 29 | 30 | export function create(obj) { 31 | return request({ 32 | url: '/system/lov', 33 | method: 'put', 34 | data: obj 35 | }) 36 | } 37 | 38 | export function check(map) { 39 | return request({ 40 | url: '/system/lov/check', 41 | method: 'post', 42 | data: map 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/api/system/menu.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getLoginUserMenu () { 4 | return request({ 5 | url: '/system/menu/router', 6 | method: 'get' 7 | }) 8 | } 9 | 10 | export function listMenu(query) { 11 | return request({ 12 | url: '/system/menu/list', 13 | method: 'get', 14 | params: query 15 | }) 16 | } 17 | 18 | export function grantList() { 19 | return request({ 20 | url: '/system/menu/grant-list', 21 | method: 'get' 22 | }) 23 | } 24 | 25 | 26 | export function addObj(obj) { 27 | return request({ 28 | url: '/system/menu', 29 | method: 'post', 30 | data: obj 31 | }) 32 | } 33 | 34 | export function delObj(id) { 35 | return request({ 36 | url: '/system/menu/' + id, 37 | method: 'delete' 38 | }) 39 | } 40 | 41 | export function putObj(obj) { 42 | return request({ 43 | url: '/system/menu', 44 | method: 'put', 45 | data: obj 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/api/system/organization.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function listOrganization() { 4 | return request({ 5 | url: '/system/organization/list', 6 | method: 'get' 7 | }) 8 | } 9 | 10 | export function addObj(obj) { 11 | return request({ 12 | url: '/system/organization', 13 | method: 'post', 14 | data: obj 15 | }) 16 | } 17 | 18 | export function delObj(id) { 19 | return request({ 20 | url: '/system/organization/' + id, 21 | method: 'delete' 22 | }) 23 | } 24 | 25 | export function putObj(obj) { 26 | return request({ 27 | url: '/system/organization', 28 | method: 'put', 29 | data: obj 30 | }) 31 | } 32 | 33 | export function revised() { 34 | return request({ 35 | url: '/system/organization/revised', 36 | method: 'patch' 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/api/system/role.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getPage (query) { 4 | return request({ 5 | url: '/system/role/page', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function getObj (id) { 12 | return request({ 13 | url: '/system/role/' + id, 14 | method: 'get' 15 | }) 16 | } 17 | 18 | export function addObj (obj) { 19 | return request({ 20 | url: '/system/role', 21 | method: 'post', 22 | data: obj 23 | }) 24 | } 25 | 26 | export function putObj (obj) { 27 | return request({ 28 | url: '/system/role', 29 | method: 'put', 30 | data: obj 31 | }) 32 | } 33 | 34 | export function delObj (id) { 35 | return request({ 36 | url: '/system/role/' + id, 37 | method: 'delete' 38 | }) 39 | } 40 | 41 | export function getSelectData () { 42 | return request({ 43 | url: '/system/role/select', 44 | method: 'get' 45 | }) 46 | } 47 | 48 | export function getPermissionCode (roleCode) { 49 | return request({ 50 | url: '/system/role/permission/code/' + roleCode, 51 | method: 'get' 52 | }) 53 | } 54 | 55 | export function putPermissionIds (roleCode, data) { 56 | return request({ 57 | url: '/system/role/permission/code/' + roleCode, 58 | method: 'put', 59 | data: data 60 | }) 61 | } 62 | 63 | export function getRoleUserPage (query) { 64 | return request({ 65 | url: '/system/role/user/page', 66 | method: 'get', 67 | params: query 68 | }) 69 | } 70 | 71 | export function unbindRoleUser (userId, roleCode) { 72 | return request({ 73 | url: '/system/role/user', 74 | method: 'delete', 75 | params: { 76 | userId: userId, 77 | roleCode: roleCode 78 | } 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /src/api/system/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import qs from 'qs' 3 | 4 | export function getPage (query) { 5 | return request({ 6 | url: '/system/user/page', 7 | method: 'get', 8 | params: query 9 | }) 10 | } 11 | 12 | export function addObj (obj) { 13 | return request({ 14 | url: '/system/user', 15 | method: 'post', 16 | data: obj 17 | }) 18 | } 19 | 20 | export function putObj (obj) { 21 | return request({ 22 | url: '/system/user', 23 | method: 'put', 24 | data: obj 25 | }) 26 | } 27 | 28 | export function delObj (id) { 29 | return request({ 30 | url: '/system/user/' + id, 31 | method: 'delete' 32 | }) 33 | } 34 | 35 | export function getUserScope (userId) { 36 | return request({ 37 | url: '/system/user/scope/' + userId, 38 | method: 'get' 39 | }) 40 | } 41 | 42 | export function putUserScope (userId, userScope) { 43 | return request({ 44 | url: '/system/user/scope/' + userId, 45 | method: 'put', 46 | data: userScope 47 | }) 48 | } 49 | 50 | export function changePassword (userId, data) { 51 | return request({ 52 | url: '/system/user/pass/' + userId, 53 | method: 'put', 54 | data: data 55 | }) 56 | } 57 | 58 | export function updateStatus (userIds, status) { 59 | return request({ 60 | url: '/system/user/status/', 61 | method: 'put', 62 | params: { 'status': status }, 63 | data: userIds 64 | }) 65 | } 66 | 67 | export function updateAvatar (userId, fileObj) { 68 | const formData = new FormData() 69 | formData.append('file', fileObj.data, fileObj.name) 70 | formData.append('userId', userId) 71 | return request.post('/system/user/avatar', formData, { contentType: false, processData: false }) 72 | } 73 | 74 | export function getSelectData (userTypesArr) { 75 | return request({ 76 | url: '/system/user/select', 77 | params: { userTypes: userTypesArr }, 78 | paramsSerializer: function (params) { 79 | return qs.stringify(params, { arrayFormat: 'repeat' }) 80 | }, 81 | method: 'get' 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /src/assets/captcha/captcha-slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballcat-projects/ballcat-ui-vue/147f5c457770014723ee156a4eabfec6af582d2f/src/assets/captcha/captcha-slider.png -------------------------------------------------------------------------------- /src/assets/captcha/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballcat-projects/ballcat-ui-vue/147f5c457770014723ee156a4eabfec6af582d2f/src/assets/captcha/close.png -------------------------------------------------------------------------------- /src/assets/captcha/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballcat-projects/ballcat-ui-vue/147f5c457770014723ee156a4eabfec6af582d2f/src/assets/captcha/refresh.png -------------------------------------------------------------------------------- /src/assets/icons/colorPicker.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/compress.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/draggable.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/expend.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/i18n.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/Dict/dictMixin.js: -------------------------------------------------------------------------------- 1 | import { mapActions, mapGetters } from 'vuex' 2 | 3 | export default { 4 | name: 'DictMixin', 5 | props: { 6 | dictCode: { 7 | type: String, 8 | required: true 9 | }, 10 | itemFilter: { // 用于过滤出指定的字典项 11 | type: Function, 12 | default: null 13 | }, 14 | itemIsDisabled: { // 给字典项添加是否禁用的属性 15 | type: Function, 16 | default: (dictItem) => { 17 | // 根据字典项启用禁用设置是否禁用 18 | return dictItem.status !== 1 19 | } 20 | } 21 | 22 | }, 23 | computed: { 24 | ...mapGetters(['lang', 'dictDataCache']), 25 | dictData () { 26 | let dictData = this.dictDataCache[this.dictCode] 27 | if (!dictData) { 28 | this.fillDictCache([this.dictCode]).finally() 29 | } 30 | // 如果没有数据,返回空对象 31 | return dictData || {} 32 | }, 33 | dictItems () { 34 | // 如果没有数据,返回空数组 35 | let dictItems = [] 36 | // 处理数据 37 | if (this.dictData && this.dictData.dictItems) { 38 | let originDictItems = this.dictData.dictItems 39 | 40 | for (let item of originDictItems){ 41 | // 过滤字典项 42 | if (this.itemFilter && !this.itemFilter(item)) { 43 | continue 44 | } 45 | // 字典项是否 disable 46 | item.disabled = this.itemIsDisabled && this.itemIsDisabled(item) 47 | // 选择名称,国际化处理 48 | item.name = this.i18nName(item) 49 | 50 | // 添加字典项 51 | dictItems.push(item) 52 | } 53 | } 54 | return dictItems 55 | } 56 | }, 57 | created () { 58 | if (!this.dictDataCache[this.dictCode]) { 59 | this.fillDictCache([this.dictCode]).finally() 60 | } 61 | }, 62 | methods: { 63 | ...mapActions(['fillDictCache']), 64 | /** 65 | * 处理字典项数据 66 | * @param dictItem 67 | * @returns {*} 68 | */ 69 | i18nName (dictItem) { 70 | let name = dictItem.name 71 | // 配置了国际化信息时,进行国际化处理 72 | const languages = dictItem.attributes.languages 73 | if (languages && languages[this.lang]) { 74 | name = languages[this.lang] 75 | } 76 | return name 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/Dict/dictPlugin.js: -------------------------------------------------------------------------------- 1 | // 字典相关组件 2 | import DictTag from '@/components/Dict/display/DictTag' 3 | import DictText from '@/components/Dict/display/DictText' 4 | import DictBadge from '@/components/Dict/display/DictBadge' 5 | 6 | import DictRadioGroup from '@/components/Dict/group/DictRadioGroup' 7 | import DictSelect from '@/components/Dict/group/DictSelect' 8 | import DictCheckBoxGroup from '@/components/Dict/group/DictCheckBoxGroup' 9 | 10 | export default { 11 | install: function (Vue) { 12 | // 字典组件 13 | Vue.component('DictCheckBoxGroup', DictCheckBoxGroup) 14 | Vue.component('DictRadioGroup', DictRadioGroup) 15 | Vue.component('DictSelect', DictSelect) 16 | Vue.component('DictTag', DictTag) 17 | Vue.component('DictText', DictText) 18 | Vue.component('DictBadge', DictBadge) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Dict/display/DictBadge.vue: -------------------------------------------------------------------------------- 1 | 4 | 29 | 31 | -------------------------------------------------------------------------------- /src/components/Dict/display/DictTag.vue: -------------------------------------------------------------------------------- 1 | 4 | 21 | 26 | -------------------------------------------------------------------------------- /src/components/Dict/display/DictText.vue: -------------------------------------------------------------------------------- 1 | 4 | 21 | -------------------------------------------------------------------------------- /src/components/Dict/display/dictDisplayMixin.js: -------------------------------------------------------------------------------- 1 | import DictMixin from '@/components/Dict/dictMixin' 2 | 3 | export default { 4 | name: 'DictDisplayMixin', 5 | mixins: [DictMixin], 6 | props: { 7 | value: { 8 | type: [String, Number, Boolean], 9 | default: null 10 | }, 11 | colors: { 12 | type: Object, 13 | default: function () { 14 | return {} 15 | } 16 | }, 17 | uniformColor: { 18 | type: String, 19 | default: null 20 | } 21 | }, 22 | computed: { 23 | dictItem() { 24 | return this.dictItems.find(dictItem => dictItem.value === this.value) || {}; 25 | }, 26 | showText() { 27 | return (this.dictItem && this.dictItem.name) || this.value + ''; 28 | } 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Dict/group/DictCheckBoxGroup.vue: -------------------------------------------------------------------------------- 1 | 18 | 26 | -------------------------------------------------------------------------------- /src/components/Dict/group/DictRadioGroup.vue: -------------------------------------------------------------------------------- 1 | 29 | 43 | -------------------------------------------------------------------------------- /src/components/Dict/group/DictSelect.vue: -------------------------------------------------------------------------------- 1 | 22 | 48 | -------------------------------------------------------------------------------- /src/components/Dict/group/dictGroupMixin.js: -------------------------------------------------------------------------------- 1 | import DictMixin from '@/components/Dict/dictMixin' 2 | 3 | export default { 4 | name: 'DictGroupMixin', 5 | mixins: [DictMixin], 6 | props: { 7 | // eslint-disable-next-line vue/require-default-prop 8 | value: { 9 | type: [String, Number, Boolean, Array] 10 | }, 11 | disabled: { 12 | type: Boolean, 13 | default: false 14 | } 15 | }, 16 | data () { 17 | return { 18 | selectedValue: this.value 19 | } 20 | }, 21 | watch: { 22 | value () { 23 | this.selectedValue = this.value 24 | } 25 | }, 26 | methods: { 27 | handleChange (val) { 28 | if (val && val.target) { 29 | this.selectedValue = val.target.value 30 | } else { 31 | this.selectedValue = val 32 | } 33 | // v-decorator 方式的表单值联动 34 | this.$emit('change', this.selectedValue) 35 | // v-model 方式的表单值联动 36 | this.$emit('input', this.selectedValue) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Editor/WangEditor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 79 | -------------------------------------------------------------------------------- /src/components/FooterToolbar/FooterToolBar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 27 | 28 | 31 | -------------------------------------------------------------------------------- /src/components/FooterToolbar/index.js: -------------------------------------------------------------------------------- 1 | import FooterToolBar from './FooterToolBar' 2 | import './index.less' 3 | 4 | export default FooterToolBar 5 | -------------------------------------------------------------------------------- /src/components/FooterToolbar/index.less: -------------------------------------------------------------------------------- 1 | @import '~@/styles/index.less'; 2 | 3 | @footer-toolbar-prefix-cls: ~"@{ant-pro-prefix}-footer-toolbar"; 4 | 5 | .@{footer-toolbar-prefix-cls} { 6 | position: fixed; 7 | width: 100%; 8 | bottom: 0; 9 | right: 0; 10 | height: 56px; 11 | line-height: 56px; 12 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03); 13 | background: #fff; 14 | border-top: 1px solid #e8e8e8; 15 | padding: 0 24px; 16 | z-index: 9; 17 | 18 | &:after { 19 | content: ""; 20 | display: block; 21 | clear: both; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/FooterToolbar/index.md: -------------------------------------------------------------------------------- 1 | # FooterToolbar 底部工具栏 2 | 3 | 固定在底部的工具栏。 4 | 5 | 6 | 7 | ## 何时使用 8 | 9 | 固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。 10 | 11 | 12 | 13 | 引用方式: 14 | 15 | ```javascript 16 | import FooterToolBar from '@/components/FooterToolbar' 17 | 18 | export default { 19 | components: { 20 | FooterToolBar 21 | } 22 | } 23 | ``` 24 | 25 | 26 | 27 | ## 代码演示 28 | 29 | ```html 30 | 31 | 提交 32 | 33 | ``` 34 | 或 35 | ```html 36 | 37 | 提交 38 | 39 | ``` 40 | 41 | 42 | ## API 43 | 44 | 参数 | 说明 | 类型 | 默认值 45 | ----|------|-----|------ 46 | children (slot) | 工具栏内容,向右对齐 | - | - 47 | extra | 额外信息,向左对齐 | String, Object | - 48 | 49 | -------------------------------------------------------------------------------- /src/components/GlobalFooter/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 27 | 28 | 54 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/AvatarDropdown.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 65 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/LangSelect.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 59 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/LeftContent.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/NoticeIcon.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 90 | 91 | 96 | 106 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/RightContent.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 55 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/index.less: -------------------------------------------------------------------------------- 1 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025); 2 | 3 | .ballcat-global-header { 4 | 5 | z-index: 19; 6 | 7 | &-tool{ 8 | 9 | &-action { 10 | display: flex; 11 | align-items: center; 12 | height: 100%; 13 | padding: 0 12px; 14 | cursor: pointer; 15 | transition: all .3s; 16 | > span { 17 | vertical-align: middle; 18 | } 19 | &:hover { 20 | background: @pro-header-hover-bg; 21 | } 22 | &:global(.opened) { 23 | background: @pro-header-hover-bg; 24 | } 25 | } 26 | 27 | &-left { 28 | display: flex; 29 | height: 48px; 30 | overflow: hidden; 31 | } 32 | 33 | &-right { 34 | display: flex; 35 | float: right; 36 | height: 48px; 37 | margin-left: auto; 38 | overflow: hidden; 39 | } 40 | 41 | 42 | &-avatar-dropdown { 43 | white-space: nowrap; 44 | 45 | .avatar { 46 | margin: 20px 8px 20px 0; 47 | color: #1890ff; 48 | vertical-align: top; 49 | background: rgba(255, 255, 255, 0.85); 50 | user-select: none; 51 | } 52 | 53 | &-menu { 54 | .ant-dropdown-menu-item { 55 | width: 160px; 56 | } 57 | 58 | .ant-dropdown-menu-item > .anticon:first-child, 59 | .ant-dropdown-menu-item > a > .anticon:first-child, 60 | .ant-dropdown-menu-submenu-title > .anticon:first-child .ant-dropdown-menu-submenu-title > a > .anticon:first-child { 61 | min-width: 12px; 62 | margin-right: 12px; 63 | } 64 | } 65 | } 66 | 67 | } 68 | } 69 | 70 | // 目前多页签的固定也是根据这个 class 71 | .ant-header-fixedHeader { 72 | position: fixed; 73 | top: 0; 74 | right: 0; 75 | z-index: 19; 76 | width: 100%; 77 | transition: width 0.3s; 78 | 79 | &.ant-header-side-opened { 80 | width: calc(100% - 208px); 81 | } 82 | 83 | &.ant-header-side-closed { 84 | width: calc(100% - 48px); 85 | } 86 | } 87 | 88 | 89 | .ballcat-top-nav-header-logo { 90 | position: relative; 91 | min-width: 165px; 92 | height: 100%; 93 | overflow: hidden; 94 | padding: 0 12px; 95 | 96 | img { 97 | display: inline-block; 98 | height: 32px; 99 | width: 32px; 100 | vertical-align: middle; 101 | border-style: none; 102 | } 103 | h1 { 104 | display: inline-block; 105 | margin: 0 0 0 12px; 106 | color: #fff; 107 | font-weight: 600; 108 | font-size: 20px; 109 | vertical-align: top; 110 | overflow: hidden; 111 | flex-shrink: 0; 112 | justify-content: center; 113 | white-space: nowrap; 114 | } 115 | } 116 | 117 | // 暗色主题时,背景颜色需要转换 118 | &.dark { 119 | .ballcat-global-header-tool-action { 120 | color: rgba(255, 255, 255, 0.85); 121 | a { 122 | color: rgba(255, 255, 255, 0.85); 123 | } 124 | 125 | &:hover { 126 | background: rgba(255, 255, 255, 0.16); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/index.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 99 | 100 | 115 | -------------------------------------------------------------------------------- /src/components/IconSelector/IconSelector.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 67 | 68 | 92 | -------------------------------------------------------------------------------- /src/components/IconSelector/IconSelectorModal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 40 | 41 | 44 | -------------------------------------------------------------------------------- /src/components/IconSelector/index.js: -------------------------------------------------------------------------------- 1 | import IconSelector from './IconSelector' 2 | import IconSelectorModal from './IconSelectorModal' 3 | 4 | export { 5 | IconSelector, 6 | IconSelectorModal 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Lov/lovOptions.js: -------------------------------------------------------------------------------- 1 | // Lov 搜索条件控件的类型 2 | const SEARCH_TYPE = { 3 | 'input': 'input', 4 | 'number-input': 'number-input', 5 | 'select': 'select', 6 | 'dict-select': 'dict-select' 7 | } 8 | 9 | import { getPage as getUserPage } from '@/api/system/user' 10 | 11 | export const sysUserLov = { 12 | multiple: true, 13 | isNumberValue: true, 14 | modalTitle: '用户', 15 | dataKey: 'userId', 16 | // 自定义选择项的展示标题 17 | customOptionTitle (record) { 18 | return record.nickname 19 | }, 20 | getPageData: getUserPage, 21 | // 搜索配置 22 | searchOptions: [ 23 | { 24 | label: '用户名', 25 | field: 'username', 26 | type: SEARCH_TYPE.input, 27 | placeholder: 'message.pleaseEnter' 28 | }, 29 | { 30 | label: '昵称', 31 | field: 'name', 32 | type: SEARCH_TYPE.input, 33 | placeholder: 'message.pleaseEnter' 34 | } 35 | ], 36 | // 表格列 37 | tableColumns: 38 | [ 39 | { 40 | title: '用户名', 41 | dataIndex: 'username' 42 | }, 43 | { 44 | title: '昵称', 45 | dataIndex: 'nickname' 46 | }, 47 | { 48 | title: '组织', 49 | dataIndex: 'organizationName' 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /src/components/Lov/lovPlugin.js: -------------------------------------------------------------------------------- 1 | import LovLocal from '@/components/Lov/LovLocal' 2 | 3 | export default { 4 | install: function (Vue) { 5 | Vue.component('LovLocal', LovLocal) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Menu/SideMenu.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 93 | 94 | 97 | -------------------------------------------------------------------------------- /src/components/Menu/TopMenu.vue: -------------------------------------------------------------------------------- 1 | 16 | 48 | -------------------------------------------------------------------------------- /src/components/Menu/index.js: -------------------------------------------------------------------------------- 1 | import SMenu from './menu' 2 | export default SMenu 3 | -------------------------------------------------------------------------------- /src/components/MultiTab/index.js: -------------------------------------------------------------------------------- 1 | import MultiTab from './MultiTab' 2 | import './index.less' 3 | 4 | MultiTab.install = function (Vue) { 5 | if (Vue.prototype.$multiTab) { 6 | return 7 | } 8 | Vue.component('MultiTab', MultiTab) 9 | } 10 | 11 | export default MultiTab 12 | -------------------------------------------------------------------------------- /src/components/MultiTab/index.less: -------------------------------------------------------------------------------- 1 | @import '~@/styles/index.less'; 2 | 3 | @multi-tab-prefix-cls: ballcat-multi-tab; 4 | 5 | .@{multi-tab-prefix-cls} { 6 | height: 40px; 7 | background: #fff; 8 | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); 9 | z-index: 16; 10 | position: relative; 11 | } 12 | 13 | .@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-bar{ 14 | margin: 0; 15 | border-bottom: none; 16 | } 17 | 18 | .@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-bar .ant-tabs-nav-container .ant-tabs-tab { 19 | padding: 0; 20 | background: none; 21 | height: 40px; 22 | line-height: 40px; 23 | transition: background .3s cubic-bezier(.645,.045,.355,1),color .3s cubic-bezier(.645,.045,.355,1); 24 | border-radius: 0; 25 | border: none; 26 | margin: 0; 27 | } 28 | 29 | .@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-bar .ant-tabs-nav-container .ant-tabs-tab-next, .@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-bar .ant-tabs-nav-container .ant-tabs-tab-prev { 30 | transition: color .3s cubic-bezier(.645,.045,.355,1),opacity .3s cubic-bezier(.645,.045,.355,1); 31 | width: 40px; 32 | pointer-events: auto; 33 | line-height: 1; 34 | opacity: 1; 35 | 36 | .anticon { 37 | font-size: 14px; 38 | } 39 | } 40 | 41 | .@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-bar .ant-tabs-nav-container { 42 | padding: 0 40px; 43 | height: auto; 44 | } 45 | 46 | 47 | .@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-bar .ant-tabs-nav-container .ant-tabs-tab>div { 48 | padding: 0 28px 0 16px; 49 | } 50 | 51 | .@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-bar .ant-tabs-nav-container .ant-tabs-tab-active { 52 | background: rgba(24,144,255,.08); 53 | } 54 | 55 | .@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-bar .ant-tabs-nav-container .ant-tabs-tab>div.ant-tabs-tab-unclosable { 56 | padding-right: 16px; 57 | } 58 | 59 | .@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-bar .ant-tabs-nav-container .ant-tabs-tab .ant-tabs-close-x { 60 | width: auto; 61 | height: auto; 62 | margin: -6px 0 0 0; 63 | position: absolute; 64 | right: 10px; 65 | top: 50%; 66 | } 67 | 68 | .@{multi-tab-prefix-cls} .ant-tabs .multi-tab-tool { 69 | width: 40px; 70 | height: 40px; 71 | line-height: 40px; 72 | text-align: center; 73 | cursor: pointer; 74 | display: inline-block; 75 | border-left: 0.8px solid #efebeb; 76 | 77 | .anticon { 78 | font-size: 14px; 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /src/components/Notify/AnnouncementModal.vue: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /src/components/Notify/AnnouncementRibbon.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 83 | 84 | 125 | -------------------------------------------------------------------------------- /src/components/PageLoading/index.jsx: -------------------------------------------------------------------------------- 1 | import { Spin } from 'ant-design-vue' 2 | 3 | export const PageLoading = { 4 | name: 'PageLoading', 5 | props: { 6 | tip: { 7 | type: String, 8 | default: 'Loading..' 9 | }, 10 | size: { 11 | type: String, 12 | default: 'large' 13 | } 14 | }, 15 | render () { 16 | const style = { 17 | textAlign: 'center', 18 | background: 'rgba(0,0,0,0.6)', 19 | position: 'fixed', 20 | top: 0, 21 | bottom: 0, 22 | left: 0, 23 | right: 0, 24 | zIndex: 1100 25 | } 26 | const spinStyle = { 27 | position: 'absolute', 28 | left: '50%', 29 | top: '40%', 30 | transform: 'translate(-50%, -50%)' 31 | } 32 | return (
33 | 34 |
) 35 | } 36 | } 37 | 38 | const version = '0.0.1' 39 | const loading = {} 40 | 41 | loading.newInstance = (Vue, options) => { 42 | let loadingElement = document.querySelector('body>div[type=loading]') 43 | if (!loadingElement) { 44 | loadingElement = document.createElement('div') 45 | loadingElement.setAttribute('type', 'loading') 46 | loadingElement.setAttribute('class', 'ant-loading-wrapper') 47 | document.body.appendChild(loadingElement) 48 | } 49 | 50 | const cdProps = Object.assign({ visible: false, size: 'large', tip: 'Loading...' }, options) 51 | 52 | const instance = new Vue({ 53 | data () { 54 | return { 55 | ...cdProps 56 | } 57 | }, 58 | render () { 59 | const { tip } = this 60 | const props = {} 61 | this.tip && (props.tip = tip) 62 | if (this.visible) { 63 | return 64 | } 65 | return null 66 | } 67 | }).$mount(loadingElement) 68 | 69 | function update (config) { 70 | const { visible, size, tip } = { ...cdProps, ...config } 71 | instance.$set(instance, 'visible', visible) 72 | if (tip) { 73 | instance.$set(instance, 'tip', tip) 74 | } 75 | if (size) { 76 | instance.$set(instance, 'size', size) 77 | } 78 | } 79 | 80 | return { 81 | instance, 82 | update 83 | } 84 | } 85 | 86 | const api = { 87 | show: function (options) { 88 | this.instance.update({ ...options, visible: true }) 89 | }, 90 | hide: function () { 91 | this.instance.update({ visible: false }) 92 | } 93 | } 94 | 95 | const install = function (Vue, options) { 96 | if (Vue.prototype.$loading) { 97 | return 98 | } 99 | api.instance = loading.newInstance(Vue, options) 100 | Vue.prototype.$loading = api 101 | } 102 | 103 | export default { 104 | version, 105 | install 106 | } 107 | -------------------------------------------------------------------------------- /src/components/ProjectLogo/ProjectLogo.less: -------------------------------------------------------------------------------- 1 | .project-logo { 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | padding: 16px 16px 16px 12px; 6 | cursor: pointer; 7 | transition: padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 8 | justify-content: center; 9 | 10 | img, 11 | svg { 12 | height: 32px; 13 | width: 32px; 14 | display: inline-block; 15 | vertical-align: middle; 16 | } 17 | 18 | a { 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | min-height: 32px; 23 | } 24 | 25 | h1 { 26 | display: inline-block; 27 | height: 32px; 28 | margin: 0 0 0 12px; 29 | color: #fff; 30 | font-weight: 600; 31 | font-size: 20px; 32 | line-height: 32px; 33 | vertical-align: middle; 34 | white-space:nowrap; 35 | } 36 | } 37 | 38 | // TODO LOGO 样式参考 ant-design-pro 39 | &.sider-light { 40 | .project-logo { 41 | background: #fff; 42 | h1 { 43 | color: unset; 44 | } 45 | } 46 | } 47 | 48 | &.light { 49 | .project-logo { 50 | background: #fff; 51 | h1 { 52 | color: unset; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/ProjectLogo/ProjectLogo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /src/components/ProjectLogo/index.js: -------------------------------------------------------------------------------- 1 | import ProjectLogo from './ProjectLogo' 2 | export default ProjectLogo 3 | -------------------------------------------------------------------------------- /src/components/Result/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 59 | 60 | 110 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 47 | 48 | 58 | -------------------------------------------------------------------------------- /src/components/SettingDrawer/index.js: -------------------------------------------------------------------------------- 1 | import SettingDrawer from './SettingDrawer' 2 | export default SettingDrawer 3 | -------------------------------------------------------------------------------- /src/components/SettingDrawer/settingConfig.js: -------------------------------------------------------------------------------- 1 | import { message } from 'ant-design-vue/es' 2 | import themeColor from './themeColor.js' 3 | 4 | // let lessNodesAppended 5 | 6 | const colorList = [ 7 | { 8 | key: '拂晓蓝(默认)', color: '#1890FF' 9 | }, 10 | { 11 | key: '薄暮', color: '#F5222D' 12 | }, 13 | { 14 | key: '火山', color: '#FA541C' 15 | }, 16 | { 17 | key: '日暮', color: '#FAAD14' 18 | }, 19 | { 20 | key: '明青', color: '#13C2C2' 21 | }, 22 | { 23 | key: '极光绿', color: '#52C41A' 24 | }, 25 | { 26 | key: '极客蓝', color: '#2F54EB' 27 | }, 28 | { 29 | key: '酱紫', color: '#722ED1' 30 | } 31 | ] 32 | 33 | const updateTheme = newPrimaryColor => { 34 | const hideMessage = message.loading('正在切换主题!', 0) 35 | themeColor.changeColor(newPrimaryColor).finally(() => { 36 | setTimeout(() => { 37 | hideMessage() 38 | }, 10) 39 | }) 40 | } 41 | 42 | const updateColorWeak = colorWeak => { 43 | // document.body.className = colorWeak ? 'colorWeak' : ''; 44 | const app = document.body.querySelector('#app') 45 | colorWeak ? app.classList.add('colorWeak') : app.classList.remove('colorWeak') 46 | } 47 | 48 | export { updateTheme, colorList, updateColorWeak } 49 | -------------------------------------------------------------------------------- /src/components/SettingDrawer/themeColor.js: -------------------------------------------------------------------------------- 1 | import client from 'webpack-theme-color-replacer/client' 2 | import {generate} from '@ant-design/colors' 3 | 4 | export default { 5 | getAntdSerials (color) { 6 | // 淡化(即less的tint) 7 | const lightens = new Array(9).fill().map((t, i) => { 8 | return client.varyColor.lighten(color, i / 10) 9 | }) 10 | // colorPalette变换得到颜色值 11 | const colorPalettes = generate(color) 12 | return lightens.concat(colorPalettes) 13 | }, 14 | changeColor (newColor) { 15 | var options = { 16 | newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors` 17 | changeUrl (cssUrl) { 18 | return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path 19 | } 20 | } 21 | return client.changer.changeColor(options, Promise) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/SideBar/Sider.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 65 | 66 | 69 | -------------------------------------------------------------------------------- /src/components/SideBar/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 76 | -------------------------------------------------------------------------------- /src/components/Table/EditableCell.vue: -------------------------------------------------------------------------------- 1 | z 18 | 19 | 60 | 61 | 99 | -------------------------------------------------------------------------------- /src/components/Table/alert.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/es/style/themes/default.less'; 2 | 3 | @pro-table-alert-prefix-cls: ~'ballcat-pro-table-alert'; 4 | 5 | .@{pro-table-alert-prefix-cls} { 6 | margin-bottom: 16px; 7 | // padding: 0 24px; 8 | 9 | .@{ant-prefix}-alert.@{ant-prefix}-alert-no-icon { 10 | padding: @padding-sm @padding-lg; 11 | } 12 | 13 | &-info { 14 | display: flex; 15 | align-items: center; 16 | transition: all 0.3s; 17 | &-content { 18 | flex: 1; 19 | } 20 | &-option { 21 | min-width: 48px; 22 | padding-left: 16px; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Table/columnSetting.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/es/style/themes/default.less'; 2 | 3 | @pro-column-setting-prefix-cls: ~'ballcat-pro-table-column-setting'; 4 | 5 | .@{pro-column-setting-prefix-cls} { 6 | width: auto; 7 | &-title { 8 | display: flex; 9 | align-items: center; 10 | justify-content: space-between; 11 | height: 32px; 12 | } 13 | 14 | &-overlay { 15 | .@{ant-prefix}-popover-inner-content { 16 | padding: 0px; 17 | padding-bottom: 8px; 18 | } 19 | 20 | .@{ant-prefix}-tree-node-content-wrapper:hover { 21 | background-color: transparent; 22 | } 23 | 24 | .@{ant-prefix}-tree-treenode { 25 | align-items: center; 26 | 27 | &:hover { 28 | background-color: @item-active-bg; 29 | .@{pro-column-setting-prefix-cls}-list-item-option { 30 | display: block; 31 | } 32 | } 33 | 34 | .@{ant-prefix}-tree-checkbox { 35 | top: 0; 36 | margin: 0; 37 | margin-right: 4px; 38 | } 39 | } 40 | } 41 | } 42 | 43 | .@{pro-column-setting-prefix-cls}-list { 44 | display: flex; 45 | flex-direction: column; 46 | width: 100%; 47 | padding-top: 8px; 48 | 49 | &.@{pro-column-setting-prefix-cls}-list-group { 50 | padding-top: 0; 51 | } 52 | 53 | &-title { 54 | margin-top: 6px; 55 | margin-bottom: 6px; 56 | padding-left: 24px; 57 | color: @text-color-secondary; 58 | font-size: 12px; 59 | } 60 | 61 | &-item { 62 | display: flex; 63 | align-items: center; 64 | 65 | &-title { 66 | flex: 1; 67 | } 68 | 69 | &-option { 70 | display: none; 71 | float: right; 72 | cursor: pointer; 73 | > span { 74 | > span.anticon { 75 | color: @primary-color; 76 | } 77 | } 78 | > span + span { 79 | margin-left: 8px; 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/components/Table/listToolBar.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/es/style/themes/default.less'; 2 | 3 | @pro-list-toolbar: ~'ballcat-pro-table-list-toolbar'; 4 | @margin-md: '16px'; 5 | @margin-lg: '24px'; // containers 6 | 7 | .@{pro-list-toolbar} { 8 | line-height: 1; 9 | 10 | &-container { 11 | display: flex; 12 | justify-content: space-between; 13 | padding: @padding-md 0; 14 | &-mobile { 15 | flex-direction: column; 16 | } 17 | } 18 | 19 | &-title { 20 | display: flex; 21 | align-items: center; 22 | justify-content: flex-start; 23 | color: @heading-color; 24 | font-weight: 500; 25 | font-size: @font-size-lg; 26 | } 27 | 28 | &-search:not(:last-child) { 29 | display: flex; 30 | align-items: center; 31 | justify-content: flex-start; 32 | } 33 | 34 | &-setting-item { 35 | margin: 0 4px; 36 | color: @icon-color-hover; 37 | font-size: @font-size-lg; 38 | cursor: pointer; 39 | 40 | > span { 41 | display: block; 42 | width: 100%; 43 | height: 100%; 44 | } 45 | 46 | &:hover { 47 | color: @primary-color; 48 | } 49 | } 50 | 51 | &-left { 52 | display: flex; 53 | align-items: center; 54 | justify-content: flex-start; 55 | } 56 | 57 | &-right { 58 | display: flex; 59 | justify-content: flex-end; 60 | } 61 | 62 | &-extra-line { 63 | margin-bottom: @margin-md; 64 | } 65 | 66 | &-filter { 67 | &:not(:last-child) { 68 | margin-right: @margin-md; 69 | } 70 | 71 | display: flex; 72 | align-items: center; 73 | 74 | .@{ant-prefix}-pro-table-search { 75 | margin: 0; 76 | padding: 0; 77 | } 78 | } 79 | 80 | &-inline-menu-item { 81 | display: inline-block; 82 | margin-right: @margin-lg; 83 | cursor: pointer; 84 | opacity: 0.75; 85 | 86 | &-active { 87 | font-weight: bold; 88 | opacity: 1; 89 | } 90 | } 91 | &-dropdownmenu-label { 92 | font-weight: bold; 93 | font-size: @font-size-lg; 94 | text-align: center; 95 | cursor: pointer; 96 | } 97 | 98 | .@{ant-prefix}-tabs-top > .@{ant-prefix}-tabs-nav { 99 | &::before { 100 | border-bottom: 0; 101 | } 102 | margin-bottom: 0; 103 | .@{ant-prefix}-tabs-nav-list { 104 | margin-top: 0; 105 | .@{ant-prefix}-tabs-tab { 106 | padding-top: 0; 107 | } 108 | } 109 | } 110 | } 111 | 112 | @media (max-width: 575px) { 113 | .@{pro-list-toolbar} { 114 | &-container { 115 | display: flex; 116 | flex-wrap: wrap; 117 | } 118 | &-left { 119 | margin-bottom: 16px; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/components/Table/proTable.less: -------------------------------------------------------------------------------- 1 | .table-fullscreen { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | overflow: auto; 8 | padding: 0 24px; 9 | box-sizing: border-box; 10 | z-index: 9999; 11 | background: white; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Table/searchForm.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/es/style/themes/default.less'; 2 | @import '~ant-design-vue/es/style/mixins/index.less'; 3 | 4 | @pro-table-search-prefix-cls: ~'ballcat-pro-table-search'; 5 | @pro-table-form-prefix-cls: ~'ballcat-pro-table-form'; 6 | 7 | .@{pro-table-search-prefix-cls} { 8 | margin-bottom: 16px; 9 | padding: 24px; 10 | padding-bottom: 0; 11 | background: @component-background; 12 | 13 | .clearfix; 14 | 15 | &.@{pro-table-search-prefix-cls}-unwrap{ 16 | margin-bottom: 0; 17 | padding: 0; 18 | } 19 | 20 | &.@{pro-table-form-prefix-cls} { 21 | margin: 0; 22 | padding: 0 16px; 23 | overflow: unset; 24 | } 25 | 26 | &-light { 27 | margin-bottom: 0; 28 | padding: 16px 0; 29 | } 30 | 31 | &-form-option { 32 | .@{ant-prefix}-form-item { 33 | margin: 0; 34 | } 35 | .@{ant-prefix}-form-item-label { 36 | opacity: 0; 37 | } 38 | .@{ant-prefix}-form-item-control-input { 39 | justify-content: flex-start; 40 | } 41 | } 42 | 43 | // 查询按钮的包装 44 | .search-actions-wrapper { 45 | text-align: right; 46 | } 47 | } 48 | 49 | @media (max-width: 575px) { 50 | .@{pro-table-search-prefix-cls} { 51 | height: auto !important; 52 | padding-bottom: 24px; 53 | .@{ant-prefix}-form-item-label { 54 | min-width: 80px; 55 | text-align: left; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Table/toolbar.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/es/style/themes/default.less'; 2 | @pro-table-prefix-cls: ~'ballcat-pro-table'; 3 | 4 | .@{pro-table-prefix-cls}-toolbar { 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | height: 64px; 9 | padding: 0 24px; 10 | 11 | &-option { 12 | display: flex; 13 | align-items: center; 14 | justify-content: flex-end; 15 | } 16 | 17 | &-title { 18 | flex: 1; 19 | color: @label-color; 20 | font-weight: 500; 21 | font-size: 16px; 22 | line-height: 24px; 23 | // opacity: 0.85; 24 | } 25 | } 26 | 27 | @media (max-width: @screen-xs) { 28 | .@{pro-table-prefix-cls} { 29 | .ant-table { 30 | width: 100%; 31 | overflow-x: auto; 32 | &-thead > tr, 33 | &-tbody > tr { 34 | > th, 35 | > td { 36 | white-space: pre; 37 | > span { 38 | display: block; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | @media (max-width: 575px) { 47 | .@{pro-table-prefix-cls}-toolbar { 48 | flex-direction: column; 49 | align-items: flex-start; 50 | justify-content: flex-start; 51 | height: auto; 52 | margin-bottom: 16px; 53 | margin-left: 16px; 54 | padding: 8px; 55 | padding-top: 16px; 56 | line-height: normal; 57 | 58 | &-title { 59 | margin-bottom: 16px; 60 | } 61 | 62 | &-option { 63 | display: flex; 64 | justify-content: space-between; 65 | width: 100%; 66 | } 67 | 68 | &-default-option { 69 | display: flex; 70 | flex: 1; 71 | align-items: center; 72 | justify-content: flex-end; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/WebSocket/GlobalWebSocketListener.vue: -------------------------------------------------------------------------------- 1 | 48 | -------------------------------------------------------------------------------- /src/config/defaultSettings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 项目默认配置项 3 | * primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage 4 | * navTheme - sidebar theme ['dark', 'light'] 两种主题 5 | * colorWeak - 色盲模式 6 | * layout - 整体布局方式 ['side', 'top'] 两种布局 7 | * fixedHeader - 固定 Header : boolean 8 | * fixSiderbar - 固定左侧菜单栏 : boolean 9 | * contentWidth - 内容区布局: 流式 | 固定 10 | * 11 | * storageOptions: {} - Vue-ls 插件配置项 (localStorage/sessionStorage) 12 | * 13 | */ 14 | export const appDefaultSetting = { 15 | primaryColor: '#1890FF', // primary color of ant design 16 | navTheme: 'dark', // theme for nav menu 17 | layout: 'side', // nav menu position: side or top or mix 18 | contentWidth: 'Fixed', // layout of content: Fluid or Fixed, Does not work in side modes 19 | fixedHeader: false, // sticky header 20 | fixSiderbar: true, // sticky siderbar 21 | colorWeak: false, 22 | multiTab: true 23 | } 24 | -------------------------------------------------------------------------------- /src/config/projectConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 项目标题 3 | projectTitle: 'Ball Cat', 4 | // 项目描述 5 | projectDesc: 'Ball Cat 一个简单的项目启动脚手架', 6 | // Vue ls 配置 7 | storageOptions: { 8 | namespace: 'ballcat/', // key prefix 9 | name: 'ls', // name variable Vue.[ls] or this.[$ls], 10 | storage: 'local' // storage name session, local, memory 11 | }, 12 | 13 | // 开启 websocket,开启此选项需要服务端同步支持 websocket 功能 14 | // 若服务端不支持,则本地启动时,抛出 socket 异常,导致 proxyServer 关闭 15 | enableWebsocket: true, 16 | 17 | // 开启布局设置 18 | enableLayoutSetting: true, 19 | 20 | // 开启登录验证码 21 | enableLoginCaptcha: true, 22 | 23 | // ------------- 国际化配置分隔符 ----------------- 24 | 25 | // 是否开启国际化 26 | enableI18n: true, 27 | // 项目默认语言 28 | defaultLanguage: 'zh-CN', 29 | // 支持的语言列表 30 | supportLanguage: { 31 | 'zh-CN': { 32 | lang: 'zh-CN', 33 | title: '简体中文', 34 | symbol: '🇨🇳' 35 | }, 36 | 'en-US': { 37 | lang: 'en-US', 38 | title: 'English', 39 | symbol: '🇺🇸' 40 | } 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /src/core/bootstrap.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '@/store/' 3 | import { 4 | SETTINGS_LS, 5 | ACCESS_TOKEN, 6 | USER_INFO, 7 | ROLES, 8 | PERMISSIONS, 9 | APP_LANGUAGE 10 | } from '@/store/storage-types' 11 | 12 | import { appDefaultSetting } from '@/config/defaultSettings' 13 | import { APP_MUTATIONS } from '@/store/mutation-types' 14 | import { switchLanguage } from '@/locales' 15 | import { enableI18n, defaultLanguage } from '@/config/projectConfig' 16 | 17 | export default function Initializer () { 18 | console.log(`API_URL: ${process.env.VUE_APP_API_BASE_URL}`) 19 | 20 | // Settings 21 | store.commit(APP_MUTATIONS.TOGGLE_NAV_THEME, Vue.ls.get(SETTINGS_LS.NAV_THEME, appDefaultSetting.navTheme)) 22 | store.commit(APP_MUTATIONS.TOGGLE_PRIMARY_COLOR, Vue.ls.get(SETTINGS_LS.PRIMARY_COLOR, appDefaultSetting.primaryColor)) 23 | store.commit(APP_MUTATIONS.TOGGLE_LAYOUT, Vue.ls.get(SETTINGS_LS.LAYOUT, appDefaultSetting.layout)) 24 | store.commit(APP_MUTATIONS.TOGGLE_CONTENT_WIDTH, Vue.ls.get(SETTINGS_LS.CONTENT_WIDTH_TYPE, appDefaultSetting.contentWidth)) 25 | store.commit(APP_MUTATIONS.TOGGLE_FIXED_HEADER, Vue.ls.get(SETTINGS_LS.FIXED_HEADER, appDefaultSetting.fixedHeader)) 26 | store.commit(APP_MUTATIONS.TOGGLE_FIXED_SIDERBAR, Vue.ls.get(SETTINGS_LS.FIXED_SIDE_MENU, appDefaultSetting.fixSiderbar)) 27 | store.commit(APP_MUTATIONS.TOGGLE_COLOR_WEAK, Vue.ls.get(SETTINGS_LS.COLOR_WEAK, appDefaultSetting.colorWeak)) 28 | 29 | store.commit(APP_MUTATIONS.TOGGLE_MULTI_TAB, Vue.ls.get(SETTINGS_LS.MULTI_TAB, appDefaultSetting.multiTab)) 30 | 31 | // 用户权限 32 | store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN)) 33 | store.commit('SET_INFO', Vue.ls.get(USER_INFO)) 34 | store.commit('SET_ROLES', Vue.ls.get(ROLES)) 35 | store.commit('SET_PERMISSIONS', Vue.ls.get(PERMISSIONS)) 36 | 37 | // 国际化处理 38 | enableI18n && switchLanguage(Vue.ls.get(APP_LANGUAGE, defaultLanguage)) 39 | } 40 | -------------------------------------------------------------------------------- /src/core/icons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom icon list 3 | * All icons are loaded here for easy management 4 | * @see https://vue.ant.design/components/icon/#Custom-Font-Icon 5 | * 6 | * 自定义图标加载表 7 | * 所有图标均从这里加载,方便管理 8 | */ 9 | import i18nIcon from '@/assets/icons/i18n.svg?inline' // path to your '*.svg?inline' file. 10 | import colorPicker from '@/assets/icons/colorPicker.svg?inline' 11 | import compress from '@/assets/icons/compress.svg?inline' 12 | import expend from '@/assets/icons/expend.svg?inline' 13 | import draggableIcon from '@/assets/icons/draggable.svg?inline' 14 | 15 | 16 | export { i18nIcon, colorPicker, compress, expend, draggableIcon } 17 | -------------------------------------------------------------------------------- /src/core/lazy_lib/components_use.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable */ 3 | /** 4 | * 该文件是为了按需加载,剔除掉了一些不需要的框架组件。 5 | * 减少了编译支持库包大小 6 | * 7 | * 当需要更多组件依赖时,在该文件加入即可 8 | */ 9 | import Vue from 'vue' 10 | import { 11 | ConfigProvider, 12 | Layout, 13 | Input, 14 | InputNumber, 15 | Button, 16 | Switch, 17 | Radio, 18 | Checkbox, 19 | Select, 20 | Card, 21 | Form, 22 | Row, 23 | Col, 24 | Modal, 25 | Table, 26 | Tabs, 27 | Icon, 28 | Badge, 29 | Popover, 30 | Dropdown, 31 | List, 32 | Avatar, 33 | Breadcrumb, 34 | Steps, 35 | Spin, 36 | Menu, 37 | Drawer, 38 | Tooltip, 39 | Alert, 40 | Tag, 41 | Divider, 42 | DatePicker, 43 | TimePicker, 44 | Upload, 45 | Progress, 46 | Skeleton, 47 | Popconfirm, 48 | message, 49 | notification 50 | } from 'ant-design-vue' 51 | // import VueCropper from 'vue-cropper' 52 | 53 | Vue.use(ConfigProvider) 54 | Vue.use(Layout) 55 | Vue.use(Input) 56 | Vue.use(InputNumber) 57 | Vue.use(Button) 58 | Vue.use(Switch) 59 | Vue.use(Radio) 60 | Vue.use(Checkbox) 61 | Vue.use(Select) 62 | Vue.use(Card) 63 | Vue.use(Form) 64 | Vue.use(Row) 65 | Vue.use(Col) 66 | Vue.use(Modal) 67 | Vue.use(Table) 68 | Vue.use(Tabs) 69 | Vue.use(Icon) 70 | Vue.use(Badge) 71 | Vue.use(Popover) 72 | Vue.use(Dropdown) 73 | Vue.use(List) 74 | Vue.use(Avatar) 75 | Vue.use(Breadcrumb) 76 | Vue.use(Steps) 77 | Vue.use(Spin) 78 | Vue.use(Menu) 79 | Vue.use(Drawer) 80 | Vue.use(Tooltip) 81 | Vue.use(Alert) 82 | Vue.use(Tag) 83 | Vue.use(Divider) 84 | Vue.use(DatePicker) 85 | Vue.use(TimePicker) 86 | Vue.use(Upload) 87 | Vue.use(Progress) 88 | Vue.use(Skeleton) 89 | Vue.use(Popconfirm) 90 | // Vue.use(VueCropper) 91 | Vue.use(notification) 92 | 93 | Vue.prototype.$confirm = Modal.confirm 94 | Vue.prototype.$message = message 95 | Vue.prototype.$notification = notification 96 | Vue.prototype.$info = Modal.info 97 | Vue.prototype.$success = Modal.success 98 | Vue.prototype.$error = Modal.error 99 | Vue.prototype.$warning = Modal.warning -------------------------------------------------------------------------------- /src/core/lazy_use.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueStorage from 'vue-ls' 3 | import { storageOptions } from '@/config/projectConfig' 4 | 5 | // base library 6 | import '@/core/lazy_lib/components_use' 7 | import Viser from 'viser-vue' 8 | 9 | // ext library 10 | import VueClipboard from 'vue-clipboard2' 11 | import './directives/action' 12 | 13 | VueClipboard.config.autoSetContainer = true 14 | 15 | Vue.use(Viser) 16 | 17 | Vue.use(VueStorage, storageOptions) 18 | Vue.use(VueClipboard) 19 | -------------------------------------------------------------------------------- /src/core/use.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueStorage from 'vue-ls' 3 | import projectConfig from '@/config/projectConfig' 4 | 5 | // base library 6 | import Antd from 'ant-design-vue' 7 | import VueCropper from 'vue-cropper' 8 | import 'ant-design-vue/dist/antd.less' 9 | 10 | // ext library 11 | import VueClipboard from 'vue-clipboard2' 12 | import MultiTab from '@/components/MultiTab' 13 | import PageLoading from '@/components/PageLoading' 14 | 15 | // 权限控制 16 | import {has, role, plugin} from '@/utils/authorize' 17 | Vue.use(has) 18 | Vue.use(role) 19 | Vue.use(plugin) 20 | 21 | 22 | VueClipboard.config.autoSetContainer = true 23 | 24 | Vue.use(Antd) 25 | Vue.use(MultiTab) 26 | Vue.use(PageLoading) 27 | Vue.use(VueStorage, projectConfig.storageOptions) 28 | Vue.use(VueClipboard) 29 | Vue.use(VueCropper) 30 | -------------------------------------------------------------------------------- /src/layouts/BlankRouterView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/layouts/ContentView.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 57 | 63 | -------------------------------------------------------------------------------- /src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import UserLayout from '@/layouts/UserLayout' 2 | import BasicLayout from '@/layouts/BasicLayout' 3 | import ContentView from '@/layouts/ContentView' 4 | 5 | export { UserLayout, BasicLayout, ContentView } 6 | -------------------------------------------------------------------------------- /src/locales/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Vue i18n loader 3 | * created by @musnow 4 | * https://github.com/musnow 5 | */ 6 | import Vue from 'vue' 7 | import VueI18n from 'vue-i18n' 8 | import store from '@/store' 9 | import { APP_LANGUAGE } from '@/store/storage-types' 10 | import { enableI18n } from '@/config/projectConfig' 11 | // import { defaultLanguage } from '@/config/projectConfig' 12 | 13 | // 已经加载的语言列表 14 | const loadedLanguages = [] 15 | 16 | // 当找不到对应语言的配置时,是否需要回退 17 | const fallbackLocale = false 18 | 19 | let vueI18n = {} 20 | if (enableI18n) { 21 | // 加载 vueI18n 22 | Vue.use(VueI18n) 23 | vueI18n = new VueI18n({ 24 | locale: 'unKnow', // 设置语言环境,这里故意给定 unKnow,方便切换 25 | fallbackLocale: fallbackLocale, 26 | messages: {} // 设置语言环境信息 27 | }) 28 | } 29 | 30 | // 这里没有加载语言,语言加载交由 bootstrap.js 中处理,这样避免默认语言和设置语言不一样时,依然要先加载默认语言的问题 31 | export const i18n = vueI18n 32 | 33 | // 当需要回退语言时,则需要预先加载默认语言的配置 34 | if (fallbackLocale !== false) { 35 | loadLanguageProperties(fallbackLocale) 36 | } 37 | 38 | /** 39 | * 切换语言 40 | * @param lang 41 | * @returns {*} 42 | */ 43 | export function switchLanguage (lang) { 44 | // 同步切换 vuex,ls, html 标识的语言,防止异常 45 | store.commit('SET_LANG', lang) 46 | Vue.ls.set(APP_LANGUAGE, lang) 47 | document.querySelector('html').setAttribute('lang', lang) 48 | // 异步切换 i18n 的语言,方便做到懒加载 49 | setI18nLanguageAsync(lang) 50 | return lang 51 | } 52 | 53 | /** 54 | * 切换 vue-i18n.locale,如果语言文件未加载,则异步加载后切换 55 | * @param lang 56 | */ 57 | function setI18nLanguageAsync (lang) { 58 | // 如果语言相同 59 | if (i18n.locale === lang) { 60 | return 61 | } 62 | 63 | // 如果语言已经加载 64 | if (loadedLanguages.includes(lang)) { 65 | i18n.locale = lang 66 | } 67 | 68 | // 如果尚未加载语言 69 | loadLanguageProperties(lang) 70 | } 71 | 72 | /** 73 | * 加载语言配置文件 74 | * @param lang 75 | */ 76 | function loadLanguageProperties (lang) { 77 | import(/* webpackChunkName: "lang-[request]" */ `./lang/${lang}.js`).then( 78 | messages => { 79 | i18n.setLocaleMessage(lang, messages.default) 80 | loadedLanguages.push(lang) 81 | i18n.locale = lang 82 | } 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /src/locales/lang/en-US.js: -------------------------------------------------------------------------------- 1 | import antLocale from 'ant-design-vue/es/locale-provider/en_US' 2 | import momentLocale from 'moment/locale/eu' 3 | 4 | const components = { 5 | antLocale: antLocale, 6 | momentName: 'eu', 7 | momentLocale: momentLocale 8 | } 9 | 10 | export default { 11 | ...components, 12 | 13 | pagination: { 14 | pageInfo: '{rangeBegin}-{rangeEnd} of {total} items', 15 | }, 16 | 17 | lov: { 18 | selectedData: 'The selected data' 19 | }, 20 | 21 | action: { 22 | query: 'Query', 23 | reset: 'Reset', 24 | expand: 'Expand', 25 | collapse: 'Collapse', 26 | more: 'More', 27 | create: 'New', 28 | delete: 'Delete', 29 | edit: 'Edit', 30 | export: 'Export', 31 | import: 'Import', 32 | details: 'Details', 33 | selectFile: 'Select File', 34 | choose: 'Choose', 35 | cancel: 'Cancel' 36 | }, 37 | 38 | common: { 39 | operation: 'Operation', 40 | createTime: 'Create Time', 41 | updateTime: 'Update Time', 42 | remarks: 'Remarks' 43 | }, 44 | 45 | message: { 46 | confirmDelete: 'Are you sure delete?', 47 | pleaseEnter: 'Please enter', 48 | pleaseSelectFile: 'Please Select File' 49 | }, 50 | 51 | import: { 52 | batchImport: 'Batch Import', 53 | downloadTemplate: 'Download Template', 54 | whenDataExisting: 'When the data already exists', 55 | skipExisting: 'Skip Existing', 56 | overwriteExisting: 'Overwrite Existing', 57 | importSuccess: 'Import Success' 58 | }, 59 | 60 | i18n: { 61 | i18nData: { 62 | text: 'I18N Data', 63 | languageTag: { 64 | text: 'Language Tag', 65 | tips: 'LanguageTag,eg. zh-CN en-Us', 66 | required: 'Please enter i18n data language-tag!' 67 | }, 68 | code: { 69 | text: 'Code', 70 | tips: 'A unique code for i18n data', 71 | required: 'Please enter i18n data code!' 72 | }, 73 | message: { 74 | text: 'Message', 75 | tips: 'The text of the i18n data', 76 | required: 'Please enter i18n data message!!' 77 | } 78 | } 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /src/locales/lang/zh-CN.js: -------------------------------------------------------------------------------- 1 | import antLocale from 'ant-design-vue/es/locale-provider/zh_CN' 2 | import momentLocale from 'moment/locale/zh-cn' 3 | 4 | const components = { 5 | antLocale: antLocale, 6 | momentName: 'zh-cn', 7 | momentLocale: momentLocale 8 | } 9 | 10 | export default { 11 | ...components, 12 | 13 | pagination: { 14 | pageInfo: '{rangeBegin}-{rangeEnd} 共 {total} 条', 15 | }, 16 | 17 | lov: { 18 | selectedData: '已选数据' 19 | }, 20 | 21 | action: { 22 | query: '查询', 23 | reset: '重置', 24 | expand: '展开', 25 | collapse: '收起', 26 | more: '更多', 27 | create: '新建', 28 | delete: '删除', 29 | edit: '编辑', 30 | export: '导出', 31 | import: '导入', 32 | details: '详情', 33 | selectFile: '选择文件', 34 | choose: '选择', 35 | cancel: '取消' 36 | }, 37 | 38 | common: { 39 | operation: '操作', 40 | createTime: '创建时间', 41 | updateTime: '更新时间', 42 | remarks: '备注' 43 | }, 44 | 45 | message: { 46 | confirmDelete: '确认要删除吗?', 47 | pleaseEnter: '请输入', 48 | pleaseSelectFile: '请选择一个文件' 49 | }, 50 | 51 | import: { 52 | batchImport: '批量导入', 53 | downloadTemplate: '下载模板文件', 54 | whenDataExisting: '当数据已存在时', 55 | skipExisting: '跳过已有', 56 | overwriteExisting: '覆盖已有', 57 | importSuccess: '导入成功' 58 | }, 59 | 60 | i18n: { 61 | i18nData: { 62 | text: '国际化信息', 63 | languageTag: { 64 | text: '语言标签', 65 | tips: '语言标签,eg. zh-CN en-Us', 66 | required: '请输入语言标签!' 67 | }, 68 | code: { 69 | text: '国际化标识', 70 | tips: '国际化信息的标识', 71 | required: '请输入国际化标识!' 72 | }, 73 | message: { 74 | text: '文本值', 75 | tips: '国际化信息的文本', 76 | required: '请输入文本值!' 77 | } 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store/' 5 | // mock 6 | // import './mock' 7 | import { enableI18n, defaultLanguage } from '@/config/projectConfig' 8 | 9 | import bootstrap from './core/bootstrap' 10 | import './core/use' 11 | import './permission' // permission control 12 | import './utils/filter' // global filter 13 | 14 | // 文件相对路径转绝对路径 15 | Vue.prototype.fileAbsoluteUrl = function(relativeUrl) { 16 | if (relativeUrl) { 17 | return 'https://hccake-img.oss-cn-shanghai.aliyuncs.com/' + relativeUrl 18 | } 19 | } 20 | 21 | // 字典注册 22 | import DictPlugin from '@/components/Dict/dictPlugin' 23 | 24 | Vue.use(DictPlugin) 25 | // lov注册 26 | import LovPlugin from '@/components/Lov/lovPlugin' 27 | 28 | Vue.use(LovPlugin) 29 | 30 | Vue.config.productionTip = false 31 | 32 | let vm = { 33 | beforeCreate() { 34 | // 全局事件总线 35 | Vue.prototype.$bus = this 36 | }, 37 | router, 38 | store, 39 | created: bootstrap, 40 | render: h => h(App) 41 | } 42 | 43 | // 按需加载国际化 44 | if (enableI18n) { 45 | import(/* webpackChunkName: "lang-[request]" */ '@/locales').then(res => { 46 | vm.i18n = res.i18n 47 | new Vue(vm).$mount('#app') 48 | }) 49 | } else { 50 | const antLocaleName = defaultLanguage.replace('-', '_') 51 | import(`ant-design-vue/es/locale-provider/${antLocaleName}`).then(res => { 52 | // 挂载到原型上,方便获取 53 | Vue.prototype.$defaultAntLocale = res.default 54 | new Vue(vm).$mount('#app') 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /src/mixins/index.js: -------------------------------------------------------------------------------- 1 | import TablePageMixin from './tablePageMixin' 2 | import FormMixin from './formMixin' 3 | import PageFormMixin from './pageFormMixin' 4 | import PopUpFormMixin from './popUpFormMixin' 5 | 6 | export { 7 | TablePageMixin, 8 | FormMixin, 9 | PageFormMixin, 10 | PopUpFormMixin 11 | } 12 | -------------------------------------------------------------------------------- /src/mixins/pageFormMixin.js: -------------------------------------------------------------------------------- 1 | import FormMixin from './formMixin' 2 | 3 | export default { 4 | mixins: [FormMixin], 5 | data () { 6 | return { 7 | // 标题 8 | title: '', 9 | } 10 | }, 11 | methods: { 12 | add (attributes) { 13 | this.title = attributes.title 14 | this.buildCreatedForm(attributes) 15 | }, 16 | update (record, attributes) { 17 | this.title = attributes.title 18 | this.buildUpdatedForm(record, attributes) 19 | }, 20 | submitSuccess (res){ 21 | this.backToPage(true); 22 | }, 23 | backToPage (needRefresh) { 24 | this.$emit('back-to-page', needRefresh) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/mixins/popUpFormMixin.js: -------------------------------------------------------------------------------- 1 | import FormMixin from './formMixin' 2 | 3 | export default { 4 | mixins: [FormMixin], 5 | data () { 6 | return { 7 | // 标题 8 | title: '', 9 | visible: false 10 | } 11 | }, 12 | methods: { 13 | show(attributes) { 14 | this.title = attributes.title 15 | this.visible = true 16 | this.submitLoading = false 17 | }, 18 | add(attributes) { 19 | this.buildCreatedForm(attributes) 20 | this.show(attributes) 21 | }, 22 | update(record, attributes) { 23 | this.buildUpdatedForm(record, attributes) 24 | this.show(attributes) 25 | }, 26 | submitSuccess (res){ 27 | this.$emit('reload-page-table', false) 28 | this.handleClose() 29 | }, 30 | handleClose(e) { 31 | this.visible = false 32 | this.submitLoading = false 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import router from './router' 3 | import store from './store' 4 | 5 | import NProgress from 'nprogress' // progress bar 6 | import '@/styles/nprogress.less' // progress bar custom style 7 | import { setDocumentTitle } from '@/utils/domUtil' 8 | import projectConfig from '@/config/projectConfig' 9 | import { ACCESS_TOKEN } from '@/store/storage-types' 10 | 11 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 12 | 13 | const whiteList = ['login', 'register', 'registerResult'] // no redirect whitelist 14 | 15 | router.beforeEach((to, from, next) => { 16 | NProgress.start() // start progress bar 17 | to.meta && (typeof to.meta.title !== 'undefined' && setDocumentTitle(`${to.meta.title} - ${projectConfig.projectTitle}`)) 18 | if (Vue.ls.get(ACCESS_TOKEN)) { 19 | /* has token */ 20 | if (to.path === '/user/login') { 21 | next({ path: '/' }) 22 | NProgress.done() 23 | } 24 | else if (to.path === '/oauth2/authorize') { 25 | next() 26 | NProgress.done() 27 | } else { 28 | if (store.getters.userRouters.length === 0) { 29 | store.dispatch('GenerateRoutes').then(() => { 30 | // 根据roles权限生成可访问的路由表 31 | // 动态添加可访问路由表 32 | router.addRoutes(store.getters.userRouters) 33 | const redirect = decodeURIComponent(from.query.redirect || to.path) 34 | if (to.path === redirect) { 35 | // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record 36 | next({ path: redirect, replace: true }) 37 | } else { 38 | // 跳转到目的路由 39 | next({ path: redirect }) 40 | } 41 | }) 42 | } else { 43 | next() 44 | } 45 | } 46 | } else { 47 | if (whiteList.includes(to.name)) { 48 | // 在免登录白名单,直接进入 49 | next() 50 | } else { 51 | next({ path: '/user/login', query: { redirect: to.fullPath } }) 52 | NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it 53 | } 54 | } 55 | }) 56 | 57 | router.afterEach(() => { 58 | NProgress.done() // finish progress bar 59 | }) 60 | -------------------------------------------------------------------------------- /src/router/constantRouter.js: -------------------------------------------------------------------------------- 1 | import { UserLayout } from '@/layouts' 2 | 3 | /** 4 | * 基础路由 5 | * @type { *[] } 6 | */ 7 | export const constantRouters = [ 8 | { 9 | path: '/redirect/:path*', 10 | component: () => import(/* webpackChunkName: "user" */ '@/views/redirect/index') 11 | }, 12 | { 13 | path: '/oauth2/authorize', 14 | component: () => import(/* webpackChunkName: "oauth2" */ '@/views/oauth2/OAuth2Authorize') 15 | }, 16 | { 17 | path: '/user', 18 | component: UserLayout, 19 | redirect: '/user/login', 20 | hidden: true, 21 | children: [ 22 | { 23 | path: 'login', 24 | name: 'login', 25 | component: () => import(/* webpackChunkName: "user" */ '@/views/user/Login') 26 | }, 27 | { 28 | path: 'register', 29 | name: 'register', 30 | component: () => import(/* webpackChunkName: "user" */ '@/views/user/Register') 31 | }, 32 | { 33 | path: 'register-result', 34 | name: 'registerResult', 35 | component: () => import(/* webpackChunkName: "user" */ '@/views/user/RegisterResult') 36 | } 37 | ] 38 | }, 39 | 40 | { 41 | path: '/403', 42 | name: '403', 43 | component: () => import(/* webpackChunkName: "fail" */ '@/views/exception') 44 | }, 45 | { 46 | path: '/404', 47 | name: '404', 48 | component: () => import(/* webpackChunkName: "fail" */ '@/views/exception') 49 | }, 50 | { 51 | path: '/500', 52 | name: '500', 53 | component: () => import(/* webpackChunkName: "fail" */ '@/views/exception') 54 | } 55 | 56 | ] 57 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import { constantRouters } from '@/router/constantRouter' 4 | 5 | // hack router push callback 6 | const originalPush = Router.prototype.push 7 | Router.prototype.push = function push (location, onResolve, onReject) { 8 | if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject) 9 | return originalPush.call(this, location).catch(err => err) 10 | } 11 | 12 | Vue.use(Router) 13 | 14 | // @see https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 15 | const createRouter = () => new Router({ 16 | mode: 'history', 17 | base: process.env.BASE_URL, 18 | scrollBehavior: () => ({ y: 0 }), 19 | routes: constantRouters 20 | }) 21 | 22 | const router = createRouter() 23 | 24 | export function resetRouter () { 25 | const newRouter = createRouter() 26 | router.matcher = newRouter.matcher // the relevant part 27 | } 28 | 29 | export default router 30 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | // https://webpack.js.org/guides/dependency-management/#requirecontext 5 | const modulesFiles = require.context('./modules', true, /\.js$/) 6 | 7 | // you do not need `import app from './modules/app'` 8 | // it will auto require all vuex module from modules file 9 | const modules = {} 10 | modulesFiles.keys().forEach(key => { 11 | let moduleName = key.replace(/(\.\/|\.js)/g, ''); 12 | modules[moduleName] = modulesFiles(key).default 13 | }) 14 | 15 | Vue.use(Vuex) 16 | 17 | export default new Vuex.Store({ 18 | modules: modules 19 | }) 20 | -------------------------------------------------------------------------------- /src/store/modules/i18n.js: -------------------------------------------------------------------------------- 1 | import { defaultLanguage } from '@/config/projectConfig' 2 | 3 | const i18n = { 4 | state: { 5 | lang: defaultLanguage 6 | }, 7 | mutations: { 8 | SET_LANG: (state, lang) => { 9 | state.lang = lang 10 | } 11 | }, 12 | getters: { 13 | lang: state => state.lang 14 | } 15 | } 16 | 17 | export default i18n 18 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { login, logout } from '@/api/auth' 3 | import { ACCESS_TOKEN, USER_INFO, PERMISSIONS, ROLES } from '@/store/storage-types' 4 | import { generatorDynamicRouter } from '@/router/dynamicRouter' 5 | import { constantRouters } from '@/router/constantRouter' 6 | 7 | const user = { 8 | state: { 9 | token: '', 10 | roles: [], 11 | permissions: [], 12 | info: {}, 13 | routers: constantRouters, 14 | userRouters: [] 15 | }, 16 | getters: { 17 | token: state => state.token, 18 | roles: state => state.roles, 19 | permissions: state => state.permissions, 20 | userInfo: state => state.info, 21 | userRouters: state => state.userRouters, 22 | }, 23 | mutations: { 24 | SET_TOKEN: (state, token) => { 25 | state.token = token 26 | }, 27 | SET_INFO: (state, userInfo) => { 28 | state.info = userInfo 29 | }, 30 | SET_ROLES: (state, roles) => { 31 | state.roles = roles 32 | }, 33 | SET_PERMISSIONS: (state, permissions) => { 34 | state.permissions = permissions 35 | }, 36 | SET_ROUTERS: (state, routers) => { 37 | state.userRouters = routers 38 | } 39 | }, 40 | 41 | actions: { 42 | // 登录 43 | Login ({ commit }, loginParam) { 44 | return new Promise((resolve, reject) => { 45 | login(loginParam).then(res => { 46 | // TODO token刷新机制 47 | const ttl = res.expires_in * 1000 48 | const accessToken = res.access_token 49 | const refreshToken = res.refresh_token 50 | const info = res.info 51 | 52 | const attributes = res.attributes || {} 53 | const permissions = attributes.permissions 54 | const roleCodes = attributes.roleCodes 55 | 56 | if (permissions.length < 0) { 57 | reject(new Error('getInfo: roles must be a non-null array !')) 58 | } 59 | 60 | Vue.ls.set(USER_INFO, info, ttl) 61 | commit('SET_INFO', info) 62 | Vue.ls.set(ROLES, roleCodes, ttl) 63 | commit('SET_ROLES', roleCodes) 64 | Vue.ls.set(PERMISSIONS, permissions, ttl) 65 | commit('SET_PERMISSIONS', permissions) 66 | Vue.ls.set(ACCESS_TOKEN, accessToken, ttl) 67 | commit('SET_TOKEN', accessToken) 68 | 69 | resolve() 70 | }).catch(error => { 71 | reject(error) 72 | }) 73 | }) 74 | }, 75 | // 登出 76 | Logout ({ dispatch, commit, state }) { 77 | return new Promise((resolve) => { 78 | logout().then(() => { 79 | resolve() 80 | }).catch(() => { 81 | resolve() 82 | }).finally(() => { 83 | dispatch('CLEAN_USER_INFO').then(() => { 84 | setTimeout(() => { 85 | window.location.reload() 86 | }, 10) 87 | }) 88 | }) 89 | }) 90 | }, 91 | 92 | CLEAN_USER_INFO ({ commit, state }) { 93 | return new Promise((resolve) => { 94 | commit('SET_TOKEN', '') 95 | commit('SET_ROLES', []) 96 | Vue.ls.remove(ACCESS_TOKEN) 97 | Vue.ls.remove(USER_INFO) 98 | Vue.ls.remove(PERMISSIONS) 99 | Vue.ls.remove(ROLES) 100 | resolve() 101 | }) 102 | }, 103 | 104 | GenerateRoutes ({ commit }) { 105 | return new Promise(resolve => { 106 | generatorDynamicRouter().then(routers => { 107 | commit('SET_ROUTERS', routers) 108 | resolve() 109 | }) 110 | }) 111 | } 112 | } 113 | } 114 | 115 | export default user 116 | -------------------------------------------------------------------------------- /src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const APP_MUTATIONS = { 2 | // settings 3 | TOGGLE_PRIMARY_COLOR: 'TOGGLE_PRIMARY_COLOR', 4 | TOGGLE_NAV_THEME: 'TOGGLE_NAV_THEME', 5 | TOGGLE_LAYOUT: 'TOGGLE_LAYOUT', 6 | TOGGLE_CONTENT_WIDTH: 'TOGGLE_CONTENT_WIDTH', 7 | TOGGLE_FIXED_HEADER: 'TOGGLE_FIXED_HEADER', 8 | TOGGLE_FIXED_SIDERBAR: 'TOGGLE_FIXED_SIDERBAR', 9 | TOGGLE_COLOR_WEAK: 'TOGGLE_COLOR_WEAK', 10 | TOGGLE_MULTI_TAB: 'TOGGLE_MULTI_TAB', 11 | 12 | // 页面状态 13 | TOGGLE_SIDE_BAR_COLLAPSED: 'TOGGLE_SIDE_BAR_COLLAPSED', 14 | TOGGLE_CONTENT_FULL_SCREEN: 'TOGGLE_CONTENT_FULL_SCREEN', 15 | TOGGLE_DEVICE: 'TOGGLE_DEVICE', 16 | TOGGLE_SET_KEEPALIVE:'TOGGLE_SET_KEEPALIVE' 17 | } 18 | 19 | export const DICT = { 20 | SET_DICT_CACHE: 'set_dict_cache', 21 | SET_DICT_REQUEST_FLAG: 'del_dict_cache', 22 | DEL_DICT_REQUEST_FLAG: 'reset_dict_request_cache_item', 23 | DEL_DICT_CACHE: 'delete_invalid_dict' 24 | } 25 | 26 | export const LOV = { 27 | SET_CACHE: 'set_lov_cache', 28 | DEL_CACHE: 'del_lov_cache', 29 | SET_REQUEST: 'set_lov_request', 30 | DEL_REQUEST: 'del_lov_request' 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/store/storage-types.js: -------------------------------------------------------------------------------- 1 | // vue-ls 中存储数据类型 2 | 3 | // 框架显示配置 4 | export const SETTINGS_LS = { 5 | PRIMARY_COLOR: 'primary_color', 6 | NAV_THEME: 'nav_theme', 7 | LAYOUT: 'layout', 8 | COLOR_WEAK: 'color_weak', 9 | FIXED_HEADER: 'fixed_header', 10 | FIXED_SIDE_MENU: 'fixed_side_menu', 11 | CONTENT_WIDTH_TYPE: 'content_width_type', 12 | MULTI_TAB: 'multi_tab' 13 | } 14 | 15 | // 用户信息 16 | export const ACCESS_TOKEN = 'access-token' 17 | export const USER_INFO = 'user_info' 18 | export const PERMISSIONS = 'permissions' 19 | export const ROLES = 'roles' 20 | 21 | // 国际化语言 22 | export const APP_LANGUAGE = 'app_language' 23 | -------------------------------------------------------------------------------- /src/styles/ballcat.less: -------------------------------------------------------------------------------- 1 | .ant-card-body .table-operator { 2 | margin-bottom: 18px; 3 | 4 | button { 5 | margin-right: 8px; 6 | } 7 | } 8 | 9 | .wordwrap { 10 | width: 100%; 11 | height: auto; 12 | white-space: normal; 13 | word-break: break-all; 14 | word-wrap: break-word 15 | } 16 | 17 | .ant-pro-table-search { 18 | margin-bottom: 16px; 19 | padding: 16px 24px 0; 20 | background: #fff; 21 | 22 | .ant-form-item { 23 | margin-bottom: 16px !important; 24 | } 25 | } 26 | 27 | .ant-pro-table-toolbar { 28 | display: flex; 29 | justify-content: space-between; 30 | padding: 16px 0; 31 | } 32 | 33 | .ant-pro-table-toolbar-title { 34 | display: flex; 35 | align-items: center; 36 | justify-content: flex-start; 37 | color: rgba(0,0,0,.85); 38 | font-weight: 500; 39 | font-size: 16px; 40 | } 41 | 42 | .ant-pro-table-toolbar-option { 43 | display: flex; 44 | align-items: center; 45 | justify-content: flex-end; 46 | 47 | button { 48 | margin-left: 8px; 49 | } 50 | } 51 | 52 | .ant-space-align-center { 53 | align-items: center; 54 | } 55 | 56 | 57 | .table-page-search-wrapper { 58 | padding-left: 8px; 59 | padding-right: 8px; 60 | text-align: right; 61 | 62 | .table-page-search-submitButtons { 63 | margin-bottom: 16px; 64 | } 65 | } 66 | 67 | .antd-pro-pages-form-advanced-form-style-card { 68 | margin-bottom: 18px !important; 69 | } 70 | 71 | .ant-pro-page-container-children-content { 72 | height: 100%; 73 | padding: 16px; 74 | } 75 | 76 | // 警告文本,多用于删除按钮 77 | .ballcat-text-danger { 78 | color: #ff4d4f; 79 | } 80 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/es/style/themes/default.less'; 2 | 3 | // The prefix to use on all css classes from ant-pro. 4 | @ant-pro-prefix : ant-pro; 5 | @ant-global-header-zindex : 105; 6 | 7 | // 修改全局表单的 label 和 wrapper 的行高为 32px 8 | .@{ant-prefix}-input { 9 | height: auto !important; 10 | } 11 | .@{ant-prefix}-form-item-label{ 12 | line-height: 31.9999px !important; 13 | } 14 | .@{ant-prefix}-form-item-control { 15 | // 这里不知道为什么写 32,就会导致 a-row colspan 8 时,第三个和第四个表单元素为连续两个 select,第二个 select 在 第二行最右侧的问题 16 | line-height: 32px !important; 17 | } 18 | .@{ant-prefix}-select-selection { 19 | border-top-width: 1px !important; 20 | } 21 | 22 | // 表格滚动条自动,防止宽度足够时也展示滚动条 23 | .@{ant-prefix}-table-body { 24 | overflow-x: auto !important; 25 | } 26 | // 缩小弹窗表头 27 | .@{ant-prefix}-modal-header { 28 | padding: 14px 24px !important; 29 | } 30 | .@{ant-prefix}-modal-close-x { 31 | width: 56px !important; 32 | height: 50px !important; 33 | line-height: 50px !important; 34 | } 35 | 36 | //表格折叠展开内容的 p 标签 37 | .@{ant-prefix}-table-expanded-row p { 38 | color: rgba(0, 0, 0, 0.85); 39 | font-weight: 500; 40 | font-size: 16px; 41 | margin-bottom: 2px; 42 | } 43 | -------------------------------------------------------------------------------- /src/styles/nprogress.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/es/style/themes/default.less'; 2 | 3 | /* Make clicks pass-through */ 4 | #nprogress { 5 | pointer-events: none; 6 | } 7 | 8 | #nprogress .bar { 9 | background: @primary-color; 10 | 11 | position: fixed; 12 | z-index: 1031; 13 | top: 0; 14 | left: 0; 15 | 16 | width: 100%; 17 | height: 2px; 18 | } 19 | 20 | /* Fancy blur effect */ 21 | #nprogress .peg { 22 | display: block; 23 | position: absolute; 24 | right: 0px; 25 | width: 100px; 26 | height: 100%; 27 | box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color; 28 | opacity: 1.0; 29 | 30 | -webkit-transform: rotate(3deg) translate(0px, -4px); 31 | -ms-transform: rotate(3deg) translate(0px, -4px); 32 | transform: rotate(3deg) translate(0px, -4px); 33 | } 34 | 35 | /* Remove these to get rid of the spinner */ 36 | #nprogress .spinner { 37 | display: block; 38 | position: fixed; 39 | z-index: 1031; 40 | top: 15px; 41 | right: 15px; 42 | } 43 | 44 | #nprogress .spinner-icon { 45 | width: 18px; 46 | height: 18px; 47 | box-sizing: border-box; 48 | 49 | border: solid 2px transparent; 50 | border-top-color: @primary-color; 51 | border-left-color: @primary-color; 52 | border-radius: 50%; 53 | 54 | -webkit-animation: nprogress-spinner 400ms linear infinite; 55 | animation: nprogress-spinner 400ms linear infinite; 56 | } 57 | 58 | .nprogress-custom-parent { 59 | overflow: hidden; 60 | position: relative; 61 | } 62 | 63 | .nprogress-custom-parent #nprogress .spinner, 64 | .nprogress-custom-parent #nprogress .bar { 65 | position: absolute; 66 | } 67 | 68 | @-webkit-keyframes nprogress-spinner { 69 | 0% { -webkit-transform: rotate(0deg); } 70 | 100% { -webkit-transform: rotate(360deg); } 71 | } 72 | @keyframes nprogress-spinner { 73 | 0% { transform: rotate(0deg); } 74 | 100% { transform: rotate(360deg); } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/utils/authorize.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '@/store' 3 | 4 | /** 5 | * Action 权限指令 6 | * 指令用法: 7 | * - 在需要控制 action 级别权限的组件上使用 v-action:[method] , 如下: 8 | * 添加用户 9 | * 删除用户 10 | * 修改 11 | * 12 | * - 当前用户没有权限时,组件上使用了该指令则会被隐藏 13 | * - 当后台权限跟 pro 提供的模式不同时,只需要针对这里的权限过滤进行修改即可 14 | * 15 | * @see https://github.com/sendya/ant-design-pro-vue/pull/53 16 | */ 17 | export const has = Vue.directive('has', { 18 | inserted: function (el, binding, vnode) { 19 | const permissionId = binding.value 20 | const permissions = store.getters.permissions 21 | if (!permissions.includes(permissionId)) { 22 | el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none') 23 | } 24 | } 25 | }) 26 | 27 | export const role = Vue.directive('role', { 28 | inserted: function (el, binding, vnode) { 29 | const role = binding.value 30 | const roles = store.getters.roles 31 | if (!roles.includes(role)) { 32 | el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none') 33 | } 34 | } 35 | }) 36 | 37 | 38 | export function plugin (Vue) { 39 | if (plugin.installed) { 40 | return 41 | } 42 | 43 | !Vue.prototype.$has && Object.defineProperties(Vue.prototype, { 44 | $has: { 45 | get () { 46 | return (permissionId) => { 47 | const permissions = store.getters.permissions 48 | return permissions.includes(permissionId); 49 | } 50 | } 51 | } 52 | }) 53 | 54 | !Vue.prototype.$role && Object.defineProperties(Vue.prototype, { 55 | $role: { 56 | get () { 57 | return (role) => { 58 | const roles = store.getters.roles 59 | return roles.includes(role); 60 | } 61 | } 62 | } 63 | }) 64 | } 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/utils/captchaUtil.js: -------------------------------------------------------------------------------- 1 | export function resetSize(vm) { 2 | let img_width, img_height, bar_width, bar_height; //图片的宽度、高度,移动条的宽度、高度 3 | 4 | const parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth 5 | const parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight 6 | 7 | if (vm.imgSize.width.indexOf('%') !== -1) { 8 | img_width = parseInt(this.imgSize.width) / 100 * parentWidth + 'px' 9 | } else { 10 | img_width = this.imgSize.width; 11 | } 12 | 13 | if (vm.imgSize.height.indexOf('%') !== -1) { 14 | img_height = parseInt(this.imgSize.height) / 100 * parentHeight + 'px' 15 | } else { 16 | img_height = this.imgSize.height 17 | } 18 | 19 | if (vm.barSize.width.indexOf('%') !== -1) { 20 | bar_width = parseInt(this.barSize.width) / 100 * parentWidth + 'px' 21 | } else { 22 | bar_width = this.barSize.width 23 | } 24 | 25 | if (vm.barSize.height.indexOf('%') !== -1) { 26 | bar_height = parseInt(this.barSize.height) / 100 * parentHeight + 'px' 27 | } else { 28 | bar_height = this.barSize.height 29 | } 30 | 31 | return {imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height} 32 | } 33 | 34 | export const _code_chars = [1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] 35 | export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0'] 36 | export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC'] 37 | -------------------------------------------------------------------------------- /src/utils/device.js: -------------------------------------------------------------------------------- 1 | import enquireJs from 'enquire.js' 2 | 3 | export const DEVICE_TYPE = { 4 | DESKTOP: 'desktop', 5 | TABLET: 'tablet', 6 | MOBILE: 'mobile' 7 | } 8 | 9 | export const deviceEnquire = function (callback) { 10 | const matchDesktop = { 11 | match: () => { 12 | callback && callback(DEVICE_TYPE.DESKTOP) 13 | } 14 | } 15 | 16 | const matchTablet = { 17 | match: () => { 18 | callback && callback(DEVICE_TYPE.TABLET) 19 | } 20 | } 21 | 22 | const matchMobile = { 23 | match: () => { 24 | callback && callback(DEVICE_TYPE.MOBILE) 25 | } 26 | } 27 | 28 | // screen and (max-width: 1087.99px) 29 | enquireJs 30 | .register('screen and (max-width: 768px)', matchMobile) 31 | .register('screen and (min-width: 768px) and (max-width: 999px)', matchTablet) 32 | .register('screen and (min-width: 1000px)', matchDesktop) 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/domUtil.js: -------------------------------------------------------------------------------- 1 | export const setDocumentTitle = function (title) { 2 | document.title = title 3 | const ua = navigator.userAgent 4 | // eslint-disable-next-line 5 | const regex = /\bMicroMessenger\/([\d\.]+)/ 6 | if (regex.test(ua) && /ip(hone|od|ad)/i.test(ua)) { 7 | const i = document.createElement('iframe') 8 | i.src = '/favicon.ico' 9 | i.style.display = 'none' 10 | i.onload = function () { 11 | setTimeout(function () { 12 | i.remove() 13 | }, 9) 14 | } 15 | document.body.appendChild(i) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/fileUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 从响应头中解析对应的文件名 3 | * @param headers 4 | * @returns {string} 5 | */ 6 | function resolveFileName (headers) { 7 | let match = headers['content-disposition'].match(/filename=(.*)/) 8 | if(match && match.length > 0){ 9 | return decodeURI(match[1]) 10 | } 11 | } 12 | 13 | /** 14 | * 远程文件下载 15 | * @param response 16 | * @param fileName 17 | */ 18 | export function remoteFileDownload (response, fileName) { 19 | if (response.data) { 20 | // 构造一个blob对象来处理数据,并设置文件类型 21 | let headers = response.headers 22 | let contentType = headers['content-type'] 23 | const blob = new Blob([response.data], { type: contentType }) 24 | 25 | // 不存在则从响应头中解析 26 | if (!fileName) { 27 | fileName = resolveFileName(headers) 28 | } 29 | 30 | if (window.navigator.msSaveOrOpenBlob) { //兼容IE10 31 | navigator.msSaveBlob(blob, fileName) 32 | } else { 33 | const href = URL.createObjectURL(blob) //创建新的URL表示指定的blob对象 34 | const a = document.createElement('a') //创建a标签 35 | a.style.display = 'none' 36 | a.href = href // 指定下载链接 37 | a.download = fileName //指定下载文件名 38 | a.click() //触发下载 39 | URL.revokeObjectURL(a.href) //释放URL对象 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/filter.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import moment from 'moment' 3 | import 'moment/locale/zh-cn' 4 | moment.locale('zh-cn') 5 | 6 | Vue.filter('NumberFormat', function (value) { 7 | if (!value) { 8 | return '0' 9 | } 10 | const intPartFormat = value.toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') // 将整数部分逢三一断 11 | return intPartFormat 12 | }) 13 | 14 | Vue.filter('dayjs', function (dataStr, pattern = 'YYYY-MM-DD HH:mm:ss') { 15 | return moment(dataStr).format(pattern) 16 | }) 17 | 18 | Vue.filter('moment', function (dataStr, pattern = 'YYYY-MM-DD HH:mm:ss') { 19 | return moment(dataStr).format(pattern) 20 | }) 21 | -------------------------------------------------------------------------------- /src/utils/mixin.js: -------------------------------------------------------------------------------- 1 | // import Vue from 'vue' 2 | import { deviceEnquire, DEVICE_TYPE } from '@/utils/device' 3 | import { mapMutations, mapGetters } from 'vuex' 4 | import { APP_MUTATIONS } from '@/store/mutation-types' 5 | 6 | // const mixinsComputed = Vue.config.optionMergeStrategies.computed 7 | // const mixinsMethods = Vue.config.optionMergeStrategies.methods 8 | 9 | const mixin = { 10 | computed: { 11 | ...mapGetters([ 12 | 'navTheme', 13 | 'layout', 14 | 'contentWidth', 15 | 'fixedHeader', 16 | 'fixSiderbar', 17 | 'primaryColor', 18 | 'colorWeak', 19 | 'multiTab' 20 | ]) 21 | }, 22 | } 23 | 24 | const mixinDevice = { 25 | computed: { 26 | ...mapGetters(['device']), 27 | isMobile () { 28 | return this.device === DEVICE_TYPE.MOBILE 29 | }, 30 | isDesktop () { 31 | return this.device === DEVICE_TYPE.DESKTOP 32 | }, 33 | isTablet () { 34 | return this.device === DEVICE_TYPE.TABLET 35 | } 36 | } 37 | } 38 | 39 | const AppDeviceEnquire = { 40 | mounted () { 41 | deviceEnquire(deviceType => { 42 | // 切换设备类型 43 | this[APP_MUTATIONS.TOGGLE_DEVICE](deviceType) 44 | }) 45 | }, 46 | methods: { 47 | ...mapMutations([ 48 | APP_MUTATIONS.TOGGLE_DEVICE, 49 | APP_MUTATIONS.TOGGLE_SIDE_BAR_COLLAPSED 50 | ]) 51 | } 52 | } 53 | 54 | export { mixin, AppDeviceEnquire, mixinDevice } 55 | -------------------------------------------------------------------------------- /src/utils/password.js: -------------------------------------------------------------------------------- 1 | import * as CryptoJS from 'crypto-js' 2 | 3 | const securityKey = '==BallCat-Auth==' 4 | 5 | export const passEncrypt = (pass) => { 6 | // 密码加密 7 | const key = CryptoJS.enc.Utf8.parse(securityKey) 8 | return CryptoJS.AES.encrypt( 9 | pass, 10 | key, { 11 | iv: key, 12 | mode: CryptoJS.mode.CBC, 13 | padding: CryptoJS.pad.Pkcs7 14 | }).toString() 15 | } 16 | 17 | /** 18 | * @word 要加密的内容 19 | * @keyWord String 服务器随机返回的关键字 20 | * */ 21 | export function captchaEncrypt (word, keyWord = 'XwKsGlMcdPMEhR1B') { 22 | const key = CryptoJS.enc.Utf8.parse(keyWord) 23 | const srcs = CryptoJS.enc.Utf8.parse(word) 24 | const encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }) 25 | return encrypted.toString() 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/strUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 下划线转驼峰 3 | * @param str 4 | */ 5 | export function underlineToLittleCamel (str) { 6 | return str.replace(/_([a-z])/g, (p, m) => m.toUpperCase()) 7 | } 8 | 9 | /** 10 | * 小驼峰转下划线 11 | * @param str 12 | * @returns {{}|*} 13 | */ 14 | export function littleCamelToUnderline (str) { 15 | return str.replace(/([A-Z])/g, (p, m) => `_${m.toLowerCase()}`) 16 | } 17 | 18 | /** 19 | * 首字母大写 20 | * @param str 21 | * @returns {*} 22 | */ 23 | export function firstUpperCase (str) { 24 | return str.replace(/^\S/, s => s.toUpperCase()) 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/treeUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 数组转树形结构 3 | * @param list 源数组 4 | * @param parentId 父ID 5 | * @param attributeFill 数据处理函数 6 | */ 7 | export const listToTree = (list, parentId, attributeFill) => { 8 | let tree = [] 9 | fillTree(list, tree, parentId, attributeFill) 10 | return tree 11 | } 12 | 13 | /** 14 | * 数组转树形结构 15 | * @param list 源数组 16 | * @param tree 树 17 | * @param parentId 父ID 18 | * @param attributeFill 属性填充函数 19 | */ 20 | export const fillTree = (list, tree, parentId, attributeFill) => { 21 | list.forEach(item => { 22 | // 判断是否为父级菜单 23 | if (item.parentId === parentId) { 24 | const treeNode = { 25 | ...item, 26 | key: item.key || item.id, 27 | children: [] 28 | } 29 | 30 | // 额外的数据转换处理 31 | if (typeof attributeFill === 'function') { 32 | attributeFill(treeNode, item) 33 | } 34 | 35 | // 迭代 list, 找到当前菜单相符合的所有子菜单 36 | fillTree(list, treeNode.children, item.id, attributeFill) 37 | // 删掉不存在 children 值的属性 38 | if (treeNode.children.length <= 0) { 39 | delete treeNode.children 40 | } 41 | // 加入到树中 42 | tree.push(treeNode) 43 | } 44 | }) 45 | } 46 | 47 | /** 48 | * 根据指定规则进行剪枝 49 | * @param treeList 50 | * @param matcher 51 | * @returns {*[]} 52 | */ 53 | export function pruneTree (treeList, matcher) { 54 | const result = [] 55 | if (treeList) { 56 | for (let treeNode of treeList) { 57 | const children = pruneTree(treeNode.children, matcher) 58 | if (children && children.length > 0) { 59 | treeNode.children = children 60 | result.push(treeNode) 61 | } else if (matcher(treeNode)) { 62 | treeNode.children = [] 63 | result.push(treeNode) 64 | } 65 | } 66 | } 67 | return result 68 | } 69 | 70 | /** 71 | * 获取匹配节点的所有祖先节点 id 72 | * @param treeList 树节点集合 73 | * @param matcher 匹配器 74 | * @returns 祖先节点 id 集合 75 | */ 76 | export function matchedParentKeys (treeList, matcher) { 77 | const result = [] 78 | fillMatchedParentKeys(treeList, matcher, result) 79 | return result 80 | } 81 | 82 | /** 83 | * 获取匹配节点的所有祖先节点 id 84 | * @param treeList 树节点集合 85 | * @param matcher 匹配器 86 | * @param result 返回值 87 | * @returns {boolean} 88 | */ 89 | export function fillMatchedParentKeys (treeList, matcher, result) { 90 | if (!treeList || treeList.length === 0) { 91 | return false 92 | } 93 | let matched = false 94 | for (let node of treeList) { 95 | // 如果孩子节点有匹配,则把自己的 id 加入返回值 96 | if (fillMatchedParentKeys(node.children, matcher, result)) { 97 | matched = true 98 | result.push(node.id) 99 | } 100 | // 如果当前节点匹配了,matched 修改为 true 101 | if (matcher(node)) { 102 | matched = true 103 | } 104 | } 105 | return matched 106 | } 107 | -------------------------------------------------------------------------------- /src/utils/util.js: -------------------------------------------------------------------------------- 1 | export function timeFix () { 2 | const time = new Date() 3 | const hour = time.getHours() 4 | return hour < 9 ? '早上好' : hour <= 11 ? '上午好' : hour <= 13 ? '中午好' : hour < 20 ? '下午好' : '晚上好' 5 | } 6 | 7 | export function welcome () { 8 | const arr = ['休息一会儿吧', '准备吃什么呢?', '要不要打一把 DOTA', '我猜你可能累了'] 9 | const index = Math.floor(Math.random() * arr.length) 10 | return arr[index] 11 | } 12 | 13 | /** 14 | * 触发 window.resize 15 | */ 16 | export function triggerWindowResizeEvent () { 17 | const event = document.createEvent('HTMLEvents') 18 | event.initEvent('resize', true, true) 19 | event.eventType = 'message' 20 | window.dispatchEvent(event) 21 | } 22 | 23 | /** 24 | * Remove loading animate 25 | * @param id parent element id or class 26 | * @param timeout 27 | */ 28 | export function removeLoadingAnimate (id = '', timeout = 1500) { 29 | if (id === '') { 30 | return 31 | } 32 | setTimeout(() => { 33 | document.body.removeChild(document.getElementById(id)) 34 | }, timeout) 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } -------------------------------------------------------------------------------- /src/views/account/settings/Binding.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /src/views/account/settings/Notification.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /src/views/account/settings/Security.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /src/views/exception/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 51 | -------------------------------------------------------------------------------- /src/views/i18n/I18nMessageModal.vue: -------------------------------------------------------------------------------- 1 | 24 | 110 | -------------------------------------------------------------------------------- /src/views/i18n/LanguageText.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 88 | 89 | 108 | -------------------------------------------------------------------------------- /src/views/i18n/i18n-data/I18nDataCreateModal.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 96 | 97 | -------------------------------------------------------------------------------- /src/views/i18n/i18n-data/I18nDataUpdateModal.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 80 | -------------------------------------------------------------------------------- /src/views/iframe/index.vue: -------------------------------------------------------------------------------- 1 |