inleft
2022-02-09 9bcb19959eeb9da9bde2561e7278f6d0a55eb151
管理后台代码初始化
794 files added
1 files modified
75979 ■■■■■ changed files
.gitattributes 4 ●●●● patch | view | raw | blame | history
.gitignore 51 ●●●●● patch | view | raw | blame | history
LICENSE 53 ●●●●● patch | view | raw | blame | history
README.md 288 ●●●●● patch | view | raw | blame | history
_web/.browserslistrc 3 ●●●●● patch | view | raw | blame | history
_web/.editorconfig 39 ●●●●● patch | view | raw | blame | history
_web/.env 3 ●●●●● patch | view | raw | blame | history
_web/.env.development 3 ●●●●● patch | view | raw | blame | history
_web/.env.preview 3 ●●●●● patch | view | raw | blame | history
_web/.eslintrc.js 77 ●●●●● patch | view | raw | blame | history
_web/.gitignore 3 ●●●●● patch | view | raw | blame | history
_web/.prettierrc 5 ●●●●● patch | view | raw | blame | history
_web/.travis.yml 7 ●●●●● patch | view | raw | blame | history
_web/LICENSE 21 ●●●●● patch | view | raw | blame | history
_web/babel.config.js 28 ●●●●● patch | view | raw | blame | history
_web/config/plugin.config.js 46 ●●●●● patch | view | raw | blame | history
_web/jest.config.js 23 ●●●●● patch | view | raw | blame | history
_web/jsconfig.json 11 ●●●●● patch | view | raw | blame | history
_web/package.json 88 ●●●●● patch | view | raw | blame | history
_web/postcss.config.js 5 ●●●●● patch | view | raw | blame | history
_web/public/avatar2.jpg patch | view | raw | blame | history
_web/public/index.html 37 ●●●●● patch | view | raw | blame | history
_web/public/loading/loading.css 1 ●●●● patch | view | raw | blame | history
_web/public/loading/loading.html 1 ●●●● patch | view | raw | blame | history
_web/public/loading/option2/html_code_segment.html 5 ●●●●● patch | view | raw | blame | history
_web/public/loading/option2/loading.css 1 ●●●● patch | view | raw | blame | history
_web/public/loading/option2/loading.svg 1 ●●●● patch | view | raw | blame | history
_web/public/logo.png patch | view | raw | blame | history
_web/src/App.vue 47 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/gen/codeGenerateManage.js 106 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/gen/sysCodeGenerateConfigManage.js 29 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/main/README.md 1 ●●●● patch | view | raw | blame | history
_web/src/api/modular/main/blogarticle/blogArticleManage.js 86 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/README.md 1 ●●●● patch | view | raw | blame | history
_web/src/api/modular/system/appManage.js 92 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/areaManage.js 16 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/configManage.js 85 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/dictDataManage.js 57 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/dictManage.js 85 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/emailManage.js 29 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/fileManage.js 115 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/logManage.js 87 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/loginManage.js 125 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/machineManage.js 15 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/menuManage.js 114 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/noticeManage.js 85 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/noticeReceivedManage.js 15 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/onlineUserManage.js 29 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/orgManage.js 100 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/posManage.js 86 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/roleManage.js 141 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/smsManage.js 43 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/timersManage.js 127 ●●●●● patch | view | raw | blame | history
_web/src/api/modular/system/userManage.js 226 ●●●●● patch | view | raw | blame | history
_web/src/assets/icons/bx-analyse.svg 1 ●●●● patch | view | raw | blame | history
_web/src/assets/logo.png patch | view | raw | blame | history
_web/src/assets/logo.svg 203 ●●●●● patch | view | raw | blame | history
_web/src/assets/welcome.png patch | view | raw | blame | history
_web/src/components/ArticleListContent/ArticleListContent.vue 89 ●●●●● patch | view | raw | blame | history
_web/src/components/ArticleListContent/index.js 3 ●●●●● patch | view | raw | blame | history
_web/src/components/AvatarList/Item.vue 46 ●●●●● patch | view | raw | blame | history
_web/src/components/AvatarList/List.vue 99 ●●●●● patch | view | raw | blame | history
_web/src/components/AvatarList/index.js 4 ●●●● patch | view | raw | blame | history
_web/src/components/AvatarList/index.less 60 ●●●●● patch | view | raw | blame | history
_web/src/components/AvatarList/index.md 64 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/Bar.vue 62 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/ChartCard.vue 120 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/Liquid.vue 67 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/MiniArea.vue 56 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/MiniBar.vue 57 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/MiniProgress.vue 75 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/MiniSmoothArea.vue 40 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/Radar.vue 68 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/RankList.vue 77 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/TagCloud.vue 113 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/TransferBar.vue 64 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/Trend.vue 82 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/chart.less 13 ●●●●● patch | view | raw | blame | history
_web/src/components/Charts/smooth.area.less 14 ●●●●● patch | view | raw | blame | history
_web/src/components/CountDown/CountDown.vue 102 ●●●●● patch | view | raw | blame | history
_web/src/components/CountDown/index.js 3 ●●●●● patch | view | raw | blame | history
_web/src/components/CountDown/index.md 34 ●●●●● patch | view | raw | blame | history
_web/src/components/DepartmentSelect/DepartmentSelect.vue 48 ●●●●● patch | view | raw | blame | history
_web/src/components/DepartmentSelect/index.js 3 ●●●●● patch | view | raw | blame | history
_web/src/components/DescriptionList/DescriptionList.vue 153 ●●●●● patch | view | raw | blame | history
_web/src/components/DescriptionList/index.js 2 ●●●●● patch | view | raw | blame | history
_web/src/components/Dialog.js 113 ●●●●● patch | view | raw | blame | history
_web/src/components/Editor/QuillEditor.vue 82 ●●●●● patch | view | raw | blame | history
_web/src/components/Editor/WangEditor.vue 126 ●●●●● patch | view | raw | blame | history
_web/src/components/Ellipsis/Ellipsis.vue 64 ●●●●● patch | view | raw | blame | history
_web/src/components/Ellipsis/index.js 3 ●●●●● patch | view | raw | blame | history
_web/src/components/Ellipsis/index.md 38 ●●●●● patch | view | raw | blame | history
_web/src/components/Exception/ExceptionPage.vue 130 ●●●●● patch | view | raw | blame | history
_web/src/components/Exception/index.js 2 ●●●●● patch | view | raw | blame | history
_web/src/components/Exception/type.js 19 ●●●●● patch | view | raw | blame | history
_web/src/components/FooterToolbar/FooterToolBar.vue 30 ●●●●● patch | view | raw | blame | history
_web/src/components/FooterToolbar/index.js 4 ●●●● patch | view | raw | blame | history
_web/src/components/FooterToolbar/index.less 23 ●●●●● patch | view | raw | blame | history
_web/src/components/FooterToolbar/index.md 48 ●●●●● patch | view | raw | blame | history
_web/src/components/GlobalFooter/GlobalFooter.vue 46 ●●●●● patch | view | raw | blame | history
_web/src/components/GlobalFooter/index.js 2 ●●●●● patch | view | raw | blame | history
_web/src/components/GlobalHeader/GlobalHeader.vue 165 ●●●●● patch | view | raw | blame | history
_web/src/components/GlobalHeader/index.js 2 ●●●●● patch | view | raw | blame | history
_web/src/components/IconSelector/IconSelector.vue 86 ●●●●● patch | view | raw | blame | history
_web/src/components/IconSelector/README.md 48 ●●●●● patch | view | raw | blame | history
_web/src/components/IconSelector/icons.js 36 ●●●●● patch | view | raw | blame | history
_web/src/components/IconSelector/index.js 2 ●●●●● patch | view | raw | blame | history
_web/src/components/Menu/SideMenu.vue 61 ●●●●● patch | view | raw | blame | history
_web/src/components/Menu/index.js 2 ●●●●● patch | view | raw | blame | history
_web/src/components/Menu/menu.js 177 ●●●●● patch | view | raw | blame | history
_web/src/components/Menu/menu.render.js 156 ●●●●● patch | view | raw | blame | history
_web/src/components/MultiTab/MultiTab.vue 163 ●●●●● patch | view | raw | blame | history
_web/src/components/MultiTab/events.js 2 ●●●●● patch | view | raw | blame | history
_web/src/components/MultiTab/index.js 40 ●●●●● patch | view | raw | blame | history
_web/src/components/MultiTab/index.less 25 ●●●●● patch | view | raw | blame | history
_web/src/components/NProgress/nprogress.less 76 ●●●●● patch | view | raw | blame | history
_web/src/components/NoticeIcon/NoticeIcon.vue 142 ●●●●● patch | view | raw | blame | history
_web/src/components/NoticeIcon/index.js 2 ●●●●● patch | view | raw | blame | history
_web/src/components/NumberInfo/NumberInfo.vue 54 ●●●●● patch | view | raw | blame | history
_web/src/components/NumberInfo/index.js 3 ●●●●● patch | view | raw | blame | history
_web/src/components/NumberInfo/index.less 55 ●●●●● patch | view | raw | blame | history
_web/src/components/NumberInfo/index.md 43 ●●●●● patch | view | raw | blame | history
_web/src/components/PageHeader/PageHeader.vue 202 ●●●●● patch | view | raw | blame | history
_web/src/components/PageHeader/index.js 2 ●●●●● patch | view | raw | blame | history
_web/src/components/PageLoading/index.jsx 106 ●●●●● patch | view | raw | blame | history
_web/src/components/Result/Result.vue 109 ●●●●● patch | view | raw | blame | history
_web/src/components/Result/index.js 2 ●●●●● patch | view | raw | blame | history
_web/src/components/Search/GlobalSearch.jsx 63 ●●●●● patch | view | raw | blame | history
_web/src/components/Search/index.less 25 ●●●●● patch | view | raw | blame | history
_web/src/components/SettingDrawer/SettingDrawer.vue 352 ●●●●● patch | view | raw | blame | history
_web/src/components/SettingDrawer/SettingItem.vue 38 ●●●●● patch | view | raw | blame | history
_web/src/components/SettingDrawer/index.js 2 ●●●●● patch | view | raw | blame | history
_web/src/components/SettingDrawer/settingConfig.js 46 ●●●●● patch | view | raw | blame | history
_web/src/components/SettingDrawer/themeColor.js 24 ●●●●● patch | view | raw | blame | history
_web/src/components/StandardFormRow/StandardFormRow.vue 122 ●●●●● patch | view | raw | blame | history
_web/src/components/StandardFormRow/index.js 3 ●●●●● patch | view | raw | blame | history
_web/src/components/Table/README.md 340 ●●●●● patch | view | raw | blame | history
_web/src/components/Table/columnSetting.vue 82 ●●●●● patch | view | raw | blame | history
_web/src/components/Table/index.js 449 ●●●●● patch | view | raw | blame | history
_web/src/components/Table/index.less 54 ●●●●● patch | view | raw | blame | history
_web/src/components/TagSelect/TagSelectOption.jsx 45 ●●●●● patch | view | raw | blame | history
_web/src/components/TagSelect/index.jsx 113 ●●●●● patch | view | raw | blame | history
_web/src/components/TextArea/index.jsx 69 ●●●●● patch | view | raw | blame | history
_web/src/components/TextArea/style.less 12 ●●●●● patch | view | raw | blame | history
_web/src/components/Tree/Tree.jsx 124 ●●●●● patch | view | raw | blame | history
_web/src/components/Trend/Trend.vue 41 ●●●●● patch | view | raw | blame | history
_web/src/components/Trend/index.js 3 ●●●●● patch | view | raw | blame | history
_web/src/components/Trend/index.less 42 ●●●●● patch | view | raw | blame | history
_web/src/components/Trend/index.md 45 ●●●●● patch | view | raw | blame | history
_web/src/components/UserSelect/UserSelect.vue 77 ●●●●● patch | view | raw | blame | history
_web/src/components/UserSelect/index.js 3 ●●●●● patch | view | raw | blame | history
_web/src/components/_util/util.js 46 ●●●●● patch | view | raw | blame | history
_web/src/components/global.less 516 ●●●●● patch | view | raw | blame | history
_web/src/components/index.js 72 ●●●●● patch | view | raw | blame | history
_web/src/components/index.less 6 ●●●●● patch | view | raw | blame | history
_web/src/components/tools/Breadcrumb.vue 45 ●●●●● patch | view | raw | blame | history
_web/src/components/tools/DetailList.vue 5 ●●●●● patch | view | raw | blame | history
_web/src/components/tools/HeadInfo.vue 67 ●●●●● patch | view | raw | blame | history
_web/src/components/tools/LangSelect.vue 46 ●●●●● patch | view | raw | blame | history
_web/src/components/tools/Logo.vue 53 ●●●●● patch | view | raw | blame | history
_web/src/components/tools/TwoStepCaptcha.vue 89 ●●●●● patch | view | raw | blame | history
_web/src/components/tools/UserMenu.vue 191 ●●●●● patch | view | raw | blame | history
_web/src/components/tools/index.js patch | view | raw | blame | history
_web/src/components/verifition/Verify.vue 473 ●●●●● patch | view | raw | blame | history
_web/src/components/verifition/Verify/VerifyPoints.vue 260 ●●●●● patch | view | raw | blame | history
_web/src/components/verifition/Verify/VerifySlide.vue 374 ●●●●● patch | view | raw | blame | history
_web/src/components/verifition/utils/ase.js 11 ●●●●● patch | view | raw | blame | history
_web/src/components/verifition/utils/axios.js 30 ●●●●● patch | view | raw | blame | history
_web/src/components/verifition/utils/util.js 52 ●●●●● patch | view | raw | blame | history
_web/src/components/xnComponents/EditorDiv.vue 95 ●●●●● patch | view | raw | blame | history
_web/src/components/xnComponents/XCard.vue 16 ●●●●● patch | view | raw | blame | history
_web/src/components/xnComponents/XDown.vue 54 ●●●●● patch | view | raw | blame | history
_web/src/config/defaultSettings.js 35 ●●●●● patch | view | raw | blame | history
_web/src/config/router.config.js 85 ●●●●● patch | view | raw | blame | history
_web/src/core/bootstrap.js 31 ●●●●● patch | view | raw | blame | history
_web/src/core/directives/action.js 34 ●●●●● patch | view | raw | blame | history
_web/src/core/icons.js 11 ●●●●● patch | view | raw | blame | history
_web/src/core/lazy_lib/components_use.js 111 ●●●●● patch | view | raw | blame | history
_web/src/core/lazy_use.js 27 ●●●●● patch | view | raw | blame | history
_web/src/core/use.js 30 ●●●●● patch | view | raw | blame | history
_web/src/layouts/BasicLayout.vue 186 ●●●●● patch | view | raw | blame | history
_web/src/layouts/BlankLayout.vue 16 ●●●●● patch | view | raw | blame | history
_web/src/layouts/Iframe.vue 29 ●●●●● patch | view | raw | blame | history
_web/src/layouts/PageView.vue 177 ●●●●● patch | view | raw | blame | history
_web/src/layouts/RouteView.vue 32 ●●●●● patch | view | raw | blame | history
_web/src/layouts/UserLayout.vue 153 ●●●●● patch | view | raw | blame | history
_web/src/layouts/index.js 8 ●●●●● patch | view | raw | blame | history
_web/src/main.js 28 ●●●●● patch | view | raw | blame | history
_web/src/permission.js 113 ●●●●● patch | view | raw | blame | history
_web/src/router/generator-routers.js 261 ●●●●● patch | view | raw | blame | history
_web/src/router/index.js 19 ●●●●● patch | view | raw | blame | history
_web/src/store/getters.js 18 ●●●●● patch | view | raw | blame | history
_web/src/store/index.js 32 ●●●●● patch | view | raw | blame | history
_web/src/store/modules/app.js 129 ●●●●● patch | view | raw | blame | history
_web/src/store/modules/async-router.js 33 ●●●●● patch | view | raw | blame | history
_web/src/store/modules/permission.js 77 ●●●●● patch | view | raw | blame | history
_web/src/store/modules/user.js 172 ●●●●● patch | view | raw | blame | history
_web/src/store/mutation-types.js 18 ●●●●● patch | view | raw | blame | history
_web/src/utils/applocation.js 11 ●●●●● patch | view | raw | blame | history
_web/src/utils/axios.js 35 ●●●●● patch | view | raw | blame | history
_web/src/utils/device.js 33 ●●●●● patch | view | raw | blame | history
_web/src/utils/domUtil.js 19 ●●●●● patch | view | raw | blame | history
_web/src/utils/filter.js 101 ●●●●● patch | view | raw | blame | history
_web/src/utils/helper/permission.js 51 ●●●●● patch | view | raw | blame | history
_web/src/utils/mixin.js 76 ●●●●● patch | view | raw | blame | history
_web/src/utils/onlyofficeUtil.js 22 ●●●●● patch | view | raw | blame | history
_web/src/utils/permissions.js 26 ●●●●● patch | view | raw | blame | history
_web/src/utils/request.js 96 ●●●●● patch | view | raw | blame | history
_web/src/utils/routeConvert.js 30 ●●●●● patch | view | raw | blame | history
_web/src/utils/util.js 67 ●●●●● patch | view | raw | blame | history
_web/src/utils/utils.less 50 ●●●●● patch | view | raw | blame | history
_web/src/views/404.vue 15 ●●●●● patch | view | raw | blame | history
_web/src/views/gen/codeGenerate/addForm.vue 316 ●●●●● patch | view | raw | blame | history
_web/src/views/gen/codeGenerate/editForm.vue 327 ●●●●● patch | view | raw | blame | history
_web/src/views/gen/codeGenerate/index.vue 263 ●●●●● patch | view | raw | blame | history
_web/src/views/gen/codeGenerate/indexConfig.vue 230 ●●●●● patch | view | raw | blame | history
_web/src/views/main/README.md 1 ●●●● patch | view | raw | blame | history
_web/src/views/main/blogarticle/addForm.vue 256 ●●●●● patch | view | raw | blame | history
_web/src/views/main/blogarticle/editForm.vue 298 ●●●●● patch | view | raw | blame | history
_web/src/views/main/blogarticle/index.vue 400 ●●●●● patch | view | raw | blame | history
_web/src/views/system/README.md 1 ●●●● patch | view | raw | blame | history
_web/src/views/system/account/center/Index.vue 308 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/center/page/App.vue 113 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/center/page/Article.vue 91 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/center/page/Project.vue 215 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/center/page/index.js 5 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/settings/AvatarModal.vue 183 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/settings/BaseSetting.vue 216 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/settings/Binding.vue 25 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/settings/Custom.vue 75 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/settings/Index.vue 155 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/settings/Notification.vue 25 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/settings/Security.vue 70 ●●●●● patch | view | raw | blame | history
_web/src/views/system/account/settings/securityItem/updPwd.vue 115 ●●●●● patch | view | raw | blame | history
_web/src/views/system/app/addForm.vue 91 ●●●●● patch | view | raw | blame | history
_web/src/views/system/app/editForm.vue 108 ●●●●● patch | view | raw | blame | history
_web/src/views/system/app/index.vue 194 ●●●●● patch | view | raw | blame | history
_web/src/views/system/area/index.vue 130 ●●●●● patch | view | raw | blame | history
_web/src/views/system/config/addForm.vue 130 ●●●●● patch | view | raw | blame | history
_web/src/views/system/config/editForm.vue 158 ●●●●● patch | view | raw | blame | history
_web/src/views/system/config/index.vue 184 ●●●●● patch | view | raw | blame | history
_web/src/views/system/dashboard/Analysis.vue 385 ●●●●● patch | view | raw | blame | history
_web/src/views/system/dashboard/Monitor.vue 15 ●●●●● patch | view | raw | blame | history
_web/src/views/system/dashboard/TestWork.vue 117 ●●●●● patch | view | raw | blame | history
_web/src/views/system/dashboard/Workplace.vue 526 ●●●●● patch | view | raw | blame | history
_web/src/views/system/dict/addForm.vue 106 ●●●●● patch | view | raw | blame | history
_web/src/views/system/dict/dictdata/addForm.vue 123 ●●●●● patch | view | raw | blame | history
_web/src/views/system/dict/dictdata/editForm.vue 137 ●●●●● patch | view | raw | blame | history
_web/src/views/system/dict/dictdata/index.vue 181 ●●●●● patch | view | raw | blame | history
_web/src/views/system/dict/editForm.vue 128 ●●●●● patch | view | raw | blame | history
_web/src/views/system/dict/index.vue 168 ●●●●● patch | view | raw | blame | history
_web/src/views/system/email/index.vue 178 ●●●●● patch | view | raw | blame | history
_web/src/views/system/exception/403.vue 17 ●●●●● patch | view | raw | blame | history
_web/src/views/system/exception/404.vue 17 ●●●●● patch | view | raw | blame | history
_web/src/views/system/exception/500.vue 17 ●●●●● patch | view | raw | blame | history
_web/src/views/system/file/detailForm.vue 99 ●●●●● patch | view | raw | blame | history
_web/src/views/system/file/index.vue 256 ●●●●● patch | view | raw | blame | history
_web/src/views/system/file/previewForm.vue 60 ●●●●● patch | view | raw | blame | history
_web/src/views/system/fileOnline/detailForm.vue 99 ●●●●● patch | view | raw | blame | history
_web/src/views/system/fileOnline/index.vue 254 ●●●●● patch | view | raw | blame | history
_web/src/views/system/fileOnline/onlineEditForm.vue 91 ●●●●● patch | view | raw | blame | history
_web/src/views/system/fileOnline/previewForm.vue 96 ●●●●● patch | view | raw | blame | history
_web/src/views/system/index/welcome.vue 15 ●●●●● patch | view | raw | blame | history
_web/src/views/system/log/oplog/details.vue 137 ●●●●● patch | view | raw | blame | history
_web/src/views/system/log/oplog/index.vue 230 ●●●●● patch | view | raw | blame | history
_web/src/views/system/log/vislog/details.vue 57 ●●●●● patch | view | raw | blame | history
_web/src/views/system/log/vislog/index.vue 230 ●●●●● patch | view | raw | blame | history
_web/src/views/system/machine/index.vue 118 ●●●●● patch | view | raw | blame | history
_web/src/views/system/menu/addForm.vue 567 ●●●●● patch | view | raw | blame | history
_web/src/views/system/menu/editForm.vue 627 ●●●●● patch | view | raw | blame | history
_web/src/views/system/menu/index.vue 187 ●●●●● patch | view | raw | blame | history
_web/src/views/system/notice/addForm.vue 213 ●●●●● patch | view | raw | blame | history
_web/src/views/system/notice/detailForm.vue 60 ●●●●● patch | view | raw | blame | history
_web/src/views/system/notice/editForm.vue 240 ●●●●● patch | view | raw | blame | history
_web/src/views/system/notice/index.vue 191 ●●●●● patch | view | raw | blame | history
_web/src/views/system/noticeReceived/detailForm.vue 75 ●●●●● patch | view | raw | blame | history
_web/src/views/system/noticeReceived/index.vue 136 ●●●●● patch | view | raw | blame | history
_web/src/views/system/onlineUser/index.vue 124 ●●●●● patch | view | raw | blame | history
_web/src/views/system/org/addForm.vue 156 ●●●●● patch | view | raw | blame | history
_web/src/views/system/org/editForm.vue 178 ●●●●● patch | view | raw | blame | history
_web/src/views/system/org/index.vue 232 ●●●●● patch | view | raw | blame | history
_web/src/views/system/pos/addForm.vue 106 ●●●●● patch | view | raw | blame | history
_web/src/views/system/pos/editForm.vue 129 ●●●●● patch | view | raw | blame | history
_web/src/views/system/pos/index.vue 186 ●●●●● patch | view | raw | blame | history
_web/src/views/system/role/addForm.vue 106 ●●●●● patch | view | raw | blame | history
_web/src/views/system/role/editForm.vue 128 ●●●●● patch | view | raw | blame | history
_web/src/views/system/role/index.vue 156 ●●●●● patch | view | raw | blame | history
_web/src/views/system/role/roleMenuForm.vue 184 ●●●●● patch | view | raw | blame | history
_web/src/views/system/role/roleOrgForm.vue 195 ●●●●● patch | view | raw | blame | history
_web/src/views/system/sms/index.vue 163 ●●●●● patch | view | raw | blame | history
_web/src/views/system/timers/addForm.vue 126 ●●●●● patch | view | raw | blame | history
_web/src/views/system/timers/editForm.vue 151 ●●●●● patch | view | raw | blame | history
_web/src/views/system/timers/index.vue 196 ●●●●● patch | view | raw | blame | history
_web/src/views/system/user/addForm.vue 468 ●●●●● patch | view | raw | blame | history
_web/src/views/system/user/editForm.vue 494 ●●●●● patch | view | raw | blame | history
_web/src/views/system/user/index.vue 336 ●●●●● patch | view | raw | blame | history
_web/src/views/system/user/userOrgForm.vue 150 ●●●●● patch | view | raw | blame | history
_web/src/views/system/user/userRoleForm.vue 117 ●●●●● patch | view | raw | blame | history
_web/src/views/userLoginReg/Login.vue 367 ●●●●● patch | view | raw | blame | history
_web/src/views/userLoginReg/Register.vue 322 ●●●●● patch | view | raw | blame | history
_web/src/views/userLoginReg/RegisterResult.vue 50 ●●●●● patch | view | raw | blame | history
_web/tests/unit/.eslintrc.js 5 ●●●●● patch | view | raw | blame | history
_web/vue.config.js 123 ●●●●● patch | view | raw | blame | history
_web/webstorm.config.js 3 ●●●●● patch | view | raw | blame | history
pom.xml 270 ●●●●● patch | view | raw | blame | history
snowy-base/README.md 1 ●●●● patch | view | raw | blame | history
snowy-base/pom.xml 24 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/README.md 3 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/pom.xml 188 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/annotion/BusinessLog.java 51 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/annotion/DataScope.java 39 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/annotion/ExpEnumType.java 56 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/annotion/Permission.java 52 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/annotion/Wrapper.java 47 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/cache/CacheOperator.java 118 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/cache/base/AbstractMemoryCacheOperator.java 105 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/cache/base/AbstractRedisCacheOperator.java 103 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/AopSortConstant.java 72 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/CommonConstant.java 134 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/ExpEnumConstant.java 89 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/MediaTypeConstant.java 124 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/SpringSecurityConstant.java 72 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/SymbolConstant.java 90 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/constant/ConstantContext.java 78 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/constant/ConstantContextHolder.java 413 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/group/RequestGroupContext.java 67 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/group/RequestParamIdContext.java 69 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/login/LoginContext.java 168 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/login/LoginContextHolder.java 41 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/param/RequestParamContext.java 89 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/requestno/RequestNoContext.java 66 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/resources/ApiResourceContext.java 93 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/system/SystemContext.java 142 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/system/SystemContextHolder.java 41 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/cryptogram/keypair.java 51 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/dbs/CurrentDataSourceContext.java 67 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/email/MailSender.java 54 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/email/modular/SimpleMailSender.java 96 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/email/modular/exception/MailSendException.java 48 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/email/modular/model/SendMailParam.java 52 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/CommonStatusEnum.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/DbIdEnum.java 79 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/DocumentFormatEnum.java 130 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/LogAnnotionOpTypeEnum.java 107 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/LogicTypeEnum.java 44 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/YesOrNotEnum.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/AuthException.java 54 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/DemoException.java 46 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/LibreOfficeException.java 46 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/PermissionException.java 55 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/RequestMethodException.java 48 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/ServiceException.java 56 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/AuthExceptionEnum.java 115 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/ParamExceptionEnum.java 64 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/PermissionExceptionEnum.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/RequestMethodExceptionEnum.java 70 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/RequestTypeExceptionEnum.java 70 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/ServerExceptionEnum.java 75 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/StatusExceptionEnum.java 75 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/WrapperExceptionEnum.java 75 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/abs/AbstractBaseExceptionEnum.java 53 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/factory/ExpEnumCodeFactory.java 54 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/factory/PageFactory.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/factory/TreeBuildFactory.java 128 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/FileOperator.java 171 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/common/enums/BucketAuthEnum.java 50 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/common/exp/FileServiceException.java 42 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/aliyun/AliyunFileOperator.java 213 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/aliyun/exp/AliyunFileServiceException.java 70 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/aliyun/prop/AliyunOssProperties.java 55 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/local/LocalFileOperator.java 187 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/local/prop/LocalFileProperties.java 48 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/tencent/TenFileOperator.java 262 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/tencent/exp/TencentFileServiceException.java 70 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/tencent/prop/TenCosProperties.java 53 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/base/entity/BaseEntity.java 72 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/base/node/BaseTreeNode.java 64 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/base/param/BaseParam.java 272 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/base/validate/UniqueValidateParam.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/base/wrapper/BaseWrapper.java 47 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/cryptogram/CryptogramConfigs.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/druid/DruidProperties.java 188 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/email/EmailConfigs.java 68 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/login/LoginEmpInfo.java 69 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/login/SnowyAuthority.java 48 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/login/SysLoginUser.java 217 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/node/AntdBaseTreeNode.java 87 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/node/CommonBaseTreeNode.java 90 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/node/LoginMenuTreeNode.java 111 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/oauth/OauthConfigs.java 52 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/page/PageResult.java 118 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/response/ErrorResponseData.java 56 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/response/ResponseData.java 99 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/response/SuccessResponseData.java 46 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/sms/AliyunSmsConfigs.java 63 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/sms/TencentSmsConfigs.java 64 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/SmsSender.java 50 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/AliyunSmsSender.java 180 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/enums/AliyunSmsResultEnum.java 148 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/exp/AliyunSmsException.java 47 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/msign/MultiSignManager.java 46 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/msign/impl/MapBasedMultiSignManager.java 88 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/prop/AliyunSmsProperties.java 78 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/tencent/TencentSmsSender.java 113 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/tencent/exp/TencentSmsException.java 47 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/tencent/prop/TencentSmsProperties.java 64 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/consts/TenantConstants.java 55 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/consts/TenantExpEnumConstant.java 45 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/context/TenantCodeHolder.java 48 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/context/TenantDbNameHolder.java 48 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/entity/TenantInfo.java 95 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/exception/TenantException.java 42 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/exception/enums/TenantExceptionEnum.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/params/TenantInfoParam.java 78 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/service/TenantInfoService.java 87 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/timer/TimerTaskRunner.java 45 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/AopTargetUtil.java 88 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/CryptogramUtil.java 139 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/HttpServletUtil.java 72 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/IpAddressUtil.java 109 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/JoinPointUtil.java 73 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/LibreOfficeUtil.java 141 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/PageUtil.java 61 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/PastTimeFormatUtil.java 173 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/PoiUtil.java 160 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/ResponseUtil.java 72 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/UaUtil.java 96 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/date/DateValue.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/date/DateValueValidator.java 59 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateordatetime/DateOrDateTimeValue.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateordatetime/DateOrDateTimeValueValidator.java 65 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateormonth/DateOrMonthValue.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateormonth/DateOrMonthValueValidator.java 67 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateortime/DateOrTimeValue.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateortime/DateOrTimeValueValidator.java 65 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/datetime/DateTimeValue.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/datetime/DateTimeValueValidator.java 59 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dict/DictValue.java 66 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dict/DictValueValidator.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/flag/FlagValue.java 62 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/flag/FlagValueValidator.java 64 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/month/MonthValue.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/month/MonthValueValidator.java 60 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/mothordatetime/MonthOrDateTimeValue.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/mothordatetime/MonthOrDateTimeValueValidator.java 67 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/time/TimeValue.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/time/TimeValueValidator.java 59 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/unique/TableUniqueValue.java 88 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/unique/TableUniqueValueValidator.java 122 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/web/SnowyRequestResponseBodyMethodProcessor.java 81 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/README.md 1 ●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/pom.xml 39 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/consts/GenConstant.java 158 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/consts/GenExpEnumConstant.java 59 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/context/XnVelocityContext.java 102 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/enums/QueryTypeEnum.java 56 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/enums/TableFilteredFieldsEnum.java 63 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/param/TableField.java 72 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/param/XnCodeGenParam.java 111 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/tool/JavaEffTool.java 54 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/tool/JavaSqlTool.java 87 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/tool/NamingConTool.java 72 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/tool/StringDateTool.java 51 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/util/Util.java 119 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/controller/CodeGenerateController.java 160 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/controller/SysCodeGenerateConfigController.java 94 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/entity/CodeGenerate.java 100 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/entity/SysCodeGenerateConfig.java 136 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/enums/CodeGenerateExceptionEnum.java 75 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/enums/SysCodeGenerateConfigExceptionEnum.java 64 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/mapper/CodeGenerateMapper.java 58 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/mapper/SysCodeGenerateConfigMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/mapper/mapping/CodeGenerateMapper.xml 241 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/mapper/mapping/SysCodeGenerateConfigMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/param/CodeGenerateParam.java 111 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/param/SysCodeGenerateConfigParam.java 138 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/result/InforMationColumnsResult.java 58 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/result/InformationResult.java 58 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/service/CodeGenerateService.java 107 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/service/SysCodeGenerateConfigService.java 90 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/service/impl/CodeGenerateServiceImpl.java 364 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/service/impl/SysCodeGenerateConfigServiceImpl.java 159 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/Controller.java.vm 148 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/ExceptionEnum.java.vm 64 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/Manage.js.vm 86 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/Mapper.java.vm 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/Mapper.xml.vm 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/Param.java.vm 74 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/Service.java.vm 97 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/ServiceImpl.java.vm 136 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/XnMysql.sql.vm 38 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/XnOracle.sql.vm 38 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/addForm.vue.vm 216 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/editForm.vue.vm 274 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/entity.java.vm 70 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-gen/src/main/resources/template/index.vue.vm 379 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/README.md 1 ●●●● patch | view | raw | blame | history
snowy-base/snowy-system/pom.xml 75 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/AopConfig.java 86 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/CacheConfig.java 106 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/DataSourceConfig.java 90 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/FileConfig.java 72 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/MailSenderConfig.java 59 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/MybatisConfig.java 95 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/SmsSenderConfig.java 65 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/SpringSecurityConfig.java 117 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/SwaggerConfig.java 124 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/WebMvcConfig.java 158 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/aop/BusinessLogAop.java 106 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/aop/DataScopeAop.java 82 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/aop/PermissionAop.java 138 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/aop/WrapperAop.java 241 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/cache/MappingCache.java 65 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/cache/OauthCache.java 72 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/cache/ResourceCache.java 63 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/cache/UserCache.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/consts/SysExpEnumConstant.java 124 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/context/SystemContextImpl.java 218 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/AdminTypeEnum.java 56 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/DataScopeTypeEnum.java 71 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/LogSuccessStatusEnum.java 56 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/MenuOpenTypeEnum.java 66 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/MenuTypeEnum.java 61 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/MenuWeightEnum.java 56 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/NoticeStatusEnum.java 67 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/NoticeUserStatusEnum.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/OauthPlatformEnum.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/OauthSexEnum.java 61 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/SexEnum.java 61 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/VisLogTypeEnum.java 56 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/error/GlobalExceptionHandler.java 371 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/error/SnowyErrorAttributes.java 70 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/filter/RequestNoFilter.java 67 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/filter/security/JwtAuthenticationTokenFilter.java 100 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/filter/security/entrypoint/JwtAuthenticationEntryPoint.java 99 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/filter/xss/XssFilter.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/filter/xss/XssHttpServletRequestWrapper.java 91 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/jwt/JwtPayLoad.java 62 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/jwt/JwtTokenUtil.java 121 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/listener/ConstantsInitListener.java 121 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/listener/RemoveRequestParamListener.java 44 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/listener/ResourceCollectListener.java 78 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/listener/TimerTaskRunListener.java 75 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/log/LogManager.java 178 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/log/factory/LogFactory.java 196 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/log/factory/LogTaskFactory.java 133 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/mybatis/dbid/SnowyDatabaseIdProvider.java 64 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/mybatis/fieldfill/CustomMetaObjectHandler.java 102 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/mybatis/sqlfilter/DemoProfileSqlInterceptor.java 81 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/provider/CaptchaCacheServiceProvider.java 72 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/redis/FastJson2JsonRedisSerializer.java 90 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/scanner/ApiResourceScanner.java 207 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/validator/SnowyValidator.java 118 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/IndexController.java 50 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/controller/SysAppController.java 148 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/entity/SysApp.java 71 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/enums/SysAppExceptionEnum.java 85 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/mapper/SysAppMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/mapper/mapping/SysAppMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/param/SysAppParam.java 70 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/service/SysAppService.java 119 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/service/impl/SysAppServiceImpl.java 265 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/controller/SysAreaController.java 63 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/entity/SysArea.java 125 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/mapper/SysAreaMapper.java 38 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/mapper/mapping/SysAreaMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/param/SysAreaParam.java 102 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/service/SysAreaService.java 50 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/service/impl/SysAreaServiceImpl.java 59 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/auth/context/LoginContextSpringSecurityImpl.java 291 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/auth/controller/SysLoginController.java 146 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/auth/factory/LoginUserFactory.java 154 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/auth/service/AuthService.java 144 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/auth/service/impl/AuthServiceImpl.java 382 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/controller/SysConfigController.java 138 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/entity/SysConfig.java 87 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/enums/SysConfigExceptionEnum.java 90 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/mapper/SysConfigMapper.java 39 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/mapper/mapping/SysConfigMapper.xml 6 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/param/SysConfigParam.java 86 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/service/SysConfigService.java 99 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/service/impl/SysConfigServiceImpl.java 210 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/controller/SysDictDataController.java 149 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/controller/SysDictTypeController.java 172 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/entity/SysDictData.java 79 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/entity/SysDictType.java 75 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/enums/SysDictDataExceptionEnum.java 70 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/enums/SysDictTypeExceptionEnum.java 75 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/mapper/SysDictDataMapper.java 50 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/mapper/SysDictTypeMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/mapper/mapping/SysDictDataMapper.xml 17 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/mapper/mapping/SysDictTypeMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/param/SysDictDataParam.java 84 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/param/SysDictTypeParam.java 78 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/result/SysDictTreeNode.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/service/SysDictDataService.java 137 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/service/SysDictTypeService.java 128 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/service/impl/SysDictDataServiceImpl.java 256 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/service/impl/SysDictTypeServiceImpl.java 266 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/email/controler/EmailController.java 122 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/email/enums/SysEmailExceptionEnum.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/entity/SysEmp.java 62 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/entity/SysEmpExtOrgPos.java 62 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/entity/SysEmpPos.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/SysEmpExtOrgPosMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/SysEmpMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/SysEmpPosMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/mapping/SysEmpExtOrgPosMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/mapping/SysEmpMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/mapping/SysEmpPosMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/param/SysEmpParam.java 79 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/result/SysEmpInfo.java 69 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/SysEmpExtOrgPosService.java 90 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/SysEmpPosService.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/SysEmpService.java 98 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/impl/SysEmpExtOrgPosPosServiceImpl.java 157 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/impl/SysEmpPosServiceImpl.java 121 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/impl/SysEmpServiceImpl.java 164 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/controller/SysFileInfoController.java 180 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/entity/SysFileInfo.java 93 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/enums/SysFileInfoExceptionEnum.java 110 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/enums/SysFileLocationEnum.java 64 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/mapper/SysFileInfoMapper.java 40 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/mapper/mapping/SysFileInfoMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/param/SysFileInfoParam.java 103 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/result/SysFileInfoResult.java 83 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/result/SysOnlineFileInfoResult.java 187 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/service/SysFileInfoService.java 168 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/service/impl/SysFileInfoServiceImpl.java 540 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/util/DownloadUtil.java 120 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/util/OnlineDocumentUtil.java 246 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/controller/SysLogController.java 135 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/entity/SysOpLog.java 164 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/entity/SysVisLog.java 126 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/mapper/SysOpLogMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/mapper/SysVisLogMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/mapper/mapping/SysOpLogMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/mapper/mapping/SysVisLogMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/param/SysOpLogParam.java 127 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/param/SysVisLogParam.java 97 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/service/SysOpLogService.java 65 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/service/SysVisLogService.java 65 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/service/impl/SysOpLogServiceImpl.java 90 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/service/impl/SysVisLogServiceImpl.java 88 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/controller/SysMenuController.java 177 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/entity/SysMenu.java 153 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/enums/SysMenuExceptionEnum.java 120 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/mapper/SysMenuMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/mapper/mapping/SysMenuMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/node/MenuBaseTreeNode.java 86 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/param/SysMenuParam.java 148 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/service/SysMenuService.java 163 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/service/impl/SysMenuServiceImpl.java 556 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/controller/SysMachineController.java 60 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/controller/SysOnlineUserController.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/param/SysOnlineUserParam.java 83 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/result/SysMachineResult.java 172 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/result/SysOnlineUserResult.java 81 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/service/SysMachineService.java 45 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/service/SysOnlineUserService.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/service/impl/SysMachineServiceImpl.java 89 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/service/impl/SysOnlineUserServiceImpl.java 109 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/controller/SysNoticeController.java 148 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/entity/SysNotice.java 102 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/entity/SysNoticeUser.java 69 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/enums/SysNoticeExceptionEnum.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/mapper/SysNoticeMapper.java 52 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/mapper/SysNoticeUserMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/mapper/mapping/SysNoticeMapper.xml 30 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/mapper/mapping/SysNoticeUserMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/param/SysNoticeParam.java 84 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/result/SysNoticeDetailResult.java 109 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/result/SysNoticeReceiveResult.java 104 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/service/SysNoticeService.java 107 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/service/SysNoticeUserService.java 82 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/service/impl/SysNoticeServiceImpl.java 257 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/service/impl/SysNoticeUserServiceImpl.java 89 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/controller/SysOauthController.java 77 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/entity/SysOauthUser.java 105 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/enums/SysOauthExceptionEnum.java 70 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/mapper/SysOauthMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/mapper/mapping/SysOauthMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/service/SysOauthService.java 62 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/service/impl/SysOauthServiceImpl.java 206 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/controller/SysOrgController.java 169 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/entity/SysOrg.java 90 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/enums/SysOrgExceptionEnum.java 90 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/mapper/SysOrgMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/mapper/mapping/SysOrgMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/param/SysOrgParam.java 78 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/service/SysOrgService.java 128 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/service/impl/SysOrgServiceImpl.java 546 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/controller/SysPosController.java 148 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/entity/SysPos.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/enums/SysPosExceptionEnum.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/mapper/SysPosMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/mapper/mapping/SysPosMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/param/SysPosParam.java 77 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/service/SysPosService.java 105 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/service/impl/SysPosServiceImpl.java 202 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/controller/SysRoleController.java 191 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/entity/SysRole.java 79 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/entity/SysRoleDataScope.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/entity/SysRoleMenu.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/enums/SysRoleExceptionEnum.java 75 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/SysRoleDataScopeMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/SysRoleMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/SysRoleMenuMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/mapping/SysRoleDataScopeMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/mapping/SysRoleMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/mapping/SysRoleMenuMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/param/SysRoleParam.java 93 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/SysRoleDataScopeService.java 77 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/SysRoleMenuService.java 77 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/SysRoleService.java 177 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleDataScopeServiceImpl.java 88 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleMenuServiceImpl.java 88 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleServiceImpl.java 358 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/controller/SmsSenderController.java 108 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/entity/SysSms.java 92 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/enums/SmsSendExceptionEnum.java 75 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/enums/SmsSendSourceEnum.java 58 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/enums/SmsSendStatusEnum.java 66 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/enums/SmsTypeEnum.java 56 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/enums/SmsVerifyEnum.java 67 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/mapper/SysSmsMapper.java 38 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/mapper/mapping/SysSmsMapper.xml 6 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/param/SysSmsInfoParam.java 83 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/param/SysSmsSendParam.java 70 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/param/SysSmsVerifyParam.java 64 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/service/SmsSenderService.java 70 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/service/SysSmsInfoService.java 84 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/service/impl/SmsSenderServiceImpl.java 122 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/service/impl/SysSmsInfoServiceImpl.java 193 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/controller/SysTimersController.java 168 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/entity/SysTimers.java 77 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/enums/TimerJobStatusEnum.java 54 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/enums/exp/SysTimersExceptionEnum.java 74 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/mapper/SysTimersMapper.java 40 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/mapper/mapping/SysTimersMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/param/SysTimersParam.java 78 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/service/SysTimersService.java 126 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/service/TimerExeService.java 61 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/service/impl/HutoolTimerExeServiceImpl.java 82 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/service/impl/SysTimersServiceImpl.java 216 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/tasks/RefreshConstantsTaskRunner.java 71 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/tasks/SystemOutTaskRunner.java 44 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/controller/SysUserController.java 281 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/entity/SysUser.java 136 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/entity/SysUserDataScope.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/entity/SysUserRole.java 57 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/enums/SysUserExceptionEnum.java 90 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/factory/SysUserFactory.java 80 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/SysUserDataScopeMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/SysUserMapper.java 52 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/SysUserRoleMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/mapping/SysUserDataScopeMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/mapping/SysUserMapper.xml 33 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/mapping/SysUserRoleMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/param/SysUserParam.java 146 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/result/SysUserResult.java 109 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserDataScopeService.java 77 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserRoleService.java 88 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserService.java 275 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserDataScopeServiceImpl.java 86 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserRoleServiceImpl.java 106 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserServiceImpl.java 572 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/wrapper/SysUserWrapper.java 36 ●●●●● patch | view | raw | blame | history
snowy-base/snowy-system/src/main/resources/META-INF/spring.factories 4 ●●●● patch | view | raw | blame | history
snowy-main/README.md 3 ●●●●● patch | view | raw | blame | history
snowy-main/pom.xml 61 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/docker/docker-assembly.xml 10 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/SnowyApplication.java 23 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/SnowyServletInitializer.java 19 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/controller/BlogArticleController.java 148 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/entity/BlogArticle.java 165 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/enums/BlogArticleExceptionEnum.java 64 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/mapper/BlogArticleMapper.java 37 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/mapper/mapping/BlogArticleMapper.xml 5 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/param/BlogArticleParam.java 169 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/service/BlogArticleService.java 97 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/service/impl/BlogArticleServiceImpl.java 196 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/modular/controller/DatasourceExampleController.java 76 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/modular/model/AbModel.java 45 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/java/vip/xiaonuo/modular/service/DatasourceExampleService.java 89 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/resources/application-dev.yml 56 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/resources/application-local.yml 59 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/resources/application-prod.yml 56 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/resources/application.yml 76 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/resources/banner.txt 9 ●●●●● patch | view | raw | blame | history
snowy-main/src/main/resources/logback-spring.xml 120 ●●●●● patch | view | raw | blame | history
snowy-main/src/test/java/vip/xiaonuo/core/BaseJunit.java 67 ●●●●● patch | view | raw | blame | history
snowy-main/src/test/java/vip/xiaonuo/core/Test.java 39 ●●●●● patch | view | raw | blame | history
snowy-main/src/test/java/vip/xiaonuo/core/Test2.java 41 ●●●●● patch | view | raw | blame | history
snowy-main/src/test/sql/test.sql 19 ●●●●● patch | view | raw | blame | history
.gitattributes
New file
@@ -0,0 +1,4 @@
*.js linguist-language=java
*.css linguist-language=java
*.html linguist-language=java
*.btl linguist-language=java
.gitignore
New file
@@ -0,0 +1,51 @@
# Compiled class file
*.class
*.iml
*.idea
target/
logs/
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*velocity.log*
# Eclipse #
.classpath
.project
.settings/
.DS_Store
_dockerCerts/
.factorypath
node_modules/
dist/
package-lock.json
yarn.lock
rebel.xml
!DmJdbcDriver18.jar
!kingbase8-8.6.0.jar
_images
_sql
LICENSE
New file
@@ -0,0 +1,53 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
README.md
@@ -1,4 +1,288 @@
## blog-admin
<div align="center">
    <p align="center">
        <img src="./_web/public/logo.png" height="150" alt="logo"/>
    </p>
</div>
blog的后端代码
### 框架介绍
<div><h5>Snowy是一款基于国产密码算法后台权限管理系统,其中采用了SM2、SM3、SM4及签名验签,软件层面完全符合等保测评要求,让更多的人认识密码,使用密码。技术框架与密码结合,让前后分离“密”不可分。</h5></div>
<div><h4>结合SpringBoot+AntDesignVue开发,注释丰富,代码简洁。适配国产数据库(金仓、达梦)、主流数据库Mysql、Oracle、Mssql、Postgresql,小诺的产品一致追求简洁干净,一套代码搞定!同时支持国产中间件部署、麒麟操作系统、Windows、Linux部署使用。</h4></div>
<div align="center"><h5 align="center">Snowy谐音“小诺”,恰应小诺团队名称;意思为”下雪的、纯洁的“,寓意框架追求简洁至上,大道至简。</h5></div>
<p align="center">
    <p align="center">
        <a href="https://gitee.com/xiaonuobase/snowy">
            <img src="https://gitee.com/xiaonuobase/snowy/badge/star.svg?theme=dark" alt="Gitee star">
        </a>
        <a href="https://gitee.com/xiaonuobase/snowy">
            <img src="https://gitee.com/xiaonuobase/snowy/badge/fork.svg?theme=dark" alt="Gitee fork">
        </a>
        <a href="https://www.antdv.com/docs/vue/introduce-cn/">
            <img src="https://img.shields.io/badge/vue-2.x-blue.svg" alt="bootstrap">
        </a>
        <a href="https://www.antdv.com/docs/vue/introduce-cn/">
            <img src="https://img.shields.io/badge/vue--ant--design-1.5.6-blue.svg" alt="bootstrap">
        </a>
        <a href="http://spring.io/projects/spring-boot">
            <img src="https://img.shields.io/badge/spring--boot-2.3.1-green.svg" alt="spring-boot">
        </a>
        <a href="http://mp.baomidou.com">
            <img src="https://img.shields.io/badge/mybatis--plus-3.3.2-blue.svg" alt="mybatis-plus">
        </a>
        <a href="./LICENSE">
            <img src="https://img.shields.io/badge/license-Apache%202-red" alt="license Apache 2.0">
        </a>
    </p>
</p>
### 快速启动
您的开发电脑需要安装:NodeJs(14.x)、npm或yarn(最新版)建议使用yarn、Mysql5.7、Jdk1.8、Maven3.6.3(最新版)、开发工具推荐idea
* 启动前端:打开_web文件夹,进行依赖下载,运行npm install或yarn命令,再运行npm run serve或 yarn run serve
* 启动后端:打开application-local中配置数据库信息,运行SnowyApplication类即可启动
* 浏览器访问:http://localhost:81 (默认前端端口为:81,后端端口为:82)
### 快速链接
* 演示地址(superAdmin/123456):https://snowy.xiaonuo.vip
* 在线文档:https://doc.xiaonuo.vip
* layui单体版本:https://gitee.com/xiaonuobase/snowy-layui
* vue前后分离版本:https://gitee.com/xiaonuobase/snowy
* cloud微服务前后分离版本:https://gitee.com/xiaonuobase/snowy-cloud
* 我们的其他产品线同样开源,如需关注最新动态可加入QQ群聊探讨:[732230670](https://wpa.qq.com/msgrd?v=3&uin=732230670&_blank)
* 如果我们的产品能满足您的需求,很期待您给我们右上角点个 star
<img src="https://pan.xiaonuo.vip/?explorer/share/fileOut&shareID=7qwJHENw&path=%7BshareItemLink%3A7qwJHENw%7D%2F"/>
### 密码分步:fire:
| 功能                        | 算法类型          |
| ----------------------      | ------------- |
| 登录        | SM2前端加密,后端解密 |
| 登录登出日志        | SM2对登录登出日志做签名完整性保护存储    |
| 操作日志        | SM2对操作日志做签名完整性保护存储    |
| Token        | SM4(cbc模式)加密,Token不再曝光暴露    |
| 用户密码        | SM3完整性保护存储,登录时做完整性校验    |
| 用户手机号        | SM4(cbc模式)加解密使用字段脱敏    |
### 视频教程:fire:
| 序号                        | 链接地址          |
| ----------------------      | ------------- |
| 1        | [小诺开源技术团队及框架介绍](https://www.bilibili.com/video/BV1Yf4y1N7YU?from=search&seid=16730766915542181758)    |
| 2        | [小诺框架Snowy基础环境介绍](https://www.bilibili.com/video/BV1yA411c7d3)    |
| 3        | [Snowy代码下载及启动](https://www.bilibili.com/video/BV1SP4y1p7M8)    |
| 4        | [Snowy生成一个完整的前后端模块](https://www.bilibili.com/video/BV1Ry4y1G7er)    |
更新中。。。
 ### 升级计划:fire:
2.0版本正在全力打造中
### 架构原理图
* 业务架构
<p align="center">
    <img src="https://pan.xiaonuo.vip/?explorer/share/fileOut&shareID=7qwKTmEw&path=%7BshareItemLink%3A7qwKTmEw%7D%2F"/>
</p>
* 应用架构
<p align="center">
    <img src="https://pan.xiaonuo.vip/?explorer/share/fileOut&shareID=7qwKfxFw&path=%7BshareItemLink%3A7qwKfxFw%7D%2F"/>
</p>
* 数据架构
<p align="center">
    <img src="https://pan.xiaonuo.vip/?explorer/share/fileOut&shareID=7qwKrjRw&path=%7BshareItemLink%3A7qwKrjRw%7D%2F"/>
</p>
* 技术架构
<p align="center">
    <img src="https://pan.xiaonuo.vip/?explorer/share/fileOut&shareID=7qwK4RoA&path=%7BshareItemLink%3A7qwK4RoA%7D%2F"/>
</p>
* 部署架构
<p align="center">
    <img src="https://pan.xiaonuo.vip/?explorer/share/fileOut&shareID=7qwLD35w&path=%7BshareItemLink%3A7qwLD35w%7D%2F"/>
</p>
### 效果图
<table>
    <tr>
        <td><img src="https://images.gitee.com/uploads/images/2021/0413/111529_02708b11_1980003.png"/></td>
        <td><img src="https://images.gitee.com/uploads/images/2021/0413/111909_5957b35a_1980003.png"/></td>
    </tr>
    <tr>
        <td><img src="https://images.gitee.com/uploads/images/2021/0413/112254_5e8a3a0b_1980003.png"/></td>
        <td><img src="https://images.gitee.com/uploads/images/2021/0413/112510_90191be8_1980003.png"/></td>
    </tr>
    <tr>
        <td><img src="https://images.gitee.com/uploads/images/2021/0413/112640_73ea49e9_1980003.png"/></td>
        <td><img src="https://images.gitee.com/uploads/images/2021/0413/112804_a21e5aef_1980003.png"/></td>
    </tr>
</table>
### 框架亮点及优势
1. 模块化架构设计,层次清晰,业务层推荐写到单独模块,框架升级不影响业务。
```
模块树
├─snowy                    ->项目工程
│  ├─snowy-base            ->框架基础模块
│       ├─snowy-core       ->核心模块
│       ├─snowy-gen        ->代码生成
│       ├─snowy-system     ->基础业务
│  ├─snowy-main            ->业务开始模块
│       ├─业务             ->您的业务
```
2、独创前端字典翻译
全部字典数据储存前端store,后端接口数据统一过滤器翻译
下拉框,多选框等取值只需1行代码:('dictData'为过滤器名称,'sex'为字典类型code)返回数组字典
```
this.$options.filters['dictData']('sex')
或直接给值
{{ code | dictData }}
```
列表数据中字典翻译:('code'为字典类型唯一code,'value'为待翻译的值)返回name
```
{{ code | dictType(value) }}
```
3、独创的数据权限范围机制
数据范围的分配也来自于给用户单独分配的数据范围,最终决定用户有几个公司的数据范围的是,用户拥有的角色的数据范围 + 用户直接分配的数据范围
若一个用户有多个角色,系统最终判定用户有哪些数据范围是以多个角色和用户数据范围的 并集 为准。
仅通过注解就可以获取当前用户的数据范围,不强制联查sql可根据业务需求极其灵活的使用
```
@DataScope
```
param类继承baseparam,使用param.getDadaScope即可获取到数据权限列表
```
@EqualsAndHashCode(callSuper = true)
@Data
public class SysUserParam extends BaseParam {
```
4、独创的文件预览系统
支持txt.doc.docx.ppt.pptx.xls.xlsx.pdf.png.jpg.jpeg.bmp.gif等
预览速度快,兼容性好,支持常见文本格式.只需在运行环境一键安装libreoffice即可,运行简单,操作方便。
```
#libreoffice文档在线预览配置
# CentOS 下安装 libreoffice:
# 安装:yum -y install libreoffice
# Linux 中文字体乱码解决:
# 1、上传 C:\Windows\Fonts 下的字体到 /usr/share/fonts/windows 目录
# 2、执行命令: chmod 644 /usr/share/fonts/windows/* && fc-cache -fv
jodconverter:
  local:
    #暂时关闭预览,启动时会有点慢
    enabled: false
    #设置libreoffice主目录 linux地址如:/usr/lib64/libreoffice
    office-home: C:\Program Files\LibreOffice
    #开启多个libreoffice进程,每个端口对应一个进程
    port-numbers: 8100
    #libreoffice进程重启前的最大进程数
    max-tasks-per-process: 100
```
5、其他优势
前后端分离架构,分离开发,分离部署,前后端互不影响。
前端技术采用vue + antdvPro + axios。
后端采用spring boot + mybatis-plus + hutool等,开源可靠。
基于spring security(jwt) + 用户UUID双重认证。
基于AOP实现的接口粒度的鉴权,最细粒度过滤权限资源。
基于hibernate validator实现的校验框架,支持自定义校验注解。
提供Request-No的响应header快速定位线上异常问题。
在线用户可查,可在线踢人,同账号登录可同时在线,可单独在线(通过系统参数配置)。
支持前端 + 后端在线代码生成。
文件,短信,缓存,邮件等,利用接口封装,方便拓展。
短信默认使用阿里云sms,缓存默认使用内存缓存。
### 框架说明及后续补充
* 纯手研发搭建框架脚手架,在自己用的时候,也为各位小伙伴打下坚固的接私活利器。
* 后续我们会行发多个版本,将适配多个数据库环境,国产化环境,并且根据多年经验会出相关系统中用到的案例,提供给大家使用!
* 如需了解我们更多,请移步官网:https://xiaonuo.vip
* 当然,有问题讨论的小伙伴还可以加入我们的QQ技术群:[732230670](https://wpa.qq.com/msgrd?v=3&uin=732230670&_blank),一起学习讨论。
### 详细功能
1. 主控面板、控制台页面,可进行工作台,分析页,统计等功能的展示。
2. 用户管理、对企业用户和系统管理员用户的维护,可绑定用户职务,机构,角色,数据权限等。
3. 应用管理、通过应用来控制不同维度的菜单展示。
4. 机构管理、公司组织架构维护,支持多层级结构的树形结构。
5. 职位管理、用户职务管理,职务可作为用户的一个标签,职务目前没有和权限等其他功能挂钩。
6. 菜单管理、菜单目录,菜单,和按钮的维护是权限控制的基本单位。
7. 角色管理、角色绑定菜单后,可限制相关角色的人员登录系统的功能范围。角色也可以绑定数据授权范围。
8. 字典管理、系统内各种枚举类型的维护。
9. 访问日志、用户的登录和退出日志的查看和管理。
10. 操作日志、用户的操作业务的日志的查看和管理。
11. 服务监控、服务器的运行状态,Java虚拟机信息,jvm等数据的查看。
12. 在线用户、当前系统在线用户的查看。
13. 数据监控、druid控制台功能,可查看sql的运行信息。
14. 公告管理、系统的公告的管理。
15. 文件管理、文件的上传下载查看等操作,文件可使用本地存储,阿里云oss,腾讯cos接入,支持拓展。
16. 定时任务、定时任务的维护,通过cron表达式控制任务的执行频率。
17. 系统配置、系统运行的参数的维护,参数的配置与系统运行机制息息相关。
18. 邮件发送、发送邮件功能。
19. 短信发送、短信发送功能,可使用阿里云sms,腾讯云sms,支持拓展。
### 官方微信群
##### 因群达到200人以上,需加微信拉群
<table>
    <tr>
        <td>微信群</td>
        <td><img src="https://pan.xiaonuo.vip/?explorer/share/fileOut&shareID=7qwFVcdA&path=%7BshareItemLink%3A7qwFVcdA%7D%2F" width="120"/></td>
    </tr>
</table>
### 参与贡献
- 欢迎各路英雄好汉参与Snowy全系版本代码贡献,期待您的加入!
- 1.  Fork 本仓库
- 2.  新建 Feat_xxx 分支
- 3.  提交代码
- 4.  新建 Pull Request
### 更新日志:
更新日志:https://doc.xiaonuo.vip/snowy_vue/#%E6%9B%B4%E6%96%B0%E6%97%A5%E5%BF%97
### 版权说明
- Snowy生态技术框架全系版本采用 Apache License2.0协议
- 代码可用于个人项目等接私活或企业项目脚手架使用,Snowy全系开源版完全免费
- 二次开发如用于商业性质或开源竞品请先联系群主审核。
- 请不要删除和修改Snowy源码头部的版权与作者声明及出处。
### 小诺技术团队荣誉作品
| 成员组成 | 负责内容 |
| :---: | :---: |
| 俞宝山 | 全栈 |
| 徐玉祥 | 全栈 |
| 董夏雨 | 全栈 |
_web/.browserslistrc
New file
@@ -0,0 +1,3 @@
> 1%
last 2 versions
not ie <= 10
_web/.editorconfig
New file
@@ -0,0 +1,39 @@
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=false
indent_style=space
indent_size=2
[{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}]
indent_style=space
indent_size=2
[{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}]
indent_style=space
indent_size=2
[{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}]
indent_style=space
indent_size=2
[*.svg]
indent_style=space
indent_size=2
[*.js.map]
indent_style=space
indent_size=2
[*.less]
indent_style=space
indent_size=2
[*.vue]
indent_style=space
indent_size=2
[{.analysis_options,*.yml,*.yaml}]
indent_style=space
indent_size=2
_web/.env
New file
@@ -0,0 +1,3 @@
NODE_ENV=production
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=http://localhost:82
_web/.env.development
New file
@@ -0,0 +1,3 @@
NODE_ENV=development
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=http://localhost:82
_web/.env.preview
New file
@@ -0,0 +1,3 @@
NODE_ENV=production
VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=http://localhost:82
_web/.eslintrc.js
New file
@@ -0,0 +1,77 @@
module.exports = {
  root: true,
  env: {
    node: true
  },
  'extends': [
    'plugin:vue/strongly-recommended',
    '@vue/standard'
  ],
  rules: {
    'no-console': 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'generator-star-spacing': 'off',
    'no-mixed-operators': 0,
    'vue/max-attributes-per-line': [
      2,
      {
        'singleline': 5,
        'multiline': {
          'max': 1,
          'allowFirstLine': false
        }
      }
    ],
    'vue/attribute-hyphenation': 0,
    'vue/html-self-closing': 0,
    'vue/component-name-in-template-casing': 0,
    'vue/html-closing-bracket-spacing': 0,
    'vue/singleline-html-element-content-newline': 0,
    'vue/no-unused-components': 0,
    'vue/multiline-html-element-content-newline': 0,
    'vue/no-use-v-if-with-v-for': 0,
    'vue/html-closing-bracket-newline': 0,
    'vue/no-parsing-error': 0,
    'no-tabs': 0,
    'quotes': [
      2,
      'single',
      {
        'avoidEscape': true,
        'allowTemplateLiterals': true
      }
    ],
    'semi': [
      2,
      'never',
      {
        'beforeStatementContinuationChars': 'never'
      }
    ],
    'no-delete-var': 2,
    'prefer-const': [
      2,
      {
        'ignoreReadBeforeAssign': false
      }
    ],
    'template-curly-spacing': 'off',
    'indent': 'off',
    "space-before-function-paren": 0,
    'no-multi-spaces': 2, //不能用多余的空格
  },
  parserOptions: {
    parser: 'babel-eslint'
  },
  overrides: [
    {
      files: [
        '**/__tests__/*.{j,t}s?(x)',
        '**/tests/unit/**/*.spec.{j,t}s?(x)'
      ],
      env: {
        jest: true
      }
    }
  ]
}
_web/.gitignore
New file
@@ -0,0 +1,3 @@
node_modules/
dist/
.idea/
_web/.prettierrc
New file
@@ -0,0 +1,5 @@
{
  "printWidth": 120,
  "semi": false,
  "singleQuote": true
}
_web/.travis.yml
New file
@@ -0,0 +1,7 @@
language: node_js
node_js:
  - 10.15.0
cache: yarn
script:
  - yarn
  - yarn run lint --no-fix && yarn run build
_web/LICENSE
New file
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Anan Yang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
_web/babel.config.js
New file
@@ -0,0 +1,28 @@
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)
const plugins = []
if (IS_PROD) {
  plugins.push('transform-remove-console')
}
// lazy load ant-design-vue
// if your use import on Demand, Use this code
plugins.push(['import', {
  'libraryName': 'ant-design-vue',
  'libraryDirectory': 'es',
  'style': true // `style: true` 会加载 less 文件
}])
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset',
    [
      '@babel/preset-env',
      {
        'useBuiltIns': 'entry',
        'corejs': 3
      }
    ]
  ],
  plugins
}
_web/config/plugin.config.js
New file
@@ -0,0 +1,46 @@
const ThemeColorReplacer = require('webpack-theme-color-replacer')
const generate = require('@ant-design/colors/lib/generate').default
const getAntdSerials = (color) => {
  // 淡化(即less的tint)
  const lightens = new Array(9).fill().map((t, i) => {
    return ThemeColorReplacer.varyColor.lighten(color, i / 10)
  })
  const colorPalettes = generate(color)
  const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace('#', '')).join(',')
  return lightens.concat(colorPalettes).concat(rgb)
}
const themePluginOption = {
  fileName: 'css/theme-colors-[contenthash:8].css',
  matchColors: getAntdSerials('#1890ff'), // 主色系列
  // 改变样式选择器,解决样式覆盖问题
  changeSelector (selector) {
    switch (selector) {
      case '.ant-calendar-today .ant-calendar-date':
        return ':not(.ant-calendar-selected-date):not(.ant-calendar-selected-day)' + selector
      case '.ant-btn:focus,.ant-btn:hover':
        return '.ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger)'
      case '.ant-btn.active,.ant-btn:active':
        return '.ant-btn.active:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:active:not(.ant-btn-primary):not(.ant-btn-danger)'
      case '.ant-steps-item-process .ant-steps-item-icon > .ant-steps-icon':
      case '.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon':
        return ':not(.ant-steps-item-process)' + selector
      case '.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover':
      case '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal > .ant-menu-submenu-selected,.ant-menu-horizontal > .ant-menu-submenu:hover':
        return '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu:hover'
      case '.ant-menu-horizontal > .ant-menu-item-selected > a':
      case '.ant-menu-horizontal>.ant-menu-item-selected>a':
        return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item-selected > a'
      case '.ant-menu-horizontal > .ant-menu-item > a:hover':
      case '.ant-menu-horizontal>.ant-menu-item>a:hover':
        return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item > a:hover'
      default :
        return selector
    }
  }
}
const createThemeColorReplacerPlugin = () => new ThemeColorReplacer(themePluginOption)
module.exports = createThemeColorReplacerPlugin
_web/jest.config.js
New file
@@ -0,0 +1,23 @@
module.exports = {
  moduleFileExtensions: [
    'js',
    'jsx',
    'json',
    'vue'
  ],
  transform: {
    '^.+\\.vue$': 'vue-jest',
    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
    '^.+\\.jsx?$': 'babel-jest'
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  snapshotSerializers: [
    'jest-serializer-vue'
  ],
  testMatch: [
    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
  ],
  testURL: 'http://localhost/'
}
_web/jsconfig.json
New file
@@ -0,0 +1,11 @@
{
  "compilerOptions": {
    "target": "es6",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "exclude": ["node_modules", "dist"],
  "include": ["src/**/*"]
}
_web/package.json
New file
@@ -0,0 +1,88 @@
{
  "name": "vue-antd-pro",
  "version": "2.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "test:unit": "vue-cli-service test:unit",
    "build:preview": "vue-cli-service build --mode preview",
    "postinstall": "opencollective-postinstall"
  },
  "dependencies": {
    "@antv/data-set": "^0.10.2",
    "ant-design-vue": "1.5.0-rc.6",
    "axios": "^0.19.0",
    "babel-polyfill": "^6.26.0",
    "clipboard": "^2.0.6",
    "compression-webpack-plugin": "5.0.1",
    "core-js": "^3.1.2",
    "crypto-js": "^4.0.0",
    "default-passive-events": "^1.0.10",
    "enquire.js": "^2.1.6",
    "font-awesome": "^4.7.0",
    "jquery": "^3.5.1",
    "lodash.clonedeep": "^4.5.0",
    "lodash.get": "^4.4.2",
    "lodash.pick": "^4.4.0",
    "md5": "^2.2.1",
    "mockjs2": "1.0.8",
    "moment": "^2.24.0",
    "nprogress": "^0.2.0",
    "print-js": "^1.0.63",
    "raphael": "^2.3.0",
    "screenfull": "^5.1.0",
    "sm-crypto": "^0.3.6",
    "viser-vue": "^2.4.6",
    "vue": "2.6.10",
    "vue-clipboard2": "^0.2.1",
    "vue-codemirror-lite": "^1.0.4",
    "vue-cropper": "0.4.9",
    "vue-ls": "^3.2.1",
    "vue-quill-editor": "^3.0.6",
    "vue-router": "^3.1.2",
    "vue-svg-component-runtime": "^1.0.1",
    "vuedraggable": "^2.23.2",
    "vuex": "^3.1.1",
    "wangeditor": "^3.1.1"
  },
  "devDependencies": {
    "@ant-design/colors": "^3.2.1",
    "@vue/cli-plugin-babel": "^4.0.4",
    "@vue/cli-plugin-eslint": "^4.0.4",
    "@vue/cli-plugin-router": "^4.0.4",
    "@vue/cli-plugin-unit-jest": "^4.0.4",
    "@vue/cli-plugin-vuex": "^4.0.4",
    "@vue/cli-service": "^4.0.4",
    "@vue/eslint-config-prettier": "^5.0.0",
    "@vue/eslint-config-standard": "^4.0.0",
    "@vue/test-utils": "^1.0.0-beta.29",
    "babel-eslint": "^10.0.1",
    "babel-plugin-import": "^1.13.0",
    "babel-plugin-transform-remove-console": "^6.9.4",
    "eslint": "^6.8.0",
    "eslint-plugin-html": "^5.0.0",
    "eslint-plugin-prettier": "^3.1.0",
    "eslint-plugin-vue": "^5.2.3",
    "less": "^3.0.4",
    "less-loader": "^5.0.0",
    "opencollective": "^1.0.3",
    "opencollective-postinstall": "^2.0.2",
    "prettier": "^1.18.2",
    "vue-svg-icon-loader": "^2.1.1",
    "vue-template-compiler": "2.6.10",
    "webpack-theme-color-replacer": "1.3.18"
  },
  "collective": {
    "type": "opencollective",
    "url": "https://opencollective.com/ant-design-pro-vue"
  },
  "main": ".eslintrc.js",
  "directories": {
    "test": "tests"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}
_web/postcss.config.js
New file
@@ -0,0 +1,5 @@
module.exports = {
  plugins: {
    autoprefixer: {}
  }
}
_web/public/avatar2.jpg
_web/public/index.html
New file
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>logo.png">
  <title>Snowy快速开发平台</title>
  <style>.first-loading-wrp{display:flex;justify-content:center;align-items:center;flex-direction:column;min-height:420px;height:100%}.first-loading-wrp>h1{font-size:128px}.first-loading-wrp .loading-wrp{padding:98px;display:flex;justify-content:center;align-items:center}.dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:32px;width:32px;height:32px;box-sizing:border-box}.dot i{width:14px;height:14px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.dot i:nth-child(1){top:0;left:0}.dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style>
  <!-- <script type="text/javascript" src="https://onlyoffice.xiaonuo.vip/web-apps/apps/api/documents/api.js"></script>  -->
  <!-- require cdn assets css -->
  <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
  <link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
  <% } %>
</head>
<body>
<noscript>
  <strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
  <div class="first-loading-wrp">
    <h1>Snowy</h1>
    <div class="loading-wrp">
      <span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
    </div>
    <div style="display: flex; justify-content: center; align-items: center;">Snowy快速开发平台</div>
  </div>
</div>
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<!-- built files will be auto injected -->
</body>
</html>
_web/public/loading/loading.css
New file
@@ -0,0 +1 @@
#preloadingAnimation{position:fixed;left:0;top:0;height:100%;width:100%;background:#ffffff;user-select:none;z-index: 9999;overflow: hidden}.lds-roller{display:inline-block;position:relative;left:50%;top:50%;transform:translate(-50%,-50%);width:64px;height:64px;}.lds-roller div{animation:lds-roller 1.2s cubic-bezier(0.5,0,0.5,1) infinite;transform-origin:32px 32px;}.lds-roller div:after{content:" ";display:block;position:absolute;width:6px;height:6px;border-radius:50%;background:#13c2c2;margin:-3px 0 0 -3px;}.lds-roller div:nth-child(1){animation-delay:-0.036s;}.lds-roller div:nth-child(1):after{top:50px;left:50px;}.lds-roller div:nth-child(2){animation-delay:-0.072s;}.lds-roller div:nth-child(2):after{top:54px;left:45px;}.lds-roller div:nth-child(3){animation-delay:-0.108s;}.lds-roller div:nth-child(3):after{top:57px;left:39px;}.lds-roller div:nth-child(4){animation-delay:-0.144s;}.lds-roller div:nth-child(4):after{top:58px;left:32px;}.lds-roller div:nth-child(5){animation-delay:-0.18s;}.lds-roller div:nth-child(5):after{top:57px;left:25px;}.lds-roller div:nth-child(6){animation-delay:-0.216s;}.lds-roller div:nth-child(6):after{top:54px;left:19px;}.lds-roller div:nth-child(7){animation-delay:-0.252s;}.lds-roller div:nth-child(7):after{top:50px;left:14px;}.lds-roller div:nth-child(8){animation-delay:-0.288s;}.lds-roller div:nth-child(8):after{top:45px;left:10px;}#preloadingAnimation .load-tips{color: #13c2c2;font-size:2rem;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin-top:80px;text-align:center;width:400px;height:64px;}  @keyframes lds-roller{0%{transform:rotate(0deg);} 100%{transform:rotate(360deg);}}
_web/public/loading/loading.html
New file
@@ -0,0 +1 @@
<div id="preloadingAnimation"><div class=lds-roller><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div><div class=load-tips>Loading</div></div>
_web/public/loading/option2/html_code_segment.html
New file
@@ -0,0 +1,5 @@
<div class="preloading-animate">
  <div class="preloading-wrapper">
    <svg class="preloading-balls" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><circle cx="67.802" cy="59.907" r="6" fill="#51CACC"><animate attributeName="cx" values="75;57.72542485937369" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="50;73.77641290737884" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#51CACC;#9DF871" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="46.079" cy="69.992" r="6" fill="#9DF871"><animate attributeName="cx" values="57.72542485937369;29.774575140626318" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="73.77641290737884;64.69463130731182" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#9DF871;#E0FF77" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="29.775" cy="52.449" r="6" fill="#E0FF77"><animate attributeName="cx" values="29.774575140626318;29.774575140626315" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="64.69463130731182;35.30536869268818" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#E0FF77;#DE9DD6" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="41.421" cy="31.521" r="6" fill="#DE9DD6"><animate attributeName="cx" values="29.774575140626315;57.72542485937368" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="35.30536869268818;26.22358709262116" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#DE9DD6;#FF708E" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="64.923" cy="36.13" r="6" fill="#FF708E"><animate attributeName="cx" values="57.72542485937368;75" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="26.22358709262116;49.99999999999999" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#FF708E;#51CACC" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle></svg>
  </div>
</div>
_web/public/loading/option2/loading.css
New file
@@ -0,0 +1 @@
.preloading-animate{background:#ffffff;width:100%;height:100%;position:fixed;left:0;top:0;z-index:299;}.preloading-animate .preloading-wrapper{position:absolute;width:5rem;height:5rem;left:50%;top:50%;transform:translate(-50%,-50%);}.preloading-animate .preloading-wrapper .preloading-balls{font-size:5rem;}
_web/public/loading/option2/loading.svg
New file
@@ -0,0 +1 @@
<svg class="preloading-balls" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><circle cx="67.802" cy="59.907" r="6" fill="#51CACC"><animate attributeName="cx" values="75;57.72542485937369" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="50;73.77641290737884" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#51CACC;#9DF871" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="46.079" cy="69.992" r="6" fill="#9DF871"><animate attributeName="cx" values="57.72542485937369;29.774575140626318" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="73.77641290737884;64.69463130731182" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#9DF871;#E0FF77" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="29.775" cy="52.449" r="6" fill="#E0FF77"><animate attributeName="cx" values="29.774575140626318;29.774575140626315" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="64.69463130731182;35.30536869268818" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#E0FF77;#DE9DD6" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="41.421" cy="31.521" r="6" fill="#DE9DD6"><animate attributeName="cx" values="29.774575140626315;57.72542485937368" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="35.30536869268818;26.22358709262116" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#DE9DD6;#FF708E" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="64.923" cy="36.13" r="6" fill="#FF708E"><animate attributeName="cx" values="57.72542485937368;75" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="26.22358709262116;49.99999999999999" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#FF708E;#51CACC" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle></svg>
_web/public/logo.png
_web/src/App.vue
New file
@@ -0,0 +1,47 @@
<template>
  <a-config-provider :locale="locale">
    <div id="app" class="app app1">
      <router-view class="scrollbar"/>
    </div>
  </a-config-provider>
</template>
<script>
import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
import { AppDeviceEnquire } from '@/utils/mixin'
export default {
  mixins: [AppDeviceEnquire],
  data () {
    return {
      locale: zhCN
    }
  },
  mounted () {
  }
}
</script>
<style>
  .app {
    overflow: auto;
    border  : none;
  }
  .scrollbar {
    margin: 0 auto;
  }
  .app1::-webkit-scrollbar {
    /*滚动条整体样式*/
    width : 8px;  /*高宽分别对应横竖滚动条的尺寸*/
  }
  .app1::-webkit-scrollbar-thumb {
    /*滚动条里面小方块*/
    border-radius: 6px;
    background   : #aaa;
  }
  .app1::-webkit-scrollbar-track {
    /*滚动条里面轨道*/
    border-radius: 8px;
    background   : #FFFFFF;
  }
</style>
_web/src/api/modular/gen/codeGenerateManage.js
New file
@@ -0,0 +1,106 @@
/**
 * 代码生成
 *
 * @author yubaoshan
 * @date 2020/12/23 15:00
 */
import { axios } from '@/utils/request'
/**
 * 查询列表
 *
 * @author yubaoshan
 * @date 2020/12/23 15:00
 */
export function codeGeneratePage (parameter) {
  return axios({
    url: '/codeGenerate/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 增加
 *
 * @author yubaoshan
 * @date 2020/12/23 15:00
 */
export function codeGenerateAdd (parameter) {
  return axios({
    url: '/codeGenerate/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑
 *
 * @author yubaoshan
 * @date 2020/12/23 15:00
 */
export function codeGenerateEdit (parameter) {
  return axios({
    url: '/codeGenerate/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除
 *
 * @author yubaoshan
 * @date 2020/12/23 15:00
 */
export function codeGenerateDelete (parameter) {
  return axios({
    url: '/codeGenerate/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 查询当前数据库用户下的所有表
 *
 * @author yubaoshan
 * @date 2020/12/23 15:00
 */
export function codeGenerateInformationList (parameter) {
  return axios({
    url: '/codeGenerate/InformationList',
    method: 'get',
    params: parameter
  })
}
/**
 * 本地生成
 *
 * @author yubaoshan
 * @date 2020/12/23 15:00
 */
export function codeGenerateRunLocal (parameter) {
  return axios({
    url: '/codeGenerate/runLocal',
    method: 'post',
    data: parameter
  })
}
/**
 * 压缩包方式下载
 *
 * @author yubaoshan
 * @date 2020/12/23 15:00
 */
export function codeGenerateRunDown (parameter) {
  return axios({
    url: '/codeGenerate/runDown',
    method: 'get',
    params: parameter,
    responseType: 'blob'
  })
}
_web/src/api/modular/gen/sysCodeGenerateConfigManage.js
New file
@@ -0,0 +1,29 @@
import { axios } from '@/utils/request'
/**
 * 代码生成详细配置列表
 *
 * @author yubaoshan
 * @date 2021-02-06 20:19:49
 */
export function sysCodeGenerateConfigList (parameter) {
  return axios({
    url: '/sysCodeGenerateConfig/list',
    method: 'get',
    params: parameter
  })
}
/**
 * 编辑代码生成详细配置
 *
 * @author yubaoshan
 * @date 2021-02-06 20:19:49
 */
export function sysCodeGenerateConfigEdit (parameter) {
  return axios({
    url: '/sysCodeGenerateConfig/edit',
    method: 'post',
    data: parameter
  })
}
_web/src/api/modular/main/README.md
New file
@@ -0,0 +1 @@
/** 您的业务接口文件全写在此文件夹下面,升级底座直接迁移代码即可 **/
_web/src/api/modular/main/blogarticle/blogArticleManage.js
New file
@@ -0,0 +1,86 @@
import { axios } from '@/utils/request'
/**
 * 查询blog文章主体
 *
 * @author inleft
 * @date 2022-01-22 16:53:06
 */
export function blogArticlePage (parameter) {
  return axios({
    url: '/blogArticle/page',
    method: 'get',
    params: parameter
  })
}
/**
 * blog文章主体列表
 *
 * @author inleft
 * @date 2022-01-22 16:53:06
 */
export function blogArticleList (parameter) {
  return axios({
    url: '/blogArticle/list',
    method: 'get',
    params: parameter
  })
}
/**
 * 添加blog文章主体
 *
 * @author inleft
 * @date 2022-01-22 16:53:06
 */
export function blogArticleAdd (parameter) {
  return axios({
    url: '/blogArticle/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑blog文章主体
 *
 * @author inleft
 * @date 2022-01-22 16:53:06
 */
export function blogArticleEdit (parameter) {
  return axios({
    url: '/blogArticle/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除blog文章主体
 *
 * @author inleft
 * @date 2022-01-22 16:53:06
 */
export function blogArticleDelete (parameter) {
  return axios({
    url: '/blogArticle/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 导出blog文章主体
 *
 * @author inleft
 * @date 2022-01-22 16:53:06
 */
export function blogArticleExport (parameter) {
  return axios({
    url: '/blogArticle/export',
    method: 'get',
    params: parameter,
    responseType: 'blob'
  })
}
_web/src/api/modular/system/README.md
New file
@@ -0,0 +1 @@
/** 此文件夹下代码尽量不要动,底座升级直接覆盖替换 **/
_web/src/api/modular/system/appManage.js
New file
@@ -0,0 +1,92 @@
/**
 * 系统应用
 *
 * @author yubaoshan
 * @date 2020年4月23日12:10:57
 */
import { axios } from '@/utils/request'
/**
 * 系统应用列表
 *
 * @author yubaoshan
 * @date 2020年7月9日15:05:01
 */
export function getAppPage (parameter) {
  return axios({
    url: '/sysApp/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 系统应用列表
 *
 * @author yubaoshan
 * @date 2020年7月9日15:05:01
 */
export function getAppList (parameter) {
  return axios({
    url: '/sysApp/list',
    method: 'get',
    params: parameter
  })
}
/**
 * 新增系统应用
 *
 * @author yubaoshan
 * @date 2020年7月9日15:05:01
 */
export function sysAppAdd (parameter) {
  return axios({
    url: '/sysApp/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑系统应用
 *
 * @author yubaoshan
 * @param parameter
 * @returns {*}
 */
export function sysAppEdit (parameter) {
  return axios({
    url: '/sysApp/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除系统应用
 *
 * @author yubaoshan
 * @date 2020年7月9日15:05:01
 */
export function sysAppDelete (parameter) {
  return axios({
    url: '/sysApp/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 设为默认应用
 *
 * @author yubaoshan
 * @date 2020年7月9日15:05:01
 */
export function sysAppSetAsDefault (parameter) {
  return axios({
    url: '/sysApp/setAsDefault',
    method: 'post',
    data: parameter
  })
}
_web/src/api/modular/system/areaManage.js
New file
@@ -0,0 +1,16 @@
import { axios } from '@/utils/request'
/**
 * 获取区域列表
 *
 * @author xuyuxiang
 * @param parameter
 * @returns {*}
 */
export function getAreaList (parameter) {
  return axios({
    url: '/sysArea/list',
    method: 'get',
    params: parameter
  })
}
_web/src/api/modular/system/configManage.js
New file
@@ -0,0 +1,85 @@
import { axios } from '@/utils/request'
/**
 * 分页查询配置列表
 *
 * @author yubaoshan
 * @date 2020/5/25 01:57
 */
export function sysConfigPage (parameter) {
  return axios({
    url: '/sysConfig/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 添加系统参数配置
 *
 * @author yubaoshan
 * @date 2020/5/25 01:57
 */
export function sysConfigAdd (parameter) {
  return axios({
    url: '/sysConfig/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑系统参数配置
 *
 * @author yubaoshan
 * @date 2020/5/25 01:57
 */
export function sysConfigEdit (parameter) {
  return axios({
    url: '/sysConfig/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除系统参数配置
 *
 * @author yubaoshan
 * @date 2020/5/25 01:57
 */
export function sysConfigDelete (parameter) {
  return axios({
    url: '/sysConfig/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 获取字典类型下所有字典,举例,返回格式为:[{code:"M",value:"男"},{code:"F",value:"女"}]
 *
 * @author yubaoshan
 * @date 2020/5/25 02:06
 */
export function sysDictTypeDropDown (parameter) {
  return axios({
    url: '/sysDictType/dropDown',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取系统的所有任务列表
 *
 * @author yubaoshan
 * @date 2020/7/8 20:46
 */
export function sysTimersGetActionClasses (parameter) {
  return axios({
    url: '/sysTimers/getActionClasses',
    method: 'get',
    params: parameter
  })
}
_web/src/api/modular/system/dictDataManage.js
New file
@@ -0,0 +1,57 @@
import { axios } from '@/utils/request'
/**
 * 查询系统字典值
 *
 * @author yubaoshan
 * @date 2020/5/17 02:24
 */
export function sysDictDataPage (parameter) {
  return axios({
    url: '/sysDictData/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 添加系统字典值
 *
 * @author yubaoshan
 * @date 2020/5/17 02:24
 */
export function sysDictDataAdd (parameter) {
  return axios({
    url: '/sysDictData/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑系统字典值
 *
 * @author yubaoshan
 * @date 2020/5/17 02:25
 */
export function sysDictDataEdit (parameter) {
  return axios({
    url: '/sysDictData/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除系统字典值
 *
 * @author yubaoshan
 * @date 2020/5/17 02:25
 */
export function sysDictDataDelete (parameter) {
  return axios({
    url: '/sysDictData/delete',
    method: 'post',
    data: parameter
  })
}
_web/src/api/modular/system/dictManage.js
New file
@@ -0,0 +1,85 @@
import { axios } from '@/utils/request'
/**
 * 分页查询系统字典类型
 *
 * @author yubaoshan
 * @date 2020/5/17 01:46
 */
export function sysDictTypePage (parameter) {
  return axios({
    url: '/sysDictType/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 添加系统字典类型
 *
 * @author yubaoshan
 * @date 2020/5/17 01:46
 */
export function sysDictTypeAdd (parameter) {
  return axios({
    url: '/sysDictType/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑系统字典类型
 *
 * @author yubaoshan
 * @date 2020/5/17 01:50
 */
export function sysDictTypeEdit (parameter) {
  return axios({
    url: '/sysDictType/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除系统字典类型
 *
 * @author yubaoshan
 * @date 2020/5/17 01:50
 */
export function sysDictTypeDelete (parameter) {
  return axios({
    url: '/sysDictType/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 获取字典类型下所有字典,举例,返回格式为:[{code:"M",value:"男"},{code:"F",value:"女"}]
 *
 * @author yubaoshan
 * @date 2020/6/10 00:10
 */
export function sysDictTypeDropDown (parameter) {
  return axios({
    url: '/sysDictType/dropDown',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取所有字典,启动时加入缓存使用
 *
 * @author yubaoshan
 * @date 2020/6/10 00:10
 */
export function sysDictTypeTree (parameter) {
  return axios({
    url: '/sysDictType/tree',
    method: 'get',
    params: parameter
  })
}
_web/src/api/modular/system/emailManage.js
New file
@@ -0,0 +1,29 @@
import { axios } from '@/utils/request'
/**
 * 发送邮件
 *
 * @author yubaoshan
 * @date 2020/7/3 23:22
 */
export function emailSendEmail (parameter) {
  return axios({
    url: '/email/sendEmail',
    method: 'post',
    data: parameter
  })
}
/**
 * 发送html邮件
 *
 * @author yubaoshan
 * @date 2020/7/3 23:23
 */
export function emailSendEmailHtml (parameter) {
  return axios({
    url: '/email/sendEmailHtml',
    method: 'post',
    data: parameter
  })
}
_web/src/api/modular/system/fileManage.js
New file
@@ -0,0 +1,115 @@
import { axios } from '@/utils/request'
/**
 * 分页查询文件信息表
 *
 * @author yubaoshan
 * @date 2020/6/30 00:20
 */
export function sysFileInfoPage (parameter) {
  return axios({
    url: '/sysFileInfo/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取全部文件信息表
 *
 * @author yubaoshan
 * @date 2020/6/30 00:20
 */
export function sysFileInfoList (parameter) {
  return axios({
    url: '/sysFileInfo/list',
    method: 'get',
    params: parameter
  })
}
/**
 * 上传文件
 *
 * @author yubaoshan
 * @date 2020/6/30 00:20
 */
export function sysFileInfoUpload (parameter) {
  return axios({
    url: '/sysFileInfo/upload',
    method: 'post',
    data: parameter
  })
}
/**
 * 下载文件
 *
 * @author yubaoshan
 * @date 2020/6/30 00:20
 */
export function sysFileInfoDownload (parameter) {
  return axios({
    url: '/sysFileInfo/download',
    method: 'get',
    params: parameter,
    responseType: 'blob'
  })
}
/**
 * 查看图片
 *
 * @author yubaoshan
 * @date 2020/6/30 00:20
 */
export function sysFileInfoPreview (parameter) {
  return axios({
    url: '/sysFileInfo/preview',
    method: 'get',
    params: parameter,
    responseType: 'arraybuffer'
  })
}
/**
 * 查看详情文件信息表
 *
 * @author yubaoshan
 * @date 2020/6/30 00:20
 */
export function sysFileInfoDetail (parameter) {
  return axios({
    url: '/sysFileInfo/detail',
    method: 'get',
    params: parameter
  })
}
/**
 * 删除文件信息表
 *
 * @author yubaoshan
 * @date 2020/6/30 00:20
 */
export function sysFileInfoDelete (parameter) {
  return axios({
    url: '/sysFileInfo/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 获取在线文档配置
 *
 * @author yubaoshan
 * @date 2020/6/30 00:20
 */
export function sysFileInfoGetOnlineConfig (parameter) {
  return axios({
    url: '/sysFileInfo/getOnlineFileConfig',
    method: 'get',
    params: parameter
  })
}
_web/src/api/modular/system/logManage.js
New file
@@ -0,0 +1,87 @@
import { axios } from '@/utils/request'
/**
 * 查询访问日志
 *
 * @author yubaoshan
 * @date 2020/5/19 11:57
 */
export function sysVisLogPage (parameter) {
  return axios({
    url: '/sysVisLog/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 查询操作日志
 *
 * @author yubaoshan
 * @date 2020/5/19 11:57
 */
export function sysOpLogPage (parameter) {
  return axios({
    url: '/sysOpLog/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 清空访问日志
 *
 * @author yubaoshan
 * @date 2020/6/23 23:09
 */
export function sysVisLogDelete (parameter) {
  return axios({
    url: '/sysVisLog/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 清空登录日志
 *
 * @author yubaoshan
 * @date 2020/6/23 23:09
 */
export function sysOpLogDelete (parameter) {
  return axios({
    url: '/sysOpLog/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 导出登录日志
 *
 * @author yubaoshan
 * @date 2021/5/30 18:03
 */
export function sysVisLogExport (parameter) {
  return axios({
    url: '/sysVisLog/export',
    method: 'get',
    params: parameter,
    responseType: 'blob'
  })
}
/**
 * 导出操作日志
 *
 * @author yubaoshan
 * @date 2021/5/30 18:03
 */
export function sysOpLogExport (parameter) {
  return axios({
    url: '/sysOpLog/export',
    method: 'get',
    params: parameter,
    responseType: 'blob'
  })
}
_web/src/api/modular/system/loginManage.js
New file
@@ -0,0 +1,125 @@
/**
 * 系统应用
 *
 * @author yubaoshan
 * @date 2020/5/26 19:06
 */
import { axios } from '@/utils/request'
/**
 * 登录
 *
 * @author yubaoshan
 * @date 2020/5/26 19:06
 */
export function login (parameter) {
  // 密码采用sm2加密传输密码
  const sm2 = require('sm-crypto').sm2
  const publicKey = '04298364ec840088475eae92a591e01284d1abefcda348b47eb324bb521bb03b0b2a5bc393f6b71dabb8f15c99a0050818b56b23f31743b93df9cf8948f15ddb54'
  const encryptData = sm2.doEncrypt(parameter.password, publicKey, 1)
  parameter.password = encryptData
  return axios({
    url: '/login',
    method: 'post',
    data: parameter
  })
}
/**
 * 登出
 *
 * @author yubaoshan
 * @date 2020/5/26 19:07
 */
export function logout (parameter) {
  return axios({
    url: '/logout',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取登录用户信息
 *
 * @author yubaoshan
 * @date 2020/5/26 19:08
 */
export function getLoginUser (parameter) {
  return axios({
    url: '/getLoginUser',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取租户开关
 *
 * @author yubaoshan
 * @date 2020/9/5 1:24
 */
export function getTenantOpen (parameter) {
  return axios({
    url: '/getTenantOpen',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取短信验证码
 *
 * @author yubaoshan
 * @date 2020/5/26 19:29
 */
export function getSmsCaptcha (parameter) {
  return axios({
    url: '/getSmsCaptcha',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取验证码开关
 *
 * @author Jax
 * @date 2021/1/22 00:00
 */
export function getCaptchaOpen (parameter) {
  return axios({
    url: '/getCaptchaOpen',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取验证图片 以及token
 *
 * @author Jax
 * @date 2021/1/22 00:00
 */
export function reqGet(data) {
  return axios({
    url: '/captcha/get',
    method: 'post',
    data
  })
}
/**
 * 滑动或者点选验证
 *
 * @author Jax
 * @date 2021/1/22 00:00
 */
export function reqCheck(data) {
  return axios({
    url: '/captcha/check',
    method: 'post',
    data
  })
}
_web/src/api/modular/system/machineManage.js
New file
@@ -0,0 +1,15 @@
import { axios } from '@/utils/request'
/**
 * 系统属性监控
 *
 * @author yubaoshan
 * @date 2020/6/8 19:47
 */
export function sysMachineQuery (parameter) {
  return axios({
    url: '/sysMachine/query',
    method: 'get',
    params: parameter
  })
}
_web/src/api/modular/system/menuManage.js
New file
@@ -0,0 +1,114 @@
import { axios } from '@/utils/request'
/**
 * 获取菜单列表
 *
 * @author yubaoshan
 * @param parameter
 * @returns {*}
 */
export function getMenuList (parameter) {
  return axios({
    url: '/sysMenu/list',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取系统菜单树,用于新增,编辑时选择上级节点
 *
 * @author yubaoshan
 * @date 2020/4/23 12:22
 */
export function getMenuTree (parameter) {
  return axios({
    url: '/sysMenu/tree',
    method: 'get',
    params: parameter
  })
}
/**
 * 增加菜单
 *
 * @author yubaoshan
 * @date 2020/4/24 23:23
 */
export function sysMenuAdd (parameter) {
  return axios({
    url: '/sysMenu/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 增加菜单
 *
 * @author yubaoshan
 * @date 2020/4/24 23:23
 */
export function sysMenuDelete (parameter) {
  return axios({
    url: '/sysMenu/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 查看菜单详情
 *
 * @author yubaoshan
 * @date 2020/4/25 01:11
 */
export function sysMenuDetail (parameter) {
  return axios({
    url: '/sysMenu/detail',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑系统菜单
 *
 * @author yubaoshan
 * @date 2020/4/25 01:11
 */
export function sysMenuEdit (parameter) {
  return axios({
    url: '/sysMenu/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 获取系统菜单树,用于给角色授权时选择
 *
 * @author yubaoshan
 * @date 2020/6/2 17:30
 */
export function SysMenuTreeForGrant (parameter) {
  return axios({
    url: '/sysMenu/treeForGrant',
    method: 'get',
    params: parameter
  })
}
/**
 * 根据系统切换菜单
 *
 * @author yubaoshan
 * @date 2020/6/28 15:25
 */
export function sysMenuChange (parameter) {
  return axios({
    url: '/sysMenu/change',
    method: 'post',
    data: parameter
  })
}
_web/src/api/modular/system/noticeManage.js
New file
@@ -0,0 +1,85 @@
import { axios } from '@/utils/request'
/**
 * 查询系统通知公告
 *
 * @author yubaoshan
 * @date 2020/6/30 01:56
 */
export function sysNoticePage (parameter) {
  return axios({
    url: '/sysNotice/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 添加系统通知公告
 *
 * @author yubaoshan
 * @date 2020/6/30 01:56
 */
export function sysNoticeAdd (parameter) {
  return axios({
    url: '/sysNotice/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑系统通知公告
 *
 * @author yubaoshan
 * @date 2020/6/30 01:56
 */
export function sysNoticeEdit (parameter) {
  return axios({
    url: '/sysNotice/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除系统通知公告
 *
 * @author yubaoshan
 * @date 2020/6/30 01:56
 */
export function sysNoticeDelete (parameter) {
  return axios({
    url: '/sysNotice/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 通知公告详情
 *
 * @author yubaoshan
 * @date 2020/6/30 01:56
 */
export function sysNoticeDetail (parameter) {
  return axios({
    url: '/sysNotice/detail',
    method: 'get',
    params: parameter
  })
}
/**
 * 修改状态
 *
 * @author yubaoshan
 * @date 2020/7/30 02:23
 */
export function sysNoticeChangeStatus (parameter) {
  return axios({
    url: '/sysNotice/changeStatus',
    method: 'post',
    data: parameter
  })
}
_web/src/api/modular/system/noticeReceivedManage.js
New file
@@ -0,0 +1,15 @@
import { axios } from '@/utils/request'
/**
 * 查询我收到的系统通知公告
 *
 * @author yubaoshan
 * @date 2020/7/3 03:02
 */
export function sysNoticeReceived (parameter) {
  return axios({
    url: '/sysNotice/received',
    method: 'get',
    params: parameter
  })
}
_web/src/api/modular/system/onlineUserManage.js
New file
@@ -0,0 +1,29 @@
import { axios } from '@/utils/request'
/**
 * 在线用户列表
 *
 * @author yubaoshan
 * @date 2020/6/8 11:11
 */
export function sysOnlineUserList (parameter) {
  return axios({
    url: '/sysOnlineUser/list',
    method: 'get',
    params: parameter
  })
}
/**
 * 强制下线
 *
 * @author yubaoshan
 * @date 2020/6/8 11:11
 */
export function sysOnlineUserForceExist (parameter) {
  return axios({
    url: '/sysOnlineUser/forceExist',
    method: 'post',
    data: parameter
  })
}
_web/src/api/modular/system/orgManage.js
New file
@@ -0,0 +1,100 @@
import { axios } from '@/utils/request'
/**
 * 获取机构树
 *
 * @author yubaoshan
 * @date 2020/4/26 12:08
 */
export function getOrgTree (parameter) {
  return axios({
    url: '/sysOrg/tree',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取机构列表
 *
 * @author yubaoshan
 * @date 2020/5/11 12:59
 */
export function getOrgList (parameter) {
  return axios({
    url: '/sysOrg/list',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取机构列表
 *
 * @author yubaoshan
 * @date 2020/5/11 16:17
 */
export function getOrgPage (parameter) {
  return axios({
    url: '/sysOrg/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 新增机构
 *
 * @author yubaoshan
 * @date 2020/5/11 13:56
 */
export function sysOrgAdd (parameter) {
  return axios({
    url: '/sysOrg/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑机构
 *
 * @author yubaoshan
 * @date 2020/5/11 13:56
 */
export function sysOrgEdit (parameter) {
  return axios({
    url: '/sysOrg/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除机构
 *
 * @author yubaoshan
 * @date 2020/5/11 12:59
 */
export function sysOrgDelete (parameter) {
  return axios({
    url: '/sysOrg/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 导出系统机构
 *
 * @author yubaoshan
 * @date 2021/5/30 12:58
 */
export function sysOrgExport (parameter) {
  return axios({
    url: '/sysOrg/export',
    method: 'get',
    params: parameter,
    responseType: 'blob'
  })
}
_web/src/api/modular/system/posManage.js
New file
@@ -0,0 +1,86 @@
import { axios } from '@/utils/request'
/**
 * 查询系统职位
 *
 * @author yubaoshan
 * @date 2020/5/25 01:31
 */
export function sysPosPage (parameter) {
  return axios({
    url: '/sysPos/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 系统职位列表
 *
 * @author yubaoshan
 * @date 2020/6/21 23:50
 */
export function sysPosList (parameter) {
  return axios({
    url: '/sysPos/list',
    method: 'get',
    params: parameter
  })
}
/**
 * 添加系统职位
 *
 * @author yubaoshan
 * @date 2020/5/25 01:31
 */
export function sysPosAdd (parameter) {
  return axios({
    url: '/sysPos/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑系统职位
 *
 * @author yubaoshan
 * @date 2020/5/25 01:31
 */
export function sysPosEdit (parameter) {
  return axios({
    url: '/sysPos/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除系统职位
 *
 * @author yubaoshan
 * @date 2020/5/25 01:31
 */
export function sysPosDelete (parameter) {
  return axios({
    url: '/sysPos/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 导出系统职位
 *
 * @author yubaoshan
 * @date 2021/5/29 16:19
 */
export function sysPosExport (parameter) {
  return axios({
    url: '/sysPos/export',
    method: 'get',
    params: parameter,
    responseType: 'blob'
  })
}
_web/src/api/modular/system/roleManage.js
New file
@@ -0,0 +1,141 @@
import { axios } from '@/utils/request'
/**
 * 获取角色列表
 *
 * @author yubaoshan
 * @date 2020/5/6 11:44
 */
export function getRolePage (parameter) {
  return axios({
    url: '/sysRole/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 增加角色
 *
 * @author yubaoshan
 * @date 2020/5/6 11:44
 */
export function sysRoleAdd (parameter) {
  return axios({
    url: '/sysRole/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑角色
 *
 * @author yubaoshan
 * @date 2020/5/6 11:44
 */
export function sysRoleEdit (parameter) {
  return axios({
    url: '/sysRole/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除角色
 *
 * @author yubaoshan
 * @date 2020/5/6 17:51
 */
export function sysRoleDelete (parameter) {
  return axios({
    url: '/sysRole/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除角色
 *
 * @author yubaoshan
 * @date 2020/5/7 11:28
 */
export function sysRoleDeteil (parameter) {
  return axios({
    url: '/sysRole/detail',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取授权角色列表
 *
 * @author yubaoshan
 * @date 2020/5/26 23:59
 */
export function sysRoleDropDown (parameter) {
  return axios({
    url: '/sysRole/dropDown',
    method: 'get',
    params: parameter
  })
}
/**
 * 拥有菜单
 *
 * @author yubaoshan
 * @date 2020/6/02 19:02
 */
export function sysRoleOwnMenu (parameter) {
  return axios({
    url: '/sysRole/ownMenu',
    method: 'get',
    params: parameter
  })
}
/**
 * 授权菜单
 *
 * @author yubaoshan
 * @date 2020/6/2 21:10
 */
export function sysRoleGrantMenu (parameter) {
  return axios({
    url: '/sysRole/grantMenu',
    method: 'post',
    data: parameter
  })
}
/**
 * 拥有数据
 *
 * @author yubaoshan
 * @date 2020/6/02 21:40
 */
export function sysRoleOwnData (parameter) {
  return axios({
    url: '/sysRole/ownData',
    method: 'get',
    params: parameter
  })
}
/**
 * 授权数据
 *
 * @author yubaoshan
 * @date 2020/6/2 21:50
 */
export function sysRoleGrantData (parameter) {
  return axios({
    url: '/sysRole/grantData',
    method: 'post',
    data: parameter
  })
}
_web/src/api/modular/system/smsManage.js
New file
@@ -0,0 +1,43 @@
import { axios } from '@/utils/request'
/**
 * 发送记录查询
 *
 * @author yubaoshan
 * @date 2020/7/3 22:11
 */
export function smsPage (parameter) {
  return axios({
    url: '/sms/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 验证短信验证码
 *
 * @author yubaoshan
 * @date 2020/7/3 22:12
 */
export function sysSendLoginMessage (parameter) {
  return axios({
    url: '/sms/sendLoginMessage',
    method: 'post',
    data: parameter
  })
}
/**
 * 验证短信验证码
 *
 * @author yubaoshan
 * @date 2020/7/3 22:12
 */
export function sysValidateMessage (parameter) {
  return axios({
    url: '/sms/validateMessage',
    method: 'post',
    data: parameter
  })
}
_web/src/api/modular/system/timersManage.js
New file
@@ -0,0 +1,127 @@
import { axios } from '@/utils/request'
/**
 * 分页查询定时任务
 *
 * @author yubaoshan
 * @date 2020/7/3 03:13
 */
export function sysTimersPage (parameter) {
  return axios({
    url: '/sysTimers/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 获取全部定时任务
 *
 * @author yubaoshan
 * @date 2020/7/3 03:23
 */
export function sysTimersList (parameter) {
  return axios({
    url: '/sysTimers/list',
    method: 'get',
    params: parameter
  })
}
/**
 * 查看详情定时任务
 *
 * @author yubaoshan
 * @date 2020/7/3 03:23
 */
export function sysTimersDetail (parameter) {
  return axios({
    url: '/sysTimers/detail',
    method: 'get',
    params: parameter
  })
}
/**
 * 添加定时任务
 *
 * @author yubaoshan
 * @date 2020/7/3 03:23
 */
export function sysTimersAdd (parameter) {
  return axios({
    url: '/sysTimers/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 删除定时任务
 *
 * @author yubaoshan
 * @date 2020/7/3 03:23
 */
export function sysTimersDelete (parameter) {
  return axios({
    url: '/sysTimers/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑定时任务
 *
 * @author yubaoshan
 * @date 2020/7/3 03:23
 */
export function sysTimersEdit (parameter) {
  return axios({
    url: '/sysTimers/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 获取系统的所有任务列表
 *
 * @author yubaoshan
 * @date 2020/7/3 03:23
 */
export function sysTimersGetActionClasses (parameter) {
  return axios({
    url: '/sysTimers/getActionClasses',
    method: 'post',
    data: parameter
  })
}
/**
 * 启动定时任务
 *
 * @author yubaoshan
 * @date 2020/7/3 03:23
 */
export function sysTimersStart (parameter) {
  return axios({
    url: '/sysTimers/start',
    method: 'post',
    data: parameter
  })
}
/**
 * 停止定时任务
 *
 * @author yubaoshan
 * @date 2020/7/3 03:23
 */
export function sysTimersStop (parameter) {
  return axios({
    url: '/sysTimers/stop',
    method: 'post',
    data: parameter
  })
}
_web/src/api/modular/system/userManage.js
New file
@@ -0,0 +1,226 @@
import { axios } from '@/utils/request'
/**
 * 获取用户列表
 *
 * @author yubaoshan
 * @date 2020/4/26 12:08
 */
export function getUserPage (parameter) {
  return axios({
    url: '/sysUser/page',
    method: 'get',
    params: parameter
  })
}
/**
 * 增加用户
 *
 * @author yubaoshan
 * @date 2020/5/5 02:08
 */
export function sysUserAdd (parameter) {
  return axios({
    url: '/sysUser/add',
    method: 'post',
    data: parameter
  })
}
/**
 * 编辑用户
 *
 * @author yubaoshan
 * @date 2020/5/5 02:08
 */
export function sysUserEdit (parameter) {
  return axios({
    url: '/sysUser/edit',
    method: 'post',
    data: parameter
  })
}
/**
 * 获取用户详情
 *
 * @author yubaoshan
 * @date 2020/5/5 19:55
 */
export function sysUserDetail (parameter) {
  return axios({
    url: '/sysUser/detail',
    method: 'get',
    params: parameter
  })
}
/**
 * 删除用户
 *
 * @author yubaoshan
 * @date 2020/5/7 19:31
 */
export function sysUserDelete (parameter) {
  return axios({
    url: '/sysUser/delete',
    method: 'post',
    data: parameter
  })
}
/**
 * 导出用户
 *
 * @author yubaoshan
 * @date 2021/5/30 00:23
 */
export function sysUserExport (parameter) {
  return axios({
    url: '/sysUser/export',
    method: 'get',
    params: parameter,
    responseType: 'blob'
  })
}
/**
 * 拥有角色
 *
 * @author yubaoshan
 * @date 2020/6/3 11:58
 */
export function sysUserOwnRole (parameter) {
  return axios({
    url: '/sysUser/ownRole',
    method: 'get',
    params: parameter
  })
}
/**
 * 授权角色
 *
 * @author yubaoshan
 * @date 2020/5/26 23:59
 */
export function sysUserGrantRole (parameter) {
  return axios({
    url: '/sysUser/grantRole',
    method: 'post',
    data: parameter
  })
}
/**
 * 拥有数据
 *
 * @author yubaoshan
 * @date 2020/6/2 23:14
 */
export function sysUserOwnData (parameter) {
  return axios({
    url: '/sysUser/ownData',
    method: 'get',
    params: parameter
  })
}
/**
 * 授权数据
 *
 * @author yubaoshan
 * @date 2020/6/2 23:15
 */
export function sysUserGrantData (parameter) {
  return axios({
    url: '/sysUser/grantData',
    method: 'post',
    data: parameter
  })
}
/**
 * 修改状态
 *
 * @author yubaoshan
 * @date 2020/6/23 21:36
 */
export function sysUserChangeStatus (parameter) {
  return axios({
    url: '/sysUser/changeStatus',
    method: 'post',
    data: parameter
  })
}
/**
 * 重置密码
 *
 * @author yubaoshan
 * @date 2020/6/23 22:04
 */
export function sysUserResetPwd (parameter) {
  return axios({
    url: '/sysUser/resetPwd',
    method: 'post',
    data: parameter
  })
}
/**
 * 修改密码
 *
 * @author yubaoshan
 * @date 2020/6/25 00:25
 */
export function sysUserUpdatePwd (parameter) {
  return axios({
    url: '/sysUser/updatePwd',
    method: 'post',
    data: parameter
  })
}
/**
 * 用户选择器
 *
 * @author yubaoshan
 * @date 2020/6/25 00:25
 */
export function sysUserSelector (parameter) {
  return axios({
    url: '/sysUser/selector',
    method: 'get',
    params: parameter
  })
}
/**
 * 修改头像
 *
 * @author yubaoshan
 * @date 2020/9/20 2:21
 */
export function sysUserUpdateAvatar (parameter) {
  return axios({
    url: '/sysUser/updateAvatar',
    method: 'post',
    data: parameter
  })
}
/**
 * 更新基本信息
 *
 * @author yubaoshan
 * @date 2020/9/20 03:12
 */
export function sysUserUpdateInfo (parameter) {
  return axios({
    url: '/sysUser/updateInfo',
    method: 'post',
    data: parameter
  })
}
_web/src/assets/icons/bx-analyse.svg
New file
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1551058675966" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7872" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M85.333333 512h85.333334a340.736 340.736 0 0 1 99.712-241.621333 337.493333 337.493333 0 0 1 108.458666-72.96 346.453333 346.453333 0 0 1 261.546667-1.749334A106.154667 106.154667 0 0 0 746.666667 298.666667C805.802667 298.666667 853.333333 251.136 853.333333 192S805.802667 85.333333 746.666667 85.333333c-29.397333 0-55.978667 11.776-75.221334 30.933334-103.722667-41.514667-222.848-40.874667-325.76 2.517333a423.594667 423.594667 0 0 0-135.68 91.264 423.253333 423.253333 0 0 0-91.306666 135.637333A426.88 426.88 0 0 0 85.333333 512z m741.248 133.205333c-17.109333 40.618667-41.685333 77.141333-72.96 108.416s-67.797333 55.850667-108.458666 72.96a346.453333 346.453333 0 0 1-261.546667 1.749334A106.154667 106.154667 0 0 0 277.333333 725.333333C218.197333 725.333333 170.666667 772.864 170.666667 832S218.197333 938.666667 277.333333 938.666667c29.397333 0 55.978667-11.776 75.221334-30.933334A425.173333 425.173333 0 0 0 512 938.666667a425.941333 425.941333 0 0 0 393.258667-260.352A426.325333 426.325333 0 0 0 938.666667 512h-85.333334a341.034667 341.034667 0 0 1-26.752 133.205333z" p-id="7873"></path><path d="M512 318.378667c-106.752 0-193.621333 86.869333-193.621333 193.621333S405.248 705.621333 512 705.621333s193.621333-86.869333 193.621333-193.621333S618.752 318.378667 512 318.378667z m0 301.909333c-59.690667 0-108.288-48.597333-108.288-108.288S452.309333 403.712 512 403.712s108.288 48.597333 108.288 108.288-48.597333 108.288-108.288 108.288z" p-id="7874"></path></svg>
_web/src/assets/logo.png
_web/src/assets/logo.svg
New file
@@ -0,0 +1,203 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="500px" height="500px" viewBox="0 0 500 500" enable-background="new 0 0 500 500" xml:space="preserve">  <image id="image0" width="500" height="500" x="0" y="0"
    href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfQAAAH0CAMAAAD8CC+4AAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAADAFBMVEX////X7v+w3//L6f/U
7v8ao/+64//i8/8lqP+v3//Z8P8HnP/S7f/v+f87sf8Amf8Onv8tq//n9f/5/f9Vu//R7f8Lnf9G
tf/z+v9txf/Q7P8Mnv9dvv/8/v+Dzf+75P/M6/8Jnf94yf+a1/+85P/I6f+T1P+04f8Bmf/F6P8E
m/8Tof8Cmv+95f/C5/8Qn//G6P/X7/8ip//A5v8Dmv8hpv/a8P/l9f8yrf83r//r9/+24v9Mt//2
+/9ev/9kwf/9/v9yx/++5f+BzP+Iz/+d2P+i2v+r3f+54//B5v+k2/8Xov/K6v8mqP/f8v/c8f8k
p/89sf/w+f/s9/+e2P9Tuv/7/f/4/P9NuP8Fm/+Z1v9pw/9iwP+Fzv97yv8Gm//L6v+W1f+Q0/+s
3v8InP/O6/+M0f8Yo/8SoP/P7P+Hz//h8//W7/8epf8Knf8+sv/x+f/m9f8vrP9XvP9Bs/9/zP9x
xv/6/f+K0P9vxf+p3f91yP+c1//T7f+y4P/J6f9lwf92yP9gv/9avf9Wu/8RoP/E5//Y7/+g2f9Q
uf80rv9OuP/b8f9jwf/e8v8oqf9Cs/99y/8Uof/d8f/3/P/1+/8jp//0+/8pqf86sP/t+P/y+v9r
xP8Vof/g8//o9v/u+P/H6f/j9P/k9P/p9v81rv8bpP9Jtv/q9/90x/+P0v9sxP9DtP9Zvf8sqv9Y
vP9mwv9qw//q9v84pv8Akv9Itv8Ck//g8v8qqv8Bkv+J0P8wrP+S0/+m2/9Puf95yf+95P+o3P+t
3v/V7v85sP+n3P+i2f9hwP9kwP98y/9PuP9Es/8/sf87sP+/5v8Alf/v+P9EtP/H6P8Mnf8Im/8E
mv+x4P8Dmf8cpP8AmP+r3v+S1P9vxv9bvv9Ouf////88sv8zqf8tpf8ppP8kov/0+v9txP8gof8b
nv8Wm/8Sm/8Omf8Kl/8Glf/d8v9ZvP8Ak/9jwP8jpP8VnP/l9P88r/84q/82qv8Bk//R7P8wp/8q
of9Suf9Js/81rP+Y1f/HFJ4WAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAHdElNRQflAwMC
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
D5OidHPCce5xV2HoNy3XZLbDvBrni+tuDT0VM90xme3KOoOrYMwY/JpTKgr9TJ/oYrfXmvkG18AY
M8Gl7jstev88e6tD+xv4z9iQ5rA3Qt/e+xTa2szd0jZ0SFoCE6yPrv6UtJllm8xW9ryB54w9abYF
Hukls9W97Czu5A+1uHHD7sn+LGlwXczG3sqkr+ObULxlVY1kNGnvgE3btystuFZQGJ6xWKDbQlqz
6JK4akUPA3cZCIPNF+i2kcZOGndmOzHVwFcGxnXT6akQ3XCtL+fd46GvvqXyluFrPP1MLzVrx1fW
18BLBkx0Sxg90UcYdWa72WxbmycHG0xe4+nbew+DJl0rOWctONf138G60Fb0G0csaGEl2hOTs911
9TpNG1mgaSKDP9QShGuaC3SLaBOajSNKikJfK1PLWr1v9h20Ba3GEelLjmq7xPhH61DN4/Tf6zwi
yibq+8PEg+kaso2k/1wjmW3UBn1vmDgxOfI1vjv9x+9G/W3xdX1PmPixNmqy76L/Nqo24dwW0zKv
2TG+2FZ0dTLb7s8LQl8aQ6JejVfc3pW1CUu49F9iM7nYSvT36L9afog/1BKdtO42ot9H/tGQQfpj
M8EYT73GK57pZHWTRXP0B2YCQn2zX6b/5Cv5X+TfCH0tjDbXpAt0Heg/kJe02cQfas2Js7KOjXPp
38tqHvRr4W0SmyF7mz7Z99G/HtO05sFm/lBrfhxo8mR/lf7xm4WNfrt+LKeiNkuuNfpmH07/tHFe
01Wu0d5cWdvwyX6R/uVDGfV/mNtldmjXGXvGv1NPy0707/bH6n+ofRXabcaJA5e1RJ9RrzPbprWh
nWZcub31phA9LbX2R4cfC+0wAyDtw+hn+oXarIlOnIqaJOwtjhJ9dk1e0/KeR0L7yqA4UD3Zr9A/
OL6x6gdcMyi5qJrsimf60SFCZHPNoGRjxi4xi/6/R4aKad+GdpHB8w8v0U1ut/7+3LdC+8f4oC3d
8nTrm5yjxjAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAM
wzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzDJyf8H/RgaDtLOxBkAAAAldEVYdGRhdGU6Y3JlYXRl
ADIwMjEtMDMtMDJUMTg6MDA6MDArMDg6MDA9+rsMAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAz
LTAyVDE4OjAwOjAwKzA4OjAwTKcDsAAAACB0RVh0c29mdHdhcmUAaHR0cHM6Ly9pbWFnZW1hZ2lj
ay5vcme8zx2dAAAAGHRFWHRUaHVtYjo6RG9jdW1lbnQ6OlBhZ2VzADGn/7svAAAAGHRFWHRUaHVt
Yjo6SW1hZ2U6OkhlaWdodAA1MDB4mAPsAAAAF3RFWHRUaHVtYjo6SW1hZ2U6OldpZHRoADUwMOtp
U7EAAAAZdEVYdFRodW1iOjpNaW1ldHlwZQBpbWFnZS9wbmc/slZOAAAAF3RFWHRUaHVtYjo6TVRp
bWUAMTYxNDY3OTIwMCAf/wcAAAATdEVYdFRodW1iOjpTaXplADExMjczQkI7FGLOAAAARnRFWHRU
aHVtYjo6VVJJAGZpbGU6Ly8vYXBwL3RtcC9pbWFnZWxjL2ltZ3ZpZXcyXzlfMTYwOTkwMzUxMTcy
MzMzODZfOTJfWzBdRG9xdgAAAABJRU5ErkJggg==" ></image>
</svg>
_web/src/assets/welcome.png
_web/src/components/ArticleListContent/ArticleListContent.vue
New file
@@ -0,0 +1,89 @@
<template>
  <div class="antd-pro-components-article-list-content-index-listContent">
    <div class="description">
      <slot>
        {{ description }}
      </slot>
    </div>
    <div class="extra">
      <a-avatar :src="avatar" size="small" />
      <a :href="href">{{ owner }}</a> 发布在 <a :href="href">{{ href }}</a>
      <em>{{ updateAt | moment }}</em>
    </div>
  </div>
</template>
<script>
export default {
  name: 'ArticleListContent',
  props: {
    prefixCls: {
      type: String,
      default: 'antd-pro-components-article-list-content-index-listContent'
    },
    description: {
      type: String,
      default: ''
    },
    owner: {
      type: String,
      required: true
    },
    avatar: {
      type: String,
      required: true
    },
    href: {
      type: String,
      required: true
    },
    updateAt: {
      type: String,
      required: true
    }
  }
}
</script>
<style lang="less" scoped>
@import '../index.less';
.antd-pro-components-article-list-content-index-listContent {
  .description {
    max-width: 720px;
    line-height: 22px;
  }
  .extra {
    margin-top: 16px;
    color: @text-color-secondary;
    line-height: 22px;
    & /deep/ .ant-avatar {
      position: relative;
      top: 1px;
      width: 20px;
      height: 20px;
      margin-right: 8px;
      vertical-align: top;
    }
    & > em {
      margin-left: 16px;
      color: @disabled-color;
      font-style: normal;
    }
  }
}
@media screen and (max-width: @screen-xs) {
  .antd-pro-components-article-list-content-index-listContent {
    .extra {
      & > em {
        display: block;
        margin-top: 8px;
        margin-left: 0;
      }
    }
  }
}
</style>
_web/src/components/ArticleListContent/index.js
New file
@@ -0,0 +1,3 @@
import ArticleListContent from './ArticleListContent'
export default ArticleListContent
_web/src/components/AvatarList/Item.vue
New file
@@ -0,0 +1,46 @@
<template>
  <tooltip v-if="tips !== ''">
    <template slot="title">{{ tips }}</template>
    <avatar :size="avatarSize" :src="src" />
  </tooltip>
  <avatar v-else :size="avatarSize" :src="src" />
</template>
<script>
import Avatar from 'ant-design-vue/es/avatar'
import Tooltip from 'ant-design-vue/es/tooltip'
export default {
  name: 'AvatarItem',
  components: {
    Avatar,
    Tooltip
  },
  props: {
    tips: {
      type: String,
      default: '',
      required: false
    },
    src: {
      type: String,
      default: ''
    }
  },
  data () {
    return {
      size: this.$parent.size
    }
  },
  computed: {
    avatarSize () {
      return this.size !== 'mini' && this.size || 20
    }
  },
  watch: {
    '$parent.size' (val) {
      this.size = val
    }
  }
}
</script>
_web/src/components/AvatarList/List.vue
New file
@@ -0,0 +1,99 @@
<!--
<template>
  <div :class="[prefixCls]">
    <ul>
      <slot></slot>
      <template v-for="item in filterEmpty($slots.default).slice(0, 3)"></template>
      <template v-if="maxLength > 0 && filterEmpty($slots.default).length > maxLength">
        <avatar-item :size="size">
          <avatar :size="size !== 'mini' && size || 20" :style="excessItemsStyle">{{ `+${maxLength}` }}</avatar>
        </avatar-item>
      </template>
    </ul>
  </div>
</template>
-->
<script>
import Avatar from 'ant-design-vue/es/avatar'
import AvatarItem from './Item'
import { filterEmpty } from '@/components/_util/util'
export default {
  AvatarItem,
  name: 'AvatarList',
  components: {
    Avatar,
    AvatarItem
  },
  props: {
    prefixCls: {
      type: String,
      default: 'ant-pro-avatar-list'
    },
    /**
       * 头像大小 类型: large、small 、mini, default
       * 默认值: default
       */
    size: {
      type: [String, Number],
      default: 'default'
    },
    /**
       * 要显示的最大项目
       */
    maxLength: {
      type: Number,
      default: 0
    },
    /**
       * 多余的项目风格
       */
    excessItemsStyle: {
      type: Object,
      default: () => {
        return {
          color: '#f56a00',
          backgroundColor: '#fde3cf'
        }
      }
    }
  },
  data () {
    return {}
  },
  methods: {
    getItems (items) {
      const classString = {
        [`${this.prefixCls}-item`]: true,
        [`${this.size}`]: true
      }
      if (this.maxLength > 0) {
        items = items.slice(0, this.maxLength)
        items.push((<Avatar size={ this.size } style={ this.excessItemsStyle }>{`+${this.maxLength}`}</Avatar>))
      }
      const itemList = items.map((item) => (
        <li class={ classString }>{ item }</li>
      ))
      return itemList
    }
  },
  render () {
    const { prefixCls, size } = this.$props
    const classString = {
      [`${prefixCls}`]: true,
      [`${size}`]: true
    }
    const items = filterEmpty(this.$slots.default)
    const itemsDom = items && items.length ? <ul class={`${prefixCls}-items`}>{ this.getItems(items) }</ul> : null
    return (
      <div class={ classString }>
        { itemsDom }
      </div>
    )
  }
}
</script>
_web/src/components/AvatarList/index.js
New file
@@ -0,0 +1,4 @@
import AvatarList from './List'
import './index.less'
export default AvatarList
_web/src/components/AvatarList/index.less
New file
@@ -0,0 +1,60 @@
@import "../index";
@avatar-list-prefix-cls: ~"@{ant-pro-prefix}-avatar-list";
@avatar-list-item-prefix-cls: ~"@{ant-pro-prefix}-avatar-list-item";
.@{avatar-list-prefix-cls} {
  display: inline-block;
  ul {
    list-style: none;
    display: inline-block;
    padding: 0;
    margin: 0 0 0 8px;
    font-size: 0;
  }
}
.@{avatar-list-item-prefix-cls} {
  display: inline-block;
  font-size: @font-size-base;
  margin-left: -8px;
  width: @avatar-size-base;
  height: @avatar-size-base;
  :global {
    .ant-avatar {
      border: 1px solid #fff;
      cursor: pointer;
    }
  }
  &.large {
    width: @avatar-size-lg;
    height: @avatar-size-lg;
  }
  &.small {
    width: @avatar-size-sm;
    height: @avatar-size-sm;
  }
  &.mini {
    width: 20px;
    height: 20px;
    :global {
      .ant-avatar {
        width: 20px;
        height: 20px;
        line-height: 20px;
        .ant-avatar-string {
          font-size: 12px;
          line-height: 18px;
        }
      }
    }
  }
}
_web/src/components/AvatarList/index.md
New file
@@ -0,0 +1,64 @@
# AvatarList 用户头像列表
一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。
引用方式:
```javascript
import AvatarList from '@/components/AvatarList'
const AvatarListItem = AvatarList.AvatarItem
export default {
    components: {
        AvatarList,
        AvatarListItem
    }
}
```
## 代码演示  [demo](https://pro.loacg.com/test/home)
```html
<avatar-list size="mini">
    <avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
    <avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
```html
<avatar-list :max-length="3">
    <avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
    <avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
## API
### AvatarList
| 参数               | 说明       | 类型                                 | 默认值       |
| ---------------- | -------- | ---------------------------------- | --------- |
| size             | 头像大小     | `large`、`small` 、`mini`, `default` | `default` |
| maxLength        | 要显示的最大项目 | number                             | -         |
| excessItemsStyle | 多余的项目风格  | CSSProperties                      | -         |
### AvatarList.Item
| 参数   | 说明     | 类型        | 默认值 |
| ---- | ------ | --------- | --- |
| tips | 头像展示文案 | string | -   |
| src  | 头像图片连接 | string    | -   |
_web/src/components/Charts/Bar.vue
New file
@@ -0,0 +1,62 @@
<template>
  <div :style="{ padding: '0 0 32px 32px' }">
    <h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
    <v-chart
      height="254"
      :data="data"
      :forceFit="true"
      :padding="['auto', 'auto', '40', '50']">
      <v-tooltip />
      <v-axis />
      <v-bar position="x*y"/>
    </v-chart>
  </div>
</template>
<script>
export default {
  name: 'Bar',
  props: {
    title: {
      type: String,
      default: ''
    },
    data: {
      type: Array,
      default: () => {
        return []
      }
    },
    scale: {
      type: Array,
      default: () => {
        return [{
          dataKey: 'x',
          min: 2
        }, {
          dataKey: 'y',
          title: '时间',
          min: 1,
          max: 22
        }]
      }
    },
    tooltip: {
      type: Array,
      default: () => {
        return [
          'x*y',
          (x, y) => ({
            name: x,
            value: y
          })
        ]
      }
    }
  },
  data () {
    return {
    }
  }
}
</script>
_web/src/components/Charts/ChartCard.vue
New file
@@ -0,0 +1,120 @@
<template>
  <a-card :loading="loading" :body-style="{ padding: '20px 24px 8px' }" :bordered="false">
    <div class="chart-card-header">
      <div class="meta">
        <span class="chart-card-title">
          <slot name="title">
            {{ title }}
          </slot>
        </span>
        <span class="chart-card-action">
          <slot name="action"></slot>
        </span>
      </div>
      <div class="total">
        <slot name="total">
          <span>{{ typeof total === 'function' && total() || total }}</span>
        </slot>
      </div>
    </div>
    <div class="chart-card-content">
      <div class="content-fix">
        <slot></slot>
      </div>
    </div>
    <div class="chart-card-footer">
      <div class="field">
        <slot name="footer"></slot>
      </div>
    </div>
  </a-card>
</template>
<script>
export default {
  name: 'ChartCard',
  props: {
    title: {
      type: String,
      default: ''
    },
    total: {
      type: [Function, Number, String],
      required: false,
      default: null
    },
    loading: {
      type: Boolean,
      default: false
    }
  }
}
</script>
<style lang="less" scoped>
  .chart-card-header {
    position: relative;
    overflow: hidden;
    width: 100%;
    .meta {
      position: relative;
      overflow: hidden;
      width: 100%;
      color: rgba(0, 0, 0, .45);
      font-size: 14px;
      line-height: 22px;
    }
  }
  .chart-card-action {
    cursor: pointer;
    position: absolute;
    top: 0;
    right: 0;
  }
  .chart-card-footer {
    border-top: 1px solid #e8e8e8;
    padding-top: 9px;
    margin-top: 8px;
    > * {
      position: relative;
    }
    .field {
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      margin: 0;
    }
  }
  .chart-card-content {
    margin-bottom: 12px;
    position: relative;
    height: 46px;
    width: 100%;
    .content-fix {
      position: absolute;
      left: 0;
      bottom: 0;
      width: 100%;
    }
  }
  .total {
    overflow: hidden;
    text-overflow: ellipsis;
    word-break: break-all;
    white-space: nowrap;
    color: #000;
    margin-top: 4px;
    margin-bottom: 0;
    font-size: 30px;
    line-height: 38px;
    height: 38px;
  }
</style>
_web/src/components/Charts/Liquid.vue
New file
@@ -0,0 +1,67 @@
<template>
  <div>
    <v-chart
      :forceFit="true"
      :height="height"
      :width="width"
      :data="data"
      :scale="scale"
      :padding="0">
      <v-tooltip />
      <v-interval
        :shape="['liquid-fill-gauge']"
        position="transfer*value"
        color=""
        :v-style="{
          lineWidth: 10,
          opacity: 0.75
        }"
        :tooltip="[
          'transfer*value',
          (transfer, value) => {
            return {
              name: transfer,
              value,
            };
          },
        ]"
      ></v-interval>
      <v-guide
        v-for="(row, index) in data"
        :key="index"
        type="text"
        :top="true"
        :position="{
          gender: row.transfer,
          value: 45
        }"
        :content="row.value + '%'"
        :v-style="{
          fontSize: 100,
          textAlign: 'center',
          opacity: 0.75,
        }"
      />
    </v-chart>
  </div>
</template>
<script>
export default {
  name: 'Liquid',
  props: {
    height: {
      type: Number,
      default: 0
    },
    width: {
      type: Number,
      default: 0
    }
  }
}
</script>
<style scoped>
</style>
_web/src/components/Charts/MiniArea.vue
New file
@@ -0,0 +1,56 @@
<template>
  <div class="antv-chart-mini">
    <div class="chart-wrapper" :style="{ height: 46 }">
      <v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 0, 18, 0]">
        <v-tooltip />
        <v-smooth-area position="x*y" />
      </v-chart>
    </div>
  </div>
</template>
<script>
import moment from 'moment'
const data = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
  data.push({
    x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
    y: Math.round(Math.random() * 10)
  })
}
const tooltip = [
  'x*y',
  (x, y) => ({
    name: x,
    value: y
  })
]
const scale = [{
  dataKey: 'x',
  min: 2
}, {
  dataKey: 'y',
  title: '时间',
  min: 1,
  max: 22
}]
export default {
  name: 'MiniArea',
  data () {
    return {
      data,
      tooltip,
      scale,
      height: 100
    }
  }
}
</script>
<style lang="less" scoped>
  @import "chart";
</style>
_web/src/components/Charts/MiniBar.vue
New file
@@ -0,0 +1,57 @@
<template>
  <div class="antv-chart-mini">
    <div class="chart-wrapper" :style="{ height: 46 }">
      <v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
        <v-tooltip />
        <v-bar position="x*y" />
      </v-chart>
    </div>
  </div>
</template>
<script>
import moment from 'moment'
const data = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
  data.push({
    x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
    y: Math.round(Math.random() * 10)
  })
}
const tooltip = [
  'x*y',
  (x, y) => ({
    name: x,
    value: y
  })
]
const scale = [{
  dataKey: 'x',
  min: 2
}, {
  dataKey: 'y',
  title: '时间',
  min: 1,
  max: 30
}]
export default {
  name: 'MiniBar',
  data () {
    return {
      data,
      tooltip,
      scale,
      height: 100
    }
  }
}
</script>
<style lang="less" scoped>
  @import "chart";
</style>
_web/src/components/Charts/MiniProgress.vue
New file
@@ -0,0 +1,75 @@
<template>
  <div class="chart-mini-progress">
    <div class="target" :style="{ left: target + '%'}">
      <span :style="{ backgroundColor: color }" />
      <span :style="{ backgroundColor: color }"/>
    </div>
    <div class="progress-wrapper">
      <div class="progress" :style="{ backgroundColor: color, width: percentage + '%', height: height }"></div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'MiniProgress',
  props: {
    target: {
      type: Number,
      default: 0
    },
    height: {
      type: String,
      default: '10px'
    },
    color: {
      type: String,
      default: '#13C2C2'
    },
    percentage: {
      type: Number,
      default: 0
    }
  }
}
</script>
<style lang="less" scoped>
  .chart-mini-progress {
    padding: 5px 0;
    position: relative;
    width: 100%;
    .target {
      position: absolute;
      top: 0;
      bottom: 0;
      span {
        border-radius: 100px;
        position: absolute;
        top: 0;
        left: 0;
        height: 4px;
        width: 2px;
        &:last-child {
          top: auto;
          bottom: 0;
        }
      }
    }
    .progress-wrapper {
      background-color: #f5f5f5;
      position: relative;
      .progress {
        transition: all .4s cubic-bezier(.08,.82,.17,1) 0s;
        border-radius: 1px 0 0 1px;
        background-color: #1890ff;
        width: 0;
        height: 100%;
      }
    }
  }
</style>
_web/src/components/Charts/MiniSmoothArea.vue
New file
@@ -0,0 +1,40 @@
<template>
  <div :class="prefixCls">
    <div class="chart-wrapper" :style="{ height: 46 }">
      <v-chart :force-fit="true" :height="100" :data="dataSource" :scale="scale" :padding="[36, 0, 18, 0]">
        <v-tooltip />
        <v-smooth-line position="x*y" :size="2" />
        <v-smooth-area position="x*y" />
      </v-chart>
    </div>
  </div>
</template>
<script>
export default {
  name: 'MiniSmoothArea',
  props: {
    prefixCls: {
      type: String,
      default: 'ant-pro-smooth-area'
    },
    scale: {
      type: [Object, Array],
      required: true
    },
    dataSource: {
      type: Array,
      required: true
    }
  },
  data () {
    return {
      height: 100
    }
  }
}
</script>
<style lang="less" scoped>
  @import "smooth.area.less";
</style>
_web/src/components/Charts/Radar.vue
New file
@@ -0,0 +1,68 @@
<template>
  <v-chart :forceFit="true" height="400" :data="data" :padding="[20, 20, 95, 20]" :scale="scale">
    <v-tooltip></v-tooltip>
    <v-axis :dataKey="axis1Opts.dataKey" :line="axis1Opts.line" :tickLine="axis1Opts.tickLine" :grid="axis1Opts.grid" />
    <v-axis :dataKey="axis2Opts.dataKey" :line="axis2Opts.line" :tickLine="axis2Opts.tickLine" :grid="axis2Opts.grid" />
    <v-legend dataKey="user" marker="circle" :offset="30" />
    <v-coord type="polar" radius="0.8" />
    <v-line position="item*score" color="user" :size="2" />
    <v-point position="item*score" color="user" :size="4" shape="circle" />
  </v-chart>
</template>
<script>
const axis1Opts = {
  dataKey: 'item',
  line: null,
  tickLine: null,
  grid: {
    lineStyle: {
      lineDash: null
    },
    hideFirstLine: false
  }
}
const axis2Opts = {
  dataKey: 'score',
  line: null,
  tickLine: null,
  grid: {
    type: 'polygon',
    lineStyle: {
      lineDash: null
    }
  }
}
const scale = [
  {
    dataKey: 'score',
    min: 0,
    max: 80
  }, {
    dataKey: 'user',
    alias: '类型'
  }
]
export default {
  name: 'Radar',
  props: {
    data: {
      type: Array,
      default: null
    }
  },
  data () {
    return {
      axis1Opts,
      axis2Opts,
      scale
    }
  }
}
</script>
<style scoped>
</style>
_web/src/components/Charts/RankList.vue
New file
@@ -0,0 +1,77 @@
<template>
  <div class="rank">
    <h4 class="title">{{ title }}</h4>
    <ul class="list">
      <li :key="index" v-for="(item, index) in list">
        <span :class="index < 3 ? 'active' : null">{{ index + 1 }}</span>
        <span>{{ item.name }}</span>
        <span>{{ item.total }}</span>
      </li>
    </ul>
  </div>
</template>
<script>
export default {
  name: 'RankList',
  // ['title', 'list']
  props: {
    title: {
      type: String,
      default: ''
    },
    list: {
      type: Array,
      default: null
    }
  }
}
</script>
<style lang="less" scoped>
  .rank {
    padding: 0 32px 32px 72px;
    .list {
      margin: 25px 0 0;
      padding: 0;
      list-style: none;
      li {
        margin-top: 16px;
        span {
          color: rgba(0, 0, 0, .65);
          font-size: 14px;
          line-height: 22px;
          &:first-child {
            background-color: #f5f5f5;
            border-radius: 20px;
            display: inline-block;
            font-size: 12px;
            font-weight: 600;
            margin-right: 24px;
            height: 20px;
            line-height: 20px;
            width: 20px;
            text-align: center;
          }
          &.active {
            background-color: #314659;
            color: #fff;
          }
          &:last-child {
            float: right;
          }
        }
      }
    }
  }
  .mobile .rank {
    padding: 0 32px 32px 32px;
  }
</style>
_web/src/components/Charts/TagCloud.vue
New file
@@ -0,0 +1,113 @@
<template>
  <v-chart :width="width" :height="height" :padding="[0]" :data="data" :scale="scale">
    <v-tooltip :show-title="false" />
    <v-coord type="rect" direction="TL" />
    <v-point position="x*y" color="category" shape="cloud" tooltip="value*category" />
  </v-chart>
</template>
<script>
import { registerShape } from 'viser-vue'
const DataSet = require('@antv/data-set')
const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png'
const scale = [
  { dataKey: 'x', nice: false },
  { dataKey: 'y', nice: false }
]
registerShape('point', 'cloud', {
  draw (cfg, container) {
    return container.addShape('text', {
      attrs: {
        fillOpacity: cfg.opacity,
        fontSize: cfg.origin._origin.size,
        rotate: cfg.origin._origin.rotate,
        text: cfg.origin._origin.text,
        textAlign: 'center',
        fontFamily: cfg.origin._origin.font,
        fill: cfg.color,
        textBaseline: 'Alphabetic',
        ...cfg.style,
        x: cfg.x,
        y: cfg.y
      }
    })
  }
})
export default {
  name: 'TagCloud',
  props: {
    tagList: {
      type: Array,
      required: true
    },
    height: {
      type: Number,
      default: 400
    },
    width: {
      type: Number,
      default: 640
    }
  },
  data () {
    return {
      data: [],
      scale
    }
  },
  watch: {
    tagList: function (val) {
      if (val.length > 0) {
        this.initTagCloud(val)
      }
    }
  },
  mounted () {
    if (this.tagList.length > 0) {
      this.initTagCloud(this.tagList)
    }
  },
  methods: {
    initTagCloud (dataSource) {
      const { height, width } = this
      const dv = new DataSet.View().source(dataSource)
      const range = dv.range('value')
      const min = range[0]
      const max = range[1]
      const imageMask = new Image()
      imageMask.crossOrigin = ''
      imageMask.src = imgUrl
      imageMask.onload = () => {
        dv.transform({
          type: 'tag-cloud',
          fields: ['name', 'value'],
          size: [width, height],
          imageMask,
          font: 'Verdana',
          padding: 0,
          timeInterval: 5000, // max execute time
          rotate () {
            let random = ~~(Math.random() * 4) % 4
            if (random === 2) {
              random = 0
            }
            return random * 90 // 0, 90, 270
          },
          fontSize (d) {
            if (d.value) {
              return ((d.value - min) / (max - min)) * (32 - 8) + 8
            }
            return 0
          }
        })
        this.data = dv.rows
      }
    }
  }
}
</script>
_web/src/components/Charts/TransferBar.vue
New file
@@ -0,0 +1,64 @@
<template>
  <div :style="{ padding: '0 0 32px 32px' }">
    <h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
    <v-chart
      height="254"
      :data="data"
      :scale="scale"
      :forceFit="true"
      :padding="['auto', 'auto', '40', '50']">
      <v-tooltip />
      <v-axis />
      <v-bar position="x*y"/>
    </v-chart>
  </div>
</template>
<script>
const tooltip = [
  'x*y',
  (x, y) => ({
    name: x,
    value: y
  })
]
const scale = [{
  dataKey: 'x',
  title: '日期(天)',
  alias: '日期(天)',
  min: 2
}, {
  dataKey: 'y',
  title: '流量(Gb)',
  alias: '流量(Gb)',
  min: 1
}]
export default {
  name: 'Bar',
  props: {
    title: {
      type: String,
      default: ''
    }
  },
  data () {
    return {
      data: [],
      scale,
      tooltip
    }
  },
  created () {
    this.getMonthBar()
  },
  methods: {
    getMonthBar () {
      this.$http.get('/analysis/month-bar')
        .then(res => {
          this.data = res.result
        })
    }
  }
}
</script>
_web/src/components/Charts/Trend.vue
New file
@@ -0,0 +1,82 @@
<template>
  <div class="chart-trend">
    {{ term }}
    <span>{{ rate }}%</span>
    <span :class="['trend-icon', trend]"><a-icon :type="'caret-' + trend"/></span>
  </div>
</template>
<script>
export default {
  name: 'Trend',
  props: {
    term: {
      type: String,
      default: '',
      required: true
    },
    percentage: {
      type: Number,
      default: null
    },
    type: {
      type: Boolean,
      default: null
    },
    target: {
      type: Number,
      default: 0
    },
    value: {
      type: Number,
      default: 0
    },
    fixed: {
      type: Number,
      default: 2
    }
  },
  data () {
    return {
      trend: this.type && 'up' || 'down',
      rate: this.percentage
    }
  },
  created () {
    const type = this.type === null ? this.value >= this.target : this.type
    this.trend = type ? 'up' : 'down'
    this.rate = (this.percentage === null ? Math.abs(this.value - this.target) * 100 / this.target : this.percentage).toFixed(this.fixed)
  }
}
</script>
<style lang="less" scoped>
  .chart-trend {
    display: inline-block;
    font-size: 14px;
    line-height: 22px;
    .trend-icon {
      font-size: 12px;
      &.up, &.down {
        margin-left: 4px;
        position: relative;
        top: 1px;
        i {
          font-size: 12px;
          transform: scale(.83);
        }
      }
      &.up {
        color: #f5222d;
      }
      &.down {
        color: #52c41a;
        top: -1px;
      }
    }
  }
</style>
_web/src/components/Charts/chart.less
New file
@@ -0,0 +1,13 @@
.antv-chart-mini {
  position: relative;
  width: 100%;
  .chart-wrapper {
    position: absolute;
    bottom: -28px;
    width: 100%;
/*    margin: 0 -5px;
    overflow: hidden;*/
  }
}
_web/src/components/Charts/smooth.area.less
New file
@@ -0,0 +1,14 @@
@import "../index";
@smoothArea-prefix-cls: ~"@{ant-pro-prefix}-smooth-area";
.@{smoothArea-prefix-cls} {
    position: relative;
    width: 100%;
    .chart-wrapper {
        position: absolute;
        bottom: -28px;
        width: 100%;
    }
}
_web/src/components/CountDown/CountDown.vue
New file
@@ -0,0 +1,102 @@
<template>
  <span>
    {{ lastTime | format }}
  </span>
</template>
<script>
function fixedZero (val) {
  return val * 1 < 10 ? `0${val}` : val
}
export default {
  name: 'CountDown',
  props: {
    format: {
      type: Function,
      default: undefined
    },
    target: {
      type: [Date, Number],
      required: true
    },
    onEnd: {
      type: Function,
      default: () => ({})
    }
  },
  data () {
    return {
      dateTime: '0',
      originTargetTime: 0,
      lastTime: 0,
      timer: 0,
      interval: 1000
    }
  },
  filters: {
    format (time) {
      const hours = 60 * 60 * 1000
      const minutes = 60 * 1000
      const h = Math.floor(time / hours)
      const m = Math.floor((time - h * hours) / minutes)
      const s = Math.floor((time - h * hours - m * minutes) / 1000)
      return `${fixedZero(h)}:${fixedZero(m)}:${fixedZero(s)}`
    }
  },
  created () {
    this.initTime()
    this.tick()
  },
  methods: {
    initTime () {
      let lastTime = 0
      let targetTime = 0
      this.originTargetTime = this.target
      try {
        if (Object.prototype.toString.call(this.target) === '[object Date]') {
          targetTime = this.target
        } else {
          targetTime = new Date(this.target).getTime()
        }
      } catch (e) {
        throw new Error('invalid target prop')
      }
      lastTime = targetTime - new Date().getTime()
      this.lastTime = lastTime < 0 ? 0 : lastTime
    },
    tick () {
      const { onEnd } = this
      this.timer = setTimeout(() => {
        if (this.lastTime < this.interval) {
          clearTimeout(this.timer)
          this.lastTime = 0
          if (typeof onEnd === 'function') {
            onEnd()
          }
        } else {
          this.lastTime -= this.interval
          this.tick()
        }
      }, this.interval)
    }
  },
  beforeUpdate () {
    if (this.originTargetTime !== this.target) {
      this.initTime()
    }
  },
  beforeDestroy () {
    clearTimeout(this.timer)
  }
}
</script>
<style scoped>
</style>
_web/src/components/CountDown/index.js
New file
@@ -0,0 +1,3 @@
import CountDown from './CountDown'
export default CountDown
_web/src/components/CountDown/index.md
New file
@@ -0,0 +1,34 @@
# CountDown 倒计时
倒计时组件。
引用方式:
```javascript
import CountDown from '@/components/CountDown/CountDown'
export default {
    components: {
        CountDown
    }
}
```
## 代码演示  [demo](https://pro.loacg.com/test/home)
```html
<count-down :target="new Date().getTime() + 3000000" :on-end="onEndHandle" />
```
## API
| 参数      | 说明                                      | 类型         | 默认值 |
|----------|------------------------------------------|-------------|-------|
| target | 目标时间 | Date | - |
| onEnd |  倒计时结束回调 | funtion | -|
_web/src/components/DepartmentSelect/DepartmentSelect.vue
New file
@@ -0,0 +1,48 @@
 <template>
  <a-tree-select
    :dropdownStyle="{ maxHeight: '300px', overflow: 'auto' }"
    allowClear
    :treeData="orgTree"
    :placeholder="placeholder"
    treeDefaultExpandAll
    @change="onchange"
  >
    <span slot="title" slot-scope="{ id }">{{ id }}</span>
  </a-tree-select>
</template>
<script>
import { getOrgTree } from '@/api/modular/system/orgManage'
export default {
  name: 'DepartSelect',
  props: {
    placeholder: {
      type: String
    },
    value: {
      type: String
    }
  },
  data() {
    return {
      orgTree: []
    }
  },
  created() {
    this.getOrgData()
  },
  methods: {
    getOrgData() {
      getOrgTree().then((res) => {
        this.orgTree = res
      })
    },
    /**
     * 选择树机构,初始化机构名称于表单中
     */
    onchange (value) {
      this.$emit('change', value)
    }
  }
}
</script>
_web/src/components/DepartmentSelect/index.js
New file
@@ -0,0 +1,3 @@
import DepartmentSelect from './DepartmentSelect'
export default DepartmentSelect
_web/src/components/DescriptionList/DescriptionList.vue
New file
@@ -0,0 +1,153 @@
<template>
  <div :class="['description-list', size, layout === 'vertical' ? 'vertical': 'horizontal']">
    <div v-if="title" class="title">{{ title }}</div>
    <a-row>
      <slot></slot>
    </a-row>
  </div>
</template>
<script>
import { Col } from 'ant-design-vue/es/grid/'
const Item = {
  name: 'DetailListItem',
  props: {
    term: {
      type: String,
      default: '',
      required: false
    }
  },
  inject: {
    col: {
      type: Number
    }
  },
  render () {
    return (
      <Col {...{ props: responsive[this.col] }}>
        <div class="term">{this.$props.term}</div>
        <div class="content">{this.$slots.default}</div>
      </Col>
    )
  }
}
const responsive = {
  1: { xs: 24 },
  2: { xs: 24, sm: 12 },
  3: { xs: 24, sm: 12, md: 8 },
  4: { xs: 24, sm: 12, md: 6 }
}
export default {
  name: 'DetailList',
  Item: Item,
  components: {
    Col
  },
  props: {
    title: {
      type: String,
      default: '',
      required: false
    },
    col: {
      type: Number,
      required: false,
      default: 3
    },
    size: {
      type: String,
      required: false,
      default: 'large'
    },
    layout: {
      type: String,
      required: false,
      default: 'horizontal'
    }
  },
  provide () {
    return {
      col: this.col > 4 ? 4 : this.col
    }
  }
}
</script>
<style lang="less" scoped>
  .description-list {
    .title {
      color: rgba(0,0,0,.85);
      font-size: 14px;
      font-weight: 500;
      margin-bottom: 16px;
    }
    /deep/ .term {
      color: rgba(0,0,0,.85);
      display: table-cell;
      line-height: 20px;
      margin-right: 8px;
      padding-bottom: 16px;
      white-space: nowrap;
      &:not(:empty):after {
        content: ":";
        margin: 0 8px 0 2px;
        position: relative;
        top: -.5px;
      }
    }
    /deep/ .content {
      color: rgba(0,0,0,.65);
      display: table-cell;
      min-height: 22px;
      line-height: 22px;
      padding-bottom: 16px;
      width: 100%;
      &:empty {
        content: ' ';
        height: 38px;
        padding-bottom: 16px;
      }
    }
    &.small {
      .title {
        font-size: 14px;
        color: rgba(0, 0, 0, .65);
        font-weight: normal;
        margin-bottom: 12px;
      }
      /deep/ .term, .content {
        padding-bottom: 8px;
      }
    }
    &.large {
      /deep/ .term, .content {
        padding-bottom: 16px;
      }
      .title {
        font-size: 16px;
      }
    }
    &.vertical {
      .term {
        padding-bottom: 8px;
      }
      /deep/ .term, .content {
        display: block;
      }
    }
  }
</style>
_web/src/components/DescriptionList/index.js
New file
@@ -0,0 +1,2 @@
import DescriptionList from './DescriptionList'
export default DescriptionList
_web/src/components/Dialog.js
New file
@@ -0,0 +1,113 @@
import Modal from 'ant-design-vue/es/modal'
export default (Vue) => {
  function dialog (component, componentProps, modalProps) {
    const _vm = this
    modalProps = modalProps || {}
    if (!_vm || !_vm._isVue) {
      return
    }
    let dialogDiv = document.querySelector('body>div[type=dialog]')
    if (!dialogDiv) {
      dialogDiv = document.createElement('div')
      dialogDiv.setAttribute('type', 'dialog')
      document.body.appendChild(dialogDiv)
    }
    const handle = function (checkFunction, afterHandel) {
      if (checkFunction instanceof Function) {
        const res = checkFunction()
        if (res instanceof Promise) {
          res.then(c => {
            c && afterHandel()
          })
        } else {
          res && afterHandel()
        }
      } else {
        // checkFunction && afterHandel()
        checkFunction || afterHandel()
      }
    }
    const dialogInstance = new Vue({
      data () {
        return {
          visible: true
        }
      },
      router: _vm.$router,
      store: _vm.$store,
      mounted () {
        this.$on('close', (v) => {
          this.handleClose()
        })
      },
      methods: {
        handleClose () {
          handle(this.$refs._component.onCancel, () => {
            this.visible = false
            this.$refs._component.$emit('close')
            this.$refs._component.$emit('cancel')
            dialogInstance.$destroy()
          })
        },
        handleOk () {
          handle(this.$refs._component.onOK || this.$refs._component.onOk, () => {
            this.visible = false
            this.$refs._component.$emit('close')
            this.$refs._component.$emit('ok')
            dialogInstance.$destroy()
          })
        }
      },
      render: function (h) {
        const that = this
        const modalModel = modalProps && modalProps.model
        if (modalModel) {
          delete modalProps.model
        }
        const ModalProps = Object.assign({}, modalModel && { model: modalModel } || {}, {
          attrs: Object.assign({}, {
            ...(modalProps.attrs || modalProps)
          }, {
            visible: this.visible
          }),
          on: Object.assign({}, {
            ...(modalProps.on || modalProps)
          }, {
            ok: () => {
              that.handleOk()
            },
            cancel: () => {
              that.handleClose()
            }
          })
        })
        const componentModel = componentProps && componentProps.model
        if (componentModel) {
          delete componentProps.model
        }
        const ComponentProps = Object.assign({}, componentModel && { model: componentModel } || {}, {
          ref: '_component',
          attrs: Object.assign({}, {
            ...((componentProps && componentProps.attrs) || componentProps)
          }),
          on: Object.assign({}, {
            ...((componentProps && componentProps.on) || componentProps)
          })
        })
        return h(Modal, ModalProps, [h(component, ComponentProps)])
      }
    }).$mount(dialogDiv)
  }
  Object.defineProperty(Vue.prototype, '$dialog', {
    get: () => {
      return function () {
        dialog.apply(this, arguments)
      }
    }
  })
}
_web/src/components/Editor/QuillEditor.vue
New file
@@ -0,0 +1,82 @@
<template>
  <div :class="prefixCls">
    <quill-editor
      v-model="content"
      ref="myQuillEditor"
      :options="editorOption"
      @blur="onEditorBlur($event)"
      @focus="onEditorFocus($event)"
      @ready="onEditorReady($event)"
      @change="onEditorChange($event)">
    </quill-editor>
  </div>
</template>
<script>
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import { quillEditor } from 'vue-quill-editor'
export default {
  name: 'QuillEditor',
  components: {
    quillEditor
  },
  props: {
    prefixCls: {
      type: String,
      default: 'ant-editor-quill'
    },
    // 表单校验用字段
    // eslint-disable-next-line
    value: {
      type: String
    }
  },
  data () {
    return {
      content: null,
      editorOption: {
        // some quill options
      }
    }
  },
  methods: {
    onEditorBlur (quill) {
      console.log('editor blur!', quill)
    },
    onEditorFocus (quill) {
      console.log('editor focus!', quill)
    },
    onEditorReady (quill) {
      console.log('editor ready!', quill)
    },
    onEditorChange ({ quill, html, text }) {
      console.log('editor change!', quill, html, text)
      this.$emit('change', html)
    }
  },
  watch: {
    value (val) {
      this.content = val
    }
  }
}
</script>
<style lang="less" scoped>
@import url('../index.less');
/* 覆盖 quill 默认边框圆角为 ant 默认圆角,用于统一 ant 组件风格 */
.ant-editor-quill {
  /deep/ .ql-toolbar.ql-snow {
    border-radius: @border-radius-base @border-radius-base 0 0;
  }
  /deep/ .ql-container.ql-snow {
    border-radius: 0 0 @border-radius-base @border-radius-base;
  }
}
</style>
_web/src/components/Editor/WangEditor.vue
New file
@@ -0,0 +1,126 @@
<template>
  <div>
    <div id="editor" ref="myEditor"></div>
    <slot></slot>
  </div>
</template>
<script>
  import WangEditor from 'wangeditor'
  export default {
    name: 'ComponentWangeditor',
    data () {
      return {
        edit: ''
      }
    },
    props: {
      value: {
        type: String,
        default: ''
      },
      config: {
        type: Object,
        default: () => {
          return {}
        }
      },
      uploadConfig: {
        type: Object,
        default: () => {
          return {
            method: 'http', // 支持custom(objurl)和http(服务器)和base64
            url: '/'
          }
        }
      }
    },
    computed: {
      customConfig () {
        return {
          pasteFilterStyle: false, // 关闭掉粘贴样式的过滤
          pasteIgnoreImg: false, // 粘贴时不忽略图片
          ...this.config
        }
      }
    },
    watch: {
    },
    components: {
    },
    methods: {
      readBlobAsDataURL (blob, callback) {
        var a = new FileReader()
        a.onload = function (e) { callback(e.target.result) }
        a.readAsDataURL(blob)
      },
      initEditor () {
        var self = this
        this.editor = new WangEditor(this.$refs.myEditor)
        // 配置 onchange 事件
        this.editor.customConfig = this.customConfig
        this.editor.customConfig.uploadImgMaxLength = 5
        this.editor.change = function () { // 编辑区域内容变化时
          self.$emit('input', this.txt.html())
          self.$emit('onchange', this.txt.html(), this.txt)
          // editor.txt.html('.....') //设置编辑器内容
          // editor.txt.clear() //清空编辑器内容
          // editor.txt.append('<p>追加的内容</p>')//继续追加内容。
          // editor.txt.text()  // 读取 text
          // editor.txt.getJSON()  // 获取 JSON 格式的内容
        }
        this.editor.customConfig.customUploadImg = function (files, insert) {
          if (self.uploadConfig.method === 'custom') {
            files.forEach(file => {
              var fileUrl = URL.createObjectURL(file)
              insert(fileUrl)
            })
          }
          if (self.uploadConfig.method === 'base64') {
            files.forEach(file => {
              self.readBlobAsDataURL(file, function (dataurl) {
                insert(dataurl)
              })
            })
          }
          if (self.uploadConfig.method === 'http') {
            if (self.uploadConfig.callback) {
              self.uploadConfig.callback(files, insert)
            } else {
              var formData = new FormData()
              files.forEach(file => {
                formData.append('file', file)
              })
              self.$axios.post(self.uploadConfig.url, formData).then(({ data }) => {
                if (data.status === 'success') {
                  insert(data.url)
                }
              })
            }
          }
        }
        this.editor.create() // 生成编辑器
        this.editor.txt.text(this.value) // 生成编辑器
        this.$emit('oninit', this.editor)
      }
    },
    beforeCreate () {
    },
    created () {
    },
    beforeMount () {
    },
    mounted () {
      this.initEditor()
    }
  }
</script>
<style >
  .w-e-toolbar{
    flex-wrap:wrap;
  }
</style>
_web/src/components/Ellipsis/Ellipsis.vue
New file
@@ -0,0 +1,64 @@
<script>
import Tooltip from 'ant-design-vue/es/tooltip'
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/util'
/*
    const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
    const TooltipOverlayStyle = {
      overflowWrap: 'break-word',
      wordWrap: 'break-word',
    };
  */
export default {
  name: 'Ellipsis',
  components: {
    Tooltip
  },
  props: {
    prefixCls: {
      type: String,
      default: 'ant-pro-ellipsis'
    },
    tooltip: {
      type: Boolean
    },
    length: {
      type: Number,
      required: true
    },
    lines: {
      type: Number,
      default: 1
    },
    fullWidthRecognition: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    getStrDom (str, fullLength) {
      return (
        <span>{ cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '') }</span>
      )
    },
    getTooltip (fullStr, fullLength) {
      return (
        <Tooltip>
          <template slot="title">{ fullStr }</template>
          { this.getStrDom(fullStr, fullLength) }
        </Tooltip>
      )
    }
  },
  render () {
    const { tooltip, length } = this.$props
    const str = this.$slots.default.map(vNode => vNode.text).join('')
    const fullLength = getStrFullLength(str)
    const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
    return (
      strDom
    )
  }
}
</script>
_web/src/components/Ellipsis/index.js
New file
@@ -0,0 +1,3 @@
import Ellipsis from './Ellipsis'
export default Ellipsis
_web/src/components/Ellipsis/index.md
New file
@@ -0,0 +1,38 @@
# Ellipsis 文本自动省略号
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
引用方式:
```javascript
import Ellipsis from '@/components/Ellipsis'
export default {
    components: {
        Ellipsis
    }
}
```
## 代码演示  [demo](https://pro.loacg.com/test/home)
```html
<ellipsis :length="100" tooltip>
        There were injuries alleged in three cases in 2015, and a
        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.
</ellipsis>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
tooltip | 移动到文本展示完整内容的提示 | boolean | -
length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -
_web/src/components/Exception/ExceptionPage.vue
New file
@@ -0,0 +1,130 @@
<template>
  <div class="exception">
    <div class="imgBlock">
      <div class="imgEle" :style="{backgroundImage: `url(${config[type].img})`}">
      </div>
    </div>
    <div class="content">
      <h1>{{ config[type].title }}</h1>
      <div class="desc">{{ config[type].desc }}</div>
      <div class="actions">
        <a-button type="primary" @click="handleToHome">返回首页</a-button>
      </div>
    </div>
  </div>
</template>
<script>
import types from './type'
export default {
  name: 'Exception',
  props: {
    type: {
      type: String,
      default: '404'
    }
  },
  data () {
    return {
      config: types
    }
  },
  methods: {
    handleToHome () {
      this.$router.push({ name: 'Console' })
    }
  }
}
</script>
<style lang="less">
@import "~ant-design-vue/lib/style/index";
.exception {
  display: flex;
  align-items: center;
  height: 80%;
  min-height: 500px;
  .imgBlock {
    flex: 0 0 62.5%;
    width: 62.5%;
    padding-right: 152px;
    zoom: 1;
    &::before,
    &::after {
      content: ' ';
      display: table;
    }
    &::after {
      clear: both;
      height: 0;
      font-size: 0;
      visibility: hidden;
    }
  }
  .imgEle {
    float: right;
    width: 100%;
    max-width: 430px;
    height: 360px;
    background-repeat: no-repeat;
    background-position: 50% 50%;
    background-size: contain;
  }
  .content {
    flex: auto;
    h1 {
      margin-bottom: 24px;
      color: #434e59;
      font-weight: 600;
      font-size: 72px;
      line-height: 72px;
    }
    .desc {
      margin-bottom: 16px;
      color: @text-color-secondary;
      font-size: 20px;
      line-height: 28px;
    }
    .actions {
      button:not(:last-child) {
        margin-right: 8px;
      }
    }
  }
}
@media screen and (max-width: @screen-xl) {
  .exception {
    .imgBlock {
      padding-right: 88px;
    }
  }
}
@media screen and (max-width: @screen-sm) {
  .exception {
    display: block;
    text-align: center;
    .imgBlock {
      margin: 0 auto 24px;
      padding-right: 0;
    }
  }
}
@media screen and (max-width: @screen-xs) {
  .exception {
    .imgBlock {
      margin-bottom: -24px;
      overflow: hidden;
    }
  }
}
</style>
_web/src/components/Exception/index.js
New file
@@ -0,0 +1,2 @@
import ExceptionPage from './ExceptionPage.vue'
export default ExceptionPage
_web/src/components/Exception/type.js
New file
@@ -0,0 +1,19 @@
const types = {
  403: {
    img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
    title: '403',
    desc: '抱歉,你无权访问该页面'
  },
  404: {
    img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
    title: '404',
    desc: '抱歉,你访问的页面不存在或仍在开发中'
  },
  500: {
    img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
    title: '500',
    desc: '抱歉,服务器出错了'
  }
}
export default types
_web/src/components/FooterToolbar/FooterToolBar.vue
New file
@@ -0,0 +1,30 @@
<template>
  <div :class="prefixCls">
    <div style="float: left">
      <slot name="extra">{{ extra }}</slot>
    </div>
    <div style="float: right">
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default {
  name: 'FooterToolBar',
  props: {
    prefixCls: {
      type: String,
      default: 'ant-pro-footer-toolbar'
    },
    extra: {
      type: [String, Object],
      default: ''
    }
  }
}
</script>
<style lang="less" scoped>
</style>
_web/src/components/FooterToolbar/index.js
New file
@@ -0,0 +1,4 @@
import FooterToolBar from './FooterToolBar'
import './index.less'
export default FooterToolBar
_web/src/components/FooterToolbar/index.less
New file
@@ -0,0 +1,23 @@
@import "../index";
@footer-toolbar-prefix-cls: ~"@{ant-pro-prefix}-footer-toolbar";
.@{footer-toolbar-prefix-cls} {
  position: fixed;
  width: 100%;
  bottom: 0;
  right: 0;
  height: 56px;
  line-height: 56px;
  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
  background: #fff;
  border-top: 1px solid #e8e8e8;
  padding: 0 24px;
  z-index: 9;
  &:after {
    content: "";
    display: block;
    clear: both;
  }
}
_web/src/components/FooterToolbar/index.md
New file
@@ -0,0 +1,48 @@
# FooterToolbar 底部工具栏
固定在底部的工具栏。
## 何时使用
固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。
引用方式:
```javascript
import FooterToolBar from '@/components/FooterToolbar'
export default {
    components: {
        FooterToolBar
    }
}
```
## 代码演示
```html
<footer-tool-bar>
    <a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
```html
<footer-tool-bar extra="扩展信息提示">
    <a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
children (slot) | 工具栏内容,向右对齐 | - | -
extra | 额外信息,向左对齐 | String, Object | -
_web/src/components/GlobalFooter/GlobalFooter.vue
New file
@@ -0,0 +1,46 @@
<template>
  <div class="footer">
    <div class="links">
    </div>
    <div class="copyright">
      Copyright © 2020 <a target="_blank" href="https://www.xiaonuo.vip/">小诺开源技术</a> All rights reserved. Snowy 1.8
    </div>
  </div>
</template>
<script>
export default {
  name: 'GlobalFooter',
  data () {
    return {}
  }
}
</script>
<style lang="less" scoped>
.footer {
  padding: 0 16px;
  margin: 48px 0 24px;
  text-align: center;
  .links {
    margin-bottom: 8px;
    a {
      color: rgba(0, 0, 0, 0.45);
      &:hover {
        color: rgba(0, 0, 0, 0.65);
      }
      &:not(:last-child) {
        margin-right: 40px;
      }
    }
  }
  .copyright {
    color: rgba(0, 0, 0, 0.45);
    font-size: 14px;
  }
}
</style>
_web/src/components/GlobalFooter/index.js
New file
@@ -0,0 +1,2 @@
import GlobalFooter from './GlobalFooter'
export default GlobalFooter
_web/src/components/GlobalHeader/GlobalHeader.vue
New file
@@ -0,0 +1,165 @@
<template>
  <transition name="showHeader">
    <div v-if="visible" class="header-animat">
      <a-layout-header
        v-if="visible"
        :class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', ]"
        :style="{ padding: '0', height: '55px' }">
        <div v-if="mode === 'sidemenu'" class="header">
          <a-menu
            style="height: 55px; border-bottom: 0px;"
            mode="horizontal"
            :default-selected-keys="this.defApp"
          >
            <a-icon v-if="device==='mobile'" class="trigger" :type="collapsed ? 'menu-fold' : 'menu-unfold'" @click="toggle"/>
            <a-icon v-else class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggle" style="padding-left: 20px; padding-right: 20px;"/>
            <a-menu-item v-for="(item) in userInfo.apps" :key="item.code" style="top:0px; line-height: 55px; padding-left: 10px; padding-right: 10px" @click="switchApp(item.code)">
              {{ item.name }}
            </a-menu-item>
            <user-menu></user-menu>
          </a-menu>
        </div>
        <div v-else :class="['top-nav-header-index', theme]">
          <div class="header-index-wide">
            <div class="header-index-left">
              <logo class="top-nav-header" :show-title="device !== 'mobile'"/>
              <s-menu v-if="device !== 'mobile'" mode="horizontal" :menu="menus" :theme="theme" />
              <a-icon v-else class="trigger" :type="collapsed ? 'menu-fold' : 'menu-unfold'" @click="toggle" />
            </div>
            <user-menu class="header-index-right"></user-menu>
          </div>
        </div>
      </a-layout-header>
    </div>
  </transition>
</template>
<script>
import UserMenu from '../tools/UserMenu'
import SMenu from '../Menu/'
import Logo from '../tools/Logo'
import { mixin } from '@/utils/mixin'
import { mapActions, mapGetters } from 'vuex'
import { ALL_APPS_MENU } from '@/store/mutation-types'
import Vue from 'vue'
import { message } from 'ant-design-vue/es'
export default {
  name: 'GlobalHeader',
  components: {
    UserMenu,
    SMenu,
    Logo
  },
  computed: {
    ...mapGetters(['userInfo'])
  },
  created () {
    this.defApp.push(Vue.ls.get(ALL_APPS_MENU)[0].code)
  },
  mixins: [mixin],
  props: {
    mode: {
      type: String,
      // sidemenu, topmenu
      default: 'sidemenu'
    },
    menus: {
      type: Array,
      required: true
    },
    theme: {
      type: String,
      required: false,
      default: 'dark'
    },
    collapsed: {
      type: Boolean,
      required: false,
      default: false
    },
    device: {
      type: String,
      required: false,
      default: 'desktop'
    }
  },
  data () {
    return {
      visible: true,
      oldScrollTop: 0,
      defApp: []
    }
  },
  mounted () {
    document.addEventListener('scroll', this.handleScroll, { passive: true })
  },
  methods: {
    ...mapActions(['MenuChange']),
    /**
     * 应用切换
     */
    switchApp (appCode) {
      this.defApp = []
      const applicationData = this.userInfo.apps.filter(item => item.code === appCode)
      const hideMessage = message.loading('正在切换应用!', 0)
      this.MenuChange(applicationData[0]).then((res) => {
          hideMessage()
      // eslint-disable-next-line handle-callback-err
      }).catch((err) => {
        message.error('应用切换异常')
      })
    },
    handleScroll () {
      if (!this.autoHideHeader) {
        return
      }
      const scrollTop = document.body.scrollTop + document.documentElement.scrollTop
      if (!this.ticking) {
        this.ticking = true
        requestAnimationFrame(() => {
          if (this.oldScrollTop > scrollTop) {
            this.visible = true
          } else if (scrollTop > 300 && this.visible) {
            this.visible = false
          } else if (scrollTop < 300 && !this.visible) {
            this.visible = true
          }
          this.oldScrollTop = scrollTop
          this.ticking = false
        })
      }
    },
    toggle () {
      this.$emit('toggle')
    }
  },
  beforeDestroy () {
    document.body.removeEventListener('scroll', this.handleScroll, true)
  }
}
</script>
<style lang="less">
@import '../index.less';
.header-animat{
  position: relative;
  z-index: @ant-global-header-zindex;
}
.showHeader-enter-active {
  transition: all 0.25s ease;
}
.showHeader-leave-active {
  transition: all 0.5s ease;
}
.showHeader-enter, .showHeader-leave-to {
  opacity: 0;
}
</style>
_web/src/components/GlobalHeader/index.js
New file
@@ -0,0 +1,2 @@
import GlobalHeader from './GlobalHeader'
export default GlobalHeader
_web/src/components/IconSelector/IconSelector.vue
New file
@@ -0,0 +1,86 @@
<template>
  <div :class="prefixCls">
    <a-tabs v-model="currentTab" @change="handleTabChange">
      <a-tab-pane v-for="v in icons" :tab="v.title" :key="v.key">
        <ul>
          <li v-for="(icon, key) in v.icons" :key="`${v.key}-${key}`" :class="{ 'active': selectedIcon==icon }" @click="handleSelectedIcon(icon)" >
            <a-icon :type="icon" :style="{ fontSize: '36px' }" />
          </li>
        </ul>
      </a-tab-pane>
    </a-tabs>
  </div>
</template>
<script>
import icons from './icons'
export default {
  name: 'IconSelect',
  props: {
    prefixCls: {
      type: String,
      default: 'ant-pro-icon-selector'
    },
    // eslint-disable-next-line
    value: {
      type: String
    }
  },
  data () {
    return {
      selectedIcon: this.value || '',
      currentTab: 'directional',
      icons
    }
  },
  watch: {
    value (val) {
      this.selectedIcon = val
      this.autoSwitchTab()
    }
  },
  created () {
    if (this.value) {
      this.autoSwitchTab()
    }
  },
  methods: {
    handleSelectedIcon (icon) {
      this.selectedIcon = icon
      this.$emit('change', icon)
    },
    handleTabChange (activeKey) {
      this.currentTab = activeKey
    },
    autoSwitchTab () {
      icons.some(item => item.icons.some(icon => icon === this.value) && (this.currentTab = item.key))
    }
  }
}
</script>
<style lang="less" scoped>
  @import "../index.less";
  ul{
    list-style: none;
    padding: 0;
    overflow-y: scroll;
    height: 250px;
    li{
      display: inline-block;
      padding: @padding-sm;
      margin: 3px 0;
      border-radius: @border-radius-base;
      &:hover, &.active{
        // box-shadow: 0px 0px 5px 2px @primary-color;
        cursor: pointer;
        color: @white;
        background-color: @primary-color;
      }
    }
  }
</style>
_web/src/components/IconSelector/README.md
New file
@@ -0,0 +1,48 @@
IconSelector
====
> 图标选择组件,常用于为某一个数据设定一个图标时使用
> eg: 设定菜单列表时,为每个菜单设定一个图标
该组件由 [@Saraka](https://github.com/saraka-tsukai) 封装
### 使用方式
```vue
<template>
    <div>
       <icon-selector @change="handleIconChange"/>
    </div>
</template>
<script>
import IconSelector from '@/components/IconSelector'
export default {
  name: 'YourView',
  components: {
    IconSelector
  },
  data () {
    return {
    }
  },
  methods: {
    handleIconChange (icon) {
      console.log('change Icon', icon)
    }
  }
}
</script>
```
### 事件
| 名称   | 说明                       | 类型   | 默认值 |
| ------ | -------------------------- | ------ | ------ |
| change | 当改变了 `icon` 选中项触发 | String | -      |
_web/src/components/IconSelector/icons.js
New file
@@ -0,0 +1,36 @@
/**
 * 增加新的图标时,请遵循以下数据结构
 * Adding new icon please follow the data structure below
 */
export default [
  {
    key: 'directional',
    title: '方向性图标',
    icons: ['step-backward', 'step-forward', 'fast-backward', 'fast-forward', 'shrink', 'arrows-alt', 'down', 'up', 'left', 'right', 'caret-up', 'caret-down', 'caret-left', 'caret-right', 'up-circle', 'down-circle', 'left-circle', 'right-circle', 'double-right', 'double-left', 'vertical-left', 'vertical-right', 'forward', 'backward', 'rollback', 'enter', 'retweet', 'swap', 'swap-left', 'swap-right', 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right', 'play-circle', 'up-square', 'down-square', 'left-square', 'right-square', 'login', 'logout', 'menu-fold', 'menu-unfold', 'border-bottom', 'border-horizontal', 'border-inner', 'border-left', 'border-right', 'border-top', 'border-verticle', 'pic-center', 'pic-left', 'pic-right', 'radius-bottomleft', 'radius-bottomright', 'radius-upleft', 'fullscreen', 'fullscreen-exit']
  },
  {
    key: 'suggested',
    title: '提示建议性图标',
    icons: ['question', 'question-circle', 'plus', 'plus-circle', 'pause', 'pause-circle', 'minus', 'minus-circle', 'plus-square', 'minus-square', 'info', 'info-circle', 'exclamation', 'exclamation-circle', 'close', 'close-circle', 'close-square', 'check', 'check-circle', 'check-square', 'clock-circle', 'warning', 'issues-close', 'stop']
  },
  {
    key: 'editor',
    title: '编辑类图标',
    icons: ['edit', 'form.vue', 'copy', 'scissor', 'delete', 'snippets', 'diff', 'highlight', 'align-center', 'align-left', 'align-right', 'bg-colors', 'bold', 'italic', 'underline', 'strikethrough', 'redo', 'undo', 'zoom-in', 'zoom-out', 'font-colors', 'font-size', 'line-height', 'column-height', 'dash', 'small-dash', 'sort-ascending', 'sort-descending', 'drag', 'ordered-list', 'radius-setting']
  },
  {
    key: 'data',
    title: '数据类图标',
    icons: ['area-chart', 'pie-chart', 'bar-chart', 'dot-chart', 'line-chart', 'radar-chart', 'heat-map', 'fall', 'rise', 'stock', 'box-plot', 'fund', 'sliders']
  },
  {
    key: 'brand_logo',
    title: '网站通用图标',
    icons: ['lock', 'unlock', 'bars', 'book', 'calendar', 'cloud', 'cloud-download', 'code', 'copy', 'credit-card', 'delete', 'desktop', 'download', 'ellipsis', 'file', 'file-text', 'file-unknown', 'file-pdf', 'file-word', 'file-excel', 'file-jpg', 'file-ppt', 'file-markdown', 'file-add', 'folder', 'folder-open', 'folder-add', 'hdd', 'frown', 'meh', 'smile', 'inbox', 'laptop', 'appstore', 'link', 'mail', 'mobile', 'notification', 'paper-clip', 'picture', 'poweroff', 'reload', 'search', 'setting', 'share-alt', 'shopping-cart', 'tablet', 'tag', 'tags', 'to-top', 'upload', 'user', 'video-camera', 'home', 'loading', 'loading-3-quarters', 'cloud-upload', 'star', 'heart', 'environment', 'eye', 'camera', 'save', 'team', 'solution', 'phone', 'filter', 'exception', 'export', 'customer-service', 'qrcode', 'scan', 'like', 'dislike', 'message', 'pay-circle', 'calculator', 'pushpin', 'bulb', 'select', 'switcher', 'rocket', 'bell', 'disconnect', 'database', 'compass', 'barcode', 'hourglass', 'key', 'flag', 'layout', 'printer', 'sound', 'usb', 'skin', 'tool', 'sync', 'wifi', 'car', 'schedule', 'user-add', 'user-delete', 'usergroup-add', 'usergroup-delete', 'man', 'woman', 'shop', 'gift', 'idcard', 'medicine-box', 'red-envelope', 'coffee', 'copyright', 'trademark', 'safety', 'wallet', 'bank', 'trophy', 'contacts', 'global', 'shake', 'api', 'fork', 'dashboard', 'table', 'profile', 'alert', 'audit', 'branches', 'build', 'border', 'crown', 'experiment', 'fire', 'money-collect', 'property-safety', 'read', 'reconciliation', 'rest', 'security-scan', 'insurance', 'interaction', 'safety-certificate', 'project', 'thunderbolt', 'block', 'cluster', 'deployment-unit', 'dollar', 'euro', 'pound', 'file-done', 'file-exclamation', 'file-protect', 'file-search', 'file-sync', 'gateway', 'gold', 'robot', 'shopping']
  },
  {
    key: 'application',
    title: '品牌和标识',
    icons: ['android', 'apple', 'windows', 'ie', 'chrome', 'github', 'aliwangwang', 'dingding', 'weibo-square', 'weibo-circle', 'taobao-circle', 'html5', 'weibo', 'twitter', 'wechat', 'youtube', 'alipay-circle', 'taobao', 'skype', 'qq', 'medium-workmark', 'gitlab', 'medium', 'linkedin', 'google-plus', 'dropbox', 'facebook', 'codepen', 'code-sandbox', 'amazon', 'google', 'codepen-circle', 'alipay', 'ant-design', 'aliyun', 'zhihu', 'slack', 'slack-square', 'behance', 'behance-square', 'dribbble', 'dribbble-square', 'instagram', 'yuque', 'alibaba', 'yahoo']
  }
]
_web/src/components/IconSelector/index.js
New file
@@ -0,0 +1,2 @@
import IconSelector from './IconSelector'
export default IconSelector
_web/src/components/Menu/SideMenu.vue
New file
@@ -0,0 +1,61 @@
<template>
  <a-layout-sider
    :class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null ]"
    width="230px"
    :collapsible="collapsible"
    v-model="collapsed"
    :trigger="null">
    <logo />
    <s-menu
      :collapsed="collapsed"
      :menu="menus"
      :theme="theme"
      :mode="mode"
      @select="onSelect"
    ></s-menu>
  </a-layout-sider>
</template>
<script>
import Logo from '@/components/tools/Logo'
import SMenu from './index'
import { mixin, mixinDevice } from '@/utils/mixin'
export default {
  name: 'SideMenu',
  components: { Logo, SMenu },
  mixins: [mixin, mixinDevice],
  props: {
    mode: {
      type: String,
      required: false,
      default: 'inline'
    },
    theme: {
      type: String,
      required: false,
      default: 'dark'
    },
    collapsible: {
      type: Boolean,
      required: false,
      default: false
    },
    collapsed: {
      type: Boolean,
      required: false,
      default: false
    },
    menus: {
      type: Array,
      required: true
    }
  },
  methods: {
    onSelect (obj) {
      this.$emit('menuSelect', obj)
    }
  }
}
</script>
_web/src/components/Menu/index.js
New file
@@ -0,0 +1,2 @@
import SMenu from './menu'
export default SMenu
_web/src/components/Menu/menu.js
New file
@@ -0,0 +1,177 @@
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
export default {
  name: 'SMenu',
  props: {
    menu: {
      type: Array,
      required: true
    },
    theme: {
      type: String,
      required: false,
      default: 'dark'
    },
    mode: {
      type: String,
      required: false,
      default: 'inline'
    },
    collapsed: {
      type: Boolean,
      required: false,
      default: false
    }
  },
  data () {
    return {
      openKeys: [],
      selectedKeys: [],
      cachedOpenKeys: []
    }
  },
  computed: {
    rootSubmenuKeys: vm => {
      const keys = []
      vm.menu.forEach(item => keys.push(item.path))
      return keys
    }
  },
  mounted () {
    this.updateMenu()
  },
  watch: {
    collapsed (val) {
      if (val) {
        this.cachedOpenKeys = this.openKeys.concat()
        this.openKeys = []
      } else {
        this.openKeys = this.cachedOpenKeys
      }
    },
    $route: function () {
      this.updateMenu()
    }
  },
  methods: {
    // select menu item
    onOpenChange (openKeys) {
      // 在水平模式下时执行,并且不再执行后续
      if (this.mode === 'horizontal') {
        this.openKeys = openKeys
        return
      }
      // 非水平模式时
      const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
      if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
        this.openKeys = openKeys
      } else {
        this.openKeys = latestOpenKey ? [latestOpenKey] : []
      }
    },
    onSelect ({ item, key, selectedKeys }) {
      this.selectedKeys = selectedKeys
      this.$emit('select', { item, key, selectedKeys })
    },
    updateMenu () {
      const routes = this.$route.matched.concat()
      const { hidden } = this.$route.meta
      if (routes.length >= 3 && hidden) {
        routes.pop()
        this.selectedKeys = [routes[routes.length - 1].path]
      } else {
        this.selectedKeys = [routes.pop().path]
      }
      const openKeys = []
      if (this.mode === 'inline') {
        routes.forEach(item => {
          openKeys.push(item.path)
        })
      }
      this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
    },
    // render
    renderItem (menu) {
      if (!menu.hidden) {
        return menu.children && !menu.hideChildrenInMenu ? this.renderSubMenu(menu) : this.renderMenuItem(menu)
      }
      return null
    },
    renderMenuItem (menu) {
      const target = menu.meta.target || null
      const CustomTag = target && 'a' || 'router-link'
      const props = { to: { name: menu.name } }
      const attrs = { href: menu.path, target: menu.meta.target }
      if (menu.children && menu.hideChildrenInMenu) {
        // 把有子菜单的 并且 父菜单是要隐藏子菜单的
        // 都给子菜单增加一个 hidden 属性
        // 用来给刷新页面时, selectedKeys 做控制用
        menu.children.forEach(item => {
          item.meta = Object.assign(item.meta, { hidden: true })
        })
      }
      return (
        <Menu.Item {...{ key: menu.path }}>
          <CustomTag {...{ props, attrs }}>
            {this.renderIcon(menu.meta.icon)}
            <span>{menu.meta.title}</span>
          </CustomTag>
        </Menu.Item>
      )
    },
    renderSubMenu (menu) {
      const itemArr = []
      if (!menu.hideChildrenInMenu) {
        menu.children.forEach(item => itemArr.push(this.renderItem(item)))
      }
      return (
        <Menu.SubMenu {...{ key: menu.path }}>
          <span slot="title">
            {this.renderIcon(menu.meta.icon)}
            <span>{menu.meta.title}</span>
          </span>
          {itemArr}
        </Menu.SubMenu>
      )
    },
    renderIcon (icon) {
      if (icon === 'none' || icon === undefined) {
        return null
      }
      const props = {}
      typeof (icon) === 'object' ? props.component = icon : props.type = icon
      return (
        <Icon {... { props } }/>
      )
    }
  },
  render () {
    const dynamicProps = {
      props: {
        mode: this.mode,
        theme: this.theme,
        openKeys: this.openKeys,
        selectedKeys: this.selectedKeys
      },
      on: {
        openChange: this.onOpenChange,
        select: this.onSelect
      }
    }
    const menuTree = this.menu.map(item => {
      if (item.hidden) {
        return null
      }
      return this.renderItem(item)
    })
    return (<Menu {...dynamicProps}>{menuTree}</Menu>)
  }
}
_web/src/components/Menu/menu.render.js
New file
@@ -0,0 +1,156 @@
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
const { Item, SubMenu } = Menu
export default {
  name: 'SMenu',
  props: {
    menu: {
      type: Array,
      required: true
    },
    theme: {
      type: String,
      required: false,
      default: 'dark'
    },
    mode: {
      type: String,
      required: false,
      default: 'inline'
    },
    collapsed: {
      type: Boolean,
      required: false,
      default: false
    }
  },
  data () {
    return {
      openKeys: [],
      selectedKeys: [],
      cachedOpenKeys: []
    }
  },
  computed: {
    rootSubmenuKeys: vm => {
      const keys = []
      vm.menu.forEach(item => keys.push(item.path))
      return keys
    }
  },
  created () {
    this.updateMenu()
  },
  watch: {
    collapsed (val) {
      if (val) {
        this.cachedOpenKeys = this.openKeys.concat()
        this.openKeys = []
      } else {
        this.openKeys = this.cachedOpenKeys
      }
    },
    $route: function () {
      this.updateMenu()
    }
  },
  methods: {
    renderIcon: function (h, icon) {
      if (icon === 'none' || icon === undefined) {
        return null
      }
      const props = {}
      typeof (icon) === 'object' ? props.component = icon : props.type = icon
      return h(Icon, { props: { ...props } })
    },
    renderMenuItem: function (h, menu, pIndex, index) {
      const target = menu.meta.target || null
      return h(Item, { key: menu.path ? menu.path : 'item_' + pIndex + '_' + index }, [
        h('router-link', { attrs: { to: { name: menu.name }, target: target } }, [
          this.renderIcon(h, menu.meta.icon),
          h('span', [menu.meta.title])
        ])
      ])
    },
    renderSubMenu: function (h, menu, pIndex, index) {
      const this2_ = this
      const subItem = [h('span', { slot: 'title' }, [this.renderIcon(h, menu.meta.icon), h('span', [menu.meta.title])])]
      const itemArr = []
      const pIndex_ = pIndex + '_' + index
      console.log('menu', menu)
      if (!menu.hideChildrenInMenu) {
        menu.children.forEach(function (item, i) {
          itemArr.push(this2_.renderItem(h, item, pIndex_, i))
        })
      }
      return h(SubMenu, { key: menu.path ? menu.path : 'submenu_' + pIndex + '_' + index }, subItem.concat(itemArr))
    },
    renderItem: function (h, menu, pIndex, index) {
      if (!menu.hidden) {
        return menu.children && !menu.hideChildrenInMenu
          ? this.renderSubMenu(h, menu, pIndex, index)
          : this.renderMenuItem(h, menu, pIndex, index)
      }
    },
    renderMenu: function (h, menuTree) {
      const this2_ = this
      const menuArr = []
      menuTree.forEach(function (menu, i) {
        if (!menu.hidden) {
          menuArr.push(this2_.renderItem(h, menu, '0', i))
        }
      })
      return menuArr
    },
    onOpenChange (openKeys) {
      const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
      if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
        this.openKeys = openKeys
      } else {
        this.openKeys = latestOpenKey ? [latestOpenKey] : []
      }
    },
    updateMenu () {
      const routes = this.$route.matched.concat()
      if (routes.length >= 4 && this.$route.meta.hidden) {
        routes.pop()
        this.selectedKeys = [routes[2].path]
      } else {
        this.selectedKeys = [routes.pop().path]
      }
      const openKeys = []
      if (this.mode === 'inline') {
        routes.forEach(item => {
          openKeys.push(item.path)
        })
      }
      this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
    }
  },
  render (h) {
    return h(
      Menu,
      {
        props: {
          theme: this.$props.theme,
          mode: this.$props.mode,
          openKeys: this.openKeys,
          selectedKeys: this.selectedKeys
        },
        on: {
          openChange: this.onOpenChange,
          select: obj => {
            this.selectedKeys = obj.selectedKeys
            this.$emit('select', obj)
          }
        }
      },
      this.renderMenu(h, this.menu)
    )
  }
}
_web/src/components/MultiTab/MultiTab.vue
New file
@@ -0,0 +1,163 @@
<script>
import events from './events'
export default {
  name: 'MultiTab',
  data () {
    return {
      fullPathList: [],
      pages: [],
      activeKey: '',
      newTabIndex: 0
    }
  },
  created () {
    // bind event
    events.$on('open', val => {
      if (!val) {
        throw new Error(`multi-tab: open tab ${val} err`)
      }
      this.activeKey = val
    }).$on('close', val => {
      if (!val) {
        this.closeThat(this.activeKey)
        return
      }
      this.closeThat(val)
    }).$on('rename', ({ key, name }) => {
      console.log('rename', key, name)
      try {
        const item = this.pages.find(item => item.path === key)
        item.meta.customTitle = name
        this.$forceUpdate()
      } catch (e) {
      }
    })
    this.pages.push(this.$route)
    this.fullPathList.push(this.$route.fullPath)
    this.selectedLastPath()
  },
  methods: {
    onEdit (targetKey, action) {
      this[action](targetKey)
    },
    remove (targetKey) {
      this.pages = this.pages.filter(page => page.fullPath !== targetKey)
      this.fullPathList = this.fullPathList.filter(path => path !== targetKey)
      // 判断当前标签是否关闭,若关闭则跳转到最后一个还存在的标签页
      if (!this.fullPathList.includes(this.activeKey)) {
        this.selectedLastPath()
      }
    },
    selectedLastPath () {
      this.activeKey = this.fullPathList[this.fullPathList.length - 1]
    },
    // content menu
    closeThat (e) {
      // 判断是否为最后一个标签页,如果是最后一个,则无法被关闭
      if (this.fullPathList.length > 1) {
        this.remove(e)
      } else {
        this.$message.info('这是最后一个标签了, 无法被关闭')
      }
    },
    closeLeft (e) {
      const currentIndex = this.fullPathList.indexOf(e)
      if (currentIndex > 0) {
        this.fullPathList.forEach((item, index) => {
          if (index < currentIndex) {
            this.remove(item)
          }
        })
      } else {
        this.$message.info('左侧没有标签')
      }
    },
    closeRight (e) {
      const currentIndex = this.fullPathList.indexOf(e)
      if (currentIndex < (this.fullPathList.length - 1)) {
        this.fullPathList.forEach((item, index) => {
          if (index > currentIndex) {
            this.remove(item)
          }
        })
      } else {
        this.$message.info('右侧没有标签')
      }
    },
    closeAll (e) {
      const currentIndex = this.fullPathList.indexOf(e)
      this.fullPathList.forEach((item, index) => {
        if (index !== currentIndex) {
          this.remove(item)
        }
      })
    },
    closeMenuClick (key, route) {
      this[key](route)
    },
    renderTabPaneMenu (e) {
      return (
        <a-menu {...{ on: { click: ({ key, item, domEvent }) => { this.closeMenuClick(key, e) } } }}>
          <a-menu-item key="closeThat">关闭当前标签</a-menu-item>
          <a-menu-item key="closeRight">关闭右侧</a-menu-item>
          <a-menu-item key="closeLeft">关闭左侧</a-menu-item>
          <a-menu-item key="closeAll">关闭全部</a-menu-item>
        </a-menu>
      )
    },
    // render
    renderTabPane (title, keyPath) {
      const menu = this.renderTabPaneMenu(keyPath)
      return (
        <a-dropdown overlay={menu} trigger={['contextmenu']}>
          <span style={{ userSelect: 'none' }}>{ title }</span>
        </a-dropdown>
      )
    }
  },
  watch: {
    '$route': function (newVal) {
      this.activeKey = newVal.fullPath
      if (this.fullPathList.indexOf(newVal.fullPath) < 0) {
        this.fullPathList.push(newVal.fullPath)
        this.pages.push(newVal)
      }
    },
    activeKey: function (newPathKey) {
      this.$router.push({ path: newPathKey })
    }
  },
  render () {
    const { onEdit, $data: { pages } } = this
    const panes = pages.map(page => {
      return (
        <a-tab-pane
          style={{ height: 0, background: '#8999ee', color: '#899ee' }}
          tab={this.renderTabPane(page.meta.customTitle || page.meta.title, page.fullPath)}
          key={page.fullPath} closable={pages.length > 1}
        >
        </a-tab-pane>)
    })
    return (
      <div class="ant-pro-multi-tab">
        <div class="ant-pro-multi-tab-wrapper">
          <a-tabs
            hideAdd
            tabBarGutter={-1}
            type={'editable-card'}
            v-model={this.activeKey}
            tabBarStyle={{ background: '#FFF', margin: 0, paddingLeft: '0px', paddingTop: '0px' }}
            {...{ on: { edit: onEdit } }}>
            {panes}
          </a-tabs>
        </div>
      </div>
    )
  }
}
</script>
_web/src/components/MultiTab/events.js
New file
@@ -0,0 +1,2 @@
import Vue from 'vue'
export default new Vue()
_web/src/components/MultiTab/index.js
New file
@@ -0,0 +1,40 @@
import events from './events'
import MultiTab from './MultiTab'
import './index.less'
const api = {
  /**
   * open new tab on route fullPath
   * @param config
   */
  open: function (config) {
    events.$emit('open', config)
  },
  rename: function (key, name) {
    events.$emit('rename', { key: key, name: name })
  },
  /**
   * close current page
   */
  closeCurrentPage: function () {
    this.close()
  },
  /**
   * close route fullPath tab
   * @param config
   */
  close: function (config) {
    events.$emit('close', config)
  }
}
MultiTab.install = function (Vue) {
  if (Vue.prototype.$multiTab) {
    return
  }
  api.instance = events
  Vue.prototype.$multiTab = api
  Vue.component('multi-tab', MultiTab)
}
export default MultiTab
_web/src/components/MultiTab/index.less
New file
@@ -0,0 +1,25 @@
@import '../index';
@multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab";
@multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper";
/*
.topmenu .@{multi-tab-prefix-cls} {
  max-width: 1200px;
  margin: -23px auto 24px auto;
}
*/
.@{multi-tab-prefix-cls} {
  margin: -23px -24px 24px -24px;
  background: #fff;
}
.topmenu .@{multi-tab-wrapper-prefix-cls} {
  max-width: 1200px;
  margin: 0 auto;
}
.topmenu.content-width-Fluid .@{multi-tab-wrapper-prefix-cls} {
  max-width: 100%;
  margin: 0 auto;
}
_web/src/components/NProgress/nprogress.less
New file
@@ -0,0 +1,76 @@
@import url('../index.less');
/* Make clicks pass-through */
#nprogress {
  pointer-events: none;
}
#nprogress .bar {
  background: @primary-color;
  position: fixed;
  z-index: 1031;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
  display: block;
  position: absolute;
  right: 0px;
  width: 100px;
  height: 100%;
  box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color;
  opacity: 1.0;
  -webkit-transform: rotate(3deg) translate(0px, -4px);
  -ms-transform: rotate(3deg) translate(0px, -4px);
  transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
  display: block;
  position: fixed;
  z-index: 1031;
  top: 15px;
  right: 15px;
}
#nprogress .spinner-icon {
  width: 18px;
  height: 18px;
  box-sizing: border-box;
  border: solid 2px transparent;
  border-top-color: @primary-color;
  border-left-color: @primary-color;
  border-radius: 50%;
  -webkit-animation: nprogress-spinner 400ms linear infinite;
  animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
  overflow: hidden;
  position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
  position: absolute;
}
@-webkit-keyframes nprogress-spinner {
  0%   { -webkit-transform: rotate(0deg); }
  100% { -webkit-transform: rotate(360deg); }
}
@keyframes nprogress-spinner {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
_web/src/components/NoticeIcon/NoticeIcon.vue
New file
@@ -0,0 +1,142 @@
<template>
  <a-popover
    v-model="visible"
    trigger="click"
    placement="bottomRight"
    overlayClassName="header-notice-wrapper"
    :getPopupContainer="() => $refs.noticeRef.parentElement"
    :autoAdjustOverflow="true"
    :arrowPointAtCenter="true"
    :overlayStyle="{ width: '300px', top: '50px' }"
  >
    <template slot="content">
      <a-spin :spinning="loading">
        <a-tabs>
          <a-tab-pane tab="通知" key="1">
            <a-list>
              <a-list-item>
                <a-list-item-meta title="你收到了 14 份新周报" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
                </a-list-item-meta>
              </a-list-item>
            </a-list>
          </a-tab-pane>
          <a-tab-pane tab="消息" key="2">
            <a-list>
              <a-list-item>
                <a-list-item-meta title="你收到了 14 份新周报" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
                </a-list-item-meta>
              </a-list-item>
            </a-list>
          </a-tab-pane>
          <a-tab-pane tab="待办" key="3">
            <a-list>
              <a-list-item>
                <a-list-item-meta title="你收到了 14 份新周报" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
                </a-list-item-meta>
              </a-list-item>
            </a-list>
          </a-tab-pane>
        </a-tabs>
      </a-spin>
    </template>
    <span @click="fetchNotice" class="header-notice" ref="noticeRef" style="padding: 0 18px">
      <a-badge count="12">
        <a-icon style="font-size: 16px; padding: 4px" type="bell" />
      </a-badge>
    </span>
  </a-popover>
</template>
<script>
export default {
  name: 'HeaderNotice',
  data () {
    return {
      loading: false,
      visible: false
    }
  },
  methods: {
    fetchNotice () {
      if (!this.visible) {
        this.loading = true
        setTimeout(() => {
          this.loading = false
        }, 100)
      } else {
        this.loading = false
      }
      this.visible = !this.visible
    }
  }
}
</script>
<style lang="css">
  .header-notice-wrapper {
    top: 50px !important;
  }
</style>
<style lang="less" scoped>
  .header-notice{
    display: inline-block;
    transition: all 0.3s;
    span {
      vertical-align: initial;
    }
  }
</style>
_web/src/components/NoticeIcon/index.js
New file
@@ -0,0 +1,2 @@
import NoticeIcon from './NoticeIcon'
export default NoticeIcon
_web/src/components/NumberInfo/NumberInfo.vue
New file
@@ -0,0 +1,54 @@
<template>
  <div :class="[prefixCls]">
    <slot name="subtitle">
      <div :class="[`${prefixCls}-subtitle`]">{{ typeof subTitle === 'string' ? subTitle : subTitle() }}</div>
    </slot>
    <div class="number-info-value">
      <span>{{ total }}</span>
      <span class="sub-total">
        {{ subTotal }}
        <icon :type="`caret-${status}`" />
      </span>
    </div>
  </div>
</template>
<script>
import Icon from 'ant-design-vue/es/icon'
export default {
  name: 'NumberInfo',
  props: {
    prefixCls: {
      type: String,
      default: 'ant-pro-number-info'
    },
    total: {
      type: Number,
      required: true
    },
    subTotal: {
      type: Number,
      required: true
    },
    subTitle: {
      type: [String, Function],
      default: ''
    },
    status: {
      type: String,
      default: 'up'
    }
  },
  components: {
    Icon
  },
  data () {
    return {}
  }
}
</script>
<style lang="less" scoped>
  @import "index";
</style>
_web/src/components/NumberInfo/index.js
New file
@@ -0,0 +1,3 @@
import NumberInfo from './NumberInfo'
export default NumberInfo
_web/src/components/NumberInfo/index.less
New file
@@ -0,0 +1,55 @@
@import "../index";
@numberInfo-prefix-cls: ~"@{ant-pro-prefix}-number-info";
.@{numberInfo-prefix-cls} {
  .ant-pro-number-info-subtitle {
    color: @text-color-secondary;
    font-size: @font-size-base;
    height: 22px;
    line-height: 22px;
    overflow: hidden;
    text-overflow: ellipsis;
    word-break: break-all;
    white-space: nowrap;
  }
  .number-info-value {
    margin-top: 4px;
    font-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    word-break: break-all;
    white-space: nowrap;
    & > span {
      color: @heading-color;
      display: inline-block;
      line-height: 32px;
      height: 32px;
      font-size: 24px;
      margin-right: 32px;
    }
    .sub-total {
      color: @text-color-secondary;
      font-size: @font-size-lg;
      vertical-align: top;
      margin-right: 0;
      i {
        font-size: 12px;
        transform: scale(0.82);
        margin-left: 4px;
      }
      :global {
        .anticon-caret-up {
          color: @red-6;
        }
        .anticon-caret-down {
          color: @green-6;
        }
      }
    }
  }
}
_web/src/components/NumberInfo/index.md
New file
@@ -0,0 +1,43 @@
# NumberInfo 数据文本
常用在数据卡片中,用于突出展示某个业务数据。
引用方式:
```javascript
import NumberInfo from '@/components/NumberInfo'
export default {
    components: {
        NumberInfo
    }
}
```
## 代码演示  [demo](https://pro.loacg.com/test/home)
```html
<number-info
    :sub-title="() => { return 'Visits this week' }"
    :total="12321"
    status="up"
    :sub-total="17.1"></number-info>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
title | 标题 | ReactNode\|string | -
subTitle | 子标题 | ReactNode\|string | -
total | 总量 | ReactNode\|string | -
subTotal | 子总量 | ReactNode\|string | -
status | 增加状态 | 'up \| down' | -
theme | 状态样式 | string | 'light'
gap | 设置数字和描述之间的间距(像素)| number | 8
_web/src/components/PageHeader/PageHeader.vue
New file
@@ -0,0 +1,202 @@
<template>
  <div class="page-header">
    <div class="page-header-index-wide">
      <s-breadcrumb />
      <div class="detail">
        <div class="main" v-if="!$route.meta.hiddenHeaderContent">
          <div class="row">
            <img v-if="logo" :src="logo" class="logo"/>
            <h1 v-if="title" class="title">{{ title }}</h1>
            <div class="action">
              <slot name="action"></slot>
            </div>
          </div>
          <div class="row">
            <div v-if="avatar" class="avatar">
              <a-avatar :src="avatar" />
            </div>
            <div v-if="this.$slots.content" class="headerContent">
              <slot name="content"></slot>
            </div>
            <div v-if="this.$slots.extra" class="extra">
              <slot name="extra"></slot>
            </div>
          </div>
          <div>
            <slot name="pageMenu"></slot>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import Breadcrumb from '@/components/tools/Breadcrumb'
export default {
  name: 'PageHeader',
  components: {
    's-breadcrumb': Breadcrumb
  },
  props: {
    title: {
      type: [String, Boolean],
      default: true,
      required: false
    },
    logo: {
      type: String,
      default: '',
      required: false
    },
    avatar: {
      type: String,
      default: '',
      required: false
    }
  },
  data () {
    return {}
  }
}
</script>
<style lang="less" scoped>
.page-header {
  background: #fff;
  padding: 10px 32px 0;
  border-bottom: 0px solid #e8e8e8;
  .breadcrumb {
    margin-bottom: 10px;
  }
  .detail {
    display: flex;
    /*margin-bottom: 16px;*/
    .avatar {
      flex: 0 1 72px;
      margin: 0 24px 8px 0;
      & > span {
        border-radius: 72px;
        display: block;
        width: 72px;
        height: 72px;
      }
    }
    .main {
      width: 100%;
      flex: 0 1 auto;
      .row {
        display: flex;
        width: 100%;
        .avatar {
          margin-bottom: 10px;
        }
      }
      .title {
        font-size: 20px;
        font-weight: 500;
        font-size: 20px;
        line-height: 28px;
        font-weight: 500;
        color: rgba(0, 0, 0, 0.85);
        margin-bottom: 16px;
        flex: auto;
      }
      .logo {
        width: 28px;
        height: 28px;
        border-radius: 4px;
        margin-right: 16px;
      }
      .content,
      .headerContent {
        flex: auto;
        color: rgba(0, 0, 0, 0.45);
        line-height: 22px;
        .link {
          margin-top: 16px;
          line-height: 24px;
          a {
            font-size: 14px;
            margin-right: 32px;
          }
        }
      }
      .extra {
        flex: 0 1 auto;
        margin-left: 88px;
        min-width: 242px;
        text-align: right;
      }
      .action {
        margin-left: 56px;
        min-width: 266px;
        flex: 0 1 auto;
        text-align: right;
        &:empty {
          display: none;
        }
      }
    }
  }
}
.mobile .page-header {
  .main {
    .row {
      flex-wrap: wrap;
      .avatar {
        flex: 0 1 25%;
        margin: 0 2% 8px 0;
      }
      .content,
      .headerContent {
        flex: 0 1 70%;
        .link {
          margin-top: 16px;
          line-height: 24px;
          a {
            font-size: 14px;
            margin-right: 10px;
          }
        }
      }
      .extra {
        flex: 1 1 auto;
        margin-left: 0;
        min-width: 0;
        text-align: right;
      }
      .action {
        margin-left: unset;
        min-width: 266px;
        flex: 0 1 auto;
        text-align: left;
        margin-bottom: 12px;
        &:empty {
          display: none;
        }
      }
    }
  }
}
</style>
_web/src/components/PageHeader/index.js
New file
@@ -0,0 +1,2 @@
import PageHeader from './PageHeader'
export default PageHeader
_web/src/components/PageLoading/index.jsx
New file
@@ -0,0 +1,106 @@
import { Spin } from 'ant-design-vue'
export const PageLoading = {
  name: 'PageLoading',
  props: {
    tip: {
      type: String,
      default: 'Loading..'
    },
    size: {
      type: String,
      default: 'large'
    }
  },
  render () {
    const style = {
      textAlign: 'center',
      background: 'rgba(0,0,0,0.6)',
      position: 'fixed',
      top: 0,
      bottom: 0,
      left: 0,
      right: 0,
      zIndex: 1100
    }
    const spinStyle = {
      position: 'absolute',
      left: '50%',
      top: '40%',
      transform: 'translate(-50%, -50%)'
    }
    return (<div style={style}>
      <Spin size={this.size} style={spinStyle} tip={this.tip} />
    </div>)
  }
}
const version = '0.0.1'
const loading = {}
loading.newInstance = (Vue, options) => {
  let loadingElement = document.querySelector('body>div[type=loading]')
  if (!loadingElement) {
    loadingElement = document.createElement('div')
    loadingElement.setAttribute('type', 'loading')
    loadingElement.setAttribute('class', 'ant-loading-wrapper')
    document.body.appendChild(loadingElement)
  }
  const cdProps = Object.assign({ visible: false, size: 'large', tip: 'Loading...' }, options)
  const instance = new Vue({
    data () {
      return {
        ...cdProps
      }
    },
    render () {
      const { tip } = this
      const props = {}
      this.tip && (props.tip = tip)
      if (this.visible) {
        return <PageLoading { ...{ props } } />
      }
      return null
    }
  }).$mount(loadingElement)
  function update (config) {
    const { visible, size, tip } = { ...cdProps, ...config }
    instance.$set(instance, 'visible', visible)
    if (tip) {
      instance.$set(instance, 'tip', tip)
    }
    if (size) {
      instance.$set(instance, 'size', size)
    }
  }
  return {
    instance,
    update
  }
}
const api = {
  show: function (options) {
    this.instance.update({ ...options, visible: true })
  },
  hide: function () {
    this.instance.update({ visible: false })
  }
}
const install = function (Vue, options) {
  if (Vue.prototype.$loading) {
    return
  }
  api.instance = loading.newInstance(Vue, options)
  Vue.prototype.$loading = api
}
export default {
  version,
  install
}
_web/src/components/Result/Result.vue
New file
@@ -0,0 +1,109 @@
<template>
  <div class="result">
    <div>
      <a-icon :class="{ 'icon': true, [`${type}`]: true }" :type="localIsSuccess ? 'check-circle' : 'close-circle'"/>
    </div>
    <div class="title">
      <slot name="title">
        {{ title }}
      </slot>
    </div>
    <div class="description">
      <slot name="description">
        {{ description }}
      </slot>
    </div>
    <div class="extra" v-if="$slots.default">
      <slot></slot>
    </div>
    <div class="action" v-if="$slots.action">
      <slot name="action"></slot>
    </div>
  </div>
</template>
<script>
const resultEnum = ['success', 'error']
export default {
  name: 'Result',
  props: {
    /** @Deprecated */
    isSuccess: {
      type: Boolean,
      default: false
    },
    type: {
      type: String,
      default: resultEnum[0],
      validator (val) {
        return (val) => resultEnum.includes(val)
      }
    },
    title: {
      type: String,
      default: ''
    },
    description: {
      type: String,
      default: ''
    }
  },
  computed: {
    localIsSuccess: function () {
      return this.type === resultEnum[0]
    }
  }
}
</script>
<style lang="less" scoped>
  .result {
    text-align: center;
    width: 72%;
    margin: 0 auto;
    padding: 24px 0 8px;
    .icon {
      font-size: 72px;
      line-height: 72px;
      margin-bottom: 24px;
    }
    .success {
      color: #52c41a;
    }
    .error {
      color: red;
    }
    .title {
      font-size: 24px;
      color: rgba(0, 0, 0, .85);
      font-weight: 500;
      line-height: 32px;
      margin-bottom: 16px;
    }
    .description {
      font-size: 14px;
      line-height: 22px;
      color: rgba(0, 0, 0, 0.45);
      margin-bottom: 24px;
    }
    .extra {
      background: #fafafa;
      padding: 24px 40px;
      border-radius: 2px;
      text-align: left;
    }
    .action {
      margin-top: 32px;
    }
  }
  .mobile {
    .result {
      width: 100%;
      margin: 0 auto;
      padding: unset;
    }
  }
</style>
_web/src/components/Result/index.js
New file
@@ -0,0 +1,2 @@
import Result from './Result.vue'
export default Result
_web/src/components/Search/GlobalSearch.jsx
New file
@@ -0,0 +1,63 @@
import { Select } from 'ant-design-vue'
import './index.less'
const GlobalSearch = {
  name: 'GlobalSearch',
  data () {
    return {
      visible: false
    }
  },
  mounted () {
    const keyboardHandle = (e) => {
      e.preventDefault()
      e.stopPropagation()
      const { ctrlKey, shiftKey, altKey, keyCode } = e
      console.log('keyCode:', e.keyCode, e)
      // key is `K` and hold ctrl
      if (keyCode === 75 && ctrlKey && !shiftKey && !altKey) {
        this.visible = !this.visible
      }
    }
    document.addEventListener('keydown', keyboardHandle)
  },
  render () {
    const { visible } = this
    const handleSearch = (e) => {
      this.$emit('search', e)
    }
    const handleChange = (e) => {
      this.$emit('change', e)
    }
    if (!visible) {
      return null
    }
    return (
      <div class={'global-search global-search-wrapper'}>
        <div class={'global-search-box'}>
          <Select
            size={'large'}
            showSearch
            placeholder="Input search text.."
            style={{ width: '100%' }}
            defaultActiveFirstOption={false}
            showArrow={false}
            filterOption={false}
            onSearch={handleSearch}
            onChange={handleChange}
            notFoundContent={null}
          >
          </Select>
          <div class={'global-search-tips'}>Open with Ctrl/⌘ + K</div>
        </div>
      </div>
    )
  }
}
GlobalSearch.install = function (Vue) {
  Vue.component(GlobalSearch.name, GlobalSearch)
}
export default GlobalSearch
_web/src/components/Search/index.less
New file
@@ -0,0 +1,25 @@
@import "~ant-design-vue/es/style/themes/default";
.global-search-wrapper {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: @zindex-modal-mask;
  background: @modal-mask-bg;
  .global-search-box {
    position: absolute;
    top: 20%;
    left: 50%;
    width: 450px;
    transform: translate(-50%, -50%);
    .global-search-tips {
      color: @white;
      font-size: @font-size-lg;
      text-align: right;
    }
  }
}
_web/src/components/SettingDrawer/SettingDrawer.vue
New file
@@ -0,0 +1,352 @@
<template>
  <div class="setting-drawer">
    <a-drawer
      width="300"
      placement="right"
      @close="onClose"
      :closable="false"
      :visible="visible"
      :drawer-style="{ position: 'absolute' }"
      style="position: absolute"
    >
      <div class="setting-drawer-index-content">
        <div :style="{ marginBottom: '24px' }">
          <h3 class="setting-drawer-index-title">整体风格设置</h3>
          <div class="setting-drawer-index-blockChecbox">
            <a-tooltip>
              <template slot="title">
                暗色菜单风格
              </template>
              <div class="setting-drawer-index-item" @click="handleMenuTheme('dark')">
                <img src="https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg" alt="dark">
                <div class="setting-drawer-index-selectIcon" v-if="navTheme === 'dark'">
                  <a-icon type="check"/>
                </div>
              </div>
            </a-tooltip>
            <a-tooltip>
              <template slot="title">
                亮色菜单风格
              </template>
              <div class="setting-drawer-index-item" @click="handleMenuTheme('light')">
                <img src="https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg" alt="light">
                <div class="setting-drawer-index-selectIcon" v-if="navTheme !== 'dark'">
                  <a-icon type="check"/>
                </div>
              </div>
            </a-tooltip>
          </div>
        </div>
        <div :style="{ marginBottom: '24px' }">
          <h3 class="setting-drawer-index-title">主题色</h3>
          <div style="height: 20px">
            <a-tooltip class="setting-drawer-theme-color-colorBlock" v-for="(item, index) in colorList" :key="index">
              <template slot="title">
                {{ item.key }}
              </template>
              <a-tag :color="item.color" @click="changeColor(item.color)">
                <a-icon type="check" v-if="item.color === primaryColor"></a-icon>
              </a-tag>
            </a-tooltip>
          </div>
        </div>
        <a-divider />
        <div :style="{ marginBottom: '24px' }">
          <h3 class="setting-drawer-index-title">导航模式</h3>
          <div class="setting-drawer-index-blockChecbox">
            <a-tooltip>
              <template slot="title">
                侧边栏导航
              </template>
              <div class="setting-drawer-index-item" @click="handleLayout('sidemenu')">
                <img src="https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg" alt="sidemenu">
                <div class="setting-drawer-index-selectIcon" v-if="layoutMode === 'sidemenu'">
                  <a-icon type="check"/>
                </div>
              </div>
            </a-tooltip>
            <a-tooltip>
              <template slot="title">
                顶部栏导航
              </template>
              <div class="setting-drawer-index-item" @click="handleLayout('topmenu')">
                <img src="https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg" alt="topmenu">
                <div class="setting-drawer-index-selectIcon" v-if="layoutMode !== 'sidemenu'">
                  <a-icon type="check"/>
                </div>
              </div>
            </a-tooltip>
          </div>
          <div :style="{ marginTop: '24px' }">
            <a-list :split="false">
              <a-list-item>
                <a-tooltip slot="actions">
                  <template slot="title">
                    该设定仅 [顶部栏导航] 时有效
                  </template>
                  <a-select size="small" style="width: 80px;" :defaultValue="contentWidth" @change="handleContentWidthChange">
                    <a-select-option value="Fixed">固定</a-select-option>
                    <a-select-option value="Fluid" v-if="layoutMode !== 'sidemenu'">流式</a-select-option>
                  </a-select>
                </a-tooltip>
                <a-list-item-meta>
                  <div slot="title">内容区域宽度</div>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-switch slot="actions" size="small" :defaultChecked="fixedHeader" @change="handleFixedHeader" />
                <a-list-item-meta>
                  <div slot="title">固定 Header</div>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-switch slot="actions" size="small" :disabled="!fixedHeader" :defaultChecked="autoHideHeader" @change="handleFixedHeaderHidden" />
                <a-list-item-meta>
                  <a-tooltip slot="title" placement="left">
                    <template slot="title">固定 Header 时可配置</template>
                    <div :style="{ opacity: !fixedHeader ? '0.5' : '1' }">下滑时隐藏 Header</div>
                  </a-tooltip>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item >
                <a-switch slot="actions" size="small" :disabled="(layoutMode === 'topmenu')" :defaultChecked="fixSiderbar" @change="handleFixSiderbar" />
                <a-list-item-meta>
                  <div slot="title" :style="{ textDecoration: layoutMode === 'topmenu' ? 'line-through' : 'unset' }">固定侧边菜单</div>
                </a-list-item-meta>
              </a-list-item>
            </a-list>
          </div>
        </div>
        <a-divider />
        <div :style="{ marginBottom: '24px' }">
          <h3 class="setting-drawer-index-title">其他设置</h3>
          <div>
            <a-list :split="false">
              <a-list-item>
                <a-switch slot="actions" size="small" :defaultChecked="colorWeak" @change="onColorWeak" />
                <a-list-item-meta>
                  <div slot="title">色弱模式</div>
                </a-list-item-meta>
              </a-list-item>
              <a-list-item>
                <a-switch slot="actions" size="small" :defaultChecked="multiTab" @change="onMultiTab" />
                <a-list-item-meta>
                  <div slot="title">多页签模式</div>
                </a-list-item-meta>
              </a-list-item>
            </a-list>
          </div>
        </div>
        <a-divider />
        <div :style="{ marginBottom: '24px' }">
          <a-button
            @click="doCopy"
            icon="copy"
            block
          >拷贝设置</a-button>
          <a-alert type="warning" :style="{ marginTop: '24px' }">
            <span slot="message">
              配置栏只在开发环境用于预览,生产环境不会展现,请手动修改配置文件。修改配置文件后,需要清空本地缓存和LocalStorage
              <a href="https://github.com/sendya/ant-design-pro-vue/blob/master/src/config/defaultSettings.js" target="_blank">src/config/defaultSettings.js</a>
            </span>
          </a-alert>
        </div>
      </div>
      <div class="setting-drawer-index-handle" @click="toggle" slot="handle">
        <a-icon type="setting" v-if="!visible"/>
        <a-icon type="close" v-else/>
      </div>
    </a-drawer>
  </div>
</template>
<script>
/* import { DetailList } from '@/components'
import SettingItem from './SettingItem' */
import config from '@/config/defaultSettings'
import { updateTheme, updateColorWeak, colorList } from './settingConfig'
import { mixin, mixinDevice } from '@/utils/mixin'
export default {
  components: {
    // DetailList,
    // SettingItem
  },
  mixins: [mixin, mixinDevice],
  data () {
    return {
      visible: false,
      colorList
    }
  },
  watch: {
  },
  mounted () {
    updateTheme(this.primaryColor)
    if (this.colorWeak !== config.colorWeak) {
      updateColorWeak(this.colorWeak)
    }
  },
  methods: {
    showDrawer () {
      this.visible = true
    },
    onClose () {
      this.visible = false
    },
    toggle () {
      this.visible = !this.visible
    },
    onColorWeak (checked) {
      this.$store.dispatch('ToggleWeak', checked)
      updateColorWeak(checked)
    },
    onMultiTab (checked) {
      this.$store.dispatch('ToggleMultiTab', checked)
    },
    handleMenuTheme (theme) {
      this.$store.dispatch('ToggleTheme', theme)
    },
    doCopy () {
      // get current settings from mixin or this.$store.state.app, pay attention to the property name
      const text = `export default {
  primaryColor: '${this.primaryColor}', // primary color of ant design
  navTheme: '${this.navTheme}', // theme for nav menu
  layout: '${this.layoutMode}', // nav menu position: sidemenu or topmenu
  contentWidth: '${this.contentWidth}', // layout of content: Fluid or Fixed, only works when layout is topmenu
  fixedHeader: ${this.fixedHeader}, // sticky header
  fixSiderbar: ${this.fixSiderbar}, // sticky siderbar
  autoHideHeader: ${this.autoHideHeader}, //  auto hide header
  colorWeak: ${this.colorWeak},
  multiTab: ${this.multiTab},
  production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true',
  // vue-ls options
  storageOptions: {
    namespace: 'pro__',
    name: 'ls',
    storage: 'local',
  }
}`
      this.$copyText(text).then(message => {
        console.log('copy', message)
        this.$message.success('复制完毕')
      }).catch(err => {
        console.log('copy.err', err)
        this.$message.error('复制失败')
      })
    },
    handleLayout (mode) {
      this.$store.dispatch('ToggleLayoutMode', mode)
      // 因为顶部菜单不能固定左侧菜单栏,所以强制关闭
      this.handleFixSiderbar(false)
    },
    handleContentWidthChange (type) {
      this.$store.dispatch('ToggleContentWidth', type)
    },
    changeColor (color) {
      if (this.primaryColor !== color) {
        this.$store.dispatch('ToggleColor', color)
        updateTheme(color)
      }
    },
    handleFixedHeader (fixed) {
      this.$store.dispatch('ToggleFixedHeader', fixed)
    },
    handleFixedHeaderHidden (autoHidden) {
      this.$store.dispatch('ToggleFixedHeaderHidden', autoHidden)
    },
    handleFixSiderbar (fixed) {
      if (this.layoutMode === 'topmenu') {
        this.$store.dispatch('ToggleFixSiderbar', false)
        return
      }
      this.$store.dispatch('ToggleFixSiderbar', fixed)
    }
  }
}
</script>
<style lang="less" scoped>
  .setting-drawer-index-content {
    .setting-drawer-index-blockChecbox {
      display: flex;
      .setting-drawer-index-item {
        margin-right: 16px;
        position: relative;
        border-radius: 4px;
        cursor: pointer;
        img {
          width: 48px;
        }
        .setting-drawer-index-selectIcon {
          position: absolute;
          top: 0;
          right: 0;
          width: 100%;
          padding-top: 15px;
          padding-left: 24px;
          height: 100%;
          color: #1890ff;
          font-size: 14px;
          font-weight: 700;
        }
      }
    }
    .setting-drawer-theme-color-colorBlock {
      width: 20px;
      height: 20px;
      border-radius: 2px;
      float: left;
      cursor: pointer;
      margin-right: 8px;
      padding-left: 0px;
      padding-right: 0px;
      text-align: center;
      color: #fff;
      font-weight: 700;
      i {
        font-size: 14px;
      }
    }
  }
  .setting-drawer-index-handle {
    position: absolute;
    top: 240px;
    background: #1890ff;
    width: 48px;
    height: 48px;
    right: 300px;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    pointer-events: auto;
    z-index: 1001;
    text-align: center;
    font-size: 16px;
    border-radius: 4px 0 0 4px;
    i {
      color: rgb(255, 255, 255);
      font-size: 20px;
    }
  }
</style>
_web/src/components/SettingDrawer/SettingItem.vue
New file
@@ -0,0 +1,38 @@
<template>
  <div class="setting-drawer-index-item">
    <h3 class="setting-drawer-index-title">{{ title }}</h3>
    <slot></slot>
    <a-divider v-if="divider"/>
  </div>
</template>
<script>
export default {
  name: 'SettingItem',
  props: {
    title: {
      type: String,
      default: ''
    },
    divider: {
      type: Boolean,
      default: false
    }
  }
}
</script>
<style lang="less" scoped>
  .setting-drawer-index-item {
    margin-bottom: 24px;
    .setting-drawer-index-title {
      font-size: 14px;
      color: rgba(0, 0, 0, .85);
      line-height: 22px;
      margin-bottom: 12px;
    }
  }
</style>
_web/src/components/SettingDrawer/index.js
New file
@@ -0,0 +1,2 @@
import SettingDrawer from './SettingDrawer'
export default SettingDrawer
_web/src/components/SettingDrawer/settingConfig.js
New file
@@ -0,0 +1,46 @@
import { message } from 'ant-design-vue/es'
import themeColor from './themeColor.js'
const colorList = [
  {
    key: '薄暮', color: '#F5222D'
  },
  {
    key: '火山', color: '#FA541C'
  },
  {
    key: '日暮', color: '#FAAD14'
  },
  {
    key: '明青', color: '#13C2C2'
  },
  {
    key: '极光绿', color: '#52C41A'
  },
  {
    key: '拂晓蓝(默认)', color: '#1890FF'
  },
  {
    key: '极客蓝', color: '#2F54EB'
  },
  {
    key: '酱紫', color: '#722ED1'
  }
]
const updateTheme = newPrimaryColor => {
  const hideMessage = message.loading('正在切换主题!', 0)
  themeColor.changeColor(newPrimaryColor).finally(t => {
    setTimeout(() => {
      hideMessage()
    }, 10)
  })
}
const updateColorWeak = colorWeak => {
  // document.body.className = colorWeak ? 'colorWeak' : '';
  const app = document.body.querySelector('#app')
  colorWeak ? app.classList.add('colorWeak') : app.classList.remove('colorWeak')
}
export { updateTheme, colorList, updateColorWeak }
_web/src/components/SettingDrawer/themeColor.js
New file
@@ -0,0 +1,24 @@
import client from 'webpack-theme-color-replacer/client'
import generate from '@ant-design/colors/lib/generate'
export default {
  getAntdSerials (color) {
    // 淡化(即less的tint)
    const lightens = new Array(9).fill().map((t, i) => {
      return client.varyColor.lighten(color, i / 10)
    })
    // colorPalette变换得到颜色值
    const colorPalettes = generate(color)
    const rgb = client.varyColor.toNum3(color.replace('#', '')).join(',')
    return lightens.concat(colorPalettes).concat(rgb)
  },
  changeColor (newColor) {
    var options = {
      newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors`
      changeUrl (cssUrl) {
        return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path
      }
    }
    return client.changer.changeColor(options, Promise)
  }
}
_web/src/components/StandardFormRow/StandardFormRow.vue
New file
@@ -0,0 +1,122 @@
<template>
  <div :class="[prefixCls, lastCls, blockCls, gridCls]">
    <div v-if="title" class="antd-pro-components-standard-form-row-index-label">
      <span>{{ title }}</span>
    </div>
    <div class="antd-pro-components-standard-form-row-index-content">
      <slot></slot>
    </div>
  </div>
</template>
<script>
const classes = [
  'antd-pro-components-standard-form-row-index-standardFormRowBlock',
  'antd-pro-components-standard-form-row-index-standardFormRowGrid',
  'antd-pro-components-standard-form-row-index-standardFormRowLast'
]
export default {
  name: 'StandardFormRow',
  props: {
    prefixCls: {
      type: String,
      default: 'antd-pro-components-standard-form-row-index-standardFormRow'
    },
    title: {
      type: String,
      default: undefined
    },
    last: {
      type: Boolean
    },
    block: {
      type: Boolean
    },
    grid: {
      type: Boolean
    }
  },
  computed: {
    lastCls () {
      return this.last ? classes[2] : null
    },
    blockCls () {
      return this.block ? classes[0] : null
    },
    gridCls () {
      return this.grid ? classes[1] : null
    }
  }
}
</script>
<style lang="less" scoped>
@import '../index.less';
.antd-pro-components-standard-form-row-index-standardFormRow {
  display: flex;
  margin-bottom: 16px;
  padding-bottom: 16px;
  border-bottom: 1px dashed @border-color-split;
  /deep/ .ant-form-item {
    margin-right: 24px;
  }
  /deep/ .ant-form-item-label label {
    margin-right: 0;
    color: @text-color;
  }
  /deep/ .ant-form-item-label,
  .ant-form-item-control {
    padding: 0;
    line-height: 32px;
  }
  .antd-pro-components-standard-form-row-index-label {
    flex: 0 0 auto;
    margin-right: 24px;
    color: @heading-color;
    font-size: @font-size-base;
    text-align: right;
    & > span {
      display: inline-block;
      height: 32px;
      line-height: 32px;
      &::after {
        content: ':';
      }
    }
  }
  .antd-pro-components-standard-form-row-index-content {
    flex: 1 1 0;
    /deep/ .ant-form-item:last-child {
      margin-right: 0;
    }
  }
  &.antd-pro-components-standard-form-row-index-standardFormRowLast {
    margin-bottom: 0;
    padding-bottom: 0;
    border: none;
  }
  &.antd-pro-components-standard-form-row-index-standardFormRowBlock {
    /deep/ .ant-form-item,
    div.ant-form-item-control-wrapper {
      display: block;
    }
  }
  &.antd-pro-components-standard-form-row-index-standardFormRowGrid {
      /deep/ .ant-form-item,
      div.ant-form-item-control-wrapper {
        display: block;
      }
      /deep/ .ant-form-item-label {
        float: left;
      }
  }
}
</style>
_web/src/components/StandardFormRow/index.js
New file
@@ -0,0 +1,3 @@
import StandardFormRow from './StandardFormRow'
export default StandardFormRow
_web/src/components/Table/README.md
New file
@@ -0,0 +1,340 @@
Table 重封装组件说明
====
封装说明
----
>  基础的使用方式与 API 与 [官方版(Table)](https://vuecomponent.github.io/ant-design-vue/components/table-cn/) 本一致,在其基础上,封装了加载数据的方法。
>
> 你无需在你是用表格的页面进行分页逻辑处理,仅需向 Table 组件传递绑定 `:data="Promise"` 对象即可
该 `table` 由 [@Saraka](https://github.com/saraka-tsukai) 完成封装
由 `小诺` 完成二次改进使用
例子1
----
(基础使用)
```vue
<template>
  <s-table
    ref="table"
    :rowKey="(record) => record.data.id"
    :columns="columns"
    :data="loadData"
    :rowSelection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
  >
  </s-table>
</template>
<script>
  import STable from '@/components'
  export default {
    components: {
      STable
    },
    data() {
      return {
        columns: [
          {
            title: '规则编号',
            dataIndex: 'no'
          },
          {
            title: '描述',
            dataIndex: 'description'
          },
          {
            title: '服务调用次数',
            dataIndex: 'callNo',
            sorter: true,
            needTotal: true,
            customRender: (text) => text + ' 次'
          },
          {
            title: '状态',
            dataIndex: 'status',
            needTotal: true
          },
          {
            title: '更新时间',
            dataIndex: 'updatedAt',
            sorter: true
          }
        ],
        // 查询条件参数
        queryParam: {},
        // 加载数据方法 必须为 Promise 对象
        loadData: parameter => {
          return this.$http.get('/service', {
            params: Object.assign(parameter, this.queryParam)
          }).then(res => {
            return res.result
          })
        },
        selectedRowKeys: [],
        selectedRows: []
      }
    },
    methods: {
      onSelectChange (selectedRowKeys, selectedRows) {
         this.selectedRowKeys = selectedRowKeys
         this.selectedRows = selectedRows
       }
    }
  }
</script>
```
例子2
----
(简单的表格,最后一列是各种操作)
```vue
<template>
  <s-table
    ref="table"
    :columns="columns"
    :data="loadData"
  >
    <span slot="action" slot-scope="text, record">
      <a>编辑</a>
      <a-divider type="vertical"/>
      <a-dropdown>
        <a class="ant-dropdown-link">
          更多 <a-icon type="down"/>
        </a>
        <a-menu slot="overlay">
          <a-menu-item>
            <a href="javascript:;">1st menu item</a>
          </a-menu-item>
          <a-menu-item>
            <a href="javascript:;">2nd menu item</a>
          </a-menu-item>
          <a-menu-item>
            <a href="javascript:;">3rd menu item</a>
          </a-menu-item>
        </a-menu>
      </a-dropdown>
    </span>
  </s-table>
</template>
<script>
  import STable from '@/components/table/'
  export default {
    components: {
      STable
    },
    data() {
      return {
        columns: [
          {
            title: '规则编号',
            dataIndex: 'no'
          },
          {
            title: '描述',
            dataIndex: 'description'
          },
          {
            title: '服务调用次数',
            dataIndex: 'callNo',
          },
          {
            title: '状态',
            dataIndex: 'status',
          },
          {
            title: '更新时间',
            dataIndex: 'updatedAt',
          },
          {
            table: '操作',
            dataIndex: 'action',
            scopedSlots: {customRender: 'action'},
          }
        ],
        // 查询条件参数
        queryParam: {},
        // 加载数据方法 必须为 Promise 对象
        loadData: parameter => {
          return this.$http.get('/service', {
            params: Object.assign(parameter, this.queryParam)
          }).then(res => {
            return res.result
          })
        },
      }
    },
    methods: {
      edit(row) {
        // axios 发送数据到后端 修改数据成功后
        // 调用 refresh() 重新加载列表数据
        // 这里 setTimeout 模拟发起请求的网络延迟..
        setTimeout(() => {
          this.$refs.table.refresh() // refresh() 不传参默认值 false 不刷新到分页第一页
        }, 1500)
      }
    }
  }
</script>
```
内置方法
----
通过 `this.$refs.table` 调用
`this.$refs.table.refresh(true)` 刷新列表 (用户新增/修改数据后,重载列表数据)
> 注意:要调用 `refresh(bool)` 需要给表格组件设定 `ref` 值
>
> `refresh()` 方法可以传一个 `bool` 值,当有传值 或值为 `true` 时,则刷新时会强制刷新到第一页(常用户页面 搜索 按钮进行搜索时,结果从第一页开始分页)
内置属性
----
> 除去 `a-table` 自带属性外,还而外提供了一些额外属性属性
| 属性           | 说明                                            | 类型              | 默认值 |
| -------------- | ----------------------------------------------- | ----------------- | ------ |
| alert          | 设置是否显示表格信息栏                          | [object, boolean] | null   |
| showPagination | 显示分页选择器,可传 'auto' \| boolean          | [string, boolean] | 'auto' |
| data           | 加载数据方法 必须为 `Promise` 对象 **必须绑定** | Promise           | -      |
`alert` 属性对象:
```javascript
alert: {
  show: Boolean,
  clear: [Function, Boolean]
}
```
注意事项
----
> 你可能需要为了与后端提供的接口返回结果一致而去修改以下代码:
> (需要注意的是,这里的修改是全局性的,意味着整个项目所有使用该 table 组件都需要遵守这个返回结果定义的字段。)
>
> 文档中的结构有可能由于组件 bug 进行修正而改动。实际修改请以当时最新版本为准
修改 `@/components/table/index.js`  第 156 行起
```javascript
result.then(r => {
          this.localPagination = this.showPagination && Object.assign({}, this.localPagination, {
            current: r.pageNo, // 返回结果中的当前分页数
            total: r.totalCount, // 返回结果中的总记录数
            showSizeChanger: this.showSizeChanger,
            pageSize: (pagination && pagination.pageSize) ||
              this.localPagination.pageSize
          }) || false
          // 为防止删除数据后导致页面当前页面数据长度为 0 ,自动翻页到上一页
          if (r.data.length === 0 && this.showPagination && this.localPagination.current > 1) {
            this.localPagination.current--
            this.loadData()
            return
          }
          // 这里用于判断接口是否有返回 r.totalCount 且 this.showPagination = true 且 pageNo 和 pageSize 存在 且 totalCount 小于等于 pageNo * pageSize 的大小
          // 当情况满足时,表示数据不满足分页大小,关闭 table 分页功能
          try {
            if ((['auto', true].includes(this.showPagination) && r.totalCount <= (r.pageNo * this.localPagination.pageSize))) {
              this.localPagination.hideOnSinglePage = true
            }
          } catch (e) {
            this.localPagination = false
          }
          console.log('loadData -> this.localPagination', this.localPagination)
          this.localDataSource = r.data // 返回结果中的数组数据
          this.localLoading = false
        })
```
返回 JSON 例子:
```json
{
  "message": "",
  "result": {
    "data": [{
        id: 1,
        cover: 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png',
        title: 'Alipay',
        description: '那是一种内在的东西, 他们到达不了,也无法触及的',
        status: 1,
        updatedAt: '2018-07-26 00:00:00'
      },
      {
        id: 2,
        cover: 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png',
        title: 'Angular',
        description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
        status: 1,
        updatedAt: '2018-07-26 00:00:00'
      },
      {
        id: 3,
        cover: 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png',
        title: 'Ant Design',
        description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
        status: 1,
        updatedAt: '2018-07-26 00:00:00'
      },
      {
        id: 4,
        cover: 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png',
        title: 'Snowy',
        description: '那时候我只会想自己想要什么,从不想自己拥有什么',
        status: 1,
        updatedAt: '2018-07-26 00:00:00'
      },
      {
        id: 5,
        cover: 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png',
        title: 'Bootstrap',
        description: '凛冬将至',
        status: 1,
        updatedAt: '2018-07-26 00:00:00'
      },
      {
        id: 6,
        cover: 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png',
        title: 'Vue',
        description: '生命就像一盒巧克力,结果往往出人意料',
        status: 1,
        updatedAt: '2018-07-26 00:00:00'
      }
    ],
    "pageSize": 10,
    "pageNo": 0,
    "totalPage": 6,
    "totalCount": 57
  },
  "status": 200,
  "timestamp": 1534955098193
}
```
更新时间
----
该文档最后更新于: 2019-06-23 PM 17:19
_web/src/components/Table/columnSetting.vue
New file
@@ -0,0 +1,82 @@
<template>
  <div slot="overlay" class="ant-dropdown-menu s-tool-column ant-dropdown-content">
    <div class="s-tool-column-header s-tool-column-item">
      <a-checkbox :indeterminate="indeterminate" :checked="checkAll" @change="onCheckAllChange">
        列展示
      </a-checkbox>
      <a @click="reset">重置</a>
    </div>
    <a-divider />
    <div class="ant-checkbox-group">
      <div>
        <draggable v-model="columnsSetting" animation="300" @end="emitColumnChange">
          <div class="s-tool-column-item" v-for="item in columnsSetting" :key="item.title">
            <div class="s-tool-column-handle" >
              <a-icon type="more"/>
              <a-icon type="more"/>
            </div>
            <a-checkbox v-model="item.checked" @change="onChange">{{ item.title }}</a-checkbox>
          </div>
        </draggable>
      </div>
    </div>
  </div>
</template>
<script>
  import draggable from 'vuedraggable'
  export default {
    props: {
      columns: {
        type: Array,
        default: () => ([])
      }
    },
    components: {
      draggable
    },
    data() {
      return {
        indeterminate: false,
        checkAll: true,
        columnsSetting: [],
        originColumns: []
      }
    },
    methods: {
      reset() {
        this.columnsSetting = JSON.parse(JSON.stringify(this.originColumns))
        this.indeterminate = false
        this.checkAll = true
        this.emitColumnChange()
      },
      onChange() {
        const checkedList = this.columnsSetting.filter(value => value.checked)
        this.indeterminate = !!checkedList.length && checkedList.length < this.columnsSetting.length
        this.checkAll = checkedList.length === this.columnsSetting.length
        this.emitColumnChange()
      },
      onCheckAllChange(e) {
        const val = e.target.checked
        Object.assign(this, {
          indeterminate: false,
          checkAll: val,
          columnsSetting: this.columns.map(value => ({ ...value, checked: val }))
        })
        this.emitColumnChange()
      },
      emitColumnChange() {
        this.$emit('columnChange', this.columnsSetting)
      }
    },
    mounted() {
      this.columnsSetting = this.columns.map(value => ({ ...value, checked: true }))
      this.originColumns = JSON.parse(JSON.stringify(this.columnsSetting))
    }
  }
</script>
<style lang="less" scoped>
</style>
_web/src/components/Table/index.js
New file
@@ -0,0 +1,449 @@
import T from 'ant-design-vue/es/table/Table'
import get from 'lodash.get'
import draggable from 'vuedraggable'
import columnSetting from './columnSetting'
import './index.less'
export default {
  components: {
    draggable, columnSetting
  },
  data () {
    return {
      needTotalList: [],
      selectedRows: [],
      selectedRowKeys: [],
      localLoading: false,
      localDataSource: [],
      localPagination: Object.assign({}, this.pagination),
      isFullscreen: false,
      customSize: this.size,
      columnsSetting: []
    }
  },
  props: Object.assign({}, T.props, {
    rowKey: {
      type: [String, Function],
      default: 'key'
    },
    data: {
      type: Function,
      required: true
    },
    pageNum: {
      type: Number,
      default: 1
    },
    pageSize: {
      type: Number,
      default: 10
    },
    showSizeChanger: {
      type: Boolean,
      default: true
    },
    size: {
      type: String,
      default: 'middle'
    },
    /**
     * alert: {
     *   show: true,
     *   clear: Function
     * }
     */
    alert: {
      type: [Object, Boolean],
      default: null
    },
    rowSelection: {
      type: Object,
      default: null
    },
    /** @Deprecated */
    showAlertInfo: {
      type: Boolean,
      default: false
    },
    showPagination: {
      type: String | Boolean,
      default: 'auto'
    },
    /**
     * enable page URI mode
     *
     * e.g:
     * /users/1
     * /users/2
     * /users/3?queryParam=test
     * ...
     */
    pageURI: {
      type: Boolean,
      default: false
    },
    extraTool: {
      type: Array,
      default: () => ([])
    }
  }),
  watch: {
    'localPagination.current' (val) {
      this.pageURI && this.$router.push({
        ...this.$route,
        name: this.$route.name,
        params: Object.assign({}, this.$route.params, {
          pageNo: val
        })
      })
    },
    pageNum (val) {
      Object.assign(this.localPagination, {
        current: val
      })
    },
    pageSize (val) {
      Object.assign(this.localPagination, {
        pageSize: val
      })
    },
    showSizeChanger (val) {
      Object.assign(this.localPagination, {
        showSizeChanger: val
      })
    },
    columns(v) {
      this.columnsSetting = v
    }
  },
  created () {
    const { pageNo } = this.$route.params
    const localPageNum = this.pageURI && (pageNo && parseInt(pageNo)) || this.pageNum
    this.localPagination = ['auto', true].includes(this.showPagination) && Object.assign({}, this.localPagination, {
      current: localPageNum,
      pageSize: this.pageSize,
      showSizeChanger: this.showSizeChanger,
      showTotal: (total, range) => {
        return range[0] + '-' + range[1] + '共' + total + '条'
      }
    }) || false
    this.needTotalList = this.initTotalList(this.columns)
    this.loadData()
    this.columnsSetting = this.columns
  },
  methods: {
    /**
     * 表格重新加载方法
     * 如果参数为 true, 则强制刷新到第一页
     * @param Boolean bool
     */
    refresh (bool = false) {
      bool && (this.localPagination = Object.assign({}, {
        current: 1, pageSize: this.pageSize
      }))
      this.loadData()
    },
    /**
     * 加载数据方法
     * @param {Object} pagination 分页选项器
     * @param {Object} filters 过滤条件
     * @param {Object} sorter 排序条件
     */
    loadData (pagination, filters, sorter) {
      this.localLoading = true
      const parameter = Object.assign({
          pageNo: (pagination && pagination.current) ||
            this.showPagination && this.localPagination.current || this.pageNum,
          pageSize: (pagination && pagination.pageSize) ||
            this.showPagination && this.localPagination.pageSize || this.pageSize
        },
        (sorter && sorter.field && {
          sortField: sorter.field
        }) || {},
        (sorter && sorter.order && {
          sortOrder: sorter.order
        }) || {}, {
          ...filters
        }
      )
      const result = this.data(parameter)
      // 对接自己的通用数据接口需要修改下方代码中的 r.pageNo, r.totalCount, r.data
      // eslint-disable-next-line
      if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
        result.then(r => {
          if (r == null) {
            this.localLoading = false
            return
          }
          this.localPagination = this.showPagination && Object.assign({}, this.localPagination, {
            current: r.pageNo, // pageNo, // 返回结果中的当前分页数
            total: r.totalRows, // totalCount, // 返回结果中的总记录数
            showSizeChanger: this.showSizeChanger,
            pageSize: (pagination && pagination.pageSize) ||
              this.localPagination.pageSize
          }) || false
          // 后端数据rows为null保存修复
          if (r.rows == null) {
            r.rows = []
          }
          // 为防止删除数据后导致页面当前页面数据长度为 0 ,自动翻页到上一页
          if (r.rows.length === 0 && this.showPagination && this.localPagination.current > 1) {
            this.localPagination.current--
            this.loadData()
            return
          }
          // 这里用于判断接口是否有返回 r.totalCount 且 this.showPagination = true 且 pageNo 和 pageSize 存在 且 totalCount 小于等于 pageNo * pageSize 的大小
          // 当情况满足时,表示数据不满足分页大小,关闭 table 分页功能
          try {
            if ((['auto', true].includes(this.showPagination) && r.totalCount <= (r.totalPage * this.localPagination.pageSize))) {
              this.localPagination.hideOnSinglePage = true
            }
          } catch (e) {
            this.localPagination = false
          }
          // 返回结果中的数组数据
          if (this.showPagination === false) {
            // 因为按住小诺的套路,不分页的直接是在data中,我们在界面中直接就是返回了data
            this.localDataSource = r
          } else {
            this.localDataSource = r.rows
          }
          this.localLoading = false
        })
      }
    },
    initTotalList (columns) {
      const totalList = []
      columns && columns instanceof Array && columns.forEach(column => {
        if (column.needTotal) {
          totalList.push({
            ...column,
            total: 0
          })
        }
      })
      return totalList
    },
    /**
     * 用于更新已选中的列表数据 total 统计
     * @param selectedRowKeys
     * @param selectedRows
     */
    updateSelect (selectedRowKeys, selectedRows) {
      this.selectedRows = selectedRows
      this.selectedRowKeys = selectedRowKeys
      const list = this.needTotalList
      this.needTotalList = list.map(item => {
        return {
          ...item,
          total: selectedRows.reduce((sum, val) => {
            const total = sum + parseInt(get(val, item.dataIndex))
            return isNaN(total) ? 0 : total
          }, 0)
        }
      })
    },
    /**
     * 清空 table 已选中项
     */
    clearSelected () {
      if (this.rowSelection) {
        this.rowSelection.onChange([], [])
        this.updateSelect([], [])
      }
    },
    /**
     * 刷新并清空已选
     */
    clearRefreshSelected (bool = false) {
      this.refresh(bool)
      this.clearSelected()
    },
    /**
     * 处理交给 table 使用者去处理 clear 事件时,内部选中统计同时调用
     * @param callback
     * @returns {*}
     */
    renderClear (callback) {
      if (this.selectedRowKeys.length <= 0) return null
      return (
        <a style="margin-left: 24px" onClick={() => {
        callback()
        this.clearSelected()
      }}>清空</a>
    )
    },
    renderAlert () {
      // 绘制统计列数据
      // eslint-disable-next-line no-unused-vars
      const needTotalItems = this.needTotalList.map((item) => {
        return (<span style="margin-right: 12px">
          {item.title}总计 <a style="font-weight: 600">{!item.customRender ? item.total : item.customRender(item.total)}</a>
        </span>)
      })
      // 绘制 清空 按钮
      // eslint-disable-next-line no-unused-vars
      const clearItem = (typeof this.alert.clear === 'boolean' && this.alert.clear) ? (
        this.renderClear(this.clearSelected)
      ) : (this.alert !== null && typeof this.alert.clear === 'function') ? (
        this.renderClear(this.alert.clear)
      ) : null
      // 绘制 alert 组件
      // 统一先去除alert组件
      return (
        <a-alert showIcon={true} style="margin-bottom: 16px">
        <template slot="message">
        <span style="margin-right: 12px">已选择: <a style="font-weight: 600">{this.selectedRows.length}</a></span>
      {needTotalItems}
      {clearItem}
    </template>
      </a-alert>
    )
    },
    columnChange(val) {
      this.columnsSetting = val
    },
    renderHeader () {
      let tools = [
        {
          icon: 'reload',
          title: '刷新',
          onClick: () => {
            this.refresh()
          }
        },
        {
          icon: 'column-height',
          title: '密度',
          isDropdown: true,
          menu: () => {
            const onClick = ({ key }) => {
              this.customSize = key
            }
            return (
              <a-menu slot="overlay" onClick={onClick} selectable defaultSelectedKeys={[this.customSize]}>
              <a-menu-item key="default">默认</a-menu-item>
              <a-menu-item key="middle">中等</a-menu-item>
              <a-menu-item key="small">紧凑</a-menu-item>
              </a-menu>
          )
          },
          onClick: () => {
          }
        },
        {
          icon: 'setting',
          title: '列设置',
          isDropdown: true,
          menu: () => {
            return <columnSetting slot="overlay" columns={this.columns} onColumnChange={this.columnChange} />
          },
          onClick: () => {
          }
        }
      ]
      if (this.extraTool.length) {
        tools = tools.concat(this.extraTool)
      }
      return (
        <div class="s-table-tool">
        <div class="s-table-tool-left">
        {this.$scopedSlots.operator && this.$scopedSlots.operator()}
        </div>
        <div class="s-table-tool-right">
        {
          tools.map(tool => {
            if (tool.isDropdown) {
              return (
                <a-dropdown trigger={['click']}>
                <a-tooltip title={tool.title} class="s-tool-item" onClick={tool.onClick}>
                <a-icon type={tool.icon}/>
              </a-tooltip>
              { tool.menu() }
            </a-dropdown>
            )
            }
            return (
              <a-tooltip title={tool.title} class="s-tool-item" onClick={tool.onClick}>
              <a-icon type={tool.icon} />
            </a-tooltip>
          )
          })
        }
        </div>
        </div>
    )
      /*
      return (
        <a-alert showIcon={true} style="margin-bottom: 16px">
          <template slot="message">
            <span style="margin-right: 12px">已选择: <a style="font-weight: 600">{this.selectedRows.length}</a></span>
            {needTotalItems}
            {clearItem}
          </template>
        </a-alert>
      )
      */
    }
  },
  render () {
    let props = {}
    const localKeys = Object.keys(this.$data)
    const showAlert = (typeof this.alert === 'object' && this.alert !== null && this.alert.show) && typeof this.rowSelection.selectedRowKeys !== 'undefined' || this.alert
    Object.keys(T.props).forEach(k => {
      const localKey = `local${k.substring(0, 1).toUpperCase()}${k.substring(1)}`
      if (localKeys.includes(localKey)) {
        props[k] = this[localKey]
        return props[k]
      }
      if (k === 'rowSelection') {
        if (showAlert && this.rowSelection) {
          // 如果需要使用alert,则重新绑定 rowSelection 事件
          props[k] = {
            ...this.rowSelection,
            selectedRows: this.selectedRows,
            selectedRowKeys: this.selectedRowKeys,
            onChange: (selectedRowKeys, selectedRows) => {
              this.updateSelect(selectedRowKeys, selectedRows)
              typeof this[k].onChange !== 'undefined' && this[k].onChange(selectedRowKeys, selectedRows)
            }
          }
          return props[k]
        } else if (!this.rowSelection) {
          // 如果没打算开启 rowSelection 则清空默认的选择项
          props[k] = null
          return props[k]
        }
      }
      this[k] && (props[k] = this[k])
      // 此处配置表格大小与要显示的列
      props = {
        ...props,
        size: this.customSize,
        columns: this.columnsSetting.filter(value => value.checked === undefined || value.checked)
      }
      return props[k]
    })
    const table = (
      <a-table {...{ props, scopedSlots: { ...this.$scopedSlots } }} onChange={this.loadData} onExpand={ (expanded, record) => { this.$emit('expand', expanded, record) } }>
    { Object.keys(this.$slots).map(name => (<template slot={name}>{this.$slots[name]}</template>)) }
      </a-table>
    )
      return (
        <div class="table-wrapper">
      { this.renderHeader() }
      { showAlert ? this.renderAlert() : null }
      { table }
    </div>
    )
    }
  }
_web/src/components/Table/index.less
New file
@@ -0,0 +1,54 @@
.table-wrapper{
  background: #fff;
}
.s-table-tool{
  display: flex;
  margin-bottom: 16px;
  .s-table-tool-left{
    flex: 1;
  }
  .s-table-tool-right{
    display: inline-flex;
    align-items: center;
    .s-tool-item{
      font-size: 16px;
      margin-left: 16px;
      cursor: pointer;
    }
  }
}
.s-tool-column-item{
  display: flex;
  align-items: center;
  padding: 4px 16px 4px 4px;
  .ant-checkbox-wrapper{
    flex: 1;
  }
  .s-tool-column-handle{
    opacity: .8;
    cursor: move;
    .anticon-more{
      font-size: 12px;
      margin-top: 2px;
      & + .anticon-more{
        margin: 2px 4px 0 -8px;
      }
    }
  }
}
.s-tool-column-header{
  padding: 5px 16px 10px 24px;
  min-width: 180px;
}
.s-tool-column{
  .ant-divider{
    margin: 0;
  }
  .ant-checkbox-group{
    padding: 4px 0;
    display: block;
  }
}
_web/src/components/TagSelect/TagSelectOption.jsx
New file
@@ -0,0 +1,45 @@
import { Tag } from 'ant-design-vue'
const { CheckableTag } = Tag
export default {
  name: 'TagSelectOption',
  props: {
    prefixCls: {
      type: String,
      default: 'ant-pro-tag-select-option'
    },
    value: {
      type: [String, Number, Object],
      default: ''
    },
    checked: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      localChecked: this.checked || false
    }
  },
  watch: {
    'checked' (val) {
      this.localChecked = val
    },
    '$parent.items': {
      handler: function (val) {
        this.value && val.hasOwnProperty(this.value) && (this.localChecked = val[this.value])
      },
      deep: true
    }
  },
  render () {
    const { $slots, value } = this
    const onChange = (checked) => {
      this.$emit('change', { value, checked })
    }
    return (<CheckableTag key={value} vModel={this.localChecked} onChange={onChange}>
      {$slots.default}
    </CheckableTag>)
  }
}
_web/src/components/TagSelect/index.jsx
New file
@@ -0,0 +1,113 @@
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import Option from './TagSelectOption.jsx'
import { filterEmpty } from '@/components/_util/util'
export default {
  Option,
  name: 'TagSelect',
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    prefixCls: {
      type: String,
      default: 'ant-pro-tag-select'
    },
    defaultValue: {
      type: PropTypes.array,
      default: null
    },
    value: {
      type: PropTypes.array,
      default: null
    },
    expandable: {
      type: Boolean,
      default: false
    },
    hideCheckAll: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      expand: false,
      localCheckAll: false,
      items: this.getItemsKey(filterEmpty(this.$slots.default)),
      val: this.value || this.defaultValue || []
    }
  },
  methods: {
    onChange (checked) {
      const key = Object.keys(this.items).filter(key => key === checked.value)
      this.items[key] = checked.checked
      const bool = Object.values(this.items).lastIndexOf(false)
      if (bool === -1) {
        this.localCheckAll = true
      } else {
        this.localCheckAll = false
      }
    },
    onCheckAll (checked) {
      Object.keys(this.items).forEach(v => {
        this.items[v] = checked.checked
      })
      this.localCheckAll = checked.checked
    },
    getItemsKey (items) {
      const totalItem = {}
      items.forEach(item => {
        totalItem[item.componentOptions.propsData && item.componentOptions.propsData.value] = false
      })
      return totalItem
    },
    // CheckAll Button
    renderCheckAll () {
      const props = {
        on: {
          change: (checked) => {
            this.onCheckAll(checked)
            checked.value = 'total'
            this.$emit('change', checked)
          }
        }
      }
      const checkAllElement = <Option key={'total'} checked={this.localCheckAll} {...props}>All</Option>
      return !this.hideCheckAll && checkAllElement || null
    },
    // expandable
    renderExpandable () {
    },
    // render option
    renderTags (items) {
      const listeners = {
        change: (checked) => {
          this.onChange(checked)
          this.$emit('change', checked)
        }
      }
      return items.map(vnode => {
        const options = vnode.componentOptions
        options.listeners = listeners
        return vnode
      })
    }
  },
  render () {
    const { $props: { prefixCls } } = this
    const classString = {
      [`${prefixCls}`]: true
    }
    const tagItems = filterEmpty(this.$slots.default)
    return (
      <div class={classString}>
        {this.renderCheckAll()}
        {this.renderTags(tagItems)}
      </div>
    )
  }
}
_web/src/components/TextArea/index.jsx
New file
@@ -0,0 +1,69 @@
import './style.less'
import { getStrFullLength, cutStrByFullLength } from '../_util/util'
import Input from 'ant-design-vue/es/input'
const TextArea = Input.TextArea
export default {
  name: 'LimitTextArea',
  model: {
    prop: 'value',
    event: 'change'
  },
  props: Object.assign({}, TextArea.props, {
    prefixCls: {
      type: String,
      default: 'ant-textarea-limit'
    },
    // eslint-disable-next-line
    value: {
      type: String
    },
    limit: {
      type: Number,
      default: 200
    }
  }),
  data () {
    return {
      currentLimit: 0
    }
  },
  watch: {
    value (val) {
      this.calcLimitNum(val)
    }
  },
  created () {
    this.calcLimitNum(this.value)
  },
  methods: {
    handleChange (e) {
      const value = e.target.value
      const len = getStrFullLength(value)
      if (len <= this.limit) {
        this.currentLimit = len
        this.$emit('change', value)
        return
      } else {
        const str = cutStrByFullLength(value, this.limit)
        this.currentLimit = getStrFullLength(str)
        this.$emit('change', str)
      }
      console.error('limit out! currentLimit:', this.currentLimit)
    },
    calcLimitNum (val) {
      const len = getStrFullLength(val)
      this.currentLimit = len
    }
  },
  render () {
    const { prefixCls, ...props } = this.$props
    return (
      <div class={this.prefixCls}>
        <TextArea {...{ props }} value={this.value} onChange={this.handleChange}>
        </TextArea>
        <span class="limit">{this.currentLimit}/{this.limit}</span>
      </div>
    )
  }
}
_web/src/components/TextArea/style.less
New file
@@ -0,0 +1,12 @@
.ant-textarea-limit {
  position: relative;
  .limit {
    position: absolute;
    color: #909399;
    background: #fff;
    font-size: 12px;
    bottom: 5px;
    right: 10px;
  }
}
_web/src/components/Tree/Tree.jsx
New file
@@ -0,0 +1,124 @@
import { Menu, Icon, Input } from 'ant-design-vue'
const { Item, ItemGroup, SubMenu } = Menu
const { Search } = Input
export default {
  name: 'Tree',
  props: {
    dataSource: {
      type: Array,
      required: true
    },
    openKeys: {
      type: Array,
      default: () => []
    },
    search: {
      type: Boolean,
      default: false
    }
  },
  created () {
    this.localOpenKeys = this.openKeys.slice(0)
  },
  data () {
    return {
      localOpenKeys: []
    }
  },
  methods: {
    handlePlus (item) {
      this.$emit('add', item)
    },
    handleTitleClick (...args) {
      this.$emit('titleClick', { args })
    },
    renderSearch () {
      return (
        <Search
          placeholder="input search text"
          style="width: 100%; margin-bottom: 1rem"
        />
      )
    },
    renderIcon (icon) {
      return icon && (<Icon type={icon} />) || null
    },
    renderMenuItem (item) {
      return (
        <Item key={item.key}>
          { this.renderIcon(item.icon) }
          { item.title }
          <a class="btn" style="width: 20px;z-index:1300" {...{ on: { click: () => this.handlePlus(item) } }}><a-icon type="plus"/></a>
        </Item>
      )
    },
    renderItem (item) {
      return item.children ? this.renderSubItem(item, item.key) : this.renderMenuItem(item, item.key)
    },
    renderItemGroup (item) {
      const childrenItems = item.children.map(o => {
        return this.renderItem(o, o.key)
      })
      return (
        <ItemGroup key={item.key}>
          <template slot="title">
            <span>{ item.title }</span>
            <a-dropdown>
              <a class="btn"><a-icon type="ellipsis" /></a>
              <a-menu slot="overlay">
                <a-menu-item key="1">新增</a-menu-item>
                <a-menu-item key="2">合并</a-menu-item>
                <a-menu-item key="3">移除</a-menu-item>
              </a-menu>
            </a-dropdown>
          </template>
          { childrenItems }
        </ItemGroup>
      )
    },
    renderSubItem (item, key) {
      const childrenItems = item.children && item.children.map(o => {
        return this.renderItem(o, o.key)
      })
      const title = (
        <span slot="title">
          { this.renderIcon(item.icon) }
          <span>{ item.title }</span>
        </span>
      )
      if (item.group) {
        return this.renderItemGroup(item)
      }
      // titleClick={this.handleTitleClick(item)}
      return (
        <SubMenu key={key}>
          { title }
          { childrenItems }
        </SubMenu>
      )
    }
  },
  render () {
    const { dataSource, search } = this.$props
    // this.localOpenKeys = openKeys.slice(0)
    const list = dataSource.map(item => {
      return this.renderItem(item)
    })
    return (
      <div class="tree-wrapper">
        { search ? this.renderSearch() : null }
        <Menu mode="inline" class="custom-tree" {...{ on: { click: item => this.$emit('click', item), 'update:openKeys': val => { this.localOpenKeys = val } } }} openKeys={this.localOpenKeys}>
          { list }
        </Menu>
      </div>
    )
  }
}
_web/src/components/Trend/Trend.vue
New file
@@ -0,0 +1,41 @@
<template>
  <div :class="[prefixCls, reverseColor && 'reverse-color' ]">
    <span>
      <slot name="term"></slot>
      <span class="item-text">
        <slot></slot>
      </span>
    </span>
    <span :class="[flag]"><a-icon :type="`caret-${flag}`"/></span>
  </div>
</template>
<script>
export default {
  name: 'Trend',
  props: {
    prefixCls: {
      type: String,
      default: 'ant-pro-trend'
    },
    /**
       * 上升下降标识:up|down
       */
    flag: {
      type: String,
      required: true
    },
    /**
       * 颜色反转
       */
    reverseColor: {
      type: Boolean,
      default: false
    }
  }
}
</script>
<style lang="less" scoped>
  @import "index";
</style>
_web/src/components/Trend/index.js
New file
@@ -0,0 +1,3 @@
import Trend from './Trend.vue'
export default Trend
_web/src/components/Trend/index.less
New file
@@ -0,0 +1,42 @@
@import "../index";
@trend-prefix-cls: ~"@{ant-pro-prefix}-trend";
.@{trend-prefix-cls} {
  display: inline-block;
  font-size: @font-size-base;
  line-height: 22px;
  .up,
  .down {
    margin-left: 4px;
    position: relative;
    top: 1px;
    i {
      font-size: 12px;
      transform: scale(0.83);
    }
  }
  .item-text {
    display: inline-block;
    margin-left: 8px;
    color: rgba(0,0,0,.85);
  }
  .up {
    color: @red-6;
  }
  .down {
    color: @green-6;
    top: -1px;
  }
  &.reverse-color .up {
    color: @green-6;
  }
  &.reverse-color .down {
    color: @red-6;
  }
}
_web/src/components/Trend/index.md
New file
@@ -0,0 +1,45 @@
# Trend 趋势标记
趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。
引用方式:
```javascript
import Trend from '@/components/Trend'
export default {
    components: {
        Trend
    }
}
```
## 代码演示  [demo](https://pro.loacg.com/test/home)
```html
<trend flag="up">5%</trend>
```
```html
<trend flag="up">
    <span slot="term">工资</span>
    5%
</trend>
```
```html
<trend flag="up" term="工资">5%</trend>
```
## API
| 参数      | 说明                                      | 类型         | 默认值 |
|----------|------------------------------------------|-------------|-------|
| flag | 上升下降标识:`up|down` | string | - |
| reverseColor | 颜色反转 | Boolean | false |
_web/src/components/UserSelect/UserSelect.vue
New file
@@ -0,0 +1,77 @@
<template>
  <a-select
    :mode="model"
    showSearch
    :value="selectValue"
    :filter-option="false"
    :placeholder="placeholder"
    :not-found-content="fetching ? undefined : null"
    @search="fetchUser"
    @change="handleChange"
  >
    <a-spin v-if="fetching" slot="notFoundContent" size="small" />
    <a-select-option v-for="d in data" :key="d.value">
      {{ d.text }}
    </a-select-option>
  </a-select>
</template>
<script>
import debounce from 'lodash/debounce'
import { getUserPage } from '@/api/modular/system/userManage'
export default {
  name: 'UserSelect',
  props: {
    placeholder: {
      type: String
    },
    value: {
      type: String
    },
    multiple: {
      type: Boolean,
      default: false
    }
  },
  data() {
    const multiple = this.multiple
    this.fetchUser = debounce(this.fetchUser, 800)
    return {
      data: [],
      fetching: false,
      selectValue: multiple ? [] : undefined,
      model: multiple ? 'multiple' : 'default'
    }
  },
  methods: {
    fetchUser(key) {
      console.log('fetching user', key)
      this.data = []
      this.fetching = true
      const params = {
        pageNo: 1,
        pageSize: 10,
        searchValue: key
      }
      this.userFetching = true
      getUserPage(params).then((res) => {
        this.data = res.data.rows.map(user => ({
          text: `${user.name} ${user.account}`,
          value: user.id
        }))
      }).finally(() => {
        this.fetching = false
      })
    },
    handleChange(value) {
      Object.assign(this, {
        selectValue: value,
        fetching: false
      })
      this.$emit('change', value)
    }
  }
}
</script>
_web/src/components/UserSelect/index.js
New file
@@ -0,0 +1,3 @@
import UserSelect from './UserSelect'
export default UserSelect
_web/src/components/_util/util.js
New file
@@ -0,0 +1,46 @@
/**
 * components util
 */
/**
 * 清理空值,对象
 * @param children
 * @returns {*[]}
 */
export function filterEmpty (children = []) {
  return children.filter(c => c.tag || (c.text && c.text.trim() !== ''))
}
/**
 * 获取字符串长度,英文字符 长度1,中文字符长度2
 * @param {*} str
 */
export const getStrFullLength = (str = '') =>
  str.split('').reduce((pre, cur) => {
    const charCode = cur.charCodeAt(0)
    if (charCode >= 0 && charCode <= 128) {
      return pre + 1
    }
    return pre + 2
  }, 0)
/**
 * 截取字符串,根据 maxLength 截取后返回
 * @param {*} str
 * @param {*} maxLength
 */
export const cutStrByFullLength = (str = '', maxLength) => {
  let showLength = 0
  return str.split('').reduce((pre, cur) => {
    const charCode = cur.charCodeAt(0)
    if (charCode >= 0 && charCode <= 128) {
      showLength += 1
    } else {
      showLength += 2
    }
    if (showLength <= maxLength) {
      return pre + cur
    }
    return pre
  }, '')
}
_web/src/components/global.less
New file
@@ -0,0 +1,516 @@
@import './index.less';
body {
}
#app {
  height: 100%;
  &.colorWeak {
    filter: invert(80%);
  }
  &.userLayout {
    overflow: auto;
  }
}
.layout.ant-layout {
  height: auto;
  overflow-x: hidden;
  &.mobile,
  &.tablet {
    .ant-layout-content {
      .content {
        margin: 24px 0 0;
      }
    }
    /**
     * ant-table-wrapper
     * 覆盖的表格手机模式样式,如果想修改在手机上表格最低宽度,可以在这里改动
     */
    .ant-table-wrapper {
      .ant-table-content {
        overflow-y: auto;
      }
      .ant-table-body {
        min-width: 800px;
      }
    }
    .topmenu {
      /* 必须为 topmenu  才能启用流式布局 */
      &.content-width-Fluid {
        .header-index-wide {
          margin-left: 0;
        }
      }
    }
  }
  &.mobile {
    .sidemenu {
      .ant-header-fixedHeader {
        &.ant-header-side-opened,
        &.ant-header-side-closed {
          width: 100%;
        }
      }
    }
  }
  &.ant-layout-has-sider {
    flex-direction: row;
  }
  .trigger {
    font-size: 20px;
    line-height: 55px;
    padding: 0 24px;
    cursor: pointer;
    transition: color 0.3s;
    &:hover {
      background: rgba(0, 0, 0, 0.025);
    }
  }
  .topmenu {
    .ant-header-fixedHeader {
      position: fixed;
      top: 0;
      right: 0;
      z-index: 9;
      width: 100%;
      transition: width 0.2s;
      &.ant-header-side-opened {
        width: 100%;
      }
      &.ant-header-side-closed {
        width: 100%;
      }
    }
    /* 必须为 topmenu  才能启用流式布局 */
    &.content-width-Fluid {
      .header-index-wide {
        max-width: unset;
        .header-index-left {
          flex: 1 1 1000px;
          .logo{
            margin-left: 25px;
          }
          .ant-menu.ant-menu-horizontal{
            max-width: calc(100vw - 190px - 238px - 25px);
            flex: 1 1 calc(100vw - 190px - 238px - 25px);
          }
        }
        .header-index-right{
          margin-right:25px;
        }
      }
      .page-header-index-wide {
        max-width: unset;
      }
    }
  }
  .sidemenu {
    .ant-header-fixedHeader {
      position: fixed;
      top: 0;
      right: 0;
      z-index: 9;
      width: 100%;
      transition: width 0.2s;
      &.ant-header-side-opened {
        width: calc(100% - 230px);
      }
      &.ant-header-side-closed {
        width: calc(100% - 80px);
      }
    }
  }
  .header {
    height: 55px;
    // padding: 0 12px 0 0;
    background: #fff;
    box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
    position: relative;
  }
  .header,
  .top-nav-header-index {
    .user-wrapper {
      float: right;
      height: 100%;
      .action {
        line-height: 55px;
        cursor: pointer;
        padding: 0 12px;
        display: inline-block;
        transition: all 0.3s;
        height: 100%;
        color: rgba(0, 0, 0, 0.65);
        &:hover {
          background: rgba(0, 0, 0, 0.025);
        }
        .avatar {
          margin: 15px 8px 15px 0;
          color: #1890ff;
          background: hsla(0, 0%, 100%, 0.85);
          vertical-align: middle;
        }
        .icon {
          font-size: 16px;
          padding: 4px;
        }
      }
    }
    &.dark {
      .user-wrapper {
        .action {
          color: rgba(255, 255, 255, 0.85);
          a {
            color: rgba(255, 255, 255, 0.85);
          }
          &:hover {
            background: rgba(255, 255, 255, 0.16);
          }
        }
      }
    }
  }
  &.mobile,
  &.tablet {
    .top-nav-header-index {
      .header-index-wide {
        .header-index-left {
          .trigger {
            color: rgba(255, 255, 255, 0.85);
            padding: 0 12px;
          }
          .logo.top-nav-header {
            flex: 0 0 56px;
            text-align: center;
            line-height: 58px;
            h1 {
              display: none;
            }
          }
        }
      }
      &.light {
        .header-index-wide {
          .header-index-left {
            .trigger {
              color: rgba(0, 0, 0, 0.65);
            }
          }
        }
      }
    }
  }
  &.tablet {
    // overflow: hidden; text-overflow:ellipsis; white-space: nowrap;
    .top-nav-header-index {
      .header-index-wide {
        .header-index-left {
          .logo > a {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
          }
        }
        .ant-menu.ant-menu-horizontal {
          flex: 1 1 auto;
          white-space: normal;
        }
      }
    }
  }
  .top-nav-header-index {
    box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
    position: relative;
    transition: background 0.3s, width 0.2s;
    .header-index-wide {
      max-width: 1200px;
      margin: auto;
      padding-left: 0;
      display: flex;
      height: 55px;
      .ant-menu.ant-menu-horizontal {
        max-width: 835px;
        flex: 0 1 835px;
        border: none;
        height: 55px;
        line-height: 55px;
      }
      .header-index-left {
        flex: 0 1 1000px;
        display: flex;
        .logo.top-nav-header {
          flex: 0 0 165px;
          width: 165px;
          height: 55px;
          position: relative;
          line-height: 55px;
          transition: all 0.3s;
          overflow: hidden;
          img,
          svg {
            display: inline-block;
            vertical-align: middle;
            height: 32px;
            width: 32px;
          }
          h1 {
            color: #fff;
            display: inline-block;
            vertical-align: top;
            font-size: 16px;
            margin: 0 0 0 12px;
            font-weight: 400;
          }
        }
      }
      .header-index-right {
        flex: 0 0 238px;
        align-self: flex-end;
        height: 55px;
        overflow: hidden;
        .content-box {
          float: right;
          .action {
            max-width: 140px;
            overflow: hidden;
            text-overflow:ellipsis;
            white-space:nowrap;
          }
        }
      }
    }
    &.light {
      background-color: #fff;
      .header-index-wide {
        .header-index-left {
          .logo {
            h1 {
              color: #002140;
            }
          }
        }
      }
    }
  }
  // 内容区
  .layout-content {
    margin: 24px 24px 0px;
    //height: 100%;
    //height: 64px;
    padding: 0 12px 0 0;
  }
  // footer
  .ant-layout-footer {
    padding: 0;
  }
}
.topmenu {
  .page-header-index-wide {
    max-width: 1200px;
    margin: 0 auto;
  }
}
// drawer-sider 自定义
.ant-drawer.drawer-sider {
  .sider {
    box-shadow: none;
  }
  &.dark {
    .ant-drawer-content {
      background-color: rgb(0, 21, 41);
    }
  }
  &.light {
    box-shadow: none;
    .ant-drawer-content {
      background-color: #fff;
    }
  }
  .ant-drawer-body {
    padding: 0;
  }
}
// 菜单样式
.sider {
  box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
  position: relative;
  z-index: @ant-global-sider-zindex;
  min-height: 100vh;
  .ant-layout-sider-children {
    overflow-y: hidden;
    &:hover {
      overflow-y: auto;
    }
  }
  &.ant-fixed-sidemenu {
    position: fixed;
    height: 100%;
  }
  // logo区域样式
  .logo {
    position: relative;
    height: 55px;
    padding-left: 24px;
    overflow: hidden;
    line-height: 55px;
    background: #002140;
    transition: all .3s;
    img,
    svg,
    h1 {
      display: inline-block;
      vertical-align: middle;
    }
    img,
    svg {
      height: 32px;
      width: 32px;
    }
    h1 {
      color: #fff;
      font-size: 20px;
      margin: 0 0 0 12px;
      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
      font-weight: 600;
      vertical-align: middle;
    }
  }
  &.light {
    background-color: #fff;
    box-shadow: 2px 0px 8px 0px rgba(29, 35, 41, 0.05);
    .logo {
      background: #fff;
      box-shadow: 1px 1px 0px 0px #e8e8e8;
      h1 {
        color: unset;
      }
    }
    .ant-menu-light {
      border-right-color: transparent;
    }
  }
}
// 外置的样式控制
.user-dropdown-menu {
  span {
    user-select: none;
  }
}
.user-dropdown-menu-wrapper.ant-dropdown-menu {
  padding: 4px 0;
  .ant-dropdown-menu-item {
    width: 160px;
  }
  .ant-dropdown-menu-item > .anticon:first-child,
  .ant-dropdown-menu-item > a > .anticon:first-child,
  .ant-dropdown-menu-submenu-title > .anticon:first-child .ant-dropdown-menu-submenu-title > a > .anticon:first-child {
    min-width: 12px;
    margin-right: 8px;
  }
}
// 数据列表 样式
.table-alert {
  margin-bottom: 16px;
}
.table-page-search-wrapper {
  .ant-form-inline {
    .ant-form-item {
      display: flex;
      margin-bottom: 24px;
      margin-right: 0;
      .ant-form-item-control-wrapper {
        flex: 1 1;
        display: inline-block;
        vertical-align: middle;
      }
      > .ant-form-item-label {
        line-height: 32px;
        padding-right: 8px;
        width: auto;
      }
      .ant-form-item-control {
        height: 32px;
        line-height: 32px;
      }
    }
  }
  .table-page-search-submitButtons {
    display: block;
    margin-bottom: 24px;
    white-space: nowrap;
  }
}
.content {
  .table-operator {
    margin-bottom: 18px;
    button {
      margin-right: 8px;
    }
  }
}
_web/src/components/index.js
New file
@@ -0,0 +1,72 @@
// chart
import Bar from '@/components/Charts/Bar'
import ChartCard from '@/components/Charts/ChartCard'
import Liquid from '@/components/Charts/Liquid'
import MiniArea from '@/components/Charts/MiniArea'
import MiniSmoothArea from '@/components/Charts/MiniSmoothArea'
import MiniBar from '@/components/Charts/MiniBar'
import MiniProgress from '@/components/Charts/MiniProgress'
import Radar from '@/components/Charts/Radar'
import RankList from '@/components/Charts/RankList'
import TransferBar from '@/components/Charts/TransferBar'
import TagCloud from '@/components/Charts/TagCloud'
// pro components
import AvatarList from '@/components/AvatarList'
import CountDown from '@/components/CountDown'
import Ellipsis from '@/components/Ellipsis'
import FooterToolbar from '@/components/FooterToolbar'
import NumberInfo from '@/components/NumberInfo'
import DescriptionList from '@/components/DescriptionList'
import Tree from '@/components/Tree/Tree'
import Trend from '@/components/Trend'
import STable from '@/components/Table'
import MultiTab from '@/components/MultiTab'
import Result from '@/components/Result'
import IconSelector from '@/components/IconSelector'
import TagSelect from '@/components/TagSelect'
import ExceptionPage from '@/components/Exception'
import StandardFormRow from '@/components/StandardFormRow'
import ArticleListContent from '@/components/ArticleListContent'
import AntdEditor from '@/components/Editor/WangEditor'
import Dialog from '@/components/Dialog'
// xn components
import XCard from '@/components/xnComponents/XCard'
import XDown from '@/components/xnComponents/XDown'
export {
  AvatarList,
  Bar,
  ChartCard,
  Liquid,
  MiniArea,
  MiniSmoothArea,
  MiniBar,
  MiniProgress,
  Radar,
  TagCloud,
  RankList,
  TransferBar,
  Trend,
  CountDown,
  Ellipsis,
  FooterToolbar,
  NumberInfo,
  DescriptionList,
  // 兼容写法,请勿继续使用
  DescriptionList as DetailList,
  Tree,
  STable,
  MultiTab,
  Result,
  ExceptionPage,
  IconSelector,
  TagSelect,
  StandardFormRow,
  ArticleListContent,
  AntdEditor,
  Dialog,
  XCard,
  XDown
}
_web/src/components/index.less
New file
@@ -0,0 +1,6 @@
@import "~ant-design-vue/lib/style/index";
// The prefix to use on all css classes from ant-pro.
@ant-pro-prefix             : ant-pro;
@ant-global-sider-zindex    : 106;
@ant-global-header-zindex   : 105;
_web/src/components/tools/Breadcrumb.vue
New file
@@ -0,0 +1,45 @@
<template>
  <a-breadcrumb class="breadcrumb">
    <a-breadcrumb-item v-for="(item, index) in breadList" :key="item.name">
      <router-link
        v-if="item.name != name && index != 1"
        :to="{ path: item.path === '' ? '/' : item.path }"
      >{{ item.meta.title }}</router-link>
      <span v-else>{{ item.meta.title }}</span>
    </a-breadcrumb-item>
  </a-breadcrumb>
</template>
<script>
export default {
  data () {
    return {
      name: '',
      breadList: []
    }
  },
  created () {
    this.getBreadcrumb()
  },
  methods: {
    getBreadcrumb () {
      this.breadList = []
      // this.breadList.push({name: 'index', path: '/dashboard/', meta: {title: '首页'}})
      this.name = this.$route.name
      this.$route.matched.forEach(item => {
        // item.name !== 'index' && this.breadList.push(item)
        this.breadList.push(item)
      })
    }
  },
  watch: {
    $route () {
      this.getBreadcrumb()
    }
  }
}
</script>
<style scoped>
</style>
_web/src/components/tools/DetailList.vue
New file
@@ -0,0 +1,5 @@
<script>
/* WARNING: 兼容老引入,请勿继续使用 */
import DescriptionList from '@/components/DescriptionList'
export default DescriptionList
</script>
_web/src/components/tools/HeadInfo.vue
New file
@@ -0,0 +1,67 @@
<template>
  <div class="head-info" :class="center && 'center'">
    <span>{{ title }}</span>
    <p>{{ content }}</p>
    <em v-if="bordered"/>
  </div>
</template>
<script>
export default {
  name: 'HeadInfo',
  props: {
    title: {
      type: String,
      default: ''
    },
    content: {
      type: String,
      default: ''
    },
    bordered: {
      type: Boolean,
      default: false
    },
    center: {
      type: Boolean,
      default: true
    }
  }
}
</script>
<style lang="less" scoped>
  .head-info {
    position: relative;
    text-align: left;
    padding: 0 32px 0 0;
    min-width: 125px;
    &.center {
      text-align: center;
      padding: 0 32px;
    }
    span {
      color: rgba(0, 0, 0, .45);
      display: inline-block;
      font-size: 14px;
      line-height: 22px;
      margin-bottom: 4px;
    }
    p {
      color: rgba(0, 0, 0, .85);
      font-size: 24px;
      line-height: 32px;
      margin: 0;
    }
    em {
      background-color: #e8e8e8;
      position: absolute;
      height: 56px;
      width: 1px;
      top: 0;
      right: 0;
    }
  }
</style>
_web/src/components/tools/LangSelect.vue
New file
@@ -0,0 +1,46 @@
<template>
  <a-dropdown>
    <span class="action global-lang">
      <a-icon type="global" style="font-size: 16px"/>
    </span>
    <a-menu slot="overlay" style="width: 150px;" @click="SwitchLang">
      <a-menu-item key="zh-CN">
        <a rel="noopener noreferrer">
          <span role="img" aria-label="简体中文">🇨🇳</span> 简体中文
        </a>
      </a-menu-item>
      <a-menu-item key="zh-TW">
        <a rel="noopener noreferrer">
          <span role="img" aria-label="繁体中文">🇭🇰</span> 繁体中文
        </a>
      </a-menu-item>
      <a-menu-item key="en-US">
        <a rel="noopener noreferrer">
          <span role="img" aria-label="English">🇬🇧</span> English
        </a>
      </a-menu-item>
      <a-menu-item key="pt-BR">
        <a rel="noopener noreferrer">
          <span role="img" aria-label="Português">🇧🇷</span> Português
        </a>
      </a-menu-item>
    </a-menu>
  </a-dropdown>
</template>
<script>
// import { mixin as langMixin } from '@/store/i18n-mixin'
export default {
  name: 'LangSelect',
  // mixins: [langMixin],
  data () {
    return {}
  },
  methods: {
    // SwitchLang (row) {
    //   this.setLang(row.key)
    // }
  }
}
</script>
_web/src/components/tools/Logo.vue
New file
@@ -0,0 +1,53 @@
<template>
  <div class="logo">
    <router-link :to="{name:'Console'}">
      <LogoSvg alt="logo" />
      <h1 v-if="showTitle">{{ this.titles }}</h1>
    </router-link>
  </div>
</template>
<script>
import LogoSvg from '@/assets/logo.svg?inline'
import { mixin, mixinDevice } from '@/utils/mixin'
export default {
  name: 'Logo',
  components: {
    LogoSvg
  },
  mixins: [mixin, mixinDevice],
  data () {
    return {
      titles: ''
    }
  },
  props: {
    title: {
      type: String,
      default: 'Snowy',
      required: false
    },
    showTitle: {
      type: Boolean,
      default: true,
      required: false
    }
  },
  created () {
    if (this.layoutMode === 'topmenu') {
      if (this.title.length > 8) {
        this.titles = this.title.substring(0, 7) + '...'
      } else {
        this.titles = this.title
      }
    } else {
      if (this.title.length > 10) {
        this.titles = this.title.substring(0, 9) + '...'
      } else {
        this.titles = this.title
      }
    }
  }
}
</script>
_web/src/components/tools/TwoStepCaptcha.vue
New file
@@ -0,0 +1,89 @@
<template>
  <!-- 两步验证 -->
  <a-modal
    centered
    v-model="visible"
    @cancel="handleCancel"
    :maskClosable="false"
  >
    <div slot="title" :style="{ textAlign: 'center' }">两步验证</div>
    <template slot="footer">
      <div :style="{ textAlign: 'center' }">
        <a-button key="back" @click="handleCancel">返回</a-button>
        <a-button key="submit" type="primary" :loading="stepLoading" @click="handleStepOk">
          继续
        </a-button>
      </div>
    </template>
    <a-spin :spinning="stepLoading">
      <a-form layout="vertical" :auto-form-create="(form)=>{this.form = form}">
        <div class="step-form-wrapper">
          <p style="text-align: center" v-if="!stepLoading">请在手机中打开 Google Authenticator 或两步验证 APP<br />输入 6 位动态码</p>
          <p style="text-align: center" v-else>正在验证..<br/>请稍后</p>
          <a-form-item
            :style="{ textAlign: 'center' }"
            hasFeedback
            fieldDecoratorId="stepCode"
            :fieldDecoratorOptions="{rules: [{ required: true, message: '请输入 6 位动态码!', pattern: /^\d{6}$/, len: 6 }]}"
          >
            <a-input :style="{ textAlign: 'center' }" @keyup.enter.native="handleStepOk" placeholder="000000" />
          </a-form-item>
          <p style="text-align: center">
            <a @click="onForgeStepCode">遗失手机?</a>
          </p>
        </div>
      </a-form>
    </a-spin>
  </a-modal>
</template>
<script>
export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      stepLoading: false,
      form: null
    }
  },
  methods: {
    handleStepOk () {
      const vm = this
      this.stepLoading = true
      this.form.validateFields((err, values) => {
        if (!err) {
          console.log('values', values)
          setTimeout(() => {
            vm.stepLoading = false
            vm.$emit('success', { values })
          }, 2000)
          return
        }
        this.stepLoading = false
        this.$emit('error', { err })
      })
    },
    handleCancel () {
      this.visible = false
      this.$emit('cancel')
    },
    onForgeStepCode () {
    }
  }
}
</script>
<style lang="less" scoped>
  .step-form-wrapper {
    margin: 0 auto;
    width: 80%;
    max-width: 400px;
  }
</style>
_web/src/components/tools/UserMenu.vue
New file
@@ -0,0 +1,191 @@
<template>
  <div class="user-wrapper">
    <div class="content-box">
      <span class="action" @click="toggleFullscreen">
        <a-icon type="fullscreen-exit" v-if="isFullscreen"/>
        <a-icon type="fullscreen" v-else/>
      </span>
      <notice-icon class="action"/>
      <a-dropdown>
        <span class="action ant-dropdown-link user-dropdown-menu">
          <a-avatar class="avatar" size="small" :src="avatar"/>
          <span>{{ nickname }}</span>
        </span>
        <a-menu slot="overlay" class="user-dropdown-menu-wrapper">
          <a-menu-item key="4" v-if="mode === 'sidemenu'">
            <a @click="appToggled()" >
              <a-icon type="swap"/>
              <span>切换应用</span>
            </a>
          </a-menu-item>
          <a-menu-item key="0">
            <router-link :to="{ name: 'center' }">
              <a-icon type="user"/>
              <span>个人中心</span>
            </router-link>
          </a-menu-item>
          <a-menu-item key="1">
            <router-link :to="{ name: 'settings' }">
              <a-icon type="setting"/>
              <span>账户设置</span>
            </router-link>
          </a-menu-item>
          <a-menu-divider/>
          <a-menu-item key="3">
            <a href="javascript:;" @click="handleLogout">
              <a-icon type="logout"/>
              <span>退出登录</span>
            </a>
          </a-menu-item>
        </a-menu>
      </a-dropdown>
    </div>
    <a-modal
      title="切换应用"
      :visible="visible"
      :footer="null"
      :confirm-loading="confirmLoading"
      @cancel="handleCancel"
    >
      <a-form :form="form1" >
        <a-form-item
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          label="选择应用"
        >
          <a-menu
            mode="inline"
            :default-selected-keys="this.defApp"
            style="border-bottom:0px;lineHeight:55px;"
          >
            <a-menu-item v-for="(item) in userInfo.apps" :key="item.code" style="top:0px;" @click="switchApp(item.code)">
              {{ item.name }}
            </a-menu-item>
          </a-menu>
        </a-form-item>
      </a-form>
    </a-modal>
  </div>
</template>
<script>
import screenfull from 'screenfull'
import NoticeIcon from '@/components/NoticeIcon'
import { mapActions, mapGetters } from 'vuex'
import { ALL_APPS_MENU } from '@/store/mutation-types'
import Vue from 'vue'
import { message } from 'ant-design-vue/es'
export default {
  name: 'UserMenu',
  components: {
    NoticeIcon,
    screenfull
  },
  props: {
    mode: {
      type: String,
      // sidemenu, topmenu
      default: 'sidemenu'
    }
  },
  data () {
    return {
      labelCol: {
        xs: { span: 24 },
        sm: { span: 5 }
      },
      wrapperCol: {
        xs: { span: 24 },
        sm: { span: 16 }
      },
      visible: false,
      confirmLoading: false,
      form1: this.$form.createForm(this),
      defApp: [],
      isFullscreen: false
    }
  },
  computed: {
    ...mapGetters(['nickname', 'avatar', 'userInfo'])
  },
  methods: {
    ...mapActions(['Logout', 'MenuChange']),
    handleLogout () {
      this.$confirm({
        title: '提示',
        content: '真的要注销登录吗 ?',
        okText: '确定',
        cancelText: '取消',
        onOk: () => {
          return this.Logout({}).then(() => {
            setTimeout(() => {
              window.location.reload()
            }, 16)
          }).catch(err => {
            this.$message.error({
              title: '错误',
              description: err.message
            })
          })
        },
        onCancel () {
        }
      })
    },
    /**
     * 打开切换应用框
     */
    appToggled () {
      this.visible = true
      this.defApp.push(Vue.ls.get(ALL_APPS_MENU)[0].code)
    },
    switchApp (appCode) {
      this.visible = false
      this.defApp = []
      const applicationData = this.userInfo.apps.filter(item => item.code === appCode)
      const hideMessage = message.loading('正在切换应用!', 0)
      this.MenuChange(applicationData[0]).then((res) => {
        hideMessage()
      // eslint-disable-next-line handle-callback-err
      }).catch((err) => {
        message.error('应用切换异常')
      })
    },
    handleCancel () {
      this.form1.resetFields()
      this.visible = false
    },
    /* 全屏切换 */
    toggleFullscreen () {
      if (!screenfull.isEnabled) {
        message.error('您的浏览器不支持全屏模式')
        return
      }
      screenfull.toggle()
      if (screenfull.isFullscreen) {
        this.isFullscreen = false
      } else {
        this.isFullscreen = true
      }
    }
  }
}
</script>
<style lang="less" scoped>
    .appRedio {
    border:1px solid #91d5ff;
    padding:10px 20px;
    background: #e6f7ff;
    border-radius:2px;
    margin-bottom:10px;
      color: #91d5ff;
    /*display: inline;
    margin-bottom:10px;*/
    }
</style>
_web/src/components/tools/index.js
_web/src/components/verifition/Verify.vue
New file
@@ -0,0 +1,473 @@
<template>
  <div :class="mode=='pop'?'mask':''" v-show="showBox">
    <div :class="mode=='pop'?'verifybox':''" :style="{'max-width':parseInt(imgSize.width)+30+'px'}">
      <div class="verifybox-top" v-if="mode=='pop'">
        请完成安全验证
        <span @click="closeBox" class="verifybox-close">
          <i class="iconfont icon-close"></i>
        </span>
      </div>
      <div :style="{padding:mode=='pop'?'15px':'0'}" class="verifybox-bottom">
        <!-- 验证码容器 -->
        <components
          :arith="arith"
          :barSize="barSize"
          :blockSize="blockSize"
          :captchaType="captchaType"
          :explain="explain"
          :figure="figure"
          :imgSize="imgSize"
          :is="componentType"
          :mode="mode"
          :type="verifyType"
          :vSpace="vSpace"
          ref="instance"
          v-if="componentType"></components>
      </div>
    </div>
  </div>
</template>
<script type="text/babel">
    /**
     * Verify 验证码组件
     * @description 分发验证码使用
     * */
    import VerifySlide from './Verify/VerifySlide'
    import VerifyPoints from './Verify/VerifyPoints'
    export default {
        name: 'Vue2Verify',
        props: {
            // 双语化
            locale: {
                require: false,
                type: String,
                default() {
                    // 默认语言不输入为浏览器语言
                    if (navigator.language) {
                        var language = navigator.language
                    } else {
                        // eslint-disable-next-line no-redeclare
                        var language = navigator.browserLanguage
                    }
                    return language
                }
            },
            captchaType: {
                type: String,
                required: true
            },
            // eslint-disable-next-line vue/require-default-prop
            figure: {
                type: Number
            },
            // eslint-disable-next-line vue/require-default-prop
            arith: {
                type: Number
            },
            mode: {
                type: String,
                default: 'pop'
            },
            // eslint-disable-next-line vue/require-default-prop
            vSpace: {
                type: Number
            },
            // eslint-disable-next-line vue/require-default-prop
            explain: {
                type: String
            },
            imgSize: {
                type: Object,
                default() {
                    return {
                        width: '310px',
                        height: '155px'
                    }
                }
            },
            // eslint-disable-next-line vue/require-default-prop
            blockSize: {
                type: Object
            },
            // eslint-disable-next-line vue/require-default-prop
            barSize: {
                type: Object
            }
        },
        data() {
            return {
                // showBox:true,
                clickShow: false,
                // 内部类型
                verifyType: undefined,
                // 所用组件类型
                componentType: undefined
            }
        },
        methods: {
            /**
             * i18n
             * @description 兼容vue-i18n 调用$t来转换ok
             * @param {String} text-被转换的目标
             * @return {String} i18n的结果
             * */
            i18n(text) {
                if (this.$t) {
                    return this.$t(text)
                } else {
                    // 兼容不存在的语言
                    const i18n = this.$options.i18n.messages[this.locale] || this.$options.i18n.messages['en-US']
                    return i18n[text]
                }
            },
            /**
             * refresh
             * @description 刷新
             * */
            refresh() {
                if (this.instance.refresh) {
                    this.instance.refresh()
                }
            },
            closeBox() {
                this.clickShow = false
                this.refresh()
            },
            show() {
                // eslint-disable-next-line eqeqeq
                if (this.mode == 'pop') {
                    this.clickShow = true
                }
            }
        },
        computed: {
            instance() {
                return this.$refs.instance || {}
            },
            showBox() {
                // eslint-disable-next-line eqeqeq
                if (this.mode == 'pop') {
                    return this.clickShow
                } else {
                    return true
                }
            }
        },
        watch: {
            captchaType: {
                immediate: true,
                handler(captchaType) {
                    switch (captchaType.toString()) {
                        case 'blockPuzzle':
                            this.verifyType = '2'
                            this.componentType = 'VerifySlide'
                            break
                        case 'clickWord':
                            this.verifyType = ''
                            this.componentType = 'VerifyPoints'
                            break
                    }
                }
            }
        },
        components: {
            VerifySlide,
            VerifyPoints
        }
    }
</script>
<style>
    .verifybox{
        position: relative;
        box-sizing: border-box;
        border-radius: 2px;
        border: 1px solid #e4e7eb;
        background-color: #fff;
        box-shadow: 0 0 10px rgba(0,0,0,.3);
        left: 50%;
        top:50%;
        transform: translate(-50%,-50%);
    }
    .verifybox-top{
        padding: 0 15px;
        height: 50px;
        line-height: 50px;
        text-align: left;
        font-size: 16px;
        color: #45494c;
        border-bottom: 1px solid #e4e7eb;
        box-sizing: border-box;
    }
    .verifybox-bottom{
        padding: 15px;
        box-sizing: border-box;
    }
    .verifybox-close{
        position: absolute;
        top: 13px;
        right: 9px;
        width: 24px;
        height: 24px;
        text-align: center;
        cursor: pointer;
    }
    .mask{
        position: fixed;
        top: 0;
        left:0;
        z-index: 1001;
        width: 100%;
        height: 100vh;
        background: rgba(0,0,0,.3);
        /* display: none; */
        transition: all .5s;
    }
    .verify-tips{
        position: absolute;
        left: 0px;
        bottom:0px;
        width: 100%;
        height: 30px;
        line-height:30px;
        color: #fff;
    }
    .suc-bg{
       background-color:rgba(92, 184, 92,.5);
       filter: progid:DXImageTransform.Microsoft.gradient(startcolorstr=#7f5CB85C, endcolorstr=#7f5CB85C);
    }
    .err-bg{
       background-color:rgba(217, 83, 79,.5);
       filter: progid:DXImageTransform.Microsoft.gradient(startcolorstr=#7fD9534F, endcolorstr=#7fD9534F);
    }
    .tips-enter,.tips-leave-to{
        bottom: -30px;
    }
    .tips-enter-active,.tips-leave-active{
        transition: bottom .5s;
    }
    /* ---------------------------- */
    /*常规验证码*/
    .verify-code {
        font-size: 20px;
        text-align: center;
        cursor: pointer;
        margin-bottom: 5px;
        border: 1px solid #ddd;
    }
    .cerify-code-panel {
        height: 100%;
        overflow: hidden;
    }
    .verify-code-area {
        float: left;
    }
    .verify-input-area {
        float: left;
        width: 60%;
        padding-right: 10px;
    }
    .verify-change-area {
        line-height: 30px;
        float: left;
    }
    .varify-input-code {
        display: inline-block;
        width: 100%;
        height: 25px;
    }
    .verify-change-code {
        color: #337AB7;
        cursor: pointer;
    }
    .verify-btn {
        width: 200px;
        height: 30px;
        background-color: #337AB7;
        color: #FFFFFF;
        border: none;
        margin-top: 10px;
    }
    /*滑动验证码*/
    .verify-bar-area {
        position: relative;
        background: #FFFFFF;
        text-align: center;
        -webkit-box-sizing: content-box;
        -moz-box-sizing: content-box;
        box-sizing: content-box;
        border: 1px solid #ddd;
        -webkit-border-radius: 4px;
    }
    .verify-bar-area .verify-move-block {
        position: absolute;
        top: 0px;
        left: 0;
        background: #fff;
        cursor: pointer;
        -webkit-box-sizing: content-box;
        -moz-box-sizing: content-box;
        box-sizing: content-box;
        box-shadow: 0 0 2px #888888;
        -webkit-border-radius: 1px;
    }
    .verify-bar-area .verify-move-block:hover {
        background-color: #337ab7;
        color: #FFFFFF;
    }
    .verify-bar-area .verify-left-bar {
        position: absolute;
        top: -1px;
        left: -1px;
        background: #f0fff0;
        cursor: pointer;
        -webkit-box-sizing: content-box;
        -moz-box-sizing: content-box;
        box-sizing: content-box;
        border: 1px solid #ddd;
    }
    .verify-img-panel {
        margin: 0;
        -webkit-box-sizing: content-box;
        -moz-box-sizing: content-box;
        box-sizing: content-box;
        border-top: 1px solid #ddd;
        border-bottom: 1px solid #ddd;
        border-radius: 3px;
        position: relative;
    }
    .verify-img-panel .verify-refresh {
        width: 25px;
        height: 25px;
        text-align: center;
        padding: 5px;
        cursor: pointer;
        position: absolute;
        top: 0;
        right: 0;
        z-index: 2;
    }
    .verify-img-panel .icon-refresh {
        font-size: 20px;
        color: #fff;
    }
    .verify-img-panel .verify-gap {
        background-color: #fff;
        position: relative;
        z-index: 2;
        border: 1px solid #fff;
    }
    .verify-bar-area .verify-move-block .verify-sub-block {
        position: absolute;
        text-align: center;
        z-index: 3;
        /* border: 1px solid #fff; */
    }
    .verify-bar-area .verify-move-block .verify-icon {
        font-size: 18px;
    }
    .verify-bar-area .verify-msg {
        z-index: 3;
    }
    /*字体图标的css*/
    /*@font-face {font-family: "iconfont";*/
    /*src: url('../fonts/iconfont.eot?t=1508229193188'); !* IE9*!*/
    /*src: url('../fonts/iconfont.eot?t=1508229193188#iefix') format('embedded-opentype'), !* IE6-IE8 *!*/
    /*url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'),*/
    /*url('../fonts/iconfont.ttf?t=1508229193188') format('truetype'), !* chrome, firefox, opera, Safari, Android, iOS 4.2+*!*/
    /*url('../fonts/iconfont.svg?t=1508229193188#iconfont') format('svg'); !* iOS 4.1- *!*/
    /*}*/
    .iconfont {
        font-family: "iconfont" !important;
        font-size: 16px;
        font-style: normal;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
    }
    .icon-check:before {
        content: " ";
        display: block;
        width: 16px;
        height: 16px;
        position: absolute;
        margin: auto;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        z-index: 9999;
        background-image: url("data:image/png;base64,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");
        background-size: contain;
    }
    .icon-close:before {
        content: " ";
        display: block;
        width: 16px;
        height: 16px;
        position: absolute;
        margin: auto;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        z-index: 9999;
        background-image: url("data:image/png;base64,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");
        background-size: contain;
    }
    .icon-right:before {
        content: " ";
        display: block;
        width: 16px;
        height: 16px;
        position: absolute;
        margin: auto;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        background-size: cover;
        z-index: 9999;
        background-image: url("data:image/png;base64,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");
        background-size: contain;
    }
    .icon-refresh:before {
        content: " ";
        display: block;
        width: 16px;
        height: 16px;
        position: absolute;
        margin: auto;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        z-index: 9999;
        background-image: url("data:image/png;base64,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");
        background-size: contain;
    }
</style>
_web/src/components/verifition/Verify/VerifyPoints.vue
New file
@@ -0,0 +1,260 @@
/* eslint-disable no-unused-vars */
<template>
  <div
    style="position: relative"
  >
    <div class="verify-img-out">
      <div
        :style="{'width': setSize.imgWidth,
                 'height': setSize.imgHeight,
                 'background-size' : setSize.imgWidth + ' '+ setSize.imgHeight,
                 'margin-bottom': vSpace + 'px'}"
        class="verify-img-panel"
      >
        <div @click="refresh" class="verify-refresh" style="z-index:3" v-show="showRefresh">
          <i class="iconfont icon-refresh"></i>
        </div>
        <img
          :src="'data:image/png;base64,'+pointBackImgBase"
          @click="bindingClick?canvasClick($event):undefined"
          alt=""
          ref="canvas"
          style="width:100%;height:100%;display:block">
        <div
          :key="index"
          :style="{
            'background-color':'#1abd6c',
            color:'#fff',
            'z-index':9999,
            width:'20px',
            height:'20px',
            'text-align':'center',
            'line-height':'20px',
            'border-radius': '50%',
            position:'absolute',
            top:parseInt(tempPoint.y-10) + 'px',
            left:parseInt(tempPoint.x-10) + 'px'
          }"
          class="point-area"
          v-for="(tempPoint, index) in tempPoints">
          {{ index + 1 }}
        </div>
      </div>
    </div>
    <!-- 'height': this.barSize.height, -->
    <div
      :style="{'width': setSize.imgWidth,
               'color': this.barAreaColor,
               'border-color': this.barAreaBorderColor,
               'line-height':this.barSize.height}"
      class="verify-bar-area">
      <span class="verify-msg">{{ text }}</span>
    </div>
  </div>
</template>
<script type="text/babel">
    /**
     * VerifyPoints
     * @description 点选
     * */
    import { resetSize } from './../utils/util'
    import { aesEncrypt } from './../utils/ase'
    import { reqGet, reqCheck } from '@/api/modular/system/loginManage'
    export default {
        name: 'VerifyPoints',
        props: {
            // 弹出式pop,固定fixed
            mode: {
                type: String,
                default: 'fixed'
            },
            // eslint-disable-next-line vue/require-default-prop
            captchaType: {
                type: String
            },
            // 间隔
            vSpace: {
                type: Number,
                default: 5
            },
            imgSize: {
                type: Object,
                default() {
                    return {
                        width: '310px',
                        height: '155px'
                    }
                }
            },
            barSize: {
                type: Object,
                default() {
                    return {
                        width: '310px',
                        height: '40px'
                    }
                }
            }
        },
        data() {
            return {
                secretKey: '', // 后端返回的ase加密秘钥
                checkNum: 3, // 默认需要点击的字数
                fontPos: [], // 选中的坐标信息
                checkPosArr: [], // 用户点击的坐标
                num: 1, // 点击的记数
                pointBackImgBase: '', // 后端获取到的背景图片
                poinTextList: [], // 后端返回的点击字体顺序
                backToken: '', // 后端返回的token值
                setSize: {
                    imgHeight: 0,
                    imgWidth: 0,
                    barHeight: 0,
                    barWidth: 0
                },
                tempPoints: [],
                text: '',
                barAreaColor: undefined,
                barAreaBorderColor: undefined,
                showRefresh: true,
                bindingClick: true
            }
        },
        computed: {
            resetSize() {
                return resetSize
            }
        },
        methods: {
            init() {
                // 加载页面
                this.fontPos.splice(0, this.fontPos.length)
                this.checkPosArr.splice(0, this.checkPosArr.length)
                this.num = 1
                this.getPictrue()
                this.$nextTick(() => {
                    this.setSize = this.resetSize(this)    // 重新设置宽度高度
                    this.$parent.$emit('ready', this)
                })
            },
            canvasClick(e) {
                this.checkPosArr.push(this.getMousePos(this.$refs.canvas, e))
                // eslint-disable-next-line eqeqeq
                if (this.num == this.checkNum) {
                    this.num = this.createPoint(this.getMousePos(this.$refs.canvas, e))
                    // 按比例转换坐标值
                    this.checkPosArr = this.pointTransfrom(this.checkPosArr, this.setSize)
                    // 等创建坐标执行完
                    setTimeout(() => {
                        // var flag = this.comparePos(this.fontPos, this.checkPosArr);
                        // 发送后端请求
                        var captchaVerification = this.secretKey ? aesEncrypt(this.backToken + '---' + JSON.stringify(this.checkPosArr), this.secretKey) : this.backToken + '---' + JSON.stringify(this.checkPosArr)
                        const data = {
                            captchaType: this.captchaType,
                            'pointJson': this.secretKey ? aesEncrypt(JSON.stringify(this.checkPosArr), this.secretKey) : JSON.stringify(this.checkPosArr),
                            'token': this.backToken
                        }
                        reqCheck(data).then(res => {
                            // eslint-disable-next-line eqeqeq
                            if (res.repCode == '0000') {
                                this.barAreaColor = '#4cae4c'
                                this.barAreaBorderColor = '#5cb85c'
                                this.text = '验证成功'
                                this.bindingClick = false
                                // eslint-disable-next-line eqeqeq
                                if (this.mode == 'pop') {
                                    setTimeout(() => {
                                        this.$parent.clickShow = false
                                        this.refresh()
                                    }, 1500)
                                }
                                this.$parent.$emit('success', { captchaVerification })
                            } else {
                                this.$parent.$emit('error', this)
                                this.barAreaColor = '#d9534f'
                                this.barAreaBorderColor = '#d9534f'
                                this.text = '验证失败'
                                setTimeout(() => {
                                    this.refresh()
                                }, 700)
                            }
                        })
                    }, 400)
                }
                if (this.num < this.checkNum) {
                    this.num = this.createPoint(this.getMousePos(this.$refs.canvas, e))
                }
            },
            // 获取坐标
            getMousePos: function (obj, e) {
                var x = e.offsetX
                var y = e.offsetY
                return { x, y }
            },
            // 创建坐标点
            createPoint: function (pos) {
                this.tempPoints.push(Object.assign({}, pos))
                return ++this.num
            },
            refresh: function () {
                this.tempPoints.splice(0, this.tempPoints.length)
                this.barAreaColor = '#000'
                this.barAreaBorderColor = '#ddd'
                this.bindingClick = true
                this.fontPos.splice(0, this.fontPos.length)
                this.checkPosArr.splice(0, this.checkPosArr.length)
                this.num = 1
                this.getPictrue()
                this.text = '验证失败'
                this.showRefresh = true
            },
            // 请求背景图片和验证图片
            getPictrue() {
                const data = {
                    captchaType: this.captchaType
                }
                reqGet(data).then(res => {
                    // eslint-disable-next-line eqeqeq
                    if (res.repCode == '0000') {
                        this.pointBackImgBase = res.repData.originalImageBase64
                        this.backToken = res.repData.token
                        this.secretKey = res.repData.secretKey
                        this.poinTextList = res.repData.wordList
                        this.text = '请依次点击【' + this.poinTextList.join(',') + '】'
                    } else {
                        this.text = res.repMsg
                    }
                })
            },
            // 坐标转换函数
            pointTransfrom(pointArr, imgSize) {
                var newPointArr = pointArr.map(p => {
                    const x = Math.round(310 * p.x / parseInt(imgSize.imgWidth))
                    const y = Math.round(155 * p.y / parseInt(imgSize.imgHeight))
                    return { x, y }
                })
                // console.log(newPointArr,"newPointArr");
                return newPointArr
            }
        },
        watch: {
            // type变化则全面刷新
            type: {
                immediate: true,
                handler() {
                    this.init()
                }
            }
        },
        mounted() {
            // 禁止拖拽
            this.$el.onselectstart = function () {
                return false
            }
        }
    }
</script>
_web/src/components/verifition/Verify/VerifySlide.vue
New file
@@ -0,0 +1,374 @@
<template>
  <div style="position: relative;">
    <div
      :style="{height: (parseInt(setSize.imgHeight) + vSpace) + 'px'}"
      class="verify-img-out"
      v-if="type === '2'"
    >
      <div
        :style="{width: setSize.imgWidth,
                 height: setSize.imgHeight,}"
        class="verify-img-panel">
        <img :src="'data:image/png;base64,'+backImgBase" alt="" style="width:100%;height:100%;display:block">
        <div @click="refresh" class="verify-refresh" v-show="showRefresh"><i class="iconfont icon-refresh"></i>
        </div>
        <transition name="tips">
          <span :class="passFlag ?'suc-bg':'err-bg'" class="verify-tips" v-if="tipWords">{{ tipWords }}</span>
        </transition>
      </div>
    </div>
    <!-- 公共部分 -->
    <div
      :style="{width: setSize.imgWidth,
               height: barSize.height,
               'line-height':barSize.height}"
      class="verify-bar-area">
      <span class="verify-msg" v-text="text"></span>
      <div
        :style="{width: (leftBarWidth!==undefined)?leftBarWidth: barSize.height, height: barSize.height, 'border-color': leftBarBorderColor, transaction: transitionWidth}"
        class="verify-left-bar">
        <span class="verify-msg" v-text="finishText"></span>
        <div
          :style="{width: barSize.height, height: barSize.height, 'background-color': moveBlockBackgroundColor, left: moveBlockLeft, transition: transitionLeft}"
          @mousedown="start"
          @touchstart="start"
          class="verify-move-block">
          <i
            :class="['verify-icon iconfont', iconClass]"
            :style="{color: iconColor}"></i>
          <div
            :style="{'width':Math.floor(parseInt(setSize.imgWidth)*47/310)+ 'px',
                     'height': setSize.imgHeight,
                     'top':'-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
                     'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
            }"
            class="verify-sub-block"
            v-if="type === '2'">
            <img :src="'data:image/png;base64,'+blockBackImgBase" alt="" style="width:100%;height:100%;display:block">
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script type="text/babel">
    /**
     * VerifySlide
     * @description 滑块
     * */
    import { aesEncrypt } from './../utils/ase'
    import { resetSize } from './../utils/util'
    import { reqGet, reqCheck } from '@/api/modular/system/loginManage'
    //  "captchaType":"blockPuzzle",
    export default {
        name: 'VerifySlide',
        props: {
            // eslint-disable-next-line vue/require-default-prop
            captchaType: {
                type: String
            },
            type: {
                type: String,
                default: '1'
            },
            // 弹出式pop,固定fixed
            mode: {
                type: String,
                default: 'fixed'
            },
            vSpace: {
                type: Number,
                default: 5
            },
            explain: {
                type: String,
                default: '向右滑动完成验证'
            },
            imgSize: {
                type: Object,
                default() {
                    return {
                        width: '310px',
                        height: '155px'
                    }
                }
            },
            blockSize: {
                type: Object,
                default() {
                    return {
                        width: '50px',
                        height: '50px'
                    }
                }
            },
            barSize: {
                type: Object,
                default() {
                    return {
                        width: '310px',
                        height: '40px'
                    }
                }
            }
        },
        data() {
            return {
                secretKey: '', // 后端返回的加密秘钥 字段
                passFlag: '', // 是否通过的标识
                backImgBase: '', // 验证码背景图片
                blockBackImgBase: '', // 验证滑块的背景图片
                backToken: '', // 后端返回的唯一token值
                startMoveTime: '', // 移动开始的时间
                endMovetime: '', // 移动结束的时间
                tipsBackColor: '', // 提示词的背景颜色
                tipWords: '',
                text: '',
                finishText: '',
                setSize: {
                    imgHeight: 0,
                    imgWidth: 0,
                    barHeight: 0,
                    barWidth: 0
                },
                top: 0,
                left: 0,
                moveBlockLeft: undefined,
                leftBarWidth: undefined,
                // 移动中样式
                moveBlockBackgroundColor: undefined,
                leftBarBorderColor: '#ddd',
                iconColor: undefined,
                iconClass: 'icon-right',
                status: false, // 鼠标状态
                isEnd: false,        // 是够验证完成
                showRefresh: true,
                transitionLeft: '',
                transitionWidth: ''
            }
        },
        computed: {
            barArea() {
                return this.$el.querySelector('.verify-bar-area')
            },
            resetSize() {
                return resetSize
            }
        },
        methods: {
            init() {
                this.text = this.explain
                this.getPictrue()
                this.$nextTick(() => {
                    const setSize = this.resetSize(this)    // 重新设置宽度高度
                    for (const key in setSize) {
                        this.$set(this.setSize, key, setSize[key])
                    }
                    this.$parent.$emit('ready', this)
                })
                var _this = this
                window.removeEventListener('touchmove', function (e) {
                    _this.move(e)
                })
                window.removeEventListener('mousemove', function (e) {
                    _this.move(e)
                })
                // 鼠标松开
                window.removeEventListener('touchend', function () {
                    _this.end()
                })
                window.removeEventListener('mouseup', function () {
                    _this.end()
                })
                window.addEventListener('touchmove', function (e) {
                    _this.move(e)
                })
                window.addEventListener('mousemove', function (e) {
                    _this.move(e)
                })
                // 鼠标松开
                window.addEventListener('touchend', function () {
                    _this.end()
                })
                window.addEventListener('mouseup', function () {
                    _this.end()
                })
            },
            // 鼠标按下
            start: function (e) {
                e = e || window.event
                if (!e.touches) { // 兼容PC端
                    var x = e.clientX
                } else { // 兼容移动端
                    // eslint-disable-next-line no-redeclare
                    var x = e.touches[0].pageX
                }
                this.startLeft = Math.floor(x - this.barArea.getBoundingClientRect().left)
                this.startMoveTime = +new Date() // 开始滑动的时间
                // eslint-disable-next-line eqeqeq
                if (this.isEnd == false) {
                    this.text = ''
                    this.moveBlockBackgroundColor = '#337ab7'
                    this.leftBarBorderColor = '#337AB7'
                    this.iconColor = '#fff'
                    e.stopPropagation()
                    this.status = true
                }
            },
            // 鼠标移动
            move: function (e) {
                e = e || window.event
                // eslint-disable-next-line eqeqeq
                if (this.status && this.isEnd == false) {
                    if (!e.touches) { // 兼容PC端
                        var x = e.clientX
                    } else { // 兼容移动端
                        // eslint-disable-next-line no-redeclare
                        var x = e.touches[0].pageX
                    }
                    // eslint-disable-next-line camelcase
                    var bar_area_left = this.barArea.getBoundingClientRect().left
                    // eslint-disable-next-line camelcase
                    var move_block_left = x - bar_area_left // 小方块相对于父元素的left值
                    // eslint-disable-next-line camelcase
                    if (move_block_left >= this.barArea.offsetWidth - parseInt(parseInt(this.blockSize.width) / 2) - 2) {
                        // eslint-disable-next-line camelcase
                        move_block_left = this.barArea.offsetWidth - parseInt(parseInt(this.blockSize.width) / 2) - 2
                    }
                    // eslint-disable-next-line camelcase
                    if (move_block_left <= 0) {
                        // eslint-disable-next-line camelcase
                        move_block_left = parseInt(parseInt(this.blockSize.width) / 2)
                    }
                    // 拖动后小方块的left值
                    // eslint-disable-next-line camelcase
                    this.moveBlockLeft = (move_block_left - this.startLeft) + 'px'
                    // eslint-disable-next-line camelcase
                    this.leftBarWidth = (move_block_left - this.startLeft) + 'px'
                }
            },
            // 鼠标松开
            end: function () {
                this.endMovetime = +new Date()
                var _this = this
                // 判断是否重合
                // eslint-disable-next-line eqeqeq
                if (this.status && this.isEnd == false) {
                    var moveLeftDistance = parseInt((this.moveBlockLeft || '').replace('px', ''))
                    moveLeftDistance = moveLeftDistance * 310 / parseInt(this.setSize.imgWidth)
                    const data = {
                        captchaType: this.captchaType,
                        'pointJson': this.secretKey ? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), this.secretKey) : JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
                        'token': this.backToken
                    }
                    reqCheck(data).then(res => {
                        // eslint-disable-next-line eqeqeq
                        if (res.repCode == '0000') {
                            this.moveBlockBackgroundColor = '#5cb85c'
                            this.leftBarBorderColor = '#5cb85c'
                            this.iconColor = '#fff'
                            this.iconClass = 'icon-check'
                            this.showRefresh = false
                            this.isEnd = true
                            // eslint-disable-next-line eqeqeq
                            if (this.mode == 'pop') {
                                setTimeout(() => {
                                    this.$parent.clickShow = false
                                    this.refresh()
                                }, 1500)
                            }
                            this.passFlag = true
                            this.tipWords = `${((this.endMovetime - this.startMoveTime) / 1000).toFixed(2)}s验证成功`
                            var captchaVerification = this.secretKey ? aesEncrypt(this.backToken + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }), this.secretKey) : this.backToken + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 })
                            setTimeout(() => {
                                this.tipWords = ''
                                this.$parent.closeBox()
                                this.$parent.$emit('success', { captchaVerification })
                            }, 1000)
                        } else {
                            this.moveBlockBackgroundColor = '#d9534f'
                            this.leftBarBorderColor = '#d9534f'
                            this.iconColor = '#fff'
                            this.iconClass = 'icon-close'
                            this.passFlag = false
                            setTimeout(function () {
                                _this.refresh()
                            }, 1000)
                            this.$parent.$emit('error', this)
                            this.tipWords = '验证失败'
                            setTimeout(() => {
                                    this.tipWords = ''
                            }, 1000)
                        }
                    })
                    this.status = false
                }
            },
            refresh: function () {
                this.showRefresh = true
                this.finishText = ''
                this.transitionLeft = 'left .3s'
                this.moveBlockLeft = 0
                this.leftBarWidth = undefined
                this.transitionWidth = 'width .3s'
                this.leftBarBorderColor = '#ddd'
                this.moveBlockBackgroundColor = '#fff'
                this.iconColor = '#000'
                this.iconClass = 'icon-right'
                this.isEnd = false
                this.getPictrue()
                setTimeout(() => {
                    this.transitionWidth = ''
                    this.transitionLeft = ''
                    this.text = this.explain
                }, 300)
            },
            // 请求背景图片和验证图片
            getPictrue() {
                const data = {
                    captchaType: this.captchaType
                }
                reqGet(data).then(res => {
                    // eslint-disable-next-line eqeqeq
                    if (res.repCode == '0000') {
                        this.backImgBase = res.repData.originalImageBase64
                        this.blockBackImgBase = res.repData.jigsawImageBase64
                        this.backToken = res.repData.token
                        this.secretKey = res.repData.secretKey
                    } else {
                        this.tipWords = res.repMsg
                    }
                })
            }
        },
        watch: {
            // type变化则全面刷新
            type: {
                immediate: true,
                handler() {
                    this.init()
                }
            }
        },
        mounted() {
            // 禁止拖拽
            this.$el.onselectstart = function () {
                return false
            }
        }
    }
</script>
_web/src/components/verifition/utils/ase.js
New file
@@ -0,0 +1,11 @@
import CryptoJS from 'crypto-js'
/**
 * @word 要加密的内容
 * @keyWord String  服务器随机返回的关键字
 */
export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') {
  var key = CryptoJS.enc.Utf8.parse(keyWord)
  var srcs = CryptoJS.enc.Utf8.parse(word)
  var encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 })
  return encrypted.toString()
}
_web/src/components/verifition/utils/axios.js
New file
@@ -0,0 +1,30 @@
import axios from 'axios'
axios.defaults.baseURL = process.env.BASE_API
const service = axios.create({
  timeout: 40000,
  headers: {
    'X-Requested-With': 'XMLHttpRequest',
    'Content-Type': 'application/json; charset=UTF-8'
  }
})
service.interceptors.request.use(
  config => {
    return config
  },
  error => {
    Promise.reject(error)
  }
)
// response interceptor
service.interceptors.response.use(
  response => {
    const res = response.data
    return res
  },
  () => {
  }
)
export default service
_web/src/components/verifition/utils/util.js
New file
@@ -0,0 +1,52 @@
export function resetSize(vm) {
    // eslint-disable-next-line camelcase
    var img_width, img_height, bar_width, bar_height    // 图片的宽度、高度,移动条的宽度、高度
    var parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth
    var parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight
    // eslint-disable-next-line eqeqeq
    if (vm.imgSize.width.indexOf('%') != -1) {
        // eslint-disable-next-line camelcase
        img_width = parseInt(this.imgSize.width) / 100 * parentWidth + 'px'
    } else {
        // eslint-disable-next-line camelcase
        img_width = this.imgSize.width
    }
    // eslint-disable-next-line eqeqeq
    if (vm.imgSize.height.indexOf('%') != -1) {
        // eslint-disable-next-line camelcase
        img_height = parseInt(this.imgSize.height) / 100 * parentHeight + 'px'
    } else {
        // eslint-disable-next-line camelcase
        img_height = this.imgSize.height
    }
    // eslint-disable-next-line eqeqeq
    if (vm.barSize.width.indexOf('%') != -1) {
        // eslint-disable-next-line camelcase
        bar_width = parseInt(this.barSize.width) / 100 * parentWidth + 'px'
    } else {
        // eslint-disable-next-line camelcase
        bar_width = this.barSize.width
    }
    // eslint-disable-next-line eqeqeq
    if (vm.barSize.height.indexOf('%') != -1) {
        // eslint-disable-next-line camelcase
        bar_height = parseInt(this.barSize.height) / 100 * parentHeight + 'px'
    } else {
        // eslint-disable-next-line camelcase
        bar_height = this.barSize.height
    }
    return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height }
}
// eslint-disable-next-line camelcase
export const _code_chars = [1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
// eslint-disable-next-line camelcase
export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0']
// eslint-disable-next-line camelcase
export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC']
_web/src/components/xnComponents/EditorDiv.vue
New file
@@ -0,0 +1,95 @@
/* eslint-disable no-undef */
<!--onlyoffice 编辑器-->
<template>
  <div id="editorDiv"></div>
</template>
<script>
  import { handleDocType } from '@/utils/onlyofficeUtil'
  export default {
    name: 'Editor',
    props: {
      option: {
        type: Object,
        default: () => {
          return {}
        }
      }
    },
    data() {
      return {
        doctype: ''
      }
    },
    mounted() {
      if (this.option.url) {
        this.setEditor(this.option)
      }
    },
    methods: {
      setEditor(option) {
        this.doctype = handleDocType(option.fileType)
        const config = {
          document: {
            fileType: option.fileType,
            key: option.key,
            title: option.title,
            permissions: {
              comment: true,
              download: true,
              modifyContentControl: true,
              modifyFilter: true,
              print: false,
              edit: option.isEdit,
              fillForms: true
              // review: false
            },
            url: option.url
          },
          type: option.type,
          documentType: this.doctype,
          editorConfig: {
            callbackUrl: option.callbackUrl,
            lang: 'zh',
            customization: {
              commentAuthorOnly: false,
              comments: true,
              compactHeader: false,
              compactToolbar: true,
              feedback: false,
              plugins: true
            },
            user: {
              id: option.user.id,
              name: option.user.name
            }
            // mode: option.mode
          },
          width: '100%',
          height: '100%',
          position: 'absolute',
          token: option.token
        }
        // eslint-disable-next-line no-unused-vars
        let docEditor = null
        // eslint-disable-next-line no-undef
        docEditor = new DocsAPI.DocEditor('editorDiv', config)
      }
    },
    watch: {
      option: {
        handler: function (n, o) {
          this.setEditor(n)
          this.doctype = handleDocType(n.fileType)
        },
        deep: true
      }
    }
  }
</script>
<style scoped>
</style>
_web/src/components/xnComponents/XCard.vue
New file
@@ -0,0 +1,16 @@
<template>
  <a-card :bordered="false" :bodyStyle="tstyle">
    <slot name="content"></slot>
  </a-card>
</template>
<script>
    export default {
        name: 'XCard',
      data() {
        return {
          tstyle: { 'padding-bottom': '0px', 'margin-bottom': '10px' }
        }
      }
    }
</script>
_web/src/components/xnComponents/XDown.vue
New file
@@ -0,0 +1,54 @@
<template>
  <a-tooltip placement="top">
    <template slot="title">
      <span>导出所有数据</span>
    </template>
    <!-- 正常来说,这里加个插槽最好了,但是这个就是为导出准备的,一般这两个字不会变 -->
    <a-button type="dashed" @click="batchExport" :loading="batchExportLoading"><a-icon type="export"/>导出</a-button>
  </a-tooltip>
</template>
<script>
export default {
  name: 'XDown',
  data () {
    return {
      batchExportLoading: false
    }
  },
  methods: {
    /**
     * 本控件中点击按钮事件
     */
    batchExport () {
      this.batchExportLoading = true
      // 将其传达到上个界面
      this.$emit('batchExport', '')
    },
    /**
     * 通过返回的元素通过浏览器下载
     * 也就是接受使用这个组件的地方吧下载的内容传过来,
     * 但是这个组件本公子编写的时候只想的适用于导出excel来使用,下载文件呀图片方面的重新整个组件即可
     */
    downloadfile (res) {
      this.batchExportLoading = false
      var blob = new Blob([res.data], { type: 'application/octet-stream;charset=UTF-8' })
      var contentDisposition = res.headers['content-disposition']
      var patt = new RegExp('filename=([^;]+\\.[^\\.;]+);*')
      var result = patt.exec(contentDisposition)
      var filename = result[1]
      var downloadElement = document.createElement('a')
      var href = window.URL.createObjectURL(blob) // 创建下载的链接
      var reg = /^["](.*)["]$/g
      downloadElement.style.display = 'none'
      downloadElement.href = href
      downloadElement.download = decodeURI(filename.replace(reg, '$1')) // 下载后文件名
      document.body.appendChild(downloadElement)
      downloadElement.click() // 点击下载
      document.body.removeChild(downloadElement) // 下载完成移除元素
      window.URL.revokeObjectURL(href)
    }
  }
}
</script>
_web/src/config/defaultSettings.js
New file
@@ -0,0 +1,35 @@
/**
 * 项目默认配置项
 * primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage
 * navTheme - sidebar theme ['dark', 'light'] 两种主题
 * colorWeak - 色盲模式
 * layout - 整体布局方式 ['sidemenu', 'topmenu'] 两种布局
 * fixedHeader - 固定 Header : boolean
 * fixSiderbar - 固定左侧菜单栏 : boolean
 * autoHideHeader - 向下滚动时,隐藏 Header : boolean
 * contentWidth - 内容区布局: 流式 |  固定
 *
 * storageOptions: {} - Vue-ls 插件配置项 (localStorage/sessionStorage)
 * production: 变量暂先设定为 false,目的是各种环境都正常显示设置抽屉,真实环境请放开注释
 *
 *
 */
export default {
  primaryColor: '#1890ff', // primary color of ant design
  navTheme: 'dark', // theme for nav menu
  layout: 'sidemenu', // nav menu position: sidemenu or topmenu
  contentWidth: 'Fixed', // layout of content: Fluid or Fixed, only works when layout is topmenu
  fixedHeader: false, // sticky header
  fixSiderbar: false, // sticky siderbar
  autoHideHeader: false, //  auto hide header
  colorWeak: false,
  multiTab: false,
  production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true',
  // vue-ls options
  storageOptions: {
    namespace: 'pro__', // key prefix
    name: 'ls', // name variable Vue.[ls] or this.[$ls],
    storage: 'local' // storage name session, local, memory
  }
}
_web/src/config/router.config.js
New file
@@ -0,0 +1,85 @@
// eslint-disable-next-line
import { UserLayout, BasicLayout, RouteView, BlankLayout, PageView } from '@/layouts'
import { bxAnaalyse } from '@/core/icons'
export const asyncRouterMap = [
  {
    path: '/',
    name: 'MenuIndex.vue',
    component: BasicLayout,
    meta: { title: '首页' },
    redirect: '/dashboard/workplace',
    children: [
      // dashboard
      {
        path: 'dashboard',
        name: 'dashboard',
        redirect: '/dashboard/workplace',
        component: RouteView,
        // eslint-disable-next-line standard/object-curly-even-spacing
        meta: { title: '仪表盘', keepAlive: true, icon: bxAnaalyse /* permission: [ 'dashboard' ] */ },
        children: [
          {
            path: 'analysis/:pageNo([1-9]\\d*)?',
            name: 'Analysis',
            component: () => import('@/views/system/dashboard/Analysis'),
            // eslint-disable-next-line standard/object-curly-even-spacing
            meta: { title: '分析页', keepAlive: true /* permission: [ 'dashboard' ] */ }
          },
          {
            path: 'workplace',
            name: 'Workplace',
            component: () => import('@/views/system/dashboard/Workplace'),
            // eslint-disable-next-line standard/object-curly-even-spacing
            meta: { title: '工作台', keepAlive: false/*, permission: [ 'dashboard' ] */ }
          }
        ]
      }
    ]
  },
  {
    path: '*', redirect: '/404', hidden: true
  }
]
/**
 * 基础路由
 * @type { *[] }
 */
export const constantRouterMap = [
  {
    path: '/user',
    component: UserLayout,
    redirect: '/user/login',
    hidden: true,
    children: [
      {
        path: 'login',
        name: 'login',
        component: () => import(/* webpackChunkName: "user" */ '@/views/userLoginReg/Login')
      },
      {
        path: 'register',
        name: 'register',
        component: () => import(/* webpackChunkName: "user" */ '@/views/userLoginReg/Register')
      },
      {
        path: 'register-result',
        name: 'registerResult',
        component: () => import(/* webpackChunkName: "user" */ '@/views/userLoginReg/RegisterResult')
      },
      {
        path: 'recover',
        name: 'recover',
        component: undefined
      }
    ]
  },
  {
    path: '/404',
    component: () => import(/* webpackChunkName: "fail" */ '@/views/system/exception/404')
  }
]
_web/src/core/bootstrap.js
New file
@@ -0,0 +1,31 @@
import Vue from 'vue'
import store from '@/store/'
import {
  ACCESS_TOKEN,
  DEFAULT_COLOR,
  DEFAULT_THEME,
  DEFAULT_LAYOUT_MODE,
  DEFAULT_COLOR_WEAK,
  SIDEBAR_TYPE,
  DEFAULT_FIXED_HEADER,
  DEFAULT_FIXED_HEADER_HIDDEN,
  DEFAULT_FIXED_SIDEMENU,
  DEFAULT_CONTENT_WIDTH_TYPE,
  DEFAULT_MULTI_TAB
} from '@/store/mutation-types'
import config from '@/config/defaultSettings'
export default function Initializer () {
  store.commit('SET_SIDEBAR_TYPE', Vue.ls.get(SIDEBAR_TYPE, true))
  store.commit('TOGGLE_THEME', Vue.ls.get(DEFAULT_THEME, config.navTheme))
  store.commit('TOGGLE_LAYOUT_MODE', Vue.ls.get(DEFAULT_LAYOUT_MODE, config.layout))
  store.commit('TOGGLE_FIXED_HEADER', Vue.ls.get(DEFAULT_FIXED_HEADER, config.fixedHeader))
  store.commit('TOGGLE_FIXED_SIDERBAR', Vue.ls.get(DEFAULT_FIXED_SIDEMENU, config.fixSiderbar))
  store.commit('TOGGLE_CONTENT_WIDTH', Vue.ls.get(DEFAULT_CONTENT_WIDTH_TYPE, config.contentWidth))
  store.commit('TOGGLE_FIXED_HEADER_HIDDEN', Vue.ls.get(DEFAULT_FIXED_HEADER_HIDDEN, config.autoHideHeader))
  store.commit('TOGGLE_WEAK', Vue.ls.get(DEFAULT_COLOR_WEAK, config.colorWeak))
  store.commit('TOGGLE_COLOR', Vue.ls.get(DEFAULT_COLOR, config.primaryColor))
  store.commit('TOGGLE_MULTI_TAB', Vue.ls.get(DEFAULT_MULTI_TAB, config.multiTab))
  store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN))
}
_web/src/core/directives/action.js
New file
@@ -0,0 +1,34 @@
import Vue from 'vue'
import store from '@/store'
/**
 * Action 权限指令
 * 指令用法:
 *  - 在需要控制 action 级别权限的组件上使用 v-action:[method] , 如下:
 *    <i-button v-action:add >添加用户</a-button>
 *    <a-button v-action:delete>删除用户</a-button>
 *    <a v-action:edit @click="edit(record)">修改</a>
 *
 *  - 当前用户没有权限时,组件上使用了该指令则会被隐藏
 *  - 当后台权限跟 pro 提供的模式不同时,只需要针对这里的权限过滤进行修改即可
 *
 *  @see https://github.com/sendya/ant-design-pro-vue/pull/53
 */
const action = Vue.directive('action', {
  inserted: function (el, binding, vnode) {
    const actionName = binding.arg
    const roles = store.getters.roles
    const elVal = vnode.context.$route.meta.permission
    const permissionId = elVal instanceof String && [elVal] || elVal
    roles.permissions.forEach(p => {
      if (!permissionId.includes(p.permissionId)) {
        return
      }
      if (p.actionList && !p.actionList.includes(actionName)) {
        el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none')
      }
    })
  }
})
export default action
_web/src/core/icons.js
New file
@@ -0,0 +1,11 @@
/**
 * Custom icon list
 * All icons are loaded here for easy management
 * @see https://vue.ant.design/components/icon/#Custom-Font-Icon
 *
 * 自定义图标加载表
 * 所有图标均从这里加载,方便管理
 */
import bxAnaalyse from '@/assets/icons/bx-analyse.svg?inline' // path to your '*.svg?inline' file.
export { bxAnaalyse }
_web/src/core/lazy_lib/components_use.js
New file
@@ -0,0 +1,111 @@
/**
 * 该文件是为了按需加载,剔除掉了一些不需要的框架组件。
 * 减少了编译支持库包大小
 * @author yubaoshan
 * 当需要更多组件依赖时,在该文件加入即可
 */
import Vue from 'vue'
import {
  ConfigProvider,
  Layout,
  Input,
  InputNumber,
  Button,
  Switch,
  Radio,
  Checkbox,
  Select,
  Card,
  Form,
  Row,
  Col,
  Modal,
  Table,
  Tabs,
  Icon,
  Badge,
  Popover,
  Dropdown,
  List,
  Avatar,
  Breadcrumb,
  Steps,
  Spin,
  Menu,
  Drawer,
  Tooltip,
  Alert,
  Tag,
  Divider,
  DatePicker,
  TimePicker,
  Upload,
  Progress,
  Skeleton,
  Popconfirm,
  message,
  notification,
  TreeSelect,
  Tree,
  Transfer,
  Empty,
  PageHeader,
  Descriptions,
  Result
} from 'ant-design-vue'
// import VueCropper from 'vue-cropper'
Vue.use(ConfigProvider)
Vue.use(Layout)
Vue.use(Input)
Vue.use(InputNumber)
Vue.use(Button)
Vue.use(Switch)
Vue.use(Radio)
Vue.use(Checkbox)
Vue.use(Select)
Vue.use(Card)
Vue.use(Form)
Vue.use(Row)
Vue.use(Col)
Vue.use(Modal)
Vue.use(Table)
Vue.use(Tabs)
Vue.use(Icon)
Vue.use(Badge)
Vue.use(Popover)
Vue.use(Dropdown)
Vue.use(List)
Vue.use(Avatar)
Vue.use(Breadcrumb)
Vue.use(Steps)
Vue.use(Spin)
Vue.use(Menu)
Vue.use(Drawer)
Vue.use(Tooltip)
Vue.use(Alert)
Vue.use(Tag)
Vue.use(Divider)
Vue.use(DatePicker)
Vue.use(TimePicker)
Vue.use(Upload)
Vue.use(Progress)
Vue.use(Skeleton)
Vue.use(Popconfirm)
// Vue.use(VueCropper)
Vue.use(notification)
Vue.use(TreeSelect)
Vue.use(Tree)
Vue.use(Transfer)
Vue.use(Empty)
Vue.use(PageHeader)
Vue.use(Descriptions)
Vue.use(Result)
Vue.prototype.$confirm = Modal.confirm
Vue.prototype.$message = message
Vue.prototype.$notification = notification
Vue.prototype.$info = Modal.info
Vue.prototype.$success = Modal.success
Vue.prototype.$error = Modal.error
Vue.prototype.$warning = Modal.warning
_web/src/core/lazy_use.js
New file
@@ -0,0 +1,27 @@
import Vue from 'vue'
import VueStorage from 'vue-ls'
import config from '@/config/defaultSettings'
// base library
import '@/core/lazy_lib/components_use'
import Viser from 'viser-vue'
// ext library
import VueClipboard from 'vue-clipboard2'
import VueCropper from 'vue-cropper'
import MultiTab from '@/components/MultiTab'
import PageLoading from '@/components/PageLoading'
import PermissionHelper from '@/utils/helper/permission'
import './directives/action'
VueClipboard.config.autoSetContainer = true
Vue.use(Viser)
Vue.use(MultiTab)
Vue.use(PageLoading)
Vue.use(VueStorage, config.storageOptions)
Vue.use(VueClipboard)
Vue.use(PermissionHelper)
Vue.use(VueCropper)
process.env.NODE_ENV !== 'production' && console.warn('[antd-pro] NOTICE: Antd use lazy-load.')
_web/src/core/use.js
New file
@@ -0,0 +1,30 @@
import Vue from 'vue'
import VueStorage from 'vue-ls'
import config from '@/config/defaultSettings'
// base library
import Antd from 'ant-design-vue'
import Viser from 'viser-vue'
import VueCropper from 'vue-cropper'
import 'ant-design-vue/dist/antd.less'
// ext library
import VueClipboard from 'vue-clipboard2'
import MultiTab from '@/components/MultiTab'
import PageLoading from '@/components/PageLoading'
import PermissionHelper from '@/utils/helper/permission'
// import '@/components/use'
import './directives/action'
VueClipboard.config.autoSetContainer = true
Vue.use(Antd)
Vue.use(Viser)
Vue.use(MultiTab)
Vue.use(PageLoading)
Vue.use(VueStorage, config.storageOptions)
Vue.use(VueClipboard)
Vue.use(PermissionHelper)
Vue.use(VueCropper)
process.env.NODE_ENV !== 'production' && console.warn('[antd-pro] WARNING: Antd now use fulled imported.')
_web/src/layouts/BasicLayout.vue
New file
@@ -0,0 +1,186 @@
<template>
  <a-layout :class="['layout', device]">
    <!-- SideMenu -->
    <a-drawer
      v-if="isMobile()"
      placement="left"
      :wrapClassName="`drawer-sider ${navTheme}`"
      :closable="false"
      :visible="collapsed"
      @close="drawerClose"
    >
      <side-menu
        mode="inline"
        :menus="menus"
        :theme="navTheme"
        :collapsed="false"
        :collapsible="true"
        @menuSelect="menuSelect"
      ></side-menu>
    </a-drawer>
    <side-menu
      v-else-if="isSideMenu()"
      mode="inline"
      :menus="menus"
      :theme="navTheme"
      :collapsed="collapsed"
      :collapsible="true"
    ></side-menu>
    <a-layout :class="[layoutMode, `content-width-${contentWidth}`]" :style="{ paddingLeft: contentPaddingLeft, minHeight: '100vh' }">
      <!-- layout header -->
      <global-header
        :mode="layoutMode"
        :menus="menus"
        :theme="navTheme"
        :collapsed="collapsed"
        :device="device"
        @toggle="toggle"
      />
      <!-- layout content -->
      <a-layout-content :style="{ height: '100%', margin: '24px 24px 0', paddingTop: fixedHeader ? '55px' : '0' }">
        <multi-tab v-if="multiTab"></multi-tab>
        <transition name="page-transition">
          <route-view />
        </transition>
      </a-layout-content>
      <!-- layout footer -->
      <a-layout-footer>
        <global-footer />
      </a-layout-footer>
      <!-- Setting Drawer (show in development mode) -->
      <setting-drawer v-if="!production"></setting-drawer>
    </a-layout>
  </a-layout>
</template>
<script>
import { triggerWindowResizeEvent } from '@/utils/util'
import { mapState, mapActions } from 'vuex'
import { mixin, mixinDevice } from '@/utils/mixin'
import config from '@/config/defaultSettings'
import RouteView from './RouteView'
import SideMenu from '@/components/Menu/SideMenu'
import GlobalHeader from '@/components/GlobalHeader'
import GlobalFooter from '@/components/GlobalFooter'
import SettingDrawer from '@/components/SettingDrawer'
import { convertRoutes } from '@/utils/routeConvert'
export default {
  name: 'BasicLayout',
  mixins: [mixin, mixinDevice],
  components: {
    RouteView,
    SideMenu,
    GlobalHeader,
    GlobalFooter,
    SettingDrawer
  },
  data () {
    return {
      production: config.production,
      collapsed: false,
      menus: []
    }
  },
  computed: {
    ...mapState({
      // 动态主路由
      mainMenu: state => state.permission.addRouters
    }),
    contentPaddingLeft () {
      if (!this.fixSidebar || this.isMobile()) {
        return '0'
      }
      if (this.sidebarOpened) {
        return '230px'
      }
      return '80px'
    }
  },
  watch: {
    sidebarOpened (val) {
      this.collapsed = !val
    },
    // 菜单变化
    mainMenu (val) {
      this.setMenus()
    }
  },
  created () {
    this.setMenus()
    /* const routes = convertRoutes(this.mainMenu.find(item => item.path === '/'))
    this.menus = (routes && routes.children) || [] */
    this.collapsed = !this.sidebarOpened
  },
  mounted () {
    const userAgent = navigator.userAgent
    if (userAgent.indexOf('Edge') > -1) {
      this.$nextTick(() => {
        this.collapsed = !this.collapsed
        setTimeout(() => {
          this.collapsed = !this.collapsed
        }, 16)
      })
    }
  },
  methods: {
    ...mapActions(['setSidebar']),
    // 重新生成
    setMenus () {
      const routes = convertRoutes(this.mainMenu.find(item => item.path === '/'))
      this.menus = (routes && routes.children) || []
    },
    toggle () {
      this.collapsed = !this.collapsed
      this.setSidebar(!this.collapsed)
      triggerWindowResizeEvent()
    },
    paddingCalc () {
      let left = ''
      if (this.sidebarOpened) {
        left = this.isDesktop() ? '230px' : '80px'
      } else {
        left = (this.isMobile() && '0') || ((this.fixSidebar && '80px') || '0')
      }
      return left
    },
    menuSelect () {
    },
    drawerClose () {
      this.collapsed = false
    }
  }
}
</script>
<style lang="less">
/*
 * The following styles are auto-applied to elements with
 * transition="page-transition" when their visibility is toggled
 * by Vue.js.
 *
 * You can easily play with the page transition by editing
 * these styles.
 */
.page-transition-enter {
  opacity: 0;
}
.page-transition-leave-active {
  opacity: 0;
}
.page-transition-enter .page-transition-container,
.page-transition-leave-active .page-transition-container {
  -webkit-transform: scale(1.1);
  transform: scale(1.1);
}
</style>
_web/src/layouts/BlankLayout.vue
New file
@@ -0,0 +1,16 @@
<template>
  <div>
    <router-view />
  </div>
</template>
<script>
export default {
  name: 'BlankLayout'
}
</script>
<style scoped>
</style>
_web/src/layouts/Iframe.vue
New file
@@ -0,0 +1,29 @@
<script>
  import Vue from 'vue'
  import { ACCESS_TOKEN } from '@/store/mutation-types'
    export default {
      name: 'Iframe',
      data () {
          return {
          }
      },
      render () {
        const { $route: { meta: { link } } } = this
        if ({ link }.link === '') {
           return '404'
        }
        let url = ''
        if ({ link }.link.indexOf('token=') > -1) {
          url = { link }.link + Vue.ls.get(ACCESS_TOKEN)
        } else {
          url = { link }.link
        }
        let height = ''
        const deviceHeight = document.documentElement.clientHeight
        height = (Number(deviceHeight) - 260) + 'px'
        return <iframe id="iframe" height={height} src={url} style="width:100%;overflow:hidden;" frameBorder="0"></iframe>
      }
   }
</script>
_web/src/layouts/PageView.vue
New file
@@ -0,0 +1,177 @@
<template>
  <div :style="!$route.meta.hiddenHeaderContent ? 'margin: -24px -24px 0px;' : null">
    <!-- pageHeader , route meta :true on hide -->
    <page-header v-if="!$route.meta.hiddenHeaderContent" :title="pageTitle" :logo="logo" :avatar="avatar">
      <slot slot="action" name="action"></slot>
      <slot slot="content" name="headerContent"></slot>
      <div slot="content" v-if="!this.$slots.headerContent && description">
        <p style="font-size: 14px;color: rgba(0,0,0,.65)">{{ description }}</p>
        <div class="link">
        </div>
      </div>
      <slot slot="extra" name="extra">
        <div class="extra-img">
          <img v-if="typeof extraImage !== 'undefined'" :src="extraImage"/>
        </div>
      </slot>
      <div slot="pageMenu">
        <div class="page-menu-search" v-if="search">
          <a-input-search
            style="width: 80%; max-width: 522px;"
            placeholder="请输入..."
            size="large"
            enterButton="搜索"
          />
        </div>
        <div class="page-menu-tabs" v-if="tabs && tabs.items">
          <!-- @change="callback" :activeKey="activeKey" -->
          <a-tabs :tabBarStyle="{margin: 0}" :activeKey="tabs.active()" @change="tabs.callback">
            <a-tab-pane v-for="item in tabs.items" :tab="item.title" :key="item.key"></a-tab-pane>
          </a-tabs>
        </div>
      </div>
    </page-header>
    <div class="content">
      <div class="page-header-index-wide">
        <slot>
          <!-- keep-alive  -->
          <keep-alive v-if="multiTab">
            <router-view ref="content" />
          </keep-alive>
          <router-view v-else ref="content" style="margin: -12px -14px 0;"/>
        </slot>
      </div>
    </div>
  </div>
</template>
<script>
import { mapState } from 'vuex'
import PageHeader from '@/components/PageHeader'
export default {
  name: 'PageView',
  components: {
    PageHeader
  },
  props: {
    avatar: {
      type: String,
      default: null
    },
    title: {
      type: [String, Boolean],
      default: true
    },
    logo: {
      type: String,
      default: null
    },
    directTabs: {
      type: Object,
      default: null
    }
  },
  data () {
    return {
      pageTitle: null,
      description: null,
      linkList: [],
      extraImage: '',
      search: false,
      tabs: {}
    }
  },
  computed: {
    ...mapState({
      multiTab: state => state.app.multiTab
    })
  },
  mounted () {
    this.tabs = this.directTabs
    this.getPageMeta()
  },
  updated () {
    this.getPageMeta()
  },
  methods: {
    getPageMeta () {
      // eslint-disable-next-line
      //文字样式//(typeof(this.title) === 'string' || !this.title) ? this.title : this.$route.meta.title
      // 为了简洁改为无
      this.pageTitle = ''
      const content = this.$refs.content
      if (content) {
        if (content.pageMeta) {
          Object.assign(this, content.pageMeta)
        } else {
          this.description = content.description
          this.linkList = content.linkList
          this.extraImage = content.extraImage
          this.search = content.search === true
          this.tabs = content.tabs
        }
      }
    }
  }
}
</script>
<style lang="less" scoped>
  .content {
    margin: 24px 24px 0;
    .link {
      margin-top: 16px;
      &:not(:empty) {
        margin-bottom: 16px;
      }
      a {
        margin-right: 32px;
        height: 24px;
        line-height: 24px;
        display: inline-block;
        i {
          font-size: 24px;
          margin-right: 8px;
          vertical-align: middle;
        }
        span {
          height: 24px;
          line-height: 24px;
          display: inline-block;
          vertical-align: middle;
        }
      }
    }
  }
  .page-menu-search {
    text-align: center;
    margin-bottom: 16px;
  }
  .page-menu-tabs {
    margin-top: 48px;
  }
  .extra-img {
    margin-top: -60px;
    text-align: center;
    width: 195px;
    img {
      width: 100%;
    }
  }
  .mobile {
    .extra-img{
      margin-top: 0;
      text-align: center;
      width: 96px;
      img{
        width: 100%;
      }
    }
  }
</style>
_web/src/layouts/RouteView.vue
New file
@@ -0,0 +1,32 @@
<script>
export default {
  name: 'RouteView',
  props: {
    keepAlive: {
      type: Boolean,
      default: true
    }
  },
  data () {
    return {}
  },
  render () {
    const { $route: { meta }, $store: { getters } } = this
    const inKeep = (
      <keep-alive>
        <router-view />
      </keep-alive>
    )
    const notKeep = (
      <router-view />
    )
    // 这里增加了 multiTab 的判断,当开启了 multiTab 时
    // 应当全部组件皆缓存,否则会导致切换页面后页面还原成原始状态
    // 若确实不需要,可改为 return meta.keepAlive ? inKeep : notKeep
    if (!getters.multiTab && !meta.keepAlive) {
      return notKeep
    }
    return this.keepAlive || getters.multiTab || meta.keepAlive ? inKeep : notKeep
  }
}
</script>
_web/src/layouts/UserLayout.vue
New file
@@ -0,0 +1,153 @@
<template>
  <div id="userLayout" :class="['user-layout-wrapper', device]">
    <div class="container">
      <div class="top">
        <div class="header">
          <a href="/">
            <img src="~@/assets/logo.png" class="logo" alt="logo">
            <span class="title">Snowy快速开发平台</span>
          </a>
        </div>
        <div class="desc">
        </div>
      </div>
      <route-view></route-view>
      <div class="footer">
        <div class="links">
          <a href="_self">帮助</a>
          <a href="_self">隐私</a>
          <a href="_self">条款</a>
        </div>
        <div class="copyright">
          Copyright © 2020 <a target="_blank" href="https://www.xiaonuo.vip/">小诺开源技术</a> All rights reserved. Snowy 1.8
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import RouteView from './RouteView'
import { mixinDevice } from '@/utils/mixin'
export default {
  name: 'UserLayout',
  components: { RouteView },
  mixins: [mixinDevice],
  data () {
    return {}
  },
  mounted () {
    document.body.classList.add('userLayout')
  },
  beforeDestroy () {
    document.body.classList.remove('userLayout')
  }
}
</script>
<style lang="less" scoped>
  #userLayout.user-layout-wrapper {
    height: 100%;
    &.mobile {
      .container {
        .main {
          max-width: 368px;
          width: 98%;
        }
      }
    }
    .container {
      width: 100%;
      min-height: 100%;
      background-color: #ffffff;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='800' viewBox='0 0 200 200'%3E%3Cdefs%3E%3ClinearGradient id='a' gradientUnits='userSpaceOnUse' x1='88' y1='88' x2='0' y2='0'%3E%3Cstop offset='0' stop-color='%23064e77'/%3E%3Cstop offset='1' stop-color='%230a7dbe'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' gradientUnits='userSpaceOnUse' x1='75' y1='76' x2='168' y2='160'%3E%3Cstop offset='0' stop-color='%238f8f8f'/%3E%3Cstop offset='0.09' stop-color='%23b3b3b3'/%3E%3Cstop offset='0.18' stop-color='%23c9c9c9'/%3E%3Cstop offset='0.31' stop-color='%23dbdbdb'/%3E%3Cstop offset='0.44' stop-color='%23e8e8e8'/%3E%3Cstop offset='0.59' stop-color='%23f2f2f2'/%3E%3Cstop offset='0.75' stop-color='%23fafafa'/%3E%3Cstop offset='1' stop-color='%23FFFFFF'/%3E%3C/linearGradient%3E%3Cfilter id='c' x='0' y='0' width='200%25' height='200%25'%3E%3CfeGaussianBlur in='SourceGraphic' stdDeviation='12' /%3E%3C/filter%3E%3C/defs%3E%3Cpolygon fill='url(%23a)' points='0 174 0 0 174 0'/%3E%3Cpath fill='%23000' fill-opacity='.5' filter='url(%23c)' d='M121.8 174C59.2 153.1 0 174 0 174s63.5-73.8 87-94c24.4-20.9 87-80 87-80S107.9 104.4 121.8 174z'/%3E%3Cpath fill='url(%23b)' d='M142.7 142.7C59.2 142.7 0 174 0 174s42-66.3 74.9-99.3S174 0 174 0S142.7 62.6 142.7 142.7z'/%3E%3C/svg%3E");
      background-attachment: fixed;
      background-repeat: no-repeat;
      background-position: top left;
      padding: 110px 0 144px;
      position: relative;
      a {
        text-decoration: none;
      }
      .top {
        text-align: center;
        .header {
          height: 44px;
          line-height: 44px;
          .badge {
            position: absolute;
            display: inline-block;
            line-height: 1;
            vertical-align: middle;
            margin-left: -12px;
            margin-top: -10px;
            opacity: 0.8;
          }
          .logo {
            height: 44px;
            vertical-align: top;
            margin-right: 16px;
            border-style: none;
          }
          .title {
            font-size: 33px;
            color: rgba(0, 0, 0, .85);
            font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
            font-weight: 600;
            position: relative;
            top: 2px;
          }
        }
        .desc {
          font-size: 14px;
          color: rgba(0, 0, 0, 0.45);
          margin-top: 12px;
          margin-bottom: 40px;
        }
      }
      .main {
        min-width: 260px;
        width: 368px;
        margin: 0 auto;
      }
      .footer {
        position: absolute;
        width: 100%;
        bottom: 0;
        padding: 0 16px;
        margin: 48px 0 24px;
        text-align: center;
        .links {
          margin-bottom: 8px;
          font-size: 14px;
          a {
            color: rgba(0, 0, 0, 0.45);
            transition: all 0.3s;
            &:not(:last-child) {
              margin-right: 40px;
            }
          }
        }
        .copyright {
          color: rgba(0, 0, 0, 0.45);
          font-size: 14px;
        }
      }
    }
  }
</style>
_web/src/layouts/index.js
New file
@@ -0,0 +1,8 @@
import UserLayout from './UserLayout'
import BlankLayout from './BlankLayout'
import BasicLayout from './BasicLayout'
import RouteView from './RouteView'
import PageView from './PageView'
import Iframe from './Iframe'
export { UserLayout, BasicLayout, BlankLayout, RouteView, PageView, Iframe }
_web/src/main.js
New file
@@ -0,0 +1,28 @@
import 'core-js/stable'
import 'regenerator-runtime/runtime'
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store/'
import { VueAxios } from './utils/request'
import bootstrap from './core/bootstrap'
import './core/lazy_use'
import './permission'
import './utils/filter'
import './components/global.less'
import { Dialog } from '@/components'
import { hasBtnPermission } from './utils/permissions'
import { sysApplication } from './utils/applocation'
Vue.use(VueAxios)
Vue.use(Dialog)
Vue.prototype.hasPerm = hasBtnPermission
Vue.prototype.applocation = sysApplication
Vue.config.productionTip = false
new Vue({
  router,
  store,
  created: bootstrap,
  render: h => h(App)
}).$mount('#app')
_web/src/permission.js
New file
@@ -0,0 +1,113 @@
import Vue from 'vue'
import router from './router'
import store from './store'
import NProgress from 'nprogress' // progress bar
import '@/components/NProgress/nprogress.less' // progress bar custom style
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
import { ACCESS_TOKEN, ALL_APPS_MENU } from '@/store/mutation-types'
import { Modal, notification } from 'ant-design-vue' // NProgress Configuration
import { timeFix } from '@/utils/util'/// es/notification
NProgress.configure({ showSpinner: false })
const whiteList = ['login', 'register', 'registerResult'] // no redirect whitelist
// 无默认首页的情况
const defaultRoutePath = '/welcome'
router.beforeEach((to, from, next) => {
  NProgress.start() // start progress bar
  to.meta && (typeof to.meta.title !== 'undefined' && setDocumentTitle(`${to.meta.title} - ${domTitle}`))
  if (Vue.ls.get(ACCESS_TOKEN)) {
    /* has token */
    if (to.path === '/user/login') {
      next({ path: defaultRoutePath })
      NProgress.done()
    } else {
      if (store.getters.roles.length === 0) {
        store
          .dispatch('GetInfo')
          .then(res => {
            if (res.menus.length < 1) {
              Modal.error({
                title: '提示:',
                content: '无菜单权限,请联系管理员',
                okText: '确定',
                onOk: () => {
                  store.dispatch('Logout').then(() => {
                    window.location.reload()
                  })
                }
              })
              return
            }
            // eslint-disable-next-line camelcase
            const all_app_menu = Vue.ls.get(ALL_APPS_MENU)
            let antDesignmenus
            // eslint-disable-next-line camelcase
            if (all_app_menu == null) {
              const applocation = []
              res.apps.forEach(item => {
                const apps = { 'code': '', 'name': '', 'active': '', 'menu': '' }
                if (item.active) {
                  apps.code = item.code
                  apps.name = item.name
                  apps.active = item.active
                  apps.menu = res.menus
                  antDesignmenus = res.menus
                } else {
                  apps.code = item.code
                  apps.name = item.name
                  apps.active = item.active
                  apps.menu = ''
                }
                applocation.push(apps)
              })
              Vue.ls.set(ALL_APPS_MENU, applocation, 7 * 24 * 60 * 60 * 1000)
              // 延迟 1 秒显示欢迎信息
              setTimeout(() => {
                notification.success({
                  message: '欢迎',
                  description: `${timeFix()},欢迎回来`
                })
              }, 1000)
            } else {
              antDesignmenus = Vue.ls.get(ALL_APPS_MENU)[0].menu
            }
            store.dispatch('GenerateRoutes', { antDesignmenus }).then(() => {
              // 动态添加可访问路由表
              router.addRoutes(store.getters.addRouters)
              // 请求带有 redirect 重定向时,登录自动重定向到该地址
              const redirect = decodeURIComponent(from.query.redirect || to.path)
              if (to.path === redirect) {
                next({ path: redirect })
                // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
                next({ ...to, replace: true })
              } else {
                // 跳转到目的路由
                next({ path: redirect })
              }
            })
          })
          .catch(() => {
            store.dispatch('Logout').then(() => {
              next({ path: '/user/login', query: { redirect: to.fullPath } })
            })
          })
      } else {
        next()
      }
    }
  } else {
    if (whiteList.includes(to.name)) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next({ path: '/user/login', query: { redirect: to.fullPath } })
      NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
    }
  }
})
router.afterEach(() => {
  NProgress.done() // finish progress bar
})
_web/src/router/generator-routers.js
New file
@@ -0,0 +1,261 @@
import { BasicLayout, BlankLayout, PageView, RouteView, Iframe } from '@/layouts'
// 前端路由表
const constantRouterComponents = {
  // 基础页面 layout 必须引入
  BasicLayout: BasicLayout,
  BlankLayout: BlankLayout,
  RouteView: RouteView,
  PageView: PageView,
  Iframe: Iframe,
  '403': () => import('@/views/system/exception/403'),
  '404': () => import('@/views/system/exception/404'),
  '500': () => import('@/views/system/exception/500'),
  'Workplace': () => import('@/views/system/dashboard/Workplace'),
  // account
  'AccountCenter': () => import('@/views/system/account/center/Index'),
  'AccountSettings': () => import('@/views/system/account/settings/Index'),
  'BaseSettings': () => import('@/views/system/account/settings/BaseSetting'),
  'SecuritySettings': () => import('@/views/system/account/settings/Security'),
  'CustomSettings': () => import('@/views/system/account/settings/Custom'),
  'BindingSettings': () => import('@/views/system/account/settings/Binding'),
  'NotificationSettings': () => import('@/views/system/account/settings/Notification'),
  // 默认首页
  'Console': () => import('@/views/system/index/welcome')
}
// 前端未找到页面路由(固定不用改)、原来为 /404
const notFoundRouter = {
  path: '*', redirect: '/welcome', hidden: true
}
// 个人中心页面
const userAccount = [
  // account
  {
    'name': 'account',
    'pid': 0,
    'id': 10028,
    'meta': {
      'title': '个人页',
      'icon': 'user',
      'show': false
    },
    'redirect': '/account/center',
    'component': 'RouteView'
  },
  {
    'name': 'center',
    'pid': 10028,
    'id': 10029,
    'meta': {
      'title': '个人中心',
      'show': false
    },
    'component': 'AccountCenter'
  },
  // 特殊三级菜单
  {
    'name': 'settings',
    'pid': '10028',
    'id': '10030',
    'meta': {
      'title': '个人设置',
      'hideHeader': true,
      'hideChildren': true,
      'show': false
    },
    'redirect': '/account/settings/base',
    'component': 'AccountSettings'
  },
  {
    'name': 'BaseSettings',
    'path': '/account/settings/base',
    'pid': 10030,
    'id': 10031,
    'meta': {
      'title': '基本设置',
      'show': false
    },
    'component': 'BaseSettings'
  },
  {
    'name': 'SecuritySettings',
    'path': '/account/settings/security',
    'pid': 10030,
    'id': 10032,
    'meta': {
      'title': '安全设置',
      'show': false
    },
    'component': 'SecuritySettings'
  },
  {
    'name': 'CustomSettings',
    'path': '/account/settings/custom',
    'pid': 10030,
    'id': 10033,
    'meta': {
      'title': '个性化设置',
      'show': false
    },
    'component': 'CustomSettings'
  },
  {
    'name': 'BindingSettings',
    'path': '/account/settings/binding',
    'pid': 10030,
    'id': 10034,
    'meta': {
      'title': '账户绑定',
      'show': false
    },
    'component': 'BindingSettings'
  },
  {
    'name': 'NotificationSettings',
    'path': '/account/settings/notification',
    'pid': 10030,
    'id': 10034,
    'meta': {
      'title': '新消息通知',
      'show': false
    },
    'component': 'NotificationSettings'
  },
  {
    'name': 'Console',
    'path': '/welcome',
    'pid': 0,
    'id': 183183,
    'meta': {
      'title': '首页',
      'show': false
    },
    'component': 'Console'
  }
]
// 根级菜单
const rootRouter = {
  key: '',
  name: 'MenuIndex.vue',
  path: '',
  component: 'BasicLayout',
  redirect: '/welcome',
  meta: {
    title: '首页'
  },
  children: []
}
/**
 * 动态生成菜单
 * @param data
 * @returns {Promise<Router>}
 */
export const generatorDynamicRouter = (data) => {
  return new Promise((resolve, reject) => {
    const resNav = data.antDesignmenus
    const menuNav = []
    const childrenNav = []
    //      后端数据, 根级树数组,  根级 PID
    listToTree(resNav, childrenNav, 0)
    /**
     * 增加静态网页
     */
    listToTree(userAccount, childrenNav, 0)
    rootRouter.children = childrenNav
    menuNav.push(rootRouter)
    const routers = generator(menuNav)
    routers.push(notFoundRouter)
    resolve(routers)
  }).catch(err => {
    // reject('加载菜单失败')
    return Promise.reject(err)
  })
}
/**
 * 格式化树形结构数据 生成 vue-router 层级路由表
 *
 * @param routerMap
 * @param parent
 * @returns {*}
 */
export const generator = (routerMap, parent) => {
  return routerMap.map(item => {
    // eslint-disable-next-line no-unused-vars
    const { title, show, hideChildren, hiddenHeaderContent, target, icon, link } = item.meta || {}
    const currentRouter = {
      // 如果路由设置了 path,则作为默认 path,否则 路由地址 动态拼接生成如 /dashboard/workplace
      path: item.path || `${parent && parent.path || ''}/${item.key}`,
      // 路由名称,建议唯一
      name: item.name || item.key || '',
      // 该路由对应页面的 组件 :方案1
      // component: constantRouterComponents[item.component || item.key],
      // 该路由对应页面的 组件 :方案2 (动态加载)
      component: (constantRouterComponents[item.component || item.key]) || (() => import(`@/views/${item.component}`)),
      // meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
      meta: {
        title: title,
        icon: icon || undefined,
        // hiddenHeaderContent: hiddenHeaderContent,
        target: target,
        link: link
      }
    }
    // 是否设置了隐藏菜单
    if (show === false) {
      currentRouter.hidden = true
    }
    // 是否设置了隐藏子菜单
    if (hideChildren) {
      currentRouter.hideChildrenInMenu = true
    }
    // 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠
    if (!currentRouter.path.startsWith('http')) {
      currentRouter.path = currentRouter.path.replace('//', '/')
    }
    // 重定向
    item.redirect && (currentRouter.redirect = item.redirect)
    // 是否有子菜单,并递归处理
    if (item.children && item.children.length > 0) {
      // Recursion
      currentRouter.children = generator(item.children, currentRouter)
    }
    return currentRouter
  })
}
/**
 * 数组转树形结构
 * @param list 源数组
 * @param tree 树
 * @param parentId 父ID
 */
const listToTree = (list, tree, parentId) => {
  list.forEach(item => {
    // 判断是否为父级菜单
    // eslint-disable-next-line eqeqeq
    if (item.pid == parentId) {
      const child = {
        ...item,
        key: item.key || item.name,
        children: []
      }
      // 迭代 list, 找到当前菜单相符合的所有子菜单
      listToTree(list, child.children, item.id)
      // 删掉不存在 children 值的属性
      if (child.children.length <= 0) {
        delete child.children
      }
      // 加入到树中
      tree.push(child)
    }
  })
}
_web/src/router/index.js
New file
@@ -0,0 +1,19 @@
import Vue from 'vue'
import Router from 'vue-router'
import { constantRouterMap } from '@/config/router.config'
// hack router push callback
const originalPush = Router.prototype.push
Router.prototype.push = function push (location, onResolve, onReject) {
  if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
  return originalPush.call(this, location).catch(err => err)
}
Vue.use(Router)
export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRouterMap
})
_web/src/store/getters.js
New file
@@ -0,0 +1,18 @@
const getters = {
  device: state => state.app.device,
  theme: state => state.app.theme,
  color: state => state.app.color,
  token: state => state.user.token,
  avatar: state => state.user.avatar,
  nickname: state => state.user.name,
  welcome: state => state.user.welcome,
  roles: state => state.user.roles,
  buttons: state => state.user.buttons,
  admintype: state => state.user.admintype,
  userInfo: state => state.user.info,
  addRouters: state => state.permission.addRouters,
  multiTab: state => state.app.multiTab,
  lang: state => state.i18n.lang
}
export default getters
_web/src/store/index.js
New file
@@ -0,0 +1,32 @@
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import user from './modules/user'
// default router permission control
// import permission from './modules/permission'
// dynamic router permission control (Experimental)
import permission from './modules/async-router'
import getters from './getters'
Vue.use(Vuex)
export default new Vuex.Store({
  modules: {
    app,
    user,
    permission
  },
  state: {
  },
  mutations: {
  },
  actions: {
  },
  getters
})
_web/src/store/modules/app.js
New file
@@ -0,0 +1,129 @@
import Vue from 'vue'
import {
  SIDEBAR_TYPE,
  DEFAULT_THEME,
  DEFAULT_LAYOUT_MODE,
  DEFAULT_COLOR,
  DEFAULT_COLOR_WEAK,
  DEFAULT_FIXED_HEADER,
  DEFAULT_FIXED_SIDEMENU,
  DEFAULT_FIXED_HEADER_HIDDEN,
  DEFAULT_CONTENT_WIDTH_TYPE,
  DEFAULT_MULTI_TAB
} from '@/store/mutation-types'
const app = {
  state: {
    sidebar: true,
    device: 'desktop',
    theme: '',
    layout: '',
    contentWidth: '',
    fixedHeader: false,
    fixSiderbar: false,
    autoHideHeader: false,
    color: null,
    weak: false,
    multiTab: true,
    hasError: false
  },
  mutations: {
    SET_SIDEBAR_TYPE: (state, type) => {
      state.sidebar = type
      Vue.ls.set(SIDEBAR_TYPE, type)
    },
    CLOSE_SIDEBAR: (state) => {
      Vue.ls.set(SIDEBAR_TYPE, true)
      state.sidebar = false
    },
    TOGGLE_DEVICE: (state, device) => {
      state.device = device
    },
    TOGGLE_THEME: (state, theme) => {
      // setStore('_DEFAULT_THEME', theme)
      Vue.ls.set(DEFAULT_THEME, theme)
      state.theme = theme
    },
    TOGGLE_LAYOUT_MODE: (state, layout) => {
      Vue.ls.set(DEFAULT_LAYOUT_MODE, layout)
      state.layout = layout
    },
    TOGGLE_FIXED_HEADER: (state, fixed) => {
      Vue.ls.set(DEFAULT_FIXED_HEADER, fixed)
      state.fixedHeader = fixed
    },
    TOGGLE_FIXED_SIDERBAR: (state, fixed) => {
      Vue.ls.set(DEFAULT_FIXED_SIDEMENU, fixed)
      state.fixSiderbar = fixed
    },
    TOGGLE_FIXED_HEADER_HIDDEN: (state, show) => {
      Vue.ls.set(DEFAULT_FIXED_HEADER_HIDDEN, show)
      state.autoHideHeader = show
    },
    TOGGLE_CONTENT_WIDTH: (state, type) => {
      Vue.ls.set(DEFAULT_CONTENT_WIDTH_TYPE, type)
      state.contentWidth = type
    },
    TOGGLE_COLOR: (state, color) => {
      Vue.ls.set(DEFAULT_COLOR, color)
      state.color = color
    },
    TOGGLE_WEAK: (state, flag) => {
      Vue.ls.set(DEFAULT_COLOR_WEAK, flag)
      state.weak = flag
    },
    TOGGLE_MULTI_TAB: (state, bool) => {
      Vue.ls.set(DEFAULT_MULTI_TAB, bool)
      state.multiTab = bool
    },
    SET_HAS_ERROR: (state, bool) => {
      state.hasError = bool
    }
  },
  actions: {
    setSidebar ({ commit }, type) {
      commit('SET_SIDEBAR_TYPE', type)
    },
    CloseSidebar ({ commit }) {
      commit('CLOSE_SIDEBAR')
    },
    ToggleDevice ({ commit }, device) {
      commit('TOGGLE_DEVICE', device)
    },
    ToggleTheme ({ commit }, theme) {
      commit('TOGGLE_THEME', theme)
    },
    ToggleLayoutMode ({ commit }, mode) {
      commit('TOGGLE_LAYOUT_MODE', mode)
    },
    ToggleFixedHeader ({ commit }, fixedHeader) {
      if (!fixedHeader) {
        commit('TOGGLE_FIXED_HEADER_HIDDEN', false)
      }
      commit('TOGGLE_FIXED_HEADER', fixedHeader)
    },
    ToggleFixSiderbar ({ commit }, fixSiderbar) {
      commit('TOGGLE_FIXED_SIDERBAR', fixSiderbar)
    },
    ToggleFixedHeaderHidden ({ commit }, show) {
      commit('TOGGLE_FIXED_HEADER_HIDDEN', show)
    },
    ToggleContentWidth ({ commit }, type) {
      commit('TOGGLE_CONTENT_WIDTH', type)
    },
    ToggleColor ({ commit }, color) {
      commit('TOGGLE_COLOR', color)
    },
    ToggleWeak ({ commit }, weakFlag) {
      commit('TOGGLE_WEAK', weakFlag)
    },
    ToggleMultiTab ({ commit }, bool) {
      commit('TOGGLE_MULTI_TAB', bool)
    },
    SetHasError ({ commit }, bool) {
      commit('SET_HAS_ERROR', bool)
    }
  }
}
export default app
_web/src/store/modules/async-router.js
New file
@@ -0,0 +1,33 @@
/**
 * 向后端请求用户的菜单,动态生成路由
 */
import { constantRouterMap } from '@/config/router.config'
import { generatorDynamicRouter } from '@/router/generator-routers'
const permission = {
  state: {
    routers: constantRouterMap,
    addRouters: []
  },
  mutations: {
    SET_ROUTERS: (state, routers) => {
      state.addRouters = routers
      state.routers = constantRouterMap.concat(routers)
    }
  },
  actions: {
    GenerateRoutes ({ commit }, data) {
      return new Promise(resolve => {
        generatorDynamicRouter(data).then(routers => {
          commit('SET_ROUTERS', routers)
          resolve()
        })
      }).catch(err => {
        // eslint-disable-next-line no-undef
        reject(err)
      })
    }
  }
}
export default permission
_web/src/store/modules/permission.js
New file
@@ -0,0 +1,77 @@
import { asyncRouterMap, constantRouterMap } from '@/config/router.config'
/**
 * 过滤账户是否拥有某一个权限,并将菜单从加载列表移除
 *
 * @param permission
 * @param route
 * @returns {boolean}
 */
function hasPermission (permission, route) {
  // if (route.meta && route.meta.permission) {
  //   let flag = false
  //   for (let i = 0, len = permission.length; i < len; i++) {
  //     flag = route.meta.permission.includes(permission[i])
  //     if (flag) {
  //       return true
  //     }
  //   }
  //   return false
  // }
  return true
}
/**
 * 单账户多角色时,使用该方法可过滤角色不存在的菜单
 *
 * @param roles
 * @param route
 * @returns {*}
 */
// eslint-disable-next-line
function hasRole(roles, route) {
  if (route.meta && route.meta.roles) {
    return route.meta.roles.includes(roles.id)
  } else {
    return true
  }
}
function filterAsyncRouter (routerMap, roles) {
  const accessedRouters = routerMap.filter(route => {
    if (hasPermission(roles.permissionList, route)) {
      if (route.children && route.children.length) {
        route.children = filterAsyncRouter(route.children, roles)
      }
      return true
    }
    return false
  })
  return accessedRouters
}
const permission = {
  state: {
    routers: constantRouterMap,
    addRouters: []
  },
  mutations: {
    SET_ROUTERS: (state, routers) => {
      state.addRouters = routers
      state.routers = constantRouterMap.concat(routers)
    }
  },
  actions: {
    GenerateRoutes ({ commit }, data) {
      return new Promise(resolve => {
        const { roles } = data
        const accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
        // console.log('动态获取到的菜单列表:'+JSON.stringify(accessedRouters))
        commit('SET_ROUTERS', accessedRouters)
        resolve()
      })
    }
  }
}
export default permission
_web/src/store/modules/user.js
New file
@@ -0,0 +1,172 @@
import Vue from 'vue'
import { login, getLoginUser, logout } from '@/api/modular/system/loginManage'
import { sysDictTypeTree } from '@/api/modular/system/dictManage'
import { sysMenuChange } from '@/api/modular/system/menuManage'
import { ACCESS_TOKEN, ALL_APPS_MENU, DICT_TYPE_TREE_DATA } from '@/store/mutation-types'
import { welcome } from '@/utils/util'
import store from '../index'
import router from '../../router'
const user = {
  state: {
    token: '',
    name: '',
    welcome: '',
    avatar: '',
    buttons: [], // 按钮权限
    admintype: '', // 是否是超管
    roles: [],
    info: {}
  },
  mutations: {
    SET_TOKEN: (state, token) => {
      state.token = token
    },
    SET_NAME: (state, { name, welcome }) => {
      state.name = name
      state.welcome = welcome
    },
    SET_AVATAR: (state, avatar) => {
      state.avatar = avatar
    },
    SET_ROLES: (state, roles) => {
      state.roles = roles
    },
    SET_INFO: (state, info) => {
      state.info = info
    },
    SET_BUTTONS: (state, buttons) => {
      state.buttons = buttons
    },
    SET_ADMINTYPE: (state, admintype) => {
      state.admintype = admintype
    }
  },
  actions: {
    // 登录
    Login ({ commit }, userInfo) {
      return new Promise((resolve, reject) => {
        login(userInfo).then(response => {
          if (!response.success) {
            reject(response.message)
            return
          }
          const result = response.data
          Vue.ls.set(ACCESS_TOKEN, result, 7 * 24 * 60 * 60 * 1000)
          commit('SET_TOKEN', result)
          resolve()
        // eslint-disable-next-line handle-callback-err
        }).catch(error => {
          // eslint-disable-next-line prefer-promise-reject-errors
          reject('后端未启动或代理错误')
        })
      })
    },
    // 获取用户信息
    GetInfo ({ commit }) {
      return new Promise((resolve, reject) => {
        getLoginUser().then(response => {
          if (response.success) {
            const data = response.data
            commit('SET_ADMINTYPE', data.adminType)
            commit('SET_ROLES', 1)
            commit('SET_BUTTONS', data.permissions)
            commit('SET_INFO', data)
            commit('SET_NAME', { name: data.name, welcome: welcome() })
            if (data.avatar != null) {
              commit('SET_AVATAR', process.env.VUE_APP_API_BASE_URL + '/sysFileInfo/preview?id=' + data.avatar)
            }
            resolve(data)
          } else {
            // eslint-disable-next-line no-undef
            reject(new Error(data.message))
          }
        }).catch(error => {
          reject(error)
        })
      })
    },
    // 登出
    Logout ({ commit, state }) {
      return new Promise((resolve) => {
        logout(state.token).then(() => {
          resolve()
        }).catch(() => {
          resolve()
        }).finally(() => {
          commit('SET_TOKEN', '')
          commit('SET_ROLES', [])
          commit('SET_BUTTONS', [])
          commit('SET_ADMINTYPE', '')
          Vue.ls.remove(ACCESS_TOKEN)
          Vue.ls.remove(ALL_APPS_MENU)
          Vue.ls.remove(DICT_TYPE_TREE_DATA)
        })
      })
    },
    // 加载所有字典数据
    dictTypeData () {
      return new Promise((resolve, reject) => {
        sysDictTypeTree().then((data) => {
          if (data.success) {
            const result = data.data
            Vue.ls.set(DICT_TYPE_TREE_DATA, result)
            resolve()
          } else {
            // eslint-disable-next-line no-undef
            reject(new Error(data.message))
          }
        }).catch(error => {
          reject(error)
        })
      })
    },
    // 切换应用菜单
    MenuChange ({ commit }, application) {
      return new Promise((resolve) => {
        sysMenuChange({ application: application.code }).then((res) => {
          const apps = { 'code': '', 'name': '', 'active': '', 'menu': '' }
          apps.active = true
          apps.menu = res.data
          // eslint-disable-next-line camelcase
          const all_app_menu = Vue.ls.get(ALL_APPS_MENU)
          // eslint-disable-next-line camelcase
          const new_false_all_app_menu = []
          // 先去除所有默认的,以为此时切换的即将成为前端缓存默认的应用
          all_app_menu.forEach(item => {
            if (item.active) {
              item.active = false
            }
            new_false_all_app_menu.push(item)
          })
          // 此时缓存中全部都是不默认的应用
          Vue.ls.set(ALL_APPS_MENU, new_false_all_app_menu)
          apps.name = application.name
          apps.code = application.code
          const applocationR = []
          applocationR.push(apps)
          Vue.ls.set(ALL_APPS_MENU, applocationR)
          resolve(res)
          const antDesignmenus = res.data
          store.dispatch('GenerateRoutes', { antDesignmenus }).then(() => {
            router.addRoutes(store.getters.addRouters)
          })
          // 切换应用刷新整体界面,暂且取消
          // window.location.reload()
        }).catch(() => {
          resolve()
        })
      })
    }
  }
}
export default user
_web/src/store/mutation-types.js
New file
@@ -0,0 +1,18 @@
export const ACCESS_TOKEN = 'Access-Token'
export const SIDEBAR_TYPE = 'SIDEBAR_TYPE'
export const ALL_APPS_MENU = 'ALL_APPS_MENU'
export const DEFAULT_THEME = 'DEFAULT_THEME'
export const DEFAULT_LAYOUT_MODE = 'DEFAULT_LAYOUT_MODE'
export const DEFAULT_COLOR = 'DEFAULT_COLOR'
export const DEFAULT_COLOR_WEAK = 'DEFAULT_COLOR_WEAK'
export const DEFAULT_FIXED_HEADER = 'DEFAULT_FIXED_HEADER'
export const DEFAULT_FIXED_SIDEMENU = 'DEFAULT_FIXED_SIDEMENU'
export const DEFAULT_FIXED_HEADER_HIDDEN = 'DEFAULT_FIXED_HEADER_HIDDEN'
export const DEFAULT_CONTENT_WIDTH_TYPE = 'DEFAULT_CONTENT_WIDTH_TYPE'
export const DEFAULT_MULTI_TAB = 'DEFAULT_MULTI_TAB'
export const DICT_TYPE_TREE_DATA = 'DICT_TYPE_TREE_DATA'
export const CONTENT_WIDTH_TYPE = {
  Fluid: 'Fluid',
  Fixed: 'Fixed'
}
_web/src/utils/applocation.js
New file
@@ -0,0 +1,11 @@
import store from '@/store'
/**
 * 缓存中的已选中应用
 *
 * @author yubaoshan
 * @date 2020/06/27 02:34
 */
export function sysApplication () {
  return store.getters.applocation
}
_web/src/utils/axios.js
New file
@@ -0,0 +1,35 @@
const VueAxios = {
  vm: {},
  // eslint-disable-next-line no-unused-vars
  install (Vue, instance) {
    if (this.installed) {
      return
    }
    this.installed = true
    if (!instance) {
      // eslint-disable-next-line no-console
      console.error('You have to install axios')
      return
    }
    Vue.axios = instance
    Object.defineProperties(Vue.prototype, {
      axios: {
        get: function get () {
          return instance
        }
      },
      $http: {
        get: function get () {
          return instance
        }
      }
    })
  }
}
export {
  VueAxios
}
_web/src/utils/device.js
New file
@@ -0,0 +1,33 @@
import enquireJs from 'enquire.js'
export const DEVICE_TYPE = {
  DESKTOP: 'desktop',
  TABLET: 'tablet',
  MOBILE: 'mobile'
}
export const deviceEnquire = function (callback) {
  const matchDesktop = {
    match: () => {
      callback && callback(DEVICE_TYPE.DESKTOP)
    }
  }
  const matchLablet = {
    match: () => {
      callback && callback(DEVICE_TYPE.TABLET)
    }
  }
  const matchMobile = {
    match: () => {
      callback && callback(DEVICE_TYPE.MOBILE)
    }
  }
  // screen and (max-width: 1087.99px)
  enquireJs
    .register('screen and (max-width: 576px)', matchMobile)
    .register('screen and (min-width: 576px) and (max-width: 1199px)', matchLablet)
    .register('screen and (min-width: 1200px)', matchDesktop)
}
_web/src/utils/domUtil.js
New file
@@ -0,0 +1,19 @@
export const setDocumentTitle = function (title) {
  document.title = title
  const ua = navigator.userAgent
  // eslint-disable-next-line
  const regex = /\bMicroMessenger\/([\d\.]+)/
  if (regex.test(ua) && /ip(hone|od|ad)/i.test(ua)) {
    const i = document.createElement('iframe')
    i.src = '/favicon.ico'
    i.style.display = 'none'
    i.onload = function () {
      setTimeout(function () {
        i.remove()
      }, 9)
    }
    document.body.appendChild(i)
  }
}
export const domTitle = 'Snowy'
_web/src/utils/filter.js
New file
@@ -0,0 +1,101 @@
import Vue from 'vue'
import { DICT_TYPE_TREE_DATA } from '@/store/mutation-types'
import moment from 'moment'
import 'moment/locale/zh-cn'
moment.locale('zh-cn')
Vue.filter('NumberFormat', function (value) {
  if (!value) {
    return '0'
  }
  const intPartFormat = value.toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') // 将整数部分逢三一断
  return intPartFormat
})
Vue.filter('dayjs', function (dataStr, pattern = 'YYYY-MM-DD HH:mm:ss') {
  return moment(dataStr).format(pattern)
})
Vue.filter('moment', function (dataStr, pattern = 'YYYY-MM-DD HH:mm:ss') {
  return moment(dataStr).format(pattern)
})
/**
 * 金额格式化 ,使用方法:{{ val | Fmoney }}
 *
 * @author yubaoshan
 * @date 2020-9-15 15:02:20
 */
Vue.filter('Fmoney', function (val) {
  // eslint-disable-next-line no-useless-escape
  val = val.toString().replace(/\$|\,/g, '')
  if (isNaN(val)) {
    val = '0'
  }
  // eslint-disable-next-line eqeqeq
  const sign = (val == (val = Math.abs(val)))
  val = Math.floor(val * 100 + 0.50000000001)
  let cents = val % 100
  val = Math.floor(val / 100).toString()
  if (cents < 10) {
    cents = '0' + cents
  }
  // eslint-disable-next-line no-undef
  for (let i = 0; i < Math.floor((val.length - (1 + i)) / 3); I++) {
    val = val.substring(0, val.length - (4 * i + 3)) + ',' + val.substring(val.length - (4 * i + 3))
  }
  return (((sign) ? '' : '-') + val + '.' + cents)
})
/**
 * 翻译使用方法,直接返回翻译后的name {{ code | dictType(value) }}
 *
 * @author yubaoshan
 * @date 2020-9-15 15:02:20
 */
Vue.filter('dictType', function (code, value) {
  const dictTypeTree = Vue.ls.get(DICT_TYPE_TREE_DATA)
  if (dictTypeTree === undefined) {
    return '需重新登录'
  }
  // eslint-disable-next-line eqeqeq
  const tree = dictTypeTree.filter(item => item.code == code)[0].children
  if (tree === undefined || tree.length === 0) {
    return '无此字典'
  }
  // eslint-disable-next-line eqeqeq
  const values = tree.filter(item => item.code == value)
  if (values.length === undefined || values.length === 0) {
    return '无此字典'
  }
  return values[0].name
})
/**
 * 获取某个code下字典的列表,多用于字典下拉框,使用方法:{{ code | dictData }}
 *
 * @author yubaoshan
 * @date 2020-9-19 22:40:22
 */
Vue.filter('dictData', function (code) {
  const dictTypeTree = Vue.ls.get(DICT_TYPE_TREE_DATA)
  if (dictTypeTree === undefined) {
    return []
  }
  // eslint-disable-next-line eqeqeq
  const tree = dictTypeTree.filter(item => item.code == code)[0].children
  if (tree === undefined) {
    return []
  }
  return tree
})
/**
 * 获取所有字典数组
 *
 * @author yubaoshan
 * @date 2021-2-8 01:13
 */
Vue.filter('dictDataAll', function () {
  return Vue.ls.get(DICT_TYPE_TREE_DATA)
})
_web/src/utils/helper/permission.js
New file
@@ -0,0 +1,51 @@
export const PERMISSION_ENUM = {
  'add': { key: 'add', label: '新增' },
  'delete': { key: 'delete', label: '删除' },
  'edit': { key: 'edit', label: '修改' },
  'query': { key: 'query', label: '查询' },
  'get': { key: 'get', label: '详情' },
  'enable': { key: 'enable', label: '启用' },
  'disable': { key: 'disable', label: '禁用' },
  'import': { key: 'import', label: '导入' },
  'export': { key: 'export', label: '导出' }
}
function plugin (Vue) {
  if (plugin.installed) {
    return
  }
  !Vue.prototype.$auth && Object.defineProperties(Vue.prototype, {
    $auth: {
      get () {
        const _this = this
        return (permissions) => {
          const [permission, action] = permissions.split('.')
          const permissionList = _this.$store.getters.roles.permissions
          return permissionList.find((val) => {
            return val.permissionId === permission
          }).actionList.findIndex((val) => {
            return val === action
          }) > -1
        }
      }
    }
  })
  !Vue.prototype.$enum && Object.defineProperties(Vue.prototype, {
    $enum: {
      get () {
        // const _this = this;
        return (val) => {
          let result = PERMISSION_ENUM
          val && val.split('.').forEach(v => {
            result = result && result[v] || null
          })
          return result
        }
      }
    }
  })
}
export default plugin
_web/src/utils/mixin.js
New file
@@ -0,0 +1,76 @@
// import Vue from 'vue'
import { deviceEnquire, DEVICE_TYPE } from '@/utils/device'
import { mapState } from 'vuex'
// const mixinsComputed = Vue.config.optionMergeStrategies.computed
// const mixinsMethods = Vue.config.optionMergeStrategies.methods
const mixin = {
  computed: {
    ...mapState({
      layoutMode: state => state.app.layout,
      navTheme: state => state.app.theme,
      primaryColor: state => state.app.color,
      colorWeak: state => state.app.weak,
      fixedHeader: state => state.app.fixedHeader,
      fixSiderbar: state => state.app.fixSiderbar,
      fixSidebar: state => state.app.fixSiderbar,
      contentWidth: state => state.app.contentWidth,
      autoHideHeader: state => state.app.autoHideHeader,
      sidebarOpened: state => state.app.sidebar,
      multiTab: state => state.app.multiTab
    })
  },
  methods: {
    isTopMenu () {
      return this.layoutMode === 'topmenu'
    },
    isSideMenu () {
      return !this.isTopMenu()
    }
  }
}
const mixinDevice = {
  computed: {
    ...mapState({
      device: state => state.app.device
    })
  },
  methods: {
    isMobile () {
      return this.device === DEVICE_TYPE.MOBILE
    },
    isDesktop () {
      return this.device === DEVICE_TYPE.DESKTOP
    },
    isTablet () {
      return this.device === DEVICE_TYPE.TABLET
    }
  }
}
const AppDeviceEnquire = {
  mounted () {
    const { $store } = this
    deviceEnquire(deviceType => {
      switch (deviceType) {
        case DEVICE_TYPE.DESKTOP:
          $store.commit('TOGGLE_DEVICE', 'desktop')
          $store.dispatch('setSidebar', true)
          break
        case DEVICE_TYPE.TABLET:
          $store.commit('TOGGLE_DEVICE', 'tablet')
          $store.dispatch('setSidebar', false)
          break
        case DEVICE_TYPE.MOBILE:
        default:
          $store.commit('TOGGLE_DEVICE', 'mobile')
          $store.dispatch('setSidebar', true)
          break
      }
    })
  }
}
export { mixin, AppDeviceEnquire, mixinDevice }
_web/src/utils/onlyofficeUtil.js
New file
@@ -0,0 +1,22 @@
export function handleDocType(fileType) {
  let docType = ''
  const fileTypesDoc = [
    'doc', 'docm', 'docx', 'dot', 'dotm', 'dotx', 'epub', 'fodt', 'htm', 'html', 'mht', 'odt', 'ott', 'pdf', 'rtf', 'txt', 'djvu', 'xps'
  ]
  const fileTypesCsv = [
    'csv', 'fods', 'ods', 'ots', 'xls', 'xlsm', 'xlsx', 'xlt', 'xltm', 'xltx'
  ]
  const fileTypesPPt = [
    'fodp', 'odp', 'otp', 'pot', 'potm', 'potx', 'pps', 'ppsm', 'ppsx', 'ppt', 'pptm', 'pptx'
  ]
  if (fileTypesDoc.includes(fileType)) {
    docType = 'text'
  }
  if (fileTypesCsv.includes(fileType)) {
    docType = 'spreadsheet'
  }
  if (fileTypesPPt.includes(fileType)) {
    docType = 'presentation'
  }
  return docType
}
_web/src/utils/permissions.js
New file
@@ -0,0 +1,26 @@
import store from '@/store'
export function actionToObject (json) {
  try {
    return JSON.parse(json)
  } catch (e) {
    console.log('err', e.message)
  }
  return []
}
/**
 * 控制按钮是否显示
 *
 * @author yubaoshan
 * @date 2020/06/27 02:34
 */
export function hasBtnPermission (permission) {
  const myBtns = store.getters.buttons
  const admintype = store.getters.admintype
  // eslint-disable-next-line eqeqeq
  if (admintype == '1') {
     return true
  }
  return myBtns.indexOf(permission) > -1
}
_web/src/utils/request.js
New file
@@ -0,0 +1,96 @@
import Vue from 'vue'
import axios from 'axios'
import store from '@/store'
// import router from './router'
import { message, Modal, notification } from 'ant-design-vue' /// es/notification
import { VueAxios } from './axios'
import { ACCESS_TOKEN } from '@/store/mutation-types'
// 创建 axios 实例
const service = axios.create({
  baseURL: '/api', // api base_url
  timeout: 6000 // 请求超时时间
})
const err = (error) => {
  if (error.response) {
    const data = error.response.data
    const token = Vue.ls.get(ACCESS_TOKEN)
    if (error.response.status === 403) {
      console.log('服务器403啦,要重新登录!')
      notification.error({
        message: 'Forbidden',
        description: data.message
      })
    }
    if (error.response.status === 500) {
      if (data.message.length > 0) {
        message.error(data.message)
      }
    }
    if (error.response.status === 401 && !(data.result && data.result.isLogin)) {
      if (token) {
        store.dispatch('Logout').then(() => {
          setTimeout(() => {
            window.location.reload()
          }, 1500)
        })
      }
    }
  }
  return Promise.reject(error)
}
// request interceptor
service.interceptors.request.use(config => {
  const token = Vue.ls.get(ACCESS_TOKEN)
  if (token) {
    config.headers['Authorization'] = 'Bearer ' + token
  }
  return config
}, err)
/**
 * response interceptor
 * 所有请求统一返回
 */
service.interceptors.response.use((response) => {
  if (response.request.responseType === 'blob') {
    return response
  }
  const resData = response.data
  const code = response.data.code
  if (!store.state.app.hasError) {
    if (code === 1011006 || code === 1011007 || code === 1011008 || code === 1011009) {
      Modal.error({
        title: '提示:',
        content: resData.message,
        okText: '重新登录',
        onOk: () => {
          Vue.ls.remove(ACCESS_TOKEN)
          store.dispatch('SetHasError', false)
          window.location.reload()
        }
      })
      store.dispatch('SetHasError', true)
    }
    if (code === 1013002 || code === 1016002 || code === 1015002) {
      message.error(response.data.message)
      return response.data
    }
  }
  return resData
}, err)
const installer = {
  vm: {},
  install (Vue) {
    Vue.use(VueAxios, service)
  }
}
export {
  installer as VueAxios,
  service as axios
}
_web/src/utils/routeConvert.js
New file
@@ -0,0 +1,30 @@
import cloneDeep from 'lodash.clonedeep'
export function convertRoutes (nodes) {
  if (!nodes) return null
  nodes = cloneDeep(nodes)
  let queue = Array.isArray(nodes) ? nodes.concat() : [nodes]
  while (queue.length) {
    const levelSize = queue.length
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift()
      if (!node.children || !node.children.length) continue
      node.children.forEach(child => {
        // 转化相对路径
        if (child.path[0] !== '/' && !child.path.startsWith('http')) {
          child.path = node.path.replace(/(\w*)[/]*$/, `$1/${child.path}`)
        }
      })
      queue = queue.concat(node.children)
    }
  }
  return nodes
}
_web/src/utils/util.js
New file
@@ -0,0 +1,67 @@
export function timeFix () {
  const time = new Date()
  const hour = time.getHours()
  return hour < 9 ? '早上好' : hour <= 11 ? '上午好' : hour <= 13 ? '中午好' : hour < 20 ? '下午好' : '晚上好'
}
export function welcome () {
  const arr = ['休息一会儿吧', '准备吃什么呢?', '要不要打一把 LOL', '我猜你可能累了']
  const index = Math.floor(Math.random() * arr.length)
  return arr[index]
}
/**
 * 触发 window.resize
 */
export function triggerWindowResizeEvent () {
  const event = document.createEvent('HTMLEvents')
  event.initEvent('resize', true, true)
  event.eventType = 'message'
  window.dispatchEvent(event)
}
export function handleScrollHeader (callback) {
  let timer = 0
  let beforeScrollTop = window.pageYOffset
  callback = callback || function () {}
  window.addEventListener(
    'scroll',
    event => {
      clearTimeout(timer)
      timer = setTimeout(() => {
        let direction = 'up'
        const afterScrollTop = window.pageYOffset
        const delta = afterScrollTop - beforeScrollTop
        if (delta === 0) {
          return false
        }
        direction = delta > 0 ? 'down' : 'up'
        callback(direction)
        beforeScrollTop = afterScrollTop
      }, 50)
    },
    false
  )
}
export function isIE () {
  const bw = window.navigator.userAgent
  const compare = (s) => bw.indexOf(s) >= 0
  const ie11 = (() => 'ActiveXObject' in window)()
  return compare('MSIE') || ie11
}
/**
 * Remove loading animate
 * @param id parent element id or class
 * @param timeout
 */
export function removeLoadingAnimate (id = '', timeout = 1500) {
  if (id === '') {
    return
  }
  setTimeout(() => {
    document.body.removeChild(document.getElementById(id))
  }, timeout)
}
_web/src/utils/utils.less
New file
@@ -0,0 +1,50 @@
.textOverflow() {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  word-break: break-all;
}
.textOverflowMulti(@line: 3, @bg: #fff) {
  position: relative;
  max-height: @line * 1.5em;
  margin-right: -1em;
  padding-right: 1em;
  overflow: hidden;
  line-height: 1.5em;
  text-align: justify;
  &::before {
    position: absolute;
    right: 14px;
    bottom: 0;
    padding: 0 1px;
    background: @bg;
    content: '...';
  }
  &::after {
    position: absolute;
    right: 14px;
    width: 1em;
    height: 1em;
    margin-top: 0.2em;
    background: white;
    content: '';
  }
}
// mixins for clearfix
// ------------------------
.clearfix() {
  zoom: 1;
  &::before,
  &::after {
    display: table;
    content: ' ';
  }
  &::after {
    clear: both;
    height: 0;
    font-size: 0;
    visibility: hidden;
  }
}
_web/src/views/404.vue
New file
@@ -0,0 +1,15 @@
<template>
  <div>
    404 page
  </div>
</template>
<script>
export default {
  name: '404'
}
</script>
<style scoped>
</style>
_web/src/views/gen/codeGenerate/addForm.vue
New file
@@ -0,0 +1,316 @@
<template>
  <a-modal
    title="新增代码生成配置"
    :width="900"
    :visible="visible"
    :confirmLoading="confirmLoading"
    @ok="handleSubmit"
    @cancel="handleCancel"
  >
    <a-spin :spinning="confirmLoading">
      <a-form :form="form">
        <a-row :gutter="24">
          <a-col :md="12" :sm="24">
            <a-form-item
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              label="生成表"
              has-feedback
            >
              <a-select style="width: 100%" placeholder="请选择数据库表" v-decorator="['tableName', {rules: [{ required: true, message: '请选择数据库表!' }]}]" >
                <a-select-option v-for="(item,index) in tableNameData" :key="index" :value="item.tableName" @click="tableNameSele(item)">{{ item.tableName }}</a-select-option>
              </a-select>
            </a-form-item>
          </a-col>
          <a-col :md="12" :sm="24">
            <a-form-item
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              label="移除前缀"
            >
              <a-radio-group v-decorator="['tablePrefix',{rules: [{ required: true, message: '请选择是否移除前缀!' }]}]" >
                <a-radio v-for="(item,index) in tablePrefixData" :key="index" :value="item.code" @click="tablePrefixRadio(item.code)">{{ item.name }}</a-radio>
              </a-radio-group>
            </a-form-item>
          </a-col>
        </a-row>
        <a-row :gutter="24">
          <a-col :md="12" :sm="24">
            <a-form-item
              label="功能名"
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              has-feedback
            >
              <a-input placeholder="请输入功能名" v-decorator="['tableComment', {rules: [{required: true, message: '请输入功能名!'}]}]" />
            </a-form-item>
          </a-col>
          <a-col :md="12" :sm="24">
            <a-form-item
              label="类名"
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              has-feedback
            >
              <a-input placeholder="请输入类名" v-decorator="['className', {rules: [{required: true, message: '请输入类名!'}]}]" />
            </a-form-item>
          </a-col>
        </a-row>
        <a-row :gutter="24">
          <a-col :md="12" :sm="24">
            <a-form-item
              label="业务名"
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              has-feedback
            >
              <a-input placeholder="请输入业务名" v-decorator="['busName', {rules: [{required: true, message: '请输入业务名!'}]}]" />
            </a-form-item>
          </a-col>
          <a-col :md="12" :sm="24">
            <a-form-item
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              label="生成方式"
            >
              <a-radio-group v-decorator="['generateType',{rules: [{ required: true, message: '请选择生成方式!' }]}]" >
                <a-radio v-for="(item,index) in generateTypeData" :key="index" :value="item.code" @click="generateTypeRadio(item.code)">{{ item.name }}</a-radio>
              </a-radio-group>
            </a-form-item>
          </a-col>
        </a-row>
        <a-row :gutter="24">
          <a-col :md="12" :sm="24">
            <a-form-item
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              label="所属应用"
              has-feedback
            >
              <a-select style="width: 100%" placeholder="请选择应用分类" v-decorator="['appCode', {rules: [{ required: true, message: '请选择应用分类!' }]}]" >
                <a-select-option v-for="(item,index) in appData" :key="index" :value="item.code" @click="changeApplication(item.code)">{{ item.name }}</a-select-option>
              </a-select>
            </a-form-item>
          </a-col>
          <a-col :md="12" :sm="24">
            <a-form-item
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              label="父级菜单"
              has-feedback
            >
              <a-tree-select
                v-decorator="['menuPid', {rules: [{ required: true, message: '请选择父级菜单!' }]}]"
                style="width: 100%"
                :dropdownStyle="{ maxHeight: '300px', overflow: 'auto' }"
                :treeData="menuTreeData"
                placeholder="请选择父级菜单"
                treeDefaultExpandAll
              >
                <span slot="title" slot-scope="{ id }">{{ id }}
                </span>
              </a-tree-select>
            </a-form-item>
          </a-col>
        </a-row>
        <a-row :gutter="24">
          <a-col :md="12" :sm="24">
            <a-form-item
              label="作者姓名"
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              has-feedback
            >
              <a-input placeholder="请输入作者姓名" v-decorator="['authorName', {rules: [{required: true, message: '请输入作者姓名!'}]}]" />
            </a-form-item>
          </a-col>
          <a-col :md="12" :sm="24" v-show="packageNameShow">
            <a-form-item
              label="代码包名"
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              has-feedback
            >
              <a-input placeholder="请输入代码包名" v-decorator="['packageName', {rules: [{required: true, message: '请输入代码包名!'}]}]" />
            </a-form-item>
          </a-col>
        </a-row>
      </a-form>
    </a-spin>
  </a-modal>
</template>
<script>
  import { codeGenerateInformationList, codeGenerateAdd } from '@/api/modular/gen/codeGenerateManage'
  import { getAppList } from '@/api/modular/system/appManage'
  import { getMenuTree } from '@/api/modular/system/menuManage'
  export default {
    data () {
      return {
        labelCol: {
          xs: { span: 24 },
          sm: { span: 5 }
        },
        wrapperCol: {
          xs: { span: 24 },
          sm: { span: 15 }
        },
        visible: false,
        tableNameData: [],
        tablePrefixData: [],
        generateTypeData: [],
        confirmLoading: false,
        tablePrefixValue: 'N',
        tableNameValue: '',
        packageNameShow: true,
        appData: [],
        menuTreeData: [],
        form: this.$form.createForm(this)
      }
    },
    methods: {
      // 初始化方法
      add () {
        this.visible = true
        this.codeGenerateInformationList()
        this.dataTypeItem()
        this.selectedByDefault()
        this.getSysApplist()
      },
      /**
       * 默认选中项
       */
      selectedByDefault () {
        this.form.getFieldDecorator('packageName', { initialValue: 'vip.xiaonuo' })
        this.form.getFieldDecorator('tablePrefix', { valuePropName: 'checked', initialValue: 'N' })
        this.form.getFieldDecorator('generateType', { valuePropName: 'checked', initialValue: '1' })
        this.tablePrefixValue = 'N'
      },
      /**
       * 获得所有数据库的表
       */
      codeGenerateInformationList () {
        codeGenerateInformationList().then((res) => {
          this.tableNameData = res.data
        })
      },
      /**
       * 获取应用列表
       */
      getSysApplist () {
        return getAppList().then((res) => {
          if (res.success) {
            this.appData = res.data
          } else {
            this.$message.warning(res.message)
          }
        })
      },
      /**
       * 通过应用获取菜单
       */
      changeApplication (value) {
        this.form.resetFields(`menuPid`, [])
        getMenuTree({ 'application': value }).then((res) => {
          if (res.success) {
            this.menuTreeData = [{
              'id': '-1',
              'parentId': '0',
              'title': '顶级',
              'value': '0',
              'pid': '0',
              'children': res.data
            }]
          } else {
            this.$message.warning(res.message)
          }
        })
      },
      /**
       * 获取字典数据
       */
      dataTypeItem () {
        this.tablePrefixData = this.$options.filters['dictData']('yes_or_no')
        this.generateTypeData = this.$options.filters['dictData']('code_gen_create_type')
      },
      /**
       * 提交表单
       */
      handleSubmit () {
        const { form: { validateFields } } = this
        validateFields((errors, values) => {
          if (!errors) {
            this.confirmLoading = true
            codeGenerateAdd(values).then((res) => {
              if (res.success) {
                this.$message.success('新增成功')
                this.$emit('ok', values)
                this.handleCancel()
              } else {
                this.$message.error('新增失败:' + res.message)
              }
            }).finally((res) => {
              this.confirmLoading = false
            })
          }
        })
      },
      handleCancel () {
        this.form.resetFields()
        this.visible = false
        // 清空他们三个
        this.form.getFieldDecorator('className', { initialValue: '' })
        this.form.getFieldDecorator('busName', { initialValue: '' })
        this.form.getFieldDecorator('tableComment', { initialValue: '' })
      },
      /**
       * 选择数据库列表
       */
      tableNameSele (item) {
        this.tableNameValue = item.tableName
        this.form.getFieldDecorator('tableComment', { initialValue: item.tableComment })
        this.settingDefaultValue()
      },
      /**
       * 选择是否移除前缀触发
       */
      tablePrefixRadio (tablePrefixType) {
        this.tablePrefixValue = tablePrefixType
        this.settingDefaultValue()
      },
      /**
       * 设置默认值
       */
      settingDefaultValue () {
        const tableName = this.classNameToHump()
        this.form.getFieldDecorator('className', { initialValue: tableName })
        this.form.getFieldDecorator('busName', { initialValue: tableName.toLowerCase() })
      },
      /**
       * 设置类名为数据库表的驼峰命名
       */
      classNameToHump () {
        const arr = this.tableNameValue.toLowerCase().split('_')
        if (this.tablePrefixValue === 'Y') {
          arr.splice(0, 1)
        }
        for (let i = 0; i < arr.length; i++) {
          // charAt()方法得到第一个字母,slice()得到第二个字母以后的字符串
          arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].slice(1)
        }
        return arr.join('')
      },
      /**
       * 选择生成方式
       */
      generateTypeRadio (generateType) {
        if (generateType === '1') {
          this.packageNameShow = true
        } else {
          this.packageNameShow = false
          this.form.setFieldsValue({ packageName: 'vip.xiaonuo' })
        }
      }
    }
  }
</script>
_web/src/views/gen/codeGenerate/editForm.vue
New file
@@ -0,0 +1,327 @@
<template>
  <a-modal
    title="编辑代码生成配置"
    :width="900"
    :visible="visible"
    :confirmLoading="confirmLoading"
    @ok="handleSubmit"
    @cancel="handleCancel"
  >
    <a-spin :spinning="confirmLoading">
      <a-form :form="form">
        <a-form-item v-show="false"><a-input v-decorator="['id']" /></a-form-item>
        <a-row :gutter="24">
          <a-col :md="12" :sm="24">
            <a-form-item
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              label="生成表"
              has-feedback
            >
              <a-select style="width: 100%" placeholder="请选择数据库表" v-decorator="['tableName', {rules: [{ required: true, message: '请选择数据库表!' }]}]" >
                <a-select-option v-for="(item,index) in tableNameData" :key="index" :value="item.tableName" @click="tableNameSele(item)">{{ item.tableName }}</a-select-option>
              </a-select>
            </a-form-item>
          </a-col>
          <a-col :md="12" :sm="24">
            <a-form-item
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              label="移除前缀"
            >
              <a-radio-group v-decorator="['tablePrefix',{rules: [{ required: true, message: '请选择是否移除前缀!' }]}]" >
                <a-radio v-for="(item,index) in tablePrefixData" :key="index" :value="item.code" @click="tablePrefixRadio(item.code)">{{ item.name }}</a-radio>
              </a-radio-group>
            </a-form-item>
          </a-col>
        </a-row>
        <a-row :gutter="24">
          <a-col :md="12" :sm="24">
            <a-form-item
              label="功能名"
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              has-feedback
            >
              <a-input placeholder="请输入功能名" v-decorator="['tableComment', {rules: [{required: true, message: '请输入功能名!'}]}]" />
            </a-form-item>
          </a-col>
          <a-col :md="12" :sm="24">
            <a-form-item
              label="类名"
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              has-feedback
            >
              <a-input placeholder="请输入类名" v-decorator="['className', {rules: [{required: true, message: '请输入类名!'}]}]" />
            </a-form-item>
          </a-col>
        </a-row>
        <a-row :gutter="24">
          <a-col :md="12" :sm="24">
            <a-form-item
              label="业务名"
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              has-feedback
            >
              <a-input placeholder="请输入业务名" v-decorator="['busName', {rules: [{required: true, message: '请输入业务名!'}]}]" />
            </a-form-item>
          </a-col>
          <a-col :md="12" :sm="24">
            <a-form-item
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              label="生成方式"
            >
              <a-radio-group v-decorator="['generateType',{rules: [{ required: true, message: '请选择生成方式!' }]}]" >
                <a-radio v-for="(item,index) in generateTypeData" :key="index" :value="item.code" @click="generateTypeRadio(item.code)">{{ item.name }}</a-radio>
              </a-radio-group>
            </a-form-item>
          </a-col>
        </a-row>
        <a-row :gutter="24">
          <a-col :md="12" :sm="24">
            <a-form-item
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              label="所属应用"
              has-feedback
            >
              <a-select style="width: 100%" placeholder="请选择应用分类" v-decorator="['appCode', {rules: [{ required: true, message: '请选择应用分类!' }]}]" >
                <a-select-option v-for="(item,index) in appData" :key="index" :value="item.code" @click="changeApplication(item.code)">{{ item.name }}</a-select-option>
              </a-select>
            </a-form-item>
          </a-col>
          <a-col :md="12" :sm="24">
            <a-form-item
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              label="父级菜单"
              has-feedback
            >
              <a-tree-select
                v-decorator="['menuPid', {rules: [{ required: true, message: '请选择父级菜单!' }]}]"
                style="width: 100%"
                :dropdownStyle="{ maxHeight: '300px', overflow: 'auto' }"
                :treeData="menuTreeData"
                placeholder="请选择父级菜单"
                treeDefaultExpandAll
              >
                <span slot="title" slot-scope="{ id }">{{ id }}
                </span>
              </a-tree-select>
            </a-form-item>
          </a-col>
        </a-row>
        <a-row :gutter="24">
          <a-col :md="12" :sm="24">
            <a-form-item
              label="作者姓名"
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              has-feedback
            >
              <a-input placeholder="请输入作者姓名" v-decorator="['authorName', {rules: [{required: true, message: '请输入作者姓名!'}]}]" />
            </a-form-item>
          </a-col>
          <a-col :md="12" :sm="24" v-show="packageNameShow">
            <a-form-item
              label="代码包名"
              :labelCol="labelCol"
              :wrapperCol="wrapperCol"
              has-feedback
            >
              <a-input placeholder="请输入代码包名" v-decorator="['packageName', {rules: [{required: true, message: '请输入代码包名!'}]}]" />
            </a-form-item>
          </a-col>
        </a-row>
      </a-form>
    </a-spin>
  </a-modal>
</template>
<script>
  import { codeGenerateInformationList, codeGenerateEdit } from '@/api/modular/gen/codeGenerateManage'
  import { getAppList } from '@/api/modular/system/appManage'
  import { getMenuTree } from '@/api/modular/system/menuManage'
  export default {
    data () {
      return {
        labelCol: {
          xs: { span: 24 },
          sm: { span: 5 }
        },
        wrapperCol: {
          xs: { span: 24 },
          sm: { span: 15 }
        },
        visible: false,
        tableNameData: [],
        tablePrefixData: [],
        generateTypeData: [],
        confirmLoading: false,
        tablePrefixValue: 'N',
        tableNameValue: '',
        packageNameShow: true,
        appData: [],
        menuTreeData: [],
        form: this.$form.createForm(this)
      }
    },
    methods: {
      // 初始化方法
      edit (record) {
        this.visible = true
        this.codeGenerateInformationList()
        this.dataTypeItem()
        this.getSysApplist()
        setTimeout(() => {
          this.form.setFieldsValue(
            {
              id: record.id,
              tableName: record.tableName,
              tablePrefix: record.tablePrefix,
              tableComment: record.tableComment,
              className: record.className,
              busName: record.busName,
              generateType: record.generateType,
              authorName: record.authorName,
              packageName: record.packageName,
              appCode: record.appCode,
              menuPid: record.menuPid
            }
          )
        }, 100)
        this.changeApplication(record.appCode)
        this.tableNameValue = record.tableName
        this.tablePrefixValue = record.tablePrefix
      },
      /**
       * 获得所有数据库的表
       */
      codeGenerateInformationList () {
        codeGenerateInformationList().then((res) => {
          this.tableNameData = res.data
        })
      },
      /**
       * 获取应用列表
       */
      getSysApplist () {
        return getAppList().then((res) => {
          if (res.success) {
            this.appData = res.data
          } else {
            this.$message.warning(res.message)
          }
        })
      },
      /**
       * 通过应用获取菜单
       */
      changeApplication (value) {
        this.form.resetFields(`menuPid`, [])
        getMenuTree({ 'application': value }).then((res) => {
          if (res.success) {
            this.menuTreeData = [{
              'id': '-1',
              'parentId': '0',
              'title': '顶级',
              'value': '0',
              'pid': '0',
              'children': res.data
            }]
          } else {
            this.$message.warning(res.message)
          }
        })
      },
      /**
       * 获取字典数据
       */
      dataTypeItem () {
        this.tablePrefixData = this.$options.filters['dictData']('yes_or_no')
        this.generateTypeData = this.$options.filters['dictData']('code_gen_create_type')
      },
      /**
       * 提交表单
       */
      handleSubmit () {
        const { form: { validateFields } } = this
        validateFields((errors, values) => {
          if (!errors) {
            this.confirmLoading = true
            codeGenerateEdit(values).then((res) => {
              if (res.success) {
                this.$message.success('编辑成功')
                this.$emit('ok', values)
                this.handleCancel()
              } else {
                this.$message.error('编辑失败' + res.message)
              }
            }).finally((res) => {
              this.confirmLoading = false
            })
          }
        })
      },
      handleCancel () {
        this.form.resetFields()
        this.visible = false
      },
      /**
       * 选择数据库列表
       */
      tableNameSele (item) {
        this.tableNameValue = item.tableName
        this.form.setFieldsValue({ className: item.tableComment })
        this.settingDefaultValue()
      },
      /**
       * 选择是否移除前缀触发
       */
      tablePrefixRadio (tablePrefixType) {
        this.tablePrefixValue = tablePrefixType
        this.settingDefaultValue()
      },
      /**
       * 设置默认值
       */
      settingDefaultValue () {
        const tableName = this.classNameToHump()
        this.form.setFieldsValue(
          {
            className: tableName,
            busName: tableName.toLowerCase()
          }
        )
      },
      /**
       * 设置类名为数据库表的驼峰命名
       */
      classNameToHump () {
        const arr = this.tableNameValue.toLowerCase().split('_')
        if (this.tablePrefixValue === 'Y') {
          arr.splice(0, 1)
        }
        for (let i = 0; i < arr.length; i++) {
          // charAt()方法得到第一个字母,slice()得到第二个字母以后的字符串
          arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].slice(1)
        }
        return arr.join('')
      },
      /**
       * 选择生成方式
       */
      generateTypeRadio (generateType) {
        if (generateType === '1') {
          this.packageNameShow = true
        } else {
          this.packageNameShow = false
          this.form.setFieldsValue({ packageName: 'vip.xiaonuo' })
        }
      }
    }
  }
</script>
_web/src/views/gen/codeGenerate/index.vue
New file
@@ -0,0 +1,263 @@
<template>
  <div>
    <a-card :bordered="false" v-show="indexOpenShow" :bodyStyle="tstyle">
      <a-spin :spinning="Loading">
        <div class="table-page-search-wrapper" v-if="hasPerm('codeGenerate:page')">
          <a-form layout="inline">
            <a-row :gutter="48">
              <a-col :md="8" :sm="24">
                <a-form-item label="表名称" >
                  <a-input v-model="queryParam.tableName" allow-clear placeholder="请输入表名称"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-button type="primary" @click="$refs.table.refresh(true)">查询</a-button>
                <a-button style="margin-left: 8px" @click="() => queryParam = {}">重置</a-button>
              </a-col>
            </a-row>
          </a-form>
        </div>
      </a-spin>
    </a-card>
    <a-card :bordered="false" v-show="indexOpenShow">
      <a-spin :spinning="Loading">
        <s-table
          ref="table"
          :columns="columns"
          :data="loadData"
          :alert="true"
          :rowKey="(record) => record.id"
          :rowSelection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
        >
          <div slot="operator" v-if="hasPerm('codeGenerate:add')" >
            <a-button type="primary" v-if="hasPerm('codeGenerate:add')" icon="plus" @click="$refs.addForm.add()">新增</a-button>
          </div>
          <span slot="tableName" slot-scope="text">
            <ellipsis :length="10" tooltip>{{ text }}</ellipsis>
          </span>
          <span slot="packageName" slot-scope="text">
            <ellipsis :length="10" tooltip>{{ text }}</ellipsis>
          </span>
          <span slot="busName" slot-scope="text">
            <ellipsis :length="10" tooltip>{{ text }}</ellipsis>
          </span>
          <span slot="className" slot-scope="text">
            <ellipsis :length="10" tooltip>{{ text }}</ellipsis>
          </span>
          <span slot="tableComment" slot-scope="text">
            <ellipsis :length="10" tooltip>{{ text }}</ellipsis>
          </span>
          <span slot="tablePrefix" slot-scope="text">
            <ellipsis :length="10" tooltip>{{ 'yes_or_no' | dictType(text) }}</ellipsis>
          </span>
          <span slot="generateType" slot-scope="text">
            <ellipsis :length="10" tooltip>{{ 'code_gen_create_type' | dictType(text) }}</ellipsis>
          </span>
          <span slot="action" slot-scope="text, record">
            <span v-if="record.generateType === '1'">
              <a v-if="hasPerm('codeGenerate:runDown')" @click="runDownCodeGenerate(record)">开始生成</a>
            </span>
            <span v-else>
              <a-popconfirm v-if="hasPerm('codeGenerate:runLocal')" placement="topRight" title="确定生成代码到本项目?" @confirm="() => runLocalCodeGenerate(record)">
                <a>开始生成</a>
              </a-popconfirm>
            </span>
            <a-divider type="vertical" v-if="hasPerm('codeGenerate:config') & hasPerm('codeGenerate:runLocal') || hasPerm('codeGenerate:runDown') "/>
            <a v-if="hasPerm('codeGenerate:config')" @click="indexConfigOpen(record)">配置</a>
            <a-divider type="vertical" v-if="hasPerm('codeGenerate:config') & hasPerm('codeGenerate:edit')"/>
            <a v-if="hasPerm('codeGenerate:edit')" @click="$refs.editForm.edit(record)">编辑</a>
            <a-divider type="vertical" v-if="hasPerm('codeGenerate:edit') & hasPerm('codeGenerate:delete')"/>
            <a-popconfirm v-if="hasPerm('codeGenerate:delete')" placement="topRight" title="确认删除?" @confirm="() => codeGenerateDelete(record)">
              <a>删除</a>
            </a-popconfirm>
          </span>
        </s-table>
        <add-form ref="addForm" @ok="handleOk" v-if="hasPerm('codeGenerate:add')"/>
        <edit-form ref="editForm" @ok="handleOk" v-if="hasPerm('codeGenerate:edit')"/>
      </a-spin>
    </a-card>
    <index-config ref="indexConfig" @ok="handleResetOpen" v-if="hasPerm('codeGenerate:config')"/>
  </div>
</template>
<script>
  import { STable, Ellipsis } from '@/components'
  import { codeGeneratePage, codeGenerateDelete, codeGenerateRunDown, codeGenerateRunLocal } from '@/api/modular/gen/codeGenerateManage'
  import addForm from './addForm'
  import editForm from './editForm'
  import indexConfig from './indexConfig'
  export default {
    components: {
      indexConfig,
      STable,
      Ellipsis,
      addForm,
      editForm
    },
    data () {
      return {
        // 查询参数
        queryParam: {},
        // 表头
        columns: [
          {
            title: '表名称',
            dataIndex: 'tableName',
            scopedSlots: { customRender: 'tableName' }
          },
          {
            title: '代码包名',
            dataIndex: 'packageName',
            scopedSlots: { customRender: 'packageName' }
          },
          {
            title: '业务名',
            dataIndex: 'busName',
            scopedSlots: { customRender: 'busName' }
          },
          {
            title: '类名',
            dataIndex: 'className',
            scopedSlots: { customRender: 'className' }
          },
          {
            title: '功能名',
            dataIndex: 'tableComment',
            scopedSlots: { customRender: 'tableComment' }
          },
          {
            title: '作者姓名',
            dataIndex: 'authorName'
          },
          {
            title: '表前缀移除',
            dataIndex: 'tablePrefix',
            scopedSlots: { customRender: 'tablePrefix' }
          },
          {
            title: '生成方式',
            dataIndex: 'generateType',
            scopedSlots: { customRender: 'generateType' }
          }
        ],
        tstyle: { 'padding-bottom': '0px', 'margin-bottom': '10px' },
        loadData: parameter => {
          return codeGeneratePage(Object.assign(parameter, this.queryParam)).then((res) => {
            return res.data
          })
        },
        selectedRowKeys: [],
        selectedRows: [],
        Loading: false,
        jdbcDriverList: [],
        indexOpenShow: true
      }
    },
    created () {
      if (this.hasPerm('codeGenerate:edit') || this.hasPerm('codeGenerate:delete')) {
        this.columns.push({
          title: '操作',
          width: '230px',
          dataIndex: 'action',
          scopedSlots: { customRender: 'action' }
        })
      }
    },
    methods: {
      /**
       * 开始生成代码(生成压缩包)
       */
      runDownCodeGenerate (record) {
        this.Loading = true
        codeGenerateRunDown({ id: record.id }).then((res) => {
          this.Loading = false
          this.downloadfile(res)
          // eslint-disable-next-line handle-callback-err
        }).catch((err) => {
          this.Loading = false
          this.$message.error('下载错误:获取文件流错误')
        })
      },
      downloadfile (res) {
        var blob = new Blob([res.data], { type: 'application/octet-stream;charset=UTF-8' })
        var contentDisposition = res.headers['content-disposition']
        var patt = new RegExp('filename=([^;]+\\.[^\\.;]+);*')
        var result = patt.exec(contentDisposition)
        var filename = result[1]
        var downloadElement = document.createElement('a')
        var href = window.URL.createObjectURL(blob) // 创建下载的链接
        var reg = /^["](.*)["]$/g
        downloadElement.style.display = 'none'
        downloadElement.href = href
        downloadElement.download = decodeURI(filename.replace(reg, '$1')) // 下载后文件名
        document.body.appendChild(downloadElement)
        downloadElement.click() // 点击下载
        document.body.removeChild(downloadElement) // 下载完成移除元素
        window.URL.revokeObjectURL(href)
      },
      /**
       * 开始生成代码(本地项目)
       */
      runLocalCodeGenerate (record) {
        codeGenerateRunLocal(record).then((res) => {
          if (res.success) {
            this.$message.success('生成成功')
            this.$refs.table.refresh()
          } else {
            this.$message.error('生成失败:' + res.message)
          }
        })
      },
      /**
       * 删除
       */
      codeGenerateDelete (record) {
        this.Loading = true
        codeGenerateDelete([{ id: record.id }]).then((res) => {
          if (res.success) {
            this.$message.success('删除成功')
            this.$refs.table.refresh()
          } else {
            this.$message.error('删除失败:' + res.message)
          }
        }).catch((err) => {
          this.$message.error('删除错误:' + err.message)
        }).finally((res) => {
          this.Loading = false
        })
      },
      /**
       * 打开配置界面
       */
      indexConfigOpen (record) {
        this.indexOpenShow = false
        this.$refs.indexConfig.open(record)
      },
      /**
       * 详细配置界面返回
       */
      handleResetOpen () {
        this.indexOpenShow = true
        this.$refs.table.refresh()
      },
      /**
       * 其他界面返回
       */
      handleOk () {
        this.$refs.table.refresh()
      },
      onSelectChange (selectedRowKeys, selectedRows) {
        this.selectedRowKeys = selectedRowKeys
        this.selectedRows = selectedRows
      }
    }
  }
</script>
<style lang="less">
  .table-operator {
    margin-bottom: 18px;
  }
  button {
    margin-right: 8px;
  }
</style>
_web/src/views/gen/codeGenerate/indexConfig.vue
New file
@@ -0,0 +1,230 @@
<template>
  <a-card :bordered="false" v-show="indexConfigShow">
    <div class="table-operator">
      <a-button class="but_item" type="dashed" @click="handleCancel" icon="rollback">返回</a-button>
      <a-button type="primary" icon="plus" @click="handleSubmit">保存</a-button>
    </div>
    <a-table
      ref="table"
      size="middle"
      :columns="columns"
      :dataSource="loadData"
      :pagination="false"
      :alert="true"
      :loading="tableLoading"
      :rowKey="(record) => record.id"
    >
      <template slot="columnComment" slot-scope="text, record">
        <a-input v-model="record.columnComment" />
      </template>
      <template slot="javaType" slot-scope="text, record">
        <a-select style="width: 120px" v-model="record.javaType" :disabled="judgeColumns(record)">
          <a-select-option v-for="(item,index) in javaTypeData" :key="index" :value="item.code">{{ item.name }}</a-select-option>
        </a-select>
      </template>
      <template slot="effectType" slot-scope="text, record">
        <a-select style="width: 120px" v-model="record.effectType" :disabled="judgeColumns(record)">
          <a-select-option v-for="(item,index) in effectTypeData" :key="index" :value="item.code">{{ item.name }}</a-select-option>
        </a-select>
      </template>
      <template slot="dictTypeCode" slot-scope="text, record">
        <a-select
          style="width: 120px"
          v-model="record.dictTypeCode"
          :disabled="record.effectType !== 'radio' &&
            record.effectType !== 'select' && record.effectType !== 'checkbox'">
          <a-select-option v-for="(item,index) in dictDataAll" :key="index" :value="item.code">{{ item.name }}</a-select-option>
        </a-select>
      </template>
      <template slot="whetherTable" slot-scope="text, record">
        <a-checkbox v-model="record.whetherTable"/>
      </template>
      <template slot="whetherRetract" slot-scope="text, record">
        <a-checkbox v-model="record.whetherRetract"/>
      </template>
      <template slot="whetherAddUpdate" slot-scope="text, record">
        <a-checkbox v-model="record.whetherAddUpdate" :disabled="judgeColumns(record)"/>
      </template>
      <template slot="whetherRequired" slot-scope="text, record">
        <a-checkbox v-model="record.whetherRequired" :disabled="judgeColumns(record)"/>
      </template>
      <template slot="queryWhether" slot-scope="text, record">
        <a-switch v-model="record.queryWhether">
          <a-icon slot="checkedChildren" type="check" />
          <a-icon slot="unCheckedChildren" type="close" />
        </a-switch>
      </template>
      <template slot="queryType" slot-scope="text, record">
        <a-select style="width: 100px" v-model="record.queryType" :disabled="!record.queryWhether">
          <a-select-option v-for="(item,index) in codeGenQueryTypeData" :key="index" :value="item.code">{{ item.name }}</a-select-option>
        </a-select>
      </template>
    </a-table>
  </a-card>
</template>
<script>
  import { sysCodeGenerateConfigList, sysCodeGenerateConfigEdit } from '@/api/modular/gen/sysCodeGenerateConfigManage'
  export default {
    data () {
      return {
        // 表头
        columns: [
          {
            title: 'java字段',
            dataIndex: 'javaName'
          },
          {
            title: '字段描述',
            dataIndex: 'columnComment',
            scopedSlots: { customRender: 'columnComment' }
          },
          {
            title: '物理类型',
            dataIndex: 'dataType'
          },
          {
            title: 'java类型',
            dataIndex: 'javaType',
            scopedSlots: { customRender: 'javaType' }
          },
          {
            title: '作用类型',
            dataIndex: 'effectType',
            scopedSlots: { customRender: 'effectType' }
          },
          {
            title: '字典',
            dataIndex: 'dictTypeCode',
            scopedSlots: { customRender: 'dictTypeCode' }
          },
          {
            title: '列表显示',
            align: 'center',
            dataIndex: 'whetherTable',
            scopedSlots: { customRender: 'whetherTable' }
          },
          {
            title: '列字段省略',
            align: 'center',
            dataIndex: 'whetherRetract',
            scopedSlots: { customRender: 'whetherRetract' }
          },
          {
            title: '增改',
            align: 'center',
            dataIndex: 'whetherAddUpdate',
            scopedSlots: { customRender: 'whetherAddUpdate' }
          },
          {
            title: '必填',
            align: 'center',
            dataIndex: 'whetherRequired',
            scopedSlots: { customRender: 'whetherRequired' }
          },
          {
            title: '是否是查询',
            align: 'center',
            dataIndex: 'queryWhether',
            scopedSlots: { customRender: 'queryWhether' }
          },
          {
            title: '查询方式',
            dataIndex: 'queryType',
            scopedSlots: { customRender: 'queryType' }
          }
        ],
        indexConfigShow: false,
        tableLoading: false,
        visible: false,
        loadData: [],
        javaTypeData: [],
        effectTypeData: [],
        dictDataAll: [],
        codeGenQueryTypeData: [],
        yesOrNoData: []
      }
    },
    methods: {
      /**
       * 打开界面
       */
      open (data) {
        this.indexConfigShow = true
        this.tableLoading = true
        const dictOption = this.$options
        this.javaTypeData = dictOption.filters['dictData']('code_gen_java_type')
        this.effectTypeData = dictOption.filters['dictData']('code_gen_effect_type')
        this.dictDataAll = dictOption.filters['dictDataAll']()
        this.yesOrNoData = dictOption.filters['dictData']('yes_or_no')
        this.codeGenQueryTypeData = dictOption.filters['dictData']('code_gen_query_type')
        const params = {
          codeGenId: data.id
        }
        sysCodeGenerateConfigList(params).then((res) => {
          this.loadData = res.data
          this.loadData.forEach(item => {
            for (const key in item) {
              if (item[key] === 'Y') {
                item[key] = true
              }
              if (item[key] === 'N') {
                item[key] = false
              }
            }
          })
          this.tableLoading = false
        })
      },
      /**
       * 提交表单
       */
      handleSubmit () {
        this.tableLoading = true
        // 做数组属性转换, 咱先来一个切断双向绑定,学习的童鞋下回记下啊
        // eslint-disable-next-line prefer-const
        let loadDatas = JSON.parse(JSON.stringify(this.loadData))
        loadDatas.forEach(item => {
          // 必填那一项转换
          for (const key in item) {
            if (item[key] === true) {
              item[key] = 'Y'
            }
            if (item[key] === false) {
              item[key] = 'N'
            }
          }
        })
        const param = {
          sysCodeGenerateConfigParamList: loadDatas
        }
        sysCodeGenerateConfigEdit(param).then((res) => {
          this.tableLoading = false
          if (res.success) {
            this.$message.success('编辑成功')
            this.handleCancel()
          } else {
            this.$message.error('编辑失败:' + res.message)
          }
        })
      },
      /**
       * 判断是否(用于是否能选择或输入等)
       */
      judgeColumns (data) {
        if (data.columnName.indexOf('create_user') > -1 ||
            data.columnName.indexOf('create_time') > -1 ||
            data.columnName.indexOf('update_user') > -1 ||
            data.columnName.indexOf('update_time') > -1 ||
            data.columnKey === 'PRI') {
          return true
        }
        return false
      },
      handleCancel () {
        this.$emit('ok')
        this.loadData = []
        this.indexConfigShow = false
      }
    }
  }
</script>
_web/src/views/main/README.md
New file
@@ -0,0 +1 @@
/** 您的业务接口文件全写在此文件夹下面,升级底座直接迁移代码即可 **/
_web/src/views/main/blogarticle/addForm.vue
New file
@@ -0,0 +1,256 @@
<template>
  <a-modal
    title="新增blog文章主体"
    :width="900"
    :visible="visible"
    :confirmLoading="confirmLoading"
    @ok="handleSubmit"
    @cancel="handleCancel"
  >
    <a-spin :spinning="confirmLoading">
      <a-form :form="form">
        <a-form-item
          label="文章标题"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入文章标题" v-decorator="['title', {rules: [{required: true, message: '请输入文章标题!'}]}]" />
        </a-form-item>
        <a-form-item
          label="文章文件id"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入文章文件id" v-decorator="['articleFileId', {rules: [{required: false, message: '请输入文章文件id!'}]}]" />
        </a-form-item>
        <a-form-item
          label="文件类型 1:markdown 2:html"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入文件类型 1:markdown 2:html" v-decorator="['articleFileType', {rules: [{required: true, message: '请输入文件类型 1:markdown 2:html!'}]}]" />
        </a-form-item>
        <a-form-item
          label="文章分类id 0:没有分类"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入文章分类id 0:没有分类" v-decorator="['articleTypeId', {rules: [{required: true, message: '请输入文章分类id 0:没有分类!'}]}]" />
        </a-form-item>
        <a-form-item
          label="文章引言"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入文章引言" v-decorator="['introduce', {rules: [{required: true, message: '请输入文章引言!'}]}]" />
        </a-form-item>
        <a-form-item
          label="封面文件地址(id)"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入封面文件地址(id)" v-decorator="['coverFileId', {rules: [{required: true, message: '请输入封面文件地址(id)!'}]}]" />
        </a-form-item>
        <a-form-item
          label="上次编辑时间"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-date-picker style="width: 100%" placeholder="请选择上次编辑时间" v-decorator="['lastEditorDate',{rules: [{ required: true, message: '请选择上次编辑时间!' }]}]" @change="lastEditorDateOnChange"/>
        </a-form-item>
        <a-form-item
          label="发布时间"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-date-picker style="width: 100%" placeholder="请选择发布时间" v-decorator="['publishDate',{rules: [{ required: true, message: '请选择发布时间!' }]}]" @change="publishDateOnChange"/>
        </a-form-item>
        <a-form-item
          label="是否置顶 0:否 1:是"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入是否置顶 0:否 1:是" v-decorator="['isTop', {rules: [{required: true, message: '请输入是否置顶 0:否 1:是!'}]}]" />
        </a-form-item>
        <a-form-item
          label="置顶值(越小越靠前)"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入置顶值(越小越靠前)" v-decorator="['topValue', {rules: [{required: true, message: '请输入置顶值(越小越靠前)!'}]}]" />
        </a-form-item>
        <a-form-item
          label="公开状态 1:公开 2:私密 3:密码授权"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入公开状态 1:公开 2:私密 3:密码授权" v-decorator="['authStatus', {rules: [{required: true, message: '请输入公开状态 1:公开 2:私密 3:密码授权!'}]}]" />
        </a-form-item>
        <a-form-item
          label="授权密码(在密码授权状态时)"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入授权密码(在密码授权状态时)" v-decorator="['authPassword', {rules: [{required: true, message: '请输入授权密码(在密码授权状态时)!'}]}]" />
        </a-form-item>
        <a-form-item
          label="编辑状态 0:草稿 1:发布"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入编辑状态 0:草稿 1:发布" v-decorator="['editorStatus', {rules: [{required: true, message: '请输入编辑状态 0:草稿 1:发布!'}]}]" />
        </a-form-item>
        <a-form-item
          label="归档年份(以初次发布时间为准)"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入归档年份(以初次发布时间为准)" v-decorator="['separateYear', {rules: [{required: true, message: '请输入归档年份(以初次发布时间为准)!'}]}]" />
        </a-form-item>
        <a-form-item
          label="归档月份"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入归档月份" v-decorator="['separateMonth', {rules: [{required: true, message: '请输入归档月份!'}]}]" />
        </a-form-item>
        <a-form-item
          label="归档日"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入归档日" v-decorator="['separateDay', {rules: [{required: true, message: '请输入归档日!'}]}]" />
        </a-form-item>
        <a-form-item
          label="是否启用 0:否 1:是"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入是否启用 0:否 1:是" v-decorator="['isEnable', {rules: [{required: true, message: '请输入是否启用 0:否 1:是!'}]}]" />
        </a-form-item>
        <a-form-item
          label="更新时间"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-date-picker style="width: 100%" placeholder="请选择更新时间" v-decorator="['updateDate',{rules: [{ required: true, message: '请选择更新时间!' }]}]" @change="updateDateOnChange"/>
        </a-form-item>
        <a-form-item
          label="创建时间"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-date-picker style="width: 100%" placeholder="请选择创建时间" v-decorator="['createDate',{rules: [{ required: true, message: '请选择创建时间!' }]}]" @change="createDateOnChange"/>
        </a-form-item>
      </a-form>
    </a-spin>
  </a-modal>
</template>
<script>
  import { blogArticleAdd } from '@/api/modular/main/blogarticle/blogArticleManage'
  export default {
    data () {
      return {
        labelCol: {
          xs: { span: 24 },
          sm: { span: 5 }
        },
        wrapperCol: {
          xs: { span: 24 },
          sm: { span: 15 }
        },
        lastEditorDateDateString: '',
        publishDateDateString: '',
        updateDateDateString: '',
        createDateDateString: '',
        visible: false,
        confirmLoading: false,
        form: this.$form.createForm(this)
      }
    },
    methods: {
      // 初始化方法
      add (record) {
        this.visible = true
      },
      /**
       * 提交表单
       */
      handleSubmit () {
        const { form: { validateFields } } = this
        this.confirmLoading = true
        validateFields((errors, values) => {
          if (!errors) {
            for (const key in values) {
              if (typeof (values[key]) === 'object' && values[key] != null) {
                values[key] = JSON.stringify(values[key])
              }
            }
            values.lastEditorDate = this.lastEditorDateDateString || null
            values.publishDate = this.publishDateDateString || null
            values.updateDate = this.updateDateDateString || null
            values.createDate = this.createDateDateString || null
            blogArticleAdd(values).then((res) => {
              if (res.success) {
                this.$message.success('新增成功')
                this.confirmLoading = false
                this.$emit('ok', values)
                this.handleCancel()
              } else {
                this.$message.error('新增失败')// + res.message
              }
            }).finally((res) => {
              this.confirmLoading = false
            })
          } else {
            this.confirmLoading = false
          }
        })
      },
      lastEditorDateOnChange(date, dateString) {
        this.lastEditorDateDateString = dateString
      },
      publishDateOnChange(date, dateString) {
        this.publishDateDateString = dateString
      },
      updateDateOnChange(date, dateString) {
        this.updateDateDateString = dateString
      },
      createDateOnChange(date, dateString) {
        this.createDateDateString = dateString
      },
      handleCancel () {
        this.lastEditorDateDateString =''
        this.form.getFieldDecorator('lastEditorDate', { initialValue: null })
        this.publishDateDateString =''
        this.form.getFieldDecorator('publishDate', { initialValue: null })
        this.updateDateDateString =''
        this.form.getFieldDecorator('updateDate', { initialValue: null })
        this.createDateDateString =''
        this.form.getFieldDecorator('createDate', { initialValue: null })
        this.form.resetFields()
        this.visible = false
      }
    }
  }
</script>
_web/src/views/main/blogarticle/editForm.vue
New file
@@ -0,0 +1,298 @@
<template>
  <a-modal
    title="编辑blog文章主体"
    :width="900"
    :visible="visible"
    :confirmLoading="confirmLoading"
    @ok="handleSubmit"
    @cancel="handleCancel"
  >
    <a-spin :spinning="confirmLoading">
      <a-form :form="form">
        <a-form-item v-show="false"><a-input v-decorator="['id']" /></a-form-item>
        <a-form-item
          label="文章标题"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入文章标题" v-decorator="['title', {rules: [{required: true, message: '请输入文章标题!'}]}]" />
        </a-form-item>
        <a-form-item
          label="文章文件id"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入文章文件id" v-decorator="['articleFileId', {rules: [{required: true, message: '请输入文章文件id!'}]}]" />
        </a-form-item>
        <a-form-item
          label="文件类型 1:markdown 2:html"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入文件类型 1:markdown 2:html" v-decorator="['articleFileType', {rules: [{required: true, message: '请输入文件类型 1:markdown 2:html!'}]}]" />
        </a-form-item>
        <a-form-item
          label="文章分类id 0:没有分类"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入文章分类id 0:没有分类" v-decorator="['articleTypeId', {rules: [{required: true, message: '请输入文章分类id 0:没有分类!'}]}]" />
        </a-form-item>
        <a-form-item
          label="文章引言"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入文章引言" v-decorator="['introduce', {rules: [{required: true, message: '请输入文章引言!'}]}]" />
        </a-form-item>
        <a-form-item
          label="封面文件地址(id)"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入封面文件地址(id)" v-decorator="['coverFileId', {rules: [{required: true, message: '请输入封面文件地址(id)!'}]}]" />
        </a-form-item>
        <a-form-item
          label="上次编辑时间"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-date-picker style="width: 100%" placeholder="请选择上次编辑时间" v-decorator="['lastEditorDate',{rules: [{ required: true, message: '请选择上次编辑时间!' }]}]" @change="lastEditorDateOnChange"/>
        </a-form-item>
        <a-form-item
          label="发布时间"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-date-picker style="width: 100%" placeholder="请选择发布时间" v-decorator="['publishDate',{rules: [{ required: true, message: '请选择发布时间!' }]}]" @change="publishDateOnChange"/>
        </a-form-item>
        <a-form-item
          label="是否置顶 0:否 1:是"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入是否置顶 0:否 1:是" v-decorator="['isTop', {rules: [{required: true, message: '请输入是否置顶 0:否 1:是!'}]}]" />
        </a-form-item>
        <a-form-item
          label="置顶值(越小越靠前)"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入置顶值(越小越靠前)" v-decorator="['topValue', {rules: [{required: true, message: '请输入置顶值(越小越靠前)!'}]}]" />
        </a-form-item>
        <a-form-item
          label="公开状态 1:公开 2:私密 3:密码授权"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入公开状态 1:公开 2:私密 3:密码授权" v-decorator="['authStatus', {rules: [{required: true, message: '请输入公开状态 1:公开 2:私密 3:密码授权!'}]}]" />
        </a-form-item>
        <a-form-item
          label="授权密码(在密码授权状态时)"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入授权密码(在密码授权状态时)" v-decorator="['authPassword', {rules: [{required: true, message: '请输入授权密码(在密码授权状态时)!'}]}]" />
        </a-form-item>
        <a-form-item
          label="编辑状态 0:草稿 1:发布"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入编辑状态 0:草稿 1:发布" v-decorator="['editorStatus', {rules: [{required: true, message: '请输入编辑状态 0:草稿 1:发布!'}]}]" />
        </a-form-item>
        <a-form-item
          label="归档年份(以初次发布时间为准)"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入归档年份(以初次发布时间为准)" v-decorator="['separateYear', {rules: [{required: true, message: '请输入归档年份(以初次发布时间为准)!'}]}]" />
        </a-form-item>
        <a-form-item
          label="归档月份"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入归档月份" v-decorator="['separateMonth', {rules: [{required: true, message: '请输入归档月份!'}]}]" />
        </a-form-item>
        <a-form-item
          label="归档日"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入归档日" v-decorator="['separateDay', {rules: [{required: true, message: '请输入归档日!'}]}]" />
        </a-form-item>
        <a-form-item
          label="是否启用 0:否 1:是"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入是否启用 0:否 1:是" v-decorator="['isEnable', {rules: [{required: true, message: '请输入是否启用 0:否 1:是!'}]}]" />
        </a-form-item>
        <a-form-item
          label="更新时间"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-date-picker style="width: 100%" placeholder="请选择更新时间" v-decorator="['updateDate',{rules: [{ required: true, message: '请选择更新时间!' }]}]" @change="updateDateOnChange"/>
        </a-form-item>
        <a-form-item
          label="创建时间"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-date-picker style="width: 100%" placeholder="请选择创建时间" v-decorator="['createDate',{rules: [{ required: true, message: '请选择创建时间!' }]}]" @change="createDateOnChange"/>
        </a-form-item>
      </a-form>
    </a-spin>
  </a-modal>
</template>
<script>
  import moment from 'moment'
  import { blogArticleEdit } from '@/api/modular/main/blogarticle/blogArticleManage'
  export default {
    data () {
      return {
        labelCol: {
          xs: { span: 24 },
          sm: { span: 5 }
        },
        wrapperCol: {
          xs: { span: 24 },
          sm: { span: 15 }
        },
        lastEditorDateDateString: '',
        publishDateDateString: '',
        updateDateDateString: '',
        createDateDateString: '',
        visible: false,
        confirmLoading: false,
        form: this.$form.createForm(this)
      }
    },
    methods: {
      moment,
      // 初始化方法
      edit (record) {
        this.visible = true
        setTimeout(() => {
          this.form.setFieldsValue(
            {
              id: record.id,
              title: record.title,
              articleFileId: record.articleFileId,
              articleFileType: record.articleFileType,
              articleTypeId: record.articleTypeId,
              introduce: record.introduce,
              coverFileId: record.coverFileId,
              isTop: record.isTop,
              topValue: record.topValue,
              authStatus: record.authStatus,
              authPassword: record.authPassword,
              editorStatus: record.editorStatus,
              separateYear: record.separateYear,
              separateMonth: record.separateMonth,
              separateDay: record.separateDay,
              isEnable: record.isEnable
            }
          )
        }, 100)
        // 时间单独处理
        if (record.lastEditorDate) {
            this.form.getFieldDecorator('lastEditorDate', { initialValue: moment(record.lastEditorDate, 'YYYY-MM-DD') })
            this.lastEditorDateDateString = moment(record.lastEditorDate).format('YYYY-MM-DD')
        }
        // 时间单独处理
        if (record.publishDate) {
            this.form.getFieldDecorator('publishDate', { initialValue: moment(record.publishDate, 'YYYY-MM-DD') })
            this.publishDateDateString = moment(record.publishDate).format('YYYY-MM-DD')
        }
        // 时间单独处理
        if (record.updateDate) {
            this.form.getFieldDecorator('updateDate', { initialValue: moment(record.updateDate, 'YYYY-MM-DD') })
            this.updateDateDateString = moment(record.updateDate).format('YYYY-MM-DD')
        }
        // 时间单独处理
        if (record.createDate) {
            this.form.getFieldDecorator('createDate', { initialValue: moment(record.createDate, 'YYYY-MM-DD') })
            this.createDateDateString = moment(record.createDate).format('YYYY-MM-DD')
        }
      },
      handleSubmit () {
        const { form: { validateFields } } = this
        this.confirmLoading = true
        validateFields((errors, values) => {
          if (!errors) {
            for (const key in values) {
              if (typeof (values[key]) === 'object' && values[key] != null) {
                values[key] = JSON.stringify(values[key])
              }
            }
            values.lastEditorDate = this.lastEditorDateDateString || null
            values.publishDate = this.publishDateDateString || null
            values.updateDate = this.updateDateDateString || null
            values.createDate = this.createDateDateString || null
            blogArticleEdit(values).then((res) => {
              if (res.success) {
                this.$message.success('编辑成功')
                this.confirmLoading = false
                this.$emit('ok', values)
                this.handleCancel()
              } else {
                this.$message.error('编辑失败')//  + res.message
              }
            }).finally((res) => {
              this.confirmLoading = false
            })
          } else {
            this.confirmLoading = false
          }
        })
      },
      lastEditorDateOnChange(date, dateString) {
        this.lastEditorDateDateString = dateString
      },
      publishDateOnChange(date, dateString) {
        this.publishDateDateString = dateString
      },
      updateDateOnChange(date, dateString) {
        this.updateDateDateString = dateString
      },
      createDateOnChange(date, dateString) {
        this.createDateDateString = dateString
      },
      handleCancel () {
        this.lastEditorDateDateString =''
        this.form.getFieldDecorator('lastEditorDate', { initialValue: null })
        this.publishDateDateString =''
        this.form.getFieldDecorator('publishDate', { initialValue: null })
        this.updateDateDateString =''
        this.form.getFieldDecorator('updateDate', { initialValue: null })
        this.createDateDateString =''
        this.form.getFieldDecorator('createDate', { initialValue: null })
        this.form.resetFields()
        this.visible = false
      }
    }
  }
</script>
_web/src/views/main/blogarticle/index.vue
New file
@@ -0,0 +1,400 @@
<template>
  <div>
    <a-card :bordered="false" :bodyStyle="tstyle">
      <div class="table-page-search-wrapper" v-if="hasPerm('blogArticle:page')">
        <a-form layout="inline">
          <a-row :gutter="48">
            <a-col :md="8" :sm="24">
              <a-form-item label="文章标题">
                <a-input v-model="queryParam.title" allow-clear placeholder="请输入文章标题"/>
              </a-form-item>
            </a-col>
            <a-col :md="8" :sm="24">
              <a-form-item label="文章文件id">
                <a-input v-model="queryParam.articleFileId" allow-clear placeholder="请输入文章文件id"/>
              </a-form-item>
            </a-col>
            <template v-if="advanced">
              <a-col :md="8" :sm="24">
                <a-form-item label="文件类型 1:markdown 2:html">
                  <a-input v-model="queryParam.articleFileType" allow-clear placeholder="请输入文件类型 1:markdown 2:html"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="文章分类id 0:没有分类">
                  <a-input v-model="queryParam.articleTypeId" allow-clear placeholder="请输入文章分类id 0:没有分类"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="文章引言">
                  <a-input v-model="queryParam.introduce" allow-clear placeholder="请输入文章引言"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="封面文件地址(id)">
                  <a-input v-model="queryParam.coverFileId" allow-clear placeholder="请输入封面文件地址(id)"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="上次编辑时间">
                  <a-date-picker style="width: 100%" placeholder="请选择上次编辑时间" v-model="queryParam.lastEditorDateDate" @change="onChangelastEditorDate"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="发布时间">
                  <a-date-picker style="width: 100%" placeholder="请选择发布时间" v-model="queryParam.publishDateDate" @change="onChangepublishDate"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="是否置顶 0:否 1:是">
                  <a-input v-model="queryParam.isTop" allow-clear placeholder="请输入是否置顶 0:否 1:是"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="置顶值(越小越靠前)">
                  <a-input v-model="queryParam.topValue" allow-clear placeholder="请输入置顶值(越小越靠前)"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="公开状态 1:公开 2:私密 3:密码授权">
                  <a-input v-model="queryParam.authStatus" allow-clear placeholder="请输入公开状态 1:公开 2:私密 3:密码授权"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="授权密码(在密码授权状态时)">
                  <a-input v-model="queryParam.authPassword" allow-clear placeholder="请输入授权密码(在密码授权状态时)"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="编辑状态 0:草稿 1:发布">
                  <a-input v-model="queryParam.editorStatus" allow-clear placeholder="请输入编辑状态 0:草稿 1:发布"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="归档年份(以初次发布时间为准)">
                  <a-input v-model="queryParam.separateYear" allow-clear placeholder="请输入归档年份(以初次发布时间为准)"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="归档月份">
                  <a-input v-model="queryParam.separateMonth" allow-clear placeholder="请输入归档月份"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="归档日">
                  <a-input v-model="queryParam.separateDay" allow-clear placeholder="请输入归档日"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="是否启用 0:否 1:是">
                  <a-input v-model="queryParam.isEnable" allow-clear placeholder="请输入是否启用 0:否 1:是"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="更新时间">
                  <a-date-picker style="width: 100%" placeholder="请选择更新时间" v-model="queryParam.updateDateDate" @change="onChangeupdateDate"/>
                </a-form-item>
              </a-col>
              <a-col :md="8" :sm="24">
                <a-form-item label="创建时间">
                  <a-date-picker style="width: 100%" placeholder="请选择创建时间" v-model="queryParam.createDateDate" @change="onChangecreateDate"/>
                </a-form-item>
              </a-col>
            </template>
            <a-col :md="8" :sm="24" >
              <span class="table-page-search-submitButtons">
                <a-button type="primary" @click="$refs.table.refresh(true)" >查询</a-button>
                <a-button style="margin-left: 8px" @click="() => queryParam = {}">重置</a-button>
                <a @click="toggleAdvanced" style="margin-left: 8px">
                  {{ advanced ? '收起' : '展开' }}
                  <a-icon :type="advanced ? 'up' : 'down'"/>
                </a>
              </span>
            </a-col>
          </a-row>
        </a-form>
      </div>
    </a-card>
    <a-card :bordered="false">
      <s-table
        ref="table"
        :columns="columns"
        :data="loadData"
        :alert="options.alert"
        :rowKey="(record) => record.id"
        :rowSelection="options.rowSelection"
      >
        <template class="table-operator" slot="operator" v-if="hasPerm('blogArticle:add')" >
          <a-button type="primary" v-if="hasPerm('blogArticle:add')" icon="plus" @click="$refs.addForm.add()">新增blog文章主体</a-button>
          <a-button type="danger" :disabled="selectedRowKeys.length < 1" v-if="hasPerm('blogArticle:delete')" @click="batchDelete"><a-icon type="delete"/>批量删除</a-button>
          <x-down
            v-if="hasPerm('blogArticle:export')"
            ref="batchExport"
            @batchExport="batchExport"
          />
        </template>
        <span slot="action" slot-scope="text, record">
          <a v-if="hasPerm('blogArticle:edit')" @click="$refs.editForm.edit(record)">编辑</a>
          <a-divider type="vertical" v-if="hasPerm('blogArticle:edit') & hasPerm('blogArticle:delete')"/>
          <a-popconfirm v-if="hasPerm('blogArticle:delete')" placement="topRight" title="确认删除?" @confirm="() => singleDelete(record)">
            <a>删除</a>
          </a-popconfirm>
        </span>
      </s-table>
      <add-form ref="addForm" @ok="handleOk" />
      <edit-form ref="editForm" @ok="handleOk" />
    </a-card>
  </div>
</template>
<script>
  import { STable, XDown } from '@/components'
  import moment from 'moment'
  import { blogArticlePage, blogArticleDelete, blogArticleExport } from '@/api/modular/main/blogarticle/blogArticleManage'
  import addForm from './addForm.vue'
  import editForm from './editForm.vue'
  export default {
    components: {
      STable,
      addForm,
      editForm,
      XDown
    },
    data () {
      return {
        // 高级搜索 展开/关闭
        advanced: false,
        // 查询参数
        queryParam: {},
        // 表头
        columns: [
          {
            title: '文章标题',
            align: 'center',
            dataIndex: 'title'
          },
          {
            title: '文章文件id',
            align: 'center',
            dataIndex: 'articleFileId'
          },
          {
            title: '文件类型 1:markdown 2:html',
            align: 'center',
            dataIndex: 'articleFileType'
          },
          {
            title: '文章分类id 0:没有分类',
            align: 'center',
            dataIndex: 'articleTypeId'
          },
          {
            title: '文章引言',
            align: 'center',
            dataIndex: 'introduce'
          },
          {
            title: '封面文件地址(id)',
            align: 'center',
            dataIndex: 'coverFileId'
          },
          {
            title: '上次编辑时间',
            align: 'center',
            dataIndex: 'lastEditorDate'
          },
          {
            title: '发布时间',
            align: 'center',
            dataIndex: 'publishDate'
          },
          {
            title: '是否置顶 0:否 1:是',
            align: 'center',
            dataIndex: 'isTop'
          },
          {
            title: '置顶值(越小越靠前)',
            align: 'center',
            dataIndex: 'topValue'
          },
          {
            title: '公开状态 1:公开 2:私密 3:密码授权',
            align: 'center',
            dataIndex: 'authStatus'
          },
          {
            title: '授权密码(在密码授权状态时)',
            align: 'center',
            dataIndex: 'authPassword'
          },
          {
            title: '编辑状态 0:草稿 1:发布',
            align: 'center',
            dataIndex: 'editorStatus'
          },
          {
            title: '归档年份(以初次发布时间为准)',
            align: 'center',
            dataIndex: 'separateYear'
          },
          {
            title: '归档月份',
            align: 'center',
            dataIndex: 'separateMonth'
          },
          {
            title: '归档日',
            align: 'center',
            dataIndex: 'separateDay'
          },
          {
            title: '是否启用 0:否 1:是',
            align: 'center',
            dataIndex: 'isEnable'
          },
          {
            title: '更新时间',
            align: 'center',
            dataIndex: 'updateDate'
          },
          {
            title: '创建时间',
            align: 'center',
            dataIndex: 'createDate'
          }
        ],
        tstyle: { 'padding-bottom': '0px', 'margin-bottom': '10px' },
        // 加载数据方法 必须为 Promise 对象
        loadData: parameter => {
          return blogArticlePage(Object.assign(parameter, this.switchingDate())).then((res) => {
            return res.data
          })
        },
        selectedRowKeys: [],
        selectedRows: [],
        options: {
          alert: { show: true, clear: () => { this.selectedRowKeys = [] } },
          rowSelection: {
            selectedRowKeys: this.selectedRowKeys,
            onChange: this.onSelectChange
          }
        }
      }
    },
    created () {
      if (this.hasPerm('blogArticle:edit') || this.hasPerm('blogArticle:delete')) {
        this.columns.push({
          title: '操作',
          width: '150px',
          dataIndex: 'action',
          scopedSlots: { customRender: 'action' }
        })
      }
    },
    methods: {
      moment,
      /**
       * 查询参数组装
       */
      switchingDate () {
        const queryParamlastEditorDate = this.queryParam.lastEditorDateDate
        if (queryParamlastEditorDate != null) {
            this.queryParam.lastEditorDate = moment(queryParamlastEditorDate).format('YYYY-MM-DD')
            if (queryParamlastEditorDate.length < 1) {
                delete this.queryParam.lastEditorDate
            }
        }
        const queryParampublishDate = this.queryParam.publishDateDate
        if (queryParampublishDate != null) {
            this.queryParam.publishDate = moment(queryParampublishDate).format('YYYY-MM-DD')
            if (queryParampublishDate.length < 1) {
                delete this.queryParam.publishDate
            }
        }
        const queryParamupdateDate = this.queryParam.updateDateDate
        if (queryParamupdateDate != null) {
            this.queryParam.updateDate = moment(queryParamupdateDate).format('YYYY-MM-DD')
            if (queryParamupdateDate.length < 1) {
                delete this.queryParam.updateDate
            }
        }
        const queryParamcreateDate = this.queryParam.createDateDate
        if (queryParamcreateDate != null) {
            this.queryParam.createDate = moment(queryParamcreateDate).format('YYYY-MM-DD')
            if (queryParamcreateDate.length < 1) {
                delete this.queryParam.createDate
            }
        }
        const obj = JSON.parse(JSON.stringify(this.queryParam))
        return obj
      },
      /**
       * 单个删除
       */
      singleDelete (record) {
        const param = [{ 'id': record.id }]
        this.blogArticleDelete(param)
      },
      /**
       * 批量删除
       */
      batchDelete () {
        const paramIds = this.selectedRowKeys.map((d) => {
            return { 'id': d }
        })
        this.blogArticleDelete(paramIds)
      },
      blogArticleDelete (record) {
        blogArticleDelete(record).then((res) => {
          if (res.success) {
            this.$message.success('删除成功')
            this.$refs.table.clearRefreshSelected()
          } else {
            this.$message.error('删除失败') // + res.message
          }
        })
      },
      toggleAdvanced () {
        this.advanced = !this.advanced
      },
      onChangelastEditorDate(date, dateString) {
        this.lastEditorDateDateString = dateString
      },
      onChangepublishDate(date, dateString) {
        this.publishDateDateString = dateString
      },
      onChangeupdateDate(date, dateString) {
        this.updateDateDateString = dateString
      },
      onChangecreateDate(date, dateString) {
        this.createDateDateString = dateString
      },
      /**
       * 批量导出
       */
      batchExport () {
        const paramIds = this.selectedRowKeys.map((d) => {
            return { 'id': d }
        })
        blogArticleExport(paramIds).then((res) => {
            this.$refs.batchExport.downloadfile(res)
        })
      },
      handleOk () {
        this.$refs.table.refresh()
      },
      onSelectChange (selectedRowKeys, selectedRows) {
        this.selectedRowKeys = selectedRowKeys
        this.selectedRows = selectedRows
      }
    }
  }
</script>
<style lang="less">
  .table-operator {
    margin-bottom: 18px;
  }
  button {
    margin-right: 8px;
  }
</style>
_web/src/views/system/README.md
New file
@@ -0,0 +1 @@
/** 此文件夹下代码尽量不要动,底座升级直接覆盖替换 **/
_web/src/views/system/account/center/Index.vue
New file
@@ -0,0 +1,308 @@
<template>
  <div class="page-header-index-wide page-header-wrapper-grid-content-main">
    <a-row :gutter="24">
      <a-col :md="24" :lg="7">
        <a-card :bordered="false">
          <div class="account-center-avatarHolder">
            <div class="avatar">
              <img :src="avatar()">
            </div>
            <div class="username">{{ nickname() }}</div>
            <div class="bio">海纳百川,有容乃大</div>
          </div>
          <div class="account-center-detail">
            <p>
              <i class="title"></i>交互专家
            </p>
            <p>
              <i class="group"></i>蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED
            </p>
            <p>
              <i class="address"></i>
              <span>浙江省</span>
              <span>杭州市</span>
            </p>
          </div>
          <a-divider/>
          <div class="account-center-tags">
            <div class="tagsTitle">标签</div>
            <div>
              <template v-for="(tag, index) in tags">
                <a-tooltip v-if="tag.length > 20" :key="tag" :title="tag">
                  <a-tag
                    :key="tag"
                    :closable="index !== 0"
                  >{{ `${tag.slice(0, 20)}...` }}</a-tag>
                </a-tooltip>
                <a-tag
                  v-else
                  :key="tag"
                  :closable="index !== 0"
                >{{ tag }}</a-tag>
              </template>
              <a-input
                v-if="tagInputVisible"
                ref="tagInput"
                type="text"
                size="small"
                :style="{ width: '78px' }"
                :value="tagInputValue"
                @change="handleInputChange"
                @blur="handleTagInputConfirm"
                @keyup.enter="handleTagInputConfirm"
              />
              <a-tag v-else @click="showTagInput" style="background: #fff; borderStyle: dashed;">
                <a-icon type="plus"/>New Tag
              </a-tag>
            </div>
          </div>
          <a-divider :dashed="true"/>
          <div class="account-center-team">
            <div class="teamTitle">团队</div>
            <a-spin :spinning="teamSpinning">
              <div class="members">
                <a-row>
                  <a-col :span="12" v-for="(item, index) in teams" :key="index">
                    <a>
                      <a-avatar size="small" :src="item.avatar"/>
                      <span class="member">{{ item.name }}</span>
                    </a>
                  </a-col>
                </a-row>
              </div>
            </a-spin>
          </div>
        </a-card>
      </a-col>
      <a-col :md="24" :lg="17">
        <a-card
          style="width:100%"
          :bordered="false"
          :tabList="tabListNoTitle"
          :activeTabKey="noTitleKey"
          @tabChange="key => handleTabChange(key, 'noTitleKey')"
        >
          <article-page v-if="noTitleKey === 'article'"></article-page>
          <app-page v-else-if="noTitleKey === 'app'"></app-page>
          <project-page v-else-if="noTitleKey === 'project'"></project-page>
        </a-card>
      </a-col>
    </a-row>
  </div>
</template>
<script>
import { PageView, RouteView } from '@/layouts'
import { AppPage, ArticlePage, ProjectPage } from './page'
import { mapGetters } from 'vuex'
export default {
  components: {
    RouteView,
    PageView,
    AppPage,
    ArticlePage,
    ProjectPage
  },
  data () {
    return {
      tags: ['很有想法的', '专注设计', '辣~', '大长腿', '川妹子', '海纳百川'],
      tagInputVisible: false,
      tagInputValue: '',
      teams: [],
      teamSpinning: true,
      tabListNoTitle: [
        {
          key: 'article',
          tab: '文章(8)'
        },
        {
          key: 'app',
          tab: '应用(8)'
        },
        {
          key: 'project',
          tab: '项目(8)'
        }
      ],
      noTitleKey: 'app'
    }
  },
  mounted () {
    this.getTeams()
  },
  methods: {
    ...mapGetters(['nickname', 'avatar']),
    getTeams () {
      this.teams = [{
        id: 1,
        name: '科学搬砖组',
        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png'
      },
        {
          id: 2,
          name: '程序员日常',
          avatar: 'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png'
        },
        {
          id: 1,
          name: '设计天团',
          avatar: 'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png'
        },
        {
          id: 1,
          name: '中二少女团',
          avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png'
        },
        {
          id: 1,
          name: '骗你学计算机',
          avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png'
        }
      ]
      this.teamSpinning = false
    },
    handleTabChange (key, type) {
      this[type] = key
    },
    handleTagClose (removeTag) {
      const tags = this.tags.filter(tag => tag !== removeTag)
      this.tags = tags
    },
    showTagInput () {
      this.tagInputVisible = true
      this.$nextTick(() => {
        this.$refs.tagInput.focus()
      })
    },
    handleInputChange (e) {
      this.tagInputValue = e.target.value
    },
    handleTagInputConfirm () {
      const inputValue = this.tagInputValue
      let tags = this.tags
      if (inputValue && !tags.includes(inputValue)) {
        tags = [...tags, inputValue]
      }
      Object.assign(this, {
        tags,
        tagInputVisible: false,
        tagInputValue: ''
      })
    }
  }
}
</script>
<style lang="less" scoped>
.page-header-wrapper-grid-content-main {
  width: 100%;
  height: 100%;
  min-height: 100%;
  transition: 0.3s;
  .account-center-avatarHolder {
    text-align: center;
    margin-bottom: 24px;
    & > .avatar {
      margin: 0 auto;
      width: 104px;
      height: 104px;
      margin-bottom: 20px;
      border-radius: 50%;
      overflow: hidden;
      img {
        height: 100%;
        width: 100%;
      }
    }
    .username {
      color: rgba(0, 0, 0, 0.85);
      font-size: 20px;
      line-height: 28px;
      font-weight: 500;
      margin-bottom: 4px;
    }
  }
  .account-center-detail {
    p {
      margin-bottom: 8px;
      padding-left: 26px;
      position: relative;
    }
    i {
      position: absolute;
      height: 14px;
      width: 14px;
      left: 0;
      top: 4px;
      background: url(https://gw.alipayobjects.com/zos/rmsportal/pBjWzVAHnOOtAUvZmZfy.svg);
    }
    .title {
      background-position: 0 0;
    }
    .group {
      background-position: 0 -22px;
    }
    .address {
      background-position: 0 -44px;
    }
  }
  .account-center-tags {
    .ant-tag {
      margin-bottom: 8px;
    }
  }
  .account-center-team {
    .members {
      a {
        display: block;
        margin: 12px 0;
        line-height: 24px;
        height: 24px;
        .member {
          font-size: 14px;
          color: rgba(0, 0, 0, 0.65);
          line-height: 24px;
          max-width: 100px;
          vertical-align: top;
          margin-left: 12px;
          transition: all 0.3s;
          display: inline-block;
        }
        &:hover {
          span {
            color: #1890ff;
          }
        }
      }
    }
  }
  .tagsTitle,
  .teamTitle {
    font-weight: 500;
    color: rgba(0, 0, 0, 0.85);
    margin-bottom: 12px;
  }
}
</style>
_web/src/views/system/account/center/page/App.vue
New file
@@ -0,0 +1,113 @@
<template>
  <div class="app-list">
    <a-list
      :grid="{ gutter: 24, lg: 3, md: 2, sm: 1, xs: 1 }"
      :dataSource="dataSource">
      <a-list-item slot="renderItem" slot-scope="item">
        <a-card :hoverable="true">
          <a-card-meta>
            <div style="margin-bottom: 3px" slot="title">{{ item.title }}</div>
            <a-avatar class="card-avatar" slot="avatar" :src="item.avatar" size="small"/>
            <div class="meta-cardInfo" slot="description">
              <div>
                <p>活跃用户</p>
                <p>
                  <span>{{ item.activeUser }}<span>万</span></span>
                </p>
              </div>
              <div>
                <p>新增用户</p>
                <p>{{ item.newUser | NumberFormat }}</p>
              </div>
            </div>
          </a-card-meta>
          <template class="ant-card-actions" slot="actions">
            <a>
              <a-icon type="download"/>
            </a>
            <a>
              <a-icon type="edit"/>
            </a>
            <a>
              <a-icon type="share-alt"/>
            </a>
            <a>
              <a-dropdown>
                <a class="ant-dropdown-link" href="javascript:;">
                  <a-icon type="ellipsis"/>
                </a>
                <a-menu slot="overlay">
                  <a-menu-item>
                    <a href="javascript:;">1st menu item</a>
                  </a-menu-item>
                  <a-menu-item>
                    <a href="javascript:;">2nd menu item</a>
                  </a-menu-item>
                  <a-menu-item>
                    <a href="javascript:;">3rd menu item</a>
                  </a-menu-item>
                </a-menu>
              </a-dropdown>
            </a>
          </template>
        </a-card>
      </a-list-item>
    </a-list>
  </div>
</template>
<script>
const dataSource = []
for (let i = 0; i < 11; i++) {
  dataSource.push({
    title: 'Alipay',
    avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png',
    activeUser: 17,
    newUser: 1700
  })
}
export default {
  name: 'Article',
  components: {},
  data () {
    return {
      dataSource
    }
  }
}
</script>
<style lang="less" scoped>
  .app-list {
    .meta-cardInfo {
      zoom: 1;
      margin-top: 16px;
      > div {
        position: relative;
        text-align: left;
        float: left;
        width: 50%;
        p {
          line-height: 32px;
          font-size: 24px;
          margin: 0;
          &:first-child {
            color: rgba(0, 0, 0, .45);
            font-size: 12px;
            line-height: 20px;
            margin-bottom: 4px;
          }
        }
      }
    }
  }
</style>
_web/src/views/system/account/center/page/Article.vue
New file
@@ -0,0 +1,91 @@
<template>
  <a-list
    size="large"
    rowKey="id"
    :loading="loading"
    itemLayout="vertical"
    :dataSource="data"
  >
    <a-list-item :key="item.id" slot="renderItem" slot-scope="item">
      <a-list-item-meta>
        <a slot="title" href="https://vue.ant.design/">{{ item.title }}</a>
        <template slot="description">
          <span>
            <a-tag>Ant Design</a-tag>
            <a-tag>设计语言</a-tag>
            <a-tag>蚂蚁金服</a-tag>
          </span>
        </template>
      </a-list-item-meta>
      <article-list-content :description="item.description" :owner="item.owner" :avatar="item.avatar" :href="item.href" :updateAt="item.updatedAt" />
    </a-list-item>
    <div slot="footer" v-if="data.length > 0" style="text-align: center; margin-top: 16px;">
      <a-button @click="loadMore" :loading="loadingMore">加载更多</a-button>
    </div>
  </a-list>
</template>
<script>
import { ArticleListContent } from '@/components'
export default {
  name: 'Article',
  components: {
    ArticleListContent
  },
  data () {
    return {
      loading: true,
      loadingMore: false,
      data: []
    }
  },
  mounted () {
    this.getList()
  },
  methods: {
    getList () {
      this.data = [
        {
          updatedAt: '2021-05-01 12:00:00',
          title: '小诺',
          owner: '俞宝山',
          description: 'snowy是小诺团队产品',
          href: 'https://xiaonuo.vip',
          avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
          name: '曲丽丽',
          id: 'member1'
        },
        {
          updatedAt: '2021-05-01 12:00:00',
          title: '小诺',
          owner: '徐玉祥',
          description: 'snowy是小诺团队产品',
          href: 'https://xiaonuo.vip',
          avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
          name: '王昭君',
          id: 'member2'
        },
        {
          updatedAt: '2021-05-01 12:00:00',
          title: '小诺',
          owner: '董夏雨',
          description: 'snowy是小诺团队产品',
          href: 'https://xiaonuo.vip',
          avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
          name: '董娜娜',
          id: 'member3'
        }
      ]
      this.loading = false
    },
    loadMore () {
      this.loadingMore = false
    }
  }
}
</script>
<style scoped>
</style>
_web/src/views/system/account/center/page/Project.vue
New file
@@ -0,0 +1,215 @@
<template>
  <div class="ant-pro-pages-account-projects-cardList">
    <a-list :loading="loading" :data-source="data" :grid="{ gutter: 24, xxl: 3, xl: 2, lg: 2, md: 2, sm: 2, xs: 1 }">
      <a-list-item slot="renderItem" slot-scope="item">
        <a-card class="ant-pro-pages-account-projects-card" hoverable>
          <img slot="cover" :src="item.cover" :alt="item.title" />
          <a-card-meta :title="item.title">
            <template slot="description">
              <ellipsis :length="50">{{ item.description }}</ellipsis>
            </template>
          </a-card-meta>
          <div class="cardItemContent">
            <span>{{ item.updatedAt | fromNow }}</span>
            <div class="avatarList">
              <avatar-list size="mini">
                <avatar-list-item
                  v-for="(member, i) in item.members"
                  :key="`${item.id}-avatar-${i}`"
                  :src="member.avatar"
                  :tips="member.name"
                />
              </avatar-list>
            </div>
          </div>
        </a-card>
      </a-list-item>
    </a-list>
  </div>
</template>
<script>
import moment from 'moment'
import { TagSelect, StandardFormRow, Ellipsis, AvatarList } from '@/components'
const TagSelectOption = TagSelect.Option
const AvatarListItem = AvatarList.AvatarItem
export default {
  name: 'Project',
  components: {
    AvatarList,
    AvatarListItem,
    Ellipsis,
    TagSelect,
    TagSelectOption,
    StandardFormRow
  },
  data () {
    return {
      data: [],
      form: this.$form.createForm(this),
      loading: true
    }
  },
  filters: {
    fromNow (date) {
      return moment(date).fromNow()
    }
  },
  mounted () {
    this.getList()
  },
  methods: {
    handleChange (value) {
      console.log(`selected ${value}`)
    },
    getList () {
      this.data = [
        {
          id: '123',
          cover: 'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png',
          content: '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
          message: '消息',
          description: '小诺框架产品',
          href: 'https://xiaonuo.vip',
          title: '小诺',
          updatedAt: '2021-05-01 12:00:00',
          members: [
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
              name: '曲丽丽',
              id: 'member1'
            },
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
              name: '王昭君',
              id: 'member2'
            },
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
              name: '董娜娜',
              id: 'member3'
            }
          ]
        },
        {
          id: '1234',
          cover: 'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png',
          content: '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
          message: '消息',
          description: '小诺框架产品',
          href: 'https://xiaonuo.vip',
          title: '小诺',
          updatedAt: '2021-05-01 12:00:00',
          members: [
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
              name: '曲丽丽',
              id: 'member1'
            },
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
              name: '王昭君',
              id: 'member2'
            },
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
              name: '董娜娜',
              id: 'member3'
            }
          ]
        },
        {
          id: '12345',
          cover: 'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png',
          content: '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
          message: '消息',
          description: '小诺框架产品',
          href: 'https://xiaonuo.vip',
          title: '小诺',
          updatedAt: '2021-05-01 12:00:00',
          members: [
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
              name: '曲丽丽',
              id: 'member1'
            },
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
              name: '王昭君',
              id: 'member2'
            },
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
              name: '董娜娜',
              id: 'member3'
            }
          ]
        },
        {
          id: '1236',
          cover: 'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
          content: '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
          message: '消息',
          description: '小诺框架产品',
          href: 'https://xiaonuo.vip',
          title: '小诺',
          updatedAt: '2021-05-01 12:00:00',
          members: [
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
              name: '曲丽丽',
              id: 'member1'
            },
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
              name: '王昭君',
              id: 'member2'
            },
            {
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
              name: '董娜娜',
              id: 'member3'
            }
          ]
        }
      ]
      this.loading = false
    }
  }
}
</script>
<style lang="less" scoped>
  .ant-pro-pages-account-projects-cardList {
    margin-top: 24px;
    /deep/ .ant-card-meta-title {
      margin-bottom: 4px;
    }
    /deep/ .ant-card-meta-description {
      height: 44px;
      overflow: hidden;
      line-height: 22px;
    }
    .cardItemContent {
      display: flex;
      height: 20px;
      margin-top: 16px;
      margin-bottom: -4px;
      line-height: 20px;
      > span {
        flex: 1 1;
        color: rgba(0,0,0,.45);
        font-size: 12px;
      }
      /deep/ .ant-pro-avatar-list {
        flex: 0 1 auto;
      }
    }
  }
</style>
_web/src/views/system/account/center/page/index.js
New file
@@ -0,0 +1,5 @@
import AppPage from './App'
import ArticlePage from './Article'
import ProjectPage from './Project'
export { AppPage, ArticlePage, ProjectPage }
_web/src/views/system/account/settings/AvatarModal.vue
New file
@@ -0,0 +1,183 @@
<template>
  <a-modal
    title="修改头像"
    :visible="visible"
    :maskClosable="false"
    :confirmLoading="confirmLoading"
    :width="800"
    :footer="null"
    @cancel="cancelHandel">
    <a-row>
      <a-col :xs="24" :md="12" :style="{height: '350px'}">
        <vue-cropper
          ref="cropper"
          :img="options.img"
          :info="true"
          :autoCrop="options.autoCrop"
          :autoCropWidth="options.autoCropWidth"
          :autoCropHeight="options.autoCropHeight"
          :fixedBox="options.fixedBox"
          @realTime="realTime"
        >
        </vue-cropper>
      </a-col>
      <a-col :xs="24" :md="12" :style="{height: '350px'}">
        <div class="avatar-upload-preview">
          <img :src="previews.url" :style="previews.img"/>
        </div>
      </a-col>
    </a-row>
    <br>
    <a-row>
      <a-col :lg="2" :md="2">
        <a-upload name="file" :beforeUpload="beforeUpload" :showUploadList="false">
          <a-button icon="upload">选择图片</a-button>
        </a-upload>
      </a-col>
      <a-col :lg="{span: 1, offset: 2}" :md="2">
        <a-button icon="plus" @click="changeScale(1)"/>
      </a-col>
      <a-col :lg="{span: 1, offset: 1}" :md="2">
        <a-button icon="minus" @click="changeScale(-1)"/>
      </a-col>
      <a-col :lg="{span: 1, offset: 1}" :md="2">
        <a-button icon="undo" @click="rotateLeft"/>
      </a-col>
      <a-col :lg="{span: 1, offset: 1}" :md="2">
        <a-button icon="redo" @click="rotateRight"/>
      </a-col>
      <a-col :lg="{span: 2, offset: 6}" :md="2">
        <a-button type="primary" @click="finish('blob')" :loading="uploading">保存</a-button>
      </a-col>
    </a-row>
  </a-modal>
</template>
<script>
  import { sysFileInfoUpload } from '@/api/modular/system/fileManage'
  import { sysUserUpdateAvatar } from '@/api/modular/system/userManage'
  export default {
  data () {
    return {
      visible: false,
      id: null,
      confirmLoading: false,
      fileList: [],
      uploading: false,
      options: {
        img: '',
        autoCrop: true,
        autoCropWidth: 200,
        autoCropHeight: 200,
        fixedBox: true
      },
      previews: {}
    }
  },
  methods: {
    edit (id) {
      this.visible = true
      this.id = id
      /* 获取原始头像 */
    },
    close () {
      this.id = null
      this.visible = false
    },
    cancelHandel () {
      this.close()
    },
    changeScale (num) {
      num = num || 1
      this.$refs.cropper.changeScale(num)
    },
    rotateLeft () {
      this.$refs.cropper.rotateLeft()
    },
    rotateRight () {
      this.$refs.cropper.rotateRight()
    },
    beforeUpload (file) {
      this.fileList = file
      const reader = new FileReader()
      // 把Array Buffer转化为blob 如果是base64不需要
      // 转化为base64
      reader.readAsDataURL(file)
      reader.onload = () => {
        this.options.img = reader.result
      }
      // 转化为blob
      // reader.readAsArrayBuffer(file)
      return false
    },
    // 上传图片(点击上传按钮)
    finish (type) {
      if (type === 'blob') {
        this.uploading = true
        this.$refs.cropper.getCropBlob((data) => {
          const files = new window.File(
            [data],
            this.fileList.name,
            { type: this.fileList.type }
          )
          const formData = new FormData()
          formData.append('file', files)
          sysFileInfoUpload(formData).then((res) => {
            if (res.success) {
              this.updateAvatar(res.data)
              this.$emit('ok', res.data)
            } else {
              this.uploading = false
              this.$message.error(res.message)
            }
          })
        })
      } else {
        this.$refs.cropper.getCropData((data) => {
          console.log(data)
        })
      }
    },
    updateAvatar (data) {
      const params = {
        id: this.id,
        avatar: data
      }
      sysUserUpdateAvatar(params).then((res) => {
        this.uploading = false
        if (res.success) {
          this.visible = false
          this.$message.success('头像上传修改成功')
        } else {
          this.$message.error(res.message)
        }
      })
    },
    realTime (data) {
      this.previews = data
    }
  }
}
</script>
<style lang="less" scoped>
  .avatar-upload-preview {
    position: absolute;
    top: 50%;
    transform: translate(50%, -50%);
    width: 200px;
    height: 200px;
    border-radius: 50%;
    box-shadow: 0 0 4px #ccc;
    overflow: hidden;
    img {
      width: 100%;
      height: 100%;
    }
  }
</style>
_web/src/views/system/account/settings/BaseSetting.vue
New file
@@ -0,0 +1,216 @@
<template>
  <div class="account-settings-info-view">
    <a-row :gutter="16">
      <a-col :md="24" :lg="16">
        <a-form layout="vertical" :form="form">
          <a-form-item
            label="昵称"
          >
            <a-input placeholder="给自己起个昵称吧" v-decorator="['nickName']"/>
          </a-form-item>
          <a-form-item
            label="生日"
          >
            <a-date-picker placeholder="请选择生日" @change="onChange" style="width: 100%" v-decorator="['birthday', {rules: [{required: true, message: '请选择生日!'}]}]" />
          </a-form-item>
          <a-form-item
            label="性别"
          >
            <a-radio-group v-decorator="['sex',{rules: [{ required: true, message: '请选择性别!' }]}]" >
              <a-radio v-for="(item,index) in sexData" :key="index" :value="item.code">{{ item.name }}</a-radio>
            </a-radio-group>
          </a-form-item>
          <a-form-item
            label="手机"
          >
            <a-input placeholder="请输入手机号" v-decorator="['phone', {rules: [{required: true, message: '请输入手机号!'}]}]"/>
          </a-form-item>
          <a-form-item
            label="电话"
          >
            <a-input placeholder="请输入电话" v-decorator="['tel', {rules: [{required: true, message: '请输入电话!'}]}]"/>
          </a-form-item>
          <a-form-item
            label="电子邮件"
            :required="false"
          >
            <a-input placeholder="请输入电子邮件地址" v-decorator="['email', {type: 'email',message: '请输入正确的邮箱号',rules: [{required: true, message: '请输入正确的邮箱号!'}]}]"/>
          </a-form-item>
          <a-form-item>
            <a-button type="primary" @click="submitUserInfo">更新基本信息</a-button>
          </a-form-item>
        </a-form>
      </a-col>
      <a-col :md="24" :lg="8" :style="{ minHeight: '180px' }">
        <div class="ant-upload-preview" @click="$refs.modal.edit(userInfo.id)" >
          <a-icon type="cloud-upload-o" class="upload-icon"/>
          <div class="mask">
            <a-icon type="plus" />
          </div>
          <img :src="option.img"/>
        </div>
      </a-col>
    </a-row>
    <avatar-modal ref="modal" @ok="setavatar"/>
  </div>
</template>
<script>
  import store from '@/store'
  import AvatarModal from './AvatarModal'
  import { mapGetters } from 'vuex'
  import moment from 'moment'
  import { sysUserUpdateInfo } from '@/api/modular/system/userManage'
// mapActions
export default {
  components: {
    AvatarModal
  },
  data () {
    return {
      // cropper
      preview: {},
      form: this.$form.createForm(this),
      sexData: [],
      option: {
        img: '/avatar2.jpg',
        info: true,
        size: 1,
        outputType: 'jpeg',
        canScale: false,
        autoCrop: true,
        // 只有自动截图开启 宽度高度才生效
        autoCropWidth: 180,
        autoCropHeight: 180,
        fixedBox: true,
        // 开启宽度和高度比例
        fixed: true,
        fixedNumber: [1, 1],
        // userInfo
        birthdayString: ''
      }
    }
  },
  computed: {
    ...mapGetters(['userInfo'])
  },
  mounted () {
    this.initUserInfo()
  },
  methods: {
    // ...mapActions(['GetInfo']),
    /**
     * 初始化用户信息
     */
    initUserInfo () {
      setTimeout(() => {
        this.form.setFieldsValue(
          {
            birthday: moment(this.userInfo.birthday, 'YYYY-MM-DD'),
            nickName: this.userInfo.nickName,
            sex: this.userInfo.sex.toString(),
            email: this.userInfo.email,
            phone: this.userInfo.phone,
            tel: this.userInfo.tel
          }
        )
        this.birthdayString = moment(this.userInfo.birthday).format('YYYY-MM-DD')
        this.option.img = process.env.VUE_APP_API_BASE_URL + '/sysFileInfo/preview?id=' + this.userInfo.avatar
        this.getSexData()
      }, 100)
    },
    /**
     * 日期需单独转换
     */
    onChange (date, dateString) {
      this.birthdayString = dateString
    },
    submitUserInfo () {
      const { form: { validateFields } } = this
      validateFields((err, values) => {
        if (!err) {
          values.birthday = this.birthdayString
          values.id = this.userInfo.id
          sysUserUpdateInfo(values).then((res) => {
            if (res.success) {
              this.$message.success('个人信息更新成功')
              store.dispatch('GetInfo').then(() => {})
            } else {
              this.$message.error(res.message)
            }
          })
        }
      })
    },
    getSexData () {
      this.sexData = this.$options.filters['dictData']('sex')
    },
    setavatar (url) {
      this.option.img = process.env.VUE_APP_API_BASE_URL + '/sysFileInfo/preview?id=' + url
      store.dispatch('GetInfo').then(() => {})
    }
  }
}
</script>
<style lang="less" scoped>
  .avatar-upload-wrapper {
    height: 200px;
    width: 100%;
  }
  .ant-upload-preview {
    position: relative;
    margin: 0 auto;
    width: 100%;
    max-width: 180px;
    border-radius: 50%;
    box-shadow: 0 0 4px #ccc;
    .upload-icon {
      position: absolute;
      top: 0;
      right: 10px;
      font-size: 1.4rem;
      padding: 0.5rem;
      background: rgba(222, 221, 221, 0.7);
      border-radius: 50%;
      border: 1px solid rgba(0, 0, 0, 0.2);
    }
    .mask {
      opacity: 0;
      position: absolute;
      background: rgba(0,0,0,0.4);
      cursor: pointer;
      transition: opacity 0.4s;
      &:hover {
        opacity: 1;
      }
      i {
        font-size: 2rem;
        position: absolute;
        top: 50%;
        left: 50%;
        margin-left: -1rem;
        margin-top: -1rem;
        color: #d6d6d6;
      }
    }
    img, .mask {
      width: 100%;
      max-width: 180px;
      height: 100%;
      border-radius: 50%;
      overflow: hidden;
    }
  }
</style>
_web/src/views/system/account/settings/Binding.vue
New file
@@ -0,0 +1,25 @@
<template>
  <a-list
    itemLayout="horizontal"
    :dataSource="data"
  >
  </a-list>
</template>
<script>
export default {
  data () {
    return {
      data: []
    }
  },
  methods: {
  }
}
</script>
<style scoped>
</style>
_web/src/views/system/account/settings/Custom.vue
New file
@@ -0,0 +1,75 @@
<script>
import { colorList } from '@/components/SettingDrawer/settingConfig'
import ASwitch from 'ant-design-vue/es/switch'
import AList from 'ant-design-vue/es/list'
import AListItem from 'ant-design-vue/es/list/Item'
import { mixin } from '@/utils/mixin'
const Meta = AListItem.Meta
export default {
  components: {
    AListItem,
    AList,
    ASwitch,
    Meta
  },
  mixins: [mixin],
  data () {
    return {
    }
  },
  filters: {
    themeFilter (theme) {
      const themeMap = {
        'dark': '暗色',
        'light': '白色'
      }
      return themeMap[theme]
    }
  },
  methods: {
    colorFilter (color) {
      const c = colorList.find(o => o.color === color)
      return c && c.key
    },
    onChange (checked) {
      if (checked) {
        this.$store.dispatch('ToggleTheme', 'dark')
      } else {
        this.$store.dispatch('ToggleTheme', 'light')
      }
    }
  },
  render () {
    return (
      <AList itemLayout="horizontal">
        <AListItem>
          <Meta>
            <a slot="title">风格配色</a>
            <span slot="description">
                整体风格配色设置
            </span>
          </Meta>
          <div slot="actions">
            <ASwitch checkedChildren="暗色" unCheckedChildren="白色" defaultChecked={this.navTheme === 'dark' && true || false} onChange={this.onChange} />
          </div>
        </AListItem>
        <AListItem>
          <Meta>
            <a slot="title">主题色</a>
            <span slot="description">
                页面风格配色: <a domPropsInnerHTML={ this.colorFilter(this.primaryColor) }/>
            </span>
          </Meta>
        </AListItem>
      </AList>
    )
  }
}
</script>
<style scoped>
</style>
_web/src/views/system/account/settings/Index.vue
New file
@@ -0,0 +1,155 @@
<template>
  <div class="page-header-index-wide">
    <a-card :bordered="false" :bodyStyle="{ padding: '16px 0', height: '100%' }" :style="{ height: '100%' }">
      <div class="account-settings-info-main" :class="device">
        <div class="account-settings-info-left">
          <a-menu
            :mode="device == 'mobile' ? 'horizontal' : 'inline'"
            :style="{ border: '0', width: device == 'mobile' ? '560px' : 'auto'}"
            :selectedKeys="selectedKeys"
            type="inner"
            @openChange="onOpenChange"
          >
            <a-menu-item key="/account/settings/base">
              <router-link :to="{ name: 'BaseSettings' }">
                基本设置
              </router-link>
            </a-menu-item>
            <a-menu-item key="/account/settings/security">
              <router-link :to="{ name: 'SecuritySettings' }">
                安全设置
              </router-link>
            </a-menu-item>
            <a-menu-item key="/account/settings/custom">
              <router-link :to="{ name: 'CustomSettings' }">
                个性化
              </router-link>
            </a-menu-item>
            <a-menu-item key="/account/settings/binding">
              <router-link :to="{ name: 'BindingSettings' }">
                账户绑定
              </router-link>
            </a-menu-item>
            <a-menu-item key="/account/settings/notification">
              <router-link :to="{ name: 'NotificationSettings' }">
                新消息通知
              </router-link>
            </a-menu-item>
          </a-menu>
        </div>
        <div class="account-settings-info-right">
          <div class="account-settings-info-title">
            <span>{{ $route.meta.title }}</span>
          </div>
          <route-view></route-view>
        </div>
      </div>
    </a-card>
  </div>
</template>
<script>
import { PageView, RouteView } from '@/layouts'
import { mixinDevice } from '@/utils/mixin.js'
export default {
  components: {
    RouteView,
    PageView
  },
  mixins: [mixinDevice],
  data () {
    return {
      // horizontal  inline
      mode: 'inline',
      openKeys: [],
      selectedKeys: [],
      // cropper
      preview: {},
      option: {
        img: '/avatar2.jpg',
        info: true,
        size: 1,
        outputType: 'jpeg',
        canScale: false,
        autoCrop: true,
        // 只有自动截图开启 宽度高度才生效
        autoCropWidth: 180,
        autoCropHeight: 180,
        fixedBox: true,
        // 开启宽度和高度比例
        fixed: true,
        fixedNumber: [1, 1]
      },
      pageTitle: ''
    }
  },
  mounted () {
    this.updateMenu()
  },
  methods: {
    onOpenChange (openKeys) {
      this.openKeys = openKeys
    },
    updateMenu () {
      const routes = this.$route.matched.concat()
      this.selectedKeys = [ routes.pop().path ]
    }
  },
  watch: {
    '$route' (val) {
      this.updateMenu()
    }
  }
}
</script>
<style lang="less" scoped>
  .account-settings-info-main {
    width: 100%;
    display: flex;
    height: 100%;
    overflow: auto;
    &.mobile {
      display: block;
      .account-settings-info-left {
        border-right: unset;
        border-bottom: 1px solid #e8e8e8;
        width: 100%;
        height: 50px;
        overflow-x: auto;
        overflow-y: scroll;
      }
      .account-settings-info-right {
        padding: 20px 40px;
      }
    }
    .account-settings-info-left {
      border-right: 1px solid #e8e8e8;
      width: 224px;
    }
    .account-settings-info-right {
      flex: 1 1;
      padding: 8px 40px;
      .account-settings-info-title {
        color: rgba(0,0,0,.85);
        font-size: 20px;
        font-weight: 500;
        line-height: 28px;
        margin-bottom: 12px;
      }
      .account-settings-info-view {
        padding-top: 12px;
      }
    }
  }
</style>
_web/src/views/system/account/settings/Notification.vue
New file
@@ -0,0 +1,25 @@
<template>
  <a-list
    itemLayout="horizontal"
    :dataSource="data"
  >
  </a-list>
</template>
<script>
export default {
  data () {
    return {
      data: []
    }
  },
  methods: {
  }
}
</script>
<style scoped>
</style>
_web/src/views/system/account/settings/Security.vue
New file
@@ -0,0 +1,70 @@
<template>
  <div>
    <a-list
      itemLayout="horizontal"
      :dataSource="data"
    >
      <a-list-item slot="renderItem" slot-scope="item, MenuIndex" :key="MenuIndex">
        <a-list-item-meta>
          <a slot="title">{{ item.title }}</a>
          <span slot="description">
            <span class="security-list-description">{{ item.description }}</span>
            <span v-if="item.value"> : </span>
            <span class="security-list-value">{{ item.value }}</span>
          </span>
        </a-list-item-meta>
        <template v-if="item.actions">
          <a slot="actions" @click="item.actions.callback">{{ item.actions.title }}</a>
        </template>
      </a-list-item>
    </a-list>
    <upd-pwd ref="updPwd"/>
  </div>
</template>
<script>
  import { mapGetters } from 'vuex'
  import UpdPwd from './securityItem/updPwd'
  export default {
    components: {
      UpdPwd
    },
    data () {
      return {
        data: []
      }
    },
    created () {
      if (this.hasPerm('sysUser:updatePwd')) {
        const updPwdMenu = {
          title: '账户密码',
          description: '当前密码强度',
          value: '强',
          actions: { title: '修改',
            callback: () => {
              this.$refs.updPwd.open(this.userInfo.id)
            }
          }
        }
        this.data.push(updPwdMenu)
      }
      const encryptedPhone = { title: '密保手机', description: '已绑定手机', value: '138****8293', actions: { title: '修改', callback: () => { this.$message.success('This is a message of success') } } }
      const encryptedProblem = { title: '密保问题', description: '未设置密保问题,密保问题可有效保护账户安全', value: '', actions: { title: '设置', callback: () => { this.$message.error('This is a message of error') } } }
      const encryptedEmail = { title: '备用邮箱', description: '已绑定邮箱', value: 'ant***sign.com', actions: { title: '修改', callback: () => { this.$message.warning('This is message of warning') } } }
      const encryptedMfa = { title: 'MFA 设备', description: '未绑定 MFA 设备,绑定后,可以进行二次确认', value: '', actions: { title: '绑定', callback: () => { this.$message.info('This is a normal message') } } }
      this.data.push(encryptedPhone)
      this.data.push(encryptedProblem)
      this.data.push(encryptedEmail)
      this.data.push(encryptedMfa)
    },
    computed: {
      ...mapGetters(['userInfo'])
    },
    methods: {
    }
  }
</script>
<style scoped>
</style>
_web/src/views/system/account/settings/securityItem/updPwd.vue
New file
@@ -0,0 +1,115 @@
<template>
  <a-modal
    title="修改密码"
    :visible="visible_updPwd"
    :confirm-loading="confirmLoading"
    @ok="handleOkUpdPwd"
    @cancel="handleCancel"
  >
    <a-form :form="formUpdPwd">
      <a-form-item
        label="原密码"
        :labelCol="labelCol"
        :wrapperCol="wrapperCol"
        has-feedback
      >
        <a-input placeholder="请输入原密码" type="password" v-decorator="['password', {rules: [{required: true, message: '请输入原密码!'}]}]" />
      </a-form-item>
      <a-form-item
        label="新密码"
        :labelCol="labelCol"
        :wrapperCol="wrapperCol"
        has-feedback
      >
        <a-input
          placeholder="请输入新密码"
          type="password"
          v-decorator="['newPassword', {rules: [{required: true, message: '请输入新密码!'},{
            validator: validateToNextPassword,
          },]}]" />
      </a-form-item>
      <a-form-item
        label="重复新密码"
        :labelCol="labelCol"
        :wrapperCol="wrapperCol"
        has-feedback
      >
        <a-input
          placeholder="请再次输入新密码"
          type="password"
          v-decorator="['confirm', {rules: [{required: true, message: '请再次输入新密码!'},
                                            {
                                              validator: compareToFirstPassword,
                                            }]}]" />
      </a-form-item>
    </a-form>
  </a-modal>
</template>
<script>
  import { sysUserUpdatePwd } from '@/api/modular/system/userManage'
  export default {
    data () {
      return {
        labelCol: {
          xs: { span: 24 },
          sm: { span: 5 }
        },
        wrapperCol: {
          xs: { span: 24 },
          sm: { span: 16 }
        },
        confirmLoading: false,
        visible_updPwd: false,
        userId: '',
        formUpdPwd: this.$form.createForm(this)
      }
    },
    methods: {
      open (id) {
        this.userId = id
        this.visible_updPwd = true
      },
      handleOkUpdPwd () {
        const { formUpdPwd: { validateFields } } = this
        validateFields((errors, values) => {
          if (!errors) {
            this.confirmLoading = true
            values.id = this.userId
            sysUserUpdatePwd(values).then((res) => {
              if (res.success) {
                this.$message.success('修改成功')
                this.handleCancel()
              } else {
                this.$message.error('修改失败:' + res.message)
              }
            // eslint-disable-next-line handle-callback-err
            }).finally((err) => {
              this.confirmLoading = false
            })
          }
        })
      },
      handleCancel () {
        this.visible_updPwd = false
      },
      compareToFirstPassword (rule, value, callback) {
        const formUpdPwd = this.formUpdPwd
        if (value && value !== formUpdPwd.getFieldValue('newPassword')) {
          // eslint-disable-next-line standard/no-callback-literal
          callback('请确认两次输入密码的一致性!')
        } else {
          callback()
        }
      },
      validateToNextPassword (rule, value, callback) {
        const formUpdPwd = this.formUpdPwd
        if (value && this.confirmDirty) {
          formUpdPwd.validateFields(['confirm'], { force: true })
        }
        callback()
      }
    }
  }
</script>
_web/src/views/system/app/addForm.vue
New file
@@ -0,0 +1,91 @@
<template>
  <a-modal
    title="新增应用"
    :width="500"
    :visible="visible"
    :confirmLoading="confirmLoading"
    @ok="handleSubmit"
    @cancel="handleCancel"
  >
    <a-spin :spinning="confirmLoading">
      <a-form :form="form">
        <a-form-item
          style="display: none;"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
        >
          <a-input v-decorator="['active']" />
        </a-form-item>
        <a-form-item
          label="应用名称"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入应用名称" v-decorator="['name', {rules: [{required: true, message: '请输入应用名称!'}]}]" />
        </a-form-item>
        <a-form-item
          label="唯一编码"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入唯一编码" v-decorator="['code', {rules: [{required: true, message: '请输入唯一编码!'}]}]" />
        </a-form-item>
      </a-form>
    </a-spin>
  </a-modal>
</template>
<script>
  import { sysAppAdd } from '@/api/modular/system/appManage'
  export default {
    data () {
      return {
        labelCol: {
          xs: { span: 24 },
          sm: { span: 5 }
        },
        wrapperCol: {
          xs: { span: 24 },
          sm: { span: 18 }
        },
        visible: false,
        confirmLoading: false,
        form: this.$form.createForm(this)
      }
    },
    methods: {
      // 初始化方法
      add () {
        this.visible = true
        this.form.getFieldDecorator('active', { initialValue: 'N' })
      },
      handleSubmit () {
        const { form: { validateFields } } = this
        this.confirmLoading = true
        validateFields((errors, values) => {
          if (!errors) {
            sysAppAdd(values).then((res) => {
              this.confirmLoading = false
              if (res.success) {
                this.$message.success('新增成功')
                this.handleCancel()
                this.$emit('ok', values)
              } else {
                this.$message.error('新增失败:' + res.message)
              }
            }).finally((res) => {
              this.confirmLoading = false
            })
          } else {
            this.confirmLoading = false
          }
        })
      },
      handleCancel () {
        this.form.resetFields()
        this.visible = false
      }
    }
  }
</script>
_web/src/views/system/app/editForm.vue
New file
@@ -0,0 +1,108 @@
<template>
  <a-modal
    title="应用编辑"
    :width="500"
    :visible="visible"
    :confirmLoading="confirmLoading"
    @ok="handleSubmit"
    @cancel="handleCancel"
  >
    <a-spin :spinning="confirmLoading">
      <a-form :form="form" >
        <a-form-item
          style="display: none;"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
        >
          <a-input v-decorator="['id']" />
        </a-form-item>
        <a-form-item
          style="display: none;"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
        >
          <a-input v-decorator="['active']" />
        </a-form-item>
        <a-form-item
          label="应用名称"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入应用名称" v-decorator="['name', {rules: [{required: true, message: '请输入应用名称!'}]}]" />
        </a-form-item>
        <a-form-item
          label="唯一编码"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入唯一编码" v-decorator="['code', {rules: [{required: true, message: '请输入唯一编码!'}]}]" />
        </a-form-item>
      </a-form>
    </a-spin>
  </a-modal>
</template>
<script>
  import { sysAppEdit } from '@/api/modular/system/appManage'
  export default {
    data () {
      return {
        labelCol: {
          xs: { span: 24 },
          sm: { span: 5 }
        },
        wrapperCol: {
          xs: { span: 24 },
          sm: { span: 18 }
        },
        visible: false,
        confirmLoading: false,
        visibleDef: false,
        form: this.$form.createForm(this)
      }
    },
    methods: {
      // 初始化方法
      edit (record) {
        this.visible = true
        setTimeout(() => {
          this.form.setFieldsValue(
            {
              id: record.id,
              name: record.name,
              code: record.code,
              active: record.active
            }
          )
        }, 100)
      },
      handleSubmit () {
        const { form: { validateFields } } = this
        this.confirmLoading = true
        validateFields((errors, values) => {
          if (!errors) {
            sysAppEdit(values).then((res) => {
              if (res.success) {
                this.$message.success('编辑成功')
                this.visible = false
                this.confirmLoading = false
                this.$emit('ok', values)
              } else {
                this.$message.error('编辑失败:' + res.message)
              }
            }).finally((res) => {
              this.confirmLoading = false
            })
          } else {
            this.confirmLoading = false
          }
        })
      },
      handleCancel () {
        this.form.resetFields()
        this.visible = false
      }
    }
  }
</script>
_web/src/views/system/app/index.vue
New file
@@ -0,0 +1,194 @@
/* eslint-disable eqeqeq */
<template>
  <div>
    <x-card v-if="hasPerm('sysApp:page')">
      <div slot="content" class="table-page-search-wrapper">
        <a-form layout="inline">
          <a-row :gutter="48">
            <a-col :md="8" :sm="24">
              <a-form-item label="应用名称">
                <a-input v-model="queryParam.name" allow-clear placeholder="请输入应用名称"/>
              </a-form-item>
            </a-col>
            <a-col :md="8" :sm="24">
              <a-form-item label="唯一编码">
                <a-input v-model="queryParam.code" allow-clear placeholder="请输入唯一编码"/>
              </a-form-item>
            </a-col>
            <a-col :md="8" :sm="24">
              <span class="table-page-search-submitButtons">
                <a-button type="primary" @click="$refs.table.refresh(true)">查询</a-button>
                <a-button style="margin-left: 8px" @click="() => queryParam = {}">重置</a-button>
              </span>
            </a-col>
          </a-row>
        </a-form>
      </div>
    </x-card>
    <a-card :bordered="false" >
      <a-spin :spinning="loading">
        <s-table
          ref="table"
          :columns="columns"
          :data="loadData"
          :alert="false"
          :rowKey="(record) => record.id"
        >
          <template slot="operator" v-if="hasPerm('sysApp:add')">
            <a-button @click="$refs.addForm.add()" icon="plus" type="primary" v-if="hasPerm('sysApp:add')">新增应用</a-button>
          </template>
          <span slot="active" slot-scope="text">
            {{ activeFilter(text) }}
          </span>
          <span slot="status" slot-scope="text">
            {{ statusFilter(text) }}
          </span>
          <span slot="action" slot-scope="text, record">
            <a v-if="hasPerm('sysApp:edit')" @click="$refs.editForm.edit(record)">编辑</a>
            <a-divider type="vertical" v-if="hasPerm('sysApp:edit') & hasPerm('sysApp:delete')" />
            <a-popconfirm v-if="hasPerm('sysApp:delete')" placement="topRight" title="确认删除?" @confirm="() => sysAppDelete(record)">
              <a>删除</a>
            </a-popconfirm>
            <a-divider type="vertical" v-if="hasPerm('sysApp:setAsDefault') & hasPerm('sysApp:delete') & record.active == 'N' || hasPerm('sysApp:edit') & hasPerm('sysApp:setAsDefault') & record.active == 'N'" />
            <a-popconfirm v-if="hasPerm('sysApp:setAsDefault') & record.active == 'N'" placement="topRight" title="设置为默认应用?" @confirm="() => sysDefault(record)">
              <a>设为默认</a>
            </a-popconfirm>
          </span>
        </s-table>
        <add-form ref="addForm" @ok="handleOk" />
        <edit-form ref="editForm" @ok="handleOk" />
      </a-spin>
    </a-card>
  </div>
</template>
<script>
  import { STable, XCard } from '@/components'
  import { getAppPage, sysAppDelete, sysAppSetAsDefault } from '@/api/modular/system/appManage'
  import { sysDictTypeDropDown } from '@/api/modular/system/dictManage'
  import editForm from './editForm'
  import addForm from './addForm'
  export default {
    components: {
      XCard,
      STable,
      editForm,
      addForm
    },
    data () {
      return {
        // description: '面包屑说明',
        // 查询参数
        queryParam: {},
        // 表头
        columns: [
          {
            title: '应用名称',
            dataIndex: 'name'
          },
          {
            title: '唯一编码',
            dataIndex: 'code'
          },
          {
            title: '是否默认',
            dataIndex: 'active',
            scopedSlots: { customRender: 'active' }
          },
          {
            title: '状态',
            dataIndex: 'status',
            scopedSlots: { customRender: 'status' }
          }
        ],
        tstyle: { 'padding-bottom': '0px', 'margin-bottom': '10px' },
        // 加载数据方法 必须为 Promise 对象
        loadData: parameter => {
          return getAppPage(Object.assign(parameter, this.queryParam)).then((res) => {
            return res.data
          })
        },
        loading: false,
        statusDict: [],
        activeDict: []
      }
    },
    created () {
      this.sysDictTypeDropDown()
      if (this.hasPerm('sysApp:edit') || this.hasPerm('sysApp:delete') || this.hasPerm('sysApp:setAsDefault')) {
        this.columns.push({
          title: '操作',
          width: '200px',
          dataIndex: 'action',
          scopedSlots: { customRender: 'action' }
        })
      }
    },
    methods: {
      activeFilter (active) {
        // eslint-disable-next-line eqeqeq
        const values = this.activeDict.filter(item => item.code == active)
        if (values.length > 0) {
          return values[0].value
        }
      },
      statusFilter (status) {
        // eslint-disable-next-line eqeqeq
        const values = this.statusDict.filter(item => item.code == status)
        if (values.length > 0) {
          return values[0].value
        }
      },
      /**
       * 获取字典数据
       */
      sysDictTypeDropDown () {
        sysDictTypeDropDown({ code: 'yes_or_no' }).then((res) => {
          this.activeDict = res.data
        })
        sysDictTypeDropDown({ code: 'common_status' }).then((res) => {
          this.statusDict = res.data
        })
      },
      handleOk () {
        this.$refs.table.refresh()
      },
      sysDefault (record) {
        this.loading = true
        sysAppSetAsDefault({ id: record.id }).then((res) => {
          this.loading = false
           if (res.success) {
             this.$message.success('设置成功')
             this.$refs.table.refresh()
           } else {
             this.$message.error('设置失败:' + res.message)
           }
        })
      },
      /**
       * 删除应用
       */
      sysAppDelete (record) {
        this.loading = true
        sysAppDelete(record).then((res) => {
          this.loading = false
          if (res.success) {
            this.$message.success('删除成功')
            this.$refs.table.refresh()
          } else {
            this.$message.error('删除失败:' + res.message)
          }
        }).catch((err) => {
          this.$message.error('删除错误:' + err.message)
        })
      }
    }
  }
</script>
<style scoped>
  .table-operator {
    margin-bottom: 18px;
  }
  button {
    margin-right: 8px;
  }
</style>
_web/src/views/system/area/index.vue
New file
@@ -0,0 +1,130 @@
/* eslint-disable */
<template>
  <a-card :bordered="false">
    <a-table
      ref="table"
      size="middle"
      :rowKey="(record) => record.id"
      :pagination="false"
      :defaultExpandAllRows="true"
      :columns="columns"
      :dataSource="data"
      :loading="loading"
      showPagination="auto"
      @expand="onExpand">
    </a-table>
  </a-card>
</template>
<script>
  import { getAreaList } from '@/api/modular/system/areaManage'
  export default {
    data () {
      return {
        queryParam: {},
        data: [],
        loading: true,
        // 定义展开过的节点的id数组
        expandedData: [],
        columns: [
          {
            title: '名称',
            dataIndex: 'name'
          },
          {
            title: '层级',
            dataIndex: 'levelCode'
          },
          {
            title: '简称',
            dataIndex: 'shortName'
          },
          {
            title: '组合名',
            dataIndex: 'mergerName'
          },
          {
            title: '拼音',
            dataIndex: 'pinyin'
          },
          {
            title: '邮编',
            dataIndex: 'zipCode'
          },
          {
            title: '经度',
            dataIndex: 'lng'
          },
          {
            title: '纬度',
            dataIndex: 'lat'
          }
        ],
        selectedRowKeys: []
      }
    },
    created () {
      this.loadData()
    },
    methods: {
      loadData () {
        this.loading = true
        getAreaList(this.queryParam).then((res) => {
          if (res.success) {
            this.data = res.data
            this.removeEmptyChildren(this.data)
          }
        }).finally(() => {
          this.loading = false
        })
      },
      removeEmptyChildren(data) {
        if (data == null || data.length === 0) return
        for (let i = 0; i < data.length; i++) {
          const item = data[i]
          // 如果为最终子节点或其为“市辖区”,则其没有子节点
          if (item.levelCode === 4 || (item.levelCode === 2 && item.name === '市辖区')) {
             item.children = null
          }
        }
      },
      onSelectChange (selectedRowKeys) {
        this.selectedRowKeys = selectedRowKeys
      },
      onExpand(expanded, record) {
        if (expanded) {
          // 判断其是否已经展开过
          const index = this.expandedData.indexOf(record.id)
          // 如果没展开过,则请求接口
          if (index === -1) {
            this.queryParam.parentCode = record.areaCode
            getAreaList(this.queryParam).then((res) => {
              if (res.success) {
                // 设置为其子节点
                record.children = res.data
                this.removeEmptyChildren(record.children)
                // 将其放入展开过的id集合
                this.expandedData.push(record.id)
              }
            }).finally(() => {
              this.loading = false
            })
          }
        }
      }
    }
  }
</script>
<style scoped>
</style>
_web/src/views/system/config/addForm.vue
New file
@@ -0,0 +1,130 @@
<template>
  <a-modal
    title="新增参数"
    :width="900"
    :visible="visible"
    :confirmLoading="confirmLoading"
    @ok="handleSubmit"
    @cancel="handleCancel"
  >
    <a-spin :spinning="formLoading">
      <a-form :form="form">
        <a-form-item
          label="参数名称"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入参数名称" v-decorator="['name', {rules: [{required: true, message: '请输入参数名称!'}]}]" />
        </a-form-item>
        <a-form-item
          label="唯一编码"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入唯一编码" v-decorator="['code', {rules: [{required: true, message: '请输入唯一编码!'}]}]" />
        </a-form-item>
        <a-form-item
          label="系统参数"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
        >
          <a-radio-group v-decorator="['sysFlag',{rules: [{ required: true, message: '请选择是否为系统参数!' }]}]" >
            <a-radio-button value="Y" > 是 </a-radio-button>
            <a-radio-button value="N" >  否 </a-radio-button>
          </a-radio-group>
        </a-form-item>
        <a-form-item
          label="所属分类"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-select style="width: 100%" placeholder="请选择所属分类" v-decorator="['groupCode', {rules: [{ required: true, message: '请选择取所属分类!' }]}]" >
            <a-select-option v-for="(item,index) in groupCodeList" :key="index" :value="item.code" >{{ item.value }}</a-select-option>
          </a-select>
        </a-form-item>
        <a-form-item
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          label="参数值"
        >
          <a-input placeholder="请输入参数值" v-decorator="['value', {rules: [{required: true, message: '请输入参数值!'}]}]" />
        </a-form-item>
        <a-form-item
          label="备注"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-textarea :rows="4" placeholder="请输入备注" v-decorator="['remark']"></a-textarea>
        </a-form-item>
      </a-form>
    </a-spin>
  </a-modal>
</template>
<script>
  import { sysDictTypeDropDown, sysConfigAdd } from '@/api/modular/system/configManage'
  export default {
    data () {
      return {
        labelCol: {
          xs: { span: 24 },
          sm: { span: 5 }
        },
        wrapperCol: {
          xs: { span: 24 },
          sm: { span: 15 }
        },
        visible: false,
        confirmLoading: false,
        formLoading: true,
        groupCodeList: [],
        form: this.$form.createForm(this)
      }
    },
    methods: {
      // 初始化方法
      add () {
        this.visible = true
        this.sysDictTypeDropDown()
      },
      /**
       * 获取所属分类
       */
      sysDictTypeDropDown () {
        sysDictTypeDropDown({ code: 'consts_type' }).then((res) => {
          this.groupCodeList = res.data
          this.formLoading = false
        })
      },
      handleSubmit () {
        const { form: { validateFields } } = this
        this.confirmLoading = true
        validateFields((errors, values) => {
          if (!errors) {
            sysConfigAdd(values).then((res) => {
              this.confirmLoading = false
              if (res.success) {
                this.$message.success('新增成功')
                this.$emit('ok', values)
                this.handleCancel()
              } else {
                this.$message.error('新增失败:' + res.message)
              }
            }).finally((res) => {
              this.confirmLoading = false
            })
          } else {
            this.confirmLoading = false
          }
        })
      },
      handleCancel () {
        this.form.resetFields()
        this.visible = false
      }
    }
  }
</script>
_web/src/views/system/config/editForm.vue
New file
@@ -0,0 +1,158 @@
<template>
  <a-modal
    title="参数编辑"
    :width="900"
    :visible="visible"
    :confirmLoading="confirmLoading"
    @ok="handleSubmit"
    @cancel="handleCancel"
  >
    <a-spin :spinning="formLoading">
      <a-form :form="form">
        <a-form-item
          style="display: none;"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input v-decorator="['id']" />
        </a-form-item>
        <a-form-item
          label="参数名称"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入参数名称" v-decorator="['name', {rules: [{required: true, message: '请输入参数名称!'}]}]" />
        </a-form-item>
        <a-form-item
          label="唯一编码"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-input placeholder="请输入唯一编码" :disabled="editDisabled" v-decorator="['code', {rules: [{required: true, message: '请输入唯一编码!'}]}]" />
        </a-form-item>
        <a-form-item
          label="系统参数"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
        >
          <a-radio-group :disabled="editDisabled" v-decorator="['sysFlag',{rules: [{ required: true, message: '请选择是否为系统参数!' }]}]" >
            <a-radio-button value="Y" > 是 </a-radio-button>
            <a-radio-button value="N" >  否 </a-radio-button>
          </a-radio-group>
        </a-form-item>
        <a-form-item
          label="所属分类"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-select :disabled="editDisabled" style="width: 100%" placeholder="请选择所属分类" v-decorator="['groupCode', {rules: [{ required: true, message: '请选择取所属分类!' }]}]" >
            <a-select-option v-for="(item,index) in groupCodeList" :key="index" :value="item.code" >{{ item.value }}</a-select-option>
          </a-select>
        </a-form-item>
        <a-form-item
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          label="参数值"
          has-feedback
        >
          <a-input placeholder="请输入参数值" v-decorator="['value', {rules: [{required: true, message: '请输入参数值!'}]}]" />
        </a-form-item>
        <a-form-item
          label="备注"
          :labelCol="labelCol"
          :wrapperCol="wrapperCol"
          has-feedback
        >
          <a-textarea :rows="4" placeholder="请输入备注" v-decorator="['remark']"></a-textarea>
        </a-form-item>
      </a-form>
    </a-spin>
  </a-modal>
</template>
<script>
  import { sysDictTypeDropDown, sysConfigEdit } from '@/api/modular/system/configManage'
  export default {
    data () {
      return {
        labelCol: {
          xs: { span: 24 },
          sm: { span: 5 }
        },
        wrapperCol: {
          xs: { span: 24 },
          sm: { span: 15 }
        },
        visible: false,
        confirmLoading: false,
        formLoading: true,
        groupCodeList: [],
        editDisabled: false,
        form: this.$form.createForm(this)
      }
    },
    methods: {
      // 初始化方法
      edit (record) {
        this.visible = true
        setTimeout(() => {
          this.form.setFieldsValue(
            {
              id: record.id,
              name: record.name,
              code: record.code,
              groupCode: record.groupCode,
              sysFlag: record.sysFlag,
              value: record.value,
              remark: record.remark
            }
          )
        }, 100)
        // eslint-disable-next-line eqeqeq
        if (record.sysFlag == 'Y') {
           this.editDisabled = true
        }
        this.sysDictTypeDropDown()
      },
      /**
       * 获取所属分类
       */
      sysDictTypeDropDown () {
        sysDictTypeDropDown({ code: 'consts_type' }).then((res) => {
          this.groupCodeList = res.data
          this.formLoading = false
        })
      },
      handleSubmit () {
        const { form: { validateFields } } = this
        this.confirmLoading = true
        validateFields((errors, values) => {
          if (!errors) {
            sysConfigEdit(values).then((res) => {
              this.confirmLoading = false
              if (res.success) {
                this.$message.success('编辑成功')
                this.$emit('ok', values)
                this.handleCancel()
              } else {
                this.$message.error('编辑失败:' + res.message)
              }
            }).finally((res) => {
              this.confirmLoading = false
            })
          } else {
            this.confirmLoading = false
          }
        })
      },
      handleCancel () {
        this.form.resetFields()
        this.visible = false
        this.editDisabled = false
      }
    }
  }
</script>
_web/src/views/system/config/index.vue
New file
@@ -0,0 +1,184 @@
<template>
  <div>
    <x-card v-if="hasPerm('sysConfig:page')">
      <div slot="content" class="table-page-search-wrapper" >
        <a-form layout="inline">
          <a-row :gutter="48">
            <a-col :md="8" :sm="24">
              <a-form-item label="参数名称">
                <a-input v-model="queryParam.name" allow-clear placeholder="请输入参数名称"/>
              </a-form-item>
            </a-col>
            <a-col :md="8" :sm="24">
              <a-form-item label="唯一编码">
                <a-input v-model="queryParam.code" allow-clear placeholder="请输入唯一编码"/>
              </a-form-item>
            </a-col>
            <template v-if="advanced">
              <a-col :md="8" :sm="24">
                <a-form-item label="所属分类">
                  <a-select v-model="queryParam.groupCode" placeholder="请选择所属分类" allow-clear>
                    <a-select-option v-for="(item,index) in groupCodeDictTypeDropDown" :key="index" :value="item.code" >{{ item.name }}</a-select-option>
                  </a-select>
                </a-form-item>
              </a-col>
            </template>
            <a-col :md="!advanced && 8 || 24" :sm="24" >
              <span class="table-page-search-submitButtons" :style="advanced && { float: 'right', overflow: 'hidden' } || {} ">
                <a-button type="primary" @click="$refs.table.refresh(true)" >查询</a-button>
                <a-button style="margin-left: 8px" @click="() => queryParam = {}">重置</a-button>
                <a @click="toggleAdvanced" style="margin-left: 8px">
                  {{ advanced ? '收起' : '展开' }}
                  <a-icon :type="advanced ? 'up' : 'down'"/>
                </a>
              </span>
            </a-col>
          </a-row>
        </a-form>
      </div>
    </x-card>
    <a-card :bordered="false">
      <s-table
        ref="table"
        :columns="columns"
        :data="loadData"
        :alert="false"
        :rowKey="(record) => record.code"
      >
        <template slot="operator" v-if="hasPerm('sysConfig:add')">
          <a-button @click="$refs.addForm.add()" icon="plus" type="primary" v-if="hasPerm('sysConfig:add')">新增配置</a-button>
        </template>
        <span slot="name" slot-scope="text">
          <ellipsis :length="20" tooltip>{{ text }}</ellipsis>
        </span>
        <span slot="code" slot-scope="text">
          <ellipsis :length="10" tooltip>{{ text }}</ellipsis>
        </span>
        <span slot="value" slot-scope="text">
          <ellipsis :length="16" tooltip>{{ text }}</ellipsis>
        </span>
        <span slot="remark" slot-scope="text">
          <ellipsis :length="16" tooltip>{{ text }}</ellipsis>
        </span>
        <span slot="groupCode" slot-scope="text">
          {{ 'consts_type' | dictType(text) }}
        </span>
        <span slot="action" slot-scope="text, record">
          <a v-if="hasPerm('sysConfig:edit')" @click="$refs.editForm.edit(record)">编辑</a>
          <a-divider type="vertical" v-if="hasPerm('sysConfig:edit') & hasPerm('sysConfig:delete')"/>
          <a-popconfirm v-if="hasPerm('sysConfig:delete')" placement="topRight" title="确认删除?" @confirm="() => sysConfigDelete(record)">
            <a>删除</a>
          </a-popconfirm>
        </span>
      </s-table>
      <add-form ref="addForm" @ok="handleOk" v-if="hasPerm('sysConfig:add')"/>
      <edit-form ref="editForm" @ok="handleOk" v-if="hasPerm('sysConfig:edit')"/>
    </a-card>
  </div>
</template>
<script>
  import { STable, Ellipsis, XCard } from '@/components'
  import { sysConfigPage, sysConfigDelete } from '@/api/modular/system/configManage'
  import addForm from './addForm'
  import editForm from './editForm'
  export default {
    components: {
      XCard,
      STable,
      Ellipsis,
      addForm,
      editForm
    },
    data () {
      return {
        // 高级搜索 展开/关闭
        advanced: false,
        // 查询参数
        queryParam: {},
        // 表头
        columns: [
          {
            title: '参数名称',
            dataIndex: 'name',
            scopedSlots: { customRender: 'name' }
          },
          {
            title: '唯一编码',
            dataIndex: 'code',
            scopedSlots: { customRender: 'code' }
          },
          {
            title: '参数值',
            dataIndex: 'value',
            scopedSlots: { customRender: 'value' }
          },
          {
            title: '所属分类',
            dataIndex: 'groupCode',
            scopedSlots: { customRender: 'groupCode' }
          },
          {
            title: '备注',
            dataIndex: 'remark',
            scopedSlots: { customRender: 'remark' }
          }
        ],
        // 加载数据方法 必须为 Promise 对象
        loadData: parameter => {
          return sysConfigPage(Object.assign(parameter, this.queryParam)).then((res) => {
            return res.data
          })
        },
        groupCodeDictTypeDropDown: []
      }
    },
    /**
     * 初始化判断按钮权限是否拥有,没有则不现实列
     */
    created () {
      this.sysDictTypeDropDown()
      if (this.hasPerm('sysConfig:edit') || this.hasPerm('sysConfig:delete')) {
        this.columns.push({
          title: '操作',
          width: '150px',
          dataIndex: 'action',
          scopedSlots: { customRender: 'action' }
        })
      }
    },
    methods: {
      /**
       * 获取字典数据
       */
      sysDictTypeDropDown () {
        this.groupCodeDictTypeDropDown = this.$options.filters['dictData']('consts_type')
      },
      sysConfigDelete (record) {
        sysConfigDelete(record).then((res) => {
          if (res.success) {
            this.$message.success('删除成功')
            this.$refs.table.refresh()
          } else {
            this.$message.error('删除失败:' + res.message)
          }
        }).catch((err) => {
          this.$message.error('删除错误:' + err.message)
        })
      },
      toggleAdvanced () {
        this.advanced = !this.advanced
      },
      handleOk () {
        this.$refs.table.refresh()
      }
    }
  }
</script>
<style lang="less">
  .table-operator {
    margin-bottom: 18px;
  }
  button {
    margin-right: 8px;
  }
</style>
_web/src/views/system/dashboard/Analysis.vue
New file
@@ -0,0 +1,385 @@
<template>
  <div class="page-header-index-wide">
    <a-row :gutter="24">
      <a-col :sm="24" :md="12" :xl="6" :style="{ marginBottom: '24px' }">
        <chart-card :loading="loading" title="总销售额" total="¥126,560">
          <a-tooltip title="指标说明" slot="action">
            <a-icon type="info-circle-o" />
          </a-tooltip>
          <div>
            <trend flag="up" style="margin-right: 16px;">
              <span slot="term">周同比</span>
              12%
            </trend>
            <trend flag="down">
              <span slot="term">日同比</span>
              11%
            </trend>
          </div>
          <template slot="footer">日均销售额<span>¥ 234.56</span></template>
        </chart-card>
      </a-col>
      <a-col :sm="24" :md="12" :xl="6" :style="{ marginBottom: '24px' }">
        <chart-card :loading="loading" title="访问量" :total="8846 | NumberFormat">
          <a-tooltip title="指标说明" slot="action">
            <a-icon type="info-circle-o" />
          </a-tooltip>
          <div>
            <mini-area />
          </div>
          <template slot="footer">日访问量<span> {{ '1234' | NumberFormat }}</span></template>
        </chart-card>
      </a-col>
      <a-col :sm="24" :md="12" :xl="6" :style="{ marginBottom: '24px' }">
        <chart-card :loading="loading" title="支付笔数" :total="6560 | NumberFormat">
          <a-tooltip title="指标说明" slot="action">
            <a-icon type="info-circle-o" />
          </a-tooltip>
          <div>
            <mini-bar />
          </div>
          <template slot="footer">转化率 <span>60%</span></template>
        </chart-card>
      </a-col>
      <a-col :sm="24" :md="12" :xl="6" :style="{ marginBottom: '24px' }">
        <chart-card :loading="loading" title="运营活动效果" total="78%">
          <a-tooltip title="指标说明" slot="action">
            <a-icon type="info-circle-o" />
          </a-tooltip>
          <div>
            <mini-progress color="rgb(19, 194, 194)" :target="80" :percentage="78" height="8px" />
          </div>
          <template slot="footer">
            <trend flag="down" style="margin-right: 16px;">
              <span slot="term">同周比</span>
              12%
            </trend>
            <trend flag="up">
              <span slot="term">日环比</span>
              80%
            </trend>
          </template>
        </chart-card>
      </a-col>
    </a-row>
    <a-card :loading="loading" :bordered="false" :body-style="{padding: '0'}">
      <div class="salesCard">
        <a-tabs default-active-key="1" size="large" :tab-bar-style="{marginBottom: '24px', paddingLeft: '16px'}">
          <div class="extra-wrapper" slot="tabBarExtraContent">
            <div class="extra-item">
              <a>今日</a>
              <a>本周</a>
              <a>本月</a>
              <a>本年</a>
            </div>
            <a-range-picker :style="{width: '230px'}" />
          </div>
          <a-tab-pane loading="true" tab="销售额" key="1">
            <a-row>
              <a-col :xl="16" :lg="12" :md="12" :sm="24" :xs="24">
                <bar :data="barData" title="销售额排行" />
              </a-col>
              <a-col :xl="8" :lg="12" :md="12" :sm="24" :xs="24">
                <rank-list title="门店销售排行榜" :list="rankList"/>
              </a-col>
            </a-row>
          </a-tab-pane>
          <a-tab-pane tab="访问量" key="2">
            <a-row>
              <a-col :xl="16" :lg="12" :md="12" :sm="24" :xs="24">
                <bar :data="barData2" title="销售额趋势" />
              </a-col>
              <a-col :xl="8" :lg="12" :md="12" :sm="24" :xs="24">
                <rank-list title="门店销售排行榜" :list="rankList"/>
              </a-col>
            </a-row>
          </a-tab-pane>
        </a-tabs>
      </div>
    </a-card>
    <div class="antd-pro-pages-dashboard-analysis-twoColLayout" :class="isDesktop() ? 'desktop' : ''">
      <a-row :gutter="24" type="flex" :style="{ marginTop: '24px' }">
        <a-col :xl="12" :lg="24" :md="24" :sm="24" :xs="24">
          <a-card :loading="loading" :bordered="false" title="线上热门搜索" :style="{ height: '100%' }">
            <a-dropdown :trigger="['click']" placement="bottomLeft" slot="extra">
              <a class="ant-dropdown-link" href="#">
                <a-icon type="ellipsis" />
              </a>
              <a-menu slot="overlay">
                <a-menu-item>
                  <a href="javascript:;">操作一</a>
                </a-menu-item>
                <a-menu-item>
                  <a href="javascript:;">操作二</a>
                </a-menu-item>
              </a-menu>
            </a-dropdown>
            <a-row :gutter="68">
              <a-col :xs="24" :sm="12" :style="{ marginBottom: ' 24px'}">
                <number-info :total="12321" :sub-total="17.1">
                  <span slot="subtitle">
                    <span>搜索用户数</span>
                    <a-tooltip title="指标说明" slot="action">
                      <a-icon type="info-circle-o" :style="{ marginLeft: '8px' }" />
                    </a-tooltip>
                  </span>
                </number-info>
                <!-- miniChart -->
                <div>
                  <mini-smooth-area :style="{ height: '45px' }" :dataSource="searchUserData" :scale="searchUserScale" />
                </div>
              </a-col>
              <a-col :xs="24" :sm="12" :style="{ marginBottom: ' 24px'}">
                <number-info :total="2.7" :sub-total="26.2" status="down">
                  <span slot="subtitle">
                    <span>人均搜索次数</span>
                    <a-tooltip title="指标说明" slot="action">
                      <a-icon type="info-circle-o" :style="{ marginLeft: '8px' }" />
                    </a-tooltip>
                  </span>
                </number-info>
                <!-- miniChart -->
                <div>
                  <mini-smooth-area :style="{ height: '45px' }" :dataSource="searchUserData" :scale="searchUserScale" />
                </div>
              </a-col>
            </a-row>
            <div class="ant-table-wrapper">
              <a-table
                row-key="index"
                size="small"
                :columns="searchTableColumns"
                :dataSource="searchData"
                :pagination="{ pageSize: 5 }"
              >
                <span slot="range" slot-scope="text, record">
                  <trend :flag="record.status === 0 ? 'up' : 'down'">
                    {{ text }}%
                  </trend>
                </span>
              </a-table>
            </div>
          </a-card>
        </a-col>
        <a-col :xl="12" :lg="24" :md="24" :sm="24" :xs="24">
          <a-card class="antd-pro-pages-dashboard-analysis-salesCard" :loading="loading" :bordered="false" title="销售额类别占比" :style="{ height: '100%' }">
            <div slot="extra" style="height: inherit;">
              <!-- style="bottom: 12px;display: inline-block;" -->
              <span class="dashboard-analysis-iconGroup">
                <a-dropdown :trigger="['click']" placement="bottomLeft">
                  <a-icon type="ellipsis" class="ant-dropdown-link" />
                  <a-menu slot="overlay">
                    <a-menu-item>
                      <a href="javascript:;">操作一</a>
                    </a-menu-item>
                    <a-menu-item>
                      <a href="javascript:;">操作二</a>
                    </a-menu-item>
                  </a-menu>
                </a-dropdown>
              </span>
              <div class="analysis-salesTypeRadio">
                <a-radio-group defaultValue="a">
                  <a-radio-button value="a">全部渠道</a-radio-button>
                  <a-radio-button value="b">线上</a-radio-button>
                  <a-radio-button value="c">门店</a-radio-button>
                </a-radio-group>
              </div>
            </div>
            <h4>销售额</h4>
            <div>
              <!-- style="width: calc(100% - 240px);" -->
              <div>
                <v-chart :force-fit="true" :height="405" :data="pieData" :scale="pieScale">
                  <v-tooltip :showTitle="false" dataKey="item*percent" />
                  <v-axis />
                  <!-- position="right" :offsetX="-140" -->
                  <v-legend dataKey="item"/>
                  <v-pie position="percent" color="item" :vStyle="pieStyle" />
                  <v-coord type="theta" :radius="0.75" :innerRadius="0.6" />
                </v-chart>
              </div>
            </div>
          </a-card>
        </a-col>
      </a-row>
    </div>
  </div>
</template>
<script>
import moment from 'moment'
import { ChartCard, MiniArea, MiniBar, MiniProgress, RankList, Bar, Trend, NumberInfo, MiniSmoothArea } from '@/components'
import { mixinDevice } from '@/utils/mixin'
const barData = []
const barData2 = []
for (let i = 0; i < 12; i += 1) {
  barData.push({
    x: `${i + 1}月`,
    y: Math.floor(Math.random() * 1000) + 200
  })
  barData2.push({
    x: `${i + 1}月`,
    y: Math.floor(Math.random() * 1000) + 200
  })
}
const rankList = []
for (let i = 0; i < 7; i++) {
  rankList.push({
    name: '白鹭岛 ' + (i + 1) + ' 号店',
    total: 1234.56 - i * 100
  })
}
const searchUserData = []
for (let i = 0; i < 7; i++) {
  searchUserData.push({
    x: moment().add(i, 'days').format('YYYY-MM-DD'),
    y: Math.ceil(Math.random() * 10)
  })
}
const searchUserScale = [
  {
    dataKey: 'x',
    alias: '时间'
  },
  {
    dataKey: 'y',
    alias: '用户数',
    min: 0,
    max: 10
  }]
const searchTableColumns = [
  {
    dataIndex: 'MenuIndex.vue',
    title: '排名',
    width: 90
  },
  {
    dataIndex: 'keyword',
    title: '搜索关键词'
  },
  {
    dataIndex: 'count',
    title: '用户数'
  },
  {
    dataIndex: 'range',
    title: '周涨幅',
    align: 'right',
    sorter: (a, b) => a.range - b.range,
    scopedSlots: { customRender: 'range' }
  }
]
const searchData = []
for (let i = 0; i < 50; i += 1) {
  searchData.push({
    index: i + 1,
    keyword: `搜索关键词-${i}`,
    count: Math.floor(Math.random() * 1000),
    range: Math.floor(Math.random() * 100),
    status: Math.floor((Math.random() * 10) % 2)
  })
}
const DataSet = require('@antv/data-set')
const sourceData = [
  { item: '家用电器', count: 32.2 },
  { item: '食用酒水', count: 21 },
  { item: '个护健康', count: 17 },
  { item: '服饰箱包', count: 13 },
  { item: '母婴产品', count: 9 },
  { item: '其他', count: 7.8 }
]
const pieScale = [{
  dataKey: 'percent',
  min: 0,
  formatter: '.0%'
}]
const dv = new DataSet.View().source(sourceData)
dv.transform({
  type: 'percent',
  field: 'count',
  dimension: 'item',
  as: 'percent'
})
const pieData = dv.rows
export default {
  name: 'Analysis',
  mixins: [mixinDevice],
  components: {
    ChartCard,
    MiniArea,
    MiniBar,
    MiniProgress,
    RankList,
    Bar,
    Trend,
    NumberInfo,
    MiniSmoothArea
  },
  data () {
    return {
      loading: true,
      rankList,
      // 搜索用户数
      searchUserData,
      searchUserScale,
      searchTableColumns,
      searchData,
      barData,
      barData2,
      //
      pieScale,
      pieData,
      sourceData,
      pieStyle: {
        stroke: '#fff',
        lineWidth: 1
      }
    }
  },
  created () {
    setTimeout(() => {
      this.loading = !this.loading
    }, 1000)
  }
}
</script>
<style lang="less" scoped>
  .extra-wrapper {
    line-height: 55px;
    padding-right: 24px;
    .extra-item {
      display: inline-block;
      margin-right: 24px;
      a {
        margin-left: 24px;
      }
    }
  }
  .antd-pro-pages-dashboard-analysis-twoColLayout {
    position: relative;
    display: flex;
    display: block;
    flex-flow: row wrap;
  }
  .antd-pro-pages-dashboard-analysis-salesCard {
    height: calc(100% - 24px);
    /deep/ .ant-card-head {
      position: relative;
    }
  }
  .dashboard-analysis-iconGroup {
    i {
      margin-left: 16px;
      color: rgba(0,0,0,.45);
      cursor: pointer;
      transition: color .32s;
      color: black;
    }
  }
  .analysis-salesTypeRadio {
    position: absolute;
    right: 54px;
    bottom: 12px;
  }
</style>
_web/src/views/system/dashboard/Monitor.vue
New file
@@ -0,0 +1,15 @@
<template>
  <div>
    Monitor
  </div>
</template>
<script>
export default {
  name: 'Monitor'
}
</script>
<style scoped>
</style>
_web/src/views/system/dashboard/TestWork.vue
New file
@@ -0,0 +1,117 @@
<template>
  <div>
    <h2>本页面内容均为测试功能,暂不提供稳定性保证</h2>
    <a-divider />
    <div class="multi-tab-test">
      <h4>多标签组件测试功能</h4>
      <a-button @click="handleCloseCurrentTab" style="margin-right: 16px;">关闭当前页</a-button>
      <a-button @click="handleOpenTab" style="margin-right: 16px;">打开 任务列表</a-button>
      <a-popconfirm :visible="visible" @confirm="confirm" @cancel="cancel" okText="确定" cancelText="取消">
        <template v-slot:title>
          <div>
            <a-form :form="form" layout="inline">
              <a-form-item label="自定义名称">
                <a-input v-decorator="['tabName', {rules: [{required: true, message: '请输入新的 Tab 名称'}]}]"/>
              </a-form-item>
            </a-form>
          </div>
        </template>
        <a-button @click="() => visible = !visible" style="margin-right: 16px;">修改当前 Tab 名称</a-button>
      </a-popconfirm>
      <a-popconfirm :visible="visible2" @confirm="confirm2" @cancel="() => visible2 = false" okText="确定" cancelText="取消">
        <template v-slot:title>
          <div>
            <p>页面 KEY 是由页面的路由 <code>path</code> 决定的</p>
            <p>如果要修改某一个页面标题,该页面必须已经被打开在 Tab 栏</p>
            <p>后期可以考虑优化到编程式 Tab 栏,就可以没有这种限制</p>
            <a-form :form="form2" layout="inline">
              <a-form-item label="页面KEY">
                <a-input v-decorator="['tabKey', { initialValue: '/dashboard/workplace' }]" />
              </a-form-item>
              <a-form-item label="自定义名称">
                <a-input v-decorator="['tabName', {rules: [{required: true, message: '请输入新的 Tab 名称'}]}]"/>
              </a-form-item>
            </a-form>
          </div>
        </template>
        <a-button @click="() => visible2 = !visible2">修改某一个 Tab 名称</a-button>
      </a-popconfirm>
    </div>
    <a-divider />
    <div class="page-loading-test">
      <h4>全局遮罩测试</h4>
      <a-button @click="handleOpenLoading" style="margin-right: 16px;">打开遮罩(5s 自动关闭)</a-button>
      <a-button @click="handleOpenLoadingCustomTip">打开遮罩(自定义提示语)</a-button>
    </div>
  </div>
</template>
<script>
export default {
  name: 'TestWork',
  data () {
    return {
      visible: false,
      visible2: false
    }
  },
  created () {
    this.form = this.$form.createForm(this)
    this.form2 = this.$form.createForm(this)
  },
  methods: {
    handleCloseCurrentTab () {
      this.$multiTab.closeCurrentPage() // or this.$multiTab.close()
    },
    handleOpenTab () {
      this.$multiTab.open('/features/task')
    },
    handleOpenLoading () {
      this.$nextTick(function () {
        console.log('this', this)
        console.log('this.$refs.tInput', this.$refs.tInput)
      })
      this.$loading.show()
      setTimeout(() => {
        this.$loading.hide()
      }, 5000)
    },
    handleOpenLoadingCustomTip () {
      this.$loading.show({ tip: '自定义提示语' })
      setTimeout(() => {
        this.$loading.hide()
      }, 5000)
    },
    // confirm
    confirm (e) {
      e.stopPropagation()
      const { path } = this.$route
      this.form.validateFields((err, values) => {
        if (!err) {
          this.$multiTab.rename(path, values.tabName)
          this.visible = false
        }
      })
    },
    cancel () {
      this.visible = false
    },
    confirm2 (e) {
      e.stopPropagation()
      this.form2.validateFields((err, values) => {
        if (!err) {
          this.$multiTab.rename(values.tabKey, values.tabName)
          this.visible2 = false
        }
      })
    }
  }
}
</script>
<style scoped>
</style>
_web/src/views/system/dashboard/Workplace.vue
New file
@@ -0,0 +1,526 @@
<template>
  <page-view :avatar="avatar" :title="false">
    <div slot="headerContent">
      <div class="title">{{ timeFix }},{{ user.name }}<span class="welcome-text">,{{ welcome }}</span></div>
      <div style="margin-bottom:10px">前端工程师 | 蚂蚁金服 - 某某某事业群 - VUE平台</div>
    </div>
    <div slot="extra">
      <a-row class="more-info">
        <a-col :span="8">
          <head-info title="项目" content="56" :center="false" :bordered="false"/>
        </a-col>
        <a-col :span="8">
          <head-info title="团队排名" content="8/24" :center="false" :bordered="false"/>
        </a-col>
        <a-col :span="8">
          <head-info title="项目数" content="2,223" :center="false" />
        </a-col>
      </a-row>
    </div>
    <div>
      <a-row :gutter="24">
        <a-col :xl="16" :lg="24" :md="24" :sm="24" :xs="24">
          <a-card
            class="project-list"
            :loading="loading"
            style="margin-bottom: 24px;"
            :bordered="false"
            title="进行中的项目"
            :body-style="{ padding: 0 }">
            <a slot="extra">全部项目</a>
            <div>
              <a-card-grid class="project-card-grid" :key="i" v-for="(item, i) in projects">
                <a-card :bordered="false" :body-style="{ padding: 0 }">
                  <a-card-meta>
                    <div slot="title" class="card-title">
                      <a-avatar size="small" :src="item.cover"/>
                      <a>{{ item.title }}</a>
                    </div>
                    <div slot="description" class="card-description">
                      {{ item.description }}
                    </div>
                  </a-card-meta>
                  <div class="project-item">
                    <a href="/#/">科学搬砖组</a>
                    <span class="datetime">9小时前</span>
                  </div>
                </a-card>
              </a-card-grid>
            </div>
          </a-card>
          <a-card :loading="loading" title="动态" :bordered="false">
            <a-list>
              <a-list-item :key="index" v-for="(item, index) in activities">
                <a-list-item-meta>
                  <a-avatar slot="avatar" :src="item.user.avatar" />
                  <div slot="title">
                    <span>{{ item.user.nickname }}</span>&nbsp;
                    在&nbsp;<a href="#">{{ item.project.name }}</a>&nbsp;
                    <span>{{ item.project.action }}</span>&nbsp;
                    <a href="#">{{ item.project.event }}</a>
                  </div>
                  <div slot="description">{{ item.time }}</div>
                </a-list-item-meta>
              </a-list-item>
            </a-list>
          </a-card>
        </a-col>
        <a-col
          style="padding: 0 12px"
          :xl="8"
          :lg="24"
          :md="24"
          :sm="24"
          :xs="24">
          <a-card title="快速开始 / 便捷导航" style="margin-bottom: 24px" :bordered="false" :body-style="{padding: 0}">
            <div class="item-group">
              <a>操作一</a>
              <a>操作二</a>
              <a>操作三</a>
              <a>操作四</a>
              <a>操作五</a>
              <a>操作六</a>
              <a-button size="small" type="primary" ghost icon="plus">添加</a-button>
            </div>
          </a-card>
          <a-card title="XX 指数" style="margin-bottom: 24px" :loading="radarLoading" :bordered="false" :body-style="{ padding: 0 }">
            <div style="min-height: 400px;">
              <!-- :scale="scale" :axis1Opts="axis1Opts" :axis2Opts="axis2Opts"  -->
              <radar :data="radarData" />
            </div>
          </a-card>
          <a-card :loading="loading" title="团队" :bordered="false">
            <div class="members">
              <a-row>
                <a-col :span="12" v-for="(item, index) in teams" :key="index">
                  <a>
                    <a-avatar size="small" :src="item.avatar" />
                    <span class="member">{{ item.name }}</span>
                  </a>
                </a-col>
              </a-row>
            </div>
          </a-card>
        </a-col>
      </a-row>
    </div>
  </page-view>
</template>
<script>
  import { timeFix } from '@/utils/util'
  import { mapState } from 'vuex'
  import { PageView } from '@/layouts'
  import HeadInfo from '@/components/tools/HeadInfo'
  import { Radar } from '@/components'
  const DataSet = require('@antv/data-set')
  export default {
    name: 'Workplace',
    components: {
      PageView,
      HeadInfo,
      Radar
    },
    data () {
      return {
        timeFix: timeFix(),
        avatar: '',
        user: {},
        projects: [],
        loading: true,
        radarLoading: true,
        activities: [],
        teams: [],
        // data
        axis1Opts: {
          dataKey: 'item',
          line: null,
          tickLine: null,
          grid: {
            lineStyle: {
              lineDash: null
            },
            hideFirstLine: false
          }
        },
        axis2Opts: {
          dataKey: 'score',
          line: null,
          tickLine: null,
          grid: {
            type: 'polygon',
            lineStyle: {
              lineDash: null
            }
          }
        },
        scale: [{
          dataKey: 'score',
          min: 0,
          max: 80
        }],
        axisData: [
          { item: '引用', a: 70, b: 30, c: 40 },
          { item: '口碑', a: 60, b: 70, c: 40 },
          { item: '产量', a: 50, b: 60, c: 40 },
          { item: '贡献', a: 40, b: 50, c: 40 },
          { item: '热度', a: 60, b: 70, c: 40 },
          { item: '引用', a: 70, b: 50, c: 40 }
        ],
        radarData: []
      }
    },
    computed: {
      ...mapState({
        nickname: (state) => state.user.nickname,
        welcome: (state) => state.user.welcome
      }),
      userInfo () {
        return this.$store.getters.userInfo
      }
    },
    created () {
      this.user = this.userInfo
      this.avatar = process.env.VUE_APP_API_BASE_URL + '/sysFileInfo/preview?id=' + this.userInfo.avatar
    },
    mounted () {
      this.getProjects()
      this.getActivity()
      this.getTeams()
      this.initRadar()
    },
    methods: {
      getProjects () {
        this.projects = [{
          id: 1,
          cover: 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png',
          title: 'Alipay',
          description: '那是一种内在的东西, 他们到达不了,也无法触及的',
          status: 1,
          updatedAt: '2018-07-26 00:00:00'
        },
          {
            id: 2,
            cover: 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png',
            title: 'Angular',
            description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
            status: 1,
            updatedAt: '2018-07-26 00:00:00'
          },
          {
            id: 3,
            cover: 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png',
            title: 'Ant Design',
            description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
            status: 1,
            updatedAt: '2018-07-26 00:00:00'
          },
          {
            id: 4,
            cover: 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png',
            title: 'Snowy',
            description: '那时候我只会想自己想要什么,从不想自己拥有什么',
            status: 1,
            updatedAt: '2018-07-26 00:00:00'
          },
          {
            id: 5,
            cover: 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png',
            title: 'Bootstrap',
            description: '凛冬将至',
            status: 1,
            updatedAt: '2018-07-26 00:00:00'
          },
          {
            id: 6,
            cover: 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png',
            title: 'Vue',
            description: '生命就像一盒巧克力,结果往往出人意料',
            status: 1,
            updatedAt: '2018-07-26 00:00:00'
          }
        ]
        this.loading = false
      },
      getActivity () {
        this.activities = [{
          id: 1,
          user: {
            nickname: '@name',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png'
          },
          project: {
            name: '白鹭酱油开发组',
            action: '更新',
            event: '番组计划'
          },
          time: '2018-08-23 14:47:00'
        },
          {
            id: 1,
            user: {
              nickname: '蓝莓酱',
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png'
            },
            project: {
              name: '白鹭酱油开发组',
              action: '更新',
              event: '番组计划'
            },
            time: '2018-08-23 09:35:37'
          },
          {
            id: 1,
            user: {
              nickname: '@name',
              avatar: '@image(64x64)'
            },
            project: {
              name: '白鹭酱油开发组',
              action: '创建',
              event: '番组计划'
            },
            time: '2017-05-27 00:00:00'
          },
          {
            id: 1,
            user: {
              nickname: '曲丽丽',
              avatar: '@image(64x64)'
            },
            project: {
              name: '高逼格设计天团',
              action: '更新',
              event: '六月迭代'
            },
            time: '2018-08-23 14:47:00'
          },
          {
            id: 1,
            user: {
              nickname: '@name',
              avatar: '@image(64x64)'
            },
            project: {
              name: '高逼格设计天团',
              action: 'created',
              event: '六月迭代'
            },
            time: '2018-08-23 14:47:00'
          },
          {
            id: 1,
            user: {
              nickname: '曲丽丽',
              avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png'
            },
            project: {
              name: '高逼格设计天团',
              action: 'created',
              event: '六月迭代'
            },
            time: '2018-08-23 14:47:00'
          }
        ]
      },
      getTeams () {
        this.teams = [{
          id: 1,
          name: '科学搬砖组',
          avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png'
        },
          {
            id: 2,
            name: '程序员日常',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png'
          },
          {
            id: 1,
            name: '设计天团',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png'
          },
          {
            id: 1,
            name: '中二少女团',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png'
          },
          {
            id: 1,
            name: '骗你学计算机',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png'
          }
        ]
      },
      initRadar () {
        this.radarLoading = true
        const dv = new DataSet.View().source(
          [{
            item: '引用',
            '个人': 70,
            '团队': 30,
            '部门': 40
          },
            {
              item: '口碑',
              '个人': 60,
              '团队': 70,
              '部门': 40
            },
            {
              item: '产量',
              '个人': 50,
              '团队': 60,
              '部门': 40
            },
            {
              item: '贡献',
              '个人': 40,
              '团队': 50,
              '部门': 40
            },
            {
              item: '热度',
              '个人': 60,
              '团队': 70,
              '部门': 40
            },
            {
              item: '引用',
              '个人': 70,
              '团队': 50,
              '部门': 40
            }
          ]
        )
        dv.transform({
          type: 'fold',
          fields: ['个人', '团队', '部门'],
          key: 'user',
          value: 'score'
        })
        this.radarData = dv.rows
        this.radarLoading = false
      }
    }
  }
</script>
<style lang="less" scoped>
  .project-list {
    .card-title {
      font-size: 0;
      a {
        color: rgba(0, 0, 0, 0.85);
        margin-left: 12px;
        line-height: 24px;
        height: 24px;
        display: inline-block;
        vertical-align: top;
        font-size: 14px;
        &:hover {
          color: #1890ff;
        }
      }
    }
    .card-description {
      color: rgba(0, 0, 0, 0.45);
      height: 44px;
      line-height: 22px;
      overflow: hidden;
    }
    .project-item {
      display: flex;
      margin-top: 8px;
      overflow: hidden;
      font-size: 12px;
      height: 20px;
      line-height: 20px;
      a {
        color: rgba(0, 0, 0, 0.45);
        display: inline-block;
        flex: 1 1 0;
        &:hover {
          color: #1890ff;
        }
      }
      .datetime {
        color: rgba(0, 0, 0, 0.25);
        flex: 0 0 auto;
        float: right;
      }
    }
    .ant-card-meta-description {
      color: rgba(0, 0, 0, 0.45);
      height: 44px;
      line-height: 22px;
      overflow: hidden;
    }
  }
  .item-group {
    padding: 20px 0 8px 24px;
    font-size: 0;
    a {
      color: rgba(0, 0, 0, 0.65);
      display: inline-block;
      font-size: 14px;
      margin-bottom: 13px;
      width: 25%;
    }
  }
  .members {
    a {
      display: block;
      margin: 12px 0;
      line-height: 24px;
      height: 24px;
      .member {
        font-size: 14px;
        color: rgba(0, 0, 0, .65);
        line-height: 24px;
        max-width: 100px;
        vertical-align: top;
        margin-left: 12px;
        transition: all 0.3s;
        display: inline-block;
      }
      &:hover {
        span {
          color: #1890ff;
        }
      }
    }
  }
  .mobile {
    .project-list {
      .project-card-grid {
        width: 100%;
      }
    }
    .more-info {
      border: 0;
      padding-top: 16px;
      margin: 16px 0 16px;
    }
    .headerContent .title .welcome-text {
      display: none;
    }
  }
</style>
Diff truncated after the above file
_web/src/views/system/dict/addForm.vue _web/src/views/system/dict/dictdata/addForm.vue _web/src/views/system/dict/dictdata/editForm.vue _web/src/views/system/dict/dictdata/index.vue _web/src/views/system/dict/editForm.vue _web/src/views/system/dict/index.vue _web/src/views/system/email/index.vue _web/src/views/system/exception/403.vue _web/src/views/system/exception/404.vue _web/src/views/system/exception/500.vue _web/src/views/system/file/detailForm.vue _web/src/views/system/file/index.vue _web/src/views/system/file/previewForm.vue _web/src/views/system/fileOnline/detailForm.vue _web/src/views/system/fileOnline/index.vue _web/src/views/system/fileOnline/onlineEditForm.vue _web/src/views/system/fileOnline/previewForm.vue _web/src/views/system/index/welcome.vue _web/src/views/system/log/oplog/details.vue _web/src/views/system/log/oplog/index.vue _web/src/views/system/log/vislog/details.vue _web/src/views/system/log/vislog/index.vue _web/src/views/system/machine/index.vue _web/src/views/system/menu/addForm.vue _web/src/views/system/menu/editForm.vue _web/src/views/system/menu/index.vue _web/src/views/system/notice/addForm.vue _web/src/views/system/notice/detailForm.vue _web/src/views/system/notice/editForm.vue _web/src/views/system/notice/index.vue _web/src/views/system/noticeReceived/detailForm.vue _web/src/views/system/noticeReceived/index.vue _web/src/views/system/onlineUser/index.vue _web/src/views/system/org/addForm.vue _web/src/views/system/org/editForm.vue _web/src/views/system/org/index.vue _web/src/views/system/pos/addForm.vue _web/src/views/system/pos/editForm.vue _web/src/views/system/pos/index.vue _web/src/views/system/role/addForm.vue _web/src/views/system/role/editForm.vue _web/src/views/system/role/index.vue _web/src/views/system/role/roleMenuForm.vue _web/src/views/system/role/roleOrgForm.vue _web/src/views/system/sms/index.vue _web/src/views/system/timers/addForm.vue _web/src/views/system/timers/editForm.vue _web/src/views/system/timers/index.vue _web/src/views/system/user/addForm.vue _web/src/views/system/user/editForm.vue _web/src/views/system/user/index.vue _web/src/views/system/user/userOrgForm.vue _web/src/views/system/user/userRoleForm.vue _web/src/views/userLoginReg/Login.vue _web/src/views/userLoginReg/Register.vue _web/src/views/userLoginReg/RegisterResult.vue _web/tests/unit/.eslintrc.js _web/vue.config.js _web/webstorm.config.js pom.xml snowy-base/README.md snowy-base/pom.xml snowy-base/snowy-core/README.md snowy-base/snowy-core/pom.xml snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/annotion/BusinessLog.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/annotion/DataScope.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/annotion/ExpEnumType.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/annotion/Permission.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/annotion/Wrapper.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/cache/CacheOperator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/cache/base/AbstractMemoryCacheOperator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/cache/base/AbstractRedisCacheOperator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/AopSortConstant.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/CommonConstant.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/ExpEnumConstant.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/MediaTypeConstant.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/SpringSecurityConstant.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/consts/SymbolConstant.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/constant/ConstantContext.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/constant/ConstantContextHolder.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/group/RequestGroupContext.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/group/RequestParamIdContext.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/login/LoginContext.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/login/LoginContextHolder.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/param/RequestParamContext.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/requestno/RequestNoContext.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/resources/ApiResourceContext.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/system/SystemContext.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/context/system/SystemContextHolder.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/cryptogram/keypair.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/dbs/CurrentDataSourceContext.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/email/MailSender.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/email/modular/SimpleMailSender.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/email/modular/exception/MailSendException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/email/modular/model/SendMailParam.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/CommonStatusEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/DbIdEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/DocumentFormatEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/LogAnnotionOpTypeEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/LogicTypeEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/enums/YesOrNotEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/AuthException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/DemoException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/LibreOfficeException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/PermissionException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/RequestMethodException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/ServiceException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/AuthExceptionEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/ParamExceptionEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/PermissionExceptionEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/RequestMethodExceptionEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/RequestTypeExceptionEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/ServerExceptionEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/StatusExceptionEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/WrapperExceptionEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/exception/enums/abs/AbstractBaseExceptionEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/factory/ExpEnumCodeFactory.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/factory/PageFactory.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/factory/TreeBuildFactory.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/FileOperator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/common/enums/BucketAuthEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/common/exp/FileServiceException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/aliyun/AliyunFileOperator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/aliyun/exp/AliyunFileServiceException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/aliyun/prop/AliyunOssProperties.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/local/LocalFileOperator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/local/prop/LocalFileProperties.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/tencent/TenFileOperator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/tencent/exp/TencentFileServiceException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/file/modular/tencent/prop/TenCosProperties.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/base/entity/BaseEntity.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/base/node/BaseTreeNode.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/base/param/BaseParam.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/base/validate/UniqueValidateParam.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/base/wrapper/BaseWrapper.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/cryptogram/CryptogramConfigs.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/druid/DruidProperties.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/email/EmailConfigs.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/login/LoginEmpInfo.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/login/SnowyAuthority.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/login/SysLoginUser.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/node/AntdBaseTreeNode.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/node/CommonBaseTreeNode.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/node/LoginMenuTreeNode.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/oauth/OauthConfigs.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/page/PageResult.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/response/ErrorResponseData.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/response/ResponseData.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/response/SuccessResponseData.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/sms/AliyunSmsConfigs.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/pojo/sms/TencentSmsConfigs.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/SmsSender.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/AliyunSmsSender.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/enums/AliyunSmsResultEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/exp/AliyunSmsException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/msign/MultiSignManager.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/msign/impl/MapBasedMultiSignManager.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/aliyun/prop/AliyunSmsProperties.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/tencent/TencentSmsSender.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/tencent/exp/TencentSmsException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/sms/modular/tencent/prop/TencentSmsProperties.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/consts/TenantConstants.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/consts/TenantExpEnumConstant.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/context/TenantCodeHolder.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/context/TenantDbNameHolder.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/entity/TenantInfo.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/exception/TenantException.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/exception/enums/TenantExceptionEnum.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/params/TenantInfoParam.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/tenant/service/TenantInfoService.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/timer/TimerTaskRunner.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/AopTargetUtil.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/CryptogramUtil.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/HttpServletUtil.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/IpAddressUtil.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/JoinPointUtil.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/LibreOfficeUtil.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/PageUtil.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/PastTimeFormatUtil.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/PoiUtil.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/ResponseUtil.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/util/UaUtil.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/date/DateValue.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/date/DateValueValidator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateordatetime/DateOrDateTimeValue.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateordatetime/DateOrDateTimeValueValidator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateormonth/DateOrMonthValue.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateormonth/DateOrMonthValueValidator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateortime/DateOrTimeValue.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dateortime/DateOrTimeValueValidator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/datetime/DateTimeValue.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/datetime/DateTimeValueValidator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dict/DictValue.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/dict/DictValueValidator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/flag/FlagValue.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/flag/FlagValueValidator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/month/MonthValue.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/month/MonthValueValidator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/mothordatetime/MonthOrDateTimeValue.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/mothordatetime/MonthOrDateTimeValueValidator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/time/TimeValue.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/time/TimeValueValidator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/unique/TableUniqueValue.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/validation/unique/TableUniqueValueValidator.java snowy-base/snowy-core/src/main/java/vip/xiaonuo/core/web/SnowyRequestResponseBodyMethodProcessor.java snowy-base/snowy-gen/README.md snowy-base/snowy-gen/pom.xml snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/consts/GenConstant.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/consts/GenExpEnumConstant.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/context/XnVelocityContext.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/enums/QueryTypeEnum.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/enums/TableFilteredFieldsEnum.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/param/TableField.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/param/XnCodeGenParam.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/tool/JavaEffTool.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/tool/JavaSqlTool.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/tool/NamingConTool.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/tool/StringDateTool.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/core/util/Util.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/controller/CodeGenerateController.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/controller/SysCodeGenerateConfigController.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/entity/CodeGenerate.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/entity/SysCodeGenerateConfig.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/enums/CodeGenerateExceptionEnum.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/enums/SysCodeGenerateConfigExceptionEnum.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/mapper/CodeGenerateMapper.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/mapper/SysCodeGenerateConfigMapper.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/mapper/mapping/CodeGenerateMapper.xml snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/mapper/mapping/SysCodeGenerateConfigMapper.xml snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/param/CodeGenerateParam.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/param/SysCodeGenerateConfigParam.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/result/InforMationColumnsResult.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/result/InformationResult.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/service/CodeGenerateService.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/service/SysCodeGenerateConfigService.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/service/impl/CodeGenerateServiceImpl.java snowy-base/snowy-gen/src/main/java/vip/xiaonuo/generate/modular/service/impl/SysCodeGenerateConfigServiceImpl.java snowy-base/snowy-gen/src/main/resources/template/Controller.java.vm snowy-base/snowy-gen/src/main/resources/template/ExceptionEnum.java.vm snowy-base/snowy-gen/src/main/resources/template/Manage.js.vm snowy-base/snowy-gen/src/main/resources/template/Mapper.java.vm snowy-base/snowy-gen/src/main/resources/template/Mapper.xml.vm snowy-base/snowy-gen/src/main/resources/template/Param.java.vm snowy-base/snowy-gen/src/main/resources/template/Service.java.vm snowy-base/snowy-gen/src/main/resources/template/ServiceImpl.java.vm snowy-base/snowy-gen/src/main/resources/template/XnMysql.sql.vm snowy-base/snowy-gen/src/main/resources/template/XnOracle.sql.vm snowy-base/snowy-gen/src/main/resources/template/addForm.vue.vm snowy-base/snowy-gen/src/main/resources/template/editForm.vue.vm snowy-base/snowy-gen/src/main/resources/template/entity.java.vm snowy-base/snowy-gen/src/main/resources/template/index.vue.vm snowy-base/snowy-system/README.md snowy-base/snowy-system/pom.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/AopConfig.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/CacheConfig.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/DataSourceConfig.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/FileConfig.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/MailSenderConfig.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/MybatisConfig.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/SmsSenderConfig.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/SpringSecurityConfig.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/SwaggerConfig.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/config/WebMvcConfig.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/aop/BusinessLogAop.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/aop/DataScopeAop.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/aop/PermissionAop.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/aop/WrapperAop.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/cache/MappingCache.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/cache/OauthCache.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/cache/ResourceCache.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/cache/UserCache.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/consts/SysExpEnumConstant.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/context/SystemContextImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/AdminTypeEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/DataScopeTypeEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/LogSuccessStatusEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/MenuOpenTypeEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/MenuTypeEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/MenuWeightEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/NoticeStatusEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/NoticeUserStatusEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/OauthPlatformEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/OauthSexEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/SexEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/enums/VisLogTypeEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/error/GlobalExceptionHandler.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/error/SnowyErrorAttributes.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/filter/RequestNoFilter.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/filter/security/JwtAuthenticationTokenFilter.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/filter/security/entrypoint/JwtAuthenticationEntryPoint.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/filter/xss/XssFilter.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/filter/xss/XssHttpServletRequestWrapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/jwt/JwtPayLoad.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/jwt/JwtTokenUtil.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/listener/ConstantsInitListener.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/listener/RemoveRequestParamListener.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/listener/ResourceCollectListener.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/listener/TimerTaskRunListener.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/log/LogManager.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/log/factory/LogFactory.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/log/factory/LogTaskFactory.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/mybatis/dbid/SnowyDatabaseIdProvider.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/mybatis/fieldfill/CustomMetaObjectHandler.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/mybatis/sqlfilter/DemoProfileSqlInterceptor.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/provider/CaptchaCacheServiceProvider.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/redis/FastJson2JsonRedisSerializer.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/scanner/ApiResourceScanner.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/core/validator/SnowyValidator.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/IndexController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/controller/SysAppController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/entity/SysApp.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/enums/SysAppExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/mapper/SysAppMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/mapper/mapping/SysAppMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/param/SysAppParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/service/SysAppService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/app/service/impl/SysAppServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/controller/SysAreaController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/entity/SysArea.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/mapper/SysAreaMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/mapper/mapping/SysAreaMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/param/SysAreaParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/service/SysAreaService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/area/service/impl/SysAreaServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/auth/context/LoginContextSpringSecurityImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/auth/controller/SysLoginController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/auth/factory/LoginUserFactory.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/auth/service/AuthService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/auth/service/impl/AuthServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/controller/SysConfigController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/entity/SysConfig.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/enums/SysConfigExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/mapper/SysConfigMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/mapper/mapping/SysConfigMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/param/SysConfigParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/service/SysConfigService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/consts/service/impl/SysConfigServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/controller/SysDictDataController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/controller/SysDictTypeController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/entity/SysDictData.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/entity/SysDictType.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/enums/SysDictDataExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/enums/SysDictTypeExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/mapper/SysDictDataMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/mapper/SysDictTypeMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/mapper/mapping/SysDictDataMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/mapper/mapping/SysDictTypeMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/param/SysDictDataParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/param/SysDictTypeParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/result/SysDictTreeNode.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/service/SysDictDataService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/service/SysDictTypeService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/service/impl/SysDictDataServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/dict/service/impl/SysDictTypeServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/email/controler/EmailController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/email/enums/SysEmailExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/entity/SysEmp.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/entity/SysEmpExtOrgPos.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/entity/SysEmpPos.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/SysEmpExtOrgPosMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/SysEmpMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/SysEmpPosMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/mapping/SysEmpExtOrgPosMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/mapping/SysEmpMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/mapper/mapping/SysEmpPosMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/param/SysEmpParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/result/SysEmpInfo.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/SysEmpExtOrgPosService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/SysEmpPosService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/SysEmpService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/impl/SysEmpExtOrgPosPosServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/impl/SysEmpPosServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/emp/service/impl/SysEmpServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/controller/SysFileInfoController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/entity/SysFileInfo.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/enums/SysFileInfoExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/enums/SysFileLocationEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/mapper/SysFileInfoMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/mapper/mapping/SysFileInfoMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/param/SysFileInfoParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/result/SysFileInfoResult.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/result/SysOnlineFileInfoResult.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/service/SysFileInfoService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/service/impl/SysFileInfoServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/util/DownloadUtil.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/file/util/OnlineDocumentUtil.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/controller/SysLogController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/entity/SysOpLog.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/entity/SysVisLog.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/mapper/SysOpLogMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/mapper/SysVisLogMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/mapper/mapping/SysOpLogMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/mapper/mapping/SysVisLogMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/param/SysOpLogParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/param/SysVisLogParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/service/SysOpLogService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/service/SysVisLogService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/service/impl/SysOpLogServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/log/service/impl/SysVisLogServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/controller/SysMenuController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/entity/SysMenu.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/enums/SysMenuExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/mapper/SysMenuMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/mapper/mapping/SysMenuMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/node/MenuBaseTreeNode.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/param/SysMenuParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/service/SysMenuService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/menu/service/impl/SysMenuServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/controller/SysMachineController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/controller/SysOnlineUserController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/param/SysOnlineUserParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/result/SysMachineResult.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/result/SysOnlineUserResult.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/service/SysMachineService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/service/SysOnlineUserService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/service/impl/SysMachineServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/monitor/service/impl/SysOnlineUserServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/controller/SysNoticeController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/entity/SysNotice.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/entity/SysNoticeUser.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/enums/SysNoticeExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/mapper/SysNoticeMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/mapper/SysNoticeUserMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/mapper/mapping/SysNoticeMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/mapper/mapping/SysNoticeUserMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/param/SysNoticeParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/result/SysNoticeDetailResult.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/result/SysNoticeReceiveResult.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/service/SysNoticeService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/service/SysNoticeUserService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/service/impl/SysNoticeServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/notice/service/impl/SysNoticeUserServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/controller/SysOauthController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/entity/SysOauthUser.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/enums/SysOauthExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/mapper/SysOauthMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/mapper/mapping/SysOauthMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/service/SysOauthService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/oauth/service/impl/SysOauthServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/controller/SysOrgController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/entity/SysOrg.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/enums/SysOrgExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/mapper/SysOrgMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/mapper/mapping/SysOrgMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/param/SysOrgParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/service/SysOrgService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/org/service/impl/SysOrgServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/controller/SysPosController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/entity/SysPos.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/enums/SysPosExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/mapper/SysPosMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/mapper/mapping/SysPosMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/param/SysPosParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/service/SysPosService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/pos/service/impl/SysPosServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/controller/SysRoleController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/entity/SysRole.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/entity/SysRoleDataScope.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/entity/SysRoleMenu.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/enums/SysRoleExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/SysRoleDataScopeMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/SysRoleMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/SysRoleMenuMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/mapping/SysRoleDataScopeMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/mapping/SysRoleMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/mapper/mapping/SysRoleMenuMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/param/SysRoleParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/SysRoleDataScopeService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/SysRoleMenuService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/SysRoleService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleDataScopeServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleMenuServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/controller/SmsSenderController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/entity/SysSms.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/enums/SmsSendExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/enums/SmsSendSourceEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/enums/SmsSendStatusEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/enums/SmsTypeEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/enums/SmsVerifyEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/mapper/SysSmsMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/mapper/mapping/SysSmsMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/param/SysSmsInfoParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/param/SysSmsSendParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/param/SysSmsVerifyParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/service/SmsSenderService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/service/SysSmsInfoService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/service/impl/SmsSenderServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/sms/service/impl/SysSmsInfoServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/controller/SysTimersController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/entity/SysTimers.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/enums/TimerJobStatusEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/enums/exp/SysTimersExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/mapper/SysTimersMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/mapper/mapping/SysTimersMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/param/SysTimersParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/service/SysTimersService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/service/TimerExeService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/service/impl/HutoolTimerExeServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/service/impl/SysTimersServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/tasks/RefreshConstantsTaskRunner.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/timer/tasks/SystemOutTaskRunner.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/controller/SysUserController.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/entity/SysUser.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/entity/SysUserDataScope.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/entity/SysUserRole.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/enums/SysUserExceptionEnum.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/factory/SysUserFactory.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/SysUserDataScopeMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/SysUserMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/SysUserRoleMapper.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/mapping/SysUserDataScopeMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/mapping/SysUserMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/mapper/mapping/SysUserRoleMapper.xml snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/param/SysUserParam.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/result/SysUserResult.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserDataScopeService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserRoleService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserService.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserDataScopeServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserRoleServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserServiceImpl.java snowy-base/snowy-system/src/main/java/vip/xiaonuo/sys/modular/user/wrapper/SysUserWrapper.java snowy-base/snowy-system/src/main/resources/META-INF/spring.factories snowy-main/README.md snowy-main/pom.xml snowy-main/src/main/docker/docker-assembly.xml snowy-main/src/main/java/vip/xiaonuo/SnowyApplication.java snowy-main/src/main/java/vip/xiaonuo/SnowyServletInitializer.java snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/controller/BlogArticleController.java snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/entity/BlogArticle.java snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/enums/BlogArticleExceptionEnum.java snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/mapper/BlogArticleMapper.java snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/mapper/mapping/BlogArticleMapper.xml snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/param/BlogArticleParam.java snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/service/BlogArticleService.java snowy-main/src/main/java/vip/xiaonuo/modular/blogarticle/service/impl/BlogArticleServiceImpl.java snowy-main/src/main/java/vip/xiaonuo/modular/controller/DatasourceExampleController.java snowy-main/src/main/java/vip/xiaonuo/modular/model/AbModel.java snowy-main/src/main/java/vip/xiaonuo/modular/service/DatasourceExampleService.java snowy-main/src/main/resources/application-dev.yml snowy-main/src/main/resources/application-local.yml snowy-main/src/main/resources/application-prod.yml snowy-main/src/main/resources/application.yml snowy-main/src/main/resources/banner.txt snowy-main/src/main/resources/logback-spring.xml snowy-main/src/test/java/vip/xiaonuo/core/BaseJunit.java snowy-main/src/test/java/vip/xiaonuo/core/Test.java snowy-main/src/test/java/vip/xiaonuo/core/Test2.java snowy-main/src/test/sql/test.sql