├── .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 |
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 |
2 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
44 |
--------------------------------------------------------------------------------
/src/components/ViaScreenfull.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/components/ViaSvgIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
16 |
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 |
2 |
8 |
11 |
![qianye]()
16 |
{{ title }}
17 |
18 |
25 |
26 |
27 |
28 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/views/account/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/views/account/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 登录
5 |
6 |
12 |
13 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
31 |
32 |
36 |
37 |
38 |
39 |
40 |
41 |
45 | 记住密码
46 |
47 |
53 |
57 | 忘记密码
58 |
59 |
60 |
66 | 登录
67 |
68 |
69 |
70 |
71 |
72 |
73 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/MainContent/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/Navbar/Breadcrumb.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
12 | {{ item.title }}
13 |
14 |
15 |
16 |
17 |
43 |
44 |
64 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/Navbar/MenuRight.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
17 |
18 |
19 |
20 |
26 | {{ getTitle(item) }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/Navbar/QuickBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
18 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/Navbar/TagsView/ScrollPane.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/Navbar/TagsView/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
28 | {{ item.title }}
29 |
30 |
31 |
32 |
33 |
34 | 刷新
35 |
36 |
40 | 关闭
41 |
42 |
43 | 关闭其他
44 |
45 |
46 | 关闭所有
47 |
48 |
49 |
50 |
51 |
52 |
210 |
211 |
217 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/Navbar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/Sidebar/Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
11 |
12 |
13 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/Sidebar/Item.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | {{ getTitle(item) }}
9 |
10 |
15 |
16 |
22 |
23 |
24 | {{ getTitle(item) }}
25 |
26 |
27 |
28 |
29 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/Sidebar/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
![]()
11 |
{{ title }}
15 |
16 |
17 |
18 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
15 |
20 |
21 |
22 |
32 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
131 |
132 |
155 |
--------------------------------------------------------------------------------
/src/views/home/components/Layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/views/home/demo/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 演示页面
5 |
6 |
7 |
11 | 跳转test
12 |
13 |
14 |
15 |
16 |
17 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/views/home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/views/home/test/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 测试页面
5 |
6 |
7 |
11 | 跳转demo
12 |
13 |
14 |
15 |
16 |
17 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/views/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------