完善保险项目和养殖端小程序

This commit is contained in:
xuqiuyun
2025-09-26 18:45:42 +08:00
parent 00dfa83fd1
commit ec3f472641
58 changed files with 4866 additions and 2233 deletions

View File

@@ -8,6 +8,8 @@
"pages/device/device",
"pages/device/eartag/eartag",
"pages/device/collar/collar",
"pages/device/host/host",
"pages/device/fence/fence",
"pages/device/eartag-detail/eartag-detail",
"pages/device/eartag-add/eartag-add",
"pages/alert/alert"

View File

@@ -1,63 +1,257 @@
// 状态映射
const statusMap = {
'在线': '在线',
'离线': '离线',
'告警': '告警',
'online': '在线',
'offline': '离线',
'alarm': '告警'
}
// 佩戴状态映射
const wearStatusMap = {
1: '已佩戴',
0: '未佩戴'
}
// 连接状态映射
const connectStatusMap = {
1: '已连接',
0: '未连接'
}
// 设备状态映射
const deviceStatusMap = {
'使用中': '使用中',
'待机': '待机',
'维护': '维护',
'故障': '故障'
}
Page({
data: {
list: [], // 项圈数据列表
searchValue: '', // 搜索值
currentPage: 1, // 当前页码
total: 0, // 总数据量
pageSize: 10 // 每页数量
pageSize: 10, // 每页数量
totalPages: 0, // 总页数
pageNumbers: [], // 页码数组
isSearching: false, // 是否在搜索状态
searchResult: null // 搜索结果
},
onLoad() {
// 检查登录状态
if (!this.checkLoginStatus()) {
return
}
this.loadData()
},
// 检查登录状态
checkLoginStatus() {
const token = wx.getStorageSync('token')
const userInfo = wx.getStorageSync('userInfo')
if (!token || !userInfo) {
console.log('用户未登录,跳转到登录页')
wx.showModal({
title: '提示',
content: '请先登录后再使用',
showCancel: false,
success: () => {
wx.reLaunch({
url: '/pages/login/login'
})
}
})
return false
}
console.log('用户已登录:', userInfo.username)
return true
},
// 加载数据
loadData() {
const { currentPage, pageSize, searchValue } = this.data
const url = `https://ad.ningmuyun.com/api/smart-devices/collars?page=${currentPage}&limit=${pageSize}&deviceId=${searchValue}&_t=${Date.now()}`
// 检查登录状态
const token = wx.getStorageSync('token')
if (!token) {
wx.showToast({
title: '请先登录',
icon: 'none'
})
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login'
})
}, 1500)
return
}
wx.showLoading({ title: '加载中...' })
wx.request({
url,
header: {
'Authorization': 'Bearer ' + getApp().globalData.token // 添加认证头
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.data) {
const data = res.data.data
this.setData({
list: (data.items || []).map(item => ({
...item,
statusText: statusMap[item.status] || item.status
})),
total: data.total || 0
console.log('API响应:', res)
if (res.statusCode === 200 && res.data) {
// 处理API响应格式
const response = res.data
console.log('API响应数据:', response)
if (response.success && response.data) {
// 处理 {success: true, data: [...]} 格式
const data = response.data
const total = response.total || response.pagination?.total || 0
const totalPages = Math.ceil(total / this.data.pageSize)
const pageNumbers = this.generatePageNumbers(this.data.currentPage, totalPages)
this.setData({
list: Array.isArray(data) ? data.map(item => this.formatItemData(item)) : [],
total: total,
totalPages: totalPages,
pageNumbers: pageNumbers
})
} else if (response.data && response.data.items) {
// 处理 {data: {items: [...]}} 格式
const data = response.data
this.setData({
list: (data.items || []).map(item => this.formatItemData(item)),
total: data.total || 0
})
} else {
// 直接数组格式
this.setData({
list: Array.isArray(response) ? response.map(item => this.formatItemData(item)) : [],
total: response.length || 0
})
}
} else if (res.statusCode === 401) {
// 处理401未授权
wx.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
})
// 清除本地存储
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
// 跳转到登录页
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login'
})
}, 1500)
} else {
wx.showToast({
title: res.data.message || '数据加载失败',
title: res.data?.message || '数据加载失败',
icon: 'none'
})
}
},
fail: (err) => {
console.error('请求失败:', err)
wx.showToast({
title: err.errMsg.includes('401') ? '请登录后重试' : '请求失败',
title: err.errMsg?.includes('401') ? '请登录后重试' : '网络请求失败',
icon: 'none'
})
console.error(err)
},
complete: () => wx.hideLoading()
complete: () => {
wx.hideLoading()
}
})
},
// 格式化单个设备数据
formatItemData(item) {
return {
...item,
// 状态映射
statusText: statusMap[item.status] || item.status || '在线',
// 佩戴状态映射
wearStatusText: wearStatusMap[item.is_wear] || wearStatusMap[item.bandge_status] || '未知',
// 连接状态映射
connectStatusText: connectStatusMap[item.is_connect] || '未知',
// 设备状态映射
deviceStatusText: deviceStatusMap[item.deviceStatus] || item.deviceStatus || '未知',
// 格式化电池电量
batteryText: `${item.battery || item.batteryPercent || 0}%`,
// 格式化温度
temperatureText: `${item.temperature || item.raw?.temperature_raw || '0'}°C`,
// 格式化步数
stepsText: `${item.steps || 0}`,
// 格式化信号强度
signalText: item.rsrp ? `${item.rsrp}dBm` : '未知',
// 格式化GPS信号
gpsText: item.gpsSignal ? `${item.gpsSignal}颗卫星` : '无信号',
// 格式化位置信息
locationText: item.location || (item.longitude && item.latitude ? '有定位' : '无定位'),
// 格式化最后更新时间
lastUpdateText: item.lastUpdate || '未知',
// 格式化设备序列号
snText: item.sn ? `SN:${item.sn}` : '未知',
// 格式化更新间隔
updateIntervalText: item.updateInterval ? `${Math.floor(item.updateInterval / 1000)}` : '未知'
}
},
// 生成页码数组
generatePageNumbers(currentPage, totalPages) {
const pageNumbers = []
const maxVisible = 5 // 最多显示5个页码
if (totalPages <= maxVisible) {
// 总页数少于等于5页显示所有页码
for (let i = 1; i <= totalPages; i++) {
pageNumbers.push(i)
}
} else {
// 总页数大于5页智能显示页码
let start = Math.max(1, currentPage - 2)
let end = Math.min(totalPages, start + maxVisible - 1)
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1)
}
for (let i = start; i <= end; i++) {
pageNumbers.push(i)
}
}
return pageNumbers
},
// 上一页
onPrevPage() {
if (this.data.currentPage > 1) {
this.setData({
currentPage: this.data.currentPage - 1
}, () => {
this.loadData()
})
}
},
// 下一页
onNextPage() {
if (this.data.currentPage < this.data.totalPages) {
this.setData({
currentPage: this.data.currentPage + 1
}, () => {
this.loadData()
})
}
},
// 搜索输入
onSearchInput(e) {
this.setData({ searchValue: e.detail.value.trim() })
@@ -65,7 +259,138 @@ Page({
// 执行搜索
onSearch() {
this.setData({ currentPage: 1 }, () => this.loadData())
const searchValue = this.data.searchValue.trim()
if (!searchValue) {
wx.showToast({
title: '请输入项圈编号',
icon: 'none'
})
return
}
// 验证项圈编号格式(数字)
if (!/^\d+$/.test(searchValue)) {
wx.showToast({
title: '项圈编号只能包含数字',
icon: 'none'
})
return
}
// 设置搜索状态
this.setData({
isSearching: true,
searchResult: null,
currentPage: 1
})
// 执行精确搜索
this.performExactSearch(searchValue)
},
// 执行精确搜索
performExactSearch(searchValue) {
const url = `https://ad.ningmuyun.com/api/smart-devices/collars?page=1&limit=1&deviceId=${searchValue}&_t=${Date.now()}`
// 检查登录状态
const token = wx.getStorageSync('token')
if (!token) {
wx.showToast({
title: '请先登录',
icon: 'none'
})
return
}
wx.showLoading({ title: '搜索中...' })
wx.request({
url,
header: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
success: (res) => {
console.log('搜索API响应:', res)
if (res.statusCode === 200 && res.data) {
const response = res.data
if (response.success && response.data) {
const data = response.data
if (Array.isArray(data) && data.length > 0) {
// 找到匹配的设备
const device = this.formatItemData(data[0])
this.setData({
searchResult: device,
list: [], // 清空列表显示
total: 1,
totalPages: 1,
pageNumbers: [1]
})
wx.showToast({
title: '搜索成功',
icon: 'success'
})
} else {
// 没有找到匹配的设备
this.setData({
searchResult: null,
list: [],
total: 0,
totalPages: 0,
pageNumbers: []
})
wx.showToast({
title: '未找到该设备',
icon: 'none'
})
}
} else {
wx.showToast({
title: '搜索失败',
icon: 'none'
})
}
} else if (res.statusCode === 401) {
wx.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
})
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login'
})
}, 1500)
} else {
wx.showToast({
title: '搜索失败',
icon: 'none'
})
}
},
fail: (err) => {
console.error('搜索请求失败:', err)
wx.showToast({
title: '网络请求失败',
icon: 'none'
})
},
complete: () => {
wx.hideLoading()
}
})
},
// 清除搜索
clearSearch() {
this.setData({
searchValue: '',
isSearching: false,
searchResult: null,
currentPage: 1
})
this.loadData()
},
// 分页切换

View File

@@ -7,31 +7,211 @@
bindinput="onSearchInput"
value="{{searchValue}}"
/>
<button bindtap="onSearch">查询</button>
<button bindtap="onSearch" class="search-btn">查询</button>
<button wx:if="{{isSearching}}" bindtap="clearSearch" class="clear-btn">清除</button>
</view>
<!-- 搜索状态提示 -->
<view wx:if="{{isSearching}}" class="search-status">
<text>搜索项圈编号: {{searchValue}}</text>
</view>
<!-- 搜索结果 -->
<view wx:if="{{isSearching && searchResult}}" class="search-result">
<view class="result-header">
<text class="result-title">搜索结果</text>
<text class="result-subtitle">找到匹配的设备</text>
</view>
<view class="item search-item" bindtap="viewCollarDetail" data-id="{{searchResult.id}}">
<!-- 设备基本信息 -->
<view class="item-header">
<text class="device-sn">{{searchResult.snText}}</text>
<text class="device-status {{searchResult.status === '在线' ? 'online' : 'offline'}}">{{searchResult.statusText}}</text>
</view>
<!-- 设备详细信息 -->
<view class="item-content">
<view class="info-row">
<text class="label">项圈编号:</text>
<text class="value">{{searchResult.sn}}</text>
</view>
<view class="info-row">
<text class="label">佩戴状态:</text>
<text class="value {{searchResult.is_wear === 1 ? 'wear-on' : 'wear-off'}}">{{searchResult.wearStatusText}}</text>
</view>
<view class="info-row">
<text class="label">连接状态:</text>
<text class="value {{searchResult.is_connect === 1 ? 'connect-on' : 'connect-off'}}">{{searchResult.connectStatusText}}</text>
</view>
<view class="info-row">
<text class="label">电池电量:</text>
<text class="value battery-{{searchResult.batteryPercent > 50 ? 'high' : searchResult.batteryPercent > 20 ? 'medium' : 'low'}}">{{searchResult.batteryText}}</text>
</view>
<view class="info-row">
<text class="label">体温:</text>
<text class="value">{{searchResult.temperatureText}}</text>
</view>
<view class="info-row">
<text class="label">步数:</text>
<text class="value">{{searchResult.stepsText}}</text>
</view>
<view class="info-row">
<text class="label">信号强度:</text>
<text class="value">{{searchResult.signalText}}</text>
</view>
<view class="info-row">
<text class="label">GPS信号:</text>
<text class="value">{{searchResult.gpsText}}</text>
</view>
<view class="info-row">
<text class="label">位置状态:</text>
<text class="value">{{searchResult.locationText}}</text>
</view>
<view class="info-row">
<text class="label">最后更新:</text>
<text class="value">{{searchResult.lastUpdateText}}</text>
</view>
<view class="info-row">
<text class="label">更新间隔:</text>
<text class="value">{{searchResult.updateIntervalText}}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="item-actions">
<button class="btn-detail" size="mini" bindtap="viewCollarDetail" data-id="{{searchResult.id}}" catchtap="true">查看详情</button>
<button class="btn-edit" size="mini" bindtap="editCollar" data-id="{{searchResult.id}}" catchtap="true">编辑</button>
</view>
</view>
</view>
<!-- 无搜索结果 -->
<view wx:if="{{isSearching && !searchResult}}" class="no-result">
<view class="no-result-icon">🔍</view>
<text class="no-result-text">未找到项圈编号为 "{{searchValue}}" 的设备</text>
<button class="retry-btn" bindtap="clearSearch">重新搜索</button>
</view>
<!-- 数据列表 -->
<view class="list">
<view wx:if="{{!isSearching}}" class="list">
<block wx:for="{{list}}" wx:key="id">
<view class="item">
<text>项圈编号: {{item.deviceId}}</text>
<text>状态: {{item.status | statusText}}</text>
<text>电量: {{item.battery}}%</text>
<text>最后在线: {{item.lastOnlineTime}}</text>
<view class="item" bindtap="viewCollarDetail" data-id="{{item.id}}">
<!-- 设备基本信息 -->
<view class="item-header">
<text class="device-sn">{{item.snText}}</text>
<text class="device-status {{item.status === '在线' ? 'online' : 'offline'}}">{{item.statusText}}</text>
</view>
<!-- 设备详细信息 -->
<view class="item-content">
<view class="info-row">
<text class="label">项圈编号:</text>
<text class="value">{{item.sn}}</text>
</view>
<view class="info-row">
<text class="label">佩戴状态:</text>
<text class="value {{item.is_wear === 1 ? 'wear-on' : 'wear-off'}}">{{item.wearStatusText}}</text>
</view>
<view class="info-row">
<text class="label">连接状态:</text>
<text class="value {{item.is_connect === 1 ? 'connect-on' : 'connect-off'}}">{{item.connectStatusText}}</text>
</view>
<view class="info-row">
<text class="label">电池电量:</text>
<text class="value battery-{{item.batteryPercent > 50 ? 'high' : item.batteryPercent > 20 ? 'medium' : 'low'}}">{{item.batteryText}}</text>
</view>
<view class="info-row">
<text class="label">体温:</text>
<text class="value">{{item.temperatureText}}</text>
</view>
<view class="info-row">
<text class="label">步数:</text>
<text class="value">{{item.stepsText}}</text>
</view>
<view class="info-row">
<text class="label">信号强度:</text>
<text class="value">{{item.signalText}}</text>
</view>
<view class="info-row">
<text class="label">GPS信号:</text>
<text class="value">{{item.gpsText}}</text>
</view>
<view class="info-row">
<text class="label">位置状态:</text>
<text class="value">{{item.locationText}}</text>
</view>
<view class="info-row">
<text class="label">最后更新:</text>
<text class="value">{{item.lastUpdateText}}</text>
</view>
<view class="info-row">
<text class="label">更新间隔:</text>
<text class="value">{{item.updateIntervalText}}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="item-actions">
<button class="btn-detail" size="mini" bindtap="viewCollarDetail" data-id="{{item.id}}" catchtap="true">查看详情</button>
<button class="btn-edit" size="mini" bindtap="editCollar" data-id="{{item.id}}" catchtap="true">编辑</button>
</view>
</view>
</block>
</view>
<!-- 分页控件 -->
<view class="pagination">
<block wx:for="{{pages}}" wx:key="index">
<text
class="{{currentPage === item ? 'active' : ''}}"
bindtap="onPageChange"
data-page="{{item}}"
<view class="pagination" wx:if="{{!isSearching && totalPages > 1}}">
<view class="pagination-info">
<text>共 {{total}} 条数据,第 {{currentPage}} / {{totalPages}} 页</text>
</view>
<view class="pagination-buttons">
<button
class="page-btn prev-btn {{currentPage <= 1 ? 'disabled' : ''}}"
bindtap="onPrevPage"
disabled="{{currentPage <= 1}}"
>
{{item}}
</text>
</block>
上一页
</button>
<view class="page-numbers">
<block wx:for="{{pageNumbers}}" wx:key="index">
<text
class="page-number {{currentPage === item ? 'active' : ''}}"
bindtap="onPageChange"
data-page="{{item}}"
>
{{item}}
</text>
</block>
</view>
<button
class="page-btn next-btn {{currentPage >= totalPages ? 'disabled' : ''}}"
bindtap="onNextPage"
disabled="{{currentPage >= totalPages}}"
>
下一页
</button>
</view>
</view>
</view>

View File

@@ -6,13 +6,137 @@
.search-box {
display: flex;
margin-bottom: 20rpx;
gap: 15rpx;
align-items: center;
}
.search-box input {
flex: 1;
border: 1rpx solid #ddd;
padding: 10rpx 20rpx;
margin-right: 20rpx;
border: 2rpx solid #e8e8e8;
border-radius: 25rpx;
padding: 20rpx 30rpx;
font-size: 28rpx;
background: #fafafa;
transition: all 0.3s ease;
}
.search-box input:focus {
border-color: #1890ff;
background: #fff;
box-shadow: 0 0 0 4rpx rgba(24, 144, 255, 0.1);
}
.search-btn {
background: linear-gradient(135deg, #1890ff, #40a9ff);
color: white;
border: none;
border-radius: 25rpx;
padding: 20rpx 30rpx;
font-size: 28rpx;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
transition: all 0.3s ease;
}
.search-btn:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(24, 144, 255, 0.3);
}
.clear-btn {
background: linear-gradient(135deg, #ff4d4f, #ff7875);
color: white;
border: none;
border-radius: 25rpx;
padding: 20rpx 30rpx;
font-size: 28rpx;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(255, 77, 79, 0.3);
transition: all 0.3s ease;
}
.clear-btn:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
}
.search-status {
background: linear-gradient(135deg, #e6f7ff, #bae7ff);
border: 2rpx solid #91d5ff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
text-align: center;
}
.search-status text {
color: #1890ff;
font-size: 28rpx;
font-weight: bold;
}
.search-result {
margin-bottom: 30rpx;
}
.result-header {
background: linear-gradient(135deg, #f6ffed, #d9f7be);
border: 2rpx solid #b7eb8f;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
text-align: center;
}
.result-title {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #52c41a;
margin-bottom: 10rpx;
}
.result-subtitle {
display: block;
font-size: 24rpx;
color: #73d13d;
}
.search-item {
border: 2rpx solid #52c41a;
box-shadow: 0 4rpx 16rpx rgba(82, 196, 26, 0.2);
}
.no-result {
text-align: center;
padding: 80rpx 40rpx;
background: #fafafa;
border-radius: 12rpx;
margin-bottom: 30rpx;
}
.no-result-icon {
font-size: 80rpx;
margin-bottom: 30rpx;
}
.no-result-text {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 40rpx;
line-height: 1.5;
}
.retry-btn {
background: linear-gradient(135deg, #1890ff, #40a9ff);
color: white;
border: none;
border-radius: 25rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
}
.list {
@@ -21,26 +145,212 @@
.item {
padding: 20rpx;
border-bottom: 1rpx solid #eee;
margin-bottom: 20rpx;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
border: 1rpx solid #eee;
}
.item text {
display: block;
margin-bottom: 10rpx;
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 15rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.device-sn {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.device-status {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: bold;
}
.device-status.online {
background: #e8f5e8;
color: #52c41a;
}
.device-status.offline {
background: #fff2e8;
color: #fa8c16;
}
.item-content {
margin-bottom: 20rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
padding: 8rpx 0;
}
.info-row .label {
font-size: 28rpx;
color: #666;
min-width: 140rpx;
}
.info-row .value {
font-size: 28rpx;
color: #333;
text-align: right;
flex: 1;
}
/* 状态颜色 */
.wear-on {
color: #52c41a;
font-weight: bold;
}
.wear-off {
color: #ff4d4f;
font-weight: bold;
}
.connect-on {
color: #52c41a;
font-weight: bold;
}
.connect-off {
color: #ff4d4f;
font-weight: bold;
}
.battery-high {
color: #52c41a;
font-weight: bold;
}
.battery-medium {
color: #fa8c16;
font-weight: bold;
}
.battery-low {
color: #ff4d4f;
font-weight: bold;
}
.item-actions {
display: flex;
justify-content: flex-end;
gap: 20rpx;
padding-top: 15rpx;
border-top: 1rpx solid #f0f0f0;
}
.btn-detail {
background: #1890ff;
color: white;
border: none;
border-radius: 6rpx;
}
.btn-edit {
background: #52c41a;
color: white;
border: none;
border-radius: 6rpx;
}
.pagination {
margin-top: 40rpx;
padding: 30rpx;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
border: 2rpx solid #e8e8e8;
}
.pagination-info {
text-align: center;
margin-bottom: 30rpx;
font-size: 28rpx;
color: #666;
font-weight: 500;
background: rgba(255, 255, 255, 0.8);
padding: 15rpx;
border-radius: 12rpx;
border: 1rpx solid #e8e8e8;
}
.pagination-buttons {
display: flex;
justify-content: center;
align-items: center;
gap: 15rpx;
flex-wrap: wrap;
}
.pagination text {
margin: 0 10rpx;
padding: 5rpx 15rpx;
border: 1rpx solid #ddd;
.page-btn {
padding: 16rpx 28rpx;
border-radius: 12rpx;
font-size: 26rpx;
font-weight: 500;
border: 2rpx solid #d9d9d9;
background: linear-gradient(135deg, #fff, #f8f9fa);
color: #333;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
min-width: 80rpx;
}
.pagination .active {
background-color: #07C160;
.page-btn:not(.disabled):active {
transform: translateY(2rpx);
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.2);
}
.page-btn.disabled {
background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
color: #ccc;
border-color: #d9d9d9;
cursor: not-allowed;
}
.page-numbers {
display: flex;
gap: 8rpx;
margin: 0 20rpx;
}
.page-number {
padding: 16rpx 20rpx;
border: 2rpx solid #d9d9d9;
border-radius: 12rpx;
font-size: 26rpx;
font-weight: 500;
color: #333;
background: linear-gradient(135deg, #fff, #f8f9fa);
min-width: 60rpx;
text-align: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.page-number:active {
transform: translateY(2rpx);
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.2);
}
.page-number.active {
background: linear-gradient(135deg, #1890ff, #40a9ff);
color: white;
border-color: #1890ff;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
transform: scale(1.05);
}

View File

@@ -0,0 +1,842 @@
Page({
data: {
// 围栏数据
fenceList: [],
loading: false,
// 设备统计
stats: {
smartCollector: 0,
smartDevice: 0
},
// 地图相关
mapCenter: {
lng: 106.2751866,
lat: 38.4689544
},
mapZoom: 15,
mapLocked: true, // 地图位置锁定
lastMapCenter: null, // 上次地图中心位置
// 当前选中的围栏
selectedFence: null,
selectedFenceIndex: 0, // 当前选中的围栏索引
// 显示控制
showPasture: true,
mapType: 'normal', // normal, satellite
// 地图标记和多边形
fenceMarkers: [],
fencePolygons: [],
// 缓存数据
cachedFenceData: null,
isOfflineMode: false,
// 地图锁定相关
includePoints: [], // 用于强制锁定地图位置的点
mapLockTimer: null, // 地图锁定监控定时器
// 围栏类型配置
fenceTypes: {
'grazing': { name: '放牧围栏', color: '#52c41a', icon: '🌿' },
'safety': { name: '安全围栏', color: '#1890ff', icon: '🛡️' },
'restricted': { name: '限制围栏', color: '#ff4d4f', icon: '⚠️' },
'collector': { name: '收集围栏', color: '#fa8c16', icon: '📦' }
},
// 搜索和过滤
searchValue: '',
selectedFenceType: '',
filteredFenceList: []
},
onLoad(options) {
console.log('电子围栏页面加载')
this.checkLoginStatus()
// 启动地图锁定监控定时器
this.startMapLockTimer()
},
onUnload() {
// 清除定时器
if (this.data.mapLockTimer) {
clearInterval(this.data.mapLockTimer)
}
},
onShow() {
// 先尝试加载缓存数据如果没有缓存再请求API
if (!this.loadCachedData()) {
this.loadFenceData()
}
},
// 检查登录状态
checkLoginStatus() {
const token = wx.getStorageSync('token')
const userInfo = wx.getStorageSync('userInfo')
if (!token || !userInfo) {
wx.showToast({
title: '请先登录',
icon: 'none'
})
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login'
})
}, 1500)
return false
}
return true
},
// 加载围栏数据
loadFenceData() {
if (!this.checkLoginStatus()) return
const token = wx.getStorageSync('token')
const url = `https://ad.ningmuyun.com/api/electronic-fence?page=1&limit=100&_t=${Date.now()}`
this.setData({ loading: true })
wx.request({
url,
method: 'GET',
timeout: 30000,
header: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
success: (res) => {
console.log('围栏API响应:', res)
if (res.statusCode === 200 && res.data) {
const response = res.data
if (response.success && response.data) {
const fenceList = response.data.map(fence => this.formatFenceData(fence))
// 生成地图标记和多边形数据
const fenceMarkers = this.generateFenceMarkers(fenceList)
const fencePolygons = this.generateFencePolygons(fenceList)
// 缓存数据
const cacheData = {
fenceList: fenceList,
fenceMarkers: fenceMarkers,
fencePolygons: fencePolygons,
timestamp: Date.now()
}
wx.setStorageSync('fenceCache', cacheData)
this.setData({
fenceList: fenceList,
fenceMarkers: fenceMarkers,
fencePolygons: fencePolygons,
cachedFenceData: cacheData,
isOfflineMode: false,
stats: {
smartCollector: 2, // 从API获取或硬编码
smartDevice: 4 // 从API获取或硬编码
}
})
// 如果有围栏数据,设置默认选中第一个围栏
if (fenceList.length > 0) {
const firstFence = fenceList[0]
const centerLng = parseFloat(firstFence.center.lng)
const centerLat = parseFloat(firstFence.center.lat)
this.setData({
selectedFence: firstFence,
selectedFenceIndex: 0,
mapCenter: {
lng: centerLng,
lat: centerLat
},
mapLocked: true, // 初始化后锁定地图
lastMapCenter: {
latitude: centerLat,
longitude: centerLng
}
})
// 立即更新include-points来强制锁定
this.updateIncludePoints()
// 多次强制锁定,确保地图不会移动
setTimeout(() => {
this.setData({
mapCenter: {
lng: centerLng,
lat: centerLat
}
})
this.updateIncludePoints()
console.log('第一次延迟强制锁定:', centerLng, centerLat)
}, 500)
setTimeout(() => {
this.setData({
mapCenter: {
lng: centerLng,
lat: centerLat
}
})
this.updateIncludePoints()
console.log('第二次延迟强制锁定:', centerLng, centerLat)
}, 1000)
setTimeout(() => {
this.setData({
mapCenter: {
lng: centerLng,
lat: centerLat
}
})
this.updateIncludePoints()
console.log('第三次延迟强制锁定:', centerLng, centerLat)
}, 2000)
}
} else {
wx.showToast({
title: response.message || '数据加载失败',
icon: 'none'
})
}
} else if (res.statusCode === 401) {
wx.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
})
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login'
})
}, 1500)
} else if (res.statusCode === 502) {
wx.showModal({
title: '服务器错误',
content: '服务器暂时不可用(502错误),请稍后重试',
confirmText: '重试',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
setTimeout(() => {
this.loadFenceData()
}, 2000)
}
}
})
} else if (res.statusCode >= 500) {
wx.showModal({
title: '服务器错误',
content: `服务器错误(${res.statusCode}),请稍后重试`,
confirmText: '重试',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
setTimeout(() => {
this.loadFenceData()
}, 2000)
}
}
})
} else {
wx.showToast({
title: res.data?.message || `请求失败(${res.statusCode})`,
icon: 'none'
})
}
},
fail: (err) => {
console.error('请求失败:', err)
// 根据错误类型显示不同的提示和处理方式
let errorMessage = '网络请求失败'
let errorTitle = '请求失败'
let showRetry = true
if (err.errMsg && err.errMsg.includes('timeout')) {
errorMessage = '请求超时,服务器响应较慢'
errorTitle = '请求超时'
} else if (err.errMsg && err.errMsg.includes('fail')) {
if (err.errMsg.includes('502')) {
errorMessage = '服务器网关错误(502),服务暂时不可用'
errorTitle = '服务器错误'
} else if (err.errMsg.includes('503')) {
errorMessage = '服务器维护中(503),请稍后重试'
errorTitle = '服务维护'
} else if (err.errMsg.includes('504')) {
errorMessage = '服务器响应超时(504),请重试'
errorTitle = '服务器超时'
} else {
errorMessage = '网络连接失败,请检查网络设置'
errorTitle = '网络错误'
}
} else if (err.errMsg && err.errMsg.includes('ssl')) {
errorMessage = 'SSL证书错误请检查网络环境'
errorTitle = '安全连接错误'
} else if (err.errMsg && err.errMsg.includes('dns')) {
errorMessage = 'DNS解析失败请检查网络连接'
errorTitle = '网络解析错误'
}
// 尝试加载缓存数据
this.loadCachedData()
if (showRetry) {
wx.showModal({
title: errorTitle,
content: errorMessage + ',是否重试?',
confirmText: '重试',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 显示重试提示
wx.showLoading({
title: '重试中...',
mask: true
})
setTimeout(() => {
wx.hideLoading()
this.loadFenceData()
}, 1500)
}
}
})
} else {
wx.showToast({
title: errorMessage,
icon: 'none',
duration: 3000
})
}
},
complete: () => {
this.setData({ loading: false })
}
})
},
// 格式化围栏数据
formatFenceData(fence) {
// 处理围栏类型映射
let fenceType = fence.type || 'grazing'
if (fenceType === '放牧围栏') fenceType = 'grazing'
else if (fenceType === '安全围栏') fenceType = 'safety'
else if (fenceType === '限制围栏') fenceType = 'restricted'
else if (fenceType === '收集围栏') fenceType = 'collector'
return {
id: fence.id,
name: fence.name,
type: fenceType,
typeName: this.data.fenceTypes[fenceType]?.name || fenceType,
typeColor: this.data.fenceTypes[fenceType]?.color || '#666',
typeIcon: this.data.fenceTypes[fenceType]?.icon || '📍',
description: fence.description,
coordinates: fence.coordinates || [],
center: fence.center,
area: fence.area,
grazingStatus: fence.grazingStatus,
insideCount: fence.insideCount,
outsideCount: fence.outsideCount,
isActive: fence.isActive,
createdAt: fence.createdAt,
updatedAt: fence.updatedAt
}
},
// 加载缓存数据
loadCachedData() {
try {
const cachedData = wx.getStorageSync('fenceCache')
if (cachedData && cachedData.timestamp) {
const cacheAge = Date.now() - cachedData.timestamp
const maxCacheAge = 24 * 60 * 60 * 1000 // 24小时
if (cacheAge < maxCacheAge) {
console.log('加载缓存数据,缓存时间:', new Date(cachedData.timestamp))
this.setData({
fenceList: cachedData.fenceList || [],
fenceMarkers: cachedData.fenceMarkers || [],
fencePolygons: cachedData.fencePolygons || [],
isOfflineMode: true,
stats: {
smartCollector: 2,
smartDevice: 4
}
})
// 如果有围栏数据,设置默认选中第一个围栏
if (cachedData.fenceList && cachedData.fenceList.length > 0) {
const firstFence = cachedData.fenceList[0]
const centerLng = parseFloat(firstFence.center.lng)
const centerLat = parseFloat(firstFence.center.lat)
this.setData({
selectedFence: firstFence,
selectedFenceIndex: 0,
mapCenter: {
lng: centerLng,
lat: centerLat
},
mapLocked: true,
lastMapCenter: {
latitude: centerLat,
longitude: centerLng
}
})
// 立即更新include-points来强制锁定
this.updateIncludePoints()
// 多次强制锁定,确保地图不会移动
setTimeout(() => {
this.setData({
mapCenter: {
lng: centerLng,
lat: centerLat
}
})
this.updateIncludePoints()
console.log('缓存数据第一次延迟强制锁定:', centerLng, centerLat)
}, 500)
setTimeout(() => {
this.setData({
mapCenter: {
lng: centerLng,
lat: centerLat
}
})
this.updateIncludePoints()
console.log('缓存数据第二次延迟强制锁定:', centerLng, centerLat)
}, 1000)
}
// 显示离线模式提示
wx.showToast({
title: '已加载缓存数据(离线模式)',
icon: 'none',
duration: 3000
})
return true
} else {
console.log('缓存数据已过期,清除缓存')
wx.removeStorageSync('fenceCache')
}
}
} catch (error) {
console.error('加载缓存数据失败:', error)
}
return false
},
// 返回上一页
onBack() {
wx.navigateBack()
},
// 显示设置菜单
onShowMenu() {
const menuItems = ['围栏设置', '设备管理', '历史记录']
// 添加地图锁定/解锁选项
const lockText = this.data.mapLocked ? '解锁地图' : '锁定地图'
menuItems.unshift(lockText)
// 如果有多个围栏,添加围栏切换选项
if (this.data.fenceList.length > 1) {
menuItems.unshift('切换围栏')
}
wx.showActionSheet({
itemList: menuItems,
success: (res) => {
const tapIndex = res.tapIndex
console.log('选择了:', tapIndex)
let actualIndex = tapIndex
// 处理围栏切换
if (this.data.fenceList.length > 1 && tapIndex === 0) {
this.showFenceSelector()
return
} else if (this.data.fenceList.length > 1) {
actualIndex = tapIndex - 1
}
// 处理地图锁定/解锁
if (actualIndex === 0) {
this.toggleMapLock()
} else {
// 其他菜单项
const menuIndex = this.data.fenceList.length > 1 ? actualIndex - 1 : actualIndex
switch (menuIndex) {
case 0: // 围栏设置
wx.showToast({ title: '围栏设置功能开发中', icon: 'none' })
break
case 1: // 设备管理
wx.showToast({ title: '设备管理功能开发中', icon: 'none' })
break
case 2: // 历史记录
wx.showToast({ title: '历史记录功能开发中', icon: 'none' })
break
}
}
}
})
},
// 显示围栏选择器
showFenceSelector() {
const fenceNames = this.data.fenceList.map(fence => fence.name)
wx.showActionSheet({
itemList: fenceNames,
success: (res) => {
const selectedIndex = res.tapIndex
const selectedFence = this.data.fenceList[selectedIndex]
this.setData({
selectedFence: selectedFence,
selectedFenceIndex: selectedIndex
})
// 自动定位到选中的围栏
this.locateToSelectedFence()
wx.showToast({
title: `已切换到${selectedFence.name}`,
icon: 'success'
})
}
})
},
// 切换牧场显示
onTogglePasture() {
const newShowPasture = !this.data.showPasture
this.setData({
showPasture: newShowPasture
})
if (newShowPasture && this.data.selectedFence) {
// 如果显示牧场,定位到选中的围栏
this.locateToSelectedFence()
}
},
// 定位到选中的围栏
locateToSelectedFence() {
if (!this.data.selectedFence) {
wx.showToast({
title: '没有选中的围栏',
icon: 'none'
})
return
}
const fence = this.data.selectedFence
// 计算围栏的边界,用于设置合适的地图视野
const coordinates = fence.coordinates
if (coordinates && coordinates.length > 0) {
let minLat = coordinates[0].lat
let maxLat = coordinates[0].lat
let minLng = coordinates[0].lng
let maxLng = coordinates[0].lng
coordinates.forEach(coord => {
minLat = Math.min(minLat, coord.lat)
maxLat = Math.max(maxLat, coord.lat)
minLng = Math.min(minLng, coord.lng)
maxLng = Math.max(maxLng, coord.lng)
})
// 计算中心点和合适的缩放级别
const centerLat = (minLat + maxLat) / 2
const centerLng = (minLng + maxLng) / 2
// 根据围栏大小调整缩放级别
const latDiff = maxLat - minLat
const lngDiff = maxLng - minLng
const maxDiff = Math.max(latDiff, lngDiff)
let zoom = 15
if (maxDiff > 0.01) zoom = 12
else if (maxDiff > 0.005) zoom = 14
else if (maxDiff > 0.002) zoom = 16
else zoom = 18
this.setData({
mapCenter: {
lng: centerLng,
lat: centerLat
},
mapZoom: zoom,
mapLocked: true, // 定位后锁定地图
lastMapCenter: {
latitude: centerLat,
longitude: centerLng
}
})
// 立即更新include-points来强制锁定
setTimeout(() => {
this.updateIncludePoints()
}, 100)
wx.showToast({
title: `已定位到${fence.name}`,
icon: 'success',
duration: 2000
})
} else {
// 如果没有坐标点,使用中心点定位
this.setData({
mapCenter: {
lng: parseFloat(fence.center.lng),
lat: parseFloat(fence.center.lat)
},
mapZoom: 15,
mapLocked: true, // 定位后锁定地图
lastMapCenter: {
latitude: parseFloat(fence.center.lat),
longitude: parseFloat(fence.center.lng)
}
})
// 立即更新include-points来强制锁定
setTimeout(() => {
this.updateIncludePoints()
}, 100)
wx.showToast({
title: `已定位到${fence.name}`,
icon: 'success',
duration: 2000
})
}
},
// 切换地图类型
onSwitchMap() {
const mapType = this.data.mapType === 'normal' ? 'satellite' : 'normal'
this.setData({
mapType: mapType
})
wx.showToast({
title: mapType === 'normal' ? '切换到普通地图' : '切换到卫星地图',
icon: 'none'
})
},
// 解锁/锁定地图
toggleMapLock() {
const newLocked = !this.data.mapLocked
this.setData({
mapLocked: newLocked
})
// 更新include-points
this.updateIncludePoints()
wx.showToast({
title: newLocked ? '地图已锁定' : '地图已解锁',
icon: 'none',
duration: 1500
})
},
// 地图标记点击事件
onMarkerTap(e) {
const markerId = e.detail.markerId
const fence = this.data.fenceList.find(f => f.id === markerId)
if (fence) {
// 选中该围栏
this.setData({
selectedFence: fence,
selectedFenceIndex: this.data.fenceList.findIndex(f => f.id === markerId)
})
wx.showModal({
title: `${fence.typeIcon} ${fence.name}`,
content: `类型: ${fence.typeName}\n状态: ${fence.grazingStatus}\n面积: ${fence.area}平方米\n坐标点: ${fence.coordinates.length}\n描述: ${fence.description || '无描述'}`,
confirmText: '定位到此围栏',
cancelText: '关闭',
success: (res) => {
if (res.confirm) {
// 定位到选中的围栏
this.locateToSelectedFence()
}
}
})
}
},
// 地图区域变化
onRegionChange(e) {
console.log('地图区域变化:', e.detail)
// 强制锁定地图 - 无论什么情况都恢复位置
if (this.data.lastMapCenter) {
console.log('强制锁定地图位置')
// 立即恢复地图位置
this.setData({
mapCenter: {
lng: this.data.lastMapCenter.longitude,
lat: this.data.lastMapCenter.latitude
}
})
// 更新include-points来强制锁定
this.updateIncludePoints()
}
},
// 更新include-points来强制锁定地图
updateIncludePoints() {
if (this.data.lastMapCenter) {
const center = this.data.lastMapCenter
// 创建更紧密的四个点来强制锁定地图视野
const offset = 0.0005 // 减小偏移量,使锁定更紧密
const points = [
{ latitude: center.latitude - offset, longitude: center.longitude - offset },
{ latitude: center.latitude + offset, longitude: center.longitude - offset },
{ latitude: center.latitude + offset, longitude: center.longitude + offset },
{ latitude: center.latitude - offset, longitude: center.longitude + offset }
]
this.setData({
includePoints: points
})
} else {
this.setData({
includePoints: []
})
}
},
// 启动地图锁定监控定时器
startMapLockTimer() {
if (this.data.mapLockTimer) {
clearInterval(this.data.mapLockTimer)
}
const timer = setInterval(() => {
if (this.data.lastMapCenter) {
// 强制更新地图位置 - 无论锁定状态如何
this.setData({
mapCenter: {
lng: this.data.lastMapCenter.longitude,
lat: this.data.lastMapCenter.latitude
}
})
// 更新include-points
this.updateIncludePoints()
console.log('定时器强制锁定地图位置:', this.data.lastMapCenter)
}
}, 500) // 每500毫秒检查一次更频繁
this.setData({
mapLockTimer: timer
})
},
// 地图点击事件
onMapTap(e) {
console.log('地图点击:', e.detail)
},
// 关闭围栏信息面板
onCloseFenceInfo() {
this.setData({
selectedFence: null
})
},
// 定位围栏
onLocateFence() {
if (this.data.selectedFence) {
this.locateToSelectedFence()
}
},
// 查看围栏详情
onViewFenceDetails() {
if (this.data.selectedFence) {
wx.showModal({
title: `${this.data.selectedFence.typeIcon} ${this.data.selectedFence.name}`,
content: `围栏ID: ${this.data.selectedFence.id}\n类型: ${this.data.selectedFence.typeName}\n状态: ${this.data.selectedFence.grazingStatus}\n面积: ${this.data.selectedFence.area}平方米\n坐标点: ${this.data.selectedFence.coordinates.length}\n内部设备: ${this.data.selectedFence.insideCount}\n外部设备: ${this.data.selectedFence.outsideCount}\n创建时间: ${this.data.selectedFence.createdAt}\n更新时间: ${this.data.selectedFence.updatedAt}\n描述: ${this.data.selectedFence.description || '无描述'}`,
showCancel: false,
confirmText: '确定'
})
}
},
// 生成围栏标记
generateFenceMarkers(fenceList) {
return fenceList.map((fence, index) => {
return {
id: fence.id,
latitude: parseFloat(fence.center.lat),
longitude: parseFloat(fence.center.lng),
iconPath: '', // 使用默认图标
width: 30,
height: 30,
title: fence.name,
callout: {
content: `${fence.typeIcon} ${fence.name}\n${fence.typeName}\n${fence.grazingStatus}\n${fence.coordinates.length}个坐标点`,
color: '#333',
fontSize: 12,
borderRadius: 8,
bgColor: '#fff',
padding: 12,
display: 'BYCLICK',
borderWidth: 1,
borderColor: fence.typeColor || '#3cc51f'
}
}
})
},
// 生成围栏多边形
generateFencePolygons(fenceList) {
return fenceList.map((fence, index) => {
const points = fence.coordinates.map(coord => ({
latitude: coord.lat,
longitude: coord.lng
}))
// 根据围栏类型设置颜色
const strokeColor = fence.typeColor || (fence.isActive ? '#3cc51f' : '#ff6b6b')
const fillColor = fence.typeColor ?
`${fence.typeColor}33` : // 添加透明度
(fence.isActive ? 'rgba(60, 197, 31, 0.2)' : 'rgba(255, 107, 107, 0.2)')
return {
points: points,
strokeWidth: 3,
strokeColor: strokeColor,
fillColor: fillColor
}
})
},
})

View File

@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "电子围栏",
"navigationBarBackgroundColor": "#3cc51f",
"navigationBarTextStyle": "white",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,148 @@
<!-- 电子围栏页面 -->
<view class="fence-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="header-left" bindtap="onBack">
<text class="back-icon"></text>
</view>
<view class="header-title">电子围栏</view>
<view class="header-right">
<text class="menu-icon" bindtap="onShowMenu">⋯</text>
<text class="minimize-icon" bindtap="onShowMenu"></text>
<text class="target-icon" bindtap="onShowMenu">◎</text>
</view>
</view>
<!-- 离线模式提示 -->
<view wx:if="{{isOfflineMode}}" class="offline-notice">
<text class="offline-icon">📡</text>
<text class="offline-text">离线模式 - 显示缓存数据</text>
</view>
<!-- 地图锁定提示 -->
<view wx:if="{{mapLocked}}" class="map-lock-notice">
<text class="lock-icon">🔒</text>
<text class="lock-text">地图已锁定 - 防止自动移动</text>
</view>
<!-- 控制面板 -->
<view class="control-panel">
<!-- 左侧控制区 -->
<view class="left-controls">
<!-- 设置按钮 -->
<view class="settings-btn" bindtap="onShowMenu">
<text class="settings-icon">⚙</text>
</view>
<!-- 显示牧场按钮 -->
<view class="pasture-btn {{showPasture ? 'active' : ''}}" bindtap="onTogglePasture">
<text>显示牧场</text>
</view>
<!-- 设备统计信息 -->
<view class="device-stats">
<view class="stats-item">
<text class="stats-label">智能采集器:</text>
<text class="stats-value">{{stats.smartCollector}}</text>
</view>
<view class="stats-item">
<text class="stats-label">智能设备:</text>
<text class="stats-value">{{stats.smartDevice}}</text>
</view>
<view class="stats-item">
<text class="stats-label">围栏总数:</text>
<text class="stats-value">{{fenceList.length}}</text>
</view>
</view>
<!-- 牧场名称 -->
<view class="pasture-name">各德</view>
</view>
<!-- 右侧控制区 -->
<view class="right-controls">
<view class="switch-map-btn" bindtap="onSwitchMap">
<text>切换地图</text>
</view>
</view>
</view>
<!-- 围栏信息面板 -->
<view wx:if="{{selectedFence}}" class="fence-info-panel">
<view class="panel-header">
<view class="fence-title">
<text class="fence-icon">{{selectedFence.typeIcon}}</text>
<text class="fence-name">{{selectedFence.name}}</text>
</view>
<view class="close-btn" bindtap="onCloseFenceInfo">
<text>✕</text>
</view>
</view>
<view class="panel-content">
<view class="info-row">
<text class="info-label">围栏类型:</text>
<text class="info-value" style="color: {{selectedFence.typeColor}}">{{selectedFence.typeName}}</text>
</view>
<view class="info-row">
<text class="info-label">放牧状态:</text>
<text class="info-value">{{selectedFence.grazingStatus}}</text>
</view>
<view class="info-row">
<text class="info-label">围栏面积:</text>
<text class="info-value">{{selectedFence.area}}平方米</text>
</view>
<view class="info-row">
<text class="info-label">坐标点数:</text>
<text class="info-value">{{selectedFence.coordinates.length}}个</text>
</view>
<view class="info-row">
<text class="info-label">围栏描述:</text>
<text class="info-value">{{selectedFence.description || '无描述'}}</text>
</view>
</view>
<view class="panel-actions">
<view class="action-btn primary" bindtap="onLocateFence">
<text>定位围栏</text>
</view>
<view class="action-btn secondary" bindtap="onViewFenceDetails">
<text>查看详情</text>
</view>
</view>
</view>
<!-- 地图区域 -->
<view class="map-container">
<map
id="fenceMap"
class="fence-map"
longitude="{{mapCenter.lng}}"
latitude="{{mapCenter.lat}}"
scale="{{mapZoom}}"
markers="{{fenceMarkers}}"
polygons="{{fencePolygons}}"
show-location="{{false}}"
enable-scroll="{{false}}"
enable-zoom="{{false}}"
enable-rotate="{{false}}"
enable-overlooking="{{false}}"
enable-satellite="{{false}}"
enable-traffic="{{false}}"
enable-3D="{{false}}"
enable-compass="{{false}}"
enable-scale="{{false}}"
enable-poi="{{false}}"
enable-building="{{false}}"
include-points="{{includePoints}}"
bindmarkertap="onMarkerTap"
bindregionchange="onRegionChange"
bindtap="onMapTap"
>
<!-- 地图加载中 -->
<view wx:if="{{loading}}" class="map-loading">
<text>地图加载中...</text>
</view>
</map>
</view>
</view>

View File

@@ -0,0 +1,358 @@
/* 电子围栏页面样式 */
.fence-container {
width: 100%;
height: 100vh;
background: #f5f5f5;
display: flex;
flex-direction: column;
}
/* 离线模式提示 */
.offline-notice {
width: 100%;
background: #ff9500;
color: #ffffff;
padding: 16rpx 32rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
font-size: 24rpx;
box-sizing: border-box;
}
.offline-icon {
font-size: 28rpx;
}
.offline-text {
font-weight: bold;
}
/* 地图锁定提示 */
.map-lock-notice {
width: 100%;
background: #007aff;
color: #ffffff;
padding: 16rpx 32rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
font-size: 24rpx;
box-sizing: border-box;
}
.lock-icon {
font-size: 28rpx;
}
.lock-text {
font-weight: bold;
}
/* 顶部导航栏 */
.header {
width: 100%;
height: 88rpx;
background: #3cc51f;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
box-sizing: border-box;
position: relative;
z-index: 1000;
}
.header-left {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 48rpx;
color: #ffffff;
font-weight: bold;
}
.header-title {
font-size: 36rpx;
color: #ffffff;
font-weight: bold;
}
.header-right {
display: flex;
align-items: center;
gap: 24rpx;
}
.menu-icon,
.minimize-icon,
.target-icon {
font-size: 32rpx;
color: #ffffff;
font-weight: bold;
}
/* 控制面板 */
.control-panel {
width: 100%;
background: #ffffff;
padding: 24rpx 32rpx;
display: flex;
justify-content: space-between;
align-items: flex-start;
box-sizing: border-box;
}
/* 左侧控制区 */
.left-controls {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.settings-btn {
width: 60rpx;
height: 60rpx;
background: #f5f5f5;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
}
.settings-icon {
font-size: 32rpx;
color: #666;
}
.pasture-btn {
padding: 16rpx 32rpx;
background: #3cc51f;
border-radius: 8rpx;
font-size: 28rpx;
color: #ffffff;
text-align: center;
min-width: 160rpx;
}
.pasture-btn.active {
background: #2a9d16;
}
.device-stats {
background: #333333;
border-radius: 8rpx;
padding: 24rpx;
min-width: 240rpx;
}
.stats-item {
display: flex;
justify-content: space-between;
margin-bottom: 8rpx;
}
.stats-item:last-child {
margin-bottom: 0;
}
.stats-label {
font-size: 24rpx;
color: #ffffff;
}
.stats-value {
font-size: 24rpx;
color: #ffffff;
font-weight: bold;
}
.pasture-name {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-top: 8rpx;
}
/* 右侧控制区 */
.right-controls {
display: flex;
align-items: center;
}
.switch-map-btn {
padding: 16rpx 32rpx;
background: #3cc51f;
border-radius: 8rpx;
font-size: 28rpx;
color: #ffffff;
text-align: center;
min-width: 160rpx;
}
/* 地图容器 */
.map-container {
flex: 1;
position: relative;
background: #f5f5f5;
}
.fence-map {
width: 100%;
height: 100%;
}
.map-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.7);
color: #ffffff;
padding: 24rpx 48rpx;
border-radius: 8rpx;
font-size: 28rpx;
z-index: 100;
}
/* 围栏信息面板 */
.fence-info-panel {
position: absolute;
top: 200rpx;
right: 32rpx;
width: 320rpx;
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
z-index: 1000;
overflow: hidden;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background: linear-gradient(135deg, #3cc51f, #2a9d16);
color: #ffffff;
}
.fence-title {
display: flex;
align-items: center;
gap: 12rpx;
}
.fence-icon {
font-size: 32rpx;
}
.fence-name {
font-size: 28rpx;
font-weight: bold;
}
.close-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
font-size: 24rpx;
color: #ffffff;
}
.panel-content {
padding: 24rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
padding: 12rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.info-row:last-child {
border-bottom: none;
margin-bottom: 0;
}
.info-label {
font-size: 24rpx;
color: #666;
font-weight: 500;
}
.info-value {
font-size: 24rpx;
color: #333;
text-align: right;
flex: 1;
margin-left: 16rpx;
}
.panel-actions {
display: flex;
gap: 16rpx;
padding: 24rpx;
background: #f8f9fa;
}
.action-btn {
flex: 1;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
font-size: 26rpx;
font-weight: bold;
}
.action-btn.primary {
background: #3cc51f;
color: #ffffff;
}
.action-btn.secondary {
background: #ffffff;
color: #3cc51f;
border: 2rpx solid #3cc51f;
}
/* 响应式设计 */
@media (max-width: 750rpx) {
.control-panel {
flex-direction: column;
gap: 24rpx;
}
.right-controls {
align-self: flex-end;
}
.device-stats {
min-width: 200rpx;
}
.fence-info-panel {
position: relative;
top: auto;
right: auto;
width: 100%;
margin: 16rpx 0;
}
}

View File

@@ -0,0 +1,477 @@
Page({
data: {
list: [],
searchValue: '',
currentPage: 1,
total: 0,
pageSize: 10,
totalPages: 0,
pageNumbers: [],
paginationList: [], // 分页页码列表
stats: {
total: 0,
online: 0,
offline: 0
},
loading: false,
isSearching: false,
searchResult: null
},
onLoad() {
console.log('智能主机页面加载')
this.checkLoginStatus()
this.loadData()
},
onShow() {
this.loadData()
},
onPullDownRefresh() {
this.loadData().then(() => {
wx.stopPullDownRefresh()
})
},
// 检查登录状态
checkLoginStatus() {
const token = wx.getStorageSync('token')
const userInfo = wx.getStorageSync('userInfo')
if (!token || !userInfo) {
console.log('用户未登录,跳转到登录页')
wx.showModal({
title: '提示',
content: '请先登录后再使用',
showCancel: false,
success: () => {
wx.reLaunch({
url: '/pages/login/login'
})
}
})
return false
}
console.log('用户已登录:', userInfo.username)
return true
},
// 加载数据
loadData() {
const { currentPage, pageSize } = this.data
const url = `https://ad.ningmuyun.com/api/smart-devices/hosts?page=${currentPage}&limit=${pageSize}&_t=${Date.now()}&refresh=true`
const token = wx.getStorageSync('token')
if (!token) {
wx.showToast({
title: '请先登录',
icon: 'none'
})
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login'
})
}, 1500)
return
}
this.setData({ loading: true })
wx.request({
url,
method: 'GET',
timeout: 30000, // 设置30秒超时
header: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
success: (res) => {
console.log('API响应:', res)
if (res.statusCode === 200 && res.data) {
const response = res.data
if (response.success && response.data) {
const data = response.data
const total = response.total || 0
const totalPages = Math.ceil(total / this.data.pageSize)
const paginationList = this.generatePaginationList(this.data.currentPage, totalPages)
this.setData({
list: Array.isArray(data) ? data.map(item => this.formatItemData(item)) : [],
total: total,
totalPages: totalPages,
paginationList: paginationList,
stats: response.stats || { total: 0, online: 0, offline: 0 }
})
} else {
wx.showToast({
title: response.message || '数据加载失败',
icon: 'none'
})
}
} else if (res.statusCode === 401) {
wx.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
})
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login'
})
}, 1500)
} else {
wx.showToast({
title: res.data?.message || '数据加载失败',
icon: 'none'
})
}
},
fail: (err) => {
console.error('请求失败:', err)
// 根据错误类型显示不同的提示
let errorMessage = '网络请求失败'
if (err.errMsg && err.errMsg.includes('timeout')) {
errorMessage = '请求超时,请检查网络连接'
} else if (err.errMsg && err.errMsg.includes('fail')) {
errorMessage = '网络连接失败,请重试'
} else if (err.errMsg && err.errMsg.includes('401')) {
errorMessage = '请登录后重试'
}
wx.showModal({
title: '请求失败',
content: errorMessage + ',是否重试?',
confirmText: '重试',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 用户选择重试
setTimeout(() => {
this.loadData()
}, 1000)
}
}
})
},
complete: () => {
this.setData({ loading: false })
}
})
},
// 格式化单个设备数据
formatItemData(item) {
return {
...item,
statusText: item.networkStatus || '未知',
signalText: item.signalValue || '未知',
batteryText: `${item.battery || 0}%`,
temperatureText: `${item.temperature || 0}°C`,
deviceNumberText: item.deviceNumber || '未知',
updateTimeText: item.updateTime || '未知'
}
},
// 生成分页页码列表
generatePaginationList(currentPage, totalPages) {
const paginationList = []
const maxVisiblePages = 5 // 最多显示5个页码
if (totalPages <= maxVisiblePages) {
// 总页数少于等于5页显示所有页码
for (let i = 1; i <= totalPages; i++) {
paginationList.push({
page: i,
active: i === currentPage,
text: i.toString()
})
}
} else {
// 总页数大于5页显示省略号
if (currentPage <= 3) {
// 当前页在前3页
for (let i = 1; i <= 4; i++) {
paginationList.push({
page: i,
active: i === currentPage,
text: i.toString()
})
}
paginationList.push({
page: -1,
active: false,
text: '...'
})
paginationList.push({
page: totalPages,
active: false,
text: totalPages.toString()
})
} else if (currentPage >= totalPages - 2) {
// 当前页在后3页
paginationList.push({
page: 1,
active: false,
text: '1'
})
paginationList.push({
page: -1,
active: false,
text: '...'
})
for (let i = totalPages - 3; i <= totalPages; i++) {
paginationList.push({
page: i,
active: i === currentPage,
text: i.toString()
})
}
} else {
// 当前页在中间
paginationList.push({
page: 1,
active: false,
text: '1'
})
paginationList.push({
page: -1,
active: false,
text: '...'
})
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
paginationList.push({
page: i,
active: i === currentPage,
text: i.toString()
})
}
paginationList.push({
page: -1,
active: false,
text: '...'
})
paginationList.push({
page: totalPages,
active: false,
text: totalPages.toString()
})
}
}
return paginationList
},
// 搜索输入
onSearchInput(e) {
this.setData({ searchValue: e.detail.value.trim() })
},
// 执行搜索
onSearch() {
const searchValue = this.data.searchValue.trim()
if (!searchValue) {
wx.showToast({
title: '请输入主机编号',
icon: 'none'
})
return
}
// 验证主机编号格式(数字和字母)
if (!/^[A-Za-z0-9]+$/.test(searchValue)) {
wx.showToast({
title: '主机编号只能包含数字和字母',
icon: 'none'
})
return
}
// 设置搜索状态
this.setData({
isSearching: true,
searchResult: null,
currentPage: 1
})
// 执行精确搜索
this.performExactSearch(searchValue)
},
// 执行精确搜索
performExactSearch(searchValue) {
const url = `https://ad.ningmuyun.com/api/smart-devices/hosts?page=1&limit=1&deviceId=${searchValue}&_t=${Date.now()}`
const token = wx.getStorageSync('token')
if (!token) {
wx.showToast({
title: '请先登录',
icon: 'none'
})
return
}
wx.showLoading({ title: '搜索中...' })
wx.request({
url,
method: 'GET',
timeout: 30000, // 设置30秒超时
header: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
success: (res) => {
console.log('搜索API响应:', res)
if (res.statusCode === 200 && res.data) {
const response = res.data
if (response.success && response.data) {
const data = response.data
if (Array.isArray(data) && data.length > 0) {
// 找到匹配的设备
const device = this.formatItemData(data[0])
this.setData({
searchResult: device,
list: [], // 清空列表显示
total: 1,
totalPages: 1,
paginationList: [{ page: 1, active: true, text: '1' }]
})
wx.showToast({
title: '搜索成功',
icon: 'success'
})
} else {
// 没有找到匹配的设备
this.setData({
searchResult: null,
list: [],
total: 0,
totalPages: 0,
paginationList: []
})
wx.showToast({
title: '未找到该设备',
icon: 'none'
})
}
} else {
wx.showToast({
title: '搜索失败',
icon: 'none'
})
}
} else if (res.statusCode === 401) {
wx.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
})
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login'
})
}, 1500)
} else {
wx.showToast({
title: '搜索失败',
icon: 'none'
})
}
},
fail: (err) => {
console.error('搜索请求失败:', err)
// 根据错误类型显示不同的提示
let errorMessage = '网络请求失败'
if (err.errMsg && err.errMsg.includes('timeout')) {
errorMessage = '搜索超时,请检查网络连接'
} else if (err.errMsg && err.errMsg.includes('fail')) {
errorMessage = '网络连接失败,请重试'
}
wx.showModal({
title: '搜索失败',
content: errorMessage + ',是否重试?',
confirmText: '重试',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 用户选择重试
setTimeout(() => {
this.performExactSearch(this.data.searchValue)
}, 1000)
}
}
})
},
complete: () => {
wx.hideLoading()
}
})
},
// 清除搜索
clearSearch() {
this.setData({
searchValue: '',
isSearching: false,
searchResult: null,
currentPage: 1
})
this.loadData()
},
// 上一页
onPrevPage() {
if (this.data.currentPage > 1) {
this.setData({
currentPage: this.data.currentPage - 1
}, () => {
this.loadData()
})
}
},
// 下一页
onNextPage() {
if (this.data.currentPage < this.data.totalPages) {
this.setData({
currentPage: this.data.currentPage + 1
}, () => {
this.loadData()
})
}
},
// 分页切换
onPageChange(e) {
const page = parseInt(e.currentTarget.dataset.page)
if (page > 0 && page !== this.data.currentPage) {
this.setData({
currentPage: page
}, () => {
this.loadData()
})
}
},
// 查看主机详情
viewHostDetail(e) {
const hostId = e.currentTarget.dataset.id
wx.navigateTo({
url: `/pages/device/host-detail/host-detail?id=${hostId}`
})
},
// 主机定位总览
onLocationOverview() {
wx.navigateTo({
url: '/pages/device/host-location/host-location'
})
}
})

View File

@@ -0,0 +1,5 @@
{
"navigationBarTitleText": "智能主机",
"enablePullDownRefresh": true,
"onReachBottomDistance": 50
}

View File

@@ -0,0 +1,177 @@
<view class="container">
<!-- 搜索区域 -->
<view class="search-section">
<view class="search-box">
<view class="search-icon">🔍</view>
<input
placeholder="搜索"
bindinput="onSearchInput"
value="{{searchValue}}"
class="search-input"
/>
<button bindtap="onSearch" class="search-btn">查询</button>
<button wx:if="{{isSearching}}" bindtap="clearSearch" class="clear-btn">清除</button>
</view>
</view>
<!-- 搜索状态提示 -->
<view wx:if="{{isSearching}}" class="search-status">
<text>搜索主机编号: {{searchValue}}</text>
</view>
<!-- 统计卡片区域 -->
<view class="stats-section">
<view class="stats-card">
<text class="stats-label">主机总数</text>
<text class="stats-value">{{stats.total}}</text>
</view>
<view class="stats-card">
<text class="stats-label">联网数量</text>
<text class="stats-value">{{stats.online}}</text>
</view>
<view class="stats-card">
<text class="stats-label">断网数量</text>
<text class="stats-value">{{stats.offline}}</text>
</view>
</view>
<!-- 搜索结果 -->
<view wx:if="{{isSearching && searchResult}}" class="search-result">
<view class="result-header">
<text class="result-title">搜索结果</text>
<text class="result-subtitle">找到匹配的设备</text>
</view>
<view class="host-item search-item" bindtap="viewHostDetail" data-id="{{searchResult.id}}">
<!-- 主机编号和状态 -->
<view class="host-header">
<text class="host-number">主机编号: {{searchResult.deviceNumberText}}</text>
<view class="status-btn {{searchResult.networkStatus === '已联网' ? 'online' : 'offline'}}">
{{searchResult.statusText}}
</view>
</view>
<!-- 设备详细信息 -->
<view class="host-details">
<view class="detail-row">
<text class="detail-label">设备电量:</text>
<text class="detail-value">{{searchResult.batteryText}}</text>
</view>
<view class="detail-row">
<text class="detail-label">设备信号值:</text>
<text class="detail-value">{{searchResult.signalText}}</text>
</view>
<view class="detail-row">
<text class="detail-label">设备温度:</text>
<text class="detail-value">{{searchResult.temperatureText}}</text>
</view>
<view class="detail-row">
<text class="detail-label">绑带状态:</text>
<text class="detail-value">{{searchResult.statusText}}</text>
</view>
<view class="detail-row">
<text class="detail-label">数据更新时间:</text>
<text class="detail-value">{{searchResult.updateTimeText}}</text>
</view>
</view>
</view>
</view>
<!-- 无搜索结果 -->
<view wx:if="{{isSearching && !searchResult}}" class="no-result">
<view class="no-result-icon">🔍</view>
<text class="no-result-text">未找到主机编号为 "{{searchValue}}" 的设备</text>
<button class="retry-btn" bindtap="clearSearch">重新搜索</button>
</view>
<!-- 主机设备列表 -->
<view wx:if="{{!isSearching}}" class="host-list">
<block wx:for="{{list}}" wx:key="id">
<view class="host-item" bindtap="viewHostDetail" data-id="{{item.id}}">
<!-- 主机编号和状态 -->
<view class="host-header">
<text class="host-number">主机编号: {{item.deviceNumberText}}</text>
<view class="status-btn {{item.networkStatus === '已联网' ? 'online' : 'offline'}}">
{{item.statusText}}
</view>
</view>
<!-- 设备详细信息 -->
<view class="host-details">
<view class="detail-row">
<text class="detail-label">设备电量:</text>
<text class="detail-value">{{item.batteryText}}</text>
</view>
<view class="detail-row">
<text class="detail-label">设备信号值:</text>
<text class="detail-value">{{item.signalText}}</text>
</view>
<view class="detail-row">
<text class="detail-label">设备温度:</text>
<text class="detail-value">{{item.temperatureText}}</text>
</view>
<view class="detail-row">
<text class="detail-label">绑带状态:</text>
<text class="detail-value">{{item.statusText}}</text>
</view>
<view class="detail-row">
<text class="detail-label">数据更新时间:</text>
<text class="detail-value">{{item.updateTimeText}}</text>
</view>
</view>
</view>
</block>
</view>
<!-- 分页组件 -->
<view wx:if="{{!isSearching && totalPages > 1}}" class="pagination-container">
<view class="pagination-info">
<text>共 {{total}} 条记录,第 {{currentPage}}/{{totalPages}} 页</text>
</view>
<view class="pagination-controls">
<!-- 上一页按钮 -->
<view
class="pagination-btn {{currentPage === 1 ? 'disabled' : ''}}"
bindtap="onPrevPage"
>
上一页
</view>
<!-- 页码列表 -->
<view class="pagination-pages">
<view
wx:for="{{paginationList}}"
wx:key="index"
class="pagination-page {{item.active ? 'active' : ''}} {{item.page === -1 ? 'ellipsis' : ''}}"
bindtap="onPageChange"
data-page="{{item.page}}"
>
{{item.text}}
</view>
</view>
<!-- 下一页按钮 -->
<view
class="pagination-btn {{currentPage === totalPages ? 'disabled' : ''}}"
bindtap="onNextPage"
>
下一页
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="footer-action">
<button class="location-btn" bindtap="onLocationOverview">
主机定位总览
</button>
</view>
</view>

View File

@@ -0,0 +1,370 @@
/* 智能主机页面样式 */
.container {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 120rpx; /* 为底部按钮留出空间 */
}
/* 搜索区域 */
.search-section {
background: #52c41a;
padding: 20rpx;
margin-bottom: 20rpx;
}
.search-box {
display: flex;
align-items: center;
background: white;
border-radius: 25rpx;
padding: 15rpx 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
gap: 15rpx;
}
.search-icon {
font-size: 32rpx;
color: #999;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
border: none;
outline: none;
}
.search-input::placeholder {
color: #999;
}
.search-btn {
background: linear-gradient(135deg, #52c41a, #73d13d);
color: white;
border: none;
border-radius: 20rpx;
padding: 12rpx 20rpx;
font-size: 24rpx;
font-weight: bold;
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
}
.clear-btn {
background: linear-gradient(135deg, #ff4d4f, #ff7875);
color: white;
border: none;
border-radius: 20rpx;
padding: 12rpx 20rpx;
font-size: 24rpx;
font-weight: bold;
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
}
.search-status {
background: linear-gradient(135deg, #e6f7ff, #bae7ff);
border: 2rpx solid #91d5ff;
border-radius: 12rpx;
padding: 20rpx;
margin: 0 20rpx 20rpx;
text-align: center;
}
.search-status text {
color: #1890ff;
font-size: 28rpx;
font-weight: bold;
}
.search-result {
margin: 0 20rpx 30rpx;
}
.result-header {
background: linear-gradient(135deg, #f6ffed, #d9f7be);
border: 2rpx solid #b7eb8f;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
text-align: center;
}
.result-title {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #52c41a;
margin-bottom: 10rpx;
}
.result-subtitle {
display: block;
font-size: 24rpx;
color: #73d13d;
}
.search-item {
border: 2rpx solid #52c41a;
box-shadow: 0 4rpx 16rpx rgba(82, 196, 26, 0.2);
}
.no-result {
text-align: center;
padding: 80rpx 40rpx;
background: #fafafa;
border-radius: 12rpx;
margin: 0 20rpx 30rpx;
}
.no-result-icon {
font-size: 80rpx;
margin-bottom: 30rpx;
}
.no-result-text {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 40rpx;
line-height: 1.5;
}
.retry-btn {
background: linear-gradient(135deg, #52c41a, #73d13d);
color: white;
border: none;
border-radius: 25rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.3);
}
/* 统计卡片区域 */
.stats-section {
display: flex;
justify-content: space-between;
padding: 0 20rpx;
margin-bottom: 20rpx;
gap: 15rpx;
}
.stats-card {
flex: 1;
background: #f8f8f8;
border-radius: 12rpx;
padding: 20rpx;
text-align: center;
border: 1rpx solid #e8e8e8;
}
.stats-label {
display: block;
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
}
.stats-value {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
/* 主机设备列表 */
.host-list {
padding: 0 20rpx;
margin-bottom: 30rpx;
}
.host-item {
background: white;
border-radius: 12rpx;
padding: 25rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
border: 1rpx solid #e8e8e8;
}
.host-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 15rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.host-number {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.status-btn {
padding: 8rpx 16rpx;
border-radius: 6rpx;
font-size: 24rpx;
font-weight: bold;
border: 2rpx solid;
}
.status-btn.online {
color: #1890ff;
border-color: #1890ff;
background: rgba(24, 144, 255, 0.1);
}
.status-btn.offline {
color: #ff4d4f;
border-color: #ff4d4f;
background: rgba(255, 77, 79, 0.1);
}
/* 设备详细信息 */
.host-details {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8rpx 0;
}
.detail-label {
font-size: 28rpx;
color: #666;
min-width: 140rpx;
}
.detail-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
/* 分页组件样式 */
.pagination-container {
padding: 32rpx;
background: #ffffff;
border-top: 1rpx solid #f0f0f0;
}
.pagination-info {
text-align: center;
margin-bottom: 24rpx;
font-size: 24rpx;
color: #666;
}
.pagination-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.pagination-btn {
padding: 16rpx 24rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 26rpx;
color: #333;
text-align: center;
min-width: 120rpx;
}
.pagination-btn.disabled {
background: #f0f0f0;
color: #ccc;
}
.pagination-pages {
display: flex;
gap: 8rpx;
}
.pagination-page {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 24rpx;
color: #333;
text-align: center;
}
.pagination-page.active {
background: #3cc51f;
color: #ffffff;
}
.pagination-page.ellipsis {
background: transparent;
color: #999;
font-weight: bold;
}
/* 底部操作按钮 */
.footer-action {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx;
background: white;
border-top: 1rpx solid #e8e8e8;
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.location-btn {
width: 100%;
background: #52c41a;
color: white;
border: none;
border-radius: 12rpx;
padding: 25rpx;
font-size: 32rpx;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.3);
transition: all 0.3s ease;
}
.location-btn:active {
background: #389e0d;
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
}
/* 加载状态 */
.loading {
text-align: center;
padding: 40rpx;
color: #999;
font-size: 28rpx;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 80rpx 40rpx;
color: #999;
}
.empty-state .empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
}
.empty-state .empty-text {
font-size: 28rpx;
}

View File

@@ -16,7 +16,7 @@ Page({
currentAlertData: [
{ title: '今日未被采集', value: '6', isAlert: false, bgIcon: '📄', url: '/pages/alert/collar' },
{ title: '项圈绑带剪断', value: '0', isAlert: false, bgIcon: '🏢', url: '/pages/alert/collar' },
{ title: '电子围栏', value: '3', isAlert: false, bgIcon: '🚧', url: '/pages/fence' },
{ title: '电子围栏', value: '3', isAlert: false, bgIcon: '🚧', url: '/pages/device/fence/fence' },
{ title: '今日运动量偏高', value: '0', isAlert: false, bgIcon: '📈', url: '/pages/alert/collar' },
{ title: '今日运动量偏低', value: '3', isAlert: true, bgIcon: '📉', url: '/pages/alert/collar' },
{ title: '传输频次过快', value: '0', isAlert: false, bgIcon: '⚡', url: '/pages/alert/collar' },
@@ -33,7 +33,7 @@ Page({
],
// 智能工具
smartTools: [
{ name: '电子围栏', icon: '🎯', color: 'orange', url: '/pages/fence' },
{ name: '电子围栏', icon: '🎯', color: 'orange', url: '/pages/device/fence/fence' },
{ name: '扫码溯源', icon: '🛡️', color: 'blue', url: '/pages/trace' },
{ name: '档案拍照', icon: '📷', color: 'red', url: '/pages/photo' },
{ name: '检测工具', icon: '📊', color: 'purple', url: '/pages/detection' }
@@ -47,6 +47,8 @@ Page({
},
onLoad() {
// 检查登录状态
this.checkLoginStatus()
this.fetchHomeData()
},
@@ -60,6 +62,30 @@ Page({
})
},
// 检查登录状态
checkLoginStatus() {
const token = wx.getStorageSync('token')
const userInfo = wx.getStorageSync('userInfo')
if (!token || !userInfo) {
console.log('用户未登录,跳转到登录页')
wx.showModal({
title: '提示',
content: '请先登录后再使用',
showCancel: false,
success: () => {
wx.reLaunch({
url: '/pages/login/login'
})
}
})
return false
}
console.log('用户已登录:', userInfo.username)
return true
},
// 获取首页数据
async fetchHomeData() {
this.setData({ loading: true })
@@ -105,7 +131,7 @@ Page({
alertData = [
{ title: '今日未被采集', value: '6', isAlert: false, bgIcon: '📄', url: '/pages/alert/collar' },
{ title: '项圈绑带剪断', value: '0', isAlert: false, bgIcon: '🏢', url: '/pages/alert/collar' },
{ title: '电子围栏', value: '3', isAlert: false, bgIcon: '🚧', url: '/pages/fence' },
{ title: '电子围栏', value: '3', isAlert: false, bgIcon: '🚧', url: '/pages/device/fence/fence' },
{ title: '今日运动量偏高', value: '0', isAlert: false, bgIcon: '📈', url: '/pages/alert/collar' },
{ title: '今日运动量偏低', value: '3', isAlert: true, bgIcon: '📉', url: '/pages/alert/collar' },
{ title: '传输频次过快', value: '0', isAlert: false, bgIcon: '⚡', url: '/pages/alert/collar' },