Browse Source

feat: center ui

master
wally 4 years ago
parent
commit
a9271c8324
  1. 6
      index.html
  2. 9305
      package-lock.json
  3. 175
      src/App.vue
  4. 83
      src/apis/index.js
  5. BIN
      src/assets/arrow-bottom.png
  6. BIN
      src/assets/bg.png
  7. BIN
      src/assets/border-1.png
  8. BIN
      src/assets/border-2.png
  9. BIN
      src/assets/border-3.png
  10. BIN
      src/assets/border-5.png
  11. BIN
      src/assets/border-main.png
  12. BIN
      src/assets/border-s-1.png
  13. BIN
      src/assets/border-s-2.png
  14. BIN
      src/assets/btn-border.png
  15. BIN
      src/assets/circle-blue.png
  16. BIN
      src/assets/circle-green.png
  17. BIN
      src/assets/circle-red.png
  18. BIN
      src/assets/circle-yellow.png
  19. BIN
      src/assets/farm-top.png
  20. BIN
      src/assets/hot.png
  21. BIN
      src/assets/icon-location.png
  22. BIN
      src/assets/icon-phone.png
  23. BIN
      src/assets/icon-play.png
  24. BIN
      src/assets/modal-bg.png
  25. BIN
      src/assets/new.png
  26. BIN
      src/assets/top.png
  27. 56
      src/components/Center/Header.vue
  28. 71
      src/components/Center/News.vue
  29. 60
      src/components/Common/Buttons.vue
  30. 39
      src/components/Common/Footer.vue
  31. 46
      src/components/Common/Left.vue
  32. 34
      src/components/Common/Tall.vue
  33. 38
      src/components/Common/Time.vue
  34. 27
      src/components/Common/Title.vue
  35. 57
      src/components/Common/Weather.vue
  36. 31
      src/components/Tall/Roles.vue
  37. 173
      src/components/Tall/Tasks.vue
  38. 85
      src/components/commands/search-commands.vue
  39. 92
      src/components/config/config-log-function.vue
  40. 93
      src/components/config/config-log-network.vue
  41. 42
      src/components/config/device-select-and-status.vue
  42. 77
      src/components/config/function-config-applied.vue
  43. 287
      src/components/config/function-config-pending.vue
  44. 65
      src/components/config/network-config-applied.vue
  45. 150
      src/components/config/network-config-pending.vue
  46. 19
      src/components/config/refresh.vue
  47. 19
      src/components/config/status-and-last-time.vue
  48. 12
      src/components/config/status-tag-pending.vue
  49. 193
      src/components/device/device-create.vue
  50. 13
      src/components/device/device-edit.vue
  51. 233
      src/components/history/device.vue
  52. 42
      src/components/history/history-log-table.vue
  53. 35
      src/components/history/history-log.vue
  54. 162
      src/components/history/local.vue
  55. 48
      src/components/history/missing-data.vue
  56. 148
      src/components/history/search-bar-data.vue
  57. 77
      src/components/history/search-history-log.vue
  58. 23
      src/components/navbar.vue
  59. 49
      src/components/overview/chart-device-count.vue
  60. 93
      src/components/overview/chart-device-detail.vue
  61. 112
      src/components/overview/device-table.vue
  62. 66
      src/components/realtime/search-bar.vue
  63. 131
      src/components/statistical/search-bar.vue
  64. 82
      src/components/statistical/stastistical-chart.vue
  65. 164
      src/config/config.js
  66. 107
      src/config/layout/dataCenterBody.js
  67. 106
      src/config/layout/dataFarmBody.js
  68. 110
      src/config/log.js
  69. 2789
      src/config/map/shanxi.js
  70. 47
      src/config/map/values.js
  71. 2
      src/config/time.js
  72. 19
      src/hooks/useDeviceCreate.js
  73. 14
      src/hooks/useLayoutConfig.js
  74. 19
      src/hooks/useScrollList.js
  75. 3
      src/main.js
  76. 118
      src/mock/news.js
  77. 71
      src/routers/index.js
  78. 163
      src/store/device.js
  79. 15
      src/store/index.js
  80. 78
      src/store/statistics.js
  81. 53
      src/store/user.js
  82. 30
      src/utils/axios.js
  83. 91
      src/utils/complexChart.js
  84. 139
      src/utils/complexConfig.js
  85. 83
      src/utils/map.js
  86. 128
      src/utils/overview.js
  87. 16
      src/utils/time.js
  88. 13
      src/views/Center.vue
  89. 7
      src/views/Farm.vue
  90. 182
      src/views/commands.vue
  91. 32
      src/views/config.vue
  92. 185
      src/views/data-realtime.vue
  93. 192
      src/views/device-edit.vue
  94. 216
      src/views/device-list.vue
  95. 22
      src/views/history.vue
  96. 23
      src/views/overview.vue
  97. 80
      src/views/statistical-report.vue
  98. 7
      vite.config.js
  99. 2194
      yarn.lock

6
index.html

@ -4,10 +4,14 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>智能大气腐蚀监测云平台</title>
<title>山西农科院农业物联网数据中心</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/qweather-icons@1.1.0/font/qweather-icons.css" />
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css" />
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts-gl/dist/echarts-gl.min.js"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

9305
package-lock.json

File diff suppressed because it is too large

175
src/App.vue

@ -1,115 +1,90 @@
<script setup>
import { computed, ref } from 'vue';
import { useStore } from 'vuex';
import { useRoute, useRouter } from 'vue-router';
import Navbar from 'components/navbar.vue';
import zhCn from 'element-plus/lib/locale/lang/zh-cn';
import { ElMessage } from 'element-plus';
import { routes } from '@/routers/index.js';
const local = zhCn;
const store = useStore();
let timer = null;
const routeList = ref(routes);
const menu = computed(() => store.state.menu);
const activeMenuIndex = ref(0);
// queryu token
const route = useRoute();
const router = useRouter();
useRouter()
.isReady()
.then(async () => {
activeMenuIndex.value = routes.findIndex(item => item.name === route.name);
const u = computed(() => route.query.u);
if (!u.value) {
// urlu,
ElMessage.error('缺少用户信息参数');
} else {
// userId token
await store.dispatch('user/getTokenByUserId', u.value);
}
});
<template>
<div class="page">
<router-view></router-view>
</div>
</template>
const token = computed(() => store.getters['user/token']);
//
const getDeviceData = async () => {
if (token && token.value) {
await store.dispatch('device/getDevices');
timer && clearTimeout(timer);
timer = null;
} else {
timer = setTimeout(() => {
getDeviceData();
}, 16);
}
};
<script setup>
import { onMounted, provide } from 'vue';
getDeviceData();
provide('scene', 'center');
/**
* 打开页面
* @param {string} path 页面路径
*/
function openPage(path) {
const { query } = route;
router.push({
path,
query,
});
function setRem(html) {
html.style.fontSize = `${html.clientWidth / 1920}px`;
}
</script>
<template>
<el-config-provider :locale="local">
<el-container>
<el-header style="padding: 0">
<Navbar />
</el-header>
<el-container class="overflow-hidden">
<!-- <el-aside width="180px" v-if="menu.show"> -->
<el-aside v-if="menu.show" :width="!menu.collapse ? '200px' : '64px'">
<el-menu :collapse="menu.collapse" :default-active="activeMenuIndex" class="el-menu-vertical-demo">
<el-menu-item
v-for="(item, index) in routeList"
:key="item.name"
:index="index"
class="flex items-center"
@click="openPage(item.path)"
>
<i :class="item.meta.icon"></i>
<template #title>{{ item.meta.title }}</template>
</el-menu-item>
</el-menu>
</el-aside>
<el-main style="padding: 0">
<div class="p-4">
<router-view></router-view>
</div>
</el-main>
</el-container>
</el-container>
</el-config-provider>
</template>
onMounted(() => {
const html = document.documentElement;
setRem(html);
window.addEventListener(
'resize',
() => {
setRem(html);
},
false,
);
});
</script>
<style>
html,
body,
#app,
#app > section {
#app {
height: 100%;
}
.el-menu {
min-height: 100%;
.page {
width: 100%;
height: 100%;
background: url('@/assets/bg.png') no-repeat center center;
background-size: cover;
}
.el-form--label-top .el-form-item__label {
padding-bottom: 0 !important;
.clear:after {
content: '';
display: block;
height: 0;
visibility: hidden;
clear: both;
}
.el-form-item {
margin-bottom: 10px !important;
.float-left {
float: left;
}
.float-right {
float: right;
}
.absolute {
position: absolute;
}
.fixed {
position: fixed;
}
.relative {
position: relative;
}
.flex {
display: flex;
}
.flex-1 {
flex: 1;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.m-t {
margin-top: 10rem;
}
.m-b {
margin-bottom: 10rem;
}
.p {
padding: 10rem;
}
.p-16 {
padding: 16rem;
}
.overflow-hidden {
overflow: hidden;
}
</style>

83
src/apis/index.js

@ -2,82 +2,7 @@
import http from 'utils/axios';
const apiUrl = import.meta.env.VITE_API_URL;
const users = `${apiUrl}/gateway/tall3/v3.0/users`;
const corrosion = `${apiUrl}/gateway/corrosion`;
// 根据userId 获取token
export const getToken = userId => http.get(`${users}/userId`, { params: { userId } });
// 获取设备列表
export const getDevices = () => http.get(`${corrosion}/devices`);
// 查设备概览 数据统计
export const getDevicesCount = () => http.get(`${corrosion}/devices/count`);
// 添加设备
export const createDevice = data => http.post(`${corrosion}/devices`, data);
// 获取设备列表 完整信息
export const getDevicesAll = (
params = {
paging: true,
deviceId: '',
page: 1,
size: 50,
sort: [
{
col: 'createdAt',
order: 'DESC',
},
],
},
) => http.post(`${corrosion}/devices/all`, params);
// 更新设备
export const updateDevice = (deviceId, data) => http.put(`${corrosion}/devices/all/${deviceId}`, data);
// 删除设备
export const deleteDevice = deviceId => http.delete(`${corrosion}/devices/all/${deviceId}`);
// 获取月累计数据分析
export const getMonthsDate = params => http.get(`${corrosion}/statistics/months`, { params });
// 获取网络配置参数 列表
export const getConfigNetwork = params => http.get(`${corrosion}/config/network`, { params });
// 获取网络配置参数 设备实时参数
export const getConfigAppliedNetwork = params => http.get(`${corrosion}/config-applied/network`, { params });
// 获取功能配置参数 列表
export const getConfigFunction = params => http.get(`${corrosion}/config/function`, { params });
// 获取功能配置参数 设备实时参数
export const getConfigAppliedFunction = params => http.get(`${corrosion}/config-applied/function`, { params });
// 提交网络配置参数
export const createConfigNetwork = data => http.post(`${corrosion}/config/network`, data);
// 提交功能配置参数
export const createConfigFunction = data => http.post(`${corrosion}/config/function`, data);
// 查上报数据
export const getDatas = params => http.post(`${corrosion}/datas`, params);
// 导出上报数据
export const exportDatas = params => http.post(`${corrosion}/export`, params);
// 查历史数据
export const getHistory = params => http.post(`${corrosion}/history/datas`, params);
// 导出历史数据
export const exportHistory = params => http.post(`${corrosion}/export`, params);
// 发送补传指令
export const sendCommand = params => http.post(`${corrosion}/history`, params);
// 补传记录
export const getSendHistory = params => http.get(`${corrosion}/history`, { params });
// 查询平台日志
export const getLog = params => http.post(`${corrosion}/tpclog/query`, params);
// 获取天气信息
// eslint-disable-next-line import/prefer-default-export
export const getWeather = () =>
http.get('https://devapi.qweather.com/v7/weather/now?key=9764d65c999349a9b42c42a8f1052a81&location=112.48699,37.94036&lang =zh');

BIN
src/assets/arrow-bottom.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

BIN
src/assets/bg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
src/assets/border-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
src/assets/border-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
src/assets/border-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
src/assets/border-5.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
src/assets/border-main.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/border-s-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
src/assets/border-s-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
src/assets/btn-border.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/assets/circle-blue.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
src/assets/circle-green.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
src/assets/circle-red.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
src/assets/circle-yellow.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
src/assets/farm-top.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
src/assets/hot.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

BIN
src/assets/icon-location.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

BIN
src/assets/icon-phone.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
src/assets/icon-play.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

BIN
src/assets/modal-bg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
src/assets/new.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

BIN
src/assets/top.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

56
src/components/Center/Header.vue

@ -0,0 +1,56 @@
<template>
<header :class="{ 'bg-center': scene === 'center', 'bg-farm': scene === 'farm' }">
<!-- 时间-->
<PageTime></PageTime>
<!-- 按钮组-->
<PageButtons @click-btn="onClickBtn"></PageButtons>
<CommonWeather></CommonWeather>
</header>
</template>
<script setup>
import PageTime from 'components/Common/Time.vue';
import PageButtons from 'components/Common/Buttons.vue';
import CommonWeather from 'components/Common/Weather.vue';
import { computed, inject } from 'vue';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
const store = useStore();
const scene = inject('scene');
const modalDisplay = computed(() => store.state.modal); // modal
const router = useRouter();
function onClickBtn({ index: btnIndex }) {
if (btnIndex === 0) {
store.commit('setModal', !modalDisplay.value);
} else if (btnIndex === 2) {
// monitorDisplay.value = !monitorDisplay.value;
if (scene === 'farm') {
// /
router.push('/farm');
} else {
// /farm
router.push('/farm/farm');
}
}
}
</script>
<style scoped>
.bg-center {
position: relative;
height: 115rem;
background: url('@/assets/top.png') no-repeat center;
background-size: 100% 114rem;
}
.bg-farm {
position: relative;
height: 115rem;
background: url('@/assets/farm-top.png') no-repeat center;
background-size: 100% 114rem;
}
</style>

71
src/components/Center/News.vue

@ -0,0 +1,71 @@
<template>
<div style="height: 100%; overflow: hidden">
<ul class="list" :style="{ transform: `translate3d(0, ${translateY}px, 0)` }">
<li class="list-item" v-for="item in news" :key="item.title">
<div class="title">{{ item.title }}</div>
<div class="date">{{ item.date }}</div>
</li>
</ul>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue';
import newsData from '@/mock/news';
import useScrollList from '@/hooks/useScrollList';
const news = ref([]);
const { translateY, scrollList } = useScrollList();
//
async function getData() {
try {
news.value = newsData;
await nextTick();
const li = document.querySelector('.list-item');
const ul = document.querySelector('.list');
const liHeight = li.offsetHeight;
const ulHeight = liHeight * news.value.length - ul.parentElement.offsetHeight;
scrollList(liHeight, ulHeight); //
} catch (error) {
console.error(error);
}
}
getData();
</script>
<style scoped>
.list {
padding-left: 0;
list-style: disc inside;
transition: transform 300ms ease;
}
.list-item {
display: flex;
align-items: center;
padding: 10rem 0 10rem;
}
.list-item:before {
content: '';
width: 10rem;
height: 10rem;
background-color: #01fffd;
border-radius: 50%;
}
.list-item .title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 16rem;
color: #fff;
padding-left: 10rem;
padding-right: 40rem;
}
.list-item .date {
color: #ccc;
font-size: 14rem;
}
</style>

60
src/components/Common/Buttons.vue

@ -0,0 +1,60 @@
<template>
<div class="container clear">
<div class="item" v-for="(item, index) in buttons" :key="item" @click="$emit('click-btn', { item, index })">
{{ item }}
</div>
</div>
</template>
<script setup>
import { defineEmits, computed, inject } from 'vue';
const scene = inject('scene');
defineEmits(['click-btn']);
const buttons = computed(() => {
if (scene === 'center') {
return ['农场连线', '时物链', '数据监控', '导出统计'];
}
return ['专家连线', '时物链', '系统总览', '导出统计'];
});
</script>
<style scoped>
.container {
height: 45rem;
position: absolute;
bottom: 14rem;
width: 100%;
}
.item {
width: 144rem;
height: 45rem;
display: flex;
align-items: center;
justify-content: center;
background: url('../../assets/btn-border.png') no-repeat center;
background-size: 100%;
color: #01f6ff;
font-size: 15rem;
cursor: pointer;
}
.item:nth-of-type(1) {
float: left;
margin-left: 20rem;
}
.item:nth-of-type(2) {
float: left;
margin-left: 24rem;
}
.item:nth-of-type(3) {
float: right;
margin-right: 24rem;
margin-left: 24rem;
}
.item:nth-of-type(4) {
float: right;
}
</style>

39
src/components/Common/Footer.vue

@ -0,0 +1,39 @@
<template>
<footer class="page-footer clear">
<div class="center">山西农业大学农业科学研究院</div>
<div class="right">
山西传控电子科技有限公司技术支持
<span style="margin-left: 16rem">400 668 1386</span>
<span style="margin-left: 16rem">181 3510 0170</span>
</div>
</footer>
</template>
<script>
export default { name: 'Footer' };
</script>
<style scoped>
.page-footer {
position: relative;
width: 100%;
height: 34rem;
display: flex;
align-items: center;
justify-content: center;
}
.center {
font-size: 16rem;
color: #fff;
}
.right {
position: absolute;
right: 10rem;
height: 34rem;
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 14rem;
color: rgba(255, 255, 255, 0.3);
}
</style>

46
src/components/Common/Left.vue

@ -0,0 +1,46 @@
<!--suppress ES6UnusedImports -->
<template>
<div :style="{ width: body.left.width }">
<div
class="item m-t"
:style="{
width: item.width,
height: item.height,
'background-image': `url(${item.background.image})`,
}"
v-for="item in body.left.data"
:key="item.title.text"
>
<!-- 区块标题-->
<DataCenterTitle :item="item"></DataCenterTitle>
<div class="p-16 flex-1 overflow-hidden">
<component :is="getComponent(item.component)"></component>
<!-- <component :is="DataCenterNews"></component>-->
</div>
</div>
</div>
</template>
<script setup>
/* eslint-disable no-unused-vars */
import DataCenterTitle from 'components/Common/Title.vue';
import useLayoutConfig from '@/hooks/useLayoutConfig';
import DataCenterNews from '@/components/Center/News.vue';
const body = useLayoutConfig();
const getComponent = name => {
return name;
};
</script>
<style scoped>
.item {
background-position: left top;
background-size: 100% 100%;
background-repeat: no-repeat;
display: flex;
flex-direction: column;
}
</style>

34
src/components/Common/Tall.vue

@ -0,0 +1,34 @@
<template>
<div class="tall-container">
<!-- 角色-->
<TallRoles :roles="roles" :role-id="roleId" @change="onChangeRole"></TallRoles>
<!-- 任务-->
<TallTasks :role-id="roleId"></TallTasks>
</div>
</template>
<script setup>
import { ref } from 'vue';
import TallRoles from 'components/Tall/Roles.vue';
import TallTasks from 'components/Tall/Tasks.vue';
const roles = ref([
{ id: '111', name: '专家' },
{ id: '112', name: '农场' },
]);
const roleId = ref('111');
function onChangeRole(clickRoleId) {
console.log('roleId: ', clickRoleId);
roleId.value = clickRoleId;
}
</script>
<style scoped>
.tall-container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
</style>

38
src/components/Common/Time.vue

@ -0,0 +1,38 @@
<template>
<div class="time-wrap">
<div class="date">{{ date }}</div>
<div class="time">{{ time }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import dayjs from '@/utils/time';
import { weeks } from '@/config/time';
const date = ref('');
const time = ref('');
setInterval(() => {
const week = weeks[dayjs().day()];
date.value = dayjs().format('YYYY年MM月DD日 星期') + week;
time.value = dayjs().format('HH:mm:ss');
}, 1000);
</script>
<style scoped>
.time-wrap {
height: 38rem;
padding-left: 15rem;
color: #fff;
display: flex;
align-items: center;
}
.date {
font-size: 14rem;
margin-right: 14rem;
}
.time {
font-size: 25rem;
}
</style>

27
src/components/Common/Title.vue

@ -0,0 +1,27 @@
<template>
<div
class="title"
:style="{
height: item.title.height,
'line-height': item.title.height,
'text-align': item.title.align,
'font-size': item.title.size,
color: item.title.color,
}"
v-if="!item.title.hide"
>
{{ item.title.text }}
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({ item: Object });
</script>
<style scoped>
.title {
font-weight: bold;
}
</style>

57
src/components/Common/Weather.vue

@ -0,0 +1,57 @@
<template>
<div class="weather-container" v-if="weather">
今日天气
<i :class="[`qi-${weather.icon}`, 'icon']"></i>
<span class="text">{{ weather.text }}</span>
<span class="temp">{{ weather.temp }}</span>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { getWeather } from 'apis';
const weather = ref(null);
async function getData() {
try {
const { code, now } = await getWeather();
console.log('code, now: ', code, now);
if (+code !== 200) {
throw new Error(code);
} else {
weather.value = now;
}
} catch (error) {
console.error(error);
}
}
getData();
</script>
<style scoped>
.weather-container {
position: absolute;
right: 20rem;
top: 10rem;
color: #fff;
display: flex;
align-items: center;
}
.temp {
font-size: 25rem;
color: #fff;
font-family: impact;
margin-left: 14rem;
}
.text {
margin-left: 14rem;
}
.icon {
margin-left: 10rem;
color: #fff100;
font-size: 26rem;
font-weight: bolder;
}
</style>

31
src/components/Tall/Roles.vue

@ -0,0 +1,31 @@
<template>
<div class="role-container clear">
<div v-for="item in roles" :key="item" class="role-item" :class="{ active: roleId === item.id }" @click="$emit('change', item.id)">
{{ item.name }}
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
defineProps({ roles: Object, roleId: String });
defineEmits(['change']);
</script>
<style scoped>
.role-item {
float: left;
padding: 8rem 2rem;
margin-left: 4rem;
margin-right: 4rem;
font-size: 16rem;
color: #fff;
cursor: pointer;
}
.role-item.active {
border-bottom: 2rem solid #00d9ff;
color: #00d9ff;
font-weight: 600;
}
</style>

173
src/components/Tall/Tasks.vue

@ -0,0 +1,173 @@
<template>
<div class="task-container">
<div v-for="(roleTasks, keyRoleId) in tasks" :key="keyRoleId">
<div class="task-item" v-for="item in roleTasks" :key="item.id" v-show="keyRoleId === roleId">
<div class="task-item-header">
<div class="task-item-icon"></div>
<div class="task-item-time flex-1">{{ item.beginTime }}</div>
</div>
<div class="task-item-content">
<div class="line"></div>
<div class="content">
{{ item.name }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import dayjs from 'dayjs';
import { defineProps } from 'vue';
defineProps({ roleId: String });
/**
* 计算转换开始时间
* @param {number} delta 当前时间的差值
*/
function computeBeginTime(delta) {
return dayjs().add(delta, 'hour').format('YYYY-MM-DD HH:00');
}
const tasks = {
111: [
{
id: 111,
name: '周会',
beginTime: computeBeginTime(0),
duration: 5,
role: { id: '111', name: '专家' },
},
{
id: 112,
name: '专家会诊',
beginTime: computeBeginTime(1),
duration: 60,
role: { id: '111', name: '专家' },
},
{
id: 113,
name: '生态园远程会诊',
beginTime: computeBeginTime(3),
duration: 90,
role: { id: '111', name: '专家' },
},
{
id: 114,
name: '生态园实地考察',
beginTime: computeBeginTime(6),
duration: 120,
role: { id: '111', name: '专家' },
},
{
id: 115,
name: '智慧农业专项会议',
beginTime: computeBeginTime(10),
duration: 100,
role: { id: '111', name: '专家' },
},
{
id: 116,
name: '数据报表导出统计分析',
beginTime: computeBeginTime(10),
duration: 100,
role: { id: '111', name: '专家' },
},
{
id: 117,
name: '生态园巡查',
beginTime: computeBeginTime(10),
duration: 100,
role: { id: '111', name: '专家' },
},
{
id: 117,
name: '智慧农业论坛',
beginTime: computeBeginTime(10),
duration: 100,
role: { id: '111', name: '专家' },
},
],
112: [
{
id: 211,
name: '周会',
beginTime: computeBeginTime(0),
duration: 5,
role: { id: '112', name: '农场' },
},
{
id: 212,
name: '浇水',
beginTime: computeBeginTime(1),
duration: 60,
role: { id: '112', name: '农场' },
},
{
id: 213,
name: '专家远程会诊',
beginTime: computeBeginTime(3),
duration: 90,
role: { id: '112', name: '农场' },
},
{
id: 214,
name: '专家考察',
beginTime: computeBeginTime(6),
duration: 120,
role: { id: '112', name: '农场' },
},
{
id: 215,
name: '施肥',
beginTime: computeBeginTime(10),
duration: 100,
role: { id: '112', name: '农场' },
},
],
};
</script>
<style scoped>
.task-container {
padding: 10rem;
overflow-y: auto;
height: 100%;
}
::-webkit-scrollbar {
display: none;
}
.task-item {
color: #fff;
}
.task-item-header {
display: flex;
align-items: center;
}
.task-item-icon {
width: 24rem;
height: 24rem;
background: url('../../assets/icon-play.png') no-repeat center center;
background-size: 100%;
margin-right: 10rem;
margin-top: 10rem;
margin-bottom: 10rem;
}
.task-item-content {
display: flex;
}
.line {
width: 2rem;
margin-left: 11rem;
margin-right: 21rem;
background-color: #00d9ff;
}
.task-item-content .content {
flex: 1;
box-shadow: 0 0 9rem #00d9ff inset;
padding: 16rem;
font-size: 18rem;
}
</style>

85
src/components/commands/search-commands.vue

@ -1,85 +0,0 @@
<template>
<el-form ref="searchDeviceForm" :inline="true" :model="searchDevice">
<el-form-item label="选择站点">
<el-select v-model="searchDevice.deviceId" placeholder="请选择站点" @change="change">
<el-option label="全部" value></el-option>
<el-option
v-for="item in devices"
:key="item.deviceId"
:label="`${item.address}-${item.deviceId}`"
:value="item.deviceId"
></el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="类型">-->
<!-- <el-select v-model="searchDevice.type" placeholder="选择查询类型">-->
<!-- <el-option label="全部" value=""></el-option>-->
<!-- <el-option label="事件上报" value="EVENT"></el-option>-->
<!-- <el-option label="业务上报" value="DATA"></el-option>-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="状态">-->
<!-- <el-select v-model="searchDevice.status" placeholder="选择查询类型">-->
<!-- <el-option label="全部" value=""></el-option>-->
<!-- <el-option label="PENDING" value="PENDING"></el-option>-->
<!-- <el-option label="SUCCESS" value="SUCCESS"></el-option>-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<el-form-item>
<el-button type="primary" @click="onSubmit">
<i class="el-icon-search mr-2"></i>
查询
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { computed, defineEmits, reactive, ref, watch } from 'vue';
import { useStore } from 'vuex';
const emit = defineEmits(['search']);
const searchDevice = reactive({
deviceId: '',
paging: true,
page: 1,
size: 50,
sort: [
{
col: 'createdAt',
order: 'DESC',
},
],
type: '',
status: '',
});
const searchDeviceForm = ref(null); // form
const store = useStore();
const devices = computed(() => store.state.device.devices);
const currentDeviceId = computed(() => store.state.device.currentDeviceId); // id
// currentDeviceId
watch(
() => currentDeviceId.value,
newValue => {
if (newValue) {
searchDevice.deviceId !== newValue && (searchDevice.deviceId = newValue);
}
},
{ immediate: true },
);
const change = e => {
store.commit('device/setCurrentDeviceId', e);
};
//
const onSubmit = () => {
searchDeviceForm.value.validate(() => {
emit('search', { ...searchDevice });
});
};
</script>

92
src/components/config/config-log-function.vue

@ -1,92 +0,0 @@
<template>
<template v-if="data">
<el-table :data="data" :max-height="contentHeight" border stripe style="width: 100%">
<el-table-column type="expand">
<template #default="props">
<div class="mx-4">
<FunctionConfigApplied v-if="props.row" :data="props.row" />
</div>
</template>
</el-table-column>
<el-table-column align="center" label="设备ID" min-min-width="100" prop="deviceId" />
<el-table-column align="center" label="上次配置时间" min-width="200">
<template #default="scope">
{{ dayjs(new Date(+scope.row.settingTime)).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column align="center" label="状态" min-width="100">
<template #default="scope">
<StatusTagPending :type="scope.row.settingStatus" />
</template>
</el-table-column>
</el-table>
<!-- <el-pagination-->
<!-- :current-page="devicesAll.page.page"-->
<!-- :default-page-size="50"-->
<!-- :page-count="devicesAll.page.count"-->
<!-- :page-size="devicesAll.page.size"-->
<!-- :page-sizes="[1, 10, 20, 50, 100]"-->
<!-- :total="devicesAll.page.total"-->
<!-- background-->
<!-- class="my-3 float-right"-->
<!-- layout="total, sizes, prev, pager, next, jumper"-->
<!-- @size-change="onSizeChange"-->
<!-- @current-change="onCurrentPageChange"-->
<!-- ></el-pagination>-->
</template>
</template>
<script setup>
import { computed, onMounted, ref, watchEffect } from 'vue';
import { useStore } from 'vuex';
import { getConfigFunction } from 'apis';
import dayjs from 'dayjs';
import { ElMessage } from 'element-plus';
import StatusTagPending from 'components/config/status-tag-pending.vue';
import FunctionConfigApplied from 'components/config/function-config-applied.vue';
const store = useStore();
const currentDeviceId = computed(() => store.state.device.currentDeviceId);
const contentHeight = ref(600);
const data = ref(null);
//
onMounted(() => {
const winHeight = document.documentElement.clientHeight;
contentHeight.value = winHeight - 250;
});
watchEffect(() => {
const deviceId = currentDeviceId.value;
onSearch(deviceId);
});
/**
* 删除
* @param {number} page 页码
* @param {number} size 每页条数
*/
async function onSearch(deviceId = currentDeviceId.value) {
const params = { deviceId };
try {
const resData = await getConfigFunction(params);
data.value = resData;
} catch (error) {
ElMessage.error(error.message);
console.error(error);
}
}
currentDeviceId.value && onSearch(currentDeviceId.value);
//
// const onCurrentPageChange = page => {
// onSearch(page, devicesAll.value.page.size || 50);
// };
//
// const onSizeChange = size => {
// onSearch(1, size);
// };
</script>

93
src/components/config/config-log-network.vue

@ -1,93 +0,0 @@
<template>
<template v-if="data">
<el-table :data="data" :max-height="contentHeight" border stripe style="width: 100%">
<el-table-column type="expand">
<template #default="props">
<div class="mx-4">
<NetworkConfigApplied v-if="props.row" :data="props.row" />
</div>
</template>
</el-table-column>
<el-table-column align="center" label="设备ID" min-min-width="100" prop="deviceId" />
<el-table-column align="center" label="上次配置时间" min-width="200">
<template #default="scope">
{{ dayjs(new Date(+scope.row.settingTime)).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column align="center" label="状态" min-width="100">
<template #default="scope">
<StatusTagPending :type="scope.row.settingStatus" />
</template>
</el-table-column>
</el-table>
<!-- <el-pagination-->
<!-- :current-page="devicesAll.page.page"-->
<!-- :default-page-size="50"-->
<!-- :page-count="devicesAll.page.count"-->
<!-- :page-size="devicesAll.page.size"-->
<!-- :page-sizes="[1, 10, 20, 50, 100]"-->
<!-- :total="devicesAll.page.total"-->
<!-- background-->
<!-- class="my-3 float-right"-->
<!-- layout="total, sizes, prev, pager, next, jumper"-->
<!-- @size-change="onSizeChange"-->
<!-- @current-change="onCurrentPageChange"-->
<!-- ></el-pagination>-->
</template>
</template>
<script setup>
import { computed, onMounted, ref, watchEffect } from 'vue';
import { useStore } from 'vuex';
import dayjs from 'dayjs';
import { ElMessage } from 'element-plus';
import StatusTagPending from 'components/config/status-tag-pending.vue';
import NetworkConfigApplied from 'components/config/network-config-applied.vue';
import { getConfigNetwork } from 'apis';
const store = useStore();
const currentDeviceId = computed(() => store.state.device.currentDeviceId);
const contentHeight = ref(600);
const data = ref(null);
//
onMounted(() => {
const winHeight = document.documentElement.clientHeight;
contentHeight.value = winHeight - 250;
});
watchEffect(() => {
const deviceId = currentDeviceId.value;
onSearch(deviceId);
});
/**
* 删除
* @param {number} page 页码
* @param {number} size 每页条数
*/
async function onSearch(deviceId = currentDeviceId.value) {
const params = { deviceId };
try {
const resData = await getConfigNetwork(params);
data.value = resData;
} catch (error) {
ElMessage.error(error.message);
console.error(error);
}
}
currentDeviceId.value && onSearch(currentDeviceId.value);
//
// const onCurrentPageChange = page => {
// onSearch(page, devicesAll.value.page.size || 50);
// };
//
// const onSizeChange = size => {
// onSearch(1, size);
// };
</script>

42
src/components/config/device-select-and-status.vue

@ -1,42 +0,0 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const devices = computed(() => store.state.device.devices); // /
const deviceId = ref(''); // id
const currentDeviceId = computed(() => store.state.device.currentDeviceId); // id
// currentDeviceId
watch(
() => currentDeviceId.value,
newValue => {
if (newValue && deviceId.value !== newValue) {
deviceId.value = newValue;
}
},
{ immediate: true },
);
/**
* 设备id修改
* 更新store里的deviceId
*/
function onChange(event) {
store.commit('device/setCurrentDeviceId', event);
}
</script>
<template>
<div class="mb-3">
<span class="text-sm text-gray-500">选择站点</span>
<el-select v-model="deviceId" placeholder="选择站点" @change="onChange">
<el-option
v-for="item in devices"
:key="item.deviceId"
:label="`${item.address}-${item.deviceId}`"
:value="item.deviceId"
></el-option>
</el-select>
</div>
</template>

77
src/components/config/function-config-applied.vue

@ -1,77 +0,0 @@
<template>
<el-row :gutter="10">
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>金属腐蚀采样频率(分钟)</span>
<span>{{ data.frequency.metal }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>温湿度采样频率(分钟)</span>
<span>{{ data.frequency.th }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>SO2采样频率(分钟)</span>
<span>{{ data.frequency.so2 }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>盐分采样频率(分钟)</span>
<span>{{ data.frequency.salt }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>电池电压低阈值(V)</span>
<span>{{ data.batteryLow }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>电池电压高阈值(V)</span>
<span>{{ data.batteryHigh }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>太阳能板电压高阈值(V)</span>
<span>{{ data.sunHigh }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>湿度高阈值(RH%)</span>
<span>{{ data.humidityHigh }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>温度低阈值()</span>
<span>{{ data.temperatureLow }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>温度高阈值()</span>
<span>{{ data.temperatureHigh }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>金属腐蚀采集个数</span>
<span>{{ data.count }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>安全模式</span>
<span>{{ SER[data.securityMode] }}</span>
</el-col>
</el-row>
<el-row>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>上报类型</span>
<span class="mr-8">{{ REPORT_TYPE[data.report.type] }}</span>
<span v-if="data.report.type === 'POINT'">
<span v-for="item in data.report.timePoints" :key="item" class="mr-3">{{ item }}</span>
</span>
<span v-else>{{ data.report.cycle }}分钟</span>
</el-col>
</el-row>
<!-- 刷新下发按钮 -->
<!-- <Refresh @refresh="onSearch(currentDeviceI, 1)" /> -->
</template>
<script setup>
import { defineProps } from 'vue';
import { SER } from '@/config/log';
import { REPORT_TYPE } from '@/config/config';
defineProps({ data: Object });
</script>

287
src/components/config/function-config-pending.vue

@ -1,287 +0,0 @@
<template>
<el-form ref="functionForm" :model="data" label-position="top">
<StatusAndLastTime v-if="data.settingStatus" :setting-status="data.settingStatus" :setting-time="+data.settingTime" />
<el-row :gutter="20">
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="金属腐蚀采样频率(分钟)" prop="frequency.metal">
<el-input-number v-model="data.frequency.metal" :min="0" />
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="温湿度采样频率(分钟)" prop="frequency.th">
<el-input-number v-model="data.frequency.th" :min="0" />
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="SO2采样频率(分钟)" prop="frequency.so2">
<el-input-number v-model="data.frequency.so2" :min="0" />
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="盐分采样频率(分钟)" prop="frequency.salt">
<el-input-number v-model="data.frequency.salt" :min="0" />
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="电池电压低阈值(V)" prop="batteryLow">
<el-input-number v-model="data.batteryLow" :min="0" />
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="电池电压高阈值(V)" prop="batteryHigh">
<el-input-number v-model="data.batteryHigh" :min="0" />
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="太阳能板电压高阈值(V)" prop="sunHigh">
<el-input-number v-model="data.sunHigh" :min="0" />
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="湿度高阈值(RH%)" prop="humidityHigh">
<el-input-number v-model="data.humidityHigh" :min="0" />
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="温度低阈值(℃)" prop="temperatureLow">
<el-input-number v-model="data.temperatureLow" :min="-30" />
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="温度高阈值(℃)" prop="temperatureHigh">
<el-input-number v-model="data.temperatureHigh" :min="0" />
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="金属腐蚀采集个数" prop="count">
<el-input v-model="data.count" :min="0" disabled style="width: 180px" />
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="安全模式" prop="securityMode">
<!-- <el-input v-model="data.securityMode"></el-input> -->
<el-radio v-model="data.securityMode" label="OPEN">不加密</el-radio>
<el-radio v-model="data.securityMode" label="ENCRYPTION">加密</el-radio>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="上报时间" prop="report.type" style="margin-bottom: 0 !important">
<el-radio-group v-model="data.report.type">
<el-radio label="POINT">定时上报</el-radio>
<el-radio label="CYCLE">周期上报(分钟)</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<!-- 定时上报输入-->
<el-row v-if="data.report.type === 'POINT'" :gutter="20">
<el-col v-for="(item, index) in data.report.timePoints" :key="index" :lg="6" :md="12" :sm="24" :xl="4">
<el-form-item prop="report.timePoints" style="margin-bottom: 10px !important">
<el-time-picker v-model="data.report.timePoints[index]" :placeholder="`上报时间点${index + 1}`" format="HH:mm"></el-time-picker>
<el-button circle type="text" @click="removeTimePoint(index)">
<i class="el-icon-circle-close text-lg text-red-600"></i>
</el-button>
</el-form-item>
</el-col>
<el-col v-if="!data.report.timePoints || data.report.timePoints.length < 6" :lg="6" :md="8" :sm="12" :xl="4">
<el-button plain @click="addTimePoints">
<i class="el-icon-plus px-6"></i>
</el-button>
</el-col>
</el-row>
<!--周期上报输入-->
<el-row v-else :gutter="20">
<el-col :span="12">
<el-form-item prop="report.cycle">
<el-input-number v-model="data.report.cycle" :max="1440" :min="10" step="10" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" class="mt-5 pl-2">
<el-form-item>
<el-button type="primary" @click="onSubmit">提交</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-row>
</el-form>
<!-- 刷新下发按钮 -->
<Refresh @refresh="onSearch(currentDeviceId, 1)" />
</template>
<script setup>
import { computed, defineEmits, defineProps, ref, watch } from 'vue';
import { useStore } from 'vuex';
import { ElMessage } from 'element-plus';
import cloneDeep from 'lodash/cloneDeep';
import isDate from 'lodash/isDate';
import { createConfigFunction, getConfigAppliedFunction } from 'apis';
import Refresh from 'components/config/refresh.vue';
import StatusAndLastTime from 'components/config/status-and-last-time.vue';
import { functionConfig } from '@/config/config';
const data = ref(functionConfig);
const functionForm = ref(null); // form
const store = useStore();
const currentDeviceId = computed(() => store.state.device.currentDeviceId);
const emit = defineEmits(['status']);
const props = defineProps({ activeName: String });
/**
* 格式化时间
*/
const formatTime = date => {
if (!isDate(date)) return '';
const hour = date.getHours();
const minute = date.getMinutes();
return `${hour}:${minute}`;
};
/**
* 查询网络参数配置
* @param {string} deviceId 设备id
* @param {number} type 0数据库查询 1刷新查询
*/
async function onSearch(deviceId, type = 0) {
const params = {
deviceId,
type,
};
const resData = await getConfigAppliedFunction(params);
if (resData && resData.time) {
// ms
resData.time = new Date(+resData.time);
}
if (resData && resData.status) {
//
emit('status', resData.status);
} else {
emit('status', '');
}
if (resData && resData.report.type === 'POINT') {
const { timePoints } = resData.report;
//
if (timePoints && timePoints.length) {
for (let i = 0; i < timePoints.length; i++) {
const date = new Date().toLocaleDateString();
const item = resData.report.timePoints[i];
const reg = /^\d{1,2}:\d{1,2}$/;
if (item && reg.test(item)) {
resData.report.timePoints[i] = new Date(`${date} ${item}`);
} else {
resData.report.timePoints[i] = '';
}
}
} else {
resData.report = [''];
}
}
data.value = resData || functionConfig;
}
watch(
currentDeviceId,
newDeviceId => {
newDeviceId && props.activeName === 'function' && onSearch(newDeviceId);
},
{ immediate: true },
);
//
const onSubmit = () => {
functionForm.value.validate(async () => {
try {
if (!currentDeviceId.value) {
return ElMessage.error('请选择站点');
}
const param = cloneDeep({
...data.value,
deviceId: currentDeviceId.value,
});
param.time = new Date(param.time).getTime();
if (param.report.type === 'POINT') {
//
const points = [...param.report.timePoints];
for (let i = 0; i < points.length; i++) {
if (points[i]) {
points[i] = formatTime(points[i]);
}
}
const validated = validateTimePoints(points);
if (!validated) {
ElMessage.error('上报时间点不能重复');
return;
}
param.report.timePoints = points;
}
await createConfigFunction(param);
ElMessage.success('提交成功');
} catch (error) {
throw new Error(error);
}
});
};
//
const onReset = () => {
functionForm.value.resetFields();
};
// currentDeviceId && onSearch(currentDeviceId.value);
//
function addTimePoints() {
const { timePoints } = data.value.report;
console.log(data.value.report);
if (!timePoints || !timePoints.length) {
data.value.report.timePoints = [''];
} else {
if (timePoints.length >= 6) return;
data.value.report.timePoints.push('');
}
}
/**
* 验证上报时间点是否重复
* @param {array|null} points ["时:分"]
* @returns {boolean}
*/
function validateTimePoints(points) {
if (!points || !points.length) return true;
const obj = {};
points.forEach(item => {
if (obj[item]) {
obj[item] += 1;
} else {
obj[item] = 1;
}
});
// eslint-disable-next-line no-restricted-syntax
for (const key in obj) {
if (obj[key] > 1) return false;
}
return true;
}
/**
* 移除上报时间点
* @param {number} index 索引
*/
function removeTimePoint(index) {
data.value.report.timePoints.splice(index, 1);
}
</script>

65
src/components/config/network-config-applied.vue

@ -1,65 +0,0 @@
<template>
<el-row :gutter="10">
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>上报IP地址1</span>
<span>{{ data.ip1 }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>上报端口号1</span>
<span>{{ data.port1 }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>上报IP地址2</span>
<span>{{ data.ip2 }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>上报端口号2</span>
<span>{{ data.port2 }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>上报IP地址3</span>
<span>{{ data.ip3 }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>上报端口号3</span>
<span>{{ data.port3 }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>上报IP地址4</span>
<span>{{ data.ip4 }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>上报端口号4</span>
<span>{{ data.port4 }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>备用ip地址</span>
<span>{{ data.ipBackup }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>备用端口号</span>
<span>{{ data.portBackup }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>用户名</span>
<span>{{ data.account }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>密码</span>
<span>{{ data.password }}</span>
</el-col>
<el-col :lg="8" :md="12" :xl="6" :xs="24" class="text-sm text-gray-500 py-3">
<span>APN</span>
<span>{{ data.apn }}</span>
</el-col>
</el-row>
<!-- 刷新下发按钮 -->
<!-- <Refresh @refresh="onSearch(currentDeviceI, 1)" /> -->
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({ data: Object });
</script>

150
src/components/config/network-config-pending.vue

@ -1,150 +0,0 @@
<template>
<el-form ref="networkForm" :model="data" label-position="top">
<StatusAndLastTime v-if="data.settingStatus" :setting-status="data.settingStatus" :setting-time="+data.settingTime" />
<el-row :gutter="20">
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="上报IP1地址" prop="ip1">
<el-input v-model="data.ip1"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="上报端口号1" prop="port1">
<el-input v-model="data.port1"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="上报IP2地址" prop="ip2">
<el-input v-model="data.ip2"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="上报端口号2" prop="port2">
<el-input v-model="data.port2"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="上报IP3地址" prop="ip3">
<el-input v-model="data.ip3"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="上报端口号3" prop="port3">
<el-input v-model="data.port3"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="备用ip地址" prop="ipBackup">
<el-input v-model="data.ipBackup"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="备用端口号" prop="portBackup">
<el-input v-model="data.portBackup"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="用户名" prop="account">
<el-input v-model="data.account"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="密码" prop="password">
<el-input v-model="data.password"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="APN" prop="apn">
<el-input v-model="data.apn"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" class="mt-5 pl-2">
<el-form-item>
<el-button type="primary" @click="onSubmit">提交</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-row>
</el-form>
<!-- 刷新下发按钮 -->
<Refresh @refresh="onSearch(currentDeviceId, 1)" />
</template>
<script setup>
import { computed, defineEmits, defineProps, ref, watch } from 'vue';
import { useStore } from 'vuex';
import { ElMessage } from 'element-plus';
import { createConfigNetwork, getConfigAppliedNetwork } from 'apis';
import Refresh from 'components/config/refresh.vue';
import StatusAndLastTime from 'components/config/status-and-last-time.vue';
import { networkConfig } from '@/config/config';
const data = ref(networkConfig);
const networkForm = ref(null); // form
const store = useStore();
const currentDeviceId = computed(() => store.state.device.currentDeviceId);
const emit = defineEmits(['status']);
const props = defineProps({ activeName: String });
/**
* 查询网络参数配置
* @param {string} deviceId 设备id
* @param {number} type 0数据库查询 1刷新查询
*/
async function onSearch(deviceId, type = 0) {
try {
const params = {
deviceId,
type,
};
const resData = await getConfigAppliedNetwork(params);
data.value = resData || networkConfig;
if (resData && resData.status) {
emit('status', resData.status);
} else {
emit('status', '');
}
} catch (error) {
throw new Error(error);
}
}
watch(
currentDeviceId,
newDeviceId => {
newDeviceId && props.activeName === 'network' && onSearch(newDeviceId);
},
{ immediate: true },
);
//
const onSubmit = () => {
networkForm.value.validate(async () => {
const param = {
...data.value,
deviceId: currentDeviceId.value,
};
try {
await createConfigNetwork(param);
ElMessage.success('提交成功');
} catch (error) {
console.error('error: ', error);
ElMessage.error('提交失败, 请稍后重试');
}
});
};
//
const onReset = () => {
networkForm.value.resetFields();
};
// currentDeviceId && onSearch(currentDeviceId.value);
</script>

19
src/components/config/refresh.vue

@ -1,19 +0,0 @@
<script setup>
import { defineEmits } from 'vue';
defineEmits(['refresh']);
</script>
<template>
<el-tooltip class="refresh-btn shadow-md" effect="dark" content="刷新" placement="top-start">
<el-button type="primary" icon="el-icon-refresh" circle @click="$emit('refresh')"></el-button>
</el-tooltip>
</template>
<style scoped>
.refresh-btn {
position: fixed;
right: 20px;
bottom: 20px;
}
</style>

19
src/components/config/status-and-last-time.vue

@ -1,19 +0,0 @@
<template>
<el-row class="text-gray-500 text-sm my-2 flex items-center">
<span class="mr-10">
配置状态<el-tag :type="PEND_TYPE[settingStatus].type">{{ PEND_TYPE[settingStatus].text }}</el-tag>
</span>
<span>最后配置时间{{ dayjs(settingTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</el-row>
</template>
<script setup>
import dayjs from 'dayjs';
import { defineProps } from 'vue';
import { PEND_TYPE } from '@/config/config';
defineProps({
settingStatus: String,
settingTime: Number,
});
</script>

12
src/components/config/status-tag-pending.vue

@ -1,12 +0,0 @@
<template>
<el-tag :type="PEND_TYPE[type].type">
{{ PEND_TYPE[type].text }}
</el-tag>
</template>
<script setup>
import { defineProps } from 'vue';
import { PEND_TYPE } from '@/config/config';
defineProps({ type: String });
</script>

193
src/components/device/device-create.vue

@ -1,193 +0,0 @@
<template>
<el-form ref="deviceCreate" :model="data" :rules="deviceRules" label-position="top">
<el-row :gutter="20">
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="设备ID(6位)" prop="deviceId">
<el-input v-model="data.deviceId"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="站点名称" prop="address">
<el-input v-model="data.address"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="产品类型" prop="type">
<!-- <el-input v-model="data.securityMode"></el-input> -->
<el-radio v-model="data.type" label="IACD">大气腐蚀</el-radio>
<el-radio v-model="data.type" label="OTHER">其他</el-radio>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="设备完整ID" prop="deviceFullId">
<el-input v-model="data.deviceFullId"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="地区" prop="area">
<el-input v-model="data.area"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="联系人" prop="contact">
<el-input v-model="data.contact"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="电话" prop="phone">
<el-input v-model="data.phone"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="经度" prop="lon">
<el-input v-model="data.lon"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="纬度" prop="lat">
<el-input v-model="data.lat"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="负责人" prop="head">
<el-input v-model="data.head"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="安装位置" prop="installLocation">
<el-input v-model="data.installLocation"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="安装时间" prop="installTime">
<el-date-picker v-model="data.installTime" placeholder="安装时间" style="width: 100%" type="datetime"></el-date-picker>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="正式运行时间" prop="runTime">
<el-date-picker v-model="data.runTime" placeholder="正式运行时间" style="width: 100%" type="datetime"></el-date-picker>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="链路地址" prop="linkAddress">
<el-input v-model="data.linkAddress"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="探头编号" prop="probNo">
<el-input v-model="data.probNo"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="试样" prop="simple">
<el-input v-model="data.simple"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="sim卡1" prop="sim1">
<el-input v-model="data.sim1"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="设备朝向" prop="deviceDirection">
<el-input v-model="data.deviceDirection"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="协议版本" prop="protocolVersion">
<el-input v-model="data.protocolVersion"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="与主站后台联调情况" prop="joint">
<el-input v-model="data.joint" type="textarea"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="data.remark" type="textarea"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="维修记录" prop="operationRecord">
<el-input v-model="data.operationRecord" type="textarea"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" class="mt-5 pl-2">
<el-form-item>
<el-button type="primary" @click="onSubmit">提交</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-row>
</el-form>
</template>
<script setup>
import { defineEmits, reactive, ref } from 'vue';
import { useStore } from 'vuex';
import { ElMessage, ElMessageBox } from 'element-plus';
import { createDevice } from 'apis';
import { deviceData, deviceRules } from '@/config/config';
const data = reactive(deviceData);
const deviceCreate = ref(null); // form
const store = useStore();
const emit = defineEmits(['hide']);
//
const onReset = () => {
deviceCreate.value.resetFields();
};
//
const onSubmit = () => {
deviceCreate.value.validate(async valid => {
if (!valid) return;
if (data.installTime) {
data.installTime = new Date(data.installTime).getTime();
}
if (data.runTime) {
data.runTime = new Date(data.runTime).getTime();
}
try {
await createDevice({ ...data });
store.commit('device/unshiftDevice', { ...data });
ElMessageBox.confirm('添加成功,是否继续添加', '提示', {
confirmButtonText: '继续添加',
cancelButtonText: '关闭',
type: 'success',
})
.then(() => {
//
onReset();
})
.catch(() => {
onReset();
emit('on-hide');
});
await store.dispatch('device/getDevices'); //
} catch (error) {
ElMessage.error(error);
throw new Error(error);
}
});
};
</script>

13
src/components/device/device-edit.vue

@ -1,13 +0,0 @@
<template>
<el-dialog v-model="show" title="编辑设备信息" fullscreen="true" @close="$emit('toggle-mdoal')">
<DeviceEdit v-if="show" :edit="true" @cancel="$emit('toggle-mdoal')" />
</el-dialog>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
import DeviceEdit from 'views/device-edit.vue';
defineProps({ show: Boolean });
defineEmits(['toggle-mdoal']);
</script>

233
src/components/history/device.vue

@ -1,233 +0,0 @@
<template>
<SearchBar :loading-search="loadingSearch" :show-command="true" :show-export="true" :show-pass-setting="true" @search="onSearch" />
<template v-if="data">
<el-table :data="data" :max-height="contentHeight" border stripe style="width: 100%">
<el-table-column align="center" fixed label="设备ID" min-width="80" prop="deviceId" />
<el-table-column align="center" label="采集时间" min-width="170">
<template #default="scope">
{{ formatTime(+scope.row.time) }}
</template>
</el-table-column>
<el-table-column align="center" label="后台接收时间" min-width="170">
<template #default="scope">
{{ formatTime(+scope.row.createdAt) }}
</template>
</el-table-column>
<el-table-column align="center" label="锌腐蚀电流(nA)" min-width="130" prop="corrosion1" />
<el-table-column align="center" label="铜腐蚀电流(nA)" min-width="130" prop="corrosion2" />
<el-table-column align="center" label="铝腐蚀电流(nA)" min-width="130" prop="corrosion3" />
<el-table-column align="center" label="钢腐蚀电流(nA)" min-width="130" prop="corrosion4" />
<el-table-column align="center" label="环境温度(℃)" min-width="110" prop="environmentTemperature" />
<el-table-column align="center" label="环境湿度(RH%)" min-width="130" prop="environmentHumidity" />
<el-table-column align="center" label="SO2(ppb)" min-width="90" prop="so2" />
<el-table-column align="center" label="盐分温度(℃)" min-width="110" prop="saltT" />
<el-table-column align="center" label="盐分阻抗(Ω)" min-width="110" prop="saltR" />
<el-table-column align="center" label="机箱温度(℃)" min-width="110" prop="deviceTemperature" />
<el-table-column align="center" label="机箱湿度(RH%)" min-width="130" prop="deviceHumidity" />
<el-table-column align="center" label="太阳能板电压(V)" min-width="140" prop="solarVoltage" />
<el-table-column align="center" label="蓄电池电压(V)" min-width="120" prop="batteryVoltage" />
<el-table-column align="center" label="电压百分比" min-width="94" prop="batteryVoltagePercentage" />
<el-table-column align="center" label="剩余电量(mAH)" min-width="140" prop="batteryVoltageRemain" />
<el-table-column align="center" label="消耗电量(mAH)" min-width="140" prop="batteryLoss" />
<el-table-column align="center" label="ICCID" min-width="190" prop="iccid" />
<el-table-column align="center" label="IMEI" min-width="150" prop="imei" />
<el-table-column align="center" label="信号强度" min-width="80" prop="signal" />
<el-table-column align="center" label="基站编号" min-width="130" prop="stationNo" />
<el-table-column align="center" label="硬件版本" min-width="80" prop="hardwareVersion" />
<el-table-column align="center" label="软件版本" min-width="80" prop="softwareVersion" />
</el-table>
<el-pagination
:current-page="page.page"
:default-page-size="50"
:page-count="page.count"
:page-size="page.size"
:page-sizes="[1, 10, 20, 50, 100]"
:total="page.total"
background
class="my-3 float-right"
layout="total, sizes, prev, pager, next, jumper"
@current-change="onCurrentPageChange"
@size-change="onSizeChange"
@prev-click="onPrev"
@next-click="onNext"
></el-pagination>
</template>
<el-badge v-if="missingData && missingData.length" :max="99" :value="missingData.length" class="pass-button animate-bounce">
<div circle class="shadow-xl btn" type="primary" @click="showMissing = true">
<i class="el-icon-upload"></i>
</div>
</el-badge>
<el-dialog v-model="showMissing" custom-class="device-dialog" title="待补传列表" top="30px" width="80%">
<MissingData :data="missingData" @on-success="onPassSuccess" />
</el-dialog>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue';
import { useStore } from 'vuex';
import { getDatas } from 'apis';
import { ElMessage } from 'element-plus';
import dayjs from 'dayjs';
import SearchBar from 'components/history/search-bar-data.vue';
import MissingData from 'components/history/missing-data.vue';
const search = ref({});
const page = ref({
page: 1,
size: 50,
});
let timer = null;
const store = useStore();
const token = computed(() => store.getters['user/token']);
const currentDeviceId = computed(() => store.state.device.currentDeviceId); // id
const contentHeight = ref(600);
const loadingSearch = ref(false);
const data = ref(null);
const showMissing = ref(false);
const missingData = ref([]);
//
const getData = async () => {
try {
if (token && token.value) {
if (!currentDeviceId.value) {
return ElMessage.error('请选择站点');
}
const { date, missingIntervalInMs } = search.value;
if (!date || date.length !== 2) {
return ElMessage.error('请选择时间 ');
}
const params = {
deviceId: currentDeviceId.value,
gatheredDateRange: date,
paging: true,
missingIntervalInMs,
page: page.value.page,
size: page.value.size,
sort: [
{
col: 'time',
order: 'DESC',
},
],
};
loadingSearch.value = true;
const resData = await getDatas(params);
loadingSearch.value = false;
data.value = resData.data;
page.value = resData.page;
timer && clearTimeout(timer);
timer = null;
if (resData.missingData) {
missingData.value = [...resData.missingData];
missingData.value.forEach(item => {
item.uuid = `${item.deviceId}-${item.startTime}-${item.endTime}`;
});
}
} else {
timer = setTimeout(() => {
getData();
}, 20);
}
} catch (error) {
ElMessage.error(error.message || '获取数据失败');
console.log('error: ', error);
}
};
// getData();
//
onMounted(() => {
const winHeight = document.documentElement.clientHeight;
contentHeight.value = winHeight - 260;
});
/**
* 监听sear h信息
* @param {object} payload search组件emi 的数据
*/
const onSearch = payload => {
search.value = { ...payload };
getData();
};
/**
* 当前 码变化
* 更新page 重新 取数据
* @param {number} e 的页码
*/
const onCurrentPageChange = e => {
page.value.page = e;
getData();
};
const onSizeChange = e => {
page.value.size = e;
getData();
};
//
const onNext = e => {
page.value.page = e + 1;
getData();
};
//
const onPrev = e => {
page.value.page = e - 1;
getData();
};
/**
* 格式化时间
* @param {number} time 时间戳
* @returns {string}
*/
function formatTime(time) {
return dayjs(new Date(time)).format('YYYY-MM-DD HH:mm:ss');
}
//
function onPassSuccess(uuid) {
// eslint-disable-next-line no-restricted-syntax
for (const item of missingData.value) {
if (item.uuid === uuid) {
item.disabled = true;
}
}
}
</script>
<style scoped>
.device-dialog :deep(.el-dialog__body) {
padding-top: 0 !important;
}
.pass-button {
z-index: 999;
position: fixed;
right: 30px;
bottom: 40px;
}
.pass-button .btn {
width: 56px;
height: 56px;
display: flex;
justify-content: center;
align-items: center;
background-color: #409eff;
border-radius: 50%;
color: #fff;
font-size: 26px;
cursor: pointer;
}
</style>

42
src/components/history/history-log-table.vue

@ -1,42 +0,0 @@
<template>
<template v-if="data">
<el-table :data="data" :max-height="contentHeight" border class="mt-6" stripe style="width: 100%">
<el-table-column align="center" label="设备ID" min-min-width="100" prop="deviceId" />
<el-table-column align="center" label="开始时间" min-width="200">
<template #default="scope">
{{ dayjs(new Date(+scope.row.startTime)).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column align="center" label="截止时间" min-width="200">
<template #default="scope">
{{ dayjs(new Date(+scope.row.endTime)).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="下发状态" min-width="150">
<template #default="scope">
<StatusTagPending :type="scope.row.status" />
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="类型" min-width="150">
<template #default="scope">
<span v-if="scope.row && scope.row.type && PENDING_TYPE[scope.row.type]">{{ PENDING_TYPE[scope.row.type].text }}</span>
</template>
</el-table-column>
</el-table>
</template>
</template>
<script setup>
import { defineProps, onMounted, ref } from 'vue';
import dayjs from 'dayjs';
import { PENDING_TYPE } from '@/config/config';
defineProps({ data: Object });
const contentHeight = ref(600);
onMounted(() => {
const winHeight = document.documentElement.clientHeight;
contentHeight.value = winHeight - 150;
});
</script>

35
src/components/history/history-log.vue

@ -1,35 +0,0 @@
<template>
<SearchHistoryLog @search="getData" />
<TableHistoryLog :data="data" />
</template>
<script setup>
import { computed, ref } from 'vue';
import { useStore } from 'vuex';
import { getSendHistory } from 'apis';
import { ElMessage } from 'element-plus';
import SearchHistoryLog from 'components/history/search-history-log.vue';
import TableHistoryLog from 'components/history/history-log-table.vue';
const store = useStore();
const currentDeviceId = computed(() => store.state.device.currentDeviceId); // id
const data = ref([]);
async function getData(
params = {
deviceId: currentDeviceId.value,
status: '',
type: '',
},
) {
try {
const resData = await getSendHistory(params);
data.value = resData ? [...resData] : [];
} catch (error) {
ElMessage.error(error.message || '查询失败');
}
}
getData();
</script>

162
src/components/history/local.vue

@ -1,162 +0,0 @@
<template>
<SearchBar :show-export="true" :show-type-select="true" @search="onSearch" />
<template v-if="data">
<el-table :data="data" :max-height="contentHeight" border stripe style="width: 100%">
<el-table-column align="center" fixed label="设备ID" min-width="80" prop="deviceId" />
<el-table-column align="center" label="采集时间" min-width="170">
<template #default="scope">
{{ formatTime(+scope.row.time) }}
</template>
</el-table-column>
<el-table-column align="center" label="后台接收时间" min-width="170">
<template #default="scope">
{{ formatTime(+scope.row.createdAt) }}
</template>
</el-table-column>
<el-table-column align="center" label="锌腐蚀电流(nA)" min-width="130" prop="corrosion1" />
<el-table-column align="center" label="铜腐蚀电流(nA)" min-width="130" prop="corrosion2" />
<el-table-column align="center" label="铝腐蚀电流(nA)" min-width="130" prop="corrosion3" />
<el-table-column align="center" label="钢腐蚀电流(nA)" min-width="130" prop="corrosion4" />
<el-table-column align="center" label="环境温度(℃)" min-width="110" prop="environmentTemperature" />
<el-table-column align="center" label="环境湿度(RH%)" min-width="130" prop="environmentHumidity" />
<el-table-column align="center" label="SO2(ppb)" min-width="90" prop="so2" />
<el-table-column align="center" label="盐分温度(℃)" min-width="110" prop="saltT" />
<el-table-column align="center" label="盐分阻抗(Ω)" min-width="110" prop="saltR" />
<el-table-column align="center" label="太阳能板电压(V)" min-width="140" prop="solarVoltage" />
<el-table-column align="center" label="蓄电池电压(V)" min-width="120" prop="batteryVoltage" />
<el-table-column align="center" label="机箱温度(℃)" min-width="110" prop="deviceTemperature" />
<el-table-column align="center" label="机箱湿度(RH%)" min-width="130" prop="deviceHumidity" />
</el-table>
<el-pagination
:current-page="page.page"
:default-page-size="50"
:page-count="page.count"
:page-size="page.size"
:page-sizes="[1, 10, 20, 50, 100]"
:total="page.total"
background
class="my-3 float-right"
layout="total, sizes, prev, pager, next, jumper"
@current-change="onCurrentPageChange"
@size-change="onSizeChange"
@prev-click="onPrev"
@next-click="onNext"
></el-pagination>
</template>
</template>
<script setup>
import { getDatas } from 'apis';
import SearchBar from 'components/history/search-bar-data.vue';
import dayjs from 'dayjs';
import { ElMessage } from 'element-plus';
import { computed, onMounted, ref } from 'vue';
import { useStore } from 'vuex';
const search = ref({});
const page = ref({
page: 1,
size: 50,
});
let timer = null;
const store = useStore();
const token = computed(() => store.getters['user/token']);
const currentDeviceId = computed(() => store.state.device.currentDeviceId); // id
const contentHeight = ref(600);
const data = ref(null);
//
const getData = async () => {
try {
if (token && token.value) {
if (!currentDeviceId.value) {
return ElMessage.error('请选择站点');
}
const options = { ...search.value };
const date = options && options.date ? options.date : [];
console.log(date);
if (!date || date.length !== 2) {
return ElMessage.error('请选择时间 ');
}
const params = {
deviceId: currentDeviceId.value,
gatheredDateRange: date,
paging: true,
page: page.value.page,
size: page.value.size,
sort: [
{
col: 'time',
order: 'DESC',
},
],
};
const resData = await getDatas(params);
data.value = resData.data;
page.value = resData.page;
timer && clearTimeout(timer);
timer = null;
} else {
timer = setTimeout(() => {
getData();
}, 20);
}
} catch (error) {
ElMessage.error(error.message || '获取数据失败');
console.log('error: ', error);
}
};
//
onMounted(() => {
const winHeight = document.documentElement.clientHeight;
contentHeight.value = winHeight - 250;
});
/**
* 监听sear h信息
* @param {object} payload search组件emi 的数据
*/
const onSearch = payload => {
search.value = { ...payload };
getData();
};
/**
* 当前 码变化
* 更新page 重新 取数据
* @param {number} e 的页码
*/
const onCurrentPageChange = e => {
page.value.page = e;
getData();
};
const onSizeChange = e => {
page.value.size = e;
getData();
};
//
const onNext = e => {
page.value.page = e + 1;
getData();
};
//
const onPrev = e => {
page.value.page = e - 1;
getData();
};
/**
* 格式化时间
* @param {number} time 时间戳
* @returns {string}
*/
function formatTime(time) {
return dayjs(new Date(time)).format('YYYY-MM-DD HH:mm:ss');
}
</script>

48
src/components/history/missing-data.vue

@ -1,48 +0,0 @@
<template>
<el-table :data="data" :max-height="contentHeight" border class="mt-6" stripe style="width: 100%; margin-top: 0">
<el-table-column align="center" label="设备ID" min-width="100" prop="deviceId" />
<el-table-column align="center" label="起始时间" min-width="200">
<template #default="scope">
{{ dayjs(new Date(+scope.row.startTime)).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column align="center" label="截止时间" min-width="200">
<template #default="scope">
{{ dayjs(new Date(+scope.row.endTime)).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="操作" min-width="100">
<template #default="scope">
<el-button :disabled="scope.row.disabled" plain size="small" type="success" @click="handlePass(scope.row)">补传 </el-button>
</template>
</el-table-column>
</el-table>
</template>
<script setup>
import { defineProps, defineEmits, ref } from 'vue';
import dayjs from 'dayjs';
import { sendCommand } from 'apis';
import { ElMessage } from 'element-plus';
defineProps({ data: Object });
const emit = defineEmits(['on-success']);
const contentHeight = ref(600);
/**
* 发送补传指令
* @param { Object } item
* @returns {Promise<void>}
*/
async function handlePass(item) {
try {
await sendCommand({ ...item });
ElMessage.success('发送成功');
emit('on-success', item.uuid);
} catch (error) {
ElMessage.error(error.message || '发送补传指令失败');
}
}
</script>

148
src/components/history/search-bar-data.vue

@ -1,148 +0,0 @@
<template>
<el-form ref="searchDeviceForm" :inline="true" :model="searchDevice">
<el-form-item label="选择站点:">
<el-select v-model="searchDevice.deviceId" placeholder="请选择站点" @change="change">
<!-- <el-option label="全部" value></el-option> -->
<el-option
v-for="item in devices"
:key="item.deviceId"
:label="`${item.address}-${item.deviceId}`"
:value="item.deviceId"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="起始时间:">
<el-date-picker
v-model="searchDevice.date[0]"
format="YYYY-MM-DD HH:mm"
placeholder="起始时间"
style="width: 180px"
type="datetime"
></el-date-picker>
</el-form-item>
<el-form-item label="截止时间:">
<el-date-picker
v-model="searchDevice.date[1]"
format="YYYY-MM-DD HH:mm"
placeholder="截止时间"
style="width: 180px"
type="datetime"
></el-date-picker>
</el-form-item>
<el-form-item v-if="showPassSetting" label="补传间隔(h):">
<el-input-number v-model="searchDevice.missingIntervalInMs" :min="0" controls-position="right" style="width: 100px" />
</el-form-item>
<el-form-item>
<template v-if="showCommand">
<el-button type="primary" @click="onSubmit">查询历史数据</el-button>
<!-- <el-button type="warning" @click="onSend('EVENT')">查询历史事件</el-button>-->
</template>
<el-button v-else :loading="loadingSearch" type="primary" @click="onSubmit">
<i class="el-icon-search mr-2"></i>
查询
</el-button>
</el-form-item>
<el-form-item v-if="showExport">
<el-button :loading="loadingExport" type="success" @click="onExport">
<i class="el-icon-download mr-2"></i>
导出
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { exportHistory } from 'apis';
import dayjs from 'dayjs';
import { computed, defineEmits, defineProps, reactive, ref, watch } from 'vue';
import { useStore } from 'vuex';
const emit = defineEmits(['search']);
const searchDevice = reactive({
deviceId: '',
date: [dayjs().subtract(7, 'day').startOf('day'), new Date()],
missingIntervalInMs: 1,
});
const searchDeviceForm = ref(null); // form
const store = useStore();
const devices = computed(() => store.state.device.devices);
const currentDeviceId = computed(() => store.state.device.currentDeviceId); // id
const loadingExport = ref(false);
defineProps({
showExport: Boolean,
loadingSearch: Boolean,
showCommand: Boolean,
showPassSetting: Boolean,
});
// currentDeviceId
watch(
[searchDeviceForm, currentDeviceId],
([newForm, newDeviceId]) => {
if (newDeviceId && newForm) {
searchDevice.deviceId !== newDeviceId && (searchDevice.deviceId = newDeviceId);
onSubmit();
}
},
{ immediate: true },
);
const change = e => {
store.commit('device/setCurrentDeviceId', e);
};
//
function generateParams() {
const { deviceId, date, missingIntervalInMs } = searchDevice;
const params = {
deviceId,
missingIntervalInMs: missingIntervalInMs * 60 * 60 * 1000,
};
if (date) {
const start = +dayjs(date[0]).format('x');
const end = +dayjs(date[1]).format('x');
params.date = [start, end];
}
return params;
}
//
function onSubmit() {
searchDeviceForm.value.validate(valid => {
if (valid) {
const params = generateParams();
emit('search', params);
}
});
}
//
async function onExport() {
try {
loadingExport.value = true;
const params = generateParams();
params.sort = [
{
col: 'time',
order: 'DESC',
},
];
params.paging = false;
params.gatheredDateRange = params.date;
const resData = await exportHistory(params);
loadingExport.value = false;
resData && (window.location.href = resData);
} catch (error) {
loadingExport.value = false;
throw new Error(error);
}
}
</script>

77
src/components/history/search-history-log.vue

@ -1,77 +0,0 @@
<template>
<el-form ref="searchHistoryLogForm" :inline="true" :model="searchDevice">
<el-form-item label="选择站点">
<el-select v-model="searchDevice.deviceId" placeholder="请选择站点" @change="change">
<el-option label="全部" value></el-option>
<el-option
v-for="item in devices"
:key="item.deviceId"
:label="`${item.address}-${item.deviceId}`"
:value="item.deviceId"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="类型">
<el-select v-model="searchDevice.type" placeholder="选择查询类型">
<el-option label="全部" value=""></el-option>
<el-option label="事件上报" value="EVENT"></el-option>
<el-option label="业务上报" value="DATA"></el-option>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchDevice.status" placeholder="选择查询类型">
<el-option label="全部" value=""></el-option>
<el-option label="待下发" value="PENDING"></el-option>
<el-option label="下发成功" value="SUCCESS"></el-option>
<el-option label="下发失败" value="FAIL"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">
<i class="el-icon-search mr-2"></i>
查询
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { computed, defineEmits, reactive, ref, watch } from 'vue';
import { useStore } from 'vuex';
const emit = defineEmits(['search']);
const searchDevice = reactive({
deviceId: '',
type: '',
status: '',
});
const searchHistoryLogForm = ref(null); // form
const store = useStore();
const devices = computed(() => store.state.device.devices);
const currentDeviceId = computed(() => store.state.device.currentDeviceId); // id
// currentDeviceId
watch(
() => currentDeviceId.value,
newValue => {
if (newValue) {
searchDevice.deviceId !== newValue && (searchDevice.deviceId = newValue);
}
},
{ immediate: true },
);
const change = e => {
store.commit('device/setCurrentDeviceId', e);
};
//
const onSubmit = () => {
searchHistoryLogForm.value.validate(() => {
emit('search', { ...searchDevice });
});
};
</script>

23
src/components/navbar.vue

@ -1,23 +0,0 @@
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const toggleCollapse = () => {
console.log('Toggle Collapse');
store.commit('toggleCollapse');
};
const menu = computed(() => store.state.menu);
</script>
<template>
<h1 class="text-lg font-medium py-3 px-6 shadow">
<i
class="el-icon-guide mr-2"
:class="{ 'text-gray-800': !menu.collapse, 'text-gray-400': menu.collapse }"
@click="toggleCollapse"
v-if="menu.show"
></i>
{{ $route.meta.title || '智能大气腐蚀监测平台' }}
</h1>
</template>

49
src/components/overview/chart-device-count.vue

@ -1,49 +0,0 @@
<template>
<div id="device-overview-container" style="height: 354px"></div>
</template>
<script setup>
import { computed, onMounted, ref, watchEffect } from 'vue';
import { useStore } from 'vuex';
import isEmpty from 'lodash/isEmpty';
import { generateChartOption } from 'utils/overview';
import * as echarts from 'echarts/core';
import { TitleComponent, TooltipComponent } from 'echarts/components';
import { PieChart } from 'echarts/charts';
import { LabelLayout } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([TitleComponent, TooltipComponent, PieChart, CanvasRenderer, LabelLayout]);
const store = useStore();
const count = computed(() => store.state.device.count);
const isMounted = ref(false);
let myChart = null;
watchEffect(() => {
if (isEmpty(count.value) || !isMounted.value) return;
if (!myChart) {
initChart();
render(count.value);
} else {
render(count.value);
}
});
// chart
function initChart() {
const chartDom = document.getElementById('device-overview-container');
myChart = echarts.init(chartDom);
}
// chart
function render(rawCount) {
const option = generateChartOption(rawCount);
option && myChart.setOption(option);
}
onMounted(() => {
initChart();
isMounted.value = true;
});
</script>

93
src/components/overview/chart-device-detail.vue

@ -1,93 +0,0 @@
<template>
<el-card class="pb-4" shadow="hover">
<el-row :gutter="20">
<el-col :span="12">
<div class="text-sm text-gray-400">设备总数</div>
<div class="text-gray-500 text-4xl mt-1 mb-3">{{ count.total }}</div>
<div class="line-wrap bg-blue-50">
<div :style="{ width: `${countPercent.total}` }" class="line bg-blue-300"></div>
</div>
</el-col>
<el-col :span="12">
<div class="text-sm text-gray-400">正常数量</div>
<div class="text-gray-500 text-4xl mt-1 mb-3">{{ count.normal }}</div>
<div class="line-wrap bg-lime-50">
<div :style="{ width: `${countPercent.normal}` }" class="line bg-lime-300"></div>
</div>
</el-col>
<el-col :span="12" class="mt-10">
<div class="text-sm text-gray-400">离线数量</div>
<div class="text-gray-500 text-4xl mt-1 mb-3">{{ count.offline }}</div>
<div class="line-wrap bg-gray-50">
<div :style="{ width: `${countPercent.offline}` }" class="line bg-gray-300"></div>
</div>
</el-col>
<el-col :span="12" class="mt-10">
<div class="text-sm text-gray-400">报警数量</div>
<div class="text-gray-500 text-4xl mt-1 mb-3">{{ count.warning }}</div>
<div class="line-wrap bg-yellow-50">
<div :style="{ width: `${countPercent.warning}` }" class="line bg-yellow-300"></div>
</div>
</el-col>
<el-col :span="12" class="mt-10">
<div class="text-sm text-gray-400">在线数量</div>
<div class="text-gray-500 text-4xl mt-1 mb-3">{{ count.online }}</div>
<div class="line-wrap bg-green-50">
<div :style="{ width: `${countPercent.online}` }" class="line bg-green-300"></div>
</div>
</el-col>
<el-col :span="12" class="mt-10">
<div class="text-sm text-gray-400">故障数量</div>
<div class="text-gray-500 text-4xl mt-1 mb-3">{{ count.fault }}</div>
<div class="line-wrap bg-red-50">
<div :style="{ width: `${countPercent.fault}` }" class="line bg-red-300"></div>
</div>
</el-col>
</el-row>
</el-card>
</template>
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const count = computed(() => store.state.device.count);
const countPercent = computed(() => {
return {
online: computeWidth(count.value.online),
offline: computeWidth(count.value.offline),
warning: computeWidth(count.value.warning),
fault: computeWidth(count.value.fault),
total: computeWidth(count.value.total),
normal: computeWidth(count.value.normal),
};
});
/**
* 计算宽度百分比
* @param {number} itemCount
* @returns {string}
*/
function computeWidth(itemCount) {
const percent = (itemCount * 100) / count.value.total;
return `${percent}%`;
}
</script>
<style scoped>
.line-wrap {
position: relative;
width: 60%;
height: 10px;
border-radius: 5px;
overflow: hidden;
}
.line {
position: absolute;
left: 0;
top: 0;
height: 100%;
}
</style>

112
src/components/overview/device-table.vue

@ -1,112 +0,0 @@
<template>
<template v-if="devicesAll && devicesAll.data">
<el-table :data="devicesAll.data" :max-height="contentHeight" border class="mt-6" stripe style="width: 100%">
<el-table-column align="center" label="站点名称" min-width="150" prop="address" />
<el-table-column align="center" label="设备ID" min-width="100" prop="deviceId" />
<el-table-column align="center" label="注册时间" min-width="200">
<template #default="scope">
{{ dayjs(new Date(+scope.row.signupTime)).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column align="center" label="最近上传时间" min-width="200">
<template #default="scope">
{{ dayjs(new Date(+scope.row.lastUploadTime)).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="设备状态" min-width="150">
<template #default="scope">
<el-tag v-if="scope.row.status" :color="STATUS_COLOR[scope.row.status].color" style="color: #fff">
{{ STATUS_COLOR[scope.row.status].text }}
</el-tag>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="产品类型" min-width="150">
<template #default="scope">
<span v-if="scope.row.type && ASDU_TYPE[scope.row.type]">{{ ASDU_TYPE[scope.row.type].text }}</span>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="协议版本" min-width="150" prop="protocolVersion" />
</el-table>
<el-pagination
:current-page="devicesAll.page.page"
:default-page-size="50"
:page-count="devicesAll.page.count"
:page-size="devicesAll.page.size"
:page-sizes="[1, 10, 20, 50, 100]"
:total="devicesAll.page.total"
background
class="my-3 float-right"
layout="total, sizes, prev, pager, next, jumper"
@size-change="onSizeChange"
@current-change="onCurrentPageChange"
></el-pagination>
</template>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue';
import { useStore } from 'vuex';
import dayjs from 'dayjs';
import { STATUS_COLOR } from '@/config/config';
import { ASDU_TYPE } from '@/config/log';
let timer = null;
const store = useStore();
const token = computed(() => store.getters['user/token']);
const devicesAll = computed(() => {
return store.state.device.devicesAll;
});
const currentDeviceId = computed(() => store.state.device.currentDeviceId);
const contentHeight = ref(600);
//
const getDevicesAllData = () => {
try {
if (token && token.value) {
store.dispatch('device/getDevicesAll');
timer && clearTimeout(timer);
timer = null;
} else {
timer = setTimeout(() => {
getDevicesAllData();
});
}
} catch (error) {
throw new Error(error);
}
};
getDevicesAllData();
//
onMounted(() => {
const winHeight = document.documentElement.clientHeight;
contentHeight.value = winHeight - 250;
});
/**
* 删除
* @param {number} page 页码
* @param {number} size 每页条数
*/
function onSearch(page, size = 50) {
const deviceId = currentDeviceId.value;
const params = {
deviceId,
page,
size,
};
store.dispatch('device/getDevicesAll', params);
}
//
const onCurrentPageChange = page => {
onSearch(page, devicesAll.value.page.size || 50);
};
//
const onSizeChange = size => {
onSearch(1, size);
};
</script>

66
src/components/realtime/search-bar.vue

@ -1,66 +0,0 @@
<template>
<el-form ref="searchDeviceForm" :inline="true" :model="searchDevice">
<el-form-item label="选择站点">
<el-select v-model="searchDevice.deviceId" placeholder="请选择站点" @change="change">
<el-option v-if="showAll" label="全部" value></el-option>
<el-option
v-for="item in devices"
:key="item.deviceId"
:label="`${item.address}-${item.deviceId}`"
:value="item.deviceId"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">
<i class="el-icon-search mr-2"></i>
查询
</el-button>
</el-form-item>
<el-form-item v-if="showRefresh">
<el-button type="primary" @click="$emit('refresh')">
<i class="el-icon-refresh mr-2"></i>
刷新
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { computed, defineEmits, defineProps, reactive, ref, watch } from 'vue';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
defineProps({ showRefresh: Boolean });
const emit = defineEmits(['getDate', 'refresh']);
const router = useRouter();
const searchDevice = reactive({ deviceId: '' });
const searchDeviceForm = ref(null); // form
const store = useStore();
const devices = computed(() => store.state.device.devices);
const currentDeviceId = computed(() => store.state.device.currentDeviceId); // id
const showAll = computed(() => router.currentRoute.value.name === 'devices'); //
// currentDeviceId
watch(
() => currentDeviceId.value,
newValue => {
if (newValue && searchDevice.deviceId !== newValue) {
searchDevice.deviceId = newValue;
}
},
{ immediate: true },
);
const change = e => {
store.commit('device/setCurrentDeviceId', e);
};
const onSubmit = () => {
searchDeviceForm.value.validate(() => {
emit('search');
});
};
</script>

131
src/components/statistical/search-bar.vue

@ -1,131 +0,0 @@
<template>
<el-form ref="searchDeviceForm" :inline="true" :model="searchDevice">
<el-form-item label="选择站点:">
<el-select v-model="searchDevice.deviceId" placeholder="请选择站点" @change="change">
<!-- <el-option label="全部" value></el-option> -->
<el-option
v-for="item in devices"
:key="item.deviceId"
:label="`${item.address}-${item.deviceId}`"
:value="item.deviceId"
></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="showTypeSelect" label="类型:">
<el-select v-model="searchDevice.dataType" placeholder="选择查询类型">
<el-option label="全部" value=""></el-option>
<el-option label="事件上报" value="ReportHistoryEvent"></el-option>
<el-option label="业务上报" value="ReportHistoryData"></el-option>
</el-select>
</el-form-item>
<el-form-item label="起始时间:">
<el-date-picker v-model="searchDevice.date[0]" format="YYYY-MM-DD HH:mm" placeholder="起始时间" type="datetime"></el-date-picker>
</el-form-item>
<el-form-item label="截止时间:">
<el-date-picker v-model="searchDevice.date[1]" format="YYYY-MM-DD HH:mm" placeholder="截止时间" type="datetime"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button :loading="loadingSearch" type="primary" @click="onSubmit">
<i class="el-icon-search mr-2"></i>
查询
</el-button>
</el-form-item>
<el-form-item v-if="showExport">
<el-button :loading="loadingExport" type="success" @click="onExport">
<i class="el-icon-download mr-2"></i>
导出
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { computed, defineEmits, defineProps, reactive, ref, watch } from 'vue';
import { useStore } from 'vuex';
import dayjs from 'dayjs';
import { exportHistory } from 'apis';
const emit = defineEmits(['search']);
const searchDevice = reactive({
deviceId: '',
date: [dayjs().subtract(7, 'day').startOf('day'), new Date()],
dataType: 'ReportHistoryData',
});
const searchDeviceForm = ref(null); // form
const store = useStore();
const devices = computed(() => store.state.device.devices);
const currentDeviceId = computed(() => store.state.device.currentDeviceId); // id
const loadingExport = ref(false);
defineProps({
showExport: Boolean,
showTypeSelect: Boolean,
loadingSearch: Boolean,
});
// currentDeviceId
watch(
() => currentDeviceId.value,
newValue => {
if (newValue) {
searchDevice.deviceId !== newValue && (searchDevice.deviceId = newValue);
}
},
{ immediate: true },
);
const change = e => {
store.commit('device/setCurrentDeviceId', e);
};
//
function generateParams() {
const { deviceId, date, dataType } = searchDevice;
let params = {
deviceId,
date,
dataType,
};
if (date) {
const start = +dayjs(date[0]).format('x');
const end = +dayjs(date[1]).format('x');
const daterange = [start, end];
params = {
deviceId,
date: daterange,
dataType,
};
}
return params;
}
//
const onSubmit = () => {
searchDeviceForm.value.validate(valid => {
if (valid) {
const params = generateParams();
emit('search', params);
}
});
};
//
async function onExport() {
try {
loadingExport.value = true;
const params = generateParams();
const resData = await exportHistory(params);
loadingExport.value = false;
resData && (window.location.href = resData);
} catch (error) {
loadingExport.value = false;
throw new Error(error);
}
}
</script>

82
src/components/statistical/stastistical-chart.vue

@ -1,82 +0,0 @@
<template>
<el-card class="box-card">
<el-empty v-if="noData" description="暂无数据"></el-empty>
<div id="realtimeContainer"></div>
</el-card>
</template>
<script setup>
import { computed, defineExpose, defineProps, onMounted, ref, watchEffect } from 'vue';
import { useStore } from 'vuex';
import * as echarts from 'echarts/core';
import { DataZoomComponent, GridComponent, LegendComponent, ToolboxComponent, TooltipComponent } from 'echarts/components';
import { LineChart } from 'echarts/charts';
import { UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import { generateChartOption } from 'utils/statistical';
echarts.use([
ToolboxComponent,
TooltipComponent,
GridComponent,
LegendComponent,
LineChart,
CanvasRenderer,
UniversalTransition,
DataZoomComponent,
]);
const store = useStore();
const props = defineProps({ searchHeight: Number });
const realtimeData = computed(() => store.state.statistics.realtimeData);
const isMounted = ref(false);
const noData = ref(true);
let myChart = null;
//
onMounted(() => {
isMounted.value = true;
window.addEventListener('resize', () => {
if (!myChart) return;
myChart.resize();
});
});
watchEffect(() => {
if (!realtimeData.value) return;
const { data } = realtimeData.value;
if (!data || !data.length || !isMounted.value) return;
initChart(data);
});
// chart
function initChart(data) {
const chartDom = document.getElementById('realtimeContainer');
const canvasHeight = document.documentElement.clientHeight - props.searchHeight - 150;
chartDom.style.height = `${canvasHeight}px`;
myChart && myChart.dispose();
myChart = echarts.init(chartDom);
render(data);
myChart.on('legendselectchanged', event => {
render(data, event.selected);
});
}
function render(data, selected) {
// myChart && myChart.clear();
if (!data || !data.length) {
noData.value = true;
return;
}
noData.value = false;
if (!myChart) {
initChart(data);
}
const option = generateChartOption(data, selected);
option && myChart.setOption(option);
}
defineExpose({ render });
</script>

164
src/config/config.js

@ -1,164 +0,0 @@
// 网络参数设置
export const networkConfig = {
ip1: '',
port1: '',
ip2: '',
port2: '',
ip3: '',
port3: '',
ipBackup: '',
portBackup: '',
account: '',
password: '',
apn: '',
status: '',
};
// 功能参数设置
export const functionConfig = {
frequency: {
so2: 0, // SO2采样频率
metal: 0, // 金属腐蚀采样频率
th: 0, // 温湿度 采样频率
salt: 0, // 盐雾 采样频率
}, // 采样频率
count: 0, // 采集个数
time: Date.now(), // 设置时间
batteryLow: 0, // 电池电压低阈值
batteryHigh: 0, // 电池电压高阈值
sunHigh: 0, // 太阳能电压高阈值
humidityHigh: 0, // 湿度高阈值
temperatureLow: 0, // 温度低阈值
temperatureHigh: 0, // 温度高阈值
securityMode: 'OPEN', // 安全模式 OPEN->不加密 ENCRYPTION->加密
report: {
type: 'CYCLE', // 上报周期类型 0->时间点 1->周期
timePoints: [''], // 设置时间点
cycle: 240, // 上报周期分钟数
},
status: '',
};
// 金属腐蚀类型
export const corrosiveTypes = [
{
value: 'XIN',
type: '锌',
},
{
value: 'LV',
type: '铝',
},
{
value: 'TONG',
type: '铜',
},
{
value: 'GANG',
type: '钢',
},
];
// 添加设备
export const deviceData = {
deviceId: '', // 设备id
deviceFullId: '', // 设备完整id
deviceDirection: '', // 设备朝向
area: '', // 地区
address: '', // 站点名称
contact: '', // 联系人
phone: '', // 联系人电话
lon: '', // 经度
lat: '', // 纬度
head: '', // 负责人
installLocation: '', // 安装位置
installTime: '', // 安装时间
runTime: '', // 正式运行时间
linkAddress: '', // 链路地址
probNo: '', // 探头编号
simple: '', // 试样
sim1: '', // sim卡1
protocolVersion: '', // 协议版本
joint: '', // 主站后台联调情况
operationRecord: '', // 维修记录
remark: '', // 备注
type: 'IACD', // 产品类型
};
// 设备添加编辑 规则
export const deviceRules = {
deviceId: [
{
required: true,
message: '请输入设备ID号',
trigger: 'blur',
},
{
len: 6,
message: '请输入6位设备ID号',
trigger: 'blur',
},
],
address: [
{
required: true,
message: '请输入站点名称',
trigger: 'blur',
},
],
};
// 下发类型
export const PEND_TYPE = {
PENDING: {
type: 'primary',
text: '待下发',
},
FAIL: {
type: 'danger',
text: '配置失败',
},
SUCCESS: {
type: 'success',
text: '配置成功',
},
};
// 上报类型
export const REPORT_TYPE = {
CYCLE: '周期上报',
POINT: '定时上报',
};
// 下发类型
export const PENDING_TYPE = {
DATA: { text: '业务上报' },
EVENT: { text: '事件上报' },
};
// 实时查询数据的轮询时间 1分钟
export const REALTIME_DATA_INTERVAL = 60000 * 5;
// 设备状态 情景
export const STATUS_COLOR = {
FAULT: {
text: '故障',
color: '#FCA5A5',
},
WARNING: {
text: '报警',
color: '#FCD34D',
},
NORMAL: {
text: '正常',
color: '#BEF264',
},
OFFLINE: {
text: '离线',
color: '#CBD5E1',
},
ONLINE: {
text: '在线',
color: '#6EE7B7',
},
};

107
src/config/layout/dataCenterBody.js

@ -0,0 +1,107 @@
// eslint-disable-next-line import/prefer-default-export
export const body = {
left: {
width: '480rem',
data: [
{
width: '480rem',
height: '308rem',
background: { image: 'src/assets/border-1.png' },
title: {
text: '行业热点',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: 'DataCenterNews',
},
{
width: '480rem',
height: '618rem',
background: { image: 'src/assets/border-2.png' },
title: {
text: '时物链',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: 'DataCenterTall',
},
],
},
middle: {
width: '920rem',
data: [
{
width: '920rem',
height: '626rem',
background: { image: 'src/assets/border-main.png' },
title: {
hide: true,
text: '3D地图',
},
component: 'DataCenterMapContainer',
},
{
width: '920rem',
height: '300rem',
background: { image: 'src/assets/border-3.png' },
title: {
text: '综合曲线',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: 'DataCenterComplexChart',
},
],
},
right: {
width: '480rem',
data: [
{
width: '480rem',
height: '308rem',
background: { image: 'src/assets/border-5.png' },
title: {
hide: true,
text: '专家会诊统计',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: '',
},
{
width: '480rem',
height: '308rem',
background: { image: 'src/assets/border-1.png' },
title: {
text: '全省产量对比',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: 'DataCenterYieldChart',
},
{
width: '480rem',
height: '300rem',
background: { image: 'src/assets/border-1.png' },
title: {
text: '全省报警统计',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: 'DataCenterFaultChart',
},
],
},
};

106
src/config/layout/dataFarmBody.js

@ -0,0 +1,106 @@
// eslint-disable-next-line import/prefer-default-export
export const body = {
left: {
width: '480rem',
data: [
{
width: '480rem',
height: '308rem',
background: { image: 'assets/border-1.png' },
title: {
text: '室内/外温度折线',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: 'DataFarmTemperature',
},
{
width: '480rem',
height: '308rem',
background: { image: 'assets/border-1.png' },
title: {
text: '室内/外湿度折线',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: 'DataFarmHumidity',
},
{
width: '480rem',
height: '308rem',
background: { image: 'assets/border-1.png' },
title: {
text: '土壤湿度',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: 'DataFarmSoilHumidity',
},
],
},
middle: {
width: '920rem',
data: [
{
width: '920rem',
height: '626rem',
background: { image: 'assets/border-main.png' },
title: {
hide: true,
text: '3D地图',
},
component: 'DataCenterMapContainer',
},
{
width: '920rem',
height: '300rem',
background: { image: 'assets/border-3.png' },
title: {
text: '综合曲线',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: 'DataCenterComplexChart',
},
],
},
right: {
width: '480rem',
data: [
{
width: '480rem',
height: '618rem',
background: { image: 'assets/border-2.png' },
title: {
text: '时物链',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: 'DataCenterTall',
},
{
width: '480rem',
height: '300rem',
background: { image: 'assets/border-1.png' },
title: {
text: '实时报警',
color: '#fff',
size: '18rem',
align: 'center',
height: '40rem',
},
component: 'DataCenterFaultChart',
},
],
},
};

110
src/config/log.js

@ -1,110 +0,0 @@
// 功能码
export const CODE = {
BUSINESS_REPORT: {
text: '业务上报',
code: 0,
},
EVENT_REPORT: {
text: '事件上报',
code: 1,
},
NET_PARAM_SETTING_OR_READING: {
code: 2,
text: '联网参数设置或读取',
},
FUNC_PARAM_SETTING_OR_READING: {
code: 3,
text: '功能参数设置或读取',
},
READ_HISTORY_DATA: {
code: 4,
text: '读取历史日志记录',
},
READ_HISTORY_EVENT: {
code: 5,
text: '读取历史事件记录',
},
UPGRADE: {
code: 6,
text: '升级启动、传输、结束',
},
ACK: {
code: 7,
text: '应答帧(固定帧)',
},
OVER: {
code: 8,
text: '结束帧',
},
RESERVED: {
code: 9,
text: '保留',
},
};
// 传输方向
export const DIRECTION = {
IACD2SERVER: '上行',
SERVER2IACD: '下行',
};
// 传输启动
export const PRM = {
ACK: '传输结束',
REQ: '传输启动',
};
// 流控
export const DFC = {
CONTINUE: '有后续包',
OVER: '无后续包',
};
// 安全方式
export const SER = {
OPEN: '不加密',
ENCRYPTION: '加密',
};
// 类型标识
export const ASDU_TYPE = {
IACD: {
code: 1,
text: '大气腐蚀',
},
OTHER: {
code: 2,
text: '其他',
},
};
// 信息体序
export const INFO_ORDER = {
NoOrder: '无序',
HasOrder: '有序',
None: '无',
};
// 传输原因
export const REASON = {
None: {
code: 0,
text: '无',
},
ReportData: {
code: 1,
text: '业务主动上报',
},
ReportEvent: {
code: 2,
text: '事件主动上报',
},
History: {
code: 3,
text: '历史数据记录或程序数据',
},
HumanOperator: {
code: 4,
text: '人工上报',
},
};

2789
src/config/map/shanxi.js

File diff suppressed because it is too large

47
src/config/map/values.js

@ -0,0 +1,47 @@
// eslint-disable-next-line import/prefer-default-export
export const values = [
{
name: '太原市',
value: 12,
},
{
name: '大同市',
value: 4,
},
{
name: '阳泉市',
value: 5,
},
{
name: '长治市',
value: 6,
},
{
name: '晋城市',
value: 2,
},
{
name: '朔州市',
value: 1,
},
{
name: '晋中市',
value: 7,
},
{
name: '运城市',
value: 3,
},
{
name: '忻州市',
value: 3,
},
{
name: '临汾市',
value: 1,
},
{
name: '吕梁市',
value: 2,
},
];

2
src/config/time.js

@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export const weeks = ['日', '一', '二', '三', '四', '五', '六'];

19
src/hooks/useDeviceCreate.js

@ -1,19 +0,0 @@
import { ref } from 'vue';
export default function useDeviceCreate() {
const display = ref(false);
function hide() {
display.value = false;
}
function show() {
display.value = true;
}
return {
display,
hide,
show,
};
}

14
src/hooks/useLayoutConfig.js

@ -0,0 +1,14 @@
import { inject, computed } from 'vue';
import { body as centerBody } from '@/config/layout/dataCenterBody';
import { body as farmBody } from '@/config/layout/dataFarmBody';
export default function useLayoutConfig() {
const scene = inject('scene');
return computed(() => {
if (scene === 'farm') {
return farmBody;
}
return centerBody;
});
}

19
src/hooks/useScrollList.js

@ -0,0 +1,19 @@
import { ref } from 'vue';
export default function useScrollList() {
const translateY = ref(0);
function scrollList(liHeight, ulHeight) {
setInterval(() => {
if (translateY.value < -ulHeight) {
translateY.value = 0;
}
translateY.value -= liHeight;
}, 4000);
}
return {
translateY,
scrollList,
};
}

3
src/main.js

@ -1,6 +1,3 @@
import 'virtual:windi.css';
import 'element-plus/dist/index.css';
import { createApp } from 'vue';
import App from './App.vue';
import router from './routers/index';

118
src/mock/news.js

@ -0,0 +1,118 @@
import dayjs from 'dayjs';
const formatDate = 'YYYY/MM/DD';
export default [
{
title: '2020两会热点话题:中国粮食安全有保障',
url: 'http://www.agricultureiot.com/h-nd-325.html#_np=4_11',
hot: true,
new: true,
date: dayjs().format(formatDate),
},
{
title: '2020中国农业展望大会在京召开',
url: 'http://www.agricultureiot.com/h-nd-316.html#_np=4_11',
hot: true,
new: false,
date: dayjs().subtract(1, 'day').format(formatDate),
},
{
title: '国家十三部委:2020农业补贴新政策',
url: 'http://www.agricultureiot.com/h-nd-324.html#_np=4_11',
hot: true,
new: false,
date: dayjs().subtract(5, 'day').format(formatDate),
},
{
title: '2020年农业补贴政策出炉',
url: 'http://www.agricultureiot.com/h-nd-310.html#_np=4_11',
hot: false,
new: true,
date: dayjs().format(formatDate),
},
{
title: '农业项目申报条件和时间',
url: 'http://www.agricultureiot.com/h-nd-323.html#_np=4_11',
hot: false,
new: true,
date: dayjs().subtract(1, 'day').format(formatDate),
},
{
title: '物联网在农业领域的应用发展对现代科学仪器的需求',
url: 'http://www.agricultureiot.com/h-nd-117.html#_np=119_336',
hot: false,
new: false,
date: dayjs().subtract(2, 'day').format(formatDate),
},
{
title: '马建:物联网使农业信息化精准',
url: 'http://www.agricultureiot.com/h-nd-119.html#_np=119_336',
hot: false,
new: false,
date: dayjs().subtract(1, 'day').format(formatDate),
},
{
title: '互联网+农业基建全面提速 激活万亿市场',
url: 'http://www.agricultureiot.com/h-nd-291.html#_np=119_336',
hot: false,
new: false,
date: dayjs().subtract(3, 'day').format(formatDate),
},
{
title: '种植业物联网工程设计施工技术培训',
url: 'http://www.agricultureiot.com/h-nd-199.html#_np=104_316',
hot: false,
new: false,
date: dayjs().subtract(4, 'day').format(formatDate),
},
{
title: '农业物联网打造真实版“开心农场”',
url: 'http://www.agricultureiot.com/h-nd-49.html#_np=6_317',
hot: false,
new: false,
date: dayjs().subtract(5, 'day').format(formatDate),
},
{
title: '物联网温室大棚解决方案',
url: 'http://www.agricultureiot.com/h-nd-113.html#_np=6_317',
hot: false,
new: false,
date: dayjs().subtract(5, 'day').format(formatDate),
},
{
title: '智能农业监测应用方案',
url: 'http://www.agricultureiot.com/h-nd-34.html#_np=6_317',
hot: false,
new: false,
date: dayjs().subtract(5, 'day').format(formatDate),
},
{
title: '物联网产业发展现状及应用分析',
url: 'http://www.agricultureiot.com/h-nd-25.html#_np=6_317',
hot: false,
new: false,
date: dayjs().subtract(6, 'day').format(formatDate),
},
{
title: '基于物联网的“感知农业”保护系统',
url: 'http://www.agricultureiot.com/h-nd-12.html#_np=6_317',
hot: false,
new: false,
date: dayjs().subtract(6, 'day').format(formatDate),
},
{
title: '物联网让蔬菜大棚有了“新管家”',
url: 'http://www.agricultureiot.com/h-nd-162.html#_np=102_318',
hot: false,
new: false,
date: dayjs().subtract(7, 'day').format(formatDate),
},
{
title: '反季蔬菜如何施肥?',
url: 'http://www.agricultureiot.com/h-nd-128.html#_np=113_335',
hot: false,
new: false,
date: dayjs().subtract(10, 'day').format(formatDate),
},
];

71
src/routers/index.js

@ -2,69 +2,8 @@ import { createRouter, createWebHistory } from 'vue-router';
// 还有 createWebHashHistory 和 createMemoryHistory
export const routes = [
{
path: '/corrosion/overview',
name: 'overview',
meta: {
title: '设备概览',
icon: 'el-icon-data-board',
},
component: () => import('@/views/overview.vue'),
},
{
path: '/corrosion/devices',
name: 'devices',
meta: {
title: '设备管理',
icon: 'el-icon-box',
},
component: () => import('@/views/device-list.vue'),
},
{
path: '/corrosion/config',
name: 'config',
meta: {
title: '参数配置',
icon: 'el-icon-set-up',
},
component: () => import('@/views/config.vue'),
},
{
path: '/corrosion/data-realtime',
name: 'data-realtime',
meta: {
title: '实时数据',
icon: 'el-icon-odometer',
},
component: () => import('@/views/data-realtime.vue'),
},
{
path: '/corrosion/statistical',
name: 'statistical',
meta: {
title: '数据统计',
icon: 'el-icon-data-line',
},
component: () => import('@/views/statistical-report.vue'),
},
{
path: '/corrosion/history',
name: 'history',
meta: {
title: '历史数据',
icon: 'el-icon-document',
},
component: () => import('@/views/history.vue'),
},
{
path: '/corrosion/commands',
name: 'commands',
meta: {
title: '平台日志',
icon: 'el-icon-moon-night',
},
component: () => import('@/views/commands.vue'),
},
{ path: '/farm/center', name: 'center', component: () => import('views/Center.vue') },
{ path: '/farm/farm', name: 'farm', component: () => import('views/Farm.vue') },
];
const router = createRouter({
@ -72,11 +11,11 @@ const router = createRouter({
routes: [
{
path: '/',
redirect: '/corrosion/overview',
redirect: '/farm/center',
},
{
path: '/corrosion',
redirect: '/corrosion/overview',
path: '/farm',
redirect: '/farm/center',
},
].concat(routes),
});

163
src/store/device.js

@ -1,163 +0,0 @@
import { getDevices, getDevicesAll, getDevicesCount } from 'apis';
import dayjs from 'dayjs';
import { ElMessage } from 'element-plus';
const user = {
namespaced: true,
state: {
devices: [], // 站点列表 设备列表简版
devicesAll: { page: {}, data: [] }, // 设备列表完整版
currentDeviceId: '', // 当前正在编辑的设备deviceId
count: {
total: 0, // 总设备数量
online: 0, // 在线
offline: 0, // 离线
warning: 0, // 警告
fault: 0, // 故障
},
},
getters: {
// 当前正在编辑的设备的完整信息
current({ devicesAll, currentDeviceId }) {
try {
return devicesAll.data.find(device => device.deviceId === currentDeviceId);
} catch (error) {
return null;
}
},
},
mutations: {
/**
* 设置devices数据
* @param {*} state
* @param {array} devices
*/
setDevices(state, devices) {
if (devices && devices.length) {
state.devices = devices;
user.mutations.setCurrentDeviceId(state, devices[0].deviceId);
} else {
state.devices = [];
}
},
/**
* 设置设备统计数据信息
* @param {object} state
* @param {object} count // 服务端返回的对象
* @param {number} count.total // 总设备数量
* @param {number} count.online // 在线
* @param {number} count.offline // 离线
* @param {number} count.warning // 报警
* @param {number} count.fault // 故障
*/
setDevicesCount(state, count) {
state.count = count;
},
/**
* 设置devicesAll的数据
* @param {*} state
* @param {object} devices {page, data}
*/
setDevicesAll(state, devices) {
if (devices && devices.data) {
for (let i = 0; i < devices.data.length; i++) {
const device = devices.data[i];
if (device.installTime) {
devices.data[i].installTime = dayjs(new Date(+device.installTime)).format('YYYY-MM-DD HH:mm:ss');
}
if (device.runTime) {
devices.data[i].runTime = dayjs(new Date(+device.runTime)).format('YYYY-MM-DD HH:mm:ss');
}
}
}
state.devicesAll = devices;
},
/**
* 设置正则编辑的设备deviceId
* @param {*} state
* @param {string} deviceId
*/
setCurrentDeviceId(state, deviceId) {
state.currentDeviceId = deviceId;
},
/**
* 更新某个设备的信息
* @param {*} param0
* @param {object} newData 设备更新后的信息
*/
updateDevice({ devicesAll }, newData) {
for (let i = 0; i < devicesAll.data.length; i++) {
const item = devicesAll.data[i];
if (item && item.deviceId === newData.deviceId) {
newData.installTime = dayjs(new Date(+item.installTime)).format('YYYY-MM-DD HH:mm:ss');
newData.runTime = dayjs(new Date(+item.runTime)).format('YYYY-MM-DD HH:mm:ss');
devicesAll.data[i] = newData;
break;
}
}
},
/**
* 删除设备
* @param {*} param0
* @param {string} deviceId 设备id
*/
deleteDevice({ devicesAll }, deviceId) {
if (!devicesAll || !devicesAll.data || !devicesAll.data.length) return;
devicesAll.data = devicesAll.data.filter(item => item.deviceId !== deviceId);
},
/**
* 新增设备后将设备数据追加到最前边
* @param {array} devicesAll
* @param {object} device 后台返回的新增设备信息
*/
unshiftDevice({ devicesAll }, device) {
devicesAll.data.unshift(device);
},
},
actions: {
// 获取设备列表(站点列表)
async getDevices({ commit }) {
try {
const data = await getDevices();
commit('setDevices', data || []);
return data;
} catch (error) {
throw new Error(error);
}
},
// 查询设备数量信息
async getDevicesCount({ commit }) {
try {
const data = await getDevicesCount();
commit('setDevicesCount', data);
} catch (error) {
ElMessage.error(error.message || '获取设备统计信息失败');
throw new Error(error);
}
},
// 获取设备列表(站点列表) 完整信息
async getDevicesAll({ commit }, params) {
try {
const data = await getDevicesAll(params);
commit('setDevicesAll', data || null);
return data;
} catch (error) {
throw new Error(error);
}
},
},
};
export default user;

15
src/store/index.js

@ -1,15 +1,16 @@
import { createStore } from 'vuex';
import device from './device';
import statistics from './statistics';
import user from './user';
export default createStore({
modules: { user, device, statistics },
state: { menu: { show: true, collapse: false } },
state: { modal: false },
getters: {},
mutations: {
toggleCollapse(state) {
state.menu.collapse = !state.menu.collapse;
/**
* 设置modal是否显示
* @param state
* @param {Boolean} display
*/
setModal(state, display) {
state.modal = display;
},
},
actions: {},

78
src/store/statistics.js

@ -1,78 +0,0 @@
import { getDatas, getMonthsDate } from 'apis/index';
export default {
namespaced: true,
state: {
electricData: null,
corrosionData: null,
moistTimeData: null,
realtimeData: null,
},
getters: {},
mutations: {
/**
* 设置积分电量数据
* @param {*} state
* @param {array} data
*/
setElectricData(state, data) {
state.electricData = data;
},
/**
* 设置月累计腐蚀的数据
* @param {*} state
* @param {*} data
*/
setCorrosionData(state, data) {
state.corrosionData = data;
},
/**
* 设置月累计湿润时间图的数据
* @param {*} state
* @param {*} data
*/
setMoistTimeData(state, data) {
state.moistTimeData = data;
},
/**
* 设置实时数据统计的数据
* @param {*} state
* @param {*} data
*/
setRealtimeData(state, data) {
state.realtimeData = data;
},
},
actions: {
// 获取积分电量数据
async getMonthsDate({ commit }, params) {
try {
const data = await getMonthsDate(params);
commit('setElectricData', data.powers || null);
commit('setCorrosionData', data.corrosions || null);
commit('setMoistTimeData', data.humids || null);
return data;
} catch (error) {
throw new Error(error);
}
},
// 获取实时数据统计数据
async getRealtimeData({ commit }, params) {
try {
const data = await getDatas(params);
commit('setRealtimeData', data || null);
return data;
} catch (error) {
throw new Error(error);
}
},
},
};

53
src/store/user.js

@ -1,53 +0,0 @@
import { getToken } from 'apis/index';
export default {
namespaced: true,
state: { user: null },
getters: {
token({ user }) {
if (!user) return null;
return user.token;
},
userId({ user }) {
if (!user) return null;
return user.id;
},
},
mutations: {
/**
* 设置state.user
* @param {*} state
* @param {object|null} user 用户信息
*/
setUser(state, user) {
state.user = user;
if (user) {
localStorage.setItem('token', user.token);
localStorage.setItem('user', JSON.stringify(user));
} else {
localStorage.removeItem('token');
localStorage.removeItem('user');
}
},
},
actions: {
/**
* 根据userId获取token级user信息
* @param {*} param0
* @param {string} userId 用户id
*/
async getTokenByUserId({ commit }, userId) {
try {
const data = await getToken(userId);
commit('setUser', data || null);
return data;
} catch (error) {
throw new Error(error);
}
},
},
};

30
src/utils/axios.js

@ -1,21 +1,10 @@
import Axios from 'axios';
import { ElMessage } from 'element-plus';
import store from 'store';
const baseUrl = '/gateway';
const instance = Axios.create({
baseUrl,
timeout: 20000,
});
const instance = Axios.create({ timeout: 20000 });
// request
instance.interceptors.request.use(
config => {
const token = store.getters['user/token'] || localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
@ -26,24 +15,9 @@ instance.interceptors.request.use(
// response
instance.interceptors.response.use(
response => {
if (response.status !== 200 || !response.data) {
return Promise.reject(response.statusText);
}
const { code, data, msg } = response.data;
if (code === 200) {
return data;
}
return Promise.reject(msg);
return response.data;
},
error => {
if (error.response && error.response.data) {
const code = error.response.status;
const msg = error.response.data.message;
ElMessage.error(`Code: ${code}, Message: ${msg}`);
console.error(`[Axios Error]`, error.response);
} else {
// ElMessage.error(`${error}`);
}
return Promise.reject(error);
},
);

91
src/utils/statistical.js → src/utils/complexChart.js

@ -1,47 +1,36 @@
/* eslint-disable max-len,object-curly-newline */
import dayjs from 'dayjs';
import max from 'lodash/max';
import { colors, defaultSelectedLegend, generateDefaultSeries, legendData, yAxisData } from '@/config/chart';
import _ from 'lodash';
import { colors, generateDefaultSeries, itemColor, legendData, yAxisData } from './complexConfig';
/**
* 生成chart所需参数
* @param {Object[]} data 服务端返回数据
* @param {string} data[].time 时间 ms
* @param {string} data[].so2 SO2
* @param {string} data[].saltR 盐阻
* @param {string} data[].saltT 盐温
* @param {string} data[].environmentTemperature 环温
* @param {string} data[].environmentHumidity 环湿
* @param {string} data[].corrosion1
* @param {string} data[].corrosion2
* @param {string} data[].corrosion3
* @param {string} data[].corrosion4
* @returns {{environmentTemperature: *[], corrosionXIN: *[], corrosionGANG: *[], corrosionTONG: *[], so2: *[], corrosionLV: *[], time: *[], saltT: *[], saltR: *[], environmentHumidity: *[]}}
*/
function generateParams(data) {
const result = {
time: [],
so2: [],
saltR: [],
saltT: [],
environmentTemperature: [],
environmentHumidity: [],
corrosionXIN: [],
corrosionTONG: [],
corrosionLV: [],
corrosionGANG: [],
roomT: [],
roomH: [],
outT: [],
outH: [],
soilT: [],
soilH: [],
windSpeed: [],
co2: [],
light: [],
};
data.forEach(item => {
result.time.push(dayjs(new Date(+item.time)).format('YY/MM/DD HH:mm'));
result.so2.push(+item.so2);
result.saltR.push(+item.saltR);
result.saltT.push(+item.saltT);
result.environmentTemperature.push(+item.environmentTemperature);
result.environmentHumidity.push(+item.environmentHumidity);
result.corrosionXIN.push(+item.corrosion1);
result.corrosionTONG.push(+item.corrosion2);
result.corrosionLV.push(+item.corrosion3);
result.corrosionGANG.push(+item.corrosion4);
result.time.push(item.time);
result.roomT.push(+item.roomT);
result.roomH.push(+item.roomH);
result.outT.push(+item.outT);
result.outH.push(+item.outH);
result.soilT.push(+item.soilT);
result.soilH.push(+item.soilH);
result.windSpeed.push(+item.windSpeed);
result.co2.push(+item.co2);
result.light.push(+item.light);
});
return result;
}
@ -74,10 +63,10 @@ export function generateYAxis(selectedLegend) {
item.show = computeYAxisShow(item.name, selectedLegend);
if (item.show) {
if (item.position === 'left') {
item.offset = 100 * leftIndex;
item.offset = 40 * leftIndex;
leftIndex += 1;
} else {
item.offset = 80 * rightIndex;
item.offset = 40 * rightIndex;
rightIndex += 1;
}
}
@ -97,15 +86,16 @@ function generateSeries(data, yAxis) {
const hideArr = seriesArr.filter(item => !yAxis.find(y => y.name === item.name));
const result = [...showArr, ...hideArr];
result.forEach(item => {
if (item.name.includes('电流')) {
item.itemStyle = { color: itemColor[item.name] };
if (item.name.includes('温度')) {
item.yAxisIndex = 0;
} else if (item.name.includes('温度')) {
item.yAxisIndex = 1;
} else if (item.name.includes('湿度')) {
item.yAxisIndex = 1;
} else if (item.name.includes('风速')) {
item.yAxisIndex = 2;
} else if (item.name.includes('SO2')) {
} else if (item.name.includes('CO2')) {
item.yAxisIndex = 3;
} else if (item.name.includes('阻抗')) {
} else if (item.name.includes('光照')) {
item.yAxisIndex = 4;
}
});
@ -130,25 +120,26 @@ function generateGrid(yAxis) {
}
});
return {
left: max(left) + 100,
right: max(right) + 80,
left: _.max(left) + 40,
right: _.max(right) + 40,
bottom: 20,
};
}
/**
* 生成chart参数
* @param {Object[]} rawData 返回段返回的data数据
* @param {Object[]} yAxis
* @returns {{yAxis: [{axisLabel: {formatter: string}, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string},{axisLabel: {formatter: string}, offset: number, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string},{axisLabel: {formatter: string}, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string},{axisLabel: {formatter: string}, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string},{axisLabel: {formatter: string}, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string},null,null,null], xAxis: [{data: *[], axisTick: {alignWithLabel: boolean}, type: string}], color: string[], grid: {right: string}, legend: {data: string[]}, series: [{data: *[], name: string, type: string},{data: *[], name: string, type: string, yAxisIndex: number},{data: *[], name: string, type: string, yAxisIndex: number},{data: *[], name: string, type: string, yAxisIndex: number},{data: *[], name: string, type: string, yAxisIndex: number},null,null,null], tooltip: {axisPointer: {type: string}, trigger: string}, toolbox: {feature: {saveAsImage: {show: boolean}, restore: {show: boolean}, dataView: {show: boolean, readOnly: boolean}}}}}
* @param {Object} selected 选中的legend
* @returns {{yAxis: ({axisLabel: {formatter: string}, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string}|{axisLabel: {formatter: string}, offset: number, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string})[], xAxis: [{data: *[], axisTick: {alignWithLabel: boolean}, type: string}], color: [string,string,string,string,string,string,string,string,string], grid: {left: number, right: number}, legend: {data: [string,string,string,string,string,string,string,string,string], type: string, selected}, series: (({data: ([]|number|string|*), name: string, type: string}|{data: ([]|number|BufferSource|string|*), name: string, type: string, yAxisIndex: number}|{data: ([]|string|*), name: string, type: string, yAxisIndex: number}|{data: [], name: string, type: string, yAxisIndex: number})[]|*[]), tooltip: {axisPointer: {type: string, snap: boolean}, trigger: string}, dataZoom: [{type: string},{type: string}]}}
*/
// eslint-disable-next-line import/prefer-default-export
export function generateChartOption(rawData, selected = defaultSelectedLegend) {
export function generateChartOption(rawData, selected) {
const data = generateParams(rawData);
const yAxis = generateYAxis(selected);
const series = generateSeries(data, yAxis);
const grid = generateGrid(yAxis);
const option = {
return {
color: colors,
darkMode: true,
tooltip: {
trigger: 'axis',
axisPointer: {
@ -158,19 +149,23 @@ export function generateChartOption(rawData, selected = defaultSelectedLegend) {
},
grid,
legend: {
type: 'scroll',
textStyle: { color: '#fff' },
pageTextStyle: { color: '#fff' },
pageIconColor: '#cacaca',
pageIconInactiveColor: '#999',
selected,
data: legendData,
},
dataZoom: [{ type: 'inside' }, { type: 'slider' }],
xAxis: [
{
type: 'category',
axisTick: { alignWithLabel: true },
data: data.time,
axisLabel: { color: '#fff' },
},
],
yAxis,
series,
};
return option;
}

139
src/config/chart.js → src/utils/complexConfig.js

@ -1,50 +1,85 @@
/* eslint-disable max-len */
export const colors = ['#5470C6', '#91CC75', '#EE6666', '#5470C6', '#91CC75', '#EE6666', '#5470C6', '#91CC75', '#5470C6'];
export const colors = ['#FCD34D', '#FDBA74', '#F9A8D4', '#FDA4AF', '#F0ABFC', '#67E8F9', '#FCD34D', '#93C5FD', '#86EFAC'];
export const itemColor = {
'室内温度(℃)': colors[5],
'室外温度(℃)': colors[6],
'土壤温度(℃)': colors[7],
'室内湿度(RH%)': colors[8],
'室外湿度(RH%)': colors[0],
'土壤湿度(RH%)': colors[1],
'风速(m/s)': colors[2],
'CO2(%)': colors[3],
'光照(klux)': colors[4],
};
export const legendData = [
'SO2(ppb)',
'盐分阻抗(Ω)',
'盐分温度(℃)',
'环境温度(℃)',
'环境湿度(RH%)',
'锌腐蚀电流(nA)',
'铜腐蚀电流(nA)',
'铝腐蚀电流(nA)',
'钢腐蚀电流(nA)',
{
name: '室外湿度(RH%)',
itemStyle: { color: colors[0] },
listStyle: { color: colors[0] },
},
{
name: '土壤湿度(RH%)',
itemStyle: { color: colors[1] },
listStyle: { color: colors[1] },
},
{
name: '风速(m/s)',
itemStyle: { color: colors[2] },
listStyle: { color: colors[2] },
},
{
name: 'CO2(%)',
itemStyle: { color: colors[3] },
listStyle: { color: colors[3] },
},
{
name: '光照(klux)',
itemStyle: { color: colors[4] },
listStyle: { color: colors[4] },
},
{
name: '室内温度(℃)',
itemStyle: { color: colors[5] },
listStyle: { color: colors[5] },
},
{
name: '室外温度(℃)',
itemStyle: { color: colors[6] },
listStyle: { color: colors[6] },
},
{
name: '土壤温度(℃)',
itemStyle: { color: colors[7] },
listStyle: { color: colors[7] },
},
{
name: '室内湿度(RH%)',
itemStyle: { color: colors[8] },
listStyle: { color: colors[8] },
},
];
// 默认legend select
export const defaultSelectedLegend = {
'钢腐蚀电流(nA)': true,
'铜腐蚀电流(nA)': true,
'铝腐蚀电流(nA)': true,
'锌腐蚀电流(nA)': true,
'SO2(ppb)': false,
'盐分阻抗(Ω)': false,
'盐分温度(℃)': false,
'环境温度(℃)': false,
'环境湿度(RH%)': false,
};
// y轴定义
export const yAxisData = [
{
type: 'value',
name: '腐蚀电流(nA)',
name: '温度',
offset: 0,
position: 'left',
axisLine: {
show: true,
lineStyle: { color: colors[0] },
lineStyle: { color: colors[7] },
},
axisLabel: { formatter: '{value}' },
axisPointer: { show: false },
},
{
type: 'value',
name: '温度(℃)',
offset: 0,
position: 'right',
name: '湿度',
offset: 70,
position: 'left',
axisLine: {
show: true,
lineStyle: { color: colors[1] },
@ -54,8 +89,8 @@ export const yAxisData = [
},
{
type: 'value',
name: '湿度(RH%)',
offset: 70,
name: '风速',
offset: 0,
position: 'right',
axisLine: {
show: true,
@ -66,10 +101,10 @@ export const yAxisData = [
},
{
type: 'value',
name: 'SO2(ppb)',
name: 'CO2',
position: 'right',
show: false,
offset: 150,
offset: 80,
axisLine: {
show: true,
lineStyle: { color: colors[3] },
@ -79,10 +114,10 @@ export const yAxisData = [
},
{
type: 'value',
name: '盐分阻抗(Ω)',
name: '光照',
show: false,
position: 'right',
offset: 220,
offset: 160,
axisLine: {
show: true,
lineStyle: { color: colors[4] },
@ -100,58 +135,58 @@ export const yAxisData = [
export function generateDefaultSeries(data) {
return [
{
name: '锌腐蚀电流(nA)',
name: '室内温度(℃)',
type: 'line',
yAxisIndex: 0,
data: data.corrosionXIN,
data: data.roomT,
},
{
name: '铜腐蚀电流(nA)',
name: '室外温度(℃)',
type: 'line',
yAxisIndex: 0,
data: data.corrosionTONG,
data: data.outT,
},
{
name: '铝腐蚀电流(nA)',
name: '土壤温度(℃)',
type: 'line',
yAxisIndex: 0,
data: data.corrosionLV,
data: data.soilT,
},
{
name: '钢腐蚀电流(nA)',
name: '室内湿度(RH%)',
type: 'line',
yAxisIndex: 0,
data: data.corrosionGANG,
data: data.roomH,
},
{
name: '环境温度(℃)',
name: 'CO2(%)',
type: 'line',
yAxisIndex: 1,
data: data.environmentTemperature,
data: data.co2,
},
{
name: '盐分温度(℃)',
name: '风速(m/s)',
type: 'line',
yAxisIndex: 1,
data: data.saltT,
data: data.windSpeed,
},
{
name: '环境湿度(RH%)',
name: '光照(klux)',
type: 'line',
yAxisIndex: 2,
data: data.environmentHumidity,
data: data.light,
},
{
name: 'SO2(ppb)',
name: '室外湿度(RH%)',
type: 'line',
yAxisIndex: 3,
data: data.so2,
data: data.outH,
},
{
name: '盐分阻抗(Ω)',
name: '土壤湿度(RH%)',
type: 'line',
yAxisIndex: 4,
data: data.saltR,
data: data.soilH,
},
];
}

83
src/utils/map.js

@ -0,0 +1,83 @@
/* eslint-disable */
import ShanXiMap from 'config/map/shanxi';
import { values as ShanXiValues } from 'config/map/values';
/**
* 获取城市的value
* @param {string} cityName 城市名称
* @returns {string|*}
*/
function getValueByCity(cityName) {
try {
const city = ShanXiValues.find(item => item.name === cityName);
return city.value;
} catch (error) {
console.log('getValueByCity value: ', error);
return '';
}
}
// 初始化城市value及height等值
function initValues() {
const regions = ShanXiMap.features.map(feature => {
const value = getValueByCity(feature.properties.name);
return {
name: feature.properties.name,
value,
height: value,
label: {
show: true,
textStyle: { color: '#000' },
},
};
});
return regions;
}
// eslint-disable-next-line import/prefer-default-export
export function initOptions() {
return {
backgroundColor: 'transparent',
visualMap: {
show: true,
min: 0,
max: 12,
inRange: {
color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026'],
},
},
series: [
{
type: 'map3D',
map: 'shanxi',
shading: 'realistic',
realisticMaterial: {
roughness: 0.6,
textureTiling: 20,
},
environment: new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{
offset: 0,
color: '#0c2771', // 天空颜色
},
{
offset: 0.7,
color: '#026fab', // 地面颜色
},
{
offset: 1,
color: '#01285c', // 地面颜色
},
],
false,
),
data: initValues(),
},
],
};
}

128
src/utils/overview.js

@ -1,128 +0,0 @@
import { STATUS_COLOR } from '@/config/config';
/**
* 生成设备概览 数量数据
* @param {object} count
* @param {number} count.online 在线数量
* @param {number} count.offline 离线数量
* @param {number} count.fault 故障数量
* @param {number} count.warning 报警数量
* @param {number} count.total 总设备数
* @returns {object} {}
*/
// eslint-disable-next-line import/prefer-default-export
export function generateChartOption(count) {
return {
tooltip: { trigger: 'item' },
title: {
text: '设备总数',
top: '45%',
left: '50%',
textAlign: 'center',
textVerticalAlign: 'middle',
textStyle: { fontSize: 22 },
subtext: count.total,
subtextStyle: { fontSize: 18 },
},
series: [
{
name: '设备统计',
type: 'pie',
radius: ['70%', '95%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: true,
position: 'inside',
fontSize: 17,
color: '#323232',
// formatter: '{b}\n{c}',
lineHeight: 40,
},
emphasis: {
label: {
show: true,
fontSize: 19,
fontWeight: 'bold',
},
},
labelLine: { show: false },
data: [
{
value: count.online,
name: STATUS_COLOR.ONLINE.text,
itemStyle: { color: STATUS_COLOR.ONLINE.color },
label: { show: count.online },
},
{
value: count.offline,
name: STATUS_COLOR.OFFLINE.text,
itemStyle: { color: STATUS_COLOR.OFFLINE.color },
label: { show: count.offline },
},
],
},
{
name: '设备统计',
type: 'pie',
radius: ['35%', '60%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: true,
position: 'inside',
fontSize: 14,
color: '#323232',
// formatter: '{b}\n{c}',
lineHeight: 40,
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold',
},
},
labelLine: { show: false },
data: [
{
value: count.fault,
name: STATUS_COLOR.FAULT.text,
itemStyle: { color: STATUS_COLOR.FAULT.color },
label: { show: count.fault },
},
{
value: count.warning,
name: STATUS_COLOR.WARNING.text,
itemStyle: { color: STATUS_COLOR.WARNING.color },
label: { show: count.warning },
},
{
value: count.normal,
name: STATUS_COLOR.NORMAL.text,
itemStyle: { color: STATUS_COLOR.NORMAL.color },
label: { show: count.normal },
},
{
value: count.offline,
name: STATUS_COLOR.OFFLINE.text,
itemStyle: {
color: STATUS_COLOR.OFFLINE.color,
opacity: 0,
},
label: { show: count.offline },
},
],
},
],
};
}

16
src/utils/time.js

@ -1,16 +1,6 @@
import dayjs from 'dayjs';
// require('dayjs/esm/locale/zh-cn');
/**
* 格式化时间
* @param {string | number} time 时间戳字符串或数字
* @param {string} formatStyle
* @returns
*/
export function formatMsTime(time, formatStyle = 'YYYY-MM-DD HH:mm:ss') {
return dayjs(new Date(+time)).format(formatStyle);
}
dayjs.locale('zh-cn');
// 图表时间轴的时间格式化
export function formatChartTime(time) {
return formatMsTime(time, 'MM/DD HH:mm');
}
export default dayjs;

13
src/views/Center.vue

@ -0,0 +1,13 @@
<template>
<PageHeader></PageHeader>
<PageLeft></PageLeft>
<PageFooter></PageFooter>
</template>
<script setup>
import PageHeader from 'components/Center/Header.vue';
import PageFooter from 'components/Common/Footer.vue';
import PageLeft from 'components/Common/Left.vue';
</script>
<style scoped></style>

7
src/views/Farm.vue

@ -0,0 +1,7 @@
<template>farm</template>
<script>
export default { name: 'Farm' };
</script>
<style scoped></style>

182
src/views/commands.vue

@ -1,182 +0,0 @@
<template>
<SearchCommands @search="onSearch" />
<template v-if="data">
<el-table :data="data" :max-height="contentHeight" border stripe style="width: 100%">
<el-table-column align="center" fixed label="设备ID" min-width="100" prop="deviceId" />
<el-table-column align="center" label="时间" min-width="200">
<template #default="scope">
{{ dayjs(new Date(+scope.row.createdAt)).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column align="center" label="传输方向" min-width="80">
<template #default="scope">
{{ log.DIRECTION[scope.row.dir] }}
</template>
</el-table-column>
<el-table-column align="center" label="传输启动" min-width="80">
<template #default="scope">
{{ log.PRM[scope.row.prm] }}
</template>
</el-table-column>
<el-table-column align="center" label="安全模式" min-width="80">
<template #default="scope">
{{ log.SER[scope.row.ser] }}
</template>
</el-table-column>
<el-table-column align="center" label="流控" min-width="80">
<template #default="scope">
{{ log.DFC[scope.row.dfc] }}
</template>
</el-table-column>
<el-table-column align="center" label="功能码" min-width="180">
<template #default="scope">
{{ log.CODE[scope.row.code].text }}
</template>
</el-table-column>
<el-table-column align="center" label="包序号" min-width="80" prop="serialNo" />
<el-table-column align="center" label="类型标识" min-width="80">
<template #default="scope">
<span v-if="scope.row.asduType && log.ASDU_TYPE[scope.row.asduType]">
{{ log.ASDU_TYPE[scope.row.asduType].text }}
</span>
</template>
</el-table-column>
<el-table-column align="center" label="信息体顺序" min-width="100">
<template #default="scope">
{{ log.INFO_ORDER[scope.row.infoOrder] }}
</template>
</el-table-column>
<el-table-column align="center" label="信息体个数" min-width="100" prop="infoNum" />
<el-table-column align="center" label="传输原因" min-width="180">
<template #default="scope">
{{ log.REASON[scope.row.reason].text }}
</template>
</el-table-column>
<el-table-column align="center" label="协议内容" min-width="80">
<template #default="scope">
<el-button type="text" @click="openContent(scope.row.content)">查看</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="page.page"
:default-page-size="50"
:page-count="page.count"
:page-size="page.size"
:page-sizes="[1, 10, 20, 50, 100]"
:total="page.total"
background
class="my-3 float-right"
layout="total, sizes, prev, pager, next, jumper"
@size-change="onSizeChange"
@current-change="onCurrentPageChange"
></el-pagination>
</template>
<!-- 弹出框-->
<el-dialog v-model="showContent" title="协议内容">
<code class="font-mono">{{ contents }}</code>
</el-dialog>
</template>
<script setup>
import { getLog } from 'apis';
import SearchCommands from 'components/commands/search-commands.vue';
import dayjs from 'dayjs';
import { ElMessage } from 'element-plus';
import { computed, onMounted, ref } from 'vue';
import { useStore } from 'vuex';
import * as log from '@/config/log';
const data = ref([]);
const page = ref({
page: 1,
size: 50,
count: 0,
total: 0,
});
const showContent = ref(false); //
const contents = ref(''); //
const contentHeight = ref(700);
const store = useStore();
const currentDeviceId = computed(() => store.state.device.currentDeviceId);
//
onMounted(() => {
const winHeight = document.documentElement.clientHeight;
contentHeight.value = winHeight - 250;
onSearch();
});
/**
* 查询
* @param {object} options
* @returns {Promise<void>}
*/
async function onSearch(
options = {
...page.value,
sort: [
{
col: 'createdAt',
order: 'DESC',
},
],
deviceId: currentDeviceId.value,
},
) {
try {
const resData = await getLog(options);
page.value = resData.page;
data.value = resData.data;
} catch (error) {
ElMessage.error('查询失败');
throw new Error(error);
}
}
/**
* 打开协议内容的弹出框 并记录值
* @param {string} content 协议内容
*/
function openContent(content) {
showContent.value = true;
contents.value = content;
}
//
const onCurrentPageChange = pageEvent => {
const options = {
page: pageEvent,
size: page.value.size || 50,
sort: [
{
col: 'createdAt',
order: 'DESC',
},
],
deviceId: currentDeviceId.value,
};
onSearch(options);
};
//
const onSizeChange = size => {
const options = {
page: 1,
size,
sort: [
{
col: 'createdAt',
order: 'DESC',
},
],
deviceId: currentDeviceId.value,
};
onSearch(options);
};
</script>

32
src/views/config.vue

@ -1,32 +0,0 @@
<template>
<!-- 设置站点选择 设备下发状态 -->
<DeviceSelectAndStatus :status="status" />
<el-tabs v-model="activeName">
<el-tab-pane :lazy="true" label="远动参数" name="function">
<FunctionConfigPending v-if="activeName === 'function'" :active-name="activeName" />
</el-tab-pane>
<el-tab-pane :lazy="true" label="网络参数" name="network">
<NetworkConfigPending v-if="activeName === 'network'" :active-name="activeName" />
</el-tab-pane>
<el-tab-pane :lazy="true" label="远动参数配置记录" name="function-log">
<ConfigLogFunction v-if="activeName === 'function-log'" :active-name="activeName" />
</el-tab-pane>
<el-tab-pane :lazy="true" label="网络参数配置记录" name="network-log">
<ConfigLogNetwork v-if="activeName === 'network-log'" :active-name="activeName" />
</el-tab-pane>
</el-tabs>
</template>
<script setup>
import { ref } from 'vue';
import DeviceSelectAndStatus from 'components/config/device-select-and-status.vue';
import FunctionConfigPending from 'components/config/function-config-pending.vue';
import NetworkConfigPending from 'components/config/network-config-pending.vue';
import ConfigLogFunction from 'components/config/config-log-function.vue';
import ConfigLogNetwork from 'components/config/config-log-network.vue';
const status = ref('');
const activeName = ref('function');
</script>

185
src/views/data-realtime.vue

@ -1,185 +0,0 @@
<template>
<SearchBar :show-refresh="true" @refresh="onRefresh" @search="getData" />
<template v-if="data">
<el-table :data="data" :max-height="contentHeight" border stripe style="width: 100%">
<el-table-column align="center" fixed label="设备ID" min-width="80" prop="deviceId" />
<el-table-column align="center" label="采集时间" min-width="170">
<template #default="scope">
{{ formatTime(+scope.row.time) }}
</template>
</el-table-column>
<el-table-column align="center" label="后台接收时间" min-width="170">
<template #default="scope">
{{ formatTime(+scope.row.createdAt) }}
</template>
</el-table-column>
<el-table-column align="center" label="锌腐蚀电流(nA)" min-width="130" prop="corrosion1" />
<el-table-column align="center" label="铜腐蚀电流(nA)" min-width="130" prop="corrosion2" />
<el-table-column align="center" label="铝腐蚀电流(nA)" min-width="130" prop="corrosion3" />
<el-table-column align="center" label="钢腐蚀电流(nA)" min-width="130" prop="corrosion4" />
<el-table-column align="center" label="环境温度(℃)" min-width="110" prop="environmentTemperature" />
<el-table-column align="center" label="环境湿度(RH%)" min-width="130" prop="environmentHumidity" />
<el-table-column align="center" label="SO2(ppb)" min-width="90" prop="so2" />
<!-- TODO:-->
<el-table-column align="center" label="盐分温度(℃)" min-width="110" prop="saltT" />
<el-table-column align="center" label="盐分阻抗(Ω)" min-width="110" prop="saltR" />
<el-table-column align="center" label="机箱温度(℃)" min-width="110" prop="deviceTemperature" />
<el-table-column align="center" label="机箱湿度(RH%)" min-width="130" prop="deviceHumidity" />
<el-table-column align="center" label="太阳能板电压(V)" min-width="140" prop="solarVoltage" />
<el-table-column align="center" label="蓄电池电压(V)" min-width="120" prop="batteryVoltage" />
<el-table-column align="center" label="电压百分比" min-width="94" prop="batteryVoltagePercentage" />
<el-table-column align="center" label="剩余电量(mAH)" min-width="140" prop="batteryVoltageRemain" />
<el-table-column align="center" label="消耗电量(mAH)" min-width="140" prop="batteryLoss" />
<el-table-column align="center" label="ICCID" min-width="190" prop="iccid" />
<el-table-column align="center" label="IMEI" min-width="150" prop="imei" />
<el-table-column align="center" label="信号强度" min-width="80" prop="signal" />
<el-table-column align="center" label="基站编号" min-width="130" prop="stationNo" />
<el-table-column align="center" label="硬件版本" min-width="80" prop="hardwareVersion" />
<el-table-column align="center" label="软件版本" min-width="80" prop="softwareVersion" />
</el-table>
<el-pagination
:current-page="page.page"
:default-page-size="50"
:page-count="page.count"
:page-size="page.size"
:page-sizes="[1, 10, 20, 50, 100]"
:total="page.total"
background
class="my-3 float-right"
layout="total, sizes, prev, pager, next, jumper"
@current-change="onCurrentPageChange"
@size-change="onSizeChange"
@prev-click="onPrev"
@next-click="onNext"
></el-pagination>
</template>
</template>
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useStore } from 'vuex';
import SearchBar from 'components/realtime/search-bar.vue';
import { getDatas } from 'apis';
import { ElMessage } from 'element-plus';
import dayjs from 'dayjs';
import { REALTIME_DATA_INTERVAL } from '@/config/config';
const page = ref({
page: 1,
size: 50,
});
let timer = null;
let apiTimer = null;
const store = useStore();
const token = computed(() => store.getters['user/token']);
const currentDeviceId = computed(() => store.state.device.currentDeviceId); // id
const contentHeight = ref(600);
const loadingSearch = ref(false);
const data = ref(null);
//
const getData = async () => {
try {
if (token && token.value) {
if (!currentDeviceId.value) {
return ElMessage.error('请选择站点');
}
const date = [dayjs().startOf('day').format('x'), dayjs().endOf('day').format('x')];
const params = {
deviceId: currentDeviceId.value,
dateRange: date,
paging: true,
page: page.value.page,
size: page.value.size,
sort: [
{
col: 'createdAt',
order: 'DESC',
},
],
};
loadingSearch.value = true;
const resData = await getDatas(params);
loadingSearch.value = false;
data.value = resData.data;
page.value = resData.page;
timer && clearTimeout(timer);
timer = null;
//
if (!apiTimer) {
apiTimer = setInterval(() => {
getData();
}, REALTIME_DATA_INTERVAL);
}
} else {
timer = setTimeout(() => {
getData();
}, 20);
}
} catch (error) {
ElMessage.error(error.message || '获取数据失败');
console.log('error: ', error);
}
};
//
onMounted(() => {
const winHeight = document.documentElement.clientHeight;
contentHeight.value = winHeight - 170;
currentDeviceId.value && getData();
});
onUnmounted(() => {
apiTimer && clearInterval(apiTimer);
apiTimer = null;
});
/**
* 当前 码变化
* 更新page 重新 取数据
* @param {number} e 的页码
*/
const onCurrentPageChange = e => {
page.value.page = e;
getData();
};
const onSizeChange = e => {
page.value.size = e;
getData();
};
//
const onNext = e => {
page.value.page = e + 1;
getData();
};
//
const onPrev = e => {
page.value.page = e - 1;
getData();
};
/**
* 格式化时间
* @param {number} time 时间戳
* @returns {string}
*/
function formatTime(time) {
return dayjs(new Date(time)).format('YYYY-MM-DD HH:mm:ss');
}
//
function onRefresh() {
apiTimer && clearInterval(apiTimer);
apiTimer = null;
getData();
}
</script>

192
src/views/device-edit.vue

@ -1,192 +0,0 @@
<template>
<el-form ref="deviceEdit" :model="data" :rules="deviceRules" label-position="top">
<el-row :gutter="20">
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="设备ID" prop="deviceId">
<el-input v-model="data.deviceId" disabled></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="站点名称" prop="address">
<el-input v-model="data.address"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="产品类型" prop="type">
<!-- <el-input v-model="data.securityMode"></el-input> -->
<el-radio v-model="data.type" label="IACD">大气腐蚀</el-radio>
<el-radio v-model="data.type" label="OTHER">其他</el-radio>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="设备完整ID" prop="deviceFullId">
<el-input v-model="data.deviceFullId"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="地区" prop="area">
<el-input v-model="data.area"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="联系人" prop="contact">
<el-input v-model="data.contact"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="电话" prop="phone">
<el-input v-model="data.phone"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="经度" prop="lon">
<el-input v-model="data.lon"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="纬度" prop="lat">
<el-input v-model="data.lat"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="负责人" prop="head">
<el-input v-model="data.head"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="安装位置" prop="installLocation">
<el-input v-model="data.installLocation"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="安装时间" prop="installTime">
<el-date-picker v-model="data.installTime" placeholder="安装时间" style="width: 100%" type="datetime"></el-date-picker>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="正式运行时间" prop="runTime">
<el-date-picker v-model="data.runTime" placeholder="正式运行时间" style="width: 100%" type="datetime"></el-date-picker>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="链路地址" prop="linkAddress">
<el-input v-model="data.linkAddress"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="探头编号" prop="probNo">
<el-input v-model="data.probNo"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="试样" prop="simple">
<el-input v-model="data.simple"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="sim卡1" prop="sim1">
<el-input v-model="data.sim1"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="设备朝向" prop="deviceDirection">
<el-input v-model="data.deviceDirection"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="协议版本" prop="protocolVersion">
<el-input v-model="data.protocolVersion"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="与主站后台联调情况" prop="joint">
<el-input v-model="data.joint" type="textarea"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :md="12" :span="12" :xl="6" :xs="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="data.remark" type="textarea"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="维修记录" prop="operationRecord">
<el-input v-model="data.operationRecord" type="textarea"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" class="mt-5 pl-2">
<el-form-item>
<el-button type="primary" @click="onSubmit">提交</el-button>
<el-button @click="$emit('cancel')">取消</el-button>
</el-form-item>
</el-row>
</el-form>
</template>
<script setup>
import { computed, defineEmits, reactive, ref, watch } from 'vue';
import { useStore } from 'vuex';
import { updateDevice } from 'apis';
import { ElMessage } from 'element-plus';
import { deviceData, deviceRules } from '@/config/config';
let data = reactive(deviceData);
const deviceEdit = ref(null); // form
const store = useStore();
const device = computed(() => store.getters['device/current']);
const currentDeviceId = computed(() => store.state.device.currentDeviceId);
const emit = defineEmits(['cancel']);
watch(
() => device.value,
newValue => {
data = newValue;
},
{
immediate: true,
deep: true,
},
);
//
const onSubmit = () => {
deviceEdit.value.validate(async valid => {
if (valid) {
if (data.installTime) {
data.installTime = new Date(data.installTime).getTime();
}
if (data.runTime) {
data.runTime = new Date(data.runTime).getTime();
}
try {
await updateDevice(currentDeviceId.value, data);
ElMessage.success('更新成功');
emit('cancel');
store.commit('device/updateDevice', data);
await store.dispatch('device/getDevices'); //
} catch (error) {
ElMessage.error('更新失败, 请稍后重试');
throw new Error(error);
}
}
});
};
</script>

216
src/views/device-list.vue

@ -1,216 +0,0 @@
<template>
<SearchBar @search="onSearch" />
<div class="my-3">
<el-button type="primary" @click="deviceCreate.show()">
<i class="el-icon-plus mr-2"></i>
添加设备
</el-button>
</div>
<template v-if="devicesAll && devicesAll.data">
<el-table :data="devicesAll.data" :max-height="contentHeight" border stripe style="width: 100%">
<el-table-column type="expand">
<template #default="props">
<el-row :gutter="20" class="px-6 text-gray-400">
<el-col :span="9">链路地址{{ props.row.linkAddress }}</el-col>
<el-col :span="9">探头编号{{ props.row.probNo }}</el-col>
<el-col :span="9">设备朝向{{ props.row.deviceDirection }}</el-col>
<el-col :span="9">试样{{ props.row.simple }}</el-col>
<el-col :span="9">安装位置{{ props.row.installLocation }}</el-col>
<el-col :span="9">sim1{{ props.row.sim1 }}</el-col>
<el-col :span="9">协议版本{{ props.row.protocolVersion }}</el-col>
<el-col :span="9">
产品类型
<span v-if="ASDU_TYPE[props.row.type]">{{ ASDU_TYPE[props.row.type].text }}</span>
</el-col>
<el-col :span="9">与主站后台联调情况{{ props.row.joint }}</el-col>
<el-col :span="9">备注{{ props.row.remark }}</el-col>
<el-col :span="9">维修记录{{ props.row.operationRecord }}</el-col>
</el-row>
</template>
</el-table-column>
<el-table-column align="center" label="设备ID" min-width="100" prop="deviceId" />
<el-table-column align="center" label="设备完整ID" min-width="150" prop="deviceFullId" />
<el-table-column align="left" header-align="center" label="站点" min-width="150" prop="address" />
<el-table-column align="left" header-align="center" label="地区" min-width="120" prop="area" />
<el-table-column align="center" label="状态" min-width="70">
<template #default="scope">
<el-tag v-if="scope.row.status" :color="STATUS_COLOR[scope.row.status].color" style="color: #fff">
{{ STATUS_COLOR[scope.row.status].text }}
</el-tag>
</template>
</el-table-column>
<el-table-column align="center" label="联系人" min-width="100" prop="contact" />
<el-table-column align="center" label="联系电话" min-width="150" prop="phone" />
<el-table-column align="center" label="安装时间" min-width="200" prop="installTime" />
<el-table-column align="center" label="正式运行时间" min-width="200" prop="runTime" />
<el-table-column align="center" fixed="right" label="操作" min-width="180">
<template #default="props">
<el-tooltip class="item" content="参数配置" effect="dark" placement="top">
<i class="el-icon-set-up text-base text-yellow-600 mx-1" @click="openPage(props.row, 'config')"></i>
</el-tooltip>
<el-tooltip class="item" content="实时数据" effect="dark" placement="top">
<i class="el-icon-odometer text-base text-green-600 mx-1" @click="openPage(props.row, 'data-realtime')"></i>
</el-tooltip>
<el-tooltip class="item" content="历史数据" effect="dark" placement="top">
<i class="el-icon-document-copy text-base text-green-600 mx-1" @click="openPage(props.row, 'history')"></i>
</el-tooltip>
<el-tooltip class="item" content="数据统计" effect="dark" placement="top">
<i class="el-icon-data-line text-base text-green-600 mx-1" @click="openPage(props.row, 'statistical')"></i>
</el-tooltip>
<el-popconfirm title="确定要删除此设备吗?" @confirm="handleDelete(props.row.deviceId)">
<template #reference>
<i class="el-icon-delete text-base text-red-600 mx-1"></i>
<!-- <el-button type="text" plain size="mini" icon="el-icon-delete"></el-button> -->
</template>
</el-popconfirm>
<el-tooltip class="item" content="编辑设备信息" effect="dark" placement="top">
<i class="el-icon-edit text-base text-blue-600 mx-1" @click="handleEdit(props.row)"></i>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="devicesAll.page.page"
:default-page-size="50"
:page-count="devicesAll.page.count"
:page-size="devicesAll.page.size"
:page-sizes="[1, 10, 20, 50, 100]"
:total="devicesAll.page.total"
background
class="my-3 float-right"
layout="total, sizes, prev, pager, next, jumper"
@size-change="onSizeChange"
@current-change="onCurrentPageChange"
></el-pagination>
</template>
<!-- 编辑设备信息 -->
<DeviceEdit :show="editing" @toggle-mdoal="editing = false" />
<el-dialog v-model="deviceCreate.display" title="添加设备" top="30px" width="80%">
<DeviceCreate @on-hide="deviceCreate.hide()" />
</el-dialog>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { useStore } from 'vuex';
import { useRoute, useRouter } from 'vue-router';
import { deleteDevice } from 'apis';
import { ElMessage } from 'element-plus';
import SearchBar from 'components/realtime/search-bar.vue';
import DeviceEdit from 'components/device/device-edit.vue';
import DeviceCreate from 'components/device/device-create.vue';
import useDeviceCreate from '@/hooks/useDeviceCreate';
import { STATUS_COLOR } from '@/config/config';
import { ASDU_TYPE } from '@/config/log';
let timer = null;
const store = useStore();
const router = useRouter();
const route = useRoute();
const token = computed(() => store.getters['user/token']);
const devicesAll = computed(() => {
return store.state.device.devicesAll;
});
const currentDeviceId = computed(() => store.state.device.currentDeviceId);
const contentHeight = ref(600);
const editing = ref(false);
const { show, display, hide } = useDeviceCreate();
const deviceCreate = reactive({
show,
display,
hide,
}); // hook
//
const getDevicesAllData = () => {
try {
if (token && token.value) {
store.dispatch('device/getDevicesAll');
timer && clearTimeout(timer);
timer = null;
} else {
timer = setTimeout(() => {
getDevicesAllData();
});
}
} catch (error) {
ElMessage.error(error);
throw new Error(error);
}
};
getDevicesAllData();
//
onMounted(() => {
const winHeight = document.documentElement.clientHeight;
contentHeight.value = winHeight - 250;
});
/**
* 删除
* @param {number} page 页码
* @param {number} size 每页条数
*/
function onSearch(page, size = 50) {
const deviceId = currentDeviceId.value;
const params = {
deviceId,
page,
size,
};
store.dispatch('device/getDevicesAll', params);
}
//
const onCurrentPageChange = page => {
onSearch(page, devicesAll.value.page.size || 50);
};
//
const onSizeChange = size => {
onSearch(1, size);
};
/**
* 删除某设备
* @param {string} deviceId 设备id
*/
const handleDelete = async deviceId => {
try {
await deleteDevice(deviceId);
store.commit('device/deleteDevice', deviceId);
ElMessage.success('删除成功');
store.dispatch('device/getDevices'); //
} catch (error) {
ElMessage.error('删除失败, 请稍后重试');
throw new Error(error);
}
};
//
const handleEdit = item => {
store.commit('device/setCurrentDeviceId', item.deviceId);
editing.value = true;
};
/**
* 打开页面
* @param {object} item 设备对象
* @param {string} pageName 页面name
*/
const openPage = (item, pageName) => {
item && store.commit('device/setCurrentDeviceId', item.deviceId);
router.push({
path: `/corrosion/${pageName}`,
query: route.query,
});
};
</script>

22
src/views/history.vue

@ -1,22 +0,0 @@
<template>
<el-tabs v-model="activeName">
<el-tab-pane :lazy="true" label="本地数据" name="local">
<Local v-if="activeName === 'local'" :active-name="activeName" />
</el-tab-pane>
<el-tab-pane :lazy="true" label="设备数据" name="device">
<Device v-if="activeName === 'device'" :active-name="activeName" />
</el-tab-pane>
<el-tab-pane :lazy="true" label="补传记录" name="log">
<HistoryLog v-if="activeName === 'log'" :active-name="activeName" />
</el-tab-pane>
</el-tabs>
</template>
<script setup>
import { ref } from 'vue';
import Local from 'components/history/local.vue';
import Device from 'components/history/device.vue';
import HistoryLog from 'components/history/history-log.vue';
const activeName = ref('local');
</script>

23
src/views/overview.vue

@ -1,23 +0,0 @@
<template>
<el-row :gutter="20">
<el-col :span="8">
<el-card shadow="hover">
<ChartDeviceCount />
</el-card>
</el-col>
<el-col :span="16">
<ChartDeviceDetail />
</el-col>
</el-row>
<DeviceTable />
</template>
<script setup>
import ChartDeviceCount from 'components/overview/chart-device-count.vue';
import ChartDeviceDetail from 'components/overview/chart-device-detail.vue';
import DeviceTable from 'components/overview/device-table.vue';
import { useStore } from 'vuex';
const store = useStore();
store.dispatch('device/getDevicesCount'); //
</script>

80
src/views/statistical-report.vue

@ -1,80 +0,0 @@
<template>
<SearchBar ref="searchBar" :loading-search="loadingSearch" @search="onSearch" />
<HistoryData :search-height="searchHeight" class="mt-4" />
</template>
<script setup>
import SearchBar from 'components/statistical/search-bar.vue';
import HistoryData from 'components/statistical/stastistical-chart.vue';
import { computed, onMounted, ref } from 'vue';
import { useStore } from 'vuex';
import { ElMessage } from 'element-plus';
import dayjs from 'dayjs';
const searchBar = ref(null);
let timer = null;
const store = useStore();
const token = computed(() => store.getters['user/token']);
const currentDeviceId = computed(() => store.state.device.currentDeviceId); // id
const search = ref({});
const loadingSearch = ref(false);
const searchHeight = ref(50);
onMounted(() => {
searchHeight.value = searchBar.value.$el.clientHeight;
});
/**
* 历史数据统计
* @param {*} deviceId // id
* @param {*} date //
* @param {*} sort // 1 -> -1->
* @param {*} page //
* @param {*} size //
* @param {*} type // 0
*/
const getData = async () => {
try {
if (token && token.value) {
if (!currentDeviceId.value) return;
loadingSearch.value = true;
const options = { ...search.value };
const date = options && options.date ? options.date : [+dayjs().subtract(7, 'day').startOf('day').format('x'), +dayjs().format('x')];
const params = {
deviceId: currentDeviceId.value,
gatheredDateRange: date,
paging: false,
sort: [
{
col: 'time',
order: 'ASC',
},
],
};
await store.dispatch('statistics/getRealtimeData', params);
timer && clearTimeout(timer);
timer = null;
loadingSearch.value = false;
} else {
loadingSearch.value = false;
timer = setTimeout(() => {
getData();
});
}
} catch (error) {
loadingSearch.value = false;
ElMessage.error(error.message || '查询失败');
}
};
getData();
/**
* 监听search信息
* @param {object} payload search组件emit的数据
*/
const onSearch = payload => {
search.value = { ...payload };
getData();
};
</script>

7
vite.config.js

@ -1,6 +1,3 @@
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import WindiCSS from 'vite-plugin-windicss';
import { defineConfig } from 'vite';
import path from 'path';
import vue from '@vitejs/plugin-vue';
@ -11,8 +8,8 @@ const resolve = dir => path.join(__dirname, dir);
// https://vitejs.dev/config/
export default defineConfig({
base: '/corrosion/',
plugins: [vue(), VitePWA(), WindiCSS(), Components({ resolvers: [ElementPlusResolver()] }), viteCompression()],
base: '/farm/',
plugins: [vue(), VitePWA(), viteCompression()],
resolve: {
alias: {
'~': __dirname,

2194
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save