1
0
Code Issues Pull Requests Packages Projects Releases Wiki Activity GitHub Gitee

添加后台管理框架雏形

This commit is contained in:
程序员小墨 2023-03-22 16:39:26 +08:00
parent 8952bfc271
commit 76ad164d4e
53 changed files with 8523 additions and 0 deletions

23
frontend/.gitignore vendored Normal file
View File

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

21
frontend/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.

137
frontend/README.md Normal file
View File

@ -0,0 +1,137 @@
# vue-manage-system
<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)

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

@ -0,0 +1,5 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
}

60
frontend/components.d.ts vendored Normal file
View File

@ -0,0 +1,60 @@
// 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 {
Calender: typeof import('./src/components/calender.vue')['default']
ContextMenu: typeof import('./src/components/context-menu.vue')['default']
ElAffix: typeof import('element-plus/es')['ElAffix']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
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']
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']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
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']
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']
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']
ElTree: typeof import('element-plus/es')['ElTree']
ElUpload: typeof import('element-plus/es')['ElUpload']
Header: typeof import('./src/components/header.vue')['default']
Popover: typeof import('./src/components/popover.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
frontend/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></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>

2603
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
frontend/package.json Normal file
View File

@ -0,0 +1,40 @@
{
"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",
"echarts": "^5.4.1",
"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-cesium": "^1.2.22",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-tsc": "^0.38.4"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

14
frontend/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>

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,24 @@
.header{
background-color: #242f42;
}
.login-wrap{
/* background: #324157; */
background: #051539;
}
.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);
}

View File

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

View File

@ -0,0 +1,142 @@
* {
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: 200px;
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: #f0f0f0;
}
.content {
width: auto;
height: 100%;
overflow: none;
box-sizing: border-box;
}
.content-collapse {
left: 65px;
}
/* 普通页面添加 class="container" 即包含滚动条 */
.container {
padding: 30px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
box-sizing: border-box;
height: 100%;
overflow-y: auto;
}
.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 0.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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,310 @@
<template>
<div class="calender-container">
<div class="calender-toolbox">
<div style="width: 100%;">
<slot name="toolbox"></slot>
</div>
</div>
<div class="calender-title">
<div v-for="w in week">
<p>{{ w }}</p>
</div>
</div>
<div class="calender-grid" :style="{ gridTemplateRows: `repeat(${rowCount}, ${Math.ceil(100 / rowCount)}fr)` }">
<div v-for="item in dayItem" class="calender-grid-item" :class="item.class">
<template v-if="item.type == 'day'">
<!-- 控制按钮 -->
<div class="calender-grid-item-ctrl-btn-container">
<div class="calender-grid-item-ctrl-btn">
<el-icon :size="20" @click="uploadFile(item)">
<UploadFilled />
</el-icon>
</div>
</div>
<!-- 日期编号 -->
<p class="calender-grid-item-day">{{ item.day }}</p>
<!-- 附件 -->
<div v-if="item.event.length > 0" class="calender-grid-item-attachment-container">
<div v-for="e in item.event" class="calender-grid-item-attachment">
<a class="download-link" :href="e.file.downloadLink" :download="e.file.displayName"
:title="e.file.displayName" @contextmenu.prevent.native="contextMenuRef.openMenu($event)"
@mouseover="showPopover($event, e)" @mouseleave="hidePopover()">
{{ e.file.displayName }}
</a>
</div>
</div>
</template>
</div>
</div>
<!-- 鼠标悬浮弹窗 -->
<filePopover ref="filePopoverRef" />
<!-- 鼠标右键菜单 -->
<contextMenu ref="contextMenuRef" />
</div>
</template>
<script setup>
import { ref, defineProps, onMounted, onBeforeUpdate, nextTick, defineExpose } from 'vue';
import contextMenu from './context-menu.vue';
import filePopover from '../components/file-popover.vue';
const filePopoverRef = ref()
const contextMenuRef = ref()
const props = defineProps({
'year': {
type: Number,
required: true,
},
'month': {
type: Number,
required: true,
},
'events': {
type: Object,
require: false,
default: [],
},
'doUpload': {
type: Function,
require: true,
},
})
const week = [
"星期一",
"星期二",
"星期三",
"星期四",
"星期五",
"星期六",
"星期日",
]
const dayItem = ref([]);
const rowCount = ref(5);
let isMounted = false;
const filePopoverInfo = ref({
visable: false,
})
//
function getMonthCander(year, month, file) {
console.log("getMonthCander", isMounted)
console.log("file", file)
let firstDay = new Date(year, month - 1, 1);
let nextMonthFirstDay = new Date(year, (month + 1) - 1, 1);
let dayOfWeek = firstDay.getDay();
let space = (dayOfWeek - 1 + 7) % 7; //
// console.log("space", space);
let dayCountInMonth = (nextMonthFirstDay - firstDay) / (1000 * 3600 * 24);
// console.log("dayCountInMonth", dayCountInMonth);
//
dayItem.value.push(...(new Array(space)).fill({
"type": "space",
"class": "space-item",
}));
//
for (let i = 1; i <= dayCountInMonth; i++) {
dayItem.value.push({
"type": "day",
"day": i,
"class": "day-item",
"event": file.filter((f) => f.day === i),
"date": new Date(year, month - 1, i),
});
}
// console.log(dayItem.value);
}
onMounted(() => {
console.log("calender onMounted.", "props:", props)
getMonthCander(props.year, props.month, props.events);
nextTick(() => {
console.log("calender onMounted nextTick.");
isMounted = true;
})
})
onBeforeUpdate(() => {
console.log("calender onBeforeUpdate.", "props:", props)
if (isMounted) {
dayItem.value = [];
getMonthCander(props.year, props.month, props.events);
}
})
// TODO
function uploadFile(item) {
console.log("uploadFile item:", item);
if (typeof (props.doUpload) === "function") {
props.doUpload(item.date);
}
}
//
function showPopover($event, fileInfo) {
// console.log("showPopover $event:", $event, "fileInfo:", fileInfo, $event.target);
filePopoverRef.value.updateInfo({
visable: true,
left: $event.clientX - $event.offsetX + $event.target.offsetWidth / 2,
top: $event.clientY - $event.offsetY + $event.target.offsetHeight + 5,
width: 100,
height: 100,
});
}
function hidePopover() {
filePopoverRef.value.updateInfo({
visable: false,
});
}
//
// function removeFile(...args) {
// console.log("removeFile args:", args)
// }
// defineExpose({
// removeFile
// })
</script>
<style scoped>
.calender-container {
width: 100%;
}
@media (min-width: 1200px) {
.calender-container {
width: 90%;
margin: 0 auto;
}
}
@media (min-width: 2400px) {
.calender-container {
width: 80%;
}
}
.calender-toolbox {
/* background-color: aqua; */
height: 80px;
display: grid;
place-items: center;
}
.calender-title {
background-color: #A0CFFF;
display: grid;
grid-template-columns: repeat(7, 1fr);
place-items: center;
height: 38px;
}
.calender-grid {
background-color: #C6E2FF;
height: 100%;
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
padding: 8px;
}
.calender-grid-item {
max-height: 160px;
overflow: hidden;
}
.calender-grid-item-day {
padding-top: 8px;
padding-left: 10px;
/* padding-bottom: 2px; */
height: 30px;
box-sizing: border-box;
}
.calender-grid-item-ctrl-btn-container {
position: relative;
}
.calender-grid-item-ctrl-btn {
position: absolute;
right: 7px;
top: 5px;
opacity: 0;
cursor: pointer;
}
.calender-grid-item:hover .calender-grid-item-ctrl-btn {
opacity: .45;
}
.calender-grid-item:hover .calender-grid-item-ctrl-btn:hover {
opacity: 0.9;
}
.calender-grid-item-attachment-container {
padding: 0 3px;
height: 100%;
height: calc(100% - 30px);
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 5px;
box-sizing: border-box;
}
.calender-grid-item.day-item {
background-color: #ECF5FF;
}
.calender-grid-item-attachment,
.download-link {
background-color: #A0CFFF;
color: black;
border-radius: 50px;
padding: 0.2px 5px;
transition: all 0.12s;
cursor: pointer;
margin-bottom: 4px;
}
.calender-grid-item-attachment:hover,
.calender-grid-item-attachment:hover>.download-link {
background-color: #409EFF;
color: white;
border-radius: 3px;
}
.download-link {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 600px) {
.calender-title,
.calender-grid-item.space-item {
display: none;
}
.calender-grid {
grid-template-columns: repeat(1, 1fr);
grid-template-rows: repeat(31, 120px) !important;
}
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div>
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
<li>菜单一</li>
<li>菜单二</li>
</ul>
</div>
</template>
<script>
export default {
components: {},
data() {
return {
visible: false,
top: 0,
left: 0,
};
},
expose: ['openMenu'],
watch: {
// newValue
visible(newValue, oldValue) {
if (newValue) {
//
// document.body.addEventListenerdocument.body.removeEventListener3
// ("" , "" , "");
// body
document.body.addEventListener("click", this.closeMenu);
} else {
//
// body
document.body.removeEventListener("click", this.closeMenu);
}
},
},
methods: {
//
openMenu(e) {
var x = e.pageX; //x0,0
var y = e.pageY; //y0,0
this.top = y + 2;
this.left = x + 2;
this.visible = true; //
},
//
closeMenu() {
this.visible = false; //
},
},
};
</script>
<style scoped>
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
/* //关键样式设置固定定位 */
position: fixed;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
}
.contextmenu li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
}
.contextmenu li:hover {
background: #eee;
}
</style>

View File

@ -0,0 +1,189 @@
<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">{{ settings.siteFullTitle }}</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 { useTagsStore } from '../store/tags';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import send_request from '../utils/send_request';
import imgurl from '../assets/img/img.jpg';
import settings from '../utils/settings';
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') {
// 退
send_request('v1/user/logout', "POST");
// ()
const tags = useTagsStore();
tags.clearTags();
// localStorage
localStorage.clear();
// localStorage.removeItem('ms_username');
// localStorage.removeItem('ms_user_id');
// localStorage.removeItem('ms_role_id');
//
router.push({
path: '/login',
query: {
redirectTo: router.currentRoute.value.path // window.location.href
},
});
ElMessage.success('已退出登录');
} 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;
/* 系统名称不换行 */
white-space: nowrap;
}
.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>

View File

@ -0,0 +1,236 @@
<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';
import settings from '../utils/settings';
const items = [
// https://element-plus.gitee.io/zh-CN/component/icon.html
{
icon: 'HomeFilled',//'/assets/image/svg/alert_warning.svg',
index: '/dashboard',
title: '系统首页', //
permiss: 'dashboard',
},
{
icon: 'Monitor',
index: '/monitor-data',
title: '监测数据',
permiss: 'monitor-data',
subs: [
{
index: '/monitor-data-view',
title: '查看数据',
permiss: 'monitor-data-view',
},
],
},
{
icon: 'BellFilled',
index: '/warning',
title: '预警信息',
permiss: 'warning',
subs: [
{
index: '/warning-view',
title: '总览',
permiss: 'warning-view',
},
{
index: '/warning-setting',
title: '预警设置',
permiss: 'warning-setting',
},
],
},
{
icon: 'OfficeBuilding',
index: '/equipment',
title: '设备信息',
permiss: 'equipment',
subs: [
{
index: '/equipment-setting',
title: '设备管理',
permiss: 'equipment-setting',
},
],
},
{
icon: 'Avatar',
index: '/privilege',
title: '用户角色',
permiss: 'privilege',
subs: [
{
index: '/privilege-user-setting',
title: '用户管理', //
permiss: 'privilege-user-setting',
},
{
index: '/privilege-role-setting',
title: '角色权限', // <;>
permiss: 'privilege-role-setting',
},
],
},
... !settings.debugMode ? [] : [
{
icon: 'Odometer',
index: '/',
title: '——————————',
permiss: 'default',
},
{
icon: 'Calendar',
index: '1',
title: '表格相关',
permiss: 'default',
subs: [
{
index: '/table',
title: '常用表格',
permiss: 'default',
},
{
index: '/import',
title: '导入Excel',
permiss: 'default',
},
{
index: '/export',
title: '导出Excel',
permiss: 'default',
},
],
},
{
icon: 'DocumentCopy',
index: '/tabs',
title: 'tab选项卡',
permiss: 'default',
},
{
icon: 'Edit',
index: '3',
title: '表单相关',
permiss: 'default',
subs: [
{
index: '/form',
title: '基本表单',
permiss: 'default',
},
{
index: '/upload',
title: '文件上传',
permiss: 'default',
},
{
index: '4',
title: '三级菜单',
permiss: 'default',
subs: [
{
index: '/editor',
title: '富文本编辑器',
permiss: 'default',
},
{
index: '/markdown',
title: 'markdown编辑器',
permiss: '9default',
},
],
},
],
},
{
icon: 'Setting',
index: '/icon',
title: '自定义图标',
permiss: 'default',
},
{
icon: 'PieChart',
index: '/charts',
title: 'schart图表',
permiss: 'default',
},
]
];
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: 200px;
}
.sidebar>ul {
min-height: 100%;
}
</style>

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
frontend/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');

View File

@ -0,0 +1,218 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import { usePermissStore } from '../store/permiss';
import Home from '../views/home.vue';
import settings from '../utils/settings'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/dashboard',
},
{
path: '/',
name: 'Home',
component: Home,
children: [
{
path: '/dashboard',
name: 'dashboard',
meta: {
title: '系统首页',
permiss: 'dashboard',
},
component: () => import('../views/dashboard.vue'),
},
{
path: '/monitor-data-view',
name: 'monitor-data-view',
meta: {
title: '查看数据',
permiss: 'monitor-data-view',
},
component: () => import('../views/monitor-data-view.vue'),
},
{
path: '/warning-view',
name: 'warning-view',
meta: {
title: '总览',
permiss: 'warning-view',
},
component: () => import('../views/warning-view.vue'),
},
{
path: '/warning-setting',
name: 'warning-setting',
meta: {
title: '预警设置',
permiss: 'warning-setting',
},
component: () => import('../views/warning-setting.vue'),
},
{
path: '/equipment-setting',
name: 'equipment-setting',
meta: {
title: '设备管理',
permiss: 'equipment-setting',
},
component: () => import('../views/equipment-setting.vue'),
},
{
path: '/privilege-user-setting',
name: 'privilege-user-setting',
meta: {
title: '用户管理',
permiss: 'privilege-user-setting',
},
component: () => import('../views/privilege-user-setting.vue'),
},
{
path: '/privilege-role-setting',
name: 'privilege-role-setting',
meta: {
title: '角色权限',
permiss: 'privilege-role-setting',
},
component: () => import('../views/privilege-role-setting.vue'),
},
{
path: '/user',
name: 'user',
meta: {
title: '个人中心',
},
component: () => import('../views/user.vue'),
},
{
path: '/table',
name: 'basetable',
meta: {
title: '表格',
permiss: 'default',
},
component: () => import('../views/demo/table.vue'),
},
{
path: '/charts',
name: 'basecharts',
meta: {
title: '图表',
permiss: 'default',
},
component: () => import('../views/demo/charts.vue'),
},
{
path: '/form',
name: 'baseform',
meta: {
title: '表单',
permiss: 'default',
},
component: () => import('../views/demo/form.vue'),
},
{
path: '/tabs',
name: 'tabs',
meta: {
title: 'tab标签',
permiss: 'default',
},
component: () => import('../views/demo/tabs.vue'),
},
{
path: '/icon',
name: 'icon',
meta: {
title: '自定义图标',
permiss: 'default',
},
component: () => import('../views/demo/icon.vue'),
},
{
path: '/editor',
name: 'editor',
meta: {
title: '富文本编辑器',
permiss: 'default',
},
component: () => import('../views/demo/editor.vue'),
},
{
path: '/markdown',
name: 'markdown',
meta: {
title: 'markdown编辑器',
permiss: 'default',
},
component: () => import('../views/demo/markdown.vue'),
},
{
path: '/export',
name: 'export',
meta: {
title: '导出Excel',
permiss: 'default',
},
component: () => import('../views/demo/export.vue'),
},
{
path: '/import',
name: 'import',
meta: {
title: '导入Excel',
permiss: 'default',
},
component: () => import('../views/demo/import.vue'),
},
],
},
{
path: '/login',
name: 'Login',
meta: {
title: '登录',
},
component: () => import('../views/login.vue'),
},
{
path: '/403',
name: '403',
meta: {
title: '没有权限',
},
component: () => import('../views/error-page/403.vue'),
},
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
router.beforeEach((to, from, next) => {
document.title = `${to.meta.title} | ${settings.siteTitle}`;
const role = localStorage.getItem('ms_username');
const permiss = usePermissStore();
if (!role && to.path !== '/login') {
next({
path: '/login',
query: {
redirectTo: router.currentRoute.value.path // window.location.href
},
});
} else if (to.meta.permiss && !permiss.key.includes(to.meta.permiss)) {
// 如果没有权限则进入403
next('/403');
} else {
next();
}
});
export default router;

View File

@ -0,0 +1,22 @@
import { defineStore } from 'pinia';
import send_request from '../utils/send_request';
interface ObjectList {
[key: string]: string[];
}
export const usePermissStore = defineStore('permiss', {
state: () => {
const keys = localStorage.getItem('ms_keys');
const defaultList = localStorage.getItem('ms_default_list');
return {
key: keys ? JSON.parse(keys) : <string[]>[],
defaultList: JSON.stringify(defaultList)
};
},
actions: {
handleSet(val: string[]) {
this.key = val;
}
}
});

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

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

View File

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

View File

@ -0,0 +1,52 @@
import request from './request';
import settings from './settings';
async function send_request(url, method = "POST", params, callback) {
if (!url) {
return false;
}
let returnData = await request({
baseURL: settings.backendHost,
url: url,
method: method,
withCredentials: true,
// POST 请求参数
data: method.toUpperCase() == "POST" ? params : null,
// GET 请求参数
params: method.toUpperCase() == "GET" ? params : null,
}).then((response) => {
let result = response.data;
// 判断后端是否处理成功
if (!result.isSuccess) {
// 用户未登录情况
if (result.data && result.data.errCode == 20003) {
ElMessage.error(result?.data?.errMsg || "用户未登录");
localStorage.clear();
// window.location.reload();
// 如果同时发出多个请求可能会多次进来第二次及之后进入时hash已经变成 #/login 了
if (!window.location.hash.includes("/login")) {
let newUrl = '/#/login?redirectTo=' + encodeURIComponent(window.location.hash.substring(1).split('?')[0])
console.log("newUrl", newUrl)
window.location.href = newUrl;
}
} else {
ElMessage.error(result?.data?.errMsg || "服务器错误");
}
return false;
}
let data = result.data;
if (typeof (callback) === "function") {
callback(data);
}
return true;
}).catch((err) => {
console.error(err);
ElMessage.error(err.message);
// ElMessage.error('请求超时,请检查网络连接');
return false;
})
return returnData;
}
export default send_request;

View File

@ -0,0 +1,27 @@
export default {
/**
* 是否是调试模式
* true: 开启调试
* false: 关闭调试
*/
debugMode: true,
/**
* 网站名称
* 网页标题 / 登录页显示
*/
siteTitle: "社区疫情防控系统",
siteFullTitle: "社区疫情防控系统 - 后台管理系统(社区管理员)",
/**
* 开发公司名称
* 留空则不显示
*/
companyName: "",
/**
* 后端接口请求地址
* / 结尾
*/
backendHost: "http://epp.only4.work/",
};

View File

@ -0,0 +1,301 @@
<template>
<div class="container">
<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">500</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 { ref, 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>

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>

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>

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>

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>

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>

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>

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>

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="'default'">
编辑
</el-button>
<el-button text :icon="Delete" class="red" @click="handleDelete(scope.$index)" v-permiss="'default'">
删除
</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>

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>

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>

View File

@ -0,0 +1,389 @@
<template>
<div>
<div class="container">
<!-- 筛选 -->
<div class="handle-box">
<el-select v-model="query.params.deviceType" placeholder="设备类型" class="handle-select mr10"
style="width: 160px;">
<el-option :key="''" label="全部设备类型" :value="''"></el-option>
<el-option v-for="opt in deviceTypeOption" :key="opt.id" :label="opt.typeName"
:value="opt.id"></el-option>
</el-select>
<!-- <el-input v-model="query.params.name" placeholder="设备名" class="handle-input mr10"></el-input>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button> -->
<el-button type="primary" :icon="Plus" @click="handleNew">新增设备</el-button>
<el-button type="primary" :icon="Plus" @click="exportExcel">导出设备列表</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="设备名称" align="center"></el-table-column>
<el-table-column prop="type" label="设备类型" align="center"></el-table-column>
<el-table-column prop="deviceName" label="设备ID" align="center"></el-table-column>
<el-table-column prop="location" label="设备位置" align="center"></el-table-column>
<el-table-column prop="contractor" label="承建商" align="center"></el-table-column>
<el-table-column prop="manufacturer" label="生产商" align="center"></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 label="操作" width="220" align="center">
<template #default="scope">
<el-button text :icon="Edit" @click="handleEdit(scope.$index, scope.row)"
v-permiss="'equipment-setting-manage'">
编辑
</el-button>
<el-button text :icon="Delete" class="red" @click="handleDelete(scope.$index, scope.row)"
v-permiss="'equipment-setting-manage'">
删除
</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="formId > 0 ? '编辑' : '新增'" v-model="editVisible" style="width: 30%; min-width: 280px;">
<el-form ref="editForm" label-width="80px" :rules="rules" :model="form">
<el-form-item label="设备名称" prop="name">
<el-input class="popup-item" v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="设备类型" prop="typeId">
<el-select class="popup-item" v-model="form.typeId" placeholder="设备类型">
<el-option :key="''" label="请选择设备类型" :value="''"></el-option>
<el-option v-for="opt in deviceTypeOption" :key="opt.id" :label="opt.typeName"
:value="opt.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="设备ID" prop="deviceName">
<el-input class="popup-item" v-model="form.deviceName"></el-input>
</el-form-item>
<el-form-item label="设备位置">
<el-input class="popup-item" v-model="form.location"></el-input>
</el-form-item>
<el-form-item label="承建商">
<el-input class="popup-item" v-model="form.contractor"></el-input>
</el-form-item>
<el-form-item label="生产商">
<el-input class="popup-item" v-model="form.manufacturer"></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(editForm)"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { ElLoading } from 'element-plus';
import { Delete, Edit, Search, Plus } from '@element-plus/icons-vue';
import send_request from '../utils/send_request';
import * as XLSX from 'xlsx';
//
const query = reactive({
params: {
deviceType: '',
name: '',
},
pageIndex: 1,
pageSize: 10
});
//
const deviceTypeOption = ref<[{ id: any, typeName: any }]>()
//
const tableData: any = ref([]);
const pageTotal = ref(0);
const deviceTypeDict: any = ref({});
// &
const getData = async () => {
const loading = ElLoading.service({
lock: true,
text: '请稍候',
background: 'rgba(0, 0, 0, 0.7)',
});
await send_request('v1/device/list', "GET", {
...query.params,
pageIndex: query.pageIndex,
pageSize: query.pageSize,
page: query.pageIndex,
}, (data: any) => {
let deviceList = data.list;
let deviceTypeList = data.optionDeviceType;
//""
console.log("deviceList/deviceTypeList:", deviceList, deviceTypeList)
deviceTypeList.forEach((item: any) => deviceTypeDict.value[item.id] = item.typeName)
//
tableData.value = deviceList.list.map((i: any) => {
i.state = "在线"
i.type = deviceTypeDict.value[i.typeId]
return i
});
// console.log("tableData", tableData);
pageTotal.value = deviceList.total;
//
deviceTypeOption.value = deviceTypeList;
});
loading.close();
};
getData();
//
const handleSearch = () => {
query.pageIndex = 1;
getData();
};
//
watch(() => query.params, function (newVal, oldVal, ...args) {
console.log("watch query.params newVal", JSON.stringify(newVal))
console.log("watch query.params oldVal", JSON.stringify(oldVal))
console.log("watch query.params args", args)
handleSearch();
}, { deep: true });
//
const handlePageChange = (val: number) => {
query.pageIndex = val;
getData();
};
//
const handleDelete = (index: number, row: any) => {
//
ElMessageBox.confirm('确定要删除吗?', '提示', {
type: 'warning'
})
.then(() => send_request('v1/device/delete', "POST", {
deviceId: row.id
}, (data: any) => {
// console.log(data);
ElMessage.success('删除成功');
tableData.value.splice(index, 1);
}))
.catch(() => {
ElMessage.success('删除失败');
});
};
// /
const editForm = ref<FormInstance>();
const rules: FormRules = {
name: [
{
required: true,
message: '请输入设备名称',
trigger: 'blur'
},
{
min: 2,
message: '设备名称过短',
trigger: 'blur'
},
{
max: 52,
message: '设备名称过长',
trigger: 'blur'
}
],
typeId: [
{
required: true,
message: '请选择设备类型',
trigger: 'blur'
}
],
deviceName: [
{
required: true,
message: '请输入设备ID',
trigger: 'blur'
},
{
min: 2,
message: '设备ID过短',
trigger: 'blur'
},
{
max: 52,
message: '设备ID过长',
trigger: 'blur'
}
],
};
//
const editVisible = ref(false);
let form = reactive({
name: '',
typeId: '',
deviceName: '',
location: '',
contractor: '',
manufacturer: '',
});
let idx: number = -1;
let formId: number = -1;
const handleEdit = (index: number, row: any) => {
idx = index;
formId = row.id;
form.name = row.name;
form.typeId = row.typeId;
form.deviceName = row.deviceName;
form.location = row.location;
form.contractor = row.contractor;
form.manufacturer = row.manufacturer;
editVisible.value = true;
};
//
const handleNew = () => {
formId = -1;
for (let formKeys of Object.keys(form)) {
form[formKeys] = ''
}
editVisible.value = true;
};
const saveEdit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
// console.log("formEl", formEl);
formEl.validate(async (valid: boolean, invalidFields: any) => {
if (!valid) {
// console.log("invalidFields", invalidFields);
//
Object.values(invalidFields).forEach((input: any) => {
//
input.forEach((element: any) => {
ElMessage.error({ message: element.message, grouping: true });
});
});
return;
}
if (formId > 0) {
//
tableData.value[idx].name = form.name;
tableData.value[idx].typeId = form.typeId;
tableData.value[idx].deviceName = form.deviceName;
tableData.value[idx].location = form.location;
tableData.value[idx].contractor = form.contractor;
tableData.value[idx].manufacturer = form.manufacturer;
//
await send_request('v1/device/edit', "POST", {
id: formId,
contractor: form.contractor,
deviceName: form.deviceName,
name: form.name,
location: form.location,
manufacturer: form.manufacturer,
typeId: form.typeId
}, (data: any) => {
// console.log(data)
//
editVisible.value = false;
ElMessage.success(`修改成功`);
});
} else {
//
await send_request('v1/device/add', "POST", {
contractor: form.contractor,
deviceName: form.deviceName,
name: form.name,
location: form.location,
manufacturer: form.manufacturer,
typeId: form.typeId
}, (data: any) => {
// console.log(data)
//
editVisible.value = false;
ElMessage.success(`添加成功`);
query.pageIndex = Math.ceil((pageTotal.value + 1) / query.pageSize);
getData();
});
}
});
};
// Excel
const exportExcel = async () => {
const loading = ElLoading.service({
lock: true,
text: '请稍候',
background: 'rgba(0, 0, 0, 0.7)',
});
let list = [['序号', '设备名称', '设备类型', '设备ID', '设备位置', '承建商', '生产商']];
await send_request('v1/device/listAll', "GET", {
}, (data: any) => {
data.list.map((item: any, i: number) => {
const arr: any[] = [i + 1];
// console.log("-----------------", arr);
arr.push(...[item.name, deviceTypeDict.value[item.typeId], item.deviceName, item.location, item.manufacturer, item.contractor]);
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`);
});
loading.close();
}
</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;
}
.popup-item {
width: 100%;
}
</style>

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>

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>

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" style="height: 100%;"></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>

View File

@ -0,0 +1,222 @@
<template>
<div class="login-wrap">
<div class="login-container">
<div class="ms-login">
<div class="ms-title">{{ settings.siteFullTitle }}</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="用户名">
<template #prepend>
<el-button :icon="User"></el-button>
</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-button :icon="Lock"></el-button>
</template>
</el-input>
</el-form-item>
<div class="login-btn">
<el-button type="primary" @click="submitForm(login)">
<!-- <el-icon><UserFilled /></el-icon>-->
&nbsp;<el-icon>
<Right />
</el-icon>
</el-button>
</div>
</el-form>
</div>
</div>
<div class="company-info" v-if="settings.companyName">
{{ settings.companyName }}
</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, ElLoading } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { Lock, User } from '@element-plus/icons-vue';
import send_request from '../utils/send_request';
import settings from '../utils/settings';
interface LoginInfo {
username: string;
password: string;
}
interface UserInfo {
username: string;
id: string;
roleId: string;
}
interface PrivilegeInfo {
"id": Number,
"roleId": Number,
"privilegeName": string,
"module": string
}
interface RoleInfo {
id: Number,
roleName: string,
comment: any,
privileges: Array<PrivilegeInfo>;
}
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: any = usePermissStore();
const login = ref<FormInstance>();
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid: boolean, invalidFields: any) => {
if (!valid) {
// ElMessage.error('');
console.log("invalidFields", invalidFields);
//
Object.values(invalidFields).forEach((input: any) => {
// console.log("input", input)
//
input.forEach((element: any) => {
// console.log("element", element)
ElMessage.error({ message: element.message, grouping: true });
});
});
return;
}
const loading = ElLoading.service({
lock: true,
text: '请稍候',
background: 'rgba(0, 0, 0, 0.7)',
});
await send_request('v1/user/login', "POST", {
userName: param.username,
passWord: param.password
}, async (data: UserInfo) => {
//
if (!data) {
ElMessage.error("用户名或密码错误");
return;
}
ElMessage.success('登录成功');
localStorage.setItem('ms_username', data.username);
localStorage.setItem('ms_user_id', data.id);
localStorage.setItem('ms_role_id', data.roleId);
let defaultList = {};
await send_request('v1/role/list', "GET", {}, (roleList: Array<RoleInfo>) => {
for (let role of roleList) {
defaultList[role.id.toString()] = role.privileges.map((i: any) => i.module)
}
});
permiss.defaultList = defaultList;
permiss.key = defaultList[data.roleId];
if (typeof (permiss.key) === "undefined") return;
localStorage.setItem('ms_keys', JSON.stringify(permiss.key));
localStorage.setItem('ms_default_list', JSON.stringify(defaultList));
let targetRoute: any = router.currentRoute?.value?.query?.redirectTo
if (targetRoute && !targetRoute.includes('/login')) {
router.push(targetRoute);
} else {
router.push('/');
}
});
loading.close();
});
};
const tags = useTagsStore();
tags.clearTags();
</script>
<style scoped>
.login-wrap {
width: 100%;
height: 100%;
}
.login-container {
width: 100%;
height: 100%;
display: grid;
place-items: center;
}
.ms-title {
width: 100%;
padding: 18px 24px;
box-sizing: border-box;
text-align: center;
font-size: 20px;
color: #fff;
border-bottom: 1px solid #ddd;
}
.ms-login {
width: min(380px, 95vw);
padding: 5px 10px;
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;
}
.company-info {
color: #7589b6;
text-align: center;
position: absolute;
left: 0;
right: 0;
bottom: 10px;
font-size: 13px;
letter-spacing: 1px;
}
</style>

View File

@ -0,0 +1,182 @@
<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">
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>

View File

@ -0,0 +1,286 @@
<template>
<div class="container">
<div class="plugins-tips">角色及其对应的权限</div>
<!-- <div>
<span class="label">角色</span>
<el-button type="primary" :icon="Search" @click="handleNew">增加</el-button>
<el-button type="primary" :icon="Search" @click="handleNew">修改</el-button>
<el-button type="primary" :icon="Search" @click="handleNew">删除</el-button>
</div> -->
<div style="display: grid; grid-template-columns: 1fr 3fr;">
<div style="margin-top: 40px;">
<div class="mgb20">
<span class="label">角色</span>
</div>
<div>
<el-radio-group v-model="selectRoleId" style="display: block;">
<el-radio :label="role.id" size="large" v-for="role in roleList" style="display: block;"
@click="() => { }">
{{ role.roleName }}
</el-radio>
</el-radio-group>
</div>
</div>
<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>
</div>
<!-- 编辑弹出框 -->
<el-dialog :title="formId > 0 ? '编辑' : '新增'" v-model="editVisible" style="width: 30%; min-width: 280px;">
<el-form ref="editForm" label-width="80px" :rules="rules" :model="form">
<el-form-item label="角色名称" prop="roleName">
<el-input class="popup-item" v-model="form.roleName"></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input class="popup-item" v-model="form.comment"></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="addRole(editForm)"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { ElTree } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { ElLoading } from 'element-plus';
import { Delete, Edit, Search, Plus } from '@element-plus/icons-vue';
import send_request from '../utils/send_request';
const editForm = ref<FormInstance>();
console.log("editForm:", editForm)
const rules: FormRules = {
deviceName: [
{
required: true,
message: '请输入设备名称',
trigger: 'blur'
},
{
min: 2,
message: '设备名称过短',
trigger: 'blur'
},
{
max: 52,
message: '设备名称过长',
trigger: 'blur'
}
],
type: [
{
required: true,
message: '请选择设备类型',
trigger: 'blur'
}
],
deviceId: [
{
required: true,
message: '请输入设备ID',
trigger: 'blur'
},
{
min: 2,
message: '设备ID过短',
trigger: 'blur'
},
{
max: 52,
message: '设备ID过长',
trigger: 'blur'
}
],
};
let formId: number = -1;
const editVisible = ref(false);
let form = reactive({
roleName: '',
comment: '',
});
const role = ref<string>('admin');
interface Tree {
id: string;
label: string;
children?: Tree[];
}
const data: Tree[] = [
{
id: 'dashboard',
label: '系统首页',
},
{
id: '3dmodel',
label: '智慧矿山'
},
{
id: 'equipment',
label: '设备信息',
children: [
{
id: 'equipment-setting-manage',
label: '设备管理'
},
]
},
{
id: 'monitor-data',
label: '监测数据',
children: [
{
id: 'monitor-data-view',
label: '查看数据'
},
]
},
{
id: 'warning',
label: '预警信息',
children: [
{
id: 'warning-view',
label: '总览(预警信息)'
},
{
id: 'warning-setting',
label: '预警设置'
},
]
},
];
//
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 roleList = ref<any[]>();
const selectRoleId = ref(-1);
// &
const getData = async () => {
// const loading = ElLoading.service({
// lock: true,
// text: '',
// background: 'rgba(0, 0, 0, 0.7)',
// });
await send_request('v1/role/list', "GET", {
// ...query.params,
// pageIndex: query.pageIndex,
// pageSize: query.pageSize,
// page: query.pageIndex,
}, (data: any) => {
// let deviceTypeOpt = data.optionDeviceType;
console.log("role/list", data)
roleList.value = data;
selectRoleId.value = data && data.length > 0 ? data[0].id : -1;
//
// tableData.value = deviceList.list.map((i: any) => {
// i.state = "线"
// return i
// });
// pageTotal.value = deviceList.total;
// //
// deviceTypeOption.value = deviceTypeOpt;
});
// loading.close();
};
getData();
// const handleChange = (val: string[]) => {
// tree.value!.setCheckedKeys(permiss.defaultList[role.value]);
// };
//
const handleNew = () => {
formId = -1;
for (let formKeys of Object.keys(form)) {
form[formKeys] = ''
}
editVisible.value = true;
};
//
const addRole = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
console.log("addRoleformEl", formEl);
// formEl.validate(async (valid: boolean, invalidFields: any) => {
// if (!valid) {
// console.log("invalidFields", invalidFields);
// //
// Object.values(invalidFields).forEach((input: any) => {
// //
// input.forEach((element: any) => {
// ElMessage.error({
// message: element.message, grouping: true
// });
// });
// });
// return;
// }
//
await send_request('v1/role/add', "POST", {
roleName: form.roleName,
comment: form.comment,
}, (data: any) => {
console.log(data);
// editVisible.value = false;
// ElMessage.success("");
});
// }
}
</script>
<style scoped>
.tree-wrapper {
max-width: 500px;
}
.label {
font-size: 14px;
}
</style>

View File

@ -0,0 +1,221 @@
<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" @click="handleNew">新增用户</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="username" label="用户名" align="center"></el-table-column>
<el-table-column prop="roleName" label="角色" align="center"></el-table-column>
<el-table-column prop="telephone" label="电话" align="center"></el-table-column>
<el-table-column prop="roleId" label="" align="center" v-if="false"></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="'user-setting'">
编辑
</el-button>
<el-button text :icon="Delete" class="red" @click="handleDelete(scope.$index, scope.row)"
v-permiss="'user-setting'">
删除
</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.username"></el-input>
</el-form-item>
<el-form-item label="角色">
<el-select v-model="form.roleName" placeholder="角色类型" class="handle-select mr10">
<el-option key="1" label="管理员" value="管理员"></el-option>
<el-option key="2" label="普通用户" value="普通用户"></el-option>
</el-select>
</el-form-item>
<el-form-item label="电话">
<el-input v-model="form.telephone"></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">
import { ref, reactive } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ElLoading } from 'element-plus';
import { Delete, Edit, Search, Plus } from '@element-plus/icons-vue';
import send_request from '../utils/send_request';
const query = reactive({
id: 0,
address: '',
name: '',
roleId: 0,
pageIndex: 1,
pageSize: 5
});
const tableData: any = ref([]);
const pageTotal = ref(0);
//
const getData = async () => {
const loading = ElLoading.service({
lock: true,
text: '请稍候',
background: 'rgba(0, 0, 0, 0.7)',
});
await send_request('v1/user/list', "GET", {
page: query.pageIndex,
pageSize: query.pageSize
}, (data: any) => {
console.log(data)
tableData.value = data.list;
pageTotal.value = data.total;
});
loading.close();
};
getData();
//
const handleSearch = () => {
query.pageIndex = 1;
getData();
};
//
const handlePageChange = (val: number) => {
query.pageIndex = val;
getData();
};
//
const handleDelete = (index: number, row: any) => {
idx = index;
//
ElMessageBox.confirm('确定要删除吗?', '提示', {
type: 'warning'
})
.then(async () => {
await send_request('v1/user/delete', "POST", {
userId: row.id
}, (data: any) => {
// console.log("delete index", index);
// console.log(data);
if (data) {
ElMessage.success('删除成功');
tableData.value.splice(idx, 1);
}
});
})
.catch(() => { });
};
//
const editVisible = ref(false);
let form = reactive({
id: 0,
username: '',
roleId: 0,
roleName: '',
telephone: ''
});
let idx: number = -1;
//
const handleEdit = (index: number, row: any) => {
idx = index;
form.id = row.id;
form.username = row.username;
form.roleName = row.roleName;
form.roleId = row.roleId;
form.telephone = row.telephone;
editVisible.value = true;
};
//
const saveEdit = async () => {
editVisible.value = false;
await send_request('v1/user/edit', "POST", {
id: form.id,
username: form.username,
roleId: form.roleId,
roleName: form.roleName,
telephone: form.telephone
}, (data: any) => {
if (data) {
ElMessage.success('修改成功');
tableData.value[idx].username = form.username;
tableData.value[idx].roleName = form.roleName;
tableData.value[idx].telephone = form.telephone;
}
});
};
const handleNew = () => {
editVisible.value = true;
ElMessage.success(`修改第 ${idx + 1} 行成功`);
tableData.value[idx].username = form.username;
tableData.value[idx].roleName = form.roleName;
tableData.value[idx].telephone = form.telephone;
};
</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>

230
frontend/src/views/user.vue Normal file
View File

@ -0,0 +1,230 @@
<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 type="password" v-model="form.new1"></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';
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
import send_request from '../utils/send_request';
const name = localStorage.getItem('ms_username');
const user_id = localStorage.getItem('ms_user_id');
const form = reactive({
user_id: user_id,
old: '',
new: '',
new1: '',
desc: ''
});
const onSubmit = async () => {
if (form.old == '' || form.new == '' || form.new1 == '') {
//
ElMessageBox.confirm('输入为空,请检查', '提示', {
type: 'warning'
})
return
} else if (form.new != form.new1) {
//
ElMessageBox.confirm('新密码2次输入的不相同', '提示', {
type: 'warning'
})
return
} else if (form.new == form.old) {
//
ElMessageBox.confirm('新、旧密码相同', '提示', {
type: 'warning'
})
return
}
ElMessageBox.confirm('确认要修改密码吗?', '提示', {
type: 'warning'
})
.then(async () => {
console.log("send_request v1/user/alterPSW")
const loading = ElLoading.service({
lock: true,
text: '请稍候',
background: 'rgba(0, 0, 0, 0.7)',
});
await send_request('v1/user/alterPSW', "POST", {
"userId": form.user_id,
"oldPSW": form.old,
"newPSW": form.new
}, (data: any) => {
console.log(data);
ElMessage.success('删除成功');
// tableData.value.splice(index, 1);
})
loading.close();
})
.catch(() => {
ElMessage.success('删除失败');
});
return;
};
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>

View File

@ -0,0 +1,302 @@
<template>
<div class="container">
<!-- 调试用 -->
<div v-if="false" style="background-color: lightgrey;">
<p>treeSelectItem: {{ treeSelectItem }}</p>
<p>monitorCodeList: {{ monitorCodeList }}</p>
<p>selectIndex: {{ selectIndex }}</p>
<p>form: {{ form }}</p>
</div>
<div style="display: grid; grid-template-columns: 250px 2fr;">
<!-- 左侧 -->
<div>
<!-- 树状结构 -->
<el-input v-model="treeFilterText" placeholder="输入关键字以搜索..." style="margin-bottom: 10px;" />
<el-tree ref="treeRef" :data="treeData" :props="defaultProps" :filter-node-method="treeFilterNode"
@node-click="handleNodeClick" default-expand-all highlight-current :expand-on-click-node="false" />
</div>
<!-- 右侧 -->
<div v-if="treeSelectItem">
<div style="text-align: center;">
<el-radio-group v-model="selectIndex" size="large" style="margin: 10px 0;">
<el-radio-button v-for="monitorCode in monitorCodeList" :label="monitorCode" />
</el-radio-group>
</div>
<el-divider border-style="dotted">
<p>
<!-- <el-icon style="vertical-align: text-top;"><star-filled /></el-icon> -->
您正在配置{{ treeSelectItem.displayMsg }}
</p>
</el-divider>
<!-- form表单 -->
<el-form :model="form" label-width="120px">
<template v-for="i in form">
<el-form-item :label="i._title">
<el-switch v-model="i.Enable" />
</el-form-item>
<!-- v-if="i.Enable" -->
<el-form-item label="预警范围">
<el-col :span="10">
<el-input-number v-model="i.Lower" :disabled="!i.Enable" style="width: 100%" />
</el-col>
<el-col :span="2" style="text-align: center;">
<span class="text-gray-500">-</span>
</el-col>
<el-col :span="10">
<el-input-number v-model="i.Upper" :disabled="!i.Enable" style="width: 100%" />
</el-col>
<el-col :span="2" style="text-align: center;">
<el-button v-if="i.Lower || i.Upper" type="danger" :icon="Delete" circle
:disabled="!i.Enable" @click="i.Enable = false; i.Lower = null; i.Upper = null;" />
</el-col>
</el-form-item>
</template>
<el-form-item>
<el-button type="primary" @click="onSubmit">保存</el-button>
</el-form-item>
</el-form>
</div>
<div v-else style="padding-top: 11vh;">
<el-empty description="请在左侧选择设备" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, computed, reactive } from 'vue'
import { ElTree, ElLoading, ElMessage } from 'element-plus'
import send_request from '../utils/send_request';
import { Delete } from '@element-plus/icons-vue'
//
interface Tree {
id: number
label: string
children?: Tree[]
detail?: any
}
//
const treeFilterText = ref('')
//
const treeData: any = ref([]);
//
const treeSelectItem: any = ref(null);
//
const treeRef = ref<InstanceType<typeof ElTree>>()
//
const allMonitorCodeMap: any = ref({});
//
const monitorCodeList: any = computed(() => {
if (treeSelectItem.value) {
console.log("monitorCodeList computed", allMonitorCodeMap.value, treeSelectItem.value.typeId)
let newRadioGroupList = allMonitorCodeMap.value[`${treeSelectItem.value.typeId}`]
if (newRadioGroupList.length > 0 && newRadioGroupList.indexOf(selectIndex.value) == -1) {
// newRadioGroupList monitorCode newRadioGroupList
selectIndex.value = newRadioGroupList[0]
}
return newRadioGroupList
} else {
return [];
}
});
//
const selectIndex: any = ref('');
//
watch(treeFilterText, (val) => {
treeRef.value!.filter(val)
})
//
const treeFilterNode = (value: string, data: Tree) => {
if (!value) return true
return data.label.includes(value) // || data.id === -1
}
const handleNodeClick = (data: Tree) => {
console.log("handleNodeClick", data)
treeSelectItem.value = data.detail ? data.detail : null;
}
const getTreeData = async () => {
const loading = ElLoading.service({
lock: true,
text: '请稍候',
background: 'rgba(0, 0, 0, 0.7)',
});
await send_request('v1/device/listAll', "GET", {
}, (data: any) => {
console.log("device/listAll", data)
let deviceTypeList = data.deviceType;
let deviceList = data.list;
let transData = []
for (let deviceType of deviceTypeList) {
transData.push({
id: deviceType.id,
label: deviceType.typeName,
children: [
{
id: `${deviceType.id}_0`,
label: "(默认)",
detail: {
deviceId: -1,
typeId: deviceType.id,
deviceName: `(默认)`,
displayMsg: ` ${deviceType.typeName}设备 默认设置`,
},
},
...deviceList.filter((device: any) => device.typeId === deviceType.id)
.map((device: any) => {
return {
id: `${deviceType.id}_${device.id}`,
label: device.name,
detail: {
deviceId: device.id,
typeId: device.typeId,
deviceName: device.name,
displayMsg: ` ${deviceType.typeName}设备:${device.name}`,
},
}
})
],
})
}
treeData.value = transData;
});
await send_request('v1/alert-model/getDeviceTypeToMonitorCodeMap', "GET", {
}, (data: any) => {
console.log("alert-model/getDeviceTypeToMonitorCodeMap", data)
allMonitorCodeMap.value = data;
});
loading.close();
};
getTreeData();
// Tree使
const defaultProps = {
children: 'children',
label: 'label',
}
watch([treeSelectItem, selectIndex], ([foo, bar], [prevFoo, prevBar]) => {
if (!treeSelectItem.value) {
return;
}
console.log("所选模型发生改变需要重新获取AlertModel")
getAlertModel()
})
const getAlertModel = async () => {
const loading = ElLoading.service({
lock: true,
text: '请稍候',
background: 'rgba(0, 0, 0, 0.7)',
});
// isDefault -> true ; -> false
const isDefault = treeSelectItem.value.deviceId < 0;
const postParams = {
modelType: isDefault ? "default" : "specific",
deviceId: isDefault ? undefined : treeSelectItem.value.deviceId,
deviceTypeId: isDefault ? treeSelectItem.value.typeId : undefined,
monitorCode: selectIndex.value,
};
console.log("getAlertModel -> postParams", postParams);
await send_request('v1/alert-model/getAlertModel', "POST", postParams, (data: any) => {
console.log("alert-model/getAlertModel", data)
form.y.Enable = !!data.yellowIsAlert;
form.y.Lower = data.yellowLowerBound;
form.y.Upper = data.yellowUpperBound;
form.o.Enable = !!data.orangeIsAlert;
form.o.Lower = data.orangeLowerBound;
form.o.Upper = data.orangeUpperBound;
form.r.Enable = !!data.redIsAlert;
form.r.Lower = data.redLowerBound;
form.r.Upper = data.redUpperBound;
});
loading.close();
}
// Form
const form = reactive({
y: {
_title: "黄色预警",
Enable: false,
Lower: null,
Upper: null,
},
o: {
_title: "橙色预警",
Enable: false,
Lower: null,
Upper: null,
},
r: {
_title: "红色预警",
Enable: false,
Lower: null,
Upper: null,
}
})
const onSubmit = async () => {
console.log('submit!')
const loading = ElLoading.service({
lock: true,
text: '请稍候',
background: 'rgba(0, 0, 0, 0.7)',
});
// isDefault -> true ; -> false
const isDefault = treeSelectItem.value.deviceId < 0;
const postParams = {
modelType: isDefault ? "default" : "specific",
deviceId: isDefault ? undefined : treeSelectItem.value.deviceId,
deviceTypeId: isDefault ? treeSelectItem.value.typeId : undefined,
monitorCode: selectIndex.value,
yLower: form.y.Lower,
yUpper: form.y.Upper,
yEnable: form.y.Enable,
oLower: form.o.Lower,
oUpper: form.o.Upper,
oEnable: form.o.Enable,
rLower: form.r.Lower,
rUpper: form.r.Upper,
rEnable: form.r.Enable,
};
console.log("setAlertModel -> postParams", postParams);
await send_request('v1/alert-model/setAlertModel', "POST", postParams, (data: any) => {
console.log("v1/alert-model/setAlertModel", data)
if (data) {
ElMessage.success('保存成功');
} else {
ElMessage.error('保存失败');
}
});
loading.close();
}
</script>
<style>
.el-tree-node__content {
height: 36px !important;
}
</style>
<style scoped></style>

View File

@ -0,0 +1,396 @@
<template>
<div class="container">
<el-row :gutter="20">
<el-col :span="18">
<el-row :gutter="20" class="mgb20">
<el-col :span="6">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-0">
<el-icon class="grid-con-icon">
<Odometer />
</el-icon>
<div class="grid-cont-right">
<div class="grid-num">{{ levelCount?.today_total }}</div>
<div>今日累计预警</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<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">{{ levelCount?.today_y }}</div>
<div>黄色预警</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<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">{{ levelCount?.today_o }}</div>
<div>橙色预警</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<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">{{ levelCount?.today_r }}</div>
<div>红色预警</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card shadow="hover" style="min-height: 403px">
<template #header>
<div class="clearfix">
<span>预警列表</span>
<el-button style="float: right; padding: 3px 0" text>查看全部</el-button>
</div>
</template>
<!-- 表格 -->
<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" v-if="false"></el-table-column>
<el-table-column prop="createTime" label="报警时间" align="center"></el-table-column>
<el-table-column prop="deviceName" label="报警设备" align="center"></el-table-column>
<el-table-column prop="alertLevel" label="报警等级" align="center"></el-table-column>
<el-table-column prop="monitorCode" label="监测项" align="center"></el-table-column>
<el-table-column prop="monitorValue" label="监测值" align="center"></el-table-column>
<el-table-column prop="threshold" label="阈值" align="center"></el-table-column>
<el-table-column prop="deviceId" label="" align="center" v-if="false"></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="'user-setting'">
编辑
</el-button>
<el-button text :icon="Delete" class="red" @click="handleDelete(scope.$index, scope.row)"
v-permiss="'user-setting'">
删除
</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>
<!-- <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-col :span="6">
<el-card shadow="hover" class="mgb20">
<p>
监测预警总览
</p>
<div class="alert-image-container">
<img class="alert-image" :class="isAlerting ? 'alert-ing' : []"
:src="isAlerting ? '/assets/image/svg/alert_warning.svg' : '/assets/image/svg/alert_default.svg'"
@click="isAlerting = !isAlerting">
</div>
<!-- 表格 -->
<el-table :data="alertLogCountTableData" style="width: 100%">
<el-table-column prop="timeRange" label="" />
<el-table-column prop="yellow" label="黄色预警" />
<el-table-column prop="orange" label="橙色预警" />
<el-table-column prop="red" label="红色预警" />
<el-table-column prop="total" label="总计" />
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts" name="dashboard">
import { ref, reactive } from 'vue';
import send_request from '../utils/send_request';
const name = localStorage.getItem('ms_username');
const role: string = name === 'admin' ? '超级管理员' : '普通用户';
//
const query = reactive({
params: {
deviceType: '',
name: '',
},
pageIndex: 1,
pageSize: 10
});
//
const isAlerting = ref(false);
const alertLevel = {
"1": "黄色预警",
"2": "橙色预警",
"3": "红色预警",
}
const tableData: any = ref([]);
const pageTotal = ref(0);
const levelCount = ref({
'today_y': '',
'today_o': '',
'today_r': '',
'today_total': '',
});
//
const alertLogCountTableData = ref([]);
const getData = async () => {
// const loading = ElLoading.service({
// lock: true,
// text: '',
// background: 'rgba(0, 0, 0, 0.7)',
// });
await send_request('v1/alert/list', "GET", {
...query.params,
pageIndex: query.pageIndex,
pageSize: query.pageSize,
page: query.pageIndex,
}, (data: any) => {
let alertList = data;
//""
//console.log("deviceList/deviceTypeList:", deviceList, deviceTypeList)
// deviceTypeList.forEach((item: any) => deviceTypeDict.value[item.id] = item.type)
//
tableData.value = alertList.list.map((i: any) => {
i.state = "在线"
i.alertLevel = alertLevel[i.alertLevel]
i.createTime = new Date(new Date(i.createTime).getTime() + 8 * 3600 * 1000).toISOString().substring(0, 19).replace('T', ' ')
return i
});
pageTotal.value = alertList.total;
// //
// deviceTypeOption.value = deviceTypeList;
});
//
await send_request('v1/chart-data/levelCount', "GET", {
}, (data: any) => {
console.log("v1/chart-data/levelCount:", data);
let timeRangeDict = {
"day": "今日",
"week": "本周",
"month": "本月",
"year": "今年",
"total": "累计",
}
alertLogCountTableData.value = data.map((row: any) => {
row.total = row.yellow + row.orange + row.red;
if (row.timeRange == "day") {
levelCount.value.today_y = row.yellow;
levelCount.value.today_o = row.orange;
levelCount.value.today_r = row.red;
levelCount.value.today_total = row.total;
console.log(levelCount.value)
}
row.timeRange = timeRangeDict[row.timeRange]
return row
})
console.log("alertLogCountTableData.value", alertLogCountTableData.value);
});
// loading.close();
};
getData();
//
const handlePageChange = (val: number) => {
query.pageIndex = val;
getData();
};
</script>
<style scoped>
/* 警铃抖动样式 */
@keyframes shaking {
0% {
transform: rotate(0deg);
}
35% {
transform: rotate(20deg);
}
65% {
transform: rotate(-20deg);
}
100% {
transform: rotate(0deg);
}
}
.alert-image-container {
text-align: center;
}
.alert-image {
width: 60%;
}
.alert-ing {
animation-duration: 1s;
animation-name: shaking;
animation-direction: normal;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.76, 0.44, 0.33, 0.75);
animation-delay: 0.8s;
}
/* ############ */
.el-row {
margin-bottom: 20px;
}
.grid-content {
display: flex;
align-items: center;
--grid-content-height: min(150px, 6vw);
height: var(--grid-content-height);
}
.grid-cont-right {
flex: 1;
text-align: center;
font-size: 14px;
color: #999;
}
.grid-num {
font-size: min(70px, 4vw);
font-weight: bold;
}
.grid-con-icon {
font-size: min(50px, 2.8vw);
width: min(150px, 5vw);
height: var(--grid-content-height);
text-align: center;
line-height: 100px;
color: #fff;
}
.grid-con-0 .grid-con-icon {
background: rgb(43 157 255);
}
.grid-con-0 .grid-num {
color: rgb(43 157 255);
}
.grid-con-1 .grid-con-icon {
background: rgb(255 223 43);
}
.grid-con-1 .grid-num {
color: rgb(255 223 43);
}
.grid-con-2 .grid-con-icon {
background: rgb(255, 135, 0);
}
.grid-con-2 .grid-num {
color: rgb(255, 135, 0);
}
.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;
}
</style>

10
frontend/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';

32
frontend/tsconfig.json Normal file
View File

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

View File

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

28
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,28 @@
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';
import cesium from 'vite-plugin-cesium';
export default defineConfig({
base: './',
server: {
host: '0.0.0.0', // 允许局域网访问
},
plugins: [
vue(),
VueSetupExtend(),
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
}),
cesium()
],
optimizeDeps: {
include: ['schart.js']
}
});