one
This commit is contained in:
commit
89b121c08c
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
239
README.md
Normal file
239
README.md
Normal 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
13
index.html
Normal 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
2266
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal 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
1
public/vite.svg
Normal 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
26
src/App.vue
Normal 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
49
src/api/report.ts
Normal 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
35
src/api/request.ts
Normal 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
1
src/assets/vue.svg
Normal 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 |
41
src/components/HelloWorld.vue
Normal file
41
src/components/HelloWorld.vue
Normal 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>
|
46
src/components/report/EnvironmentAnalysis.vue
Normal file
46
src/components/report/EnvironmentAnalysis.vue
Normal 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>
|
48
src/components/report/MonitorDataChart.vue
Normal file
48
src/components/report/MonitorDataChart.vue
Normal 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>
|
47
src/components/report/SpeciesAnalysis.vue
Normal file
47
src/components/report/SpeciesAnalysis.vue
Normal 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
226
src/layout/AdminLayout.vue
Normal 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>
|
17
src/layout/components/Sidebar/menuData.ts
Normal file
17
src/layout/components/Sidebar/menuData.ts
Normal 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
20
src/main.ts
Normal 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
107
src/router/index.ts
Normal 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
79
src/style.css
Normal 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
23
src/styles/variables.scss
Normal 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);
|
415
src/views/dashboard/index.vue
Normal file
415
src/views/dashboard/index.vue
Normal 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>
|
365
src/views/dashboard/screen/index.vue
Normal file
365
src/views/dashboard/screen/index.vue
Normal 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
96
src/views/login/index.vue
Normal 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>
|
222
src/views/monitor/environment/index.vue
Normal file
222
src/views/monitor/environment/index.vue
Normal 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>
|
210
src/views/monitor/species/index.vue
Normal file
210
src/views/monitor/species/index.vue
Normal 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>
|
421
src/views/patrol/points/index.vue
Normal file
421
src/views/patrol/points/index.vue
Normal 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>
|
364
src/views/patrol/records/index.vue
Normal file
364
src/views/patrol/records/index.vue
Normal 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>
|
745
src/views/patrol/tasks/index.vue
Normal file
745
src/views/patrol/tasks/index.vue
Normal 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>
|
469
src/views/report/analysis/index.vue
Normal file
469
src/views/report/analysis/index.vue
Normal 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>
|
241
src/views/report/daily/index.vue
Normal file
241
src/views/report/daily/index.vue
Normal 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>
|
345
src/views/reports/analysis/index.vue
Normal file
345
src/views/reports/analysis/index.vue
Normal 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>
|
344
src/views/reports/daily/index.vue
Normal file
344
src/views/reports/daily/index.vue
Normal 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>
|
284
src/views/system/data/index.vue
Normal file
284
src/views/system/data/index.vue
Normal 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>
|
664
src/views/system/devices/index.vue
Normal file
664
src/views/system/devices/index.vue
Normal 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>
|
301
src/views/system/permissions/index.vue
Normal file
301
src/views/system/permissions/index.vue
Normal 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>
|
120
src/views/system/roles/index.vue
Normal file
120
src/views/system/roles/index.vue
Normal 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>
|
180
src/views/system/settings/index.vue
Normal file
180
src/views/system/settings/index.vue
Normal 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>
|
123
src/views/system/users/index.vue
Normal file
123
src/views/system/users/index.vue
Normal 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
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
14
tsconfig.app.json
Normal file
14
tsconfig.app.json
Normal 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
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
24
tsconfig.node.json
Normal file
24
tsconfig.node.json
Normal 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
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user