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

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.removeEventListener它们都接受3个参数
// ("事件名" , "事件处理函数" , "布尔值");
// 在body上添加事件处理程序
document.body.addEventListener("click", this.closeMenu);
} else {
//菜单隐藏的时候
// 移除body上添加的事件处理程序
document.body.removeEventListener("click", this.closeMenu);
}
},
},
methods: {
//右击
openMenu(e) {
var x = e.pageX; //这个应该是相对于整个浏览器页面的x坐标左上角为坐标原点0,0
var y = e.pageY; //这个应该是相对于整个浏览器页面的y坐标左上角为坐标原点0,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';