This commit is contained in:
Gitea 2023-04-28 11:07:53 +08:00
parent c515fbc428
commit 8570aa1893
48 changed files with 4825 additions and 3 deletions

23
.gitignore vendored
View File

@ -9,3 +9,26 @@ docs/_book
# TODO: where does this rule come from?
test/
.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?

138
README.md
View File

@ -1,3 +1,137 @@
# vue-manage-sys
# vue-manage-system
基于 Vue3 + pinia + Element Plus 的后台管理系统解决方案
<a href="https://github.com/vuejs/vue">
<img src="https://img.shields.io/badge/vue-3.1.2-brightgreen.svg" alt="vue">
</a>
<a href="https://github.com/vuejs/pinia">
<img src="https://img.shields.io/badge/pinia-2.0.14-brightgreen.svg" alt="pinia">
</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>
基于 Vue3 + pinia + Element Plus 的后台管理系统解决方案。[线上地址](https://lin-xin.gitee.io/example/work/)
> Vue2 版本请看 [tag-V4.2.0](https://github.com/lin-xin/vue-manage-system/tree/V4.2.0)
[English document](https://github.com/lin-xin/manage-system/blob/master/README_EN.md)
## 赞助商
### 好问
[<img src="https://static.bestqa.net/logo/bestqa_haowen.png" width="220" height="100">](https://www.bestqa.net/home/index.html)
专业问卷服务,一对一客服,按需定制
## 支持作者
请作者喝杯咖啡吧!(微信号linxin_20)
![微信扫一扫](https://lin-xin.gitee.io/images/weixin.jpg)
## 前言
该方案作为一套多功能的后台框架模板,适用于绝大部分的后台管理系统开发。基于 Vue3 + pinia + typescript引用 Element Plus 组件库,方便开发。实现逻辑简单,适合外包项目,快速交付。
## 功能
- [x] Element Plus
- [x] vite 3
- [x] pinia
- [x] typescript
- [x] 登录/注销
- [x] Dashboard
- [x] 表格
- [x] Tab 选项卡
- [x] 表单
- [x] 图表 :bar_chart:
- [x] 富文本/markdown编辑器
- [x] 图片拖拽/裁剪上传
- [x] 权限管理
- [x] 三级菜单
- [x] 自定义图标
## 安装步骤
> 因为使用vite3node版本需要 14.18+
```
git clone https://github.com/lin-xin/vue-manage-system.git // 把模板下载到本地
cd vue-manage-system // 进入模板目录
npm install // 安装项目依赖,等待安装完成之后,安装失败可用 cnpm 或 yarn
// 运行
npm run dev
// 执行构建命令生成的dist文件夹放在服务器下即可访问
npm run build
```
## 组件使用说明与演示
### vue-schart
vue.js 封装 sChart.js 的图表组件。访问地址:[vue-schart](https://github.com/lin-xin/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>
```html
<template>
<div>
<schart class="wrapper" canvasId="myCanvas" :options="options"></schart>
</div>
</template>
<script setup lang="ts">
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],
},
{
label: "百货",
data: [164, 178, 190, 135, 160],
},
{
label: "食品",
data: [144, 198, 150, 235, 120],
},
],
})
</script>
<style>
.wrapper {
width: 7rem;
height: 5rem;
}
</style>
```
## 项目截图
### 登录
![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms3.png)
### 首页
![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms1.png)
## License
[MIT](https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE)

119
README_EN.md Normal file
View File

@ -0,0 +1,119 @@
# 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](https://lin-xin.gitee.io/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 Vue3 and ElementPlus. Color separation of color style, support manual switch themes, and it is convenient to use a custom theme color.
## Function
- [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
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
npm run dev
## Constructing production
// Constructing project
npm run build
## Component description and presentation
### vue-schart
Vue.js wrapper for sChart.js. Github : [vue-schart](https://github.com/lin-xin/vue-schart#/)
```html
<template>
<div>
<schart class="wrapper" canvasId="myCanvas" :options="options"></schart>
</div>
</template>
<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],
},
{
label: "百货",
data: [164, 178, 190, 135, 160],
},
{
label: "食品",
data: [144, 198, 150, 235, 120],
},
],
})
</script>
<style>
.wrapper {
width: 7rem;
height: 5rem;
}
</style>
```
## Screenshot
### Default theme
![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms1.png)
### Login
![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 {
}

53
components.d.ts vendored Normal file
View File

@ -0,0 +1,53 @@
// 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 {
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
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']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
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']
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']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
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']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload']
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']
Tags: typeof import('./src/components/tags.vue')['default']
}
}

22
index.html Normal file
View File

@ -0,0 +1,22 @@
<!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="https://at.alicdn.com/t/font_830376_qzecyukz0s.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>

1298
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "vue-manage-system",
"version": "5.3.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.0.9",
"axios": "^0.27.2",
"element-plus": "^2.2.14",
"md-editor-v3": "^2.2.1",
"pinia": "^2.0.20",
"vue": "^3.2.37",
"vue-cropperjs": "^5.0.0",
"vue-router": "^4.1.3",
"vue-schart": "^2.0.0",
"wangeditor": "^4.7.15",
"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"
]
}

40
public/table.json Normal file
View File

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

BIN
public/template.xlsx Normal file

Binary file not shown.

14
src/App.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<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';
</script>
<style>
@import './assets/css/main.css';
@import './assets/css/color-dark.css';
</style>

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

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

View File

@ -0,0 +1,23 @@
.header{
background-color: #242f42;
}
.login-wrap{
background: #324157;
}
.plugins-tips{
background: #eef1f6;
}
.plugins-tips a{
color: #20a0ff;
}
.tags-li.active {
border: 1px solid #409EFF;
background-color: #409EFF;
}
.message-title{
color: #20a0ff;
}
.collapse-btn:hover{
background: rgb(40,52,70);
}

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;
}

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

@ -0,0 +1,137 @@
* {
margin: 0;
padding: 0;
}
html,
body,
#app,
.wrapper {
width: 100%;
height: 100%;
overflow: hidden;
}
body {
font-family: 'PingFang SC', "Helvetica Neue", Helvetica, "microsoft yahei", arial, STHeiTi, sans-serif;
}
a {
text-decoration: none
}
.content-box {
position: absolute;
left: 250px;
right: 0;
top: 70px;
bottom: 0;
padding-bottom: 30px;
-webkit-transition: left .3s ease-in-out;
transition: left .3s ease-in-out;
background: #f0f0f0;
}
.content {
width: auto;
height: 100%;
padding: 10px;
overflow-y: scroll;
box-sizing: border-box;
}
.content-collapse {
left: 65px;
}
.container {
padding: 30px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
}
.crumbs {
margin: 10px 0;
}
.el-table th {
background-color: #f5f7fa !important;
}
.pagination {
margin: 20px 0;
text-align: right;
}
.plugins-tips {
padding: 20px 10px;
margin-bottom: 20px;
}
.el-button+.el-tooltip {
margin-left: 10px;
}
.el-table tr:hover {
background: #f6faff;
}
.mgb20 {
margin-bottom: 20px;
}
.move-enter-active,
.move-leave-active {
transition: opacity .1s ease;
}
.move-enter-from,
.move-leave-to {
opacity: 0;
}
/*BaseForm*/
.form-box {
width: 600px;
}
.form-box .line {
text-align: center;
}
.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;
}
[class*=" el-icon-"], [class^=el-icon-] {
speak: none;
font-style: normal;
font-weight: 400;
font-variant: normal;
text-transform: none;
line-height: 1;
vertical-align: baseline;
display: inline-block;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.el-sub-menu [class^=el-icon-] {
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
font-size: 18px;
}
[hidden]{
display: none !important;
}

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

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

@ -0,0 +1,154 @@
<template>
<div class="header">
<!-- 折叠按钮 -->
<div class="collapse-btn" @click="collapseChage">
<el-icon v-if="sidebar.collapse"><Expand /></el-icon>
<el-icon v-else><Fold /></el-icon>
</div>
<div class="logo">后台管理系统</div>
<div class="header-right">
<div class="header-user-con">
<!-- 消息中心 -->
<div class="btn-bell" @click="router.push('/tabs')">
<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>
<!-- 用户头像 -->
<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>
<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('ms_username');
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('ms_username');
router.push('/login');
} else if (command == 'user') {
router.push('/user');
}
};
</script>
<style scoped>
.header {
position: relative;
box-sizing: border-box;
width: 100%;
height: 70px;
font-size: 22px;
color: #fff;
}
.collapse-btn {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
float: left;
padding: 0 21px;
cursor: pointer;
}
.header .logo {
float: left;
width: 250px;
line-height: 70px;
}
.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-bell,
.btn-fullscreen {
position: relative;
width: 30px;
height: 30px;
text-align: center;
border-radius: 15px;
cursor: pointer;
display: flex;
align-items: center;
}
.btn-bell-badge {
position: absolute;
right: 4px;
top: 0px;
width: 8px;
height: 8px;
border-radius: 4px;
background: #f56c6c;
color: #fff;
}
.btn-bell .el-icon-lx-notice {
color: #fff;
}
.user-name {
margin-left: 10px;
}
.user-avator {
margin-left: 20px;
}
.el-dropdown-link {
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
}
.el-dropdown-menu__item {
text-align: center;
}
</style>

181
src/components/sidebar.vue Normal file
View File

@ -0,0 +1,181 @@
<template>
<div class="sidebar">
<el-menu
class="sidebar-el-menu"
:default-active="onRoutes"
:collapse="sidebar.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-sub-menu :index="item.index" :key="item.index" v-permiss="item.permiss">
<template #title>
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<span>{{ item.title }}</span>
</template>
<template v-for="subItem in item.subs">
<el-sub-menu
v-if="subItem.subs"
:index="subItem.index"
:key="subItem.index"
v-permiss="item.permiss"
>
<template #title>{{ subItem.title }}</template>
<el-menu-item v-for="(threeItem, i) in subItem.subs" :key="i" :index="threeItem.index">
{{ threeItem.title }}
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="subItem.index" v-permiss="item.permiss">
{{ 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.permiss">
<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';
const items = [
{
icon: 'Odometer',
index: '/dashboard',
title: '系统首页',
permiss: '1',
},
{
icon: 'Calendar',
index: '1',
title: '表格相关',
permiss: '2',
subs: [
{
index: '/table',
title: '常用表格',
permiss: '2',
},
{
index: '/import',
title: '导入Excel',
permiss: '2',
},
{
index: '/export',
title: '导出Excel',
permiss: '2',
},
],
},
{
icon: 'DocumentCopy',
index: '/tabs',
title: 'tab选项卡',
permiss: '3',
},
{
icon: 'Edit',
index: '3',
title: '表单相关',
permiss: '4',
subs: [
{
index: '/form',
title: '基本表单',
permiss: '5',
},
{
index: '/upload',
title: '文件上传',
permiss: '6',
},
{
index: '4',
title: '三级菜单',
permiss: '7',
subs: [
{
index: '/editor',
title: '富文本编辑器',
permiss: '8',
},
{
index: '/markdown',
title: 'markdown编辑器',
permiss: '9',
},
],
},
],
},
{
icon: 'Setting',
index: '/icon',
title: '自定义图标',
permiss: '10',
},
{
icon: 'PieChart',
index: '/charts',
title: 'schart图表',
permiss: '11',
},
{
icon: 'Warning',
index: '/permission',
title: '权限管理',
permiss: '13',
},
{
icon: 'CoffeeCup',
index: '/donate',
title: '支持作者',
permiss: '14',
},
];
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 > ul {
height: 100%;
}
</style>

168
src/components/tags.vue Normal file
View File

@ -0,0 +1,168 @@
<template>
<div class="tags" v-if="tags.show">
<ul>
<li
class="tags-li"
v-for="(item, index) in tags.list"
:class="{ active: isActive(item.path) }"
:key="index"
>
<router-link :to="item.path" class="tags-li-title">{{ item.title }}</router-link>
<el-icon @click="closeTags(index)"><Close /></el-icon>
</li>
</ul>
<div class="tags-close-box">
<el-dropdown @command="handleTags">
<el-button size="small" type="primary">
标签选项
<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="all">关闭所有</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup lang="ts">
import { useTagsStore } from '../store/tags';
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const isActive = (path: string) => {
return path === route.fullPath;
};
const tags = useTagsStore();
//
const closeTags = (index: number) => {
const delItem = tags.list[index];
tags.delTagsItem(index);
const item = tags.list[index] ? tags.list[index] : tags.list[index - 1];
if (item) {
delItem.path === route.fullPath && router.push(item.path);
} else {
router.push('/');
}
};
//
const setTags = (route: any) => {
const isExist = tags.list.some(item => {
return item.path === route.fullPath;
});
if (!isExist) {
if (tags.list.length >= 8) tags.delTagsItem(0);
tags.setTagsItem({
name: route.name,
title: route.meta.title,
path: route.fullPath
});
}
};
setTags(route);
onBeforeRouteUpdate(to => {
setTags(to);
});
//
const closeAll = () => {
tags.clearTags();
router.push('/');
};
//
const closeOther = () => {
const curItem = tags.list.filter(item => {
return item.path === route.fullPath;
});
tags.closeTagsOther(curItem);
};
const handleTags = (command: string) => {
command === 'other' ? closeOther() : closeAll();
};
//
// tags.closeCurrentTag({
// $router: router,
// $route: route
// });
</script>
<style>
.tags {
position: relative;
height: 30px;
overflow: hidden;
background: #fff;
padding-right: 120px;
box-shadow: 0 5px 10px #ddd;
}
.tags ul {
box-sizing: border-box;
width: 100%;
height: 100%;
}
.tags-li {
display: flex;
align-items: center;
float: left;
margin: 3px 5px 2px 3px;
border-radius: 3px;
font-size: 12px;
overflow: hidden;
cursor: pointer;
height: 23px;
border: 1px solid #e9eaec;
background: #fff;
padding: 0 5px 0 12px;
color: #666;
-webkit-transition: all 0.3s ease-in;
-moz-transition: all 0.3s ease-in;
transition: all 0.3s ease-in;
}
.tags-li:not(.active):hover {
background: #f8f8f8;
}
.tags-li.active {
color: #fff;
}
.tags-li-title {
float: left;
max-width: 80px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 5px;
color: #666;
}
.tags-li.active .tags-li-title {
color: #fff;
}
.tags-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>

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 (!permiss.key.includes(String(binding.value))) {
el['hidden'] = true;
}
},
});
app.mount('#app');

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

@ -0,0 +1,179 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import { usePermissStore } from '../store/permiss';
import Home from '../views/home.vue';
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/dashboard',
},
{
path: '/',
name: 'Home',
component: Home,
children: [
{
path: '/dashboard',
name: 'dashboard',
meta: {
title: '系统首页',
permiss: '1',
},
component: () => import(/* webpackChunkName: "dashboard" */ '../views/dashboard.vue'),
},
{
path: '/table',
name: 'basetable',
meta: {
title: '表格',
permiss: '2',
},
component: () => import(/* webpackChunkName: "table" */ '../views/table.vue'),
},
{
path: '/charts',
name: 'basecharts',
meta: {
title: '图表',
permiss: '11',
},
component: () => import(/* webpackChunkName: "charts" */ '../views/charts.vue'),
},
{
path: '/form',
name: 'baseform',
meta: {
title: '表单',
permiss: '5',
},
component: () => import(/* webpackChunkName: "form" */ '../views/form.vue'),
},
{
path: '/tabs',
name: 'tabs',
meta: {
title: 'tab标签',
permiss: '3',
},
component: () => import(/* webpackChunkName: "tabs" */ '../views/tabs.vue'),
},
{
path: '/donate',
name: 'donate',
meta: {
title: '鼓励作者',
permiss: '14',
},
component: () => import(/* webpackChunkName: "donate" */ '../views/donate.vue'),
},
{
path: '/permission',
name: 'permission',
meta: {
title: '权限管理',
permiss: '13',
},
component: () => import(/* webpackChunkName: "permission" */ '../views/permission.vue'),
},
{
path: '/upload',
name: 'upload',
meta: {
title: '上传插件',
permiss: '6',
},
component: () => import(/* webpackChunkName: "upload" */ '../views/upload.vue'),
},
{
path: '/icon',
name: 'icon',
meta: {
title: '自定义图标',
permiss: '10',
},
component: () => import(/* webpackChunkName: "icon" */ '../views/icon.vue'),
},
{
path: '/user',
name: 'user',
meta: {
title: '个人中心',
},
component: () => import(/* webpackChunkName: "user" */ '../views/user.vue'),
},
{
path: '/editor',
name: 'editor',
meta: {
title: '富文本编辑器',
permiss: '8',
},
component: () => import(/* webpackChunkName: "editor" */ '../views/editor.vue'),
},
{
path: '/markdown',
name: 'markdown',
meta: {
title: 'markdown编辑器',
permiss: '9',
},
component: () => import(/* webpackChunkName: "markdown" */ '../views/markdown.vue'),
},
{
path: '/export',
name: 'export',
meta: {
title: '导出Excel',
permiss: '2',
},
component: () => import(/* webpackChunkName: "export" */ '../views/export.vue'),
},
{
path: '/import',
name: 'import',
meta: {
title: '导入Excel',
permiss: '2',
},
component: () => import(/* webpackChunkName: "import" */ '../views/import.vue'),
},
],
},
{
path: '/login',
name: 'Login',
meta: {
title: '登录',
},
component: () => import(/* webpackChunkName: "login" */ '../views/login.vue'),
},
{
path: '/403',
name: '403',
meta: {
title: '没有权限',
},
component: () => import(/* webpackChunkName: "403" */ '../views/403.vue'),
},
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
router.beforeEach((to, from, next) => {
document.title = `${to.meta.title} | vue-manage-system`;
const role = localStorage.getItem('ms_username');
const permiss = usePermissStore();
if (!role && to.path !== '/login') {
next('/login');
} else if (to.meta.permiss && !permiss.key.includes(to.meta.permiss)) {
// 如果没有权限则进入403
next('/403');
} else {
next();
}
});
export default router;

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

@ -0,0 +1,23 @@
import { defineStore } from 'pinia';
interface ObjectList {
[key: string]: string[];
}
export const usePermissStore = defineStore('permiss', {
state: () => {
const keys = localStorage.getItem('ms_keys');
return {
key: keys ? JSON.parse(keys) : <string[]>[],
defaultList: <ObjectList>{
admin: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16'],
user: ['1', '2', '3', '11', '13', '14', '15']
}
};
},
actions: {
handleSet(val: string[]) {
this.key = val;
}
}
});

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

@ -0,0 +1,15 @@
import { defineStore } from 'pinia';
export const useSidebarStore = defineStore('sidebar', {
state: () => {
return {
collapse: false
};
},
getters: {},
actions: {
handleCollapse() {
this.collapse = !this.collapse;
}
}
});

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

@ -0,0 +1,53 @@
import { defineStore } from 'pinia';
interface ListItem {
name: string;
path: string;
title: string;
}
export const useTagsStore = defineStore('tags', {
state: () => {
return {
list: <ListItem[]>[]
};
},
getters: {
show: state => {
return state.list.length > 0;
},
nameList: state => {
return state.list.map(item => item.name);
}
},
actions: {
delTagsItem(index: number) {
this.list.splice(index, 1);
},
setTagsItem(data: ListItem) {
this.list.push(data);
},
clearTags() {
this.list = [];
},
closeTagsOther(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;
}
}
}
}
});

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

@ -0,0 +1,31 @@
import axios, {AxiosInstance, AxiosError, AxiosResponse, AxiosRequestConfig} from 'axios';
const service:AxiosInstance = axios.create({
timeout: 5000
});
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
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;

54
src/views/403.vue Normal file
View File

@ -0,0 +1,54 @@
<template>
<div class="error-page">
<div class="error-code">4<span>0</span>3</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" type="primary" size="large" @click="goBack">返回上一页</el-button>
</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: 100%;
background: #f3f3f3;
box-sizing: border-box;
}
.error-code {
line-height: 1;
font-size: 250px;
font-weight: bolder;
color: #f02d2d;
}
.error-code span {
color: #00a854;
}
.error-desc {
font-size: 30px;
color: #777;
}
.error-handle {
margin-top: 30px;
padding-bottom: 200px;
}
.error-btn {
margin-left: 100px;
}
</style>

54
src/views/404.vue Normal file
View File

@ -0,0 +1,54 @@
<template>
<div class="error-page">
<div class="error-code">4<span>0</span>4</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" type="primary" size="large" @click="goBack">返回上一页</el-button>
</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: 100%;
background: #f3f3f3;
box-sizing: border-box;
}
.error-code {
line-height: 1;
font-size: 250px;
font-weight: bolder;
color: #2d8cf0;
}
.error-code span {
color: #00a854;
}
.error-desc {
font-size: 30px;
color: #777;
}
.error-handle {
margin-top: 30px;
padding-bottom: 200px;
}
.error-btn {
margin-left: 100px;
}
</style>

127
src/views/charts.vue Normal file
View File

@ -0,0 +1,127 @@
<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>
<div class="schart-box">
<div class="content-title">柱状图</div>
<schart class="schart" canvasId="bar" :options="options1"></schart>
</div>
<div class="schart-box">
<div class="content-title">折线图</div>
<schart class="schart" canvasId="line" :options="options2"></schart>
</div>
<div class="schart-box">
<div class="content-title">饼状图</div>
<schart class="schart" canvasId="pie" :options="options3"></schart>
</div>
<div class="schart-box">
<div class="content-title">环形图</div>
<schart class="schart" canvasId="ring" :options="options4"></schart>
</div>
</div>
</template>
<script setup lang="ts" name="basecharts">
import Schart from 'vue-schart';
const options1 = {
type: 'bar',
title: {
text: '最近一周各品类销售图'
},
bgColor: '#fbfbfb',
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: '最近几个月各品类销售趋势图'
},
bgColor: '#fbfbfb',
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'
},
bgColor: '#fbfbfb',
labels: ['T恤', '牛仔裤', '连衣裙', '毛衣', '七分裤', '短裙', '羽绒服'],
datasets: [
{
data: [334, 278, 190, 235, 260, 200, 141]
}
]
};
const options4 = {
type: 'ring',
title: {
text: '环形三等分'
},
showValue: false,
legend: {
position: 'bottom',
bottom: 40
},
bgColor: '#fbfbfb',
labels: ['vue', 'react', 'angular'],
datasets: [
{
data: [500, 500, 500]
}
]
};
</script>
<style scoped>
.schart-box {
display: inline-block;
margin: 20px;
}
.schart {
width: 600px;
height: 400px;
}
.content-title {
clear: both;
font-weight: 400;
line-height: 50px;
margin: 10px 0;
font-size: 22px;
color: #1f2f3d;
}
</style>

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

@ -0,0 +1,301 @@
<template>
<div>
<el-row :gutter="20">
<el-col :span="8">
<el-card shadow="hover" class="mgb20" style="height: 252px">
<div class="user-info">
<el-avatar :size="120" :src="imgurl" />
<div class="user-info-cont">
<div class="user-info-name">{{ name }}</div>
<div>{{ role }}</div>
</div>
</div>
<div class="user-info-list">
上次登录时间
<span>2022-10-01</span>
</div>
<div class="user-info-list">
上次登录地点
<span>东莞</span>
</div>
</el-card>
<el-card shadow="hover" style="height: 252px">
<template #header>
<div class="clearfix">
<span>语言详情</span>
</div>
</template>
Vue
<el-progress :percentage="79.4" color="#42b983"></el-progress>
TypeScript
<el-progress :percentage="14" color="#f1e05a"></el-progress>
CSS
<el-progress :percentage="5.6"></el-progress>
HTML
<el-progress :percentage="1" color="#f56c6c"></el-progress>
</el-card>
</el-col>
<el-col :span="16">
<el-row :gutter="20" class="mgb20">
<el-col :span="8">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-1">
<el-icon class="grid-con-icon"><User /></el-icon>
<div class="grid-cont-right">
<div class="grid-num">1234</div>
<div>用户访问量</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-2">
<el-icon class="grid-con-icon"><ChatDotRound /></el-icon>
<div class="grid-cont-right">
<div class="grid-num">321</div>
<div>系统消息</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-3">
<el-icon class="grid-con-icon"><Goods /></el-icon>
<div class="grid-cont-right">
<div class="grid-num">5000</div>
<div>商品数量</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card shadow="hover" style="height: 403px">
<template #header>
<div class="clearfix">
<span>待办事项</span>
<el-button style="float: right; padding: 3px 0" text>添加</el-button>
</div>
</template>
<el-table :show-header="false" :data="todoList" style="width: 100%">
<el-table-column width="40">
<template #default="scope">
<el-checkbox v-model="scope.row.status"></el-checkbox>
</template>
</el-table-column>
<el-table-column>
<template #default="scope">
<div
class="todo-item"
:class="{
'todo-item-del': scope.row.status
}"
>
{{ scope.row.title }}
</div>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="hover">
<schart ref="bar" class="schart" canvasId="bar" :options="options"></schart>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<schart ref="line" class="schart" canvasId="line" :options="options2"></schart>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts" name="dashboard">
import Schart from 'vue-schart';
import { reactive } from 'vue';
import imgurl from '../assets/img/img.jpg';
const name = localStorage.getItem('ms_username');
const role: string = name === 'admin' ? '超级管理员' : '普通用户';
const options = {
type: 'bar',
title: {
text: '最近一周各品类销售图'
},
xRorate: 25,
labels: ['周一', '周二', '周三', '周四', '周五'],
datasets: [
{
label: '家电',
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: '最近几个月各品类销售趋势图'
},
labels: ['6月', '7月', '8月', '9月', '10月'],
datasets: [
{
label: '家电',
data: [234, 278, 270, 190, 230]
},
{
label: '百货',
data: [164, 178, 150, 135, 160]
},
{
label: '食品',
data: [74, 118, 200, 235, 90]
}
]
};
const todoList = reactive([
{
title: '今天要修复100个bug',
status: false
},
{
title: '今天要修复100个bug',
status: false
},
{
title: '今天要写100行代码加几个bug吧',
status: false
},
{
title: '今天要修复100个bug',
status: false
},
{
title: '今天要修复100个bug',
status: true
},
{
title: '今天要写100行代码加几个bug吧',
status: true
}
]);
</script>
<style scoped>
.el-row {
margin-bottom: 20px;
}
.grid-content {
display: flex;
align-items: center;
height: 100px;
}
.grid-cont-right {
flex: 1;
text-align: center;
font-size: 14px;
color: #999;
}
.grid-num {
font-size: 30px;
font-weight: bold;
}
.grid-con-icon {
font-size: 50px;
width: 100px;
height: 100px;
text-align: center;
line-height: 100px;
color: #fff;
}
.grid-con-1 .grid-con-icon {
background: rgb(45, 140, 240);
}
.grid-con-1 .grid-num {
color: rgb(45, 140, 240);
}
.grid-con-2 .grid-con-icon {
background: rgb(100, 213, 114);
}
.grid-con-2 .grid-num {
color: rgb(100, 213, 114);
}
.grid-con-3 .grid-con-icon {
background: rgb(242, 94, 67);
}
.grid-con-3 .grid-num {
color: rgb(242, 94, 67);
}
.user-info {
display: flex;
align-items: center;
padding-bottom: 20px;
border-bottom: 2px solid #ccc;
margin-bottom: 20px;
}
.user-info-cont {
padding-left: 50px;
flex: 1;
font-size: 14px;
color: #999;
}
.user-info-cont div:first-child {
font-size: 30px;
color: #222;
}
.user-info-list {
font-size: 14px;
color: #999;
line-height: 25px;
}
.user-info-list span {
margin-left: 70px;
}
.mgb20 {
margin-bottom: 20px;
}
.todo-item {
font-size: 14px;
}
.todo-item-del {
text-decoration: line-through;
color: #999;
}
.schart {
width: 100%;
height: 300px;
}
</style>

14
src/views/donate.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="container">
<div class="plugins-tips">
如果该框架对你有帮助那就请作者喝杯饮料吧<el-icon><ColdDrink /></el-icon> 加微信号linxin_20探讨问题
</div>
<div>
<img src="https://lin-xin.gitee.io/images/weixin.jpg" />
</div>
</div>
</template>
<script setup lang="ts" name="donate"></script>
<style></style>

37
src/views/editor.vue Normal file
View File

@ -0,0 +1,37 @@
<template>
<div class="container">
<div class="plugins-tips">
wangEditor轻量级 web 富文本编辑器配置方便使用简单 访问地址
<a href="https://www.wangeditor.com/doc/" target="_blank">wangEditor</a>
</div>
<div class="mgb20" ref="editor"></div>
<el-button type="primary" @click="syncHTML">提交</el-button>
</div>
</template>
<script setup lang="ts" name="editor">
import WangEditor from 'wangeditor';
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
const editor = ref(null);
const content = reactive({
html: '',
text: ''
});
let instance: any;
onMounted(() => {
instance = new WangEditor(editor.value);
instance.config.zIndex = 1;
instance.create();
});
onBeforeUnmount(() => {
instance.destroy();
instance = null;
});
const syncHTML = () => {
content.html = instance.txt.html();
console.log(content.html);
};
</script>
<style></style>

98
src/views/export.vue Normal file
View File

@ -0,0 +1,98 @@
<template>
<div>
<div class="container">
<div class="handle-box">
<el-button type="primary" @click="exportXlsx">导出Excel</el-button>
</div>
<el-table :data="tableData" border class="table" header-cell-class-name="table-header">
<el-table-column prop="id" label="ID" width="55" align="center"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="sno" label="学号"></el-table-column>
<el-table-column prop="class" label="班级"></el-table-column>
<el-table-column prop="age" label="年龄"></el-table-column>
<el-table-column prop="sex" label="性别"></el-table-column>
</el-table>
</div>
</div>
</template>
<script setup lang="ts" name="export">
import { ref } from 'vue';
import * as XLSX from 'xlsx';
interface TableItem {
id: number;
name: string;
sno: string;
class: string;
age: string;
sex: string;
}
const tableData = ref<TableItem[]>([]);
//
const getData = () => {
tableData.value = [
{
id: 1,
name: '小明',
sno: 'S001',
class: '一班',
age: '10',
sex: '男',
},
{
id: 2,
name: '小红',
sno: 'S002',
class: '一班',
age: '9',
sex: '女',
},
];
};
getData();
const list = [['序号', '姓名', '学号', '班级', '年龄', '性别']];
const exportXlsx = () => {
tableData.value.map((item: any, i: number) => {
const arr: any[] = [i + 1];
arr.push(...[item.name, item.sno, item.class, item.age, item.sex]);
list.push(arr);
});
let WorkSheet = XLSX.utils.aoa_to_sheet(list);
let new_workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(new_workbook, WorkSheet, '第一页');
XLSX.writeFile(new_workbook, `表格.xlsx`);
};
</script>
<style scoped>
.handle-box {
margin-bottom: 20px;
}
.handle-select {
width: 120px;
}
.handle-input {
width: 300px;
}
.table {
width: 100%;
font-size: 14px;
}
.red {
color: #f56c6c;
}
.mr10 {
margin-right: 10px;
}
.table-td-thumb {
display: block;
margin: auto;
width: 40px;
height: 40px;
}
</style>

156
src/views/form.vue Normal file
View File

@ -0,0 +1,156 @@
<template>
<div class="container">
<div class="form-box">
<el-form ref="formRef" :rules="rules" :model="form" label-width="80px">
<el-form-item label="表单名称" prop="name">
<el-input v-model="form.name"></el-input>
</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="日期时间">
<el-col :span="11">
<el-form-item prop="date1">
<el-date-picker
type="date"
placeholder="选择日期"
v-model="form.date1"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
<el-col class="line" :span="2">-</el-col>
<el-col :span="11">
<el-form-item prop="date2">
<el-time-picker placeholder="选择时间" v-model="form.date2" style="width: 100%">
</el-time-picker>
</el-form-item>
</el-col>
</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="delivery">
<el-switch v-model="form.delivery"></el-switch>
</el-form-item>
<el-form-item label="多选框" prop="type">
<el-checkbox-group v-model="form.type">
<el-checkbox label="小明" name="type"></el-checkbox>
<el-checkbox label="小红" name="type"></el-checkbox>
<el-checkbox label="小白" 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="小明"></el-radio>
<el-radio label="小红"></el-radio>
<el-radio label="小白"></el-radio>
</el-radio-group>
</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-form-item>
<el-button type="primary" @click="onSubmit(formRef)">表单提交</el-button>
<el-button @click="onReset(formRef)">重置表单</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup lang="ts" name="baseform">
import { reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
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: '',
date1: '',
date2: '',
delivery: true,
type: ['小明'],
resource: '小红',
desc: '',
options: [],
});
//
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>

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

@ -0,0 +1,26 @@
<template>
<v-header />
<v-sidebar />
<div class="content-box" :class="{ 'content-collapse': sidebar.collapse }">
<v-tags></v-tags>
<div class="content">
<router-view v-slot="{ Component }">
<transition name="move" mode="out-in">
<keep-alive :include="tags.nameList">
<component :is="Component"></component>
</keep-alive>
</transition>
</router-view>
</div>
</div>
</template>
<script setup lang="ts">
import { useSidebarStore } from '../store/sidebar';
import { useTagsStore } from '../store/tags';
import vHeader from '../components/header.vue';
import vSidebar from '../components/sidebar.vue';
import vTags from '../components/tags.vue';
const sidebar = useSidebarStore();
const tags = useTagsStore();
</script>

212
src/views/icon.vue Normal file
View File

@ -0,0 +1,212 @@
<template>
<div class="container">
<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>
</div>
</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'
];
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;
}
</style>

118
src/views/import.vue Normal file
View File

@ -0,0 +1,118 @@
<template>
<div>
<div class="container">
<div class="handle-box">
<el-upload
action="#"
:limit="1"
accept=".xlsx, .xls"
:show-file-list="false"
:before-upload="beforeUpload"
:http-request="handleMany"
>
<el-button class="mr10" type="success">批量导入</el-button>
</el-upload>
<el-link href="/template.xlsx" target="_blank">下载模板</el-link>
</div>
<el-table :data="tableData" border class="table" header-cell-class-name="table-header">
<el-table-column prop="id" label="ID" width="55" align="center"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="sno" label="学号"></el-table-column>
<el-table-column prop="class" label="班级"></el-table-column>
<el-table-column prop="age" label="年龄"></el-table-column>
<el-table-column prop="sex" label="性别"></el-table-column>
</el-table>
</div>
</div>
</template>
<script setup lang="ts" name="import">
import { UploadProps } from 'element-plus';
import { ref, reactive } from 'vue';
import * as XLSX from 'xlsx';
interface TableItem {
id: number;
name: string;
sno: string;
class: string;
age: string;
sex: string;
}
const tableData = ref<TableItem[]>([]);
//
const getData = () => {
tableData.value = [
{
id: 1,
name: '小明',
sno: 'S001',
class: '一班',
age: '10',
sex: '男',
},
{
id: 2,
name: '小红',
sno: 'S002',
class: '一班',
age: '9',
sex: '女',
},
];
};
getData();
const importList = ref<any>([]);
const beforeUpload: UploadProps['beforeUpload'] = async (rawFile) => {
importList.value = await analysisExcel(rawFile);
return true;
};
const analysisExcel = (file: any) => {
return new Promise(function (resolve, reject) {
const reader = new FileReader();
reader.onload = function (e: any) {
const data = e.target.result;
let datajson = XLSX.read(data, {
type: 'binary',
});
const sheetName = datajson.SheetNames[0];
const result = XLSX.utils.sheet_to_json(datajson.Sheets[sheetName]);
resolve(result);
};
reader.readAsBinaryString(file);
});
};
const handleMany = async () => {
//
const list = importList.value.map((item: any, index: number) => {
return {
id: index,
name: item['姓名'],
sno: item['学号'],
class: item['班级'],
age: item['年龄'],
sex: item['性别'],
};
});
tableData.value.push(...list);
};
</script>
<style scoped>
.handle-box {
display: flex;
margin-bottom: 20px;
}
.table {
width: 100%;
font-size: 14px;
}
.mr10 {
margin-right: 10px;
}
</style>

129
src/views/login.vue Normal file
View File

@ -0,0 +1,129 @@
<template>
<div class="login-wrap">
<div class="ms-login">
<div class="ms-title">后台管理系统</div>
<el-form :model="param" :rules="rules" ref="login" label-width="0px" class="ms-content">
<el-form-item prop="username">
<el-input v-model="param.username" placeholder="username">
<template #prepend>
<el-button :icon="User"></el-button>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
type="password"
placeholder="password"
v-model="param.password"
@keyup.enter="submitForm(login)"
>
<template #prepend>
<el-button :icon="Lock"></el-button>
</template>
</el-input>
</el-form-item>
<div class="login-btn">
<el-button type="primary" @click="submitForm(login)">登录</el-button>
</div>
<p class="login-tips">Tips : 用户名和密码随便填</p>
</el-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { useTagsStore } from '../store/tags';
import { usePermissStore } from '../store/permiss';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { Lock, User } from '@element-plus/icons-vue';
interface LoginInfo {
username: string;
password: string;
}
const router = useRouter();
const param = reactive<LoginInfo>({
username: 'admin',
password: '123123'
});
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('ms_username', param.username);
const keys = permiss.defaultList[param.username == 'admin' ? 'admin' : 'user'];
permiss.handleSet(keys);
localStorage.setItem('ms_keys', JSON.stringify(keys));
router.push('/');
} else {
ElMessage.error('登录成功');
return false;
}
});
};
const tags = useTagsStore();
tags.clearTags();
</script>
<style scoped>
.login-wrap {
position: relative;
width: 100%;
height: 100%;
background-image: url(../assets/img/login-bg.jpg);
background-size: 100%;
}
.ms-title {
width: 100%;
line-height: 50px;
text-align: center;
font-size: 20px;
color: #fff;
border-bottom: 1px solid #ddd;
}
.ms-login {
position: absolute;
left: 50%;
top: 50%;
width: 350px;
margin: -190px 0 0 -175px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.3);
overflow: hidden;
}
.ms-content {
padding: 30px 30px;
}
.login-btn {
text-align: center;
}
.login-btn button {
width: 100%;
height: 36px;
margin-bottom: 10px;
}
.login-tips {
font-size: 12px;
line-height: 30px;
color: #fff;
}
</style>

21
src/views/markdown.vue Normal file
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>

137
src/views/permission.vue Normal file
View File

@ -0,0 +1,137 @@
<template>
<div class="container">
<div class="plugins-tips">通过 v-permiss 自定义指令实现权限管理使用非 admin 账号登录可查看效果</div>
<div class="mgb20">
<span class="label">角色</span>
<el-select v-model="role" @change="handleChange">
<el-option label="超级管理员" value="admin"></el-option>
<el-option label="普通用户" value="user"></el-option>
</el-select>
</div>
<div class="mgb20 tree-wrapper">
<el-tree
ref="tree"
:data="data"
node-key="id"
default-expand-all
show-checkbox
:default-checked-keys="checkedKeys"
/>
</div>
<el-button type="primary" @click="onSubmit">保存权限</el-button>
</div>
</template>
<script setup lang="ts" name="permission">
import { ref } from 'vue';
import { ElTree } from 'element-plus';
import { usePermissStore } from '../store/permiss';
const role = ref<string>('admin');
interface Tree {
id: string;
label: string;
children?: Tree[];
}
const data: Tree[] = [
{
id: '1',
label: '系统首页'
},
{
id: '2',
label: '基础表格',
children: [
{
id: '15',
label: '编辑'
},
{
id: '16',
label: '删除'
}
]
},
{
id: '3',
label: 'tab选项卡'
},
{
id: '4',
label: '表单相关',
children: [
{
id: '5',
label: '基本表单'
},
{
id: '6',
label: '文件上传'
},
{
id: '7',
label: '三级菜单',
children: [
{
id: '8',
label: '富文本编辑器'
},
{
id: '9',
label: 'markdown编辑器'
}
]
}
]
},
{
id: '10',
label: '自定义图标'
},
{
id: '11',
label: 'schart图表'
},
{
id: '13',
label: '权限管理'
},
{
id: '14',
label: '支持作者'
}
];
const permiss = usePermissStore();
//
const checkedKeys = ref<string[]>([]);
const getPremission = () => {
//
checkedKeys.value = permiss.defaultList[role.value];
};
getPremission();
//
const tree = ref<InstanceType<typeof ElTree>>();
const onSubmit = () => {
//
console.log(tree.value!.getCheckedKeys(false));
};
const handleChange = (val: string[]) => {
tree.value!.setCheckedKeys(permiss.defaultList[role.value]);
};
</script>
<style scoped>
.tree-wrapper {
max-width: 500px;
}
.label {
font-size: 14px;
}
</style>

191
src/views/table.vue Normal file
View File

@ -0,0 +1,191 @@
<template>
<div>
<div class="container">
<div class="handle-box">
<el-select v-model="query.address" 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="query.name" placeholder="用户名" class="handle-input mr10"></el-input>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button type="primary" :icon="Plus">新增</el-button>
</div>
<el-table :data="tableData" border class="table" ref="multipleTable" header-cell-class-name="table-header">
<el-table-column prop="id" label="ID" width="55" align="center"></el-table-column>
<el-table-column prop="name" label="用户名"></el-table-column>
<el-table-column label="账户余额">
<template #default="scope">{{ scope.row.money }}</template>
</el-table-column>
<el-table-column label="头像(查看大图)" align="center">
<template #default="scope">
<el-image
class="table-td-thumb"
:src="scope.row.thumb"
:z-index="10"
:preview-src-list="[scope.row.thumb]"
preview-teleported
>
</el-image>
</template>
</el-table-column>
<el-table-column prop="address" label="地址"></el-table-column>
<el-table-column label="状态" align="center">
<template #default="scope">
<el-tag
:type="scope.row.state === '成功' ? 'success' : scope.row.state === '失败' ? 'danger' : ''"
>
{{ scope.row.state }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="date" label="注册时间"></el-table-column>
<el-table-column label="操作" width="220" align="center">
<template #default="scope">
<el-button text :icon="Edit" @click="handleEdit(scope.$index, scope.row)" v-permiss="15">
编辑
</el-button>
<el-button text :icon="Delete" class="red" @click="handleDelete(scope.$index)" v-permiss="16">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
background
layout="total, prev, pager, next"
:current-page="query.pageIndex"
:page-size="query.pageSize"
:total="pageTotal"
@current-change="handlePageChange"
></el-pagination>
</div>
</div>
<!-- 编辑弹出框 -->
<el-dialog title="编辑" v-model="editVisible" width="30%">
<el-form label-width="70px">
<el-form-item label="用户名">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="地址">
<el-input v-model="form.address"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editVisible = false"> </el-button>
<el-button type="primary" @click="saveEdit"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="basetable">
import { ref, reactive } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Delete, Edit, Search, Plus } from '@element-plus/icons-vue';
import { fetchData } from '../api/index';
interface TableItem {
id: number;
name: string;
money: string;
state: string;
date: string;
address: string;
}
const query = reactive({
address: '',
name: '',
pageIndex: 1,
pageSize: 10
});
const tableData = ref<TableItem[]>([]);
const pageTotal = ref(0);
//
const getData = () => {
fetchData().then(res => {
tableData.value = res.data.list;
pageTotal.value = res.data.pageTotal || 50;
});
};
getData();
//
const handleSearch = () => {
query.pageIndex = 1;
getData();
};
//
const handlePageChange = (val: number) => {
query.pageIndex = val;
getData();
};
//
const handleDelete = (index: number) => {
//
ElMessageBox.confirm('确定要删除吗?', '提示', {
type: 'warning'
})
.then(() => {
ElMessage.success('删除成功');
tableData.value.splice(index, 1);
})
.catch(() => {});
};
//
const editVisible = ref(false);
let form = reactive({
name: '',
address: ''
});
let idx: number = -1;
const handleEdit = (index: number, row: any) => {
idx = index;
form.name = row.name;
form.address = row.address;
editVisible.value = true;
};
const saveEdit = () => {
editVisible.value = false;
ElMessage.success(`修改第 ${idx + 1} 行成功`);
tableData.value[idx].name = form.name;
tableData.value[idx].address = form.address;
};
</script>
<style scoped>
.handle-box {
margin-bottom: 20px;
}
.handle-select {
width: 120px;
}
.handle-input {
width: 300px;
}
.table {
width: 100%;
font-size: 14px;
}
.red {
color: #F56C6C;
}
.mr10 {
margin-right: 10px;
}
.table-td-thumb {
display: block;
margin: auto;
width: 40px;
height: 40px;
}
</style>

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

@ -0,0 +1,116 @@
<template>
<div class="container">
<el-tabs v-model="message">
<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="150"></el-table-column>
<el-table-column width="120">
<template #default="scope">
<el-button type="danger" @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="150"></el-table-column>
<el-table-column width="120">
<template #default="scope">
<el-button @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>
</div>
</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;
}
.handle-row {
margin-top: 30px;
}
</style>

48
src/views/upload.vue Normal file
View File

@ -0,0 +1,48 @@
<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-cropperjs一个封装了 cropperjs Vue 组件 访问地址
<a href="https://github.com/Agontuk/vue-cropperjs" target="_blank">vue-cropperjs</a> 示例请查看
<router-link to="/user">个人中心</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>

174
src/views/user.vue Normal file
View File

@ -0,0 +1,174 @@
<template>
<div>
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="clearfix">
<span>基础信息</span>
</div>
</template>
<div class="info">
<div class="info-image" @click="showDialog">
<el-avatar :size="100" :src="avatarImg" />
<span class="info-edit">
<i class="el-icon-lx-camerafill"></i>
</span>
</div>
<div class="info-name">{{ name }}</div>
<div class="info-desc">不可能我的代码怎么可能会有bug</div>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="clearfix">
<span>账户编辑</span>
</div>
</template>
<el-form label-width="90px">
<el-form-item label="用户名:"> {{ name }} </el-form-item>
<el-form-item label="旧密码:">
<el-input type="password" v-model="form.old"></el-input>
</el-form-item>
<el-form-item label="新密码:">
<el-input type="password" v-model="form.new"></el-input>
</el-form-item>
<el-form-item label="个人简介:">
<el-input v-model="form.desc"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">保存</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
<el-dialog title="裁剪图片" v-model="dialogVisible" width="600px">
<vue-cropper
ref="cropper"
:src="imgSrc"
:ready="cropImage"
:zoom="cropImage"
:cropmove="cropImage"
style="width: 100%; height: 400px"
></vue-cropper>
<template #footer>
<span class="dialog-footer">
<el-button class="crop-demo-btn" type="primary"
>选择图片
<input class="crop-input" type="file" name="image" accept="image/*" @change="setImage" />
</el-button>
<el-button type="primary" @click="saveAvatar">上传并保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="user">
import { reactive, ref } from 'vue';
import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';
import avatar from '../assets/img/img.jpg';
const name = localStorage.getItem('ms_username');
const form = reactive({
old: '',
new: '',
desc: '不可能我的代码怎么可能会有bug'
});
const onSubmit = () => {};
const avatarImg = ref(avatar);
const imgSrc = ref('');
const cropImg = ref('');
const dialogVisible = ref(false);
const cropper: any = ref();
const showDialog = () => {
dialogVisible.value = true;
imgSrc.value = avatarImg.value;
};
const setImage = (e: any) => {
const file = e.target.files[0];
if (!file.type.includes('image/')) {
return;
}
const reader = new FileReader();
reader.onload = (event: any) => {
dialogVisible.value = true;
imgSrc.value = event.target.result;
cropper.value && cropper.value.replace(event.target.result);
};
reader.readAsDataURL(file);
};
const cropImage = () => {
cropImg.value = cropper.value.getCroppedCanvas().toDataURL();
};
const saveAvatar = () => {
avatarImg.value = cropImg.value;
dialogVisible.value = false;
};
</script>
<style scoped>
.info {
text-align: center;
padding: 35px 0;
}
.info-image {
position: relative;
margin: auto;
width: 100px;
height: 100px;
background: #f8f8f8;
border: 1px solid #eee;
border-radius: 50px;
overflow: hidden;
}
.info-edit {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.3s ease;
}
.info-edit i {
color: #eee;
font-size: 25px;
}
.info-image:hover .info-edit {
opacity: 1;
}
.info-name {
margin: 15px 0 10px;
font-size: 24px;
font-weight: 500;
color: #262626;
}
.crop-demo-btn {
position: relative;
}
.crop-input {
position: absolute;
width: 100px;
height: 40px;
left: 0;
top: 0;
opacity: 0;
cursor: pointer;
}
</style>

10
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module 'vue-schart';
declare module 'vue-cropperjs';

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts","src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

9
tsconfig.node.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

22
vite.config.ts Normal file
View File

@ -0,0 +1,22 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import VueSetupExtend from 'vite-plugin-vue-setup-extend';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
export default defineConfig({
base: './',
plugins: [
vue(),
VueSetupExtend(),
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
],
optimizeDeps: {
include: ['schart.js']
}
});