Compare commits

...

148 Commits

Author SHA1 Message Date
林鑫 6a7019ec1a
Update FUNDING.yml 2024-05-25 10:29:18 +08:00
林鑫 7375986a8e
Update README.md 2024-05-25 10:24:33 +08:00
林鑫 251936dcdc
Update README.md 2024-05-25 10:21:48 +08:00
lin-xin 921c38cea2 修改username 2024-04-18 22:51:36 +08:00
lin-xin 718f255817 优化样式和权限 2024-04-18 22:46:26 +08:00
lin-xin 433b60e720 更新文档 2024-04-13 11:07:56 +08:00
lin-xin b9a4402431 大版本更新 2024-04-13 10:59:18 +08:00
林鑫 daaf393872
Update README.md 2024-01-12 10:34:18 +08:00
lin-xin c8cae1b200 升级了部分功能 2024-01-06 18:57:49 +08:00
lin-xin f70c93715b 修复样式 2022-12-17 13:51:29 +08:00
lin-xin 60a293514d 新增了导入导出excel,以及做了一些优化 2022-12-17 13:37:31 +08:00
林鑫 8312ba1d6c
Update README.md 2022-09-02 21:04:06 +08:00
lin-xin eea5e368a9 fix:修改引入Home.vue为小写 2022-09-02 20:57:57 +08:00
lin-xin 759beaf940 fix:修改element-plus默认语言为中文 2022-09-02 20:54:09 +08:00
lin-xin 9237de9b89 fix:文件名改为小写 2022-09-02 20:50:30 +08:00
lin-xin 995ab69b1e 改为typescript;升级elementplus 2022-08-21 16:59:12 +08:00
lin-xin a072752609 feat:使用pinia替换vuex;新增markdown编辑器 2022-07-03 11:26:09 +08:00
林鑫 d28f504a80
Merge pull request #314 from zhufengning/master
The import should be "wangeditor" instead of "wangEditor"
2022-04-15 18:53:56 +08:00
林鑫 e82a457a2c
Merge pull request #327 from qmzorvim123/master
fix:修改 vue-schart访问地址找不到问题
2022-04-15 18:49:38 +08:00
qmzorvim123 256b3354ca
vue-schart访问地址找不到 2022-01-18 17:17:13 +08:00
qmzorvim123 be39b534c1
vue-schart访问地址找不到 2022-01-18 17:16:24 +08:00
林鑫 274198a7bd
Merge pull request #317 from swuecho/master
添加 好问 到赞助商
2021-11-27 20:54:07 +08:00
Hao Wu 439d48f8a3
添加 好问 到赞助商
谢谢
2021-11-27 00:43:04 +08:00
zhufengning 8aeb8b006e
The import should be "wangeditor" instead of "wangEditor"
error when starting dev server:
```
Error: The following dependencies are imported but could not be resolved:

  wangEditor (imported by /home/zfn/vue-manage-system/src/views/Editor.vue)

Are they installed?
```
2021-11-14 00:03:36 +08:00
linxin 039c9c3da4 fix:修复上传组件代码误删bug 2021-06-28 20:39:38 +08:00
lin-xin 9bc43e2efc feat:升级vite2和组合式api 2021-06-27 14:48:07 +08:00
林鑫 4dd1b4aae6
Update package.json 2021-04-15 16:34:50 +08:00
林鑫 3358fd7942
Update package.json 2021-04-08 19:34:49 +08:00
lin_xin 221aa3a5b9 style:更新文档 2021-03-17 22:19:34 +08:00
lin_xin 9e7ec0cea7 feature:路由缓存 2021-03-17 22:18:49 +08:00
lin_xin 98261e3eba Merge branch 'master' of https://github.com/lin-xin/vue-manage-system 2021-03-17 21:25:43 +08:00
lin_xin 1ae7fc8613 'style:更新文档' 2021-03-17 21:18:01 +08:00
林鑫 2874f01c34
Delete vue.config.js 2021-03-16 09:03:09 +08:00
lin_xin 96be359eab feature:升级到vue3和element-plus 2021-03-15 23:13:23 +08:00
林鑫 0f641c0f08
Create FUNDING.yml 2020-07-06 11:10:27 +08:00
林鑫 31f40a5e15
Merge pull request #223 from lin-xin/dev
Update README.md
2019-11-21 08:55:35 +08:00
林鑫 e8d1bfcad9
Update README.md 2019-11-21 08:54:51 +08:00
林鑫 6b73235ba3
Merge pull request #219 from lin-xin/dev
更新截图
2019-11-05 17:29:39 +08:00
linxin ce56f86f0d update:wms1.png 2019-11-05 17:28:57 +08:00
林鑫 03f67de289
Merge pull request #218 from lin-xin/dev
更新图表组件
2019-11-01 17:01:45 +08:00
linxin 5173cac28f 更新readme 2019-11-01 16:58:19 +08:00
linxin c5c018ac0f fix:修复bug和更新图表组件 2019-11-01 16:54:03 +08:00
linxin 3cfc7032bc 变更域名 2019-09-29 10:49:23 +08:00
林鑫 838a03086e
Merge pull request #208 from lin-xin/dev
EventBus折叠改为链式调用
2019-09-05 11:16:24 +08:00
linxin 2ebf10e9ba Login:添加登录 2019-08-30 15:03:18 +08:00
linxin 60eeaaf3eb EventBus折叠改为链式调用 2019-08-30 15:02:11 +08:00
林鑫 5b294b781b
Merge pull request #200 from lin-xin/dev
Table:修改搜索逻辑
2019-08-20 15:49:35 +08:00
linxin 3022814f8a Table:修改搜索逻辑 2019-08-20 15:46:42 +08:00
林鑫 21efb45431
Merge pull request #199 from lin-xin/dev
更新旧代码和依赖
2019-08-17 16:00:57 +08:00
linxin f7fb309680 合并pr 2019-08-17 15:58:46 +08:00
林鑫 d45bdbeeda
Merge pull request #195 from swuecho/patch-1
add backtop
2019-08-17 15:56:45 +08:00
林鑫 ed014e9304
Merge pull request #197 from swuecho/patch-2
左上角 el-icon-menu 改成 el-icon-s-fold 和 el-icon-s-unfold
2019-08-17 15:55:12 +08:00
linxin 289f817180 更新依赖 2019-08-17 15:53:42 +08:00
linxin a7b561baf5 更新依赖 2019-08-17 15:52:45 +08:00
linxin 49f8a69675 变更折叠侧边栏图标 2019-08-17 15:03:16 +08:00
linxin 3c2f4e891d 更新绿色主题 2019-08-17 14:58:33 +08:00
林鑫 c442dcfeb3
Merge pull request #198 from lin-xin/dev
优化表格、登录页面代码
2019-08-17 14:42:53 +08:00
linxin 2cb246ec3e 优化表格代码 2019-08-17 14:41:53 +08:00
linxin 3dd0b2fd8a 丰富表格组件 2019-08-17 11:47:15 +08:00
Hao Wu ba74b86448
Update Header.vue 2019-08-15 14:19:37 +08:00
Hao Wu 0ee9965612
add backtop 2019-08-15 14:00:36 +08:00
linxin 285ea37245 修改细节 2019-08-13 09:36:53 +08:00
linxin 53d067f0d1 添加prettier格式化 2019-08-09 10:34:32 +08:00
linxin a905850345 优化登录页面代码 2019-08-09 10:31:42 +08:00
林鑫 eb36dad968
Merge pull request #184 from lin-xin/dev
修复config文件注释错误
2019-06-28 11:07:06 +08:00
lin-xin d553a75d09 修复config文件注释错误 2019-06-28 11:05:46 +08:00
林鑫 38a5bd4bd0
Merge pull request #183 from lin-xin/dev
请求不使用代理
2019-06-23 15:39:19 +08:00
lin-xin bee36e1260 请求不使用代理 2019-06-23 15:38:24 +08:00
林鑫 5ac78cfd18
Merge pull request #178 from lin-xin/dev
修复表格顺序改变后误删其他数据
2019-06-13 17:07:08 +08:00
lin-xin ce21d13774 修复表格顺序改变后误删其他数据 2019-06-13 16:57:05 +08:00
林鑫 dc3405876e
Merge pull request #177 from lin-xin/dev
Update README.md
2019-05-26 17:02:57 +08:00
lin-xin 8c33d1b5b3 update 2019-05-26 17:02:06 +08:00
林鑫 e314ff0d3d
Merge pull request #171 from lin-xin/dev
更新LICENSE
2019-04-28 14:39:50 +08:00
lin-xin e8bb046ae6 更新LICENSE 2019-04-28 14:39:06 +08:00
林鑫 0e07e3f155
Merge pull request #170 from lin-xin/dev
修复小细节
2019-04-28 14:37:00 +08:00
lin-xin 55c00abf40 更新vue版本 2019-04-28 14:33:18 +08:00
lin-xin b9a79c943c '添加LICENCE' 2019-04-28 14:32:53 +08:00
林鑫 aa00e053ef
Merge pull request #169 from lin-xin/dev
新增国际化功能
2019-04-25 11:50:26 +08:00
lin-xin 95d97f1b37 新增国际化功能 2019-04-25 11:48:59 +08:00
林鑫 e0b844f0a3
Merge pull request #167 from lin-xin/dev
更新文档
2019-04-24 14:44:49 +08:00
lin-xin f5c8515803 更新文档 2019-04-24 14:42:57 +08:00
lin-xin 56b7dd9ac7 Merge branch 'master' into dev 2019-03-22 17:29:48 +08:00
lin-xin 41b0298e6a Merge branch 'master' of https://github.com/lin-xin/manage-system 2019-03-22 17:27:14 +08:00
林鑫 481c03937a
Update README.md 2019-03-15 17:20:10 +08:00
lin-xin 20cee7db43 iconfont-css兼容https 2019-01-08 14:21:13 +08:00
lin-xin 8f085b27fd 中断循环 2019-01-08 11:52:59 +08:00
林鑫 8608ee5385
Merge pull request #153 from lin-xin/dev
增加关闭当前标签页的方法
2019-01-08 11:41:58 +08:00
lin-xin 4ff06b7b9f 增加关闭当前标签页的方法 2019-01-08 11:40:35 +08:00
林鑫 05d6a7b586
Merge pull request #152 from lin-xin/dev
升级到vue-cli3
2019-01-07 11:11:41 +08:00
lin-xin 52f9f419e0 升级到vue-cli3 2019-01-07 11:10:04 +08:00
林鑫 718756b538
Merge pull request #150 from lin-xin/dev
修复使用index作为v-for的key
2018-12-28 16:31:31 +08:00
lin-xin 9b3052774b 修复使用index作为v-for的key 2018-12-28 16:29:50 +08:00
lin-xin 02c8872b67 '添加可拖拽弹框' 2018-09-12 20:56:20 +08:00
林鑫 81f571ec15
Merge pull request #128 from lin-xin/dev
'离开首页时移除监听事件'
2018-09-12 19:37:44 +08:00
lin-xin 6ac729663f '离开首页时移动监听事件' 2018-09-12 16:40:23 +08:00
林鑫 9d08cc0cf5
Merge pull request #127 from lin-xin/dev
'修改配置文件'
2018-09-11 16:46:51 +08:00
lin-xin 23235f33bc Merge branch 'dev' 2018-09-11 16:45:43 +08:00
lin-xin 8b9aca0665 '修改配置文件' 2018-09-11 16:44:31 +08:00
林鑫 0e2c295789
Merge pull request #126 from lin-xin/dev
'引入图标外链样式'
2018-09-11 16:31:31 +08:00
lin-xin 2eb44dd44e '引入图标外链样式' 2018-09-11 16:28:14 +08:00
林鑫 332a7d5b25
Merge pull request #125 from lin-xin/dev
'修改构建错误'
2018-09-11 16:06:07 +08:00
lin-xin 9f0e033db2 '修改构建错误' 2018-09-11 16:05:09 +08:00
林鑫 10c33fc587
Merge pull request #124 from lin-xin/dev
更新V3.2.0版本
2018-09-11 15:23:50 +08:00
lin-xin c1d7c1a1b2 '更新版本号' 2018-09-11 15:21:43 +08:00
lin-xin bdb412b039 '扩展更多图标' 2018-09-11 15:16:15 +08:00
lin-xin 286a7cc717 '调整首页样式' 2018-09-11 11:38:12 +08:00
lin-xin a532876a43 '修改页面图标和截图' 2018-09-11 10:34:41 +08:00
lin_xin 03a3ae4bda 修改登录页面 2018-09-10 22:54:26 +08:00
lin-xin aa05136933 '自动更新图表日期' 2018-09-10 21:04:40 +08:00
lin-xin 865f7d53d0 'dashboard增加图表' 2018-09-10 20:26:51 +08:00
lin-xin cd96319f97 '添加自定义图标' 2018-09-10 16:37:33 +08:00
lin-xin 90f44405ba '调整路由' 2018-09-10 11:49:22 +08:00
lin-xin 38a5d315aa '调整table组件样式' 2018-09-10 11:17:53 +08:00
林鑫 bc4a63f72f
Merge pull request #120 from lin-xin/dev
'更新package-lock.json'
2018-08-31 09:38:11 +08:00
lin-xin 77b67eedb9 '更新package-lock.json' 2018-08-31 09:37:35 +08:00
林鑫 905d001e89
Merge pull request #119 from lin-xin/dev
'更新package-lock.json'
2018-08-31 09:36:13 +08:00
lin-xin d2b0a2e777 '更新package-lock.json' 2018-08-31 09:34:54 +08:00
林鑫 81c08aadac
Merge pull request #118 from lin-xin/dev
解决个别issue
2018-08-31 09:33:09 +08:00
lin-xin 05c3625ed3 '更新readme' 2018-08-31 09:31:16 +08:00
lin-xin 23a33b27d6 '添加支持三级导航' 2018-08-02 08:32:22 +08:00
lin-xin 8b87d79f47 'tags栏保持在顶部/tags标签最多显示8个' 2018-07-26 17:26:43 +08:00
lin-xin 14e48d2c6e '更新element版本' 2018-07-26 17:02:46 +08:00
林鑫 188e3f1c6a
Merge pull request #112 from lin-xin/dev
更新mime依赖
2018-07-23 09:59:15 +08:00
lin-xin b4ccd9e353 更新mime依赖 2018-07-23 09:57:56 +08:00
林鑫 c6e9ee77e7
Merge pull request #98 from lin-xin/dev
修复bug
2018-06-01 14:54:15 +08:00
lin-xin c6a5a64419 '关闭标签后销毁组件' 2018-06-01 14:50:23 +08:00
林鑫 365149beb4
Merge pull request #97 from lin-xin/dev
'恢复tags路径'
2018-06-01 10:49:33 +08:00
lin-xin 367252196c '恢复tags路径' 2018-06-01 10:48:36 +08:00
林鑫 8758556822
Merge pull request #96 from lin-xin/dev
修复一些小细节
2018-06-01 10:27:04 +08:00
lin-xin 32c184ecbe 修改设置tags为全路径 2018-05-29 11:45:10 +08:00
lin_xin debd824a69 小屏幕默认收起侧边栏 2018-05-24 20:51:00 +08:00
lin-xin 25322e0bb0 '更新vue-schart版本' 2018-05-05 14:46:24 +08:00
林鑫 a61779b7dc
Merge pull request #80 from lin-xin/dev
合并V3.1.0
2018-04-23 20:50:36 +08:00
lin-xin ffd10f7d29 'V3.1.0' 2018-04-23 20:45:29 +08:00
lin-xin 00a7348e77 '新增favicon' 2018-04-23 20:33:34 +08:00
lin-xin fa98640a36 '修复beforEnter死循环' 2018-04-23 20:28:31 +08:00
lin-xin 7edde21472 '修改样式' 2018-04-23 20:17:21 +08:00
lin-xin df14656d8e '新增dashboard,移除readme.vue' 2018-04-23 19:43:32 +08:00
lin-xin 9601818407 'header新增全屏和消息功能' 2018-04-23 12:00:01 +08:00
lin-xin 13361b55ef '新增tabs标签页' 2018-04-19 20:51:27 +08:00
lin-xin 7880389033 '新增403页面' 2018-04-19 19:25:47 +08:00
lin-xin 6e54b705f7 '新增404页面' 2018-04-18 21:00:15 +08:00
lin-xin 2f1fe92a4f '升级mavonEditor兼容IE样式' 2018-04-18 17:19:28 +08:00
lin-xin 267dc854d8 '新增标签页切换功能' 2018-04-18 15:39:58 +08:00
lin-xin 6592cfde3a '移除DataSource相关' 2018-04-18 09:48:31 +08:00
lin-xin 05c0ab1db2 '增加表格编辑和删除操作' 2018-04-18 08:58:54 +08:00
林鑫 5c1ee6bd0b
Merge pull request #79 from lin-xin/dev
'更新README.md'
2018-04-13 17:02:34 +08:00
lin-xin 16e7be1234 '更新README.md' 2018-04-13 16:53:43 +08:00
131 changed files with 8187 additions and 3270 deletions

View File

@ -1,12 +0,0 @@
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime"]
}

View File

@ -1,9 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: https://lin-xin.github.io/images/weixin.jpg

27
.gitignore vendored
View File

@ -1,4 +1,23 @@
.DS_Store
node_modules/
dist/
npm-debug.log
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,9 +0,0 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
// to edit target browsers: use "browserslist" field in package.json
"postcss-import": {},
"autoprefixer": {}
}
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016-2023 vue-manage-system
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

223
README.md
View File

@ -1,193 +1,80 @@
# manage-system #
基于Vue.js 2.x系列 + Element UI 的后台管理系统解决方案。[线上地址](http://blog.gdfengshuo.com/example/work/)
# vue-manage-system
<a href="https://github.com/lin-xin/vue-manage-system/releases">
<img src="https://img.shields.io/github/release/lin-xin/vue-manage-system.svg" alt="GitHub release">
</a>
<a href="https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/mashape/apistatus.svg" alt="license">
</a>
基于 Vue3 + pinia + Element Plus 的后台管理系统解决方案。[线上演示](https://lin-xin.github.io/example/vue-manage-system/)
> Vue2 版本请看 [tag-V4.2.0](https://github.com/lin-xin/vue-manage-system/tree/V4.2.0),带后台功能请看 [tsrpc-manage-system](https://github.com/lin-xin/tsrpc-manage-system)
[文档地址](https://lin-xin.github.io/example/vuems-doc/)
[English document](https://github.com/lin-xin/manage-system/blob/master/README_EN.md)
## 捐赠
![微信扫一扫](http://blog.gdfengshuo.com/images/weixin.jpg)
## 赞助商
## 前言 ##
之前在公司用了Vue + Element组件库做了个后台管理系统基本很多组件可以直接引用组件库的但是也有一些需求无法满足。像图片裁剪上传、富文本编辑器、图表等这些在后台管理系统中很常见的功能就需要引用其他的组件才能完成。从寻找组件到使用组件的过程中遇到了很多问题也积累了宝贵的经验。所以我就把开发这个后台管理系统的经验总结成这个后台管理系统解决方案。
### 好问
该方案作为一套多功能的后台框架模板适用于绝大部分的后台管理系统Web Management System开发。基于vue.js,使用vue-cli脚手架快速生成项目目录引用Element UI组件库方便开发快速简洁好看的组件。分离颜色样式支持手动切换主题色而且很方便使用自定义主题色。
[<img src="https://static.bestqa.net/logo/bestqa_haowen.png" width="220" height="100">](https://www.bestqa.net/home/index.html)
## 功能 ##
- [x] Element UI
- [x] 登录/注销
- [x] 表格
- [x] 表单
- [x] 图表 :bar_chart:
- [x] 富文本编辑器
- [x] markdown编辑器
- [x] 图片拖拽/裁剪上传
- [x] 支持切换主题色 :sparkles:
- [x] 列表拖拽排序
专业问卷服务,一对一客服,按需定制
## 支持作者
## 目录结构介绍 ##
请作者喝杯咖啡吧!(微信号linxin_20)
|-- build // webpack配置文件
|-- config // 项目打包路径
|-- src // 源码目录
| |-- components // 组件
| |-- common // 公共组件
| |-- bus.js // Event Bus
| |-- Header.vue // 公共头部
| |-- Home.vue // 公共路由入口
| |-- Sidebar.vue // 公共左边栏
| |-- page // 主要路由页面
| |-- BaseCharts.vue // 基础图表
| |-- BaseForm.vue // 基础表单
| |-- BaseTable.vue // 基础表格
| |-- DragList.vue // 拖拽列表组件
| |-- Login.vue // 登录
| |-- Markdown.vue // markdown组件
| |-- Premission.vue // 权限测试组件
| |-- Readme.vue // 自述组件
| |-- Upload.vue // 图片上传
| |-- VueEditor.vue // 富文本编辑器
| |-- VueTable.vue // datasource表格组件
| |-- App.vue // 页面入口文件
| |-- main.js // 程序入口文件,加载各种公共组件
|-- .babelrc // ES6语法编译配置
|-- .editorconfig // 代码编写规格
|-- .gitignore // 忽略的文件
|-- index.html // 入口html文件
|-- package.json // 项目及工具的依赖配置文件
|-- README.md // 说明
![微信扫一扫](https://lin-xin.github.io/images/weixin.jpg)
## 前言
## 安装步骤 ##
该方案作为一套多功能的后台框架模板,适用于绝大部分的后台管理系统开发。基于 Vue3 + pinia + typescript引用 Element Plus 组件库,方便开发。实现逻辑简单,适合外包项目,快速交付。
git clone https://github.com/lin-xin/vue-manage-system.git // 把模板下载到本地
cd vue-manage-system // 进入模板目录
npm install // 安装项目依赖,等待安装完成之后
## 功能
## 本地开发 ##
- [x] Element Plus
- [x] vite 3
- [x] pinia
- [x] typescript
- [x] 登录/注册
- [x] Dashboard
- [x] 表格/表单
- [x] 图表 :bar_chart:
- [x] 富文本/markdown 编辑器
- [x] 图片拖拽/裁剪上传
- [x] 权限管理
- [x] 三级菜单
- [x] 自定义图标
- [x] 主题切换
// 开启服务器,浏览器访问 http://localhost:8080
npm run dev
## 安装步骤
## 构建生产 ##
> 因为使用 vite3node 版本需要 14.18+
// 执行构建命令生成的dist文件夹放在服务器下即可访问
npm run build
```
git clone https://github.com/lin-xin/vue-manage-system.git // 把模板下载到本地
cd vue-manage-system // 进入模板目录
npm install // 安装项目依赖,等待安装完成之后,安装失败可用 cnpm 或 yarn
## 组件使用说明与演示 ##
// 运行
npm run dev
### vue-schart ###
vue.js封装sChart.js的图表组件。访问地址[vue-schart](https://github.com/linxin/vue-schart)
<p><a href="https://www.npmjs.com/package/vue-schart"><img src="https://img.shields.io/npm/dm/vue-schart.svg" alt="Downloads"></a></p>
```JavaScript
<template>
<div>
<schart :canvasId="canvasId"
:type="type"
:width="width"
:height="height"
:data="data"
:options="options"
></schart>
</div>
</template>
<script>
import Schart from 'vue-schart'; // 导入Schart组件
export default {
data: function(){
return {
canvasId: 'myCanvas', // canvas的id
type: 'bar', // 图表类型
width: 500,
height: 400,
data: [
{name: '2014', value: 1342},
{name: '2015', value: 2123},
{name: '2016', value: 1654},
{name: '2017', value: 1795},
],
options: { // 图表可选参数
title: 'Total sales of stores in recent years'
}
}
},
components: {
Schart
}
}
</script>
// 执行构建命令生成的dist文件夹放在服务器下即可访问
npm run build
```
### element-ui ###
一套基于vue.js2.0的桌面组件库。访问地址:[element](http://element.eleme.io/#/zh-CN/component/layout)
## 项目截图
### vue-datasource ###
一个用于动态创建表格的vue.js服务端组件。访问地址[vue-datasource](https://github.com/coderdiaz/vue-datasource)
### Vue-Quill-Editor ###
基于Quill、适用于Vue2的富文本编辑器。访问地址[vue-quill-editor](https://github.com/surmon-china/vue-quill-editor)
IE10及以下不支持
### mavonEditor ###
基于Vue的markdown编辑器。访问地址[mavonEditor](https://github.com/hinesboy/mavonEditor)
### vue-cropperjs ###
一个封装了 cropperjs 的 Vue 组件,用于裁剪图片。访问地址:[vue-cropperjs](https://github.com/Agontuk/vue-cropperjs)
## 其他注意事项 ##
### 一、如果我不想用到上面的某些组件呢,那我怎么在模板中删除掉不影响到其他功能呢? ###
举个栗子,我不想用 vue-datasource 这个组件,那我需要分四步走。
第一步:删除该组件的路由,在目录 src/router/index.js 中,找到引入改组件的路由,删除下面这段代码。
```JavaScript
{
path: '/vuetable',
component: resolve => require(['../components/page/VueTable.vue'], resolve) // vue-datasource组件
},
```
第二步:删除引入该组件的文件。在目录 src/components/page/ 删除 VueTable.vue 文件。
第三步:删除该页面的入口。在目录 src/components/common/Sidebar.vue 中,找到该入口,删除下面这段代码。
```HTML
<el-menu-item index="vuetable">Vue表格组件</el-menu-item>
```
第四步:卸载该组件。执行以下命令:
npm un vue-datasource -S
完成。
### 二、如何切换主题色呢? ###
第一步:打开 src/main.js 文件,找到引入 element 样式的地方,换成浅绿色主题。
```javascript
import 'element-ui/lib/theme-default/index.css'; // 默认主题
// import '../static/css/theme-green/index.css'; // 浅绿色主题
```
第二步:打开 src/App.vue 文件,找到 style 标签引入样式的地方,切换成浅绿色主题。
```javascript
@import "../static/css/main.css";
@import "../static/css/color-dark.css"; /*深色主题*/
/*@import "../static/css/theme-green/color-green.css"; !*浅绿色主题*!*/
```
第三步:打开 src/components/common/Sidebar.vue 文件,找到 el-menu 标签,把 background-color/text-color/active-text-color 属性去掉即可。
## 项目截图 ##
### 默认皮肤 ###
### 首页
![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms1.png)
### 浅绿色皮肤 ###
### 登录
![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms2.png)
![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms3.png)
## License
[MIT](https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE)

View File

@ -1,187 +1,119 @@
# manage-system #
The web management system solution based on Vue2 and Element-UI。[live demo](http://blog.gdfengshuo.com/example/work/)
# vue-manage-system
<a href="https://github.com/vuejs/vue">
<img src="https://img.shields.io/badge/vue-2.6.10-brightgreen.svg" alt="vue">
</a>
<a href="https://github.com/ElemeFE/element">
<img src="https://img.shields.io/badge/element--ui-2.8.2-brightgreen.svg" alt="element-ui">
</a>
<a href="https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/mashape/apistatus.svg" alt="license">
</a>
<a href="https://github.com/lin-xin/vue-manage-system/releases">
<img src="https://img.shields.io/github/release/lin-xin/vue-manage-system.svg" alt="GitHub release">
</a>
<a href="https://lin-xin.gitee.io/example/work/#/donate">
<img src="https://img.shields.io/badge/%24-donate-ff69b4.svg" alt="donate">
</a>
The web management system solution based on Vue3 and ElementPlus。[live demo](https://lin-xin.gitee.io/example/work/)
Please check the version of vue2 in [tag V4.2.0](https://github.com/lin-xin/vue-manage-system/tree/V4.2.0)
## Donation
![WeChat](http://blog.gdfengshuo.com/images/weixin.jpg)
## Preface ##
The scheme as a set of multi-function background frame templates, suitable for most of the WEB management system development. Convenient development fast simple good components based on Vue2 and Element-UI. Color separation of color style, support manual switch themes, and it is convenient to use a custom theme color.
![WeChat](https://lin-xin.gitee.io/images/weixin.jpg)
## Function ##
- [x] Element-UI
- [x] Login/Logout
- [x] Table
- [x] From
- [x] Chart :bar_chart:
- [x] Editor
- [x] Markdown
- [x] Upload pictures by clipping or dragging
- [x] Support manual switch themes :sparkles:
- [x] List drag sort
## Preface
The scheme as a set of multi-function background frame templates, suitable for most of the WEB management system development. Convenient development fast simple good components based on Vue3 and ElementPlus. Color separation of color style, support manual switch themes, and it is convenient to use a custom theme color.
## Directory structure ##
## Function
|-- build // Webpack configuration file
|-- config // Project package path
|-- src // Source directory
| |-- components // Components
| |-- common // Common component
| |-- bus.js // Event Bus
| |-- Header.vue // Header component
| |-- Home.vue // Home component
| |-- Sidebar.vue // Sidebar component
| |-- page // Router page
| |-- BaseCharts.vue // BaseCharts
| |-- BaseForm.vue // BaseForm
| |-- BaseTable.vue // BaseTable
| |-- Login.vue // Login
| |-- DragList.vue
| |-- Markdown.vue // Markdown
| |-- Premission.vue
| |-- Readme.vue // Readme
| |-- Upload.vue // Upload
| |-- VueEditor.vue // VueEditor
| |-- VueTable.vue // VueTable
| |-- App.vue // Main component
| |-- main.js // Entry file
|-- .babelrc // ES6 syntax compiler configuration
|-- .editorconfig // Code specification
|-- .gitignore // Ignored file
|-- index.html // Entry HTML file
|-- package.json // Dependent configuration file
|-- README.md // Readme
- [x] Element-UI
- [x] Login/Logout
- [x] Dashboard
- [x] Table
- [x] Tabs
- [x] From
- [x] Chart :bar_chart:
- [x] Editor
- [x] Markdown
- [x] Upload pictures by clipping or dragging
- [x] Permission
- [x] Three level menu
- [x] Custom icon
## Installation steps
## Installation steps ##
git clone https://github.com/lin-xin/vue-manage-system.git // Clone templates
cd vue-manage-system // Enter template directory
npm install // Installation dependency
git clone https://github.com/lin-xin/vue-manage-system.git // Clone templates
cd vue-manage-system // Enter template directory
npm install // Installation dependency
## Local development
## Local development ##
npm run dev
// Open server and access http://localhost:8080 in browser
npm run dev
## Constructing production
## Constructing production ##
// Constructing project
npm run build
// Constructing project
npm run build
## Component description and presentation
## Component description and presentation ##
### vue-schart
### vue-schart ###
Vue.js wrapper for sChart.js. Github : [vue-schart](https://github.com/linxin/vue-schart)
Vue.js wrapper for sChart.js. Github : [vue-schart](https://github.com/lin-xin/vue-schart#/)
```JavaScript
```html
<template>
<div>
<schart :canvasId="canvasId"
:type="type"
:width="width"
:height="height"
:data="data"
:options="options"
></schart>
<schart class="wrapper" canvasId="myCanvas" :options="options"></schart>
</div>
</template>
<script>
import Schart from 'vue-schart';
export default {
data: function(){
return {
canvasId: 'myCanvas',
type: 'bar',
width: 500,
height: 400,
data: [
{name: '2014', value: 1342},
{name: '2015', value: 2123},
{name: '2016', value: 1654},
{name: '2017', value: 1795},
],
options: {
title: 'Total sales of stores in recent years'
}
}
<script setup>
import { ref } from 'vue';
import Schart from "vue-schart"; // 导入Schart组件
const options = ref({
type: "bar",
title: {
text: "最近一周各品类销售图",
},
labels: ["周一", "周二", "周三", "周四", "周五"],
datasets: [
{
label: "家电",
data: [234, 278, 270, 190, 230],
},
components: {
Schart
}
}
{
label: "百货",
data: [164, 178, 190, 135, 160],
},
{
label: "食品",
data: [144, 198, 150, 235, 120],
},
],
})
</script>
<style>
.wrapper {
width: 7rem;
height: 5rem;
}
</style>
```
### element-ui ###
A desktop component library based on vue.js2.0 . Github : [element](http://element.eleme.io/#/zh-CN/component/layout)
## Screenshot
### vue-datasource ###
A Vue.js server side component to create dynamic tables. Github : [vue-datasource](https://github.com/coderdiaz/vue-datasource)
### Vue-Quill-Editor ###
Quill editor component for Vue2. Github : [vue-quill-editor](https://github.com/surmon-china/vue-quill-editor)
### mavonEditor ###
A markdown editor based on Vue that supports a variety of personalized features. Github: [mavonEditor](https://github.com/hinesboy/mavonEditor)
### vue-cropperjs ###
A Vue wrapper component for cropperjs. Github: [vue-cropperjs](https://github.com/Agontuk/vue-cropperjs)
## Notice ##
### 一、If I don't want to use some components, how can I delete it? ###
For example, I don't want to use the vue-datasource component, I need to take four steps.
The first step to remove the component of the routing. Enter 'src/router/index.js' and delete the code below.
```JavaScript
{
path: '/vuetable',
component: resolve => require(['../components/page/VueTable.vue'], resolve)
},
```
Second,delete the component files. Enter 'src/components/page/' and delete 'VueTable.vue' file.
The third step is to delete the entry. Enter 'src/components/common/Sidebar.vue' and delete the code below.
```HTML
<el-menu-item index="vuetable">Vue表格组件</el-menu-item>
```
Finally, uninstall this component.
npm un vue-datasource -S
Complete!
### 二、How to switch themes? ###
The first step to enter 'src/main.js' and change into green theme.
```javascript
import 'element-ui/lib/theme-default/index.css'; // default theme
// import '../static/css/theme-green/index.css'; // green theme
```
The second step to enter 'src/App.vue' and change into green theme.
```javascript
@import "../static/css/main.css";
@import "../static/css/color-dark.css"; /*深色主题*/
/*@import "../static/css/theme-green/color-green.css"; !*浅绿色主题*!*/
```
Finally,enter 'src/components/common/Sidebar.vue' and find el-menu Tags,delete 'background-color/text-color/active-text-color'。
## Screenshot ##
### Default theme ###
### Default theme
![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms1.png)
### Green theme ###
### Login
![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms2.png)
![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms3.png)
## License
[MIT](https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE)

5
auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
}

View File

@ -1,41 +0,0 @@
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
const spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})

View File

@ -1,54 +0,0 @@
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
}
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,101 +0,0 @@
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}

View File

@ -1,22 +0,0 @@
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}

View File

@ -1,83 +0,0 @@
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'static': path.resolve(__dirname, '../static'),
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}

View File

@ -1,80 +0,0 @@
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: true,
hot: true,
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// publish the new Port, necessary for e2e tests
process.env.PORT = port
// add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
resolve(devWebpackConfig)
}
})
})

View File

@ -1,29 +0,0 @@
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
vendor: ['vue/dist/vue.common.js','vue-router', 'babel-polyfill','axios']
},
output: {
path: path.join(__dirname, '../static/js'),
filename: '[name].dll.js',
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, '.', '[name]-manifest.json'),
name: '[name]_library'
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
})
]
};

View File

@ -1,145 +0,0 @@
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const env = require('../config/prod.env')
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// keep module.id stable when vender modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig

81
components.d.ts vendored Normal file
View File

@ -0,0 +1,81 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
Countup: typeof import('./src/components/countup.vue')['default']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCalendar: typeof import('element-plus/es')['ElCalendar']
ElCard: typeof import('element-plus/es')['ElCard']
ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElCountdown: typeof import('element-plus/es')['ElCountdown']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRate: typeof import('element-plus/es')['ElRate']
ElResult: typeof import('element-plus/es')['ElResult']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElSpace: typeof import('element-plus/es')['ElSpace']
ElStatistic: typeof import('element-plus/es')['ElStatistic']
ElStep: typeof import('element-plus/es')['ElStep']
ElSteps: typeof import('element-plus/es')['ElSteps']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTour: typeof import('element-plus/es')['ElTour']
ElTourStep: typeof import('element-plus/es')['ElTourStep']
ElTransfer: typeof import('element-plus/es')['ElTransfer']
ElUpload: typeof import('element-plus/es')['ElUpload']
ElWatermark: typeof import('element-plus/es')['ElWatermark']
Header: typeof import('./src/components/header.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Sidebar: typeof import('./src/components/sidebar.vue')['default']
TableCustom: typeof import('./src/components/table-custom.vue')['default']
TableDetail: typeof import('./src/components/table-detail.vue')['default']
TableEdit: typeof import('./src/components/table-edit.vue')['default']
TableSearch: typeof import('./src/components/table-search.vue')['default']
Tabs: typeof import('./src/components/tabs.vue')['default']
}
}

View File

@ -1,7 +0,0 @@
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

View File

@ -1,85 +0,0 @@
'use strict'
// Template version: 1.2.7
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/api':{
target:'http://jsonplaceholder.typicode.com',
changeOrigin:true,
pathRewrite:{
'/api':''
}
},
'/ms':{
target: 'https://www.easy-mock.com/mock/592501a391470c0ac1fab128',
changeOrigin: true
}
},
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
cssSourceMap: false,
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: './',
/**
* Source Maps
*/
productionSourceMap: false,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
}
}

View File

@ -1,4 +0,0 @@
'use strict'
module.exports = {
NODE_ENV: '"production"'
}

View File

@ -1,23 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>vue-manage-system | 基于Vue 的后台管理系统</title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
<meta name="keywords" content="vue.js, wms, vue2, 后台模板, 管理系统, element" />
<meta name="description" content="基于Vue2 + Element UI 的后台管理系统解决方案" />
</head>
<body>
<div id="app"></div>
<!--<script src="./static/js/vendor.dll.js"></script>-->
<!-- <script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?1c343a080809502ac823823ae4c9ffe3";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script> -->
</body>
</html>
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>vue-manage-system后台管理系统</title>
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_830376_92o68tc95je.css">
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,71 +1,44 @@
{
"name": "vue-manage-system",
"version": "3.0.0",
"description": "基于Vue.js 2.x系列 + element-ui 内容管理系统解决方案",
"author": "lin-xin <2981207131@qq.com>",
"private": true,
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"build": "node build/build.js",
"build:dll": "webpack --config build/webpack.dll.conf.js"
},
"dependencies": {
"axios": "^0.15.3",
"babel-polyfill": "^6.23.0",
"element-ui": "2.3.3",
"mavon-editor": "^2.5.2",
"vue": "^2.5.16",
"vue-cropperjs": "^2.2.0",
"vue-datasource": "1.0.12",
"vue-quill-editor": "3.0.6",
"vue-router": "^3.0.1",
"vue-schart": "^0.1.4",
"vuedraggable": "^2.16.0"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.1.1",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.22.0",
"chalk": "^2.0.1",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 5 versions",
"not ie <= 8"
]
}
{
"name": "vue-manage-system",
"version": "5.5.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "*",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.6.3",
"countup.js": "^2.8.0",
"echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.6.3",
"md-editor-v3": "^2.11.2",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"vue": "^3.4.5",
"vue-cropper": "1.1.1",
"vue-echarts": "^6.6.9",
"vue-router": "^4.2.5",
"vue-schart": "^2.0.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.0.0",
"@vue/compiler-sfc": "^3.1.2",
"typescript": "^4.6.4",
"unplugin-auto-import": "^0.11.2",
"unplugin-vue-components": "^0.22.4",
"vite": "^3.0.0",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-tsc": "^0.38.4"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

46
public/mock/role.json Normal file
View File

@ -0,0 +1,46 @@
{
"list": [
{
"id": 1,
"name": "管理员",
"key": "admin",
"status": true,
"permiss": [
"0",
"1",
"11",
"12",
"13",
"2",
"21",
"22",
"23",
"24",
"3",
"31",
"32",
"33",
"331",
"332",
"4",
"41",
"42",
"5"
]
},
{
"id": 2,
"name": "普通用户",
"key": "user",
"status": true,
"permiss": [
"0",
"1",
"11",
"12",
"13"
]
}
],
"pageTotal": 2
}

41
public/mock/table.json Normal file
View File

@ -0,0 +1,41 @@
{
"list": [
{
"id": 1,
"name": "张三",
"money": 123,
"address": "广东省东莞市长安镇",
"state": true,
"date": "2019-11-1",
"thumb": "https://lin-xin.gitee.io/images/post/wms.png"
},
{
"id": 2,
"name": "李四",
"money": 456,
"address": "广东省广州市白云区",
"state": true,
"date": "2019-10-11",
"thumb": "https://lin-xin.gitee.io/images/post/node3.png"
},
{
"id": 3,
"name": "王五",
"money": 789,
"address": "湖南省长沙市",
"state": false,
"date": "2019-11-11",
"thumb": "https://lin-xin.gitee.io/images/post/parcel.png"
},
{
"id": 4,
"name": "赵六",
"money": 1011,
"address": "福建省厦门市鼓浪屿",
"state": true,
"date": "2019-10-20",
"thumb": "https://lin-xin.gitee.io/images/post/notice.png"
}
],
"pageTotal": 4
}

23
public/mock/user.json Normal file
View File

@ -0,0 +1,23 @@
{
"list": [
{
"id": 1,
"name": "张三",
"password": "123",
"email": "123@qq.com",
"phone": "12345678944",
"date": "2024-01-01",
"role": "管理员"
},
{
"id": 2,
"name": "李四",
"password": "123",
"email": "1234@qq.com",
"phone": "12345678945",
"date": "2024-01-01",
"role": "普通用户"
}
],
"pageTotal": 2
}

BIN
public/template.xlsx Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
screenshots/wms3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

View File

@ -1,10 +1,17 @@
<template>
<div id="app">
<router-view></router-view>
</div>
<el-config-provider :locale="zhCn">
<router-view />
</el-config-provider>
</template>
<script setup lang="ts">
import { ElConfigProvider } from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import { useThemeStore } from './store/theme';
const theme = useThemeStore();
theme.initTheme();
</script>
<style>
@import "../static/css/main.css";
@import "../static/css/color-dark.css"; /*深色主题*/
/*@import "../static/css/theme-green/color-green.css"; 浅绿色主题*/
</style>
@import './assets/css/main.css';
</style>

22
src/api/index.ts Normal file
View File

@ -0,0 +1,22 @@
import request from '../utils/request';
export const fetchData = () => {
return request({
url: './mock/table.json',
method: 'get'
});
};
export const fetchUserData = () => {
return request({
url: './mock/user.json',
method: 'get'
});
};
export const fetchRoleData = () => {
return request({
url: './mock/role.json',
method: 'get'
});
};

4
src/assets/css/icon.css Normal file
View File

@ -0,0 +1,4 @@
[class*=" el-icon-lx"],
[class^=el-icon-lx] {
font-family: lx-iconfont !important;
}

87
src/assets/css/main.css Normal file
View File

@ -0,0 +1,87 @@
* {
margin: 0;
padding: 0;
outline: 0 !important;
}
body {
font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, 'microsoft yahei', arial, STHeiTi, sans-serif;
}
a {
text-decoration: none;
}
i {
font-style: normal;
}
.container {
padding: 30px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
}
.el-table th {
background-color: #f5f7fa !important;
}
.plugins-tips {
padding: 20px 10px;
margin-bottom: 20px;
background: #eef1f6;
}
.plugins-tips a {
color: var(--el-color-primary);
}
.el-button + .el-tooltip {
margin-left: 10px;
}
.mgb20 {
margin-bottom: 20px;
}
.mgb10 {
margin-bottom: 10px;
}
.mr10 {
margin-right: 10px;
}
.move-enter-active,
.move-leave-active {
transition: opacity 0.1s ease;
}
.move-enter-from,
.move-leave-to {
opacity: 0;
}
.el-time-panel__content::after,
.el-time-panel__content::before {
margin-top: -7px;
}
.el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) {
padding-bottom: 0;
}
[hidden] {
display: none !important;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
:root {
--header-bg-color: #242f42;
--header-text-color: #fff;
--active-color: var(--el-color-primary);
}

BIN
src/assets/img/img.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
src/assets/img/login-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

1
src/assets/img/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144.08 128.61"><title>&#x8D44;&#x6E90; 82</title><path d="M72.23 128.61c-7.1-.23-11.51-3.72-14.76-9.36C48 102.87 38.43 86.59 29.1 70.16a36 36 0 0 1-4.47-11.35A14.61 14.61 0 0 1 34 42.51c7.49-2.71 15.71-.21 19.67 6.43 7.52 12.56 14.77 25.27 22.12 37.92 3 5.17 5.89 10.43 9 15.51 5 8 3.45 18-4.22 23.31-2.3 1.62-5.52 1.99-8.34 2.93z" fill="#2ef2e9"/><path d="M72.66.33c6-.57 10.39 2.6 13.51 8C95.61 24.69 105 41.1 114.52 57.4c3.9 6.65-.28 17.13-6.39 20.44-8.93 4.83-17.88 1.28-21.86-5.62C76.82 55.86 67.14 39.62 58.11 23 52.06 12 59.61.24 72.66.33z" fill="#fa6663"/><path d="M144.08 15.83c-.58 8.62-6.73 15.57-15.51 15.66-9.31.09-16.87-7-16.95-15.62S119 0 127.87 0c9.13.09 16.22 7 16.21 15.83z" fill="#fbb355"/><path d="M16.24 31.5C7 31.33-.19 24.42 0 15.8.19 7.5 7.19-.06 14.64 0c10.53.08 18.27 6.73 17.61 15.9-.64 8.96-6.25 15.28-16.01 15.6z" fill="#8a56c2"/></svg>

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,97 +0,0 @@
<template>
<div class="header">
<div class="collapse-btn" @click="collapseChage">
<i class="el-icon-menu"></i>
</div>
<div class="logo">后台管理系统</div>
<div class="user-info">
<el-dropdown trigger="click" @command="handleCommand">
<span class="el-dropdown-link">
<img class="user-logo" src="../../../static/img/img.jpg">
{{username}}
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="loginout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
import bus from '../common/bus';
export default {
data() {
return {
collapse: false,
name: 'linxin'
}
},
computed:{
username(){
let username = localStorage.getItem('ms_username');
return username ? username : this.name;
}
},
methods:{
handleCommand(command) {
if(command == 'loginout'){
localStorage.removeItem('ms_username')
this.$router.push('/login');
}
},
collapseChage(){
this.collapse = !this.collapse;
bus.$emit('collapse', this.collapse);
}
}
}
</script>
<style scoped>
.header {
position: relative;
box-sizing: border-box;
width: 100%;
height: 70px;
font-size: 22px;
line-height: 70px;
color: #fff;
}
.collapse-btn{
float: left;
padding: 0 21px;
cursor: pointer;
}
.collapse-btn:hover{
background: rgb(40,52,70);
}
.header .logo{
float: left;
width:250px;
/* text-align: center; */
}
.user-info {
float: right;
padding-right: 50px;
font-size: 16px;
color: #fff;
}
.user-info .el-dropdown-link{
position: relative;
display: inline-block;
padding-left: 50px;
color: #fff;
cursor: pointer;
vertical-align: middle;
}
.user-info .user-logo{
position: absolute;
left:0;
top:15px;
width:40px;
height:40px;
border-radius: 50%;
}
.el-dropdown-menu__item{
text-align: center;
}
</style>

View File

@ -1,30 +0,0 @@
<template>
<div class="wrapper">
<v-head></v-head>
<v-sidebar></v-sidebar>
<div class="content" :class="{'content-collapse':collapse}">
<transition name="move" mode="out-in"><router-view></router-view></transition>
</div>
</div>
</template>
<script>
import vHead from './Header.vue';
import vSidebar from './Sidebar.vue';
import bus from '../common/bus';
export default {
data(){
return {
collapse: false
}
},
components:{
vHead, vSidebar
},
created(){
bus.$on('collapse', msg => {
this.collapse = msg;
})
}
}
</script>

View File

@ -1,122 +0,0 @@
<template>
<div class="sidebar">
<el-menu class="sidebar-el-menu" :default-active="onRoutes" :collapse="collapse" background-color="#324157"
text-color="#bfcbd9" active-text-color="#20a0ff" unique-opened router>
<template v-for="item in items">
<template v-if="item.subs">
<el-submenu :index="item.index" :key="item.index">
<template slot="title">
<i :class="item.icon"></i><span slot="title">{{ item.title }}</span>
</template>
<el-menu-item v-for="(subItem,i) in item.subs" :key="i" :index="subItem.index">
{{ subItem.title }}
</el-menu-item>
</el-submenu>
</template>
<template v-else>
<el-menu-item :index="item.index" :key="item.index">
<i :class="item.icon"></i><span slot="title">{{ item.title }}</span>
</el-menu-item>
</template>
</template>
</el-menu>
</div>
</template>
<script>
import bus from '../common/bus';
export default {
data() {
return {
collapse: false,
items: [
{
icon: 'el-icon-setting',
index: 'readme',
title: '自述文件'
},
{
icon: 'el-icon-tickets',
index: '2',
title: '常用表格',
subs: [
{
index: 'table',
title: '基础表格'
},
{
index: 'datasource',
title: 'datasource表格'
}
]
},
{
icon: 'el-icon-date',
index: '3',
title: '表单相关',
subs: [
{
index: 'form',
title: '基本表单'
},
{
index: 'editor',
title: '富文本编辑器'
},
{
index: 'markdown',
title: 'markdown编辑器'
},
{
index: 'upload',
title: '文件上传'
}
]
},
{
icon: 'el-icon-star-on',
index: 'charts',
title: 'schart图表'
},
{
icon: 'el-icon-rank',
index: 'drag',
title: '拖拽列表'
},
{
icon: 'el-icon-warning',
index: 'permission',
title: '权限测试'
}
]
}
},
computed:{
onRoutes(){
return this.$route.path.replace('/','');
}
},
created(){
// Event Bus
bus.$on('collapse', msg => {
this.collapse = msg;
})
}
}
</script>
<style scoped>
.sidebar{
display: block;
position: absolute;
left: 0;
top: 70px;
bottom:0;
}
.sidebar-el-menu:not(.el-menu--collapse){
width: 250px;
}
.sidebar > ul {
height:100%;
}
</style>

View File

@ -1,6 +0,0 @@
import Vue from 'vue';
// 使用 Event Bus
const bus = new Vue();
export default bus;

View File

@ -0,0 +1,39 @@
<template>
<span ref="countRef"></span>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { CountUp } from 'countup.js';
const props = defineProps({
end: {
type: Number,
required: true,
},
options: {
type: Object,
default: () => ({}),
required: false,
},
});
const countRef = ref<any>(null);
let countUp: any;
onMounted(() => {
countUp = new CountUp(countRef.value, props.end, props.options);
if (countUp.error) {
console.error(countUp.error);
return;
}
countUp.start();
});
watch(() => props.end, (newVal) => {
if (countUp) {
countUp.update(newVal);
}
});
</script>

204
src/components/header.vue Normal file
View File

@ -0,0 +1,204 @@
<template>
<div class="header">
<!-- 折叠按钮 -->
<div class="header-left">
<img class="logo" src="../assets/img/logo.svg" alt="" />
<div class="web-title">后台管理系统</div>
<div class="collapse-btn" @click="collapseChage">
<el-icon v-if="sidebar.collapse">
<Expand />
</el-icon>
<el-icon v-else>
<Fold />
</el-icon>
</div>
</div>
<div class="header-right">
<div class="header-user-con">
<div class="btn-icon" @click="router.push('/theme')">
<el-tooltip effect="dark" content="设置主题" placement="bottom">
<i class="el-icon-lx-skin"></i>
</el-tooltip>
</div>
<div class="btn-icon" @click="router.push('/ucenter')">
<el-tooltip
effect="dark"
:content="message ? `有${message}条未读消息` : `消息中心`"
placement="bottom"
>
<i class="el-icon-lx-notice"></i>
</el-tooltip>
<span class="btn-bell-badge" v-if="message"></span>
</div>
<div class="btn-icon" @click="setFullScreen">
<el-tooltip effect="dark" content="全屏" placement="bottom">
<i class="el-icon-lx-full"></i>
</el-tooltip>
</div>
<!-- 用户头像 -->
<el-avatar class="user-avator" :size="30" :src="imgurl" />
<!-- 用户名下拉菜单 -->
<el-dropdown class="user-name" trigger="click" @command="handleCommand">
<span class="el-dropdown-link">
{{ username }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<a href="https://github.com/lin-xin/vue-manage-system" target="_blank">
<el-dropdown-item>项目仓库</el-dropdown-item>
</a>
<a href="https://lin-xin.gitee.io/example/vuems-doc/" target="_blank">
<el-dropdown-item>官方文档</el-dropdown-item>
</a>
<el-dropdown-item command="user">个人中心</el-dropdown-item>
<el-dropdown-item divided command="loginout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { useSidebarStore } from '../store/sidebar';
import { useRouter } from 'vue-router';
import imgurl from '../assets/img/img.jpg';
const username: string | null = localStorage.getItem('vuems_name');
const message: number = 2;
const sidebar = useSidebarStore();
//
const collapseChage = () => {
sidebar.handleCollapse();
};
onMounted(() => {
if (document.body.clientWidth < 1500) {
collapseChage();
}
});
//
const router = useRouter();
const handleCommand = (command: string) => {
if (command == 'loginout') {
localStorage.removeItem('vuems_name');
router.push('/login');
} else if (command == 'user') {
router.push('/ucenter');
}
};
const setFullScreen = () => {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.body.requestFullscreen.call(document.body);
}
};
</script>
<style scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
width: 100%;
height: 70px;
color: var(--header-text-color);
background-color: var(--header-bg-color);
border-bottom: 1px solid #ddd;
}
.header-left {
display: flex;
align-items: center;
padding-left: 20px;
height: 100%;
}
.logo {
width: 35px;
}
.web-title {
margin: 0 40px 0 10px;
font-size: 22px;
}
.collapse-btn {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 0 10px;
cursor: pointer;
opacity: 0.8;
font-size: 22px;
}
.collapse-btn:hover {
opacity: 1;
}
.header-right {
float: right;
padding-right: 50px;
}
.header-user-con {
display: flex;
height: 70px;
align-items: center;
}
.btn-fullscreen {
transform: rotate(45deg);
margin-right: 5px;
font-size: 24px;
}
.btn-icon {
position: relative;
width: 30px;
height: 30px;
text-align: center;
cursor: pointer;
display: flex;
align-items: center;
color: var(--header-text-color);
margin: 0 5px;
font-size: 20px;
}
.btn-bell-badge {
position: absolute;
right: 4px;
top: 0px;
width: 8px;
height: 8px;
border-radius: 4px;
background: #f56c6c;
color: var(--header-text-color);
}
.user-avator {
margin: 0 10px 0 20px;
}
.el-dropdown-link {
color: var(--header-text-color);
cursor: pointer;
display: flex;
align-items: center;
}
.el-dropdown-menu__item {
text-align: center;
}
</style>

221
src/components/menu.ts Normal file
View File

@ -0,0 +1,221 @@
import { Menus } from '@/types/menu';
export const menuData: Menus[] = [
{
id: '0',
title: '系统首页',
index: '/dashboard',
icon: 'Odometer',
},
{
id: '1',
title: '系统管理',
index: '1',
icon: 'HomeFilled',
children: [
{
id: '11',
pid: '1',
index: '/system-user',
title: '用户管理',
},
{
id: '12',
pid: '1',
index: '/system-role',
title: '角色管理',
},
{
id: '13',
pid: '1',
index: '/system-menu',
title: '菜单管理',
},
],
},
{
id: '2',
title: '组件',
index: '2-1',
icon: 'Calendar',
children: [
{
id: '21',
pid: '3',
index: '/form',
title: '表单',
},
{
id: '22',
pid: '3',
index: '/upload',
title: '上传',
},
{
id: '23',
pid: '2',
index: '/carousel',
title: '走马灯',
},
{
id: '24',
pid: '2',
index: '/calendar',
title: '日历',
},
{
id: '25',
pid: '2',
index: '/watermark',
title: '水印',
},
{
id: '26',
pid: '2',
index: '/tour',
title: '分布引导',
},
{
id: '27',
pid: '2',
index: '/steps',
title: '步骤条',
},
{
id: '28',
pid: '2',
index: '/statistic',
title: '统计',
},
{
id: '29',
pid: '3',
index: '29',
title: '三级菜单',
children: [
{
id: '291',
pid: '29',
index: '/editor',
title: '富文本编辑器',
},
{
id: '292',
pid: '29',
index: '/markdown',
title: 'markdown编辑器',
},
],
},
],
},
{
id: '3',
title: '表格',
index: '3',
icon: 'Calendar',
children: [
{
id: '31',
pid: '3',
index: '/table',
title: '基础表格',
},
{
id: '32',
pid: '3',
index: '/table-editor',
title: '可编辑表格',
},
{
id: '33',
pid: '3',
index: '/import',
title: '导入Excel',
},
{
id: '34',
pid: '3',
index: '/export',
title: '导出Excel',
},
],
},
{
id: '4',
icon: 'PieChart',
index: '4',
title: '图表',
children: [
{
id: '41',
pid: '4',
index: '/schart',
title: 'schart图表',
},
{
id: '42',
pid: '4',
index: '/echarts',
title: 'echarts图表',
},
],
},
{
id: '5',
icon: 'Guide',
index: '/icon',
title: '图标',
permiss: '5',
},
{
id: '7',
icon: 'Brush',
index: '/theme',
title: '主题',
},
{
id: '6',
icon: 'DocumentAdd',
index: '6',
title: '附加页面',
children: [
{
id: '61',
pid: '6',
index: '/ucenter',
title: '个人中心',
},
{
id: '62',
pid: '6',
index: '/login',
title: '登录',
},
{
id: '63',
pid: '6',
index: '/register',
title: '注册',
},
{
id: '64',
pid: '6',
index: '/reset-pwd',
title: '重设密码',
},
{
id: '65',
pid: '6',
index: '/403',
title: '403',
},
{
id: '66',
pid: '6',
index: '/404',
title: '404',
},
],
},
];

View File

@ -1,88 +0,0 @@
<template>
<div>
<div class="crumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item><i class="el-icon-date"></i> 图表</el-breadcrumb-item>
<el-breadcrumb-item>基础图表</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="container">
<div class="plugins-tips">
vue-schartvue.js封装sChart.js的图表组件
访问地址<a href="https://github.com/lin-xin/vue-schart" target="_blank">vue-schart</a>
</div>
<div class="schart">
<div class="content-title">柱状图</div>
<schart canvasId="bar" width="500" height="400" :data="data1" type="bar" :options="options1"></schart>
</div>
<div class="schart">
<div class="content-title">折线图</div>
<schart canvasId="line" width="500" height="400" :data="data1" type="line" :options="options1"></schart>
</div>
<div class="schart">
<div class="content-title">饼状图</div>
<schart canvasId="pie" width="500" height="400" :data="data2" type="pie" :options="options2"></schart>
</div>
<div class="schart">
<div class="content-title">环形图</div>
<schart canvasId="ring" width="500" height="400" :data="data2" type="ring" :options="options2"></schart>
</div>
</div>
</div>
</template>
<script>
import Schart from 'vue-schart';
export default {
components: {
Schart
},
data: () => ({
data1:[
{name:'2012',value:1141},
{name:'2013',value:1499},
{name:'2014',value:2260},
{name:'2015',value:1170},
{name:'2016',value:970},
{name:'2017',value:1450}
],
data2 : [
{name:'短袖',value:1200},
{name:'休闲裤',value:1222},
{name:'连衣裙',value:1283},
{name:'外套',value:1314},
{name:'羽绒服',value:2314}
],
options1: {
title: '某商店近年营业总额',
bgColor: '#009688',
titleColor: '#ffffff',
fillColor: '#e0f2f1',
axisColor: '#ffffff',
contentColor: '#999'
},
options2: {
title: '某商店各商品年度销量',
bgColor: '#607d8b',
titleColor: '#ffffff',
legendColor: '#ffffff'
}
})
}
</script>
<style scoped>
.schart{
width: 600px;
display: inline-block;
}
.content-title{
clear: both;
font-weight: 400;
line-height: 50px;
margin: 10px 0;
font-size: 22px;
color: #1f2f3d;
}
</style>

View File

@ -1,140 +0,0 @@
<template>
<div>
<div class="crumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item><i class="el-icon-date"></i> 表单</el-breadcrumb-item>
<el-breadcrumb-item>基本表单</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="container">
<div class="form-box">
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="表单名称">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="选择器">
<el-select v-model="form.region" placeholder="请选择">
<el-option key="bbk" label="步步高" value="bbk"></el-option>
<el-option key="xtc" label="小天才" value="xtc"></el-option>
<el-option key="imoo" label="imoo" value="imoo"></el-option>
</el-select>
</el-form-item>
<el-form-item label="日期时间">
<el-col :span="11">
<el-date-picker type="date" placeholder="选择日期" v-model="form.date1" style="width: 100%;"></el-date-picker>
</el-col>
<el-col class="line" :span="2">-</el-col>
<el-col :span="11">
<el-time-picker placeholder="选择时间" v-model="form.date2" style="width: 100%;"></el-time-picker>
</el-col>
</el-form-item>
<el-form-item label="城市级联">
<el-cascader :options="options" v-model="form.options"></el-cascader>
</el-form-item>
<el-form-item label="选择开关">
<el-switch v-model="form.delivery"></el-switch>
</el-form-item>
<el-form-item label="多选框">
<el-checkbox-group v-model="form.type">
<el-checkbox label="步步高" name="type"></el-checkbox>
<el-checkbox label="小天才" name="type"></el-checkbox>
<el-checkbox label="imoo" name="type"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="单选框">
<el-radio-group v-model="form.resource">
<el-radio label="步步高"></el-radio>
<el-radio label="小天才"></el-radio>
<el-radio label="imoo"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="文本框">
<el-input type="textarea" rows="5" v-model="form.desc"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit"></el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
<script>
export default {
data: function(){
return {
options:[
{
value: 'guangdong',
label: '广东省',
children: [
{
value: 'guangzhou',
label: '广州市',
children: [
{
value: 'tianhe',
label: '天河区'
},
{
value: 'haizhu',
label: '海珠区'
}
]
},
{
value: 'dongguan',
label: '东莞市',
children: [
{
value: 'changan',
label: '长安镇'
},
{
value: 'humen',
label: '虎门镇'
}
]
}
]
},
{
value: 'hunan',
label: '湖南省',
children: [
{
value: 'changsha',
label: '长沙市',
children: [
{
value: 'yuelu',
label: '岳麓区'
}
]
}
]
}
],
form: {
name: '',
region: '',
date1: '',
date2: '',
delivery: true,
type: ['步步高'],
resource: '小天才',
desc: '',
options: []
}
}
},
methods: {
onSubmit() {
this.$message.success('提交成功!');
}
}
}
</script>

View File

@ -1,144 +0,0 @@
<template>
<div class="table">
<div class="crumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item><i class="el-icon-tickets"></i> 表格</el-breadcrumb-item>
<el-breadcrumb-item>基础表格</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="container">
<div class="handle-box">
<el-button type="primary" icon="delete" class="handle-del mr10" @click="delAll"></el-button>
<el-select v-model="select_cate" placeholder="筛选省份" class="handle-select mr10">
<el-option key="1" label="广东省" value="广东省"></el-option>
<el-option key="2" label="湖南省" value="湖南省"></el-option>
</el-select>
<el-input v-model="select_word" placeholder="筛选关键词" class="handle-input mr10"></el-input>
<el-button type="primary" icon="search" @click="search"></el-button>
</div>
<el-table :data="data" border style="width: 100%" ref="multipleTable" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="date" label="日期" sortable width="150">
</el-table-column>
<el-table-column prop="name" label="姓名" width="120">
</el-table-column>
<el-table-column prop="address" label="地址" :formatter="formatter">
</el-table-column>
<el-table-column label="操作" width="180">
<template slot-scope="scope">
<el-button size="small"
@click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button size="small" type="danger"
@click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
@current-change ="handleCurrentChange"
layout="prev, pager, next"
:total="1000">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
url: './static/vuetable.json',
tableData: [],
cur_page: 1,
multipleSelection: [],
select_cate: '',
select_word: '',
del_list: [],
is_search: false
}
},
created(){
this.getData();
},
computed: {
data(){
return this.tableData.filter((d) => {
let is_del = false;
for (let i = 0; i < this.del_list.length; i++) {
if(d.name === this.del_list[i].name){
is_del = true;
break;
}
}
if(!is_del){
if(d.address.indexOf(this.select_cate) > -1 &&
(d.name.indexOf(this.select_word) > -1 ||
d.address.indexOf(this.select_word) > -1)
){
return d;
}
}
})
}
},
methods: {
//
handleCurrentChange(val){
this.cur_page = val;
this.getData();
},
// easy-mock
getData(){
// 使 easy-mock 使 json
if(process.env.NODE_ENV === 'development'){
this.url = '/ms/table/list';
};
this.$axios.post(this.url, {page:this.cur_page}).then((res) => {
this.tableData = res.data.list;
})
},
search(){
this.is_search = true;
},
formatter(row, column) {
return row.address;
},
filterTag(value, row) {
return row.tag === value;
},
handleEdit(index, row) {
this.$message('编辑第'+(index+1)+'行');
},
handleDelete(index, row) {
this.$message.error('删除第'+(index+1)+'行');
},
delAll(){
const length = this.multipleSelection.length;
let str = '';
this.del_list = this.del_list.concat(this.multipleSelection);
for (let i = 0; i < length; i++) {
str += this.multipleSelection[i].name + ' ';
}
this.$message.error('删除了'+str);
this.multipleSelection = [];
},
handleSelectionChange(val) {
this.multipleSelection = val;
}
}
}
</script>
<style scoped>
.handle-box{
margin-bottom: 20px;
}
.handle-select{
width: 120px;
}
.handle-input{
width: 300px;
display: inline-block;
}
</style>

View File

@ -1,163 +0,0 @@
<template>
<section class="main">
<div class="crumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item><i class="el-icon-rank"></i> 拖拽排序</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="container">
<div class="plugins-tips">
Vue.Draggable基于 Sortable.js Vue 拖拽组件
访问地址<a href="https://github.com/SortableJS/Vue.Draggable" target="_blank">Vue.Draggable</a>
</div>
<div class="drag-box">
<div class="drag-box-item">
<div class="item-title">todo</div>
<draggable v-model="todo" @remove="removeHandle" :options="dragOptions">
<transition-group tag="div" id="todo" class="item-ul">
<div v-for="(item,index) in todo" class="drag-list" :key="index">
{{item.content}}
</div>
</transition-group>
</draggable>
</div>
<div class="drag-box-item">
<div class="item-title">doing</div>
<draggable v-model="doing" @remove="removeHandle" :options="dragOptions">
<transition-group tag="div" id="doing" class="item-ul">
<div v-for="(item,index) in doing" class="drag-list" :key="index">
{{item.content}}
</div>
</transition-group>
</draggable>
</div>
<div class="drag-box-item">
<div class="item-title">done</div>
<draggable v-model="done" @remove="removeHandle" :options="dragOptions">
<transition-group tag="div" id="done" class="item-ul">
<div v-for="(item,index) in done" class="drag-list" :key="index">
{{item.content}}
</div>
</transition-group>
</draggable>
</div>
</div>
</div>
</section>
</template>
<script>
import draggable from 'vuedraggable'
export default {
data() {
return {
dragOptions:{
animation: 120,
scroll: true,
group: 'sortlist',
ghostClass: 'ghost-style'
},
todo: [
{
content: '开发图表组件'
},
{
content: '开发拖拽组件'
},
{
content: '开发权限测试组件'
}
],
doing: [
{
content: '开发登录注册页面'
},
{
content: '开发头部组件'
},
{
content: '开发表格相关组件'
},
{
content: '开发表单相关组件'
}
],
done:[
{
content: '初始化项目,生成工程目录,完成相关配置'
},
{
content: '开发项目整体框架'
}
]
}
},
components:{
draggable
},
methods: {
removeHandle(event){
console.log(event);
this.$message.success(`${event.from.id} 移动到 ${event.to.id} `);
}
}
}
</script>
<style scoped>
.drag-box{
display: flex;
user-select: none;
}
.drag-box-item {
flex: 1;
max-width: 330px;
min-width: 300px;
background-color: #eff1f5;
margin-right: 16px;
border-radius: 6px;
border: 1px #e1e4e8 solid;
}
.item-title{
padding: 8px 8px 8px 12px;
font-size: 14px;
line-height: 1.5;
color: #24292e;
font-weight: 600;
}
.item-ul{
padding: 0 8px 8px;
height: 500px;
overflow-y: scroll;
}
.item-ul::-webkit-scrollbar{
width: 0;
}
.drag-list {
border: 1px #e1e4e8 solid;
padding: 10px;
margin: 5px 0 10px;
list-style: none;
background-color: #fff;
border-radius: 6px;
cursor: pointer;
-webkit-transition: border .3s ease-in;
transition: border .3s ease-in;
}
.drag-list:hover {
border: 1px solid #20a0ff;
}
.drag-title {
font-weight: 400;
line-height: 25px;
margin: 10px 0;
font-size: 22px;
color: #1f2f3d;
}
.ghost-style{
display: block;
color: transparent;
border-style: dashed
}
</style>

View File

@ -1,89 +0,0 @@
<template>
<div class="login-wrap">
<div class="ms-title">后台管理系统</div>
<div class="ms-login">
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="0px" class="demo-ruleForm">
<el-form-item prop="username">
<el-input v-model="ruleForm.username" placeholder="username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" placeholder="password" v-model="ruleForm.password" @keyup.enter.native="submitForm('ruleForm')"></el-input>
</el-form-item>
<div class="login-btn">
<el-button type="primary" @click="submitForm('ruleForm')"></el-button>
</div>
<p style="font-size:12px;line-height:30px;color:#999;">Tips : 用户名和密码随便填</p>
</el-form>
</div>
</div>
</template>
<script>
export default {
data: function(){
return {
ruleForm: {
username: 'admin',
password: '123123'
},
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
]
}
}
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
localStorage.setItem('ms_username',this.ruleForm.username);
this.$router.push('/readme');
} else {
console.log('error submit!!');
return false;
}
});
}
}
}
</script>
<style scoped>
.login-wrap{
position: relative;
width:100%;
height:100%;
}
.ms-title{
position: absolute;
top:50%;
width:100%;
margin-top: -230px;
text-align: center;
font-size:30px;
color: #fff;
}
.ms-login{
position: absolute;
left:50%;
top:50%;
width:300px;
height:160px;
margin:-150px 0 0 -190px;
padding:40px;
border-radius: 5px;
background: #fff;
}
.login-btn{
text-align: center;
}
.login-btn button{
width:100%;
height:36px;
}
</style>

View File

@ -1,68 +0,0 @@
<template>
<div>
<div class="crumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item><i class="el-icon-date"></i> 表单</el-breadcrumb-item>
<el-breadcrumb-item>markdown</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="container">
<div class="plugins-tips">
mavonEditor基于Vue的markdown编辑器
访问地址<a href="https://github.com/hinesboy/mavonEditor" target="_blank">mavonEditor</a>
</div>
<mavon-editor v-model="content" ref="md" @imgAdd="$imgAdd" @change="change" style="min-height: 600px"/>
<el-button class="editor-btn" type="primary" @click="submit"></el-button>
</div>
</div>
</template>
<script>
import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
// mavonflexIE
import 'static/css/mavon-flex.css'
export default {
data: function(){
return {
content:'',
html:'',
configs: {
}
}
},
components: {
mavonEditor
},
methods: {
// md
$imgAdd(pos, $file){
var formdata = new FormData();
formdata.append('file', $file);
//
this.$axios({
url: '/common/upload',
method: 'post',
data: formdata,
headers: { 'Content-Type': 'multipart/form-data' },
}).then((url) => {
this.$refs.md.$img2Url(pos, url);
})
},
change(value, render){
// render markdown
this.html = render;
},
submit(){
console.log(this.content);
console.log(this.html);
this.$message.success('提交成功!');
}
}
}
</script>
<style scoped>
.editor-btn{
margin-top: 20px;
}
</style>

View File

@ -1,38 +0,0 @@
<template>
<div>
<div class="crumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item><i class="el-icon-warning"></i> 权限测试</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="container">
<h1>管理员权限页面</h1>
<p>只有用 admin 账号登录的才拥有管理员权限才能进到这个页面其他账号想进来都会跳到登录页面重新用管理员账号登录才有权限</p>
<p>想尝试一下<router-link to="/login" class="logout">退出登录</router-link>便</p>
</div>
</div>
</template>
<script>
export default {
data: function(){
return {}
}
}
</script>
<style scoped>
h1{
text-align: center;
margin: 30px 0;
}
p{
line-height: 30px;
margin-bottom: 10px;
text-indent: 2em;
}
.logout{
color: #409EFF;
}
</style>

View File

@ -1,88 +0,0 @@
<template>
<div>
<div class="crumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item><i class="el-icon-setting"></i> 自述</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="ms-doc">
<h3>README.md</h3>
<article>
<h1>manage-system</h1>
<p>基于Vue.js 2.x系列 + Element UI 的后台管理系统解决方案</p>
<h2>前言</h2>
<p>之前在公司用了Vue + Element组件库做了个后台管理系统基本很多组件可以直接引用组件库的但是也有一些需求无法满足像图片裁剪上传富文本编辑器图表等这些在后台管理系统中很常见的功能就需要引用其他的组件才能完成从寻找组件到使用组件的过程中遇到了很多问题也积累了宝贵的经验所以我就把开发这个后台管理系统的经验总结成这个后台管理系统解决方案</p>
<p>该方案作为一套多功能的后台框架模板适用于绝大部分的后台管理系统Web Management System开发基于vue.js,使用vue-cli脚手架快速生成项目目录引用Element UI组件库方便开发快速简洁好看的组件分离颜色样式支持手动切换主题色而且很方便使用自定义主题色</p>
<h2>功能</h2>
<el-checkbox disabled checked>Element UI</el-checkbox>
<br>
<el-checkbox disabled checked>登录/注销</el-checkbox>
<br>
<el-checkbox disabled checked>表格</el-checkbox>
<br>
<el-checkbox disabled checked>表单</el-checkbox>
<br>
<el-checkbox disabled checked>图表</el-checkbox>
<br>
<el-checkbox disabled checked>富文本编辑器</el-checkbox>
<br>
<el-checkbox disabled checked>markdown编辑器</el-checkbox>
<br>
<el-checkbox disabled checked>图片拖拽/裁剪上传</el-checkbox>
<br>
<el-checkbox disabled checked>支持切换主题色</el-checkbox>
<br>
<el-checkbox disabled checked>列表拖拽排序</el-checkbox>
<br>
</article>
</div>
</div>
</template>
<style scoped>
.ms-doc{
width:100%;
max-width: 980px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
.ms-doc h3{
padding: 9px 10px 10px;
margin: 0;
font-size: 14px;
line-height: 17px;
background-color: #f5f5f5;
border: 1px solid #d8d8d8;
border-bottom: 0;
border-radius: 3px 3px 0 0;
}
.ms-doc article{
padding: 45px;
word-wrap: break-word;
background-color: #fff;
border: 1px solid #ddd;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
}
.ms-doc article h1{
font-size:32px;
padding-bottom: 10px;
margin-bottom: 15px;
border-bottom: 1px solid #ddd;
}
.ms-doc article h2 {
margin: 24px 0 16px;
font-weight: 600;
line-height: 1.25;
padding-bottom: 7px;
font-size: 24px;
border-bottom: 1px solid #eee;
}
.ms-doc article p{
margin-bottom: 15px;
line-height: 1.5;
}
.ms-doc article .el-checkbox{
margin-bottom: 5px;
}
</style>

View File

@ -1,140 +0,0 @@
<template>
<div>
<div class="crumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item><i class="el-icon-date"></i> 表单</el-breadcrumb-item>
<el-breadcrumb-item>图片上传</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="container">
<div class="content-title">支持拖拽</div>
<div class="plugins-tips">
Element UI自带上传组件
访问地址<a href="http://element.eleme.io/#/zh-CN/component/upload" target="_blank">Element UI Upload</a>
</div>
<el-upload
class="upload-demo"
drag
action="/api/posts/"
multiple>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">只能上传jpg/png文件且不超过500kb</div>
</el-upload>
<div class="content-title">支持裁剪</div>
<div class="plugins-tips">
vue-cropperjs一个封装了 cropperjs Vue 组件
访问地址<a href="https://github.com/Agontuk/vue-cropperjs" target="_blank">vue-cropperjs</a>
</div>
<div class="crop-demo">
<img :src="cropImg" class="pre-img">
<div class="crop-demo-btn">选择图片
<input class="crop-input" type="file" name="image" accept="image/*" @change="setImage"/>
</div>
</div>
<el-dialog title="裁剪图片" :visible.sync="dialogVisible" width="30%">
<vue-cropper ref='cropper' :src="imgSrc" :ready="cropImage" :zoom="cropImage" :cropmove="cropImage" style="width:100%;height:300px;"></vue-cropper>
<span slot="footer" class="dialog-footer">
<el-button @click="cancelCrop"> </el-button>
<el-button type="primary" @click="dialogVisible = false"> </el-button>
</span>
</el-dialog>
</div>
</div>
</template>
<script>
import VueCropper from 'vue-cropperjs';
export default {
data: function(){
return {
defaultSrc: './static/img/img.jpg',
fileList: [],
imgSrc: '',
cropImg: '',
dialogVisible: false,
}
},
components: {
VueCropper
},
methods:{
setImage(e){
const file = e.target.files[0];
if (!file.type.includes('image/')) {
return;
}
const reader = new FileReader();
reader.onload = (event) => {
this.dialogVisible = true;
this.imgSrc = event.target.result;
this.$refs.cropper && this.$refs.cropper.replace(event.target.result);
};
reader.readAsDataURL(file);
},
cropImage () {
this.cropImg = this.$refs.cropper.getCroppedCanvas().toDataURL();
},
cancelCrop(){
this.dialogVisible = false;
this.cropImg = this.defaultSrc;
},
imageuploaded(res) {
console.log(res)
},
handleError(){
this.$notify.error({
title: '上传失败',
message: '图片上传接口上传失败,可更改为自己的服务器接口'
});
}
},
created(){
this.cropImg = this.defaultSrc;
}
}
</script>
<style scoped>
.content-title{
font-weight: 400;
line-height: 50px;
margin: 10px 0;
font-size: 22px;
color: #1f2f3d;
}
.pre-img{
width: 100px;
height: 100px;
background: #f8f8f8;
border: 1px solid #eee;
border-radius: 5px;
}
.crop-demo{
display: flex;
align-items: flex-end;
}
.crop-demo-btn{
position: relative;
width: 100px;
height: 40px;
line-height: 40px;
padding: 0 20px;
margin-left: 30px;
background-color: #409eff;
color: #fff;
font-size: 14px;
border-radius: 4px;
box-sizing: border-box;
}
.crop-input{
position: absolute;
width: 100px;
height: 40px;
left: 0;
top: 0;
opacity: 0;
cursor: pointer;
}
</style>

View File

@ -1,52 +0,0 @@
<template>
<div>
<div class="crumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item><i class="el-icon-date"></i> 表单</el-breadcrumb-item>
<el-breadcrumb-item>编辑器</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="container">
<div class="plugins-tips">
Vue-Quill-Editor基于Quill适用于Vue2的富文本编辑器
访问地址<a href="https://github.com/surmon-china/vue-quill-editor" target="_blank">vue-quill-editor</a>
</div>
<quill-editor ref="myTextEditor" v-model="content" :options="editorOption"></quill-editor>
<el-button class="editor-btn" type="primary" @click="submit"></el-button>
</div>
</div>
</template>
<script>
import 'quill/dist/quill.core.css';
import 'quill/dist/quill.snow.css';
import 'quill/dist/quill.bubble.css';
import { quillEditor } from 'vue-quill-editor';
export default {
data: function(){
return {
content: '',
editorOption: {
placeholder: 'Hello World'
}
}
},
components: {
quillEditor
},
methods: {
onEditorChange({ editor, html, text }) {
this.content = html;
},
submit(){
console.log(this.content);
this.$message.success('提交成功!');
}
}
}
</script>
<style scoped>
.editor-btn{
margin-top: 20px;
}
</style>

View File

@ -1,97 +0,0 @@
<template>
<div class="table">
<div class="crumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item><i class="el-icon-tickets"></i> 表格</el-breadcrumb-item>
<el-breadcrumb-item>VueDatasource</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="container">
<div class="plugins-tips">
vue-datasource一个用于动态创建表格的vue.js服务端组件
访问地址<a href="https://github.com/coderdiaz/vue-datasource" target="_blank">vue-datasource</a>
</div>
<datasource language="en" :table-data="getData" :columns="columns" :pagination="information.pagination"
:actions="actions"
v-on:change="changePage"
v-on:searching="onSearch"
></datasource>
</div>
</div>
</template>
<script>
import axios from 'axios';
import Datasource from 'vue-datasource';
export default {
data(){
return {
url: './static/datasource.json',
information: {
pagination:{},
data:[]
},
columns: [
{
name: 'Id',
key: 'id',
},
{
name: 'Name',
key: 'name',
},
{
name: 'email',
key: 'email',
},
{
name: 'ip',
key: 'ip',
}
],
actions: [
{
text: 'Click',
class: 'btn-primary',
event: (e, row)=>{
row && this.$message('选中的行数: ' + row.row.id);
}
}
],
query:''
}
},
components: {
Datasource
},
methods: {
changePage(values) {
this.information.pagination.per_page = values.perpage;
this.information.data = this.information.data;
},
onSearch(searchQuery) {
this.query = searchQuery;
}
},
computed:{
getData(){
return this.information.data.filter((d) =>{
if(d.name.indexOf(this.query) > -1){
return d;
}
})
}
},
beforeMount(){
// 使 easy-mock 使 json
if(process.env.NODE_ENV === 'development'){
this.url = '/ms/table/source';
};
axios.get(this.url).then( (res) => {
this.information = res.data;
})
}
}
</script>
<style src="../../../static/css/datasource.css"></style>

View File

@ -0,0 +1,90 @@
<template>
<div class="sidebar">
<el-menu
class="sidebar-el-menu"
:default-active="onRoutes"
:collapse="sidebar.collapse"
:background-color="sidebar.bgColor"
:text-color="sidebar.textColor"
router
>
<template v-for="item in menuData">
<template v-if="item.children">
<el-sub-menu :index="item.index" :key="item.index" v-permiss="item.id">
<template #title>
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<span>{{ item.title }}</span>
</template>
<template v-for="subItem in item.children">
<el-sub-menu
v-if="subItem.children"
:index="subItem.index"
:key="subItem.index"
v-permiss="item.id"
>
<template #title>{{ subItem.title }}</template>
<el-menu-item
v-for="(threeItem, i) in subItem.children"
:key="i"
:index="threeItem.index"
>
{{ threeItem.title }}
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="subItem.index" v-permiss="item.id">
{{ subItem.title }}
</el-menu-item>
</template>
</el-sub-menu>
</template>
<template v-else>
<el-menu-item :index="item.index" :key="item.index" v-permiss="item.id">
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<template #title>{{ item.title }}</template>
</el-menu-item>
</template>
</template>
</el-menu>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useSidebarStore } from '../store/sidebar';
import { useRoute } from 'vue-router';
import { menuData } from '@/components/menu';
const route = useRoute();
const onRoutes = computed(() => {
return route.path;
});
const sidebar = useSidebarStore();
</script>
<style scoped>
.sidebar {
display: block;
position: absolute;
left: 0;
top: 70px;
bottom: 0;
overflow-y: scroll;
}
.sidebar::-webkit-scrollbar {
width: 0;
}
.sidebar-el-menu:not(.el-menu--collapse) {
width: 250px;
}
.sidebar-el-menu {
min-height: 100%;
}
</style>

View File

@ -0,0 +1,211 @@
<template>
<div>
<div class="table-toolbar" v-if="hasToolbar">
<div class="table-toolbar-left">
<slot name="toolbarBtn"></slot>
</div>
<div class="table-toolbar-right flex-center">
<template v-if="multipleSelection.length > 0">
<el-tooltip effect="dark" content="删除选中" placement="top">
<el-icon class="columns-setting-icon" @click="delSelection(multipleSelection)">
<Delete />
</el-icon>
</el-tooltip>
<el-divider direction="vertical" />
</template>
<el-tooltip effect="dark" content="刷新" placement="top">
<el-icon class="columns-setting-icon" @click="refresh">
<Refresh />
</el-icon>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip effect="dark" content="列设置" placement="top">
<el-dropdown :hide-on-click="false" size="small" trigger="click">
<el-icon class="columns-setting-icon">
<Setting />
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="c in columns">
<el-checkbox v-model="c.visible" :label="c.label" />
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
</div>
</div>
<el-table class="mgb20" :style="{ width: '100%' }" border :data="tableData" :row-key="rowKey"
@selection-change="handleSelectionChange" table-layout="auto">
<template v-for="item in columns" :key="item.prop">
<el-table-column v-if="item.visible" :prop="item.prop" :label="item.label" :width="item.width"
:type="item.type" :align="item.align || 'center'">
<template #default="{ row, column, $index }" v-if="item.type === 'index'">
{{ getIndex($index) }}
</template>
<template #default="{ row, column, $index }" v-if="!item.type">
<slot :name="item.prop" :rows="row" :index="$index">
<template v-if="item.prop == 'operator'">
<el-button type="warning" size="small" :icon="View" @click="viewFunc(row)">
查看
</el-button>
<el-button type="primary" size="small" :icon="Edit" @click="editFunc(row)">
编辑
</el-button>
<el-button type="danger" size="small" :icon="Delete" @click="handleDelete(row)">
删除
</el-button>
</template>
<span v-else-if="item.formatter">
{{ item.formatter(row[item.prop]) }}
</span>
<span v-else>
{{ row[item.prop] }}
</span>
</slot>
</template>
</el-table-column>
</template>
</el-table>
<el-pagination v-if="hasPagination" :current-page="currentPage" :page-size="pageSize" :background="true"
:layout="layout" :total="total" @current-change="handleCurrentChange" />
</div>
</template>
<script setup lang="ts">
import { toRefs, PropType, ref } from 'vue'
import { Delete, Edit, View, Refresh } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
const props = defineProps({
//
tableData: {
type: Array,
default: []
},
columns: {
type: Array as PropType<any[]>,
default: []
},
rowKey: {
type: String,
default: 'id'
},
hasToolbar: {
type: Boolean,
default: true
},
//
hasPagination: {
type: Boolean,
default: true
},
total: {
type: Number,
default: 0
},
currentPage: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
layout: {
type: String,
default: 'total, prev, pager, next'
},
delFunc: {
type: Function,
default: () => { }
},
viewFunc: {
type: Function,
default: () => { }
},
editFunc: {
type: Function,
default: () => { }
},
delSelection: {
type: Function,
default: () => { }
},
refresh: {
type: Function,
default: () => { }
},
changePage: {
type: Function,
default: () => { }
}
})
let {
tableData,
columns,
rowKey,
hasToolbar,
hasPagination,
total,
currentPage,
pageSize,
layout,
} = toRefs(props)
columns.value.forEach((item) => {
if (item.visible === undefined) {
item.visible = true
}
})
//
const multipleSelection = ref([])
const handleSelectionChange = (selection: any[]) => {
multipleSelection.value = selection
}
//
const handleCurrentChange = (val: number) => {
props.changePage(val)
}
const handleDelete = (row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
type: 'warning'
})
.then(async () => {
props.delFunc(row);
})
.catch(() => { });
};
const getIndex = (index: number) => {
return index + 1 + (currentPage.value - 1) * pageSize.value
}
</script>
<style scoped>
.table-toolbar {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 10px;
}
.columns-setting-icon {
display: block;
font-size: 18px;
cursor: pointer;
color: #676767;
}
</style>
<style>
.table-header .cell {
color: #333;
}
</style>

View File

@ -0,0 +1,21 @@
<template>
<el-descriptions :title="title" :column="column" border>
<el-descriptions-item v-for="item in list" :span="item.span">
<template #label> {{ item.label }} </template>
<slot :name="item.prop" :rows="row">
{{ item.value || row[item.prop] }}
</slot>
</el-descriptions-item>
</el-descriptions>
</template>
<script lang="ts" setup>
const props = defineProps({
data: {
type: Object,
required: true,
}
});
const { row, title, column = 2, list } = props.data;
</script>

View File

@ -0,0 +1,111 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" :label-width="options.labelWidth">
<el-row>
<el-col :span="options.span" v-for="item in options.list">
<el-form-item :label="item.label" :prop="item.prop">
<!-- 文本框数字框下拉框日期框开关上传 -->
<el-input v-if="item.type === 'input'" v-model="form[item.prop]" :disabled="item.disabled"
:placeholder="item.placeholder" clearable></el-input>
<el-input-number v-else-if="item.type === 'number'" v-model="form[item.prop]"
:disabled="item.disabled" controls-position="right"></el-input-number>
<el-select v-else-if="item.type === 'select'" v-model="form[item.prop]" :disabled="item.disabled"
:placeholder="item.placeholder" clearable>
<el-option v-for="opt in item.opts" :label="opt.label" :value="opt.value"></el-option>
</el-select>
<el-date-picker v-else-if="item.type === 'date'" type="date" v-model="form[item.prop]"
:value-format="item.format"></el-date-picker>
<el-switch v-else-if="item.type === 'switch'" v-model="form[item.prop]"
:active-value="item.activeValue" :inactive-value="item.inactiveValue"
:active-text="item.activeText" :inactive-text="item.inactiveText"></el-switch>
<el-upload v-else-if="item.type === 'upload'" class="avatar-uploader" action="#"
:show-file-list="false" :on-success="handleAvatarSuccess">
<img v-if="form[item.prop]" :src="form[item.prop]" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
<slot :name="item.prop" v-else>
</slot>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button type="primary" @click="saveEdit(formRef)"> </el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { FormOption } from '@/types/form-option';
import { FormInstance, FormRules, UploadProps } from 'element-plus';
import { PropType, ref } from 'vue';
const { options, formData, edit, update } = defineProps({
options: {
type: Object as PropType<FormOption>,
required: true
},
formData: {
type: Object,
required: true
},
edit: {
type: Boolean,
required: false
},
update: {
type: Function,
required: true
}
});
const form = ref({ ...(edit ? formData : {}) });
const rules: FormRules = options.list.map(item => {
if (item.required) {
return { [item.prop]: [{ required: true, message: `${item.label}不能为空`, trigger: 'blur' }] };
}
return {};
}).reduce((acc, cur) => ({ ...acc, ...cur }), {});
const formRef = ref<FormInstance>();
const saveEdit = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(valid => {
if (!valid) return false;
update(form.value);
});
};
const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
form.value.thumb = URL.createObjectURL(uploadFile.raw!);
};
</script>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>

View File

@ -0,0 +1,60 @@
<template>
<div class="search-container">
<el-form ref="searchRef" :model="query" :inline="true">
<el-form-item :label="item.label" :prop="item.prop" v-for="item in options">
<!-- 文本框下拉框日期框 -->
<el-input v-if="item.type === 'input'" v-model="query[item.prop]" :disabled="item.disabled"
:placeholder="item.placeholder" clearable></el-input>
<el-select v-else-if="item.type === 'select'" v-model="query[item.prop]" :disabled="item.disabled"
:placeholder="item.placeholder" clearable>
<el-option v-for="opt in item.opts" :label="opt.label" :value="opt.value"></el-option>
</el-select>
<el-date-picker v-else-if="item.type === 'date'" type="date" v-model="query[item.prop]"
:value-format="item.format"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="search"></el-button>
<el-button :icon="Refresh" @click="resetForm(searchRef)"></el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { FormInstance } from 'element-plus';
import { Search, Refresh } from '@element-plus/icons-vue';
import { PropType, ref } from 'vue';
import { FormOptionList } from '@/types/form-option';
const props = defineProps({
query: {
type: Object,
required: true
},
options: {
type: Array as PropType<Array<FormOptionList>>,
required: true
},
search: {
type: Function,
default: () => { }
}
});
const searchRef = ref<FormInstance>();
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
props.search();
}
</script>
<style scoped>
.search-container {
padding: 20px 30px 0;
background-color: #fff;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 5px
}
</style>

148
src/components/tabs.vue Normal file
View File

@ -0,0 +1,148 @@
<template>
<div class="tabs-container">
<el-tabs v-model="activePath" class="tabs" type="card" closable @tab-click="clickTabls" @tab-remove="closeTabs">
<el-tab-pane
v-for="item in tabs.list"
:key="item.path"
:label="item.title"
:name="item.path"
@click="setTags(item)"
></el-tab-pane>
</el-tabs>
<div class="Tabs-close-box">
<el-dropdown @command="handleTags">
<el-button size="small" type="primary" plain>
标签选项
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu size="small">
<el-dropdown-item command="other">关闭其他</el-dropdown-item>
<el-dropdown-item command="current">关闭当前</el-dropdown-item>
<el-dropdown-item command="all">关闭所有</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useTabsStore } from '../store/tabs';
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const activePath = ref(route.fullPath);
const tabs = useTabsStore();
//
const setTags = (route: any) => {
const isExist = tabs.list.some((item) => {
return item.path === route.fullPath;
});
if (!isExist) {
tabs.setTabsItem({
name: route.name,
title: route.meta.title,
path: route.fullPath,
});
}
};
setTags(route);
onBeforeRouteUpdate((to) => {
setTags(to);
});
//
const closeAll = () => {
tabs.clearTabs();
router.push('/');
};
//
const closeOther = () => {
const curItem = tabs.list.filter((item) => {
return item.path === route.fullPath;
});
tabs.closeTabsOther(curItem);
};
const handleTags = (command: string) => {
switch (command) {
case 'current':
//
tabs.closeCurrentTag({
$router: router,
$route: route,
});
break;
case 'all':
closeAll();
break;
case 'other':
closeOther();
break;
}
};
const clickTabls = (item: any) => {
router.push(item.props.name);
};
const closeTabs = (path: string) => {
const index = tabs.list.findIndex((item) => item.path === path);
tabs.delTabsItem(index);
const item = tabs.list[index] || tabs.list[index - 1];
router.push(item ? item.path : '/');
};
watch(
() => route.fullPath,
(newVal, oldVal) => {
activePath.value = newVal;
}
);
</script>
<style scss>
.tabs-container {
position: relative;
overflow: hidden;
background: #fff;
padding: 2px 120px 0 0;
}
.tabs {
.el-tabs__header {
margin-bottom: 0;
}
.el-tabs__nav {
height: 28px;
}
.el-tabs__nav-next,
.el-tabs__nav-prev {
line-height: 32px;
}
&.el-tabs {
--el-tabs-header-height: 28px;
}
}
.Tabs-close-box {
position: absolute;
right: 0;
top: 0;
box-sizing: border-box;
padding-top: 1px;
text-align: center;
width: 110px;
height: 30px;
background: #fff;
box-shadow: -3px 0 15px 3px rgba(0, 0, 0, 0.1);
z-index: 10;
}
</style>

View File

@ -1,34 +0,0 @@
import Vue from 'vue';
import App from './App';
import router from './router';
import axios from 'axios';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css'; // 默认主题
// import '../static/css/theme-green/index.css'; // 浅绿色主题
import "babel-polyfill";
Vue.use(ElementUI, { size: 'small' });
Vue.prototype.$axios = axios;
//使用钩子函数对路由进行权限跳转
router.beforeEach((to, from, next) => {
if(to.meta.permission){
const role = localStorage.getItem('ms_username');
// 如果是管理员权限则可进入,这里只是简单的模拟管理员权限而已
role === 'admin' ? next() : next('/login');
}else{
// 简单的判断IE10及以下不进入富文本编辑器该组件不兼容
if(navigator.userAgent.indexOf('MSIE') > -1 && to.path === '/editor'){
Vue.prototype.$alert('vue-quill-editor组件不兼容IE10及以下浏览器请使用更高版本的浏览器查看', '浏览器不兼容通知', {
confirmButtonText: '确定'
});
}else{
next();
}
}
})
new Vue({
router,
render: h => h(App)
}).$mount('#app');

28
src/main.ts Normal file
View File

@ -0,0 +1,28 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import App from './App.vue';
import router from './router';
import { usePermissStore } from './store/permiss';
import 'element-plus/dist/index.css';
import './assets/css/icon.css';
const app = createApp(App);
app.use(createPinia());
app.use(router);
// 注册elementplus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
// 自定义权限指令
const permiss = usePermissStore();
app.directive('permiss', {
mounted(el, binding) {
if (binding.value && !permiss.key.includes(String(binding.value))) {
el['hidden'] = true;
}
},
});
app.mount('#app');

View File

@ -1,71 +0,0 @@
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
redirect: '/login'
},
{
path: '/readme',
component: resolve => require(['../components/common/Home.vue'], resolve),
children:[
{
path: '/',
component: resolve => require(['../components/page/Readme.vue'], resolve)
},
{
path: '/table',
component: resolve => require(['../components/page/BaseTable.vue'], resolve)
},
{
// vue-datasource组件
path: '/datasource',
component: resolve => require(['../components/page/VueTable.vue'], resolve)
},
{
path: '/form',
component: resolve => require(['../components/page/BaseForm.vue'], resolve)
},
{
// 富文本编辑器组件
path: '/editor',
component: resolve => require(['../components/page/VueEditor.vue'], resolve)
},
{
// markdown组件
path: '/markdown',
component: resolve => require(['../components/page/Markdown.vue'], resolve)
},
{
// 图片上传组件
path: '/upload',
component: resolve => require(['../components/page/Upload.vue'], resolve)
},
{
// vue-schart组件
path: '/charts',
component: resolve => require(['../components/page/BaseCharts.vue'], resolve)
},
{
// 拖拽列表组件
path: '/drag',
component: resolve => require(['../components/page/DragList.vue'], resolve)
},
{
// 权限页面
path: '/permission',
component: resolve => require(['../components/page/Permission.vue'], resolve),
meta: {permission: true}
}
]
},
{
path: '/login',
component: resolve => require(['../components/page/Login.vue'], resolve)
},
]
})

293
src/router/index.ts Normal file
View File

@ -0,0 +1,293 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import { usePermissStore } from '../store/permiss';
import Home from '../views/home.vue';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/dashboard',
},
{
path: '/',
name: 'Home',
component: Home,
children: [
{
path: '/dashboard',
name: 'dashboard',
meta: {
title: '系统首页',
noAuth: true,
},
component: () => import(/* webpackChunkName: "dashboard" */ '../views/dashboard.vue'),
},
{
path: '/system-user',
name: 'system-user',
meta: {
title: '用户管理',
permiss: '11',
},
component: () => import(/* webpackChunkName: "system-user" */ '../views/system/user.vue'),
},
{
path: '/system-role',
name: 'system-role',
meta: {
title: '角色管理',
permiss: '12',
},
component: () => import(/* webpackChunkName: "system-role" */ '../views/system/role.vue'),
},
{
path: '/system-menu',
name: 'system-menu',
meta: {
title: '菜单管理',
permiss: '13',
},
component: () => import(/* webpackChunkName: "system-menu" */ '../views/system/menu.vue'),
},
{
path: '/table',
name: 'basetable',
meta: {
title: '基础表格',
permiss: '31',
},
component: () => import(/* webpackChunkName: "table" */ '../views/table/basetable.vue'),
},
{
path: '/table-editor',
name: 'table-editor',
meta: {
title: '可编辑表格',
permiss: '32',
},
component: () => import(/* webpackChunkName: "table-editor" */ '../views/table/table-editor.vue'),
},
{
path: '/schart',
name: 'schart',
meta: {
title: 'schart图表',
permiss: '41',
},
component: () => import(/* webpackChunkName: "schart" */ '../views/chart/schart.vue'),
},
{
path: '/echarts',
name: 'echarts',
meta: {
title: 'echarts图表',
permiss: '42',
},
component: () => import(/* webpackChunkName: "echarts" */ '../views/chart/echarts.vue'),
},
{
path: '/icon',
name: 'icon',
meta: {
title: '图标',
permiss: '5',
},
component: () => import(/* webpackChunkName: "icon" */ '../views/pages/icon.vue'),
},
{
path: '/ucenter',
name: 'ucenter',
meta: {
title: '个人中心',
},
component: () => import(/* webpackChunkName: "ucenter" */ '../views/pages/ucenter.vue'),
},
{
path: '/editor',
name: 'editor',
meta: {
title: '富文本编辑器',
permiss: '291',
},
component: () => import(/* webpackChunkName: "editor" */ '../views/pages/editor.vue'),
},
{
path: '/markdown',
name: 'markdown',
meta: {
title: 'markdown编辑器',
permiss: '292',
},
component: () => import(/* webpackChunkName: "markdown" */ '../views/pages/markdown.vue'),
},
{
path: '/export',
name: 'export',
meta: {
title: '导出Excel',
permiss: '34',
},
component: () => import(/* webpackChunkName: "export" */ '../views/table/export.vue'),
},
{
path: '/import',
name: 'import',
meta: {
title: '导入Excel',
permiss: '33',
},
component: () => import(/* webpackChunkName: "import" */ '../views/table/import.vue'),
},
{
path: '/theme',
name: 'theme',
meta: {
title: '主题设置',
permiss: '7',
},
component: () => import(/* webpackChunkName: "theme" */ '../views/pages/theme.vue'),
},
{
path: '/calendar',
name: 'calendar',
meta: {
title: '日历',
permiss: '24',
},
component: () => import(/* webpackChunkName: "calendar" */ '../views/element/calendar.vue'),
},
{
path: '/watermark',
name: 'watermark',
meta: {
title: '水印',
permiss: '25',
},
component: () => import(/* webpackChunkName: "watermark" */ '../views/element/watermark.vue'),
},
{
path: '/carousel',
name: 'carousel',
meta: {
title: '走马灯',
permiss: '23',
},
component: () => import(/* webpackChunkName: "carousel" */ '../views/element/carousel.vue'),
},
{
path: '/tour',
name: 'tour',
meta: {
title: '分步引导',
permiss: '26',
},
component: () => import(/* webpackChunkName: "tour" */ '../views/element/tour.vue'),
},
{
path: '/steps',
name: 'steps',
meta: {
title: '步骤条',
permiss: '27',
},
component: () => import(/* webpackChunkName: "steps" */ '../views/element/steps.vue'),
},
{
path: '/form',
name: 'forms',
meta: {
title: '表单',
permiss: '21',
},
component: () => import(/* webpackChunkName: "form" */ '../views/element/form.vue'),
},
{
path: '/upload',
name: 'upload',
meta: {
title: '上传',
permiss: '22',
},
component: () => import(/* webpackChunkName: "upload" */ '../views/element/upload.vue'),
},
{
path: '/statistic',
name: 'statistic',
meta: {
title: '统计',
permiss: '28',
},
component: () => import(/* webpackChunkName: "statistic" */ '../views/element/statistic.vue'),
},
],
},
{
path: '/login',
meta: {
title: '登录',
noAuth: true,
},
component: () => import(/* webpackChunkName: "login" */ '../views/pages/login.vue'),
},
{
path: '/register',
meta: {
title: '注册',
noAuth: true,
},
component: () => import(/* webpackChunkName: "register" */ '../views/pages/register.vue'),
},
{
path: '/reset-pwd',
meta: {
title: '重置密码',
noAuth: true,
},
component: () => import(/* webpackChunkName: "reset-pwd" */ '../views/pages/reset-pwd.vue'),
},
{
path: '/403',
meta: {
title: '没有权限',
noAuth: true,
},
component: () => import(/* webpackChunkName: "403" */ '../views/pages/403.vue'),
},
{
path: '/404',
meta: {
title: '找不到页面',
noAuth: true,
},
component: () => import(/* webpackChunkName: "404" */ '../views/pages/404.vue'),
},
{ path: '/:path(.*)', redirect: '/404' },
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
router.beforeEach((to, from, next) => {
NProgress.start();
const role = localStorage.getItem('vuems_name');
const permiss = usePermissStore();
if (!role && to.meta.noAuth !== true) {
next('/login');
} else if (typeof to.meta.permiss == 'string' && !permiss.key.includes(to.meta.permiss)) {
// 如果没有权限则进入403
next('/403');
} else {
next();
}
});
router.afterEach(() => {
NProgress.done();
});
export default router;

60
src/store/permiss.ts Normal file
View File

@ -0,0 +1,60 @@
import { defineStore } from 'pinia';
interface ObjectList {
[key: string]: string[];
}
export const usePermissStore = defineStore('permiss', {
state: () => {
const defaultList: ObjectList = {
admin: [
'0',
'1',
'11',
'12',
'13',
'2',
'21',
'22',
'23',
'24',
'25',
'26',
'27',
'28',
'29',
'291',
'292',
'3',
'31',
'32',
'33',
'34',
'4',
'41',
'42',
'5',
'7',
'6',
'61',
'62',
'63',
'64',
'65',
'66',
],
user: ['0', '1', '11', '12', '13'],
};
const username = localStorage.getItem('vuems_name');
console.log(username);
return {
key: (username == 'admin' ? defaultList.admin : defaultList.user) as string[],
defaultList,
};
},
actions: {
handleSet(val: string[]) {
this.key = val;
},
},
});

25
src/store/sidebar.ts Normal file
View File

@ -0,0 +1,25 @@
import { defineStore } from 'pinia';
export const useSidebarStore = defineStore('sidebar', {
state: () => {
return {
collapse: false,
bgColor: localStorage.getItem('sidebar-bg-color') || '#324157',
textColor: localStorage.getItem('sidebar-text-color') || '#bfcbd9'
};
},
getters: {},
actions: {
handleCollapse() {
this.collapse = !this.collapse;
},
setBgColor(color: string) {
this.bgColor = color;
localStorage.setItem('sidebar-bg-color', color);
},
setTextColor(color: string) {
this.textColor = color;
localStorage.setItem('sidebar-text-color', color);
}
}
});

53
src/store/tabs.ts Normal file
View File

@ -0,0 +1,53 @@
import { defineStore } from 'pinia';
interface ListItem {
name: string;
path: string;
title: string;
}
export const useTabsStore = defineStore('tabs', {
state: () => {
return {
list: <ListItem[]>[]
};
},
getters: {
show: state => {
return state.list.length > 0;
},
nameList: state => {
return state.list.map(item => item.name);
}
},
actions: {
delTabsItem(index: number) {
this.list.splice(index, 1);
},
setTabsItem(data: ListItem) {
this.list.push(data);
},
clearTabs() {
this.list = [];
},
closeTabsOther(data: ListItem[]) {
this.list = data;
},
closeCurrentTag(data: any) {
for (let i = 0, len = this.list.length; i < len; i++) {
const item = this.list[i];
if (item.path === data.$route.fullPath) {
if (i < len - 1) {
data.$router.push(this.list[i + 1].path);
} else if (i > 0) {
data.$router.push(this.list[i - 1].path);
} else {
data.$router.push('/');
}
this.list.splice(i, 1);
break;
}
}
}
}
});

58
src/store/theme.ts Normal file
View File

@ -0,0 +1,58 @@
import { mix, setProperty } from '@/utils';
import { defineStore } from 'pinia';
export const useThemeStore = defineStore('theme', {
state: () => {
return {
primary: '',
success: '',
warning: '',
danger: '',
info: '',
headerBgColor: '#242f42',
headerTextColor: '#fff',
};
},
getters: {},
actions: {
initTheme() {
['primary', 'success', 'warning', 'danger', 'info'].forEach((type) => {
const color = localStorage.getItem(`theme-${type}`) || '';
if (color) {
this.setPropertyColor(color, type); // 设置主题色
}
});
const headerBgColor = localStorage.getItem('header-bg-color');
headerBgColor && this.setHeaderBgColor(headerBgColor);
const headerTextColor = localStorage.getItem('header-text-color');
headerTextColor && this.setHeaderTextColor(headerTextColor);
},
resetTheme() {
['primary', 'success', 'warning', 'danger', 'info'].forEach((type) => {
this.setPropertyColor('', type); // 重置主题色
});
},
setPropertyColor(color: string, type: string = 'primary') {
this[type] = color;
setProperty(`--el-color-${type}`, color);
localStorage.setItem(`theme-${type}`, color);
this.setThemeLight(type);
},
setThemeLight(type: string = 'primary') {
[3, 5, 7, 8, 9].forEach((v) => {
setProperty(`--el-color-${type}-light-${v}`, mix('#ffffff', this[type], v / 10));
});
setProperty(`--el-color-${type}-dark-2`, mix('#ffffff', this[type], 0.2));
},
setHeaderBgColor(color: string) {
this.headerBgColor = color;
setProperty('--header-bg-color', color);
localStorage.setItem(`header-bg-color`, color);
},
setHeaderTextColor(color: string) {
this.headerTextColor = color;
setProperty('--header-text-color', color);
localStorage.setItem(`header-text-color`, color);
}
}
});

21
src/types/form-option.ts Normal file
View File

@ -0,0 +1,21 @@
export interface FormOption {
list: FormOptionList[];
labelWidth?: number | string;
span?: number;
}
export interface FormOptionList {
prop: string;
label: string;
type: string;
placeholder?: string;
disabled?: boolean;
opts?: any[];
format?: string;
activeValue?: any;
inactiveValue?: any;
activeText?: string;
inactiveText?: string;
required?: boolean;
}

9
src/types/menu.ts Normal file
View File

@ -0,0 +1,9 @@
export interface Menus {
id: string;
pid?: string;
icon?: string;
index: string;
title: string;
permiss?: string;
children?: Menus[];
}

8
src/types/role.ts Normal file
View File

@ -0,0 +1,8 @@
export interface Role {
id: number;
name: string;
key: string;
status: boolean;
permiss: string[]
}

9
src/types/table.ts Normal file
View File

@ -0,0 +1,9 @@
export interface TableItem {
id: number;
name: string;
thumb: string;
money: number;
state: string;
date: string;
address: string;
}

16
src/types/user.ts Normal file
View File

@ -0,0 +1,16 @@
export interface User {
id: number;
name: string;
password: string;
email: string;
phone: string;
role: string;
date: string;
}
export interface Register {
username: string;
password: string;
email: string;
}

3
src/utils/china.ts Normal file

File diff suppressed because one or more lines are too long

14
src/utils/index.ts Normal file
View File

@ -0,0 +1,14 @@
export const setProperty = (prop: string, val: any, dom = document.documentElement) => {
dom.style.setProperty(prop, val);
};
export const mix = (color1: string, color2: string, weight: number = 0.5): string => {
let color = '#';
for (let i = 0; i <= 2; i++) {
const c1 = parseInt(color1.substring(1 + i * 2, 3 + i * 2), 16);
const c2 = parseInt(color2.substring(1 + i * 2, 3 + i * 2), 16);
const c = Math.round(c1 * weight + c2 * (1 - weight));
color += c.toString(16).padStart(2, '0');
}
return color;
};

31
src/utils/request.ts Normal file
View File

@ -0,0 +1,31 @@
import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
const service: AxiosInstance = axios.create({
timeout: 5000
});
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
return config;
},
(error: AxiosError) => {
console.log(error);
return Promise.reject();
}
);
service.interceptors.response.use(
(response: AxiosResponse) => {
if (response.status === 200) {
return response;
} else {
Promise.reject();
}
},
(error: AxiosError) => {
console.log(error);
return Promise.reject();
}
);
export default service;

View File

@ -0,0 +1,87 @@
<template>
<div class="container">
<div class="plugins-tips">
vue-echartsApache ECharts Vue.js 组件 访问地址
<a href="https://github.com/ecomfe/vue-echarts" target="_blank">vue-echarts</a>
</div>
<el-card class="mgb20" shadow="hover">
<template #header>
<div class="content-title">柱状图</div>
</template>
<v-chart class="schart" :option="barOptions" />
</el-card>
<el-card class="mgb20" shadow="hover">
<template #header>
<div class="content-title">折线图</div>
</template>
<v-chart class="schart" :option="lineOptions" />
</el-card>
<el-card class="mgb20" shadow="hover">
<template #header>
<div class="content-title">饼状图</div>
</template>
<v-chart class="schart" :option="pieOptions" />
</el-card>
<el-card class="mgb20" shadow="hover">
<template #header>
<div class="content-title">环形图</div>
</template>
<v-chart class="schart" :option="ringOptions" />
</el-card>
<el-card class="mgb20" shadow="hover">
<template #header>
<div class="content-title">词云图</div>
</template>
<v-chart class="schart" :option="wordOptions" />
</el-card>
<el-card class="mgb20" shadow="hover">
<template #header>
<div class="content-title">地图</div>
</template>
<v-chart class="schart" :option="mapOptions" />
</el-card>
</div>
</template>
<script setup lang="ts" name="echarts">
import { registerMap, use } from 'echarts/core';
import { BarChart, LineChart, PieChart, MapChart } from 'echarts/charts';
import {
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent,
VisualMapComponent,
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import VChart from 'vue-echarts';
import 'echarts-wordcloud';
import { barOptions, lineOptions, pieOptions, ringOptions, wordOptions, mapOptions } from './options';
import chinaMap from '@/utils/china';
use([
CanvasRenderer,
BarChart,
GridComponent,
LineChart,
PieChart,
MapChart,
TooltipComponent,
LegendComponent,
TitleComponent,
VisualMapComponent,
]);
registerMap('china', chinaMap);
</script>
<style scoped>
.schart {
width: 100%;
height: 400px;
}
.content-title {
font-weight: 400;
font-size: 22px;
color: #1f2f3d;
}
</style>

345
src/views/chart/options.ts Normal file
View File

@ -0,0 +1,345 @@
import { graphic } from 'echarts/core';
export const barOptions = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
color: ['#009688', '#f44336'],
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar',
},
{
data: [180, 230, 190, 120, 110, 230, 235],
type: 'bar',
},
],
};
export const lineOptions = {
tooltip: {
trigger: 'axis',
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
},
color: ['#009688', '#f44336'],
series: [
{
name: 'Email',
type: 'line',
stack: 'Total',
areaStyle: {},
smooth: true,
data: [120, 132, 101, 134, 90, 230, 210],
},
{
name: 'Union Ads',
type: 'line',
stack: 'Total',
areaStyle: {},
smooth: true,
data: [220, 182, 191, 234, 290, 330, 310],
},
],
};
export const pieOptions = {
title: {
text: 'Referer of a Website',
subtext: 'Fake Data',
left: 'center',
},
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: 'Access From',
type: 'pie',
radius: '50%',
data: [
{ value: 1048, name: 'Search Engine' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' },
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
};
export const wordOptions = {
series: [
{
type: 'wordCloud',
rotationRange: [0, 0],
autoSize: {
enable: true,
minSize: 14,
},
textStyle: {
fontFamily: '微软雅黑,sans-serif',
color: function () {
return (
'rgb(' +
[
Math.round(Math.random() * 160),
Math.round(Math.random() * 160),
Math.round(Math.random() * 160),
].join(',') +
')'
);
},
},
data: [
{
name: 'Vue',
value: 10000,
},
{
name: 'React',
value: 9000,
},
{
name: '图表',
value: 4000,
},
{
name: '产品',
value: 7000,
},
{
name: 'vue-manage-system',
value: 2000,
},
{
name: 'element-plus',
value: 6000,
},
{
name: '管理系统',
value: 5000,
},
{
name: '前端',
value: 4000,
},
{
name: '测试',
value: 3000,
},
{
name: '后端',
value: 8000,
},
{
name: '软件开发',
value: 6000,
},
{
name: '程序员',
value: 4000,
},
],
},
],
};
export const ringOptions = {
tooltip: {
trigger: 'item',
},
legend: {
top: '5%',
left: 'center',
},
series: [
{
name: 'Access From',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [
{ value: 1048, name: 'Search Engine' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' },
],
},
],
};
export const dashOpt1 = {
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
},
grid: {
top: '2%',
left: '2%',
right: '3%',
bottom: '2%',
containLabel: true,
},
color: ['#009688', '#f44336'],
series: [
{
type: 'line',
areaStyle: {
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(0, 150, 136,0.8)',
},
{
offset: 1,
color: 'rgba(0, 150, 136,0.2)',
},
]),
},
smooth: true,
data: [120, 132, 301, 134, 90, 230, 210],
},
{
type: 'line',
smooth: true,
data: [220, 122, 191, 234, 190, 130, 310],
},
],
};
export const dashOpt2 = {
legend: {
bottom: '1%',
left: 'center',
},
color: ['#3f51b5', '#009688', '#f44336', '#00bcd4', '#1ABC9C'],
series: [
{
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
data: [
{ value: 1048, name: '数码' },
{ value: 735, name: '食品' },
{ value: 580, name: '母婴' },
{ value: 484, name: '家电' },
{ value: 300, name: '运动' },
],
},
],
};
export const mapOptions = {
tooltip: {
trigger: 'item',
},
geo: {
map: 'china',
roam: false,
emphasis: {
label: {
show: false,
},
},
},
visualMap: {
show: false,
min: 0,
max: 100,
realtime: false,
calculable: false,
inRange: {
color: ['#d2e0f5', '#71A9FF'],
},
},
series: [
{
geoIndex: 0,
name: '地域分布',
type: 'map',
coordinateSystem: 'geo',
map: 'china',
data: [
{ name: '北京', value: 100 },
{ name: '上海', value: 100 },
{ name: '广东', value: 100 },
{ name: '浙江', value: 90 },
{ name: '江西', value: 80 },
{ name: '山东', value: 70 },
{ name: '广西', value: 60 },
{ name: '河南', value: 50 },
{ name: '河南', value: 40 },
{ name: '青海', value: 70 },
{ name: '河南', value: 30 },
{ name: '黑龙江', value: 20 },
{ name: '新疆', value: 20 },
{ name: '云南', value: 20 },
{ name: '甘肃', value: 20 },
],
},
],
};

129
src/views/chart/schart.vue Normal file
View File

@ -0,0 +1,129 @@
<template>
<div class="container">
<div class="plugins-tips">
vue-schartvue.js封装sChart.js的图表组件 访问地址
<a href="https://github.com/lin-xin/vue-schart" target="_blank">vue-schart</a>
</div>
<el-card class="mgb20" shadow="hover">
<template #header>
<div class="content-title">柱状图</div>
</template>
<schart class="schart" canvasId="bar" :options="options1"></schart>
</el-card>
<el-card class="mgb20" shadow="hover">
<template #header>
<div class="content-title">折线图</div>
</template>
<schart class="schart" canvasId="line" :options="options2"></schart>
</el-card>
<el-card class="mgb20" shadow="hover">
<template #header>
<div class="content-title">饼状图</div>
</template>
<schart class="schart" canvasId="pie" :options="options3"></schart>
</el-card>
<el-card class="mgb20" shadow="hover">
<template #header>
<div class="content-title">环形图</div>
</template>
<schart class="schart" canvasId="ring" :options="options4"></schart>
</el-card>
</div>
</template>
<script setup lang="ts" name="schart">
import Schart from 'vue-schart';
const options1 = {
type: 'bar',
title: {
text: '最近一周各品类销售图'
},
colorList: ["#3f51b5", "#009688", "#f44336", "#00bcd4", "#1ABC9C"],
labels: ['周一', '周二', '周三', '周四', '周五'],
datasets: [
{
label: '家电',
// fillColor: 'rgba(241, 49, 74, 0.5)',
data: [234, 278, 270, 190, 230]
},
{
label: '百货',
data: [164, 178, 190, 135, 160]
},
{
label: '食品',
data: [144, 198, 150, 235, 120]
}
]
};
const options2 = {
type: 'line',
title: {
text: '最近几个月各品类销售趋势图'
},
colorList: ["#3f51b5", "#009688", "#f44336", "#00bcd4", "#1ABC9C"],
labels: ['6月', '7月', '8月', '9月', '10月'],
datasets: [
{
label: '家电',
data: [234, 278, 270, 190, 230]
},
{
label: '百货',
data: [164, 178, 150, 135, 160]
},
{
label: '食品',
data: [114, 138, 200, 235, 190]
}
]
};
const options3 = {
type: 'pie',
title: {
text: '服装品类销售饼状图'
},
legend: {
position: 'left'
},
colorList: ["#2196f3", '#673ab7', "#009688", "#1ABC9C", "#3f51b5", "#f44336", "#00bcd4"],
labels: ['T恤', '牛仔裤', '连衣裙', '毛衣', '七分裤', '短裙', '羽绒服'],
datasets: [
{
data: [334, 278, 190, 235, 260, 200, 141]
}
]
};
const options4 = {
type: 'ring',
title: {
text: '环形三等分'
},
showValue: false,
legend: {
position: 'bottom',
bottom: 40
},
colorList: ["#3f51b5", "#009688", "#f44336", "#00bcd4", "#1ABC9C"],
labels: ['vue', 'react', 'angular'],
datasets: [
{
data: [500, 500, 500]
}
]
};
</script>
<style scoped>
.schart {
width: 100%;
height: 400px;
}
.content-title {
font-weight: 400;
font-size: 22px;
color: #1f2f3d;
}
</style>

357
src/views/dashboard.vue Normal file
View File

@ -0,0 +1,357 @@
<template>
<div>
<el-row :gutter="20" class="mgb20">
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon bg1">
<User />
</el-icon>
<div class="card-content">
<countup class="card-num color1" :end="6666" />
<div>用户访问量</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon bg2">
<ChatDotRound />
</el-icon>
<div class="card-content">
<countup class="card-num color2" :end="168" />
<div>系统消息</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon bg3">
<Goods />
</el-icon>
<div class="card-content">
<countup class="card-num color3" :end="8888" />
<div>商品数量</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon bg4">
<ShoppingCartFull />
</el-icon>
<div class="card-content">
<countup class="card-num color4" :end="568" />
<div>今日订单量</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mgb20">
<el-col :span="18">
<el-card shadow="hover">
<div class="card-header">
<p class="card-header-title">订单动态</p>
<p class="card-header-desc">最近一周订单状态包括订单成交量和订单退货量</p>
</div>
<v-chart class="chart" :option="dashOpt1" />
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="card-header">
<p class="card-header-title">品类分布</p>
<p class="card-header-desc">最近一个月销售商品的品类情况</p>
</div>
<v-chart class="chart" :option="dashOpt2" />
</el-card>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="7">
<el-card shadow="hover" :body-style="{ height: '400px' }">
<div class="card-header">
<p class="card-header-title">时间线</p>
<p class="card-header-desc">最新的销售动态和活动信息</p>
</div>
<el-timeline>
<el-timeline-item v-for="(activity, index) in activities" :key="index" :color="activity.color">
<div class="timeline-item">
<div>
<p>{{ activity.content }}</p>
<p class="timeline-desc">{{ activity.description }}</p>
</div>
<div class="timeline-time">{{ activity.timestamp }}</div>
</div>
</el-timeline-item>
</el-timeline>
</el-card>
</el-col>
<el-col :span="10">
<el-card shadow="hover" :body-style="{ height: '400px' }">
<div class="card-header">
<p class="card-header-title">渠道统计</p>
<p class="card-header-desc">最近一个月的订单来源统计</p>
</div>
<v-chart class="map-chart" :option="mapOptions" />
</el-card>
</el-col>
<el-col :span="7">
<el-card shadow="hover" :body-style="{ height: '400px' }">
<div class="card-header">
<p class="card-header-title">排行榜</p>
<p class="card-header-desc">销售商品的热门榜单Top5</p>
</div>
<div>
<div class="rank-item" v-for="(rank, index) in ranks">
<div class="rank-item-avatar">{{ index + 1 }}</div>
<div class="rank-item-content">
<div class="rank-item-top">
<div class="rank-item-title">{{ rank.title }}</div>
<div class="rank-item-desc">销量{{ rank.value }}</div>
</div>
<el-progress
:show-text="false"
striped
:stroke-width="10"
:percentage="rank.percent"
:color="rank.color"
/>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts" name="dashboard">
import countup from '@/components/countup.vue';
import { use, registerMap } from 'echarts/core';
import { BarChart, LineChart, PieChart, MapChart } from 'echarts/charts';
import {
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent,
VisualMapComponent,
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import VChart from 'vue-echarts';
import { dashOpt1, dashOpt2, mapOptions } from './chart/options';
import chinaMap from '@/utils/china';
use([
CanvasRenderer,
BarChart,
GridComponent,
LineChart,
PieChart,
TooltipComponent,
LegendComponent,
TitleComponent,
VisualMapComponent,
MapChart,
]);
registerMap('china', chinaMap);
const activities = [
{
content: '收藏商品',
description: 'xxx收藏了你的商品就是不买',
timestamp: '30分钟前',
color: '#00bcd4',
},
{
content: '用户评价',
description: 'xxx给了某某商品一个差评吐血啊',
timestamp: '55分钟前',
color: '#1ABC9C',
},
{
content: '订单提交',
description: 'xxx提交了订单快去收钱吧',
timestamp: '1小时前',
color: '#3f51b5',
},
{
content: '退款申请',
description: 'xxx申请了仅退款又要亏钱了',
timestamp: '15小时前',
color: '#f44336',
},
{
content: '商品上架',
description: '运营专员瞒着你上架了一辆飞机',
timestamp: '1天前',
color: '#009688',
},
];
const ranks = [
{
title: '手机',
value: 10000,
percent: 80,
color: '#f25e43',
},
{
title: '电脑',
value: 8000,
percent: 70,
color: '#00bcd4',
},
{
title: '相机',
value: 6000,
percent: 60,
color: '#64d572',
},
{
title: '衣服',
value: 5000,
percent: 55,
color: '#e9a745',
},
{
title: '书籍',
value: 4000,
percent: 50,
color: '#009688',
},
];
</script>
<style>
.card-body {
display: flex;
align-items: center;
height: 100px;
padding: 0;
}
</style>
<style scoped>
.card-content {
flex: 1;
text-align: center;
font-size: 14px;
color: #999;
padding: 0 20px;
}
.card-num {
font-size: 30px;
}
.card-icon {
font-size: 50px;
width: 100px;
height: 100px;
text-align: center;
line-height: 100px;
color: #fff;
}
.bg1 {
background: #2d8cf0;
}
.bg2 {
background: #64d572;
}
.bg3 {
background: #f25e43;
}
.bg4 {
background: #e9a745;
}
.color1 {
color: #2d8cf0;
}
.color2 {
color: #64d572;
}
.color3 {
color: #f25e43;
}
.color4 {
color: #e9a745;
}
.chart {
width: 100%;
height: 400px;
}
.card-header {
padding-left: 10px;
margin-bottom: 20px;
}
.card-header-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 5px;
}
.card-header-desc {
font-size: 14px;
color: #999;
}
.timeline-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
color: #000;
}
.timeline-time,
.timeline-desc {
font-size: 12px;
color: #787878;
}
.rank-item {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.rank-item-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #f2f2f2;
text-align: center;
line-height: 40px;
margin-right: 10px;
}
.rank-item-content {
flex: 1;
}
.rank-item-top {
display: flex;
justify-content: space-between;
align-items: center;
color: #343434;
margin-bottom: 10px;
}
.rank-item-desc {
font-size: 14px;
color: #999;
}
.map-chart {
width: 100%;
height: 350px;
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<div class="container">
<el-calendar v-model="value">
<template #date-cell="{ data }">
<div>{{ data.date.getDate() }}</div>
<div class="notes-container" v-if="notes[data.day.toString()]">
<div class="notes" v-for="note in notes[data.day.toString()]">
<span :class="note.status === 1 ? 'text-success' : 'text-danger'"></span>
<div class="note-title">{{ note.title }}</div>
</div>
</div>
</template>
</el-calendar>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const today = new Date();
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
const value = ref(today);
const todayDate = today.toISOString().slice(0, 10);
const yesterdayDate = yesterday.toISOString().slice(0, 10);
const notes: any = {
[todayDate]: [
{ title: '吃饭', status: 1 },
{ title: '睡觉', status: 0 },
{ title: '吃饭', status: 1 },
{ title: '睡觉', status: 0 },
{ title: '吃饭', status: 1 },
{ title: '睡觉', status: 0 },
],
[yesterdayDate]: [{ title: '参加会议', status: 0 }],
};
</script>
<style scoped>
.notes-container {
height: 60px;
overflow-y: auto;
}
.notes-container::-webkit-scrollbar {
width: 0;
}
.notes {
display: flex;
align-items: center;
width: 100%;
font-size: 12px;
}
.notes:hover {
background-color: #eee;
}
.note-title {
flex: 1;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.notes span {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 5px;
}
.text-success {
background-color: #5cb85c;
}
.text-danger {
background-color: #d9534f;
}
</style>

View File

@ -0,0 +1,66 @@
<template>
<div>
<el-card class="mgb20">
<template #header>基础用法</template>
<el-carousel height="400px">
<el-carousel-item v-for="item in 4" :key="item">
<h3>{{ item }}</h3>
</el-carousel-item>
</el-carousel>
</el-card>
<el-row :gutter="20">
<el-col :span="12">
<el-card class="mgb20">
<template #header>轮播图</template>
<el-carousel height="300px">
<el-carousel-item v-for="item in imgs" :key="item">
<el-image class="carousel-img" :src="item" fit="cover" />
</el-carousel-item>
</el-carousel>
</el-card>
</el-col>
<el-col :span="12">
<el-card class="mgb20">
<template #header>卡片模式</template>
<el-carousel height="300px" type="card">
<el-carousel-item v-for="item in imgs" :key="item">
<el-image class="carousel-img" :src="item" fit="cover" />
</el-carousel-item>
</el-carousel>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
const imgs = [
'https://cdn.pixabay.com/photo/2017/08/07/08/23/sea-2601374_640.jpg',
'https://cdn.pixabay.com/photo/2020/02/11/10/24/lake-4839058_640.jpg',
'https://cdn.pixabay.com/photo/2024/02/21/08/06/coast-8587004_640.jpg',
'https://cdn.pixabay.com/photo/2023/07/29/10/21/grasshopper-8156626_640.jpg',
];
</script>
<style scoped>
.el-carousel__item h3 {
color: #475669;
line-height: 400px;
margin: 0;
text-align: center;
}
.el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}
.el-carousel__item:nth-child(2n + 1) {
background-color: #d3dce6;
}
.carousel-img {
width: 100%;
height: 100%;
}
</style>

189
src/views/element/form.vue Normal file
View File

@ -0,0 +1,189 @@
<template>
<div class="container">
<el-radio-group class="mgb20" v-model="labelPosition">
<el-radio-button value="left">Left</el-radio-button>
<el-radio-button value="right">Right</el-radio-button>
<el-radio-button value="top">Top</el-radio-button>
</el-radio-group>
<el-form ref="formRef" :rules="rules" :model="form" label-width="120px" :label-position="labelPosition">
<el-row :gutter="50">
<el-col :span="10">
<el-form-item label="文本框" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="数字框" prop="num">
<el-input-number v-model="form.num" :min="1" :max="10" />
</el-form-item>
<el-form-item label="日期选择" prop="date">
<el-date-picker type="date" placeholder="选择日期" v-model="form.date"></el-date-picker>
</el-form-item>
<el-form-item label="时间选择" prop="time">
<el-time-picker placeholder="选择时间" v-model="form.time">
</el-time-picker>
</el-form-item>
<el-form-item label="选择器" prop="region">
<el-select v-model="form.region" placeholder="请选择">
<el-option key="小明" label="小明" value="小明"></el-option>
<el-option key="小红" label="小红" value="小红"></el-option>
<el-option key="小白" label="小白" value="小白"></el-option>
</el-select>
</el-form-item>
<el-form-item label="城市级联" prop="options">
<el-cascader :options="options" v-model="form.options"></el-cascader>
</el-form-item>
<el-form-item label="文本框" prop="desc">
<el-input type="textarea" rows="5" v-model="form.desc"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="评分" prop="rate">
<el-rate v-model="form.rate" allow-half />
</el-form-item>
<el-form-item label="滑块" prop="num">
<el-slider v-model="form.num" :step="1" show-stops :max="10" />
</el-form-item>
<el-form-item label="开关" prop="delivery">
<el-switch v-model="form.delivery"></el-switch>
</el-form-item>
<el-form-item label="颜色选择" prop="color">
<el-color-picker v-model="form.color" />
</el-form-item>
<el-form-item label="多选框" prop="type">
<el-checkbox-group v-model="form.type">
<el-checkbox label="小明" value="小明" name="type"></el-checkbox>
<el-checkbox label="小红" value="小红" name="type"></el-checkbox>
<el-checkbox label="小白" value="小白" name="type"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="单选框" prop="resource">
<el-radio-group v-model="form.resource">
<el-radio label="小明" value="小明"></el-radio>
<el-radio label="小红" value="小红"></el-radio>
<el-radio label="小白" value="小白"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="穿梭框" prop="transfer">
<el-transfer v-model="form.transfer" :data="transferData" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item>
<el-button type="primary" @click="onSubmit(formRef)"></el-button>
<el-button @click="onReset(formRef)"></el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script setup lang="ts" name="forms">
import { reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormProps, FormRules } from 'element-plus';
const labelPosition = ref<FormProps['labelPosition']>('right')
const options = [
{
value: 'guangdong',
label: '广东省',
children: [
{
value: 'guangzhou',
label: '广州市',
children: [
{
value: 'tianhe',
label: '天河区',
},
{
value: 'haizhu',
label: '海珠区',
},
],
},
{
value: 'dongguan',
label: '东莞市',
children: [
{
value: 'changan',
label: '长安镇',
},
{
value: 'humen',
label: '虎门镇',
},
],
},
],
},
{
value: 'hunan',
label: '湖南省',
children: [
{
value: 'changsha',
label: '长沙市',
children: [
{
value: 'yuelu',
label: '岳麓区',
},
],
},
],
},
];
const rules: FormRules = {
name: [{ required: true, message: '请输入表单名称', trigger: 'blur' }],
};
const formRef = ref<FormInstance>();
const form = reactive({
name: '',
region: '',
date: '',
time: '',
delivery: true,
type: ['小明'],
resource: '小红',
desc: '',
options: [],
color: '',
num: 1,
rate: 0,
transfer: [],
});
const generateData = () => {
const data = []
for (let i = 1; i <= 15; i++) {
data.push({
key: i,
label: `Option ${i}`,
disabled: i % 4 === 0,
})
}
return data
}
const transferData = ref(generateData())
//
const onSubmit = (formEl: FormInstance | undefined) => {
//
if (!formEl) return;
formEl.validate((valid) => {
if (valid) {
console.log(form);
ElMessage.success('提交成功!');
} else {
return false;
}
});
};
//
const onReset = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
};
</script>

View File

@ -0,0 +1,340 @@
<template>
<div>
<el-card class="mgb20" shadow="hover">
<template #header>基础用法</template>
<el-row>
<el-col :span="6" style="text-align: center">
<el-statistic title="Daily active users" :value="268500" />
</el-col>
<el-col :span="6" style="text-align: center">
<el-statistic :value="138">
<template #title>
<div style="display: inline-flex; align-items: center">
Ratio of men to women
</div>
</template>
<template #suffix>/100</template>
</el-statistic>
</el-col>
<el-col :span="6" style="text-align: center">
<el-statistic title="数字滚动" :value="outputValue" />
</el-col>
<el-col :span="6" style="text-align: center">
<el-countdown title="倒计时" :value="value" />
</el-col>
</el-row>
</el-card>
<el-card class="mgb20" shadow="hover">
<template #header>CountUp.js</template>
<div class="plugins-tips">
countup.js用于快速创建以更有趣的方式显示数字数据的动画 访问地址
<a href="https://github.com/inorganik/countUp.js" target="_blank">countUp.js</a>
</div>
<el-row>
<el-col :span="8" style="text-align: center">
<p>基础用法</p>
<countup class="countup" :end="6666" />
</el-col>
<el-col :span="8" style="text-align: center">
<p>具体配置</p>
<countup class="countup" :end="8888.5" :options="options" />
</el-col>
<el-col :span="8" style="text-align: center">
<p>更新数值</p>
<countup class="countup" :end="value1" />
</el-col>
</el-row>
</el-card>
<el-card class="mgb20" shadow="never">
<template #header>统计卡片</template>
<el-row :gutter="20" class="mgb20">
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon color1">
<User />
</el-icon>
<div class="card-content text-right">
<el-statistic title="日活跃用户量" :value="268500" />
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon color2">
<ChatDotRound />
</el-icon>
<div class="card-content text-right">
<el-statistic title="系统消息" :value="16800" />
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon color3">
<Goods />
</el-icon>
<div class="card-content text-right">
<el-statistic title="商品数量" :value="8888" />
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon color4">
<ShoppingCartFull />
</el-icon>
<div class="card-content text-right">
<el-statistic title="今日订单量" :value="56888" />
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mgb20">
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<div class="card-content text-left">
<el-statistic :value-style="{ color: '#2d8cf0' }" title="日活跃用户量" :value="268500" />
</div>
<el-icon class="card-icon color1">
<User />
</el-icon>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<div class="card-content text-left">
<el-statistic :value-style="{ color: '#64d572' }" title="系统消息" :value="16800" />
</div>
<el-icon class="card-icon color2">
<ChatDotRound />
</el-icon>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<div class="card-content text-left">
<el-statistic :value-style="{ color: '#f25e43' }" title="商品数量" :value="8888" />
</div>
<el-icon class="card-icon color3">
<Goods />
</el-icon>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<div class="card-content text-left">
<el-statistic :value-style="{ color: '#e9a745' }" title="今日订单量" :value="56888" />
</div>
<el-icon class="card-icon color4">
<ShoppingCartFull />
</el-icon>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mgb20">
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon bg1">
<User />
</el-icon>
<div class="card-content">
<countup class="card-num color1" :end="6666" />
<div>用户访问量</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon bg2">
<ChatDotRound />
</el-icon>
<div class="card-content">
<countup class="card-num color2" :end="168" />
<div>系统消息</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon bg3">
<Goods />
</el-icon>
<div class="card-content">
<countup class="card-num color3" :end="8888" />
<div>商品数量</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body">
<el-icon class="card-icon bg4">
<ShoppingCartFull />
</el-icon>
<div class="card-content">
<countup class="card-num color4" :end="568" />
<div>今日订单量</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mgb20">
<el-col :span="6">
<el-card shadow="hover" body-class="card-body bg1">
<el-icon class="card-icon ">
<User />
</el-icon>
<div class="card-content color0">
<countup class="card-num" :end="6666" />
<div>用户访问量</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body bg2">
<el-icon class="card-icon">
<ChatDotRound />
</el-icon>
<div class="card-content color0">
<countup class="card-num" :end="168" />
<div>系统消息</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body bg3">
<el-icon class="card-icon">
<Goods />
</el-icon>
<div class="card-content color0">
<countup class="card-num " :end="8888" />
<div>商品数量</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-class="card-body bg4">
<el-icon class="card-icon">
<ShoppingCartFull />
</el-icon>
<div class="card-content color0">
<countup class="card-num " :end="568" />
<div>今日订单量</div>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useTransition } from '@vueuse/core'
import countup from '@/components/countup.vue';
const source = ref(0)
const outputValue = useTransition(source, {
duration: 1500,
})
source.value = 172000
const value = ref(Date.now() + 1000 * 60 * 60 * 7)
const value1 = ref(1000);
setTimeout(() => {
value1.value = 8000;
}, 5000);
const options = {
startVal: 1000,
decimalPlaces: 2,
duration: 5,
useGrouping: false,
prefix: '$',
separator: ',',
decimal: '.',
suffix: '',
}
</script>
<style>
.card-body {
display: flex;
align-items: center;
height: 100px;
padding: 0;
}
.bg1 {
background: #2d8cf0;
}
.bg2 {
background: #64d572;
}
.bg3 {
background: #f25e43;
}
.bg4 {
background: #e9a745;
}
</style>
<style scoped>
.countup {
font-size: 24px;
}
.card-content {
flex: 1;
text-align: center;
font-size: 14px;
color: #999;
padding: 0 20px;
}
.card-num {
font-size: 30px;
}
.card-icon {
font-size: 50px;
width: 100px;
height: 100px;
text-align: center;
line-height: 100px;
color: #fff;
}
.color0 {
color: #fff;
}
.color1 {
color: #2d8cf0;
}
.color2 {
color: #64d572;
}
.color3 {
color: #f25e43;
}
.color4 {
color: #e9a745;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<div class="container">
<div class="step-div" v-if="step === 0">
<p>输入注册时的邮箱我们会发送验证码到您的邮箱</p>
<el-input placeholder="请输入邮箱"></el-input>
<el-button class="step-btn" type="primary" @click="step++"></el-button>
</div>
<div class="step-div" v-else-if="step === 1">
<p>验证码已发送至您的邮箱请输入验证码</p>
<el-input placeholder="请输入验证码"></el-input>
<el-button class="step-btn" type="primary" @click="step++"></el-button>
</div>
<div class="step-div" v-else-if="step === 2">
<p>请输入6位以上密码</p>
<el-input placeholder="请输入新密码"></el-input>
<el-button class="step-btn" type="primary" @click="step++"></el-button>
</div>
<div v-else>
<el-result icon="success" title="保存成功" sub-title="退"></el-result>
</div>
<el-steps class="step-style" :active="step" align-center finish-status="success">
<el-step title="Step 1" description="填写邮箱" />
<el-step title="Step 2" description="填写验证码" />
<el-step title="Step 3" description="修改密码" />
</el-steps>
<el-steps class="step-style" :active="step" finish-status="success" simple>
<el-step title="填写邮箱" />
<el-step title="填写验证码" />
<el-step title="修改密码" />
</el-steps>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const step = ref(0)
</script>
<style scoped>
.step-div {
max-width: 500px;
margin: 0 auto;
}
.step-div p {
margin-bottom: 20px;
color: #787878;
}
.step-btn {
display: block;
width: 100%;
margin: 20px 0;
}
.step-style {
max-width: 800px;
margin: 40px auto;
}
</style>

116
src/views/element/tabs.vue Normal file
View File

@ -0,0 +1,116 @@
<template>
<el-tabs v-model="message" type="card">
<el-tab-pane :label="`未读消息(${state.unread.length})`" name="first">
<el-table :data="state.unread" :show-header="false" style="width: 100%">
<el-table-column>
<template #default="scope">
<span class="message-title">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column prop="date" width="180"></el-table-column>
<el-table-column width="120">
<template #default="scope">
<el-button size="small" @click="handleRead(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
<div class="handle-row">
<el-button type="primary">全部标为已读</el-button>
</div>
</el-tab-pane>
<el-tab-pane :label="`已读消息(${state.read.length})`" name="second">
<template v-if="message === 'second'">
<el-table :data="state.read" :show-header="false" style="width: 100%">
<el-table-column>
<template #default="scope">
<span class="message-title">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column prop="date" width="180"></el-table-column>
<el-table-column width="120">
<template #default="scope">
<el-button type="danger" size="small" @click="handleDel(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
<div class="handle-row">
<el-button type="danger">删除全部</el-button>
</div>
</template>
</el-tab-pane>
<el-tab-pane :label="`回收站(${state.recycle.length})`" name="third">
<template v-if="message === 'third'">
<el-table :data="state.recycle" :show-header="false" style="width: 100%">
<el-table-column>
<template #default="scope">
<span class="message-title">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column prop="date" width="180"></el-table-column>
<el-table-column width="120">
<template #default="scope">
<el-button size="small" @click="handleRestore(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
<div class="handle-row">
<el-button type="danger">清空回收站</el-button>
</div>
</template>
</el-tab-pane>
</el-tabs>
</template>
<script setup lang="ts" name="tabs">
import { ref, reactive } from 'vue';
const message = ref('first');
const state = reactive({
unread: [
{
date: '2018-04-19 20:00:00',
title: '【系统通知】该系统将于今晚凌晨2点到5点进行升级维护'
},
{
date: '2018-04-19 21:00:00',
title: '今晚12点整发大红包先到先得'
}
],
read: [
{
date: '2018-04-19 20:00:00',
title: '【系统通知】该系统将于今晚凌晨2点到5点进行升级维护'
}
],
recycle: [
{
date: '2018-04-19 20:00:00',
title: '【系统通知】该系统将于今晚凌晨2点到5点进行升级维护'
}
]
});
const handleRead = (index: number) => {
const item = state.unread.splice(index, 1);
state.read = item.concat(state.read);
};
const handleDel = (index: number) => {
const item = state.read.splice(index, 1);
state.recycle = item.concat(state.recycle);
};
const handleRestore = (index: number) => {
const item = state.recycle.splice(index, 1);
state.read = item.concat(state.read);
};
</script>
<style>
.message-title {
cursor: pointer;
color: var(--el-color-primary);
}
.handle-row {
margin-top: 30px;
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="container">
<el-button type="primary" @click="open = true">开始引导</el-button>
<el-divider />
<el-space>
<el-button ref="ref1">上传</el-button>
<el-button ref="ref2" type="primary">保存</el-button>
<el-button ref="ref3" :icon="MoreFilled" />
</el-space>
<el-tour v-model="open">
<el-tour-step :target="ref1?.$el" title="上传文件">
<img style="width: 120px" src="../../assets/img/img.jpg" alt="tour.png" />
<div>点击这里选择文件</div>
</el-tour-step>
<el-tour-step :target="ref2?.$el" title="保存" description="点击进行上传" />
<el-tour-step :target="ref3?.$el" title="更多操作" description="点击查看更多操作" />
</el-tour>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { MoreFilled } from '@element-plus/icons-vue'
const ref1 = ref()
const ref2 = ref()
const ref3 = ref()
const open = ref(false)
</script>

View File

@ -0,0 +1,44 @@
<template>
<div class="container">
<div class="content-title">支持拖拽</div>
<div class="plugins-tips">
Element Plus自带上传组件 访问地址
<a href="https://element-plus.org/zh-CN/component/upload.html" target="_blank">Element Plus Upload</a>
</div>
<el-upload class="upload-demo" drag action="http://jsonplaceholder.typicode.com/api/posts/" multiple
:on-change="handle">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
</el-upload>
<div class="content-title">支持裁剪</div>
<div class="plugins-tips">
vue-cropper一个简单的vue图片裁剪插件 访问地址
<a href="https://github.com/xyxiao001/vue-cropper" target="_blank">vue-cropper</a> 示例请查看
<router-link to="/ucenter">个人中心-我的头像</router-link>
</div>
</div>
</template>
<script setup lang="ts">
const handle = (rawFile: any) => {
console.log(rawFile);
};
</script>
<style scoped>
.content-title {
font-weight: 400;
line-height: 50px;
margin: 10px 0;
font-size: 22px;
color: #1f2f3d;
}
.upload-demo {
width: 360px;
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<div class="container">
<el-row :gutter="20">
<el-col :span="18">
<el-watermark :content="config.content" :font="config.font" :z-index="config.zIndex"
:rotate="config.rotate" :gap="config.gap" :offset="config.offset">
<div style="height: 600px" />
</el-watermark>
</el-col>
<el-col :span="6">
<el-form class="form" :model="config" label-position="top" label-width="50px">
<el-form-item label="Content">
<el-input v-model="config.content" />
</el-form-item>
<el-form-item label="Color">
<el-color-picker v-model="config.font.color" show-alpha />
</el-form-item>
<el-form-item label="FontSize">
<el-slider v-model="config.font.fontSize" />
</el-form-item>
<el-form-item label="zIndex">
<el-slider v-model="config.zIndex" />
</el-form-item>
<el-form-item label="Rotate">
<el-slider v-model="config.rotate" :min="-180" :max="180" />
</el-form-item>
<el-form-item label="Gap">
<el-space>
<el-input-number v-model="config.gap[0]" controls-position="right" />
<el-input-number v-model="config.gap[1]" controls-position="right" />
</el-space>
</el-form-item>
<el-form-item label="Offset">
<el-space>
<el-input-number v-model="config.offset[0]" placeholder="offsetLeft"
controls-position="right" />
<el-input-number v-model="config.offset[1]" placeholder="offsetTop"
controls-position="right" />
</el-space>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const config = reactive({
content: 'vue-manage-system',
font: {
fontSize: 16,
color: 'rgba(0, 0, 0, 0.15)',
},
zIndex: -1,
rotate: -22,
gap: [100, 100] as [number, number],
offset: [] as unknown as [number, number],
})
</script>

63
src/views/home.vue Normal file
View File

@ -0,0 +1,63 @@
<template>
<div class="wrapper">
<v-header />
<v-sidebar />
<div class="content-box" :class="{ 'content-collapse': sidebar.collapse }">
<v-tabs></v-tabs>
<div class="content">
<router-view v-slot="{ Component }">
<transition name="move" mode="out-in">
<keep-alive :include="tabs.nameList">
<component :is="Component"></component>
</keep-alive>
</transition>
</router-view>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useSidebarStore } from '@/store/sidebar';
import { useTabsStore } from '@/store/tabs';
import vHeader from '@/components/header.vue';
import vSidebar from '@/components/sidebar.vue';
import vTabs from '@/components/tabs.vue';
const sidebar = useSidebarStore();
const tabs = useTabsStore();
</script>
<style>
.wrapper {
height: 100vh;
overflow: hidden;
}
.content-box {
position: absolute;
left: 250px;
right: 0;
top: 70px;
bottom: 0;
padding-bottom: 30px;
-webkit-transition: left 0.3s ease-in-out;
transition: left 0.3s ease-in-out;
background: #eef0fc;
overflow: hidden;
}
.content {
width: auto;
height: 100%;
padding: 20px;
overflow-y: scroll;
box-sizing: border-box;
}
.content::-webkit-scrollbar {
width: 0;
}
.content-collapse {
left: 65px;
}
</style>

67
src/views/pages/403.vue Normal file
View File

@ -0,0 +1,67 @@
<template>
<div class="error-page">
<div class="error-box">
<div class="error-code">403</div>
<div class="error-desc">啊哦~ 你没有权限访问该页面哦</div>
<div class="error-handle">
<router-link to="/">
<el-button type="primary" size="large">返回首页</el-button>
</router-link>
<el-button class="error-btn" size="large" @click="goBack"></el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="403">
import { useRouter } from 'vue-router';
const router = useRouter();
const goBack = () => {
router.go(-2);
};
</script>
<style scoped>
.error-page {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
height: 100vh;
background: #eef0fc;
box-sizing: border-box;
}
.error-box {
width: 400px;
background-color: #fff;
padding: 80px 50px;
border-radius: 5px;
}
.error-code {
line-height: 1;
font-size: 100px;
font-weight: bold;
color: var(--el-color-primary);
margin-bottom: 20px;
text-align: center;
}
.error-desc {
font-size: 20px;
color: #777;
text-align: center;
}
.error-handle {
margin-top: 50px;
text-align: center;
}
.error-btn {
margin-left: 100px;
}
</style>

67
src/views/pages/404.vue Normal file
View File

@ -0,0 +1,67 @@
<template>
<div class="error-page">
<div class="error-box">
<div class="error-code">404</div>
<div class="error-desc">啊哦~ 你所访问的页面不存在</div>
<div class="error-handle">
<router-link to="/">
<el-button type="primary" size="large">返回首页</el-button>
</router-link>
<el-button class="error-btn" size="large" @click="goBack"></el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="404">
import { useRouter } from 'vue-router';
const router = useRouter();
const goBack = () => {
router.go(-1);
};
</script>
<style scoped>
.error-page {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
height: 100vh;
background: #eef0fc;
box-sizing: border-box;
}
.error-box {
width: 400px;
background-color: #fff;
padding: 80px 50px;
border-radius: 5px;
}
.error-code {
line-height: 1;
font-size: 100px;
font-weight: bold;
color: var(--el-color-primary);
margin-bottom: 20px;
text-align: center;
}
.error-desc {
font-size: 20px;
color: #777;
text-align: center;
}
.error-handle {
margin-top: 50px;
text-align: center;
}
.error-btn {
margin-left: 100px;
}
</style>

View File

@ -0,0 +1,55 @@
<template>
<div class="container">
<div class="plugins-tips">
wangEditor轻量级 web 富文本编辑器配置方便使用简单 访问地址
<a href="https://www.wangeditor.com/doc/" target="_blank">wangEditor</a>
</div>
<div style="border: 1px solid #ccc; margin-bottom: 10px">
<Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig" />
<Editor
style="height: 500px; overflow-y: hidden"
v-model="valueHtml"
:defaultConfig="editorConfig"
@onCreated="handleCreated"
/>
</div>
<el-button type="primary" @click="syncHTML"></el-button>
</div>
</template>
<script setup lang="ts" name="editor">
import '@wangeditor/editor/dist/css/style.css'; // css
import { onBeforeUnmount, ref, reactive, shallowRef, onMounted } from 'vue';
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
// shallowRef
const editorRef = shallowRef();
// HTML
const valueHtml = ref('<p>hello</p>');
// ajax
onMounted(() => {
setTimeout(() => {
valueHtml.value = '<p>模拟 Ajax 异步设置内容</p>';
}, 1500);
});
const toolbarConfig = {};
const editorConfig = { placeholder: '请输入内容...' };
//
onBeforeUnmount(() => {
const editor = editorRef.value;
if (editor == null) return;
editor.destroy();
});
const handleCreated = (editor: any) => {
editorRef.value = editor; // editor
};
const syncHTML = () => {
console.log(valueHtml.value);
};
</script>
<style></style>

257
src/views/pages/icon.vue Normal file
View File

@ -0,0 +1,257 @@
<template>
<el-tabs type="border-card">
<el-tab-pane label="自定义图标">
<h2>使用方法</h2>
<p style="line-height: 50px">
直接通过设置类名为 el-icon-lx-iconName 来使用即可例如{{ iconList.length }}个图标
</p>
<p class="example-p">
<i class="el-icon-lx-redpacket_fill" style="font-size: 30px; color: #ff5900"></i>
<span>&lt;i class=&quot;el-icon-lx-redpacket_fill&quot;&gt;&lt;/i&gt;</span>
</p>
<p class="example-p">
<i class="el-icon-lx-weibo" style="font-size: 30px; color: #fd5656"></i>
<span>&lt;i class=&quot;el-icon-lx-weibo&quot;&gt;&lt;/i&gt;</span>
</p>
<p class="example-p">
<i class="el-icon-lx-emojifill" style="font-size: 30px; color: #ffc300"></i>
<span>&lt;i class=&quot;el-icon-lx-emojifill&quot;&gt;&lt;/i&gt;</span>
</p>
<br />
<h2>图标</h2>
<div class="search-box">
<el-input class="search" size="large" v-model="keyword" clearable placeholder="请输入图标名称"></el-input>
</div>
<ul>
<li class="icon-li" v-for="(item, index) in list" :key="index">
<div class="icon-li-content">
<i :class="`el-icon-lx-${item}`"></i>
<span>{{ item }}</span>
</div>
</li>
</ul>
</el-tab-pane>
<el-tab-pane label="Element图标">
<el-link type="primary" href="https://element-plus.org/zh-CN/component/icon.html#icon-collection"
target="_blank">前往官方文档查看</el-link>
</el-tab-pane>
</el-tabs>
</template>
<script setup lang="ts" name="icon">
import { computed, ref } from 'vue';
const iconList: Array<string> = [
'attentionforbid',
'attentionforbidfill',
'attention',
'attentionfill',
'tag',
'tagfill',
'people',
'peoplefill',
'notice',
'noticefill',
'mobile',
'mobilefill',
'voice',
'voicefill',
'unlock',
'lock',
'home',
'homefill',
'delete',
'deletefill',
'notification',
'notificationfill',
'notificationforbidfill',
'like',
'likefill',
'comment',
'commentfill',
'camera',
'camerafill',
'warn',
'warnfill',
'time',
'timefill',
'location',
'locationfill',
'favor',
'favorfill',
'skin',
'skinfill',
'news',
'newsfill',
'record',
'recordfill',
'emoji',
'emojifill',
'message',
'messagefill',
'goods',
'goodsfill',
'crown',
'crownfill',
'move',
'add',
'hot',
'hotfill',
'service',
'servicefill',
'present',
'presentfill',
'pic',
'picfill',
'rank',
'rankfill',
'male',
'female',
'down',
'top',
'recharge',
'rechargefill',
'forward',
'forwardfill',
'info',
'infofill',
'redpacket',
'redpacket_fill',
'roundadd',
'roundaddfill',
'friendadd',
'friendaddfill',
'cart',
'cartfill',
'more',
'moreandroid',
'back',
'right',
'shop',
'shopfill',
'question',
'questionfill',
'roundclose',
'roundclosefill',
'roundcheck',
'roundcheckfill',
'global',
'mail',
'punch',
'exit',
'upload',
'read',
'file',
'link',
'full',
'group',
'friend',
'profile',
'addressbook',
'calendar',
'text',
'copy',
'share',
'wifi',
'vipcard',
'weibo',
'remind',
'refresh',
'filter',
'settings',
'scan',
'qrcode',
'cascades',
'apps',
'sort',
'searchlist',
'search',
'edit',
'apple-line',
'baidu-fill',
'amazon-fill',
'netease-cloud-music-fill',
'qq-line',
'wechat-fill',
'alipay-fill',
'android-fill',
'android-line',
'whatsapp-line',
'whatsapp-fill',
'bilibili-fill',
'chrome-fill',
'dingding-fill',
'dingding-line',
'apple-fill',
'github-fill',
'qq-fill',
'wechat-pay-fill',
'windows-line',
'windows-fill',
'youtube-line',
'youtube-fill',
'wechat-pay-line',
'zhihu-line'
];
const keyword = ref('');
const list = computed(() => {
return iconList.filter(item => {
return item.indexOf(keyword.value) !== -1;
});
});
</script>
<style scoped>
.example-p {
height: 45px;
display: flex;
align-items: center;
}
.search-box {
text-align: center;
margin-top: 10px;
}
.search {
width: 300px;
}
ul,
li {
list-style: none;
}
.icon-li {
display: inline-block;
padding: 10px;
width: 120px;
height: 120px;
}
.icon-li-content {
display: flex;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}
.icon-li-content i {
font-size: 36px;
color: #606266;
}
.icon-li-content span {
margin-top: 10px;
color: #787878;
}
.iframe {
width: 100%;
height: 700px;
}
</style>

171
src/views/pages/login.vue Normal file
View File

@ -0,0 +1,171 @@
<template>
<div class="login-bg">
<div class="login-container">
<div class="login-header">
<img class="logo mr10" src="../../assets/img/logo.svg" alt="" />
<div class="login-title">后台管理系统</div>
</div>
<el-form :model="param" :rules="rules" ref="login" size="large">
<el-form-item prop="username">
<el-input v-model="param.username" placeholder="用户名">
<template #prepend>
<el-icon>
<User />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
type="password"
placeholder="密码"
v-model="param.password"
@keyup.enter="submitForm(login)"
>
<template #prepend>
<el-icon>
<Lock />
</el-icon>
</template>
</el-input>
</el-form-item>
<div class="pwd-tips">
<el-checkbox class="pwd-checkbox" v-model="checked" label="记住密码" />
<el-link type="primary" @click="$router.push('/reset-pwd')"></el-link>
</div>
<el-button class="login-btn" type="primary" size="large" @click="submitForm(login)"></el-button>
<p class="login-tips">Tips : 用户名和密码随便填</p>
<p class="login-text">
没有账号<el-link type="primary" @click="$router.push('/register')"></el-link>
</p>
</el-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { useTabsStore } from '@/store/tabs';
import { usePermissStore } from '@/store/permiss';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
interface LoginInfo {
username: string;
password: string;
}
const lgStr = localStorage.getItem('login-param');
const defParam = lgStr ? JSON.parse(lgStr) : null;
const checked = ref(lgStr ? true : false);
const router = useRouter();
const param = reactive<LoginInfo>({
username: defParam ? defParam.username : '',
password: defParam ? defParam.password : '',
});
const rules: FormRules = {
username: [
{
required: true,
message: '请输入用户名',
trigger: 'blur',
},
],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
};
const permiss = usePermissStore();
const login = ref<FormInstance>();
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate((valid: boolean) => {
if (valid) {
ElMessage.success('登录成功');
localStorage.setItem('vuems_name', param.username);
const keys = permiss.defaultList[param.username == 'admin' ? 'admin' : 'user'];
permiss.handleSet(keys);
router.push('/');
if (checked.value) {
localStorage.setItem('login-param', JSON.stringify(param));
} else {
localStorage.removeItem('login-param');
}
} else {
ElMessage.error('登录失败');
return false;
}
});
};
const tabs = useTabsStore();
tabs.clearTabs();
</script>
<style scoped>
.login-bg {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
background: url(../../assets/img/login-bg.jpg) center/cover no-repeat;
}
.login-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40px;
}
.logo {
width: 35px;
}
.login-title {
font-size: 22px;
color: #333;
font-weight: bold;
}
.login-container {
width: 450px;
border-radius: 5px;
background: #fff;
padding: 40px 50px 50px;
box-sizing: border-box;
}
.pwd-tips {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
margin: -10px 0 10px;
color: #787878;
}
.pwd-checkbox {
height: auto;
}
.login-btn {
display: block;
width: 100%;
}
.login-tips {
font-size: 12px;
color: #999;
}
.login-text {
display: flex;
align-items: center;
margin-top: 20px;
font-size: 14px;
color: #787878;
}
</style>

View File

@ -0,0 +1,21 @@
<template>
<div class="container">
<div class="plugins-tips">
md-editor-v3vue3版本的 markdown 编辑器配置丰富请详看文档 访问地址
<a href="https://imzbf.github.io/md-editor-v3/index" target="_blank">md-editor-v3</a>
</div>
<md-editor class="mgb20" v-model="text" @on-upload-img="onUploadImg" />
<el-button type="primary">提交</el-button>
</div>
</template>
<script setup lang="ts" name="md">
import { ref } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
const text = ref('Hello Editor!');
const onUploadImg = (files: any) => {
console.log(files);
};
</script>

Some files were not shown because too many files have changed in this diff Show More