agri-app/pages/index.vue

774 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page-container">
<z-paging ref="home" refresher-only bg-color="radial-gradient(circle at top left, #E8F4F8 40%, #F8FCFE 100%)"
:show-empty="false" :show-footer="false" @onRefresh="refresh">
<template #refresher="{refresherStatus}">
<!-- 此处的custom-refresh为demo中自定义的组件非z-paging的内置组件请在实际项目中自行创建这里插入什么view下拉刷新就显示什么view -->
<custom-refresher :status="refresherStatus" />
</template>
<template #top>
<uni-row class="demo-uni-row" >
<uni-col :span="7">
<uni-data-select align="left" :clear="false" v-model="value" @change="change" :localdata="range" ></uni-data-select>
</uni-col>
<uni-col :span="17">
<!-- confirm:confirm; input:change; cancel:
clearfocus:blur -->
<uni-search-bar bgColor="#fbfdfe"
v-model="searchValue"
cancelButton="always"
@input="input"
clearButton="always"
@cancel="cancel"
@clear="cancel"
placeholder="请输入搜索内容">
<template v-slot:searchIcon>
<yt-scanCode @getScanCode="getScanCode"></yt-scanCode>
</template>
</uni-search-bar>
</uni-col>
</uni-row>
</template>
<uni-section class="mb-10" title="温室列表" style="background:transparent;" type="line">
<template v-slot:right>
<view>
<text style="color: #f64040">{{ dataDetails.online }} 在线 </text>
<text style="color: #2d56d8;margin-left: 10rpx">{{ dataDetails.offline }} 离线</text>
</view>
</template>
<scroll-view
scroll-y
class="list-scroll"
@scrolltolower="loadMore"
lower-threshold="50"
scroll-with-animation
>
<uni-swipe-action>
<uni-swipe-action-item
v-for="(item, index) in listData"
:key="index"
@click="onDeleteItem($event, item)"
:class="['list-item', { 'list-item-disabled': item.online === '离线' }]"
:right-options="options">
<view class="item-card"
@click="onItemTap(item)">
<view class="item-title">
<view class="title-text">{{ item.agriName }}</view>
<text :class="['tag','tag-status',{'tag-manual':item.workModeDesc==='手动模式'}]" v-if="item.workModeDesc">{{ item.workModeDesc }}</text>
<text :class="['tag','tag-online',{'tag-offline':item.online==='离线'}]" v-if="item.online">{{ item.online }}</text>
</view>
<view class="item-subtitle">
设备编号:{{ item.imei }}
</view>
<view class="item-tags">
<text class="tag tag-temp1">温度1: {{ item.temp1 }}℃</text>
<text class="tag tag-temp2">温度2: {{ item.temp2 }}℃</text>
<text class="tag tag-temp3">温度3: {{ item.temp3 }}℃</text>
<text class="tag tag-temp4">温度4: {{ item.temp4 }}℃</text>
</view>
</view>
</uni-swipe-action-item>
<view v-if="isLoading" class="loading-more">
<uni-icons type="spinner-cycle" size="20" color="#999"></uni-icons>
<text>加载中...</text>
</view>
<view v-else-if="noMore" class="no-more">没有更多数据了</view>
<!-- v-if="store.getters.name !== 'admin'"-->
<view class="add-agri" @tap="handleAddAgri" v-if="!isLoading">
<image :src="imageSrc" :style="'width:'+80+'rpx;height:'+80+'rpx'"></image>
<text class="text"></text>
</view>
</uni-swipe-action>
</scroll-view>
</uni-section>
<!-- <uni-fab ref="fab" :pattern="pattern" :content="content" :horizontal="horizontal" :vertical="vertical"-->
<!-- :direction="direction" @trigger="trigger" @fabClick="fabClick" />-->
</z-paging>
<add-agri ref="addAgri" @reload="getListData"/>
</view>
</template>
<script>
import ZPaging from "../uni_modules/z-paging/components/z-paging/z-paging.vue";
import CustomRefresher from "../components/custom-refresher/custom-refresher.vue";
import { getAgriInfo} from "../api/system/assets/agri";
import store from "../store";
import TimeUtil from "../utils/TimeUtil";
import {getNewSpecialData} from "../api/data/specialData";
import AddAgri from "../components/addAgri/addAgri.vue";
import {removeAgri} from "../api/system/assets/userAgri";
import * as mqttUtil from "../utils/mqtt";
import {getAgriStatus} from "../api/system/mqtt";
export default {
computed: {
store() {
return store
}
},
components: {AddAgri, CustomRefresher, ZPaging},
data() {
return {
listData: [],
datas: [],
pageSize: 6,
isLoading: false,
noMore: false,
total: 20, // 模拟总条数
value: 0,
options: [
{
text: '删除',
icon: 'delete',
style: {
backgroundColor: '#E83A30'
}
}
],
range: [{
"value": 0,
"text": "大棚名称",
}, {
"value": 1,
"text": "设备编号",
}],
imageSrc:'/static/agri.png',
scrollHeight: 0,
// horizontal: 'right',
// vertical: 'bottom',
// direction: 'horizontal',
searchValue: null,
imeiToDeviceMap: new Map(),
dataDetails: {
tip: null,
online: 0,
offline: 0
}
/* pattern: {
color: '#7A7E83',
backgroundColor: '#fff',
selectedColor: '#007AFF',
buttonColor: '#007AFF',
iconColor: '#fff'
},
content: [{
iconPath: '/static/image.png',
selectedIconPath: '/static/image-active.png',
text: '添加大棚',
active: false
},
{
iconPath: '/static/home.png',
selectedIconPath: '/static/home-active.png',
text: '',
active: false
},
{
iconPath: '/static/star.png',
selectedIconPath: '/static/star-active.png',
text: '收藏',
active: false
}
]*/
}
},
onBackPress() {
/* if (this.$refs.fab.isShow) {
this.$refs.fab.close()
return true
}
return false*/
},
onShow() {
console.info("首页在线状态注册回调")
this.$modal.loading("数据加载中,请耐心等待...")
this.refresh();
},
onLoad() {
},
onHide() {
console.info("首页在线状态注销回调")
mqttUtil.removeOnMessageCallback();
},
onReady() {
},
methods: {
getScanCode(res){
this.searchValue = res;
this.value = 1;
if (this.searchValue) {
this.change(this.searchValue)
}
},
handleAddAgri() {
this.$refs.addAgri.open();
// 这里可以添加跳转到添加大棚页面的逻辑
// uni.navigateTo({ url: '/pages/add-agri/add-agri' })
},
change(e) {
this.input(this.searchValue)
},
input(res) {
if (!res) {
this.cancel(res)
return;
}
this.listData = this.datas.filter(item =>
item[this.value === 0 ? 'agriName' : 'imei'].includes(res));
},
cancel(res) {
this.listData = [...this.datas]
},
/*trigger(e) {
console.log(e)
this.content[e.index].active = !e.item.active
uni.showModal({
title: '提示',
content: `您${this.content[e.index].active ? '选中了' : '取消了'}${e.item.text}`,
success: function(res) {
if (res.confirm) {
console.log('用户点击确定')
} else if (res.cancel) {
console.log('用户点击取消')
}
}
})
},
fabClick() {
uni.showToast({
title: '点击了悬浮按钮',
icon: 'none'
})
},*/
// 模拟获取列表数据
getListData() {
this.noMore = false;
this.listData = [];
this.isLoading = true
getAgriInfo().then(response => {
if (response.code === 200) {
// 先清空旧索引
this.imeiToDeviceMap.clear();
// 1. 用 map 处理数据,返回新数组,避免 forEach 返回 undefined
// 2. 区分刷新/加载更多,更新 listData
this.listData = response.data.map(item => {
const deviceItem = {
...item,
deviceStatus: TimeUtil.isLessThanSpecifiedSeconds(item.time, 90) ? '在线' : '离线',
online: TimeUtil.isLessThanSpecifiedSeconds(item.time, 90) ? '在线' : '离线',
agriName: item.agriName || '未知大棚',
imei: `${item.imei || '未知'}`,
workModeDesc: item.workMode === 0 ? '手动模式':'自动模式',
workMode: item.workMode,
temp1: item.temp1,
temp2: item.temp2,
temp3: item.temp3,
temp4: item.temp4
};
// 存入索引Map
this.imeiToDeviceMap.set(deviceItem.imei, deviceItem);
return deviceItem;
});
// 5. 判断是否还有更多数据
this.noMore = this.listData.length >= this.total;
if (store.getters && store.getters.name === 'admin') {
this.getNewSpecialData();
}
// 左上角详情
this.getDataDetails();
this.datas = [...this.listData]
this.getAgriStatus();
}
}).catch(err => {
console.error("获取大棚信息失败:", err);
}).finally(() => {
// 4. 无论成功失败,都结束加载状态
this.isLoading = false;
this.$modal.closeLoading()
this.$refs.home.complete();
});
},
// 加载更多(防抖:避免快速上拉重复触发)
loadMore() {
if (!this.isLoading && !this.noMore) {
}
},
refresh() {
mqttUtil.setOnMessageCallback(this.ackMessage);
this.getListData();
},
onDeleteItem(item, agri) {
if (item.content.text==='删除') {
uni.showModal({
title: '操作提示:',
content: `确定删除大棚【${agri.agriName}】?`,
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
removeAgri(agri).then(response => {
if (response.code === 200) {
this.$modal.msgSuccess("删除成功!")
this.getListData();
} else {
this.$modal.msgError("删除失败!")
}
})
}
}
});
}
},
// 点击列表项
onItemTap(item) {
var agri = JSON.stringify(
{
imei:item.imei,
agriName:item.agriName,
agriId:item.id,
workMode: item.workMode,
quiltNum:item.quiltNum,
filmNum:item.filmNum,
blindNum:item.blindNum
}
);
this.$tab.navigateTo('/pages/home/control/index?agriInfo='+encodeURIComponent(agri))
},
getNewSpecialData() {
getNewSpecialData().then(response => {
if (response.code === 200 && response.data) {
this.makeSpecialData(response.data);
}
this.datas = [...this.listData]
})
},
makeSpecialData: function (msgData){
const online = TimeUtil.isLessThanSpecifiedSeconds(msgData.ts,90)?'在线':'离线';
const deviceConfigs = [
{ agriName: "八方南棚", imei: "A", temps: [1,2,3,4] },
{ agriName: "九方春棚", imei: "B", temps: [5,6,7,8] },
{ agriName: "十二方棚", imei: "C", temps: [9,10,11,12] }
];
const devices = deviceConfigs.map(({agriName, imei, temps}) => ({
agriName,
imei,
temp1: msgData[`temp${temps[0]}`],
temp2: msgData[`temp${temps[1]}`],
temp3: msgData[`temp${temps[2]}`],
temp4: msgData[`temp${temps[3]}`],
online
}));
this.listData.push(...devices);
// 状态和温度不需要监听不需要搞
// devices.forEach(d => this.imeiToDeviceMap.set(d.imei, d));
},
ackMessage(topic, payload) {
const regex = /^device\/\d+\/status$/;
const regex1 = /^frontend\/.+\/dtu\/\d+\/listener$/;
if (!regex.test(topic) && !regex1.test(topic)) return;
let msgData = {};
// 优化捕获JSON解析异常
try {
msgData = JSON.parse(payload);
} catch (e) {
console.error("MQTT消息解析失败", e, payload);
return;
}
// 设备在线状态
if (regex.test(topic) && msgData.online && "time" in msgData && "imei" in msgData) {
this.updateDeviceStatusFast(msgData.imei, msgData.online)
}
// 温度
const allKeysNumeric2 = Object.keys(msgData).every(key => /^\d+$/.test(key));
if (regex1.test(topic) && Object.keys(msgData).length > 0 && allKeysNumeric2) {
this.updateTempFast(msgData, topic)
}
this.getDataDetails();
},
// 离线状态查询 是先获取大棚表,无线表不存在大棚表里
updateDeviceStatusFast(targetImei, isOnline) {
console.info("监听到状态",targetImei, isOnline)
// 直接从Map里取设备项无需遍历数组
const targetDevice = this.imeiToDeviceMap.get(targetImei);
if (!targetDevice) return;
// 直接修改对象属性,响应式正常生效
targetDevice.deviceStatus = isOnline;
targetDevice.online = isOnline;
},
updateTempFast: function (msgData,topic) {
const regexWithGroup = /^frontend\/(.+)\/dtu\/(\d+)\/listener$/;
const matchResult = topic.match(regexWithGroup);
const imei = matchResult[2]; // 提取结果:'1234567890'
// 直接从Map里取设备项无需遍历数组
const targetDevice = this.imeiToDeviceMap.get(imei);
if (!targetDevice) return;
const div10 = (v) => (v == null ? null : Math.round((Number(v) / 10) * 10) / 10);
// 直接修改对象属性,响应式正常生效
targetDevice.temp1 = div10(msgData['201']);
targetDevice.temp2 = div10(msgData['202']);
targetDevice.temp3 = div10(msgData['203']);
targetDevice.temp4 = div10(msgData['204']);
// if (imei === '862538065276061') {
// const deviceConfigs = [
// { agriName: "八方南棚", imei: "A", temps: [1,2,3,4] },
// { agriName: "九方春棚", imei: "B", temps: [5,6,7,8] },
// { agriName: "十二方棚", imei: "C", temps: [9,10,11,12] }
// ];
//
// deviceConfigs.forEach(({agriName, imei, temps}) => {
// var target = this.imeiToDeviceMap.get(imei);
// target.temp1 = div10(msgData[`20${temps[0]}`]);
// target.temp2 = div10(msgData[`20${temps[1]}`]);
// target.temp3 = div10(msgData[`20${temps[2]}`]);
// target.temp4 = div10(msgData[`20${temps[3]}`]);
// })
// }
},
getDataDetails() {
this.dataDetails = {
tip: null,
online: 0,
offline: 0
}
var online = 0;
var offline = 0;
this.listData.forEach(item => {
if (item.online === '离线') {
offline += 1;
} else {
online += 1;
}
})
this.dataDetails = {
tip: null,
online: online,
offline: offline,
}
},
getAgriStatus() {
if (!this.listData || this.listData.length === 0) return []; // 空列表处理
const allImei = this.listData.map(item => item.imei).filter(imei => imei);
getAgriStatus([...new Set(allImei)])
}
}
}
</script>
<style scoped>
/* 适配小程序/H5的盒模型避免padding导致宽度溢出 */
page {
box-sizing: border-box;
}
.page-container {
padding: 20rpx;
box-sizing: border-box;
}
.list-scroll {
/* 把背景色改为透明,才能透出背景渐变 */
background-color: transparent;
border-radius: 8rpx;
overflow: hidden;
margin-top: 10rpx;
/* 新增H5端避免滚动条样式不一致 */
scrollbar-width: thin;
/* 去掉固定高度,改为自适应(避免小程序滚动异常) */
height: calc(100vh - 180rpx);
/* #ifdef MP-WEIXIN */
height: calc(100vh - var(--status-bar-height) - 180rpx);
/* #endif */
}
/* H5端滚动条样式优化可选 */
.list-scroll::-webkit-scrollbar {
width: 4rpx;
}
.list-scroll::-webkit-scrollbar-thumb {
background-color: #e0e0e0;
border-radius: 2rpx;
}
.item-card {
background-color: #FFFFFF;
border-radius: 12rpx;
box-shadow: 8rpx 5rpx 10rpx #999;
margin-bottom: 20rpx;
// #ifndef H5
//margin-right: 10rpx;
// #endif
}
/deep/ .uni-swipe {
// #ifdef MP-WEIXIN
height: 214rpx !important;
// #endif
}
/deep/ .uni-swipe_button {
// #ifdef MP-WEIXIN
height: 198rpx !important;
// #endif
// #ifdef H5
height: 192rpx !important;
// #endif
margin-right: 10rpx;
border-radius: 12rpx;
box-shadow: 8rpx 5rpx 10rpx #999;
}
.list-item,/deep/.tn-swipe-action-item {
height: 190rpx !important;
/* 移除border-bottom改为margin-top并且把列表项背景设为半透明白色 */
// #ifdef H5
margin-bottom: 20rpx;
background-color: #FFFFFF;
border-radius: 12rpx;
/* 新增:点击态样式,双端统一 */
transition: all 0.2s ease;
overflow: hidden;
box-shadow: 8rpx 5rpx 10rpx #999;
pointer-events: auto !important;
// #endif
}
.list-item:active {
transform: scale(0.98);
}
.item-card:active {
background-color: #F5F5F5;
// #ifdef MP-WEIXIN
transform: scale(0.98);
// #endif
}
.item-title {
font-size: 28rpx;
line-height: 52rpx;
font-weight: bolder;
color: #333;
/* 核心flex布局让文字和标签在同一行 */
display: flex;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 14rpx 25rpx 0 25rpx;
}
.title-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 在线标签样式 */
.tag-online {
font-size: 20rpx;
border-radius: 4rpx;
background-color: #f0f9eb;
color: #67c23a;
white-space: nowrap;
line-height: 30rpx;
/* 兜底如果flex失效用margin-left:auto强制居右 */
margin-left: auto;
display: inline-block; /* 确保样式生效 */
}
.tag-status.tag-manual {
background-color: #dde3f6;
color: #7491ef;
}
/* 离线:深灰色系 */
.tag-offline {
background-color: #f2f2f2;
color: #c5c3c3;
}
.item-subtitle {
font-size: 26rpx;
color: #666;
line-height: 52rpx;
display: block;
padding: 0 25rpx;
}
.item-tags {
display: flex;
gap: 8rpx;
flex-wrap: wrap; /* 新增:标签过多时换行,避免溢出 */
padding: 14rpx 25rpx 28rpx 25rpx;
}
.tag {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 4rpx;
display: inline-block; /* 修复H5端padding不生效问题 */
}
.tag-status {
background-color: #f8f1f1;
color: #e66060;
margin-left: 20rpx;
font-weight: lighter;
line-height: 30rpx;
padding: 4rpx 6rpx;
font-size: 20rpx;
}
.tag-temp1 {
background-color: #e8f4ff;
color: #409eff;
}
.tag-temp2 {
background-color: #fdf4e4;
color: #f5961d;
}
.tag-temp3 {
background-color: #f4f4f5;
color: #909399;
}
.tag-temp4 {
background-color: #e7faf4;
color: #1bd1a7;
}
.loading-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx;
color: #999;
font-size: 26rpx;
gap: 10rpx;
margin-top: 20rpx;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12rpx;
}
.no-more {
/* 核心flex布局实现居中 */
display: flex;
align-items: center;
justify-content: center;
/* 占满滚动容器剩余高度,保证垂直居中 */
min-height: 200rpx; /* 最小高度,避免内容太少时不居中 */
/* 文字样式保留 */
color: #999;
font-size: 26rpx;
/* 可选:增加上下内边距,视觉更舒适 */
padding: 30rpx 0;
margin-top: 20rpx;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12rpx;
}
/* 新增:禁用状态样式 */
.list-item-disabled {
/* 文字置灰 */
color: #999;
/* 禁用光标 */
cursor: not-allowed;
/* 背景色变浅,视觉区分 */
background-color: #fafafa;
/* 可选:添加透明度 */
opacity: 0.7;
}
/* 禁用状态下的子元素也置灰 */
.list-item-disabled .tag {
color: #ccc;
background-color: #f8f8f8;
}
/deep/ .uni-searchbar {
padding: 20rpx 0;
}
/deep/ .uni-searchbar__box, /deep/ .uni-select {
box-shadow: 8rpx 5rpx 10rpx #999;
background: #fbfdfe;
border-radius: 0 !important;
height: 70rpx !important;
}
/deep/ .uni-searchbar__box {
border-radius: 0 10rpx 10rpx 0 !important;
}
.uni-stat__select {
padding: 20rpx 0;
}
/deep/ .uni-select {
border: 0;
// #ifdef MP-WEIXIN
margin-top: 20rpx !important;
// #endif
border-radius: 10rpx 0 0 10rpx !important;
}
/deep/ .uni-section {
background: transparent !important;
}
/* 强制隐藏搜索图标容器 */
/*/deep/ .uni-searchbar__box-icon-search {
display: none !important;
}*/
/deep/ .uni-section .uni-section-header {
padding: 12rpx 10rpx;
}
/* 添加大棚容器样式 */
.add-agri {
/* 核心flex布局实现一行显示 + 垂直居中 */
display: flex;
align-items: center;
/* 水平居中(可选,根据需求调整) */
justify-content: center;
/* 内边距,增加点击区域和视觉间距 */
padding: 30rpx 0;
/* 可选:添加背景和圆角,和列表项风格统一 */
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12rpx;
/* 点击态效果,和列表项保持一致 */
transition: all 0.2s ease;
}
.add-agri:active {
background-color: rgba(255, 255, 255, 0.95);
transform: scale(0.98);
}
/* 图片和文字的间距 */
.add-agri image {
margin-right: 20rpx;
/* 可选:图片垂直对齐,兜底兼容 */
vertical-align: middle;
}
/* 文字样式优化 */
.add-agri .text {
/* 文字大小,和标签风格统一 */
font-size: 26rpx;
/* 文字颜色(保留原有绿色) */
color: #1AAD19;
/* 可选:加粗,突出按钮感 */
font-weight: 500;
/* 兜底:垂直对齐 */
vertical-align: middle;
}
/deep/ .z-paging-content-fixed {
padding: 20rpx !important;
/* 可选防止margin塌陷加overflow */
overflow: hidden;
}
</style>