功能添加

master
chris 2 weeks ago
parent 01da3ace7d
commit 1b510b8cd5

@ -0,0 +1,60 @@
/*
* @Author: chris
* @Date: 2025-09-05 11:34:53
* @LastEditors: chris
* @LastEditTime: 2025-09-17 15:28:22
*/
import request from "@/utils/request";
// 查询果园列表
export function listDevice(query) {
return request({
url: "/business/device/list",
method: "get",
params: query,
});
}
// 查询果园详细
export function getDevice(id) {
return request({
url: "/business/device/" + id,
method: "get",
});
}
// 新增果园
export function addDevice(data) {
return request({
url: "/business/device",
method: "post",
data,
});
}
// 修改果园
export function updateDevice(data) {
return request({
url: "/business/device",
method: "put",
data,
});
}
// 删除果园
export function delDevice(id) {
return request({
url: "/business/device/" + id,
method: "delete",
});
}
// 导出果园数据
export function exportDevice(query) {
return request({
url: "/business/device/export",
method: "post",
params: query,
responseType: "blob",
});
}

@ -0,0 +1,69 @@
/*
* @Author: chris
* @Date: 2025-09-05 11:34:53
* @LastEditors: chris
* @LastEditTime: 2025-09-18 14:45:50
*/
import request from "@/utils/request";
// 查询设备数据列表
export function listDeviceData(query) {
return request({
url: "/business/device-data/list",
method: "get",
params: query,
});
}
// 查询设备数据详细
export function getDeviceData(id) {
return request({
url: "/business/device-data/" + id,
method: "get",
});
}
// 查询设备数据统计分析数据
export function getDeviceDataAnalysis(params) {
return request({
url: "/business/device-data/analysis",
method: "get",
params,
});
}
// 新增设备数据
export function addDeviceData(data) {
return request({
url: "/business/device-data",
method: "post",
data,
});
}
// 修改设备数据
export function updateDeviceData(data) {
return request({
url: "/business/device-data",
method: "put",
data,
});
}
// 删除设备数据
export function delDeviceData(id) {
return request({
url: "/business/device-data/" + id,
method: "delete",
});
}
// 导出设备数据
export function exportDeviceData(query) {
return request({
url: "/business/device-data/export",
method: "post",
params: query,
responseType: "blob",
});
}

@ -0,0 +1,50 @@
/*
* @Author: chris
* @Date: 2025-09-19 11:37:22
* @LastEditors: chris
* @LastEditTime: 2025-09-19 11:37:30
*/
import request from "@/utils/request";
// 查询虫情预警推送人员接收列列表
export function listMember(query) {
return request({
url: "/business/member/list",
method: "get",
params: query,
});
}
// 查询虫情预警推送人员接收列详细
export function getMember(id) {
return request({
url: "/business/member/" + id,
method: "get",
});
}
// 新增虫情预警推送人员接收列
export function addMember(data) {
return request({
url: "/business/member",
method: "post",
data: data,
});
}
// 修改虫情预警推送人员接收列
export function updateMember(data) {
return request({
url: "/business/member",
method: "put",
data: data,
});
}
// 删除虫情预警推送人员接收列
export function delMember(id) {
return request({
url: "/business/member/" + id,
method: "delete",
});
}

@ -2,23 +2,23 @@
* @Author: chris
* @Date: 2025-09-05 11:34:53
* @LastEditors: chris
* @LastEditTime: 2025-09-05 11:37:30
* @LastEditTime: 2025-09-17 11:53:48
*/
import request from "@/utils/request";
// 查询果园列表
export function listPest(query) {
return request({
url: "/business/pest/list",
url: "/leilinglitchi/business/device/list",
method: "get",
params: query,
params: Object.assign({ type: 1 }, query),
});
}
// 查询果园详细
export function getPest(id) {
return request({
url: "/business/pest/" + id,
url: "/leilinglitchi/business/device/" + id,
method: "get",
});
}
@ -26,25 +26,25 @@ export function getPest(id) {
// 新增果园
export function addPest(data) {
return request({
url: "/business/pest",
url: "/leilinglitchi/business/device",
method: "post",
data: data,
data,
});
}
// 修改果园
export function updatePest(data) {
return request({
url: "/business/pest",
url: "/leilinglitchi/business/device",
method: "put",
data: data,
data,
});
}
// 删除果园
export function delPest(id) {
return request({
url: "/business/pest/" + id,
url: "/leilinglitchi/business/device/" + id,
method: "delete",
});
}
@ -52,8 +52,8 @@ export function delPest(id) {
// 导出果园数据
export function exportPest(query) {
return request({
url: "/business/pest/export",
method: "get",
url: "/leilinglitchi/business/device/export",
method: "post",
params: query,
responseType: "blob",
});

@ -2,28 +2,29 @@
* @Author: chris
* @Date: 2025-02-06 16:43:54
* @LastEditors: chris
* @LastEditTime: 2025-08-06 16:13:59
* @LastEditTime: 2025-09-22 17:33:54
-->
<template>
<div class="device-list-container">
<div class="device-list">
<div
v-for="device in deviceData"
v-for="device in props.list"
:key="device.deviceAddr"
@click="handleDeviceClick(device)"
>
<div class="device-item" :class="{ 'device-item-selected': selectedDevice === device.deviceAddr }">
<Icon :icon="getDeviceIcon(device.deviceType)" class="device-icon mr-5px" />
<Icon :icon="getDeviceIcon(device.type)" class="device-icon mr-5px" />
<div class="device-details">
<div class="device-name font-medium">{{ device.deviceName }}</div>
<el-tag :type="device.deviceEnabled ? 'success' : 'danger'" size="small">
<div class="device-name font-medium">{{ device.deviceNo }}</div>
<div class="device-model">{{ device.model }}</div>
<!-- <el-tag :type="device.deviceEnabled ? 'success' : 'danger'" size="small">
{{ device.deviceEnabled ? '在线' : '离线' }}
</el-tag>
</el-tag> -->
</div>
</div>
</div>
</div>
<div v-if="deviceData.length === 0" class="empty-state text-center py-10">
<div v-if="props.list.length === 0" class="empty-state text-center py-10">
<Icon icon="carbon:error" class="text-gray-300 text-4xl mb-2" />
<p class="text-gray-400">暂无设备数据</p>
</div>
@ -33,16 +34,22 @@
<script setup name="DeviceFlatList">
import { ref, defineEmits } from 'vue'
import { Icon } from '@iconify/vue'
import { deviceData } from '@/views/indexComponents/testData'
// import { deviceData } from '@/views/indexComponents/testData'
//
const deviceIconDict = {
worm: 'carbon:pest', //
weather: 'carbon:radar-weather', //
soil: 'carbon:soil-moisture', //
irrigation: 'carbon:rain-drop', //
default: 'carbon:devices'
1: 'carbon:pest', // carbon:radar-weather', //
2: 'carbon:soil-moisture', //
3: 'carbon:radar-weather', // '
}
const props = defineProps({
list: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['change'])
const selectedDevice = ref('')
@ -65,7 +72,7 @@ function handleDeviceClick(device) {
}
.device-item {
@apply flex items-center cursor-pointer transition-all duration-200 hover:bg-blue-50 my-4px py-6px px-6px border-rounded-6px;
@apply flex items-center cursor-pointer transition-all duration-200 hover:bg-blue-50 my-4px py-10px px-6px border-rounded-6px;
&-selected {
@apply bg-blue-100;
@ -73,7 +80,7 @@ function handleDeviceClick(device) {
}
.device-icon {
@apply text-blue-500 w-5 h-5;
@apply text-blue-500 w-26px h-26px;
}
.device-details {
@ -84,6 +91,11 @@ function handleDeviceClick(device) {
@apply text-gray-800;
}
.device-model {
@apply text-xs color-#9ca3af;
}
.device-id {
@apply text-xs;
}

@ -1,3 +1,9 @@
/*
* @Author: chris
* @Date: 2025-01-13 09:34:10
* @LastEditors: chris
* @LastEditTime: 2025-09-17 09:20:33
*/
import router from "./router";
import { ElMessage } from "element-plus";
import NProgress from "nprogress";
@ -19,8 +25,8 @@ const isWhiteList = (path) => {
router.beforeEach((to, from, next) => {
// TODO 测试,先跳过
next();
return;
// next();
// return;
NProgress.start();
if (getToken()) {
to.meta.title && useSettingsStore().setTitle(to.meta.title);

@ -69,61 +69,61 @@ export const constantRoutes = [
meta: { title: "首页", icon: "dashboard", affix: true },
},
// TODO 测试开始, 后续删除
{
path: "/pest/pest-monitor",
component: () => import("@/views/pest/pestMonitor/index"),
name: "PestMonitor",
},
{
path: "/orchard",
component: () => import("@/views/orchard"),
name: "Orchard",
},
{
path: "/pest",
component: () => import("@/views/pest"),
name: "Pest",
},
{
path: "/pest/imgAnalysis",
component: () => import("@/views/pest/imgAnalysis"),
name: "ImgAnalysis",
},
{
path: "/pest/pest-statistics",
component: () => import("@/views/pest/pestStatistics"),
name: "PestStatistics",
},
{
path: "/pest/trendAnalysis",
component: () => import("@/views/pest/trendAnalysis"),
name: "TrendAnalysis",
},
{
path: "/weather/monitor",
component: () => import("@/views/weather/monitor"),
name: "WeatherMonitor",
},
{
path: "/weather/history",
component: () => import("@/views/weather/history"),
name: "WeatherHistory",
},
{
path: "/soil/monitor",
component: () => import("@/views/soil/monitor"),
name: "SoilMonitor",
},
{
path: "/soil/history",
component: () => import("@/views/soil/history"),
name: "SoilHistory",
},
// {
// path: "/orchard-screen",
// component: () => import("@/views/orchardScreen"),
// name: "OrchardScreen",
// path: "/pest/pest-monitor",
// component: () => import("@/views/pest/pestMonitor/index"),
// name: "PestMonitor",
// },
// {
// path: "/orchard",
// component: () => import("@/views/orchard"),
// name: "Orchard",
// },
// {
// path: "/pest",
// component: () => import("@/views/pest"),
// name: "Pest",
// },
// {
// path: "/pest/imgAnalysis",
// component: () => import("@/views/pest/imgAnalysis"),
// name: "ImgAnalysis",
// },
// {
// path: "/pest/pest-statistics",
// component: () => import("@/views/pest/pestStatistics"),
// name: "PestStatistics",
// },
// {
// path: "/pest/trendAnalysis",
// component: () => import("@/views/pest/trendAnalysis"),
// name: "TrendAnalysis",
// },
// {
// path: "/weather/monitor",
// component: () => import("@/views/weather/monitor"),
// name: "WeatherMonitor",
// },
// {
// path: "/weather/history",
// component: () => import("@/views/weather/history"),
// name: "WeatherHistory",
// },
// {
// path: "/soil/monitor",
// component: () => import("@/views/soil/monitor"),
// name: "SoilMonitor",
// },
// {
// path: "/soil/history",
// component: () => import("@/views/soil/history"),
// name: "SoilHistory",
// },
// // {
// // path: "/orchard-screen",
// // component: () => import("@/views/orchardScreen"),
// // name: "OrchardScreen",
// // },
// TODO 测试结束, 后续删除
],
},

@ -1,5 +1,11 @@
import useDictStore from '@/store/modules/dict'
import { getDicts } from '@/api/system/dict/data'
/*
* @Author: chris
* @Date: 2025-01-13 09:34:10
* @LastEditors: chris
* @LastEditTime: 2025-09-17 16:54:35
*/
import useDictStore from "@/store/modules/dict";
import { getDicts } from "@/api/system/dict/data";
/**
* 获取字典数据
@ -13,12 +19,17 @@ export function useDict(...args) {
if (dicts) {
res.value[dictType] = dicts;
} else {
getDicts(dictType).then(resp => {
res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
getDicts(dictType).then((resp) => {
res.value[dictType] = resp.data.map((p) => ({
label: p.dictLabel,
value: p.dictValue,
elTagType: p.listClass,
elTagClass: p.cssClass,
}));
useDictStore().setDict(dictType, res.value[dictType]);
})
});
}
})
});
return toRefs(res.value);
})()
})();
}

@ -1,114 +1,155 @@
import axios from 'axios'
import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from '@/utils/ruoyi'
import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
import useUserStore from '@/store/modules/user'
import axios from "axios";
import {
ElNotification,
ElMessageBox,
ElMessage,
ElLoading,
} from "element-plus";
import { getToken } from "@/utils/auth";
import errorCode from "@/utils/errorCode";
import { tansParams, blobValidate } from "@/utils/ruoyi";
import cache from "@/plugins/cache";
import { saveAs } from "file-saver";
import useUserStore from "@/store/modules/user";
let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = { show: false };
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
axios.defaults.headers["Content-Type"] = "application/json;charset=utf-8";
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项表示请求URL公共部分
baseURL: import.meta.env.VITE_APP_BASE_API,
// 超时
timeout: 10000
})
timeout: 10000,
});
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
service.interceptors.request.use(
(config) => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false;
if (getToken() && !isToken) {
config.headers["Authorization"] = "Bearer " + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
}
const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小
const limitSize = 5 * 1024 * 1024; // 限制存放数据5M
if (requestSize >= limitSize) {
console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制无法进行防重复提交验证。')
return config;
// get请求映射params参数
if (config.method === "get" && config.params) {
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
if (
!isRepeatSubmit &&
(config.method === "post" || config.method === "put")
) {
const requestObj = {
url: config.url,
data:
typeof config.data === "object"
? JSON.stringify(config.data)
: config.data,
time: new Date().getTime(),
};
const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小
const limitSize = 5 * 1024 * 1024; // 限制存放数据5M
if (requestSize >= limitSize) {
console.warn(
`[${config.url}]: ` +
"请求数据大小超出允许的5M限制无法进行防重复提交验证。"
);
return config;
}
const sessionObj = cache.session.getJSON("sessionObj");
if (
sessionObj === undefined ||
sessionObj === null ||
sessionObj === ""
) {
cache.session.setJSON("sessionObj", requestObj);
} else {
cache.session.setJSON('sessionObj', requestObj)
const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
if (
s_data === requestObj.data &&
requestObj.time - s_time < interval &&
s_url === requestObj.url
) {
const message = "数据正在处理,请勿重复提交";
console.warn(`[${s_url}]: ` + message);
return Promise.reject(new Error(message));
} else {
cache.session.setJSON("sessionObj", requestObj);
}
}
}
return config;
},
(error) => {
console.log(error);
Promise.reject(error);
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
);
// 响应拦截器
service.interceptors.response.use(res => {
service.interceptors.response.use(
(res) => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
const msg = errorCode[code] || res.data.msg || errorCode["default"];
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
if (
res.request.responseType === "blob" ||
res.request.responseType === "arraybuffer"
) {
return res.data;
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false;
useUserStore().logOut().then(() => {
location.href = '/index';
ElMessageBox.confirm(
"登录状态已过期,您可以继续留在该页面,或者重新登录",
"系统提示",
{
confirmButtonText: "重新登录",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
isRelogin.show = false;
useUserStore()
.logOut()
.then(() => {
location.href = "/index";
});
})
}).catch(() => {
isRelogin.show = false;
});
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
.catch(() => {
isRelogin.show = false;
});
}
return Promise.reject("无效的会话,或者会话已过期,请重新登录。");
} else if (code === 500) {
ElMessage({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
ElMessage({ message: msg, type: "error" });
return Promise.reject(new Error(msg));
} else if (code === 601) {
ElMessage({ message: msg, type: 'warning' })
return Promise.reject(new Error(msg))
ElMessage({ message: msg, type: "warning" });
return Promise.reject(new Error(msg));
} else if (code !== 200) {
ElNotification.error({ title: msg })
return Promise.reject('error')
ElNotification.error({ title: msg });
return Promise.reject("error");
} else {
return Promise.resolve(res.data)
return Promise.resolve(res.data);
}
},
error => {
console.log('err' + error)
(error) => {
console.log("err" + error);
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
@ -117,36 +158,50 @@ service.interceptors.response.use(res => {
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
ElMessage({ message: message, type: "error", duration: 5 * 1000 });
return Promise.reject(error);
}
)
);
// 通用下载方法
export function download(url, params, filename, config) {
downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
return service.post(url, params, {
transformRequest: [(params) => { return tansParams(params) }],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'blob',
...config
}).then(async (data) => {
const isBlob = blobValidate(data);
if (isBlob) {
const blob = new Blob([data])
saveAs(blob, filename)
} else {
const resText = await data.text();
const rspObj = JSON.parse(resText);
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
ElMessage.error(errMsg);
}
downloadLoadingInstance.close();
}).catch((r) => {
console.error(r)
ElMessage.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close();
})
downloadLoadingInstance = ElLoading.service({
text: "正在下载数据,请稍候",
background: "rgba(0, 0, 0, 0.7)",
});
let res = null;
return service
.post(url, params, {
transformRequest: [
(params) => {
return tansParams(params);
},
],
headers: { "Content-Type": "application/x-www-form-urlencoded" },
responseType: "blob",
...config,
})
.then(async (data) => {
const isBlob = blobValidate(data);
if (isBlob) {
const blob = new Blob([data]);
saveAs(blob, filename);
} else {
const resText = await data.text();
const rspObj = JSON.parse(resText);
const errMsg =
errorCode[rspObj.code] || rspObj.msg || errorCode["default"];
// ElMessage.error(errMsg);
res = errMsg;
}
downloadLoadingInstance.close();
return res;
})
.catch((r) => {
console.error(r);
ElMessage.error("下载文件出现错误,请联系管理员!");
downloadLoadingInstance.close();
});
}
export default service
export default service;

@ -1,5 +1,3 @@
/**
* 通用js方法封装处理
* Copyright (c) 2019 ruoyi
@ -8,22 +6,25 @@
// 日期格式化
export function parseTime(time, pattern) {
if (arguments.length === 0 || !time) {
return null
return null;
}
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
const format = pattern || "{y}-{m}-{d} {h}:{i}:{s}";
let date;
if (typeof time === "object") {
date = time;
} else {
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
time = parseInt(time)
} else if (typeof time === 'string') {
time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
if (typeof time === "string" && /^[0-9]+$/.test(time)) {
time = parseInt(time);
} else if (typeof time === "string") {
time = time
.replace(new RegExp(/-/gm), "/")
.replace("T", " ")
.replace(new RegExp(/\.[\d]{3}/gm), "");
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000
if (typeof time === "number" && time.toString().length === 10) {
time = time * 1000;
}
date = new Date(time)
date = new Date(time);
}
const formatObj = {
y: date.getFullYear(),
@ -32,18 +33,20 @@ export function parseTime(time, pattern) {
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
a: date.getDay(),
};
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key]
let value = formatObj[key];
// Note: getDay() returns 0 on Sunday
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
if (key === "a") {
return ["日", "一", "二", "三", "四", "五", "六"][value];
}
if (result.length > 0 && value < 10) {
value = '0' + value
value = "0" + value;
}
return value || 0
})
return time_str
return value || 0;
});
return time_str;
}
// 表单重置
@ -56,14 +59,19 @@ export function resetForm(refName) {
// 添加日期范围
export function addDateRange(params, dateRange, propName) {
let search = params;
search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
search.params =
typeof search.params === "object" &&
search.params !== null &&
!Array.isArray(search.params)
? search.params
: {};
dateRange = Array.isArray(dateRange) ? dateRange : [];
if (typeof (propName) === 'undefined') {
search.params['beginTime'] = dateRange[0];
search.params['endTime'] = dateRange[1];
if (typeof propName === "undefined") {
search.params["beginTime"] = dateRange[0];
search.params["endTime"] = dateRange[1];
} else {
search.params['begin' + propName] = dateRange[0];
search.params['end' + propName] = dateRange[1];
search.params["begin" + propName] = dateRange[0];
search.params["end" + propName] = dateRange[1];
}
return search;
}
@ -75,20 +83,20 @@ export function selectDictLabel(datas, value) {
}
var actions = [];
Object.keys(datas).some((key) => {
if (datas[key].value == ('' + value)) {
if (datas[key].value == "" + value) {
actions.push(datas[key].label);
return true;
}
})
});
if (actions.length === 0) {
actions.push(value);
}
return actions.join('');
return actions.join("");
}
// 回显数据字典(字符串数组)
export function selectDictLabels(datas, value, separator) {
if (value === undefined || value.length ===0) {
if (value === undefined || value.length === 0) {
return "";
}
if (Array.isArray(value)) {
@ -100,30 +108,32 @@ export function selectDictLabels(datas, value, separator) {
Object.keys(value.split(currentSeparator)).some((val) => {
var match = false;
Object.keys(datas).some((key) => {
if (datas[key].value == ('' + temp[val])) {
if (datas[key].value == "" + temp[val]) {
actions.push(datas[key].label + currentSeparator);
match = true;
}
})
});
if (!match) {
actions.push(temp[val] + currentSeparator);
}
})
return actions.join('').substring(0, actions.join('').length - 1);
});
return actions.join("").substring(0, actions.join("").length - 1);
}
// 字符串格式化(%s )
export function sprintf(str) {
var args = arguments, flag = true, i = 1;
var args = arguments,
flag = true,
i = 1;
str = str.replace(/%s/g, function () {
var arg = args[i++];
if (typeof arg === 'undefined') {
if (typeof arg === "undefined") {
flag = false;
return '';
return "";
}
return arg;
});
return flag ? str : '';
return flag ? str : "";
}
// 转换字符串undefined,null等转化为""
@ -148,7 +158,7 @@ export function mergeRecursive(source, target) {
}
}
return source;
};
}
/**
* 构造树型结构数据
@ -159,9 +169,9 @@ export function mergeRecursive(source, target) {
*/
export function handleTree(data, id, parentId, children) {
let config = {
id: id || 'id',
parentId: parentId || 'parentId',
childrenList: children || 'children'
id: id || "id",
parentId: parentId || "parentId",
childrenList: children || "children",
};
var childrenListMap = {};
@ -202,19 +212,23 @@ export function handleTree(data, id, parentId, children) {
}
/**
* 参数处理
* @param {*} params 参数
*/
* 参数处理
* @param {*} params 参数
*/
export function tansParams(params) {
let result = ''
let result = "";
for (const propName of Object.keys(params)) {
const value = params[propName];
var part = encodeURIComponent(propName) + "=";
if (value !== null && value !== "" && typeof (value) !== "undefined") {
if (typeof value === 'object') {
if (value !== null && value !== "" && typeof value !== "undefined") {
if (typeof value === "object") {
for (const key of Object.keys(value)) {
if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
let params = propName + '[' + key + ']';
if (
value[key] !== null &&
value[key] !== "" &&
typeof value[key] !== "undefined"
) {
let params = propName + "[" + key + "]";
var subPart = encodeURIComponent(params) + "=";
result += subPart + encodeURIComponent(value[key]) + "&";
}
@ -224,23 +238,22 @@ export function tansParams(params) {
}
}
}
return result
return result;
}
// 返回项目路径
export function getNormalPath(p) {
if (p.length === 0 || !p || p == 'undefined') {
return p
};
let res = p.replace('//', '/')
if (res[res.length - 1] === '/') {
return res.slice(0, res.length - 1)
if (p.length === 0 || !p || p == "undefined") {
return p;
}
let res = p.replace("//", "/");
if (res[res.length - 1] === "/") {
return res.slice(0, res.length - 1);
}
return res;
}
// 验证是否为blob格式
export function blobValidate(data) {
return data.type !== 'application/json'
return data.type !== "application/json";
}

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-08 16:17:48
* @LastEditors: chris
* @LastEditTime: 2025-08-12 10:09:05
* @LastEditTime: 2025-09-17 17:14:23
-->
<template>
<common-action-toolbar
@ -31,7 +31,7 @@ const showSearch = computed({
})
//
const buttons = [
const buttons = reactive([
{
key: 'add',
text: '新增',
@ -47,7 +47,7 @@ const buttons = [
plain: true,
icon: 'Edit',
action: 'update',
disabled: computed(() => props.selectedRows.length !== 1).value
disabled: computed(() => props.selectedRows.length !== 1)
},
{
key: 'delete',
@ -56,7 +56,7 @@ const buttons = [
plain: true,
icon: 'Delete',
action: 'delete',
disabled: computed(() => props.selectedRows.length === 0).value
disabled: computed(() => props.selectedRows.length === 0)
},
{
key: 'export',
@ -66,7 +66,7 @@ const buttons = [
icon: 'Download',
action: 'export'
}
]
])
//
function handleButtonClick(action) {

@ -0,0 +1,152 @@
<template>
<el-dialog :title="title" v-model="dialogOpen" width="600px" append-to-body>
<el-form :model="props.form" :rules="rules" ref="deviceRef" label-width="80px">
<el-row :gutter="10">
<el-col :span="24">
<el-form-item label="设备图片" prop="imgUrl">
<image-upload v-model="uploadImg" :limit="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备类型" prop="type">
<el-select v-model="props.form.type" placeholder="请选择设备类型" clearable style="width: 200px">
<el-option v-for="dict in deviceTypes" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备号" prop="deviceNo">
<el-input v-model="props.form.deviceNo" placeholder="请输入设备号" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备型号" prop="model">
<el-input v-model="props.form.model" placeholder="请输入设备型号" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备地址" prop="deviceAddr">
<el-input v-model="props.form.deviceAddr" placeholder="请输入设备地址" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备点位" prop="installPointDes">
<el-input v-model="props.form.installPointDes" placeholder="请输入设备点位" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="props.form.status">
<el-radio v-for="item in statusOptions" :key="item.value" :value="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="props.form.remark" type="textarea" placeholder="请输入备注内容"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel"> </el-button>
<el-button type="primary" @click="submitForm"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { addDevice, updateDevice } from '@/api/device'
const props = defineProps({
open: { type: Boolean, required: true },
form: { type: Object, required: true },
title: { type: String, required: true },
deviceTypes: { type: Array, required: true },
statusOptions: { type: Array, required: true },
})
const emits = defineEmits(['update:open', 'success'])
// open
const dialogOpen = ref(props.open)
const uploadImg = ref('')
watch(() => uploadImg.value, (newVal) => {
props.form.imgUrl = newVal
})
//
const rules = {
imgUrl: [{ required: true, message: '请上传设备图片', trigger: 'blur' }],
type: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
deviceNo: [{ required: true, message: '设备号不能为空', trigger: 'blur' }],
deviceAddr: [{ required: true, message: '设备地址不能为空', trigger: 'blur' }],
model: [{ required: true, message: '设备型号不能为空', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'blur' }],
installPointDes: [{ required: true, message: '安装点描述不能为空', trigger: 'blur' }],
}
//
const deviceRef = ref(null)
const { proxy } = getCurrentInstance()
onMounted(() => {
//
setTimeout(() => {
deviceRef.value?.resetFields()
}, 0)
})
// props.open
watch(
() => props.open,
(newValue) => {
dialogOpen.value = newValue
}
)
//
watch(
dialogOpen,
(newValue) => {
if (newValue !== props.open) {
emits('update:open', newValue)
}
}
)
// emits
//
async function submitForm() {
try {
await deviceRef.value.validate()
if (props.form.id) {
await updateDevice(props.form)
proxy.$modal.msgSuccess('修改成功')
} else {
await addDevice(props.form)
proxy.$modal.msgSuccess('新增成功')
}
dialogOpen.value = false
emits('success')
} catch (error) {
//
if (error !== 'cancel') {
proxy.$modal.msgError('操作失败: ' + (error.message || '未知错误'))
}
}
}
//
function cancel() {
dialogOpen.value = false
deviceRef.value?.resetFields()
}
</script>

@ -2,16 +2,18 @@
* @Author: chris
* @Date: 2025-08-08 16:17:37
* @LastEditors: chris
* @LastEditTime: 2025-09-05 11:02:44
* @LastEditTime: 2025-09-18 14:50:47
-->
<template>
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
<el-form-item label="设备名称" prop="orchardName">
<el-input v-model="queryParams.orchardName" placeholder="请输入设备名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
<el-form-item label="设备类型" prop="deviceType">
<el-select v-model="queryParams.type" placeholder="设备类型" clearable style="width: 240px">
<el-option v-for="dict in props.deviceTypes" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="设备状态" clearable style="width: 240px">
<el-option v-for="dict in pestStatus" :key="dict.value" :label="dict.label" :value="dict.value" />
<el-option v-for="dict in props.statusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
@ -22,18 +24,16 @@
</template>
<script setup>
import { useDict } from '@/utils/dict'
// props
const props = defineProps({
queryParams: { type: Object, required: true },
deviceTypes: { type: Array, required: true },
statusOptions: { type: Array, required: true }
})
//
const emits = defineEmits(['query', 'reset', 'update:dateRange'])
//
const { pest_status: pestStatus } = useDict('pest_status')
const emits = defineEmits(['query', 'reset'])
//
function handleQuery() {
@ -42,7 +42,6 @@ function handleQuery() {
//
function resetQuery() {
emits('update:dateRange', [])
emits('reset')
}
</script>

@ -2,12 +2,12 @@
* @Author: chris
* @Date: 2025-08-08 16:17:54
* @LastEditors: chris
* @LastEditTime: 2025-09-09 09:50:07
* @LastEditTime: 2025-09-17 16:59:45
-->
<template>
<el-table
v-loading="loading"
:data="orchardList"
:data="list"
@selection-change="handleSelectionChange"
row-key="orchardId"
border
@ -15,27 +15,30 @@
style="width: 100%"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="设备编号" align="center" prop="no" v-if="columns[0].visible"/>
<el-table-column label="设备名称" align="center" prop="deviceName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
<el-table-column label="型号" align="center" prop="model" v-if="columns[2].visible" />
<el-table-column label="ip地址" align="center" prop="address" v-if="columns[3].visible" />
<el-table-column label="状态" align="center" v-if="columns[4].visible">
<el-table-column label="设备号" align="center" prop="deviceNo" v-if="columns[0].visible" :show-overflow-tooltip="true" />
<el-table-column label="型号" align="center" prop="model" v-if="columns[1].visible" />
<el-table-column label="类型" align="center" prop="type" v-if="columns[2].visible">
<template #default="scope">
<span>{{ selectDictLabel(props.deviceTypes, scope.row.type) }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" v-if="columns[3].visible">
<template #default="scope">
<el-tag :type="statusColorMap[scope.row.status]">
{{ scope.row.status === '0' ? '正常' : '停用' }}
{{ props.statusOptions.find(item => item.value == scope.row.status)?.label || '未知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[5].visible">
<el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[4].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<!-- <el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="Eye" @click="handleView(scope.row)" v-hasPermi="['business:orchard:query']"></el-button>
</el-tooltip>
</el-tooltip> -->
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['business:orchard:edit']"></el-button>
</el-tooltip>
@ -48,13 +51,15 @@
</template>
<script setup>
import { parseTime } from '@/utils/ruoyi'
import { parseTime, selectDictLabel } from '@/utils/ruoyi'
import { statusColorMap } from '../config'
const props = defineProps({
orchardList: { type: Array, required: true },
list: { type: Array, required: true },
loading: { type: Boolean, required: true },
columns: { type: Array, required: true }
columns: { type: Array, required: true },
statusOptions: { type: Array, required: true },
deviceTypes: { type: Array, required: true }
})
const emits = defineEmits(['selection-change', 'view', 'update', 'delete'])

@ -0,0 +1,21 @@
/*
* @Author: chris
* @Date: 2025-09-05 10:12:41
* @LastEditors: chris
* @LastEditTime: 2025-09-18 17:26:54
*/
import request from "@/utils/request";
// 列配置
export const columnsConfig = [
{ key: 0, label: "设备号", visible: true },
{ key: 1, label: "型号", visible: true },
{ key: 2, label: "类型", visible: true },
{ key: 3, label: "状态", visible: true },
{ key: 4, label: "创建时间", visible: true },
];
// 状态颜色映射
export const statusColorMap = {
0: "success",
1: "danger",
};

@ -4,7 +4,8 @@
<pest-search-form
v-show="showSearch"
:query-params="queryParams"
v-model:dateRange="dateRange"
:device-types="deviceTypes"
:status-options="statusOptions"
@query="handleQuery"
@reset="resetQuery"
/>
@ -22,10 +23,12 @@
/>
<!-- 数据表格 -->
<pest-table
<device-table
:list="deviceList"
:loading="loading"
:columns="columns"
:statusOptions="statusOptions"
:deviceTypes="deviceTypes"
@selection-change="handleSelectionChange"
@view="handleView"
@update="handleUpdate"
@ -35,30 +38,33 @@
<!-- 分页 -->
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 添加或修改果园对话框 -->
<pest-form-dialog
<device-form-dialog
v-model:open="open"
:device-types="deviceTypes"
:status-options="statusOptions"
:form="form"
:title="title"
@success="getList"
/>
<!-- 查看果园对话框 -->
<pest-view-dialog
<device-view-dialog
v-model:open="viewOpen"
:pest-id="currentId"
/>
</div>
</template>
<script setup name="Pest">
import { listPest, delPest } from '@/api/pest'
<script setup name="Device">
import { listDevice, delDevice } from '@/api/device'
import { columnsConfig } from './config.js'
import Pagination from '@/components/Pagination'
import PestSearchForm from './components/PestSearchForm'
import ActionButtons from './components/ActionButtons'
import PestTable from './components/PestTable'
import PestFormDialog from './components/PestFormDialog'
import PestViewDialog from './components/PestViewDialog'
import DeviceTable from './components/PestTable'
import DeviceFormDialog from './components/PestFormDialog'
import DeviceViewDialog from './components/PestViewDialog'
import { useDict } from '@/utils/dict'
//
const { proxy } = getCurrentInstance()
@ -76,17 +82,26 @@ const title = ref('')
const dateRange = ref([])
const currentId = ref(null)
//
const { monitor_device_type: deviceTypes } = useDict('monitor_device_type')
const { device_status: statusOptions } = useDict('device_status')
//
const data = reactive({
form: {},
form: {
deviceNo: '',
deviceAddr: '',
model: '',
installPointDes: '',
imgUrl: '',
type: null,
status: '0'
},
queryParams: {
type: null,
status: null,
pageNum: 1,
pageSize: 10,
deviceName: undefined,
manager: undefined,
status: undefined,
beginTime: undefined,
endTime: undefined
}
})
@ -101,20 +116,11 @@ onMounted(() => {
async function getList() {
loading.value = true
try {
//
if (dateRange.value && dateRange.value.length) {
queryParams.value.beginTime = dateRange.value[0]
queryParams.value.endTime = dateRange.value[1]
} else {
queryParams.value.beginTime = undefined
queryParams.value.endTime = undefined
}
const res = await listPest(queryParams.value)
const res = await listDevice(queryParams.value)
deviceList.value = res.rows || []
total.value = res.total || 0
} catch (error) {
proxy.$model.msgError('获取虫情设备数据失败: ' + (error.message || '未知错误'))
proxy.$modal.msgError('获取虫情设备数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get pest list:', error)
} finally {
loading.value = false
@ -138,7 +144,7 @@ function resetQuery() {
function handleAdd() {
open.value = true
title.value = '新增虫情设备'
form.value = { status: '0' } //
// form.value = { status: '1' } //
}
/** 修改按钮操作 */
@ -165,21 +171,22 @@ async function handleDelete(row) {
try {
await proxy.$confirm(`是否确认删除选中的${deviceNames.join(', ')}虫情设备数据?`)
await delPest(ids)
proxy.$model.msgSuccess('删除成功')
await delDevice(ids)
proxy.$modal.msgSuccess('删除成功')
getList()
selectedRows.value = [] //
} catch (error) {
//
if (error !== 'cancel') {
proxy.$model.msgError('删除失败: ' + (error.message || '未知错误'))
proxy.$modal.msgError('删除失败: ' + (error.message || '未知错误'))
}
}
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('/business/pest/export', queryParams.value, `虫情设备数据_${new Date().getTime()}.xlsx`)
async function handleExport() {
const fileName = await proxy.download('/business/device/export', queryParams.value, `虫情设备数据_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}
/** 选择框选中数据变化 */

@ -95,7 +95,7 @@ const loginRules = {
const codeUrl = ref("");
const loading = ref(false);
//
const captchaEnabled = ref(true);
const captchaEnabled = ref(false);
//
const register = ref(false);
const redirect = ref(undefined);
@ -106,8 +106,8 @@ watch(route, (newRoute) => {
function handleLogin() {
// TODO
router.push({ path: "/" });
return;
// router.push({ path: "/" });
// return;
proxy.$refs.loginRef.validate(valid => {
if (valid) {
@ -166,8 +166,8 @@ function getCookie() {
}
// TODO:
// getCode();
// getCookie();
captchaEnabled.value && getCode();
getCookie();
</script>
<style lang='scss' scoped>

@ -0,0 +1,286 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="用户昵称" prop="nickname">
<el-input
v-model="queryParams.nickname"
placeholder="请输入用户昵称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input
v-model="queryParams.mobile"
placeholder="请输入手机号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="openId" prop="openId">
<el-input
v-model="queryParams.openId"
placeholder="请输入openId"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['business:member:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['business:member:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['business:member:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['business:member:export']"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="memberList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="用户昵称" align="center" prop="nickname" />
<el-table-column label="用户头像" align="center" prop="avatarUrl" width="100">
<template #default="scope">
<image-preview :src="scope.row.avatarUrl" :width="50" :height="50"/>
</template>
</el-table-column>
<el-table-column label="性别" align="center" prop="gender" />
<el-table-column label="手机号" align="center" prop="mobile" />
<el-table-column label="openId" align="center" prop="openId" />
<el-table-column label="unionId" align="center" prop="unionId" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['business:member:edit']"></el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['business:member:remove']"></el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改虫情预警推送人员接收列对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="memberRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="用户昵称" prop="nickname">
<el-input v-model="form.nickname" placeholder="请输入用户昵称" />
</el-form-item>
<el-form-item label="用户头像" prop="avatarUrl">
<image-upload :limit="1" v-model="form.avatarUrl"/>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="form.mobile" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Member">
import { listMember, getMember, delMember, addMember, updateMember } from "@/api/memberPush"
const { proxy } = getCurrentInstance()
const memberList = ref([])
const open = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref("")
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
nickname: null,
mobile: null,
openId: null,
},
rules: {
nickname: [
{ required: true, message: "用户昵称不能为空", trigger: "blur" }
],
mobile: [
{ required: true, message: "手机号不能为空", trigger: "blur" }
],
openId: [
{ required: true, message: "openId不能为空", trigger: "blur" }
],
}
})
const { queryParams, form, rules } = toRefs(data)
/** 查询虫情预警推送人员接收列列表 */
function getList() {
loading.value = true
listMember(queryParams.value).then(response => {
memberList.value = response.rows
total.value = response.total
loading.value = false
})
}
//
function cancel() {
open.value = false
reset()
}
//
function reset() {
form.value = {
id: null,
deptId: null,
nickname: null,
avatarUrl: null,
gender: null,
country: null,
province: null,
city: null,
mobile: null,
openId: null,
unionId: null,
createBy: null,
createTime: null,
updateBy: null,
updateTime: null,
remark: null
}
proxy.resetForm("memberRef")
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1
getList()
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef")
handleQuery()
}
//
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.id)
single.value = selection.length != 1
multiple.value = !selection.length
}
/** 新增按钮操作 */
function handleAdd() {
reset()
open.value = true
title.value = "添加虫情预警推送人员接收列"
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset()
const _id = row.id || ids.value
getMember(_id).then(response => {
form.value = response.data
open.value = true
title.value = "修改虫情预警推送人员接收列"
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["memberRef"].validate(valid => {
if (valid) {
if (form.value.id != null) {
updateMember(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功")
open.value = false
getList()
})
} else {
addMember(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功")
open.value = false
getList()
})
}
}
})
}
/** 删除按钮操作 */
function handleDelete(row) {
const _ids = row.id || ids.value
proxy.$modal.confirm('是否确认删除虫情预警推送人员接收列编号为"' + _ids + '"的数据项?').then(function() {
return delMember(_ids)
}).then(() => {
getList()
proxy.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('business/member/export', {
...queryParams.value
}, `member_${new Date().getTime()}.xlsx`)
}
getList()
</script>

@ -1,125 +0,0 @@
<template>
<el-dialog :title="title" v-model="dialogOpen" width="600px" append-to-body>
<el-form :model="form" :rules="rules" ref="deviceRef" label-width="80px">
<el-row :gutter="10">
<el-col :span="24">
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="form.deviceName" placeholder="请输入设备名称" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备型号" prop="model">
<el-input v-model="form.model" placeholder="请输入设备型号" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="ip地址" prop="address">
<el-input v-model="form.address" placeholder="请输入ip地址" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :value="'0'">正常</el-radio>
<el-radio :value="'1'">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注内容"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel"> </el-button>
<el-button type="primary" @click="submitForm"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { addPest, updatePest } from '@/api/pest'
const props = defineProps({
open: { type: Boolean, required: true },
form: { type: Object, required: true },
title: { type: String, required: true }
})
const emits = defineEmits(['update:open', 'success'])
// open
const dialogOpen = ref(props.open)
//
const rules = {
deviceName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
address: [{ required: true, message: 'ip地址不能为空', trigger: 'blur' }],
model: [{ required: true, message: '设备型号不能为空', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'blur' }]
}
//
const deviceRef = ref(null)
const { proxy } = getCurrentInstance()
onMounted(() => {
//
setTimeout(() => {
deviceRef.value?.resetFields()
}, 0)
})
// props.open
watch(
() => props.open,
(newValue) => {
dialogOpen.value = newValue
}
)
//
watch(
dialogOpen,
(newValue) => {
if (newValue !== props.open) {
emits('update:open', newValue)
}
}
)
// emits
//
async function submitForm() {
try {
await deviceRef.value.validate()
if (props.form.id) {
await updatePest(props.form)
proxy.$model.msgSuccess('修改成功')
} else {
await addPest(props.form)
proxy.$model.msgSuccess('新增成功')
}
dialogOpen.value = false
emits('success')
} catch (error) {
//
if (error !== 'cancel') {
proxy.$model.msgError('操作失败: ' + (error.message || '未知错误'))
}
}
}
//
function cancel() {
dialogOpen.value = false
deviceRef.value?.resetFields()
}
</script>

@ -1,21 +0,0 @@
/*
* @Author: chris
* @Date: 2025-09-05 10:12:41
* @LastEditors: chris
* @LastEditTime: 2025-09-09 09:50:16
*/
// 列配置
export const columnsConfig = [
{ key: 0, label: "设备编号", visible: true },
{ key: 1, label: "设备名称", visible: true },
{ key: 2, label: "型号", visible: true },
{ key: 3, label: "ip地址", visible: true },
{ key: 4, label: "状态", visible: true },
{ key: 5, label: "创建时间", visible: true },
];
// 状态颜色映射
export const statusColorMap = {
0: "success",
1: "danger",
};

@ -4,15 +4,18 @@
<!-- 左侧部分 -->
<el-col :span="4" class="page-left">
<el-card class="device-three-card">
<device-flat-list />
<template #header>
<span class="model-title">设备列表</span>
</template>
<device-flat-list :list="deviceList" @change="handleChangeDevice" />
</el-card>
</el-col>
<!-- 右侧部分 -->
<el-col :span="20" class="page-right">
<div class="page-title text-center">
<div class="page-title text-center" v-if="false">
检测设备名称
</div>
<el-card class="run-model-container">
<el-card class="run-model-container" v-if="false">
<template #header>
<span class="model-title">运行模式</span>
<el-segmented v-model="modelValue" :options="modelOptions" size="default" @change="handleModelChange">
@ -38,7 +41,7 @@
<el-row :gutter="10" justify="space-between">
<el-col :span="14" class="model-header__left">
<span class="model-title">历史数据</span>
<el-segmented v-model="historyValue" :options="historyOptions" size="default">
<el-segmented v-model="historyValue" :options="historyOptions" size="default" v-if="false">
<template #default="{ item }">
<p>{{ item.label }}</p>
</template>
@ -52,8 +55,9 @@
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
<el-button type="primary">查询</el-button>
<el-button type="primary" @click="getDeviceDataList"></el-button>
</el-col>
</el-row>
</template>
@ -64,11 +68,14 @@
<el-table-column prop="userName" label="操作用户" />
<el-table-column prop="createTime" label="操作时间" />
</el-table>
<el-table :data="operationData" style="width: 100%" v-if="historyValue === '1'">
<el-table-column prop="deviceAddr" label="地址码" />
<el-table-column prop="value" label="详情" />
<el-table-column prop="createTime" label="操作时间" />
<el-table :data="operationData" v-loading="loading" style="width: 100%" v-if="historyValue === '1'">
<el-table-column prop="name" label="名称" />
<el-table-column prop="value" label="数量" />
<el-table-column prop="unit" label="单位" />
<el-table-column prop="createTime" label="时间" />
</el-table>
<!-- 分页 -->
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getDeviceDataList" />
</div>
</el-card>
</el-col>
@ -78,8 +85,11 @@
<script setup>
import DeviceFlatList from '@/components/deviceFlatList';
import Pagination from '@/components/Pagination'
import useSettingsStore from '@/store/modules/settings';
import pestImg from '@/assets/images/devices/worm.png';
import pestImg from '@/assets/images/devices/pest.png';
import { listDevice } from '@/api/device';
import { listDeviceData } from '@/api/deviceData';
const settingsStore = useSettingsStore();
@ -93,13 +103,73 @@ const historyOptions = readonly([
{ label: '历史数据', value: '1' },
])
const { proxy } = getCurrentInstance()
const total = ref(0)
const modelValue = ref('0')
const historyValue = ref('0')
const dateRange = ref('')
const historyValue = ref('1')
const dateRange = ref(null)
const operationData = ref([])
const loading = ref(false)
const liveLoading = ref(false)
const deviceList = ref([])
const liveData = ref({})
const queryParams = ref({
pageNum: 1,
pageSize: 20,
deviceType: 1,
deviceId: null,
beginTime: null,
endTime: null
})
const hasTags = computed(() => settingsStore.tagsView);
watch(() => dateRange.value, () => {
if (!dateRange.value.length) return;
queryParams.value.beginTime = dateRange.value[0]
queryParams.value.endTime = dateRange.value[1]
})
getDeviceList();
getDeviceDataList();
async function getDeviceList () {
const params = {
pageNum: 1,
pageSize: 20,
}
loading.value = true;
try {
const res = await listDevice(params);
deviceList.value = res.rows;
} catch (error) {
proxy.$modal.msgError('获取虫情设备数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get pest list:', error)
} finally {
loading.value = false;
}
}
async function getDeviceDataList () {
loading.value = true;
try {
const res = await listDeviceData(queryParams.value);
operationData.value = res.rows;
total.value = res.total;
} catch (error) {
proxy.$modal.msgError('获取虫情设备实时数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get device live data:', error)
} finally {
loading.value = false;
}
}
function handleChangeDevice(device) {
queryParams.value.deviceId = device.id
getDeviceDataList()
}
function handleModelChange(val) {
console.log(val)
}
@ -119,7 +189,7 @@ function handleModelChange(val) {
}
.run-model-container {
@apply h-600px;
@apply h-450px;
:deep(.el-card__body) {
height: calc(100% - 54px);
@ -163,7 +233,7 @@ function handleModelChange(val) {
}
.history-container {
@apply mt-10px flex-1;
@apply flex-1;
}
.page-title {

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-08 16:17:54
* @LastEditors: chris
* @LastEditTime: 2025-09-09 16:50:46
* @LastEditTime: 2025-09-19 17:36:13
-->
<template>
<el-table
@ -17,7 +17,8 @@
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="参数名" align="center" prop="name" v-if="columns[0].visible"/>
<el-table-column label="数值" align="center" prop="value" v-if="columns[1].visible" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[2].visible">
<el-table-column label="单位" align="center" prop="unit" v-if="columns[2].visible" />
<el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[3].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
@ -34,7 +35,7 @@ const props = defineProps({
columns: { type: Array, required: true }
})
const emits = defineEmits(['selection-change', 'view', 'update', 'delete'])
const emits = defineEmits(['selection-change'])
function handleSelectionChange(selection) {
emits('selection-change', selection)

@ -2,16 +2,13 @@
* @Author: chris
* @Date: 2025-08-18 09:27:23
* @LastEditors: chris
* @LastEditTime: 2025-09-12 14:51:11
* @LastEditTime: 2025-09-19 17:28:35
-->
<script setup>
const props = defineProps({
params: {
type: Object,
default: () => ({
nodeId: null,
dateRange: []
})
default: () => ({})
},
soilOptions: {
type: Object,
@ -19,24 +16,33 @@ const props = defineProps({
}
})
const emits = defineEmits(['update:params'])
const emits = defineEmits(['update:params', 'reset', 'query', 'export'])
const queryRef = ref()
const queryParams = reactive({...props.params});
const queryParams = ref({...props.params});
const dateRange = ref(null);
watch(() => queryParams, (newVal) => {
emits('update:params', newVal)
watch(() => dateRange.value, () => {
if (!dateRange.value.length) return;
queryParams.value.beginTime = dateRange.value[0]
queryParams.value.endTime = dateRange.value[1]
})
watch(() => queryParams.value, (newVal) => {
emits('update:params', newVal)
}, {deep: true})
watch(() => props.params, (newVal) => {
Object.assign(queryParams, newVal)
Object.assign(queryParams.value, newVal)
})
function resetQuery() {
emits('reset')
dateRange.value = [];
emits('reset', queryRef.value)
}
function handleQuery() {
// query
emits('query')
}
@ -48,14 +54,14 @@ function exportData() {
<template>
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
<el-form-item label="参数" style="width: 260px;">
<el-select v-model="queryParams.nodeId" placeholder="请选择参数">
<el-option v-for="value in soilOptions" :key="value.id" :label="value.name" :value="value.id"></el-option>
<el-select v-model="queryParams.identifier" placeholder="请选择参数">
<el-option v-for="item in soilOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="记录时间">
<el-date-picker
style="width: 300px"
v-model="queryParams.dateRange"
v-model="dateRange"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="至"
@ -70,6 +76,3 @@ function exportData() {
</el-form-item>
</el-form>
</template>
<style lang="scss" scoped>
</style>

@ -2,13 +2,14 @@
* @Author: chris
* @Date: 2025-09-05 10:12:41
* @LastEditors: chris
* @LastEditTime: 2025-09-09 16:17:59
* @LastEditTime: 2025-09-19 17:35:58
*/
// 列配置
export const columnsConfig = [
{ key: 0, label: "参数名称", visible: true },
{ key: 1, label: "数值", visible: true },
{ key: 2, label: "记录时间", visible: true },
{ key: 2, label: "单位", visible: true },
{ key: 3, label: "记录时间", visible: true },
];
// 状态颜色映射
@ -16,3 +17,13 @@ export const statusColorMap = {
0: "success",
1: "danger",
};
export const defaultQueryParams = {
pageNum: 1,
pageSize: 10,
type: 2,
deviceId: null,
identifier: null,
beginTime: null,
endTime: null,
};

@ -2,33 +2,71 @@
* @Author: chris
* @Date: 2025-09-11 17:11:16
* @LastEditors: chris
* @LastEditTime: 2025-09-12 14:48:24
* @LastEditTime: 2025-09-19 17:26:21
-->
<script setup>
import HistoryTable from './components/HistoryTable.vue';
import SearchForm from './components/SearchForm.vue';
import { columnsConfig } from './config';
import { columnsConfig, defaultQueryParams } from './config';
import { useSettings } from '@/hooks/useSettings';
import { listDeviceData, exportDeviceData } from '@/api/deviceData';
import { useDict } from '@/utils/dict.js'
const { hasTags } = useSettings();
const { proxy } = getCurrentInstance();
const historyList = ref([]);
const queryParams = ref({});
const total = ref(0);
const loading = ref(false);
const queryParams = ref({...defaultQueryParams});
getHistoryList();
const { soil_type: soilTypeOptions } = useDict('soil_type')
function getHistoryList() {
getDeviceDataList();
async function getDeviceDataList () {
loading.value = true;
try {
const res = await listDeviceData(queryParams.value);
historyList.value = res.rows;
total.value = res.total;
} catch (error) {
proxy.$modal.msgError('获取土壤墒情设备实时数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get device live data:', error)
} finally {
loading.value = false;
}
}
function handleSelectionChange(selection) {
console.log(selection)
}
function handleQuery() {
queryParams.value.pageNum = 1
getDeviceDataList();
}
function handleReset(queryRef) {
queryParams.value = {...defaultQueryParams}
queryRef.resetFields()
getDeviceDataList()
}
async function handleExport() {
const fileName = await proxy.download('/business/device-data/export', queryParams.value, `土壤墒情设备数据_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}
</script>
<template>
<div :class="['soil-history-page', 'page-height', { 'hasTagsView': hasTags }]">
<search-form v-model:params="queryParams" class="mb-12px" />
<history-table :dataList="historyList" :columns="columnsConfig" @selection-change="handleSelectionChange" />
<!-- 搜索表单 -->
<search-form v-model:params="queryParams" :soil-options="soilTypeOptions" class="mb-12px" @query="handleQuery" @reset="handleReset" @export="handleExport" />
<!-- 历史记录表格 -->
<history-table :dataList="historyList" :columns="columnsConfig" :loading="loading" @selection-change="handleSelectionChange" />
<!-- 分页 -->
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getDeviceDataList" />
</div>
</template>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-08 16:17:54
* @LastEditors: chris
* @LastEditTime: 2025-09-09 16:50:46
* @LastEditTime: 2025-09-19 17:34:55
-->
<template>
<el-table
@ -17,7 +17,8 @@
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="参数名" align="center" prop="name" v-if="columns[0].visible"/>
<el-table-column label="数值" align="center" prop="value" v-if="columns[1].visible" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[2].visible">
<el-table-column label="单位" align="center" prop="unit" v-if="columns[2].visible" />
<el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[3].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
@ -34,7 +35,7 @@ const props = defineProps({
columns: { type: Array, required: true }
})
const emits = defineEmits(['selection-change', 'view', 'update', 'delete'])
const emits = defineEmits(['selection-change'])
function handleSelectionChange(selection) {
emits('selection-change', selection)

@ -2,16 +2,13 @@
* @Author: chris
* @Date: 2025-08-18 09:27:23
* @LastEditors: chris
* @LastEditTime: 2025-09-09 16:54:23
* @LastEditTime: 2025-09-19 17:33:45
-->
<script setup>
const props = defineProps({
params: {
type: Object,
default: () => ({
nodeId: null,
dateRange: []
})
default: () => ({})
},
weatherOptions: {
type: Object,
@ -19,21 +16,29 @@ const props = defineProps({
}
})
const emits = defineEmits(['update:params'])
const emits = defineEmits(['update:params', 'reset', 'query', 'export'])
const queryRef = ref()
const queryParams = reactive({...props.params});
const queryParams = ref({...props.params});
const dateRange = ref([]);
watch(() => queryParams, (newVal) => {
emits('update:params', newVal)
watch(() => dateRange.value, () => {
if (!dateRange.value.length) return;
queryParams.value.beginTime = dateRange.value[0]
queryParams.value.endTime = dateRange.value[1]
})
watch(() => queryParams.value, (newVal) => {
emits('update:params', newVal)
}, {deep: true})
watch(() => props.params, (newVal) => {
Object.assign(queryParams, newVal)
Object.assign(queryParams.value, newVal)
})
function resetQuery() {
emits('reset')
dateRange.value = [];
emits('reset', queryRef.value)
}
function handleQuery() {
@ -48,14 +53,14 @@ function exportData() {
<template>
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
<el-form-item label="参数" style="width: 260px;">
<el-select v-model="queryParams.nodeId" placeholder="请选择参数">
<el-option v-for="value in weatherOptions" :key="value.id" :label="value.name" :value="value.id"></el-option>
<el-select v-model="queryParams.identifier" placeholder="请选择参数">
<el-option v-for="item in weatherOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="记录时间">
<el-date-picker
style="width: 300px"
v-model="queryParams.dateRange"
v-model="dateRange"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="至"
@ -70,6 +75,3 @@ function exportData() {
</el-form-item>
</el-form>
</template>
<style lang="scss" scoped>
</style>

@ -2,13 +2,14 @@
* @Author: chris
* @Date: 2025-09-05 10:12:41
* @LastEditors: chris
* @LastEditTime: 2025-09-09 16:17:59
* @LastEditTime: 2025-09-19 17:35:15
*/
// 列配置
export const columnsConfig = [
{ key: 0, label: "参数名称", visible: true },
{ key: 1, label: "数值", visible: true },
{ key: 2, label: "记录时间", visible: true },
{ key: 2, label: "单位", visible: true },
{ key: 3, label: "记录时间", visible: true },
];
// 状态颜色映射
@ -16,3 +17,13 @@ export const statusColorMap = {
0: "success",
1: "danger",
};
export const defaultQueryParams = {
pageNum: 1,
pageSize: 10,
type: 3,
deviceId: null,
identifier: null,
beginTime: null,
endTime: null,
};

@ -2,38 +2,76 @@
* @Author: chris
* @Date: 2025-09-11 17:11:16
* @LastEditors: chris
* @LastEditTime: 2025-09-11 17:21:28
* @LastEditTime: 2025-09-19 11:12:17
-->
<script setup>
import HistoryTable from './components/HistoryTable.vue';
import SearchForm from './components/SearchForm.vue';
import { columnsConfig } from './config';
import { columnsConfig, defaultQueryParams } from './config';
import { useSettings } from '@/hooks/useSettings';
import { listDeviceData, exportDeviceData } from '@/api/deviceData';
import { useDict } from '@/utils/dict.js'
const { hasTags } = useSettings();
const { proxy } = getCurrentInstance();
const historyList = ref([]);
const queryParams = ref({});
const total = ref(0);
const loading = ref(false);
const queryParams = ref({...defaultQueryParams});
getHistoryList();
const { weather_type: weatherTypeOptions } = useDict('weather_type')
function getHistoryList() {
getDeviceDataList();
async function getDeviceDataList () {
loading.value = true;
try {
const res = await listDeviceData(queryParams.value);
historyList.value = res.rows;
total.value = res.total;
} catch (error) {
proxy.$modal.msgError('获取土壤墒情设备实时数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get device live data:', error)
} finally {
loading.value = false;
}
}
function handleSelectionChange(selection) {
console.log(selection)
}
function handleQuery() {
queryParams.value.pageNum = 1
getDeviceDataList();
}
function handleReset(queryRef) {
queryParams.value = {...defaultQueryParams}
queryRef.resetFields()
getDeviceDataList()
}
async function handleExport() {
const fileName = await proxy.download('/business/device-data/export', queryParams.value, `土壤墒情设备数据_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}
</script>
<template>
<div :class="['weather-history-page', 'page-height', { 'hasTagsView': hasTags }]">
<search-form v-model:params="queryParams" class="mb-12px" />
<history-table :dataList="historyList" :columns="columnsConfig" @selection-change="handleSelectionChange" />
<div :class="['soil-history-page', 'page-height', { 'hasTagsView': hasTags }]">
<!-- 搜索表单 -->
<search-form v-model:params="queryParams" :weather-options="weatherTypeOptions" class="mb-12px" @query="handleQuery" @reset="handleReset" @export="handleExport" />
<!-- 历史记录表格 -->
<history-table :dataList="historyList" :columns="columnsConfig" :loading="loading" @selection-change="handleSelectionChange" />
<!-- 分页 -->
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getDeviceDataList" />
</div>
</template>
<style lang="scss" scoped>
.weather-history-page {
.soil-history-page {
@apply p-20px;
}
</style>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-09-09 15:30:24
* @LastEditors: chris
* @LastEditTime: 2025-09-12 17:36:39
* @LastEditTime: 2025-09-22 17:25:39
-->
<script setup>
import weatherImg from '@/assets/images/devices/weather.png'
@ -11,7 +11,7 @@ const testDataList = [
{
id: 1,
title: '温度',
type: 'temperature',
type: 'Temperature',
value: '25',
icon: 'temperature2',
color: '#FF6B6B',
@ -19,58 +19,40 @@ const testDataList = [
},
{
id: 2,
title: '湿度',
type: 'humidity',
value: '60',
icon: 'humidity',
color: '#4ECDC4',
unit: '%',
},
{
id: 3,
title: '光照',
type: 'light',
type: 'Illuminance',
value: '10000',
icon: 'light',
color: '#FFD166',
unit: 'lux',
},
{
id: 4,
id: 3,
title: '降雨量',
type: 'rainfall',
type: 'DailyRainfall',
value: '10',
icon: 'rain',
color: '#6A0572',
unit: 'mm',
},
{
id: 5,
id: 4,
title: '风向',
type: 'windDirection',
type: 'WindDirection',
value: '东南风',
icon: 'wind',
color: '#1A535C',
unit: '°',
},
{
id: 6,
id: 5,
title: '风速',
type: 'windSpeed',
type: 'WindSpeed',
value: '2',
icon: 'windPower',
color: '#77DD77',
unit: 'm/s',
},
{
id: 7,
title: '气压',
type: 'pressure',
value: '1013',
icon: 'hpa',
color: '#845EC2',
unit: 'hPa',
}
]
const emits = defineEmits([ 'select' ])
@ -147,7 +129,7 @@ $transition-base: all 0.3s ease;
//
.weather-photo {
@apply w-full h-480px rounded-8px overflow-hidden;
@apply w-full h-600px rounded-8px overflow-hidden;
box-shadow: $shadow-normal;
transition: $transition-base;
}

@ -2,12 +2,13 @@
* @Author: chris
* @Date: 2025-09-12 17:20:00
* @LastEditors: chris
* @LastEditTime: 2025-09-11 17:35:22
* @LastEditTime: 2025-09-22 17:15:25
* @Description: 气象曲线图组件支持7天/30/90天数据切换
-->
<script setup>
import { ref, watch, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { weatherDict, weatherTypes } from '../config'
// props
const props = defineProps({
@ -17,7 +18,7 @@ const props = defineProps({
required: true,
validator: (value) => {
//
const validTypes = ['temperature', 'humidity', 'rainfall', 'windSpeed', 'pressure', 'light']
const validTypes = weatherTypes
return validTypes.includes(value)
}
},
@ -62,171 +63,172 @@ const chartInstance = ref(null)
const chartContainer = ref(null)
//
const weatherTypeConfig = {
temperature: {
name: '温度',
unit: '℃',
color: '#FF6B6B',
yAxis: {
name: '温度 (℃)',
min: function(value) { return value.min - 2; },
max: function(value) { return value.max + 2; },
show: true,
axisLabel: {
show: true,
formatter: '{value}',
color: '#333',
fontSize: 12
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
humidity: {
name: '湿度',
unit: '%',
color: '#4ECDC4',
yAxis: {
name: '湿度 (%)',
min: 0,
max: 100,
show: true,
axisLabel: {
show: true,
formatter: '{value}'
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
rainfall: {
name: '降雨量',
unit: 'mm',
color: '#6A0572',
yAxis: {
name: '降雨量 (mm)',
min: 0,
show: true,
axisLabel: {
show: true,
formatter: '{value}'
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
windSpeed: {
name: '风速',
unit: 'm/s',
color: '#77DD77',
yAxis: {
name: '风速 (m/s)',
min: 0,
show: true,
axisLabel: {
show: true,
formatter: '{value}'
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
pressure: {
name: '气压',
unit: 'hPa',
color: '#845EC2',
yAxis: {
name: '气压 (hPa)',
min: function(value) { return value.min - 5; },
max: function(value) { return value.max + 5; },
show: true,
axisLabel: {
show: true,
formatter: '{value}',
color: '#333',
fontSize: 12
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
light: {
name: '光照',
unit: 'lux',
color: '#FFD166',
yAxis: {
name: '光照 (lux)',
min: 0,
show: true, // Y
axisLabel: {
show: true, //
formatter: '{value}'
},
axisTick: {
show: true // 线
},
axisLine: {
show: true // 线
},
splitLine: {
show: true, // 线
lineStyle: {
type: 'dashed'
}
}
}
}
}
let weatherTypeConfig = null
// const weatherTypeConfig = {
// temperature: {
// name: '',
// unit: '',
// color: '#FF6B6B',
// yAxis: {
// name: ' ()',
// min: function(value) { return value.min - 2; },
// max: function(value) { return value.max + 2; },
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}',
// color: '#333',
// fontSize: 12
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// humidity: {
// name: '湿',
// unit: '%',
// color: '#4ECDC4',
// yAxis: {
// name: '湿 (%)',
// min: 0,
// max: 100,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}'
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// rainfall: {
// name: '',
// unit: 'mm',
// color: '#6A0572',
// yAxis: {
// name: ' (mm)',
// min: 0,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}'
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// windSpeed: {
// name: '',
// unit: 'm/s',
// color: '#77DD77',
// yAxis: {
// name: ' (m/s)',
// min: 0,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}'
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// pressure: {
// name: '',
// unit: 'hPa',
// color: '#845EC2',
// yAxis: {
// name: ' (hPa)',
// min: function(value) { return value.min - 5; },
// max: function(value) { return value.max + 5; },
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}',
// color: '#333',
// fontSize: 12
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// light: {
// name: '',
// unit: 'lux',
// color: '#FFD166',
// yAxis: {
// name: ' (lux)',
// min: 0,
// show: true, // Y
// axisLabel: {
// show: true, //
// formatter: '{value}'
// },
// axisTick: {
// show: true // 线
// },
// axisLine: {
// show: true // 线
// },
// splitLine: {
// show: true, // 线
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// }
// }
//
const initChart = () => {
@ -256,7 +258,7 @@ const updateChart = () => {
const option = {
title: {
text: `${config.name}趋势图 (近${props.selectedTimeRange})`,
text: `${config.name}趋势图 (近24小时)`,
left: 'center',
textStyle: {
fontSize: 16,
@ -383,6 +385,7 @@ watch(
//
onMounted(() => {
weatherTypeConfig = createWeatherConfig()
initChart()
})
@ -398,17 +401,52 @@ onUnmounted(() => {
defineExpose({
refreshChart: updateChart
})
function createWeatherConfig () {
const config = {}
weatherTypes.forEach(type => {
const typeDict = weatherDict[type]
if (!typeDict) return
config[type] = {
name: typeDict.name,
unit: typeDict.unit,
color: typeDict.color,
yAxis: {
name: `${typeDict.name} (${typeDict.unit})`,
min: 0,
show: true, // Y
axisLabel: {
show: true, //
formatter: '{value}'
},
axisTick: {
show: true // 线
},
axisLine: {
show: true // 线
},
splitLine: {
show: true, // 线
lineStyle: {
type: 'dashed'
}
}
}
}
})
return config
}
</script>
<template>
<div class="weather-chart-container">
<!-- 时间选择器 -->
<div class="chart-controls">
<el-radio-group v-model="selectedDays" class="time-range-selector">
<!-- <el-radio-group v-model="selectedDays" class="time-range-selector">
<el-radio-button :value="7" :disabled="loading">近7天</el-radio-button>
<el-radio-button :value="30" :disabled="loading">近30天</el-radio-button>
<el-radio-button :value="90" :disabled="loading">近90天</el-radio-button>
</el-radio-group>
</el-radio-group> -->
<!-- 加载状态指示器 -->
<!-- <div v-if="loading" class="loading-indicator">

@ -0,0 +1,34 @@
export const weatherTypes = [
"Temperature",
"Illuminance",
"WindSpeed",
"WindDirection",
"DailyRainfall",
];
export const weatherDict = {
Temperature: {
name: "温度",
unit: "°C",
color: "#FF6B6B",
},
Illuminance: {
name: "光照",
unit: "lux",
color: "#FFD166",
},
WindSpeed: {
name: "风速",
unit: "km/h",
color: "#009688",
},
WindDirection: {
name: "风向",
unit: "°",
color: "#845EC2",
},
DailyRainfall: {
name: "降雨量",
unit: "mm",
color: "#6A0572",
},
};

@ -2,46 +2,107 @@
* @Author: chris
* @Date: 2025-09-05 09:29:59
* @LastEditors: chris
* @LastEditTime: 2025-09-11 17:11:53
* @LastEditTime: 2025-09-22 17:23:27
-->
<script setup>
import LiveData from './components/LiveData.vue'
import WeatherChart from './components/WeatherChart.vue';
import { useSettings } from '@/hooks/useSettings';
import { getFormattedWeatherData } from './mock';
// import { getFormattedWeatherData } from './mock';
import { getDeviceDataAnalysis } from '@/api/deviceData';
import { formatDate } from '@/utils';
import { weatherTypes } from './config';
const { hasTags } = useSettings()
const { proxy } = getCurrentInstance()
const liveData = ref({})
const chartData = ref({ dates: [], values: []})
const weatherType = ref('temperature')
const weatherType = ref('Temperature')
const weatherData = ref({})
const selectedTimeRange = ref(7)
const chartLoading = ref(false)
const loading = ref(false)
getChartData()
// getChartData()
getDeviceAnalysisData()
async function getDeviceAnalysisData() {
loading.value = true
const beginTime = formatDate(new Date().getTime() - 24 * 60 * 60 * 1000)
const endTime = formatDate(new Date().getTime())
try {
const res = await getDeviceDataAnalysis({ type: 3 })
weatherData.value = formatData(res.rows)
chartData.value =createChartData(weatherData.value, weatherType.value)
} catch (error) {
proxy.$modal.msgError('获取气象分析数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get device live data:', error)
} finally {
loading.value = false;
}
function getChartData() {
chartLoading.value = true
setTimeout(() => {
console.log(getFormattedWeatherData(weatherType.value, selectedTimeRange.value))
chartData.value = getFormattedWeatherData(weatherType.value, selectedTimeRange.value)
chartLoading.value = false
}, 300)
return chartData.value
}
// function getChartData() {
// chartLoading.value = true
// setTimeout(() => {
// console.log(getFormattedWeatherData(weatherType.value, selectedTimeRange.value))
// chartData.value = getFormattedWeatherData(weatherType.value, selectedTimeRange.value)
// chartLoading.value = false
// }, 300)
// return chartData.value
// }
function handleSelectionChange(selection) {
console.log(selection)
}
function handleSelectWeatherItem(item) {
weatherType.value = item.type
getChartData()
chartData.value = createChartData(weatherData.value, weatherType.value)
}
function handleTimeRangeChange(range) {
selectedTimeRange.value = range
getChartData()
}
function createChartData(data, type) {
const dateList = getHourList()
return {
dates: dateList,
values: data[type] || []
}
}
function formatData (data) {
const result = {};
data.sort((a,b) => weatherTypes.indexOf(a.identifier) - weatherTypes.indexOf(b.identifier))
data.forEach(item => {
const identifier = item.identifier;
if (!identifier || identifier === 'null') return false;
result[identifier] ? result[identifier].push(item.value) : (result[identifier] = [item.value])
})
console.log(result)
// TODO: ,
delete result.Humidity
delete result.WindPower
delete result.DailyRainfall
return result
}
function getHourList () {
const hours = [];
const now = new Date();
for (let i = 23; i >= 0; i--) {
const hourDate = new Date(now);
hourDate.setHours(hourDate.getHours() - i);
const hourStr = hourDate.getHours().toString().padStart(2, '0');
hours.push(`${hourStr}:00`);
}
return hours;
}
</script>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-01-13 09:33:28
* @LastEditors: chris
* @LastEditTime: 2025-08-14 11:51:41
* @LastEditTime: 2025-09-16 10:07:43
*/
import { defineConfig, loadEnv } from "vite";
import path from "path";
@ -38,7 +38,7 @@ export default defineConfig(({ mode, command }) => {
// https://cn.vitejs.dev/config/#server-proxy
"/dev-api": {
// target: "http://192.168.0.111:8080",
target: "http://aeo.gdguanhui.com/aeo",
target: "http://goose.gdguanhui.com/leilinglitchi",
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, ""),
},

Loading…
Cancel
Save