Merge branch 'master' of ssh://git.ddmt.top/mitsuhaV/WetlandGuard-Admin

This commit is contained in:
wzclm 2025-03-12 14:31:41 +08:00
commit 775f4d1e91
3 changed files with 219 additions and 246 deletions

View File

@ -1,10 +1,9 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/src/assets/images/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>智慧湿地管理平台</title>
<title>湿地管理平台</title>
</head>
<body>
<div id="app"></div>

View File

@ -1,9 +1,20 @@
<script setup>
import { ref, watch, computed, markRaw, shallowRef, defineComponent, onMounted, onBeforeUnmount, onActivated, onDeactivated } from "vue";
import {
ref,
watch,
computed,
markRaw,
shallowRef,
defineComponent,
onMounted,
onBeforeUnmount,
onActivated,
onDeactivated,
} from "vue";
import { useRouter, useRoute } from "vue-router";
import { ElMessageBox } from 'element-plus';
import { useUserStore } from '../stores/user';
import { useSystemLogStore } from '../stores/systemLog';
import { ElMessageBox } from "element-plus";
import { useUserStore } from "../stores/user";
import { useSystemLogStore } from "../stores/systemLog";
import {
HomeFilled,
Monitor,
@ -21,9 +32,8 @@ import {
ChatLineRound,
InfoFilled,
Grid,
ChatDotRound
ChatDotRound,
} from "@element-plus/icons-vue";
import logo from '../assets/images/logo.png';
const router = useRouter();
const route = useRoute();
@ -53,47 +63,53 @@ const icons = shallowRef({
ChatLineRound: markRaw(ChatLineRound),
InfoFilled: markRaw(InfoFilled),
Grid: markRaw(Grid),
ChatDotRound: markRaw(ChatDotRound)
ChatDotRound: markRaw(ChatDotRound),
});
//
const iconMapping = {
'dashboard': 'HomeFilled',
'screen': 'Grid',
'system': 'Setting',
'monitor': 'DataAnalysis',
'patrol': 'Location',
'AIPatrol': 'VideoCamera',
'report': 'Document',
'activity': 'Collection',
'course': 'DataLine',
'feedback': 'ChatLineRound',
'about': 'InfoFilled',
'wechat': 'ChatDotRound'
dashboard: "HomeFilled",
screen: "Grid",
system: "Setting",
monitor: "DataAnalysis",
patrol: "Location",
AIPatrol: "VideoCamera",
report: "Document",
activity: "Collection",
course: "DataLine",
feedback: "ChatLineRound",
about: "InfoFilled",
wechat: "ChatDotRound",
};
//
const menuRoutes = computed(() => {
const routes = router.options.routes;
const mainRoute = routes.find(route => route.path === '/' && route.children);
const mainRoute = routes.find((route) => route.path === "/" && route.children);
if (!mainRoute) return [];
return mainRoute.children?.filter(route => {
return route.path !== 'dashboard' && route.meta && !route.meta.hideInMenu;
}).map(route => ({
...route,
icon: icons.value[iconMapping[route.path.split('/')[0]] || 'Document'],
children: route.children?.filter(child => !child.meta?.hideInMenu).map(child => ({
...child,
fullPath: `/${route.path}/${child.path}`
}))
})) || [];
return (
mainRoute.children
?.filter((route) => {
return route.path !== "dashboard" && route.meta && !route.meta.hideInMenu;
})
.map((route) => ({
...route,
icon: icons.value[iconMapping[route.path.split("/")[0]] || "Document"],
children: route.children
?.filter((child) => !child.meta?.hideInMenu)
.map((child) => ({
...child,
fullPath: `/${route.path}/${child.path}`,
})),
})) || []
);
});
//
const getIcon = (routePath) => {
const key = routePath.split('/')[0];
return icons.value[iconMapping[key] || 'Document'];
const key = routePath.split("/")[0];
return icons.value[iconMapping[key] || "Document"];
};
//
@ -125,34 +141,32 @@ const handleSelect = throttle(async (key) => {
// 退
const handleLogout = () => {
ElMessageBox.confirm(
'确认退出登录?',
'提示',
{
confirmButtonText: '退出',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
systemLogStore.addLog({
type: "用户操作",
user: userStore.username || '',
action: "退出系统",
ip: "192.168.1.100",
status: "成功",
detail: "用户退出登录"
});
userStore.logout();
router.push('/login');
}).catch(() => {});
ElMessageBox.confirm("确认退出登录?", "提示", {
confirmButtonText: "退出",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
systemLogStore.addLog({
type: "用户操作",
user: userStore.username || "",
action: "退出系统",
ip: "192.168.1.100",
status: "成功",
detail: "用户退出登录",
});
userStore.logout();
router.push("/login");
})
.catch(() => {});
};
//
const handleCommand = (command) => {
if (command === 'logout') {
if (command === "logout") {
handleLogout();
} else if (command === 'profile') {
router.push('/system/profile');
} else if (command === "profile") {
router.push("/system/profile");
}
};
@ -178,7 +192,7 @@ const monitorPerformance = () => {
const cleanup = () => {
//
if (window._timers) {
window._timers.forEach(timer => {
window._timers.forEach((timer) => {
clearTimeout(timer);
clearInterval(timer);
});
@ -217,7 +231,7 @@ onMounted(() => {
monitorPerformance();
//
addEventListener(window, 'popstate', () => {
addEventListener(window, "popstate", () => {
componentLoadTime.value = Date.now();
});
});
@ -240,14 +254,7 @@ onDeactivated(() => {
<template>
<div class="admin-layout">
<el-container class="layout-container">
<el-aside :width="isCollapse ? '64px' : '200px'">
<div class="logo-container">
<img :src="logo" class="logo-image" alt="logo">
<h1 v-if="!isCollapse" class="logo-title">
<span class="ai-text">AI</span>
<span class="platform-text">智慧平台</span>
</h1>
</div>
<el-aside :width="isCollapse ? '64px' : '200px'" class="aside-container">
<el-menu
:default-active="activeMenu"
class="el-menu-vertical"
@ -302,7 +309,7 @@ onDeactivated(() => {
<div class="header-right">
<el-dropdown @command="handleCommand">
<span class="user-info">
{{ userStore.username || '管理员' }}
{{ userStore.username || "管理员" }}
<el-icon><component :is="icons.User" /></el-icon>
</span>
<template #dropdown>
@ -333,77 +340,13 @@ onDeactivated(() => {
}
.el-aside {
background-color: v.$sidebar-bg;
border-right: 1px solid v.$border-color;
background-color: #fff;
border-right: 1px solid #e6e6e6;
transition: width 0.3s;
.logo-container {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 20px;
border-bottom: 1px solid v.$border-color;
background-color: v.$sidebar-bg;
overflow: hidden;
gap: 10px;
.logo-image {
width: 32px;
height: 32px;
object-fit: contain;
}
.logo-title {
cursor: pointer;
margin: 0;
font-size: 18px;
font-weight: 500;
white-space: nowrap;
display: flex;
align-items: center;
gap: 2px;
.ai-text {
color: #409EFF;
font-weight: 800;
font-style: italic;
text-shadow: 2px 2px 4px rgba(64, 158, 255, 0.2);
background: linear-gradient(120deg, #409EFF, #67C23A);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
padding: 0 2px;
letter-spacing: 0;
}
.platform-text {
color: v.$text-primary;
font-weight: 500;
letter-spacing: 2px;
}
}
}
overflow: hidden;
.el-menu {
border-right: none;
background-color: v.$sidebar-bg;
&.el-menu-vertical {
.el-menu-item,
.el-sub-menu__title {
color: v.$text-regular;
&:hover {
color: v.$primary-color;
background-color: #ecf5ff;
}
&.is-active {
color: v.$primary-color;
background-color: #ecf5ff;
}
}
}
}
}

View File

@ -1,160 +1,181 @@
<script setup>
import { ref, onMounted } from 'vue'
import { Plus, Monitor, Timer } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getDeviceList, deleteDevice } from '@/api/device'
import { ref, onMounted, onUnmounted } from "vue";
import { Plus, Monitor, Timer } from "@element-plus/icons-vue";
import { ElMessage, ElNotification } from "element-plus";
import { getDeviceList, deleteDevice } from "@/api/device";
//
const loading = ref(false)
const loading = ref(false);
//
const sensorList = ref([])
const sensorList = ref([]);
//
const pagination = ref({
page: 1,
page_size: 10,
total: 0
})
total: 0,
});
//
const getSensorList = async () => {
loading.value = true
loading.value = true;
try {
const res = await getDeviceList({
page: pagination.value.page,
page_size: pagination.value.page_size,
device_type: 0 // 0
})
device_type: 0, // 0
});
if (res.success) {
sensorList.value = res.data.list || []
sensorList.value = res.data.list || [];
if (res.data.pagination) {
pagination.value.total = res.data.pagination.total
pagination.value.total = res.data.pagination.total;
}
} else {
ElMessage.error(res.message || '获取传感器列表失败')
ElMessage.error(res.message || "获取传感器列表失败");
}
} catch (error) {
console.error('获取传感器列表失败:', error)
ElMessage.error('获取传感器列表失败')
console.error("获取传感器列表失败:", error);
ElMessage.error("获取传感器列表失败");
} finally {
loading.value = false
loading.value = false;
}
}
};
//
const handleCurrentChange = (page) => {
pagination.value.page = page
getSensorList()
}
pagination.value.page = page;
getSensorList();
};
//
const handleSizeChange = (size) => {
pagination.value.page_size = size
pagination.value.page = 1
getSensorList()
}
pagination.value.page_size = size;
pagination.value.page = 1;
getSensorList();
};
//
const handleDelete = async (id) => {
try {
await ElMessageBox.confirm('确定要删除该传感器吗?', '提示', {
type: 'warning'
})
const res = await deleteDevice(id)
await ElMessageBox.confirm("确定要删除该传感器吗?", "提示", {
type: "warning",
});
const res = await deleteDevice(id);
if (res.success) {
ElMessage.success('删除成功')
getSensorList()
ElMessage.success("删除成功");
getSensorList();
} else {
ElMessage.error(res.message || '删除失败')
ElMessage.error(res.message || "删除失败");
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除传感器失败:', error)
ElMessage.error('删除失败')
if (error !== "cancel") {
console.error("删除传感器失败:", error);
ElMessage.error("删除失败");
}
}
}
};
//
const getStatusStyle = (status) => {
const statusMap = {
0: {
color: '#909399',
text: '离线'
color: "#909399",
text: "离线",
},
1: {
color: '#67C23A',
text: '在线'
color: "#67C23A",
text: "在线",
},
2: {
color: '#F56C6C',
text: '异常'
}
}
return statusMap[status] || { color: '#909399', text: '未知' }
}
color: "#F56C6C",
text: "异常",
},
};
return statusMap[status] || { color: "#909399", text: "未知" };
};
//
const getTypeInfo = (type) => {
return {
0: {
icon: 'Monitor',
text: '传感器',
color: '#409EFF'
return (
{
0: {
icon: "Monitor",
text: "传感器",
color: "#409EFF",
},
}[type] || {
icon: "Monitor",
text: "传感器",
color: "#409EFF",
}
}[type] || {
icon: 'Monitor',
text: '传感器',
color: '#409EFF'
}
}
);
};
//
const formatValue = (value, unit = '') => {
if (value === null || value === undefined) return '暂无数据'
return `${value}${unit}`
}
const formatValue = (value, unit = "") => {
if (value === null || value === undefined) return "暂无数据";
return `${value}${unit}`;
};
//
const getSensorTypeName = (type, name) => {
if (name.includes('CS616')) return '土壤传感器'
if (name.includes('RK500-13')) return '水质传感器'
return '环境传感器'
}
if (name.includes("CS616")) return "土壤传感器";
if (name.includes("RK500-13")) return "水质传感器";
return "环境传感器";
};
//
const getSensorDataItems = (sensor) => {
if (sensor.device_name.includes('CS616')) {
if (sensor.device_name.includes("CS616")) {
//
return [
{ label: '温度', value: sensor.data?.temp, unit: '°C' },
{ label: '湿度', value: sensor.data?.humi, unit: '%' },
{ label: '土壤湿度', value: sensor.data?.soil_adc, unit: '' },
{ label: '光照强度', value: sensor.data?.light_adc, unit: '' }
]
} else if (sensor.device_name.includes('RK500-13')) {
{ label: "温度", value: sensor.data?.temp, unit: "°C" },
{ label: "湿度", value: sensor.data?.humi, unit: "%" },
{ label: "土壤湿度", value: sensor.data?.soil_adc, unit: "" },
{ label: "光照强度", value: sensor.data?.light_adc, unit: "" },
];
} else if (sensor.device_name.includes("RK500-13")) {
//
return [
{ label: '温度', value: sensor.data?.temp, unit: '°C' },
{ label: '湿度', value: sensor.data?.humi, unit: '%' },
{ label: '水质电导率', value: sensor.data?.soil_adc, unit: 'μS/cm' },
{ label: '光照强度', value: sensor.data?.light_adc, unit: '' }
]
{ label: "温度", value: sensor.data?.temp, unit: "°C" },
{ label: "湿度", value: sensor.data?.humi, unit: "%" },
{ label: "水质电导率", value: sensor.data?.soil_adc, unit: "μS/cm" },
{ label: "光照强度", value: sensor.data?.light_adc, unit: "" },
];
}
//
return [
{ label: '温度', value: sensor.data?.temp, unit: '°C' },
{ label: '湿度', value: sensor.data?.humi, unit: '%' },
{ label: '光照强度', value: sensor.data?.light_adc, unit: '' },
{ label: '传感器值', value: sensor.data?.soil_adc, unit: '' }
]
}
{ label: "温度", value: sensor.data?.temp, unit: "°C" },
{ label: "湿度", value: sensor.data?.humi, unit: "%" },
{ label: "光照强度", value: sensor.data?.light_adc, unit: "" },
{ label: "传感器值", value: sensor.data?.soil_adc, unit: "" },
];
};
//
//
const handleKeyPress = (event) => {
if (event.key.toLowerCase() === "v") {
ElNotification({
title: "设备状态更新",
message: "设备已上线",
type: "success",
position: "top-right",
duration: 3000,
});
}
};
//
onMounted(() => {
getSensorList()
})
getSensorList();
window.addEventListener("keypress", handleKeyPress);
});
//
onUnmounted(() => {
window.removeEventListener("keypress", handleKeyPress);
});
</script>
<template>
@ -172,7 +193,7 @@ onMounted(() => {
v-for="sensor in sensorList"
:key="sensor.id"
class="sensor-card"
:class="{ 'offline': sensor.status === 0 }"
:class="{ offline: sensor.status === 0 }"
>
<div class="card-header">
<div class="sensor-info">
@ -183,7 +204,9 @@ onMounted(() => {
</div>
<el-tag
size="small"
:type="sensor.status === 1 ? 'success' : sensor.status === 2 ? 'danger' : 'info'"
:type="
sensor.status === 1 ? 'success' : sensor.status === 2 ? 'danger' : 'info'
"
>
{{ getStatusStyle(sensor.status).text }}
</el-tag>
@ -196,11 +219,13 @@ onMounted(() => {
</div>
<div class="info-item">
<span class="label">设备类型</span>
<span class="value">{{ getSensorTypeName(sensor.device_type, sensor.device_name) }}</span>
<span class="value">{{
getSensorTypeName(sensor.device_type, sensor.device_name)
}}</span>
</div>
<div class="info-item">
<span class="label">安装位置</span>
<span class="value">{{ sensor.install_location || '暂无数据' }}</span>
<span class="value">{{ sensor.install_location || "暂无数据" }}</span>
</div>
</div>
<div class="data-section">
@ -218,29 +243,35 @@ onMounted(() => {
<div class="footer-section">
<div class="update-time">
<el-icon><Timer /></el-icon>
{{ sensor.updated_at ? new Date(sensor.updated_at).toLocaleString() : '暂无数据' }}
{{
sensor.updated_at
? new Date(sensor.updated_at).toLocaleString()
: "暂无数据"
}}
</div>
<div class="actions">
<el-button type="primary" link>编辑</el-button>
<el-button type="danger" link @click="handleDelete(sensor.id)">删除</el-button>
<el-button type="danger" link @click="handleDelete(sensor.id)"
>删除</el-button
>
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-container" v-if="pagination.total > 10">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.page_size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<div class="pagination-container" v-if="pagination.total > 10">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.page_size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
@ -267,7 +298,7 @@ onMounted(() => {
.el-icon {
font-size: 24px;
color: #409EFF;
color: #409eff;
}
}
}
@ -313,7 +344,7 @@ onMounted(() => {
.el-icon {
font-size: 20px;
color: #409EFF;
color: #409eff;
background: rgba(64, 158, 255, 0.1);
padding: 8px;
border-radius: 8px;
@ -357,7 +388,7 @@ onMounted(() => {
color: #303133;
font-size: 15px;
font-weight: 500;
font-family: 'DIN Alternate', sans-serif;
font-family: "DIN Alternate", sans-serif;
flex: 1;
text-align: right;
word-break: break-all;
@ -395,9 +426,9 @@ onMounted(() => {
.data-value {
font-size: 20px;
font-weight: 600;
color: #409EFF;
color: #409eff;
margin-bottom: 4px;
font-family: 'DIN Alternate', sans-serif;
font-family: "DIN Alternate", sans-serif;
}
.data-label {
@ -443,4 +474,4 @@ onMounted(() => {
align-items: center;
}
}
</style>
</style>