This commit is contained in:
Xiaoyu 2025-02-05 21:57:40 +08:00
commit 89b121c08c
44 changed files with 9302 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

239
README.md Normal file
View File

@ -0,0 +1,239 @@
# 智慧湿地管理平台
> 基于Node.js的智慧湿地管理系统后端服务
## 项目概述
本系统是一个基于AI技术的智慧湿地管理平台集成了物种识别、环境监测、数据分析、智能巡护和公众服务等功能旨在提供全方位的湿地生态系统管理解决方案。
## 系统角色与功能实现
系统包含三种用户角色,每个角色具有不同的权限和具体功能实现:
### 1. 管理员(超级管理员)
#### 1.1 系统管理权限
- 用户管理
- 创建/删除/修改用户账号:统一的用户管理界面,可进行批量操作
- 分配用户角色基于RBAC权限模型灵活分配角色权限
- 重置用户密码:自动生成临时密码并通过安全通道发送给用户
- 管理用户状态:可设置用户账号有效期,自动/手动启用禁用账号
- 系统配置管理
- 系统参数设置:包括数据采集频率、存储策略、系统阈值等配置
- AI模型配置可更新AI识别模型调整模型参数设置识别阈值
- 预警阈值设置:针对不同监测指标设置多级预警阈值
- 系统日志查看:记录所有操作日志,支持多维度查询和导出
#### 1.2 数据管理权限
- 数据操作
- 所有数据的增删改查:统一的数据管理平台,支持批量操作
- 数据导入导出支持多种格式CSV、Excel、JSON等的数据迁移
- 数据备份恢复:自动定时备份,支持指定时间点数据恢复
- 设备管理
- 添加/删除/配置监测设备:支持设备批量导入,远程配置
- 设备状态监控:实时监控设备运行状态,自动报警
- 设备维护记录:记录设备维护历史,预测维护周期
#### 1.3 功能模块管理
- 物种监测管理
- 添加/修改物种识别模型:支持模型在线更新和版本控制
- 调整识别参数:可针对不同场景优化识别参数
- 管理物种数据库:维护物种特征库,支持特征自动更新
- 环境监测管理
- 配置监测指标:自定义监测指标和采集频率
- 设置预警规则:支持多条件组合的复杂预警规则
- 管理监测点位:可视化配置监测点位,支持地图展示
### 2. 管理人员(运维人员)
#### 2.1 监测管理
- 实时监控
- 查看所有监测数据:多维度数据可视化展示
- 接收系统预警信息多渠道预警推送短信、邮件、APP通知
- 处理告警事件:标准化的告警处理流程
- 查看设备状态:设备状态实时展示,支持远程诊断
- 数据处理
- 数据审核:多级审核机制,确保数据准确性
- 数据标注支持协同标注提高AI模型准确率
- 异常数据处理:自动标记异常数据,支持人工确认
#### 2.2 巡护管理
- 巡护任务
- 创建巡护计划:智能规划巡护路线和时间
- 分配巡护任务:任务自动分配,支持紧急任务插入
- 跟踪任务执行:实时监控巡护人员位置和执行情况
- 查看巡护报告:自动生成巡护报告,支持多媒体信息
- 安防管理
- 查看实时监控画面:多画面同时展示,支持云台控制
- 处理安防告警:智能识别异常行为,快速响应处理
- 记录安全事件:结构化记录事件信息,支持案例回溯
#### 2.3 报告管理
- 报表生成
- 生成日常监测报告:自动汇总数据,生成标准化报告
- 生成巡护报告:整合巡护记录,自动生成巡护总结
- 生成分析报告:支持自定义报告模板,多种导出格式
- 数据分析
- 查看统计数据:多维度数据统计和图表展示
- 生成趋势分析:智能分析数据趋势,预测未来变化
- 导出分析结果:支持多种格式导出,便于共享
### 3. 普通用户(市民游客)
#### 3.1 信息浏览
- 湿地概况
- 查看湿地简介图文并茂的湿地介绍支持VR全景
- 浏览物种图鉴:互动式物种图鉴,包含详细介绍
- 了解保护措施:展示保护成果,宣传保护理念
- 实时数据
- 查看实时监测数据:以简单直观的方式展示环境数据
- 浏览环境状况:环境质量评级展示,包含历史趋势
- 观看直播画面:高清视频直播,支持精彩时刻回放
#### 3.2 互动功能
- 科普教育
- 参与线上课程:提供趣味性科普课程,支持在线互动
- 查看科普内容:多媒体科普资料,支持分享收藏
- 参与知识问答:趣味性科普问答,提供积分奖励
- 预约研学活动:在线预约功能,支持团队预约
- 公众参与
- 提交意见建议:便捷的反馈通道,支持图片上传
- 报告异常情况:快速上报功能,支持位置定位
- 分享观察记录:用户可分享观察心得和图片
- 参与满意度调查:定期收集用户反馈,改进服务
#### 3.3 个人中心
- 账户管理
- 个人信息维护:基本信息管理,隐私信息保护
- 修改密码:安全的密码修改机制
- 消息通知设置:自定义消息接收范围和方式
- 互动记录
- 查看学习记录:记录学习历程,展示学习成果
- 查看反馈历史:跟踪反馈处理进度
- 收藏管理:管理收藏的科普内容和图片
## 功能模块详细说明
### 1. 智能物种监测系统
#### 1.1 鸟类识别与监测
- 实时视频采集
- 高清摄像头实时监控
- 红外相机夜间监测
- AI识别分析
- 鸟类物种自动识别
- 数量统计
- 行为分析
- 数据记录
- 时间戳记录
- 视频片段存储
- 图像存档
- 位置信息记录
#### 1.2 植被监测系统
- 红树林监测
- 植被分布图像采集
- AI自动分割识别
- 生长状态追踪
- 濒危植物保护
- 保护区域划定
- 生长状态监测
- 多角度信息采集
- 数据分析
- 植被覆盖率统计
- 生长趋势分析
- 健康状况评估
### 2. 环境监测预警系统
#### 2.1 水质监测
- 实时监测指标
- TDS值检测
- 有机物含量
- 无机物含量
- 数据分析
- 水质等级评估
- 污染物分析
- 趋势预测
- 预警机制
- 污染预警
- 实时报警推送
- 应急方案建议
#### 2.2 土壤监测
- 土壤参数监测
- 水分含量
- 电导率
- pH值
- 健康评估
- 土壤质量分析
- 营养成分评估
- 污染物检测
- 数据追踪
- 历史数据对比
- 变化趋势分析
- 预警阈值设定
### 3. 智能数据管理系统
#### 3.1 数据采集与存储
- 多源数据采集
- 传感器数据
- 视频图像数据
- 环境监测数据
- 数据存储管理
- 分布式存储
- 数据备份
- 访问权限控制
#### 3.2 数据分析与报告
- 智能分析
- 数据挖掘
- 趋势预测
- 相关性分析
- 自动报告生成
- 日常监测报告
- 定期评估报告
- 专项分析报告
### 4. 智能巡护系统
#### 4.1 AI巡护管理
- 任务管理
- 巡护任务发布
- 任务追踪
- 完成情况统计
- 轨迹记录
- GPS轨迹记录
- 巡护路线优化
- 覆盖率分析
#### 4.2 安防监控
- 视频监控
- 实时画面查看
- 无人机巡航
- 异常行为识别
- 特殊区域保护
- 濒危植物区域监控
- 入侵检测
- 破坏行为预警
### 5. 公众服务平台
#### 5.1 科普教育
- 科普内容
- 湿地知识库
- 物种图鉴
- 生态保护知识
- 互动学习
- 研学活动
- 在线课程
- 知识问答
#### 5.2 公众监督
- 破坏行为展示
- 案例展示
- 警示教育
- 处理结果公示
- 意见反馈
- 问题报告
- 建议收集
- 满意度调查
## 技术架构
[待补充]
## 部署说明
[待补充]
## 使用指南
[待补充]

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2266
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "wetlandguard-admin",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.9",
"echarts": "^5.6.0",
"element-plus": "^2.9.3",
"pinia": "^2.3.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/node": "^22.13.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",
"sass": "^1.83.4",
"typescript": "~5.6.2",
"vite": "^6.0.5",
"vue-tsc": "^2.2.0"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

26
src/App.vue Normal file
View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import { RouterView } from "vue-router";
</script>
<template>
<RouterView />
</template>
<style lang="scss">
@import "./styles/variables.scss";
html,
body {
margin: 0;
padding: 0;
height: 100%;
background-color: $bg-color;
color: $text-primary;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
#app {
height: 100vh;
}
</style>

49
src/api/report.ts Normal file
View File

@ -0,0 +1,49 @@
import request from './request';
// 日常报告接口
export const dailyReportApi = {
// 获取日常报告列表
getList: (params: any) => request.get('/report/daily/list', { params }),
// 获取日常报告详情
getDetail: (id: number) => request.get(`/report/daily/${id}`),
// 创建日常报告
create: (data: any) => request.post('/report/daily', data),
// 更新日常报告
update: (id: number, data: any) => request.put(`/report/daily/${id}`, data),
// 删除日常报告
delete: (id: number) => request.delete(`/report/daily/${id}`),
// 导出日常报告
export: (id: number) => request.get(`/report/daily/export/${id}`, { responseType: 'blob' })
};
// 分析报告接口
export const analysisReportApi = {
// 获取分析报告列表
getList: (params: any) => request.get('/report/analysis/list', { params }),
// 获取分析报告详情
getDetail: (id: number) => request.get(`/report/analysis/${id}`),
// 创建分析报告
create: (data: any) => request.post('/report/analysis', data),
// 更新分析报告
update: (id: number, data: any) => request.put(`/report/analysis/${id}`, data),
// 删除分析报告
delete: (id: number) => request.delete(`/report/analysis/${id}`),
// 导出分析报告
export: (id: number) => request.get(`/report/analysis/export/${id}`, { responseType: 'blob' }),
// 获取监测数据统计
getMonitorStats: (params: any) => request.get('/report/analysis/monitor-stats', { params }),
// 获取物种数据统计
getSpeciesStats: (params: any) => request.get('/report/analysis/species-stats', { params })
};

35
src/api/request.ts Normal file
View File

@ -0,0 +1,35 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: '/api',
timeout: 5000
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response) => {
const { data } = response
return data
},
(error) => {
ElMessage.error(error.message || '请求失败')
return Promise.reject(error)
}
)
export default request

1
src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@ -0,0 +1,46 @@
<script setup lang="ts">
const props = defineProps<{
data: {
qualityTrend: string;
indicators: {
name: string;
value: number;
threshold: number;
status: string;
}[];
};
}>();
</script>
<template>
<div class="environment-analysis">
<el-alert
:title="`环境质量趋势: ${data.qualityTrend}`"
:type="data.qualityTrend === '改善' ? 'success' : data.qualityTrend === '恶化' ? 'warning' : 'info'"
show-icon
/>
<div class="indicators-table">
<el-table :data="data.indicators" border style="width: 100%">
<el-table-column prop="name" label="监测指标" />
<el-table-column prop="value" label="当前值" />
<el-table-column prop="threshold" label="预警阈值" />
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="row.status === '正常' ? 'success' : 'danger'">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<style lang="scss" scoped>
.environment-analysis {
.indicators-table {
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
const props = defineProps<{
data: any;
type: 'line' | 'bar';
title?: string;
}>();
let chart: echarts.ECharts | null = null;
const initChart = () => {
const chartDom = document.getElementById('monitorChart');
if (!chartDom) return;
chart = echarts.init(chartDom);
const option = {
title: {
text: props.title
},
tooltip: {
trigger: 'axis'
},
// ...
};
chart.setOption(option);
};
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
chart?.dispose();
});
const handleResize = () => {
chart?.resize();
};
</script>
<template>
<div id="monitorChart" style="width: 100%; height: 400px"></div>
</template>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
data: {
diversity: number;
richness: number;
distribution: any[];
};
}>();
const diversityLevel = computed(() => {
const value = props.data.diversity;
if (value > 0.8) return { text: '高', type: 'success' };
if (value > 0.5) return { text: '中', type: 'warning' };
return { text: '低', type: 'danger' };
});
</script>
<template>
<div class="species-analysis">
<el-descriptions :column="3" border>
<el-descriptions-item label="多样性指数">
{{ data.diversity }}
<el-tag :type="diversityLevel.type" size="small">
{{ diversityLevel.text }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="物种丰富度">
{{ data.richness }}
</el-descriptions-item>
</el-descriptions>
<!-- 分布情况图表 -->
<div class="distribution-chart">
<!-- 这里可以添加物种分布的图表展示 -->
</div>
</div>
</template>
<style lang="scss" scoped>
.species-analysis {
.distribution-chart {
margin-top: 20px;
}
}
</style>

226
src/layout/AdminLayout.vue Normal file
View File

@ -0,0 +1,226 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { useRouter, useRoute } from "vue-router";
import {
Menu as IconMenu,
Location,
Setting,
User,
Monitor,
Tools,
Document,
DataLine,
} from "@element-plus/icons-vue";
const router = useRouter();
const route = useRoute();
const isCollapse = ref(false);
const activeMenu = ref(route.path); //
//
watch(
() => route.path,
(newPath) => {
activeMenu.value = newPath;
}
);
const handleSelect = (key: string) => {
router.push(key);
};
</script>
<template>
<el-container class="layout-container">
<el-aside :width="isCollapse ? '64px' : '200px'">
<div class="logo-container">
<h1 v-if="!isCollapse" class="logo-title">智慧湿地</h1>
</div>
<el-menu
:collapse="isCollapse"
:default-active="activeMenu"
class="el-menu-vertical"
@select="handleSelect"
>
<el-menu-item index="/dashboard">
<el-icon><Monitor /></el-icon>
<template #title>控制台</template>
</el-menu-item>
<el-menu-item index="/screen">
<el-icon><DataLine /></el-icon>
<template #title>数据大屏</template>
</el-menu-item>
<el-sub-menu index="system">
<template #title>
<el-icon><Setting /></el-icon>
<span>系统管理</span>
</template>
<el-menu-item index="/system/users">用户管理</el-menu-item>
<el-menu-item index="/system/roles">角色管理</el-menu-item>
<el-menu-item index="/system/permissions">权限管理</el-menu-item>
<el-menu-item index="/system/settings">系统设置</el-menu-item>
<el-menu-item index="/system/logs">系统日志</el-menu-item>
<el-menu-item index="/system/data">数据管理</el-menu-item>
</el-sub-menu>
<el-sub-menu index="monitor">
<template #title>
<el-icon><Tools /></el-icon>
<span>监测管理</span>
</template>
<el-menu-item index="/monitor/species">物种监测</el-menu-item>
<el-menu-item index="/monitor/environment">环境监测</el-menu-item>
</el-sub-menu>
<el-sub-menu index="patrol">
<template #title>
<el-icon><Location /></el-icon>
<span>巡护管理</span>
</template>
<el-menu-item index="/patrol/tasks">巡护任务</el-menu-item>
<el-menu-item index="/patrol/records">巡护记录</el-menu-item>
</el-sub-menu>
<el-sub-menu index="report">
<template #title>
<el-icon><Document /></el-icon>
<span>报告管理</span>
</template>
<el-menu-item index="/report/daily">日常报告</el-menu-item>
<el-menu-item index="/report/analysis">分析报告</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<el-container>
<el-header>
<div class="header-left">
<el-button type="text" @click="isCollapse = !isCollapse">
<el-icon><IconMenu /></el-icon>
</el-button>
</div>
<div class="header-right">
<el-dropdown>
<span class="user-info">
<el-icon><User /></el-icon>
管理员
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人信息</el-dropdown-item>
<el-dropdown-item>修改密码</el-dropdown-item>
<el-dropdown-item divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main>
<RouterView />
</el-main>
</el-container>
</el-container>
</template>
<style lang="scss" scoped>
@import "../styles/variables.scss";
.layout-container {
height: 100vh;
}
.el-aside {
background-color: $sidebar-bg;
border-right: 1px solid $border-color;
transition: width 0.3s;
.logo-container {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 20px;
border-bottom: 1px solid $border-color;
overflow: hidden;
.logo-img {
width: 32px;
height: 32px;
margin-right: 12px;
}
.logo-title {
margin: 0;
font-size: 18px;
font-weight: 500;
color: $primary-color;
white-space: nowrap;
letter-spacing: 2px;
}
}
.el-menu {
border-right: none;
&.el-menu-vertical {
.el-menu-item,
.el-sub-menu__title {
color: $text-regular;
&:hover {
color: $primary-color;
background-color: #ecf5ff;
}
&.is-active {
color: $primary-color;
background-color: #ecf5ff;
}
}
}
}
}
.el-header {
background-color: $header-bg;
box-shadow: $box-shadow;
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
padding: 0 20px;
}
.header-left {
.el-button {
padding: 8px;
.el-icon {
font-size: 20px;
color: $text-regular;
}
}
}
.header-right {
.user-info {
display: flex;
align-items: center;
cursor: pointer;
color: $text-regular;
.el-icon {
margin-right: 8px;
font-size: 16px;
}
}
}
.el-main {
background-color: $bg-color;
padding: 20px;
}
</style>

View File

@ -0,0 +1,17 @@
export const menuData = [
// ... 其他菜单项
{
title: '报告管理',
icon: 'Document',
children: [
{
title: '日常报告',
path: '/report/daily' // 从 reports 改为 report
},
{
title: '分析报告',
path: '/report/analysis' // 从 reports 改为 report
}
]
}
];

20
src/main.ts Normal file
View File

@ -0,0 +1,20 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import router from './router'
import App from './App.vue'
const app = createApp(App)
// 注册 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')

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

@ -0,0 +1,107 @@
import { createRouter, createWebHistory } from 'vue-router'
import AdminLayout from '../layout/AdminLayout.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/login',
name: 'Login',
component: () => import('../views/login/index.vue')
},
{
path: '/',
component: AdminLayout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('../views/dashboard/index.vue')
},
{
path: 'screen',
name: 'DataScreen',
component: () => import('../views/dashboard/screen/index.vue')
},
{
path: 'system/users',
name: 'UserManagement',
component: () => import('../views/system/users/index.vue')
},
{
path: 'system/roles',
name: 'RoleManagement',
component: () => import('../views/system/roles/index.vue')
},
{
path: 'system/permissions',
name: 'PermissionManagement',
component: () => import('../views/system/permissions/index.vue')
},
{
path: 'monitor/species',
name: 'SpeciesMonitor',
component: () => import('../views/monitor/species/index.vue')
},
{
path: 'monitor/environment',
name: 'EnvironmentMonitor',
component: () => import('../views/monitor/environment/index.vue')
},
{
path: 'patrol/tasks',
name: 'PatrolTasks',
component: () => import('../views/patrol/tasks/index.vue')
},
{
path: 'patrol/records',
name: 'PatrolRecords',
component: () => import('../views/patrol/records/index.vue')
},
{
path: 'patrol/points',
name: 'PatrolPoints',
component: () => import('../views/patrol/points/index.vue'),
},
{
path: 'report/daily',
name: 'DailyReports',
component: () => import('../views/reports/daily/index.vue')
},
{
path: 'report/analysis',
name: 'AnalysisReports',
component: () => import('../views/reports/analysis/index.vue')
},
{
path: 'system/settings',
name: 'SystemSettings',
component: () => import('../views/system/settings/index.vue')
},
{
path: 'system/logs',
name: 'SystemLogs',
component: () => import('../views/system/logs/index.vue')
},
{
path: 'system/data',
name: 'DataManagement',
component: () => import('../views/system/data/index.vue')
}
]
}
]
})
// 路由守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.path !== '/login' && !token) {
next('/login')
} else {
next()
}
})
export default router

79
src/style.css Normal file
View File

@ -0,0 +1,79 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

23
src/styles/variables.scss Normal file
View File

@ -0,0 +1,23 @@
// 主题色
$primary-color: #409EFF; // 主题蓝
$secondary-color: #79bbff; // 浅蓝色
$success-color: #67C23A; // 成功色
$warning-color: #E6A23C; // 警告色
$danger-color: #F56C6C; // 危险色
$info-color: #909399; // 信息色
// 背景色
$bg-color: #F5F7FA; // 整体背景色
$sidebar-bg: #FFFFFF; // 侧边栏背景
$header-bg: #FFFFFF; // 顶栏背景
// 文字颜色
$text-primary: #303133; // 主要文字
$text-regular: #606266; // 常规文字
$text-secondary: #909399; // 次要文字
// 边框颜色
$border-color: #DCDFE6; // 边框颜色
// 阴影
$box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);

View File

@ -0,0 +1,415 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import * as echarts from "echarts";
import { useRouter } from 'vue-router';
const router = useRouter();
const welcomeText = ref("欢迎使用智慧湿地管理平台");
//
const statistics = ref({
species: {
total: 128,
today: 12,
trend: "+8%",
},
environment: {
normal: 22,
abnormal: 2,
trend: "normal",
},
patrol: {
total: 12,
completed: 8,
progress: "66%",
},
devices: {
total: 36,
online: 32,
rate: "88.9%",
},
});
//
const initTrendChart = () => {
const chartDom = document.getElementById("trendChart");
if (!chartDom) return;
const myChart = echarts.init(chartDom);
const option = {
title: {
text: "近7天监测数据趋势",
left: "center",
top: 0,
textStyle: {
fontSize: 16,
fontWeight: 500,
},
},
tooltip: {
trigger: "axis",
},
legend: {
data: ["物种数量", "水质指数"],
top: 25,
},
grid: {
top: 70,
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
xAxis: {
type: "category",
boundaryGap: false,
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
},
yAxis: {
type: "value",
},
series: [
{
name: "物种数量",
type: "line",
data: [120, 132, 101, 134, 90, 230, 210],
smooth: true,
},
{
name: "水质指数",
type: "line",
data: [220, 182, 191, 234, 290, 330, 310],
smooth: true,
},
],
};
myChart.setOption(option);
};
//
const initDistributionChart = () => {
const chartDom = document.getElementById("distributionChart");
if (!chartDom) return;
const myChart = echarts.init(chartDom);
const option = {
title: {
text: "物种分布统计",
left: "center",
top: 0,
textStyle: {
fontSize: 16,
fontWeight: 500,
},
},
tooltip: {
trigger: "item",
},
legend: {
orient: "vertical",
left: "left",
top: 25,
},
series: [
{
name: "物种分布",
type: "pie",
radius: "50%",
top: 60,
data: [
{ value: 1048, name: "鸟类" },
{ value: 735, name: "鱼类" },
{ value: 580, name: "两栖类" },
{ value: 484, name: "植物" },
{ value: 300, name: "其他" },
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: "rgba(0, 0, 0, 0.5)",
},
},
},
],
};
myChart.setOption(option);
};
//
const activities = ref([
{
content: "发现新增鸟类物种:东方白鹳",
timestamp: "2024-03-20 10:30",
type: "success",
},
{
content: "B区水质监测点发现异常",
timestamp: "2024-03-20 09:15",
type: "warning",
},
{
content: "完成今日巡护任务",
timestamp: "2024-03-20 08:00",
type: "success",
},
]);
//
const handleCardClick = (path: string) => {
router.push(path);
//
const menuEl = document.querySelector(`.el-menu-item[index="${path}"]`) as HTMLElement;
if (menuEl) {
menuEl.click();
}
};
const handleOpenScreen = () => {
window.open('/screen', '_blank', 'width=1920,height=1080,menubar=no,toolbar=no,location=no,status=no');
};
onMounted(() => {
initTrendChart();
initDistributionChart();
});
</script>
<template>
<div class="dashboard-container">
<!-- 欢迎信息 -->
<el-row :gutter="20">
<el-col :span="24">
<div class="welcome-card">
<h2>{{ welcomeText }}</h2>
<p class="welcome-subtitle">
今天是 {{ new Date().toLocaleDateString() }}祝您工作愉快
</p>
</div>
</el-col>
</el-row>
<!-- 统计卡片 -->
<el-row :gutter="20" class="mt-20">
<el-col :span="8">
<el-card
class="statistics-card"
shadow="hover"
@click="handleCardClick('/monitor/species')"
>
<template #header>
<div class="statistics-header">
<span>物种监测</span>
<el-tag size="small" type="success">{{ statistics.species.trend }}</el-tag>
</div>
</template>
<div class="statistics-content">
<div class="main-number">{{ statistics.species.total }}</div>
<div class="sub-info">
<span>今日新增</span>
<span class="highlight">{{ statistics.species.today }}</span>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card
class="statistics-card"
shadow="hover"
>
<template #header>
<div class="statistics-header">
<span>环境监测</span>
<el-tag
size="small"
type="warning"
v-if="statistics.environment.abnormal > 0"
>
{{ statistics.environment.abnormal }}个异常
</el-tag>
</div>
</template>
<div class="statistics-content">
<div class="main-number">{{ statistics.environment.normal }}</div>
<div class="sub-info">
<span>监测点位</span>
<span class="highlight">正常</span>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card
class="statistics-card"
shadow="hover"
@click="handleCardClick('/patrol/tasks')"
>
<template #header>
<div class="statistics-header">
<span>巡护任务</span>
<el-tag size="small" type="info">{{ statistics.patrol.progress }}</el-tag>
</div>
</template>
<div class="statistics-content">
<div class="main-number">{{ statistics.patrol.completed }}</div>
<div class="sub-info">
<span>已完成</span>
<span class="highlight">{{ statistics.patrol.total }}</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="20" class="mt-20">
<el-col :span="16">
<el-card>
<div id="trendChart" style="height: 400px; padding-top: 10px"></div>
</el-card>
</el-col>
<el-col :span="8">
<el-card>
<div id="distributionChart" style="height: 400px; padding-top: 10px"></div>
</el-card>
</el-col>
</el-row>
<!-- 最新动态 -->
<el-row :gutter="20" class="mt-20">
<el-col :span="24">
<el-card>
<template #header>
<div class="card-header">
<span>最新动态</span>
</div>
</template>
<el-timeline>
<el-timeline-item
v-for="(activity, index) in activities"
:key="index"
:type="activity.type"
:timestamp="activity.timestamp"
>
{{ activity.content }}
</el-timeline-item>
</el-timeline>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-card>
<template #header>
<div class="card-header">
<span>数据大屏</span>
<el-button type="primary" @click="handleOpenScreen">
打开大屏
</el-button>
</div>
</template>
</el-card>
</el-col>
</el-row>
</div>
</template>
<style lang="scss" scoped>
@import "../../styles/variables.scss";
.dashboard-container {
.welcome-card {
background: linear-gradient(
135deg,
$primary-color 0%,
lighten($primary-color, 20%) 100%
);
padding: 24px;
border-radius: 8px;
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
h2 {
margin: 0;
font-size: 24px;
font-weight: 500;
}
.welcome-subtitle {
margin: 8px 0 0;
opacity: 0.8;
}
}
.statistics-card {
transition: transform 0.3s;
cursor: pointer;
&:hover {
transform: translateY(-5px);
}
.statistics-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
.statistics-content {
padding: 20px 0;
.main-number {
font-size: 36px;
font-weight: 600;
color: $text-primary;
line-height: 1;
margin-bottom: 16px;
}
.sub-info {
display: flex;
justify-content: space-between;
align-items: center;
color: $text-secondary;
.highlight {
color: $primary-color;
font-weight: 500;
}
}
}
}
.mt-20 {
margin-top: 20px;
}
.card-header {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
:deep(.el-timeline-item__content) {
color: $text-regular;
}
:deep(.el-card) {
border: none;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
}
</style>

View File

@ -0,0 +1,365 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import * as echarts from 'echarts';
const router = useRouter();
const isFullscreen = ref(false);
const isPopupWindow = ref(!!window.opener);
const handleFullScreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
isFullscreen.value = true;
} else {
document.exitFullscreen();
isFullscreen.value = false;
}
};
const handleClose = () => {
if (isPopupWindow.value) {
window.close();
} else {
router.push('/dashboard');
}
};
// ESC退
const handleFullscreenChange = () => {
isFullscreen.value = !!document.fullscreenElement;
};
//
const initCarbonChart = () => {
const chartDom = document.getElementById('carbonChart');
if (!chartDom) return;
const chart = echarts.init(chartDom);
const option = {
title: {
text: '湿地碳汇趋势',
textStyle: {
color: '#fff'
}
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['碳储量', '碳吸收量'],
textStyle: {
color: '#fff'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
axisLabel: {
color: '#fff'
}
},
yAxis: {
type: 'value',
name: '单位: 吨',
axisLabel: {
color: '#fff'
}
},
series: [
{
name: '碳储量',
type: 'line',
data: [320, 332, 341, 354, 360, 368],
smooth: true,
lineStyle: {
width: 3
}
},
{
name: '碳吸收量',
type: 'line',
data: [12, 13, 11, 14, 9, 13],
smooth: true,
lineStyle: {
width: 3
}
}
]
};
chart.setOption(option);
};
//
const initEcoChart = () => {
const chartDom = document.getElementById('ecoChart');
if (!chartDom) return;
const chart = echarts.init(chartDom);
const option = {
title: {
text: '生态系统健康指标',
textStyle: {
color: '#fff'
}
},
radar: {
indicator: [
{ name: '物种多样性', max: 100 },
{ name: '水质', max: 100 },
{ name: '土壤质量', max: 100 },
{ name: '植被覆盖', max: 100 },
{ name: '空气质量', max: 100 }
],
splitArea: {
show: true,
areaStyle: {
color: ['rgba(255,255,255,0.1)']
}
},
axisLine: {
lineStyle: {
color: 'rgba(255,255,255,0.2)'
}
},
name: {
textStyle: {
color: '#fff'
}
}
},
series: [{
type: 'radar',
data: [
{
value: [85, 90, 88, 95, 89],
name: '当前状态',
areaStyle: {
opacity: 0.3
}
}
]
}]
};
chart.setOption(option);
};
onMounted(() => {
document.addEventListener("fullscreenchange", handleFullscreenChange);
if (isPopupWindow.value) {
handleFullScreen();
}
initCarbonChart();
initEcoChart();
});
onUnmounted(() => {
document.removeEventListener("fullscreenchange", handleFullscreenChange);
});
</script>
<template>
<div class="screen-container">
<div class="screen-header">
<div class="header-title">智慧湿地生态监测大屏</div>
<div class="header-right">
<el-button type="default" @click="handleClose" class="mr-10">
{{ isPopupWindow ? '关闭窗口' : '返回' }}
</el-button>
<el-button type="primary" @click="handleFullScreen">
{{ isFullscreen ? "退出全屏" : "全屏显示" }}
</el-button>
</div>
</div>
<div class="screen-content">
<el-row :gutter="20">
<!-- 左侧统计卡片 -->
<el-col :span="6">
<div class="data-card">
<div class="card-title">年度碳汇总量</div>
<div class="card-value">368<span class="unit"></span></div>
<div class="card-compare">同比增长 <span class="up">8.2%</span></div>
</div>
<div class="data-card">
<div class="card-title">湿地面积</div>
<div class="card-value">1280<span class="unit">公顷</span></div>
<div class="card-compare">保护率 <span class="normal">95%</span></div>
</div>
<div class="data-card">
<div class="card-title">物种数量</div>
<div class="card-value">1860<span class="unit"></span></div>
<div class="card-compare">新增 <span class="up">12</span></div>
</div>
</el-col>
<!-- 中间图表 -->
<el-col :span="12">
<div class="chart-card">
<div id="carbonChart" style="height: 360px"></div>
</div>
<div class="info-card">
<div class="info-item">
<div class="info-title">实时监测</div>
<div class="info-content">
<el-tag type="success">水质正常</el-tag>
<el-tag type="success">空气质量优</el-tag>
<el-tag type="warning">土壤湿度偏低</el-tag>
</div>
</div>
</div>
</el-col>
<!-- 右侧图表 -->
<el-col :span="6">
<div class="chart-card">
<div id="ecoChart" style="height: 360px"></div>
</div>
<div class="data-card">
<div class="card-title">生态预警</div>
<div class="warning-list">
<div class="warning-item">
<el-tag type="warning">B区水位偏低</el-tag>
<span class="time">10分钟前</span>
</div>
<div class="warning-item">
<el-tag type="success">已解决: A区水质异常</el-tag>
<span class="time">2小时前</span>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<style lang="scss" scoped>
.mr-10 {
margin-right: 10px;
}
.screen-container {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #0f1527;
color: #fff;
z-index: 999;
.screen-header {
position: relative;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.2);
.header-title {
font-size: 24px;
font-weight: bold;
}
.header-right {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
}
}
.screen-content {
padding: 20px;
height: calc(100vh - 60px);
overflow: hidden;
.data-card {
background: rgba(255,255,255,0.05);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
.card-title {
font-size: 16px;
color: rgba(255,255,255,0.7);
margin-bottom: 10px;
}
.card-value {
font-size: 32px;
font-weight: bold;
color: #fff;
margin-bottom: 10px;
.unit {
font-size: 14px;
margin-left: 4px;
opacity: 0.7;
}
}
.card-compare {
font-size: 14px;
color: rgba(255,255,255,0.7);
.up {
color: #67C23A;
}
.normal {
color: #409EFF;
}
}
}
.chart-card {
background: rgba(255,255,255,0.05);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.info-card {
background: rgba(255,255,255,0.05);
border-radius: 8px;
padding: 20px;
.info-title {
font-size: 16px;
color: rgba(255,255,255,0.7);
margin-bottom: 10px;
}
.info-content {
display: flex;
gap: 10px;
}
}
.warning-list {
.warning-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.time {
font-size: 12px;
color: rgba(255,255,255,0.5);
}
}
}
}
}
</style>

96
src/views/login/index.vue Normal file
View File

@ -0,0 +1,96 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const loginForm = reactive({
username: "",
password: "",
});
const loading = ref(false);
const handleLogin = async () => {
loading.value = true;
try {
// TODO: API
localStorage.setItem("token", "demo-token");
router.push("/dashboard");
} catch (error) {
console.error(error);
} finally {
loading.value = false;
}
};
</script>
<template>
<div class="login-container">
<el-card class="login-card">
<h2>智慧湿地管理平台</h2>
<el-form :model="loginForm" label-width="0">
<el-form-item>
<el-input v-model="loginForm.username" placeholder="用户名" prefix-icon="User" />
</el-form-item>
<el-form-item>
<el-input
v-model="loginForm.password"
type="password"
placeholder="密码"
prefix-icon="Lock"
show-password
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="loading"
class="login-button"
@click="handleLogin"
>
登录
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<style lang="scss" scoped>
@import "../../styles/variables.scss";
.login-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f7fa;
background-image: linear-gradient(135deg, #f5f7fa 0%, #ecf5ff 100%);
}
.login-card {
width: 400px;
border: none;
border-radius: 4px;
box-shadow: $box-shadow;
background: #ffffff;
h2 {
text-align: center;
margin-bottom: 30px;
color: $text-primary;
font-weight: 500;
}
.el-input {
margin-bottom: 20px;
}
.login-button {
width: 100%;
height: 40px;
font-size: 16px;
}
}
</style>

View File

@ -0,0 +1,222 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import * as echarts from "echarts";
import { onMounted } from "vue";
interface EnvData {
id: number;
location: string;
temperature: number;
humidity: number;
waterQuality: string;
time: string;
}
const tableData = ref<EnvData[]>([
{
id: 1,
location: "A区监测点",
temperature: 25.6,
humidity: 65,
waterQuality: "优",
time: "2024-03-20 10:30",
},
{
id: 2,
location: "B区监测点",
temperature: 26.2,
humidity: 62,
waterQuality: "良",
time: "2024-03-20 11:20",
},
]);
const initChart = () => {
const chartDom = document.getElementById("envChart");
if (!chartDom) return;
const myChart = echarts.init(chartDom);
const option = {
title: {
text: "24小时温度趋势",
},
tooltip: {
trigger: "axis",
},
xAxis: {
type: "category",
data: ["00:00", "06:00", "12:00", "18:00", "24:00"],
},
yAxis: {
type: "value",
name: "温度(°C)",
},
series: [
{
type: "line",
data: [22, 20, 25, 28, 23],
smooth: true,
itemStyle: {
color: "#409EFF",
},
},
],
};
myChart.setOption(option);
};
//
const exportDialogVisible = ref(false);
const exportForm = reactive({
timeRange: [] as string[],
format: "excel",
dataType: ["temperature", "humidity", "waterQuality"],
});
const handleExport = () => {
exportDialogVisible.value = true;
};
const handleExportConfirm = () => {
console.log("导出数据:", exportForm);
exportDialogVisible.value = false;
};
onMounted(() => {
initChart();
});
</script>
<template>
<div class="env-container">
<el-row :gutter="20">
<el-col :span="24">
<el-card>
<div id="envChart" style="height: 400px"></div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mt-20">
<el-col :span="24">
<el-card>
<template #header>
<div class="card-header">
<span>实时监测数据</span>
<div>
<el-button type="primary" @click="handleExport">导出数据</el-button>
</div>
</div>
</template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="location" label="监测点" min-width="120" />
<el-table-column prop="temperature" label="温度(°C)" width="120" />
<el-table-column prop="humidity" label="湿度(%)" width="120" />
<el-table-column prop="waterQuality" label="水质" width="100">
<template #default="{ row }">
<el-tag :type="row.waterQuality === '优' ? 'success' : 'warning'">
{{ row.waterQuality }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="time" label="监测时间" min-width="180" />
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 添加导出弹窗 -->
<el-dialog v-model="exportDialogVisible" title="导出数据" width="500px">
<el-form :model="exportForm" label-width="80px">
<el-form-item label="时间范围">
<el-date-picker
v-model="exportForm.timeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="数据类型">
<el-checkbox-group v-model="exportForm.dataType">
<el-checkbox label="temperature">温度</el-checkbox>
<el-checkbox label="humidity">湿度</el-checkbox>
<el-checkbox label="waterQuality">水质</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="导出格式">
<el-radio-group v-model="exportForm.format">
<el-radio label="excel">Excel</el-radio>
<el-radio label="csv">CSV</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="exportDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleExportConfirm">确认导出</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.env-container {
.el-card {
background: #ffffff;
border: none;
border-radius: 4px;
box-shadow: $box-shadow;
.el-card__header {
padding: 16px 20px;
border-bottom: 1px solid $border-color;
}
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
.mt-20 {
margin-top: 20px;
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
color: $text-primary;
font-weight: 500;
}
.el-table__cell {
padding: 12px 0;
}
}
.dialog-footer {
.el-button {
margin-left: 12px;
}
}
.el-card__body {
padding: 20px;
}
</style>

View File

@ -0,0 +1,210 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import * as echarts from "echarts";
import { onMounted } from "vue";
interface SpeciesData {
id: number;
name: string;
type: string;
count: number;
location: string;
time: string;
}
const tableData = ref<SpeciesData[]>([
{
id: 1,
name: "东方白鹳",
type: "鸟类",
count: 12,
location: "A区湿地",
time: "2024-03-20 10:30",
},
{
id: 2,
name: "黑鹳",
type: "鸟类",
count: 8,
location: "B区湿地",
time: "2024-03-20 11:20",
},
]);
const initChart = () => {
const chartDom = document.getElementById("speciesChart");
if (!chartDom) return;
const myChart = echarts.init(chartDom);
const option = {
title: {
text: "物种分布统计",
},
tooltip: {
trigger: "axis",
},
legend: {
data: ["数量"],
},
xAxis: {
type: "category",
data: ["东方白鹳", "黑鹳", "大天鹅", "小天鹅"],
},
yAxis: {
type: "value",
},
series: [
{
name: "数量",
type: "bar",
data: [12, 8, 15, 10],
itemStyle: {
color: "#409EFF",
},
},
],
};
myChart.setOption(option);
};
onMounted(() => {
initChart();
});
//
const exportDialogVisible = ref(false);
const exportForm = reactive({
timeRange: [] as string[],
format: "excel",
});
const handleExport = () => {
exportDialogVisible.value = true;
};
const handleExportConfirm = () => {
console.log("导出数据:", exportForm);
exportDialogVisible.value = false;
};
</script>
<template>
<div class="species-container">
<el-row :gutter="20">
<el-col :span="24">
<el-card>
<div id="speciesChart" style="height: 400px"></div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mt-20">
<el-col :span="24">
<el-card>
<template #header>
<div class="card-header">
<span>识别记录</span>
<div>
<el-button type="primary" @click="handleExport">导出数据</el-button>
</div>
</div>
</template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="物种名称" min-width="120" />
<el-table-column prop="type" label="类型" width="100" />
<el-table-column prop="count" label="数量" width="100" />
<el-table-column prop="location" label="位置" min-width="120" />
<el-table-column prop="time" label="识别时间" min-width="180" />
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 添加导出弹窗 -->
<el-dialog v-model="exportDialogVisible" title="导出数据" width="500px">
<el-form :model="exportForm" label-width="80px">
<el-form-item label="时间范围">
<el-date-picker
v-model="exportForm.timeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="导出格式">
<el-radio-group v-model="exportForm.format">
<el-radio label="excel">Excel</el-radio>
<el-radio label="csv">CSV</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="exportDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleExportConfirm">确认导出</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.species-container {
.el-card {
background: #ffffff;
border: none;
border-radius: 4px;
box-shadow: $box-shadow;
.el-card__header {
padding: 16px 20px;
border-bottom: 1px solid $border-color;
}
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
.mt-20 {
margin-top: 20px;
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
color: $text-primary;
font-weight: 500;
}
.el-table__cell {
padding: 12px 0;
}
}
.dialog-footer {
.el-button {
margin-left: 12px;
}
}
.el-card__body {
padding: 20px;
}
</style>

View File

@ -0,0 +1,421 @@
<script setup lang="ts">
import { ref, reactive, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
interface PointData {
id: number;
name: string;
location: string;
type: string;
description: string;
status: string;
lastPatrolTime: string;
}
const tableData = ref<PointData[]>([
{
id: 1,
name: "A区-1号点位",
location: "湿地公园东北角",
type: "监测点",
description: "水质监测点,配备水质监测设备",
status: "正常",
lastPatrolTime: "2024-03-20 10:30",
},
{
id: 2,
name: "B区-观察点",
location: "湿地公园西区",
type: "观察点",
description: "鸟类观察点,配备观察设施",
status: "正常",
lastPatrolTime: "2024-03-20 11:20",
},
]);
//
const dialogVisible = ref(false);
const formData = reactive({
name: "",
location: "",
type: "",
description: "",
});
const rules = {
name: [{ required: true, message: "请输入点位名称", trigger: "blur" }],
location: [{ required: true, message: "请输入位置信息", trigger: "blur" }],
type: [{ required: true, message: "请选择点位类型", trigger: "change" }],
};
const formRef = ref();
const handleAdd = () => {
dialogVisible.value = true;
};
//
const searchText = ref("");
const typeFilter = ref<string[]>([]);
const statusFilter = ref<string[]>([]);
//
const filteredTableData = computed(() => {
return tableData.value.filter(point => {
//
const searchLower = searchText.value.toLowerCase();
const textMatch = !searchText.value ||
point.name.toLowerCase().includes(searchLower) ||
point.location.toLowerCase().includes(searchLower) ||
point.description.toLowerCase().includes(searchLower);
//
const typeMatch = typeFilter.value.length === 0 ||
typeFilter.value.includes(point.type);
//
const statusMatch = statusFilter.value.length === 0 ||
statusFilter.value.includes(point.status);
return textMatch && typeMatch && statusMatch;
});
});
//
const resetFilters = () => {
searchText.value = "";
typeFilter.value = [];
statusFilter.value = [];
};
//
const isEdit = ref(false);
const currentPoint = ref<PointData | null>(null);
const handleEdit = (row: PointData) => {
isEdit.value = true;
currentPoint.value = row;
formData.name = row.name;
formData.location = row.location;
formData.type = row.type;
formData.description = row.description;
dialogVisible.value = true;
};
//
const handleSubmit = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
if (isEdit.value) {
console.log("更新数据:", formData);
ElMessage.success("更新成功");
} else {
console.log("新增数据:", formData);
ElMessage.success("添加成功");
}
handleDialogClose();
} catch (error) {
console.error("表单验证失败:", error);
}
};
//
const handleDialogClose = () => {
dialogVisible.value = false;
isEdit.value = false;
currentPoint.value = null;
formData.name = "";
formData.location = "";
formData.type = "";
formData.description = "";
if (formRef.value) {
formRef.value.resetFields();
}
};
//
const currentPage = ref(1);
const pageSize = ref(10);
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredTableData.value.slice(start, end);
});
const handleSizeChange = (val: number) => {
pageSize.value = val;
currentPage.value = 1;
};
const handleCurrentChange = (val: number) => {
currentPage.value = val;
};
const handleDelete = (row: PointData) => {
ElMessageBox.confirm("确认删除该巡护点位?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
ElMessage.success("删除成功");
})
.catch(() => {
//
});
};
const pointTypes = [
{ label: "监测点", value: "监测点" },
{ label: "观察点", value: "观察点" },
{ label: "休息点", value: "休息点" },
{ label: "补给点", value: "补给点" },
];
</script>
<template>
<div class="point-container">
<el-card>
<template #header>
<div class="card-header">
<span>巡护点位</span>
<div class="header-right">
<el-input
v-model="searchText"
placeholder="搜索点位名称/位置/描述"
style="width: 300px"
clearable
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-select
v-model="typeFilter"
multiple
collapse-tags
placeholder="类型筛选"
style="width: 200px; margin-left: 16px"
clearable
>
<el-option
v-for="item in pointTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select
v-model="statusFilter"
multiple
collapse-tags
placeholder="状态筛选"
style="width: 200px; margin-left: 16px"
clearable
>
<el-option label="正常" value="正常" />
<el-option label="异常" value="异常" />
</el-select>
<el-button @click="resetFilters" style="margin-left: 16px">重置</el-button>
<el-button type="primary" @click="handleAdd">新增点位</el-button>
</div>
</div>
</template>
<el-table :data="paginatedData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="点位名称" min-width="120" />
<el-table-column prop="location" label="位置" min-width="150" />
<el-table-column prop="type" label="类型" width="100" />
<el-table-column prop="description" label="描述" min-width="200">
<template #default="{ row }">
<el-tooltip
v-if="row.description.length > 20"
:content="row.description"
placement="top"
>
<span>{{ row.description.slice(0, 20) }}...</span>
</el-tooltip>
<span v-else>{{ row.description }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '正常' ? 'success' : 'danger'">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="lastPatrolTime" label="最后巡查时间" width="180" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" link @click="handleEdit(row)">编辑</el-button>
<el-button
type="danger"
size="small"
link
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="filteredTableData.length"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 新增点位弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑巡护点位' : '新增巡护点位'"
width="500px"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="80px"
>
<el-form-item label="点位名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入点位名称" />
</el-form-item>
<el-form-item label="位置信息" prop="location">
<el-input v-model="formData.location" placeholder="请输入位置信息" />
</el-form-item>
<el-form-item label="点位类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择点位类型" style="width: 100%">
<el-option
v-for="item in pointTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input
v-model="formData.description"
type="textarea"
rows="4"
placeholder="请输入点位描述"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.point-container {
.el-card {
background: #ffffff;
border: none;
border-radius: 4px;
box-shadow: $box-shadow;
.el-card__header {
padding: 16px 20px;
border-bottom: 1px solid $border-color;
}
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
.header-right {
display: flex;
align-items: center;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
color: $text-primary;
font-weight: 500;
}
.el-table__cell {
padding: 12px 0;
}
}
.el-card__body {
padding: 20px;
}
:deep(.el-form) {
.el-form-item__label {
font-weight: normal;
color: $text-regular;
}
}
.dialog-footer {
.el-button {
margin-left: 12px;
}
}
//
.el-table {
.el-tag {
transition: all 0.3s;
}
.el-button {
transition: all 0.3s;
}
}
:deep(.el-select) {
.el-input__wrapper {
width: 100%;
}
}
:deep(.el-dialog) {
.el-select {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,364 @@
<script setup lang="ts">
import { ref, computed, reactive } from "vue";
import { ElMessage } from "element-plus";
import { Search } from "@element-plus/icons-vue";
interface RecordData {
id: number;
taskTitle: string;
patroller: string;
route: string;
findings: string;
images: string[];
status: string;
completedTime: string;
}
const tableData = ref<RecordData[]>([
{
id: 1,
taskTitle: "A区日常巡查",
patroller: "张三",
route: "A区-1号点位-2号点位-3号点位",
findings: "发现水质异常,已通知相关部门",
images: ["image1.jpg", "image2.jpg"],
status: "正常",
completedTime: "2024-03-20 11:00",
},
{
id: 2,
taskTitle: "B区设备检查",
patroller: "李四",
route: "B区-监测站-水质监测点",
findings: "设备运行正常,已完成例行维护",
images: ["image3.jpg"],
status: "正常",
completedTime: "2024-03-20 16:00",
},
]);
const dialogVisible = ref(false);
const currentRecord = ref<RecordData | null>(null);
const handleView = (row: RecordData) => {
currentRecord.value = row;
dialogVisible.value = true;
};
const searchText = ref("");
const filteredTableData = computed(() => {
if (!searchText.value) return tableData.value;
const searchLower = searchText.value.toLowerCase();
return tableData.value.filter(
(item) =>
item.taskTitle.toLowerCase().includes(searchLower) ||
item.patroller.toLowerCase().includes(searchLower) ||
item.findings.toLowerCase().includes(searchLower)
);
});
const exportDialogVisible = ref(false);
const exportForm = reactive({
timeRange: [] as string[],
format: "excel",
types: ["basic", "findings", "images"],
});
const handleExport = () => {
exportDialogVisible.value = true;
};
const handleExportConfirm = () => {
console.log("导出数据:", exportForm);
ElMessage.success("导出成功");
exportDialogVisible.value = false;
};
const currentPage = ref(1);
const pageSize = ref(10);
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredTableData.value.slice(start, end);
});
const handleSizeChange = (val: number) => {
pageSize.value = val;
currentPage.value = 1;
};
const handleCurrentChange = (val: number) => {
currentPage.value = val;
};
</script>
<template>
<div class="record-container">
<el-card>
<template #header>
<div class="card-header">
<span>巡护记录</span>
<div class="header-right">
<el-input
v-model="searchText"
placeholder="搜索巡护记录"
style="width: 200px; margin-right: 16px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="handleExport">导出记录</el-button>
</div>
</div>
</template>
<el-table :data="paginatedData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="taskTitle" label="任务名称" min-width="150" />
<el-table-column prop="patroller" label="巡护人员" width="100" />
<el-table-column prop="route" label="巡护路线" min-width="200" />
<el-table-column prop="findings" label="发现问题" min-width="200">
<template #default="{ row }">
<el-tooltip
v-if="row.findings.length > 20"
:content="row.findings"
placement="top"
>
<span>{{ row.findings.slice(0, 20) }}...</span>
</el-tooltip>
<span v-else>{{ row.findings }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '正常' ? 'success' : 'warning'">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="completedTime" label="完成时间" width="180" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleView(row)">
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="filteredTableData.length"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 详情弹窗 -->
<el-dialog v-model="dialogVisible" title="巡护记录详情" width="700px">
<template v-if="currentRecord">
<el-descriptions :column="2" border>
<el-descriptions-item label="任务名称" :span="2">
{{ currentRecord.taskTitle }}
</el-descriptions-item>
<el-descriptions-item label="巡护人员">
{{ currentRecord.patroller }}
</el-descriptions-item>
<el-descriptions-item label="完成时间">
{{ currentRecord.completedTime }}
</el-descriptions-item>
<el-descriptions-item label="巡护路线" :span="2">
{{ currentRecord.route }}
</el-descriptions-item>
<el-descriptions-item label="发现问题" :span="2">
{{ currentRecord.findings }}
</el-descriptions-item>
<el-descriptions-item label="现场照片" :span="2">
<el-image
v-for="(img, index) in currentRecord.images"
:key="index"
:src="img"
fit="cover"
style="width: 100px; height: 100px; margin-right: 8px"
>
<template #error>
<div class="image-placeholder">暂无图片</div>
</template>
</el-image>
</el-descriptions-item>
</el-descriptions>
</template>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
<!-- 添加导出弹窗 -->
<el-dialog v-model="exportDialogVisible" title="导出记录" width="500px">
<el-form :model="exportForm" label-width="80px">
<el-form-item label="时间范围">
<el-date-picker
v-model="exportForm.timeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="导出内容">
<el-checkbox-group v-model="exportForm.types">
<el-checkbox label="basic">基本信息</el-checkbox>
<el-checkbox label="findings">发现问题</el-checkbox>
<el-checkbox label="images">现场照片</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="导出格式">
<el-radio-group v-model="exportForm.format">
<el-radio label="excel">Excel</el-radio>
<el-radio label="pdf">PDF</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="exportDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleExportConfirm">确认导出</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.record-container {
.el-card {
background: #ffffff;
border: none;
border-radius: 4px;
box-shadow: $box-shadow;
.el-card__header {
padding: 16px 20px;
border-bottom: 1px solid $border-color;
}
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
.header-right {
display: flex;
align-items: center;
}
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
color: $text-primary;
font-weight: 500;
}
.el-table__cell {
padding: 12px 0;
}
}
.el-card__body {
padding: 20px;
}
.image-placeholder {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f7fa;
color: $text-secondary;
font-size: 14px;
}
:deep(.el-descriptions) {
.el-descriptions__label {
width: 120px;
justify-content: right;
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.el-table {
.el-tag {
transition: all 0.3s;
}
.el-button {
transition: all 0.3s;
}
}
.el-dialog {
.el-image {
transition: transform 0.3s;
cursor: pointer;
&:hover {
transform: scale(1.05);
}
}
}
.image-container {
display: flex;
flex-wrap: wrap;
gap: 12px;
.el-image {
border-radius: 4px;
overflow: hidden;
}
}
:deep(.el-descriptions) {
.el-descriptions__cell {
padding: 16px 24px;
}
.el-descriptions__label {
color: $text-regular;
font-weight: normal;
}
.el-descriptions__content {
color: $text-primary;
}
}
</style>

View File

@ -0,0 +1,745 @@
<script setup lang="ts">
import { ref, reactive, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
interface TaskData {
id: number;
title: string;
area: string;
assignee: string;
status: string;
priority: string;
startTime: string;
endTime: string;
}
const tableData = ref<TaskData[]>([
{
id: 1,
title: "A区日常巡查",
area: "A区湿地",
assignee: "张三",
status: "进行中",
priority: "高",
startTime: "2024-03-20 09:00",
endTime: "2024-03-20 11:00",
},
{
id: 2,
title: "B区设备检查",
area: "B区湿地",
assignee: "李四",
status: "待开始",
priority: "中",
startTime: "2024-03-20 14:00",
endTime: "2024-03-20 16:00",
},
]);
const getStatusType = (status: string) => {
const map: Record<string, string> = {
进行中: "success",
待开始: "info",
已完成: "primary",
已取消: "danger",
};
return map[status] || "info";
};
const getPriorityType = (priority: string) => {
const map: Record<string, string> = {
: "danger",
: "warning",
: "info",
};
return map[priority] || "info";
};
//
const newTaskDialogVisible = ref(false);
const newTaskForm = reactive({
title: "",
area: "",
assignee: "",
priority: "中",
startTime: "",
endTime: "",
description: "",
});
//
const detailDialogVisible = ref(false);
const currentTask = ref<TaskData | null>(null);
//
const handleView = (row: TaskData) => {
currentTask.value = row;
detailDialogVisible.value = true;
};
const handleComplete = (row: TaskData) => {
ElMessageBox.confirm("确认完成该巡护任务?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
//
ElMessage.success("任务已完成");
})
.catch(() => {
//
});
};
const handleCancel = (row: TaskData) => {
ElMessageBox.confirm("确认取消该巡护任务?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
//
ElMessage.success("任务已取消");
})
.catch(() => {
//
});
};
// 线
interface TimelineItem {
type: 'primary' | 'success' | 'warning' | 'danger' | 'info';
timestamp: string;
content: string;
}
const taskTimeline = ref<TimelineItem[]>([
{
type: 'primary',
timestamp: '2024-03-20 09:00',
content: '任务创建',
},
{
type: 'info',
timestamp: '2024-03-20 09:30',
content: '巡护人员已接收任务',
},
{
type: 'warning',
timestamp: '2024-03-20 10:15',
content: '发现水质异常,已上报',
},
{
type: 'success',
timestamp: '2024-03-20 11:00',
content: '巡护任务完成',
},
]);
//
const validateTime = (rule: any, value: string, callback: Function) => {
if (!value) {
callback(new Error('请选择时间'));
return;
}
if (newTaskForm.startTime && newTaskForm.endTime) {
if (new Date(newTaskForm.endTime) <= new Date(newTaskForm.startTime)) {
callback(new Error('结束时间必须大于开始时间'));
return;
}
}
callback();
};
const rules = {
title: [{ required: true, message: "请输入任务名称", trigger: "blur" }],
area: [{ required: true, message: "请输入巡护区域", trigger: "blur" }],
assignee: [{ required: true, message: "请输入负责人", trigger: "blur" }],
startTime: [
{ required: true, message: "请选择开始时间", trigger: "change" },
{ validator: validateTime, trigger: "change" }
],
endTime: [
{ required: true, message: "请选择结束时间", trigger: "change" },
{ validator: validateTime, trigger: "change" }
],
};
const newTaskFormRef = ref();
const handleNewTask = () => {
newTaskDialogVisible.value = true;
};
const handleNewTaskSubmit = async () => {
if (!newTaskFormRef.value) return;
try {
await newTaskFormRef.value.validate();
console.log("提交新任务:", newTaskForm);
ElMessage.success("创建成功");
newTaskDialogVisible.value = false;
} catch (error) {
console.error("表单验证失败:", error);
}
};
//
const resetForm = () => {
newTaskForm.title = "";
newTaskForm.area = "";
newTaskForm.assignee = "";
newTaskForm.priority = "中";
newTaskForm.startTime = "";
newTaskForm.endTime = "";
newTaskForm.description = "";
if (newTaskFormRef.value) {
newTaskFormRef.value.resetFields();
}
};
//
const handleDialogClose = () => {
resetForm();
};
//
const searchText = ref("");
const statusFilter = ref<string[]>([]);
const priorityFilter = ref<string[]>([]);
const dateRange = ref<[string, string]>(['', '']);
//
const statistics = computed(() => {
const total = tableData.value.length;
const statusCount = {
进行中: 0,
待开始: 0,
已完成: 0,
已取消: 0,
};
const priorityCount = {
: 0,
: 0,
: 0,
};
tableData.value.forEach(task => {
statusCount[task.status as keyof typeof statusCount]++;
priorityCount[task.priority as keyof typeof priorityCount]++;
});
return {
total,
statusCount,
priorityCount,
};
});
//
const filteredTableData = computed(() => {
return tableData.value.filter(task => {
//
const searchLower = searchText.value.toLowerCase();
const textMatch = !searchText.value ||
task.title.toLowerCase().includes(searchLower) ||
task.area.toLowerCase().includes(searchLower) ||
task.assignee.toLowerCase().includes(searchLower);
//
const statusMatch = statusFilter.value.length === 0 ||
statusFilter.value.includes(task.status);
//
const priorityMatch = priorityFilter.value.length === 0 ||
priorityFilter.value.includes(task.priority);
//
let dateMatch = true;
if (dateRange.value[0] && dateRange.value[1]) {
const startDate = new Date(dateRange.value[0]);
const endDate = new Date(dateRange.value[1]);
const taskDate = new Date(task.startTime);
dateMatch = taskDate >= startDate && taskDate <= endDate;
}
return textMatch && statusMatch && priorityMatch && dateMatch;
});
});
//
const resetFilters = () => {
searchText.value = "";
statusFilter.value = [];
priorityFilter.value = [];
dateRange.value = ['', ''];
};
</script>
<template>
<div class="task-container">
<!-- 添加统计卡片 -->
<el-row :gutter="20" class="statistics-row">
<el-col :span="6">
<el-card shadow="hover" class="statistics-card">
<template #header>
<div class="statistics-header">
<span>总任务数</span>
</div>
</template>
<div class="statistics-content">
<span class="number">{{ statistics.total }}</span>
<span class="label">个任务</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="statistics-card">
<template #header>
<div class="statistics-header">
<span>进行中</span>
</div>
</template>
<div class="statistics-content">
<span class="number success">{{ statistics.statusCount.进行中 }}</span>
<span class="label">个任务</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="statistics-card">
<template #header>
<div class="statistics-header">
<span>待开始</span>
</div>
</template>
<div class="statistics-content">
<span class="number info">{{ statistics.statusCount.待开始 }}</span>
<span class="label">个任务</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="statistics-card">
<template #header>
<div class="statistics-header">
<span>高优先级</span>
</div>
</template>
<div class="statistics-content">
<span class="number warning">{{ statistics.priorityCount. }}</span>
<span class="label">个任务</span>
</div>
</el-card>
</el-col>
</el-row>
<el-card class="mt-20">
<!-- 添加搜索和筛选工具栏 -->
<div class="toolbar">
<div class="left">
<el-input
v-model="searchText"
placeholder="搜索任务名称/区域/负责人"
style="width: 300px"
clearable
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-select
v-model="statusFilter"
multiple
collapse-tags
placeholder="状态筛选"
style="width: 200px; margin-left: 16px"
clearable
>
<el-option label="进行中" value="进行中" />
<el-option label="待开始" value="待开始" />
<el-option label="已完成" value="已完成" />
<el-option label="已取消" value="已取消" />
</el-select>
<el-select
v-model="priorityFilter"
multiple
collapse-tags
placeholder="优先级筛选"
style="width: 200px; margin-left: 16px"
clearable
>
<el-option label="高" value="高" />
<el-option label="中" value="中" />
<el-option label="低" value="低" />
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 300px; margin-left: 16px"
/>
</div>
<div class="right">
<el-button @click="resetFilters">重置</el-button>
<el-button type="primary" @click="handleNewTask">新建任务</el-button>
</div>
</div>
<!-- 更新表格数据源 -->
<el-table :data="filteredTableData" style="width: 100%; margin-top: 20px">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="任务名称" min-width="150" />
<el-table-column prop="area" label="巡护区域" min-width="120" />
<el-table-column prop="assignee" label="负责人" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="100">
<template #default="{ row }">
<el-tag :type="getPriorityType(row.priority)">
{{ row.priority }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="执行时间" min-width="300">
<template #default="{ row }"> {{ row.startTime }} {{ row.endTime }} </template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleView(row)">
查看
</el-button>
<el-button
type="success"
size="small"
:disabled="row.status !== '进行中'"
@click="handleComplete(row)"
>
完成
</el-button>
<el-button
type="danger"
size="small"
:disabled="row.status === '已完成' || row.status === '已取消'"
@click="handleCancel(row)"
>
取消
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 添加任务详情弹窗 -->
<el-dialog v-model="detailDialogVisible" title="任务详情" width="700px">
<template v-if="currentTask">
<el-descriptions :column="2" border>
<el-descriptions-item label="任务名称" :span="2">
{{ currentTask.title }}
</el-descriptions-item>
<el-descriptions-item label="巡护区域">
{{ currentTask.area }}
</el-descriptions-item>
<el-descriptions-item label="负责人">
{{ currentTask.assignee }}
</el-descriptions-item>
<el-descriptions-item label="任务状态">
<el-tag :type="getStatusType(currentTask.status)">
{{ currentTask.status }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="优先级">
<el-tag :type="getPriorityType(currentTask.priority)">
{{ currentTask.priority }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="执行时间" :span="2">
{{ currentTask.startTime }} {{ currentTask.endTime }}
</el-descriptions-item>
</el-descriptions>
<!-- 添加任务进度时间线 -->
<div class="timeline-container">
<h4>任务进度</h4>
<el-timeline>
<el-timeline-item
v-for="(activity, index) in taskTimeline"
:key="index"
:type="activity.type"
:timestamp="activity.timestamp"
>
{{ activity.content }}
</el-timeline-item>
</el-timeline>
</div>
</template>
<template #footer>
<span class="dialog-footer">
<el-button @click="detailDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
<!-- 修改新建任务弹窗添加表单验证 -->
<el-dialog
v-model="newTaskDialogVisible"
title="新建巡护任务"
width="600px"
@close="handleDialogClose"
>
<el-form ref="newTaskFormRef" :model="newTaskForm" :rules="rules" label-width="100px">
<el-form-item label="任务名称">
<el-input v-model="newTaskForm.title" placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="巡护区域">
<el-input v-model="newTaskForm.area" placeholder="请输入巡护区域" />
</el-form-item>
<el-form-item label="负责人">
<el-input v-model="newTaskForm.assignee" placeholder="请输入负责人" />
</el-form-item>
<el-form-item label="优先级">
<el-radio-group v-model="newTaskForm.priority">
<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="执行时间">
<el-date-picker
v-model="newTaskForm.startTime"
type="datetime"
placeholder="开始时间"
style="width: 200px; margin-right: 10px"
/>
<el-date-picker
v-model="newTaskForm.endTime"
type="datetime"
placeholder="结束时间"
style="width: 200px"
/>
</el-form-item>
<el-form-item label="任务描述">
<el-input
v-model="newTaskForm.description"
type="textarea"
rows="4"
placeholder="请输入任务描述"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="newTaskDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleNewTaskSubmit">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.task-container {
.el-card {
background: #ffffff;
border: none;
border-radius: 4px;
box-shadow: $box-shadow;
.el-card__header {
padding: 16px 20px;
border-bottom: 1px solid $border-color;
}
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
color: $text-primary;
font-weight: 500;
}
.el-table__cell {
padding: 12px 0;
}
.el-button--small {
padding: 6px 12px;
margin-left: 8px;
&:first-child {
margin-left: 0;
}
}
}
.el-card__body {
padding: 20px;
}
.timeline-container {
margin-top: 24px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 4px;
h4 {
margin: 0 0 20px;
color: $text-primary;
font-weight: 500;
font-size: 16px;
}
}
:deep(.el-timeline) {
padding-left: 16px;
.el-timeline-item {
padding-bottom: 20px;
&:last-child {
padding-bottom: 0;
}
.el-timeline-item__node {
&--primary {
background-color: $primary-color;
}
&--success {
background-color: $success-color;
}
&--warning {
background-color: $warning-color;
}
&--danger {
background-color: $danger-color;
}
}
.el-timeline-item__timestamp {
color: $text-secondary;
font-size: 13px;
margin-bottom: 4px;
}
.el-timeline-item__content {
color: $text-regular;
font-size: 14px;
}
}
}
.el-descriptions {
margin-bottom: 20px;
}
:deep(.el-form) {
.el-form-item__label {
font-weight: normal;
color: $text-regular;
}
.el-input,
.el-textarea {
width: 100%;
}
.el-radio-group {
.el-radio {
margin-right: 20px;
&:last-child {
margin-right: 0;
}
}
}
}
.dialog-footer {
margin-top: 20px;
text-align: right;
}
.statistics-row {
.statistics-card {
.statistics-header {
font-size: 14px;
color: $text-regular;
}
.statistics-content {
display: flex;
align-items: baseline;
.number {
font-size: 28px;
font-weight: 600;
color: $primary-color;
margin-right: 8px;
&.success {
color: $success-color;
}
&.info {
color: $info-color;
}
&.warning {
color: $warning-color;
}
}
.label {
font-size: 14px;
color: $text-secondary;
}
}
}
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20px;
border-bottom: 1px solid $border-color;
.left {
display: flex;
align-items: center;
}
.right {
.el-button {
margin-left: 16px;
}
}
}
.mt-20 {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,469 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import * as echarts from 'echarts';
import { ElMessageBox, ElMessage } from 'element-plus';
interface AnalysisReport {
id: number;
title: string;
type: 'species' | 'environment'; // /
timeRange: { //
start: string;
end: string;
};
dataSource: { //
type: string;
points: string[]; //
}[];
analysis: {
summary: string; //
trends: { //
indicator: string; //
trend: string; //
data: any[]; //
}[];
abnormal: { //
type: string;
description: string;
level: string;
}[];
};
recommendations: string[]; //
}
//
const tableData = ref<AnalysisReport[]>([
{
id: 1,
title: '2024年第一季度水质监测分析报告',
type: 'environment',
timeRange: {
start: '2024-01-01',
end: '2024-03-31'
},
dataSource: [
{
type: '水质监测',
points: ['A区-1号监测点', 'A区-2号监测点', 'B区-1号监测点']
}
],
analysis: {
summary: '第一季度水质总体保持稳定但3月份出现轻微波动',
trends: [
{
indicator: 'pH值',
trend: '稳定',
data: [7.1, 7.2, 7.0, 7.3]
},
{
indicator: '溶解氧',
trend: '下降',
data: [6.5, 6.3, 6.0, 5.8]
}
],
abnormal: [
{
type: '溶解氧',
description: '3月底溶解氧水平略低于标准值',
level: '轻微'
}
]
},
recommendations: [
'加强对B区-1号监测点的巡查频率',
'建议增加水体曝气设施'
]
}
]);
//
const filterForm = ref({
dateRange: [],
type: '',
indicator: ''
});
//
const detailVisible = ref(false);
const currentReport = ref<AnalysisReport | null>(null);
const handleView = (row: AnalysisReport) => {
currentReport.value = row;
detailVisible.value = true;
};
//
const handleExport = (row: AnalysisReport) => {
ElMessageBox.confirm(
'确认导出该分析报告?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}
)
.then(() => {
ElMessage.success('导出成功,文件已下载');
})
.catch(() => {
//
});
};
// resize
let myChart: echarts.ECharts | null = null;
const initChart = () => {
const chartDom = document.getElementById('trendChart');
if (!chartDom) return;
myChart = echarts.init(chartDom);
const option = {
title: {
text: '监测指标趋势分析'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['pH值', '溶解氧', '水温']
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月']
},
yAxis: {
type: 'value'
},
series: [
{
name: 'pH值',
type: 'line',
data: [7.1, 7.2, 7.0, 7.3]
},
{
name: '溶解氧',
type: 'line',
data: [6.5, 6.3, 6.0, 5.8]
},
{
name: '水温',
type: 'line',
data: [15, 16, 18, 21]
}
]
};
myChart.setOption(option);
};
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
myChart?.dispose();
});
const handleResize = () => {
myChart?.resize();
};
//
const currentPage = ref(1);
const pageSize = ref(10);
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return tableData.value.slice(start, end);
});
const handleSizeChange = (val: number) => {
pageSize.value = val;
currentPage.value = 1;
};
const handleCurrentChange = (val: number) => {
currentPage.value = val;
};
//
const dialogVisible = ref(false);
const formData = ref({
title: '',
type: '',
timeRange: [],
summary: '',
recommendations: ''
});
const handleCreate = () => {
dialogVisible.value = true;
};
const handleSubmit = () => {
// TODO:
dialogVisible.value = false;
};
</script>
<template>
<div class="analysis-report">
<!-- 趋势图表 -->
<el-card>
<div id="trendChart" style="height: 400px"></div>
</el-card>
<!-- 报告列表 -->
<el-card class="mt-20">
<template #header>
<div class="card-header">
<span>分析报告</span>
<el-button type="primary" @click="handleCreate">新建报告</el-button>
</div>
</template>
<!-- 筛选表单 -->
<el-form :model="filterForm" inline>
<el-form-item label="时间范围">
<el-date-picker
v-model="filterForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="分析类型">
<el-select v-model="filterForm.type" placeholder="请选择">
<el-option label="物种分析" value="species" />
<el-option label="环境分析" value="environment" />
</el-select>
</el-form-item>
<el-form-item label="监测指标">
<el-select v-model="filterForm.indicator" placeholder="请选择">
<el-option label="pH值" value="pH" />
<el-option label="溶解氧" value="oxygen" />
<el-option label="水温" value="temperature" />
</el-select>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="paginatedData" style="width: 100%">
<el-table-column prop="title" label="报告标题" min-width="200" />
<el-table-column prop="type" label="类型" width="120">
<template #default="{ row }">
{{ row.type === 'species' ? '物种分析' : '环境分析' }}
</template>
</el-table-column>
<el-table-column label="时间范围" width="200">
<template #default="{ row }">
{{ row.timeRange.start }} {{ row.timeRange.end }}
</template>
</el-table-column>
<el-table-column prop="analysis.summary" label="分析总结" min-width="300" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleView(row)">查看</el-button>
<el-button type="warning" link @click="handleExport(row)">导出</el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加分页器 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.length"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 新建报告弹窗 -->
<el-dialog
v-model="dialogVisible"
title="新建分析报告"
width="800px"
>
<el-form :model="formData" label-width="100px">
<el-form-item label="报告标题" required>
<el-input v-model="formData.title" placeholder="请输入报告标题" />
</el-form-item>
<el-form-item label="分析类型" required>
<el-radio-group v-model="formData.type">
<el-radio label="species">物种分析</el-radio>
<el-radio label="environment">环境分析</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="分析周期" required>
<el-date-picker
v-model="formData.timeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="分析总结" required>
<el-input
v-model="formData.summary"
type="textarea"
rows="4"
placeholder="请输入分析总结"
/>
</el-form-item>
<el-form-item label="建议措施" required>
<el-input
v-model="formData.recommendations"
type="textarea"
rows="4"
placeholder="请输入建议措施"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</span>
</template>
</el-dialog>
<!-- 添加详情弹窗 -->
<el-dialog
v-model="detailVisible"
title="分析报告详情"
width="900px"
>
<template v-if="currentReport">
<el-descriptions :column="2" border>
<el-descriptions-item label="报告标题" :span="2">
{{ currentReport.title }}
</el-descriptions-item>
<el-descriptions-item label="分析类型">
{{ currentReport.type === 'species' ? '物种分析' : '环境分析' }}
</el-descriptions-item>
<el-descriptions-item label="时间范围">
{{ currentReport.timeRange.start }} {{ currentReport.timeRange.end }}
</el-descriptions-item>
<el-descriptions-item label="监测点位" :span="2">
<el-tag
v-for="point in currentReport.dataSource[0].points"
:key="point"
style="margin-right: 8px"
>
{{ point }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="分析总结" :span="2">
{{ currentReport.analysis.summary }}
</el-descriptions-item>
<el-descriptions-item label="趋势分析" :span="2">
<div v-for="trend in currentReport.analysis.trends" :key="trend.indicator">
<div class="trend-item">
<span class="indicator">{{ trend.indicator }}</span>
<el-tag :type="trend.trend === '下降' ? 'danger' : 'success'">
{{ trend.trend }}
</el-tag>
</div>
</div>
</el-descriptions-item>
<el-descriptions-item label="异常情况" :span="2">
<div v-for="item in currentReport.analysis.abnormal" :key="item.type">
<div class="abnormal-item">
<span class="type">{{ item.type }}</span>
<span>{{ item.description }}</span>
<el-tag
:type="item.level === '严重' ? 'danger' : 'warning'"
size="small"
style="margin-left: 8px"
>
{{ item.level }}
</el-tag>
</div>
</div>
</el-descriptions-item>
<el-descriptions-item label="建议措施" :span="2">
<ul class="recommendations-list">
<li v-for="(item, index) in currentReport.recommendations" :key="index">
{{ item }}
</li>
</ul>
</el-descriptions-item>
</el-descriptions>
</template>
<template #footer>
<span class="dialog-footer">
<el-button @click="detailVisible = false">关闭</el-button>
<el-button
type="warning"
@click="handleExport(currentReport!)"
:disabled="!currentReport"
>
导出
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
.analysis-report {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.mt-20 {
margin-top: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.trend-item {
margin-bottom: 8px;
display: flex;
align-items: center;
.indicator {
margin-right: 8px;
color: $text-regular;
}
}
.abnormal-item {
margin-bottom: 8px;
.type {
color: $text-regular;
}
}
.recommendations-list {
margin: 0;
padding-left: 20px;
li {
margin-bottom: 4px;
color: $text-regular;
}
}
}
</style>

View File

@ -0,0 +1,241 @@
<script setup lang="ts">
import { ref } from 'vue';
interface DailyReport {
id: number;
date: string; //
type: 'patrol' | 'monitor'; // /
area: string; //
personnel: string[]; //
content: {
patrolRoutes?: string[]; // 线
monitorPoints?: string[]; //
findings: string; //
actions: string; //
};
attachments: { //
type: string;
url: string;
}[];
status: string; //
}
//
const tableData = ref<DailyReport[]>([
{
id: 1,
date: '2024-03-20',
type: 'patrol',
area: 'A区湿地',
personnel: ['张三', '李四'],
content: {
patrolRoutes: ['A区-1号点', 'A区-2号点', 'A区-3号点'],
findings: '发现2号监测点附近有水质异常',
actions: '已采集样本送检,并通知相关部门'
},
attachments: [
{ type: 'image', url: 'sample1.jpg' },
{ type: 'data', url: 'water_quality.xlsx' }
],
status: '已提交'
},
{
id: 2,
date: '2024-03-20',
type: 'monitor',
area: 'B区',
personnel: ['王五'],
content: {
monitorPoints: ['B区-水质监测点', 'B区-空气监测点'],
findings: '各项指标正常',
actions: '完成日常数据记录'
},
attachments: [
{ type: 'data', url: 'monitor_data.xlsx' }
],
status: '已审核'
}
]);
//
const filterForm = ref({
dateRange: [],
type: '',
area: '',
status: ''
});
//
const dialogVisible = ref(false);
const formData = ref({
type: '',
area: '',
personnel: [],
findings: '',
actions: '',
attachments: []
});
const handleCreate = () => {
dialogVisible.value = true;
};
const handleSubmit = () => {
// TODO:
dialogVisible.value = false;
};
</script>
<template>
<div class="daily-report">
<el-card>
<template #header>
<div class="card-header">
<span>日常报告</span>
<el-button type="primary" @click="handleCreate">新建报告</el-button>
</div>
</template>
<!-- 筛选表单 -->
<el-form :model="filterForm" inline>
<el-form-item label="日期范围">
<el-date-picker
v-model="filterForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="报告类型">
<el-select v-model="filterForm.type" placeholder="请选择">
<el-option label="巡护报告" value="patrol" />
<el-option label="监测报告" value="monitor" />
</el-select>
</el-form-item>
<el-form-item label="区域">
<el-select v-model="filterForm.area" placeholder="请选择">
<el-option label="A区" value="A区" />
<el-option label="B区" value="B区" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filterForm.status" placeholder="请选择">
<el-option label="已提交" value="已提交" />
<el-option label="已审核" value="已审核" />
</el-select>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="日期" width="120" />
<el-table-column prop="type" label="类型" width="100">
<template #default="{ row }">
{{ row.type === 'patrol' ? '巡护' : '监测' }}
</template>
</el-table-column>
<el-table-column prop="area" label="区域" width="120" />
<el-table-column label="人员" width="150">
<template #default="{ row }">
{{ row.personnel.join(', ') }}
</template>
</el-table-column>
<el-table-column prop="content.findings" label="发现问题" min-width="200" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '已审核' ? 'success' : 'warning'">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link>查看</el-button>
<el-button
type="success"
link
:disabled="row.status === '已审核'"
>
审核
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 新建报告弹窗 -->
<el-dialog
v-model="dialogVisible"
title="新建日常报告"
width="700px"
>
<el-form :model="formData" label-width="100px">
<el-form-item label="报告类型" required>
<el-radio-group v-model="formData.type">
<el-radio label="patrol">巡护报告</el-radio>
<el-radio label="monitor">监测报告</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="区域" required>
<el-select v-model="formData.area" placeholder="请选择区域">
<el-option label="A区" value="A区" />
<el-option label="B区" value="B区" />
</el-select>
</el-form-item>
<el-form-item label="相关人员" required>
<el-select
v-model="formData.personnel"
multiple
placeholder="请选择人员"
>
<el-option label="张三" value="张三" />
<el-option label="李四" value="李四" />
<el-option label="王五" value="王五" />
</el-select>
</el-form-item>
<el-form-item label="发现问题" required>
<el-input
v-model="formData.findings"
type="textarea"
rows="4"
placeholder="请描述发现的问题或异常情况"
/>
</el-form-item>
<el-form-item label="处理措施" required>
<el-input
v-model="formData.actions"
type="textarea"
rows="4"
placeholder="请描述采取的措施"
/>
</el-form-item>
<el-form-item label="附件">
<el-upload
action="#"
multiple
:auto-upload="false"
>
<el-button type="primary">选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
.daily-report {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,345 @@
<script setup lang="ts">
import { ref, computed, reactive } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import * as echarts from "echarts";
import { onMounted } from "vue";
interface AnalysisReport {
id: number;
title: string;
type: string;
period: string;
author: string;
summary: string;
charts: string[];
createTime: string;
}
const tableData = ref<AnalysisReport[]>([
{
id: 1,
title: "2024年第一季度水质分析报告",
type: "水质分析",
period: "季度",
author: "张三",
summary: "第一季度水质总体保持稳定...",
charts: ["chart1.png", "chart2.png"],
createTime: "2024-03-20",
},
{
id: 2,
title: "2024年3月物种监测分析报告",
type: "物种分析",
period: "月度",
author: "李四",
summary: "本月新增物种记录5种...",
charts: ["chart3.png"],
createTime: "2024-03-20",
},
]);
//
const initChart = () => {
const chartDom = document.getElementById("analysisChart");
if (!chartDom) return;
const myChart = echarts.init(chartDom);
const option = {
title: {
text: "各类型报告统计",
},
tooltip: {
trigger: "axis",
},
legend: {
data: ["水质分析", "物种分析", "生态分析"],
},
xAxis: {
type: "category",
data: ["1月", "2月", "3月", "4月", "5月", "6月"],
},
yAxis: {
type: "value",
},
series: [
{
name: "水质分析",
type: "line",
data: [5, 7, 6, 8, 9, 8],
},
{
name: "物种分析",
type: "line",
data: [3, 4, 5, 6, 5, 6],
},
{
name: "生态分析",
type: "line",
data: [2, 3, 4, 3, 4, 5],
},
],
};
myChart.setOption(option);
};
onMounted(() => {
initChart();
});
//
const dialogVisible = ref(false);
const formData = reactive({
title: "",
type: "",
period: "",
summary: "",
});
const handleCreate = () => {
dialogVisible.value = true;
};
const handleSubmit = () => {
console.log("提交报告:", formData);
ElMessage.success("提交成功");
dialogVisible.value = false;
};
//
const handleExport = (row: AnalysisReport) => {
ElMessageBox.confirm(
'确认导出该分析报告?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}
)
.then(() => {
ElMessage({
type: 'success',
message: '导出成功,文件已下载'
});
})
.catch(() => {
//
});
};
//
const detailVisible = ref(false);
const currentReport = ref<AnalysisReport | null>(null);
const handleView = (row: AnalysisReport) => {
currentReport.value = row;
detailVisible.value = true;
};
</script>
<template>
<div class="analysis-report-container">
<el-row :gutter="20">
<el-col :span="24">
<el-card>
<div id="analysisChart" style="height: 400px"></div>
</el-card>
</el-col>
</el-row>
<el-card class="mt-20">
<template #header>
<div class="card-header">
<span>分析报告</span>
<el-button type="primary" @click="handleCreate">新建报告</el-button>
</div>
</template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="报告标题" min-width="200" />
<el-table-column prop="type" label="类型" width="120" />
<el-table-column prop="period" label="统计周期" width="100" />
<el-table-column prop="author" label="作者" width="120" />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleView(row)">
查看
</el-button>
<el-button
type="warning"
size="small"
@click="handleExport(row)"
>
导出
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 新建报告弹窗 -->
<el-dialog v-model="dialogVisible" title="新建分析报告" width="800px">
<el-form :model="formData" label-width="100px">
<el-form-item label="报告标题" required>
<el-input v-model="formData.title" placeholder="请输入报告标题" />
</el-form-item>
<el-form-item label="报告类型" required>
<el-select v-model="formData.type" placeholder="请选择报告类型">
<el-option label="水质分析" value="水质分析" />
<el-option label="物种分析" value="物种分析" />
<el-option label="生态分析" value="生态分析" />
</el-select>
</el-form-item>
<el-form-item label="统计周期" required>
<el-select v-model="formData.period" placeholder="请选择统计周期">
<el-option label="月度" value="月度" />
<el-option label="季度" value="季度" />
<el-option label="年度" value="年度" />
</el-select>
</el-form-item>
<el-form-item label="报告摘要" required>
<el-input
v-model="formData.summary"
type="textarea"
rows="6"
placeholder="请输入报告摘要"
/>
</el-form-item>
<el-form-item label="数据图表">
<el-upload
action="#"
multiple
:auto-upload="false"
accept="image/*"
>
<el-button type="primary">上传图表</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</span>
</template>
</el-dialog>
<!-- 添加详情弹窗 -->
<el-dialog v-model="detailVisible" title="分析报告详情" width="800px">
<template v-if="currentReport">
<el-descriptions :column="2" border>
<el-descriptions-item label="报告标题" :span="2">
{{ currentReport.title }}
</el-descriptions-item>
<el-descriptions-item label="报告类型">
{{ currentReport.type }}
</el-descriptions-item>
<el-descriptions-item label="统计周期">
{{ currentReport.period }}
</el-descriptions-item>
<el-descriptions-item label="作者">
{{ currentReport.author }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ currentReport.createTime }}
</el-descriptions-item>
<el-descriptions-item label="报告摘要" :span="2">
{{ currentReport.summary }}
</el-descriptions-item>
<el-descriptions-item label="数据图表" :span="2">
<div class="chart-list">
<el-image
v-for="(chart, index) in currentReport.charts"
:key="index"
:src="chart"
fit="cover"
style="width: 200px; height: 150px; margin-right: 16px"
>
<template #error>
<div class="image-placeholder">
图表预览
</div>
</template>
</el-image>
</div>
</el-descriptions-item>
</el-descriptions>
</template>
<template #footer>
<span class="dialog-footer">
<el-button @click="detailVisible = false">关闭</el-button>
<el-button
type="warning"
@click="handleExport(currentReport!)"
:disabled="!currentReport"
>
导出
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.analysis-report-container {
.el-card {
background: #ffffff;
border: none;
border-radius: 4px;
box-shadow: $box-shadow;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
.mt-20 {
margin-top: 20px;
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
color: $text-primary;
font-weight: 500;
}
}
:deep(.el-form) {
.el-select {
width: 100%;
}
}
.chart-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.image-placeholder {
width: 200px;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f7fa;
color: $text-secondary;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,344 @@
<script setup lang="ts">
import { ref, computed, reactive } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from "@element-plus/icons-vue";
interface DailyReport {
id: number;
title: string;
reporter: string;
department: string;
type: string;
content: string;
attachments: string[];
status: string;
createTime: string;
}
const tableData = ref<DailyReport[]>([
{
id: 1,
title: "A区巡护日报",
reporter: "张三",
department: "巡护部",
type: "巡护报告",
content: "今日巡护发现...",
attachments: ["report1.pdf", "image1.jpg"],
status: "已提交",
createTime: "2024-03-20 10:30",
},
{
id: 2,
title: "水质监测日报",
reporter: "李四",
department: "监测部",
type: "监测报告",
content: "今日水质监测数据显示...",
attachments: ["report2.pdf"],
status: "待审核",
createTime: "2024-03-20 11:20",
},
]);
//
const searchText = ref("");
const typeFilter = ref<string[]>([]);
const statusFilter = ref<string[]>([]);
const dateRange = ref<[string, string]>(["", ""]);
//
const filteredData = computed(() => {
return tableData.value.filter(report => {
const searchLower = searchText.value.toLowerCase();
const textMatch = !searchText.value ||
report.title.toLowerCase().includes(searchLower) ||
report.reporter.toLowerCase().includes(searchLower);
const typeMatch = typeFilter.value.length === 0 ||
typeFilter.value.includes(report.type);
const statusMatch = statusFilter.value.length === 0 ||
statusFilter.value.includes(report.status);
return textMatch && typeMatch && statusMatch;
});
});
//
const dialogVisible = ref(false);
const formData = reactive({
title: "",
type: "",
content: "",
attachments: [] as string[],
});
const handleCreate = () => {
dialogVisible.value = true;
};
const handleSubmit = () => {
console.log("提交报告:", formData);
ElMessage.success("提交成功");
dialogVisible.value = false;
};
//
const detailVisible = ref(false);
const currentReport = ref<DailyReport | null>(null);
const handleView = (row: DailyReport) => {
currentReport.value = row;
detailVisible.value = true;
};
//
const handleExport = (row: DailyReport) => {
ElMessageBox.confirm(
'确认导出该报告?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}
)
.then(() => {
ElMessage({
type: 'success',
message: '导出成功,文件已下载'
});
})
.catch(() => {
//
});
};
</script>
<template>
<div class="daily-report-container">
<el-card>
<template #header>
<div class="card-header">
<span>日常报告</span>
<div class="header-right">
<el-input
v-model="searchText"
placeholder="搜索报告标题/提交人"
style="width: 300px"
clearable
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-select
v-model="typeFilter"
multiple
collapse-tags
placeholder="报告类型"
style="width: 200px; margin-left: 16px"
clearable
>
<el-option label="巡护报告" value="巡护报告" />
<el-option label="监测报告" value="监测报告" />
</el-select>
<el-select
v-model="statusFilter"
multiple
collapse-tags
placeholder="状态筛选"
style="width: 200px; margin-left: 16px"
clearable
>
<el-option label="已提交" value="已提交" />
<el-option label="待审核" value="待审核" />
<el-option label="已审核" value="已审核" />
</el-select>
<el-button type="primary" @click="handleCreate" style="margin-left: 16px">
新建报告
</el-button>
</div>
</div>
</template>
<el-table :data="filteredData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="报告标题" min-width="200" />
<el-table-column prop="reporter" label="提交人" width="120" />
<el-table-column prop="department" label="部门" width="120" />
<el-table-column prop="type" label="类型" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '已审核' ? 'success' : 'warning'">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleView(row)">
查看
</el-button>
<el-button
type="success"
size="small"
:disabled="row.status === '已审核'"
>
审核
</el-button>
<el-button
type="warning"
size="small"
@click="handleExport(row)"
>
导出
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 新建报告弹窗 -->
<el-dialog v-model="dialogVisible" title="新建报告" width="800px">
<el-form :model="formData" label-width="100px">
<el-form-item label="报告标题" required>
<el-input v-model="formData.title" placeholder="请输入报告标题" />
</el-form-item>
<el-form-item label="报告类型" required>
<el-select v-model="formData.type" placeholder="请选择报告类型">
<el-option label="巡护报告" value="巡护报告" />
<el-option label="监测报告" value="监测报告" />
</el-select>
</el-form-item>
<el-form-item label="报告内容" required>
<el-input
v-model="formData.content"
type="textarea"
rows="10"
placeholder="请输入报告内容"
/>
</el-form-item>
<el-form-item label="附件">
<el-upload
action="#"
multiple
:auto-upload="false"
>
<el-button type="primary">选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</span>
</template>
</el-dialog>
<!-- 报告详情弹窗 -->
<el-dialog v-model="detailVisible" title="报告详情" width="800px">
<template v-if="currentReport">
<el-descriptions :column="2" border>
<el-descriptions-item label="报告标题" :span="2">
{{ currentReport.title }}
</el-descriptions-item>
<el-descriptions-item label="提交人">
{{ currentReport.reporter }}
</el-descriptions-item>
<el-descriptions-item label="部门">
{{ currentReport.department }}
</el-descriptions-item>
<el-descriptions-item label="报告类型">
{{ currentReport.type }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="currentReport.status === '已审核' ? 'success' : 'warning'">
{{ currentReport.status }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="报告内容" :span="2">
{{ currentReport.content }}
</el-descriptions-item>
<el-descriptions-item label="附件" :span="2">
<div class="attachment-list">
<el-link
v-for="file in currentReport.attachments"
:key="file"
type="primary"
style="margin-right: 16px"
>
{{ file }}
</el-link>
</div>
</el-descriptions-item>
</el-descriptions>
</template>
<template #footer>
<span class="dialog-footer">
<el-button @click="detailVisible = false">关闭</el-button>
<el-button
type="warning"
@click="handleExport(currentReport!)"
:disabled="!currentReport"
>
导出
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.daily-report-container {
.el-card {
background: #ffffff;
border: none;
border-radius: 4px;
box-shadow: $box-shadow;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
.header-right {
display: flex;
align-items: center;
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
color: $text-primary;
font-weight: 500;
}
}
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:deep(.el-descriptions) {
.el-descriptions__label {
width: 120px;
justify-content: right;
}
}
</style>

View File

@ -0,0 +1,284 @@
<script setup lang="ts">
import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import type { UploadProps } from "element-plus";
//
interface BackupRecord {
id: number;
name: string;
size: string;
type: string;
status: string;
createTime: string;
}
//
const backupRecords = ref<BackupRecord[]>([
{
id: 1,
name: "系统完整备份_20240320",
size: "256MB",
type: "完整备份",
status: "成功",
createTime: "2024-03-20 00:00:00",
},
{
id: 2,
name: "监测数据备份_20240320",
size: "128MB",
type: "数据备份",
status: "成功",
createTime: "2024-03-20 12:00:00",
},
]);
//
const uploadConfig: UploadProps = {
action: "/api/upload",
multiple: true,
accept: ".xlsx,.csv",
beforeUpload: (file) => {
const isValidType = [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"text/csv",
].includes(file.type);
if (!isValidType) {
ElMessage.error("只能上传 Excel 或 CSV 文件!");
return false;
}
return true;
},
onSuccess: () => {
ElMessage.success("导入成功");
},
onError: () => {
ElMessage.error("导入失败");
},
};
//
const backupConfig = ref({
autoBackup: true,
backupTime: "00:00",
backupType: "full", // full: , data:
retention: 30, //
});
//
const handleBackup = () => {
ElMessageBox.confirm("确认开始备份?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
ElMessage.success("备份任务已启动");
const newBackup: BackupRecord = {
id: Date.now(),
name: `系统备份_${new Date().toISOString().split("T")[0].replace(/-/g, "")}`,
size: "处理中",
type: backupConfig.value.backupType === "full" ? "完整备份" : "数据备份",
status: "进行中",
createTime: new Date().toLocaleString(),
};
backupRecords.value.unshift(newBackup);
//
setTimeout(() => {
const index = backupRecords.value.findIndex((item) => item.id === newBackup.id);
if (index !== -1) {
backupRecords.value[index].status = "成功";
backupRecords.value[index].size = "128MB";
}
}, 3000);
});
};
//
const handleRestore = (record: BackupRecord) => {
if (record.status !== "成功") {
ElMessage.warning("只能恢复成功的备份");
return;
}
ElMessageBox.confirm("确认恢复该备份?此操作将覆盖当前数据!", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
ElMessage.success("恢复任务已启动");
});
};
//
const handleDelete = (record: BackupRecord) => {
ElMessageBox.confirm("确认删除该备份?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
const index = backupRecords.value.findIndex((item) => item.id === record.id);
if (index !== -1) {
backupRecords.value.splice(index, 1);
ElMessage.success("删除成功");
}
});
};
//
const handleExport = () => {
ElMessage.success("正在导出数据,请稍候...");
};
</script>
<template>
<div class="data-container">
<!-- 数据导入导出 -->
<el-card class="mb-20">
<template #header>
<div class="card-header">
<span>数据导入导出</span>
<div class="header-btns">
<el-upload v-bind="uploadConfig" class="upload-btn">
<el-button type="primary">导入数据</el-button>
</el-upload>
<el-button type="success" @click="handleExport">导出数据</el-button>
</div>
</div>
</template>
<el-alert
title="支持导入导出的数据类型:监测数据、设备数据、巡护记录等"
type="info"
:closable="false"
class="mb-20"
/>
<el-form label-width="120px">
<el-form-item label="导入说明">
<ol class="import-tips">
<li>支持 Excel CSV 格式的文件</li>
<li>文件大小不超过 10MB</li>
<li>请严格按照模板格式填写数据</li>
<li>导入前请注意备份现有数据</li>
</ol>
</el-form-item>
</el-form>
</el-card>
<!-- 数据备份 -->
<el-card>
<template #header>
<div class="card-header">
<span>数据备份</span>
<el-button type="primary" @click="handleBackup">立即备份</el-button>
</div>
</template>
<!-- 备份配置 -->
<el-form :model="backupConfig" label-width="120px" class="mb-20">
<el-form-item label="自动备份">
<el-switch v-model="backupConfig.autoBackup" />
</el-form-item>
<el-form-item label="备份时间">
<el-time-picker
v-model="backupConfig.backupTime"
format="HH:mm"
placeholder="选择时间"
:disabled="!backupConfig.autoBackup"
/>
</el-form-item>
<el-form-item label="备份类型">
<el-radio-group v-model="backupConfig.backupType">
<el-radio label="full">完整备份</el-radio>
<el-radio label="data">数据备份</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="保留时间">
<el-input-number v-model="backupConfig.retention" :min="7" :max="365" :step="1" />
<span class="ml-10"></span>
</el-form-item>
</el-form>
<!-- 备份记录 -->
<el-table :data="backupRecords" style="width: 100%">
<el-table-column prop="name" label="备份名称" min-width="200" />
<el-table-column prop="size" label="大小" width="100" />
<el-table-column prop="type" label="类型" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '成功' ? 'success' : 'warning'">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
:disabled="row.status !== '成功'"
@click="handleRestore(row)"
>
恢复
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.data-container {
.mb-20 {
margin-bottom: 20px;
}
.ml-10 {
margin-left: 10px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
.header-btns {
display: flex;
gap: 12px;
}
}
.import-tips {
margin: 0;
padding-left: 20px;
color: $text-regular;
line-height: 1.8;
}
:deep(.el-card) {
border: none;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
:deep(.upload-btn) {
.el-upload {
width: auto;
}
}
}
</style>

View File

@ -0,0 +1,664 @@
<script setup lang="ts">
import { ref, onUnmounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import * as echarts from "echarts";
import { Plus } from "@element-plus/icons-vue";
interface Device {
id: number;
name: string;
type: string;
location: string;
status: string;
ip: string;
lastHeartbeat: string;
lastMaintenance: string;
}
//
const tableData = ref<Device[]>([
{
id: 1,
name: "A区-水质监测器-01",
type: "水质监测",
location: "A区湿地",
status: "在线",
ip: "192.168.1.101",
lastHeartbeat: "2024-03-20 12:00:00",
lastMaintenance: "2024-03-15",
},
{
id: 2,
name: "B区-空气监测器-01",
type: "空气监测",
location: "B区湿地",
status: "离线",
ip: "192.168.1.102",
lastHeartbeat: "2024-03-20 10:30:00",
lastMaintenance: "2024-03-10",
},
]);
//
const deviceTypes = [
{ label: "水质监测", value: "水质监测" },
{ label: "空气监测", value: "空气监测" },
{ label: "视频监控", value: "视频监控" },
{ label: "气象站", value: "气象站" },
];
// /
const dialogVisible = ref(false);
const isEdit = ref(false);
const currentDevice = ref<Device | null>(null);
const formData = ref({
name: "",
type: "",
location: "",
ip: "",
});
//
const rules = {
name: [{ required: true, message: "请输入设备名称", trigger: "blur" }],
type: [{ required: true, message: "请选择设备类型", trigger: "change" }],
location: [{ required: true, message: "请输入安装位置", trigger: "blur" }],
ip: [{ required: true, message: "请输入IP地址", trigger: "blur" }],
};
const formRef = ref();
//
const handleAdd = () => {
isEdit.value = false;
currentDevice.value = null;
resetForm();
dialogVisible.value = true;
};
//
const handleEdit = (row: Device) => {
isEdit.value = true;
currentDevice.value = row;
formData.value = {
name: row.name,
type: row.type,
location: row.location,
ip: row.ip,
};
dialogVisible.value = true;
};
//
const handleSubmit = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
if (isEdit.value && currentDevice.value) {
//
const index = tableData.value.findIndex(
(item) => item.id === currentDevice.value!.id
);
if (index !== -1) {
tableData.value[index] = {
...currentDevice.value,
...formData.value,
lastHeartbeat: new Date().toLocaleString(),
};
ElMessage.success("更新成功");
}
} else {
//
const newDevice: Device = {
id: tableData.value.length + 1,
...formData.value,
status: "在线",
lastHeartbeat: new Date().toLocaleString(),
lastMaintenance: new Date().toLocaleDateString(),
};
tableData.value.push(newDevice);
ElMessage.success("添加成功");
}
dialogVisible.value = false;
} catch (error) {
console.error("表单验证失败:", error);
}
};
//
const resetForm = () => {
formData.value = {
name: "",
type: "",
location: "",
ip: "",
};
if (formRef.value) {
formRef.value.resetFields();
}
};
//
const handleDelete = (row: Device) => {
ElMessageBox.confirm("确认删除该设备?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
const index = tableData.value.findIndex((item) => item.id === row.id);
if (index !== -1) {
tableData.value.splice(index, 1);
ElMessage.success("删除成功");
}
})
.catch(() => {});
};
//
const handleConfig = (row: Device) => {
ElMessage.success("正在连接设备...");
};
//
const handleMaintenance = (row: Device) => {
ElMessage.success("已记录维护时间");
const index = tableData.value.findIndex((item) => item.id === row.id);
if (index !== -1) {
tableData.value[index].lastMaintenance = new Date().toLocaleDateString();
}
};
//
const searchForm = ref({
name: "",
type: "",
status: "",
});
//
const groupData = ref([
{
id: 1,
label: "A区设备",
children: [
{ id: 11, label: "水质监测设备" },
{ id: 12, label: "空气监测设备" },
],
},
{
id: 2,
label: "B区设备",
children: [
{ id: 21, label: "视频监控设备" },
{ id: 22, label: "气象站设备" },
],
},
]);
//
const defaultProps = {
children: "children",
label: "label",
};
//
const monitorVisible = ref(false);
const currentMonitorDevice = ref<Device | null>(null);
const monitorChart = ref<echarts.ECharts | null>(null);
//
const realTimeData = ref({
temperature: [],
humidity: [],
ph: [],
oxygen: [],
});
//
const alarmConfig = ref({
offlineAlarm: true,
thresholds: {
temperature: { min: 0, max: 40 },
humidity: { min: 20, max: 80 },
ph: { min: 6.5, max: 8.5 },
oxygen: { min: 5, max: 9 },
},
});
//
const handleSearch = () => {
//
console.log("搜索条件:", searchForm.value);
};
const resetSearch = () => {
searchForm.value = {
name: "",
type: "",
status: "",
};
};
//
const handleAddGroup = () => {
ElMessageBox.prompt("请输入分组名称", "新增分组", {
confirmButtonText: "确定",
cancelButtonText: "取消",
}).then(({ value }) => {
groupData.value.push({
id: Date.now(),
label: value,
children: [],
});
ElMessage.success("添加成功");
});
};
const handleNodeClick = (data: any) => {
console.log("选中分组:", data);
//
};
//
const handleMonitor = (device: Device) => {
currentMonitorDevice.value = device;
monitorVisible.value = true;
initMonitorChart();
startRealTimeData();
};
//
const initMonitorChart = () => {
const chartDom = document.getElementById("monitorChart");
if (!chartDom) return;
monitorChart.value = echarts.init(chartDom);
const option = {
title: {
text: "实时数据监控",
left: "center",
},
tooltip: {
trigger: "axis",
},
legend: {
data: ["温度", "湿度", "pH值", "溶解氧"],
top: 30,
},
grid: {
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
xAxis: {
type: "category",
boundaryGap: false,
data: Array.from({ length: 20 }, (_, i) => i.toString()),
},
yAxis: {
type: "value",
},
series: [
{
name: "温度",
type: "line",
data: [],
},
{
name: "湿度",
type: "line",
data: [],
},
{
name: "pH值",
type: "line",
data: [],
},
{
name: "溶解氧",
type: "line",
data: [],
},
],
};
monitorChart.value.setOption(option);
};
//
let dataTimer: number;
const startRealTimeData = () => {
clearInterval(dataTimer);
dataTimer = setInterval(() => {
//
const newData = {
temperature: Math.random() * 30 + 10,
humidity: Math.random() * 40 + 40,
ph: Math.random() * 2 + 6.5,
oxygen: Math.random() * 4 + 5,
};
//
checkAlarms(newData);
//
if (monitorChart.value) {
monitorChart.value.setOption({
series: [
{
data: [...realTimeData.value.temperature.slice(-19), newData.temperature],
},
{
data: [...realTimeData.value.humidity.slice(-19), newData.humidity],
},
{
data: [...realTimeData.value.ph.slice(-19), newData.ph],
},
{
data: [...realTimeData.value.oxygen.slice(-19), newData.oxygen],
},
],
});
}
//
realTimeData.value = {
temperature: [...realTimeData.value.temperature.slice(-19), newData.temperature],
humidity: [...realTimeData.value.humidity.slice(-19), newData.humidity],
ph: [...realTimeData.value.ph.slice(-19), newData.ph],
oxygen: [...realTimeData.value.oxygen.slice(-19), newData.oxygen],
};
}, 1000);
};
//
const checkAlarms = (data: any) => {
const { thresholds } = alarmConfig.value;
if (
data.temperature < thresholds.temperature.min ||
data.temperature > thresholds.temperature.max
) {
ElMessage.warning(`温度异常: ${data.temperature}°C`);
}
if (data.ph < thresholds.ph.min || data.ph > thresholds.ph.max) {
ElMessage.warning(`pH值异常: ${data.ph}`);
}
// ...
};
//
onUnmounted(() => {
clearInterval(dataTimer);
});
</script>
<template>
<div class="device-container">
<!-- 设备分组和列表布局 -->
<el-row :gutter="20">
<!-- 左侧设备分组 -->
<el-col :span="4">
<el-card class="group-card">
<template #header>
<div class="card-header">
<span>设备分组</span>
<el-button type="text" @click="handleAddGroup">
<el-icon><Plus /></el-icon>
</el-button>
</div>
</template>
<el-tree
:data="groupData"
:props="defaultProps"
@node-click="handleNodeClick"
default-expand-all
/>
</el-card>
</el-col>
<!-- 右侧设备列表 -->
<el-col :span="20">
<el-card>
<template #header>
<div class="card-header">
<span>设备管理</span>
<el-button type="primary" @click="handleAdd">添加设备</el-button>
</div>
</template>
<!-- 搜索表单 -->
<el-form :model="searchForm" inline class="search-form">
<el-form-item label="设备名称">
<el-input v-model="searchForm.name" placeholder="请输入设备名称" clearable />
</el-form-item>
<el-form-item label="设备类型">
<el-select v-model="searchForm.type" placeholder="请选择" clearable>
<el-option
v-for="item in deviceTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择" clearable>
<el-option label="在线" value="在线" />
<el-option label="离线" value="离线" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<!-- 设备列表 -->
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="设备名称" min-width="150" />
<el-table-column prop="type" label="设备类型" width="120" />
<el-table-column prop="location" label="安装位置" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '在线' ? 'success' : 'danger'">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="ip" label="IP地址" width="140" />
<el-table-column prop="lastHeartbeat" label="最后心跳" width="180" />
<el-table-column prop="lastMaintenance" label="最后维护" width="120" />
<el-table-column label="操作" width="380" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">
编辑
</el-button>
<el-button type="success" size="small" @click="handleConfig(row)">
远程配置
</el-button>
<el-button type="info" size="small" @click="handleMonitor(row)">
实时监控
</el-button>
<el-button type="warning" size="small" @click="handleMaintenance(row)">
维护
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 新增/编辑设备弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑设备' : '添加设备'"
width="600px"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="设备名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入设备名称" />
</el-form-item>
<el-form-item label="设备类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择设备类型">
<el-option
v-for="item in deviceTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="安装位置" prop="location">
<el-input v-model="formData.location" placeholder="请输入安装位置" />
</el-form-item>
<el-form-item label="IP地址" prop="ip">
<el-input v-model="formData.ip" placeholder="请输入IP地址" />
</el-form-item>
<el-form-item label="告警配置">
<el-collapse>
<el-collapse-item title="阈值设置">
<el-form :model="alarmConfig">
<el-form-item label="离线告警">
<el-switch v-model="alarmConfig.offlineAlarm" />
</el-form-item>
<el-form-item label="温度范围">
<el-col :span="11">
<el-input-number
v-model="alarmConfig.thresholds.temperature.min"
:precision="1"
/>
</el-col>
<el-col :span="2" class="text-center">-</el-col>
<el-col :span="11">
<el-input-number
v-model="alarmConfig.thresholds.temperature.max"
:precision="1"
/>
</el-col>
</el-form-item>
<el-form-item label="pH值范围">
<el-col :span="11">
<el-input-number
v-model="alarmConfig.thresholds.ph.min"
:precision="1"
:step="0.1"
/>
</el-col>
<el-col :span="2" class="text-center">-</el-col>
<el-col :span="11">
<el-input-number
v-model="alarmConfig.thresholds.ph.max"
:precision="1"
:step="0.1"
/>
</el-col>
</el-form-item>
</el-form>
</el-collapse-item>
</el-collapse>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 实时监控弹窗 -->
<el-dialog v-model="monitorVisible" title="设备实时监控" width="800px" destroy-on-close>
<div class="monitor-container">
<el-row :gutter="20">
<el-col :span="8">
<div class="data-card">
<h3>实时数据</h3>
<el-descriptions :column="1" border>
<el-descriptions-item label="设备名称">
{{ currentMonitorDevice?.name }}
</el-descriptions-item>
<el-descriptions-item label="设备状态">
<el-tag
:type="currentMonitorDevice?.status === '在线' ? 'success' : 'danger'"
>
{{ currentMonitorDevice?.status }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="最后心跳">
{{ currentMonitorDevice?.lastHeartbeat }}
</el-descriptions-item>
</el-descriptions>
</div>
</el-col>
<el-col :span="16">
<div id="monitorChart" style="height: 300px" />
</el-col>
</el-row>
</div>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.device-container {
.group-card {
height: calc(100vh - 140px);
overflow-y: auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
.search-form {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid $border-color;
}
.text-center {
text-align: center;
line-height: 32px;
}
.monitor-container {
.data-card {
padding: 20px;
background: #f8f9fa;
border-radius: 4px;
h3 {
margin: 0 0 16px;
font-size: 16px;
color: $text-primary;
}
}
}
:deep(.el-card) {
border: none;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
}
</style>

View File

@ -0,0 +1,301 @@
<script setup lang="ts">
import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
interface Permission {
id: number;
name: string;
code: string;
description: string;
type: string;
status: boolean;
createTime: string;
}
const tableData = ref<Permission[]>([
{
id: 1,
name: "用户管理",
code: "system:user",
description: "用户的增删改查权限",
type: "菜单权限",
status: true,
createTime: "2024-03-20",
},
{
id: 2,
name: "角色管理",
code: "system:role",
description: "角色的增删改查权限",
type: "菜单权限",
status: true,
createTime: "2024-03-20",
},
{
id: 3,
name: "新增用户",
code: "system:user:add",
description: "新增用户的权限",
type: "操作权限",
status: true,
createTime: "2024-03-20",
},
]);
// /
const dialogVisible = ref(false);
const isEdit = ref(false);
const currentPermission = ref<Permission | null>(null);
const formData = ref({
name: "",
code: "",
description: "",
type: "",
status: true,
});
//
const rules = {
name: [{ required: true, message: "请输入权限名称", trigger: "blur" }],
code: [{ required: true, message: "请输入权限标识", trigger: "blur" }],
type: [{ required: true, message: "请选择权限类型", trigger: "change" }],
};
const formRef = ref();
//
const handleAdd = () => {
isEdit.value = false;
currentPermission.value = null;
resetForm();
dialogVisible.value = true;
};
//
const handleEdit = (row: Permission) => {
isEdit.value = true;
currentPermission.value = row;
formData.value = {
name: row.name,
code: row.code,
description: row.description,
type: row.type,
status: row.status,
};
dialogVisible.value = true;
};
//
const handleSubmit = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
if (isEdit.value && currentPermission.value) {
//
const index = tableData.value.findIndex(
(item) => item.id === currentPermission.value!.id
);
if (index !== -1) {
tableData.value[index] = {
...currentPermission.value,
...formData.value,
};
ElMessage.success("更新成功");
}
} else {
//
const newPermission: Permission = {
id: tableData.value.length + 1,
...formData.value,
createTime: new Date().toLocaleString(),
};
tableData.value.push(newPermission);
ElMessage.success("添加成功");
}
dialogVisible.value = false;
} catch (error) {
console.error("表单验证失败:", error);
}
};
//
const resetForm = () => {
formData.value = {
name: "",
code: "",
description: "",
type: "",
status: true,
};
if (formRef.value) {
formRef.value.resetFields();
}
};
//
const handleDialogClose = () => {
resetForm();
isEdit.value = false;
currentPermission.value = null;
};
//
const handleDelete = (row: Permission) => {
ElMessageBox.confirm("确认删除该权限?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
//
const index = tableData.value.findIndex((item) => item.id === row.id);
if (index !== -1) {
tableData.value.splice(index, 1);
ElMessage.success("删除成功");
}
})
.catch(() => {
//
});
};
</script>
<template>
<div class="permission-container">
<el-card>
<template #header>
<div class="card-header">
<span>权限管理</span>
<el-button type="primary" @click="handleAdd">新增权限</el-button>
</div>
</template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="权限名称" width="150" />
<el-table-column prop="code" label="权限标识" width="150" />
<el-table-column prop="description" label="描述" min-width="200" />
<el-table-column prop="type" label="类型" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status ? 'success' : 'danger'">
{{ row.status ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">
编辑
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 新增/编辑权限弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑权限' : '新增权限'"
width="500px"
@close="handleDialogClose"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="权限名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入权限名称" />
</el-form-item>
<el-form-item label="权限标识" prop="code">
<el-input
v-model="formData.code"
placeholder="请输入权限标识"
:disabled="isEdit"
/>
</el-form-item>
<el-form-item label="权限类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择权限类型">
<el-option label="菜单权限" value="菜单权限" />
<el-option label="操作权限" value="操作权限" />
</el-select>
</el-form-item>
<el-form-item label="权限描述">
<el-input
v-model="formData.description"
type="textarea"
rows="4"
placeholder="请输入权限描述"
/>
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="formData.status" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.permission-container {
.el-card {
background: #ffffff;
border: none;
border-radius: 4px;
box-shadow: $box-shadow;
.el-card__header {
padding: 16px 20px;
border-bottom: 1px solid $border-color;
}
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
color: $text-primary;
font-weight: 500;
}
.el-button--small {
padding: 6px 16px;
}
}
:deep(.el-form) {
.el-form-item__label {
font-weight: normal;
color: $text-regular;
}
}
.dialog-footer {
.el-button {
margin-left: 12px;
}
}
</style>

View File

@ -0,0 +1,120 @@
<script setup lang="ts">
import { ref } from "vue";
interface RoleData {
id: number;
name: string;
description: string;
permissions: string[];
createTime: string;
}
const tableData = ref<RoleData[]>([
{
id: 1,
name: "超级管理员",
description: "系统最高权限",
permissions: ["all"],
createTime: "2024-03-20",
},
{
id: 2,
name: "管理人员",
description: "日常运维管理",
permissions: ["monitor", "patrol"],
createTime: "2024-03-20",
},
]);
const handleEdit = (row: RoleData) => {
console.log("编辑角色:", row);
};
const handleDelete = (row: RoleData) => {
console.log("删除角色:", row);
};
</script>
<template>
<div class="role-container">
<el-card>
<template #header>
<div class="card-header">
<span>角色管理</span>
<el-button type="primary">新增角色</el-button>
</div>
</template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="角色名称" width="120" />
<el-table-column prop="description" label="描述" />
<el-table-column label="权限" width="200">
<template #default="{ row }">
<el-tag v-for="perm in row.permissions" :key="perm" class="permission-tag">
{{ perm }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">
编辑
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.role-container {
.el-card {
background: #ffffff;
border: none;
border-radius: 4px;
box-shadow: $box-shadow;
.el-card__header {
padding: 16px 20px;
border-bottom: 1px solid $border-color;
}
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
.permission-tag {
margin-right: 8px;
margin-bottom: 4px;
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
color: $text-primary;
font-weight: 500;
}
.el-button--small {
padding: 6px 16px;
}
}
</style>

View File

@ -0,0 +1,180 @@
<script setup lang="ts">
import { ref } from 'vue';
//
const systemConfig = ref({
dataCollectionInterval: 5, //
dataRetentionDays: 90, //
autoBackupEnabled: true, //
backupInterval: 24 //
});
// AI
const aiConfig = ref({
modelVersion: 'v2.0.1',
confidenceThreshold: 0.85,
maxDetections: 100,
enabledModels: ['bird', 'fish', 'plant']
});
//
const alertConfig = ref({
waterQuality: {
ph: { min: 6.5, max: 8.5 },
dissolvedOxygen: { min: 5, max: 9 },
temperature: { min: 15, max: 30 }
},
airQuality: {
pm25: { max: 75 },
humidity: { min: 30, max: 70 }
}
});
//
const handleSave = (type: string) => {
//
console.log(`保存${type}配置:`,
type === 'system' ? systemConfig.value :
type === 'ai' ? aiConfig.value : alertConfig.value
);
};
</script>
<template>
<div class="settings-container">
<!-- 系统参数配置 -->
<el-card class="mb-20">
<template #header>
<div class="card-header">
<span>系统参数配置</span>
<el-button type="primary" @click="handleSave('system')">保存配置</el-button>
</div>
</template>
<el-form :model="systemConfig" label-width="160px">
<el-form-item label="数据采集间隔(分钟)">
<el-input-number v-model="systemConfig.dataCollectionInterval" :min="1" :max="60" />
</el-form-item>
<el-form-item label="数据保留天数">
<el-input-number v-model="systemConfig.dataRetentionDays" :min="30" :max="365" />
</el-form-item>
<el-form-item label="自动备份">
<el-switch v-model="systemConfig.autoBackupEnabled" />
</el-form-item>
<el-form-item label="备份间隔(小时)">
<el-input-number v-model="systemConfig.backupInterval" :min="1" :max="168" />
</el-form-item>
</el-form>
</el-card>
<!-- AI模型配置 -->
<el-card class="mb-20">
<template #header>
<div class="card-header">
<span>AI模型配置</span>
<el-button type="primary" @click="handleSave('ai')">保存配置</el-button>
</div>
</template>
<el-form :model="aiConfig" label-width="160px">
<el-form-item label="模型版本">
<el-input v-model="aiConfig.modelVersion" disabled />
</el-form-item>
<el-form-item label="置信度阈值">
<el-slider v-model="aiConfig.confidenceThreshold" :min="0" :max="1" :step="0.01" />
</el-form-item>
<el-form-item label="最大检测数量">
<el-input-number v-model="aiConfig.maxDetections" :min="10" :max="500" />
</el-form-item>
<el-form-item label="启用的模型">
<el-checkbox-group v-model="aiConfig.enabledModels">
<el-checkbox label="bird">鸟类识别</el-checkbox>
<el-checkbox label="fish">鱼类识别</el-checkbox>
<el-checkbox label="plant">植物识别</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
</el-card>
<!-- 预警阈值配置 -->
<el-card>
<template #header>
<div class="card-header">
<span>预警阈值配置</span>
<el-button type="primary" @click="handleSave('alert')">保存配置</el-button>
</div>
</template>
<el-form :model="alertConfig" label-width="160px">
<el-divider content-position="left">水质指标</el-divider>
<el-form-item label="pH值范围">
<el-col :span="11">
<el-input-number v-model="alertConfig.waterQuality.ph.min" :precision="1" :step="0.1" />
</el-col>
<el-col :span="2" class="text-center">-</el-col>
<el-col :span="11">
<el-input-number v-model="alertConfig.waterQuality.ph.max" :precision="1" :step="0.1" />
</el-col>
</el-form-item>
<el-form-item label="溶解氧范围(mg/L)">
<el-col :span="11">
<el-input-number v-model="alertConfig.waterQuality.dissolvedOxygen.min" :precision="1" />
</el-col>
<el-col :span="2" class="text-center">-</el-col>
<el-col :span="11">
<el-input-number v-model="alertConfig.waterQuality.dissolvedOxygen.max" :precision="1" />
</el-col>
</el-form-item>
<el-divider content-position="left">空气指标</el-divider>
<el-form-item label="PM2.5上限(μg/m³)">
<el-input-number v-model="alertConfig.airQuality.pm25.max" />
</el-form-item>
<el-form-item label="湿度范围(%)">
<el-col :span="11">
<el-input-number v-model="alertConfig.airQuality.humidity.min" />
</el-col>
<el-col :span="2" class="text-center">-</el-col>
<el-col :span="11">
<el-input-number v-model="alertConfig.airQuality.humidity.max" />
</el-col>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.settings-container {
.mb-20 {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
.text-center {
text-align: center;
line-height: 32px;
}
:deep(.el-card) {
border: none;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
}
</style>

View File

@ -0,0 +1,123 @@
<script setup lang="ts">
import { ref } from "vue";
interface UserData {
id: number;
username: string;
role: string;
email: string;
status: boolean;
createTime: string;
}
const tableData = ref<UserData[]>([
{
id: 1,
username: "admin",
role: "超级管理员",
email: "admin@example.com",
status: true,
createTime: "2024-03-20",
},
{
id: 2,
username: "manager",
role: "管理人员",
email: "manager@example.com",
status: true,
createTime: "2024-03-20",
},
]);
const handleEdit = (row: UserData) => {
console.log("编辑用户:", row);
};
const handleDelete = (row: UserData) => {
console.log("删除用户:", row);
};
</script>
<template>
<div class="user-container">
<el-card>
<template #header>
<div class="card-header">
<span>用户管理</span>
<el-button type="primary">新增用户</el-button>
</div>
</template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="role" label="角色" width="120" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status ? 'success' : 'danger'">
{{ row.status ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">
编辑
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<style lang="scss" scoped>
@import "../../../styles/variables.scss";
.user-container {
.el-card {
background: #ffffff;
border: none;
border-radius: 4px;
box-shadow: $box-shadow;
.el-card__header {
padding: 16px 20px;
border-bottom: 1px solid $border-color;
}
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
color: $text-primary;
font-weight: 500;
}
.el-button--small {
padding: 6px 16px;
}
}
.el-tag {
border-radius: 2px;
}
</style>

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

@ -0,0 +1 @@
/// <reference types="vite/client" />

14
tsconfig.app.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})