├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── jsconfig.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── assets │ └── images │ │ ├── bg-account-layout.jpg │ │ ├── logo.png │ │ └── temp-image.png ├── components │ ├── ViaFadeTransform.vue │ ├── ViaScreenfull.vue │ └── ViaSvgIcon.vue ├── config │ ├── devServer.js │ ├── dict.js │ ├── index.js │ ├── menuRight.js │ ├── request.js │ ├── sidebar.js │ └── tailwind │ │ ├── index.js │ │ ├── rem.js │ │ └── utils.js ├── directive │ ├── clampAuto.js │ ├── clickAway.js │ ├── index.js │ └── observer.js ├── icons │ ├── index.js │ ├── svg │ │ ├── bottom.svg │ │ ├── edit.svg │ │ ├── exit-fullscreen.svg │ │ ├── fold.svg │ │ ├── fullscreen.svg │ │ ├── home.svg │ │ ├── merchant.svg │ │ ├── password.svg │ │ ├── promote.svg │ │ ├── search.svg │ │ ├── unfold.svg │ │ ├── user.svg │ │ ├── userAdmin.svg │ │ └── withdraw.svg │ └── svgo.yml ├── main.js ├── mixins │ ├── index.js │ ├── screenResize.js │ └── windowSize │ │ ├── index.js │ │ ├── plugin.js │ │ └── spare.js ├── plugins │ ├── element-ui │ │ ├── all.js │ │ ├── index.js │ │ ├── index.scss │ │ └── single.js │ ├── encrypt │ │ ├── index.js │ │ └── jsencrypt.js │ ├── modal │ │ ├── element-plus.js │ │ ├── index.js │ │ └── vant.js │ ├── request │ │ ├── axios-encrypt.js │ │ ├── axios.js │ │ ├── index.js │ │ └── utils.js │ ├── router │ │ ├── index.js │ │ ├── new.js │ │ ├── old.js │ │ └── utils.js │ └── storages │ │ ├── index.js │ │ └── universal.js ├── request │ └── index.js ├── router │ └── index.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── demo.js │ │ ├── site.js │ │ ├── sms.js │ │ ├── tagsView.js │ │ └── user.js ├── styles │ ├── css │ │ ├── base.css │ │ ├── element-ui.css │ │ ├── index.css │ │ ├── mobile.css │ │ ├── pc.css │ │ └── tailwind.css │ └── scss │ │ ├── element-ui │ │ ├── base.scss │ │ ├── custom.scss │ │ └── index.scss │ │ └── index.scss ├── utils │ ├── dom.js │ ├── index.js │ └── vue.js └── views │ ├── account │ ├── components │ │ └── Layout │ │ │ └── index.vue │ ├── index.vue │ └── login │ │ └── index.vue │ ├── home │ ├── components │ │ └── Layout │ │ │ ├── MainContent │ │ │ └── index.vue │ │ │ ├── Navbar │ │ │ ├── Breadcrumb.vue │ │ │ ├── MenuRight.vue │ │ │ ├── QuickBar.vue │ │ │ ├── TagsView │ │ │ │ ├── ScrollPane.vue │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ │ ├── Sidebar │ │ │ ├── Icon.vue │ │ │ ├── Item.vue │ │ │ ├── Logo.vue │ │ │ └── index.vue │ │ │ └── index.vue │ ├── demo │ │ └── index.vue │ ├── index.vue │ └── test │ │ └── index.vue │ └── index.vue ├── tailwind.config.js ├── vue.config.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | "plugin:vue/recommended", 8 | "plugin:vue/vue3-essential", 9 | "plugin:vue/vue3-strongly-recommended", 10 | "eslint:recommended", 11 | ], 12 | parserOptions: { 13 | parser: "babel-eslint", 14 | }, 15 | rules: { 16 | "vue/no-v-for-template-key": "off", 17 | "vue/no-v-model-argument": "off", 18 | "vue/max-attributes-per-line": [ 19 | "error", 20 | { 21 | singleline: 1, 22 | multiline: { 23 | max: 1, 24 | allowFirstLine: false, 25 | }, 26 | }, 27 | ], 28 | "vue/no-unused-components": "warn", 29 | "vue/singleline-html-element-content-newline": "off", 30 | "vue/html-self-closing": "off", 31 | "vue/valid-template-root": "off", 32 | "vue/no-multiple-template-root": "off", 33 | "no-console": "off", 34 | "no-debugger": "off", 35 | "no-empty": "warn", 36 | "no-unused-vars": "warn", 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 viarotel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-admin-cli 2 | 3 | 基于vue3.x的中后台管理系统,router已配置路由表自动生成(可通过混入选项更改个别的路由配置) ui库使用 element-plus(已配置按需导入), css 框架使用 tailwindcss(下一代css框架), 请求使用axios的方式(完善了aes+rsa接口加密功能) [查看预览地址(用户名密码不为空即可)](https://vue-admin-cli.vercel.app/) 4 | 5 | 6 | 7 | [![Contributors][contributors-shield]][contributors-url] 8 | [![Forks][forks-shield]][forks-url] 9 | [![Stargazers][stars-shield]][stars-url] 10 | [![Issues][issues-shield]][issues-url] 11 | [![MIT License][license-shield]][license-url] 12 | 13 | 14 | 15 |
16 | 17 |

18 | 19 | viarotel 20 | 21 |

vue-admin-cli

22 |

23 | 基于vue3.x的中后台管理系统(elementPlus+tailwindcss) 24 |
25 | 探索本项目的文档 » 26 |
27 |
28 | 查看Demo 29 | · 30 | 报告Bug 31 | · 32 | 提出新特性 33 |

34 | 35 | 36 | ## 目录 37 | 38 | - [上手指南](#上手指南) 39 | - [获取本项目](#获取本项目) 40 | - [运行本项目](#运行本项目) 41 | - [打包构建](#打包构建) 42 | - [文件目录说明](#文件目录说明) 43 | - [使用到的框架](#使用到的框架) 44 | - [版本控制](#版本控制) 45 | - [作者](#作者) 46 | - [鸣谢](#鸣谢) 47 | 48 | ### 上手指南 49 | 50 | ###### **获取本项目** 51 | 52 | 1. clone 本项目 或 直接下载main包 53 | 54 | ```sh 55 | git clone https://github.com/viarotel/vue-admin-cli.git 56 | ``` 57 | 58 | ###### 运行本项目 59 | 60 | 1. 安装依赖 61 | 2. 运行项目 62 | 63 | ```sh 64 | npm install //or yarn 65 | npm run serve //or yarn serve 66 | ``` 67 | 68 | ###### 打包构建 69 | 70 | 1. 使用命令打包项目 71 | 2. 将dist中的文件放到服务器中 72 | 73 | ```sh 74 | npm build //or yarn build 75 | ``` 76 | 77 | ### 文件目录说明 78 | 79 | ``` 80 | filetree 81 | ├── /dist //打包生成的静态资源文件,用于生产部署。 82 | ├── /node_modules //存放npm命令下载的开发环境和生产环境的依赖包。 83 | ├── /public/ //存放在该文件夹的东西不会被打包影响,而是会原封不动的输出到dist文件夹中 84 | │ ├── /favicon.ico //浏览器中显示的图标 85 | │ ├── /index.html // 入口模板文件 86 | ├── /src/ 87 | │ ├── /assets/ //存放项目中需要用到的资源文件,css、js、images等。 88 | │ ├── /components/ //存放vue开发中一些公共组件 89 | │ ├── /config/ //全局配置文件 90 | │ ├── /directive/ //公共vue指令 91 | │ ├── /icons/ //存放svg图标 92 | │ ├── /mixins/ //公共vue混入 93 | │ ├── /plugins/ //项目用到的插件集合 94 | │ ├── /request/ //接口配置 95 | │ ├── /router/ //路由表 96 | │ ├── /store/ //vuex状态管理 97 | │ ├── /styles/ //公共样式文件 98 | │ ├── /utils/ //存放vue开发过程中一些公共的js方法。 99 | │ ├── /store/ //vuex状态管理 100 | │ ├── /views/ //存放.vue视图文件 101 | │ ├── /main.js //入口文件 102 | ├── .gitignore //git忽略文件配置 103 | ├── babel.config.js //对js文件进行编译转换增强的配置文件 104 | ├── jsconfig.json /JavaScript语言服务的配置文件 代码提示 文件索引提示等 105 | ├── LICENSE //开源许可说明 106 | ├── package.json //包管理配置文件 107 | ├── postcss.config.js //对css文件进行编译转换增强的配置文件 108 | ├── README.md 109 | ├── tailwind.config.js //tailwindcss的配置文件 110 | ├── vue.config.js //vuecli配置文件 111 | └── yarn.lock //yarn锁定依赖版本 防止环境不一致导致项目无法运行的问题 112 | ``` 113 | 114 | ### 使用到的框架 115 | 116 | - [Vue-CLI](https://cli.vuejs.org) 117 | - [element-plus](https://element-plus.org/) 118 | - [tailwindcss](https://www.tailwindcss.cn/) 119 | - [axios](http://www.axios-js.com/) 120 | 121 | ### 关键字 122 | 123 | - vue3.x 124 | - element-plus 125 | - tailwindcss 126 | - axios 127 | 128 | ### 版本控制 129 | 130 | 该项目使用Git进行版本管理。 131 | 132 | ### 作者 133 | 134 | viarotel@qq.com 135 | 136 | qq:523469508 wx: luyao-ing 137 | 138 | *您也可以在贡献者名单中参看所有参与该项目的开发者。* 139 | 140 | ### 版权说明 141 | 142 | 该项目签署了MIT 授权许可,详情请参阅 [LICENSE](LICENSE) 143 | 144 | ### 鸣谢 145 | 146 | 147 | - 感谢[PanJiaChen/vue-element-admin](https://github.com/PanJiaChen/vue-element-admin/) 带给我灵感 148 | 149 | 150 | 151 | [your-project-path]:viarotel/vue-admin-cli 152 | [contributors-shield]: https://img.shields.io/github/contributors/viarotel/vue-admin-cli.svg?style=flat-square 153 | [contributors-url]: https://github.com/viarotel/vue-admin-cli/graphs/contributors 154 | [forks-shield]: https://img.shields.io/github/forks/viarotel/vue-admin-cli.svg?style=flat-square 155 | [forks-url]: https://github.com/viarotel/vue-admin-cli/network/members 156 | [stars-shield]: https://img.shields.io/github/stars/viarotel/vue-admin-cli.svg?style=flat-square 157 | [stars-url]: https://github.com/viarotel/vue-admin-cli/stargazers 158 | [issues-shield]: https://img.shields.io/github/issues/viarotel/vue-admin-cli.svg?style=flat-square 159 | [issues-url]: https://img.shields.io/github/issues/viarotel/vue-admin-cli.svg 160 | [license-shield]: https://img.shields.io/github/license/viarotel/vue-admin-cli.svg?style=flat-square 161 | [license-url]: https://github.com/viarotel/vue-admin-cli/blob/master/LICENSE 162 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555 163 | [linkedin-url]: https://linkedin.com/in/viarotel -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const isProduction = process.env.NODE_ENV === "production"; 2 | // const isDevelopment = process.env.NODE_ENV === 'development'; 3 | 4 | const plugins = [ 5 | [ 6 | "component", 7 | { 8 | libraryName: "element-plus", 9 | libDir: "lib", 10 | style: false 11 | } 12 | ] 13 | ]; 14 | if (isProduction) { 15 | plugins.push("transform-remove-console"); // 生产环境移除console 16 | } 17 | 18 | module.exports = { 19 | plugins, 20 | presets: ["@vue/cli-plugin-babel/preset"] 21 | }; -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "experimentalDecorators": true, 5 | "baseUrl": "./", 6 | "paths": { 7 | "@/*": ["src/*"] 8 | } 9 | }, 10 | "exclude": ["node_modules", "dist"], 11 | "include": ["src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cli", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.21.1", 13 | "core-js": "^3.6.5", 14 | "crypto-js": "^4.0.0", 15 | "element-plus": "^1.0.1-beta.21", 16 | "jsencrypt": "^3.0.0-rc.1", 17 | "lodash-es": "^4.17.20", 18 | "nprogress": "^0.2.0", 19 | "screenfull": "^5.1.0", 20 | "store": "^2.0.12", 21 | "v-contextmenu": "^3.0.0-alpha.4", 22 | "vue": "^3.0.0", 23 | "vue-router": "^4.0.0-0", 24 | "vue-window-size": "^1.0.3", 25 | "vuex": "^4.0.0-0" 26 | }, 27 | "devDependencies": { 28 | "@tailwindcss/postcss7-compat": "^2.0.1", 29 | "@vue/cli-plugin-babel": "~4.5.0", 30 | "@vue/cli-plugin-eslint": "~4.5.0", 31 | "@vue/cli-plugin-router": "~4.5.0", 32 | "@vue/cli-plugin-vuex": "~4.5.0", 33 | "@vue/cli-service": "~4.5.0", 34 | "@vue/compiler-sfc": "^3.0.0", 35 | "autoprefixer": "^9", 36 | "babel-eslint": "^10.1.0", 37 | "babel-plugin-component": "^1.1.1", 38 | "babel-plugin-transform-remove-console": "^6.9.4", 39 | "eslint": "^6.7.2", 40 | "eslint-plugin-vue": "^7.0.0-0", 41 | "postcss": "^7", 42 | "sass": "^1.26.5", 43 | "sass-loader": "^8.0.2", 44 | "script-ext-html-webpack-plugin": "^2.1.5", 45 | "svg-sprite-loader": "^5.2.1", 46 | "tailwindcss": "npm:@tailwindcss/postcss7-compat", 47 | "tailwindcss-line-clamp": "^1.0.5", 48 | "tailwindcss-textshadow": "^2.0.0" 49 | }, 50 | "browserslist": [ 51 | "> 1%", 52 | "last 2 versions", 53 | "not dead" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viarotel-org/vue-admin-cli/0d8462be38492319ddbd91b24ede79ab92b266c1/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/images/bg-account-layout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viarotel-org/vue-admin-cli/0d8462be38492319ddbd91b24ede79ab92b266c1/src/assets/images/bg-account-layout.jpg -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viarotel-org/vue-admin-cli/0d8462be38492319ddbd91b24ede79ab92b266c1/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/temp-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viarotel-org/vue-admin-cli/0d8462be38492319ddbd91b24ede79ab92b266c1/src/assets/images/temp-image.png -------------------------------------------------------------------------------- /src/components/ViaFadeTransform.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 44 | -------------------------------------------------------------------------------- /src/components/ViaScreenfull.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/components/ViaSvgIcon.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 51 | 52 | 67 | -------------------------------------------------------------------------------- /src/config/devServer.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 7777, 3 | proxyUrl: "https://api.ipify.org/", 4 | apiRoot: "api" 5 | }; -------------------------------------------------------------------------------- /src/config/dict.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | router: [ 3 | { dictLabel: "首页", dictValue: "home" }, 4 | { dictLabel: "实例", dictValue: "demo" }, 5 | { dictLabel: "测试", dictValue: "test" }, 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: "vue-admin-cli", 3 | logo: () => require("@/assets/images/logo.png"), 4 | isSidebarLogo: true, 5 | theme: { 6 | // 主题色 7 | "$--color-primary": "#44BA81", 8 | }, 9 | }; -------------------------------------------------------------------------------- /src/config/menuRight.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: "test", 4 | }, 5 | { 6 | name: "demo", 7 | }, 8 | { 9 | name: "login", 10 | query: {}, 11 | divided: true, 12 | logout: true, 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /src/config/request.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | //加密 3 | encrypt: { 4 | on: false, 5 | publicKey: "", 6 | iv: "", 7 | toBase64: false 8 | }, 9 | //请求域名 10 | // baseUrl: "https://api.ipify.org/", 11 | baseUrl: process.env.BASE_URL + "api/", 12 | authorization: { 13 | key: "Authorization", 14 | prefix: "Bearer " 15 | }, 16 | //响应成功code值 17 | responseSuccessCode: 10000, 18 | //超时时间 19 | timeout: 20000 20 | }; 21 | -------------------------------------------------------------------------------- /src/config/sidebar.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | // id: "2", 4 | name: "demo", 5 | icon: "el-icon-edit", 6 | query: {}, 7 | }, 8 | { 9 | // id: "1", 10 | name: "test", 11 | icon: "fold", 12 | query: {}, 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /src/config/tailwind/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./rem"); 2 | -------------------------------------------------------------------------------- /src/config/tailwind/rem.js: -------------------------------------------------------------------------------- 1 | const { colors, mapClass, baseConfig } = require("./utils"); 2 | // const isProduction = process.env.NODE_ENV === "production"; 3 | 4 | module.exports = { 5 | ...baseConfig, 6 | theme: { 7 | extend: { 8 | colors: { 9 | ...colors, 10 | }, 11 | fontSize: { 12 | "3xs": "0.125rem", 13 | "2xs": "0.5rem", 14 | }, 15 | flexGrow: { 16 | // '2': '2' 17 | ...mapClass("$:$", 2, 10), 18 | }, 19 | borderRadius: { 20 | // 'px-5': '5px' 21 | ...mapClass("px-$:$px", 5, 25, 1, 5), 22 | }, 23 | spacing: { 24 | // 'px-2': '2px', 25 | ...mapClass("px-$:$px", 2, 10), 26 | 27 | // '1': '0.25rem', 28 | ...mapClass("$:$rem", 1, 10, 0.25), 29 | 30 | // '12': '3rem' 31 | ...mapClass("$:$rem", 12, 300, 0.25, 2), 32 | }, 33 | inset: { 34 | "1/2": "50%", 35 | "-1/2": "-50%", 36 | }, 37 | minWidth: { 38 | // 'px-2': '2px', 39 | ...mapClass("px-$:$px", 2, 10), 40 | 41 | // '1': '0.25rem', 42 | ...mapClass("$:$rem", 1, 10, 0.25), 43 | 44 | // '12': '3rem' 45 | ...mapClass("$:$rem", 12, 300, 0.25, 2), 46 | }, 47 | minHeight: { 48 | // 'px-2': '2px', 49 | ...mapClass("px-$:$px", 2, 10), 50 | 51 | // '1': '0.25rem', 52 | ...mapClass("$:$rem", 1, 10, 0.25), 53 | 54 | // '12': '3rem' 55 | ...mapClass("$:$rem", 12, 300, 0.25, 2), 56 | }, 57 | backgroundSize: { 58 | // 'w-1': '0.25rem auto', 59 | ...mapClass("w-$:$rem 100%", 1, 10, 0.25), 60 | // 'w-12': '3rem auto', 61 | ...mapClass("w-$:$rem 100%", 12, 100, 0.25, 2), 62 | 63 | // 'h-1': 'auto 0.25rem', 64 | ...mapClass("h-$:100% $rem", 1, 10, 0.25), 65 | // 'h-12': 'auto 3rem' 66 | ...mapClass("h-$:100% $rem", 12, 100, 0.25, 2), 67 | 68 | "w-full": "100% auto", 69 | "h-full": "auto 100%", 70 | full: "100% 100%", 71 | }, 72 | }, 73 | ...baseConfig.theme, 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /src/config/tailwind/utils.js: -------------------------------------------------------------------------------- 1 | const isProduction = process.env.NODE_ENV === "production"; 2 | 3 | const baseConfig = { 4 | important: true, 5 | prefix: "", 6 | corePlugins: { 7 | // preflight: false, 8 | }, 9 | purge: { 10 | enabled: isProduction, 11 | preserveHtmlElements: true, 12 | content: [ 13 | "./src/components/**/*.vue", 14 | "./src/views/**/*.vue", 15 | "./main.js", 16 | "./vue.config.js", 17 | ], 18 | options: { 19 | safelist: [/^el-/], 20 | blocklist: [], 21 | keyframes: true, 22 | fontFace: false, 23 | }, 24 | }, 25 | theme: { 26 | // 字体阴影 text-shadow-xs 27 | textShadow: { 28 | none: "none", 29 | DEFAULT: "0px 0px 1px rgb(0 0 0 / 20%), 0px 0px 1px rgb(1 0 5 / 10%)", 30 | sm: "1px 1px 3px rgb(36 37 47 / 25%)", 31 | md: "0px 1px 2px rgb(30 29 39 / 19%), 1px 2px 4px rgb(54 64 147 / 18%)", 32 | lg: "3px 3px 6px rgb(0 0 0 / 26%), 0 0 5px rgb(15 3 86 / 22%)", 33 | xl: "1px 1px 3px rgb(0 0 0 / 29%), 2px 4px 7px rgb(73 64 125 / 35%)", 34 | "2xl": "1px 1px 5px rgb(33 34 43 / 20%)", 35 | "3xl": "0 0 3px rgba(0, 0, 0, .8), 0 0 5px rgba(0, 0, 0, .9)", 36 | }, 37 | // 文字截断 .clamp-1 38 | lineClamp: { 39 | 1: 1, 40 | 2: 2, 41 | 3: 3, 42 | 4: 4, 43 | 5: 5, 44 | }, 45 | }, 46 | plugins: [ 47 | require("tailwindcss-textshadow"), // 文字阴影 48 | require("tailwindcss-line-clamp"), // 文字截断 49 | ], 50 | variants: {}, 51 | }; 52 | 53 | const colors = { 54 | red: { 55 | DEFAULT: "red", 56 | 100: "#ffcdd2", 57 | 200: "#ef9a9a", 58 | 300: "#e57373", 59 | 400: "#ef5350", 60 | 500: "#f44336", 61 | 600: "#e53935", 62 | 700: "#d32f2f", 63 | 800: "#c62828", 64 | 900: "#b71c1c", 65 | }, 66 | pink: { 67 | DEFAULT: "pink", 68 | 100: "#F8BBD0", 69 | 200: "#F48FB1", 70 | 300: "#F06292", 71 | 400: "#EC407A", 72 | 500: "#E91E63", 73 | 600: "#D81B60", 74 | 700: "#C2185B", 75 | 800: "#AD1457", 76 | 900: "#880E4F", 77 | }, 78 | purple: { 79 | DEFAULT: "purple", 80 | 100: "#E1BEE7", 81 | 200: "#CE93D8", 82 | 300: "#BA68C8", 83 | 400: "#AB47BC", 84 | 500: "#9C27B0", 85 | 600: "#8E24AA", 86 | 700: "#7B1FA2", 87 | 800: "#6A1B9A", 88 | 900: "#4A148C", 89 | }, 90 | "deep-purple": { 91 | DEFAULT: "#673AB7", 92 | 100: "#D1C4E9", 93 | 200: "#B39DDB", 94 | 300: "#9575CD", 95 | 400: "#7E57C2", 96 | 500: "#673AB7", 97 | 600: "#5E35B1", 98 | 700: "#512DA8", 99 | 800: "#4527A0", 100 | 900: "#311B92", 101 | }, 102 | indigo: { 103 | DEFAULT: "indigo", 104 | 100: "#C5CAE9", 105 | 200: "#9FA8DA", 106 | 300: "#7986CB", 107 | 400: "#5C6BC0", 108 | 500: "#3F51B5", 109 | 600: "#3949AB", 110 | 700: "#303F9F", 111 | 800: "#283593", 112 | 900: "#1A237E", 113 | }, 114 | blue: { 115 | DEFAULT: "blue", 116 | 100: "#BBDEFB", 117 | 200: "#90CAF9", 118 | 300: "#64B5F6", 119 | 400: "#42A5F5", 120 | 500: "#2196F3", 121 | 600: "#1E88E5", 122 | 700: "#1976D2", 123 | 800: "#1565C0", 124 | 900: "#0D47A1", 125 | }, 126 | "light-blue": { 127 | DEFAULT: "lightblue", 128 | 100: "#B3E5FC", 129 | 200: "#81D4FA", 130 | 300: "#4FC3F7", 131 | 400: "#29B6F6", 132 | 500: "#03A9F4", 133 | 600: "#039BE5", 134 | 700: "#0288D1", 135 | 800: "#0277BD", 136 | 900: "#01579B", 137 | }, 138 | cyan: { 139 | DEFAULT: "cyan", 140 | 100: "#B2EBF2", 141 | 200: "#80DEEA", 142 | 300: "#4DD0E1", 143 | 400: "#26C6DA", 144 | 500: "#00BCD4", 145 | 600: "#00ACC1", 146 | 700: "#0097A7", 147 | 800: "#00838F", 148 | 900: "#006064", 149 | }, 150 | teal: { 151 | DEFAULT: "teal", 152 | 100: "#B2DFDB", 153 | 200: "#80CBC4", 154 | 300: "#4DB6AC", 155 | 400: "#26A69A", 156 | 500: "#009688", 157 | 600: "#00897B", 158 | 700: "#00796B", 159 | 800: "#00695C", 160 | 900: "#004D40", 161 | }, 162 | green: { 163 | DEFAULT: "green", 164 | 100: "#C8E6C9", 165 | 200: "#A5D6A7", 166 | 300: "#81C784", 167 | 400: "#66BB6A", 168 | 500: "#4CAF50", 169 | 600: "#43A047", 170 | 700: "#388E3C", 171 | 800: "#2E7D32", 172 | 900: "#1B5E20", 173 | }, 174 | "light-greeen": { 175 | DEFAULT: "lightgreen", 176 | 100: "#DCEDC8", 177 | 200: "#C5E1A5", 178 | 300: "#AED581", 179 | 400: "#9CCC65", 180 | 500: "#8BC34A", 181 | 600: "#7CB342", 182 | 700: "#689F38", 183 | 800: "#558B2F", 184 | 900: "#33691E", 185 | }, 186 | lime: { 187 | DEFAULT: "lime", 188 | 100: "#F0F4C3", 189 | 200: "#E6EE9C", 190 | 300: "#DCE775", 191 | 400: "#D4E157", 192 | 500: "#CDDC39", 193 | 600: "#C0CA33", 194 | 700: "#AFB42B", 195 | 800: "#9E9D24", 196 | 900: "#827717", 197 | }, 198 | yellow: { 199 | DEFAULT: "yellow", 200 | 100: "#FFF9C4", 201 | 200: "#FFF59D", 202 | 300: "#FFF176", 203 | 400: "#FFEE58", 204 | 500: "#FFEB3B", 205 | 600: "#FDD835", 206 | 700: "#FBC02D", 207 | 800: "#F9A825", 208 | 900: "#F57F17", 209 | }, 210 | amber: { 211 | DEFAULT: "#FFC107", 212 | 100: "#FFECB3", 213 | 200: "#FFE082", 214 | 300: "#FFD54F", 215 | 400: "#FFCA28", 216 | 500: "#FFC107", 217 | 600: "#FFB300", 218 | 700: "#FFA000", 219 | 800: "#FF8F00", 220 | 900: "#FF6F00", 221 | }, 222 | orange: { 223 | DEFAULT: "orange", 224 | 100: "#FFE0B2", 225 | 200: "#FFCC80", 226 | 300: "#FFB74D", 227 | 400: "#FFA726", 228 | 500: "#FF9800", 229 | 600: "#FB8C00", 230 | 700: "#F57C00", 231 | 800: "#EF6C00", 232 | 900: "#E65100", 233 | }, 234 | "deep-orange": { 235 | DEFAULT: "#FF5722", 236 | 100: "#FFCCBC", 237 | 200: "#FFAB91", 238 | 300: "#FF8A65", 239 | 400: "#FF7043", 240 | 500: "#FF5722", 241 | 600: "#F4511E", 242 | 700: "#E64A19", 243 | 800: "#D84315", 244 | 900: "#BF360C", 245 | }, 246 | brown: { 247 | DEFAULT: "brown", 248 | 100: "#D7CCC8", 249 | 200: "#BCAAA4", 250 | 300: "#A1887F", 251 | 400: "#8D6E63", 252 | 500: "#795548", 253 | 600: "#6D4C41", 254 | 700: "#5D4037", 255 | 800: "#4E342E", 256 | 900: "#3E2723", 257 | }, 258 | gray: { 259 | DEFAULT: "gray", 260 | 100: "#F5F5F5", 261 | 200: "#EEEEEE", 262 | 300: "#E0E0E0", 263 | 400: "#BDBDBD", 264 | 500: "#9E9E9E", 265 | 600: "#757575", 266 | 700: "#616161", 267 | 800: "#424242", 268 | 900: "#212121", 269 | }, 270 | "blue-gray": { 271 | DEFAULT: "#607D8B", 272 | 100: "#CFD8DC", 273 | 200: "#B0BEC5", 274 | 300: "#90A4AE", 275 | 400: "#78909C", 276 | 500: "#607D8B", 277 | 600: "#546E7A", 278 | 700: "#455A64", 279 | 800: "#37474F", 280 | 900: "#263238", 281 | }, 282 | }; 283 | 284 | /** 285 | * @todo 286 | * @desc 根据提供的模板生成自定义样式所需要的数据 287 | * @param {*} template 288 | * @param {*} start 289 | * @param {*} end 290 | * @param {*} baseValue 291 | * @param {*} step 292 | */ 293 | const mapClass = function(template, start, end, baseValue = 1, step = 1) { 294 | const mapReplace = (target, value, rule = /\$/g) => 295 | target.replace(rule, value); 296 | const index = template.indexOf(":"); 297 | const key = template.slice(0, index); 298 | const value = template.slice(index + 1); 299 | const tempObj = {}; 300 | for (start; start <= end; start += step) { 301 | tempObj[mapReplace(key, start)] = mapReplace(value, start * baseValue); 302 | } 303 | // console.log(tempObj); 304 | return tempObj; 305 | }; 306 | 307 | module.exports = { 308 | baseConfig, 309 | colors, 310 | mapClass, 311 | }; 312 | -------------------------------------------------------------------------------- /src/directive/clampAuto.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc 指令 文本超出显示省略号 v-text-clamp:[arg]="value" 3 | * @param arg arg 为行数 4 | * @param value value为最多显示value个文字 5 | */ 6 | export function clampAuto(el, binding) { 7 | // console.log("el", el); 8 | // console.log("binding", binding); 9 | const { arg: lineNumber = 1, value: maxTextNumber = 0 } = binding; 10 | 11 | const truncate = ` 12 | word-wrap:break-word; 13 | text-overflow: ellipsis; 14 | overflow: hidden; 15 | display: -webkit-box; 16 | -webkit-box-orient: vertical; 17 | -webkit-line-clamp: ${lineNumber}; 18 | `; 19 | // eslint-disable-next-line no-control-regex 20 | const halfText = el.innerText.match(/[\x00-\xff]/g); 21 | const halfTextNumber = halfText ? halfText.length : 0; //计算半角字体的个数 22 | const normalTextNumber = 23 | el.innerText.length - halfTextNumber + halfTextNumber / 2; 24 | let width = ""; 25 | if (maxTextNumber && maxTextNumber < normalTextNumber) { 26 | width = ` 27 | width: ${Number(maxTextNumber) + 0.786}em; 28 | `; 29 | } 30 | el.style.cssText += truncate + width; 31 | } 32 | 33 | export default { 34 | install: (app) => app.directive("clampAuto", clampAuto), 35 | clampAuto, 36 | }; 37 | 38 | // const truncate = ` 39 | // overflow: hidden; 40 | // display: -webkit-box; 41 | // -webkit-line-clamp: ${lineNumber}; 42 | // -webkit-box-orient: vertical; 43 | // `; 44 | 45 | // const truncate = ` 46 | // word-wrap:break-word; 47 | // overflow: hidden; 48 | // display: -webkit-box; 49 | // text-overflow: ellipsis; 50 | // -webkit-box-orient: vertical; 51 | // -webkit-line-clamp: ${lineNumber}; 52 | // `; 53 | -------------------------------------------------------------------------------- /src/directive/clickAway.js: -------------------------------------------------------------------------------- 1 | const clickEventType = document.ontouchstart !== null ? "click" : "touchstart"; 2 | 3 | const UNIQUE_ID = "__vue_click_away__"; 4 | 5 | const onMounted = (el, binding, vnode) => { 6 | onUnmounted(el); 7 | 8 | let vm = vnode.context; 9 | let callback = binding.value; 10 | 11 | let nextTick = false; 12 | setTimeout(function() { 13 | nextTick = true; 14 | }, 0); 15 | 16 | el[UNIQUE_ID] = (event) => { 17 | if ( 18 | (!el || !el.contains(event.target)) && 19 | callback && 20 | nextTick && 21 | typeof callback === "function" 22 | ) { 23 | return callback.call(vm, event); 24 | } 25 | }; 26 | 27 | document.addEventListener(clickEventType, el[UNIQUE_ID], false); 28 | }; 29 | 30 | const onUnmounted = (el) => { 31 | document.removeEventListener(clickEventType, el[UNIQUE_ID], false); 32 | delete el[UNIQUE_ID]; 33 | }; 34 | 35 | const onUpdated = (el, binding, vnode) => { 36 | if (binding.value === binding.oldValue) { 37 | return; 38 | } 39 | onMounted(el, binding, vnode); 40 | }; 41 | 42 | /** 43 | * @desc 点击除元素本身以外的地方执行 44 | * @demo v-click-away="callback" 45 | */ 46 | export const clickAway = { 47 | mounted: onMounted, 48 | updated: onUpdated, 49 | unmounted: onUnmounted, 50 | }; 51 | 52 | export default { 53 | install: (app) => app.directive("clickAway", clickAway), 54 | clickAway, 55 | }; 56 | -------------------------------------------------------------------------------- /src/directive/index.js: -------------------------------------------------------------------------------- 1 | export { default as clickAway } from "./clickAway"; 2 | export { default as observer } from "./observer"; 3 | export { default as clampAuto } from "./clampAuto"; -------------------------------------------------------------------------------- /src/directive/observer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc 指令 交叉观察者 3 | */ 4 | export function observer(el, binding) { 5 | const { arg: options = {}, value: callback } = binding; 6 | const io = new IntersectionObserver(callback, options); 7 | io.observe(el); 8 | } 9 | 10 | export default { 11 | install: (app) => app.directive("observer", observer), 12 | observer, 13 | }; 14 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import ViaSvgIcon from "@/components/ViaSvgIcon"; // svg component 2 | 3 | export default { 4 | install(app) { 5 | app.component("ViaSvgIcon", ViaSvgIcon); 6 | }, 7 | }; 8 | 9 | const svgs = require.context("./svg", false, /\.svg$/); 10 | const requireAll = (requireContext) => 11 | requireContext.keys().map(requireContext); 12 | 13 | requireAll(svgs); 14 | -------------------------------------------------------------------------------- /src/icons/svg/bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/fold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/merchant.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/promote.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/unfold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/userAdmin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/withdraw.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./views"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | //引入全局样式 6 | import "./styles/scss/index.scss"; 7 | import "./styles/css/index.css"; 8 | 9 | const app = createApp(App); 10 | //挂载全局指令 11 | import { clampAuto } from "./directive/index"; 12 | app.use(clampAuto); 13 | //全局混入 14 | // import { screenResize } from "./mixins"; 15 | // app.use(screenResize); 16 | //全局挂载请求 通过 this.$req进行使用 17 | import request from "./request"; 18 | app.use(request); 19 | //使用svg icon 20 | import icons from "./icons"; 21 | app.use(icons); 22 | //引入element-ui 23 | import elementUi from "./plugins/element-ui"; 24 | app.use(elementUi); 25 | //引入上下文菜单插件 26 | import contentmenu from "v-contextmenu"; 27 | import "v-contextmenu/dist/themes/default.css"; 28 | app.use(contentmenu); 29 | //全局挂载的方法对象 30 | app.config.globalProperties.$via = {}; 31 | 32 | app.use(store); 33 | app.use(router); 34 | app.mount("#app"); 35 | -------------------------------------------------------------------------------- /src/mixins/index.js: -------------------------------------------------------------------------------- 1 | export { default as windowSize } from "./windowSize"; 2 | export { default as screenResize } from "./screenResize"; -------------------------------------------------------------------------------- /src/mixins/screenResize.js: -------------------------------------------------------------------------------- 1 | import { windowSize } from "./windowSize"; 2 | 3 | const screens = { 4 | sm: 640, 5 | md: 768, 6 | lg: 1024, 7 | xl: 1280, 8 | "2xl": 1536, 9 | }; 10 | 11 | const breakpoint = Object.keys(screens).reduce((obj, key) => { 12 | obj[key] = false; 13 | return obj; 14 | }, {}); 15 | 16 | export const screenResize = { 17 | mixins: [windowSize], 18 | data() { 19 | return { 20 | _breakpoint: { ...breakpoint }, 21 | }; 22 | }, 23 | computed: { 24 | $_screen() { 25 | return this._breakpoint; 26 | }, 27 | }, 28 | beforeMount() { 29 | this.$watch("$windowWidth", this.$_resizeHandler, { immediate: true }); 30 | }, 31 | methods: { 32 | // use $_ for mixins properties 33 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 34 | $_resizeHandler(width) { 35 | // console.log("width", width); 36 | const _breakpoint = this._breakpoint; 37 | for (const key in _breakpoint) { 38 | if (Object.hasOwnProperty.call(_breakpoint, key)) { 39 | _breakpoint[key] = width >= screens[key]; 40 | } 41 | } 42 | }, 43 | }, 44 | }; 45 | export default { 46 | install: (app) => app.mixin(screenResize), 47 | screenResize, 48 | }; 49 | -------------------------------------------------------------------------------- /src/mixins/windowSize/index.js: -------------------------------------------------------------------------------- 1 | // export * from "./plugin"; 2 | export * from "./spare"; -------------------------------------------------------------------------------- /src/mixins/windowSize/plugin.js: -------------------------------------------------------------------------------- 1 | import { vueWindowSizeMixin } from "vue-window-size/option-api"; 2 | 3 | export const windowSize = vueWindowSizeMixin(); 4 | 5 | export default { 6 | install: (app) => app.mixin(windowSize), 7 | windowSize, 8 | }; -------------------------------------------------------------------------------- /src/mixins/windowSize/spare.js: -------------------------------------------------------------------------------- 1 | const { body } = document; 2 | 3 | export const windowSize = { 4 | data() { 5 | return { 6 | _windowWidth: 0, 7 | _windowHeight: 0, 8 | }; 9 | }, 10 | computed: { 11 | $windowWidth() { 12 | return this._windowWidth; 13 | }, 14 | $windowHeight() { 15 | return this._windowHeight; 16 | }, 17 | }, 18 | beforeMount() { 19 | window.addEventListener("resize", this.$_getBoundingClientRect); 20 | }, 21 | beforeUnmount() { 22 | window.removeEventListener("resize", this.$_getBoundingClientRect); 23 | }, 24 | mounted() { 25 | this.$_getBoundingClientRect(); 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_getBoundingClientRect() { 31 | if (document.hidden) return; 32 | const { width, height } = body.getBoundingClientRect(); 33 | this._windowWidth = width; 34 | this._windowHeight = height; 35 | }, 36 | }, 37 | }; 38 | export default { 39 | install: (app) => app.mixin(windowSize), 40 | windowSize, 41 | }; 42 | -------------------------------------------------------------------------------- /src/plugins/element-ui/all.js: -------------------------------------------------------------------------------- 1 | import ElementPlus from "element-plus"; 2 | export default { 3 | install(app) { 4 | app.use(ElementPlus); 5 | } 6 | }; -------------------------------------------------------------------------------- /src/plugins/element-ui/index.js: -------------------------------------------------------------------------------- 1 | // import elementUi from "./all"; 2 | import elementUi from "./single"; 3 | 4 | export default elementUi; -------------------------------------------------------------------------------- /src/plugins/element-ui/index.scss: -------------------------------------------------------------------------------- 1 | $--font-path: "~element-plus/packages/theme-chalk/src/fonts"; 2 | @import "~element-plus/packages/theme-chalk/src/index.scss"; -------------------------------------------------------------------------------- /src/plugins/element-ui/single.js: -------------------------------------------------------------------------------- 1 | import { 2 | ElAlert, 3 | ElAside, 4 | ElAutocomplete, 5 | ElAvatar, 6 | ElBacktop, 7 | ElBadge, 8 | ElBreadcrumb, 9 | ElBreadcrumbItem, 10 | ElButton, 11 | ElButtonGroup, 12 | ElCalendar, 13 | ElCard, 14 | ElCarousel, 15 | ElCarouselItem, 16 | ElCascader, 17 | ElCascaderPanel, 18 | ElCheckbox, 19 | ElCheckboxButton, 20 | ElCheckboxGroup, 21 | ElCol, 22 | ElCollapse, 23 | ElCollapseItem, 24 | ElCollapseTransition, 25 | ElColorPicker, 26 | ElContainer, 27 | ElDatePicker, 28 | ElDialog, 29 | ElDivider, 30 | ElDrawer, 31 | ElDropdown, 32 | ElDropdownItem, 33 | ElDropdownMenu, 34 | ElFooter, 35 | ElForm, 36 | ElFormItem, 37 | ElHeader, 38 | ElIcon, 39 | ElImage, 40 | ElInput, 41 | ElInputNumber, 42 | ElLink, 43 | ElMain, 44 | ElMenu, 45 | ElMenuItem, 46 | ElMenuItemGroup, 47 | ElOption, 48 | ElOptionGroup, 49 | ElPageHeader, 50 | ElPagination, 51 | ElPopconfirm, 52 | ElPopover, 53 | ElPopper, 54 | ElProgress, 55 | ElRadio, 56 | ElRadioButton, 57 | ElRadioGroup, 58 | ElRate, 59 | ElRow, 60 | ElScrollbar, 61 | ElSelect, 62 | ElSlider, 63 | ElStep, 64 | ElSteps, 65 | ElSubmenu, 66 | ElSwitch, 67 | ElTabPane, 68 | ElTable, 69 | ElTableColumn, 70 | ElTabs, 71 | ElTag, 72 | ElTimePicker, 73 | ElTimeSelect, 74 | ElTimeline, 75 | ElTimelineItem, 76 | ElTooltip, 77 | ElTransfer, 78 | ElTree, 79 | ElUpload, 80 | ElInfiniteScroll, 81 | ElLoading, 82 | ElMessage, 83 | ElMessageBox, 84 | ElNotification 85 | } from "element-plus"; 86 | 87 | const components = [ 88 | ElAlert, 89 | ElAside, 90 | ElAutocomplete, 91 | ElAvatar, 92 | ElBacktop, 93 | ElBadge, 94 | ElBreadcrumb, 95 | ElBreadcrumbItem, 96 | ElButton, 97 | ElButtonGroup, 98 | ElCalendar, 99 | ElCard, 100 | ElCarousel, 101 | ElCarouselItem, 102 | ElCascader, 103 | ElCascaderPanel, 104 | ElCheckbox, 105 | ElCheckboxButton, 106 | ElCheckboxGroup, 107 | ElCol, 108 | ElCollapse, 109 | ElCollapseItem, 110 | ElCollapseTransition, 111 | ElColorPicker, 112 | ElContainer, 113 | ElDatePicker, 114 | ElDialog, 115 | ElDivider, 116 | ElDrawer, 117 | ElDropdown, 118 | ElDropdownItem, 119 | ElDropdownMenu, 120 | ElFooter, 121 | ElForm, 122 | ElFormItem, 123 | ElHeader, 124 | ElIcon, 125 | ElImage, 126 | ElInput, 127 | ElInputNumber, 128 | ElLink, 129 | ElMain, 130 | ElMenu, 131 | ElMenuItem, 132 | ElMenuItemGroup, 133 | ElOption, 134 | ElOptionGroup, 135 | ElPageHeader, 136 | ElPagination, 137 | ElPopconfirm, 138 | ElPopover, 139 | ElPopper, 140 | ElProgress, 141 | ElRadio, 142 | ElRadioButton, 143 | ElRadioGroup, 144 | ElRate, 145 | ElRow, 146 | ElScrollbar, 147 | ElSelect, 148 | ElSlider, 149 | ElStep, 150 | ElSteps, 151 | ElSubmenu, 152 | ElSwitch, 153 | ElTabPane, 154 | ElTable, 155 | ElTableColumn, 156 | ElTabs, 157 | ElTag, 158 | ElTimePicker, 159 | ElTimeSelect, 160 | ElTimeline, 161 | ElTimelineItem, 162 | ElTooltip, 163 | ElTransfer, 164 | ElTree, 165 | ElUpload 166 | ]; 167 | 168 | const plugins = [ 169 | ElInfiniteScroll, 170 | ElLoading, 171 | ElMessage, 172 | ElMessageBox, 173 | ElNotification 174 | ]; 175 | 176 | export default { 177 | install(app, options) { 178 | if (options) { 179 | app.config.globalProperties.$ELEMENT = options; 180 | } 181 | components.forEach(component => { 182 | app.component(component.name, component); 183 | }); 184 | 185 | plugins.forEach(plugin => { 186 | app.use(plugin); 187 | }); 188 | } 189 | }; 190 | -------------------------------------------------------------------------------- /src/plugins/encrypt/index.js: -------------------------------------------------------------------------------- 1 | import jsencrypt from "./jsencrypt"; 2 | 3 | import CryptoJS from "crypto-js"; 4 | import requestConfig from "@/config/request"; 5 | 6 | const publicKey = requestConfig.encrypt.publicKey; 7 | const iv = CryptoJS.enc.Utf8.parse(requestConfig.encrypt.iv); 8 | const mode = CryptoJS.mode.CBC; 9 | const padding = CryptoJS.pad.Pkcs7; 10 | 11 | const isBase64 = requestConfig.encrypt.toBase64; //是否使用base64进行处理 12 | 13 | const JSEncrypt = new jsencrypt(); 14 | JSEncrypt.setPublicKey(publicKey); 15 | export const rsa = { 16 | encrypt(content) { 17 | return JSEncrypt.encrypt(content); 18 | }, 19 | }; 20 | 21 | export const aes = { 22 | encrypt(key, content) { 23 | content = JSON.stringify(content); 24 | content = CryptoJS.enc.Utf8.parse(content); 25 | key = CryptoJS.enc.Utf8.parse(key); 26 | content = CryptoJS.AES.encrypt(content, key, { 27 | iv, 28 | mode, 29 | padding, 30 | }).toString(); 31 | 32 | isBase64 && 33 | (content = CryptoJS.enc.Base64.stringify( 34 | CryptoJS.enc.Utf8.parse(content) 35 | )); 36 | 37 | return content; 38 | }, 39 | decrypt(key, content) { 40 | isBase64 && 41 | (content = CryptoJS.enc.Utf8.stringify( 42 | CryptoJS.enc.Base64.parse(content) 43 | )); 44 | 45 | key = CryptoJS.enc.Utf8.parse(key); 46 | content = CryptoJS.AES.decrypt(content, key, { iv, mode, padding }); 47 | content = CryptoJS.enc.Utf8.stringify(content); 48 | content = JSON.parse(content); 49 | 50 | return content; 51 | }, 52 | }; 53 | 54 | export function getKey(len = 16, radix = 16) { 55 | var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split( 56 | "" 57 | ); 58 | var uuid = [], 59 | i; 60 | radix = radix || chars.length; 61 | 62 | if (len) { 63 | // Compact form 64 | for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)]; 65 | } else { 66 | // rfc4122, version 4 form 67 | var r; 68 | 69 | // rfc4122 requires these characters 70 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = "-"; 71 | uuid[14] = "4"; 72 | 73 | // Fill in random data. At i==19 set the high bits of clock sequence as 74 | // per rfc4122, sec. 4.1.5 75 | for (i = 0; i < 36; i++) { 76 | if (!uuid[i]) { 77 | r = 0 | (Math.random() * 16); 78 | uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r]; 79 | } 80 | } 81 | } 82 | 83 | return uuid.join(""); 84 | } 85 | 86 | export default { 87 | rsa, 88 | aes, 89 | getKey, 90 | }; 91 | -------------------------------------------------------------------------------- /src/plugins/encrypt/jsencrypt.js: -------------------------------------------------------------------------------- 1 | // import jsencrypt from "uni-jsencrypt"; 2 | import jsencrypt from "jsencrypt"; 3 | 4 | export default jsencrypt; 5 | -------------------------------------------------------------------------------- /src/plugins/modal/element-plus.js: -------------------------------------------------------------------------------- 1 | import { 2 | ElMessageBox, 3 | ElMessage, 4 | ElLoading, 5 | ElNotification 6 | } from "element-plus"; 7 | 8 | export function dialog( 9 | content, 10 | { 11 | isCancel = false, 12 | title = "提示", 13 | confirmText = "确认", 14 | cancelText = "取消", 15 | ...moreObj 16 | } = {} 17 | ) { 18 | return new Promise(resolve => { 19 | ElMessageBox({ 20 | title, 21 | message: content, 22 | confirmButtonText: confirmText, 23 | showCancelButton: isCancel, 24 | cancelButtonText: cancelText, 25 | ...moreObj 26 | }) 27 | .then(() => { 28 | resolve(true); 29 | }) 30 | .catch(() => { 31 | resolve(false); 32 | }); 33 | }); 34 | } 35 | 36 | export function toast( 37 | content, 38 | { duration = 2000, showClose = true, ...moreObj } = {} 39 | ) { 40 | return new Promise(resolve => { 41 | ElMessage({ 42 | message: content, 43 | duration, 44 | showClose, 45 | onClose() { 46 | resolve(true); 47 | }, 48 | ...moreObj 49 | }); 50 | }); 51 | } 52 | 53 | let loadingInstance = {}; 54 | export function loading(content, { ...moreObj } = {}) { 55 | if (content) { 56 | loadingInstance = ElLoading.service({ 57 | text: content, 58 | ...moreObj 59 | }); 60 | } else { 61 | loadingInstance.close(); 62 | } 63 | return loadingInstance; 64 | } 65 | 66 | export function notify( 67 | content, 68 | { title = "提示", type = "info", duration = 2000, ...moreObj } = {} 69 | ) { 70 | return new Promise(resolve => { 71 | ElNotification({ 72 | title, 73 | message: content, 74 | type, 75 | duration, 76 | onClose() { 77 | resolve(true); 78 | }, 79 | ...moreObj 80 | }); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /src/plugins/modal/index.js: -------------------------------------------------------------------------------- 1 | export * from "./element-plus"; 2 | // export * from "./vant.js"; 3 | -------------------------------------------------------------------------------- /src/plugins/modal/vant.js: -------------------------------------------------------------------------------- 1 | import { Dialog, Notify, Toast } from "vant"; 2 | 3 | export function dialog( 4 | content, 5 | { 6 | isCancel = false, 7 | title = "提示", 8 | confirmText = "确认", 9 | cancelText = "取消", 10 | ...moreObj 11 | } = {} 12 | ) { 13 | return new Promise(resolve => { 14 | Dialog({ 15 | title, 16 | message: content, 17 | confirmButtonText: confirmText, 18 | showCancelButton: isCancel, 19 | cancelButtonText: cancelText, 20 | ...moreObj 21 | }) 22 | .then(() => { 23 | resolve(true); 24 | }) 25 | .catch(() => { 26 | resolve(false); 27 | }); 28 | }); 29 | } 30 | 31 | export function toast( 32 | content, 33 | { position = "middle", duration = 2000, forbidClick = true, ...moreObj } = {} 34 | ) { 35 | return new Promise(resolve => { 36 | Toast({ 37 | message: content, 38 | position, 39 | duration, 40 | forbidClick, 41 | onClose() { 42 | resolve(true); 43 | }, 44 | ...moreObj 45 | }); 46 | }); 47 | } 48 | 49 | export function loading( 50 | content, 51 | { duration = 0, forbidClick = true, ...moreObj } = {} 52 | ) { 53 | if (content) { 54 | Toast.loading({ 55 | message: content, 56 | duration, 57 | forbidClick, 58 | ...moreObj 59 | }); 60 | } else { 61 | Toast.clear(); 62 | } 63 | return Toast; 64 | } 65 | 66 | export function notify(content, { duration = 2000, ...moreObj } = {}) { 67 | return new Promise(resolve => { 68 | Notify({ 69 | message: content, 70 | type, 71 | duration, 72 | onClose() { 73 | resolve(true); 74 | }, 75 | ...moreObj 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /src/plugins/request/axios-encrypt.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import qs from "qs"; 3 | import requestConfig from "@/config/request"; 4 | import { rsa, aes, getKey } from "@/plugins/encrypt"; 5 | import { isAttrs, debounce } from "@/utils/index.js"; 6 | import { loading, toast, dialog } from "@/plugins/modal/index.js"; 7 | import { getStorages, removeStorages } from "@/plugins/storages"; 8 | import { 9 | mapRequest as MapRequest, 10 | fileToFormData, 11 | requestAdapter, 12 | } from "./utils"; 13 | 14 | if (requestAdapter) { 15 | axios.defaults.adapter = requestAdapter; //请求适配器 16 | } 17 | 18 | axios.defaults.headers["Content-Type"] = "application/json;charset=utf-8"; 19 | 20 | const service = axios.create({ 21 | baseURL: requestConfig.baseUrl, 22 | timeout: requestConfig.timeout, 23 | }); 24 | 25 | const statusCode = { 26 | 200: "服务器成功返回请求的数据。", 27 | 201: "新建或修改数据成功。", 28 | 202: "一个请求已经进入后台排队(异步任务)。", 29 | 204: "删除数据成功。", 30 | 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。", 31 | 401: "认证失败,无法访问系统资源", 32 | 403: "请重新登录", 33 | 404: "访问资源不存在", 34 | 406: "请求的格式不可得。", 35 | 410: "请求的资源被永久删除,且不会再得到的。", 36 | 422: "当创建一个对象时,发生一个验证错误。", 37 | 500: "服务器发生错误,请检查服务器。", 38 | 502: "网关错误。", 39 | 503: "服务不可用,服务器暂时过载或维护。", 40 | 504: "网关超时。", 41 | default: "系统未知错误,请反馈给管理员", 42 | }; 43 | 44 | // 请求拦截 45 | service.interceptors.request.use( 46 | (request) => { 47 | const { url, data, params, isToken, isUpload, isLoading } = request; 48 | console.log( 49 | `请求拦截: url: ${url}, data: ${JSON.stringify( 50 | data 51 | )}, params: ${JSON.stringify(params)}` 52 | ); 53 | 54 | //解决query无法传递数组的问题 55 | request.paramsSerializer = (params) => 56 | qs.stringify(params, { arrayFormat: "repeat" }); 57 | 58 | if (isToken) { 59 | request.headers[requestConfig.authorization.key] = 60 | requestConfig.authorization.prefix + getStorages("token"); 61 | } 62 | 63 | if (isLoading) { 64 | loading("请稍后..."); 65 | } 66 | 67 | if (isUpload) { 68 | request.headers["Content-Type"] = "multipart/form-data;"; 69 | request.data = fileToFormData(request.data); 70 | } 71 | 72 | return request; 73 | }, 74 | (error) => { 75 | console.log(error); 76 | Promise.reject(error); 77 | } 78 | ); 79 | 80 | const clearLoading = debounce(loading); //清除定时器 81 | const goLogin = debounce(async (callback) => { 82 | const result = await dialog("该服务需要进行登录后才能正常使用,是否登录?", { 83 | isCancel: true, 84 | }); 85 | if (result) { 86 | callback(); 87 | } 88 | }); 89 | // 响应拦截 90 | service.interceptors.response.use( 91 | async (response) => { 92 | const { 93 | data, 94 | url, 95 | config: { isLoading, isIntercept, encryptObj }, 96 | } = response; 97 | 98 | // const { code, message: msg } = data; 99 | const { code, msg } = data; 100 | 101 | console.log(`响应拦截: url: ${url}, data: ${JSON.stringify(data)}`); 102 | 103 | if (isLoading) { 104 | clearLoading(); 105 | } 106 | 107 | if (!isIntercept) { 108 | return data; 109 | } 110 | 111 | const message = statusCode[code] || msg || statusCode["default"]; 112 | 113 | switch (code) { 114 | case 401: 115 | removeStorages("token"); 116 | goLogin(); 117 | break; 118 | case 403: 119 | removeStorages("token"); 120 | goLogin(); 121 | break; 122 | case 500: 123 | toast(message); 124 | break; 125 | 126 | default: 127 | if (code === requestConfig.responseSuccessCode || code === 200) { 128 | if (encryptObj.on) { 129 | data.data = aes.decrypt(encryptObj.key, data.data); 130 | } 131 | return data; 132 | } else { 133 | return Promise.reject("error"); 134 | } 135 | } 136 | }, 137 | (error) => { 138 | console.log("err:" + error); 139 | return Promise.reject(error); 140 | } 141 | ); 142 | 143 | /** 144 | * 145 | * @param {string} url 146 | * @param {object} params 147 | * @param {object} options isBody 是否body方式传参 isEncrypt 是否加密 isToken 是否传token isLoading是否显示loading isIntercept是否自动控制状态 moreOptions更多选项 148 | */ 149 | export function request( 150 | url = "", 151 | params = {}, 152 | { 153 | method = "post", 154 | isBody = true, 155 | isEncrypt = requestConfig.encrypt.on, 156 | isToken = true, 157 | isLoading = true, 158 | isIntercept = true, 159 | isUpload = false, 160 | ...moreOptions 161 | } = {} 162 | ) { 163 | if (isUpload) { 164 | isBody = true; 165 | method = "post"; 166 | isEncrypt = false; 167 | } 168 | 169 | const way = isBody ? "data" : "params"; 170 | 171 | let data = formatData(params, way); 172 | function formatData(params, way) { 173 | let tempObj = { 174 | data: {}, 175 | params: {}, 176 | }; 177 | if (isAttrs(params, "body")) { 178 | tempObj.data = params.body; 179 | } 180 | if (isAttrs(params, "query")) { 181 | tempObj.params = params.query; 182 | } 183 | 184 | if (!isAttrs(params, "body") && !isAttrs(params, "query")) { 185 | tempObj[way] = params; 186 | } 187 | return tempObj; 188 | } 189 | 190 | let encryptObj = { 191 | on: isEncrypt, 192 | key: "", 193 | }; 194 | if (isEncrypt) { 195 | const encryptDataObj = formatEncryptData(data[way]); 196 | 197 | encryptObj.key = encryptDataObj.key; 198 | 199 | data[way] = encryptDataObj.data; 200 | } 201 | 202 | return service({ 203 | url, 204 | method, 205 | ...data, 206 | 207 | encryptObj, 208 | isToken, 209 | isLoading, 210 | isIntercept, 211 | isUpload, 212 | ...moreOptions, 213 | }); 214 | } 215 | 216 | /** 217 | * @desc 格式化加密数据 218 | * @param {*} params 219 | */ 220 | function formatEncryptData(params) { 221 | const key = getKey(); //生成随机key 222 | const _rsa = rsa.encrypt(key); //生成对key 进行加密 223 | const _cipher = aes.encrypt(key, params); //对传入的参数进行加密 224 | return { 225 | data: { 226 | _rsa, 227 | _cipher, 228 | }, 229 | key, 230 | }; 231 | } 232 | 233 | //请求辅助函数 234 | export function mapRequest(arr, { ...moreOptions } = {}) { 235 | return MapRequest(arr, { ...moreOptions, request }); 236 | } 237 | 238 | export default { 239 | install(app) { 240 | app.config.globalProperties.$req = request; 241 | }, 242 | request, 243 | }; 244 | -------------------------------------------------------------------------------- /src/plugins/request/axios.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import qs from "qs"; 3 | import requestConfig from "@/config/request"; 4 | import { isAttrs, debounce } from "@/utils/index.js"; 5 | import { loading, toast, dialog } from "@/plugins/modal/index.js"; 6 | import { getStorages, removeStorages } from "@/plugins/storages/index.js"; 7 | import { 8 | mapRequest as MapRequest, 9 | fileToFormData, 10 | requestAdapter, 11 | } from "./utils"; 12 | 13 | if (requestAdapter) { 14 | axios.defaults.adapter = requestAdapter; //请求适配器 15 | } 16 | 17 | axios.defaults.headers["Content-Type"] = "application/json;charset=utf-8"; 18 | 19 | const service = axios.create({ 20 | baseURL: requestConfig.baseUrl, 21 | timeout: requestConfig.timeout, 22 | }); 23 | 24 | const statusCode = { 25 | 200: "服务器成功返回请求的数据。", 26 | 201: "新建或修改数据成功。", 27 | 202: "一个请求已经进入后台排队(异步任务)。", 28 | 204: "删除数据成功。", 29 | 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。", 30 | 401: "认证失败,无法访问系统资源", 31 | 403: "请重新登录", 32 | 404: "访问资源不存在", 33 | 406: "请求的格式不可得。", 34 | 410: "请求的资源被永久删除,且不会再得到的。", 35 | 422: "当创建一个对象时,发生一个验证错误。", 36 | 500: "服务器发生错误,请检查服务器。", 37 | 502: "网关错误。", 38 | 503: "服务不可用,服务器暂时过载或维护。", 39 | 504: "网关超时。", 40 | default: "系统未知错误,请反馈给管理员", 41 | }; 42 | 43 | // 请求拦截 44 | service.interceptors.request.use( 45 | (request) => { 46 | const { 47 | baseURL, 48 | url, 49 | data, 50 | params, 51 | isToken, 52 | isUpload, 53 | isLoading, 54 | } = request; 55 | console.log( 56 | `请求拦截: url: ${baseURL + url}, data: ${JSON.stringify( 57 | data 58 | )}, params: ${JSON.stringify(params)}` 59 | ); 60 | 61 | //解决query无法传递数组的问题 62 | request.paramsSerializer = (params) => 63 | qs.stringify(params, { arrayFormat: "repeat" }); 64 | 65 | if (isToken) { 66 | request.headers[requestConfig.authorization.key] = 67 | requestConfig.authorization.prefix + getStorages("token"); 68 | } 69 | 70 | if (isLoading) { 71 | loading("请稍后..."); 72 | } 73 | 74 | if (isUpload) { 75 | request.headers["Content-Type"] = "multipart/form-data;"; 76 | request.data = fileToFormData(request.data); 77 | } 78 | 79 | return request; 80 | }, 81 | (error) => { 82 | console.log(error); 83 | Promise.reject(error); 84 | } 85 | ); 86 | 87 | const clearLoading = debounce(loading); //清除定时器 88 | const goLogin = debounce(async (callback) => { 89 | const result = await dialog("该服务需要进行登录后才能正常使用,是否登录?", { 90 | isCancel: true, 91 | }); 92 | if (result) { 93 | callback(); 94 | } 95 | }); 96 | // 响应拦截 97 | service.interceptors.response.use( 98 | async (response) => { 99 | console.log("response", response); 100 | const { 101 | data, 102 | config: { isLoading, isIntercept, baseURL, url }, 103 | } = response; 104 | 105 | // const { code, message: msg } = data; 106 | const { code, msg } = data; 107 | 108 | console.log( 109 | `响应拦截: url: ${baseURL + url}, data: ${JSON.stringify(data)}` 110 | ); 111 | 112 | if (isLoading) { 113 | clearLoading(); 114 | } 115 | 116 | if (!isIntercept) { 117 | return data; 118 | } 119 | 120 | const message = statusCode[code] || msg || statusCode["default"]; 121 | 122 | switch (code) { 123 | case 401: 124 | removeStorages("token"); 125 | goLogin(); 126 | break; 127 | case 403: 128 | removeStorages("token"); 129 | goLogin(); 130 | break; 131 | case 500: 132 | toast(message); 133 | break; 134 | 135 | default: 136 | if (code === requestConfig.responseSuccessCode || code === 200) { 137 | return data; 138 | } else { 139 | return Promise.reject("error"); 140 | } 141 | } 142 | }, 143 | (error) => { 144 | console.log("err:" + error); 145 | return Promise.reject(error); 146 | } 147 | ); 148 | 149 | /** 150 | * 151 | * @param {string} url 152 | * @param {object} params 153 | * @param {object} options isBody 是否body方式传参 isToken 是否传token isLoading是否显示loading isIntercept是否自动控制状态 moreOptions更多选项 154 | */ 155 | export function request( 156 | url = "", 157 | params = {}, 158 | { 159 | method = "post", 160 | isBody = true, 161 | isToken = true, 162 | isLoading = true, 163 | isIntercept = true, 164 | isUpload = false, 165 | ...moreOptions 166 | } = {} 167 | ) { 168 | if (isUpload) { 169 | isBody = true; 170 | method = "post"; 171 | } 172 | 173 | const way = isBody ? "data" : "params"; 174 | 175 | let data = formatData(params, way); 176 | function formatData(params, way) { 177 | let tempObj = { 178 | data: {}, 179 | params: {}, 180 | }; 181 | 182 | if (isAttrs(params, "body")) { 183 | tempObj.data = params.body; 184 | } 185 | 186 | if (isAttrs(params, "query")) { 187 | tempObj.params = params.query; 188 | } 189 | 190 | if (!isAttrs(params, "body") && !isAttrs(params, "query")) { 191 | tempObj[way] = params; 192 | } 193 | return tempObj; 194 | } 195 | 196 | return service({ 197 | url, 198 | method, 199 | ...data, 200 | 201 | isToken, 202 | isLoading, 203 | isIntercept, 204 | isUpload, 205 | ...moreOptions, 206 | }); 207 | } 208 | 209 | //请求辅助函数 210 | export function mapRequest(arr, { ...moreOptions } = {}) { 211 | return MapRequest(arr, { ...moreOptions, request }); 212 | } 213 | 214 | export default { 215 | install(app) { 216 | app.config.globalProperties.$req = request; 217 | }, 218 | request, 219 | }; 220 | -------------------------------------------------------------------------------- /src/plugins/request/index.js: -------------------------------------------------------------------------------- 1 | export * from "./axios-encrypt.js"; 2 | // export * from "./axios.js"; 3 | -------------------------------------------------------------------------------- /src/plugins/request/utils.js: -------------------------------------------------------------------------------- 1 | import { isArray, isObject } from "@/utils/index"; 2 | /** 3 | * @desc 请求辅助函数 4 | */ 5 | export function mapRequest( 6 | arr, 7 | { keyName = "key", valueName = "value", request } = {} 8 | ) { 9 | return arr.reduce( 10 | ( 11 | obj, 12 | { [keyName]: key, [valueName]: value, params: p = {}, options: o = {} } 13 | ) => { 14 | obj[key] = (params = {}, options = {}) => 15 | request(value, { ...p, ...params }, { ...o, ...options }); 16 | return obj; 17 | }, 18 | {} 19 | ); 20 | } 21 | 22 | /** 23 | * @desc 文件转FormData 24 | * @param {*} fileObj 25 | */ 26 | export function fileToFormData(fileObj) { 27 | if (!isObject(fileObj)) { 28 | return new Error("传入的文件不是对象格式"); 29 | } 30 | const key = Object.keys(fileObj)[0]; 31 | const value = fileObj[key]; 32 | if (!value) { 33 | return new Error("传入的文件对象值键名所对应的值不能为空"); 34 | } 35 | const formData = new FormData(); 36 | if (isArray(value)) { 37 | value.forEach(file => { 38 | formData.append(key, file); 39 | }); 40 | } else { 41 | formData.append(key, value); 42 | } 43 | return formData; 44 | } 45 | 46 | /** 47 | * @desc axios 适配器 48 | */ 49 | // export * as requestAdapter from "axios-apicloud-adapter"; 50 | // export * as requestAdapter from "axios-adapter-uniapp"; 51 | export const requestAdapter = null; 52 | -------------------------------------------------------------------------------- /src/plugins/router/index.js: -------------------------------------------------------------------------------- 1 | export * from "./utils"; 2 | export * from "./new"; 3 | // export * from "./old"; 4 | -------------------------------------------------------------------------------- /src/plugins/router/new.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { 3 | createRouter as CreateRouter, 4 | createWebHashHistory, 5 | // createWebHistory, 6 | // createMemoryHistory 7 | } from "vue-router"; //vue 3 8 | 9 | /** 10 | * @param {array} routes 路由表数组 11 | * @param {object} 路由配置 12 | * @returns {object} 路由实例 13 | */ 14 | export function createRouter( 15 | routes = [], 16 | { base = process.env.BASE_URL, mode = "hash", ...moreOptions } = {} 17 | ) { 18 | let createHistory = ""; 19 | switch (mode) { 20 | case "hash": 21 | createHistory = createWebHashHistory; 22 | break; 23 | // case "history": 24 | // createHistory = createWebHistory; 25 | // break; 26 | // case "abstract": 27 | // createHistory = createMemoryHistory; 28 | // break; 29 | } 30 | const router = CreateRouter({ 31 | history: createHistory(base), 32 | routes, 33 | ...moreOptions, 34 | }); 35 | return router; 36 | } 37 | -------------------------------------------------------------------------------- /src/plugins/router/old.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | 4 | /** 5 | * @param {array} routes 路由表数组 6 | * @param {object} 路由配置 7 | * @returns {object} 路由实例 8 | */ 9 | export function createRouter( 10 | routes = [], 11 | { base = process.env.BASE_URL, mode = "hash", ...moreOptions } = {} 12 | ) { 13 | //路由防抖 14 | const originalPush = VueRouter.prototype.push; 15 | VueRouter.prototype.push = function push(location) { 16 | return originalPush.call(this, location).catch(err => err); 17 | }; 18 | 19 | Vue.use(VueRouter); 20 | 21 | return new VueRouter({ 22 | mode, 23 | base, 24 | routes, 25 | ...moreOptions 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/plugins/router/utils.js: -------------------------------------------------------------------------------- 1 | import { merge } from "lodash-es"; 2 | import { isBoolean, isFunction } from "@/utils/index"; 3 | import { getDictLabel } from "@/utils/index"; 4 | import dictConfig from "@/config/dict"; 5 | /** 6 | * @desc 生成动态路由配置表 7 | * @param {object} banList 黑名单 8 | * @param {object} mixins 路由混入列表 9 | */ 10 | export function getAsyncRouterList({ banList = {}, mixins = {} } = {}) { 11 | // eslint-disable-next-line no-useless-escape 12 | const getFileName = (path) => path.replace(/(.*\/)*([^\/]+).*/gi, "$2"); 13 | const arrayToTreeObject = (arr) => 14 | arr.reduceRight((result, key) => ({ [key]: result }), {}); 15 | const treeObjFormat = (obj, callBack) => 16 | Object.keys(obj).map((key) => ({ 17 | ...(Object.keys(obj[key] || []).length 18 | ? { children: treeObjFormat(obj[key], callBack) } 19 | : {}), 20 | ...callBack(key), 21 | })); 22 | const getFilePathList = (banList) => { 23 | const files = require.context("@/views", true, /\/$/); 24 | const isBan = (path) => { 25 | const isBool = banList[getFileName(path)]; 26 | return isBoolean(isBool) && isBool; 27 | }; 28 | return files 29 | .keys() 30 | .filter( 31 | (path) => path !== "./" && !/components/i.test(path) && !isBan(path) 32 | ); 33 | }; 34 | 35 | const filePathArr = getFilePathList(banList); 36 | 37 | const routerIndexObj = filePathArr.reduce((obj, i) => { 38 | const name = getFileName(i); 39 | const path = i.slice(1, -1); 40 | const title = getDictLabel(dictConfig.router, name); 41 | const mixinsValue = mixins[name] || {}; 42 | const baseObj = { 43 | name, 44 | path, 45 | component: () => import(`@/views${path}`), 46 | meta: { 47 | title, 48 | }, 49 | }; 50 | obj[name] = { 51 | ...baseObj, 52 | ...(isFunction(mixinsValue) ? mixinsValue(baseObj) : mixinsValue), 53 | }; 54 | return obj; 55 | }, {}); 56 | 57 | const treeObj = filePathArr.reduce((obj, i) => { 58 | let pathArr = i.split("/").slice(1, -1); 59 | obj = merge(obj, arrayToTreeObject(pathArr)); 60 | return obj; 61 | }, {}); 62 | return treeObjFormat(treeObj, (key) => routerIndexObj[key]); 63 | // console.log(filePathArr); 64 | // console.log(treeObj); 65 | } 66 | -------------------------------------------------------------------------------- /src/plugins/storages/index.js: -------------------------------------------------------------------------------- 1 | // export * from "./apicloud"; 2 | export * from "./universal"; 3 | -------------------------------------------------------------------------------- /src/plugins/storages/universal.js: -------------------------------------------------------------------------------- 1 | import store from "store"; 2 | 3 | export const storages = store; 4 | 5 | export function setStorages(key, value) { 6 | return storages.set(key, value); 7 | } 8 | 9 | export function getStorages(key) { 10 | return storages.get(key); 11 | } 12 | 13 | export function removeStorages(key) { 14 | if (key) { 15 | storages.remove(key); 16 | } else { 17 | storages.clearAll(); 18 | } 19 | return storages; 20 | } 21 | -------------------------------------------------------------------------------- /src/request/index.js: -------------------------------------------------------------------------------- 1 | import { mapRequest } from "@/plugins/request/index"; 2 | // import { requestConfig } from "@/config/index"; 3 | 4 | const mapRequestExe = mapRequest([ 5 | //获取模拟数据 6 | { 7 | key: "getDemoData", 8 | value: "", 9 | options: { 10 | method: "get", 11 | isIntercept: false, 12 | isLoading: true 13 | } 14 | } 15 | // //获取验证码 16 | // { 17 | // key: 'getSmsCode', 18 | // value: 'Member/edit_memberavatar' 19 | // }, 20 | // //获取用户信息 21 | // { 22 | // key: "getUserInfo", 23 | // value: "Member/index", 24 | // }, 25 | // //获取站点信息 26 | // { 27 | // key: "getSiteInfo", 28 | // value: "Index/getEditablePageConfigList", 29 | // }, 30 | ]); 31 | 32 | export default { 33 | install(app) { 34 | app.config.globalProperties.$req = mapRequestExe; 35 | }, 36 | ...mapRequestExe 37 | }; 38 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, getAsyncRouterList } from "@/plugins/router"; 2 | import store from "@/store"; 3 | import NProgress from "nprogress"; // progress bar 4 | import "nprogress/nprogress.css"; // progress bar style 5 | import { toast } from "@/plugins/modal"; 6 | 7 | //自动生成并配置路由表 8 | const dynamicRouterArr = getAsyncRouterList({ 9 | banList: { 10 | // account: true, 11 | }, 12 | mixins: { 13 | home: { 14 | redirect: { name: "demo" }, 15 | }, 16 | account: { 17 | redirect: { name: "login" }, 18 | }, 19 | demo: ({ meta }) => ({ 20 | meta: { ...meta, affix: true }, 21 | }), 22 | }, 23 | }); 24 | // console.log(dynamicRouterArr); 25 | 26 | const router = createRouter( 27 | [ 28 | ...dynamicRouterArr, 29 | { 30 | path: "/:pathMatch(.*)*", 31 | name: "intercept", 32 | redirect: { name: "account" }, 33 | }, 34 | ], 35 | { mode: "hash" } 36 | ); 37 | 38 | router.beforeEach((to, from, next) => { 39 | console.log("router.beforeEach.to:", to); 40 | console.log("router.beforeEach.from:", from); 41 | const includes = (name) => to.path.includes(`/${name}`); 42 | const token = store.getters.token; 43 | NProgress.start(); 44 | if (token) { 45 | if (!includes("home")) { 46 | return next({ name: "home" }); 47 | } 48 | } else { 49 | if (includes("home")) { 50 | toast("请先登录", { type: "warning" }); 51 | return next({ name: "account" }); 52 | } 53 | } 54 | next(); 55 | }); 56 | 57 | router.afterEach((to, from) => { 58 | console.log("router.afterEach.to:", to); 59 | console.log("router.afterEach.from:", from); 60 | NProgress.done(); 61 | }); 62 | 63 | export default router; 64 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demoData: state => state.demo.demoData, 3 | token: state => state.user.token, 4 | userInfo: state => state.user.userInfo, 5 | siteInfo: state => state.site.siteInfo 6 | }; 7 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | import getters from "./getters"; 3 | 4 | const getModules = (banObj) => { 5 | const files = require.context("./modules", true, /\.js$/); 6 | const rules = (path) => path.replace(/(.*\/)*([^.]+).*/gi, "$2"); 7 | const isBan = (path) => { 8 | const tempBool = banObj[rules(path)]; 9 | return typeof tempBool === "boolean" && !tempBool; 10 | }; 11 | const filePathArr = files.keys().filter((path) => !isBan(path)); 12 | return filePathArr.reduce((obj, path) => { 13 | let name = rules(path); 14 | obj[name] = { 15 | ...files(path).default, 16 | namespaced: true, 17 | }; 18 | return obj; 19 | }, {}); 20 | }; 21 | 22 | const modules = getModules({ 23 | // sms: false, 24 | }); 25 | 26 | // console.log("modules", modules); 27 | 28 | export default createStore({ 29 | modules, 30 | getters, 31 | }); -------------------------------------------------------------------------------- /src/store/modules/demo.js: -------------------------------------------------------------------------------- 1 | import req from "@/request"; 2 | export default { 3 | namespaced: true, 4 | state: () => ({ 5 | demoData: "hello word" 6 | }), 7 | getters: {}, 8 | mutations: { 9 | setDemoData(state, data) { 10 | state.demoData = data; 11 | } 12 | }, 13 | actions: { 14 | //获取演示数据 15 | async getDemoData({ commit }, params = {}) { 16 | const data = await req.getDemoData(params); 17 | commit("setDemoData", data.data); 18 | return data; 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/store/modules/site.js: -------------------------------------------------------------------------------- 1 | import { getStorages, setStorages } from "@/plugins/storages"; 2 | import request from "@/request"; 3 | 4 | export default { 5 | namespaced: true, 6 | state: () => ({ 7 | siteInfo: getStorages("siteInfo") || { 8 | themeColor: "", 9 | isDark: "", 10 | fileBaseUrl: "", 11 | appLogo: "", 12 | appName: "", 13 | tabbarArr: [] 14 | } 15 | }), 16 | getters: { 17 | themeTextColorStyle(state) { 18 | return `color: ${state.siteInfo.themeColor};`; 19 | }, 20 | themeBgColorStyle(state) { 21 | return `background-color: ${state.siteInfo.themeColor};`; 22 | } 23 | }, 24 | mutations: { 25 | setSiteInfo(state, data) { 26 | state.siteInfo = data; 27 | setStorages("siteInfo", data); 28 | } 29 | }, 30 | actions: { 31 | //获取站点信息 32 | // eslint-disable-next-line no-empty-pattern 33 | async getSiteInfo({}, { params = {}, options = {} } = {}) { 34 | const { result: data } = await request.getSiteInfo(params, options); 35 | return data; 36 | } 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/store/modules/sms.js: -------------------------------------------------------------------------------- 1 | import { toast } from "@/plugins/modal"; 2 | import request from "@/request"; 3 | export default { 4 | namespaced: true, 5 | state: () => ({ 6 | downCount: 0 7 | }), 8 | getters: {}, 9 | mutations: { 10 | setDownCount(state, value) { 11 | state.downCount = value; 12 | } 13 | }, 14 | actions: { 15 | //获取短信验证码 16 | async getSmsCode({ commit, state }, params) { 17 | if (state.downCount === 0) { 18 | const { msg } = await request.getSmsCode(params); 19 | commit("setDownCount", 60); 20 | let timer = setInterval(() => { 21 | if (state.downCount > 0) { 22 | commit("setDownCount", state.downCount - 1); 23 | } else { 24 | clearInterval(timer); 25 | } 26 | }, 1000); 27 | 28 | toast.success(msg); 29 | } 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/store/modules/tagsView.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state: () => ({ 4 | viewsArr: [], 5 | cachedViewsArr: [], 6 | }), 7 | mutations: { 8 | ADD_VISITED_VIEW: (state, view) => { 9 | if (state.viewsArr.some((v) => v.path === view.path)) return; 10 | // console.log("view", view); 11 | state.viewsArr.push({ 12 | ...view, 13 | name: view.name, 14 | title: view.meta.title || "no-name", 15 | affix: !!view.meta.affix, 16 | }); 17 | }, 18 | ADD_CACHED_VIEW: (state, view) => { 19 | if (state.cachedViewsArr.includes(view.name)) return; 20 | if (!view.meta.noCache) { 21 | state.cachedViewsArr.push(view.name); 22 | } 23 | }, 24 | 25 | DEL_VISITED_VIEW: (state, view) => { 26 | for (const [i, v] of state.viewsArr.entries()) { 27 | if (v.path === view.path) { 28 | state.viewsArr.splice(i, 1); 29 | break; 30 | } 31 | } 32 | }, 33 | DEL_CACHED_VIEW: (state, view) => { 34 | const index = state.cachedViewsArr.indexOf(view.name); 35 | index > -1 && state.cachedViewsArr.splice(index, 1); 36 | }, 37 | 38 | DEL_OTHERS_VISITED_VIEWS: (state, view) => { 39 | state.viewsArr = state.viewsArr.filter((v) => { 40 | return v.meta.affix || v.path === view.path; 41 | }); 42 | }, 43 | DEL_OTHERS_CACHED_VIEWS: (state, view) => { 44 | const index = state.cachedViewsArr.indexOf(view.name); 45 | if (index > -1) { 46 | state.cachedViewsArr = state.cachedViewsArr.slice(index, index + 1); 47 | } else { 48 | // if index = -1, there is no cached tags 49 | state.cachedViewsArr = []; 50 | } 51 | }, 52 | 53 | DEL_ALL_VISITED_VIEWS: (state) => { 54 | // keep affix tags 55 | const affixTags = state.viewsArr.filter((tag) => tag.meta.affix); 56 | state.viewsArr = affixTags; 57 | }, 58 | DEL_ALL_CACHED_VIEWS: (state) => { 59 | state.cachedViewsArr = []; 60 | }, 61 | 62 | UPDATE_VISITED_VIEW: (state, view) => { 63 | for (let v of state.viewsArr) { 64 | if (v.path === view.path) { 65 | v = Object.assign(v, view); 66 | break; 67 | } 68 | } 69 | }, 70 | }, 71 | actions: { 72 | addView({ dispatch }, view) { 73 | dispatch("addVisitedView", view); 74 | dispatch("addCachedView", view); 75 | }, 76 | addVisitedView({ commit }, view) { 77 | commit("ADD_VISITED_VIEW", view); 78 | }, 79 | addCachedView({ commit }, view) { 80 | commit("ADD_CACHED_VIEW", view); 81 | }, 82 | 83 | delView({ dispatch, state }, view) { 84 | return new Promise((resolve) => { 85 | dispatch("delVisitedView", view); 86 | dispatch("delCachedView", view); 87 | resolve({ 88 | viewsArr: [...state.viewsArr], 89 | cachedViewsArr: [...state.cachedViewsArr], 90 | }); 91 | }); 92 | }, 93 | delVisitedView({ commit, state }, view) { 94 | return new Promise((resolve) => { 95 | commit("DEL_VISITED_VIEW", view); 96 | resolve([...state.viewsArr]); 97 | }); 98 | }, 99 | delCachedView({ commit, state }, view) { 100 | return new Promise((resolve) => { 101 | commit("DEL_CACHED_VIEW", view); 102 | resolve([...state.cachedViewsArr]); 103 | }); 104 | }, 105 | 106 | delOthersViews({ dispatch, state }, view) { 107 | return new Promise((resolve) => { 108 | dispatch("delOthersVisitedViews", view); 109 | dispatch("delOthersCachedViews", view); 110 | resolve({ 111 | viewsArr: [...state.viewsArr], 112 | cachedViewsArr: [...state.cachedViewsArr], 113 | }); 114 | }); 115 | }, 116 | delOthersVisitedViews({ commit, state }, view) { 117 | return new Promise((resolve) => { 118 | commit("DEL_OTHERS_VISITED_VIEWS", view); 119 | resolve([...state.viewsArr]); 120 | }); 121 | }, 122 | delOthersCachedViews({ commit, state }, view) { 123 | return new Promise((resolve) => { 124 | commit("DEL_OTHERS_CACHED_VIEWS", view); 125 | resolve([...state.cachedViewsArr]); 126 | }); 127 | }, 128 | 129 | delAllViews({ dispatch, state }, view) { 130 | return new Promise((resolve) => { 131 | dispatch("delAllVisitedViews", view); 132 | dispatch("delAllCachedViews", view); 133 | resolve({ 134 | viewsArr: [...state.viewsArr], 135 | cachedViewsArr: [...state.cachedViewsArr], 136 | }); 137 | }); 138 | }, 139 | delAllVisitedViews({ commit, state }) { 140 | return new Promise((resolve) => { 141 | commit("DEL_ALL_VISITED_VIEWS"); 142 | resolve([...state.viewsArr]); 143 | }); 144 | }, 145 | delAllCachedViews({ commit, state }) { 146 | return new Promise((resolve) => { 147 | commit("DEL_ALL_CACHED_VIEWS"); 148 | resolve([...state.cachedViewsArr]); 149 | }); 150 | }, 151 | 152 | updateVisitedView({ commit }, view) { 153 | commit("UPDATE_VISITED_VIEW", view); 154 | }, 155 | }, 156 | }; 157 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { getStorages, setStorages, removeStorages } from "@/plugins/storages"; 2 | import request from "@/request"; 3 | export default { 4 | namespaced: true, 5 | state: () => ({ 6 | userInfo: {}, 7 | token: getStorages("token") 8 | }), 9 | getters: {}, 10 | mutations: { 11 | setUserInfo(state, data) { 12 | state.userInfo = { ...data }; 13 | }, 14 | removeToken(state) { 15 | state.token = ""; 16 | removeStorages("token"); 17 | }, 18 | setToken(state, token) { 19 | state.token = token; 20 | setStorages("token", token); 21 | } 22 | }, 23 | actions: { 24 | //获取用户详情 25 | async getUserInfo({ commit, state }, { params = {}, options = {} } = {}) { 26 | const { 27 | result: { member_info: data } 28 | } = await request.getUserInfo(params, options); 29 | const info = { 30 | ...data, 31 | id: data.member_id, 32 | avatar: data.member_avatar, 33 | nickname: data.member_name, 34 | isBindPhoneNumber: data.member_mobilebind === 1 ? true : false, 35 | birthday: data.member_birthday, 36 | sex: data.member_sex, //0保密1男2女 37 | areaInfo: data.member_areainfo, 38 | phoneNumber: data.member_mobile, 39 | realname: data.member_truename, 40 | email: data.member_email 41 | }; 42 | commit("setUserInfo", info); 43 | return state.userInfo; 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/styles/css/base.css: -------------------------------------------------------------------------------- 1 | @layer base { 2 | html { 3 | font-size: 14px; 4 | } 5 | } 6 | 7 | @layer utilities { 8 | @responsive { 9 | .scrolling-touch { 10 | -webkit-overflow-scrolling: touch; 11 | } 12 | .scrolling-auto { 13 | -webkit-overflow-scrolling: auto; 14 | } 15 | } 16 | } 17 | 18 | @layer components { 19 | /* 纵向平滑滚动 */ 20 | .overflow-y-scroll-smooth { 21 | @apply overflow-y-scroll scrolling-touch static; 22 | } 23 | 24 | /* 横向平滑滚动 */ 25 | .overflow-x-scroll-smooth { 26 | @apply overflow-x-scroll scrolling-touch static; 27 | } 28 | 29 | /*宽高等比*/ 30 | .wh-const::before { 31 | content: ""; 32 | padding-top: 100%; 33 | float: left; 34 | } 35 | .wh-const::after { 36 | content: ""; 37 | display: block; 38 | clear: both; 39 | } 40 | 41 | /* 隐藏滚动条 */ 42 | .scrollbar-none::-webkit-scrollbar { 43 | display: none; 44 | } 45 | 46 | /* 文字渐变 */ 47 | .bg-clip-text { 48 | -webkit-background-clip: text; 49 | } 50 | 51 | /*高斯模糊*/ 52 | .backdrop-filter-blur-px-5 { 53 | backdrop-filter: blur(5px); 54 | } 55 | 56 | /*vue 渲染完成后显示*/ 57 | [v-cloak] { 58 | display: none !important; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/styles/css/element-ui.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .el-message-box { 3 | max-width: 90%; 4 | } 5 | .el-message { 6 | max-width: 90%; 7 | min-width: 350px !important; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/css/index.css: -------------------------------------------------------------------------------- 1 | @import "./tailwind.css"; 2 | @import "./base.css"; 3 | @import "./pc.css"; 4 | @import "./element-ui.css"; 5 | -------------------------------------------------------------------------------- /src/styles/css/mobile.css: -------------------------------------------------------------------------------- 1 | @layer base { 2 | html, 3 | body { 4 | -webkit-touch-callout: none; /*禁用系统默认菜单*/ 5 | -webkit-text-size-adjust: none; /*禁止移动端 文字缩放*/ 6 | -webkit-tap-highlight-color: transparent; /*重置ios点击灰色背景*/ 7 | -webkit-user-select: none; /*禁止选中文字*/ 8 | } 9 | 10 | input { 11 | outline: none !important; 12 | -webkit-appearance: none; /*去除ios阴影*/ 13 | } 14 | 15 | /* 解决使用有赞下拉刷新组件,sticky无效的问题 */ 16 | .van-pull-refresh { 17 | overflow: initial !important; 18 | } 19 | 20 | img { 21 | position: relative; 22 | } 23 | img::after { 24 | content: ""; 25 | height: 100%; 26 | width: 100%; 27 | position: absolute; 28 | left: 0; 29 | top: 0; 30 | z-index: -1; 31 | /* background-color: #e0e0e0; */ 32 | background: #e0e0e0 url("~@/assets/images/temp-image.png") center / 60% 33 | no-repeat; 34 | } 35 | } 36 | 37 | @layer components { 38 | /* 安全区 */ 39 | :root { 40 | --env-safe-area-inset-top: env(safe-area-inset-top); 41 | --env-safe-area-inset-bottom: env(safe-area-inset-bottom); 42 | --constant-safe-area-inset-top: constant(safe-area-inset-top); 43 | --constant-safe-area-inset-bottom: constant(safe-area-inset-bottom); 44 | } 45 | 46 | .border-t-safe { 47 | border-top: 25px solid transparent; 48 | } 49 | @supports ( 50 | (border: env(safe-area-inset-top)) or 51 | (border: constant(safe-area-inset-top)) 52 | ) { 53 | .border-t-safe { 54 | border-top: max( 55 | var(--env-safe-area-inset-top, var(--constant-safe-area-inset-top)), 56 | 25px 57 | ) 58 | solid transparent; 59 | } 60 | } 61 | 62 | .border-b-safe { 63 | border-bottom: var( 64 | --env-safe-area-inset-bottom, 65 | var(--constant-safe-area-inset-bottom) 66 | ) 67 | solid transparent; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/styles/css/pc.css: -------------------------------------------------------------------------------- 1 | @layer base { 2 | html, 3 | body { 4 | @apply overflow-x-hidden; 5 | } 6 | 7 | svg { 8 | @apply inline-block align-baseline; 9 | } 10 | 11 | button:focus { 12 | @apply outline-none; 13 | } 14 | 15 | img { 16 | position: relative; 17 | } 18 | img::after { 19 | content: ""; 20 | height: 100%; 21 | width: 100%; 22 | position: absolute; 23 | left: 0; 24 | top: 0; 25 | z-index: -1; 26 | /* background-color: #e0e0e0; */ 27 | background: #e0e0e0 url("~@/assets/images/temp-image.png") center / 60% 28 | no-repeat; 29 | } 30 | } 31 | 32 | @layer components { 33 | @screen sm { 34 | .scrollbar-beautiful::-webkit-scrollbar { 35 | width: 10px !important; 36 | height: 10px !important; 37 | -webkit-appearance: none; 38 | } 39 | 40 | .scrollbar-beautiful::-webkit-scrollbar-thumb { 41 | height: 5px; 42 | border-radius: 6px; 43 | -webkit-border-radius: 6px; 44 | background-clip: padding-box; 45 | @apply bg-gray-300 border border-solid border-transparent; 46 | } 47 | 48 | .scrollbar-beautiful:hover::-webkit-scrollbar-thumb { 49 | @apply bg-gray-400; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/styles/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /src/styles/scss/element-ui/base.scss: -------------------------------------------------------------------------------- 1 | @import "~@/plugins/element-ui/index.scss"; -------------------------------------------------------------------------------- /src/styles/scss/element-ui/custom.scss: -------------------------------------------------------------------------------- 1 | /* 改变主题色变量 */ 2 | // $--color-primary: teal; 3 | // $--msgbox-width: 320px; -------------------------------------------------------------------------------- /src/styles/scss/element-ui/index.scss: -------------------------------------------------------------------------------- 1 | @import "./custom.scss"; 2 | @import "./base.scss"; 3 | -------------------------------------------------------------------------------- /src/styles/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import "./element-ui/index.scss"; -------------------------------------------------------------------------------- /src/utils/dom.js: -------------------------------------------------------------------------------- 1 | import { toRawType, camelize } from "./index.js"; 2 | 3 | export function getElement(el) { 4 | if (toRawType(el) === "String") { 5 | el = document.querySelector(el); 6 | } 7 | return el; 8 | } 9 | 10 | export function hasClass(el, className) { 11 | const reg = new RegExp("(^|\\s)" + className + "(\\s|$)"); 12 | return reg.test(getElement(el).className); 13 | } 14 | 15 | export function addClass(el, className) { 16 | if (!hasClass(getElement(el), className)) { 17 | const newClass = getElement(el).className.split(" "); 18 | newClass.push(className); 19 | getElement(el).className = newClass.join(" "); 20 | } 21 | } 22 | 23 | export function removeClass(el, className) { 24 | if (hasClass(getElement(el), className)) { 25 | const reg = new RegExp("(^|\\s)" + className + "(\\s|$)", "g"); 26 | getElement(el).className = getElement(el).className.replace(reg, " "); 27 | } 28 | } 29 | 30 | /** 31 | * @desc 获取标签元素自定义属性数据 32 | */ 33 | export function getElData(el, name) { 34 | const prefix = "data-"; 35 | return getElement(el).getAttribute(prefix + name); 36 | } 37 | /** 38 | * @desc 设置标签元素自定义属性数据 39 | */ 40 | export function setElData(el, name, value) { 41 | const prefix = "data-"; 42 | getElement(el).setAttribute(prefix + name, value); 43 | } 44 | 45 | /** 46 | * @desc getRect是获取相对的父元素(position非static)的位置 如果想获取相对页面的位置,请使用getBoundingClientRect 47 | */ 48 | export function getRect(el) { 49 | return { 50 | top: getElement(el).offsetTop, 51 | left: getElement(el).offsetLeft, 52 | width: getElement(el).offsetWidth, 53 | height: getElement(el).offsetHeight 54 | }; 55 | } 56 | 57 | /** 58 | * @desc 为css样式添加兼容前缀 59 | * @param {string} style 60 | */ 61 | export function prefixStyle(style) { 62 | const elementStyle = document.createElement("div").style; 63 | 64 | const endEventListenerList = ["transitionend", "animationend"]; 65 | 66 | const browserPrefix = { 67 | standard: "", 68 | webkit: "webkit", 69 | Moz: "Moz", 70 | O: "O", 71 | ms: "ms" 72 | }; 73 | 74 | const endEventListenerPrefixList = { 75 | transition: { 76 | transition: "transitionend", 77 | webkitTransition: "webkitTransitionEnd", 78 | MozTransition: "transitionend", 79 | OTransition: "oTransitionEnd", 80 | msTransition: "msTransitionEnd" 81 | }, 82 | animation: { 83 | animation: "animationend", 84 | webkitAnimation: "webkitAnimationEnd", 85 | MozAnimation: "animationend", 86 | OAnimation: "oAnimationEnd", 87 | msAnimation: "msAnimationEnd" 88 | } 89 | }; 90 | 91 | let baseStyle = ""; 92 | if (endEventListenerList.indexOf(style) !== -1) { 93 | baseStyle = style.replace(/end/i, ""); 94 | } 95 | 96 | for (let key in browserPrefix) { 97 | if (baseStyle) { 98 | let cssPrefixStyle = browserPrefix[key] 99 | ? browserPrefix[key] + "-" + baseStyle 100 | : baseStyle; 101 | let keyName = camelize(cssPrefixStyle); 102 | if (elementStyle[keyName] !== undefined) { 103 | return endEventListenerPrefixList[baseStyle][keyName]; 104 | } 105 | } else { 106 | let cssPrefixStyle = browserPrefix[key] 107 | ? browserPrefix[key] + "-" + style 108 | : style; 109 | let keyName = camelize(cssPrefixStyle); 110 | if (elementStyle[keyName] !== undefined) { 111 | return keyName; 112 | } 113 | } 114 | } 115 | return ""; 116 | } 117 | 118 | /** 119 | * @desc 返回当前节点在兄弟节点中的索引 120 | * @param {*} el class id node 121 | */ 122 | export function nodeIndex(el) { 123 | return Array.prototype.findIndex.call( 124 | getElement(el).parentNode.children, 125 | item => getElement(el).isSameNode(item) 126 | ); 127 | } 128 | 129 | /** 130 | * @desc 滚动到顶部 131 | * @param {string} el 元素的选择器或或者元素本身 132 | */ 133 | export function scrollToTop(el = "body") { 134 | const scrollTop = getElement(el).scrollTop; 135 | if (scrollTop > 0) { 136 | window.requestAnimationFrame(() => scrollToTop(getElement(el))); 137 | getElement(el).scrollTop = scrollTop - scrollTop / 8; 138 | } 139 | } 140 | 141 | /** 142 | * @desc 页面调试工具 143 | */ 144 | export function layoutDebug() { 145 | var styleEl = document.createElement("style"); 146 | styleEl.classList.add("page-debug"); 147 | styleEl.innerHTML = ` 148 | * { background-color: rgba(255,0,0,.2); } 149 | * * { background-color: rgba(0,255,0,.2); } 150 | * * * { background-color: rgba(0,0,255,.2); } 151 | * * * * { background-color: rgba(255,0,255,.2); } 152 | * * * * * { background-color: rgba(0,255,255,.2); } 153 | * * * * * * { background-color: rgba(255,255,0,.2); } 154 | * * * * * * * { background-color: rgba(255,0,0,.2); } 155 | * * * * * * * * { background-color: rgba(0,255,0,.2); } 156 | * * * * * * * * * { background-color: rgba(0,0,255,.2); } 157 | * * * * * * * * * * { background-color: rgba(0,0,255,.2); } 158 | `; 159 | document.querySelector("head").appendChild(styleEl); 160 | } 161 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file apicloud 常用工具封装 3 | * @author viarotel 4 | */ 5 | 6 | const _hasOwnProperty = Object.prototype.hasOwnProperty; 7 | 8 | const _toString = Object.prototype.toString; 9 | 10 | /** 11 | * @desc 获取原始类型 12 | * @param {any} value toRawType('') String Array Date Number Function Boolean Null 13 | */ 14 | export function toRawType(value) { 15 | return _toString.call(value).slice(8, -1); 16 | } 17 | 18 | export function isObject(input) { 19 | return toRawType(input) === "Object"; 20 | } 21 | export function isArray(input) { 22 | return input instanceof Array || toRawType(input) === "Array"; 23 | } 24 | export function isDate(input) { 25 | return input instanceof Date || toRawType(input) === "Date"; 26 | } 27 | export function isNumber(input) { 28 | return input instanceof Number || toRawType(input) === "Number"; 29 | } 30 | 31 | export function isString(input) { 32 | return input instanceof String || toRawType(input) === "String"; 33 | } 34 | 35 | export function isStringNumber(value) { 36 | return /^\d+$/.test(value) && isString(value); 37 | } 38 | 39 | export function isBoolean(input) { 40 | return typeof input == "boolean"; 41 | } 42 | export function isFunction(input) { 43 | return typeof input == "function"; 44 | } 45 | export function isNull(input) { 46 | return input === undefined || input === null; 47 | } 48 | 49 | export function isNullObject(input) { 50 | return !Object.keys(input).length; 51 | } 52 | 53 | export function isPlainObject(obj) { 54 | if ( 55 | obj && 56 | toRawType(obj) === "Object" && 57 | obj.constructor === Object && 58 | !_hasOwnProperty.call(obj, "constructor") 59 | ) { 60 | var key; 61 | for (key in obj) { 62 | // console.log("key", key); 63 | } 64 | return key === undefined || _hasOwnProperty.call(obj, key); 65 | } 66 | return false; 67 | } 68 | export function freeze(obj) { 69 | //冻结对象 70 | Object.freeze(obj); 71 | Object.keys(obj).forEach(function(key) { 72 | if (isObject(obj[key])) { 73 | freeze(obj[key]); 74 | } 75 | }); 76 | return obj; 77 | } 78 | 79 | /** 80 | * @desc 深度克隆 81 | * @param {object} value 82 | * @returns {object} 83 | */ 84 | export function deepClone(value) { 85 | let ret; 86 | 87 | switch (toRawType(value)) { 88 | case "Object": 89 | ret = {}; 90 | break; 91 | case "Array": 92 | ret = []; 93 | break; 94 | default: 95 | return value; 96 | } 97 | 98 | Object.keys(value).forEach((key) => { 99 | const copy = value[key]; 100 | ret[key] = deepClone(copy); 101 | }); 102 | 103 | return ret; 104 | } 105 | 106 | /** 107 | * @desc 深度合并克隆 108 | * @param {object} origin 原对象 109 | * @param {...object} params 多个对象 110 | */ 111 | export function deepAssign(origin, ...objs) { 112 | let tempObj = objs.reduce((obj, i) => { 113 | obj = { ...obj, ...i }; 114 | return obj; 115 | }, {}); 116 | 117 | return deepClone({ ...origin, ...tempObj }); 118 | } 119 | 120 | /** 121 | * @desc 检测指定的一个或多个keys在obj中是否同时存在 122 | * @param {object} obj 123 | * @param {string} keys 124 | */ 125 | export function isAttrs(obj, ...keys) { 126 | return !keys.some((i) => !_hasOwnProperty.call(obj, i)); 127 | } 128 | 129 | /** 130 | * @desc 检测指定的一个或多个keys在obj中是否具有不存在的 131 | * @param {object} obj 132 | * @param {string} keys 133 | */ 134 | export function isNoAttrs(obj, ...keys) { 135 | return keys.some((i) => !_hasOwnProperty.call(obj, i)); 136 | } 137 | 138 | /** 139 | * @desc 获取指定路径的对象的值 140 | * @param {object} obj 141 | * @param {string} keypath 路径 142 | * @returns {any} 143 | */ 144 | export function getKeyValue(obj, keypath) { 145 | if (!isObject(obj)) { 146 | return null; 147 | } 148 | let array = null; 149 | if (isArray(keypath)) { 150 | array = keypath; 151 | } else if (isString(keypath)) { 152 | array = keypath.split("."); 153 | } 154 | if (array == null || array.length == 0) { 155 | return null; 156 | } 157 | let value = null; 158 | let key = array.shift(); 159 | const keyTest = key.match(new RegExp("^(\\w+)\\[(\\d+)\\]$")); 160 | if (keyTest) { 161 | key = keyTest[1]; 162 | let index = keyTest[2]; 163 | value = obj[key]; 164 | if (isArray(value) && value.length > index) { 165 | value = value[index]; 166 | } 167 | } else { 168 | value = obj[key]; 169 | } 170 | 171 | if (array.length > 0) { 172 | return getKeyValue(value, array); 173 | } 174 | return value; 175 | } 176 | /** 177 | * @desc 根据指定的路径设置对象的值 178 | * @param {*} obj 179 | * @param {*} keypath 路径 180 | * @param {*} value 181 | * @param {*} orignal 182 | * @returns {object} 183 | */ 184 | export function setKeyValue(obj, keypath, value, orignal) { 185 | if (!isObject(obj)) { 186 | return false; 187 | } 188 | let array = null; 189 | if (isArray(keypath)) { 190 | array = keypath; 191 | } else if (isString(keypath)) { 192 | array = keypath.split("."); 193 | orignal = obj; 194 | } 195 | if (array == null || array.length == 0) { 196 | return false; 197 | } 198 | let children = null; 199 | let index = 0; 200 | let key = array.shift(); 201 | const keyTest = key.match(new RegExp("^(\\w+)\\[(\\d+)\\]$")); 202 | if (keyTest) { 203 | key = keyTest[1]; 204 | index = keyTest[2]; 205 | children = obj[key]; 206 | if (isArray(children) && children.length > index) { 207 | if (array.length > 0) { 208 | return setKeyValue(children[index], array, value, orignal); 209 | } 210 | children[index] = value; 211 | } 212 | } else { 213 | if (array.length > 0) { 214 | return setKeyValue(obj[key], array, value, orignal); 215 | } 216 | obj[key] = value; 217 | } 218 | return orignal; 219 | } 220 | 221 | /** 222 | * @desc 返回范围返回范围内的随机整数 223 | * @param {number} min 224 | * @param {number} max 225 | * @returns {number} 226 | */ 227 | export function getRandomInt(min, max) { 228 | // Math.random()不包括1,有缺陷 229 | return (Math.random() * (max - min + 1) + min) | 0; 230 | } 231 | 232 | /** 233 | * @desc 传入数组 打乱数组中值的顺序 234 | * @param {array} arr 235 | * @returns {array} 236 | */ 237 | export function shuffle(arr) { 238 | let _arr = arr.slice(); 239 | for (let i = 0; i < _arr.length; i++) { 240 | let j = getRandomInt(0, i); 241 | let t = _arr[i]; 242 | _arr[i] = _arr[j]; 243 | _arr[j] = t; 244 | } 245 | return _arr; 246 | } 247 | 248 | /** 249 | * @desc 重复字符串 250 | * @param {string} str 字符串 251 | * @param {number} num 重复的次数 252 | * @returns {string} 253 | */ 254 | export function stringRepeat(str, num) { 255 | return new Array(num + 1).join(str); 256 | } 257 | /** 258 | * @desc 左侧补零 259 | * @param {string} str 字符串 260 | * @param {number} n 总位数 padLeftZero('0', n = 2) 00 padLeftZero('11', n = 3) 011 261 | * @returns {string} 262 | */ 263 | export function padLeftZero(str, n = 2) { 264 | return (stringRepeat("0", n) + str).substr(str.length); 265 | } 266 | 267 | /** 268 | * @desc 格式化时间 269 | * @param {*} date 270 | * @param {*} format 271 | * @returns {string} 272 | */ 273 | export function formatDate(date, format = "YYYY-MM-DD hh:mm:ss") { 274 | const o = { 275 | "Y+": date.getFullYear(), 276 | "M+": date.getMonth() + 1, 277 | "D+": date.getDate(), 278 | "h+": date.getHours(), 279 | "m+": date.getMinutes(), 280 | "s+": date.getSeconds(), 281 | "t+": date.getMilliseconds(), 282 | }; 283 | for (const k in o) { 284 | if (new RegExp(`(${k})`).test(format)) { 285 | const str = o[k] + ""; 286 | format = format.replace(RegExp.$1, padLeftZero(str, RegExp.$1.length)); 287 | } 288 | } 289 | return format; 290 | } 291 | 292 | /** 293 | * @desc 格式化倒计时 294 | * @param {number} countDownStamp 295 | * @param {string} format 296 | * @returns {string} 297 | */ 298 | export function formatCountDown(countDownStamp, format = "DD天 hh:mm:ss") { 299 | if (countDownStamp < 0) { 300 | countDownStamp = 0; 301 | } 302 | const millisecond = countDownStamp % 1000; 303 | const restSecond = (countDownStamp - millisecond) / 1000; 304 | const second = restSecond % 60; 305 | const restMinute = (restSecond - second) / 60; 306 | const minute = restMinute % 60; 307 | const restHour = (restMinute - minute) / 60; 308 | const hour = restHour % 24; 309 | const restDay = (restHour - hour) / 24; 310 | const day = restDay; 311 | const o = { 312 | "D+": day, 313 | "h+": hour, 314 | "m+": minute, 315 | "s+": second, 316 | "t+": millisecond, 317 | }; 318 | for (const k in o) { 319 | if (new RegExp(`(${k})`).test(format)) { 320 | const str = o[k] + ""; 321 | format = format.replace(RegExp.$1, padLeftZero(str, RegExp.$1.length)); 322 | } 323 | } 324 | return format; 325 | } 326 | 327 | /** 328 | * @desc 等待该函数执行成功后进行下一步 329 | * @param {number}} time 等待的时间 330 | */ 331 | export function wait(time) { 332 | return new Promise((resolve) => { 333 | setTimeout(() => { 334 | resolve(true); 335 | }, Number(time)); 336 | }); 337 | } 338 | 339 | /** 340 | * @desc 获取url参数 341 | * @param {string} url 要提取参数的url 342 | * @returns {object} 返回值为对象 343 | */ 344 | export function getUrlParams(url) { 345 | url = url == null ? window.location.href : url; 346 | const search = url.substring(url.lastIndexOf("?") + 1); 347 | const obj = {}; 348 | const reg = /([^?&=]+)=([^?&=]*)/g; 349 | search.replace(reg, (rs, $1, $2) => { 350 | const name = decodeURIComponent($1); 351 | let val = decodeURIComponent($2); 352 | val = String(val); 353 | obj[name] = val; 354 | return rs; 355 | }); 356 | return obj; 357 | } 358 | 359 | /** 360 | * @desc 将参数附加到url中 361 | * @param {string} originUrl 362 | * @param {object} data 363 | * @returns {string} url 364 | */ 365 | export function parseParamUrl(originUrl, data) { 366 | let url = ""; 367 | for (const k in data) { 368 | let value = data[k] !== undefined ? data[k] : ""; 369 | url += `&${k}=${encodeURIComponent(value)}`; 370 | } 371 | url = url ? url.substring(1) : ""; 372 | 373 | originUrl += (originUrl.indexOf("?") === -1 ? "?" : "&") + url; 374 | 375 | return originUrl; 376 | } 377 | 378 | /** 379 | * @desc "-"转驼峰 380 | * @param {string} str 381 | */ 382 | export function camelize(str) { 383 | str = String(str); 384 | return str.replace(/-(\w)/g, function(m, c) { 385 | return c ? c.toUpperCase() : ""; 386 | }); 387 | } 388 | 389 | /** 390 | * @desc 驼峰转"-" 391 | * @param {string} str 392 | */ 393 | export function middleline(str) { 394 | str = String(str); 395 | return str.replace(/([A-Z])/g, "-$1").toLowerCase(); 396 | } 397 | 398 | /* 399 | * 版本号比较方法 400 | * 传入两个字符串,当前版本号:reqV;比较版本号:curV 401 | * 调用方法举例:compare("0.0.2","0.0.1"),将返回true 402 | */ 403 | export function compare(reqV, curV) { 404 | if (reqV && curV) { 405 | //将两个版本号拆成数字 406 | var arr1 = reqV.split("."), 407 | arr2 = curV.split("."); 408 | var minLength = Math.min(arr1.length, arr2.length), 409 | position = 0, 410 | diff = 0; 411 | //依次比较版本号每一位大小,当对比得出结果后跳出循环 412 | while ( 413 | position < minLength && 414 | (diff = parseInt(arr1[position]) - parseInt(arr2[position])) == 0 415 | ) { 416 | position++; 417 | } 418 | diff = diff != 0 ? diff : arr1.length - arr2.length; 419 | //若reqV大于curV,则返回true 420 | return diff > 0; 421 | } else { 422 | //输入为空 423 | // console.log("版本号不能为空"); 424 | return false; 425 | } 426 | } 427 | 428 | /** 429 | * @desc 递归返回无重复的数组组合 430 | * @param {array} ...chunks 多个数组 例: combine(["iPhone X", "iPhone XS"],["黑色", "白色"]) 431 | * @returns [[ 'iPhone X', '黑色' ],[ 'iPhone X', '白色' ],[ 'iPhone XS', '黑色' ],[ 'iPhone XS', '白色' ]] 432 | */ 433 | export function combine(...chunks) { 434 | let res = []; 435 | 436 | let helper = function(chunkIndex, prev) { 437 | let chunk = chunks[chunkIndex]; 438 | let isLast = chunkIndex === chunks.length - 1; 439 | for (let val of chunk) { 440 | let cur = prev.concat(val); 441 | if (isLast) { 442 | // 如果已经处理到数组的最后一项了 则把拼接的结果放入返回值中 443 | res.push(cur); 444 | } else { 445 | helper(chunkIndex + 1, cur); 446 | } 447 | } 448 | }; 449 | 450 | // 从属性数组下标为 0 开始处理 451 | // 并且此时的 prev 是个空数组 452 | helper(0, []); 453 | 454 | return res; 455 | } 456 | 457 | /** 458 | * @desc 函数防抖 459 | * @param func 函数 460 | * @param wait 延迟执行毫秒数 461 | * @param immediate true 表立即执行,false 表非立即执行 462 | */ 463 | export function debounce(func, { wait = 500, immediate } = {}) { 464 | let timeout; 465 | return function() { 466 | let context = this; 467 | let args = arguments; 468 | 469 | if (timeout) clearTimeout(timeout); 470 | if (immediate) { 471 | let callNow = !timeout; 472 | timeout = setTimeout(() => { 473 | timeout = null; 474 | }, wait); 475 | if (callNow) func.apply(context, args); 476 | } else { 477 | timeout = setTimeout(() => { 478 | func.apply(context, args); 479 | }, wait); 480 | } 481 | }; 482 | } 483 | 484 | /** 485 | * @desc 函数节流 486 | * @param fn 函数 487 | * @param wait 延迟执行毫秒数 488 | * @param {object} trailing false 表示禁用停止触发的回调; leading false 表示禁用第一次执行 489 | */ 490 | export function throttle(fn, wait, { trailing = true, leading = true } = {}) { 491 | let timer; 492 | let previous = 0; 493 | let throttled = function() { 494 | let now = +new Date(); 495 | // remaining 不触发下一次函数的剩余时间 496 | if (!previous && leading === false) previous = now; 497 | let remaining = wait - (now - previous); 498 | if (remaining < 0) { 499 | if (timer) { 500 | clearTimeout(timer); 501 | timer = null; 502 | } 503 | previous = now; 504 | fn.apply(this, arguments); 505 | } else if (!timer && trailing !== false) { 506 | timer = setTimeout(() => { 507 | previous = leading === false ? 0 : new Date().getTime(); 508 | timer = null; 509 | fn.apply(this, arguments); 510 | }, remaining); 511 | } 512 | }; 513 | return throttled; 514 | } 515 | 516 | /** 517 | * @desc 获取uuid 518 | * @returns {string} 519 | */ 520 | export function uuid() { 521 | const s4 = () => { 522 | return Math.floor((1 + Math.random()) * 0x10000) 523 | .toString(16) 524 | .substring(1); 525 | }; 526 | return ( 527 | s4() + 528 | s4() + 529 | "-" + 530 | s4() + 531 | "-" + 532 | s4() + 533 | "-" + 534 | s4() + 535 | "-" + 536 | s4() + 537 | s4() + 538 | s4() 539 | ); 540 | } 541 | 542 | /** 543 | * @desc 返回占位图 544 | * @param w 宽 545 | * @param h 高 546 | * @returns {string} 链接地址 547 | */ 548 | export function tempImage(w, h) { 549 | // let tempStr = "http://placekitten.com/" + w + "/"; 550 | // let tempStr = "https://dummyimage.com/" + w + "x"; 551 | let tempStr = "http://lorempixel.com/" + w + "/"; 552 | if (arguments.length === 1) { 553 | tempStr += w; 554 | } else { 555 | tempStr += h; 556 | } 557 | return tempStr; 558 | } 559 | 560 | /** 561 | * @todo 562 | * @desc 文件转Base64 563 | * @param {string || array} paths 文件路径 564 | * @returns {promise} base64 565 | */ 566 | export function getBase64(paths) { 567 | if (isArray(paths)) { 568 | let asyncArr = paths.reduce((arr, i) => { 569 | arr.push(fileToBase64(i)); 570 | return arr; 571 | }, []); 572 | return Promise.all(asyncArr); 573 | } else { 574 | return fileToBase64(paths); 575 | } 576 | 577 | function fileToBase64(path) { 578 | return new Promise((resolve) => { 579 | const reader = new FileReader(); 580 | reader.addEventListener("load", () => resolve(reader.result)); 581 | reader.readAsDataURL(path); 582 | }); 583 | } 584 | } 585 | 586 | /** 587 | * @desc 检测当前设备类型 588 | * @returns {object} 589 | */ 590 | export function checkDevice() { 591 | const ua = window.navigator.userAgent; 592 | 593 | const isAndroid = /(Android);?[\s/]+([\d.]+)?/.test(ua); 594 | 595 | const isIpad = /(iPad).*OS\s([\d_]+)/.test(ua); 596 | const isIpod = /(iPod)(.*OS\s([\d_]+))?/.test(ua); 597 | const isIphone = !isIpad && /(iPhone\sOS)\s([\d_]+)/.test(ua); 598 | const isMac = 599 | /macintosh|mac os x/i.test(ua) && !isIpad && !isIpod && !isIphone; 600 | 601 | const isWechat = /micromessenger/i.test(ua); 602 | const isWindows = /windows|win32/i.test(ua); 603 | 604 | //window.navigator.userAgent.match(/APICloud/i) 开发环境和生产环境都有效,但需要在 config.xml中配置 配置 并云编译环境下才有效 ios loader状态下无效 605 | //window.location.protocol === 'file:' 判断是否为file 从而推断实在手机上运行 如果当前环境为开发环境则无效 606 | //false | true 为手动控制 607 | const isAPICloud = 608 | !!ua.match(/APICloud/i) || 609 | window.location.protocol === "file:" || 610 | !!window.api; 611 | 612 | return { 613 | isIpad, 614 | isIpod, 615 | isIphone, 616 | isMac, 617 | isWindows, 618 | isWechat, 619 | 620 | isAndroid, 621 | isIos: isIpad || isIpod || isIphone, 622 | isPc: isWindows || isMac, 623 | isAPICloud, 624 | }; 625 | } 626 | export const isAndroid = () => checkDevice().isAndroid; 627 | export const isIos = () => checkDevice().isIos; 628 | export const isPc = () => checkDevice().isPc; 629 | export const isAPICloud = () => checkDevice().isAPICloud; 630 | 631 | /** 632 | * @todo 633 | * @desc 解析blob响应内容并下载 634 | * @param {object} res blob响应内容 635 | */ 636 | export function resolveBlob(response) { 637 | // 提取文件名 638 | const fileName = response.headers["content-disposition"].match( 639 | /filename=(.*)/ 640 | )[1]; 641 | // 将二进制流转为blob 642 | const blob = new Blob([response.data], { type: "application/octet-stream" }); 643 | if (typeof window.navigator.msSaveBlob !== "undefined") { 644 | // 兼容IE,window.navigator.msSaveBlob:以本地方式保存文件 645 | window.navigator.msSaveBlob(blob, decodeURI(fileName)); 646 | } else { 647 | // 创建新的URL并指向File对象或者Blob对象的地址 648 | const blobURL = window.URL.createObjectURL(blob); 649 | // 创建a标签,用于跳转至下载链接 650 | const tempLink = document.createElement("a"); 651 | tempLink.style.display = "none"; 652 | tempLink.href = blobURL; 653 | tempLink.setAttribute("download", decodeURI(fileName)); 654 | // 兼容:某些浏览器不支持HTML5的download属性 655 | if (typeof tempLink.download === "undefined") { 656 | tempLink.setAttribute("target", "_blank"); 657 | } 658 | // 挂载a标签 659 | document.body.appendChild(tempLink); 660 | tempLink.click(); 661 | document.body.removeChild(tempLink); 662 | // 释放blob URL地址 663 | window.URL.revokeObjectURL(blobURL); 664 | } 665 | } 666 | 667 | // 回显数据字典 668 | export function getDictLabel(datas, value) { 669 | // console.log("datas", datas); 670 | // console.log("value", value); 671 | var actions = []; 672 | Object.keys(datas).map((key) => { 673 | if (datas[key].dictValue == "" + value) { 674 | actions.push(datas[key].dictLabel); 675 | return false; 676 | } 677 | }); 678 | return actions.join(""); 679 | } 680 | 681 | /** 682 | * 683 | * @desc 对象结构转数组结构 684 | * @param {object} obj 685 | * @param {object} options 参数配置 686 | */ 687 | export function objectToArray( 688 | obj, 689 | { keyName = "id", valueName = "name", mixin } = {} 690 | ) { 691 | return Object.keys(obj).reduce((arr, i, iIndex) => { 692 | const tempObj = { 693 | [keyName]: i, 694 | [valueName]: obj[i], 695 | }; 696 | arr.push({ 697 | ...tempObj, 698 | ...(mixin ? mixin(tempObj, iIndex) : {}), 699 | }); 700 | return arr; 701 | }, []); 702 | } 703 | 704 | /** 705 | * 706 | * @desc 对对象进行遍历 707 | * @param {object} obj 708 | * @param {function} callBack 回调 709 | */ 710 | export function mapObject(obj, callBack) { 711 | if (!callBack) return Error("回调不能为空!"); 712 | 713 | return Object.keys(obj).map((key, index) => 714 | callBack(obj[key], { key, index, obj }) 715 | ); 716 | } 717 | 718 | /** 719 | * @desc 检测输入的色值是否合法 720 | * @param value 色值 721 | */ 722 | export function isColorValue(value) { 723 | let type = ""; 724 | if (/^rgb\(/.test(value)) { 725 | //如果是rgb开头,200-249,250-255,0-199 726 | type = 727 | "^[rR][gG][Bb][(]([\\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\\s]*,){2}[\\s]*(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)[\\s]*[)]{1}$"; 728 | } else if (/^rgba\(/.test(value)) { 729 | //如果是rgba开头,判断0-255:200-249,250-255,0-199 判断0-1:0 1 1.0 0.0-0.9 730 | type = 731 | "^[rR][gG][Bb][Aa][(]([\\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\\s]*,){3}[\\s]*(1|1.0|0|0.[0-9])[\\s]*[)]{1}$"; 732 | } else if (/^#/.test(value)) { 733 | //六位或者三位 734 | type = "^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$"; 735 | } else if (/^hsl\(/.test(value)) { 736 | //判断0-360 判断0-100%(0可以没有百分号) 737 | type = 738 | "^[hH][Ss][Ll][(]([\\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*)[)]$"; 739 | } else if (/^hsla\(/.test(value)) { 740 | type = 741 | "^[hH][Ss][Ll][Aa][(]([\\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*,){2}([\\s]*(1|1.0|0|0.[0-9])[\\s]*)[)]$"; 742 | } 743 | let re = new RegExp(type); 744 | if (value.match(re) == null) { 745 | return false; 746 | } else { 747 | return true; 748 | } 749 | } 750 | 751 | export * from "./dom.js"; 752 | export * from "./vue.js"; 753 | -------------------------------------------------------------------------------- /src/utils/vue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc 由一个组件,向上找到最近的指定组件;由一个组件,向上找到所有的指定组件; 由一个组件,向下找到最近的指定组件;由一个组件,向下找到所有指定的组件;由一个组件,找到指定组件的兄弟组件。 3 | * @param {*} ctx 上下文 4 | * @param {*} componentName 组件名称 5 | */ 6 | 7 | export function findComponentUpward(componentName, ctx = this) { 8 | let parent = ctx.$parent; 9 | let name = parent.$options.name; 10 | 11 | while (parent && (!name || [componentName].indexOf(name) < 0)) { 12 | parent = parent.$parent; 13 | if (parent) name = parent.$options.name; 14 | } 15 | return parent; 16 | } -------------------------------------------------------------------------------- /src/views/account/components/Layout/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/views/account/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/views/account/login/index.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/MainContent/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/Navbar/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 43 | 44 | 64 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/Navbar/MenuRight.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/Navbar/QuickBar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/Navbar/TagsView/ScrollPane.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/Navbar/TagsView/index.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 210 | 211 | 217 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/Navbar/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/Sidebar/Icon.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 131 | 132 | 155 | -------------------------------------------------------------------------------- /src/views/home/components/Layout/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/views/home/demo/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/views/home/test/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/views/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const tailwindConfig = require("./src/config/tailwind"); 2 | module.exports = tailwindConfig; -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const rimraf = require("rimraf"); 3 | const resolve = (dir) => path.join(__dirname, dir); 4 | const devServerConfig = require("./src/config/devServer"); 5 | const pageConfig = require("./src/config"); 6 | const isProduction = process.env.NODE_ENV === "production"; 7 | const isDevelopment = process.env.NODE_ENV === "development"; 8 | const port = devServerConfig.port || 8000; 9 | isDevelopment && 10 | rimraf(resolve("./dist"), (err) => { 11 | if (err) throw err; 12 | }); 13 | 14 | module.exports = { 15 | publicPath: isProduction ? "./" : "./", 16 | outputDir: "dist", 17 | assetsDir: "static", 18 | lintOnSave: process.env.NODE_ENV === "development", 19 | productionSourceMap: isProduction, // 生产环境时禁用定位源码 20 | filenameHashing: isProduction, // 生产环境时开启文件哈希值, 21 | devServer: { 22 | // 环境配置 23 | port, 24 | hot: true, // false防止开发模式白屏(使用路由缓存时) 25 | open: false, // 编译完成后打开浏览器 26 | proxy: { 27 | /** 解决本地测试跨域问题 */ 28 | [`/${devServerConfig.apiRoot}`]: { 29 | target: devServerConfig.proxyUrl, 30 | pathRewrite: { 31 | [`^/${devServerConfig.apiRoot}`]: "", 32 | }, 33 | }, 34 | }, 35 | }, 36 | //指定需要编译的依赖 37 | transpileDependencies: [ 38 | // "vuetify", 39 | // "lottie-player-vue", 40 | // "vue-awesome-swiper", 41 | // "vue-tippy", 42 | // "axios-apicloud-adapter", 43 | // "vue-screen" 44 | ], 45 | 46 | chainWebpack(config) { 47 | // it can improve the speed of the first screen, it is recommended to turn on preload 48 | config.plugin("preload").tap(() => [ 49 | { 50 | rel: "preload", 51 | // to ignore runtime.js 52 | // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171 53 | fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], 54 | include: "initial", 55 | }, 56 | ]); 57 | 58 | // when there are many pages, it will cause too many meaningless requests 59 | config.plugins.delete("prefetch"); 60 | 61 | // set svg-sprite-loader 62 | config.module 63 | .rule("svg") 64 | .exclude.add(resolve("src/icons")) 65 | .end(); 66 | config.module 67 | .rule("icons") 68 | .test(/\.svg$/) 69 | .include.add(resolve("src/icons")) 70 | .end() 71 | .use("svg-sprite-loader") 72 | .loader("svg-sprite-loader") 73 | .options({ 74 | symbolId: "icon-[name]", 75 | }) 76 | .end(); 77 | 78 | config.when(isProduction, (config) => { 79 | config 80 | .plugin("ScriptExtHtmlWebpackPlugin") 81 | .after("html") 82 | .use("script-ext-html-webpack-plugin", [ 83 | { 84 | // `runtime` must same as runtimeChunk name. default is `runtime` 85 | inline: /runtime\..*\.js$/, 86 | }, 87 | ]) 88 | .end(); 89 | config.optimization.splitChunks({ 90 | chunks: "all", 91 | cacheGroups: { 92 | libs: { 93 | name: "chunk-libs", 94 | test: /[\\/]node_modules[\\/]/, 95 | priority: 10, 96 | chunks: "initial", // only package third parties that are initially dependent 97 | }, 98 | elementUI: { 99 | name: "chunk-elementUI", // split elementUI into a single package 100 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app 101 | test: /[\\/]node_modules[\\/]_?element-plus(.*)/, // in order to adapt to cnpm 102 | }, 103 | commons: { 104 | name: "chunk-commons", 105 | test: resolve("src/components"), // can customize your rules 106 | minChunks: 3, // minimum common number 107 | priority: 5, 108 | reuseExistingChunk: true, 109 | }, 110 | }, 111 | }); 112 | // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk 113 | config.optimization.runtimeChunk("single"); 114 | }); 115 | }, 116 | 117 | configureWebpack: { 118 | // provide the app's title in webpack's name field, so that 119 | // it can be accessed in index.html to inject the correct title. 120 | name: pageConfig.title, 121 | resolve: { 122 | alias: { 123 | "@": resolve("src"), 124 | }, 125 | }, 126 | }, 127 | 128 | css: { 129 | loaderOptions: { 130 | // sass: { 131 | // prependData: `@import "~@/assets/css/vuetify-custom.scss"`, 132 | // }, 133 | scss: { 134 | // prependData: `@import "~@/assets/css/vuetify-custom.scss";`, 135 | prependData: Object.keys(pageConfig.theme) 136 | .map((key) => `${key}: ${pageConfig.theme[key]};`) 137 | .join("\n"), 138 | }, 139 | }, 140 | }, 141 | }; 142 | --------------------------------------------------------------------------------