commit d7c45eaeeb8cb30a6a59c943fa895c8d86a55bc8 Author: enoch Date: Tue Apr 6 10:05:42 2021 +0800 init diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000..8f96043 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,3 @@ +> 1% +last 2 versions +not ie <= 10 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6f77dff --- /dev/null +++ b/.editorconfig @@ -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 + diff --git a/.env b/.env new file mode 100644 index 0000000..6cb9595 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +NODE_ENV=production +VUE_APP_PREVIEW=false +VUE_APP_BASE_API=/api \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..4126bbe --- /dev/null +++ b/.env.development @@ -0,0 +1,3 @@ +NODE_ENV=development +VUE_APP_PREVIEW=true +VUE_APP_BASE_API=/api \ No newline at end of file diff --git a/.env.preview b/.env.preview new file mode 100644 index 0000000..9684d40 --- /dev/null +++ b/.env.preview @@ -0,0 +1,3 @@ +NODE_ENV=production +VUE_APP_PREVIEW=true +VUE_APP_BASE_API=/api \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..7c2225a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,76 @@ +module.exports = { + root: true, + env: { + node: true + }, + 'extends': [ + 'plugin:vue/strongly-recommended', + '@vue/standard' + ], + rules: { + "space-before-function-paren": 0, + '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' + }, + parserOptions: { + parser: 'babel-eslint' + }, + overrides: [ + { + files: [ + '**/__tests__/*.{j,t}s?(x)', + '**/tests/unit/**/*.spec.{j,t}s?(x)' + ], + env: { + jest: true + } + } + ] +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..ed223c7 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "space-before-function-paren": 0 + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e507319 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +public/* linguist-vendored \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3beccd --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw* +package-lock.json +yarn.lock diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b0e80f7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "semi": false, + "singleQuote": true, + "prettier.spaceBeforeFunctionParen": true +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a08bfcb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - 10.15.0 +cache: yarn +script: + - yarn + - yarn run lint --no-fix && yarn run build diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..413013a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 扶醉 + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e300805 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +

RuoYi-Antdv

+ +> RuoYi-Antdv 是RuoYi-Vue和Ant Design Vue Pro的结合。 + +

+ Build Status  + Build Status  + Build Status  + Build Status  + Build Status +

+ + +------------------------------ + +## 简介 + +**RuoYi-Antdv**,使用[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue)作为后端,改其原有Element Ui为Ant Design Vue。将会持续完全适配RuoYi-Vue。 + +> RuoYi-Vue是基于SpringBoot,Spring Security,JWT,Vue 的前后端分离权限管理系统。 +> +> 拥有用户管理、部门管理、岗位管理、菜单管理、角色管理、字典管理、参数管理、通知公告、操作日志、登录日志、在线用户、定时任务、代码生成、系统接口、服务监控、在线构建器、连接池监视等功能。 + +*当前暂不支持RuoYi中的在线构建器功能。* + +* 预览:[https://ruoyi.setworld.net/](https://ruoyi.setworld.net/) + +* 文档:[https://docs.geekera.cn/RuoYi-Antdv/](https://docs.geekera.cn/RuoYi-Antdv/) + +* RuoYi-Vue文档:[https://doc.ruoyi.vip/ruoyi-vue/](https://doc.ruoyi.vip/ruoyi-vue/) + +* Ant Design Vue文档:[https://www.antdv.com/docs/vue/introduce-cn/](https://www.antdv.com/docs/vue/introduce-cn/) + + + +## 开始使用 + +1. 环境准备 + * 运行启动RuoYi-Vue,参考[RuoYi文档](https://doc.ruoyi.vip/ruoyi-vue/) + * 安装[node](http://nodejs.org/)和[git](https://git-scm.com/) + +1. 安装 + + ```shell + git clone git@gitee.com:fuzui/RuoYi-Antdv.git + ``` + + 或 + + ```shell + git clone git@github.com:fuzui/RuoYi-Antdv.git + ``` + +2. 本地开发 + + 进入项目根目录 + + ```shell + npm install + ``` + + > 若耗时太长可使用`npm install --registry=https://registry.npm.taobao.org` + + ```shell + npm run serve + ``` + + > 打开浏览器访问 [http://localhost:8000](http://localhost:8080/) + + + +## 注意事项 + +* 若需使用代码生成,请按照文档描述修改: + [代码生成前端适配](./docs/gen/) + + + +## 致谢 + +* [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) 基于SpringBoot,Spring Security,JWT,Vue 的前后端分离权限管理系统 +* [Ant Design Vue](https://github.com/vueComponent/ant-design-vue/) An enterprise-class UI components based on Ant Design and Vue +* [Ant Design Vue Pro](https://github.com/vueComponent/ant-design-vue-pro) Use Ant Design Vue like a Pro +* [vditor](https://github.com/Vanessa219/vditor) 一款浏览器端的 Markdown 编辑 + + + +## 推荐 + +* [RuoYi-Cloud-Ant-Design](https://gitee.com/xuezipeng/ruoyi-cloud-ant-design) RuoYi-Cloud的Ant Design(Antdv)版前端 + +* [RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud) 基于Spring Boot、Spring Cloud & Alibaba的分布式微服务架构权限管理系统 + +## 联系 + +如果您发现了什么bug,或者有什么界面建议或意见, + +欢迎 [issue](https://github.com/fuzui/RuoYi-Antdv/issues) + +`RuoYi-Antdv(SpringBoot/SpringCloud)`群: + + [![加入QQ群](https://img.shields.io/badge/Q群-1038609759-blue.svg)](https://jq.qq.com/?_wv=1027&k=Cq8fZnrj) + +如果讨论后端,推荐添加若依前后端分离交流群(官方): + + [![加入QQ群](https://img.shields.io/badge/Q群-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) + + + +## 演示图 + + + + + + + + + + + + + + +
+ + + diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..e80ad97 --- /dev/null +++ b/babel.config.js @@ -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 +} diff --git a/config/plugin.config.js b/config/plugin.config.js new file mode 100644 index 0000000..bb51b98 --- /dev/null +++ b/config/plugin.config.js @@ -0,0 +1,49 @@ +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 + // fixed https://github.com/vueComponent/ant-design-vue-pro/issues/876 + case '.ant-steps-item-process .ant-steps-item-icon': + return ':not(.ant-steps-item-custom)' + 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 diff --git a/config/themePluginConfig.js b/config/themePluginConfig.js new file mode 100644 index 0000000..d35ef8b --- /dev/null +++ b/config/themePluginConfig.js @@ -0,0 +1,115 @@ +export default { + theme: [ + { + key: 'dark', + fileName: 'dark.css', + theme: 'dark' + }, + { + key: '#F5222D', + fileName: '#F5222D.css', + modifyVars: { + '@primary-color': '#F5222D' + } + }, + { + key: '#FA541C', + fileName: '#FA541C.css', + modifyVars: { + '@primary-color': '#FA541C' + } + }, + { + key: '#FAAD14', + fileName: '#FAAD14.css', + modifyVars: { + '@primary-color': '#FAAD14' + } + }, + { + key: '#13C2C2', + fileName: '#13C2C2.css', + modifyVars: { + '@primary-color': '#13C2C2' + } + }, + { + key: '#52C41A', + fileName: '#52C41A.css', + modifyVars: { + '@primary-color': '#52C41A' + } + }, + { + key: '#2F54EB', + fileName: '#2F54EB.css', + modifyVars: { + '@primary-color': '#2F54EB' + } + }, + { + key: '#722ED1', + fileName: '#722ED1.css', + modifyVars: { + '@primary-color': '#722ED1' + } + }, + + { + key: '#F5222D', + theme: 'dark', + fileName: 'dark-#F5222D.css', + modifyVars: { + '@primary-color': '#F5222D' + } + }, + { + key: '#FA541C', + theme: 'dark', + fileName: 'dark-#FA541C.css', + modifyVars: { + '@primary-color': '#FA541C' + } + }, + { + key: '#FAAD14', + theme: 'dark', + fileName: 'dark-#FAAD14.css', + modifyVars: { + '@primary-color': '#FAAD14' + } + }, + { + key: '#13C2C2', + theme: 'dark', + fileName: 'dark-#13C2C2.css', + modifyVars: { + '@primary-color': '#13C2C2' + } + }, + { + key: '#52C41A', + theme: 'dark', + fileName: 'dark-#52C41A.css', + modifyVars: { + '@primary-color': '#52C41A' + } + }, + { + key: '#2F54EB', + theme: 'dark', + fileName: 'dark-#2F54EB.css', + modifyVars: { + '@primary-color': '#2F54EB' + } + }, + { + key: '#722ED1', + theme: 'dark', + fileName: 'dark-#722ED1.css', + modifyVars: { + '@primary-color': '#722ED1' + } + } + ] +} diff --git a/docs/add-page-loading-animate.md b/docs/add-page-loading-animate.md new file mode 100644 index 0000000..dfe4919 --- /dev/null +++ b/docs/add-page-loading-animate.md @@ -0,0 +1,31 @@ +为首屏增加 加载动画 +==== + + + +## 需求 + +> 为了缓解用户第一次访问时,加载 JS 过大所导致用户等待白屏时间过长导致的用户体验不好,进行的一个优化动效。 + + + +## 实现方案 + +1. 将 动画加载 dom 元素放在 #app 内,Vue 生命周期开始时,会自动清掉 #app 下的所有元素。 +2. 将 动画加载 dom 元素放在 body 下,Vue 生命周期开始时 App.vue (created, mounted) 调用 `@/utils/utll` 下的 removeLoadingAnimate(#id, timeout) 则会移除加载动画 + +最后一步: +​ 将样式插入到 `public/index.html` 文件的 `` 最好写成内联 `` + + + +---- + +目前提供有两个样式,均在 `public/loading` 文件夹内。且 pro 已经默认使用了一套 loading 动画方案,可以直接参考 `public/index.html` + + +## 写在最后 + +目前 pro 有页面 overflow 显示出浏览器滚动条时,页面会抖动一下的问题。 + +欢迎各位提供能解决的方案和实现 demo。如果在条件允许的情况下,建议请直接使用 pro 进行改造,也欢迎直接 PR 到 pro 的仓库 diff --git a/docs/gen/README.md b/docs/gen/README.md new file mode 100644 index 0000000..9c67785 --- /dev/null +++ b/docs/gen/README.md @@ -0,0 +1,24 @@ +代码生成 +==== + + + +## 需求 + +> 因代码生成由后端实现,所以若需要使用代码生成功能来生成前端代码的话,需要以下修改。 + + + +## 实现方案 + +1. 将本目录下`antdv`文件夹放入`RuoYi-Vue`中代码生成模块(ruoyi-generator)中`resource`的`vm`文件夹下; + +2. 将本目录中`VelocityUtils.java`文件替换掉代码生成模块中`com.ruoyi.generator.util`的`VelocityUtils.java`文件; + +3. 重启后台项目,代码生成即由`element-ui`成功改为`ant design vue`。 + + + +![需替换及新增的位置](https://oss.fuzui.net/img/20210202004013.png) + + diff --git a/docs/gen/VelocityUtils.java b/docs/gen/VelocityUtils.java new file mode 100644 index 0000000..0405c61 --- /dev/null +++ b/docs/gen/VelocityUtils.java @@ -0,0 +1,374 @@ +package com.ruoyi.generator.util; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import org.apache.velocity.VelocityContext; +import com.alibaba.fastjson.JSONObject; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 模板处理工具类 + * + * @author ruoyi + */ +public class VelocityUtils +{ + /** 项目空间路径 */ + private static final String PROJECT_PATH = "main/java"; + + /** mybatis空间路径 */ + private static final String MYBATIS_PATH = "main/resources/mapper"; + + /** 默认上级菜单,系统工具 */ + private static final String DEFAULT_PARENT_MENU_ID = "3"; + + /** + * 设置模板变量信息 + * + * @return 模板列表 + */ + public static VelocityContext prepareContext(GenTable genTable) + { + String moduleName = genTable.getModuleName(); + String businessName = genTable.getBusinessName(); + String packageName = genTable.getPackageName(); + String tplCategory = genTable.getTplCategory(); + String functionName = genTable.getFunctionName(); + + VelocityContext velocityContext = new VelocityContext(); + velocityContext.put("tplCategory", genTable.getTplCategory()); + velocityContext.put("tableName", genTable.getTableName()); + velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); + velocityContext.put("ClassName", genTable.getClassName()); + velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName())); + velocityContext.put("moduleName", genTable.getModuleName()); + velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName())); + velocityContext.put("businessName", genTable.getBusinessName()); + velocityContext.put("basePackage", getPackagePrefix(packageName)); + velocityContext.put("packageName", packageName); + velocityContext.put("author", genTable.getFunctionAuthor()); + velocityContext.put("datetime", DateUtils.getDate()); + velocityContext.put("pkColumn", genTable.getPkColumn()); + velocityContext.put("importList", getImportList(genTable)); + velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName)); + velocityContext.put("columns", genTable.getColumns()); + velocityContext.put("table", genTable); + setMenuVelocityContext(velocityContext, genTable); + if (GenConstants.TPL_TREE.equals(tplCategory)) + { + setTreeVelocityContext(velocityContext, genTable); + } + if (GenConstants.TPL_SUB.equals(tplCategory)) + { + setSubVelocityContext(velocityContext, genTable); + } + return velocityContext; + } + + public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSONObject.parseObject(options); + String parentMenuId = getParentMenuId(paramsObj); + context.put("parentMenuId", parentMenuId); + } + + public static void setTreeVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSONObject.parseObject(options); + String treeCode = getTreecode(paramsObj); + String treeParentCode = getTreeParentCode(paramsObj); + String treeName = getTreeName(paramsObj); + + context.put("treeCode", treeCode); + context.put("treeParentCode", treeParentCode); + context.put("treeName", treeName); + context.put("expandColumn", getExpandColumn(genTable)); + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + context.put("tree_parent_code", paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + context.put("tree_name", paramsObj.getString(GenConstants.TREE_NAME)); + } + } + + public static void setSubVelocityContext(VelocityContext context, GenTable genTable) + { + GenTable subTable = genTable.getSubTable(); + String subTableName = genTable.getSubTableName(); + String subTableFkName = genTable.getSubTableFkName(); + String subClassName = genTable.getSubTable().getClassName(); + String subTableFkClassName = StringUtils.convertToCamelCase(subTableFkName); + + context.put("subTable", subTable); + context.put("subTableName", subTableName); + context.put("subTableFkName", subTableFkName); + context.put("subTableFkClassName", subTableFkClassName); + context.put("subTableFkclassName", StringUtils.uncapitalize(subTableFkClassName)); + context.put("subClassName", subClassName); + context.put("subclassName", StringUtils.uncapitalize(subClassName)); + context.put("subImportList", getImportList(genTable.getSubTable())); + } + + /** + * 获取模板信息 + * + * @return 模板列表 + */ + public static List getTemplateList(String tplCategory) + { + List templates = new ArrayList(); + templates.add("vm/java/domain.java.vm"); + templates.add("vm/java/mapper.java.vm"); + templates.add("vm/java/service.java.vm"); + templates.add("vm/java/serviceImpl.java.vm"); + templates.add("vm/java/controller.java.vm"); + templates.add("vm/xml/mapper.xml.vm"); + templates.add("vm/sql/sql.vm"); + templates.add("vm/antdv/js/api.js.vm"); + if (GenConstants.TPL_CRUD.equals(tplCategory) || GenConstants.TPL_TREE.equals(tplCategory)) + { + templates.add("vm/antdv/index.vue.vm"); + templates.add("vm/antdv/modules/CreateForm.vue.vm"); + } + else if (GenConstants.TPL_SUB.equals(tplCategory)) + { + templates.add("vm/antdv/index.vue.vm"); + templates.add("vm/antdv/modules/CreateForm.vue.vm"); + templates.add("vm/antdv/modules/SubTable.vue.vm"); + templates.add("vm/antdv/modules/CreateSubForm.vue.vm"); + templates.add("vm/java/sub-domain.java.vm"); + } + return templates; + } + + /** + * 获取文件名 + */ + public static String getFileName(String template, GenTable genTable) + { + // 文件名称 + String fileName = ""; + // 包路径 + String packageName = genTable.getPackageName(); + // 模块名 + String moduleName = genTable.getModuleName(); + // 大写类名 + String className = genTable.getClassName(); + // 业务名称 + String businessName = genTable.getBusinessName(); + + String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/"); + String mybatisPath = MYBATIS_PATH + "/" + moduleName; + String vuePath = "vue"; + + if (template.contains("domain.java.vm")) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, className); + } + if (template.contains("sub-domain.java.vm") && StringUtils.equals(GenConstants.TPL_SUB, genTable.getTplCategory())) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, genTable.getSubTable().getClassName()); + } + else if (template.contains("mapper.java.vm")) + { + fileName = StringUtils.format("{}/mapper/{}Mapper.java", javaPath, className); + } + else if (template.contains("service.java.vm")) + { + fileName = StringUtils.format("{}/service/I{}Service.java", javaPath, className); + } + else if (template.contains("serviceImpl.java.vm")) + { + fileName = StringUtils.format("{}/service/impl/{}ServiceImpl.java", javaPath, className); + } + else if (template.contains("controller.java.vm")) + { + fileName = StringUtils.format("{}/controller/{}Controller.java", javaPath, className); + } + else if (template.contains("mapper.xml.vm")) + { + fileName = StringUtils.format("{}/{}Mapper.xml", mybatisPath, className); + } + else if (template.contains("sql.vm")) + { + fileName = businessName + "Menu.sql"; + } + else if (template.contains("api.js.vm")) + { + fileName = StringUtils.format("{}/api/{}/{}.js", vuePath, moduleName, businessName); + } + else if (template.contains("index.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + else if (template.contains("CreateForm.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/modules/CreateForm.vue", vuePath, moduleName, businessName); + } + else if (template.contains("index-tree.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + else if (template.contains("SubTable.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/modules/SubTable.vue", vuePath, moduleName, businessName); + } + else if (template.contains("CreateSubForm.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/modules/CreateSubForm.vue", vuePath, moduleName, businessName); + } + System.out.println(fileName); + return fileName; + } + + /** + * 获取包前缀 + * + * @param packageName 包名称 + * @return 包前缀名称 + */ + public static String getPackagePrefix(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + String basePackage = StringUtils.substring(packageName, 0, lastIndex); + return basePackage; + } + + /** + * 根据列类型获取导入包 + * + * @param genTable 业务表对象 + * @return 返回需要导入的包列表 + */ + public static HashSet getImportList(GenTable genTable) + { + List columns = genTable.getColumns(); + GenTable subGenTable = genTable.getSubTable(); + HashSet importList = new HashSet(); + if (StringUtils.isNotNull(subGenTable)) + { + importList.add("java.util.List"); + } + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) + { + importList.add("java.util.Date"); + importList.add("com.fasterxml.jackson.annotation.JsonFormat"); + } + else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) + { + importList.add("java.math.BigDecimal"); + } + } + return importList; + } + + /** + * 获取权限前缀 + * + * @param moduleName 模块名称 + * @param businessName 业务名称 + * @return 返回权限前缀 + */ + public static String getPermissionPrefix(String moduleName, String businessName) + { + return StringUtils.format("{}:{}", moduleName, businessName); + } + + /** + * 获取上级菜单ID字段 + * + * @param paramsObj 生成其他选项 + * @return 上级菜单ID字段 + */ + public static String getParentMenuId(JSONObject paramsObj) + { + if (StringUtils.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID)) + { + return paramsObj.getString(GenConstants.PARENT_MENU_ID); + } + return DEFAULT_PARENT_MENU_ID; + } + + /** + * 获取树编码 + * + * @param paramsObj 生成其他选项 + * @return 树编码 + */ + public static String getTreecode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树父编码 + * + * @param paramsObj 生成其他选项 + * @return 树父编码 + */ + public static String getTreeParentCode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树名称 + * + * @param paramsObj 生成其他选项 + * @return 树名称 + */ + public static String getTreeName(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_NAME)); + } + return StringUtils.EMPTY; + } + + /** + * 获取需要在哪一列上面显示展开按钮 + * + * @param genTable 业务表对象 + * @return 展开按钮列序号 + */ + public static int getExpandColumn(GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSONObject.parseObject(options); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + int num = 0; + for (GenTableColumn column : genTable.getColumns()) + { + if (column.isList()) + { + num++; + String columnName = column.getColumnName(); + if (columnName.equals(treeName)) + { + break; + } + } + } + return num; + } +} diff --git a/docs/gen/antdv/index.vue.vm b/docs/gen/antdv/index.vue.vm new file mode 100644 index 0000000..acafd19 --- /dev/null +++ b/docs/gen/antdv/index.vue.vm @@ -0,0 +1,461 @@ + + + diff --git a/docs/gen/antdv/js/api.js.vm b/docs/gen/antdv/js/api.js.vm new file mode 100644 index 0000000..0b28367 --- /dev/null +++ b/docs/gen/antdv/js/api.js.vm @@ -0,0 +1,53 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName} (query) { + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName} (${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName} (data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName} (data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName} (${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} + +// 导出${functionName} +export function export${BusinessName} (query) { + return request({ + url: '/${moduleName}/${businessName}/export', + method: 'get', + params: query + }) +} diff --git a/docs/gen/antdv/modules/CreateForm.vue.vm b/docs/gen/antdv/modules/CreateForm.vue.vm new file mode 100644 index 0000000..72ecbe1 --- /dev/null +++ b/docs/gen/antdv/modules/CreateForm.vue.vm @@ -0,0 +1,428 @@ +#set($isInsert=0) +#set($isEdit=0) +#set($hasRichText=0) +#foreach($column in $columns) +#if($column.insert) +#set($isInsert=1) +#end +#if($column.edit) +#set($isEdit=1) +#end +#if($isInsert == 1 && $isEdit == 1) +#break +#end +#end +#if($isInsert == 0 && $isEdit == 0) + + +#stop +#end + + + diff --git a/docs/gen/antdv/modules/CreateSubForm.vue.vm b/docs/gen/antdv/modules/CreateSubForm.vue.vm new file mode 100644 index 0000000..2af48a5 --- /dev/null +++ b/docs/gen/antdv/modules/CreateSubForm.vue.vm @@ -0,0 +1,106 @@ + + + diff --git a/docs/gen/antdv/modules/SubTable.vue.vm b/docs/gen/antdv/modules/SubTable.vue.vm new file mode 100644 index 0000000..1e34252 --- /dev/null +++ b/docs/gen/antdv/modules/SubTable.vue.vm @@ -0,0 +1,152 @@ + + + diff --git a/docs/webpack-bundle-analyzer.md b/docs/webpack-bundle-analyzer.md new file mode 100644 index 0000000..c313767 --- /dev/null +++ b/docs/webpack-bundle-analyzer.md @@ -0,0 +1,40 @@ +先增加依赖 + +```bash +// npm +$ npm install --save-dev webpack-bundle-analyzer + +// or yarn +$ yarn add webpack-bundle-analyzer -D +``` + +配置文件 `vue.config.js` 增加 `configureWebpack.plugins` 参数 + +``` +const path = require('path') +const webpack = require('webpack') +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin + +function resolve (dir) { + return path.join(__dirname, dir) +} + +// vue.config.js +module.exports = { + configureWebpack: { + plugins: [ + // Ignore all locale files of moment.js + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), + // 依赖大小分析工具 + new BundleAnalyzerPlugin(), + ] + }, + + + ... +} +``` + + + +启动 `cli` 的 `build` 命令进行项目编译,编译完成时,会自动运行一个 http://localhost:8888 的地址,完整显示了支持库依赖 \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..29fee32 --- /dev/null +++ b/jest.config.js @@ -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: { + '^@/(.*)$': '/src/$1' + }, + snapshotSerializers: [ + 'jest-serializer-vue' + ], + testMatch: [ + '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' + ], + testURL: 'http://localhost/' +} diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..1bd0da4 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es6", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "exclude": ["node_modules", "dist"], + "include": ["src/**/*"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..822352d --- /dev/null +++ b/package.json @@ -0,0 +1,66 @@ +{ + "name": "vue-antd-pro", + "version": "3.0.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "test:unit": "vue-cli-service test:unit", + "lint": "vue-cli-service lint", + "build:preview": "vue-cli-service build --mode preview", + "lint:nofix": "vue-cli-service lint --no-fix" + }, + "dependencies": { + "ant-design-vue": "^1.7.2", + "axios": "^0.19.0", + "core-js": "^3.1.2", + "echarts": "^5.0.0", + "enquire.js": "^2.1.6", + "highlight.js": "^10.5.0", + "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", + "screenfull": "^5.1.0", + "store": "^2.0.12", + "vditor": "^3.7.3", + "vue": "^2.6.12", + "vue-clipboard2": "^0.2.1", + "vue-container-query": "^0.1.0", + "vue-copy-to-clipboard": "^1.0.3", + "vue-cropper": "0.4.9", + "vue-i18n": "^8.17.4", + "vue-router": "^3.1.2", + "vue-svg-component-runtime": "^1.0.1", + "vuex": "^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-standard": "^4.0.0", + "@vue/test-utils": "^1.0.0-beta.29", + "babel-eslint": "^10.0.1", + "babel-plugin-import": "^1.12.2", + "babel-plugin-transform-remove-console": "^6.9.4", + "compression-webpack-plugin": "^5.0.1", + "eslint": "^5.16.0", + "eslint-plugin-html": "^5.0.0", + "eslint-plugin-vue": "^5.2.3", + "git-revision-webpack-plugin": "^3.0.6", + "less": "^3.0.4", + "less-loader": "^5.0.0", + "opencollective": "^1.0.3", + "opencollective-postinstall": "^2.0.2", + "vue-svg-icon-loader": "^2.1.1", + "vue-template-compiler": "^2.6.12", + "webpack-theme-color-replacer": "^1.3.12" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..961986e --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + autoprefixer: {} + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..834cf67 --- /dev/null +++ b/public/index.html @@ -0,0 +1,33 @@ + + + + + + + + 算力云后台管理系统 + + + <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %> + + <% } %> + + + +
+
+

算力云

+
+ +
+
+
+ + <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %> + + <% } %> + + + diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..d45249b Binary files /dev/null and b/public/logo.png differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..070f28a --- /dev/null +++ b/src/App.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/api/ad.js b/src/api/ad.js new file mode 100644 index 0000000..3842585 --- /dev/null +++ b/src/api/ad.js @@ -0,0 +1,39 @@ +import request from '@/utils/request' + +const api = { + create: '/swipe/create', + list: '/swipe/list', + delete: '/swipe/delete' +} +export default api + +export function createAd (parameter) { + console.log('createAd') + return request({ + url: api.create, + headers: { + 'Content-Type': 'application/json;charset=UTF-8' + }, + method: 'post', + data: parameter + }) +} + +export function getAdList (parameters) { + console.log('getAdList') + return request({ + url: api.list, + method: 'get', + params: parameters + }) +} + +export function deleteAd (parameter) { + console.log('deleteAd') + return request({ + url: api.delete, + method: 'post', + params: parameter + + }) +} diff --git a/src/api/coin.js b/src/api/coin.js new file mode 100644 index 0000000..8f43b8a --- /dev/null +++ b/src/api/coin.js @@ -0,0 +1,66 @@ +import request from '@/utils/request' + +const api = { + coinList: '/coin/list', + coinAddressList: '/address/list', + coinAddressDelete: '/address/delete', + coinPackageUnit: '/coin/package/unit/list', + coinPackageCreate: '/coin/package/create', + coinPackageList: '/coin/package/list', + coinPackageDelete: '/coin/package/delete' + +} +export default api + +export function getCoinList () { + console.log('getCoinList') + return request({ + url: api.coinList, + headers: { + 'Content-Type': 'application/json;charset=UTF-8' + }, + method: 'get' + }) +} + +export function getCoinAddressList () { + console.log('getCoinAddressList') + return request({ + url: api.coinAddressList, + method: 'get' + }) +} + +export function getCoinPackageUnit () { + console.log('loadData.getCoinPackageUnit') + return request({ + url: api.coinPackageUnit, + method: 'get' + }) +} + +export function onCoinPackageCreate (params) { + console.log('loadData.coinPackageCreate') + return request({ + url: api.coinPackageCreate, + method: 'post', + data: params + }) +} + +export function deleteCoinPackage (params) { + console.log('loadData.coinPackageDelete') + return request({ + url: api.coinPackageDelete, + method: 'post', + data: params + }) +} + +export function onCoinPackageList () { + console.log('loadData.onCoinPackageList') + return request({ + url: api.coinPackageList, + method: 'get' + }) +} diff --git a/src/api/common.js b/src/api/common.js new file mode 100644 index 0000000..95e45a9 --- /dev/null +++ b/src/api/common.js @@ -0,0 +1,16 @@ +import request from '@/utils/request' + +const api = { + upload: '/app/uploadImg' +} +export default api + +export function Upload () { + console.log('loadData.getCoinList.parameter') + return request({ + url: api.upload, + method: 'post' + }) +} + +export const uploadUrl = api.upload diff --git a/src/api/dashboard.js b/src/api/dashboard.js new file mode 100644 index 0000000..9a8ae86 --- /dev/null +++ b/src/api/dashboard.js @@ -0,0 +1,14 @@ +import request from '@/utils/request' + +const api = { + dashboard: '/console/dashboard' +} +export default api + +export function getDashboard() { + console.log('loadData.getDashboard') + return request({ + url: api.dashboard, + method: 'get' + }) +} diff --git a/src/api/finance/order.js b/src/api/finance/order.js new file mode 100644 index 0000000..63831e2 --- /dev/null +++ b/src/api/finance/order.js @@ -0,0 +1,35 @@ +import request from '@/utils/request' + +const api = { + list: '/order/list', + updateOrderPayPrice: '/order/updatePayPrice', + confirmPayment: '/order/confirmPayment' +} +export default api + +export function getOrderList(parameters) { + console.log('getAdList') + return request({ + url: api.list, + method: 'get', + params: parameters + }) +} + +export function onOrderConfirmPayment(parameters) { + console.log('confirmPayment') + return request({ + url: api.confirmPayment, + method: 'post', + data: parameters + }) +} + +export function onUpdateOrderPayPrice(parameters) { + console.log('updateOrderPayPrice') + return request({ + url: api.updateOrderPayPrice, + method: 'post', + data: parameters + }) +} diff --git a/src/api/finance/payment.js b/src/api/finance/payment.js new file mode 100644 index 0000000..5eca52c --- /dev/null +++ b/src/api/finance/payment.js @@ -0,0 +1,15 @@ +import request from '@/utils/request' + +const api = { + list: '/wallet/payment/list' +} +export default api + +export function getPaymentRecordList(parameters) { + console.log('getPaymentRecordList') + return request({ + url: api.list, + method: 'get', + params: parameters + }) +} diff --git a/src/api/login.js b/src/api/login.js new file mode 100644 index 0000000..75cb80a --- /dev/null +++ b/src/api/login.js @@ -0,0 +1,49 @@ +import request from '@/utils/request' + +const userApi = { + Login: '/login', + Logout: '/logout', + // get my info + UserInfo: '/getInfo' +} + +/** + * login func + * @param parameter + * @returns {*} + */ +export function login (parameter) { + return request({ + url: userApi.Login, + method: 'post', + data: parameter + }) +} + +export function getInfo () { + return request({ + url: userApi.UserInfo, + method: 'get', + headers: { + 'Content-Type': 'application/json;charset=UTF-8' + } + }) +} + +export function logout () { + return request({ + url: userApi.Logout, + method: 'delete', + headers: { + 'Content-Type': 'application/json;charset=UTF-8' + } + }) +} + +// 获取验证码 +export function getCodeImg () { + return request({ + url: '/captchaImage', + method: 'get' + }) +} diff --git a/src/api/manage.js b/src/api/manage.js new file mode 100644 index 0000000..46a4a41 --- /dev/null +++ b/src/api/manage.js @@ -0,0 +1,70 @@ +import request from '@/utils/request' + +const api = { + user: '/user', + role: '/role', + service: '/service', + permission: '/permission', + permissionNoPager: '/permission/no-pager', + orgTree: '/org/tree' +} + +export default api + +export function getUserList (parameter) { + return request({ + url: api.user, + method: 'get', + params: parameter + }) +} + +export function getRoleList (parameter) { + return request({ + url: api.role, + method: 'get', + params: parameter + }) +} + +export function getServiceList (parameter) { + return request({ + url: api.service, + method: 'get', + params: parameter + }) +} + +export function getPermissions (parameter) { + return request({ + url: api.permissionNoPager, + method: 'get', + params: parameter + }) +} + +export function getOrgTree (parameter) { + return request({ + url: api.orgTree, + method: 'get', + params: parameter + }) +} + +// id == 0 add post +// id != 0 update put +export function saveService (parameter) { + return request({ + url: api.service, + method: parameter.id === 0 ? 'post' : 'put', + data: parameter + }) +} + +export function saveSub (sub) { + return request({ + url: '/sub', + method: sub.id === 0 ? 'post' : 'put', + data: sub + }) +} diff --git a/src/api/member.js b/src/api/member.js new file mode 100644 index 0000000..cbfe3ea --- /dev/null +++ b/src/api/member.js @@ -0,0 +1,58 @@ +import request from '@/utils/request' + +const api = { + userList: '/member/list', + register: '/member/register', + updateStatus: '/member/updateStatus', + memberDelete: '/member/delete', + orgTree: '/org/tree' +} +export default api + +export function getUserList (parameters) { + console.log('loadData.getUserList.parameter', parameters) + return request({ + url: api.userList, + method: 'get', + params: parameters + }) +} + +export function createUser (parameters) { + console.log('createUser', parameters) + return request({ + url: api.register, + headers: { + 'Content-Type': 'application/json;charset=UTF-8' + }, + method: 'post', + data: parameters, + params: parameters + }) +} + +export function updateUser (parameters) { + console.log('createUser', parameters) + return request({ + url: api.updateStatus, + headers: { + 'Content-Type': 'application/json;charset=UTF-8' + }, + method: 'post', + data: parameters, + params: parameters + }) +} + +export function onMemberDelete (parameters) { + console.log('createUser', parameters) + return request({ + url: api.memberDelete, + headers: { + 'Content-Type': 'application/json;charset=UTF-8' + }, + method: 'post', + data: parameters, + params: parameters + }) +} diff --git a/src/api/menu.js b/src/api/menu.js new file mode 100644 index 0000000..95aaa17 --- /dev/null +++ b/src/api/menu.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 获取路由 +export const getRouters = () => { + return request({ + url: '/getRouters', + method: 'get' + }) +} diff --git a/src/api/monitor/cache.js b/src/api/monitor/cache.js new file mode 100644 index 0000000..4609104 --- /dev/null +++ b/src/api/monitor/cache.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 查询缓存详细 +export function getCache () { + return request({ + url: '/monitor/cache', + method: 'get' + }) +} diff --git a/src/api/monitor/logininfor.js b/src/api/monitor/logininfor.js new file mode 100644 index 0000000..c5e3b4f --- /dev/null +++ b/src/api/monitor/logininfor.js @@ -0,0 +1,35 @@ +import request from '@/utils/request' + +// 查询登录日志列表 +export function list (query) { + return request({ + url: '/monitor/logininfor/list', + method: 'get', + params: query + }) +} + +// 删除登录日志 +export function delLogininfor (infoId) { + return request({ + url: '/monitor/logininfor/' + infoId, + method: 'delete' + }) +} + +// 清空登录日志 +export function cleanLogininfor () { + return request({ + url: '/monitor/logininfor/clean', + method: 'delete' + }) +} + +// 导出登录日志 +export function exportLogininfor (query) { + return request({ + url: '/monitor/logininfor/export', + method: 'get', + params: query + }) +} diff --git a/src/api/monitor/online.js b/src/api/monitor/online.js new file mode 100644 index 0000000..76da959 --- /dev/null +++ b/src/api/monitor/online.js @@ -0,0 +1,18 @@ +import request from '@/utils/request' + +// 查询在线用户列表 +export function list (query) { + return request({ + url: '/monitor/online/list', + method: 'get', + params: query + }) +} + +// 强退用户 +export function forceLogout (tokenId) { + return request({ + url: '/monitor/online/' + tokenId, + method: 'delete' + }) +} diff --git a/src/api/monitor/operlog.js b/src/api/monitor/operlog.js new file mode 100644 index 0000000..a80d5cc --- /dev/null +++ b/src/api/monitor/operlog.js @@ -0,0 +1,35 @@ +import request from '@/utils/request' + +// 查询操作日志列表 +export function list (query) { + return request({ + url: '/monitor/operlog/list', + method: 'get', + params: query + }) +} + +// 删除操作日志 +export function delOperlog (operId) { + return request({ + url: '/monitor/operlog/' + operId, + method: 'delete' + }) +} + +// 清空操作日志 +export function cleanOperlog () { + return request({ + url: '/monitor/operlog/clean', + method: 'delete' + }) +} + +// 导出操作日志 +export function exportOperlog (query) { + return request({ + url: '/monitor/operlog/export', + method: 'get', + params: query + }) +} diff --git a/src/api/monitor/server.js b/src/api/monitor/server.js new file mode 100644 index 0000000..f30882a --- /dev/null +++ b/src/api/monitor/server.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 查询服务器详细 +export function getServer () { + return request({ + url: '/monitor/server', + method: 'get' + }) +} diff --git a/src/api/system/config.js b/src/api/system/config.js new file mode 100644 index 0000000..9582ef5 --- /dev/null +++ b/src/api/system/config.js @@ -0,0 +1,69 @@ +import request from '@/utils/request' + +// 查询参数列表 +export function listConfig (query) { + return request({ + url: '/system/config/list', + method: 'get', + params: query + }) +} + +// 查询参数详细 +export function getConfig (configId) { + return request({ + url: '/system/config/' + configId, + method: 'get' + }) +} + +// 根据参数键名查询参数值 +export function getConfigKey (configKey) { + return request({ + url: '/system/config/configKey/' + configKey, + method: 'get' + }) +} + +// 新增参数配置 +export function addConfig (data) { + return request({ + url: '/system/config', + method: 'post', + data: data + }) +} + +// 修改参数配置 +export function updateConfig (data) { + return request({ + url: '/system/config', + method: 'put', + data: data + }) +} + +// 删除参数配置 +export function delConfig (configId) { + return request({ + url: '/system/config/' + configId, + method: 'delete' + }) +} + +// 清理参数缓存 +export function clearCache () { + return request({ + url: '/system/config/clearCache', + method: 'delete' + }) +} + +// 导出参数 +export function exportConfig (query) { + return request({ + url: '/system/config/export', + method: 'get', + params: query + }) +} diff --git a/src/api/system/dept.js b/src/api/system/dept.js new file mode 100644 index 0000000..0dac690 --- /dev/null +++ b/src/api/system/dept.js @@ -0,0 +1,68 @@ +import request from '@/utils/request' + +// 查询部门列表 +export function listDept (query) { + return request({ + url: '/system/dept/list', + method: 'get', + params: query + }) +} + +// 查询部门列表(排除节点) +export function listDeptExcludeChild (deptId) { + return request({ + url: '/system/dept/list/exclude/' + deptId, + method: 'get' + }) +} + +// 查询部门详细 +export function getDept (deptId) { + return request({ + url: '/system/dept/' + deptId, + method: 'get' + }) +} + +// 查询部门下拉树结构 +export function treeselect () { + return request({ + url: '/system/dept/treeselect', + method: 'get' + }) +} + +// 根据角色ID查询部门树结构 +export function roleDeptTreeselect (roleId) { + return request({ + url: '/system/dept/roleDeptTreeselect/' + roleId, + method: 'get' + }) +} + +// 新增部门 +export function addDept (data) { + return request({ + url: '/system/dept', + method: 'post', + data: data + }) +} + +// 修改部门 +export function updateDept (data) { + return request({ + url: '/system/dept', + method: 'put', + data: data + }) +} + +// 删除部门 +export function delDept (deptId) { + return request({ + url: '/system/dept/' + deptId, + method: 'delete' + }) +} diff --git a/src/api/system/dict/data.js b/src/api/system/dict/data.js new file mode 100644 index 0000000..5bc65fe --- /dev/null +++ b/src/api/system/dict/data.js @@ -0,0 +1,61 @@ +import request from '@/utils/request' + +// 查询字典数据列表 +export function listData (query) { + return request({ + url: '/system/dict/data/list', + method: 'get', + params: query + }) +} + +// 查询字典数据详细 +export function getData (dictCode) { + return request({ + url: '/system/dict/data/' + dictCode, + method: 'get' + }) +} + +// 根据字典类型查询字典数据信息 +export function getDicts (dictType) { + return request({ + url: '/system/dict/data/type/' + dictType, + method: 'get' + }) +} + +// 新增字典数据 +export function addData (data) { + return request({ + url: '/system/dict/data', + method: 'post', + data: data + }) +} + +// 修改字典数据 +export function updateData (data) { + return request({ + url: '/system/dict/data', + method: 'put', + data: data + }) +} + +// 删除字典数据 +export function delData (dictCode) { + return request({ + url: '/system/dict/data/' + dictCode, + method: 'delete' + }) +} + +// 导出字典数据 +export function exportData (query) { + return request({ + url: '/system/dict/data/export', + method: 'get', + params: query + }) +} diff --git a/src/api/system/dict/type.js b/src/api/system/dict/type.js new file mode 100644 index 0000000..ec4d8a0 --- /dev/null +++ b/src/api/system/dict/type.js @@ -0,0 +1,69 @@ +import request from '@/utils/request' + +// 查询字典类型列表 +export function listType (query) { + return request({ + url: '/system/dict/type/list', + method: 'get', + params: query + }) +} + +// 查询字典类型详细 +export function getType (dictId) { + return request({ + url: '/system/dict/type/' + dictId, + method: 'get' + }) +} + +// 新增字典类型 +export function addType (data) { + return request({ + url: '/system/dict/type', + method: 'post', + data: data + }) +} + +// 修改字典类型 +export function updateType (data) { + return request({ + url: '/system/dict/type', + method: 'put', + data: data + }) +} + +// 删除字典类型 +export function delType (dictId) { + return request({ + url: '/system/dict/type/' + dictId, + method: 'delete' + }) +} + +// 清理参数缓存 +export function clearCache () { + return request({ + url: '/system/dict/type/clearCache', + method: 'delete' + }) +} + +// 导出字典类型 +export function exportType (query) { + return request({ + url: '/system/dict/type/export', + method: 'get', + params: query + }) +} + +// 获取字典选择框列表 +export function optionselect () { + return request({ + url: '/system/dict/type/optionselect', + method: 'get' + }) +} diff --git a/src/api/system/menu.js b/src/api/system/menu.js new file mode 100644 index 0000000..6f82014 --- /dev/null +++ b/src/api/system/menu.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询菜单列表 +export function listMenu (query) { + return request({ + url: '/system/menu/list', + method: 'get', + params: query + }) +} + +// 查询菜单详细 +export function getMenu (menuId) { + return request({ + url: '/system/menu/' + menuId, + method: 'get' + }) +} + +// 查询菜单下拉树结构 +export function treeselect () { + return request({ + url: '/system/menu/treeselect', + method: 'get' + }) +} + +// 根据角色ID查询菜单下拉树结构 +export function roleMenuTreeselect (roleId) { + return request({ + url: '/system/menu/roleMenuTreeselect/' + roleId, + method: 'get' + }) +} + +// 新增菜单 +export function addMenu (data) { + return request({ + url: '/system/menu', + method: 'post', + data: data + }) +} + +// 修改菜单 +export function updateMenu (data) { + return request({ + url: '/system/menu', + method: 'put', + data: data + }) +} + +// 删除菜单 +export function delMenu (menuId) { + return request({ + url: '/system/menu/' + menuId, + method: 'delete' + }) +} diff --git a/src/api/system/notice.js b/src/api/system/notice.js new file mode 100644 index 0000000..99b3d7b --- /dev/null +++ b/src/api/system/notice.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询公告列表 +export function listNotice (query) { + return request({ + url: '/system/notice/list', + method: 'get', + params: query + }) +} + +// 查询公告详细 +export function getNotice (noticeId) { + return request({ + url: '/system/notice/' + noticeId, + method: 'get' + }) +} + +// 新增公告 +export function addNotice (data) { + return request({ + url: '/system/notice', + method: 'post', + data: data + }) +} + +// 修改公告 +export function updateNotice (data) { + return request({ + url: '/system/notice', + method: 'put', + data: data + }) +} + +// 删除公告 +export function delNotice (noticeId) { + return request({ + url: '/system/notice/' + noticeId, + method: 'delete' + }) +} diff --git a/src/api/system/post.js b/src/api/system/post.js new file mode 100644 index 0000000..6559e59 --- /dev/null +++ b/src/api/system/post.js @@ -0,0 +1,53 @@ +import request from '@/utils/request' + +// 查询岗位列表 +export function listPost (query) { + return request({ + url: '/system/post/list', + method: 'get', + params: query + }) +} + +// 查询岗位详细 +export function getPost (postId) { + return request({ + url: '/system/post/' + postId, + method: 'get' + }) +} + +// 新增岗位 +export function addPost (data) { + return request({ + url: '/system/post', + method: 'post', + data: data + }) +} + +// 修改岗位 +export function updatePost (data) { + return request({ + url: '/system/post', + method: 'put', + data: data + }) +} + +// 删除岗位 +export function delPost (postId) { + return request({ + url: '/system/post/' + postId, + method: 'delete' + }) +} + +// 导出岗位 +export function exportPost (query) { + return request({ + url: '/system/post/export', + method: 'get', + params: query + }) +} diff --git a/src/api/system/role.js b/src/api/system/role.js new file mode 100644 index 0000000..30da94d --- /dev/null +++ b/src/api/system/role.js @@ -0,0 +1,75 @@ +import request from '@/utils/request' + +// 查询角色列表 +export function listRole (query) { + return request({ + url: '/system/role/list', + method: 'get', + params: query + }) +} + +// 查询角色详细 +export function getRole (roleId) { + return request({ + url: '/system/role/' + roleId, + method: 'get' + }) +} + +// 新增角色 +export function addRole (data) { + return request({ + url: '/system/role', + method: 'post', + data: data + }) +} + +// 修改角色 +export function updateRole (data) { + return request({ + url: '/system/role', + method: 'put', + data: data + }) +} + +// 角色数据权限 +export function dataScope (data) { + return request({ + url: '/system/role/dataScope', + method: 'put', + data: data + }) +} + +// 角色状态修改 +export function changeRoleStatus (roleId, status) { + const data = { + roleId, + status + } + return request({ + url: '/system/role/changeStatus', + method: 'put', + data: data + }) +} + +// 删除角色 +export function delRole (roleId) { + return request({ + url: '/system/role/' + roleId, + method: 'delete' + }) +} + +// 导出角色 +export function exportRole (query) { + return request({ + url: '/system/role/export', + method: 'get', + params: query + }) +} diff --git a/src/api/system/user.js b/src/api/system/user.js new file mode 100644 index 0000000..ab635f7 --- /dev/null +++ b/src/api/system/user.js @@ -0,0 +1,127 @@ +import request from '@/utils/request' +import { praseStrEmpty } from '@/utils/ruoyi' + +// 查询用户列表 +export function listUser (query) { + return request({ + url: '/system/user/list', + method: 'get', + params: query + }) +} + +// 查询用户详细 +export function getUser (userId) { + return request({ + url: '/system/user/' + praseStrEmpty(userId), + method: 'get' + }) +} + +// 新增用户 +export function addUser (data) { + return request({ + url: '/system/user', + method: 'post', + data: data + }) +} + +// 修改用户 +export function updateUser (data) { + return request({ + url: '/system/user', + method: 'put', + data: data + }) +} + +// 删除用户 +export function delUser (userId) { + return request({ + url: '/system/user/' + userId, + method: 'delete' + }) +} + +// 导出用户 +export function exportUser (query) { + return request({ + url: '/system/user/export', + method: 'get', + params: query + }) +} + +// 用户密码重置 +export function resetUserPwd (userId, password) { + const data = { + userId, + password + } + return request({ + url: '/system/user/resetPwd', + method: 'put', + data: data + }) +} + +// 用户状态修改 +export function changeUserStatus (userId, status) { + const data = { + userId, + status + } + return request({ + url: '/system/user/changeStatus', + method: 'put', + data: data + }) +} + +// 查询用户个人信息 +export function getUserProfile () { + return request({ + url: '/system/user/profile', + method: 'get' + }) +} + +// 修改用户个人信息 +export function updateUserProfile (data) { + return request({ + url: '/system/user/profile', + method: 'put', + data: data + }) +} + +// 用户密码重置 +export function updateUserPwd (oldPassword, newPassword) { + const data = { + oldPassword, + newPassword + } + return request({ + url: '/system/user/profile/updatePwd', + method: 'put', + params: data + }) +} + +// 用户头像上传 +export function uploadAvatar (data) { + return request({ + url: '/system/user/profile/avatar', + method: 'post', + data: data + }) +} + +// 下载用户导入模板 +export function importTemplate () { + return request({ + url: '/system/user/importTemplate', + method: 'get' + }) +} diff --git a/src/api/tool/gen.js b/src/api/tool/gen.js new file mode 100644 index 0000000..5024f0f --- /dev/null +++ b/src/api/tool/gen.js @@ -0,0 +1,76 @@ +import request from '@/utils/request' + +// 查询生成表数据 +export function listTable (query) { + return request({ + url: '/tool/gen/list', + method: 'get', + params: query + }) +} +// 查询db数据库列表 +export function listDbTable (query) { + return request({ + url: '/tool/gen/db/list', + method: 'get', + params: query + }) +} + +// 查询表详细信息 +export function getGenTable (tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'get' + }) +} + +// 修改代码生成信息 +export function updateGenTable (data) { + return request({ + url: '/tool/gen', + method: 'put', + data: data + }) +} + +// 导入表 +export function importTable (data) { + return request({ + url: '/tool/gen/importTable', + method: 'post', + params: data + }) +} + +// 预览生成代码 +export function previewTable (tableId) { + return request({ + url: '/tool/gen/preview/' + tableId, + method: 'get' + }) +} + +// 删除表数据 +export function delTable (tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'delete' + }) +} + +// 生成代码(自定义路径) +export function genCode (tableName) { + return request({ + url: '/tool/gen/genCode/' + tableName, + method: 'get' + }) +} + +// 同步数据库 +export function synchDb (tableName) { + return request({ + url: '/tool/gen/synchDb/' + tableName, + method: 'get' + }) +} diff --git a/src/assets/background.svg b/src/assets/background.svg new file mode 100644 index 0000000..89c2597 --- /dev/null +++ b/src/assets/background.svg @@ -0,0 +1,69 @@ + + + + Group 21 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/bug.svg b/src/assets/icons/bug.svg new file mode 100644 index 0000000..b7fbf75 --- /dev/null +++ b/src/assets/icons/bug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/build.svg b/src/assets/icons/build.svg new file mode 100644 index 0000000..97c4688 --- /dev/null +++ b/src/assets/icons/build.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/button.svg b/src/assets/icons/button.svg new file mode 100644 index 0000000..ef85344 --- /dev/null +++ b/src/assets/icons/button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/bxAnalyse.svg b/src/assets/icons/bxAnalyse.svg new file mode 100644 index 0000000..b02a8d6 --- /dev/null +++ b/src/assets/icons/bxAnalyse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/cascader.svg b/src/assets/icons/cascader.svg new file mode 100644 index 0000000..e256024 --- /dev/null +++ b/src/assets/icons/cascader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/chart.svg b/src/assets/icons/chart.svg new file mode 100644 index 0000000..750d377 --- /dev/null +++ b/src/assets/icons/chart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/checkbox.svg b/src/assets/icons/checkbox.svg new file mode 100644 index 0000000..013fd3a --- /dev/null +++ b/src/assets/icons/checkbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/clipboard.svg b/src/assets/icons/clipboard.svg new file mode 100644 index 0000000..2f290c6 --- /dev/null +++ b/src/assets/icons/clipboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/code.svg b/src/assets/icons/code.svg new file mode 100644 index 0000000..5f9c5ab --- /dev/null +++ b/src/assets/icons/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/color.svg b/src/assets/icons/color.svg new file mode 100644 index 0000000..44a81aa --- /dev/null +++ b/src/assets/icons/color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/component.svg b/src/assets/icons/component.svg new file mode 100644 index 0000000..29c3458 --- /dev/null +++ b/src/assets/icons/component.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/dashboard.svg b/src/assets/icons/dashboard.svg new file mode 100644 index 0000000..79065e6 --- /dev/null +++ b/src/assets/icons/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/date.svg b/src/assets/icons/date.svg new file mode 100644 index 0000000..52dc73e --- /dev/null +++ b/src/assets/icons/date.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/dateRange.svg b/src/assets/icons/dateRange.svg new file mode 100644 index 0000000..fda571e --- /dev/null +++ b/src/assets/icons/dateRange.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/dict.svg b/src/assets/icons/dict.svg new file mode 100644 index 0000000..4849377 --- /dev/null +++ b/src/assets/icons/dict.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/documentation.svg b/src/assets/icons/documentation.svg new file mode 100644 index 0000000..ecf1bfe --- /dev/null +++ b/src/assets/icons/documentation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/download.svg b/src/assets/icons/download.svg new file mode 100644 index 0000000..c896951 --- /dev/null +++ b/src/assets/icons/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/drag.svg b/src/assets/icons/drag.svg new file mode 100644 index 0000000..0a5460b --- /dev/null +++ b/src/assets/icons/drag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/druid.svg b/src/assets/icons/druid.svg new file mode 100644 index 0000000..a2b4b4e --- /dev/null +++ b/src/assets/icons/druid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/edit.svg b/src/assets/icons/edit.svg new file mode 100644 index 0000000..e3cc776 --- /dev/null +++ b/src/assets/icons/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/eyeOpen.svg b/src/assets/icons/eyeOpen.svg new file mode 100644 index 0000000..88dcc98 --- /dev/null +++ b/src/assets/icons/eyeOpen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/github.svg b/src/assets/icons/github.svg new file mode 100644 index 0000000..db0a0d4 --- /dev/null +++ b/src/assets/icons/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/guide.svg b/src/assets/icons/guide.svg new file mode 100644 index 0000000..60195f7 --- /dev/null +++ b/src/assets/icons/guide.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/input.svg b/src/assets/icons/input.svg new file mode 100644 index 0000000..ab91381 --- /dev/null +++ b/src/assets/icons/input.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/job.svg b/src/assets/icons/job.svg new file mode 100644 index 0000000..2a93a25 --- /dev/null +++ b/src/assets/icons/job.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/log.svg b/src/assets/icons/log.svg new file mode 100644 index 0000000..d879d33 --- /dev/null +++ b/src/assets/icons/log.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/logininfor.svg b/src/assets/icons/logininfor.svg new file mode 100644 index 0000000..267f844 --- /dev/null +++ b/src/assets/icons/logininfor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/message.svg b/src/assets/icons/message.svg new file mode 100644 index 0000000..74986f3 --- /dev/null +++ b/src/assets/icons/message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/monitor.svg b/src/assets/icons/monitor.svg new file mode 100644 index 0000000..bc308cb --- /dev/null +++ b/src/assets/icons/monitor.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/src/assets/icons/number.svg b/src/assets/icons/number.svg new file mode 100644 index 0000000..ad5ce9a --- /dev/null +++ b/src/assets/icons/number.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/online.svg b/src/assets/icons/online.svg new file mode 100644 index 0000000..330a202 --- /dev/null +++ b/src/assets/icons/online.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/password.svg b/src/assets/icons/password.svg new file mode 100644 index 0000000..6c64def --- /dev/null +++ b/src/assets/icons/password.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/pdf.svg b/src/assets/icons/pdf.svg new file mode 100644 index 0000000..957aa0c --- /dev/null +++ b/src/assets/icons/pdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/peoples.svg b/src/assets/icons/peoples.svg new file mode 100644 index 0000000..07bb38b --- /dev/null +++ b/src/assets/icons/peoples.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/phone.svg b/src/assets/icons/phone.svg new file mode 100644 index 0000000..ab8e8c4 --- /dev/null +++ b/src/assets/icons/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/post.svg b/src/assets/icons/post.svg new file mode 100644 index 0000000..2922c61 --- /dev/null +++ b/src/assets/icons/post.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/question.svg b/src/assets/icons/question.svg new file mode 100644 index 0000000..cf75bd4 --- /dev/null +++ b/src/assets/icons/question.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/radio.svg b/src/assets/icons/radio.svg new file mode 100644 index 0000000..0cde345 --- /dev/null +++ b/src/assets/icons/radio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/rate.svg b/src/assets/icons/rate.svg new file mode 100644 index 0000000..aa3b14d --- /dev/null +++ b/src/assets/icons/rate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/redis.svg b/src/assets/icons/redis.svg new file mode 100644 index 0000000..2f1d62d --- /dev/null +++ b/src/assets/icons/redis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/row.svg b/src/assets/icons/row.svg new file mode 100644 index 0000000..0780992 --- /dev/null +++ b/src/assets/icons/row.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/select.svg b/src/assets/icons/select.svg new file mode 100644 index 0000000..d628382 --- /dev/null +++ b/src/assets/icons/select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/server.svg b/src/assets/icons/server.svg new file mode 100644 index 0000000..eb287e3 --- /dev/null +++ b/src/assets/icons/server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/swagger.svg b/src/assets/icons/swagger.svg new file mode 100644 index 0000000..05d4e7b --- /dev/null +++ b/src/assets/icons/swagger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/switch.svg b/src/assets/icons/switch.svg new file mode 100644 index 0000000..0ba61e3 --- /dev/null +++ b/src/assets/icons/switch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/system.svg b/src/assets/icons/system.svg new file mode 100644 index 0000000..5992593 --- /dev/null +++ b/src/assets/icons/system.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/src/assets/icons/textarea.svg b/src/assets/icons/textarea.svg new file mode 100644 index 0000000..2709f29 --- /dev/null +++ b/src/assets/icons/textarea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/time.svg b/src/assets/icons/time.svg new file mode 100644 index 0000000..b376e32 --- /dev/null +++ b/src/assets/icons/time.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/timeRange.svg b/src/assets/icons/timeRange.svg new file mode 100644 index 0000000..13c1202 --- /dev/null +++ b/src/assets/icons/timeRange.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/tool.svg b/src/assets/icons/tool.svg new file mode 100644 index 0000000..48e0e35 --- /dev/null +++ b/src/assets/icons/tool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/tree.svg b/src/assets/icons/tree.svg new file mode 100644 index 0000000..5ec9429 --- /dev/null +++ b/src/assets/icons/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/treeTable.svg b/src/assets/icons/treeTable.svg new file mode 100644 index 0000000..189533c --- /dev/null +++ b/src/assets/icons/treeTable.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/upload.svg b/src/assets/icons/upload.svg new file mode 100644 index 0000000..bae49c0 --- /dev/null +++ b/src/assets/icons/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/user.svg b/src/assets/icons/user.svg new file mode 100644 index 0000000..573056c --- /dev/null +++ b/src/assets/icons/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/validCode.svg b/src/assets/icons/validCode.svg new file mode 100644 index 0000000..cfb1021 --- /dev/null +++ b/src/assets/icons/validCode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/dark.svg b/src/assets/images/dark.svg new file mode 100644 index 0000000..f646bd7 --- /dev/null +++ b/src/assets/images/dark.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/light.svg b/src/assets/images/light.svg new file mode 100644 index 0000000..ab7cc08 --- /dev/null +++ b/src/assets/images/light.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/login-background.jpg b/src/assets/images/login-background.jpg new file mode 100644 index 0000000..8a89eb8 Binary files /dev/null and b/src/assets/images/login-background.jpg differ diff --git a/src/assets/images/profile.jpg b/src/assets/images/profile.jpg new file mode 100644 index 0000000..b3a940b Binary files /dev/null and b/src/assets/images/profile.jpg differ diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..d45249b Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/assets/projects/antdv.png b/src/assets/projects/antdv.png new file mode 100644 index 0000000..4ca0b60 Binary files /dev/null and b/src/assets/projects/antdv.png differ diff --git a/src/assets/projects/ruoyi.png b/src/assets/projects/ruoyi.png new file mode 100644 index 0000000..755c0d8 Binary files /dev/null and b/src/assets/projects/ruoyi.png differ diff --git a/src/components/ArticleListContent/ArticleListContent.vue b/src/components/ArticleListContent/ArticleListContent.vue new file mode 100644 index 0000000..8f39978 --- /dev/null +++ b/src/components/ArticleListContent/ArticleListContent.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/src/components/ArticleListContent/index.js b/src/components/ArticleListContent/index.js new file mode 100644 index 0000000..37d35c7 --- /dev/null +++ b/src/components/ArticleListContent/index.js @@ -0,0 +1,3 @@ +import ArticleListContent from './ArticleListContent' + +export default ArticleListContent diff --git a/src/components/AvatarList/Item.jsx b/src/components/AvatarList/Item.jsx new file mode 100644 index 0000000..c5a69b2 --- /dev/null +++ b/src/components/AvatarList/Item.jsx @@ -0,0 +1,24 @@ +import PropTypes from 'ant-design-vue/es/_util/vue-types' +import { Tooltip, Avatar } from 'ant-design-vue' +import { getSlotOptions } from 'ant-design-vue/lib/_util/props-util' +import { warning } from 'ant-design-vue/lib/vc-util/warning' + +export const AvatarListItemProps = { + tips: PropTypes.string.def(null), + src: PropTypes.string.def('') +} + +const Item = { + __ANT_AVATAR_CHILDREN: true, + name: 'AvatarListItem', + props: AvatarListItemProps, + created () { + warning(getSlotOptions(this.$parent).__ANT_AVATAR_LIST, 'AvatarListItem must be a subcomponent of AvatarList') + }, + render () { + const AvatarDom = + return this.tips && {AvatarDom} || + } +} + +export default Item diff --git a/src/components/AvatarList/List.jsx b/src/components/AvatarList/List.jsx new file mode 100644 index 0000000..74ad5ff --- /dev/null +++ b/src/components/AvatarList/List.jsx @@ -0,0 +1,72 @@ +import './index.less' + +import PropTypes from 'ant-design-vue/es/_util/vue-types' +import Avatar from 'ant-design-vue/es/avatar' +import Item from './Item.jsx' +import { filterEmpty } from '@/components/_util/util' + +/** + * size: `number`、 `large`、`small`、`default` 默认值: default + * maxLength: number + * excessItemsStyle: CSSProperties + */ +const AvatarListProps = { + prefixCls: PropTypes.string.def('ant-pro-avatar-list'), + size: { + validator: val => { + return typeof val === 'number' || ['small', 'large', 'default'].includes(val) + }, + default: 'default' + }, + maxLength: PropTypes.number.def(0), + excessItemsStyle: PropTypes.object.def({ + color: '#f56a00', + backgroundColor: '#fde3cf' + }) +} + +const AvatarList = { + __ANT_AVATAR_LIST: true, + Item, + name: 'AvatarList', + props: AvatarListProps, + render (h) { + const { prefixCls, size } = this.$props + const className = { + [`${prefixCls}`]: true, + [`${size}`]: true + } + const items = filterEmpty(this.$slots.default) + const itemsDom = items && items.length ?
    {this.getItems(items)}
: null + + return ( +
+ {itemsDom} +
+ ) + }, + methods: { + getItems (items) { + const className = { + [`${this.prefixCls}-item`]: true, + [`${this.size}`]: true + } + const totalSize = items.length + + if (this.maxLength > 0) { + items = items.slice(0, this.maxLength) + items.push(({`+${totalSize - this.maxLength}`})) + } + return items.map((item) => ( +
  • {item}
  • + )) + } + } +} + +AvatarList.install = function (Vue) { + Vue.component(AvatarList.name, AvatarList) + Vue.component(AvatarList.Item.name, AvatarList.Item) +} + +export default AvatarList diff --git a/src/components/AvatarList/index.js b/src/components/AvatarList/index.js new file mode 100644 index 0000000..b047432 --- /dev/null +++ b/src/components/AvatarList/index.js @@ -0,0 +1,9 @@ +import AvatarList from './List' +import Item from './Item' + +export { + AvatarList, + Item as AvatarListItem +} + +export default AvatarList diff --git a/src/components/AvatarList/index.less b/src/components/AvatarList/index.less new file mode 100644 index 0000000..9ce073f --- /dev/null +++ b/src/components/AvatarList/index.less @@ -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; + } + } + } + } +} + diff --git a/src/components/AvatarList/index.md b/src/components/AvatarList/index.md new file mode 100644 index 0000000..75e022c --- /dev/null +++ b/src/components/AvatarList/index.md @@ -0,0 +1,64 @@ +# AvatarList 用户头像列表 + + +一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。 + + + +引用方式: + +```javascript +import AvatarList from '@/components/AvatarList' +const AvatarListItem = AvatarList.Item + +export default { + components: { + AvatarList, + AvatarListItem + } +} +``` + + + +## 代码演示 [demo](https://pro.loacg.com/test/home) + +```html + + + + + +``` +或 +```html + + + + + + + + + +``` + + + +## API + +### AvatarList + +| 参数 | 说明 | 类型 | 默认值 | +| ---------------- | -------- | ---------------------------------- | --------- | +| size | 头像大小 | `large`、`small` 、`mini`, `default` | `default` | +| maxLength | 要显示的最大项目 | number | - | +| excessItemsStyle | 多余的项目风格 | CSSProperties | - | + +### AvatarList.Item + +| 参数 | 说明 | 类型 | 默认值 | +| ---- | ------ | --------- | --- | +| tips | 头像展示文案 | string | - | +| src | 头像图片连接 | string | - | + diff --git a/src/components/Dialog.js b/src/components/Dialog.js new file mode 100644 index 0000000..78e95b2 --- /dev/null +++ b/src/components/Dialog.js @@ -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) + } + } + }) +} diff --git a/src/components/Ellipsis/Ellipsis.vue b/src/components/Ellipsis/Ellipsis.vue new file mode 100644 index 0000000..5d59200 --- /dev/null +++ b/src/components/Ellipsis/Ellipsis.vue @@ -0,0 +1,64 @@ + diff --git a/src/components/Ellipsis/index.js b/src/components/Ellipsis/index.js new file mode 100644 index 0000000..91e3ff4 --- /dev/null +++ b/src/components/Ellipsis/index.js @@ -0,0 +1,3 @@ +import Ellipsis from './Ellipsis' + +export default Ellipsis diff --git a/src/components/Ellipsis/index.md b/src/components/Ellipsis/index.md new file mode 100644 index 0000000..f528ac7 --- /dev/null +++ b/src/components/Ellipsis/index.md @@ -0,0 +1,38 @@ +# Ellipsis 文本自动省略号 + +文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。 + + + +引用方式: + +```javascript +import Ellipsis from '@/components/Ellipsis' + +export default { + components: { + Ellipsis + } +} +``` + + + +## 代码演示 [demo](https://pro.loacg.com/test/home) + +```html + + 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. + +``` + + + +## API + + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +tooltip | 移动到文本展示完整内容的提示 | boolean | - +length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - \ No newline at end of file diff --git a/src/components/FileUpload/index.vue b/src/components/FileUpload/index.vue new file mode 100644 index 0000000..4229a64 --- /dev/null +++ b/src/components/FileUpload/index.vue @@ -0,0 +1,101 @@ + + + + diff --git a/src/components/FooterToolbar/FooterToolBar.vue b/src/components/FooterToolbar/FooterToolBar.vue new file mode 100644 index 0000000..ea07123 --- /dev/null +++ b/src/components/FooterToolbar/FooterToolBar.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/src/components/FooterToolbar/index.js b/src/components/FooterToolbar/index.js new file mode 100644 index 0000000..a0bf145 --- /dev/null +++ b/src/components/FooterToolbar/index.js @@ -0,0 +1,4 @@ +import FooterToolBar from './FooterToolBar' +import './index.less' + +export default FooterToolBar diff --git a/src/components/FooterToolbar/index.less b/src/components/FooterToolbar/index.less new file mode 100644 index 0000000..f56273f --- /dev/null +++ b/src/components/FooterToolbar/index.less @@ -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; + } +} \ No newline at end of file diff --git a/src/components/FooterToolbar/index.md b/src/components/FooterToolbar/index.md new file mode 100644 index 0000000..c1aec2c --- /dev/null +++ b/src/components/FooterToolbar/index.md @@ -0,0 +1,48 @@ +# FooterToolbar 底部工具栏 + +固定在底部的工具栏。 + + + +## 何时使用 + +固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。 + + + +引用方式: + +```javascript +import FooterToolBar from '@/components/FooterToolbar' + +export default { + components: { + FooterToolBar + } +} +``` + + + +## 代码演示 + +```html + + 提交 + +``` +或 +```html + + 提交 + +``` + + +## API + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +children (slot) | 工具栏内容,向右对齐 | - | - +extra | 额外信息,向左对齐 | String, Object | - + diff --git a/src/components/GlobalFooter/index.vue b/src/components/GlobalFooter/index.vue new file mode 100644 index 0000000..701fa8f --- /dev/null +++ b/src/components/GlobalFooter/index.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/components/GlobalHeader/AvatarDropdown.vue b/src/components/GlobalHeader/AvatarDropdown.vue new file mode 100644 index 0000000..b17f6fa --- /dev/null +++ b/src/components/GlobalHeader/AvatarDropdown.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/src/components/GlobalHeader/RightContent.vue b/src/components/GlobalHeader/RightContent.vue new file mode 100644 index 0000000..4ed26f2 --- /dev/null +++ b/src/components/GlobalHeader/RightContent.vue @@ -0,0 +1,78 @@ + + + diff --git a/src/components/GridContent/index.js b/src/components/GridContent/index.js new file mode 100644 index 0000000..a468126 --- /dev/null +++ b/src/components/GridContent/index.js @@ -0,0 +1,44 @@ +'use strict' + +Object.defineProperty(exports, '__esModule', { + value: true +}) +exports['default'] = void 0 + +require('./index.less') + +var _vueTypes = _interopRequireDefault(require('ant-design-vue/es/_util/vue-types')) + +var _util = require('../../utils/util') + +function _interopRequireDefault (obj) { return obj && obj.__esModule ? obj : { 'default': obj } } + +function _defineProperty (obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }) + } else { + obj[key] = value + } return obj +} + +var GridContent = { + name: 'GridContent', + functional: true, + props: { + children: _vueTypes['default'].any, + contentWidth: _vueTypes['default'].oneOf(['Fluid', 'Fixed']).def('Fluid') + }, + render: function render (h, content) { + var _classNames + + var contentWidth = content.props.contentWidth + var children = content.children + var propsContentWidth = (0, _util.layoutContentWidth)(contentWidth) + var classNames = (_classNames = {}, _defineProperty(_classNames, 'ant-pro-grid-content', true), _defineProperty(_classNames, 'wide', propsContentWidth), _classNames) + return h('div', { + 'class': classNames + }, [children]) + } +} +var _default = GridContent +exports['default'] = _default diff --git a/src/components/GridContent/index.less b/src/components/GridContent/index.less new file mode 100644 index 0000000..9f15f75 --- /dev/null +++ b/src/components/GridContent/index.less @@ -0,0 +1,13 @@ +@import "~ant-design-vue/es/style/themes/default"; + +@grid-content-prefix-cls: ~'@{ant-prefix}-pro-grid-content'; + +.@{grid-content-prefix-cls} { + width: 100%; + min-height: 100%; + transition: 0.3s; + &.wide { + max-width: 1200px; + margin: 0 auto; + } +} diff --git a/src/components/IconSelector/IconSelector.vue b/src/components/IconSelector/IconSelector.vue new file mode 100644 index 0000000..dcf96b5 --- /dev/null +++ b/src/components/IconSelector/IconSelector.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/src/components/IconSelector/README.md b/src/components/IconSelector/README.md new file mode 100644 index 0000000..503095d --- /dev/null +++ b/src/components/IconSelector/README.md @@ -0,0 +1,48 @@ +IconSelector +==== + +> 图标选择组件,常用于为某一个数据设定一个图标时使用 +> eg: 设定菜单列表时,为每个菜单设定一个图标 + +该组件由 [@Saraka](https://github.com/saraka-tsukai) 封装 + + + +### 使用方式 + +```vue + + + +``` + + + +### 事件 + + +| 名称 | 说明 | 类型 | 默认值 | +| ------ | -------------------------- | ------ | ------ | +| change | 当改变了 `icon` 选中项触发 | String | - | diff --git a/src/components/IconSelector/icons.js b/src/components/IconSelector/icons.js new file mode 100644 index 0000000..920f464 --- /dev/null +++ b/src/components/IconSelector/icons.js @@ -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', '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', 'colum-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', 'interation', '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'] + } +] diff --git a/src/components/IconSelector/index.js b/src/components/IconSelector/index.js new file mode 100644 index 0000000..2d27d70 --- /dev/null +++ b/src/components/IconSelector/index.js @@ -0,0 +1,2 @@ +import IconSelector from './IconSelector' +export default IconSelector diff --git a/src/components/MultiTab/MultiTab.vue b/src/components/MultiTab/MultiTab.vue new file mode 100644 index 0000000..3d62bc4 --- /dev/null +++ b/src/components/MultiTab/MultiTab.vue @@ -0,0 +1,163 @@ + diff --git a/src/components/MultiTab/events.js b/src/components/MultiTab/events.js new file mode 100644 index 0000000..b0230b5 --- /dev/null +++ b/src/components/MultiTab/events.js @@ -0,0 +1,2 @@ +import Vue from 'vue' +export default new Vue() diff --git a/src/components/MultiTab/index.js b/src/components/MultiTab/index.js new file mode 100644 index 0000000..02a1c77 --- /dev/null +++ b/src/components/MultiTab/index.js @@ -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 diff --git a/src/components/MultiTab/index.less b/src/components/MultiTab/index.less new file mode 100644 index 0000000..99eadbe --- /dev/null +++ b/src/components/MultiTab/index.less @@ -0,0 +1,21 @@ +@import '../index'; + +@multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab"; +@multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper"; + +.@{multi-tab-prefix-cls} { + margin: -23px -24px 24px -24px; + background: #fff; +} + +.topmenu .@{multi-tab-wrapper-prefix-cls} { + max-width: 98%; + margin: 0 auto; +} + +.@{multi-tab-wrapper-prefix-cls} > .ant-tabs-card > .ant-tabs-bar .ant-tabs-tab-active { + border-color: #fff; + border-top-right-radius: 30px; + background: #fff; + box-shadow: 2px 0 4px rgb(0 21 41 / 5%); +} \ No newline at end of file diff --git a/src/components/NProgress/nprogress.less b/src/components/NProgress/nprogress.less new file mode 100644 index 0000000..7826c0e --- /dev/null +++ b/src/components/NProgress/nprogress.less @@ -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); } +} + diff --git a/src/components/NoticeIcon/NoticeIcon.vue b/src/components/NoticeIcon/NoticeIcon.vue new file mode 100644 index 0000000..8ae1c80 --- /dev/null +++ b/src/components/NoticeIcon/NoticeIcon.vue @@ -0,0 +1,90 @@ + + + + + + diff --git a/src/components/NoticeIcon/index.js b/src/components/NoticeIcon/index.js new file mode 100644 index 0000000..659b9ec --- /dev/null +++ b/src/components/NoticeIcon/index.js @@ -0,0 +1,2 @@ +import NoticeIcon from './NoticeIcon' +export default NoticeIcon diff --git a/src/components/NumberInfo/NumberInfo.vue b/src/components/NumberInfo/NumberInfo.vue new file mode 100644 index 0000000..bdde3e0 --- /dev/null +++ b/src/components/NumberInfo/NumberInfo.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/components/NumberInfo/index.js b/src/components/NumberInfo/index.js new file mode 100644 index 0000000..659a2f3 --- /dev/null +++ b/src/components/NumberInfo/index.js @@ -0,0 +1,3 @@ +import NumberInfo from './NumberInfo' + +export default NumberInfo diff --git a/src/components/NumberInfo/index.less b/src/components/NumberInfo/index.less new file mode 100644 index 0000000..719113d --- /dev/null +++ b/src/components/NumberInfo/index.less @@ -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; + } + } + } + } +} \ No newline at end of file diff --git a/src/components/NumberInfo/index.md b/src/components/NumberInfo/index.md new file mode 100644 index 0000000..147adc4 --- /dev/null +++ b/src/components/NumberInfo/index.md @@ -0,0 +1,43 @@ +# NumberInfo 数据文本 + +常用在数据卡片中,用于突出展示某个业务数据。 + + + +引用方式: + +```javascript +import NumberInfo from '@/components/NumberInfo' + +export default { + components: { + NumberInfo + } +} +``` + + + +## 代码演示 [demo](https://pro.loacg.com/test/home) + +```html + +``` + + + +## API + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +title | 标题 | ReactNode\|string | - +subTitle | 子标题 | ReactNode\|string | - +total | 总量 | ReactNode\|string | - +subTotal | 子总量 | ReactNode\|string | - +status | 增加状态 | 'up \| down' | - +theme | 状态样式 | string | 'light' +gap | 设置数字和描述之间的间距(像素)| number | 8 diff --git a/src/components/Other/CarbonAds.vue b/src/components/Other/CarbonAds.vue new file mode 100644 index 0000000..1e53ba6 --- /dev/null +++ b/src/components/Other/CarbonAds.vue @@ -0,0 +1,114 @@ + + diff --git a/src/components/PageLoading/index.jsx b/src/components/PageLoading/index.jsx new file mode 100644 index 0000000..af6d6d6 --- /dev/null +++ b/src/components/PageLoading/index.jsx @@ -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 (
    + +
    ) + } +} + +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 + } + 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 +} diff --git a/src/components/ProLayout/BasicLayout.jsx b/src/components/ProLayout/BasicLayout.jsx new file mode 100644 index 0000000..c1992e9 --- /dev/null +++ b/src/components/ProLayout/BasicLayout.jsx @@ -0,0 +1,165 @@ +import './BasicLayout.less' + +import PropTypes from 'ant-design-vue/es/_util/vue-types' + +import 'ant-design-vue/es/layout/style' +import Layout from 'ant-design-vue/es/layout' + +import { ContainerQuery } from 'vue-container-query' +import { SiderMenuWrapper } from './components' +import { getComponentFromProp, isFun } from './utils/util' +import { SiderMenuProps } from './components/SiderMenu' +import HeaderView, { HeaderViewProps } from './Header' +import WrapContent from './WrapContent' +import ConfigProvider from './components/ConfigProvider' +import PageHeaderWrapper from './components/PageHeaderWrapper' + +export const BasicLayoutProps = { + ...SiderMenuProps, + ...HeaderViewProps, + contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'), + // contentWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).def('Fluid'), + locale: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).def('en-US'), + breadcrumbRender: PropTypes.func, + disableMobile: PropTypes.bool.def(false), + mediaQuery: PropTypes.object.def({}), + handleMediaQuery: PropTypes.func, + footerRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(undefined) +} + +const MediaQueryEnum = { + 'screen-xs': { + maxWidth: 575 + }, + 'screen-sm': { + minWidth: 576, + maxWidth: 767 + }, + 'screen-md': { + minWidth: 768, + maxWidth: 991 + }, + 'screen-lg': { + minWidth: 992, + maxWidth: 1199 + }, + 'screen-xl': { + minWidth: 1200, + maxWidth: 1599 + }, + 'screen-xxl': { + minWidth: 1600 + } +} + +const getPaddingLeft = ( + hasLeftPadding, + collapsed = undefined, + siderWidth +) => { + if (hasLeftPadding) { + return collapsed ? 80 : siderWidth + } + return 0 +} + +const headerRender = (h, props) => { + if (props.headerRender === false) { + return null + } + return +} + +const defaultI18nRender = (key) => key + +const BasicLayout = { + name: 'BasicLayout', + functional: true, + props: BasicLayoutProps, + render (h, content) { + const { props, children } = content + const { + layout, + // theme, + isMobile, + collapsed, + mediaQuery, + handleMediaQuery, + handleCollapse, + siderWidth, + fixSiderbar, + i18nRender = defaultI18nRender + } = props + + const footerRender = getComponentFromProp(content, 'footerRender') + const rightContentRender = getComponentFromProp(content, 'rightContentRender') + const collapsedButtonRender = getComponentFromProp(content, 'collapsedButtonRender') + const menuHeaderRender = getComponentFromProp(content, 'menuHeaderRender') + const breadcrumbRender = getComponentFromProp(content, 'breadcrumbRender') + const headerContentRender = getComponentFromProp(content, 'headerContentRender') + const menuRender = getComponentFromProp(content, 'menuRender') + + const isTopMenu = layout === 'topmenu' + const hasSiderMenu = !isTopMenu + // If it is a fix menu, calculate padding + // don't need padding in phone mode + const hasLeftPadding = fixSiderbar && !isTopMenu && !isMobile + const cdProps = { + ...props, + hasSiderMenu, + footerRender, + menuHeaderRender, + rightContentRender, + collapsedButtonRender, + breadcrumbRender, + headerContentRender, + menuRender + } + + return ( + + + + + + {headerRender(h, { + ...cdProps, + mode: 'horizontal' + })} + + {children} + + { footerRender !== false && ( + + { isFun(footerRender) && footerRender(h) || footerRender } + + ) || null + } + + + + + ) + } +} + +BasicLayout.install = function (Vue) { + Vue.component(PageHeaderWrapper.name, PageHeaderWrapper) + Vue.component('PageContainer', PageHeaderWrapper) + Vue.component('ProLayout', BasicLayout) +} + +export default BasicLayout diff --git a/src/components/ProLayout/BasicLayout.less b/src/components/ProLayout/BasicLayout.less new file mode 100644 index 0000000..548446d --- /dev/null +++ b/src/components/ProLayout/BasicLayout.less @@ -0,0 +1,101 @@ +@import "~ant-design-vue/es/style/themes/default"; + +@basicLayout-prefix-cls: ~'@{ant-prefix}-pro-basicLayout'; +@sider-menu-prefix-cls: ~'@{ant-prefix}-pro-sider-menu'; +@nav-header-height: @layout-header-height; + +.@{basicLayout-prefix-cls} { + + &:not('.ant-pro-basicLayout-mobile') { + ::-webkit-scrollbar { + width: 6px; + height: 6px; + } + ::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.06); + border-radius: 3px; + -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.08); + } + /* 滚动条滑块 */ + ::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.12); + border-radius: 3px; + -webkit-box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2); + } + } + + // BFC + display: flex; + flex-direction: column; + width: 100%; + min-height: 100%; + + .ant-layout-header { + &:not(.ant-pro-top-menu) { + background: @component-background; + } + + &.ant-pro-fixed-header { + position: fixed; + top: 0; + } + } + + &-content { + position: relative; + margin: 24px; + transition: all 0.2s; + .@{ant-prefix}-pro-page-header-wrap { + margin: -24px -24px 0; + } + &-disable-margin { + margin: 0; + > div > .@{ant-prefix}-pro-page-header-wrap { + margin: 0; + } + } + > .ant-layout { + max-height: 100%; + } + } + + // append hook styles + + .ant-layout-sider-children { + height: 100%; + } + + .trigger { + font-size: 18px; + line-height: 64px; + padding: 0 24px; + cursor: pointer; + transition: color 0.3s; + + &:hover { + color: #1890ff; + } + } + + &-content { + position: relative; + margin: 24px; + transition: all 0.2s; + .@{ant-prefix}-pro-page-header-wrap { + margin: -24px -24px 0; + } + &-disable-margin { + margin: 0; + > div > .@{ant-prefix}-pro-page-header-wrap { + margin: 0; + } + } + > .ant-layout { + max-height: 100%; + } + } + + .color-picker { + margin: 10px 0; + } +} diff --git a/src/components/ProLayout/BlockLayout.jsx b/src/components/ProLayout/BlockLayout.jsx new file mode 100644 index 0000000..48b6157 --- /dev/null +++ b/src/components/ProLayout/BlockLayout.jsx @@ -0,0 +1,9 @@ +const BlockLayout = { + name: 'BlockLayout', + functional: true, + render (createElement, content) { + return content.children + } +} + +export default BlockLayout diff --git a/src/components/ProLayout/Header.jsx b/src/components/ProLayout/Header.jsx new file mode 100644 index 0000000..28b9890 --- /dev/null +++ b/src/components/ProLayout/Header.jsx @@ -0,0 +1,109 @@ +import './Header.less' + +import 'ant-design-vue/es/layout/style' +import Layout from 'ant-design-vue/es/layout' + +import PropTypes from 'ant-design-vue/es/_util/vue-types' +import BaseMenu from './components/RouteMenu/BaseMenu' +import { defaultRenderLogoAntTitle, SiderMenuProps } from './components/SiderMenu/SiderMenu' +import GlobalHeader, { GlobalHeaderProps } from './components/GlobalHeader' +import { VueFragment } from './components' +import { isFun } from './utils/util' + +const { Header } = Layout + +export const HeaderViewProps = { + ...GlobalHeaderProps, + ...SiderMenuProps, + isMobile: PropTypes.bool.def(false), + collapsed: PropTypes.bool, + logo: PropTypes.any, + hasSiderMenu: PropTypes.bool, + autoHideHeader: PropTypes.bool, + menuRender: PropTypes.any, + headerRender: PropTypes.any, + rightContentRender: PropTypes.any, + visible: PropTypes.bool.def(true) +} + +const renderContent = (h, props) => { + const isTop = props.layout === 'topmenu' + const maxWidth = 1200 - 280 - 120 + const contentWidth = props.contentWidth === 'Fixed' + const baseCls = 'ant-pro-top-nav-header' + const { logo, title, theme, isMobile, headerRender, rightContentRender, menuHeaderRender } = props + const rightContentProps = { theme, isTop, isMobile } + let defaultDom = + if (isTop && !isMobile) { + defaultDom = ( +
    +
    + {menuHeaderRender && ( +
    + +
    + )} +
    + +
    + {isFun(rightContentRender) && rightContentRender(h, rightContentProps) || rightContentRender} +
    +
    + ) + } + if (headerRender) { + return headerRender(h, props) + } + return defaultDom +} + +const HeaderView = { + name: 'HeaderView', + props: HeaderViewProps, + render (h) { + const { + visible, + isMobile, + layout, + collapsed, + siderWidth, + fixedHeader, + hasSiderMenu + } = this.$props + const props = this.$props + const isTop = layout === 'topmenu' + + const needSettingWidth = fixedHeader && hasSiderMenu && !isTop && !isMobile + + const className = { + 'ant-pro-fixed-header': fixedHeader, + 'ant-pro-top-menu': isTop + } + + // 没有 <> 暂时代替写法 + return ( + visible ? ( + + { fixedHeader &&
    } +
    + {renderContent(h, props)} +
    + + ) : null + ) + } +} + +export default HeaderView diff --git a/src/components/ProLayout/Header.less b/src/components/ProLayout/Header.less new file mode 100644 index 0000000..7bd9f57 --- /dev/null +++ b/src/components/ProLayout/Header.less @@ -0,0 +1,91 @@ +@import "~ant-design-vue/es/style/themes/default"; + +@top-nav-header-prefix-cls: ~'@{ant-prefix}-pro-top-nav-header'; +@pro-layout-fixed-header-prefix-cls: ~'@{ant-prefix}-pro-fixed-header'; + +.@{top-nav-header-prefix-cls} { + position: relative; + width: 100%; + height: @layout-header-height; + box-shadow: @box-shadow-base; + transition: background 0.3s, width 0.2s; + .ant-menu-submenu.ant-menu-submenu-horizontal { + height: 100%; + line-height: @layout-header-height; + .ant-menu-submenu-title { + height: 100%; + } + } + + &.light { + background-color: @component-background; + h1 { + color: #002140; + } + } + + &-main { + display: flex; + height: @layout-header-height; + padding-left: 24px; + &.wide { + max-width: 1200px; + margin: auto; + padding-left: 0; + } + .left { + display: flex; + flex: 1; + } + .right { + width: 324px; + } + } + + &-logo { + position: relative; + width: 165px; + height: @layout-header-height; + overflow: hidden; + line-height: @layout-header-height; + transition: all 0.3s; + img, svg { + display: inline-block; + height: 32px; + width: 32px; + vertical-align: middle; + } + h1 { + display: inline-block; + margin: 0 0 0 12px; + color: @btn-primary-color; + font-weight: 400; + font-size: 16px; + vertical-align: top; + } + } + &-menu { + .ant-menu.ant-menu-horizontal { + height: @layout-header-height; + line-height: @layout-header-height; + border: none; + } + } +} + +.@{pro-layout-fixed-header-prefix-cls} { + z-index: 9; + width: 100%; + transition: width 0.2s; +} + +.drop-down { + &.menu { + .anticon { + margin-right: 8px; + } + .ant-dropdown-menu-item { + min-width: 160px; + } + } +} diff --git a/src/components/ProLayout/PageView.jsx b/src/components/ProLayout/PageView.jsx new file mode 100644 index 0000000..66ad750 --- /dev/null +++ b/src/components/ProLayout/PageView.jsx @@ -0,0 +1,14 @@ +import { PageHeaderWrapper } from './components' + +const PageView = { + name: 'PageView', + render () { + return ( + + + + ) + } +} + +export default PageView diff --git a/src/components/ProLayout/WrapContent.jsx b/src/components/ProLayout/WrapContent.jsx new file mode 100644 index 0000000..983af65 --- /dev/null +++ b/src/components/ProLayout/WrapContent.jsx @@ -0,0 +1,44 @@ +import PropTypes from 'ant-design-vue/es/_util/vue-types' + +import 'ant-design-vue/es/layout/style' +import Layout from 'ant-design-vue/es/layout' +import ConfigProvider from 'ant-design-vue/es/config-provider' +import GridContent from './components/GridContent' + +const { Content } = Layout + +const WrapContentProps = { + isChildrenLayout: PropTypes.bool, + location: PropTypes.any, + contentHeight: PropTypes.number, + contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid') +} + +const WrapContent = { + name: 'WrapContent', + props: WrapContentProps, + render (h) { + const { + isChildrenLayout, + contentWidth + } = this.$props + return ( + + { + if (isChildrenLayout) { + return el.parentNode() + } + return document.body + }} + > +
    + {this.$slots.default} +
    +
    +
    + ) + } +} + +export default WrapContent diff --git a/src/components/ProLayout/components/ConfigProvider/index.js b/src/components/ProLayout/components/ConfigProvider/index.js new file mode 100644 index 0000000..a246071 --- /dev/null +++ b/src/components/ProLayout/components/ConfigProvider/index.js @@ -0,0 +1,27 @@ +import PropTypes from 'ant-design-vue/es/_util/vue-types' + +const ProConfigProviderProps = { + i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false), + contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'), + breadcrumbRender: PropTypes.func +} + +const ConfigProvider = { + name: 'ProConfigProvider', + props: ProConfigProviderProps, + provide () { + const _self = this + return { + locale: _self.$props.i18nRender, + contentWidth: _self.$props.contentWidth, + breadcrumbRender: _self.$props.breadcrumbRender + } + }, + render () { + const { $scopedSlots } = this + const children = this.children || $scopedSlots.default + return children() + } +} + +export default ConfigProvider diff --git a/src/components/ProLayout/components/DocumentTitle/SideEffect/index.js b/src/components/ProLayout/components/DocumentTitle/SideEffect/index.js new file mode 100644 index 0000000..1645a08 --- /dev/null +++ b/src/components/ProLayout/components/DocumentTitle/SideEffect/index.js @@ -0,0 +1,17 @@ +/* eslint-disable */ +class SideEffect { + constructor ({ propsToState, handleStateChange }) { + if (typeof propsToState !== 'function') { + throw new Error('Expected propsToState to be a function.') + } + if (typeof handleStateChange !== 'function') { + throw new Error('Expected handleStateChange to be a function.') + } + this.options = { + propsToState, + handleStateChange + } + } +} + +export default SideEffect diff --git a/src/components/ProLayout/components/DocumentTitle/index.js b/src/components/ProLayout/components/DocumentTitle/index.js new file mode 100644 index 0000000..533f958 --- /dev/null +++ b/src/components/ProLayout/components/DocumentTitle/index.js @@ -0,0 +1,89 @@ +// import SideEffect from './SideEffect' +import { setDocumentTitle } from './util' + +// const sideEffect = new SideEffect({ +// propsToState (propsList) { +// var innermostProps = propsList[propsList.length - 1] +// if (innermostProps) { +// return innermostProps.title +// } +// }, +// handleStateChange (title, prefix) { +// console.log('title', title, prefix) +// const nextTitle = `${(title || '')} - ${prefix}` +// if (nextTitle !== document.title) { +// setDocumentTitle(nextTitle) +// } +// } +// }) + +const handleStateChange = (title, prefix) => { + const nextTitle = `${(title || '')} - ${prefix}` + if (nextTitle !== document.title) { + setDocumentTitle(nextTitle) + } +} + +const DocumentTitle = { + name: 'DocumentTitle', + functional: true, + props: { + prefix: { + type: String, + required: false, + default: 'Ant Design Pro' + }, + title: { + type: String, + required: true + } + }, + // { props, data, children } + // eslint-disable-next-line + render (createElement, { props, data, children }) { + handleStateChange(props.title, props.prefix) + return children + } +} + +DocumentTitle.install = function (Vue) { + // const mountedInstances = [] + // let state + + // function __emit (sideEffect) { + // const options = sideEffect.options + // state = options.propsToState(mountedInstances.map(function (instance) { + // return instance + // })) + + // options.handleStateChange(state) + // } + + // Vue.mixin({ + // beforeMount () { + // const sideEffect = this.$options.sideEffect + // if (sideEffect) { + // mountedInstances.push(this) + // __emit(sideEffect) + // } + // }, + // updated () { + // const sideEffect = this.$options.sideEffect + // if (sideEffect) { + // __emit(sideEffect) + // } + // }, + // beforeDestroy () { + // const sideEffect = this.$options.sideEffect + // if (sideEffect) { + // const index = mountedInstances.indexOf(this) + // mountedInstances.splice(index, 1) + // __emit(sideEffect) + // } + // } + // }) + + Vue.component(DocumentTitle.name, DocumentTitle) +} + +export default DocumentTitle diff --git a/src/components/ProLayout/components/DocumentTitle/util.js b/src/components/ProLayout/components/DocumentTitle/util.js new file mode 100644 index 0000000..f820754 --- /dev/null +++ b/src/components/ProLayout/components/DocumentTitle/util.js @@ -0,0 +1,17 @@ +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) + } +} diff --git a/src/components/ProLayout/components/Fragment.jsx b/src/components/ProLayout/components/Fragment.jsx new file mode 100644 index 0000000..1ed3e54 --- /dev/null +++ b/src/components/ProLayout/components/Fragment.jsx @@ -0,0 +1,7 @@ +export default { + name: 'VueFragment', + functional: true, + render (h, ctx) { + return ctx.children.length > 1 ? h('div', {}, ctx.children) : ctx.children + } +} diff --git a/src/components/ProLayout/components/GlobalFooter/index.jsx b/src/components/ProLayout/components/GlobalFooter/index.jsx new file mode 100644 index 0000000..2d57c04 --- /dev/null +++ b/src/components/ProLayout/components/GlobalFooter/index.jsx @@ -0,0 +1,43 @@ +import './index.less' + +import PropTypes from 'ant-design-vue/es/_util/vue-types' +import { getComponentFromProp, hasProp } from 'ant-design-vue/lib/_util/props-util' + +const GlobalFooterProps = { + links: PropTypes.array, + copyright: PropTypes.any +} + +const GlobalFooter = { + name: 'GlobalFooter', + props: GlobalFooterProps, + render () { + const copyright = getComponentFromProp(this, 'copyright') + const links = getComponentFromProp(this, 'links') + const linksType = hasProp(links) + + return ( +
    + + {copyright && ( + + )} +
    + ) + } +} + +export default GlobalFooter diff --git a/src/components/ProLayout/components/GlobalFooter/index.less b/src/components/ProLayout/components/GlobalFooter/index.less new file mode 100644 index 0000000..67f4b17 --- /dev/null +++ b/src/components/ProLayout/components/GlobalFooter/index.less @@ -0,0 +1,31 @@ +@import "~ant-design-vue/es/style/themes/default"; + +@global-footer-prefix-cls: ~'@{ant-prefix}-pro-global-footer'; + +.@{global-footer-prefix-cls} { + margin: 48px 0 24px 0; + padding: 0 16px; + text-align: center; + + &-links { + margin-bottom: 8px; + + a { + color: @text-color-secondary; + transition: all 0.3s; + + &:not(:last-child) { + margin-right: 40px; + } + + &:hover { + color: @text-color; + } + } + } + + &-copyright { + color: @text-color-secondary; + font-size: @font-size-base; + } +} diff --git a/src/components/ProLayout/components/GlobalHeader/index.jsx b/src/components/ProLayout/components/GlobalHeader/index.jsx new file mode 100644 index 0000000..a455820 --- /dev/null +++ b/src/components/ProLayout/components/GlobalHeader/index.jsx @@ -0,0 +1,81 @@ +import './index.less' + +import debounce from 'lodash/debounce' +import PropTypes from 'ant-design-vue/es/_util/vue-types' +import { triggerEvent, inBrowser, isFun } from '../../utils/util' +import 'ant-design-vue/es/icon/style' +import Icon from 'ant-design-vue/es/icon' +import { defaultRenderLogo } from '../SiderMenu/SiderMenu' + +export const GlobalHeaderProps = { + collapsed: PropTypes.bool, + handleCollapse: PropTypes.func, + isMobile: PropTypes.bool.def(false), + fixedHeader: PropTypes.bool.def(false), + logo: PropTypes.any, + menuRender: PropTypes.any, + collapsedButtonRender: PropTypes.any, + headerContentRender: PropTypes.any, + rightContentRender: PropTypes.any +} + +const defaultRenderCollapsedButton = (h, collapsed) => ( + +) + +const GlobalHeader = { + name: 'GlobalHeader', + props: GlobalHeaderProps, + render (h) { + const { isMobile, logo, rightContentRender, headerContentRender } = this.$props + const toggle = () => { + const { collapsed, handleCollapse } = this.$props + if (handleCollapse) handleCollapse(!collapsed) + this.triggerResizeEvent() + } + const renderCollapsedButton = () => { + const { + collapsed, + collapsedButtonRender = defaultRenderCollapsedButton, + menuRender + } = this.$props + if (collapsedButtonRender !== false && menuRender !== false) { + return ( + + {isFun(collapsedButtonRender) && collapsedButtonRender(h, collapsed) || collapsedButtonRender} + + ) + } + return null + } + + const headerCls = 'ant-pro-global-header' + + return ( +
    + {isMobile && ( + + {defaultRenderLogo(h, logo)} + + )} + {renderCollapsedButton()} + {headerContentRender && ( +
    + {isFun(headerContentRender) && headerContentRender(h, this.$props) || headerContentRender} +
    + )} + {isFun(rightContentRender) && rightContentRender(h, this.$props) || rightContentRender} +
    + ) + }, + methods: { + triggerResizeEvent: debounce(() => { + inBrowser && triggerEvent(window, 'resize') + }) + }, + beforeDestroy () { + this.triggerResizeEvent.cancel && this.triggerResizeEvent.cancel() + } +} + +export default GlobalHeader diff --git a/src/components/ProLayout/components/GlobalHeader/index.less b/src/components/ProLayout/components/GlobalHeader/index.less new file mode 100644 index 0000000..a7fb1eb --- /dev/null +++ b/src/components/ProLayout/components/GlobalHeader/index.less @@ -0,0 +1,109 @@ +@import "~ant-design-vue/es/style/themes/default"; + +@global-header-prefix-cls: ~'@{ant-prefix}-pro-global-header'; + +@pro-header-bg: @component-background; +@pro-header-hover-bg: @component-background; +@pro-header-box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); +@pro-header-hover-trigger-action-bg: rgba(0,0,0,.025); + +.@{global-header-prefix-cls} { + position: relative; + height: @layout-header-height; + padding: 0; + background: @pro-header-bg; + box-shadow: @pro-header-box-shadow; + + &-index-right { + float: right; + height: 100%; + margin-left: auto; + overflow: hidden; + + .@{global-header-prefix-cls}-index-action { + display: inline-block; + height: 100%; + padding: 0 12px; + cursor: pointer; + transition: all .3s; + + &:hover { + background: @pro-header-hover-trigger-action-bg; + } + } + } + + &-logo { + display: inline-block; + height: @layout-header-height; + padding: 0 0 0 24px; + font-size: 20px; + line-height: @layout-header-height; + vertical-align: top; + cursor: pointer; + img, svg { + display: inline-block; + width: 32px; + height: 32px; + vertical-align: middle; + } + } + + &-menu { + .anticon { + margin-right: 8px; + } + .ant-dropdown-menu-item { + min-width: 160px; + } + } + + &-trigger { + height: @layout-header-height; + line-height: @layout-header-height; + vertical-align: top; + padding: 0 22px; + display: inline-block; + cursor: pointer; + transition: all 0.3s, padding 0s; + .anticon { + font-size: 20px; + vertical-align: -0.225em; + } + &:hover { + background: @pro-header-hover-trigger-action-bg; + } + } + + &-content { + height: @layout-header-height; + line-height: @layout-header-height; + vertical-align: top; + display: inline-block; + } + + .dark { + height: @layout-header-height; + .action { + color: rgba(255, 255, 255, 0.85); + > i { + color: rgba(255, 255, 255, 0.85); + } + &:hover, + &.opened { + background: @primary-color; + } + .ant-badge { + color: rgba(255, 255, 255, 0.85); + } + } + } + + .ant-pro-global-header-index-action { + i { + color: rgba(0,0,0,.65); + vertical-align: middle; + } + } + +} diff --git a/src/components/ProLayout/components/GridContent/index.jsx b/src/components/ProLayout/components/GridContent/index.jsx new file mode 100644 index 0000000..4038f12 --- /dev/null +++ b/src/components/ProLayout/components/GridContent/index.jsx @@ -0,0 +1,27 @@ +import './index.less' + +import PropTypes from 'ant-design-vue/es/_util/vue-types' +import { layoutContentWidth } from '../../utils/util' + +const GridContent = { + name: 'GridContent', + functional: true, + props: { + children: PropTypes.any, + contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid') + }, + render (h, content) { + const { contentWidth } = content.props + const children = content.children + + const propsContentWidth = layoutContentWidth(contentWidth) + const classNames = { + 'ant-pro-grid-content': true, + 'wide': propsContentWidth + } + + return
    {children}
    + } +} + +export default GridContent diff --git a/src/components/ProLayout/components/GridContent/index.less b/src/components/ProLayout/components/GridContent/index.less new file mode 100644 index 0000000..9f15f75 --- /dev/null +++ b/src/components/ProLayout/components/GridContent/index.less @@ -0,0 +1,13 @@ +@import "~ant-design-vue/es/style/themes/default"; + +@grid-content-prefix-cls: ~'@{ant-prefix}-pro-grid-content'; + +.@{grid-content-prefix-cls} { + width: 100%; + min-height: 100%; + transition: 0.3s; + &.wide { + max-width: 1200px; + margin: 0 auto; + } +} diff --git a/src/components/ProLayout/components/PageHeaderWrapper/index.jsx b/src/components/ProLayout/components/PageHeaderWrapper/index.jsx new file mode 100644 index 0000000..8e8289a --- /dev/null +++ b/src/components/ProLayout/components/PageHeaderWrapper/index.jsx @@ -0,0 +1,234 @@ +import './index.less' + +import PropTypes from 'ant-design-vue/es/_util/vue-types' +import { isArray } from 'ant-design-vue/lib/_util/vue-types/utils' +import GridContent from '../GridContent' +import 'ant-design-vue/es/page-header/style' +import PageHeader, { PageHeaderProps } from 'ant-design-vue/es/page-header' +import 'ant-design-vue/es/tabs/style' +import Tabs from 'ant-design-vue/es/tabs' +import { getComponentFromProp } from 'ant-design-vue/lib/_util/props-util' + +const prefixedClassName = 'ant-pro-page-header-wrap' + +const PageHeaderTabConfig = { + tabList: PropTypes.array, + tabActiveKey: PropTypes.string, + tabProps: PropTypes.object, + tabChange: PropTypes.func +} + +const PageHeaderWrapperProps = { + ...PageHeaderTabConfig, + ...PageHeaderProps, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + content: PropTypes.any, + extraContent: PropTypes.any, + pageHeaderRender: PropTypes.func, + breadcrumb: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).def(true), + back: PropTypes.func, + + // only use `pro-layout` in children + i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false) +} + +const defaultI18nRender = (t) => t + +const useContext = (route) => { + return route && { + ...route.meta + } || null +} + +const noop = () => { +} + +// TODO :: tabList tab 支持图标 优化 +const renderFooter = (h, tabConfigProps, i18nRender) => { + const { + tabList, + tabActiveKey, + tabChange: onTabChange, + tabBarExtraContent, + tabProps + } = tabConfigProps + return tabList && tabList.length > 0 && ( + { + if (onTabChange) { + onTabChange(key) + } + }} + tabBarExtraContent={tabBarExtraContent} + {...tabProps} + > + {tabList.map(item => ( + + ))} + + ) +} + +const renderPageHeader = (h, content, extraContent) => { + if (!content && !extraContent) { + return null + } + return ( +
    +
    +
    + {content && ( +
    {content}
    + )} + {extraContent && ( +
    + {extraContent} +
    + )} +
    +
    +
    + ) +} + +const defaultPageHeaderRender = (h, props, pageMeta, i18nRender) => { + const { + title: propTitle, + tags, + content, + pageHeaderRender, + extra, + extraContent, + breadcrumb, + back: handleBack, + ...restProps + } = props + if (pageHeaderRender) { + return pageHeaderRender({ ...props }) + } + let pageHeaderTitle = propTitle + if (!propTitle && propTitle !== false) { + // pageHeaderTitle = pageMeta.title + // 2021.02.03 去除title显示 + pageHeaderTitle = undefined + } + // title props 不是 false 且不是 array 则直接渲染 title + // 反之认为是 VNode, 作为 render 参数直接传入到 PageHeader + const title = isArray(pageHeaderTitle) + ? pageHeaderTitle + : pageHeaderTitle && i18nRender(pageHeaderTitle) + const tabProps = { + breadcrumb, + extra, + tags, + title, + footer: renderFooter(h, restProps, i18nRender) + } + if (!handleBack) { + tabProps.backIcon = false + } + + return ( + + {renderPageHeader(h, content, extraContent)} + + ) + // return +} + +const PageHeaderWrapper = { + name: 'PageHeaderWrapper', + props: PageHeaderWrapperProps, + inject: ['locale', 'contentWidth', 'breadcrumbRender'], + render (h) { + const { $route, $listeners } = this + const children = this.$slots.default + const title = getComponentFromProp(this, 'title') + const tags = getComponentFromProp(this, 'tags') + const content = getComponentFromProp(this, 'content') + const extra = getComponentFromProp(this, 'extra') + const extraContent = getComponentFromProp(this, 'extraContent') + + const pageMeta = useContext(this.$props.route || $route) + const i18n = this.$props.i18nRender || this.locale || defaultI18nRender + const contentWidth = this.$props.contentWidth || this.contentWidth || false + // 当未设置 back props 或未监听 @back,不显示 back + // props 的 back 事件优先级高于 @back,需要注意 + const onBack = this.$props.back || $listeners.back + const back = onBack && (() => { + // this.$emit('back') + // call props back func + onBack && onBack() + }) || undefined + + const onTabChange = this.$props.tabChange + const tabChange = (key) => { + this.$emit('tabChange', key) + onTabChange && onTabChange(key) + } + + let breadcrumb = {} + const propsBreadcrumb = this.$props.breadcrumb + if (propsBreadcrumb === true) { + const routes = $route.matched.concat().map(route => { + return { + path: route.path, + breadcrumbName: i18n(route.meta.title), + redirect: route.redirect + } + }) + + const defaultItemRender = ({ route, params, routes, paths, h }) => { + return (route.redirect === 'noRedirect' || routes.indexOf(route) === routes.length - 1) && ( + {route.breadcrumbName} + ) || ( + {route.breadcrumbName} + ) + } + + // If custom breadcrumb render undefined + // use default breadcrumb.. + const itemRender = this.breadcrumbRender || defaultItemRender + + breadcrumb = { props: { routes, itemRender } } + } else { + breadcrumb = propsBreadcrumb || null + } + + const props = { + ...this.$props, + title, + tags, + content, + extra, + extraContent, + breadcrumb, + tabChange, + back + } + + return ( +
    +
    + {defaultPageHeaderRender(h, props, pageMeta, i18n)} +
    + {children ? ( + +
    + {children} +
    +
    + ) : null} +
    + ) + } +} + +PageHeaderWrapper.install = function (Vue) { + Vue.component(PageHeaderWrapper.name, PageHeaderWrapper) + Vue.component('page-container', PageHeaderWrapper) +} + +export default PageHeaderWrapper diff --git a/src/components/ProLayout/components/PageHeaderWrapper/index.less b/src/components/ProLayout/components/PageHeaderWrapper/index.less new file mode 100644 index 0000000..10c14fa --- /dev/null +++ b/src/components/ProLayout/components/PageHeaderWrapper/index.less @@ -0,0 +1,93 @@ +@import "~ant-design-vue/es/style/themes/default"; + +@ant-pro-page-header-wrap: ~'@{ant-prefix}-pro-page-header-wrap'; + +.@{ant-pro-page-header-wrap}-children-content { + margin: 24px 24px 0; +} + +.@{ant-pro-page-header-wrap}-page-header-warp { + background-color: @component-background; +} + +.@{ant-pro-page-header-wrap}-main { + .@{ant-pro-page-header-wrap}-detail { + display: flex; + } + + .@{ant-pro-page-header-wrap}-row { + display: flex; + width: 100%; + } + + .@{ant-pro-page-header-wrap}-title-content { + margin-bottom: 16px; + } + + .@{ant-pro-page-header-wrap}-title, + .@{ant-pro-page-header-wrap}-content { + flex: auto; + } + + .@{ant-pro-page-header-wrap}-extraContent, + .@{ant-pro-page-header-wrap}-main { + flex: 0 1 auto; + } + + .@{ant-pro-page-header-wrap}-main { + width: 100%; + } + + .@{ant-pro-page-header-wrap}-title { + margin-bottom: 16px; + } + + .@{ant-pro-page-header-wrap}-logo { + margin-bottom: 16px; + } + + .@{ant-pro-page-header-wrap}-extraContent { + min-width: 242px; + margin-left: 88px; + text-align: right; + } +} + +@media screen and (max-width: @screen-xl) { + .@{ant-pro-page-header-wrap}-main { + .@{ant-pro-page-header-wrap}-extraContent { + margin-left: 44px; + } + } +} + +@media screen and (max-width: @screen-lg) { + .@{ant-pro-page-header-wrap}-main { + .@{ant-pro-page-header-wrap}-extraContent { + margin-left: 20px; + } + } +} + +@media screen and (max-width: @screen-md) { + .@{ant-pro-page-header-wrap}-main { + .@{ant-pro-page-header-wrap}-row { + display: block; + } + + .@{ant-pro-page-header-wrap}-action, + .@{ant-pro-page-header-wrap}-extraContent { + margin-left: 0; + text-align: left; + } + } +} + +@media screen and (max-width: @screen-sm) { + .@{ant-pro-page-header-wrap}-detail { + display: block; + } + .@{ant-pro-page-header-wrap}-extraContent { + margin-left: 0; + } +} diff --git a/src/components/ProLayout/components/RouteMenu/BaseMenu.jsx b/src/components/ProLayout/components/RouteMenu/BaseMenu.jsx new file mode 100644 index 0000000..0162a7d --- /dev/null +++ b/src/components/ProLayout/components/RouteMenu/BaseMenu.jsx @@ -0,0 +1,180 @@ +import PropTypes from 'ant-design-vue/es/_util/vue-types' + +import 'ant-design-vue/es/menu/style' +import Menu from 'ant-design-vue/es/menu' +import 'ant-design-vue/es/icon/style' +import Icon from 'ant-design-vue/es/icon' + +const { + Item: MenuItem, + SubMenu +} = Menu + +export const RouteMenuProps = { + menus: PropTypes.array, + theme: PropTypes.string.def('dark'), + mode: PropTypes.string.def('inline'), + collapsed: PropTypes.bool.def(false), + i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false) +} + +const httpReg = /(http|https|ftp):\/\/([\w.]+\/?)\S*/ + +const renderMenu = (h, item, i18nRender) => { + if (item && !item.hidden) { + const bool = item.children && !item.hideChildrenInMenu + return bool ? renderSubMenu(h, item, i18nRender) : renderMenuItem(h, item, i18nRender) + } + return null +} + +const renderSubMenu = (h, item, i18nRender) => { + return ( + + {renderIcon(h, item.meta.icon)} + {renderTitle(h, item.meta.title, i18nRender)} + + )}> + {!item.hideChildrenInMenu && item.children.map(cd => renderMenu(h, cd, i18nRender))} + + ) +} + +const renderMenuItem = (h, item, i18nRender) => { + const meta = Object.assign({}, item.meta) + const target = meta.target || null + const hasRemoteUrl = httpReg.test(item.path) + const CustomTag = target && 'a' || 'router-link' + const props = { to: { name: item.name } } + const attrs = (hasRemoteUrl || target) ? { href: item.path, target: target } : {} + if (item.children && item.hideChildrenInMenu) { + // 把有子菜单的 并且 父菜单是要隐藏子菜单的 + // 都给子菜单增加一个 hidden 属性 + // 用来给刷新页面时, selectedKeys 做控制用 + item.children.forEach(cd => { + cd.meta = Object.assign(cd.meta || {}, { hidden: true }) + }) + } + return ( + + + {renderIcon(h, meta.icon)} + {renderTitle(h, meta.title, i18nRender)} + + + ) +} + +const renderIcon = (h, icon) => { + if (icon === undefined || icon === 'none' || icon === null) { + return null + } + const props = {} + typeof (icon) === 'object' ? (props.component = icon) : (props.type = icon) + return +} + +const renderTitle = (h, title, i18nRender) => { + return { i18nRender && i18nRender(title) || title } +} + +const RouteMenu = { + name: 'RouteMenu', + props: RouteMenuProps, + data () { + return { + openKeys: [], + selectedKeys: [], + cachedOpenKeys: [], + cachedSelectedKeys: [] + } + }, + render (h) { + const { mode, theme, menus, i18nRender } = this + const handleOpenChange = (openKeys) => { + // 在水平模式下时,不再执行后续 + if (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] : [] + } + } + + const dynamicProps = { + props: { + mode, + theme, + openKeys: this.openKeys, + selectedKeys: this.selectedKeys + }, + on: { + select: menu => { + this.$emit('select', menu) + if (!httpReg.test(menu.key)) { + this.selectedKeys = menu.selectedKeys + } + }, + openChange: handleOpenChange + } + } + + const menuItems = menus.map(item => { + if (item.hidden) { + return null + } + return renderMenu(h, item, i18nRender) + }) + return {menuItems} + }, + methods: { + 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 => { + item.path && openKeys.push(item.path) + }) + } + + this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys) + } + }, + computed: { + rootSubmenuKeys: vm => { + const keys = [] + vm.menus.forEach(item => keys.push(item.path)) + return keys + } + }, + created () { + this.$watch('$route', () => { + this.updateMenu() + }) + this.$watch('collapsed', val => { + if (val) { + this.cachedOpenKeys = this.openKeys.concat() + this.openKeys = [] + } else { + this.openKeys = this.cachedOpenKeys + } + }) + }, + mounted () { + this.updateMenu() + } +} + +export default RouteMenu diff --git a/src/components/ProLayout/components/RouteMenu/index.js b/src/components/ProLayout/components/RouteMenu/index.js new file mode 100644 index 0000000..b6dbfee --- /dev/null +++ b/src/components/ProLayout/components/RouteMenu/index.js @@ -0,0 +1,2 @@ +import BaseMenu from './BaseMenu' +export default BaseMenu diff --git a/src/components/ProLayout/components/SettingDrawer/BlockCheckbox.jsx b/src/components/ProLayout/components/SettingDrawer/BlockCheckbox.jsx new file mode 100644 index 0000000..51cc2eb --- /dev/null +++ b/src/components/ProLayout/components/SettingDrawer/BlockCheckbox.jsx @@ -0,0 +1,69 @@ +import PropTypes from 'ant-design-vue/es/_util/vue-types' + +import 'ant-design-vue/es/tooltip/style' +import Tooltip from 'ant-design-vue/es/tooltip' +import 'ant-design-vue/es/icon/style' +import Icon from 'ant-design-vue/es/icon' + +const BlockCheckboxProps = { + value: PropTypes.string, + // Item: { key, url, title } + list: PropTypes.array, + + i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false) +} + +const baseClassName = 'ant-pro-setting-drawer-block-checbox' +const BlockCheckbox = { + props: BlockCheckboxProps, + inject: ['locale'], + render (h) { + const { value, list } = this + const i18n = this.$props.i18nRender || this.locale + + const items = list || [ + { + key: 'sidemenu', + url: + 'https://gw.alipayobjects.com/zos/antfincdn/XwFOFbLkSM/LCkqqYNmvBEbokSDscrm.svg', + title: i18n('app.setting.sidemenu') + }, + { + key: 'topmenu', + url: + 'https://gw.alipayobjects.com/zos/antfincdn/URETY8%24STp/KDNDBbriJhLwuqMoxcAr.svg', + title: i18n('app.setting.topmenu') + } + ] + + const handleChange = (key) => { + this.$emit('change', key) + } + + const disableStyle = { + cursor: 'not-allowed' + } + + return ( +
    + {items.map(item => ( + +
    !item.disable && handleChange(item.key)}> + {item.key} +
    + +
    +
    +
    + ))} +
    + ) + } +} + +export default BlockCheckbox diff --git a/src/components/ProLayout/components/SettingDrawer/LayoutChange.jsx b/src/components/ProLayout/components/SettingDrawer/LayoutChange.jsx new file mode 100644 index 0000000..cfc6735 --- /dev/null +++ b/src/components/ProLayout/components/SettingDrawer/LayoutChange.jsx @@ -0,0 +1,107 @@ +import PropTypes from 'ant-design-vue/es/_util/vue-types' + +import 'ant-design-vue/es/tooltip/style' +import Tooltip from 'ant-design-vue/es/tooltip' +import 'ant-design-vue/es/list/style' +import List from 'ant-design-vue/es/list' +import 'ant-design-vue/es/select/style' +import Select from 'ant-design-vue/es/select' +import 'ant-design-vue/es/switch/style' +import Switch from 'ant-design-vue/es/switch' + +export const renderLayoutSettingItem = (h, item) => { + const action = { ...item.action } + return ( + + + {item.title} + + + ) +} + +export const LayoutSettingProps = { + contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'), + fixedHeader: PropTypes.bool, + fixSiderbar: PropTypes.bool, + multiTab: PropTypes.bool, + layout: PropTypes.oneOf(['sidemenu', 'topmenu']), + + i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false) +} + +export default { + props: LayoutSettingProps, + inject: ['locale'], + render (h) { + const i18n = this.$props.i18nRender || this.locale + const { contentWidth, fixedHeader, layout, fixSiderbar, multiTab } = this + + const handleChange = (type, value) => { + this.$emit('change', { type, value }) + } + + return ( + handleChange('contentWidth', value)} + style={{ width: '80px' }} + > + {layout === 'sidemenu' ? null : ( + + {i18n('app.setting.content-width.fixed')} + + )} + + {i18n('app.setting.content-width.fluid')} + + + ) + }, + { + title: i18n('app.setting.fixedheader'), + action: ( + handleChange('fixedHeader', checked)} + /> + ) + }, + { + title: i18n('app.setting.fixedsidebar'), + disabled: layout === 'topmenu', + disabledReason: i18n('app.setting.fixedsidebar.hint'), + action: ( + handleChange('fixSiderbar', checked)} + /> + ) + }, + { + title: i18n('app.setting.multitab'), + disabledReason: i18n('app.setting.fixedsidebar.hint'), + action: ( + handleChange('multiTab', checked)} + /> + ) + } + ]} + renderItem={(item, index) => renderLayoutSettingItem(h, item)} + /> + ) + } +} diff --git a/src/components/ProLayout/components/SettingDrawer/ThemeColor.jsx b/src/components/ProLayout/components/SettingDrawer/ThemeColor.jsx new file mode 100644 index 0000000..5eddbea --- /dev/null +++ b/src/components/ProLayout/components/SettingDrawer/ThemeColor.jsx @@ -0,0 +1,75 @@ +import './ThemeColor.less' + +import PropTypes from 'ant-design-vue/es/_util/vue-types' +import { genThemeToString } from '../../utils/util' +import 'ant-design-vue/es/tooltip/style' +import Tooltip from 'ant-design-vue/es/tooltip' +import 'ant-design-vue/es/icon/style' +import Icon from 'ant-design-vue/es/icon' + +const baseClassName = 'theme-color' + +export const TagProps = { + color: PropTypes.string, + check: PropTypes.bool +} + +const Tag = { + props: TagProps, + functional: true, + render (h, content) { + const { props: { color, check }, data } = content + return ( +
    + { check ? : null } +
    + ) + } +} + +export const ThemeColorProps = { + colors: PropTypes.array, + title: PropTypes.string, + value: PropTypes.string, + + i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false) +} + +const ThemeColor = { + props: ThemeColorProps, + inject: ['locale'], + render (h) { + const { title, value, colors = [] } = this + const i18n = this.$props.i18nRender || this.locale + const handleChange = (key) => { + this.$emit('change', key) + } + + return ( +
    +

    {title}

    +
    + {colors.map(item => { + const themeKey = genThemeToString(item.key) + const check = value === item.key || genThemeToString(value) === item.key + return ( + + handleChange(item.key)} + /> + + ) + })} +
    +
    + ) + } +} + +export default ThemeColor diff --git a/src/components/ProLayout/components/SettingDrawer/ThemeColor.less b/src/components/ProLayout/components/SettingDrawer/ThemeColor.less new file mode 100644 index 0000000..0d18b79 --- /dev/null +++ b/src/components/ProLayout/components/SettingDrawer/ThemeColor.less @@ -0,0 +1,26 @@ +@import './index.less'; + +.@{ant-pro-setting-drawer}-content { + .theme-color { + margin-top: 24px; + overflow: hidden; + + .theme-color-title { + margin-bottom: 12px; + font-size: 14px; + line-height: 22px; + } + + .theme-color-block { + float: left; + width: 20px; + height: 20px; + margin-right: 8px; + color: #fff; + font-weight: bold; + text-align: center; + border-radius: 2px; + cursor: pointer; + } + } +} diff --git a/src/components/ProLayout/components/SettingDrawer/index.jsx b/src/components/ProLayout/components/SettingDrawer/index.jsx new file mode 100644 index 0000000..90b2021 --- /dev/null +++ b/src/components/ProLayout/components/SettingDrawer/index.jsx @@ -0,0 +1,344 @@ +import './index.less' + +import omit from 'omit.js' +import PropTypes from 'ant-design-vue/es/_util/vue-types' + +import 'ant-design-vue/es/divider/style' +import Divider from 'ant-design-vue/es/divider' + +import 'ant-design-vue/es/drawer/style' +import Drawer from 'ant-design-vue/es/drawer' + +import 'ant-design-vue/es/list/style' +import List from 'ant-design-vue/es/list' + +import 'ant-design-vue/es/switch/style' +import Switch from 'ant-design-vue/es/switch' + +import 'ant-design-vue/es/button/style' +import Button from 'ant-design-vue/es/button' + +import 'ant-design-vue/es/icon/style' +import Icon from 'ant-design-vue/es/icon' + +import 'ant-design-vue/es/alert/style' +import Alert from 'ant-design-vue/es/alert' + +import antPortal from 'ant-design-vue/es/_util/portalDirective' + +import 'ant-design-vue/es/message/style' +import message from 'ant-design-vue/es/message' + +import BlockCheckbox from './BlockCheckbox' +import ThemeColor from './ThemeColor' +import LayoutSetting, { renderLayoutSettingItem } from './LayoutChange' +import { updateTheme, updateColorWeak } from '../../utils/dynamicTheme' +import { genStringToTheme } from '../../utils/util' +import CopyToClipboard from 'vue-copy-to-clipboard' + +const baseClassName = 'ant-pro-setting-drawer' + +const BodyProps = { + title: PropTypes.string.def('') +} + +const Body = { + props: BodyProps, + render (h) { + const { title } = this + + return ( +
    +

    {title}

    + {this.$slots.default} +
    + ) + } +} + +const defaultI18nRender = (t) => t + +const getThemeList = (i18nRender) => { + const list = window.umi_plugin_ant_themeVar || [] + + const themeList = [ + { + key: 'light', + url: 'https://gw.alipayobjects.com/zos/antfincdn/NQ%24zoisaD2/jpRkZQMyYRryryPNtyIC.svg', + title: i18nRender('app.setting.pagestyle.light') + }, + { + key: 'dark', + url: 'https://gw.alipayobjects.com/zos/antfincdn/XwFOFbLkSM/LCkqqYNmvBEbokSDscrm.svg', + title: i18nRender('app.setting.pagestyle.dark') + } + ] + + const darkColorList = [ + { + key: '#1890ff', + color: '#1890ff', + theme: 'dark' + } + ] + + const lightColorList = [ + { + key: '#1890ff', + color: '#1890ff', + theme: 'dark' + } + ] + + if (list.find((item) => item.theme === 'dark')) { + // themeList.push({ + // // disable click + // // disable: true, + // key: 'realDark', + // url: 'https://gw.alipayobjects.com/zos/antfincdn/hmKaLQvmY2/LCkqqYNmvBEbokSDscrm.svg', + // title: i18nRender('app.setting.pagestyle.realdark') + // }) + } + // insert theme color List + list.forEach(item => { + const color = (item.modifyVars || {})['@primary-color'] + if (item.theme === 'dark' && color) { + darkColorList.push({ + color, + ...item + }) + } + if (!item.theme || item.theme === 'light') { + lightColorList.push({ + color, + ...item + }) + } + }) + + return { + colorList: { + dark: darkColorList, + light: lightColorList + }, + themeList + } +} + +const handleChangeSetting = (key, value, hideMessageLoading) => { + if (key === 'primaryColor') { + // 更新主色调 + updateTheme(value) + } + if (key === 'colorWeak') { + updateColorWeak(value) + } +} + +const genCopySettingJson = (settings) => + JSON.stringify( + omit( + { + ...settings, + primaryColor: genStringToTheme(settings.primaryColor) + }, + ['colorWeak'] + ), + null, + 2 + ) + +export const settings = { + theme: PropTypes.oneOf(['dark', 'light']), + primaryColor: PropTypes.string, + layout: PropTypes.oneOf(['sidemenu', 'topmenu']), + colorWeak: PropTypes.bool, + contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'), + // 替换兼容 PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid') + // contentWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).def('Fluid'), + fixedHeader: PropTypes.bool, + fixSiderbar: PropTypes.bool, + multiTab: PropTypes.bool, + hideHintAlert: PropTypes.bool.def(false), + hideCopyButton: PropTypes.bool.def(false) +} + +export const SettingDrawerProps = { + getContainer: PropTypes.func, + settings: PropTypes.objectOf(settings), + + i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false) +} + +const SettingDrawer = { + name: 'SettingDrawer', + props: SettingDrawerProps, + inject: ['locale'], + data () { + return { + show: false + } + }, + render (h) { + const { + setShow, + getContainer, + settings + } = this + + const { + theme = 'dark', + primaryColor = 'daybreak', + layout = 'sidemenu', + fixedHeader = false, + fixSiderbar = false, + multiTab = true, + contentWidth, + hideHintAlert, + hideCopyButton, + colorWeak + } = settings + + const i18n = this.$props.i18nRender || this.locale || defaultI18nRender + const themeList = getThemeList(i18n) + const isTopMenu = layout === 'topmenu' + + const iconStyle = { + color: '#fff', + fontSize: 20 + } + + const changeSetting = (type, value) => { + this.$emit('change', { type, value }) + handleChangeSetting(type, value, false) + } + + return ( + setShow(false)} + placement="right" + getContainer={getContainer} + /* handle={ +
    setShow(!this.show)}> + {this.show + ? () + : () + } +
    + } */ + style={{ + zIndex: 999 + }} + > + +
    + + { + changeSetting('theme', val) + }} /> + + + { + changeSetting('primaryColor', color, null) + }} + /> + + + + + { + changeSetting('layout', value, null) + }} /> + + + { + changeSetting(type, value) + }} + /> + + + + renderLayoutSettingItem(h, item)} + dataSource={[ + { + title: i18n('app.setting.weakmode'), + action: ( + changeSetting('colorWeak', checked)} + /> + ) + } + ]} + /> + + + {hideHintAlert && hideCopyButton ? null : } + + {hideHintAlert ? null : ( + )} + showIcon + style={{ marginBottom: '16px' }} + /> + )} + + {hideCopyButton ? null : ( + + message.success(i18n('app.setting.copyinfo')) + } + > + + + )} + + +
    +
    + ) + }, + methods: { + setShow (flag) { + this.show = flag + } + } +} + +SettingDrawer.install = function (Vue) { + Vue.use(antPortal) + Vue.component(SettingDrawer.name, SettingDrawer) +} + +export default SettingDrawer diff --git a/src/components/ProLayout/components/SettingDrawer/index.less b/src/components/ProLayout/components/SettingDrawer/index.less new file mode 100644 index 0000000..cc1d38e --- /dev/null +++ b/src/components/ProLayout/components/SettingDrawer/index.less @@ -0,0 +1,90 @@ +@import '~ant-design-vue/es/style/themes/default.less'; + +@ant-pro-setting-drawer: ~'@{ant-prefix}-pro-setting-drawer'; + +.@{ant-pro-setting-drawer} { + &-content { + position: relative; + min-height: 100%; + + .ant-list-item { + span { + flex: 1; + } + } + } + + &-block-checbox { + display: flex; + + &-item { + position: relative; + margin-right: 16px; + // box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1); + border-radius: @border-radius-base; + cursor: pointer; + + img { + width: 48px; + } + } + + &-selectIcon { + position: absolute; + top: 0; + right: 0; + width: 100%; + height: 100%; + padding-top: 15px; + padding-left: 24px; + color: @primary-color; + font-weight: bold; + font-size: 14px; + + .action { + color: @primary-color; + } + } + } + + &-color_block { + display: inline-block; + width: 38px; + height: 22px; + margin: 4px; + margin-right: 12px; + vertical-align: middle; + border-radius: 4px; + cursor: pointer; + } + + &-title { + margin-bottom: 12px; + color: @heading-color; + font-size: 14px; + line-height: 22px; + } + + &-handle { + position: absolute; + top: 240px; + right: 300px; + z-index: 0; + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + font-size: 16px; + text-align: center; + background: @primary-color; + border-radius: 4px 0 0 4px; + cursor: pointer; + pointer-events: auto; + } + + &-production-hint { + margin-top: 16px; + font-size: 12px; + } +} diff --git a/src/components/ProLayout/components/SiderMenu/SiderMenu.jsx b/src/components/ProLayout/components/SiderMenu/SiderMenu.jsx new file mode 100644 index 0000000..2274a67 --- /dev/null +++ b/src/components/ProLayout/components/SiderMenu/SiderMenu.jsx @@ -0,0 +1,133 @@ +import './index.less' + +import PropTypes from 'ant-design-vue/es/_util/vue-types' +import 'ant-design-vue/es/layout/style' +import Layout from 'ant-design-vue/es/layout' +import { isFun } from '../../utils/util' +import BaseMenu from '../RouteMenu' + +const { Sider } = Layout + +export const SiderMenuProps = { + i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false), + mode: PropTypes.string.def('inline'), + theme: PropTypes.string.def('dark'), + contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'), + collapsible: PropTypes.bool, + collapsed: PropTypes.bool, + handleCollapse: PropTypes.func, + menus: PropTypes.array, + siderWidth: PropTypes.number.def(256), + isMobile: PropTypes.bool, + layout: PropTypes.string.def('inline'), + fixSiderbar: PropTypes.bool, + logo: PropTypes.any, + title: PropTypes.string.def(''), + // render function or vnode + menuHeaderRender: PropTypes.oneOfType([PropTypes.func, PropTypes.array, PropTypes.object, PropTypes.bool]), + menuRender: PropTypes.oneOfType([PropTypes.func, PropTypes.array, PropTypes.object, PropTypes.bool]) +} + +export const defaultRenderLogo = (h, logo) => { + if (typeof logo === 'string') { + return logo + } + if (typeof logo === 'function') { + return logo() + } + return h(logo) +} + +export const defaultRenderLogoAntTitle = (h, props) => { + const { + logo = 'https://gw.alipayobjects.com/zos/antfincdn/PmY%24TNNDBI/logo.svg', + title, + menuHeaderRender + } = props + + if (menuHeaderRender === false) { + return null + } + const logoDom = defaultRenderLogo(h, logo) + const titleDom =

    {title}

    + + if (menuHeaderRender) { + return isFun(menuHeaderRender) && + menuHeaderRender(h, logoDom, props.collapsed ? null : titleDom, props) || + menuHeaderRender + } + return ( + + {logoDom} + {titleDom} + + ) +} + +const SiderMenu = { + name: 'SiderMenu', + model: { + prop: 'collapsed', + event: 'collapse' + }, + props: SiderMenuProps, + render (h) { + const { + collapsible, + collapsed, + siderWidth, + fixSiderbar, + mode, + theme, + menus, + logo, + title, + onMenuHeaderClick = () => null, + i18nRender, + menuHeaderRender, + menuRender + } = this + const siderCls = ['ant-pro-sider-menu-sider'] + if (fixSiderbar) siderCls.push('fix-sider-bar') + if (theme === 'light') siderCls.push('light') + // + // const handleCollapse = (collapsed, type) => { + // this.$emit('collapse', collapsed) + // } + + const headerDom = defaultRenderLogoAntTitle(h, { + logo, title, menuHeaderRender, collapsed + }) + + return ( + {headerDom && ( + + )} + {menuRender && ( + isFun(menuRender) && + menuRender(h, this.$props) || + menuRender + ) || ( + + )} + ) + } +} + +export default SiderMenu diff --git a/src/components/ProLayout/components/SiderMenu/index.jsx b/src/components/ProLayout/components/SiderMenu/index.jsx new file mode 100644 index 0000000..ccf2c9d --- /dev/null +++ b/src/components/ProLayout/components/SiderMenu/index.jsx @@ -0,0 +1,54 @@ +import './index.less' + +import 'ant-design-vue/es/drawer/style' +import Drawer from 'ant-design-vue/es/drawer' +import SiderMenu, { SiderMenuProps } from './SiderMenu' + +const SiderMenuWrapper = { + name: 'SiderMenuWrapper', + model: { + prop: 'collapsed', + event: 'collapse' + }, + props: SiderMenuProps, + render (h) { + const { + layout, + isMobile, + collapsed + } = this + const isTopMenu = layout === 'topmenu' + const handleCollapse = (e) => { + this.$emit('collapse', true) + } + return isMobile ? ( + + + + ) : !isTopMenu && ( + + ) + } +} + +SiderMenuWrapper.install = function (Vue) { + Vue.component(SiderMenuWrapper.name, SiderMenuWrapper) +} + +export { + SiderMenu, + SiderMenuProps +} + +export default SiderMenuWrapper diff --git a/src/components/ProLayout/components/SiderMenu/index.less b/src/components/ProLayout/components/SiderMenu/index.less new file mode 100644 index 0000000..2276d28 --- /dev/null +++ b/src/components/ProLayout/components/SiderMenu/index.less @@ -0,0 +1,125 @@ +@import "~ant-design-vue/es/style/themes/default.less"; + +@sider-menu-prefix-cls: ~'@{ant-prefix}-pro-sider-menu'; + +@nav-header-height: @layout-header-height; +.@{sider-menu-prefix-cls} { + + &-logo { + position: relative; + height: 64px; + padding-left: 24px; + overflow: hidden; + transition: all .3s; + line-height: @nav-header-height; + background: @layout-sider-background; + + svg, img, h1 { + display: inline-block; + } + + svg, img { + height: 32px; + width: 32px; + vertical-align: middle; + } + + h1 { + color: @white; + font-size: 20px; + margin: 0 0 0 12px; + font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; + font-weight: 600; + vertical-align: middle; + } + } + + &-sider { + position: relative; + z-index: 10; + min-height: 100vh; + box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35); + + &.fix-sider-bar { + position: fixed; + top: 0; + left: 0; + box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05); + + .ant-menu-root { + height: ~'calc(100vh - @{nav-header-height})'; + overflow-y: auto; + } + + .ant-menu-inline { + border-right: 0; + + .ant-menu-item, + .ant-menu-submenu-title { + width: 100%; + } + } + } + + &.light { + background-color: white; + box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05); + + .@{sider-menu-prefix-cls}-logo { + background: white; + box-shadow: 1px 1px 0 0 @border-color-split; + + h1 { + color: @primary-color; + } + } + + .ant-menu-light { + border-right-color: transparent; + } + } + } + + &-icon { + width: 14px; + vertical-align: baseline; + } + + .top-nav-menu li.ant-menu-item { + height: @nav-header-height; + line-height: @nav-header-height; + } + + .drawer .drawer-content { + background: @layout-sider-background; + } + + .ant-menu-inline-collapsed { + & > .ant-menu-item .sider-menu-item-img + span, + & + > .ant-menu-item-group + > .ant-menu-item-group-list + > .ant-menu-item + .sider-menu-item-img + + span, + & + > .ant-menu-submenu + > .ant-menu-submenu-title + .sider-menu-item-img + + span { + display: inline-block; + max-width: 0; + opacity: 0; + } + } + + .ant-menu-item .sider-menu-item-img + span, + .ant-menu-submenu-title .sider-menu-item-img + span { + opacity: 1; + transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out; + } + + .ant-drawer-body { + padding: 0; + } +} diff --git a/src/components/ProLayout/components/index.js b/src/components/ProLayout/components/index.js new file mode 100644 index 0000000..bf8f679 --- /dev/null +++ b/src/components/ProLayout/components/index.js @@ -0,0 +1,15 @@ +import RouteMenu from './RouteMenu' +import SiderMenuWrapper, { SiderMenu, SiderMenuProps } from './SiderMenu' +import PageHeaderWrapper from './PageHeaderWrapper' +import GlobalFooter from './GlobalFooter' +import VueFragment from './Fragment' + +export { + RouteMenu, + SiderMenu, + SiderMenuProps, + SiderMenuWrapper, + PageHeaderWrapper, + GlobalFooter, + VueFragment +} diff --git a/src/components/ProLayout/index.js b/src/components/ProLayout/index.js new file mode 100644 index 0000000..1588228 --- /dev/null +++ b/src/components/ProLayout/index.js @@ -0,0 +1,10 @@ +export { default, BasicLayoutProps } from './BasicLayout' +export { default as BlockLayout } from './BlockLayout' +export { default as PageHeaderWrapper } from './components/PageHeaderWrapper' +export { default as SiderMenuWrapper } from './components/SiderMenu' +export { default as GlobalFooter } from './components/GlobalFooter' +export { default as SettingDrawer } from './components/SettingDrawer' +export { default as DocumentTitle } from './components/DocumentTitle' +export { default as BaseMenu } from './components/RouteMenu' +// func +export { updateTheme, updateColorWeak } from './utils/dynamicTheme' diff --git a/src/components/ProLayout/utils/dynamicTheme.js b/src/components/ProLayout/utils/dynamicTheme.js new file mode 100644 index 0000000..e30f1b0 --- /dev/null +++ b/src/components/ProLayout/utils/dynamicTheme.js @@ -0,0 +1,38 @@ +import client from 'webpack-theme-color-replacer/client' +import generate from '@ant-design/colors/lib/generate' +import { message } from 'ant-design-vue' + +export const themeColor = { + 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) { + const 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) + } +} + +export const updateTheme = newPrimaryColor => { + const hideMessage = message.loading('正在切换主题', 0) + themeColor.changeColor(newPrimaryColor).then(r => { + hideMessage() + }) +} + +export const updateColorWeak = colorWeak => { + // document.body.className = colorWeak ? 'colorWeak' : ''; + const app = document.body.querySelector('#app') + colorWeak ? app.classList.add('colorWeak') : app.classList.remove('colorWeak') +} diff --git a/src/components/ProLayout/utils/request.js b/src/components/ProLayout/utils/request.js new file mode 100644 index 0000000..ff4e3c8 --- /dev/null +++ b/src/components/ProLayout/utils/request.js @@ -0,0 +1,62 @@ +import request, { extend } from 'umi-request' +import { notification } from 'ant-design-vue' + +const codeMessage = { + 200: '服务器成功返回请求的数据。', + 201: '新建或修改数据成功。', + 202: '一个请求已经进入后台排队(异步任务)。', + 204: '删除数据成功。', + 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', + 401: '用户没有权限(令牌、用户名、密码错误)。', + 403: '用户得到授权,但是访问是被禁止的。', + 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', + 406: '请求的格式不可得。', + 410: '请求的资源被永久删除,且不会再得到的。', + 422: '当创建一个对象时,发生一个验证错误。', + 500: '服务器发生错误,请检查服务器。', + 502: '网关错误。', + 503: '服务不可用,服务器暂时过载或维护。', + 504: '网关超时。' +} + +const errorHandler = (error) => { + const { response = {} } = error + const errortext = codeMessage[response.status] || response.statusText + const { status, url } = response + + notification.error({ + message: `请求错误 ${status}: ${url}`, + description: errortext + }) +} + +export const BASE_URL = process.env.VUE_APP_API_URL || '/api/v1' + +const customRequest = extend({ + prefix: BASE_URL, + timeout: 1000, + errorHandler +}) + +// request 拦截器 +customRequest.interceptors.request.use((url, options) => { + return ( + { + url: `${url}&interceptors=yes`, + options: { ...options, interceptors: true } + } + ) +}) + +// response 拦截器 +customRequest.interceptors.response.use((response, options) => { + response.headers.append('interceptors', 'yes yo') + return response +}) + +export { + request, + extend +} + +export default customRequest diff --git a/src/components/ProLayout/utils/util.js b/src/components/ProLayout/utils/util.js new file mode 100644 index 0000000..88d3b38 --- /dev/null +++ b/src/components/ProLayout/utils/util.js @@ -0,0 +1,64 @@ +import triggerEvent from 'ant-design-vue/es/_util/triggerEvent' +import { inBrowser } from 'ant-design-vue/es/_util/env' + +const getComponentFromProp = (instance, prop) => { + const slots = instance.slots && instance.slots() + return slots[prop] || instance.props[prop] +} + +const isFun = (func) => { + return typeof func === 'function' +} + +// 兼容 0.3.4~0.3.8 +export const contentWidthCheck = (contentWidth) => { + return Object.prototype.toString.call(contentWidth) === '[object Boolean]' + ? contentWidth === true && 'Fixed' || 'Fluid' + : contentWidth +} + +export const layoutContentWidth = (contentType) => { + return contentType !== 'Fluid' +} + +const themeConfig = { + daybreak: 'daybreak', + '#1890ff': 'daybreak', + '#F5222D': 'dust', + '#FA541C': 'volcano', + '#FAAD14': 'sunset', + '#13C2C2': 'cyan', + '#52C41A': 'green', + '#2F54EB': 'geekblue', + '#722ED1': 'purple' +} + +const invertKeyValues = (obj) => + Object.keys(obj).reduce((acc, key) => { + acc[obj[key]] = key + return acc + }, {}) + +/** + * #1890ff -> daybreak + * @param val + */ +export function genThemeToString (val) { + return val && themeConfig[val] ? themeConfig[val] : val +} + +/** + * daybreak-> #1890ff + * @param val + */ +export function genStringToTheme (val) { + const stringConfig = invertKeyValues(themeConfig) + return val && stringConfig[val] ? stringConfig[val] : val +} + +export { + triggerEvent, + inBrowser, + getComponentFromProp, + isFun +} diff --git a/src/components/Screenfull/index.vue b/src/components/Screenfull/index.vue new file mode 100644 index 0000000..1ca13be --- /dev/null +++ b/src/components/Screenfull/index.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/Search/GlobalSearch.jsx b/src/components/Search/GlobalSearch.jsx new file mode 100644 index 0000000..bd9e604 --- /dev/null +++ b/src/components/Search/GlobalSearch.jsx @@ -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 ( + + ) + } +} + +GlobalSearch.install = function (Vue) { + Vue.component(GlobalSearch.name, GlobalSearch) +} + +export default GlobalSearch diff --git a/src/components/Search/index.less b/src/components/Search/index.less new file mode 100644 index 0000000..d397852 --- /dev/null +++ b/src/components/Search/index.less @@ -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; + } + } +} \ No newline at end of file diff --git a/src/components/SelectLang/index.jsx b/src/components/SelectLang/index.jsx new file mode 100644 index 0000000..85a342a --- /dev/null +++ b/src/components/SelectLang/index.jsx @@ -0,0 +1,54 @@ +import './index.less' + +import { Icon, Menu, Dropdown } from 'ant-design-vue' +import { i18nRender } from '@/locales' +import i18nMixin from '@/store/i18n-mixin' + +const locales = ['zh-CN', 'en-US'] +const languageLabels = { + 'zh-CN': '简体中文', + 'en-US': 'English(暂不支持)' +} +// eslint-disable-next-line +const languageIcons = { + 'zh-CN': '🇨🇳', + 'en-US': '🇺🇸' +} + +const SelectLang = { + props: { + prefixCls: { + type: String, + default: 'ant-pro-drop-down' + } + }, + name: 'SelectLang', + mixins: [i18nMixin], + render () { + const { prefixCls } = this + const changeLang = ({ key }) => { + this.setLang(key) + } + const langMenu = ( + + {locales.map(locale => ( + + + {languageIcons[locale]} + {' '} + {languageLabels[locale]} + + ))} + + ) + return ( + + + + + + ) + } +} + +export default SelectLang diff --git a/src/components/SelectLang/index.less b/src/components/SelectLang/index.less new file mode 100644 index 0000000..750c9c7 --- /dev/null +++ b/src/components/SelectLang/index.less @@ -0,0 +1,31 @@ +@import "~ant-design-vue/es/style/themes/default"; + +@header-menu-prefix-cls: ~'@{ant-prefix}-pro-header-menu'; +@header-drop-down-prefix-cls: ~'@{ant-prefix}-pro-drop-down'; + +.@{header-menu-prefix-cls} { + + .anticon { + margin-right: 8px; + } + .ant-dropdown-menu-item { + min-width: 160px; + } +} + +.@{header-drop-down-prefix-cls} { + + line-height: @layout-header-height; + vertical-align: top; + cursor: pointer; + + > i { + font-size: 16px !important; + transform: none !important; + + svg { + position: relative; + top: -1px; + } + } +} diff --git a/src/components/SettingDrawer/SettingDrawer.vue b/src/components/SettingDrawer/SettingDrawer.vue new file mode 100644 index 0000000..f041ae4 --- /dev/null +++ b/src/components/SettingDrawer/SettingDrawer.vue @@ -0,0 +1,343 @@ + + + + + diff --git a/src/components/SettingDrawer/SettingItem.vue b/src/components/SettingDrawer/SettingItem.vue new file mode 100644 index 0000000..2b3b553 --- /dev/null +++ b/src/components/SettingDrawer/SettingItem.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/components/SettingDrawer/index.js b/src/components/SettingDrawer/index.js new file mode 100644 index 0000000..8260f2d --- /dev/null +++ b/src/components/SettingDrawer/index.js @@ -0,0 +1,2 @@ +import SettingDrawer from './SettingDrawer' +export default SettingDrawer diff --git a/src/components/SettingDrawer/settingConfig.js b/src/components/SettingDrawer/settingConfig.js new file mode 100644 index 0000000..2cfab79 --- /dev/null +++ b/src/components/SettingDrawer/settingConfig.js @@ -0,0 +1,48 @@ +import message from 'ant-design-vue/es/message' +// import defaultSettings from '../defaultSettings'; +import themeColor from './themeColor.js' + +// let lessNodesAppended +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(() => { + 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 } diff --git a/src/components/SettingDrawer/themeColor.js b/src/components/SettingDrawer/themeColor.js new file mode 100644 index 0000000..10dfbd4 --- /dev/null +++ b/src/components/SettingDrawer/themeColor.js @@ -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) + } +} diff --git a/src/components/StandardFormRow/StandardFormRow.vue b/src/components/StandardFormRow/StandardFormRow.vue new file mode 100644 index 0000000..a4e261b --- /dev/null +++ b/src/components/StandardFormRow/StandardFormRow.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/src/components/StandardFormRow/index.js b/src/components/StandardFormRow/index.js new file mode 100644 index 0000000..8155cc7 --- /dev/null +++ b/src/components/StandardFormRow/index.js @@ -0,0 +1,3 @@ +import StandardFormRow from './StandardFormRow' + +export default StandardFormRow diff --git a/src/components/TagSelect/TagSelectOption.jsx b/src/components/TagSelect/TagSelectOption.jsx new file mode 100644 index 0000000..b5ae799 --- /dev/null +++ b/src/components/TagSelect/TagSelectOption.jsx @@ -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 ( + {$slots.default} + ) + } +} diff --git a/src/components/TagSelect/index.jsx b/src/components/TagSelect/index.jsx new file mode 100644 index 0000000..af98ad7 --- /dev/null +++ b/src/components/TagSelect/index.jsx @@ -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 = + 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 ( +
    + {this.renderCheckAll()} + {this.renderTags(tagItems)} +
    + ) + } +} diff --git a/src/components/TextArea/index.jsx b/src/components/TextArea/index.jsx new file mode 100644 index 0000000..00aeb2f --- /dev/null +++ b/src/components/TextArea/index.jsx @@ -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 ( +
    + + {this.currentLimit}/{this.limit} +
    + ) + } +} diff --git a/src/components/TextArea/style.less b/src/components/TextArea/style.less new file mode 100644 index 0000000..6dee494 --- /dev/null +++ b/src/components/TextArea/style.less @@ -0,0 +1,12 @@ +.ant-textarea-limit { + position: relative; + + .limit { + position: absolute; + color: #909399; + background: #fff; + font-size: 12px; + bottom: 5px; + right: 10px; + } +} \ No newline at end of file diff --git a/src/components/Tree/Tree.jsx b/src/components/Tree/Tree.jsx new file mode 100644 index 0000000..e5a2a11 --- /dev/null +++ b/src/components/Tree/Tree.jsx @@ -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 ( + + ) + }, + renderIcon (icon) { + return icon && () || null + }, + renderMenuItem (item) { + return ( + + { this.renderIcon(item.icon) } + { item.title } + this.handlePlus(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 ( + + + { childrenItems } + + ) + }, + renderSubItem (item, key) { + const childrenItems = item.children && item.children.map(o => { + return this.renderItem(o, o.key) + }) + + const title = ( + + { this.renderIcon(item.icon) } + { item.title } + + ) + + if (item.group) { + return this.renderItemGroup(item) + } + // titleClick={this.handleTitleClick(item)} + return ( + + { title } + { childrenItems } + + ) + } + }, + render () { + const { dataSource, search } = this.$props + + // this.localOpenKeys = openKeys.slice(0) + const list = dataSource.map(item => { + return this.renderItem(item) + }) + + return ( +
    + { search ? this.renderSearch() : null } + this.$emit('click', item), 'update:openKeys': val => { this.localOpenKeys = val } } }} openKeys={this.localOpenKeys}> + { list } + +
    + ) + } +} diff --git a/src/components/Trend/Trend.vue b/src/components/Trend/Trend.vue new file mode 100644 index 0000000..526e1cc --- /dev/null +++ b/src/components/Trend/Trend.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/components/Trend/index.js b/src/components/Trend/index.js new file mode 100644 index 0000000..9f14228 --- /dev/null +++ b/src/components/Trend/index.js @@ -0,0 +1,3 @@ +import Trend from './Trend.vue' + +export default Trend diff --git a/src/components/Trend/index.less b/src/components/Trend/index.less new file mode 100644 index 0000000..8a3d24c --- /dev/null +++ b/src/components/Trend/index.less @@ -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; + } +} \ No newline at end of file diff --git a/src/components/Trend/index.md b/src/components/Trend/index.md new file mode 100644 index 0000000..8881f0e --- /dev/null +++ b/src/components/Trend/index.md @@ -0,0 +1,45 @@ +# Trend 趋势标记 + +趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。 + + + +引用方式: + +```javascript +import Trend from '@/components/Trend' + +export default { + components: { + Trend + } +} +``` + + + +## 代码演示 [demo](https://pro.loacg.com/test/home) + +```html +5% +``` +或 +```html + + 工资 + 5% + +``` +或 +```html +5% +``` + + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| flag | 上升下降标识:`up|down` | string | - | +| reverseColor | 颜色反转 | Boolean | false | + diff --git a/src/components/_util/util.js b/src/components/_util/util.js new file mode 100644 index 0000000..dd33231 --- /dev/null +++ b/src/components/_util/util.js @@ -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 + }, '') +} diff --git a/src/components/index.js b/src/components/index.js new file mode 100644 index 0000000..62226ea --- /dev/null +++ b/src/components/index.js @@ -0,0 +1,37 @@ +// pro components +import AvatarList from '@/components/AvatarList' +import Ellipsis from '@/components/Ellipsis' +import FooterToolbar from '@/components/FooterToolbar' +import NumberInfo from '@/components/NumberInfo' +import Tree from '@/components/Tree/Tree' +import Trend from '@/components/Trend' +import MultiTab from '@/components/MultiTab' +import IconSelector from '@/components/IconSelector' +import TagSelect from '@/components/TagSelect' +import StandardFormRow from '@/components/StandardFormRow' +import ArticleListContent from '@/components/ArticleListContent' + +import Dialog from '@/components/Dialog' + +import SettingDrawer from '@/components/SettingDrawer' + +import ProLayout from '@/components/ProLayout' + +export { + AvatarList, + Trend, + Ellipsis, + FooterToolbar, + NumberInfo, + Tree, + MultiTab, + IconSelector, + TagSelect, + StandardFormRow, + ArticleListContent, + + Dialog, + SettingDrawer, + + ProLayout +} diff --git a/src/components/index.less b/src/components/index.less new file mode 100644 index 0000000..e831c41 --- /dev/null +++ b/src/components/index.less @@ -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; \ No newline at end of file diff --git a/src/config/defaultSettings.js b/src/config/defaultSettings.js new file mode 100644 index 0000000..02e61b0 --- /dev/null +++ b/src/config/defaultSettings.js @@ -0,0 +1,32 @@ +/** + * 项目默认配置项 + * primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage + * navTheme - sidebar theme ['dark', 'light'] 两种主题 + * colorWeak - 色盲模式 + * layout - 整体布局方式 ['sidemenu', 'topmenu'] 两种布局 + * fixedHeader - 固定 Header : boolean + * fixSiderbar - 固定左侧菜单栏 : boolean + * contentWidth - 内容区布局: 流式 | 固定 + * + * storageOptions: {} - Vue-ls 插件配置项 (localStorage/sessionStorage) + * + */ + +export default { + navTheme: 'dark', // theme for nav menu + primaryColor: '#1890ff', // primary color of ant design + layout: 'sidemenu', // nav menu position: `sidemenu` or `topmenu` + contentWidth: 'Fluid', // layout of content: `Fluid` or `Fixed`, only works when layout is topmenu + fixedHeader: true, // sticky header + fixSiderbar: true, // sticky siderbar + colorWeak: false, + multiTab: true, + menu: { + locale: true + }, + title: '算力云后台管理系统', + pwa: false, + iconfontUrl: '', + production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true', + tableSize: 'middle' +} diff --git a/src/config/router.config.js b/src/config/router.config.js new file mode 100644 index 0000000..34390e6 --- /dev/null +++ b/src/config/router.config.js @@ -0,0 +1,81 @@ +import { UserLayout } from '@/layouts' + +/** + * 首页 + */ +export const indexRouterMap = [ + { + path: '/index', + name: 'index', + component: 'Index', + meta: { title: '控制台', keepAlive: true, icon: 'dashboard' } + }, + { + path: '/account/center', + name: 'center', + component: 'AccountCenter', + meta: { title: '个人中心', keepAlive: true }, + hidden: true + }, + { + path: '/account/settings', + name: 'settings', + component: 'AccountSettings', + meta: { title: '个人设置', hideHeader: true }, + redirect: '/account/settings/base', + hidden: true, + children: [ + { + path: '/account/settings/base', + name: 'BaseSettings', + component: 'BaseSettings', + hidden: true, + meta: { title: '基本设置', hidden: true } + }, + { + path: '/account/settings/security', + name: 'SecuritySettings', + component: 'SecuritySettings', + meta: { title: '安全设置', hidden: true, keepAlive: true } + } + ] + }, + { + path: '/system/notice/form', + name: 'NoticeForm', + component: 'NoticeForm', + meta: { title: '公告编辑', keepAlive: true }, + hidden: true + }, + { + path: '/gen/edit', + name: 'GenEdit', + component: 'GenEdit', + meta: { title: '修改生成配置', keepAlive: true }, + 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/user/Login') + } + ] + }, + { + path: '/404', + component: () => import(/* webpackChunkName: "fail" */ '@/views/exception/404') + } + +] diff --git a/src/core/bootstrap.js b/src/core/bootstrap.js new file mode 100644 index 0000000..ff35244 --- /dev/null +++ b/src/core/bootstrap.js @@ -0,0 +1,31 @@ +import store from '@/store' +import storage from 'store' +import { + ACCESS_TOKEN, + APP_LANGUAGE, + TOGGLE_CONTENT_WIDTH, + TOGGLE_FIXED_HEADER, + TOGGLE_FIXED_SIDEBAR, TOGGLE_HIDE_HEADER, + TOGGLE_LAYOUT, TOGGLE_NAV_THEME, TOGGLE_WEAK, + TOGGLE_COLOR, TOGGLE_MULTI_TAB +} from '@/store/mutation-types' +import { printANSI } from '@/utils/screenLog' +import defaultSettings from '@/config/defaultSettings' + +export default function Initializer () { + printANSI() // 请自行移除该行. please remove this line + + store.commit(TOGGLE_LAYOUT, storage.get(TOGGLE_LAYOUT, defaultSettings.layout)) + store.commit(TOGGLE_FIXED_HEADER, storage.get(TOGGLE_FIXED_HEADER, defaultSettings.fixedHeader)) + store.commit(TOGGLE_FIXED_SIDEBAR, storage.get(TOGGLE_FIXED_SIDEBAR, defaultSettings.fixSiderbar)) + store.commit(TOGGLE_CONTENT_WIDTH, storage.get(TOGGLE_CONTENT_WIDTH, defaultSettings.contentWidth)) + store.commit(TOGGLE_HIDE_HEADER, storage.get(TOGGLE_HIDE_HEADER, defaultSettings.autoHideHeader)) + store.commit(TOGGLE_NAV_THEME, storage.get(TOGGLE_NAV_THEME, defaultSettings.navTheme)) + store.commit(TOGGLE_WEAK, storage.get(TOGGLE_WEAK, defaultSettings.colorWeak)) + store.commit(TOGGLE_COLOR, storage.get(TOGGLE_COLOR, defaultSettings.primaryColor)) + store.commit(TOGGLE_MULTI_TAB, storage.get(TOGGLE_MULTI_TAB, defaultSettings.multiTab)) + store.commit('SET_TOKEN', storage.get(ACCESS_TOKEN)) + + store.dispatch('setLang', storage.get(APP_LANGUAGE, 'zh-CN')) + // last step +} diff --git a/src/core/directives/action.js b/src/core/directives/action.js new file mode 100644 index 0000000..18b7584 --- /dev/null +++ b/src/core/directives/action.js @@ -0,0 +1,34 @@ +import Vue from 'vue' +import store from '@/store' + +/** + * Action 权限指令 + * 指令用法: + * - 在需要控制 action 级别权限的组件上使用 v-action:[method] , 如下: + * 添加用户 + * 删除用户 + * 修改 + * + * - 当前用户没有权限时,组件上使用了该指令则会被隐藏 + * - 当后台权限跟 pro 提供的模式不同时,只需要针对这里的权限过滤进行修改即可 + * + * @see https://github.com/vueComponent/ant-design-vue-pro/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 diff --git a/src/core/icons.js b/src/core/icons.js new file mode 100644 index 0000000..99cda5b --- /dev/null +++ b/src/core/icons.js @@ -0,0 +1,124 @@ +/** + * Custom icon list + * All icons are loaded here for easy management + * @see https://vue.ant.design/components/icon/#Custom-Font-Icon + * + * 自定义图标加载表 + * 所有图标均从这里加载,方便管理 + */ + +import bugIcon from '@/assets/icons/bug.svg?inline' +import buildIcon from '@/assets/icons/build.svg?inline' +import buttonIcon from '@/assets/icons/button.svg?inline' +import bxAnalyseIcon from '@/assets/icons/bxAnalyse.svg?inline' +import cascaderIcon from '@/assets/icons/cascader.svg?inline' +import chartIcon from '@/assets/icons/chart.svg?inline' +import checkboxIcon from '@/assets/icons/checkbox.svg?inline' +import clipboardIcon from '@/assets/icons/clipboard.svg?inline' +import codeIcon from '@/assets/icons/code.svg?inline' +import colorIcon from '@/assets/icons/color.svg?inline' +import componentIcon from '@/assets/icons/component.svg?inline' +import dashboardIcon from '@/assets/icons/dashboard.svg?inline' +import dateRangeIcon from '@/assets/icons/dateRange.svg?inline' +import dateIcon from '@/assets/icons/date.svg?inline' +import dictIcon from '@/assets/icons/dict.svg?inline' +import documentationIcon from '@/assets/icons/documentation.svg?inline' +import downloadIcon from '@/assets/icons/download.svg?inline' +import dragIcon from '@/assets/icons/drag.svg?inline' +import druidIcon from '@/assets/icons/druid.svg?inline' +import editIcon from '@/assets/icons/edit.svg?inline' +import eyeOpenIcon from '@/assets/icons/eyeOpen.svg?inline' +import githubIcon from '@/assets/icons/github.svg?inline' +import guideIcon from '@/assets/icons/guide.svg?inline' +import inputIcon from '@/assets/icons/input.svg?inline' +import jobIcon from '@/assets/icons/job.svg?inline' +import logIcon from '@/assets/icons/log.svg?inline' +import logininforIcon from '@/assets/icons/logininfor.svg?inline' +import messageIcon from '@/assets/icons/message.svg?inline' +import monitorIcon from '@/assets/icons/monitor.svg?inline' +import numberIcon from '@/assets/icons/number.svg?inline' +import onlineIcon from '@/assets/icons/online.svg?inline' +import passwordIcon from '@/assets/icons/password.svg?inline' +import pdfIcon from '@/assets/icons/pdf.svg?inline' +import peoplesIcon from '@/assets/icons/peoples.svg?inline' +import phoneIcon from '@/assets/icons/phone.svg?inline' +import postIcon from '@/assets/icons/post.svg?inline' +import questionIcon from '@/assets/icons/question.svg?inline' +import radioIcon from '@/assets/icons/radio.svg?inline' +import rateIcon from '@/assets/icons/rate.svg?inline' +import redisIcon from '@/assets/icons/redis.svg?inline' +import rowIcon from '@/assets/icons/row.svg?inline' +import selectIcon from '@/assets/icons/select.svg?inline' +import serverIcon from '@/assets/icons/server.svg?inline' +import swaggerIcon from '@/assets/icons/swagger.svg?inline' +import switchIcon from '@/assets/icons/switch.svg?inline' +import systemIcon from '@/assets/icons/system.svg?inline' +import textareaIcon from '@/assets/icons/textarea.svg?inline' +import timeRangeIcon from '@/assets/icons/timeRange.svg?inline' +import timeIcon from '@/assets/icons/time.svg?inline' +import toolIcon from '@/assets/icons/tool.svg?inline' +import treeTableIcon from '@/assets/icons/treeTable.svg?inline' +import treeIcon from '@/assets/icons/tree.svg?inline' +import uploadIcon from '@/assets/icons/upload.svg?inline' +import userIcon from '@/assets/icons/user.svg?inline' +import validCodeIcon from '@/assets/icons/validCode.svg?inline' + +const allIcon = { + bugIcon, + buildIcon, + buttonIcon, + bxAnalyseIcon, + cascaderIcon, + chartIcon, + checkboxIcon, + clipboardIcon, + codeIcon, + colorIcon, + componentIcon, + dashboardIcon, + dateRangeIcon, + dateIcon, + dictIcon, + documentationIcon, + downloadIcon, + dragIcon, + druidIcon, + editIcon, + eyeOpenIcon, + githubIcon, + guideIcon, + inputIcon, + jobIcon, + logIcon, + logininforIcon, + messageIcon, + monitorIcon, + numberIcon, + onlineIcon, + passwordIcon, + pdfIcon, + peoplesIcon, + phoneIcon, + postIcon, + questionIcon, + radioIcon, + rateIcon, + redisIcon, + rowIcon, + selectIcon, + serverIcon, + swaggerIcon, + switchIcon, + systemIcon, + textareaIcon, + timeRangeIcon, + timeIcon, + toolIcon, + treeTableIcon, + treeIcon, + uploadIcon, + userIcon, + validCodeIcon +} + +export default allIcon diff --git a/src/core/lazy_use.js b/src/core/lazy_use.js new file mode 100644 index 0000000..2269f10 --- /dev/null +++ b/src/core/lazy_use.js @@ -0,0 +1,123 @@ +import Vue from 'vue' + +// base library +import { + ConfigProvider, + Layout, + Input, + InputNumber, + Button, + Switch, + Radio, + Checkbox, + Select, + TreeSelect, + 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, + Pagination, + FormModel, + PageHeader, + Result, + Statistic, + Descriptions, + message, + notification, + Space, + Tree +} from 'ant-design-vue' + +// ext library +import VueCropper from 'vue-cropper' +import Dialog from '@/components/Dialog' +import MultiTab from '@/components/MultiTab' +import PageLoading from '@/components/PageLoading' +import PermissionHelper from '@/core/permission/permission' +import './directives/action' + +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(TreeSelect) +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(Pagination) +Vue.use(FormModel) +Vue.use(PageHeader) +Vue.use(Result) +Vue.use(Statistic) +Vue.use(Descriptions) +Vue.use(Space) +Vue.use(Tree) + +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 + +Vue.use(Dialog) // this.$dialog func +Vue.use(MultiTab) +Vue.use(PageLoading) +Vue.use(PermissionHelper) +Vue.use(VueCropper) +process.env.NODE_ENV !== 'production' && console.warn('[antd-pro] NOTICE: Antd use lazy-load.') diff --git a/src/core/permission/permission.js b/src/core/permission/permission.js new file mode 100644 index 0000000..fb69844 --- /dev/null +++ b/src/core/permission/permission.js @@ -0,0 +1,55 @@ +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: '导出' } +} + +/** + * Button + * @param Vue + */ +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 diff --git a/src/core/use.js b/src/core/use.js new file mode 100644 index 0000000..db92c05 --- /dev/null +++ b/src/core/use.js @@ -0,0 +1,25 @@ +import Vue from 'vue' + +// base library +import Antd from 'ant-design-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 '@/core/permission/permission' +// import '@/components/use' +import './directives/action' + +VueClipboard.config.autoSetContainer = true + +Vue.use(Antd) +Vue.use(MultiTab) +Vue.use(PageLoading) +Vue.use(VueClipboard) +Vue.use(PermissionHelper) +Vue.use(VueCropper) + +process.env.NODE_ENV !== 'production' && console.warn('[antd-pro] WARNING: Antd now use fulled imported.') diff --git a/src/directive/permission/hasPermi.js b/src/directive/permission/hasPermi.js new file mode 100644 index 0000000..4a3f929 --- /dev/null +++ b/src/directive/permission/hasPermi.js @@ -0,0 +1,27 @@ + /** + * 操作权限处理 + * Copyright (c) 2019 ruoyi + */ +import store from '@/store' + +export default { + inserted (el, binding, vnode) { + const { value } = binding + const allPermission = '*:*:*' + const permissions = store.getters && store.getters.permissions + + if (value && value instanceof Array && value.length > 0) { + const permissionFlag = value + + const hasPermissions = permissions.some(permission => { + return allPermission === permission || permissionFlag.includes(permission) + }) + + if (!hasPermissions) { + el.parentNode && el.parentNode.removeChild(el) + } + } else { + throw new Error(`请设置操作权限标签值`) + } + } +} diff --git a/src/directive/permission/hasRole.js b/src/directive/permission/hasRole.js new file mode 100644 index 0000000..e1a0bab --- /dev/null +++ b/src/directive/permission/hasRole.js @@ -0,0 +1,27 @@ + /** + * 角色权限处理 + * Copyright (c) 2019 ruoyi + */ +import store from '@/store' + +export default { + inserted (el, binding, vnode) { + const { value } = binding + const superAdmin = 'admin' + const roles = store.getters && store.getters.roles + + if (value && value instanceof Array && value.length > 0) { + const roleFlag = value + + const hasRole = roles.some(role => { + return superAdmin === role || roleFlag.includes(role) + }) + + if (!hasRole) { + el.parentNode && el.parentNode.removeChild(el) + } + } else { + throw new Error(`请设置角色权限标签值"`) + } + } +} diff --git a/src/directive/permission/index.js b/src/directive/permission/index.js new file mode 100644 index 0000000..6bda3f8 --- /dev/null +++ b/src/directive/permission/index.js @@ -0,0 +1,15 @@ +import hasRole from './hasRole' +import hasPermi from './hasPermi' + +const install = function (Vue) { + Vue.directive('hasRole', hasRole) + Vue.directive('hasPermi', hasPermi) +} + +if (window.Vue) { + window['hasRole'] = hasRole + window['hasPermi'] = hasPermi + Vue.use(install); // eslint-disable-line +} + +export default install diff --git a/src/directive/style/highlight.js b/src/directive/style/highlight.js new file mode 100644 index 0000000..c9e193b --- /dev/null +++ b/src/directive/style/highlight.js @@ -0,0 +1,18 @@ +import Hljs from 'highlight.js' +import 'highlight.js/styles/vs.css' // 代码高亮风格,选择更多风格需导入 node_modules/hightlight.js/styles/ 目录下其它css文件 + +export default { + inserted: function (el) { + const blocks = el.querySelectorAll('pre code') + for (let i = 0; i < blocks.length; i++) { + Hljs.highlightBlock(blocks[i]) + } + }, + // 指令所在组件的 VNode 及其子 VNode 全部更新后调用 + componentUpdated: function (el) { + const blocks = el.querySelectorAll('pre code') + for (let i = 0; i < blocks.length; i++) { + Hljs.highlightBlock(blocks[i]) + } + } +} diff --git a/src/directive/style/index.js b/src/directive/style/index.js new file mode 100644 index 0000000..36697c3 --- /dev/null +++ b/src/directive/style/index.js @@ -0,0 +1,12 @@ +import highlight from './highlight' + +const install = function (Vue) { + Vue.directive('highlight', highlight) +} + +if (window.Vue) { + window['highlight'] = highlight + Vue.use(install); // eslint-disable-line +} + +export default install diff --git a/src/global.less b/src/global.less new file mode 100644 index 0000000..723d76d --- /dev/null +++ b/src/global.less @@ -0,0 +1,119 @@ +@import '~ant-design-vue/es/style/themes/default.less'; + +html, +body, +#app, #root { + height: 100%; +} + +.colorWeak { + filter: invert(80%); +} + +.ant-layout.layout-basic { + height: 100vh; + min-height: 100vh; +} + +canvas { + display: block; +} + +body { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +ul, +ol { + list-style: none; +} + +// 数据列表 样式 +.table-alert { + margin-bottom: 16px; +} +// 数据列表 操作 +.table-operator { + margin-bottom: 18px; + + button { + margin-right: 8px; + } +} +// 数据列表 搜索条件 +.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; + } +} + +@media (max-width: @screen-xs) { + .ant-table { + width: 100%; + overflow-x: auto; + &-thead > tr, + &-tbody > tr { + > th, + > td { + white-space: pre; + > span { + display: block; + } + } + } + } +} +//表格上方操作条 +.table-operations { + margin-bottom: 16px; +} + +.table-operations > button { + margin-right: 8px; +} +// 详情页底部按钮 +.bottom-control { + position: absolute; + bottom: 0; + width: 100%; + border-top: 1px solid #e8e8e8; + padding: 10px 16px; + text-align: right; + left: 0; + background: #fff; + border-radius: 0 0 4px 4px; +} +.ant-drawer-body { + padding: 24px 24px 50px 24px; + font-size: 14px; + line-height: 1.5; +} \ No newline at end of file diff --git a/src/layouts/BasicLayout.less b/src/layouts/BasicLayout.less new file mode 100644 index 0000000..0a1fcec --- /dev/null +++ b/src/layouts/BasicLayout.less @@ -0,0 +1,61 @@ +@import "~ant-design-vue/es/style/themes/default.less"; + +.ant-pro-global-header-index-right { + margin-right: 8px; + + &.ant-pro-global-header-index-dark { + .ant-pro-global-header-index-action { + color: hsla(0, 0%, 100%, .85); + + &:hover { + background: #1890ff; + } + } + } + + .ant-pro-account-avatar { + .antd-pro-global-header-index-avatar { + margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0; + margin-right: 8px; + color: @primary-color; + vertical-align: top; + background: rgba(255, 255, 255, 0.85); + } + } + + .menu { + .anticon { + margin-right: 8px; + } + + .ant-dropdown-menu-item { + min-width: 100px; + } + } +} +.ant-layout-sider-children { + ::-webkit-scrollbar-track { + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); + background-color: transparent; + border-radius: 10px; + } + + ::-webkit-scrollbar { + width: 6px; + } + + ::-webkit-scrollbar-thumb { + border-radius: 10px; + background-color: skyblue; + background-image: -webkit-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.2) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.2) 50%, + rgba(255, 255, 255, 0.2) 75%, + transparent 75%, + transparent + ); + } +} diff --git a/src/layouts/BasicLayout.vue b/src/layouts/BasicLayout.vue new file mode 100644 index 0000000..9ecdc3e --- /dev/null +++ b/src/layouts/BasicLayout.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/src/layouts/BlankLayout.vue b/src/layouts/BlankLayout.vue new file mode 100644 index 0000000..1bfbfbf --- /dev/null +++ b/src/layouts/BlankLayout.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/src/layouts/PageView.vue b/src/layouts/PageView.vue new file mode 100644 index 0000000..86df485 --- /dev/null +++ b/src/layouts/PageView.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/layouts/RouteView.vue b/src/layouts/RouteView.vue new file mode 100644 index 0000000..edae19e --- /dev/null +++ b/src/layouts/RouteView.vue @@ -0,0 +1,32 @@ + diff --git a/src/layouts/UserLayout.vue b/src/layouts/UserLayout.vue new file mode 100644 index 0000000..f9800fd --- /dev/null +++ b/src/layouts/UserLayout.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/src/layouts/index.js b/src/layouts/index.js new file mode 100644 index 0000000..1d62d6c --- /dev/null +++ b/src/layouts/index.js @@ -0,0 +1,7 @@ +import UserLayout from './UserLayout' +import BlankLayout from './BlankLayout' +import BasicLayout from './BasicLayout' +import RouteView from './RouteView' +import PageView from './PageView' + +export { UserLayout, BasicLayout, BlankLayout, RouteView, PageView } diff --git a/src/locales/index.js b/src/locales/index.js new file mode 100644 index 0000000..99a11a7 --- /dev/null +++ b/src/locales/index.js @@ -0,0 +1,59 @@ +import Vue from 'vue' +import VueI18n from 'vue-i18n' +import storage from 'store' +import moment from 'moment' + +// default lang +import zhCn from './lang/zh-CN' + +Vue.use(VueI18n) + +export const defaultLang = 'zh-CN' + +const messages = { + 'zh-CN': { + ...zhCn + } +} + +const i18n = new VueI18n({ + silentTranslationWarn: true, + locale: defaultLang, + fallbackLocale: defaultLang, + messages +}) + +const loadedLanguages = [defaultLang] + +function setI18nLanguage (lang) { + i18n.locale = lang + // request.headers['Accept-Language'] = lang + document.querySelector('html').setAttribute('lang', lang) + return lang +} + +export function loadLanguageAsync (lang = defaultLang) { + return new Promise(resolve => { + // 缓存语言设置 + storage.set('lang', lang) + if (i18n.locale !== lang) { + if (!loadedLanguages.includes(lang)) { + return import(/* webpackChunkName: "lang-[request]" */ `./lang/${lang}`).then(msg => { + const locale = msg.default + i18n.setLocaleMessage(lang, locale) + loadedLanguages.push(lang) + moment.updateLocale(locale.momentName, locale.momentLocale) + return setI18nLanguage(lang) + }) + } + return resolve(setI18nLanguage(lang)) + } + return resolve(lang) + }) +} + +export function i18nRender (key) { + return i18n.t(`${key}`) +} + + export default i18n diff --git a/src/locales/lang/en-US.js b/src/locales/lang/en-US.js new file mode 100644 index 0000000..137fc5b --- /dev/null +++ b/src/locales/lang/en-US.js @@ -0,0 +1,16 @@ +import antdEnUS from 'ant-design-vue/es/locale-provider/en_US' +import momentEU from 'moment/locale/eu' +import setting from './en-US/setting' + +const components = { + antLocale: antdEnUS, + momentName: 'eu', + momentLocale: momentEU +} + +export default { + 'message': '-', + + ...components, + ...setting +} diff --git a/src/locales/lang/en-US/setting.js b/src/locales/lang/en-US/setting.js new file mode 100644 index 0000000..baa1e97 --- /dev/null +++ b/src/locales/lang/en-US/setting.js @@ -0,0 +1,30 @@ +export default { + // 'app.setting.pagestyle': 'Page style setting', + // 'app.setting.pagestyle.light': 'Light style', + // 'app.setting.pagestyle.dark': 'Dark style', + // 'app.setting.pagestyle.realdark': 'RealDark style', + // 'app.setting.themecolor': 'Theme Color', + // 'app.setting.navigationmode': 'Navigation Mode', + // 'app.setting.content-width': 'Content Width', + // 'app.setting.fixedheader': 'Fixed Header', + // 'app.setting.fixedsidebar': 'Fixed Sidebar', + // 'app.setting.multitab': 'View multitab', + // 'app.setting.sidemenu': 'Side Menu Layout', + // 'app.setting.topmenu': 'Top Menu Layout', + // 'app.setting.content-width.fixed': 'Fixed', + // 'app.setting.content-width.fluid': 'Fluid', + // 'app.setting.othersettings': 'Other Settings', + // 'app.setting.weakmode': 'Weak Mode', + // 'app.setting.copy': 'Copy Setting', + // 'app.setting.loading': 'Loading theme', + // 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/config/defaultSettings.js', + // 'app.setting.production.hint': 'Setting panel shows in development environment only, please manually modify', + // 'app.setting.themecolor.daybreak': 'Daybreak Blue', + // 'app.setting.themecolor.dust': 'Dust Red', + // 'app.setting.themecolor.volcano': 'Volcano', + // 'app.setting.themecolor.sunset': 'Sunset Orange', + // 'app.setting.themecolor.cyan': 'Cyan', + // 'app.setting.themecolor.green': 'Polar Green', + // 'app.setting.themecolor.geekblue': 'Geek Blue', + // 'app.setting.themecolor.purple': 'Golden Purple' +} diff --git a/src/locales/lang/zh-CN.js b/src/locales/lang/zh-CN.js new file mode 100644 index 0000000..bb1b13e --- /dev/null +++ b/src/locales/lang/zh-CN.js @@ -0,0 +1,16 @@ +import antd from 'ant-design-vue/es/locale-provider/zh_CN' +import momentCN from 'moment/locale/zh-cn' +import setting from './zh-CN/setting' + +const components = { + antLocale: antd, + momentName: 'zh-cn', + momentLocale: momentCN +} + +export default { + 'message': '-', + + ...components, + ...setting +} diff --git a/src/locales/lang/zh-CN/setting.js b/src/locales/lang/zh-CN/setting.js new file mode 100644 index 0000000..5f91a1a --- /dev/null +++ b/src/locales/lang/zh-CN/setting.js @@ -0,0 +1,30 @@ +export default { + 'app.setting.pagestyle': '整体风格设置', + 'app.setting.pagestyle.light': '亮色菜单风格', + 'app.setting.pagestyle.dark': '暗色菜单风格', + 'app.setting.pagestyle.realdark': '暗黑模式', + 'app.setting.themecolor': '主题色', + 'app.setting.navigationmode': '导航模式', + 'app.setting.content-width': '内容区域宽度', + 'app.setting.fixedheader': '固定 Header', + 'app.setting.fixedsidebar': '固定侧边栏', + 'app.setting.multitab': '显示页签', + 'app.setting.sidemenu': '侧边菜单布局', + 'app.setting.topmenu': '顶部菜单布局', + 'app.setting.content-width.fixed': 'Fixed', + 'app.setting.content-width.fluid': 'Fluid', + 'app.setting.othersettings': '其他设置', + 'app.setting.weakmode': '色弱模式', + 'app.setting.copy': '拷贝设置', + 'app.setting.loading': '加载主题中', + 'app.setting.copyinfo': '拷贝设置成功 src/config/defaultSettings.js', + 'app.setting.production.hint': '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', + 'app.setting.themecolor.daybreak': '拂晓蓝', + 'app.setting.themecolor.dust': '薄暮', + 'app.setting.themecolor.volcano': '火山', + 'app.setting.themecolor.sunset': '日暮', + 'app.setting.themecolor.cyan': '明青', + 'app.setting.themecolor.green': '极光绿', + 'app.setting.themecolor.geekblue': '极客蓝', + 'app.setting.themecolor.purple': '酱紫' +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..f56d81c --- /dev/null +++ b/src/main.js @@ -0,0 +1,65 @@ +// with polyfills +import 'core-js/stable' +import 'regenerator-runtime/runtime' + +import Vue from 'vue' +import App from './App.vue' +import router from './router' +import permission from './directive/permission' +import style from './directive/style' +import store from './store/' +import i18n from './locales' +import { VueAxios } from './utils/request' +import ProLayout, { PageHeaderWrapper } from '@/components/ProLayout' +import FooterToolBar from '@/components/FooterToolbar' +import FileUpload from '@/components/FileUpload' +import themePluginConfig from '../config/themePluginConfig' + +// mock +// WARNING: `mockjs` NOT SUPPORT `IE` PLEASE DO NOT USE IN `production` ENV. +// import './mock' + +import bootstrap from './core/bootstrap' +import './core/lazy_use' // use lazy load components +import './permission' // permission control +import './utils/filter' // global filter +import './global.less' // global style +import config from '@/config/defaultSettings' +import { getDicts } from '@/api/system/dict/data' +import { getConfigKey } from '@/api/system/config' +import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, download, handleTree } from '@/utils/ruoyi' + +// 全局方法挂载 +Vue.prototype.getDicts = getDicts +Vue.prototype.getConfigKey = getConfigKey +Vue.prototype.parseTime = parseTime +Vue.prototype.resetForm = resetForm +Vue.prototype.addDateRange = addDateRange +Vue.prototype.selectDictLabel = selectDictLabel +Vue.prototype.selectDictLabels = selectDictLabels +Vue.prototype.download = download +Vue.prototype.handleTree = handleTree +Vue.prototype.tableSize = config.tableSize +Vue.config.productionTip = false + +// mount axios to `Vue.$http` and `this.$http` +Vue.use(VueAxios) +// use pro-layout components +Vue.component('pro-layout', ProLayout) +Vue.component('page-container', PageHeaderWrapper) +Vue.component('page-header-wrapper', PageHeaderWrapper) +Vue.component('footer-tool-bar', FooterToolBar) +Vue.component('file-upload', FileUpload) + +Vue.use(permission) +Vue.use(style) + +window.umi_plugin_ant_themeVar = themePluginConfig.theme +new Vue({ + router, + store, + i18n, + // init localstorage, vuex + created: bootstrap, + render: h => h(App) +}).$mount('#app') diff --git a/src/mock/index.js b/src/mock/index.js new file mode 100644 index 0000000..ebc2013 --- /dev/null +++ b/src/mock/index.js @@ -0,0 +1,18 @@ +import { isIE } from '@/utils/util' + +// 判断环境不是 prod 或者 preview 是 true 时,加载 mock 服务 +if (process.env.NODE_ENV !== 'production' || process.env.VUE_APP_PREVIEW === 'true') { + if (isIE()) { + console.error('[antd-pro] ERROR: `mockjs` NOT SUPPORT `IE` PLEASE DO NOT USE IN `production` ENV.') + } + // 使用同步加载依赖 + // 防止 vuex 中的 GetInfo 早于 mock 运行,导致无法 mock 请求返回结果 + console.log('[antd-pro] mock mounting') + const Mock = require('mockjs2') + require('./services/user') + + Mock.setup({ + timeout: 800 // setter delay time + }) + console.log('[antd-pro] mock mounted') +} diff --git a/src/mock/services/user.js b/src/mock/services/user.js new file mode 100644 index 0000000..47fe15c --- /dev/null +++ b/src/mock/services/user.js @@ -0,0 +1,26 @@ +import Mock from 'mockjs2' +import { builder } from '../util' + +const info = (options) => { + console.log('options', options) + const userInfo = { + 'id': '4291d7da9005377ec9aec4a71ea837f', + 'name': '天野远子', + 'username': 'admin', + 'password': '', + 'avatar': '/avatar2.jpg', + 'status': 1, + 'telephone': '', + 'lastLoginIp': '27.154.74.117', + 'lastLoginTime': 1534837621348, + 'creatorId': 'admin', + 'createTime': 1497160610259, + 'merchantCode': 'TLif2btpzg079h15bk', + 'deleted': 0, + 'roleId': 'admin', + 'role': {} + } + return builder(userInfo) +} + +Mock.mock(/\/api\/user\/info/, 'get', info) diff --git a/src/mock/util.js b/src/mock/util.js new file mode 100644 index 0000000..a4be036 --- /dev/null +++ b/src/mock/util.js @@ -0,0 +1,38 @@ +const responseBody = { + message: '', + timestamp: 0, + result: null, + code: 0 +} + +export const builder = (data, message, code = 0, headers = {}) => { + responseBody.result = data + if (message !== undefined && message !== null) { + responseBody.message = message + } + if (code !== undefined && code !== 0) { + responseBody.code = code + responseBody._status = code + } + if (headers !== null && typeof headers === 'object' && Object.keys(headers).length > 0) { + responseBody._headers = headers + } + responseBody.timestamp = new Date().getTime() + return responseBody +} + +export const getQueryParameters = (options) => { + const url = options.url + const search = url.split('?')[1] + if (!search) { + return {} + } + return JSON.parse('{"' + decodeURIComponent(search) + .replace(/"/g, '\\"') + .replace(/&/g, '","') + .replace(/=/g, '":"') + '"}') +} + +export const getBody = (options) => { + return options.body && JSON.parse(options.body) +} diff --git a/src/permission.js b/src/permission.js new file mode 100644 index 0000000..6bd44fb --- /dev/null +++ b/src/permission.js @@ -0,0 +1,79 @@ +import router from './router' +import store from './store' +import storage from 'store' +import NProgress from 'nprogress' // progress bar +import '@/components/NProgress/nprogress.less' // progress bar custom style +import notification from 'ant-design-vue/es/notification' +import { setDocumentTitle, domTitle } from '@/utils/domUtil' +import { ACCESS_TOKEN } from '@/store/mutation-types' +import { i18nRender } from '@/locales' + +NProgress.configure({ showSpinner: false }) // NProgress Configuration + +const allowList = ['login'] // no redirect allowList +const loginRoutePath = '/user/login' +const defaultRoutePath = '/index' + +router.beforeEach((to, from, next) => { + NProgress.start() // start progress bar + to.meta && (typeof to.meta.title !== 'undefined' && setDocumentTitle(`${i18nRender(to.meta.title)} - ${domTitle}`)) + /* has token */ + if (storage.get(ACCESS_TOKEN)) { + if (to.path === loginRoutePath || to.path === '/') { + next({ path: defaultRoutePath }) + NProgress.done() + } else { + // check login user.roles is null + if (store.getters.roles.length === 0) { + // request login userInfo + store + .dispatch('GetInfo') + .then(res => { + // const roles = res.result && res.result.role + const roles = res.roles + // generate dynamic router + store.dispatch('GenerateRoutes', { roles }).then(() => { + // 根据roles权限生成可访问的路由表 + // 动态添加可访问路由表 + router.addRoutes(store.getters.addRouters) + // router.addRoutes(accessRoutes) + // 请求带有 redirect 重定向时,登录自动重定向到该地址 + next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 + // const redirect = decodeURIComponent(from.query.redirect || to.path) + // if (to.path === redirect) { + // // set the replace: true so the navigation will not leave a history record + // next({ ...to, replace: true }) + // } else { + // // 跳转到目的路由 + // next({ path: redirect }) + // } + }) + }) + .catch(() => { + notification.error({ + message: '错误', + description: '请求用户信息失败,请重试' + }) + // 失败时,获取用户信息失败时,调用登出,来清空历史保留信息 + store.dispatch('Logout').then(() => { + next({ path: '/' }) + }) + }) + } else { + next() + } + } + } else { + if (allowList.includes(to.name)) { + // 在免登录名单,直接进入 + next() + } else { + next({ path: loginRoutePath, 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 +}) diff --git a/src/router/README.md b/src/router/README.md new file mode 100644 index 0000000..48b7520 --- /dev/null +++ b/src/router/README.md @@ -0,0 +1,134 @@ +路由/菜单说明 +==== + + +格式和说明 +---- + +```ecmascript 6 +const routerObject = { + redirect: noredirect, + name: 'router-name', + hidden: true, + meta: { + title: 'title', + icon: 'a-icon', + target: '_blank|_self|_top|_parent', + keepAlive: true, + hiddenHeaderContent: true, + } +} +``` + + + +`{ Route }` 对象 + +| 参数 | 说明 | 类型 | 默认值 | +| -------- | ----------------------------------------- | ------- | ------ | +| hidden | 控制路由是否显示在 sidebar | boolean | false | +| redirect | 重定向地址, 访问这个路由时,自定进行重定向 | string | - | +| name | 路由名称, 必须设置,且不能重名 | string | - | +| meta | 路由元信息(路由附带扩展信息) | object | {} | +| hideChildrenInMenu | 强制菜单显示为Item而不是SubItem(配合 meta.hidden) | boolean | - | + + +`{ Meta }` 路由元信息对象 + +| 参数 | 说明 | 类型 | 默认值 | +| ------------------- | ------------------------------------------------------------ | ------- | ------ | +| title | 路由标题, 用于显示面包屑, 页面标题 *推荐设置 | string | - | +| icon | 路由在 menu 上显示的图标 | [string,svg] | - | +| keepAlive | 缓存该路由 | boolean | false | +| target | 菜单链接跳转目标(参考 html a 标记) | string | - | +| hidden | 配合`hideChildrenInMenu`使用,用于隐藏菜单时,提供递归到父菜单显示 选中菜单项_(可参考 个人页 配置方式)_ | boolean | false | +| hiddenHeaderContent | *特殊 隐藏 [PageHeader](https://github.com/vueComponent/ant-design-vue-pro/blob/master/src/components/PageHeader/PageHeader.vue#L6) 组件中的页面带的 面包屑和页面标题栏 | boolean | false | +| permission | 与项目提供的权限拦截匹配的权限,如果不匹配,则会被禁止访问该路由页面 | array | [] | + +> 路由自定义 `Icon` 请引入自定义 `svg` Icon 文件,然后传递给路由的 `meta.icon` 参数即可 + +路由构建例子方案1 + +路由例子 +---- + +```ecmascript 6 +const asyncRouterMap = [ + { + path: '/', + name: 'index', + component: BasicLayout, + meta: { title: '首页' }, + redirect: '/dashboard/analysis', + children: [ + { + path: '/dashboard', + component: RouteView, + name: 'dashboard', + redirect: '/dashboard/workplace', + meta: {title: '仪表盘', icon: 'dashboard', permission: ['dashboard']}, + children: [ + { + path: '/dashboard/analysis', + name: 'Analysis', + component: () => import('@/views/dashboard/Analysis'), + meta: {title: '分析页', permission: ['dashboard']} + }, + { + path: '/dashboard/monitor', + name: 'Monitor', + hidden: true, + component: () => import('@/views/dashboard/Monitor'), + meta: {title: '监控页', permission: ['dashboard']} + }, + { + path: '/dashboard/workplace', + name: 'Workplace', + component: () => import('@/views/dashboard/Workplace'), + meta: {title: '工作台', permission: ['dashboard']} + } + ] + }, + + // result + { + path: '/result', + name: 'result', + component: PageView, + redirect: '/result/success', + meta: { title: '结果页', icon: 'check-circle-o', permission: [ 'result' ] }, + children: [ + { + path: '/result/success', + name: 'ResultSuccess', + component: () => import(/* webpackChunkName: "result" */ '@/views/result/Success'), + // 该页面隐藏面包屑和页面标题栏 + meta: { title: '成功', hiddenHeaderContent: true, permission: [ 'result' ] } + }, + { + path: '/result/fail', + name: 'ResultFail', + component: () => import(/* webpackChunkName: "result" */ '@/views/result/Error'), + // 该页面隐藏面包屑和页面标题栏 + meta: { title: '失败', hiddenHeaderContent: true, permission: [ 'result' ] } + } + ] + }, + ... + ] + }, +] +``` + +> 1. 请注意 `component: () => import('..') ` 方式引入路由的页面组件为 懒加载模式。具体可以看 [Vue 官方文档](https://router.vuejs.org/zh/guide/advanced/lazy-loading.html) +> 2. 增加新的路由应该增加在 '/' (index) 路由的 `children` 内 +> 3. 子路由的父级路由必须有 `router-view` 才能让子路由渲染出来,请仔细查阅 vue-router 文档 +> 4. `permission` 可以进行自定义修改,只需要对这个模块进行自定义修改即可 [src/store/modules/permission.js#L10](https://github.com/vueComponent/ant-design-vue-pro/blob/master/src/store/modules/permission.js#L10) + + +附权限路由结构: + +![权限结构](https://static-2.loacg.com/open/static/github/permissions.png) + + +第二种前端路由由后端动态生成的设计,可以前往官网文档 https://pro.antdv.com/docs/authority-management 参考 diff --git a/src/router/generator-routers.js b/src/router/generator-routers.js new file mode 100644 index 0000000..d0c4a50 --- /dev/null +++ b/src/router/generator-routers.js @@ -0,0 +1,134 @@ +// eslint-disable-next-line +import { getRouters } from '@/api/menu' +import { UserLayout, BasicLayout, BlankLayout, PageView, RouteView } from '@/layouts' +import { indexRouterMap } from '@/config/router.config' +import allIcon from '@/core/icons' +import { validURL } from '@/utils/validate' +// 前端路由表 +const constantRouterComponents = { + // 基础页面 layout 必须引入 + BasicLayout: BasicLayout, + BlankLayout: BlankLayout, + RouteView: RouteView, + PageView: PageView, + UserLayout: UserLayout, // 登陆注册页面的通用布局 + // 你需要动态引入的页面组件 + 'Index': () => import('@/views/index'), + // account + 'AccountCenter': () => import('@/views/account/center'), + 'AccountSettings': () => import('@/views/account/settings/index'), + 'BaseSettings': () => import('@/views/account/settings/BaseSetting'), + 'SecuritySettings': () => import('@/views/account/settings/Security'), + // 公告新增修改 + 'NoticeForm': () => import('@/views/system/notice/CreateForm'), + // 修改生成配置 + 'GenEdit': () => import('@/views/tool/gen/modules/GenEdit') +} + +// 前端未找到页面路由(固定不用改) +const notFoundRouter = { + path: '*', redirect: '/404', hidden: true +} + +// 根级菜单 +const rootRouter = { + key: '', + name: 'index', + path: '', + component: 'BasicLayout', + redirect: '/index', + meta: { + title: '首页' + }, + children: [] +} + +/** + * 动态生成菜单 + * @param token + * @returns {Promise} + */ +export const generatorDynamicRouter = (token) => { + return new Promise((resolve, reject) => { + // 向后端请求路由数据 + getRouters().then(res => { + const menuNav = [] + rootRouter.children = indexRouterMap.concat(res.data) + menuNav.push(rootRouter) + const routers = generator(menuNav) + routers.push(notFoundRouter) + resolve(routers) + }).catch(err => { + reject(err) + }) + }) +} + +/** + * 格式化树形结构数据 生成 vue-router 层级路由表 + * + * @param routerMap + * @param parent + * @returns {*} + */ +export const generator = (routerMap, parent) => { + return routerMap.map(item => { + const { title, show, hideChildren, hiddenHeaderContent, hidden, icon, noCache } = item.meta || {} + if (item.component) { + // Layout ParentView 组件特殊处理 + if (item.component === 'Layout') { + item.component = 'RouteView' + } else if (item.component === 'ParentView') { + // 三级菜单处理 + item.component = 'RouteView' + item.path = '/' + item.path + } + } + window.console.log('path:' + item.name + '/' + item.path + '|' + item.isFrame) + if (item.path) { + // item.path = '/' + item.path + } + if (item.isFrame === 0) { + item.target = '_blank' + } + const currentRouter = { + // 如果路由设置了 path,则作为默认 path,否则 路由地址 动态拼接生成如 /dashboard/workplace + path: item.path || `${parent && parent.path || ''}/${item.path}`, + // 路由名称,建议唯一 + name: item.name || item.key || '', + // 该路由对应页面的 组件(动态加载) + component: (constantRouterComponents[item.component || item.key]) || (() => import(`@/views/${item.component}`)), + hidden: item.hidden, + // meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉) + meta: { + title: title, + icon: allIcon[icon + 'Icon'] || icon, + hiddenHeaderContent: hiddenHeaderContent, + // 目前只能通过判断path的http链接来判断是否外链,适配若依 + target: validURL(item.path) ? '_blank' : '', + permission: item.name, + keepAlive: noCache, + hidden: hidden + }, + redirect: item.redirect + } + // 是否设置了隐藏菜单 + if (show === false) { + currentRouter.hidden = true + } + // 适配若依,若依为缩写路径,而antdv-pro的pro-layout要求每个路径需为全路径 + if (!constantRouterComponents[item.component || item.key]) { + currentRouter.path = `${parent && parent.path || ''}/${item.path}` + } + // 是否设置了隐藏子菜单 + if (hideChildren) { + currentRouter.hideChildrenInMenu = true + } + // 是否有子菜单,并递归处理,并将父path传入 + if (item.children && item.children.length > 0) { + // Recursion + currentRouter.children = generator(item.children, currentRouter) + } + return currentRouter + }) +} diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..afda622 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,17 @@ +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', + routes: constantRouterMap +}) diff --git a/src/store/app-mixin.js b/src/store/app-mixin.js new file mode 100644 index 0000000..c910ae9 --- /dev/null +++ b/src/store/app-mixin.js @@ -0,0 +1,32 @@ +import { mapState } from 'vuex' + +const baseMixin = { + computed: { + ...mapState({ + layout: state => state.app.layout, + navTheme: state => state.app.theme, + primaryColor: state => state.app.color, + colorWeak: state => state.app.weak, + fixedHeader: state => state.app.fixedHeader, + fixedSidebar: state => state.app.fixedSidebar, + contentWidth: state => state.app.contentWidth, + autoHideHeader: state => state.app.autoHideHeader, + + isMobile: state => state.app.isMobile, + sideCollapsed: state => state.app.sideCollapsed, + multiTab: state => state.app.multiTab + }), + isTopMenu () { + return this.layout === 'topmenu' + } + }, + methods: { + isSideMenu () { + return !this.isTopMenu + } + } +} + +export { + baseMixin +} diff --git a/src/store/device-mixin.js b/src/store/device-mixin.js new file mode 100644 index 0000000..2510707 --- /dev/null +++ b/src/store/device-mixin.js @@ -0,0 +1,11 @@ +import { mapState } from 'vuex' + +const deviceMixin = { + computed: { + ...mapState({ + isMobile: state => state.app.isMobile + }) + } +} + +export { deviceMixin } diff --git a/src/store/getters.js b/src/store/getters.js new file mode 100644 index 0000000..1a518c0 --- /dev/null +++ b/src/store/getters.js @@ -0,0 +1,17 @@ +const getters = { + isMobile: state => state.app.isMobile, + lang: state => state.app.lang, + 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, + permissions: state => state.user.permissions, + userInfo: state => state.user.info, + addRouters: state => state.permission.addRouters, + multiTab: state => state.app.multiTab +} + +export default getters diff --git a/src/store/i18n-mixin.js b/src/store/i18n-mixin.js new file mode 100644 index 0000000..715b0c8 --- /dev/null +++ b/src/store/i18n-mixin.js @@ -0,0 +1,16 @@ +import { mapState } from 'vuex' + +const i18nMixin = { + computed: { + ...mapState({ + currentLang: state => state.app.lang + }) + }, + methods: { + setLang (lang) { + this.$store.dispatch('setLang', lang) + } + } +} + +export default i18nMixin diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..28f5c34 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,33 @@ +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' +import permission from './modules/async-router' + +// 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 +}) diff --git a/src/store/modules/app.js b/src/store/modules/app.js new file mode 100644 index 0000000..a3ec761 --- /dev/null +++ b/src/store/modules/app.js @@ -0,0 +1,99 @@ +import storage from 'store' +import { + SIDEBAR_TYPE, + TOGGLE_MOBILE_TYPE, + TOGGLE_NAV_THEME, + TOGGLE_LAYOUT, + TOGGLE_FIXED_HEADER, + TOGGLE_FIXED_SIDEBAR, + TOGGLE_CONTENT_WIDTH, + TOGGLE_HIDE_HEADER, + TOGGLE_COLOR, + TOGGLE_WEAK, + TOGGLE_MULTI_TAB, + // i18n + APP_LANGUAGE +} from '@/store/mutation-types' +import { loadLanguageAsync } from '@/locales' + +const app = { + state: { + sideCollapsed: false, + isMobile: false, + theme: 'dark', + layout: '', + contentWidth: '', + fixedHeader: false, + fixedSidebar: false, + autoHideHeader: false, + color: '', + weak: false, + multiTab: true, + lang: 'zh-CN', + _antLocale: {} + }, + mutations: { + [SIDEBAR_TYPE]: (state, type) => { + state.sideCollapsed = type + storage.set(SIDEBAR_TYPE, type) + }, + [TOGGLE_MOBILE_TYPE]: (state, isMobile) => { + state.isMobile = isMobile + }, + [TOGGLE_NAV_THEME]: (state, theme) => { + state.theme = theme + storage.set(TOGGLE_NAV_THEME, theme) + }, + [TOGGLE_LAYOUT]: (state, mode) => { + state.layout = mode + storage.set(TOGGLE_LAYOUT, mode) + }, + [TOGGLE_FIXED_HEADER]: (state, mode) => { + state.fixedHeader = mode + storage.set(TOGGLE_FIXED_HEADER, mode) + }, + [TOGGLE_FIXED_SIDEBAR]: (state, mode) => { + state.fixedSidebar = mode + storage.set(TOGGLE_FIXED_SIDEBAR, mode) + }, + [TOGGLE_CONTENT_WIDTH]: (state, type) => { + state.contentWidth = type + storage.set(TOGGLE_CONTENT_WIDTH, type) + }, + [TOGGLE_HIDE_HEADER]: (state, type) => { + state.autoHideHeader = type + storage.set(TOGGLE_HIDE_HEADER, type) + }, + [TOGGLE_COLOR]: (state, color) => { + state.color = color + storage.set(TOGGLE_COLOR, color) + }, + [TOGGLE_WEAK]: (state, mode) => { + state.weak = mode + storage.set(TOGGLE_WEAK, mode) + }, + [APP_LANGUAGE]: (state, lang, antd = {}) => { + state.lang = lang + state._antLocale = antd + storage.set(APP_LANGUAGE, lang) + }, + [TOGGLE_MULTI_TAB]: (state, bool) => { + storage.set(TOGGLE_MULTI_TAB, bool) + state.multiTab = bool + } + }, + actions: { + setLang ({ commit }, lang) { + return new Promise((resolve, reject) => { + commit(APP_LANGUAGE, lang) + loadLanguageAsync(lang).then(() => { + resolve() + }).catch((e) => { + reject(e) + }) + }) + } + } +} + +export default app diff --git a/src/store/modules/async-router.js b/src/store/modules/async-router.js new file mode 100644 index 0000000..37f6af9 --- /dev/null +++ b/src/store/modules/async-router.js @@ -0,0 +1,31 @@ +/** + * 向后端请求用户的菜单,动态生成路由 + */ +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 = constantRouterMap.concat(routers) + state.routers = constantRouterMap.concat(routers) + // state.routers = constantRouterMap + } + }, + actions: { + GenerateRoutes ({ commit }, data) { + return new Promise(resolve => { + generatorDynamicRouter(data).then(routers => { + commit('SET_ROUTERS', routers) + resolve() + }) + }) + } + } +} + +export default permission diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js new file mode 100644 index 0000000..676905e --- /dev/null +++ b/src/store/modules/permission.js @@ -0,0 +1,104 @@ +import { constantRouterMap } from '@/config/router.config' +import { getRouters } from '@/api/menu' +import { BasicLayout } from '@/layouts' + +const RouteView = { + name: 'RouteView', + render: (h) => h('router-view') +} +// /** +// * 过滤账户是否拥有某一个权限,并将菜单从加载列表移除 +// * +// * @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 (asyncRouterMap) { + const accessedRouters = asyncRouterMap.filter(route => { + // if (hasPermission(roles.permissionList, route)) { + // if (route.children && route.children.length) { + // route.children = filterAsyncRouter(route.children) + // } + // return true + // } + // if (route.children && route.children.length) { + // route.children = filterAsyncRouter(route.children) + // } + if (route.component) { + // Layout ParentView 组件特殊处理 + if (route.component === 'Layout') { + route.component = BasicLayout + } else { + route.component = RouteView + } + } + if (route.children != null && route.children && route.children.length) { + route.children = filterAsyncRouter(route.children) + } + return true + }) + return accessedRouters +} + +const permission = { + state: { + routers: constantRouterMap, + addRouters: [] + }, + mutations: { + SET_ROUTERS: (state, routers) => { + state.addRouters = routers + state.routers = constantRouterMap.concat(routers) + } + }, + actions: { + GenerateRoutes ({ commit }) { + return new Promise(resolve => { + getRouters().then(res => { + const accessedRoutes = filterAsyncRouter(res.data) + // const accessedRoutes = filterAsyncRouter(constantRouterMap) + // const accessedRoutes = constantRouterMap + // accessedRoutes.push({ path: '*', redirect: '/404', hidden: true }) + commit('SET_ROUTERS', accessedRoutes) + resolve(accessedRoutes) + }) + // const { roles } = data + // const accessedRouters = filterAsyncRouter(asyncRouterMap, roles) + // commit('SET_ROUTERS', accessedRouters) + // resolve() + }) + } + } +} + +export default permission diff --git a/src/store/modules/user.js b/src/store/modules/user.js new file mode 100644 index 0000000..6f52d9d --- /dev/null +++ b/src/store/modules/user.js @@ -0,0 +1,91 @@ +import storage from 'store' +import { login, getInfo, logout } from '@/api/login' +import { ACCESS_TOKEN } from '@/store/mutation-types' + +const user = { + state: { + token: '', + name: '', + welcome: '', + avatar: '', + roles: [], + info: {} + }, + + mutations: { + SET_TOKEN: (state, token) => { + state.token = token + }, + SET_NAME: (state, name) => { + state.name = name + }, + SET_AVATAR: (state, avatar) => { + state.avatar = avatar + }, + SET_ROLES: (state, roles) => { + state.roles = roles + }, + SET_INFO: (state, info) => { + state.info = info + }, + SET_PERMISSIONS: (state, permissions) => { + state.permissions = permissions + } + }, + + actions: { + // 登录 + Login ({ commit }, userInfo) { + return new Promise((resolve, reject) => { + login(userInfo).then(res => { + storage.set(ACCESS_TOKEN, res.token, 7 * 24 * 60 * 60 * 1000) + commit('SET_TOKEN', res.token) + resolve() + }) + .catch(error => { + reject(error) + }) + }) + }, + + // 获取用户信息 + GetInfo ({ commit, state }) { + return new Promise((resolve, reject) => { + getInfo(state.token).then(res => { + const user = res.user + const avatar = user.avatar === '' ? require('@/assets/images/profile.jpg') : process.env.VUE_APP_BASE_API + user.avatar + if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 + commit('SET_ROLES', res.roles) + commit('SET_PERMISSIONS', res.permissions) + } else { + commit('SET_ROLES', ['ROLE_DEFAULT']) + } + commit('SET_NAME', user.nickName) + commit('SET_AVATAR', avatar) + resolve(res) + }).catch(error => { + reject(error) + }) + }) + }, + + // 登出 + Logout ({ commit, state }) { + return new Promise((resolve, reject) => { + logout(state.token).then(() => { + commit('SET_TOKEN', '') + commit('SET_ROLES', []) + commit('SET_PERMISSIONS', []) + storage.remove(ACCESS_TOKEN) + resolve() + }).catch(error => { + reject(error) + }).finally(() => { + }) + }) + } + + } +} + +export default user diff --git a/src/store/mutation-types.js b/src/store/mutation-types.js new file mode 100644 index 0000000..591b69e --- /dev/null +++ b/src/store/mutation-types.js @@ -0,0 +1,24 @@ +export const ACCESS_TOKEN = 'access_token' + +export const SIDEBAR_TYPE = 'sidebar_type' +export const TOGGLE_MOBILE_TYPE = 'is_mobile' +export const TOGGLE_NAV_THEME = 'nav_theme' +export const TOGGLE_LAYOUT = 'layout' +export const TOGGLE_FIXED_HEADER = 'fixed_header' +export const TOGGLE_FIXED_SIDEBAR = 'fixed_sidebar' +export const TOGGLE_CONTENT_WIDTH = 'content_width' +export const TOGGLE_HIDE_HEADER = 'auto_hide_header' +export const TOGGLE_COLOR = 'color' +export const TOGGLE_WEAK = 'weak' +export const TOGGLE_MULTI_TAB = 'multi_tab' +export const APP_LANGUAGE = 'app_language' + +export const CONTENT_WIDTH_TYPE = { + Fluid: 'Fluid', + Fixed: 'Fixed' +} + +export const NAV_THEME = { + LIGHT: 'light', + DARK: 'dark' +} diff --git a/src/utils/axios.js b/src/utils/axios.js new file mode 100644 index 0000000..3b91f6b --- /dev/null +++ b/src/utils/axios.js @@ -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 +} diff --git a/src/utils/domUtil.js b/src/utils/domUtil.js new file mode 100644 index 0000000..be93027 --- /dev/null +++ b/src/utils/domUtil.js @@ -0,0 +1,21 @@ +import config from '@/config/defaultSettings' + +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 = config.title diff --git a/src/utils/errorCode.js b/src/utils/errorCode.js new file mode 100644 index 0000000..d2111ee --- /dev/null +++ b/src/utils/errorCode.js @@ -0,0 +1,6 @@ +export default { + '401': '认证失败,无法访问系统资源', + '403': '当前操作没有权限', + '404': '访问资源不存在', + 'default': '系统未知错误,请反馈给管理员' +} diff --git a/src/utils/filter.js b/src/utils/filter.js new file mode 100644 index 0000000..45702c6 --- /dev/null +++ b/src/utils/filter.js @@ -0,0 +1,20 @@ +import Vue from 'vue' +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) +}) diff --git a/src/utils/highlight.js b/src/utils/highlight.js new file mode 100644 index 0000000..c1046c8 --- /dev/null +++ b/src/utils/highlight.js @@ -0,0 +1,29 @@ +// src/utils/highlight.js 文件路径,纯属自定义 + +// highlight.js 代码高亮指令 +import Hljs from 'highlight.js' +import 'highlight.js/styles/vs.css' // 代码高亮风格,选择更多风格需导入 node_modules/hightlight.js/styles/ 目录下其它css文件 + +const Highlight = {} +// 自定义插件 +Highlight.install = function (Vue) { + // 自定义指令 v-highlight + Vue.directive('highlight', { + // 被绑定元素插入父节点时调用 + inserted: function (el) { + const blocks = el.querySelectorAll('pre code') + for (let i = 0; i < blocks.length; i++) { + Hljs.highlightBlock(blocks[i]) + } + }, + // 指令所在组件的 VNode 及其子 VNode 全部更新后调用 + componentUpdated: function (el) { + const blocks = el.querySelectorAll('pre code') + for (let i = 0; i < blocks.length; i++) { + Hljs.highlightBlock(blocks[i]) + } + } + }) +} + +export default Highlight diff --git a/src/utils/request.js b/src/utils/request.js new file mode 100644 index 0000000..5b8a1e9 --- /dev/null +++ b/src/utils/request.js @@ -0,0 +1,145 @@ +import axios from 'axios' +import store from '@/store' +import storage from 'store' +import notification from 'ant-design-vue/es/notification' +import { VueAxios } from './axios' +import { ACCESS_TOKEN } from '@/store/mutation-types' +import errorCode from '@/utils/errorCode' + +// 创建 axios 实例 +const request = axios.create({ + // API 请求的默认前缀 + // baseURL: process.env.VUE_APP_BASE_API, + // baseURL: 'http://sapi.sliyun.cn', + // baseURL: 'http://192.168.16.118:8891', + baseURL: 'http://192.168.1.102:8891', + timeout: 6000 // 请求超时时间 +}) + +// 异常拦截处理器 +const errorHandler = (error) => { + console.log('err' + error) + const data = error.response.data + const status = error.response.status + console.log('error status :' + status) + console.log('error:' + data) + let { message } = error + if (error.response) { + if (status === 403) { + } else if (status === 400) { + } else if (error.response.status === 401) { + } else { + } + message = data.msg + } else { + if (message === 'Network Error') { + message = '后端接口连接异常' + } else if (message.includes('timeout')) { + message = '系统接口请求超时' + } else if (message.includes('Request failed with status code')) { + message = '系统接口' + message.substr(message.length - 3) + '异常' + } + } + + notification.error({ + message: message, + duration: 5 * 1000 + }) + return Promise.reject(error) +} + +// request interceptor +request.interceptors.request.use(config => { + const token = storage.get(ACCESS_TOKEN) + // 如果 token 存在 + // 让每个请求携带自定义 token 请根据实际情况自行修改 + if (token) { + config.headers['Authorization'] = 'Bearer ' + token // 让每个请求携带自定义token 请根据实际情况自行修改 + // config.headers['accessAccess-Token'] = token + } + // get请求映射params参数 + if (config.method === 'get' && config.params) { + let url = config.url + '?' + for (const propName of Object.keys(config.params)) { + const value = config.params[propName] + var part = encodeURIComponent(propName) + '=' + if (value && typeof (value) !== 'undefined') { + if (typeof value === 'object') { + for (const key of Object.keys(value)) { + const params = propName + '[' + key + ']' + var subPart = encodeURIComponent(params) + '=' + url += subPart + encodeURIComponent(value[key]) + '&' + } + } else { + url += part + encodeURIComponent(value) + '&' + } + } + } + url = url.slice(0, -1) + config.params = {} + config.url = url + } + return config +}, errorHandler) + +// response interceptor +request.interceptors.response.use((res) => { + // 请求rul + const requestUrl = res.config.url + // 未设置状态码则默认成功状态 + const code = res.data.code || 200 + // 获取错误信息 + const msg = errorCode[code] || res.data.msg || errorCode['default'] + console.log('code:' + code) + console.log('msg:' + msg) + console.log('msg:' + requestUrl) + + if (code === 401) { + notification.open({ + message: '系统提示', + description: '登录状态已过期,请重新登录', + btn: h => { + return h( + 'a-button', + { + props: { + type: 'primary', + size: 'small' + }, + on: { + click: () => { + store.dispatch('Logout').then(() => { + location.href = '/index' + }) + } + } + }, + '确认' + ) + }, + duration: 0, + onClose: close + }) + } else if (code !== 200) { + notification.error({ + message: msg + }) + } else { + return res.data + } + return Promise.reject(msg) +}, errorHandler) + +const installer = { + vm: {}, + install (Vue) { + Vue.use(VueAxios, request) + } +} + +export default request + +export { + installer as VueAxios, + request as axios +} diff --git a/src/utils/requireIcons.js b/src/utils/requireIcons.js new file mode 100644 index 0000000..2705e95 --- /dev/null +++ b/src/utils/requireIcons.js @@ -0,0 +1,9 @@ +const req = require.context('../assets/icons', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys() +const re = /\.\/(.*)\.svg/ + +const icons = requireAll(req).map(i => { + return i.match(re)[1] +}) + +export default icons diff --git a/src/utils/routeConvert.js b/src/utils/routeConvert.js new file mode 100644 index 0000000..e88b0d6 --- /dev/null +++ b/src/utils/routeConvert.js @@ -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 +} diff --git a/src/utils/ruoyi.js b/src/utils/ruoyi.js new file mode 100644 index 0000000..4d1cc17 --- /dev/null +++ b/src/utils/ruoyi.js @@ -0,0 +1,158 @@ +/** + * 通用js方法封装处理 + * Copyright (c) 2019 ruoyi + */ + +const baseURL = process.env.VUE_APP_BASE_API + +// 日期格式化 +export function parseTime (time, pattern) { + if (arguments.length === 0 || !time) { + return null + } + const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { + time = parseInt(time) + } else if (typeof time === 'string') { + time = time.replace(new RegExp(/-/gm), '/') + } + if ((typeof time === 'number') && (time.toString().length === 10)) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const timeStr = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return timeStr +} + +// 表单重置 +export function resetForm (refName) { + if (this[refName]) { + this[refName].resetFields() + } +} + +// 添加日期范围 +export function addDateRange (params, dateRange, propName) { + var search = params + search.params = {} + if (dateRange !== null && dateRange !== '' && dateRange.length === 2) { + if (typeof (propName) === 'undefined') { + search.params['beginTime'] = dateRange[0] + search.params['endTime'] = dateRange[1] + } else { + search.params[propName + 'BeginTime'] = dateRange[0] + search.params[propName + 'EndTime'] = dateRange[1] + } + } + return search +} + +// 回显数据字典 +export function selectDictLabel (datas, value) { + var actions = [] + Object.keys(datas).some((key) => { + if (datas[key].dictValue === ('' + value)) { + actions.push(datas[key].dictLabel) + return true + } + }) + return actions.join('') +} + +// 回显数据字典(字符串数组) +export function selectDictLabels (datas, value, separator) { + var actions = [] + var currentSeparator = undefined === separator ? ',' : separator + var temp = value.split(currentSeparator) + Object.keys(value.split(currentSeparator)).some((val) => { + Object.keys(datas).some((key) => { + if (datas[key].dictValue === ('' + temp[val])) { + actions.push(datas[key].dictLabel + currentSeparator) + } + }) + }) + return actions.join('').substring(0, actions.join('').length - 1) +} + +// 通用下载方法 +export function download (fileName) { + window.location.href = baseURL + '/common/download?fileName=' + encodeURI(fileName) + '&delete=' + true +} + +// 字符串格式化(%s ) +export function sprintf (str) { + var args = arguments + var flag = true + var i = 1 + str = str.replace(/%s/g, function () { + var arg = args[i++] + if (typeof arg === 'undefined') { + flag = false + return '' + } + return arg + }) + return flag ? str : '' +} + +// 转换字符串,undefined,null等转化为'' +export function praseStrEmpty (str) { + if (!str || str === 'undefined' || str === 'null') { + return '' + } + return str +} + +/** + * 构造树型结构数据 + * @param {*} data 数据源 + * @param {*} id id字段 默认 'id' + * @param {*} parentId 父节点字段 默认 'parentId' + * @param {*} children 孩子节点字段 默认 'children' + * @param {*} rootId 根Id 默认 0 + */ +export function handleTree (data, id, parentId, children, rootId) { + id = id || 'id' + parentId = parentId || 'parentId' + children = children || 'children' + rootId = rootId || Math.min.apply(Math, data.map(item => { return item[parentId] })) || 0 + // 对源数据深度克隆 + const cloneData = JSON.parse(JSON.stringify(data)) + // 循环所有项 + const treeData = cloneData.filter(father => { + var branchArr = cloneData.filter(child => { + // 返回每一项的子级数组 + return father[id] === child[parentId] + }) + if (branchArr.length > 0) { + father.children = branchArr + } else { + father.children = '' + } + // 返回第一层 + return father[parentId] === rootId + }) + return treeData !== '' ? treeData : data +} diff --git a/src/utils/screenLog.js b/src/utils/screenLog.js new file mode 100644 index 0000000..04746e7 --- /dev/null +++ b/src/utils/screenLog.js @@ -0,0 +1,20 @@ +/* eslint-disable */ +export const printANSI = () => { + // console.clear() + console.log('[RuoYi-Antdv] created()') + console.log('POWERED BY RuoYi-Vue. Antd-Vue. Antd-Vue-Pro') + // ASCII - ANSI Shadow + let text = ` + 8888888b. Y88b d88P d8b d8888 888 888 + 888 Y88b Y88b d88P Y8P d88888 888 888 + 888 888 Y88o88P d88P888 888 888 + 888 d88P 888 888 .d88b. Y888P 888 d88P 888 88888b. 888888 .d88888 888 888 + 8888888P" 888 888 d88""88b 888 888 d88P 888 888 "88b 888 d88" 888 888 888 + 888 T88b 888 888 888 888 888 888 888888 d88P 888 888 888 888 888 888 Y88 88P + 888 T88b Y88b 888 Y88..88P 888 888 d8888888888 888 888 Y88b. Y88b 888 Y8bd8P + 888 T88b "Y88888 "Y88P" 888 888 d88P 888 888 888 "Y888 "Y88888 Y88P +` + console.log(`%c${text}`, 'color: #fc4d50') + console.log('%cGithub: https://github.com/fuzui/RuoYi-Antdv', 'color: #fff; font-size: 14px; font-weight: 300; text-shadow:#000 1px 0 0,#000 0 1px 0,#000 -1px 0 0,#000 0 -1px 0;') + console.log('%cGithub: https://gitee.com/fuzui/RuoYi-Antdv', 'color: #fff; font-size: 14px; font-weight: 300; text-shadow:#000 1px 0 0,#000 0 1px 0,#000 -1px 0 0,#000 0 -1px 0;') +} diff --git a/src/utils/util.js b/src/utils/util.js new file mode 100644 index 0000000..274ff18 --- /dev/null +++ b/src/utils/util.js @@ -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 = ['休息一会儿吧', '准备吃什么呢?', '要不要打一把 DOTA', '我猜你可能累了'] + 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) +} diff --git a/src/utils/utils.less b/src/utils/utils.less new file mode 100644 index 0000000..ba75a67 --- /dev/null +++ b/src/utils/utils.less @@ -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; + } +} \ No newline at end of file diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 0000000..70460b9 --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,83 @@ +/** + * @param {string} path + * @returns {Boolean} + */ +export function isExternal (path) { + return /^(https?:|mailto:|tel:)/.test(path) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUsername (str) { + const validMap = ['admin', 'editor'] + return validMap.indexOf(str.trim()) >= 0 +} + +/** + * @param {string} url + * @returns {Boolean} + */ +export function validURL (url) { + const reg = /^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~/])+$/ + return reg.test(url) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validLowerCase (str) { + const reg = /^[a-z]+$/ + return reg.test(str) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUpperCase (str) { + const reg = /^[A-Z]+$/ + return reg.test(str) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validAlphabets (str) { + const reg = /^[A-Za-z]+$/ + return reg.test(str) +} + +/** + * @param {string} email + * @returns {Boolean} + */ +export function validEmail (email) { + const reg = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + return reg.test(email) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function isString (str) { + if (typeof str === 'string' || str instanceof String) { + return true + } + return false +} + +/** + * @param {Array} arg + * @returns {Boolean} + */ +export function isArray (arg) { + if (typeof Array.isArray === 'undefined') { + return Object.prototype.toString.call(arg) === '[object Array]' + } + return Array.isArray(arg) +} diff --git a/src/utils/zipdownload.js b/src/utils/zipdownload.js new file mode 100644 index 0000000..0aaf3a2 --- /dev/null +++ b/src/utils/zipdownload.js @@ -0,0 +1,41 @@ +import axios from 'axios' +import storage from 'store' +import { ACCESS_TOKEN } from '@/store/mutation-types' + +const mimeMap = { + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + zip: 'application/zip' +} + +const baseUrl = process.env.VUE_APP_BASE_API +export function downLoadZip (str, filename) { + var url = baseUrl + str + axios({ + method: 'get', + url: url, + responseType: 'blob', + headers: { 'Authorization': 'Bearer ' + storage.get(ACCESS_TOKEN) } + }).then(res => { + resolveBlob(res, mimeMap.zip) + }) +} +/** + * 解析blob响应内容并下载 + * @param {*} res blob响应内容 + * @param {String} mimeType MIME类型 + */ +export function resolveBlob (res, mimeType) { + const aLink = document.createElement('a') + var blob = new Blob([res.data], { type: mimeType }) + // //从response的headers中获取filename, 后端response.setHeader("Content-disposition", "attachment; filename=xxxx.docx") 设置的文件名; + var patt = new RegExp('filename=([^;]+\\.[^\\.;]+);*') + var contentDisposition = decodeURI(res.headers['content-disposition']) + var result = patt.exec(contentDisposition) + var fileName = result[1] + fileName = fileName.replace(/"/g, '') + aLink.href = URL.createObjectURL(blob) + aLink.setAttribute('download', fileName) // 设置下载文件名称 + document.body.appendChild(aLink) + aLink.click() + document.body.appendChild(aLink) +} diff --git a/src/views/404.vue b/src/views/404.vue new file mode 100644 index 0000000..8c1d8a1 --- /dev/null +++ b/src/views/404.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/src/views/account/center/index.vue b/src/views/account/center/index.vue new file mode 100644 index 0000000..053aded --- /dev/null +++ b/src/views/account/center/index.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/src/views/account/center/page/App.vue b/src/views/account/center/page/App.vue new file mode 100644 index 0000000..853aeab --- /dev/null +++ b/src/views/account/center/page/App.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/views/account/center/page/Article.vue b/src/views/account/center/page/Article.vue new file mode 100644 index 0000000..e5266b6 --- /dev/null +++ b/src/views/account/center/page/Article.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/views/account/center/page/Project.vue b/src/views/account/center/page/Project.vue new file mode 100644 index 0000000..f741539 --- /dev/null +++ b/src/views/account/center/page/Project.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/src/views/account/center/page/index.js b/src/views/account/center/page/index.js new file mode 100644 index 0000000..b579b6a --- /dev/null +++ b/src/views/account/center/page/index.js @@ -0,0 +1,5 @@ +import AppPage from './App' +import ArticlePage from './Article' +import ProjectPage from './Project' + +export { AppPage, ArticlePage, ProjectPage } diff --git a/src/views/account/settings/AvatarModal.vue b/src/views/account/settings/AvatarModal.vue new file mode 100644 index 0000000..be1747e --- /dev/null +++ b/src/views/account/settings/AvatarModal.vue @@ -0,0 +1,183 @@ + + + + diff --git a/src/views/account/settings/BaseSetting.vue b/src/views/account/settings/BaseSetting.vue new file mode 100644 index 0000000..909cf56 --- /dev/null +++ b/src/views/account/settings/BaseSetting.vue @@ -0,0 +1,192 @@ + + + + + diff --git a/src/views/account/settings/Security.vue b/src/views/account/settings/Security.vue new file mode 100644 index 0000000..da0587e --- /dev/null +++ b/src/views/account/settings/Security.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/views/account/settings/UpdatePassword.vue b/src/views/account/settings/UpdatePassword.vue new file mode 100644 index 0000000..2249729 --- /dev/null +++ b/src/views/account/settings/UpdatePassword.vue @@ -0,0 +1,121 @@ + + diff --git a/src/views/account/settings/index.vue b/src/views/account/settings/index.vue new file mode 100644 index 0000000..17b47fb --- /dev/null +++ b/src/views/account/settings/index.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/src/views/ad/AdList.vue b/src/views/ad/AdList.vue new file mode 100644 index 0000000..caea2cc --- /dev/null +++ b/src/views/ad/AdList.vue @@ -0,0 +1,267 @@ + + + + + diff --git a/src/views/ad/modules/AdCreateForm.vue b/src/views/ad/modules/AdCreateForm.vue new file mode 100644 index 0000000..2b69dcd --- /dev/null +++ b/src/views/ad/modules/AdCreateForm.vue @@ -0,0 +1,130 @@ + + + diff --git a/src/views/coin/CoinAddressList.vue b/src/views/coin/CoinAddressList.vue new file mode 100644 index 0000000..2188e1d --- /dev/null +++ b/src/views/coin/CoinAddressList.vue @@ -0,0 +1,183 @@ + + + + diff --git a/src/views/coin/CoinHashPackageList.vue b/src/views/coin/CoinHashPackageList.vue new file mode 100644 index 0000000..5849f59 --- /dev/null +++ b/src/views/coin/CoinHashPackageList.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/src/views/coin/CoinList.vue b/src/views/coin/CoinList.vue new file mode 100644 index 0000000..7267e3e --- /dev/null +++ b/src/views/coin/CoinList.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/src/views/coin/modules/CoinPackageCreateForm.vue b/src/views/coin/modules/CoinPackageCreateForm.vue new file mode 100644 index 0000000..3393486 --- /dev/null +++ b/src/views/coin/modules/CoinPackageCreateForm.vue @@ -0,0 +1,138 @@ + + + diff --git a/src/views/exception/403.vue b/src/views/exception/403.vue new file mode 100644 index 0000000..fb1bf36 --- /dev/null +++ b/src/views/exception/403.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/views/exception/404.vue b/src/views/exception/404.vue new file mode 100644 index 0000000..3142e7b --- /dev/null +++ b/src/views/exception/404.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/views/exception/500.vue b/src/views/exception/500.vue new file mode 100644 index 0000000..2770b77 --- /dev/null +++ b/src/views/exception/500.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/views/finance/OrderList.vue b/src/views/finance/OrderList.vue new file mode 100644 index 0000000..de66576 --- /dev/null +++ b/src/views/finance/OrderList.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/src/views/finance/PaymentRecord.vue b/src/views/finance/PaymentRecord.vue new file mode 100644 index 0000000..76038cf --- /dev/null +++ b/src/views/finance/PaymentRecord.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/src/views/finance/modules/UpdateOrderPayPrice.vue b/src/views/finance/modules/UpdateOrderPayPrice.vue new file mode 100644 index 0000000..6b09021 --- /dev/null +++ b/src/views/finance/modules/UpdateOrderPayPrice.vue @@ -0,0 +1,97 @@ + + + diff --git a/src/views/index.less b/src/views/index.less new file mode 100644 index 0000000..005b73a --- /dev/null +++ b/src/views/index.less @@ -0,0 +1,107 @@ +@import '~ant-design-vue/es/style/themes/default.less'; + +.text-overflow() { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + word-break: break-all; +} + +// mixins for clearfix +// ------------------------ +.clearfix() { + zoom: 1; + &::before, + &::after { + display: table; + content: ' '; + } + &::after { + clear: both; + height: 0; + font-size: 0; + visibility: hidden; + } +} + + +.page-header-content { + display: flex; + + .avatar { + flex: 0 1 72px; + + & > span { + display: block; + width: 72px; + height: 72px; + border-radius: 72px; + } + } + + .content { + position: relative; + top: 4px; + flex: 1 1 auto; + margin-left: 24px; + color: @text-color-secondary; + line-height: 22px; + + .content-title { + margin-bottom: 12px; + color: @heading-color; + font-weight: 500; + font-size: 20px; + line-height: 28px; + } + } +} + +.extra-content { + .clearfix(); + float: right; + white-space: nowrap; + + .stat-item { + position: relative; + display: inline-block; + padding: 0 32px; + + > p:first-child { + margin-bottom: 4px; + color: @text-color-secondary; + font-size: @font-size-base; + line-height: 22px; + } + + > p { + margin: 0; + color: @heading-color; + font-size: 30px; + line-height: 38px; + + > span { + color: @text-color-secondary; + font-size: 20px; + } + } + + &::after { + position: absolute; + top: 8px; + right: 0; + width: 1px; + height: 40px; + background-color: @border-color-split; + content: ''; + } + + &:last-child { + padding-right: 0; + + &::after { + display: none; + } + } + } +} \ No newline at end of file diff --git a/src/views/index.vue b/src/views/index.vue new file mode 100644 index 0000000..5824770 --- /dev/null +++ b/src/views/index.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/views/member/MemberList.vue b/src/views/member/MemberList.vue new file mode 100644 index 0000000..b405c80 --- /dev/null +++ b/src/views/member/MemberList.vue @@ -0,0 +1,421 @@ + + + + + diff --git a/src/views/member/modules/AddMemberFrom.vue b/src/views/member/modules/AddMemberFrom.vue new file mode 100644 index 0000000..523dbea --- /dev/null +++ b/src/views/member/modules/AddMemberFrom.vue @@ -0,0 +1,151 @@ + + + diff --git a/src/views/monitor/cache/index.vue b/src/views/monitor/cache/index.vue new file mode 100644 index 0000000..d49cb1a --- /dev/null +++ b/src/views/monitor/cache/index.vue @@ -0,0 +1,148 @@ + + + diff --git a/src/views/monitor/druid/index.vue b/src/views/monitor/druid/index.vue new file mode 100644 index 0000000..51d74f2 --- /dev/null +++ b/src/views/monitor/druid/index.vue @@ -0,0 +1,42 @@ +