├── frontend ├── .gitattributes ├── .browserslistrc ├── .env ├── .env.preview ├── src │ ├── components │ │ ├── MultiTab │ │ │ ├── events.js │ │ │ ├── index.less │ │ │ └── index.js │ │ ├── Trend │ │ │ ├── index.js │ │ │ ├── index.less │ │ │ ├── index.md │ │ │ └── Trend.vue │ │ ├── Ellipsis │ │ │ ├── index.js │ │ │ ├── index.md │ │ │ └── Ellipsis.vue │ │ ├── NoticeIcon │ │ │ ├── index.js │ │ │ └── NoticeIcon.vue │ │ ├── IconSelector │ │ │ ├── index.js │ │ │ ├── README.md │ │ │ └── IconSelector.vue │ │ ├── NumberInfo │ │ │ ├── index.js │ │ │ ├── index.md │ │ │ ├── NumberInfo.vue │ │ │ └── index.less │ │ ├── SettingDrawer │ │ │ ├── index.js │ │ │ ├── SettingItem.vue │ │ │ ├── themeColor.js │ │ │ └── settingConfig.js │ │ ├── StandardFormRow │ │ │ └── index.js │ │ ├── ArticleListContent │ │ │ ├── index.js │ │ │ └── ArticleListContent.vue │ │ ├── FooterToolbar │ │ │ ├── index.js │ │ │ ├── index.less │ │ │ ├── index.md │ │ │ └── FooterToolBar.vue │ │ ├── AvatarList │ │ │ ├── index.js │ │ │ ├── Item.jsx │ │ │ ├── index.less │ │ │ ├── List.jsx │ │ │ └── index.md │ │ ├── index.less │ │ ├── TextArea │ │ │ ├── style.less │ │ │ └── index.jsx │ │ ├── Charts │ │ │ ├── chart.less │ │ │ ├── smooth.area.less │ │ │ ├── MiniSmoothArea.vue │ │ │ ├── MiniBar.vue │ │ │ ├── MiniArea.vue │ │ │ ├── TransferBar.vue │ │ │ ├── Bar.vue │ │ │ ├── Liquid.vue │ │ │ ├── Radar.vue │ │ │ ├── MiniProgress.vue │ │ │ ├── RankList.vue │ │ │ ├── Trend.vue │ │ │ ├── ChartCard.vue │ │ │ └── TagCloud.vue │ │ ├── Search │ │ │ ├── index.less │ │ │ └── GlobalSearch.jsx │ │ ├── SelectLang │ │ │ ├── index.less │ │ │ └── index.jsx │ │ ├── GlobalFooter │ │ │ └── index.vue │ │ ├── _util │ │ │ └── util.js │ │ ├── TagSelect │ │ │ └── TagSelectOption.jsx │ │ ├── Editor │ │ │ ├── WangEditor.vue │ │ │ └── QuillEditor.vue │ │ ├── GlobalHeader │ │ │ ├── RightContent.vue │ │ │ └── AvatarDropdown.vue │ │ ├── index.js │ │ ├── NProgress │ │ │ └── nprogress.less │ │ ├── tools │ │ │ └── TwoStepCaptcha.vue │ │ ├── PageLoading │ │ │ └── index.jsx │ │ └── Other │ │ │ └── CarbonAds.vue │ ├── assets │ │ ├── logo.png │ │ └── icons │ │ │ └── bx-analyse.svg │ ├── utils │ │ ├── permissions.js │ │ ├── filter.js │ │ ├── domUtil.js │ │ ├── axios.js │ │ ├── routeConvert.js │ │ ├── utils.less │ │ ├── screenLog.js │ │ ├── helper │ │ │ └── permission.js │ │ ├── util.js │ │ └── request.js │ ├── views │ │ ├── account │ │ │ ├── center │ │ │ │ └── page │ │ │ │ │ ├── index.js │ │ │ │ │ └── Article.vue │ │ │ └── settings │ │ │ │ ├── Binding.vue │ │ │ │ ├── Notification.vue │ │ │ │ ├── Security.vue │ │ │ │ └── Custom.vue │ │ ├── 404.vue │ │ ├── dashboard │ │ │ ├── Monitor.vue │ │ │ └── Workplace.less │ │ ├── list │ │ │ ├── search │ │ │ │ ├── components │ │ │ │ │ ├── IconText.vue │ │ │ │ │ └── CardInfo.vue │ │ │ │ └── SearchLayout.vue │ │ │ ├── QueryList.vue │ │ │ ├── components │ │ │ │ └── Info.vue │ │ │ └── modules │ │ │ │ ├── CreateForm.vue │ │ │ │ └── TaskForm.vue │ │ ├── exception │ │ │ ├── 403.vue │ │ │ ├── 404.vue │ │ │ └── 500.vue │ │ ├── user │ │ │ └── RegisterResult.vue │ │ ├── other │ │ │ ├── IconSelectorView.vue │ │ │ └── modules │ │ │ │ └── OrgModal.vue │ │ ├── helloworld │ │ │ └── HelloWorld.vue │ │ ├── result │ │ │ └── Error.vue │ │ └── form │ │ │ └── stepForm │ │ │ ├── StepForm.vue │ │ │ ├── Step3.vue │ │ │ └── Step2.vue │ ├── api │ │ ├── hello.js │ │ ├── manage.js │ │ └── login.js │ ├── layouts │ │ ├── PageView.vue │ │ ├── BlankLayout.vue │ │ ├── index.js │ │ ├── RouteView.vue │ │ └── BasicLayout.less │ ├── store │ │ ├── device-mixin.js │ │ ├── i18n-mixin.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── mutation-types.js │ │ ├── modules │ │ │ ├── async-router.js │ │ │ ├── permission.js │ │ │ ├── app.js │ │ │ └── user.js │ │ └── app-mixin.js │ ├── mock │ │ ├── services │ │ │ ├── hello.js │ │ │ ├── auth.js │ │ │ └── article.js │ │ ├── index.js │ │ └── util.js │ ├── core │ │ ├── icons.js │ │ ├── use.js │ │ ├── directives │ │ │ └── action.js │ │ ├── bootstrap.js │ │ └── lazy_use.js │ ├── locales │ │ ├── lang │ │ │ ├── zh-CN.js │ │ │ └── en-US.js │ │ └── index.js │ ├── router │ │ └── index.js │ ├── App.vue │ ├── config │ │ ├── defaultSettings.js │ │ └── router.config.js │ ├── main.js │ ├── global.less │ └── permission.js ├── .env.development ├── tests │ └── unit │ │ └── .eslintrc.js ├── .prettierrc ├── postcss.config.js ├── public │ ├── logo.png │ ├── avatar2.jpg │ └── index.html ├── webstorm.config.js ├── .travis.yml ├── jsconfig.json ├── .gitignore ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── need-help-issue.md │ │ ├── feature_request.md │ │ └── bug_report.md │ └── pull_request_template.md ├── jest.config.js ├── babel.config.js ├── docs │ ├── add-page-loading-animate.md │ └── webpack-bundle-analyzer.md ├── .editorconfig ├── LICENSE ├── .eslintrc.js ├── package.json └── config │ └── themePluginConfig.js ├── backend ├── requirements.txt ├── public │ ├── logo.png │ ├── avatar2.jpg │ ├── js │ │ ├── fail.6f77b8ab.js │ │ └── lang-zh-CN.ddc47f61.js │ ├── css │ │ └── user.01ff6ef1.css │ └── index.html └── .gitignore └── LICENSE /frontend/.gitattributes: -------------------------------------------------------------------------------- 1 | public/* linguist-vendored -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | aiofiles -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 10 4 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | VUE_APP_PREVIEW=false 3 | VUE_APP_API_BASE_URL=/api -------------------------------------------------------------------------------- /frontend/.env.preview: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | VUE_APP_PREVIEW=true 3 | VUE_APP_API_BASE_URL=/api -------------------------------------------------------------------------------- /frontend/src/components/MultiTab/events.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | export default new Vue() 3 | -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | VUE_APP_PREVIEW=true 3 | VUE_APP_API_BASE_URL=/api -------------------------------------------------------------------------------- /frontend/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/components/Trend/index.js: -------------------------------------------------------------------------------- 1 | import Trend from './Trend.vue' 2 | 3 | export default Trend 4 | -------------------------------------------------------------------------------- /backend/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elprup/fastapi-vue-template/HEAD/backend/public/logo.png -------------------------------------------------------------------------------- /frontend/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elprup/fastapi-vue-template/HEAD/frontend/public/logo.png -------------------------------------------------------------------------------- /frontend/src/components/Ellipsis/index.js: -------------------------------------------------------------------------------- 1 | import Ellipsis from './Ellipsis' 2 | 3 | export default Ellipsis 4 | -------------------------------------------------------------------------------- /backend/public/avatar2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elprup/fastapi-vue-template/HEAD/backend/public/avatar2.jpg -------------------------------------------------------------------------------- /frontend/public/avatar2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elprup/fastapi-vue-template/HEAD/frontend/public/avatar2.jpg -------------------------------------------------------------------------------- /frontend/src/components/NoticeIcon/index.js: -------------------------------------------------------------------------------- 1 | import NoticeIcon from './NoticeIcon' 2 | export default NoticeIcon 3 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elprup/fastapi-vue-template/HEAD/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/components/IconSelector/index.js: -------------------------------------------------------------------------------- 1 | import IconSelector from './IconSelector' 2 | export default IconSelector 3 | -------------------------------------------------------------------------------- /frontend/src/components/NumberInfo/index.js: -------------------------------------------------------------------------------- 1 | import NumberInfo from './NumberInfo' 2 | 3 | export default NumberInfo 4 | -------------------------------------------------------------------------------- /frontend/src/components/SettingDrawer/index.js: -------------------------------------------------------------------------------- 1 | import SettingDrawer from './SettingDrawer' 2 | export default SettingDrawer 3 | -------------------------------------------------------------------------------- /frontend/src/components/StandardFormRow/index.js: -------------------------------------------------------------------------------- 1 | import StandardFormRow from './StandardFormRow' 2 | 3 | export default StandardFormRow 4 | -------------------------------------------------------------------------------- /frontend/src/components/ArticleListContent/index.js: -------------------------------------------------------------------------------- 1 | import ArticleListContent from './ArticleListContent' 2 | 3 | export default ArticleListContent 4 | -------------------------------------------------------------------------------- /frontend/webstorm.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const webpackConfig = require('@vue/cli-service/webpack.config.js') 3 | module.exports = webpackConfig 4 | -------------------------------------------------------------------------------- /frontend/src/components/FooterToolbar/index.js: -------------------------------------------------------------------------------- 1 | import FooterToolBar from './FooterToolBar' 2 | import './index.less' 3 | 4 | export default FooterToolBar 5 | -------------------------------------------------------------------------------- /frontend/.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 | -------------------------------------------------------------------------------- /frontend/src/utils/permissions.js: -------------------------------------------------------------------------------- 1 | export function actionToObject (json) { 2 | try { 3 | return JSON.parse(json) 4 | } catch (e) { 5 | console.log('err', e.message) 6 | } 7 | return [] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/views/account/center/page/index.js: -------------------------------------------------------------------------------- 1 | import AppPage from './App' 2 | import ArticlePage from './Article' 3 | import ProjectPage from './Project' 4 | 5 | export { AppPage, ArticlePage, ProjectPage } 6 | -------------------------------------------------------------------------------- /frontend/src/components/AvatarList/index.js: -------------------------------------------------------------------------------- 1 | import AvatarList from './List' 2 | import Item from './Item' 3 | 4 | export { 5 | AvatarList, 6 | Item as AvatarListItem 7 | } 8 | 9 | export default AvatarList 10 | -------------------------------------------------------------------------------- /frontend/src/views/404.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /frontend/src/api/hello.js: -------------------------------------------------------------------------------- 1 | import { axios } from '@/utils/request' 2 | 3 | const api = { 4 | Hello: 'hello' 5 | } 6 | 7 | export default api 8 | 9 | export function getHello() { 10 | return axios.get(api.Hello, {}) 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/layouts/PageView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/Monitor.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /frontend/src/store/device-mixin.js: -------------------------------------------------------------------------------- 1 | import { mapState } from 'vuex' 2 | 3 | const deviceMixin = { 4 | computed: { 5 | ...mapState({ 6 | isMobile: state => state.app.isMobile 7 | }) 8 | } 9 | } 10 | 11 | export { deviceMixin } 12 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/src/components/index.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/lib/style/index"; 2 | 3 | // The prefix to use on all css classes from ant-pro. 4 | @ant-pro-prefix : ant-pro; 5 | @ant-global-sider-zindex : 106; 6 | @ant-global-header-zindex : 105; -------------------------------------------------------------------------------- /frontend/src/layouts/BlankLayout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /frontend/src/mock/services/hello.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs2' 2 | import { builder } from '../util' 3 | 4 | // 5 | const hello = () => { 6 | return builder([{ value: 200, name: 'my hello world' }]) 7 | } 8 | 9 | Mock.mock(/\/hello/, 'get', hello) 10 | -------------------------------------------------------------------------------- /frontend/src/components/TextArea/style.less: -------------------------------------------------------------------------------- 1 | .ant-textarea-limit { 2 | position: relative; 3 | 4 | .limit { 5 | position: absolute; 6 | color: #909399; 7 | background: #fff; 8 | font-size: 12px; 9 | bottom: 5px; 10 | right: 10px; 11 | } 12 | } -------------------------------------------------------------------------------- /frontend/src/components/Charts/chart.less: -------------------------------------------------------------------------------- 1 | .antv-chart-mini { 2 | position: relative; 3 | width: 100%; 4 | 5 | .chart-wrapper { 6 | position: absolute; 7 | bottom: -28px; 8 | width: 100%; 9 | 10 | /* margin: 0 -5px; 11 | overflow: hidden;*/ 12 | } 13 | } -------------------------------------------------------------------------------- /frontend/src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import UserLayout from './UserLayout' 2 | import BlankLayout from './BlankLayout' 3 | import BasicLayout from './BasicLayout' 4 | import RouteView from './RouteView' 5 | import PageView from './PageView' 6 | 7 | export { UserLayout, BasicLayout, BlankLayout, RouteView, PageView } 8 | -------------------------------------------------------------------------------- /frontend/.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 | package-lock.json 23 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/smooth.area.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @smoothArea-prefix-cls: ~"@{ant-pro-prefix}-smooth-area"; 4 | 5 | .@{smoothArea-prefix-cls} { 6 | position: relative; 7 | width: 100%; 8 | 9 | .chart-wrapper { 10 | position: absolute; 11 | bottom: -28px; 12 | width: 100%; 13 | } 14 | } -------------------------------------------------------------------------------- /frontend/src/store/i18n-mixin.js: -------------------------------------------------------------------------------- 1 | import { mapState } from 'vuex' 2 | 3 | const i18nMixin = { 4 | computed: { 5 | ...mapState({ 6 | currentLang: state => state.app.lang 7 | }) 8 | }, 9 | methods: { 10 | setLang (lang) { 11 | this.$store.dispatch('setLang', lang) 12 | } 13 | } 14 | } 15 | 16 | export default i18nMixin 17 | -------------------------------------------------------------------------------- /frontend/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 bxAnaalyse from '@/assets/icons/bx-analyse.svg?inline' // path to your '*.svg?inline' file. 10 | 11 | export { bxAnaalyse } 12 | -------------------------------------------------------------------------------- /frontend/src/views/account/settings/Binding.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /frontend/src/views/account/settings/Notification.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /frontend/src/views/list/search/components/IconText.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /frontend/.github/ISSUE_TEMPLATE/need-help-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Need help issue 3 | about: Question for use(问题求助) 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Question (问题描述)** 11 | How to use component `s-table` paging 12 | 13 | **Describe the solution you'd like (你期待的是什么?)** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context(附加信息)** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /frontend/src/views/exception/403.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /frontend/src/views/exception/404.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /frontend/src/views/exception/500.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /frontend/src/components/FooterToolbar/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 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 | } -------------------------------------------------------------------------------- /frontend/src/locales/lang/zh-CN.js: -------------------------------------------------------------------------------- 1 | import antd from 'ant-design-vue/es/locale-provider/zh_CN' 2 | import momentCN from 'moment/locale/zh-cn' 3 | 4 | const components = { 5 | antLocale: antd, 6 | momentName: 'zh-cn', 7 | momentLocale: momentCN 8 | } 9 | 10 | const locale = { 11 | message: '-', 12 | 'menu.home': '主页', 13 | 'menu.hello': '问候', 14 | 'menu.dashboard': '仪表盘', 15 | 'menu.dashboard.analysis': '分析页', 16 | 'menu.dashboard.monitor': '监控页', 17 | 'menu.dashboard.workplace': '工作台' 18 | } 19 | 20 | export default { 21 | ...components, 22 | ...locale 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/components/Search/index.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/es/style/themes/default"; 2 | 3 | .global-search-wrapper { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | z-index: @zindex-modal-mask; 10 | background: @modal-mask-bg; 11 | 12 | .global-search-box { 13 | position: absolute; 14 | top: 20%; 15 | left: 50%; 16 | width: 450px; 17 | transform: translate(-50%, -50%); 18 | 19 | .global-search-tips { 20 | color: @white; 21 | font-size: @font-size-lg; 22 | text-align: right; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /frontend/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | isMobile: state => state.app.isMobile, 3 | lang: state => state.app.lang, 4 | theme: state => state.app.theme, 5 | color: state => state.app.color, 6 | token: state => state.user.token, 7 | avatar: state => state.user.avatar, 8 | nickname: state => state.user.name, 9 | welcome: state => state.user.welcome, 10 | roles: state => state.user.roles, 11 | userInfo: state => state.user.info, 12 | addRouters: state => state.permission.addRouters, 13 | multiTab: state => state.app.multiTab 14 | } 15 | 16 | export default getters 17 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import { constantRouterMap } from '@/config/router.config' 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 | export default new Router({ 15 | mode: 'history', 16 | routes: constantRouterMap 17 | }) 18 | -------------------------------------------------------------------------------- /backend/public/js/fail.6f77b8ab.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["fail"],{cc89:function(t,e,o){"use strict";o.r(e);var n=function(){var t=this,e=t.$createElement,o=t._self._c||e;return o("a-result",{attrs:{status:"404",title:"404","sub-title":"Sorry, the page you visited does not exist."},scopedSlots:t._u([{key:"extra",fn:function(){return[o("a-button",{attrs:{type:"primary"},on:{click:t.toHome}},[t._v(" Back Home ")])]},proxy:!0}])})},s=[],r={name:"Exception404",methods:{toHome:function(){this.$router.push({path:"/"})}}},u=r,a=o("2877"),i=Object(a["a"])(u,n,s,!1,null,null,null);e["default"]=i.exports}}]); -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/src/components/MultiTab/index.less: -------------------------------------------------------------------------------- 1 | @import '../index'; 2 | 3 | @multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab"; 4 | @multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper"; 5 | 6 | /* 7 | .topmenu .@{multi-tab-prefix-cls} { 8 | max-width: 1200px; 9 | margin: -23px auto 24px auto; 10 | } 11 | */ 12 | .@{multi-tab-prefix-cls} { 13 | margin: -23px -24px 24px -24px; 14 | background: #fff; 15 | } 16 | 17 | .topmenu .@{multi-tab-wrapper-prefix-cls} { 18 | max-width: 1200px; 19 | margin: 0 auto; 20 | } 21 | 22 | .topmenu.content-width-Fluid .@{multi-tab-wrapper-prefix-cls} { 23 | max-width: 100%; 24 | margin: 0 auto; 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import app from './modules/app' 5 | import user from './modules/user' 6 | 7 | // default router permission control 8 | import permission from './modules/permission' 9 | 10 | // dynamic router permission control (Experimental) 11 | // import permission from './modules/async-router' 12 | import getters from './getters' 13 | 14 | Vue.use(Vuex) 15 | 16 | export default new Vuex.Store({ 17 | modules: { 18 | app, 19 | user, 20 | permission 21 | }, 22 | state: { 23 | 24 | }, 25 | mutations: { 26 | 27 | }, 28 | actions: { 29 | 30 | }, 31 | getters 32 | }) 33 | -------------------------------------------------------------------------------- /frontend/src/utils/domUtil.js: -------------------------------------------------------------------------------- 1 | import config from '@/config/defaultSettings' 2 | 3 | export const setDocumentTitle = function (title) { 4 | document.title = title 5 | const ua = navigator.userAgent 6 | // eslint-disable-next-line 7 | const regex = /\bMicroMessenger\/([\d\.]+)/ 8 | if (regex.test(ua) && /ip(hone|od|ad)/i.test(ua)) { 9 | const i = document.createElement('iframe') 10 | i.src = '/favicon.ico' 11 | i.style.display = 'none' 12 | i.onload = function () { 13 | setTimeout(function () { 14 | i.remove() 15 | }, 9) 16 | } 17 | document.body.appendChild(i) 18 | } 19 | } 20 | 21 | export const domTitle = config.title 22 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV) 2 | 3 | const plugins = [] 4 | if (IS_PROD) { 5 | plugins.push('transform-remove-console') 6 | } 7 | 8 | // lazy load ant-design-vue 9 | // if your use import on Demand, Use this code 10 | plugins.push(['import', { 11 | 'libraryName': 'ant-design-vue', 12 | 'libraryDirectory': 'es', 13 | 'style': true // `style: true` 会加载 less 文件 14 | }]) 15 | 16 | module.exports = { 17 | presets: [ 18 | '@vue/cli-plugin-babel/preset', 19 | [ 20 | '@babel/preset-env', 21 | { 22 | 'useBuiltIns': 'entry', 23 | 'corejs': 3 24 | } 25 | ] 26 | ], 27 | plugins 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /frontend/.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /frontend/src/components/SelectLang/index.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/es/style/themes/default"; 2 | 3 | @header-menu-prefix-cls: ~'@{ant-prefix}-pro-header-menu'; 4 | @header-drop-down-prefix-cls: ~'@{ant-prefix}-pro-drop-down'; 5 | 6 | .@{header-menu-prefix-cls} { 7 | 8 | .anticon { 9 | margin-right: 8px; 10 | } 11 | .ant-dropdown-menu-item { 12 | min-width: 160px; 13 | } 14 | } 15 | 16 | .@{header-drop-down-prefix-cls} { 17 | 18 | line-height: @layout-header-height; 19 | vertical-align: top; 20 | cursor: pointer; 21 | 22 | > i { 23 | font-size: 16px !important; 24 | transform: none !important; 25 | 26 | svg { 27 | position: relative; 28 | top: -1px; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/docs/add-page-loading-animate.md: -------------------------------------------------------------------------------- 1 | 为首屏增加 加载动画 2 | ==== 3 | 4 | 5 | 6 | ## 需求 7 | 8 | > 为了缓解用户第一次访问时,加载 JS 过大所导致用户等待白屏时间过长导致的用户体验不好,进行的一个优化动效。 9 | 10 | 11 | 12 | ## 实现方案 13 | 14 | 1. 将 动画加载 dom 元素放在 #app 内,Vue 生命周期开始时,会自动清掉 #app 下的所有元素。 15 | 2. 将 动画加载 dom 元素放在 body 下,Vue 生命周期开始时 App.vue (created, mounted) 调用 `@/utils/utll` 下的 removeLoadingAnimate(#id, timeout) 则会移除加载动画 16 | 17 | 最后一步: 18 | ​ 将样式插入到 `public/index.html` 文件的 `` 最好写成内联 `` 19 | 20 | 21 | 22 | ---- 23 | 24 | 目前提供有两个样式,均在 `public/loading` 文件夹内。且 pro 已经默认使用了一套 loading 动画方案,可以直接参考 `public/index.html` 25 | 26 | 27 | ## 写在最后 28 | 29 | 目前 pro 有页面 overflow 显示出浏览器滚动条时,页面会抖动一下的问题。 30 | 31 | 欢迎各位提供能解决的方案和实现 demo。如果在条件允许的情况下,建议请直接使用 pro 进行改造,也欢迎直接 PR 到 pro 的仓库 32 | -------------------------------------------------------------------------------- /frontend/src/utils/axios.js: -------------------------------------------------------------------------------- 1 | const VueAxios = { 2 | vm: {}, 3 | // eslint-disable-next-line no-unused-vars 4 | install (Vue, instance) { 5 | if (this.installed) { 6 | return 7 | } 8 | this.installed = true 9 | 10 | if (!instance) { 11 | // eslint-disable-next-line no-console 12 | console.error('You have to install axios') 13 | return 14 | } 15 | 16 | Vue.axios = instance 17 | 18 | Object.defineProperties(Vue.prototype, { 19 | axios: { 20 | get: function get () { 21 | return instance 22 | } 23 | }, 24 | $http: { 25 | get: function get () { 26 | return instance 27 | } 28 | } 29 | }) 30 | } 31 | } 32 | 33 | export { 34 | VueAxios 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/components/Trend/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @trend-prefix-cls: ~"@{ant-pro-prefix}-trend"; 4 | 5 | .@{trend-prefix-cls} { 6 | display: inline-block; 7 | font-size: @font-size-base; 8 | line-height: 22px; 9 | 10 | .up, 11 | .down { 12 | margin-left: 4px; 13 | position: relative; 14 | top: 1px; 15 | 16 | i { 17 | font-size: 12px; 18 | transform: scale(0.83); 19 | } 20 | } 21 | 22 | .item-text { 23 | display: inline-block; 24 | margin-left: 8px; 25 | color: rgba(0,0,0,.85); 26 | } 27 | 28 | .up { 29 | color: @red-6; 30 | } 31 | .down { 32 | color: @green-6; 33 | top: -1px; 34 | } 35 | 36 | &.reverse-color .up { 37 | color: @green-6; 38 | } 39 | &.reverse-color .down { 40 | color: @red-6; 41 | } 42 | } -------------------------------------------------------------------------------- /frontend/src/components/GlobalFooter/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /frontend/src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const ACCESS_TOKEN = 'Access-Token' 2 | 3 | export const SIDEBAR_TYPE = 'sidebar_type' 4 | export const TOGGLE_MOBILE_TYPE = 'is_mobile' 5 | export const TOGGLE_NAV_THEME = 'nav_theme' 6 | export const TOGGLE_LAYOUT = 'layout' 7 | export const TOGGLE_FIXED_HEADER = 'fixed_header' 8 | export const TOGGLE_FIXED_SIDEBAR = 'fixed_sidebar' 9 | export const TOGGLE_CONTENT_WIDTH = 'content_width' 10 | export const TOGGLE_HIDE_HEADER = 'auto_hide_header' 11 | export const TOGGLE_COLOR = 'color' 12 | export const TOGGLE_WEAK = 'weak' 13 | export const TOGGLE_MULTI_TAB = 'multi_tab' 14 | export const APP_LANGUAGE = 'app_language' 15 | 16 | export const CONTENT_WIDTH_TYPE = { 17 | Fluid: 'Fluid', 18 | Fixed: 'Fixed' 19 | } 20 | 21 | export const NAV_THEME = { 22 | LIGHT: 'light', 23 | DARK: 'dark' 24 | } 25 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | insert_final_newline=false 5 | indent_style=space 6 | indent_size=2 7 | 8 | [{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}] 9 | indent_style=space 10 | indent_size=2 11 | 12 | [{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}] 13 | indent_style=space 14 | indent_size=2 15 | 16 | [{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}] 17 | indent_style=space 18 | indent_size=2 19 | 20 | [*.svg] 21 | indent_style=space 22 | indent_size=2 23 | 24 | [*.js.map] 25 | indent_style=space 26 | indent_size=2 27 | 28 | [*.less] 29 | indent_style=space 30 | indent_size=2 31 | 32 | [*.vue] 33 | indent_style=space 34 | indent_size=2 35 | 36 | [{.analysis_options,*.yml,*.yaml}] 37 | indent_style=space 38 | indent_size=2 39 | 40 | -------------------------------------------------------------------------------- /frontend/src/components/SettingDrawer/SettingItem.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 39 | -------------------------------------------------------------------------------- /frontend/src/layouts/RouteView.vue: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /frontend/src/utils/routeConvert.js: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash.clonedeep' 2 | 3 | export function convertRoutes (nodes) { 4 | if (!nodes) return null 5 | 6 | nodes = cloneDeep(nodes) 7 | 8 | let queue = Array.isArray(nodes) ? nodes.concat() : [nodes] 9 | 10 | while (queue.length) { 11 | const levelSize = queue.length 12 | 13 | for (let i = 0; i < levelSize; i++) { 14 | const node = queue.shift() 15 | 16 | if (!node.children || !node.children.length) continue 17 | 18 | node.children.forEach(child => { 19 | // 转化相对路径 20 | if (child.path[0] !== '/' && !child.path.startsWith('http')) { 21 | child.path = node.path.replace(/(\w*)[/]*$/, `$1/${child.path}`) 22 | } 23 | }) 24 | 25 | queue = queue.concat(node.children) 26 | } 27 | } 28 | 29 | return nodes 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/mock/index.js: -------------------------------------------------------------------------------- 1 | import { isIE } from '@/utils/util' 2 | 3 | // 判断环境不是 prod 或者 preview 是 true 时,加载 mock 服务 4 | if (process.env.NODE_ENV !== 'production' || process.env.VUE_APP_PREVIEW === 'true') { 5 | if (isIE()) { 6 | console.error('[antd-pro] ERROR: `mockjs` NOT SUPPORT `IE` PLEASE DO NOT USE IN `production` ENV.') 7 | } 8 | // 使用同步加载依赖 9 | // 防止 vuex 中的 GetInfo 早于 mock 运行,导致无法 mock 请求返回结果 10 | console.log('[antd-pro] mock mounting') 11 | const Mock = require('mockjs2') 12 | require('./services/auth') 13 | require('./services/user') 14 | require('./services/manage') 15 | require('./services/other') 16 | require('./services/tagCloud') 17 | require('./services/article') 18 | require('./services/hello') 19 | Mock.setup({ 20 | timeout: 800 // setter delay time 21 | }) 22 | console.log('[antd-pro] mock mounted') 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/core/use.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // base library 4 | import Antd from 'ant-design-vue' 5 | import Viser from 'viser-vue' 6 | import VueCropper from 'vue-cropper' 7 | import 'ant-design-vue/dist/antd.less' 8 | 9 | // ext library 10 | import VueClipboard from 'vue-clipboard2' 11 | import MultiTab from '@/components/MultiTab' 12 | import PageLoading from '@/components/PageLoading' 13 | import PermissionHelper from '@/utils/helper/permission' 14 | // import '@/components/use' 15 | import './directives/action' 16 | 17 | VueClipboard.config.autoSetContainer = true 18 | 19 | Vue.use(Antd) 20 | Vue.use(Viser) 21 | Vue.use(MultiTab) 22 | Vue.use(PageLoading) 23 | Vue.use(VueClipboard) 24 | Vue.use(PermissionHelper) 25 | Vue.use(VueCropper) 26 | 27 | process.env.NODE_ENV !== 'production' && console.warn('[antd-pro] WARNING: Antd now use fulled imported.') 28 | -------------------------------------------------------------------------------- /frontend/src/store/modules/async-router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 向后端请求用户的菜单,动态生成路由 3 | */ 4 | import { constantRouterMap } from '@/config/router.config' 5 | import { generatorDynamicRouter } from '@/router/generator-routers' 6 | 7 | const permission = { 8 | state: { 9 | routers: constantRouterMap, 10 | addRouters: [] 11 | }, 12 | mutations: { 13 | SET_ROUTERS: (state, routers) => { 14 | state.addRouters = routers 15 | state.routers = constantRouterMap.concat(routers) 16 | } 17 | }, 18 | actions: { 19 | GenerateRoutes ({ commit }, data) { 20 | return new Promise(resolve => { 21 | const { token } = data 22 | generatorDynamicRouter(token).then(routers => { 23 | commit('SET_ROUTERS', routers) 24 | resolve() 25 | }) 26 | }) 27 | } 28 | } 29 | } 30 | 31 | export default permission 32 | -------------------------------------------------------------------------------- /frontend/src/components/Ellipsis/index.md: -------------------------------------------------------------------------------- 1 | # Ellipsis 文本自动省略号 2 | 3 | 文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。 4 | 5 | 6 | 7 | 引用方式: 8 | 9 | ```javascript 10 | import Ellipsis from '@/components/Ellipsis' 11 | 12 | export default { 13 | components: { 14 | Ellipsis 15 | } 16 | } 17 | ``` 18 | 19 | 20 | 21 | ## 代码演示 [demo](https://pro.loacg.com/test/home) 22 | 23 | ```html 24 | 25 | There were injuries alleged in three cases in 2015, and a 26 | fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall. 27 | 28 | ``` 29 | 30 | 31 | 32 | ## API 33 | 34 | 35 | 参数 | 说明 | 类型 | 默认值 36 | ----|------|-----|------ 37 | tooltip | 移动到文本展示完整内容的提示 | boolean | - 38 | length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - -------------------------------------------------------------------------------- /frontend/src/layouts/BasicLayout.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/es/style/themes/default.less"; 2 | 3 | .ant-pro-global-header-index-right { 4 | margin-right: 8px; 5 | 6 | &.ant-pro-global-header-index-dark { 7 | .ant-pro-global-header-index-action { 8 | color: hsla(0, 0%, 100%, .85); 9 | 10 | &:hover { 11 | background: #1890ff; 12 | } 13 | } 14 | } 15 | 16 | .ant-pro-account-avatar { 17 | .antd-pro-global-header-index-avatar { 18 | margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0; 19 | margin-right: 8px; 20 | color: @primary-color; 21 | vertical-align: top; 22 | background: rgba(255, 255, 255, 0.85); 23 | } 24 | } 25 | 26 | .menu { 27 | .anticon { 28 | margin-right: 8px; 29 | } 30 | 31 | .ant-dropdown-menu-item { 32 | min-width: 100px; 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /frontend/src/components/Trend/index.md: -------------------------------------------------------------------------------- 1 | # Trend 趋势标记 2 | 3 | 趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。 4 | 5 | 6 | 7 | 引用方式: 8 | 9 | ```javascript 10 | import Trend from '@/components/Trend' 11 | 12 | export default { 13 | components: { 14 | Trend 15 | } 16 | } 17 | ``` 18 | 19 | 20 | 21 | ## 代码演示 [demo](https://pro.loacg.com/test/home) 22 | 23 | ```html 24 | 5% 25 | ``` 26 | 或 27 | ```html 28 | 29 | 工资 30 | 5% 31 | 32 | ``` 33 | 或 34 | ```html 35 | 5% 36 | ``` 37 | 38 | 39 | ## API 40 | 41 | | 参数 | 说明 | 类型 | 默认值 | 42 | |----------|------------------------------------------|-------------|-------| 43 | | flag | 上升下降标识:`up|down` | string | - | 44 | | reverseColor | 颜色反转 | Boolean | false | 45 | 46 | -------------------------------------------------------------------------------- /frontend/src/components/Trend/Trend.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/src/components/NumberInfo/index.md: -------------------------------------------------------------------------------- 1 | # NumberInfo 数据文本 2 | 3 | 常用在数据卡片中,用于突出展示某个业务数据。 4 | 5 | 6 | 7 | 引用方式: 8 | 9 | ```javascript 10 | import NumberInfo from '@/components/NumberInfo' 11 | 12 | export default { 13 | components: { 14 | NumberInfo 15 | } 16 | } 17 | ``` 18 | 19 | 20 | 21 | ## 代码演示 [demo](https://pro.loacg.com/test/home) 22 | 23 | ```html 24 | 29 | ``` 30 | 31 | 32 | 33 | ## API 34 | 35 | 参数 | 说明 | 类型 | 默认值 36 | ----|------|-----|------ 37 | title | 标题 | ReactNode\|string | - 38 | subTitle | 子标题 | ReactNode\|string | - 39 | total | 总量 | ReactNode\|string | - 40 | subTotal | 子总量 | ReactNode\|string | - 41 | status | 增加状态 | 'up \| down' | - 42 | theme | 状态样式 | string | 'light' 43 | gap | 设置数字和描述之间的间距(像素)| number | 8 44 | -------------------------------------------------------------------------------- /frontend/src/components/AvatarList/Item.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'ant-design-vue/es/_util/vue-types' 2 | import { Tooltip, Avatar } from 'ant-design-vue' 3 | import { getSlotOptions } from 'ant-design-vue/lib/_util/props-util' 4 | import { warning } from 'ant-design-vue/lib/vc-util/warning' 5 | 6 | export const AvatarListItemProps = { 7 | tips: PropTypes.string.def(null), 8 | src: PropTypes.string.def('') 9 | } 10 | 11 | const Item = { 12 | __ANT_AVATAR_CHILDREN: true, 13 | name: 'AvatarListItem', 14 | props: AvatarListItemProps, 15 | created () { 16 | warning(getSlotOptions(this.$parent).__ANT_AVATAR_LIST, 'AvatarListItem must be a subcomponent of AvatarList') 17 | }, 18 | render () { 19 | const AvatarDom = 20 | return this.tips && {AvatarDom} || 21 | } 22 | } 23 | 24 | export default Item 25 | -------------------------------------------------------------------------------- /frontend/docs/webpack-bundle-analyzer.md: -------------------------------------------------------------------------------- 1 | 先增加依赖 2 | 3 | ```bash 4 | // npm 5 | $ npm install --save-dev webpack-bundle-analyzer 6 | 7 | // or yarn 8 | $ yarn add webpack-bundle-analyzer -D 9 | ``` 10 | 11 | 配置文件 `vue.config.js` 增加 `configureWebpack.plugins` 参数 12 | 13 | ``` 14 | const path = require('path') 15 | const webpack = require('webpack') 16 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 17 | 18 | function resolve (dir) { 19 | return path.join(__dirname, dir) 20 | } 21 | 22 | // vue.config.js 23 | module.exports = { 24 | configureWebpack: { 25 | plugins: [ 26 | // Ignore all locale files of moment.js 27 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 28 | // 依赖大小分析工具 29 | new BundleAnalyzerPlugin(), 30 | ] 31 | }, 32 | 33 | 34 | ... 35 | } 36 | ``` 37 | 38 | 39 | 40 | 启动 `cli` 的 `build` 命令进行项目编译,编译完成时,会自动运行一个 http://localhost:8888 的地址,完整显示了支持库依赖 -------------------------------------------------------------------------------- /frontend/src/components/IconSelector/README.md: -------------------------------------------------------------------------------- 1 | IconSelector 2 | ==== 3 | 4 | > 图标选择组件,常用于为某一个数据设定一个图标时使用 5 | > eg: 设定菜单列表时,为每个菜单设定一个图标 6 | 7 | 该组件由 [@Saraka](https://github.com/saraka-tsukai) 封装 8 | 9 | 10 | 11 | ### 使用方式 12 | 13 | ```vue 14 | 19 | 20 | 39 | ``` 40 | 41 | 42 | 43 | ### 事件 44 | 45 | 46 | | 名称 | 说明 | 类型 | 默认值 | 47 | | ------ | -------------------------- | ------ | ------ | 48 | | change | 当改变了 `icon` 选中项触发 | String | - | 49 | -------------------------------------------------------------------------------- /frontend/src/store/app-mixin.js: -------------------------------------------------------------------------------- 1 | import { mapState } from 'vuex' 2 | 3 | const baseMixin = { 4 | computed: { 5 | ...mapState({ 6 | layout: state => state.app.layout, 7 | navTheme: state => state.app.theme, 8 | primaryColor: state => state.app.color, 9 | colorWeak: state => state.app.weak, 10 | fixedHeader: state => state.app.fixedHeader, 11 | fixedSidebar: state => state.app.fixedSidebar, 12 | contentWidth: state => state.app.contentWidth, 13 | autoHideHeader: state => state.app.autoHideHeader, 14 | 15 | isMobile: state => state.app.isMobile, 16 | sideCollapsed: state => state.app.sideCollapsed, 17 | multiTab: state => state.app.multiTab 18 | }), 19 | isTopMenu () { 20 | return this.layout === 'topmenu' 21 | } 22 | }, 23 | methods: { 24 | isSideMenu () { 25 | return !this.isTopMenu 26 | } 27 | } 28 | } 29 | 30 | export { 31 | baseMixin 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/components/MultiTab/index.js: -------------------------------------------------------------------------------- 1 | import events from './events' 2 | import MultiTab from './MultiTab' 3 | import './index.less' 4 | 5 | const api = { 6 | /** 7 | * open new tab on route fullPath 8 | * @param config 9 | */ 10 | open: function (config) { 11 | events.$emit('open', config) 12 | }, 13 | rename: function (key, name) { 14 | events.$emit('rename', { key: key, name: name }) 15 | }, 16 | /** 17 | * close current page 18 | */ 19 | closeCurrentPage: function () { 20 | this.close() 21 | }, 22 | /** 23 | * close route fullPath tab 24 | * @param config 25 | */ 26 | close: function (config) { 27 | events.$emit('close', config) 28 | } 29 | } 30 | 31 | MultiTab.install = function (Vue) { 32 | if (Vue.prototype.$multiTab) { 33 | return 34 | } 35 | api.instance = events 36 | Vue.prototype.$multiTab = api 37 | Vue.component('multi-tab', MultiTab) 38 | } 39 | 40 | export default MultiTab 41 | -------------------------------------------------------------------------------- /frontend/src/components/SettingDrawer/themeColor.js: -------------------------------------------------------------------------------- 1 | import client from 'webpack-theme-color-replacer/client' 2 | import generate from '@ant-design/colors/lib/generate' 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 | const rgb = client.varyColor.toNum3(color.replace('#', '')).join(',') 13 | return lightens.concat(colorPalettes).concat(rgb) 14 | }, 15 | changeColor (newColor) { 16 | var options = { 17 | newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors` 18 | changeUrl (cssUrl) { 19 | return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path 20 | } 21 | } 22 | return client.changer.changeColor(options, Promise) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/views/user/RegisterResult.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 35 | 36 | 38 | -------------------------------------------------------------------------------- /frontend/.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | First of all, thank you for your contribution! 😄 2 | 3 | Pull request will be merged after one of collaborators approve. 4 | Please makes sure that these form are filled before submitting your pull request, thank you! 5 | 6 | 7 | ### 这个变动的性质是 8 | 9 | - [ ] 新特性提交 10 | - [ ] 日常 bug 修复 11 | - [ ] 文档改进 12 | - [ ] 组件样式改进 13 | - [ ] 重构 14 | - [ ] 代码风格优化 15 | - [ ] 分支合并 16 | - [ ] 其他改动(是关于什么的改动?) 17 | 18 | ### 需求背景 19 | 20 | > 1. 描述相关需求的来源。 21 | > 2. 要解决的问题。 22 | > 3. 相关的 issue 讨论链接。 23 | 24 | ### 实现方案和 API(非新功能可选) 25 | 26 | > 1. 基本的解决思路和其他可选方案。 27 | > 2. 列出最终的 API 实现和用法。 28 | > 3. 涉及UI/交互变动需要有截图或 GIF。 29 | 30 | ### 对用户的影响和可能的风险(非新功能可选) 31 | 32 | > 1. 这个改动对用户端是否有影响?影响的方面有哪些? 33 | > 2. 是否有可能隐含的 break change 和其他风险? 34 | 35 | ### Changelog 描述(非新功能可选) 36 | 37 | > 1. 英文描述 38 | > 2. 中文描述(可选) 39 | 40 | ### 请求合并前的自查清单 41 | 42 | - [ ] 文档已补充或无须补充 43 | - [ ] 代码演示已提供或无须提供 44 | - [ ] Changelog 已提供或无须提供 45 | 46 | ### 后续计划(非新功能可选) 47 | 48 | > 如果这个提交后面还有相关的其他提交和跟进信息,可以写在这里。 -------------------------------------------------------------------------------- /frontend/src/components/Charts/MiniSmoothArea.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /frontend/src/views/other/IconSelectorView.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | -------------------------------------------------------------------------------- /frontend/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 | } -------------------------------------------------------------------------------- /frontend/src/config/defaultSettings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 项目默认配置项 3 | * primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage 4 | * navTheme - sidebar theme ['dark', 'light'] 两种主题 5 | * colorWeak - 色盲模式 6 | * layout - 整体布局方式 ['sidemenu', 'topmenu'] 两种布局 7 | * fixedHeader - 固定 Header : boolean 8 | * fixSiderbar - 固定左侧菜单栏 : boolean 9 | * contentWidth - 内容区布局: 流式 | 固定 10 | * 11 | * storageOptions: {} - Vue-ls 插件配置项 (localStorage/sessionStorage) 12 | * 13 | */ 14 | 15 | export default { 16 | navTheme: 'dark', // theme for nav menu 17 | primaryColor: '#52C41A', // primary color of ant design 18 | layout: 'sidemenu', // nav menu position: `sidemenu` or `topmenu` 19 | contentWidth: 'Fluid', // layout of content: `Fluid` or `Fixed`, only works when layout is topmenu 20 | fixedHeader: false, // sticky header 21 | fixSiderbar: false, // sticky siderbar 22 | colorWeak: false, 23 | menu: { 24 | locale: true 25 | }, 26 | title: 'Ant Design Pro', 27 | pwa: false, 28 | iconfontUrl: '', 29 | production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true' 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/components/FooterToolbar/FooterToolBar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 44 | 45 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 elprup 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/src/mock/util.js: -------------------------------------------------------------------------------- 1 | const responseBody = { 2 | message: '', 3 | timestamp: 0, 4 | result: null, 5 | code: 0 6 | } 7 | 8 | export const builder = (data, message, code = 0, headers = {}) => { 9 | responseBody.result = data 10 | if (message !== undefined && message !== null) { 11 | responseBody.message = message 12 | } 13 | if (code !== undefined && code !== 0) { 14 | responseBody.code = code 15 | responseBody._status = code 16 | } 17 | if (headers !== null && typeof headers === 'object' && Object.keys(headers).length > 0) { 18 | responseBody._headers = headers 19 | } 20 | responseBody.timestamp = new Date().getTime() 21 | return responseBody 22 | } 23 | 24 | export const getQueryParameters = (options) => { 25 | const url = options.url 26 | const search = url.split('?')[1] 27 | if (!search) { 28 | return {} 29 | } 30 | return JSON.parse('{"' + decodeURIComponent(search) 31 | .replace(/"/g, '\\"') 32 | .replace(/&/g, '","') 33 | .replace(/=/g, '":"') + '"}') 34 | } 35 | 36 | export const getBody = (options) => { 37 | return options.body && JSON.parse(options.body) 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/views/helloworld/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 55 | -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Anan Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /frontend/src/components/_util/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * components util 3 | */ 4 | 5 | /** 6 | * 清理空值,对象 7 | * @param children 8 | * @returns {*[]} 9 | */ 10 | export function filterEmpty (children = []) { 11 | return children.filter(c => c.tag || (c.text && c.text.trim() !== '')) 12 | } 13 | 14 | /** 15 | * 获取字符串长度,英文字符 长度1,中文字符长度2 16 | * @param {*} str 17 | */ 18 | export const getStrFullLength = (str = '') => 19 | str.split('').reduce((pre, cur) => { 20 | const charCode = cur.charCodeAt(0) 21 | if (charCode >= 0 && charCode <= 128) { 22 | return pre + 1 23 | } 24 | return pre + 2 25 | }, 0) 26 | 27 | /** 28 | * 截取字符串,根据 maxLength 截取后返回 29 | * @param {*} str 30 | * @param {*} maxLength 31 | */ 32 | export const cutStrByFullLength = (str = '', maxLength) => { 33 | let showLength = 0 34 | return str.split('').reduce((pre, cur) => { 35 | const charCode = cur.charCodeAt(0) 36 | if (charCode >= 0 && charCode <= 128) { 37 | showLength += 1 38 | } else { 39 | showLength += 2 40 | } 41 | if (showLength <= maxLength) { 42 | return pre + cur 43 | } 44 | return pre 45 | }, '') 46 | } 47 | -------------------------------------------------------------------------------- /frontend/.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve(Bug 反馈) 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug (描述 Bug)** 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | 15 | 16 | **To Reproduce (重现步骤)** 17 | Steps to reproduce the behavior: 18 | 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | 25 | 26 | **Expected behavior(你期待的是什么?)** 27 | A clear and concise description of what you expected to happen. 28 | 29 | 30 | 31 | **Screenshots(截图)** 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | 35 | 36 | **Desktop (please complete the following information):** 37 | 38 | - OS: [e.g. iOS] 39 | - Browser [e.g. chrome, safari] 40 | - Version [e.g. 22] 41 | 42 | 43 | 44 | **Smartphone (please complete the following information):** 45 | 46 | - Device: [e.g. iPhone6] 47 | - OS: [e.g. iOS8.1] 48 | - Browser [e.g. stock browser, safari] 49 | - Version [e.g. 22] 50 | 51 | 52 | 53 | **Additional context(附加信息)** 54 | Add any other context about the problem here. -------------------------------------------------------------------------------- /frontend/src/core/directives/action.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/vueComponent/ant-design-vue-pro/pull/53 16 | */ 17 | const action = Vue.directive('action', { 18 | inserted: function (el, binding, vnode) { 19 | const actionName = binding.arg 20 | const roles = store.getters.roles 21 | const elVal = vnode.context.$route.meta.permission 22 | const permissionId = elVal instanceof String && [elVal] || elVal 23 | roles.permissions.forEach(p => { 24 | if (!permissionId.includes(p.permissionId)) { 25 | return 26 | } 27 | if (p.actionList && !p.actionList.includes(actionName)) { 28 | el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none') 29 | } 30 | }) 31 | } 32 | }) 33 | 34 | export default action 35 | -------------------------------------------------------------------------------- /frontend/src/views/list/search/components/CardInfo.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 59 | -------------------------------------------------------------------------------- /frontend/src/views/result/Error.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 38 | -------------------------------------------------------------------------------- /frontend/src/components/TagSelect/TagSelectOption.jsx: -------------------------------------------------------------------------------- 1 | import { Tag } from 'ant-design-vue' 2 | const { CheckableTag } = Tag 3 | 4 | export default { 5 | name: 'TagSelectOption', 6 | props: { 7 | prefixCls: { 8 | type: String, 9 | default: 'ant-pro-tag-select-option' 10 | }, 11 | value: { 12 | type: [String, Number, Object], 13 | default: '' 14 | }, 15 | checked: { 16 | type: Boolean, 17 | default: false 18 | } 19 | }, 20 | data () { 21 | return { 22 | localChecked: this.checked || false 23 | } 24 | }, 25 | watch: { 26 | 'checked' (val) { 27 | this.localChecked = val 28 | }, 29 | '$parent.items': { 30 | handler: function (val) { 31 | this.value && val.hasOwnProperty(this.value) && (this.localChecked = val[this.value]) 32 | }, 33 | deep: true 34 | } 35 | }, 36 | render () { 37 | const { $slots, value } = this 38 | const onChange = (checked) => { 39 | this.$emit('change', { value, checked }) 40 | } 41 | return ( 42 | {$slots.default} 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | // with polyfills 2 | import 'core-js/stable' 3 | import 'regenerator-runtime/runtime' 4 | 5 | import Vue from 'vue' 6 | import App from './App.vue' 7 | import router from './router' 8 | import store from './store/' 9 | import i18n from './locales' 10 | import { VueAxios } from './utils/request' 11 | import ProLayout, { PageHeaderWrapper } from '@ant-design-vue/pro-layout' 12 | import themePluginConfig from '../config/themePluginConfig' 13 | 14 | // mock 15 | // WARNING: `mockjs` NOT SUPPORT `IE` PLEASE DO NOT USE IN `production` ENV. 16 | import './mock' 17 | 18 | import bootstrap from './core/bootstrap' 19 | import './core/lazy_use' 20 | import './permission' // permission control 21 | import './utils/filter' // global filter 22 | import './global.less' 23 | 24 | Vue.config.productionTip = false 25 | 26 | // mount axios to `Vue.$http` and `this.$http` 27 | Vue.use(VueAxios) 28 | Vue.component('pro-layout', ProLayout) 29 | Vue.component('page-header-wrapper', PageHeaderWrapper) 30 | 31 | window.umi_plugin_ant_themeVar = themePluginConfig.theme 32 | 33 | new Vue({ 34 | router, 35 | store, 36 | i18n, 37 | created: bootstrap, 38 | render: h => h(App) 39 | }).$mount('#app') 40 | -------------------------------------------------------------------------------- /frontend/src/views/list/QueryList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 51 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/MiniBar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/MiniArea.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 53 | 54 | 57 | -------------------------------------------------------------------------------- /frontend/src/views/list/components/Info.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | 58 | -------------------------------------------------------------------------------- /frontend/src/utils/screenLog.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export const printANSI = () => { 3 | // console.clear() 4 | console.log('[antd pro] created()') 5 | // ASCII - ANSI Shadow 6 | let text = ` 7 | █████╗ ███╗ ██╗████████╗██████╗ ██████╗ ██████╗ ██████╗ 8 | ██╔══██╗████╗ ██║╚══██╔══╝██╔══██╗ ██╔══██╗██╔══██╗██╔═══██╗ 9 | ███████║██╔██╗ ██║ ██║ ██║ ██║ ██████╔╝██████╔╝██║ ██║ 10 | ██╔══██║██║╚██╗██║ ██║ ██║ ██║ ██╔═══╝ ██╔══██╗██║ ██║ 11 | ██║ ██║██║ ╚████║ ██║ ██████╔╝ ██║ ██║ ██║╚██████╔╝ 12 | ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ 13 | \t\t\t\t\tPublished ${APP_VERSION}-${GIT_HASH} @ antdv.com 14 | \t\t\t\t\tBuild date: ${BUILD_DATE}` 15 | console.log(`%c${text}`, 'color: #fc4d50') 16 | console.log('%c感谢使用 antd pro!', 'color: #000; font-size: 14px; font-family: Hiragino Sans GB,Microsoft YaHei,\\\\5FAE\\8F6F\\96C5\\9ED1,Droid Sans Fallback,Source Sans,Wenquanyi Micro Hei,WenQuanYi Micro Hei Mono,WenQuanYi Zen Hei,Apple LiGothic Medium,SimHei,ST Heiti,WenQuanYi Zen Hei Sharp,sans-serif;') 17 | console.log('%cThanks for using antd pro!', 'color: #fff; font-size: 14px; font-weight: 300; text-shadow:#000 1px 0 0,#000 0 1px 0,#000 -1px 0 0,#000 0 -1px 0;') 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/NumberInfo/NumberInfo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 51 | 52 | 55 | -------------------------------------------------------------------------------- /frontend/src/components/SettingDrawer/settingConfig.js: -------------------------------------------------------------------------------- 1 | import message from 'ant-design-vue/es/message' 2 | // import defaultSettings from '../defaultSettings'; 3 | import themeColor from './themeColor.js' 4 | 5 | // let lessNodesAppended 6 | const colorList = [ 7 | { 8 | key: '薄暮', color: '#F5222D' 9 | }, 10 | { 11 | key: '火山', color: '#FA541C' 12 | }, 13 | { 14 | key: '日暮', color: '#FAAD14' 15 | }, 16 | { 17 | key: '明青', color: '#13C2C2' 18 | }, 19 | { 20 | key: '极光绿', color: '#52C41A' 21 | }, 22 | { 23 | key: '拂晓蓝(默认)', color: '#1890FF' 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 | -------------------------------------------------------------------------------- /frontend/src/components/Editor/WangEditor.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 50 | 51 | 58 | -------------------------------------------------------------------------------- /frontend/src/components/AvatarList/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @avatar-list-prefix-cls: ~"@{ant-pro-prefix}-avatar-list"; 4 | @avatar-list-item-prefix-cls: ~"@{ant-pro-prefix}-avatar-list-item"; 5 | 6 | .@{avatar-list-prefix-cls} { 7 | display: inline-block; 8 | 9 | ul { 10 | list-style: none; 11 | display: inline-block; 12 | padding: 0; 13 | margin: 0 0 0 8px; 14 | font-size: 0; 15 | } 16 | } 17 | 18 | .@{avatar-list-item-prefix-cls} { 19 | display: inline-block; 20 | font-size: @font-size-base; 21 | margin-left: -8px; 22 | width: @avatar-size-base; 23 | height: @avatar-size-base; 24 | 25 | :global { 26 | .ant-avatar { 27 | border: 1px solid #fff; 28 | cursor: pointer; 29 | } 30 | } 31 | 32 | &.large { 33 | width: @avatar-size-lg; 34 | height: @avatar-size-lg; 35 | } 36 | 37 | &.small { 38 | width: @avatar-size-sm; 39 | height: @avatar-size-sm; 40 | } 41 | 42 | &.mini { 43 | width: 20px; 44 | height: 20px; 45 | 46 | :global { 47 | .ant-avatar { 48 | width: 20px; 49 | height: 20px; 50 | line-height: 20px; 51 | 52 | .ant-avatar-string { 53 | font-size: 12px; 54 | line-height: 18px; 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/TransferBar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 65 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/Bar.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 63 | -------------------------------------------------------------------------------- /backend/public/css/user.01ff6ef1.css: -------------------------------------------------------------------------------- 1 | .step-form-wrapper[data-v-4a462ef6]{margin:0 auto;width:80%;max-width:400px}.user-layout-login label[data-v-0cea91d7]{font-size:14px}.user-layout-login .getCaptcha[data-v-0cea91d7]{display:block;width:100%;height:40px}.user-layout-login .forge-password[data-v-0cea91d7]{font-size:14px}.user-layout-login button.login-button[data-v-0cea91d7]{padding:0 15px;font-size:16px;height:40px;width:100%}.user-layout-login .user-login-other[data-v-0cea91d7]{text-align:left;margin-top:24px;line-height:22px}.user-layout-login .user-login-other .item-icon[data-v-0cea91d7]{font-size:24px;color:rgba(0,0,0,.2);margin-left:16px;vertical-align:middle;cursor:pointer;-webkit-transition:color .3s;transition:color .3s}.user-layout-login .user-login-other .item-icon[data-v-0cea91d7]:hover{color:#1890ff}.user-layout-login .user-login-other .register[data-v-0cea91d7]{float:right}.user-register.error{color:red}.user-register.warning{color:#ff7e05}.user-register.success{color:#52c41a}.user-layout-register .ant-input-group-addon:first-child{background-color:#fff}.user-layout-register>h3[data-v-2ddecc5e]{font-size:16px;margin-bottom:20px}.user-layout-register .getCaptcha[data-v-2ddecc5e]{display:block;width:100%;height:40px}.user-layout-register .register-button[data-v-2ddecc5e]{width:50%}.user-layout-register .login[data-v-2ddecc5e]{float:right;line-height:40px} -------------------------------------------------------------------------------- /frontend/src/components/NumberInfo/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @numberInfo-prefix-cls: ~"@{ant-pro-prefix}-number-info"; 4 | 5 | .@{numberInfo-prefix-cls} { 6 | 7 | .ant-pro-number-info-subtitle { 8 | color: @text-color-secondary; 9 | font-size: @font-size-base; 10 | height: 22px; 11 | line-height: 22px; 12 | overflow: hidden; 13 | text-overflow: ellipsis; 14 | word-break: break-all; 15 | white-space: nowrap; 16 | } 17 | 18 | .number-info-value { 19 | margin-top: 4px; 20 | font-size: 0; 21 | overflow: hidden; 22 | text-overflow: ellipsis; 23 | word-break: break-all; 24 | white-space: nowrap; 25 | 26 | & > span { 27 | color: @heading-color; 28 | display: inline-block; 29 | line-height: 32px; 30 | height: 32px; 31 | font-size: 24px; 32 | margin-right: 32px; 33 | } 34 | 35 | .sub-total { 36 | color: @text-color-secondary; 37 | font-size: @font-size-lg; 38 | vertical-align: top; 39 | margin-right: 0; 40 | i { 41 | font-size: 12px; 42 | transform: scale(0.82); 43 | margin-left: 4px; 44 | } 45 | :global { 46 | .anticon-caret-up { 47 | color: @red-6; 48 | } 49 | .anticon-caret-down { 50 | color: @green-6; 51 | } 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /frontend/src/components/GlobalHeader/RightContent.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 59 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/Liquid.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 64 | 65 | 68 | -------------------------------------------------------------------------------- /frontend/src/api/manage.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | const api = { 4 | user: '/user', 5 | role: '/role', 6 | service: '/service', 7 | permission: '/permission', 8 | permissionNoPager: '/permission/no-pager', 9 | orgTree: '/org/tree' 10 | } 11 | 12 | export default api 13 | 14 | export function getUserList (parameter) { 15 | return request({ 16 | url: api.user, 17 | method: 'get', 18 | params: parameter 19 | }) 20 | } 21 | 22 | export function getRoleList (parameter) { 23 | return request({ 24 | url: api.role, 25 | method: 'get', 26 | params: parameter 27 | }) 28 | } 29 | 30 | export function getServiceList (parameter) { 31 | return request({ 32 | url: api.service, 33 | method: 'get', 34 | params: parameter 35 | }) 36 | } 37 | 38 | export function getPermissions (parameter) { 39 | return request({ 40 | url: api.permissionNoPager, 41 | method: 'get', 42 | params: parameter 43 | }) 44 | } 45 | 46 | export function getOrgTree (parameter) { 47 | return request({ 48 | url: api.orgTree, 49 | method: 'get', 50 | params: parameter 51 | }) 52 | } 53 | 54 | // id == 0 add post 55 | // id != 0 update put 56 | export function saveService (parameter) { 57 | return request({ 58 | url: api.service, 59 | method: parameter.id === 0 ? 'post' : 'put', 60 | data: parameter 61 | }) 62 | } 63 | 64 | export function saveSub (sub) { 65 | return request({ 66 | url: '/sub', 67 | method: sub.id === 0 ? 'post' : 'put', 68 | data: sub 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /frontend/src/core/bootstrap.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | import storage from 'store' 3 | import { 4 | ACCESS_TOKEN, 5 | APP_LANGUAGE, 6 | TOGGLE_CONTENT_WIDTH, 7 | TOGGLE_FIXED_HEADER, 8 | TOGGLE_FIXED_SIDEBAR, TOGGLE_HIDE_HEADER, 9 | TOGGLE_LAYOUT, TOGGLE_NAV_THEME, TOGGLE_WEAK, 10 | TOGGLE_COLOR, TOGGLE_MULTI_TAB 11 | } from '@/store/mutation-types' 12 | import { printANSI } from '@/utils/screenLog' 13 | import defaultSettings from '@/config/defaultSettings' 14 | 15 | export default function Initializer () { 16 | printANSI() // 请自行移除该行. please remove this line 17 | 18 | store.commit(TOGGLE_LAYOUT, storage.get(TOGGLE_LAYOUT, defaultSettings.layout)) 19 | store.commit(TOGGLE_FIXED_HEADER, storage.get(TOGGLE_FIXED_HEADER, defaultSettings.fixedHeader)) 20 | store.commit(TOGGLE_FIXED_SIDEBAR, storage.get(TOGGLE_FIXED_SIDEBAR, defaultSettings.fixSiderbar)) 21 | store.commit(TOGGLE_CONTENT_WIDTH, storage.get(TOGGLE_CONTENT_WIDTH, defaultSettings.contentWidth)) 22 | store.commit(TOGGLE_HIDE_HEADER, storage.get(TOGGLE_HIDE_HEADER, defaultSettings.autoHideHeader)) 23 | store.commit(TOGGLE_NAV_THEME, storage.get(TOGGLE_NAV_THEME, defaultSettings.navTheme)) 24 | store.commit(TOGGLE_WEAK, storage.get(TOGGLE_WEAK, defaultSettings.colorWeak)) 25 | store.commit(TOGGLE_COLOR, storage.get(TOGGLE_COLOR, defaultSettings.primaryColor)) 26 | store.commit(TOGGLE_MULTI_TAB, storage.get(TOGGLE_MULTI_TAB, defaultSettings.multiTab)) 27 | store.commit('SET_TOKEN', storage.get(ACCESS_TOKEN)) 28 | 29 | store.dispatch('setLang', storage.get(APP_LANGUAGE, 'en-US')) 30 | // last step 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/locales/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import storage from 'store' 4 | import moment from 'moment' 5 | 6 | // default lang 7 | import enUS from './lang/en-US' 8 | 9 | Vue.use(VueI18n) 10 | 11 | export const defaultLang = 'en-US' 12 | 13 | const messages = { 14 | 'en-US': { 15 | ...enUS 16 | } 17 | } 18 | 19 | const i18n = new VueI18n({ 20 | silentTranslationWarn: true, 21 | locale: defaultLang, 22 | fallbackLocale: defaultLang, 23 | messages 24 | }) 25 | 26 | const loadedLanguages = [defaultLang] 27 | 28 | function setI18nLanguage (lang) { 29 | i18n.locale = lang 30 | // request.headers['Accept-Language'] = lang 31 | document.querySelector('html').setAttribute('lang', lang) 32 | return lang 33 | } 34 | 35 | export function loadLanguageAsync (lang = defaultLang) { 36 | return new Promise(resolve => { 37 | // 缓存语言设置 38 | storage.set('lang', lang) 39 | if (i18n.locale !== lang) { 40 | if (!loadedLanguages.includes(lang)) { 41 | return import(/* webpackChunkName: "lang-[request]" */ `./lang/${lang}`).then(msg => { 42 | const locale = msg.default 43 | i18n.setLocaleMessage(lang, locale) 44 | loadedLanguages.push(lang) 45 | moment.updateLocale(locale.momentName, locale.momentLocale) 46 | return setI18nLanguage(lang) 47 | }) 48 | } 49 | return resolve(setI18nLanguage(lang)) 50 | } 51 | return resolve(lang) 52 | }) 53 | } 54 | 55 | export function i18nRender (key) { 56 | return i18n.t(`${key}`) 57 | } 58 | 59 | export default i18n 60 | -------------------------------------------------------------------------------- /frontend/src/utils/helper/permission.js: -------------------------------------------------------------------------------- 1 | export const PERMISSION_ENUM = { 2 | 'add': { key: 'add', label: '新增' }, 3 | 'delete': { key: 'delete', label: '删除' }, 4 | 'edit': { key: 'edit', label: '修改' }, 5 | 'query': { key: 'query', label: '查询' }, 6 | 'get': { key: 'get', label: '详情' }, 7 | 'enable': { key: 'enable', label: '启用' }, 8 | 'disable': { key: 'disable', label: '禁用' }, 9 | 'import': { key: 'import', label: '导入' }, 10 | 'export': { key: 'export', label: '导出' } 11 | } 12 | 13 | function plugin (Vue) { 14 | if (plugin.installed) { 15 | return 16 | } 17 | 18 | !Vue.prototype.$auth && Object.defineProperties(Vue.prototype, { 19 | $auth: { 20 | get () { 21 | const _this = this 22 | return (permissions) => { 23 | const [permission, action] = permissions.split('.') 24 | const permissionList = _this.$store.getters.roles.permissions 25 | return permissionList.find((val) => { 26 | return val.permissionId === permission 27 | }).actionList.findIndex((val) => { 28 | return val === action 29 | }) > -1 30 | } 31 | } 32 | } 33 | }) 34 | 35 | !Vue.prototype.$enum && Object.defineProperties(Vue.prototype, { 36 | $enum: { 37 | get () { 38 | // const _this = this; 39 | return (val) => { 40 | let result = PERMISSION_ENUM 41 | val && val.split('.').forEach(v => { 42 | result = result && result[v] || null 43 | }) 44 | return result 45 | } 46 | } 47 | } 48 | }) 49 | } 50 | 51 | export default plugin 52 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/Radar.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 65 | 66 | 69 | -------------------------------------------------------------------------------- /frontend/src/views/form/stepForm/StepForm.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 60 | 61 | 67 | -------------------------------------------------------------------------------- /frontend/src/components/SelectLang/index.jsx: -------------------------------------------------------------------------------- 1 | import './index.less' 2 | 3 | import { Icon, Menu, Dropdown } from 'ant-design-vue' 4 | import { i18nRender } from '@/locales' 5 | import i18nMixin from '@/store/i18n-mixin' 6 | 7 | const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'] 8 | const languageLabels = { 9 | 'zh-CN': '简体中文', 10 | 'zh-TW': '繁体中文', 11 | 'en-US': 'English', 12 | 'pt-BR': 'Português' 13 | } 14 | // eslint-disable-next-line 15 | const languageIcons = { 16 | 'zh-CN': '🇨🇳', 17 | 'zh-TW': '🇭🇰', 18 | 'en-US': '🇺🇸', 19 | 'pt-BR': '🇧🇷' 20 | } 21 | 22 | const SelectLang = { 23 | props: { 24 | prefixCls: { 25 | type: String, 26 | default: 'ant-pro-drop-down' 27 | } 28 | }, 29 | name: 'SelectLang', 30 | mixins: [i18nMixin], 31 | render () { 32 | const { prefixCls } = this 33 | const changeLang = ({ key }) => { 34 | this.setLang(key) 35 | } 36 | const langMenu = ( 37 | 38 | {locales.map(locale => ( 39 | 40 | 41 | {languageIcons[locale]} 42 | {' '} 43 | {languageLabels[locale]} 44 | 45 | ))} 46 | 47 | ) 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | } 57 | 58 | export default SelectLang 59 | -------------------------------------------------------------------------------- /backend/public/js/lang-zh-CN.ddc47f61.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["lang-zh-CN"],{2807:function(e,a,o){"use strict";o.r(a);var t=o("5530"),l=o("3579"),n=o("41b2"),r=o.n(n),c={today:"今天",now:"此刻",backToToday:"返回今天",ok:"确定",timeSelect:"选择时间",dateSelect:"选择日期",weekSelect:"选择周",clear:"清除",month:"月",year:"年",previousMonth:"上个月 (翻页上键)",nextMonth:"下个月 (翻页下键)",monthSelect:"选择月份",yearSelect:"选择年份",decadeSelect:"选择年代",yearFormat:"YYYY年",dayFormat:"D日",dateFormat:"YYYY年M月D日",dateTimeFormat:"YYYY年M月D日 HH时mm分ss秒",previousYear:"上一年 (Control键加左方向键)",nextYear:"下一年 (Control键加右方向键)",previousDecade:"上一年代",nextDecade:"下一年代",previousCentury:"上一世纪",nextCentury:"下一世纪"},i={placeholder:"请选择时间"},d=i,m={lang:r()({placeholder:"请选择日期",rangePlaceholder:["开始日期","结束日期"]},c),timePickerLocale:r()({},d)};m.lang.ok="确 定";var s=m,p=s,h={locale:"zh-cn",Pagination:l["a"],DatePicker:s,TimePicker:d,Calendar:p,global:{placeholder:"请选择"},Table:{filterTitle:"筛选",filterConfirm:"确定",filterReset:"重置",selectAll:"全选当页",selectInvert:"反选当页",sortTitle:"排序",expand:"展开行",collapse:"关闭行"},Modal:{okText:"确定",cancelText:"取消",justOkText:"知道了"},Popconfirm:{cancelText:"取消",okText:"确定"},Transfer:{searchPlaceholder:"请输入搜索内容",itemUnit:"项",itemsUnit:"项"},Upload:{uploading:"文件上传中",removeFile:"删除文件",uploadError:"上传错误",previewFile:"预览文件",downloadFile:"下载文件"},Empty:{description:"暂无数据"},Icon:{icon:"图标"},Text:{edit:"编辑",copy:"复制",copied:"复制成功",expand:"展开"},PageHeader:{back:"返回"}},u=h,k=o("5c3a"),T=o.n(k),Y={antLocale:u,momentName:"zh-cn",momentLocale:T.a},b={message:"-","menu.home":"主页","menu.hello":"问候","menu.dashboard":"仪表盘","menu.dashboard.analysis":"分析页","menu.dashboard.monitor":"监控页","menu.dashboard.workplace":"工作台"};a["default"]=Object(t["a"])(Object(t["a"])({},Y),b)}}]); -------------------------------------------------------------------------------- /frontend/src/mock/services/auth.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs2' 2 | import { builder, getBody } from '../util' 3 | 4 | const username = ['admin', 'super'] 5 | // 强硬要求 ant.design 相同密码 6 | // '21232f297a57a5a743894a0e4a801fc3', 7 | const password = ['8914de686ab28dc22f30d3d8e107ff6c', '21232f297a57a5a743894a0e4a801fc3'] // admin, ant.design 8 | 9 | const login = (options) => { 10 | const body = getBody(options) 11 | console.log('mock: body', body) 12 | if (!username.includes(body.username) || !password.includes(body.password)) { 13 | return builder({ isLogin: true }, '账户或密码错误', 401) 14 | } 15 | 16 | return builder({ 17 | 'id': Mock.mock('@guid'), 18 | 'name': Mock.mock('@name'), 19 | 'username': 'admin', 20 | 'password': '', 21 | 'avatar': 'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png', 22 | 'status': 1, 23 | 'telephone': '', 24 | 'lastLoginIp': '27.154.74.117', 25 | 'lastLoginTime': 1534837621348, 26 | 'creatorId': 'admin', 27 | 'createTime': 1497160610259, 28 | 'deleted': 0, 29 | 'roleId': 'admin', 30 | 'lang': 'zh-CN', 31 | 'token': '4291d7da9005377ec9aec4a71ea837f' 32 | }, '', 200, { 'Custom-Header': Mock.mock('@guid') }) 33 | } 34 | 35 | const logout = () => { 36 | return builder({}, '[测试接口] 注销成功') 37 | } 38 | 39 | const smsCaptcha = () => { 40 | return builder({ captcha: Mock.mock('@integer(10000, 99999)') }) 41 | } 42 | 43 | const twofactor = () => { 44 | return builder({ stepCode: Mock.mock('@integer(0, 1)') }) 45 | } 46 | 47 | Mock.mock(/\/auth\/login/, 'post', login) 48 | Mock.mock(/\/auth\/logout/, 'post', logout) 49 | Mock.mock(/\/account\/sms/, 'post', smsCaptcha) 50 | Mock.mock(/\/auth\/2step-code/, 'post', twofactor) 51 | -------------------------------------------------------------------------------- /frontend/src/components/Ellipsis/Ellipsis.vue: -------------------------------------------------------------------------------- 1 | 65 | -------------------------------------------------------------------------------- /frontend/src/locales/lang/en-US.js: -------------------------------------------------------------------------------- 1 | import antdEnUS from 'ant-design-vue/es/locale-provider/en_US' 2 | import momentEU from 'moment/locale/eu' 3 | 4 | const components = { 5 | antLocale: antdEnUS, 6 | momentName: 'eu', 7 | momentLocale: momentEU 8 | } 9 | 10 | const locale = { 11 | message: '-', 12 | 'menu.home': 'Home', 13 | 'menu.dashboard': 'Dashboard', 14 | 'menu.hello': 'Hello', 15 | 'menu.dashboard.analysis': 'Analysis', 16 | 'menu.dashboard.monitor': 'Monitor', 17 | 'menu.dashboard.workplace': 'Workplace', 18 | 19 | 'layouts.usermenu.dialog.title': 'Message', 20 | 'layouts.usermenu.dialog.content': 'Do you really log-out.', 21 | 22 | 'app.setting.pagestyle': 'Page style setting', 23 | 'app.setting.pagestyle.light': 'Light style', 24 | 'app.setting.pagestyle.dark': 'Dark style', 25 | 'app.setting.pagestyle.realdark': 'RealDark style', 26 | 'app.setting.themecolor': 'Theme Color', 27 | 'app.setting.navigationmode': 'Navigation Mode', 28 | 'app.setting.content-width': 'Content Width', 29 | 'app.setting.fixedheader': 'Fixed Header', 30 | 'app.setting.fixedsidebar': 'Fixed Sidebar', 31 | 'app.setting.sidemenu': 'Side Menu Layout', 32 | 'app.setting.topmenu': 'Top Menu Layout', 33 | 'app.setting.content-width.fixed': 'Fixed', 34 | 'app.setting.content-width.fluid': 'Fluid', 35 | 'app.setting.othersettings': 'Other Settings', 36 | 'app.setting.weakmode': 'Weak Mode', 37 | 'app.setting.copy': 'Copy Setting', 38 | 'app.setting.loading': 'Loading theme', 39 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js', 40 | 'app.setting.production.hint': 'Setting panel shows in development environment only, please manually modify' 41 | } 42 | 43 | export default { 44 | ...components, 45 | ...locale 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/api/login.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | const userApi = { 4 | Login: '/auth/login', 5 | Logout: '/auth/logout', 6 | ForgePassword: '/auth/forge-password', 7 | Register: '/auth/register', 8 | twoStepCode: '/auth/2step-code', 9 | SendSms: '/account/sms', 10 | SendSmsErr: '/account/sms_err', 11 | // get my info 12 | UserInfo: '/user/info', 13 | UserMenu: '/user/nav' 14 | } 15 | 16 | /** 17 | * login func 18 | * parameter: { 19 | * username: '', 20 | * password: '', 21 | * remember_me: true, 22 | * captcha: '12345' 23 | * } 24 | * @param parameter 25 | * @returns {*} 26 | */ 27 | export function login (parameter) { 28 | return request({ 29 | url: userApi.Login, 30 | method: 'post', 31 | data: parameter 32 | }) 33 | } 34 | 35 | export function getSmsCaptcha (parameter) { 36 | return request({ 37 | url: userApi.SendSms, 38 | method: 'post', 39 | data: parameter 40 | }) 41 | } 42 | 43 | export function getInfo () { 44 | return request({ 45 | url: userApi.UserInfo, 46 | method: 'get', 47 | headers: { 48 | 'Content-Type': 'application/json;charset=UTF-8' 49 | } 50 | }) 51 | } 52 | 53 | export function getCurrentUserNav () { 54 | return request({ 55 | url: userApi.UserMenu, 56 | method: 'get' 57 | }) 58 | } 59 | 60 | export function logout () { 61 | return request({ 62 | url: userApi.Logout, 63 | method: 'post', 64 | headers: { 65 | 'Content-Type': 'application/json;charset=UTF-8' 66 | } 67 | }) 68 | } 69 | 70 | /** 71 | * get user 2step code open? 72 | * @param parameter {*} 73 | */ 74 | export function get2step (parameter) { 75 | return request({ 76 | url: userApi.twoStepCode, 77 | method: 'post', 78 | data: parameter 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/MiniProgress.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | 76 | -------------------------------------------------------------------------------- /frontend/src/components/index.js: -------------------------------------------------------------------------------- 1 | // chart 2 | import Bar from '@/components/Charts/Bar' 3 | import ChartCard from '@/components/Charts/ChartCard' 4 | import Liquid from '@/components/Charts/Liquid' 5 | import MiniArea from '@/components/Charts/MiniArea' 6 | import MiniSmoothArea from '@/components/Charts/MiniSmoothArea' 7 | import MiniBar from '@/components/Charts/MiniBar' 8 | import MiniProgress from '@/components/Charts/MiniProgress' 9 | import Radar from '@/components/Charts/Radar' 10 | import RankList from '@/components/Charts/RankList' 11 | import TransferBar from '@/components/Charts/TransferBar' 12 | import TagCloud from '@/components/Charts/TagCloud' 13 | 14 | // pro components 15 | import AvatarList from '@/components/AvatarList' 16 | import Ellipsis from '@/components/Ellipsis' 17 | import FooterToolbar from '@/components/FooterToolbar' 18 | import NumberInfo from '@/components/NumberInfo' 19 | import Tree from '@/components/Tree/Tree' 20 | import Trend from '@/components/Trend' 21 | import STable from '@/components/Table' 22 | import MultiTab from '@/components/MultiTab' 23 | import IconSelector from '@/components/IconSelector' 24 | import TagSelect from '@/components/TagSelect' 25 | import StandardFormRow from '@/components/StandardFormRow' 26 | import ArticleListContent from '@/components/ArticleListContent' 27 | 28 | import Dialog from '@/components/Dialog' 29 | 30 | export { 31 | AvatarList, 32 | Bar, 33 | ChartCard, 34 | Liquid, 35 | MiniArea, 36 | MiniSmoothArea, 37 | MiniBar, 38 | MiniProgress, 39 | Radar, 40 | TagCloud, 41 | RankList, 42 | TransferBar, 43 | Trend, 44 | Ellipsis, 45 | FooterToolbar, 46 | NumberInfo, 47 | Tree, 48 | STable, 49 | MultiTab, 50 | IconSelector, 51 | TagSelect, 52 | StandardFormRow, 53 | ArticleListContent, 54 | 55 | Dialog 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/RankList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | 31 | 78 | -------------------------------------------------------------------------------- /frontend/src/global.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/es/style/themes/default.less'; 2 | 3 | html, 4 | body, 5 | #app, #root { 6 | height: 100%; 7 | } 8 | 9 | .colorWeak { 10 | filter: invert(80%); 11 | } 12 | 13 | .ant-layout.layout-basic { 14 | height: 100vh; 15 | min-height: 100vh; 16 | } 17 | 18 | canvas { 19 | display: block; 20 | } 21 | 22 | body { 23 | text-rendering: optimizeLegibility; 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | ul, 29 | ol { 30 | list-style: none; 31 | } 32 | 33 | // 数据列表 样式 34 | .table-alert { 35 | margin-bottom: 16px; 36 | } 37 | // 数据列表 操作 38 | .table-operator { 39 | margin-bottom: 18px; 40 | 41 | button { 42 | margin-right: 8px; 43 | } 44 | } 45 | // 数据列表 搜索条件 46 | .table-page-search-wrapper { 47 | 48 | .ant-form-inline { 49 | .ant-form-item { 50 | display: flex; 51 | margin-bottom: 24px; 52 | margin-right: 0; 53 | 54 | .ant-form-item-control-wrapper { 55 | flex: 1 1; 56 | display: inline-block; 57 | vertical-align: middle; 58 | } 59 | 60 | > .ant-form-item-label { 61 | line-height: 32px; 62 | padding-right: 8px; 63 | width: auto; 64 | } 65 | .ant-form-item-control { 66 | height: 32px; 67 | line-height: 32px; 68 | } 69 | } 70 | } 71 | 72 | .table-page-search-submitButtons { 73 | display: block; 74 | margin-bottom: 24px; 75 | white-space: nowrap; 76 | } 77 | } 78 | 79 | @media (max-width: @screen-xs) { 80 | .ant-table { 81 | width: 100%; 82 | overflow-x: auto; 83 | &-thead > tr, 84 | &-tbody > tr { 85 | > th, 86 | > td { 87 | white-space: pre; 88 | > span { 89 | display: block; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /frontend/src/views/account/settings/Security.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/bx-analyse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/Search/GlobalSearch.jsx: -------------------------------------------------------------------------------- 1 | import { Select } from 'ant-design-vue' 2 | import './index.less' 3 | 4 | const GlobalSearch = { 5 | name: 'GlobalSearch', 6 | data () { 7 | return { 8 | visible: false 9 | } 10 | }, 11 | mounted () { 12 | const keyboardHandle = (e) => { 13 | e.preventDefault() 14 | e.stopPropagation() 15 | const { ctrlKey, shiftKey, altKey, keyCode } = e 16 | console.log('keyCode:', e.keyCode, e) 17 | // key is `K` and hold ctrl 18 | if (keyCode === 75 && ctrlKey && !shiftKey && !altKey) { 19 | this.visible = !this.visible 20 | } 21 | } 22 | document.addEventListener('keydown', keyboardHandle) 23 | }, 24 | render () { 25 | const { visible } = this 26 | const handleSearch = (e) => { 27 | this.$emit('search', e) 28 | } 29 | 30 | const handleChange = (e) => { 31 | this.$emit('change', e) 32 | } 33 | if (!visible) { 34 | return null 35 | } 36 | return ( 37 | 55 | ) 56 | } 57 | } 58 | 59 | GlobalSearch.install = function (Vue) { 60 | Vue.component(GlobalSearch.name, GlobalSearch) 61 | } 62 | 63 | export default GlobalSearch 64 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/Trend.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 52 | 53 | 83 | -------------------------------------------------------------------------------- /frontend/src/views/form/stepForm/Step3.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 50 | 65 | -------------------------------------------------------------------------------- /frontend/src/components/NProgress/nprogress.less: -------------------------------------------------------------------------------- 1 | @import url('../index.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 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ['plugin:vue/strongly-recommended', '@vue/standard'], 7 | rules: { 8 | 'comma-dangle': 'off', 9 | 'space-before-function-paren': 'off', 10 | 'no-console': 'off', 11 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'generator-star-spacing': 'off', 13 | 'no-mixed-operators': 0, 14 | 'vue/max-attributes-per-line': [ 15 | 2, 16 | { 17 | singleline: 5, 18 | multiline: { 19 | max: 1, 20 | allowFirstLine: false 21 | } 22 | } 23 | ], 24 | 'vue/attribute-hyphenation': 0, 25 | 'vue/html-self-closing': 0, 26 | 'vue/component-name-in-template-casing': 0, 27 | 'vue/html-closing-bracket-spacing': 0, 28 | 'vue/singleline-html-element-content-newline': 0, 29 | 'vue/no-unused-components': 0, 30 | 'vue/multiline-html-element-content-newline': 0, 31 | 'vue/no-use-v-if-with-v-for': 0, 32 | 'vue/html-closing-bracket-newline': 0, 33 | 'vue/no-parsing-error': 0, 34 | 'no-tabs': 0, 35 | quotes: [ 36 | 2, 37 | 'single', 38 | { 39 | avoidEscape: true, 40 | allowTemplateLiterals: true 41 | } 42 | ], 43 | semi: [ 44 | 2, 45 | 'never', 46 | { 47 | beforeStatementContinuationChars: 'never' 48 | } 49 | ], 50 | 'no-delete-var': 2, 51 | 'prefer-const': [ 52 | 2, 53 | { 54 | ignoreReadBeforeAssign: false 55 | } 56 | ], 57 | 'template-curly-spacing': 'off', 58 | indent: 'off' 59 | }, 60 | parserOptions: { 61 | parser: 'babel-eslint' 62 | }, 63 | overrides: [ 64 | { 65 | files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'], 66 | env: { 67 | jest: true 68 | } 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/components/TextArea/index.jsx: -------------------------------------------------------------------------------- 1 | import './style.less' 2 | import { getStrFullLength, cutStrByFullLength } from '../_util/util' 3 | import Input from 'ant-design-vue/es/input' 4 | const TextArea = Input.TextArea 5 | 6 | export default { 7 | name: 'LimitTextArea', 8 | model: { 9 | prop: 'value', 10 | event: 'change' 11 | }, 12 | props: Object.assign({}, TextArea.props, { 13 | prefixCls: { 14 | type: String, 15 | default: 'ant-textarea-limit' 16 | }, 17 | // eslint-disable-next-line 18 | value: { 19 | type: String 20 | }, 21 | limit: { 22 | type: Number, 23 | default: 200 24 | } 25 | }), 26 | data () { 27 | return { 28 | currentLimit: 0 29 | } 30 | }, 31 | watch: { 32 | value (val) { 33 | this.calcLimitNum(val) 34 | } 35 | }, 36 | created () { 37 | this.calcLimitNum(this.value) 38 | }, 39 | methods: { 40 | handleChange (e) { 41 | const value = e.target.value 42 | const len = getStrFullLength(value) 43 | if (len <= this.limit) { 44 | this.currentLimit = len 45 | this.$emit('change', value) 46 | return 47 | } else { 48 | const str = cutStrByFullLength(value, this.limit) 49 | this.currentLimit = getStrFullLength(str) 50 | this.$emit('change', str) 51 | } 52 | console.error('limit out! currentLimit:', this.currentLimit) 53 | }, 54 | calcLimitNum (val) { 55 | const len = getStrFullLength(val) 56 | this.currentLimit = len 57 | } 58 | }, 59 | render () { 60 | const { prefixCls, ...props } = this.$props 61 | return ( 62 |
63 | 65 | {this.currentLimit}/{this.limit} 66 |
67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /frontend/src/views/account/settings/Custom.vue: -------------------------------------------------------------------------------- 1 | 32 | 69 | -------------------------------------------------------------------------------- /frontend/src/views/list/modules/CreateForm.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 73 | -------------------------------------------------------------------------------- /frontend/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 | export function handleScrollHeader (callback) { 24 | let timer = 0 25 | 26 | let beforeScrollTop = window.pageYOffset 27 | callback = callback || function () {} 28 | window.addEventListener( 29 | 'scroll', 30 | event => { 31 | clearTimeout(timer) 32 | timer = setTimeout(() => { 33 | let direction = 'up' 34 | const afterScrollTop = window.pageYOffset 35 | const delta = afterScrollTop - beforeScrollTop 36 | if (delta === 0) { 37 | return false 38 | } 39 | direction = delta > 0 ? 'down' : 'up' 40 | callback(direction) 41 | beforeScrollTop = afterScrollTop 42 | }, 50) 43 | }, 44 | false 45 | ) 46 | } 47 | 48 | export function isIE () { 49 | const bw = window.navigator.userAgent 50 | const compare = (s) => bw.indexOf(s) >= 0 51 | const ie11 = (() => 'ActiveXObject' in window)() 52 | return compare('MSIE') || ie11 53 | } 54 | 55 | /** 56 | * Remove loading animate 57 | * @param id parent element id or class 58 | * @param timeout 59 | */ 60 | export function removeLoadingAnimate (id = '', timeout = 1500) { 61 | if (id === '') { 62 | return 63 | } 64 | setTimeout(() => { 65 | document.body.removeChild(document.getElementById(id)) 66 | }, timeout) 67 | } 68 | -------------------------------------------------------------------------------- /frontend/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import store from '@/store' 3 | import storage from 'store' 4 | import notification from 'ant-design-vue/es/notification' 5 | import { VueAxios } from './axios' 6 | import { ACCESS_TOKEN } from '@/store/mutation-types' 7 | 8 | // 创建 axios 实例 9 | const request = axios.create({ 10 | // API 请求的默认前缀 11 | baseURL: process.env.VUE_APP_API_BASE_URL, 12 | timeout: 6000 // 请求超时时间 13 | }) 14 | 15 | // 异常拦截处理器 16 | const errorHandler = (error) => { 17 | if (error.response) { 18 | const data = error.response.data 19 | // 从 localstorage 获取 token 20 | const token = storage.get(ACCESS_TOKEN) 21 | if (error.response.status === 403) { 22 | notification.error({ 23 | message: 'Forbidden', 24 | description: data.message 25 | }) 26 | } 27 | if (error.response.status === 401 && !(data.result && data.result.isLogin)) { 28 | notification.error({ 29 | message: 'Unauthorized', 30 | description: 'Authorization verification failed' 31 | }) 32 | if (token) { 33 | store.dispatch('Logout').then(() => { 34 | setTimeout(() => { 35 | window.location.reload() 36 | }, 1500) 37 | }) 38 | } 39 | } 40 | } 41 | return Promise.reject(error) 42 | } 43 | 44 | // request interceptor 45 | request.interceptors.request.use(config => { 46 | const token = storage.get(ACCESS_TOKEN) 47 | // 如果 token 存在 48 | // 让每个请求携带自定义 token 请根据实际情况自行修改 49 | if (token) { 50 | config.headers['Access-Token'] = token 51 | } 52 | return config 53 | }, errorHandler) 54 | 55 | // response interceptor 56 | request.interceptors.response.use((response) => { 57 | return response.data 58 | }, errorHandler) 59 | 60 | const installer = { 61 | vm: {}, 62 | install (Vue) { 63 | Vue.use(VueAxios, request) 64 | } 65 | } 66 | 67 | export default request 68 | 69 | export { 70 | installer as VueAxios, 71 | request as axios 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/config/router.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import { UserLayout, BasicLayout, BlankLayout } from '@/layouts' 3 | // import { bxAnaalyse } from '@/core/icons' 4 | import HelloWorld from '@/views/helloworld/HelloWorld' 5 | // const RouteView = { 6 | // name: 'RouteView', 7 | // render: h => h('router-view') 8 | // } 9 | 10 | export const asyncRouterMap = [ 11 | { 12 | path: '/', 13 | name: 'index', 14 | component: BasicLayout, 15 | meta: { title: 'menu.home' }, 16 | redirect: '/helloworld', 17 | children: [ 18 | // helloworld 19 | { 20 | path: '/helloworld', 21 | name: 'hello-world', 22 | component: HelloWorld, 23 | meta: { 24 | title: 'menu.hello', 25 | icon: 'folder', 26 | permission: ['dashboard'] 27 | } 28 | } 29 | ] 30 | }, 31 | { 32 | path: '*', 33 | redirect: '/404', 34 | hidden: true 35 | } 36 | ] 37 | 38 | /** 39 | * 基础路由 40 | * @type { *[] } 41 | */ 42 | export const constantRouterMap = [ 43 | { 44 | path: '/user', 45 | component: UserLayout, 46 | redirect: '/user/login', 47 | hidden: true, 48 | children: [ 49 | { 50 | path: 'login', 51 | name: 'login', 52 | component: () => import(/* webpackChunkName: "user" */ '@/views/user/Login') 53 | }, 54 | { 55 | path: 'register', 56 | name: 'register', 57 | component: () => import(/* webpackChunkName: "user" */ '@/views/user/Register') 58 | }, 59 | { 60 | path: 'register-result', 61 | name: 'registerResult', 62 | component: () => import(/* webpackChunkName: "user" */ '@/views/user/RegisterResult') 63 | }, 64 | { 65 | path: 'recover', 66 | name: 'recover', 67 | component: undefined 68 | } 69 | ] 70 | }, 71 | 72 | { 73 | path: '/404', 74 | component: () => import(/* webpackChunkName: "fail" */ '@/views/exception/404') 75 | } 76 | ] 77 | -------------------------------------------------------------------------------- /frontend/src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { asyncRouterMap, constantRouterMap } from '@/config/router.config' 2 | 3 | /** 4 | * 过滤账户是否拥有某一个权限,并将菜单从加载列表移除 5 | * 6 | * @param permission 7 | * @param route 8 | * @returns {boolean} 9 | */ 10 | function hasPermission (permission, route) { 11 | if (route.meta && route.meta.permission) { 12 | let flag = false 13 | for (let i = 0, len = permission.length; i < len; i++) { 14 | flag = route.meta.permission.includes(permission[i]) 15 | if (flag) { 16 | return true 17 | } 18 | } 19 | return false 20 | } 21 | return true 22 | } 23 | 24 | /** 25 | * 单账户多角色时,使用该方法可过滤角色不存在的菜单 26 | * 27 | * @param roles 28 | * @param route 29 | * @returns {*} 30 | */ 31 | // eslint-disable-next-line 32 | function hasRole(roles, route) { 33 | if (route.meta && route.meta.roles) { 34 | return route.meta.roles.includes(roles.id) 35 | } else { 36 | return true 37 | } 38 | } 39 | 40 | function filterAsyncRouter (routerMap, roles) { 41 | const accessedRouters = routerMap.filter(route => { 42 | if (hasPermission(roles.permissionList, route)) { 43 | if (route.children && route.children.length) { 44 | route.children = filterAsyncRouter(route.children, roles) 45 | } 46 | return true 47 | } 48 | return false 49 | }) 50 | return accessedRouters 51 | } 52 | 53 | const permission = { 54 | state: { 55 | routers: constantRouterMap, 56 | addRouters: [] 57 | }, 58 | mutations: { 59 | SET_ROUTERS: (state, routers) => { 60 | state.addRouters = routers 61 | state.routers = constantRouterMap.concat(routers) 62 | } 63 | }, 64 | actions: { 65 | GenerateRoutes ({ commit }, data) { 66 | return new Promise(resolve => { 67 | const { roles } = data 68 | const accessedRouters = filterAsyncRouter(asyncRouterMap, roles) 69 | commit('SET_ROUTERS', accessedRouters) 70 | resolve() 71 | }) 72 | } 73 | } 74 | } 75 | 76 | export default permission 77 | -------------------------------------------------------------------------------- /frontend/src/views/list/search/SearchLayout.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 73 | 74 | 80 | -------------------------------------------------------------------------------- /frontend/src/components/Editor/QuillEditor.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 69 | 70 | 84 | -------------------------------------------------------------------------------- /frontend/src/components/ArticleListContent/ArticleListContent.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 47 | 48 | 90 | -------------------------------------------------------------------------------- /frontend/src/components/AvatarList/List.jsx: -------------------------------------------------------------------------------- 1 | import './index.less' 2 | 3 | import PropTypes from 'ant-design-vue/es/_util/vue-types' 4 | import Avatar from 'ant-design-vue/es/avatar' 5 | import Item from './Item.jsx' 6 | import { filterEmpty } from '@/components/_util/util' 7 | 8 | /** 9 | * size: `number`、 `large`、`small`、`default` 默认值: default 10 | * maxLength: number 11 | * excessItemsStyle: CSSProperties 12 | */ 13 | const AvatarListProps = { 14 | prefixCls: PropTypes.string.def('ant-pro-avatar-list'), 15 | size: { 16 | validator: val => { 17 | return typeof val === 'number' || ['small', 'large', 'default'].includes(val) 18 | }, 19 | default: 'default' 20 | }, 21 | maxLength: PropTypes.number.def(0), 22 | excessItemsStyle: PropTypes.object.def({ 23 | color: '#f56a00', 24 | backgroundColor: '#fde3cf' 25 | }) 26 | } 27 | 28 | const AvatarList = { 29 | __ANT_AVATAR_LIST: true, 30 | Item, 31 | name: 'AvatarList', 32 | props: AvatarListProps, 33 | render (h) { 34 | const { prefixCls, size } = this.$props 35 | const className = { 36 | [`${prefixCls}`]: true, 37 | [`${size}`]: true 38 | } 39 | const items = filterEmpty(this.$slots.default) 40 | const itemsDom = items && items.length ?
    {this.getItems(items)}
: null 41 | 42 | return ( 43 |
44 | {itemsDom} 45 |
46 | ) 47 | }, 48 | methods: { 49 | getItems (items) { 50 | const className = { 51 | [`${this.prefixCls}-item`]: true, 52 | [`${this.size}`]: true 53 | } 54 | const totalSize = items.length 55 | 56 | if (this.maxLength > 0) { 57 | items = items.slice(0, this.maxLength) 58 | items.push(({`+${totalSize - this.maxLength}`})) 59 | } 60 | return items.map((item) => ( 61 |
  • {item}
  • 62 | )) 63 | } 64 | } 65 | } 66 | 67 | AvatarList.install = function (Vue) { 68 | Vue.component(AvatarList.name, AvatarList) 69 | Vue.component(AvatarList.Item.name, AvatarList.Item) 70 | } 71 | 72 | export default AvatarList 73 | -------------------------------------------------------------------------------- /frontend/src/components/IconSelector/IconSelector.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 62 | 63 | 87 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-antd-pro", 3 | "version": "3.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:unit": "vue-cli-service test:unit", 9 | "lint": "vue-cli-service lint", 10 | "build:preview": "vue-cli-service build --mode preview", 11 | "lint:nofix": "vue-cli-service lint --no-fix" 12 | }, 13 | "dependencies": { 14 | "@ant-design-vue/pro-layout": "^0.3.12", 15 | "@antv/data-set": "^0.10.2", 16 | "ant-design-vue": "^1.6.2", 17 | "axios": "^0.19.0", 18 | "core-js": "^3.1.2", 19 | "enquire.js": "^2.1.6", 20 | "lodash.clonedeep": "^4.5.0", 21 | "lodash.get": "^4.4.2", 22 | "lodash.pick": "^4.4.0", 23 | "md5": "^2.2.1", 24 | "mockjs2": "1.0.8", 25 | "moment": "^2.24.0", 26 | "nprogress": "^0.2.0", 27 | "store": "^2.0.12", 28 | "viser-vue": "^2.4.6", 29 | "vue": "^2.6.10", 30 | "vue-clipboard2": "^0.2.1", 31 | "vue-cropper": "0.4.9", 32 | "vue-i18n": "^8.17.4", 33 | "vue-quill-editor": "^3.0.6", 34 | "vue-router": "^3.1.2", 35 | "vue-svg-component-runtime": "^1.0.1", 36 | "vuex": "^3.1.1", 37 | "wangeditor": "^3.1.1" 38 | }, 39 | "devDependencies": { 40 | "@ant-design/colors": "^3.2.1", 41 | "@vue/cli-plugin-babel": "^4.0.4", 42 | "@vue/cli-plugin-eslint": "^4.0.4", 43 | "@vue/cli-plugin-router": "^4.0.4", 44 | "@vue/cli-plugin-unit-jest": "^4.0.4", 45 | "@vue/cli-plugin-vuex": "^4.0.4", 46 | "@vue/cli-service": "^4.0.4", 47 | "@vue/eslint-config-standard": "^4.0.0", 48 | "@vue/test-utils": "^1.0.0-beta.29", 49 | "babel-eslint": "^10.0.1", 50 | "babel-plugin-import": "^1.12.2", 51 | "babel-plugin-transform-remove-console": "^6.9.4", 52 | "eslint": "^5.16.0", 53 | "eslint-plugin-html": "^5.0.0", 54 | "eslint-plugin-vue": "^5.2.3", 55 | "git-revision-webpack-plugin": "^3.0.6", 56 | "less": "^3.0.4", 57 | "less-loader": "^5.0.0", 58 | "opencollective": "^1.0.3", 59 | "opencollective-postinstall": "^2.0.2", 60 | "vue-svg-icon-loader": "^2.1.1", 61 | "vue-template-compiler": "^2.6.10", 62 | "webpack-theme-color-replacer": "^1.3.12" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/views/account/center/page/Article.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 72 | 73 | 76 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/Workplace.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/es/style/themes/default.less'; 2 | 3 | .text-overflow() { 4 | overflow: hidden; 5 | white-space: nowrap; 6 | text-overflow: ellipsis; 7 | word-break: break-all; 8 | } 9 | 10 | // mixins for clearfix 11 | // ------------------------ 12 | .clearfix() { 13 | zoom: 1; 14 | &::before, 15 | &::after { 16 | display: table; 17 | content: ' '; 18 | } 19 | &::after { 20 | clear: both; 21 | height: 0; 22 | font-size: 0; 23 | visibility: hidden; 24 | } 25 | } 26 | 27 | 28 | .page-header-content { 29 | display: flex; 30 | 31 | .avatar { 32 | flex: 0 1 72px; 33 | 34 | & > span { 35 | display: block; 36 | width: 72px; 37 | height: 72px; 38 | border-radius: 72px; 39 | } 40 | } 41 | 42 | .content { 43 | position: relative; 44 | top: 4px; 45 | flex: 1 1 auto; 46 | margin-left: 24px; 47 | color: @text-color-secondary; 48 | line-height: 22px; 49 | 50 | .content-title { 51 | margin-bottom: 12px; 52 | color: @heading-color; 53 | font-weight: 500; 54 | font-size: 20px; 55 | line-height: 28px; 56 | } 57 | } 58 | } 59 | 60 | .extra-content { 61 | .clearfix(); 62 | float: right; 63 | white-space: nowrap; 64 | 65 | .stat-item { 66 | position: relative; 67 | display: inline-block; 68 | padding: 0 32px; 69 | 70 | > p:first-child { 71 | margin-bottom: 4px; 72 | color: @text-color-secondary; 73 | font-size: @font-size-base; 74 | line-height: 22px; 75 | } 76 | 77 | > p { 78 | margin: 0; 79 | color: @heading-color; 80 | font-size: 30px; 81 | line-height: 38px; 82 | 83 | > span { 84 | color: @text-color-secondary; 85 | font-size: 20px; 86 | } 87 | } 88 | 89 | &::after { 90 | position: absolute; 91 | top: 8px; 92 | right: 0; 93 | width: 1px; 94 | height: 40px; 95 | background-color: @border-color-split; 96 | content: ''; 97 | } 98 | 99 | &:last-child { 100 | padding-right: 0; 101 | 102 | &::after { 103 | display: none; 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /frontend/src/components/AvatarList/index.md: -------------------------------------------------------------------------------- 1 | # AvatarList 用户头像列表 2 | 3 | 4 | 一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。 5 | 6 | 7 | 8 | 引用方式: 9 | 10 | ```javascript 11 | import AvatarList from '@/components/AvatarList' 12 | const AvatarListItem = AvatarList.Item 13 | 14 | export default { 15 | components: { 16 | AvatarList, 17 | AvatarListItem 18 | } 19 | } 20 | ``` 21 | 22 | 23 | 24 | ## 代码演示 [demo](https://pro.loacg.com/test/home) 25 | 26 | ```html 27 | 28 | 29 | 30 | 31 | 32 | ``` 33 | 或 34 | ```html 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ``` 45 | 46 | 47 | 48 | ## API 49 | 50 | ### AvatarList 51 | 52 | | 参数 | 说明 | 类型 | 默认值 | 53 | | ---------------- | -------- | ---------------------------------- | --------- | 54 | | size | 头像大小 | `large`、`small` 、`mini`, `default` | `default` | 55 | | maxLength | 要显示的最大项目 | number | - | 56 | | excessItemsStyle | 多余的项目风格 | CSSProperties | - | 57 | 58 | ### AvatarList.Item 59 | 60 | | 参数 | 说明 | 类型 | 默认值 | 61 | | ---- | ------ | --------- | --- | 62 | | tips | 头像展示文案 | string | - | 63 | | src | 头像图片连接 | string | - | 64 | 65 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /frontend/src/components/GlobalHeader/AvatarDropdown.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 70 | 71 | 81 | -------------------------------------------------------------------------------- /frontend/src/views/list/modules/TaskForm.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 95 | -------------------------------------------------------------------------------- /frontend/src/views/other/modules/OrgModal.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 101 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Ant Design Pro 9 | 10 | 11 | <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %> 12 | 13 | <% } %> 14 | 15 | 16 | 19 |
    20 |
    21 |

    Pro

    22 |
    23 | 24 |
    25 |
    Ant Design
    26 |
    27 |
    28 | 29 | <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %> 30 | 31 | <% } %> 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/core/lazy_use.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // base library 4 | import { 5 | ConfigProvider, 6 | Layout, 7 | Input, 8 | InputNumber, 9 | Button, 10 | Switch, 11 | Radio, 12 | Checkbox, 13 | Select, 14 | Card, 15 | Form, 16 | Row, 17 | Col, 18 | Modal, 19 | Table, 20 | Tabs, 21 | Icon, 22 | Badge, 23 | Popover, 24 | Dropdown, 25 | List, 26 | Avatar, 27 | Breadcrumb, 28 | Steps, 29 | Spin, 30 | Menu, 31 | Drawer, 32 | Tooltip, 33 | Alert, 34 | Tag, 35 | Divider, 36 | DatePicker, 37 | TimePicker, 38 | Upload, 39 | Progress, 40 | Skeleton, 41 | Popconfirm, 42 | PageHeader, 43 | Result, 44 | Statistic, 45 | Descriptions, 46 | message, 47 | notification 48 | } from 'ant-design-vue' 49 | import Viser from 'viser-vue' 50 | 51 | // ext library 52 | import VueCropper from 'vue-cropper' 53 | import Dialog from '@/components/Dialog' 54 | import MultiTab from '@/components/MultiTab' 55 | import PageLoading from '@/components/PageLoading' 56 | import PermissionHelper from '@/utils/helper/permission' 57 | import './directives/action' 58 | 59 | Vue.use(ConfigProvider) 60 | Vue.use(Layout) 61 | Vue.use(Input) 62 | Vue.use(InputNumber) 63 | Vue.use(Button) 64 | Vue.use(Switch) 65 | Vue.use(Radio) 66 | Vue.use(Checkbox) 67 | Vue.use(Select) 68 | Vue.use(Card) 69 | Vue.use(Form) 70 | Vue.use(Row) 71 | Vue.use(Col) 72 | Vue.use(Modal) 73 | Vue.use(Table) 74 | Vue.use(Tabs) 75 | Vue.use(Icon) 76 | Vue.use(Badge) 77 | Vue.use(Popover) 78 | Vue.use(Dropdown) 79 | Vue.use(List) 80 | Vue.use(Avatar) 81 | Vue.use(Breadcrumb) 82 | Vue.use(Steps) 83 | Vue.use(Spin) 84 | Vue.use(Menu) 85 | Vue.use(Drawer) 86 | Vue.use(Tooltip) 87 | Vue.use(Alert) 88 | Vue.use(Tag) 89 | Vue.use(Divider) 90 | Vue.use(DatePicker) 91 | Vue.use(TimePicker) 92 | Vue.use(Upload) 93 | Vue.use(Progress) 94 | Vue.use(Skeleton) 95 | Vue.use(Popconfirm) 96 | Vue.use(PageHeader) 97 | Vue.use(Result) 98 | Vue.use(Statistic) 99 | Vue.use(Descriptions) 100 | 101 | Vue.prototype.$confirm = Modal.confirm 102 | Vue.prototype.$message = message 103 | Vue.prototype.$notification = notification 104 | Vue.prototype.$info = Modal.info 105 | Vue.prototype.$success = Modal.success 106 | Vue.prototype.$error = Modal.error 107 | Vue.prototype.$warning = Modal.warning 108 | 109 | Vue.use(Viser) 110 | Vue.use(Dialog) // this.$dialog func 111 | Vue.use(MultiTab) 112 | Vue.use(PageLoading) 113 | Vue.use(PermissionHelper) 114 | Vue.use(VueCropper) 115 | 116 | process.env.NODE_ENV !== 'production' && console.warn('[antd-pro] NOTICE: Antd use lazy-load.') 117 | -------------------------------------------------------------------------------- /frontend/src/components/tools/TwoStepCaptcha.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 83 | 90 | -------------------------------------------------------------------------------- /backend/public/index.html: -------------------------------------------------------------------------------- 1 | Ant Design Pro

    Pro

    Ant Design
    -------------------------------------------------------------------------------- /frontend/config/themePluginConfig.js: -------------------------------------------------------------------------------- 1 | export default { 2 | theme: [ 3 | { 4 | key: 'dark', 5 | fileName: 'dark.css', 6 | theme: 'dark' 7 | }, 8 | { 9 | key: '#F5222D', 10 | fileName: '#F5222D.css', 11 | modifyVars: { 12 | '@primary-color': '#F5222D' 13 | } 14 | }, 15 | { 16 | key: '#FA541C', 17 | fileName: '#FA541C.css', 18 | modifyVars: { 19 | '@primary-color': '#FA541C' 20 | } 21 | }, 22 | { 23 | key: '#FAAD14', 24 | fileName: '#FAAD14.css', 25 | modifyVars: { 26 | '@primary-color': '#FAAD14' 27 | } 28 | }, 29 | { 30 | key: '#13C2C2', 31 | fileName: '#13C2C2.css', 32 | modifyVars: { 33 | '@primary-color': '#13C2C2' 34 | } 35 | }, 36 | { 37 | key: '#52C41A', 38 | fileName: '#52C41A.css', 39 | modifyVars: { 40 | '@primary-color': '#52C41A' 41 | } 42 | }, 43 | { 44 | key: '#2F54EB', 45 | fileName: '#2F54EB.css', 46 | modifyVars: { 47 | '@primary-color': '#2F54EB' 48 | } 49 | }, 50 | { 51 | key: '#722ED1', 52 | fileName: '#722ED1.css', 53 | modifyVars: { 54 | '@primary-color': '#722ED1' 55 | } 56 | }, 57 | 58 | { 59 | key: '#F5222D', 60 | theme: 'dark', 61 | fileName: 'dark-#F5222D.css', 62 | modifyVars: { 63 | '@primary-color': '#F5222D' 64 | } 65 | }, 66 | { 67 | key: '#FA541C', 68 | theme: 'dark', 69 | fileName: 'dark-#FA541C.css', 70 | modifyVars: { 71 | '@primary-color': '#FA541C' 72 | } 73 | }, 74 | { 75 | key: '#FAAD14', 76 | theme: 'dark', 77 | fileName: 'dark-#FAAD14.css', 78 | modifyVars: { 79 | '@primary-color': '#FAAD14' 80 | } 81 | }, 82 | { 83 | key: '#13C2C2', 84 | theme: 'dark', 85 | fileName: 'dark-#13C2C2.css', 86 | modifyVars: { 87 | '@primary-color': '#13C2C2' 88 | } 89 | }, 90 | { 91 | key: '#52C41A', 92 | theme: 'dark', 93 | fileName: 'dark-#52C41A.css', 94 | modifyVars: { 95 | '@primary-color': '#52C41A' 96 | } 97 | }, 98 | { 99 | key: '#2F54EB', 100 | theme: 'dark', 101 | fileName: 'dark-#2F54EB.css', 102 | modifyVars: { 103 | '@primary-color': '#2F54EB' 104 | } 105 | }, 106 | { 107 | key: '#722ED1', 108 | theme: 'dark', 109 | fileName: 'dark-#722ED1.css', 110 | modifyVars: { 111 | '@primary-color': '#722ED1' 112 | } 113 | } 114 | ] 115 | } 116 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/ChartCard.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 53 | 54 | 121 | -------------------------------------------------------------------------------- /frontend/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import storage from 'store' 2 | import { 3 | SIDEBAR_TYPE, 4 | TOGGLE_MOBILE_TYPE, 5 | TOGGLE_NAV_THEME, 6 | TOGGLE_LAYOUT, 7 | TOGGLE_FIXED_HEADER, 8 | TOGGLE_FIXED_SIDEBAR, 9 | TOGGLE_CONTENT_WIDTH, 10 | TOGGLE_HIDE_HEADER, 11 | TOGGLE_COLOR, 12 | TOGGLE_WEAK, 13 | TOGGLE_MULTI_TAB, 14 | // i18n 15 | APP_LANGUAGE 16 | } from '@/store/mutation-types' 17 | import { loadLanguageAsync } from '@/locales' 18 | 19 | const app = { 20 | state: { 21 | sideCollapsed: false, 22 | isMobile: false, 23 | theme: 'dark', 24 | layout: '', 25 | contentWidth: '', 26 | fixedHeader: false, 27 | fixedSidebar: false, 28 | autoHideHeader: false, 29 | color: '', 30 | weak: false, 31 | multiTab: true, 32 | lang: 'en-US', 33 | _antLocale: {} 34 | }, 35 | mutations: { 36 | [SIDEBAR_TYPE]: (state, type) => { 37 | state.sideCollapsed = type 38 | storage.set(SIDEBAR_TYPE, type) 39 | }, 40 | [TOGGLE_MOBILE_TYPE]: (state, isMobile) => { 41 | state.isMobile = isMobile 42 | }, 43 | [TOGGLE_NAV_THEME]: (state, theme) => { 44 | state.theme = theme 45 | storage.set(TOGGLE_NAV_THEME, theme) 46 | }, 47 | [TOGGLE_LAYOUT]: (state, mode) => { 48 | state.layout = mode 49 | storage.set(TOGGLE_LAYOUT, mode) 50 | }, 51 | [TOGGLE_FIXED_HEADER]: (state, mode) => { 52 | state.fixedHeader = mode 53 | storage.set(TOGGLE_FIXED_HEADER, mode) 54 | }, 55 | [TOGGLE_FIXED_SIDEBAR]: (state, mode) => { 56 | state.fixedSidebar = mode 57 | storage.set(TOGGLE_FIXED_SIDEBAR, mode) 58 | }, 59 | [TOGGLE_CONTENT_WIDTH]: (state, type) => { 60 | state.contentWidth = type 61 | storage.set(TOGGLE_CONTENT_WIDTH, type) 62 | }, 63 | [TOGGLE_HIDE_HEADER]: (state, type) => { 64 | state.autoHideHeader = type 65 | storage.set(TOGGLE_HIDE_HEADER, type) 66 | }, 67 | [TOGGLE_COLOR]: (state, color) => { 68 | state.color = color 69 | storage.set(TOGGLE_COLOR, color) 70 | }, 71 | [TOGGLE_WEAK]: (state, mode) => { 72 | state.weak = mode 73 | storage.set(TOGGLE_WEAK, mode) 74 | }, 75 | [APP_LANGUAGE]: (state, lang, antd = {}) => { 76 | state.lang = lang 77 | state._antLocale = antd 78 | storage.set(APP_LANGUAGE, lang) 79 | }, 80 | [TOGGLE_MULTI_TAB]: (state, bool) => { 81 | storage.set(TOGGLE_MULTI_TAB, bool) 82 | state.multiTab = bool 83 | } 84 | }, 85 | actions: { 86 | setLang ({ commit }, lang) { 87 | return new Promise((resolve, reject) => { 88 | commit(APP_LANGUAGE, lang) 89 | loadLanguageAsync(lang).then(() => { 90 | resolve() 91 | }).catch((e) => { 92 | reject(e) 93 | }) 94 | }) 95 | } 96 | } 97 | } 98 | 99 | export default app 100 | -------------------------------------------------------------------------------- /frontend/src/components/NoticeIcon/NoticeIcon.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 75 | 76 | 81 | 91 | -------------------------------------------------------------------------------- /frontend/src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import storage from 'store' 4 | import NProgress from 'nprogress' // progress bar 5 | import '@/components/NProgress/nprogress.less' // progress bar custom style 6 | import notification from 'ant-design-vue/es/notification' 7 | import { setDocumentTitle, domTitle } from '@/utils/domUtil' 8 | import { ACCESS_TOKEN } from '@/store/mutation-types' 9 | import { i18nRender } from '@/locales' 10 | 11 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 12 | 13 | const whiteList = ['login', 'register', 'registerResult'] // no redirect whitelist 14 | const loginRoutePath = '/user/login' 15 | const defaultRoutePath = '/dashboard/workplace' 16 | 17 | router.beforeEach((to, from, next) => { 18 | NProgress.start() // start progress bar 19 | to.meta && (typeof to.meta.title !== 'undefined' && setDocumentTitle(`${i18nRender(to.meta.title)} - ${domTitle}`)) 20 | /* has token */ 21 | if (storage.get(ACCESS_TOKEN)) { 22 | if (to.path === loginRoutePath) { 23 | next({ path: defaultRoutePath }) 24 | NProgress.done() 25 | } else { 26 | // check login user.roles is null 27 | if (store.getters.roles.length === 0) { 28 | // request login userInfo 29 | store 30 | .dispatch('GetInfo') 31 | .then(res => { 32 | const roles = res.result && res.result.role 33 | // generate dynamic router 34 | store.dispatch('GenerateRoutes', { roles }).then(() => { 35 | // 根据roles权限生成可访问的路由表 36 | // 动态添加可访问路由表 37 | router.addRoutes(store.getters.addRouters) 38 | // 请求带有 redirect 重定向时,登录自动重定向到该地址 39 | const redirect = decodeURIComponent(from.query.redirect || to.path) 40 | if (to.path === redirect) { 41 | // set the replace: true so the navigation will not leave a history record 42 | next({ ...to, replace: true }) 43 | } else { 44 | // 跳转到目的路由 45 | next({ path: redirect }) 46 | } 47 | }) 48 | }) 49 | .catch(() => { 50 | notification.error({ 51 | message: '错误', 52 | description: '请求用户信息失败,请重试' 53 | }) 54 | // 失败时,获取用户信息失败时,调用登出,来清空历史保留信息 55 | store.dispatch('Logout').then(() => { 56 | next({ path: loginRoutePath, query: { redirect: to.fullPath } }) 57 | }) 58 | }) 59 | } else { 60 | next() 61 | } 62 | } 63 | } else { 64 | if (whiteList.includes(to.name)) { 65 | // 在免登录白名单,直接进入 66 | next() 67 | } else { 68 | next({ path: loginRoutePath, query: { redirect: to.fullPath } }) 69 | NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it 70 | } 71 | } 72 | }) 73 | 74 | router.afterEach(() => { 75 | NProgress.done() // finish progress bar 76 | }) 77 | -------------------------------------------------------------------------------- /frontend/src/components/Other/CarbonAds.vue: -------------------------------------------------------------------------------- 1 | 50 | 115 | -------------------------------------------------------------------------------- /frontend/src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import storage from 'store' 2 | import { login, getInfo, logout } from '@/api/login' 3 | import { ACCESS_TOKEN } from '@/store/mutation-types' 4 | import { welcome } from '@/utils/util' 5 | 6 | const user = { 7 | state: { 8 | token: '', 9 | name: '', 10 | welcome: '', 11 | avatar: '', 12 | roles: [], 13 | info: {} 14 | }, 15 | 16 | mutations: { 17 | SET_TOKEN: (state, token) => { 18 | state.token = token 19 | }, 20 | SET_NAME: (state, { name, welcome }) => { 21 | state.name = name 22 | state.welcome = welcome 23 | }, 24 | SET_AVATAR: (state, avatar) => { 25 | state.avatar = avatar 26 | }, 27 | SET_ROLES: (state, roles) => { 28 | state.roles = roles 29 | }, 30 | SET_INFO: (state, info) => { 31 | state.info = info 32 | } 33 | }, 34 | 35 | actions: { 36 | // 登录 37 | Login ({ commit }, userInfo) { 38 | return new Promise((resolve, reject) => { 39 | login(userInfo).then(response => { 40 | const result = response.result 41 | storage.set(ACCESS_TOKEN, result.token, 7 * 24 * 60 * 60 * 1000) 42 | commit('SET_TOKEN', result.token) 43 | resolve() 44 | }).catch(error => { 45 | reject(error) 46 | }) 47 | }) 48 | }, 49 | 50 | // 获取用户信息 51 | GetInfo ({ commit }) { 52 | return new Promise((resolve, reject) => { 53 | getInfo().then(response => { 54 | const result = response.result 55 | 56 | if (result.role && result.role.permissions.length > 0) { 57 | const role = result.role 58 | role.permissions = result.role.permissions 59 | role.permissions.map(per => { 60 | if (per.actionEntitySet != null && per.actionEntitySet.length > 0) { 61 | const action = per.actionEntitySet.map(action => { return action.action }) 62 | per.actionList = action 63 | } 64 | }) 65 | role.permissionList = role.permissions.map(permission => { return permission.permissionId }) 66 | commit('SET_ROLES', result.role) 67 | commit('SET_INFO', result) 68 | } else { 69 | reject(new Error('getInfo: roles must be a non-null array !')) 70 | } 71 | 72 | commit('SET_NAME', { name: result.name, welcome: welcome() }) 73 | commit('SET_AVATAR', result.avatar) 74 | 75 | resolve(response) 76 | }).catch(error => { 77 | reject(error) 78 | }) 79 | }) 80 | }, 81 | 82 | // 登出 83 | Logout ({ commit, state }) { 84 | return new Promise((resolve) => { 85 | logout(state.token).then(() => { 86 | resolve() 87 | }).catch(() => { 88 | resolve() 89 | }).finally(() => { 90 | commit('SET_TOKEN', '') 91 | commit('SET_ROLES', []) 92 | storage.remove(ACCESS_TOKEN) 93 | }) 94 | }) 95 | } 96 | 97 | } 98 | } 99 | 100 | export default user 101 | -------------------------------------------------------------------------------- /frontend/src/mock/services/article.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs2' 2 | import { builder, getQueryParameters } from '../util' 3 | 4 | const titles = [ 5 | 'Alipay', 6 | 'Angular', 7 | 'Ant Design', 8 | 'Ant Design Pro', 9 | 'Bootstrap', 10 | 'React', 11 | 'Vue', 12 | 'Webpack' 13 | ] 14 | 15 | const avatar = ['https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', 16 | 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', 17 | 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', 18 | 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', 19 | 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png' 20 | ] 21 | 22 | const covers = [ 23 | 'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png', 24 | 'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png', 25 | 'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png', 26 | 'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png' 27 | ] 28 | 29 | const owner = [ 30 | '付小小', 31 | '吴加好', 32 | '周星星', 33 | '林东东', 34 | '曲丽丽' 35 | ] 36 | 37 | const content = '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。' 38 | const description = '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。' 39 | const href = 'https://ant.design' 40 | 41 | const article = (options) => { 42 | const queryParameters = getQueryParameters(options) 43 | console.log('queryParameters', queryParameters) 44 | if (queryParameters && !queryParameters.count) { 45 | queryParameters.count = 5 46 | } 47 | const data = [] 48 | for (let i = 0; i < queryParameters.count; i++) { 49 | const tmpKey = i + 1 50 | const num = parseInt(Math.random() * (4 + 1), 10) 51 | data.push({ 52 | id: tmpKey, 53 | avatar: avatar[num], 54 | owner: owner[num], 55 | content: content, 56 | star: Mock.mock('@integer(1, 999)'), 57 | percent: Mock.mock('@integer(1, 999)'), 58 | like: Mock.mock('@integer(1, 999)'), 59 | message: Mock.mock('@integer(1, 999)'), 60 | description: description, 61 | href: href, 62 | title: titles[ i % 8 ], 63 | updatedAt: Mock.mock('@datetime'), 64 | members: [ 65 | { 66 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png', 67 | name: '曲丽丽', 68 | id: 'member1' 69 | }, 70 | { 71 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png', 72 | name: '王昭君', 73 | id: 'member2' 74 | }, 75 | { 76 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png', 77 | name: '董娜娜', 78 | id: 'member3' 79 | } 80 | ], 81 | activeUser: Math.ceil(Math.random() * 100000) + 100000, 82 | newUser: Math.ceil(Math.random() * 1000) + 1000, 83 | cover: parseInt(i / 4, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)] 84 | }) 85 | } 86 | return builder(data) 87 | } 88 | 89 | Mock.mock(/\/list\/article/, 'get', article) 90 | -------------------------------------------------------------------------------- /frontend/src/views/form/stepForm/Step2.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 99 | 100 | 111 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/TagCloud.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 114 | --------------------------------------------------------------------------------