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

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

@@ -48,26 +48,26 @@ server {
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# WebSocket支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# WebSocket支持(已移除)
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
}
# WebSocket专用代理
location /socket.io/ {
proxy_pass http://backend:5350;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket特定超时
proxy_read_timeout 86400;
}
# WebSocket专用代理(已移除)
# location /socket.io/ {
# proxy_pass http://backend:5350;
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
#
# # WebSocket特定超时
# proxy_read_timeout 86400;
# }
# 百度地图API代理解决跨域问题
location /map-api/ {

View File

@@ -19,7 +19,6 @@
"moment": "^2.29.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"socket.io-client": "^4.7.4",
"vue": "^3.4.15",
"vue-router": "^4.2.5",
"xlsx": "^0.18.5"
@@ -771,11 +770,6 @@
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"node_modules/@types/chai": {
"version": "4.3.20",
"resolved": "https://registry.npmmirror.com/@types/chai/-/chai-4.3.20.tgz",
@@ -2031,42 +2025,6 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
@@ -3414,7 +3372,8 @@
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/muggle-string": {
"version": "0.3.1",
@@ -4125,64 +4084,6 @@
"node": ">=8"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
@@ -5095,26 +4996,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
@@ -5144,14 +5025,6 @@
"node": ">=12"
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -5605,11 +5478,6 @@
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true
},
"@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"@types/chai": {
"version": "4.3.20",
"resolved": "https://registry.npmmirror.com/@types/chai/-/chai-4.3.20.tgz",
@@ -6544,33 +6412,6 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
},
"dependencies": {
"debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"requires": {
"ms": "^2.1.3"
}
}
}
},
"engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="
},
"entities": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
@@ -7551,7 +7392,8 @@
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"muggle-string": {
"version": "0.3.1",
@@ -8031,46 +7873,6 @@
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
},
"socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"dependencies": {
"debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"requires": {
"ms": "^2.1.3"
}
}
}
},
"socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"dependencies": {
"debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"requires": {
"ms": "^2.1.3"
}
}
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
@@ -8659,12 +8461,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"ws": {
"version": "8.17.1",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"requires": {}
},
"xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
@@ -8685,11 +8481,6 @@
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
"dev": true
},
"xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -39,7 +39,6 @@
"file-saver": "^2.0.5",
"moment": "^2.29.4",
"pinia": "^2.1.7",
"socket.io-client": "^4.7.4",
"vue": "^3.4.15",
"vue-router": "^4.2.5",
"xlsx": "^0.18.5",

View File

@@ -276,43 +276,43 @@ export const useDataStore = defineStore('data', () => {
}
// 实时数据更新方法WebSocket调用
function updateDeviceRealtime(deviceData) {
const index = devices.value.findIndex(device => device.id === deviceData.id)
if (index !== -1) {
// 更新现有设备数据
devices.value[index] = { ...devices.value[index], ...deviceData }
console.log(`设备 ${deviceData.id} 实时数据已更新`)
} else {
// 如果是新设备,添加到列表
devices.value.push(deviceData)
console.log(`新设备 ${deviceData.id} 已添加`)
}
}
// function updateDeviceRealtime(deviceData) {
// const index = devices.value.findIndex(device => device.id === deviceData.id)
// if (index !== -1) {
// // 更新现有设备数据
// devices.value[index] = { ...devices.value[index], ...deviceData }
// console.log(`设备 ${deviceData.id} 实时数据已更新`)
// } else {
// // 如果是新设备,添加到列表
// devices.value.push(deviceData)
// console.log(`新设备 ${deviceData.id} 已添加`)
// }
// }
function addNewAlert(alertData) {
// 添加新预警到列表顶部
alerts.value.unshift(alertData)
console.log(`新预警 ${alertData.id} 已添加`)
}
// function addNewAlert(alertData) {
// // 添加新预警到列表顶部
// alerts.value.unshift(alertData)
// console.log(`新预警 ${alertData.id} 已添加`)
// }
function updateAnimalRealtime(animalData) {
const index = animals.value.findIndex(animal => animal.id === animalData.id)
if (index !== -1) {
// 更新现有动物数据
animals.value[index] = { ...animals.value[index], ...animalData }
console.log(`动物 ${animalData.id} 实时数据已更新`)
} else {
// 如果是新动物记录,添加到列表
animals.value.push(animalData)
console.log(`新动物记录 ${animalData.id} 已添加`)
}
}
// function updateAnimalRealtime(animalData) {
// const index = animals.value.findIndex(animal => animal.id === animalData.id)
// if (index !== -1) {
// // 更新现有动物数据
// animals.value[index] = { ...animals.value[index], ...animalData }
// console.log(`动物 ${animalData.id} 实时数据已更新`)
// } else {
// // 如果是新动物记录,添加到列表
// animals.value.push(animalData)
// console.log(`新动物记录 ${animalData.id} 已添加`)
// }
// }
function updateStatsRealtime(statsData) {
// 更新统计数据
stats.value = { ...stats.value, ...statsData }
console.log('系统统计数据已实时更新')
}
// function updateStatsRealtime(statsData) {
// // 更新统计数据
// stats.value = { ...stats.value, ...statsData }
// console.log('系统统计数据已实时更新')
// }
function updateAlertStatus(alertId, status) {
const index = alerts.value.findIndex(alert => alert.id === alertId)
@@ -360,10 +360,10 @@ export const useDataStore = defineStore('data', () => {
fetchAllData,
// 实时数据更新方法
updateDeviceRealtime,
addNewAlert,
updateAnimalRealtime,
updateStatsRealtime,
// updateDeviceRealtime,
// addNewAlert,
// updateAnimalRealtime,
// updateStatsRealtime,
updateAlertStatus
}
})

View File

@@ -71,7 +71,7 @@ export const useUserStore = defineStore('user', () => {
localStorage.setItem('user', JSON.stringify(userData.value));
// 建立WebSocket连接
await connectWebSocket();
// await connectWebSocket();
}
return result;
@@ -87,42 +87,42 @@ export const useUserStore = defineStore('user', () => {
}
// WebSocket连接状态
const isWebSocketConnected = ref(false)
// const isWebSocketConnected = ref(false)
// 建立WebSocket连接
async function connectWebSocket() {
if (!token.value) {
console.log('无token跳过WebSocket连接')
return
}
// async function connectWebSocket() {
// if (!token.value) {
// console.log('无token跳过WebSocket连接')
// return
// }
try {
const webSocketService = await import('../utils/websocketService')
webSocketService.default.connect(token.value)
isWebSocketConnected.value = true
console.log('WebSocket连接已建立')
} catch (error) {
console.error('WebSocket连接失败:', error)
isWebSocketConnected.value = false
}
}
// try {
// const webSocketService = await import('../utils/websocketService')
// webSocketService.default.connect(token.value)
// isWebSocketConnected.value = true
// console.log('WebSocket连接已建立')
// } catch (error) {
// console.error('WebSocket连接失败:', error)
// isWebSocketConnected.value = false
// }
// }
// 断开WebSocket连接
async function disconnectWebSocket() {
try {
const webSocketService = await import('../utils/websocketService')
webSocketService.default.disconnect()
isWebSocketConnected.value = false
console.log('WebSocket连接已断开')
} catch (error) {
console.error('断开WebSocket连接失败:', error)
}
}
// async function disconnectWebSocket() {
// try {
// const webSocketService = await import('../utils/websocketService')
// webSocketService.default.disconnect()
// isWebSocketConnected.value = false
// console.log('WebSocket连接已断开')
// } catch (error) {
// console.error('断开WebSocket连接失败:', error)
// }
// }
// 登出操作
async function logout() {
// 断开WebSocket连接
await disconnectWebSocket()
// await disconnectWebSocket()
token.value = ''
userData.value = null
@@ -222,14 +222,14 @@ export const useUserStore = defineStore('user', () => {
token,
userData,
isLoggedIn,
isWebSocketConnected,
// isWebSocketConnected,
checkLoginStatus,
validateToken,
login,
logout,
updateUserInfo,
connectWebSocket,
disconnectWebSocket,
// connectWebSocket,
// disconnectWebSocket,
hasPermission,
hasRole,
canAccessMenu,

View File

@@ -1,379 +0,0 @@
/**
* WebSocket实时通信服务
* @file websocketService.js
* @description 前端WebSocket客户端处理实时数据接收
*/
import { io } from 'socket.io-client';
import { useUserStore } from '../stores/user';
import { useDataStore } from '../stores/data';
import { message, notification } from 'ant-design-vue';
class WebSocketService {
constructor() {
this.socket = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectInterval = 3000; // 3秒重连间隔
this.userStore = null;
this.dataStore = null;
}
/**
* 连接WebSocket服务器
* @param {string} token JWT认证令牌
*/
connect(token) {
if (this.socket && this.isConnected) {
console.log('WebSocket已连接无需重复连接');
return;
}
// 初始化store
this.userStore = useUserStore();
this.dataStore = useDataStore();
const serverUrl = import.meta.env.VITE_API_URL || 'http://localhost:5350';
console.log('正在连接WebSocket服务器:', serverUrl);
this.socket = io(serverUrl, {
auth: {
token: token
},
transports: ['websocket', 'polling'],
timeout: 20000,
reconnection: true,
reconnectionAttempts: this.maxReconnectAttempts,
reconnectionDelay: this.reconnectInterval
});
this.setupEventListeners();
}
/**
* 设置事件监听器
*/
setupEventListeners() {
if (!this.socket) return;
// 连接成功
this.socket.on('connect', () => {
this.isConnected = true;
this.reconnectAttempts = 0;
console.log('WebSocket连接成功连接ID:', this.socket.id);
message.success('实时数据连接已建立');
});
// 连接确认
this.socket.on('connected', (data) => {
console.log('收到服务器连接确认:', data);
});
// 设备状态更新
this.socket.on('device_update', (data) => {
console.log('收到设备状态更新:', data);
this.handleDeviceUpdate(data);
});
// 预警更新
this.socket.on('alert_update', (data) => {
console.log('收到预警更新:', data);
this.handleAlertUpdate(data);
});
// 紧急预警
this.socket.on('urgent_alert', (data) => {
console.log('收到紧急预警:', data);
this.handleUrgentAlert(data);
});
// 动物健康状态更新
this.socket.on('animal_update', (data) => {
console.log('收到动物健康状态更新:', data);
this.handleAnimalUpdate(data);
});
// 系统统计数据更新
this.socket.on('stats_update', (data) => {
console.log('收到系统统计数据更新:', data);
this.handleStatsUpdate(data);
});
// 性能监控数据(仅管理员)
this.socket.on('performance_update', (data) => {
console.log('收到性能监控数据:', data);
this.handlePerformanceUpdate(data);
});
// 连接断开
this.socket.on('disconnect', (reason) => {
this.isConnected = false;
console.log('WebSocket连接断开:', reason);
if (reason === 'io server disconnect') {
// 服务器主动断开,需要重新连接
this.reconnect();
}
});
// 连接错误
this.socket.on('connect_error', (error) => {
console.error('WebSocket连接错误:', error);
if (error.message.includes('认证失败') || error.message.includes('未提供认证令牌')) {
message.error('实时连接认证失败,请重新登录');
this.userStore.logout();
} else {
this.handleReconnect();
}
});
// 心跳响应
this.socket.on('pong', (data) => {
console.log('收到心跳响应:', data);
});
}
/**
* 处理设备状态更新
* @param {Object} data 设备数据
*/
handleDeviceUpdate(data) {
// 更新数据存储中的设备状态
if (this.dataStore) {
this.dataStore.updateDeviceRealtime(data.data);
}
// 如果设备状态异常,显示通知
if (data.data.status === 'offline') {
notification.warning({
message: '设备状态变化',
description: `设备 ${data.data.name} 已离线`,
duration: 4.5,
});
} else if (data.data.status === 'maintenance') {
notification.info({
message: '设备状态变化',
description: `设备 ${data.data.name} 进入维护模式`,
duration: 4.5,
});
}
}
/**
* 处理预警更新
* @param {Object} data 预警数据
*/
handleAlertUpdate(data) {
// 更新数据存储中的预警数据
if (this.dataStore) {
this.dataStore.addNewAlert(data.data);
}
// 显示预警通知
const alertLevel = data.data.level;
let notificationType = 'info';
if (alertLevel === 'critical') {
notificationType = 'error';
} else if (alertLevel === 'high') {
notificationType = 'warning';
}
notification[notificationType]({
message: '新预警',
description: `${data.data.farm_name}: ${data.data.message}`,
duration: 6,
});
}
/**
* 处理紧急预警
* @param {Object} data 紧急预警数据
*/
handleUrgentAlert(data) {
// 紧急预警使用模态框显示
notification.error({
message: '🚨 紧急预警',
description: `${data.alert.farm_name}: ${data.alert.message}`,
duration: 0, // 不自动关闭
style: {
backgroundColor: '#fff2f0',
border: '1px solid #ffccc7'
}
});
// 播放警报声音(如果浏览器支持)
this.playAlertSound();
}
/**
* 处理动物健康状态更新
* @param {Object} data 动物数据
*/
handleAnimalUpdate(data) {
// 更新数据存储
if (this.dataStore) {
this.dataStore.updateAnimalRealtime(data.data);
}
// 如果动物健康状态异常,显示通知
if (data.data.health_status === 'sick') {
notification.warning({
message: '动物健康状态变化',
description: `${data.data.farm_name}${data.data.type}出现健康问题`,
duration: 5,
});
} else if (data.data.health_status === 'quarantined') {
notification.error({
message: '动物健康状态变化',
description: `${data.data.farm_name}${data.data.type}已隔离`,
duration: 6,
});
}
}
/**
* 处理系统统计数据更新
* @param {Object} data 统计数据
*/
handleStatsUpdate(data) {
// 更新数据存储中的统计信息
if (this.dataStore) {
this.dataStore.updateStatsRealtime(data.data);
}
}
/**
* 处理性能监控数据更新
* @param {Object} data 性能数据
*/
handlePerformanceUpdate(data) {
// 只有管理员才能看到性能数据
if (this.userStore?.user?.roles?.includes('admin')) {
console.log('收到性能监控数据:', data);
// 可以通过事件总线通知性能监控组件更新
window.dispatchEvent(new CustomEvent('performance_update', { detail: data }));
}
}
/**
* 播放警报声音
*/
playAlertSound() {
try {
// 创建音频上下文
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 生成警报音
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.5);
} catch (error) {
console.log('无法播放警报声音:', error);
}
}
/**
* 订阅农场数据
* @param {number} farmId 农场ID
*/
subscribeFarm(farmId) {
if (this.socket && this.isConnected) {
this.socket.emit('subscribe_farm', farmId);
console.log(`已订阅农场 ${farmId} 的实时数据`);
}
}
/**
* 取消订阅农场数据
* @param {number} farmId 农场ID
*/
unsubscribeFarm(farmId) {
if (this.socket && this.isConnected) {
this.socket.emit('unsubscribe_farm', farmId);
console.log(`已取消订阅农场 ${farmId} 的实时数据`);
}
}
/**
* 订阅设备数据
* @param {number} deviceId 设备ID
*/
subscribeDevice(deviceId) {
if (this.socket && this.isConnected) {
this.socket.emit('subscribe_device', deviceId);
console.log(`已订阅设备 ${deviceId} 的实时数据`);
}
}
/**
* 发送心跳
*/
sendHeartbeat() {
if (this.socket && this.isConnected) {
this.socket.emit('ping');
}
}
/**
* 处理重连
*/
handleReconnect() {
this.reconnectAttempts++;
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
console.log(`尝试重连WebSocket (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => {
this.reconnect();
}, this.reconnectInterval * this.reconnectAttempts);
} else {
message.error('实时连接已断开,请刷新页面重试');
}
}
/**
* 重新连接
*/
reconnect() {
if (this.userStore?.token) {
this.connect(this.userStore.token);
}
}
/**
* 断开连接
*/
disconnect() {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
this.isConnected = false;
console.log('WebSocket连接已断开');
}
}
/**
* 获取连接状态
* @returns {boolean} 连接状态
*/
getConnectionStatus() {
return this.isConnected;
}
}
// 创建单例实例
const webSocketService = new WebSocketService();
export default webSocketService;

View File

@@ -29,7 +29,6 @@
"redis": "^4.6.12",
"sequelize": "^6.35.2",
"sharp": "^0.33.2",
"socket.io": "^4.7.4",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"winston": "^3.11.0",
@@ -1821,11 +1820,6 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1867,14 +1861,6 @@
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmmirror.com/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz",
@@ -2472,14 +2458,6 @@
"integrity": "sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==",
"optional": true
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.4",
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.4.tgz",
@@ -3502,62 +3480,6 @@
"node": ">= 0.8"
}
},
"node_modules/engine.io": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/engine.io/-/engine.io-6.6.4.tgz",
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz",
@@ -8529,107 +8451,6 @@
"node": ">=8"
}
},
"node_modules/socket.io": {
"version": "4.8.1",
"resolved": "https://registry.npmmirror.com/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmmirror.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-adapter/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
@@ -9715,26 +9536,6 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
@@ -11088,11 +10889,6 @@
"@sinonjs/commons": "^3.0.0"
}
},
"@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -11134,14 +10930,6 @@
"@babel/types": "^7.28.2"
}
},
"@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmmirror.com/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"requires": {
"@types/node": "*"
}
},
"@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz",
@@ -11615,11 +11403,6 @@
"integrity": "sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==",
"optional": true
},
"base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
},
"baseline-browser-mapping": {
"version": "2.8.4",
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.4.tgz",
@@ -12367,47 +12150,6 @@
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
},
"engine.io": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/engine.io/-/engine.io-6.6.4.tgz",
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"requires": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"dependencies": {
"cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="
},
"debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"requires": {
"ms": "^2.1.3"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
"engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="
},
"error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz",
@@ -15990,83 +15732,6 @@
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
},
"socket.io": {
"version": "4.8.1",
"resolved": "https://registry.npmmirror.com/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"requires": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"dependencies": {
"debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"requires": {
"ms": "^2.1.3"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
"socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmmirror.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"requires": {
"debug": "~4.3.4",
"ws": "~8.17.1"
},
"dependencies": {
"debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"requires": {
"ms": "^2.1.3"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
"socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"dependencies": {
"debug": {
"version": "4.3.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"requires": {
"ms": "^2.1.3"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
@@ -16858,12 +16523,6 @@
"signal-exit": "^3.0.7"
}
},
"ws": {
"version": "8.17.1",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"requires": {}
},
"xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",

View File

@@ -59,7 +59,6 @@
"redis": "^4.6.12",
"sequelize": "^6.35.2",
"sharp": "^0.33.2",
"socket.io": "^4.7.4",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"winston": "^3.11.0",

View File

@@ -6,7 +6,6 @@ const multer = require('multer');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./config/swagger');
const { sequelize } = require('./config/database-simple');
const webSocketManager = require('./utils/websocket');
const logger = require('./utils/logger');
const {
apiRateLimiter,
@@ -264,24 +263,23 @@ app.use((err, req, res, next) => {
});
// 初始化WebSocket
webSocketManager.init(server);
// webSocketManager.init(server);
// 初始化实时数据推送服务
const realtimeService = require('./services/realtimeService');
// const realtimeService = require('./services/realtimeService');
// 启动服务器
server.listen(PORT, '0.0.0.0', () => {
console.log(`服务器运行在端口 ${PORT}`);
console.log(`服务器监听所有网络接口 (0.0.0.0:${PORT})`);
console.log(`API 文档地址: http://localhost:${PORT}/api-docs`);
console.log(`WebSocket 服务已启动`);
// 启动实时数据推送服务
realtimeService.start();
console.log(`实时数据推送服务已启动`);
// realtimeService.start();
// console.log(`实时数据推送服务已启动`);
logger.info(`宁夏智慧养殖监管平台服务器启动成功,端口: ${PORT}`);
});
// 导出app和webSocketManager供其他模块使用
module.exports = { app, webSocketManager };
// 导出app供其他模块使用
module.exports = { app };

View File

@@ -1,364 +0,0 @@
/**
* 实时数据推送服务
* @file realtimeService.js
* @description 定期检查数据变化并通过WebSocket推送给客户端
*/
const cron = require('node-cron');
const { Device, Alert, Animal, Farm } = require('../models');
const { sequelize } = require('../config/database-simple');
const webSocketManager = require('../utils/websocket');
const notificationService = require('./notificationService');
const logger = require('../utils/logger');
const { Op } = require('sequelize');
class RealtimeService {
constructor() {
this.isRunning = false;
this.lastUpdateTimes = {
devices: null,
alerts: null,
animals: null,
stats: null
};
this.updateInterval = 30; // 30秒更新间隔符合文档要求
}
/**
* 启动实时数据推送服务
*/
start() {
if (this.isRunning) {
logger.warn('实时数据推送服务已在运行中');
return;
}
this.isRunning = true;
// 设备状态监控 - 每30秒检查一次
cron.schedule(`*/${this.updateInterval} * * * * *`, () => {
this.checkDeviceUpdates();
});
// 预警监控 - 每10秒检查一次预警更紧急
cron.schedule('*/10 * * * * *', () => {
this.checkAlertUpdates();
});
// 动物健康状态监控 - 每60秒检查一次
cron.schedule('*/60 * * * * *', () => {
this.checkAnimalUpdates();
});
// 系统统计数据更新 - 每2分钟更新一次
cron.schedule('*/120 * * * * *', () => {
this.updateSystemStats();
});
logger.info('实时数据推送服务已启动');
console.log(`实时数据推送服务已启动,更新间隔: ${this.updateInterval}`);
}
/**
* 停止实时数据推送服务
*/
stop() {
this.isRunning = false;
logger.info('实时数据推送服务已停止');
}
/**
* 检查设备状态更新
*/
async checkDeviceUpdates() {
try {
const lastCheck = this.lastUpdateTimes.devices || new Date(Date.now() - 60000); // 默认检查最近1分钟
const updatedDevices = await Device.findAll({
where: {
updated_at: {
[Op.gt]: lastCheck
}
},
include: [{
model: Farm,
as: 'farm',
attributes: ['id', 'name']
}],
order: [['updated_at', 'DESC']]
});
if (updatedDevices.length > 0) {
logger.info(`检测到 ${updatedDevices.length} 个设备状态更新`);
// 为每个更新的设备推送数据
for (const device of updatedDevices) {
webSocketManager.broadcastDeviceUpdate({
id: device.id,
name: device.name,
type: device.type,
status: device.status,
farm_id: device.farm_id,
farm_name: device.farm?.name,
last_maintenance: device.last_maintenance,
metrics: device.metrics,
updated_at: device.updated_at
});
}
this.lastUpdateTimes.devices = new Date();
}
} catch (error) {
logger.error('检查设备更新失败:', error);
}
}
/**
* 检查预警更新
*/
async checkAlertUpdates() {
try {
const lastCheck = this.lastUpdateTimes.alerts || new Date(Date.now() - 30000); // 默认检查最近30秒
const newAlerts = await Alert.findAll({
where: {
created_at: {
[Op.gt]: lastCheck
}
},
include: [
{
model: Farm,
as: 'farm',
attributes: ['id', 'name', 'contact', 'phone']
},
{
model: Device,
as: 'device',
attributes: ['id', 'name', 'type']
}
],
order: [['created_at', 'DESC']]
});
if (newAlerts.length > 0) {
logger.info(`检测到 ${newAlerts.length} 个新预警`);
// 推送新预警
for (const alert of newAlerts) {
webSocketManager.broadcastAlert({
id: alert.id,
type: alert.type,
level: alert.level,
message: alert.message,
status: alert.status,
farm_id: alert.farm_id,
farm_name: alert.farm?.name,
device_id: alert.device_id,
device_name: alert.device?.name,
created_at: alert.created_at
});
// 如果是高级或紧急预警,立即发送通知
if (alert.level === 'high' || alert.level === 'critical') {
await this.sendUrgentNotification(alert);
}
}
this.lastUpdateTimes.alerts = new Date();
}
} catch (error) {
logger.error('检查预警更新失败:', error);
}
}
/**
* 检查动物健康状态更新
*/
async checkAnimalUpdates() {
try {
const lastCheck = this.lastUpdateTimes.animals || new Date(Date.now() - 120000); // 默认检查最近2分钟
const updatedAnimals = await Animal.findAll({
where: {
updated_at: {
[Op.gt]: lastCheck
}
},
include: [{
model: Farm,
as: 'farm',
attributes: ['id', 'name']
}],
order: [['updated_at', 'DESC']]
});
if (updatedAnimals.length > 0) {
logger.info(`检测到 ${updatedAnimals.length} 个动物健康状态更新`);
for (const animal of updatedAnimals) {
webSocketManager.broadcastAnimalUpdate({
id: animal.id,
type: animal.type,
count: animal.count,
health_status: animal.health_status,
farm_id: animal.farm_id,
farm_name: animal.farm?.name,
last_inspection: animal.last_inspection,
updated_at: animal.updated_at
});
}
this.lastUpdateTimes.animals = new Date();
}
} catch (error) {
logger.error('检查动物更新失败:', error);
}
}
/**
* 更新系统统计数据
*/
async updateSystemStats() {
try {
const stats = await this.getSystemStats();
webSocketManager.broadcastStatsUpdate(stats);
this.lastUpdateTimes.stats = new Date();
logger.info('系统统计数据已推送');
} catch (error) {
logger.error('更新系统统计失败:', error);
}
}
/**
* 获取系统统计数据
* @returns {Promise<Object>} 统计数据
*/
async getSystemStats() {
try {
const [farmCount, deviceCount, animalCount, alertCount] = await Promise.all([
Farm.count(),
Device.count(),
Animal.sum('count'),
Alert.count({ where: { status: 'active' } })
]);
const deviceStatusStats = await Device.findAll({
attributes: [
'status',
[sequelize.fn('COUNT', sequelize.col('status')), 'count']
],
group: ['status']
});
const alertLevelStats = await Alert.findAll({
where: { status: 'active' },
attributes: [
'level',
[sequelize.fn('COUNT', sequelize.col('level')), 'count']
],
group: ['level']
});
return {
farmCount: farmCount || 0,
deviceCount: deviceCount || 0,
animalCount: animalCount || 0,
alertCount: alertCount || 0,
deviceStatus: deviceStatusStats.reduce((acc, item) => {
acc[item.status] = parseInt(item.dataValues.count);
return acc;
}, {}),
alertLevels: alertLevelStats.reduce((acc, item) => {
acc[item.level] = parseInt(item.dataValues.count);
return acc;
}, {}),
timestamp: new Date()
};
} catch (error) {
logger.error('获取系统统计数据失败:', error);
return {
error: '获取统计数据失败',
timestamp: new Date()
};
}
}
/**
* 发送紧急预警通知
* @param {Object} alert 预警对象
*/
async sendUrgentNotification(alert) {
try {
logger.warn(`紧急预警: ${alert.message} (级别: ${alert.level})`);
// 发送实时WebSocket通知给管理员
webSocketManager.broadcastAlert({
id: alert.id,
type: alert.type,
level: alert.level,
message: alert.message,
farm_id: alert.farm_id,
farm_name: alert.farm?.name,
created_at: alert.created_at
});
// 发送邮件/短信通知
const isUrgent = alert.level === 'critical' || alert.level === 'high';
await notificationService.sendAlertNotification(alert, [], {
urgent: isUrgent,
includeSMS: alert.level === 'critical', // 仅紧急预警发送短信
maxResponseTime: 300000 // 5分钟响应时间
});
logger.info(`预警通知已发送: 预警ID ${alert.id}, 紧急程度: ${isUrgent}`);
} catch (error) {
logger.error('发送紧急预警通知失败:', error);
}
}
/**
* 模拟设备数据变化(用于演示)
*/
async simulateDeviceChange(deviceId) {
try {
const device = await Device.findByPk(deviceId);
if (!device) return;
// 随机改变设备状态
const statuses = ['online', 'offline', 'maintenance'];
const randomStatus = statuses[Math.floor(Math.random() * statuses.length)];
await device.update({
status: randomStatus,
metrics: {
temperature: Math.random() * 10 + 20, // 20-30度
humidity: Math.random() * 20 + 50, // 50-70%
lastUpdate: new Date()
}
});
logger.info(`模拟设备 ${deviceId} 状态变化为: ${randomStatus}`);
} catch (error) {
logger.error('模拟设备变化失败:', error);
}
}
/**
* 获取服务状态
* @returns {Object} 服务状态
*/
getStatus() {
return {
isRunning: this.isRunning,
lastUpdateTimes: this.lastUpdateTimes,
updateInterval: this.updateInterval,
connectedClients: webSocketManager.getConnectionStats()
};
}
}
// 创建单例实例
const realtimeService = new RealtimeService();
module.exports = realtimeService;

View File

@@ -1,339 +0,0 @@
/**
* WebSocket实时通信系统
* @file websocket.js
* @description 实现实时数据推送,替代轮询机制
*/
const socketIO = require('socket.io');
const jwt = require('jsonwebtoken');
const { User, Role } = require('../models');
const logger = require('./logger');
class WebSocketManager {
constructor() {
this.io = null;
this.connectedClients = new Map(); // 存储连接的客户端信息
this.rooms = {
admins: 'admin_room',
users: 'user_room',
farms: 'farm_', // farm_1, farm_2 等
devices: 'device_', // device_1, device_2 等
};
}
/**
* 初始化WebSocket服务器
* @param {Object} server HTTP服务器实例
*/
init(server) {
this.io = socketIO(server, {
cors: {
origin: ["http://localhost:5300", "http://localhost:3000"],
methods: ["GET", "POST"],
credentials: true
},
pingTimeout: 60000,
pingInterval: 25000
});
// 中间件:认证
this.io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.split(' ')[1];
if (!token) {
return next(new Error('未提供认证令牌'));
}
// 验证JWT令牌
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your_jwt_secret_key');
// 获取用户信息和角色
const user = await User.findByPk(decoded.id, {
include: [{
model: Role,
as: 'role',
attributes: ['name']
}]
});
if (!user) {
return next(new Error('用户不存在'));
}
// 将用户信息附加到socket
socket.user = {
id: user.id,
username: user.username,
email: user.email,
roles: user.role ? [user.role.name] : []
};
next();
} catch (error) {
logger.error('WebSocket认证失败:', error);
next(new Error('认证失败'));
}
});
// 连接事件处理
this.io.on('connection', (socket) => {
this.handleConnection(socket);
});
logger.info('WebSocket服务器初始化完成');
return this.io;
}
/**
* 处理客户端连接
* @param {Object} socket Socket实例
*/
handleConnection(socket) {
const user = socket.user;
// 存储客户端信息
this.connectedClients.set(socket.id, {
userId: user.id,
username: user.username,
roles: user.roles,
connectedAt: new Date()
});
// 加入相应的房间
this.joinRooms(socket, user);
logger.info(`用户 ${user.username} 已连接 WebSocket连接ID: ${socket.id}`);
// 发送连接成功消息
socket.emit('connected', {
message: '实时连接已建立',
timestamp: new Date(),
user: {
id: user.id,
username: user.username
}
});
// 处理客户端事件
this.setupSocketEvents(socket);
// 断开连接处理
socket.on('disconnect', () => {
this.handleDisconnection(socket);
});
}
/**
* 让用户加入相应的房间
* @param {Object} socket Socket实例
* @param {Object} user 用户信息
*/
joinRooms(socket, user) {
// 所有用户加入用户房间
socket.join(this.rooms.users);
// 管理员加入管理员房间
if (user.roles && user.roles.includes('admin')) {
socket.join(this.rooms.admins);
}
// 可以根据业务需求加入特定的农场或设备房间
// 这里暂时加入全局房间,后续可以根据用户权限细化
}
/**
* 设置Socket事件监听
* @param {Object} socket Socket实例
*/
setupSocketEvents(socket) {
// 订阅特定农场的数据
socket.on('subscribe_farm', (farmId) => {
socket.join(`${this.rooms.farms}${farmId}`);
logger.info(`用户 ${socket.user.username} 订阅农场 ${farmId} 的实时数据`);
});
// 取消订阅农场数据
socket.on('unsubscribe_farm', (farmId) => {
socket.leave(`${this.rooms.farms}${farmId}`);
logger.info(`用户 ${socket.user.username} 取消订阅农场 ${farmId} 的实时数据`);
});
// 订阅特定设备的数据
socket.on('subscribe_device', (deviceId) => {
socket.join(`${this.rooms.devices}${deviceId}`);
logger.info(`用户 ${socket.user.username} 订阅设备 ${deviceId} 的实时数据`);
});
// 心跳检测
socket.on('ping', () => {
socket.emit('pong', { timestamp: new Date() });
});
}
/**
* 处理客户端断开连接
* @param {Object} socket Socket实例
*/
handleDisconnection(socket) {
const clientInfo = this.connectedClients.get(socket.id);
if (clientInfo) {
logger.info(`用户 ${clientInfo.username} 断开 WebSocket 连接连接ID: ${socket.id}`);
this.connectedClients.delete(socket.id);
}
}
/**
* 广播设备状态更新
* @param {Object} deviceData 设备数据
*/
broadcastDeviceUpdate(deviceData) {
if (!this.io) return;
// 向所有用户广播设备更新
this.io.to(this.rooms.users).emit('device_update', {
type: 'device_status',
data: deviceData,
timestamp: new Date()
});
// 向特定设备的订阅者发送更新
this.io.to(`${this.rooms.devices}${deviceData.id}`).emit('device_detail_update', {
type: 'device_detail',
data: deviceData,
timestamp: new Date()
});
logger.info(`设备状态更新已广播: 设备ID ${deviceData.id}`);
}
/**
* 广播预警信息
* @param {Object} alertData 预警数据
*/
broadcastAlert(alertData) {
if (!this.io) return;
// 向管理员发送紧急预警
if (alertData.level === 'critical' || alertData.level === 'high') {
this.io.to(this.rooms.admins).emit('urgent_alert', {
type: 'urgent_alert',
data: alertData,
timestamp: new Date()
});
}
// 向所有用户广播预警
this.io.to(this.rooms.users).emit('alert_update', {
type: 'new_alert',
data: alertData,
timestamp: new Date()
});
// 向特定农场的订阅者发送预警
if (alertData.farm_id) {
this.io.to(`${this.rooms.farms}${alertData.farm_id}`).emit('farm_alert', {
type: 'farm_alert',
data: alertData,
timestamp: new Date()
});
}
logger.info(`预警信息已广播: 预警ID ${alertData.id}, 级别: ${alertData.level}`);
}
/**
* 广播动物健康数据更新
* @param {Object} animalData 动物数据
*/
broadcastAnimalUpdate(animalData) {
if (!this.io) return;
this.io.to(this.rooms.users).emit('animal_update', {
type: 'animal_health',
data: animalData,
timestamp: new Date()
});
// 向特定农场的订阅者发送动物更新
if (animalData.farm_id) {
this.io.to(`${this.rooms.farms}${animalData.farm_id}`).emit('farm_animal_update', {
type: 'farm_animal',
data: animalData,
timestamp: new Date()
});
}
logger.info(`动物健康数据更新已广播: 动物ID ${animalData.id}`);
}
/**
* 广播系统统计数据更新
* @param {Object} statsData 统计数据
*/
broadcastStatsUpdate(statsData) {
if (!this.io) return;
this.io.to(this.rooms.users).emit('stats_update', {
type: 'system_stats',
data: statsData,
timestamp: new Date()
});
logger.info('系统统计数据更新已广播');
}
/**
* 发送性能监控数据
* @param {Object} performanceData 性能数据
*/
broadcastPerformanceUpdate(performanceData) {
if (!this.io) return;
// 只向管理员发送性能数据
this.io.to(this.rooms.admins).emit('performance_update', {
type: 'system_performance',
data: performanceData,
timestamp: new Date()
});
logger.info('性能监控数据已发送给管理员');
}
/**
* 获取连接统计信息
* @returns {Object} 连接统计
*/
getConnectionStats() {
return {
totalConnections: this.connectedClients.size,
connectedUsers: Array.from(this.connectedClients.values()).map(client => ({
userId: client.userId,
username: client.username,
connectedAt: client.connectedAt
}))
};
}
/**
* 向特定用户发送消息
* @param {number} userId 用户ID
* @param {string} event 事件名称
* @param {Object} data 数据
*/
sendToUser(userId, event, data) {
if (!this.io) return;
for (const [socketId, clientInfo] of this.connectedClients.entries()) {
if (clientInfo.userId === userId) {
this.io.to(socketId).emit(event, data);
logger.info(`消息已发送给用户 ${clientInfo.username}: ${event}`);
}
}
}
}
// 创建单例实例
const webSocketManager = new WebSocketManager();
module.exports = webSocketManager;

View File

@@ -19,10 +19,10 @@
"vue-router": "^4.2.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"eslint": "^8.45.0",
"eslint-plugin-vue": "^9.15.1",
"vite": "^4.4.5"
"@vitejs/plugin-vue": "^4.6.2",
"eslint": "^8.57.1",
"eslint-plugin-vue": "^9.33.0",
"vite": "^4.5.14"
}
},
"node_modules/@ant-design/colors": {

View File

@@ -298,24 +298,34 @@ const isModuleIndeterminate = (module) => {
}
// 获取所有角色和权限数据
const fetchData = async () => {
loading.value = true
const loadData = async () => {
try {
loading.value = true
const [rolesResponse, permissionsResponse] = await Promise.all([
rolePermissionAPI.getAllRolesWithPermissions(),
rolePermissionAPI.getAllPermissions()
])
if (rolesResponse.success) {
roles.value = rolesResponse.data
// 处理角色数据
if (rolesResponse.data && rolesResponse.data.status === 'success') {
roles.value = rolesResponse.data.data.roles || []
console.log('加载角色数据成功:', roles.value.length, '个角色')
} else {
console.error('角色数据响应格式错误:', rolesResponse)
message.error('加载角色数据失败: 响应格式错误')
}
if (permissionsResponse.success) {
permissions.value = permissionsResponse.data
// 处理权限数据
if (permissionsResponse.data && permissionsResponse.data.status === 'success') {
permissions.value = permissionsResponse.data.data || []
console.log('加载权限数据成功:', permissions.value.length, '个权限')
} else {
console.error('权限数据响应格式错误:', permissionsResponse)
message.error('加载权限数据失败: 响应格式错误')
}
} catch (error) {
console.error('获取数据失败:', error)
message.error('获取数据失败')
console.error('加载数据失败:', error)
message.error('加载数据失败: ' + error.message)
} finally {
loading.value = false
}
@@ -325,26 +335,47 @@ const fetchData = async () => {
const handleRoleChange = async (roleId) => {
if (!roleId) return
console.log('=== 角色变化处理开始 ===');
console.log('选择的角色ID:', roleId);
try {
const response = await rolePermissionAPI.getRolePermissions(roleId)
if (response.success) {
// 重置选择状态
console.log('获取角色权限API响应:', JSON.stringify(response, null, 2));
if (response.data && response.data.status === 'success') {
// 清空之前的选择
console.log('清空前的权限状态:', JSON.stringify(selectedPermissions, null, 2));
Object.keys(selectedPermissions).forEach(key => {
selectedPermissions[key] = false
})
console.log('清空后的权限状态:', JSON.stringify(selectedPermissions, null, 2));
// 设置当前角色的权限
response.data.permissions.forEach(permission => {
selectedPermissions[permission.id] = true
})
if (response.data.data && response.data.data.allPermissions) {
const assignedPermissions = response.data.data.allPermissions.filter(p => p.assigned);
console.log('角色已分配的权限:', assignedPermissions.length, '个');
console.log('已分配权限详情:', assignedPermissions.map(p => ({ id: p.id, name: p.name, code: p.code })));
assignedPermissions.forEach(permission => {
selectedPermissions[permission.id] = true
})
console.log('设置权限后的状态:', JSON.stringify(selectedPermissions, null, 2));
}
// 默认展开所有模块
// 展开所有模块
activeModules.value = permissionModules.value.map(m => m.module)
console.log('展开的模块:', activeModules.value);
} else {
console.error('获取角色权限详情失败:', response)
message.error('获取角色权限详情失败')
}
} catch (error) {
console.error('获取角色权限失败:', error)
message.error('获取角色权限失败')
}
console.log('=== 角色变化处理结束 ===');
}
// 权限变化处理
@@ -364,9 +395,17 @@ const handleModuleCheckChange = (e, module) => {
// 全选处理
const handleSelectAll = () => {
console.log('=== 全选操作开始 ===');
console.log('全选前的权限状态:', JSON.stringify(selectedPermissions, null, 2));
console.log('当前权限列表长度:', permissions.value.length);
permissions.value.forEach(permission => {
selectedPermissions[permission.id] = true
})
console.log('全选后的权限状态:', JSON.stringify(selectedPermissions, null, 2));
console.log('全选操作完成,选中权限数量:', Object.values(selectedPermissions).filter(Boolean).length);
console.log('=== 全选操作结束 ===');
}
// 全不选处理
@@ -405,27 +444,43 @@ const handleSavePermissions = async () => {
return
}
console.log('=== 保存权限设置开始 ===');
console.log('当前选择的角色ID:', selectedRoleId.value);
console.log('保存前的权限状态:', JSON.stringify(selectedPermissions, null, 2));
saveLoading.value = true
try {
const permissionIds = Object.keys(selectedPermissions)
.filter(id => selectedPermissions[id])
.map(id => parseInt(id))
const response = await rolePermissionAPI.assignRolePermissions(selectedRoleId.value, {
console.log('准备保存的权限ID列表:', permissionIds);
console.log('权限ID数量:', permissionIds.length);
const requestData = {
permissionIds,
mode: 'replace'
})
};
console.log('发送的请求数据:', JSON.stringify(requestData, null, 2));
if (response.success) {
const response = await rolePermissionAPI.assignRolePermissions(selectedRoleId.value, requestData)
console.log('保存权限API响应:', JSON.stringify(response, null, 2));
if (response.data && response.data.status === 'success') {
message.success('权限设置保存成功')
console.log('✅ 权限保存成功');
} else {
message.error(response.message || '保存失败')
console.error('❌ 保存权限设置失败:', response)
message.error('保存权限设置失败')
}
} catch (error) {
console.error('保存权限设置失败:', error)
console.error('保存权限设置异常:', error)
console.error('错误详情:', error.response?.data || error.message)
message.error('保存权限设置失败')
} finally {
saveLoading.value = false
console.log('=== 保存权限设置结束 ===');
}
}
@@ -450,13 +505,13 @@ const handleConfirmCopy = async () => {
}
try {
const response = await rolePermissionAPI.copyRolePermissions(
copyForm.sourceRoleId,
copyForm.targetRoleId,
copyForm.mode
)
const response = await rolePermissionAPI.copyRolePermissions({
sourceRoleId: copyForm.sourceRoleId,
targetRoleId: copyForm.targetRoleId,
mode: copyForm.mode
})
if (response.success) {
if (response.data && response.data.status === 'success') {
message.success('权限复制成功')
copyModalVisible.value = false
@@ -465,7 +520,8 @@ const handleConfirmCopy = async () => {
handleRoleChange(selectedRoleId.value)
}
} else {
message.error(response.message || '复制失败')
console.error('复制权限失败:', response)
message.error('复制权限失败')
}
} catch (error) {
console.error('复制权限失败:', error)
@@ -475,7 +531,7 @@ const handleConfirmCopy = async () => {
// 刷新数据
const handleRefresh = () => {
fetchData()
loadData()
if (selectedRoleId.value) {
handleRoleChange(selectedRoleId.value)
}
@@ -492,7 +548,7 @@ watch(permissions, (newPermissions) => {
// 组件挂载时获取数据
onMounted(() => {
fetchData()
loadData()
})
</script>

View File

@@ -20,6 +20,7 @@ import RangePickerTest from '@/views/RangePickerTest.vue'
import LoginTest from '@/views/LoginTest.vue'
import LivestockPolicyManagement from '@/views/LivestockPolicyManagement.vue'
import SystemSettings from '@/views/SystemSettings.vue'
import TokenDebug from '@/views/TokenDebug.vue'
const routes = [
{
@@ -162,6 +163,12 @@ const routes = [
name: 'LoginTest',
component: LoginTest,
meta: { title: '登录和API测试' }
},
{
path: 'token-debug',
name: 'TokenDebug',
component: TokenDebug,
meta: { title: 'Token调试' }
}
]
}
@@ -177,7 +184,7 @@ router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
// 如果访问登录页面且已登录,重定向到仪表板
if (to.path === '/login' && (userStore.token || userStore.accessToken)) {
if (to.path === '/login' && userStore.accessToken) {
next('/dashboard')
return
}
@@ -189,7 +196,7 @@ router.beforeEach(async (to, from, next) => {
await userStore.ensureValidToken()
// 检查是否有有效的Token
if (!userStore.accessToken && !userStore.token) {
if (!userStore.accessToken) {
// 尝试自动重新登录
const autoLoginSuccess = await userStore.autoRelogin()

View File

@@ -75,6 +75,14 @@ export const useUserStore = defineStore('user', () => {
})
}
// 兼容性方法 - 支持旧的setToken调用
const setToken = (token) => {
if (token) {
accessToken.value = token
debouncedUpdateStorage('accessToken', token)
}
}
const logout = () => {
// 清除状态
accessToken.value = ''
@@ -199,6 +207,7 @@ export const useUserStore = defineStore('user', () => {
// 方法
setAuthData,
setToken, // 兼容性方法
logout,
refreshAccessToken,
ensureValidToken,

View File

@@ -138,7 +138,7 @@ export const livestockPolicyApi = {
getById: (id) => api.get(`/livestock-policies/${id}`),
updateStatus: (id, data) => api.patch(`/livestock-policies/${id}/status`, data),
getStats: () => api.get('/livestock-policies/stats'),
getLivestockTypes: () => api.get('/livestock-types?status=active')
getLivestockTypes: () => api.get('/livestock-types/active')
}
export const livestockClaimApi = {
@@ -171,7 +171,7 @@ export const permissionAPI = {
getRolePermissions: (roleId) => api.get(`/permissions/roles/${roleId}`)
}
// 角色权限管理API
// 角色权限管理API - 使用统一的请求方法
export const rolePermissionAPI = {
// 获取所有角色及其权限
getAllRolesWithPermissions: () => api.get('/role-permissions/roles'),
@@ -180,18 +180,16 @@ export const rolePermissionAPI = {
getAllPermissions: () => api.get('/role-permissions/permissions'),
// 获取指定角色的详细权限信息
getRolePermissions: (roleId) => api.get(`/role-permissions/roles/${roleId}`),
getRolePermissions: (roleId) => api.get(`/role-permissions/${roleId}`),
// 批量分配角色权限
assignRolePermissions: (roleId, data) => api.post(`/role-permissions/roles/${roleId}/assign`, data),
assignRolePermissions: (roleId, requestData) => api.post(`/role-permissions/${roleId}/assign`, requestData),
// 复制角色权限
copyRolePermissions: (sourceRoleId, targetRoleId, mode) =>
api.post(`/role-permissions/roles/${sourceRoleId}/copy/${targetRoleId}`, { mode }),
copyRolePermissions: (copyData) => api.post('/role-permissions/copy', copyData),
// 检查用户权限
checkUserPermission: (userId, permissionCode) =>
api.get(`/role-permissions/users/${userId}/check/${permissionCode}`),
checkUserPermission: (userId, permissionCode) => api.get(`/role-permissions/check/${userId}/${permissionCode}`),
// 获取权限统计
getPermissionStats: () => api.get('/role-permissions/stats')

View File

@@ -469,18 +469,21 @@ const loadApplications = async () => {
...searchForm
}
const response = await applicationAPI.getList(params)
console.log('申请列表API响应:', response)
// 使用数据验证工具处理响应
const validatedResponse = validateListResponse(response, '保险申请列表')
applications.value = validatedResponse.data
// 设置分页信息
const validatedPagination = validatePagination(validatedResponse.pagination)
pagination.total = validatedPagination.total
if (response.data && response.data.status === 'success') {
// API返回的数据直接是数组格式不是{list: [], total: 146}格式
applications.value = response.data.data || []
pagination.total = response.data.pagination?.total || 0
console.log('申请列表数据设置成功:', applications.value.length, '条')
} else {
console.log('申请列表响应格式错误:', response)
message.error('加载申请数据失败')
applications.value = []
}
} catch (error) {
console.error('加载申请数据失败:', error)
message.error('加载申请数据失败')
// 确保在错误情况下 applications 也是数组
applications.value = []
} finally {
loading.value = false
@@ -490,14 +493,19 @@ const loadApplications = async () => {
const loadInsuranceTypes = async () => {
try {
const response = await insuranceTypeAPI.getList()
console.log('保险类型API响应:', response)
// 使用数据验证工具处理响应
const validatedResponse = validateListResponse(response, '险种列表')
insuranceTypes.value = validatedResponse.data
if (response.data && response.data.status === 'success') {
insuranceTypes.value = response.data.data || []
console.log('保险类型数据设置成功:', insuranceTypes.value.length, '个')
} else {
console.log('保险类型响应格式错误:', response)
message.error('加载险种数据失败')
insuranceTypes.value = []
}
} catch (error) {
console.error('加载险种数据失败:', error)
message.error('加载险种数据失败')
// 确保在错误情况下 insuranceTypes 也是数组
insuranceTypes.value = []
}
}

View File

@@ -366,6 +366,7 @@ import {
RedoOutlined,
FileTextOutlined
} from '@ant-design/icons-vue'
import { claimAPI } from '@/utils/api'
const loading = ref(false)
const modalVisible = ref(false)
@@ -530,60 +531,20 @@ const loadClaims = async () => {
...searchForm
}
// 这里应该是实际的API调用
// const response = await claimAPI.getList(params)
// claimList.value = response.data.list
// pagination.total = response.data.total
console.log('理赔管理API请求参数:', params)
const response = await claimAPI.getList(params)
console.log('理赔管理API响应:', response)
// 模拟数据
claimList.value = [
{
id: 1,
claim_number: 'CLM20240001',
policy_number: 'POL20240001',
applicant_name: '张三',
phone: '13800138000',
claim_amount: 50000,
approved_amount: 45000,
apply_date: '2024-01-15',
accident_date: '2024-01-10',
status: 'approved',
accident_description: '交通事故,车辆前部受损',
process_description: '已核实事故情况,符合理赔条件',
reject_reason: '',
reviewer_name: '李审核员',
review_date: '2024-01-16',
documents: [
{ name: '事故照片.jpg', size: '2.5MB', url: '#' },
{ name: '维修报价单.pdf', size: '1.2MB', url: '#' }
],
created_at: '2024-01-15 10:00:00',
updated_at: '2024-01-16 14:30:00'
},
{
id: 2,
claim_number: 'CLM20240002',
policy_number: 'POL20240002',
applicant_name: '李四',
phone: '13800138001',
claim_amount: 100000,
approved_amount: null,
apply_date: '2024-01-20',
accident_date: '2024-01-18',
status: 'pending',
accident_description: '家庭财产损失,水管爆裂',
process_description: '',
reject_reason: '',
reviewer_name: null,
review_date: null,
documents: [
{ name: '损失评估报告.pdf', size: '3.1MB', url: '#' }
],
created_at: '2024-01-20 14:30:00',
updated_at: '2024-01-20 14:30:00'
}
]
pagination.total = 2
if (response.data && response.data.status === 'success') {
// 后端返回的数据直接是数组格式,不是{list: [], total: 8}格式
claimList.value = response.data.data || []
pagination.total = response.data.pagination?.total || 0
console.log('理赔管理数据设置成功:', claimList.value.length, '条')
} else {
console.log('理赔管理响应格式错误:', response)
message.error(response.data?.message || '加载理赔列表失败')
claimList.value = []
}
} catch (error) {
message.error('加载理赔列表失败')
} finally {

View File

@@ -34,7 +34,7 @@
<a-range-picker
v-model:value="searchForm.dateRange"
style="width: 100%"
placeholder="['开始日期', '结束日期']"
:placeholder="['开始日期', '结束日期']"
/>
</a-col>
<a-col :span="6">

View File

@@ -209,25 +209,33 @@ const loadDashboardData = async () => {
try {
// 获取统计信息
const statsResponse = await dashboardAPI.getStats()
if (statsResponse.status === 'success') {
console.log('统计数据响应:', statsResponse)
if (statsResponse.data && statsResponse.data.status === 'success') {
stats.value = {
totalUsers: statsResponse.data.totalUsers || 0,
totalApplications: statsResponse.data.totalApplications || 0,
totalPolicies: statsResponse.data.totalPolicies || 0,
totalClaims: statsResponse.data.totalClaims || 0
totalUsers: statsResponse.data.data.totalUsers || 0,
totalApplications: statsResponse.data.data.totalApplications || 0,
totalPolicies: statsResponse.data.data.totalPolicies || 0,
totalClaims: statsResponse.data.data.totalClaims || 0
}
console.log('统计数据设置成功:', stats.value)
} else {
console.log('统计数据响应格式错误:', statsResponse)
}
// 获取最近活动
const activitiesResponse = await dashboardAPI.getRecentActivities()
if (activitiesResponse.status === 'success') {
recentActivities.value = activitiesResponse.data.map(activity => ({
console.log('最近活动响应:', activitiesResponse)
if (activitiesResponse.data && activitiesResponse.data.status === 'success') {
recentActivities.value = activitiesResponse.data.data.map(activity => ({
id: activity.id,
type: getLogType(activity.action),
title: getLogTitle('info', activity.action),
description: activity.action,
created_at: activity.createdAt
type: getLogType(activity.type),
title: activity.title,
description: activity.description,
created_at: activity.timestamp || new Date().toISOString()
})) || []
console.log('最近活动设置成功:', recentActivities.value)
} else {
console.log('最近活动响应格式错误:', activitiesResponse)
}
} catch (error) {
console.error('加载仪表板数据失败:', error)
@@ -281,68 +289,80 @@ const loadDashboardData = async () => {
// 加载图表数据
const loadChartData = async () => {
console.log('开始加载图表数据...')
chartLoading.value = true
try {
// 获取申请趋势数据
console.log('正在获取申请趋势数据...')
const applicationTrendResponse = await dashboardAPI.getChartData({
type: 'applications',
period: '7d'
})
if (applicationTrendResponse.status === 'success') {
setupApplicationTrendChart(applicationTrendResponse.data)
console.log('申请趋势响应:', applicationTrendResponse)
if (applicationTrendResponse.data && applicationTrendResponse.data.status === 'success') {
console.log('设置申请趋势图表,数据:', applicationTrendResponse.data.data)
setupApplicationTrendChart(applicationTrendResponse.data.data)
} else {
console.log('申请趋势响应格式错误:', applicationTrendResponse)
}
// 获取保单分布数据
// 获取保单状态分布数据
console.log('正在获取保单状态分布数据...')
const policyDistributionResponse = await dashboardAPI.getChartData({
type: 'policies',
period: '30d'
type: 'policy_status'
})
if (policyDistributionResponse.status === 'success') {
setupPolicyDistributionChart(policyDistributionResponse.data)
console.log('保单状态分布响应:', policyDistributionResponse)
if (policyDistributionResponse.data && policyDistributionResponse.data.status === 'success') {
console.log('设置保单状态分布图表,数据:', policyDistributionResponse.data.data)
setupPolicyDistributionChart(policyDistributionResponse.data.data)
} else {
console.log('保单状态分布响应格式错误:', policyDistributionResponse)
}
} catch (error) {
console.error('加载图表数据失败:', error)
// 使用模拟数据
setupApplicationTrendChart([
{ date: '2024-01-01', count: 5 },
{ date: '2024-01-02', count: 8 },
{ date: '2024-01-03', count: 12 },
{ date: '2024-01-04', count: 7 },
{ date: '2024-01-05', count: 15 },
{ date: '2024-01-06', count: 10 },
{ date: '2024-01-07', count: 18 }
])
setupPolicyDistributionChart([
{ date: '2024-01-01', count: 3 },
{ date: '2024-01-02', count: 5 },
{ date: '2024-01-03', count: 8 },
{ date: '2024-01-04', count: 4 },
{ date: '2024-01-05', count: 12 }
])
console.error('错误详情:', error.response?.data || error.message)
message.error('加载图表数据失败')
} finally {
chartLoading.value = false
console.log('图表数据加载完成')
}
}
// 设置申请趋势图表
const setupApplicationTrendChart = (data) => {
console.log('设置申请趋势图表,接收数据:', data)
if (!data || !Array.isArray(data) || data.length === 0) {
console.warn('申请趋势数据为空或格式错误')
return
}
const dates = data.map(item => item.date)
const counts = data.map(item => item.count)
const counts = data.map(item => item.value || item.count)
console.log('处理后的数据 - 日期:', dates, '数量:', counts)
applicationTrendOption.value = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#ccc',
borderWidth: 1,
textStyle: {
color: '#333'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '10%',
containLabel: true
},
xAxis: {
@@ -350,15 +370,29 @@ const setupApplicationTrendChart = (data) => {
data: dates,
axisLine: {
lineStyle: {
color: '#8c8c8c'
color: '#d9d9d9'
}
},
axisTick: {
alignWithLabel: true
},
axisLabel: {
color: '#666'
}
},
yAxis: {
type: 'value',
axisLine: {
lineStyle: {
color: '#8c8c8c'
color: '#d9d9d9'
}
},
axisLabel: {
color: '#666'
},
splitLine: {
lineStyle: {
color: '#f0f0f0'
}
}
},
@@ -367,12 +401,25 @@ const setupApplicationTrendChart = (data) => {
name: '申请数量',
type: 'bar',
data: counts,
barWidth: '60%',
itemStyle: {
color: '#1890ff'
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: '#1890ff'
}, {
offset: 1, color: '#40a9ff'
}]
},
borderRadius: [4, 4, 0, 0]
},
emphasis: {
itemStyle: {
color: '#40a9ff'
color: '#0050b3'
}
}
}
@@ -380,27 +427,47 @@ const setupApplicationTrendChart = (data) => {
}
}
// 设置保单分布图表
// 设置保单状态分布图表
const setupPolicyDistributionChart = (data) => {
console.log('设置保单状态分布图表,接收数据:', data)
if (!data || !Array.isArray(data) || data.length === 0) {
console.warn('保单状态分布数据为空或格式错误')
return
}
const chartData = data.map(item => ({
name: item.date,
name: getPolicyStatusLabel(item.status),
value: item.count
}))
console.log('处理后的图表数据:', chartData)
policyDistributionOption.value = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
formatter: '{a} <br/>{b}: {c} ({d}%)',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#ccc',
borderWidth: 1,
textStyle: {
color: '#333'
}
},
legend: {
orient: 'vertical',
left: 'left'
left: 'left',
top: 'center',
textStyle: {
color: '#666',
fontSize: 12
},
itemGap: 15
},
series: [
{
name: '保单数量',
type: 'pie',
radius: ['40%', '70%'],
radius: ['45%', '75%'],
center: ['65%', '50%'],
avoidLabelOverlap: false,
label: {
show: false,
@@ -409,8 +476,14 @@ const setupPolicyDistributionChart = (data) => {
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
fontSize: '16',
fontWeight: 'bold',
color: '#333'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
labelLine: {
@@ -418,17 +491,56 @@ const setupPolicyDistributionChart = (data) => {
},
data: chartData,
itemStyle: {
color: function(params) {
const colors = ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1']
return colors[params.dataIndex % colors.length]
}
borderRadius: 8,
borderColor: '#fff',
borderWidth: 2
}
}
]
}
}
// 获取保单状态标签
const getPolicyStatusLabel = (status) => {
const statusMap = {
'active': '有效',
'expired': '已过期',
'cancelled': '已取消',
'suspended': '暂停'
}
return statusMap[status] || status
}
onMounted(() => {
console.log('Dashboard组件已挂载')
console.log('图表选项初始状态:', {
applicationTrendOption: applicationTrendOption.value,
policyDistributionOption: policyDistributionOption.value
})
// 设置测试图表数据
console.log('设置测试图表数据...')
applicationTrendOption.value = {
title: { text: '测试图表' },
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
yAxis: { type: 'value' },
series: [{ data: [120, 200, 150], type: 'bar' }]
}
policyDistributionOption.value = {
title: { text: '测试饼图' },
series: [{
type: 'pie',
data: [
{ value: 1048, name: '搜索引擎' },
{ value: 735, name: '直接访问' },
{ value: 580, name: '邮件营销' }
]
}]
}
console.log('测试图表设置完成')
loadDashboardData()
})
</script>

View File

@@ -78,7 +78,7 @@
</template>
<script setup>
import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue';
import { ref, onMounted, onUnmounted, getCurrentInstance, nextTick } from 'vue';
import * as echarts from 'echarts';
import { message } from 'ant-design-vue';
import { dataWarehouseAPI } from '@/utils/api';
@@ -192,13 +192,20 @@ const fetchTypeDistribution = async () => {
const response = await dataWarehouseAPI.getInsuranceTypeDistribution();
console.log('保险类型分布响应:', response);
if (response.data && response.data.success) {
renderTypeDistributionChart(response.data.data);
console.log('保险类型分布数据:', response.data.data);
await nextTick(); // 确保DOM更新
ensureChartReady(typeDistributionChart, () => renderTypeDistributionChart(response.data.data));
} else {
console.log('保险类型分布响应格式错误:', response);
message.error('获取保险类型分布数据失败');
await nextTick();
ensureChartReady(typeDistributionChart, () => renderTypeDistributionChart([]));
}
} catch (error) {
message.error('获取保险类型分布数据失败');
console.error('获取保险类型分布数据错误:', error);
await nextTick();
ensureChartReady(typeDistributionChart, () => renderTypeDistributionChart([]));
}
};
@@ -208,8 +215,11 @@ const fetchStatusDistribution = async () => {
const response = await dataWarehouseAPI.getApplicationStatusDistribution();
console.log('申请状态分布响应:', response);
if (response.data && response.data.success) {
console.log('申请状态分布数据:', response.data.data);
await nextTick(); // 确保DOM更新
renderStatusDistributionChart(response.data.data);
} else {
console.log('申请状态分布响应格式错误:', response);
message.error('获取申请状态分布数据失败');
}
} catch (error) {
@@ -224,8 +234,10 @@ const fetchTrendData = async () => {
const response = await dataWarehouseAPI.getTrendData();
console.log('趋势数据响应:', response);
if (response.data && response.data.success) {
console.log('趋势数据:', response.data.data);
renderTrendChart(response.data.data);
} else {
console.log('趋势数据响应格式错误:', response);
message.error('获取趋势数据失败');
}
} catch (error) {
@@ -240,9 +252,10 @@ const fetchClaimStats = async () => {
const response = await dataWarehouseAPI.getClaimStats();
console.log('赔付统计响应:', response);
if (response && response.data && response.data.success) {
console.log('赔付统计数据:', response.data.data);
renderClaimStatsChart(response.data.data || { statusDistribution: [], monthlyTrend: [] });
} else {
console.warn('赔付统计数据响应格式不正确:', response);
console.log('赔付统计数据响应格式错误:', response);
message.error('获取赔付统计数据失败');
// 使用空数据渲染图表,避免显示错误
renderClaimStatsChart({ statusDistribution: [], monthlyTrend: [] });
@@ -272,6 +285,18 @@ const refreshData = async () => {
}
};
// 确保图表容器准备就绪后初始化
const ensureChartReady = (chartRef, initCallback) => {
const checkReady = () => {
if (chartRef.value && chartRef.value.clientWidth > 0 && chartRef.value.clientHeight > 0) {
initCallback();
} else {
setTimeout(checkReady, 50);
}
};
checkReady();
};
// 处理日期范围变化
const handleDateChange = (dates) => {
dateRange.value = dates;
@@ -281,11 +306,40 @@ const handleDateChange = (dates) => {
// 渲染保险类型分布图表
const renderTypeDistributionChart = (data) => {
if (!typeChartInstance) {
typeChartInstance = echarts.init(typeDistributionChart.value);
console.log('=== 开始渲染保险类型分布图表 ===');
console.log('接收到的数据:', data);
console.log('数据类型:', typeof data);
console.log('是否为数组:', Array.isArray(data));
console.log('数据长度:', data ? data.length : 'N/A');
// 检查DOM元素是否存在
if (!typeDistributionChart.value) {
console.log('DOM元素不存在延迟渲染');
setTimeout(() => renderTypeDistributionChart(data), 200);
return;
}
console.log('保险类型分布数据:', data);
console.log('DOM元素存在开始渲染。尺寸信息:', {
clientWidth: typeDistributionChart.value.clientWidth,
clientHeight: typeDistributionChart.value.clientHeight,
offsetWidth: typeDistributionChart.value.offsetWidth,
offsetHeight: typeDistributionChart.value.offsetHeight
});
console.log('DOM元素已准备好开始初始化图表');
if (!typeChartInstance) {
typeChartInstance = echarts.init(typeDistributionChart.value);
console.log('图表实例已创建');
}
// 验证数据格式
if (!data || !Array.isArray(data) || data.length === 0) {
console.warn('保险类型分布数据为空或格式错误:', data);
return;
}
console.log('保险类型分布数据验证通过:', data);
const option = {
tooltip: {
@@ -318,16 +372,46 @@ const renderTypeDistributionChart = (data) => {
]
};
console.log('设置图表选项:', option);
typeChartInstance.setOption(option);
console.log('保险类型分布图表渲染完成');
};
// 渲染申请状态分布图表
const renderStatusDistributionChart = (data) => {
if (!statusChartInstance) {
statusChartInstance = echarts.init(statusDistributionChart.value);
console.log('=== 开始渲染申请状态分布图表 ===');
console.log('接收到的数据:', data);
console.log('数据类型:', typeof data);
console.log('是否为数组:', Array.isArray(data));
console.log('数据长度:', data ? data.length : 'N/A');
// 检查DOM元素是否存在且有尺寸
if (!statusDistributionChart.value || statusDistributionChart.value.clientWidth === 0 || statusDistributionChart.value.clientHeight === 0) {
console.log('DOM元素未准备好延迟渲染。当前状态:', {
element: statusDistributionChart.value,
clientWidth: statusDistributionChart.value?.clientWidth,
clientHeight: statusDistributionChart.value?.clientHeight,
offsetWidth: statusDistributionChart.value?.offsetWidth,
offsetHeight: statusDistributionChart.value?.offsetHeight
});
setTimeout(() => renderStatusDistributionChart(data), 200);
return;
}
console.log('申请状态分布数据:', data);
console.log('DOM元素已准备好开始初始化图表');
if (!statusChartInstance) {
statusChartInstance = echarts.init(statusDistributionChart.value);
console.log('图表实例已创建');
}
// 验证数据格式
if (!data || !Array.isArray(data) || data.length === 0) {
console.warn('申请状态分布数据为空或格式错误:', data);
return;
}
console.log('申请状态分布数据验证通过:', data);
const option = {
tooltip: {
@@ -360,11 +444,19 @@ const renderStatusDistributionChart = (data) => {
]
};
console.log('设置图表选项:', option);
statusChartInstance.setOption(option);
console.log('申请状态分布图表渲染完成');
};
// 渲染趋势图表
const renderTrendChart = (data) => {
// 检查DOM元素是否存在且有尺寸
if (!trendChart.value || trendChart.value.clientWidth === 0 || trendChart.value.clientHeight === 0) {
setTimeout(() => renderTrendChart(data), 100);
return;
}
if (!trendChartInstance) {
trendChartInstance = echarts.init(trendChart.value);
}
@@ -465,6 +557,12 @@ const renderTrendChart = (data) => {
// 渲染赔付统计图表
const renderClaimStatsChart = (data) => {
// 检查DOM元素是否存在且有尺寸
if (!claimStatsChart.value || claimStatsChart.value.clientWidth === 0 || claimStatsChart.value.clientHeight === 0) {
setTimeout(() => renderClaimStatsChart(data), 100);
return;
}
if (!claimChartInstance) {
claimChartInstance = echarts.init(claimStatsChart.value);
}
@@ -539,7 +637,30 @@ const handleResize = () => {
// 组件挂载时初始化
onMounted(() => {
refreshData();
console.log('=== DataWarehouse组件已挂载 ===');
console.log('图表容器引用:', {
typeDistributionChart: typeDistributionChart.value,
statusDistributionChart: statusDistributionChart.value,
trendChart: trendChart.value,
claimStatsChart: claimStatsChart.value
});
// 延迟一点时间确保DOM完全渲染
setTimeout(() => {
console.log('延迟检查图表容器尺寸:', {
typeDistributionChart: typeDistributionChart.value ? {
clientWidth: typeDistributionChart.value.clientWidth,
clientHeight: typeDistributionChart.value.clientHeight
} : 'null',
statusDistributionChart: statusDistributionChart.value ? {
clientWidth: statusDistributionChart.value.clientWidth,
clientHeight: statusDistributionChart.value.clientHeight
} : 'null'
});
refreshData();
}, 100);
window.addEventListener('resize', handleResize);
});
@@ -626,6 +747,8 @@ onUnmounted(() => {
.chart {
width: 100%;
height: calc(100% - 50px);
min-height: 300px;
min-width: 200px;
}
@media (max-width: 1200px) {

View File

@@ -239,12 +239,15 @@ const fetchInstallationTasks = async () => {
};
const response = await installationTaskApi.getInstallationTasks(params);
console.log('安装任务API响应:', response);
if (response.code === 200) {
tableData.value = response.data.rows || response.data.list || [];
pagination.total = response.data.total || 0;
if (response.data && response.data.status === 'success') {
tableData.value = response.data.data.list || [];
pagination.total = response.data.data.total || 0;
console.log('安装任务数据设置成功:', tableData.value.length, '条');
} else {
message.error(response.message || '获取安装任务列表失败');
console.log('安装任务响应格式错误:', response);
message.error(response.data?.message || '获取安装任务列表失败');
}
} catch (error) {
console.error('获取安装任务列表失败:', error);

View File

@@ -365,12 +365,16 @@ const loadInsuranceTypes = async () => {
}
const response = await insuranceTypeAPI.getList(params)
console.log('险种管理API响应:', response)
if (response.status === 'success') {
typeList.value = response.data.list
pagination.total = response.data.total
if (response.data && response.data.status === 'success') {
// API返回的数据直接是数组格式
typeList.value = response.data.data || []
pagination.total = response.data.pagination?.total || 0
console.log('险种管理数据设置成功:', typeList.value.length, '个')
} else {
message.error(response.message || '加载险种列表失败')
console.log('险种管理响应格式错误:', response)
message.error(response.data?.message || '加载险种列表失败')
}
} catch (error) {
message.error('加载险种列表失败')

View File

@@ -55,7 +55,7 @@
<a-col :span="8">
<a-range-picker
v-model:value="searchForm.dateRange"
placeholder="['开始日期', '结束日期']"
:placeholder="['开始日期', '结束日期']"
style="width: 100%"
/>
</a-col>
@@ -474,10 +474,21 @@ const fetchData = async () => {
}
delete params.dateRange
console.log('生资保单API请求参数:', params)
const response = await livestockPolicyApi.getList(params)
console.log('生资保单API响应:', response)
console.log('响应数据类型:', typeof response)
console.log('响应code:', response.code)
console.log('响应data:', response.data)
if (response.code === 200) {
tableData.value = response.data.list
pagination.total = response.data.total
// 后端返回的数据直接是数组格式,不是{list: [], total: 18}格式
tableData.value = response.data || []
pagination.total = response.pagination?.total || 0
console.log('生资保单数据设置成功:', tableData.value.length, '条')
} else {
console.log('生资保单响应格式错误:', response)
message.error('获取数据失败')
}
} catch (error) {
message.error('获取数据失败')

View File

@@ -107,9 +107,8 @@ const onFinish = async (values) => {
userStore.setAuthData({
accessToken: data.accessToken,
refreshToken: data.refreshToken,
accessTokenExpiresAt: data.accessTokenExpiresAt,
refreshTokenExpiresAt: data.refreshTokenExpiresAt,
user: data.user || data.userInfo // 修复用户信息在data.user中
user: data.user || data.userInfo,
expiresIn: data.accessTokenExpiresIn || 900 // 默认15分钟
})
} else if (data.token) {
// 兼容旧的单Token格式

View File

@@ -1,122 +1,247 @@
<template>
<div style="padding: 20px;">
<h2>登录和API测试</h2>
<div style="margin-bottom: 20px;">
<h3>当前状态</h3>
<p>Token: {{ userStore.token ? '已设置' : '未设置' }}</p>
<p>用户信息: {{ JSON.stringify(userStore.userInfo) }}</p>
</div>
<div class="login-test">
<a-card title="登录测试" style="margin: 20px;">
<a-space direction="vertical" style="width: 100%;">
<div>
<h3>快速登录测试</h3>
<a-form :model="loginForm" @finish="handleLogin">
<a-form-item>
<a-input v-model:value="loginForm.username" placeholder="用户名" />
</a-form-item>
<a-form-item>
<a-input-password v-model:value="loginForm.password" placeholder="密码" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" :loading="loginLoading" block>
登录
</a-button>
</a-form-item>
</a-form>
</div>
<div style="margin-bottom: 20px;">
<h3>快速登录</h3>
<a-button type="primary" @click="quickLogin" :loading="loginLoading">
使用 admin/123456 登录
</a-button>
</div>
<div>
<h3>Token状态检查</h3>
<a-descriptions bordered :column="1">
<a-descriptions-item label="Store中的Access Token">
<span v-if="userStore.accessToken">
{{ userStore.accessToken.substring(0, 50) }}...
<br>
<small>长度: {{ userStore.accessToken.length }}</small>
</span>
<span v-else style="color: red;">未找到</span>
</a-descriptions-item>
<a-descriptions-item label="LocalStorage中的Access Token">
<span v-if="localStorageToken">
{{ localStorageToken.substring(0, 50) }}...
<br>
<small>长度: {{ localStorageToken.length }}</small>
</span>
<span v-else style="color: red;">未找到</span>
</a-descriptions-item>
<a-descriptions-item label="是否已登录">
<span :style="{ color: userStore.isLoggedIn ? 'green' : 'red' }">
{{ userStore.isLoggedIn ? '是' : '否' }}
</span>
</a-descriptions-item>
<a-descriptions-item label="Token是否过期">
<span :style="{ color: userStore.isTokenExpired ? 'red' : 'green' }">
{{ userStore.isTokenExpired ? '是' : '否' }}
</span>
</a-descriptions-item>
</a-descriptions>
</div>
<div style="margin-bottom: 20px;">
<h3>测试API调用</h3>
<a-button @click="testStatsAPI" :loading="statsLoading" style="margin-right: 10px;">
测试系统统计API
</a-button>
<a-button @click="testAuthAPI" :loading="authLoading">
测试认证API
</a-button>
</div>
<div>
<h3>API测试</h3>
<a-space>
<a-button @click="testPermissionsAPI" :loading="permissionsLoading">
测试权限API
</a-button>
<a-button @click="testRolesAPI" :loading="rolesLoading">
测试角色API
</a-button>
<a-button @click="refreshStatus">
刷新状态
</a-button>
</a-space>
</div>
<div v-if="apiResults.length">
<h3>API调用结果</h3>
<div v-for="(result, index) in apiResults" :key="index" style="margin-bottom: 10px; padding: 10px; border: 1px solid #ddd;">
<strong>{{ result.name }}:</strong>
<pre>{{ result.data }}</pre>
</div>
</div>
<div v-if="testResults.length > 0">
<h3>测试结果</h3>
<a-list :data-source="testResults" bordered>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #title>
<span :style="{ color: item.success ? 'green' : 'red' }">
{{ item.name }} - {{ item.success ? '成功' : '失败' }}
</span>
</template>
<template #description>
<div>
<p><strong>状态:</strong> {{ item.status }}</p>
<p><strong>响应:</strong></p>
<pre style="background: #f5f5f5; padding: 8px; border-radius: 4px; font-size: 12px;">{{ item.response }}</pre>
</div>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</div>
</a-space>
</a-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { useUserStore } from '@/stores/user'
import { authAPI, dashboardAPI } from '@/utils/api'
import { authAPI, rolePermissionAPI } from '@/utils/api'
const userStore = useUserStore()
const loginLoading = ref(false)
const statsLoading = ref(false)
const authLoading = ref(false)
const apiResults = ref([])
const permissionsLoading = ref(false)
const rolesLoading = ref(false)
const testResults = ref([])
const quickLogin = async () => {
const loginForm = reactive({
username: 'admin',
password: '123456'
})
const localStorageToken = ref('')
const refreshStatus = () => {
localStorageToken.value = localStorage.getItem('accessToken') || ''
}
const handleLogin = async () => {
loginLoading.value = true
try {
const response = await authAPI.login({
username: 'admin',
password: '123456'
})
const response = await authAPI.login(loginForm)
if (response.status === 'success') {
userStore.setToken(response.data.token)
userStore.setUserInfo(response.data.user)
message.success('登录成功!')
if (response.data && (response.data.status === 'success' || response.data.code === 200)) {
const data = response.data.data
apiResults.value.unshift({
name: '登录API',
data: JSON.stringify(response, null, 2)
})
if (data.accessToken && data.refreshToken) {
userStore.setAuthData({
accessToken: data.accessToken,
refreshToken: data.refreshToken,
user: data.user || data.userInfo,
expiresIn: data.accessTokenExpiresIn || 900
})
testResults.value.unshift({
name: '登录测试',
success: true,
status: '成功',
response: JSON.stringify({
message: '登录成功',
tokenLength: data.accessToken.length,
user: data.user?.username
}, null, 2)
})
message.success('登录成功')
refreshStatus()
} else {
throw new Error('响应数据格式不正确缺少token信息')
}
} else {
message.error(response.message || '登录失败')
throw new Error(response.data?.message || '登录失败')
}
} catch (error) {
message.error(error.response?.data?.message || '登录失败')
apiResults.value.unshift({
name: '登录API (错误)',
data: JSON.stringify(error.response?.data || error.message, null, 2)
testResults.value.unshift({
name: '登录测试',
success: false,
status: '失败',
response: JSON.stringify(error.response?.data || error.message, null, 2)
})
message.error('登录失败: ' + (error.response?.data?.message || error.message))
} finally {
loginLoading.value = false
}
}
const testStatsAPI = async () => {
statsLoading.value = true
const testPermissionsAPI = async () => {
permissionsLoading.value = true
try {
const response = await dashboardAPI.getStats()
message.success('系统统计API调用成功')
const response = await rolePermissionAPI.getAllPermissions()
apiResults.value.unshift({
name: '系统统计API',
data: JSON.stringify(response, null, 2)
testResults.value.unshift({
name: '权限API测试',
success: response.success,
status: response.success ? '成功' : '失败',
response: JSON.stringify(response, null, 2)
})
if (response.success) {
message.success('权限API测试成功')
} else {
message.error('权限API测试失败: ' + response.error)
}
} catch (error) {
message.error('系统统计API调用失败')
apiResults.value.unshift({
name: '系统统计API (错误)',
data: JSON.stringify(error.response?.data || error.message, null, 2)
testResults.value.unshift({
name: '权限API测试',
success: false,
status: '失败',
response: JSON.stringify(error.message, null, 2)
})
message.error('权限API测试失败: ' + error.message)
} finally {
statsLoading.value = false
permissionsLoading.value = false
}
}
const testAuthAPI = async () => {
authLoading.value = true
const testRolesAPI = async () => {
rolesLoading.value = true
try {
const response = await authAPI.getProfile()
message.success('认证API调用成功')
const response = await rolePermissionAPI.getAllRolesWithPermissions()
apiResults.value.unshift({
name: '用户资料API',
data: JSON.stringify(response, null, 2)
testResults.value.unshift({
name: '角色API测试',
success: response.success,
status: response.success ? '成功' : '失败',
response: JSON.stringify(response, null, 2)
})
if (response.success) {
message.success('角色API测试成功')
} else {
message.error('角色API测试失败: ' + response.error)
}
} catch (error) {
message.error('认证API调用失败')
apiResults.value.unshift({
name: '用户资料API (错误)',
data: JSON.stringify(error.response?.data || error.message, null, 2)
testResults.value.unshift({
name: '角色API测试',
success: false,
status: '失败',
response: JSON.stringify(error.message, null, 2)
})
message.error('角色API测试失败: ' + error.message)
} finally {
authLoading.value = false
rolesLoading.value = false
}
}
</script>
onMounted(() => {
refreshStatus()
})
</script>
<style scoped>
.login-test {
padding: 20px;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
max-height: 200px;
overflow-y: auto;
}
</style>

View File

@@ -302,6 +302,7 @@ import {
SearchOutlined,
RedoOutlined
} from '@ant-design/icons-vue'
import { policyAPI } from '@/utils/api'
const loading = ref(false)
const modalVisible = ref(false)
@@ -443,51 +444,26 @@ const loadPolicies = async () => {
...searchForm
}
// 这里应该是实际的API调用
// const response = await policyAPI.getList(params)
// policyList.value = response.data.list
// pagination.total = response.data.total
console.log('保单管理API请求参数:', params)
const response = await policyAPI.getList(params)
console.log('保单管理API响应:', response)
console.log('响应数据类型:', typeof response)
console.log('响应data字段:', response.data)
console.log('响应data.status:', response.data?.status)
console.log('响应data.data:', response.data?.data)
console.log('响应data.data长度:', response.data?.data?.length)
// 模拟数据
policyList.value = [
{
id: 1,
policy_number: 'POL20240001',
insurance_type_id: 1,
insurance_type_name: '综合意外险',
policyholder_name: '张三',
insured_name: '张三',
premium_amount: 1200,
coverage_amount: 500000,
start_date: '2024-01-01',
end_date: '2025-01-01',
status: 'active',
phone: '13800138000',
email: 'zhangsan@example.com',
address: '北京市朝阳区',
created_at: '2024-01-01 10:00:00',
updated_at: '2024-01-01 10:00:00'
},
{
id: 2,
policy_number: 'POL20240002',
insurance_type_id: 2,
insurance_type_name: '终身寿险',
policyholder_name: '李四',
insured_name: '李四',
premium_amount: 5000,
coverage_amount: 1000000,
start_date: '2024-01-02',
end_date: '2074-01-02',
status: 'active',
phone: '13800138001',
email: 'lisi@example.com',
address: '上海市浦东新区',
created_at: '2024-01-02 14:30:00',
updated_at: '2024-01-02 14:30:00'
}
]
pagination.total = 2
if (response.data && response.data.status === 'success') {
// 后端返回的数据直接是数组格式,不是{list: [], total: 28}格式
policyList.value = response.data.data || []
pagination.total = response.data.pagination?.total || 0
console.log('保单管理数据设置成功:', policyList.value.length, '条')
console.log('policyList.value:', policyList.value)
} else {
console.log('保单管理响应格式错误:', response)
message.error(response.data?.message || '加载保单列表失败')
policyList.value = []
}
} catch (error) {
message.error('加载保单列表失败')
} finally {

View File

@@ -441,11 +441,14 @@ export default {
}
const response = await supervisionTaskApi.getList(params)
if (response.code === 200) {
tableData.value = response.data.list
pagination.total = response.data.total
console.log('监管任务API响应:', response)
if (response.data && response.data.status === 'success') {
tableData.value = response.data.data.list
pagination.total = response.data.data.total
console.log('监管任务数据设置成功:', tableData.value.length, '条')
} else {
message.error(response.message || '获取数据失败')
console.log('监管任务响应格式错误:', response)
message.error(response.data?.message || '获取数据失败')
}
} catch (error) {
console.error('加载数据失败:', error)

View File

@@ -427,9 +427,14 @@ const fetchTaskList = async () => {
}
const response = await supervisoryTaskApi.getList(params)
if (response.code === 200) {
taskList.value = response.data.list
pagination.total = response.data.total
console.log('监管任务API响应:', response)
if (response.data && response.data.status === 'success') {
taskList.value = response.data.data.list
pagination.total = response.data.data.total
console.log('监管任务数据设置成功:', taskList.value.length, '条')
} else {
console.log('监管任务响应格式错误:', response)
message.error(response.data?.message || '获取任务列表失败')
}
} catch (error) {
console.error('获取任务列表失败:', error)

View File

@@ -422,28 +422,28 @@ const handleTableChange = (pag) => {
const loadLogs = async () => {
loading.value = true
try {
// const params = {
// page: pagination.current,
// pageSize: pagination.pageSize,
// ...searchForm,
// start_time: searchForm.timeRange?.[0]?.format('YYYY-MM-DD HH:mm:ss'),
// end_time: searchForm.timeRange?.[1]?.format('YYYY-MM-DD HH:mm:ss')
// }
// const response = await systemAPI.getLogs(params)
// logList.value = response.data.list
// pagination.total = response.data.total
const params = {
page: pagination.current,
pageSize: pagination.pageSize,
...searchForm,
start_time: searchForm.timeRange?.[0]?.format('YYYY-MM-DD HH:mm:ss'),
end_time: searchForm.timeRange?.[1]?.format('YYYY-MM-DD HH:mm:ss')
}
// 模拟数据
const filteredLogs = logList.value.filter(log => {
if (searchForm.log_type && log.log_type !== searchForm.log_type) return false
if (searchForm.module && log.module !== searchForm.module) return false
if (searchForm.operator && !log.operator.includes(searchForm.operator)) return false
if (searchForm.ip_address && !log.ip_address.includes(searchForm.ip_address)) return false
return true
})
console.log('系统日志API请求参数:', params)
const response = await operationLogAPI.getList(params)
console.log('系统日志API响应:', response)
logList.value = filteredLogs
pagination.total = filteredLogs.length
if (response.data && response.data.status === 'success') {
// 后端返回的数据在data.logs字段中
logList.value = response.data.data.logs || []
pagination.total = response.data.data.total || 0
console.log('系统日志数据设置成功:', logList.value.length, '条')
} else {
console.log('系统日志响应格式错误:', response)
message.error(response.data?.message || '加载日志失败')
logList.value = []
}
} catch (error) {
message.error('加载日志失败')
} finally {

View File

@@ -0,0 +1,260 @@
<template>
<div class="token-debug">
<a-card title="Token调试信息" style="margin: 20px;">
<a-space direction="vertical" style="width: 100%;">
<div>
<h3>LocalStorage中的Token信息:</h3>
<a-descriptions bordered :column="1">
<a-descriptions-item label="Access Token">
<span v-if="tokenInfo.accessToken">
{{ tokenInfo.accessToken.substring(0, 50) }}...
<br>
<small>长度: {{ tokenInfo.accessToken.length }}</small>
</span>
<span v-else style="color: red;">未找到</span>
</a-descriptions-item>
<a-descriptions-item label="Refresh Token">
<span v-if="tokenInfo.refreshToken">
{{ tokenInfo.refreshToken.substring(0, 50) }}...
<br>
<small>长度: {{ tokenInfo.refreshToken.length }}</small>
</span>
<span v-else style="color: red;">未找到</span>
</a-descriptions-item>
<a-descriptions-item label="User Info">
<pre v-if="tokenInfo.userInfo">{{ JSON.stringify(tokenInfo.userInfo, null, 2) }}</pre>
<span v-else style="color: red;">未找到</span>
</a-descriptions-item>
<a-descriptions-item label="Token Expires At">
<span v-if="tokenInfo.tokenExpiresAt">
{{ new Date(tokenInfo.tokenExpiresAt).toLocaleString() }}
<br>
<small :style="{ color: tokenInfo.isExpired ? 'red' : 'green' }">
{{ tokenInfo.isExpired ? '已过期' : '未过期' }}
</small>
</span>
<span v-else style="color: red;">未找到</span>
</a-descriptions-item>
</a-descriptions>
</div>
<div>
<h3>Store中的Token信息:</h3>
<a-descriptions bordered :column="1">
<a-descriptions-item label="Access Token">
<span v-if="userStore.accessToken">
{{ userStore.accessToken.substring(0, 50) }}...
<br>
<small>长度: {{ userStore.accessToken.length }}</small>
</span>
<span v-else style="color: red;">未找到</span>
</a-descriptions-item>
<a-descriptions-item label="Refresh Token">
<span v-if="userStore.refreshToken">
{{ userStore.refreshToken.substring(0, 50) }}...
<br>
<small>长度: {{ userStore.refreshToken.length }}</small>
</span>
<span v-else style="color: red;">未找到</span>
</a-descriptions-item>
<a-descriptions-item label="User Info">
<pre v-if="userStore.userInfo">{{ JSON.stringify(userStore.userInfo, null, 2) }}</pre>
<span v-else style="color: red;">未找到</span>
</a-descriptions-item>
<a-descriptions-item label="Is Logged In">
<span :style="{ color: userStore.isLoggedIn ? 'green' : 'red' }">
{{ userStore.isLoggedIn ? '是' : '否' }}
</span>
</a-descriptions-item>
<a-descriptions-item label="Is Token Expired">
<span :style="{ color: userStore.isTokenExpired ? 'red' : 'green' }">
{{ userStore.isTokenExpired ? '是' : '否' }}
</span>
</a-descriptions-item>
</a-descriptions>
</div>
<div>
<h3>API测试:</h3>
<a-space>
<a-button type="primary" @click="testLogin" :loading="loginLoading">
测试登录
</a-button>
<a-button @click="testPermissionsAPI" :loading="permissionsLoading">
测试权限API
</a-button>
<a-button @click="testRolesAPI" :loading="rolesLoading">
测试角色API
</a-button>
<a-button @click="refreshTokenInfo">
刷新Token信息
</a-button>
</a-space>
</div>
<div v-if="apiTestResults.length > 0">
<h3>API测试结果:</h3>
<a-list :data-source="apiTestResults" bordered>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #title>
<span :style="{ color: item.success ? 'green' : 'red' }">
{{ item.name }} - {{ item.success ? '成功' : '失败' }}
</span>
</template>
<template #description>
<div>
<p><strong>状态码:</strong> {{ item.status }}</p>
<p><strong>响应:</strong></p>
<pre style="background: #f5f5f5; padding: 8px; border-radius: 4px; font-size: 12px;">{{ item.response }}</pre>
</div>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</div>
</a-space>
</a-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { useUserStore } from '@/stores/user'
import { authAPI, rolePermissionAPI } from '@/utils/api'
const userStore = useUserStore()
const loginLoading = ref(false)
const permissionsLoading = ref(false)
const rolesLoading = ref(false)
const apiTestResults = ref([])
const tokenInfo = reactive({
accessToken: null,
refreshToken: null,
userInfo: null,
tokenExpiresAt: null,
isExpired: false
})
const refreshTokenInfo = () => {
tokenInfo.accessToken = localStorage.getItem('accessToken')
tokenInfo.refreshToken = localStorage.getItem('refreshToken')
tokenInfo.userInfo = JSON.parse(localStorage.getItem('userInfo') || 'null')
tokenInfo.tokenExpiresAt = parseInt(localStorage.getItem('tokenExpiresAt') || '0')
tokenInfo.isExpired = tokenInfo.tokenExpiresAt ? Date.now() >= tokenInfo.tokenExpiresAt : true
}
const testLogin = async () => {
loginLoading.value = true
try {
const response = await authAPI.login({
username: 'admin',
password: '123456'
})
apiTestResults.value.unshift({
name: '登录测试',
success: true,
status: response.status || '200',
response: JSON.stringify(response.data, null, 2)
})
message.success('登录测试成功')
refreshTokenInfo()
} catch (error) {
apiTestResults.value.unshift({
name: '登录测试',
success: false,
status: error.response?.status || 'Error',
response: JSON.stringify(error.response?.data || error.message, null, 2)
})
message.error('登录测试失败')
} finally {
loginLoading.value = false
}
}
const testPermissionsAPI = async () => {
permissionsLoading.value = true
try {
const response = await rolePermissionAPI.getAllPermissions()
apiTestResults.value.unshift({
name: '权限API测试',
success: response.success,
status: response.success ? '200' : 'Error',
response: JSON.stringify(response, null, 2)
})
if (response.success) {
message.success('权限API测试成功')
} else {
message.error('权限API测试失败')
}
} catch (error) {
apiTestResults.value.unshift({
name: '权限API测试',
success: false,
status: 'Error',
response: JSON.stringify(error.message, null, 2)
})
message.error('权限API测试失败')
} finally {
permissionsLoading.value = false
}
}
const testRolesAPI = async () => {
rolesLoading.value = true
try {
const response = await rolePermissionAPI.getAllRolesWithPermissions()
apiTestResults.value.unshift({
name: '角色API测试',
success: response.success,
status: response.success ? '200' : 'Error',
response: JSON.stringify(response, null, 2)
})
if (response.success) {
message.success('角色API测试成功')
} else {
message.error('角色API测试失败')
}
} catch (error) {
apiTestResults.value.unshift({
name: '角色API测试',
success: false,
status: 'Error',
response: JSON.stringify(error.message, null, 2)
})
message.error('角色API测试失败')
} finally {
rolesLoading.value = false
}
}
onMounted(() => {
refreshTokenInfo()
})
</script>
<style scoped>
.token-debug {
padding: 20px;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
max-height: 200px;
overflow-y: auto;
}
</style>

View File

@@ -265,35 +265,20 @@ const loadUsers = async () => {
...searchForm
}
// 这里应该是实际的API调用
// const response = await userAPI.getList(params)
// userList.value = response.data.list
// pagination.total = response.data.total
console.log('用户管理API请求参数:', params)
const response = await userAPI.getList(params)
console.log('用户管理API响应:', response)
// 模拟数据
userList.value = [
{
id: 1,
username: 'admin',
real_name: '管理员',
email: 'admin@example.com',
phone: '13800138000',
role_name: '管理员',
status: 'active',
created_at: '2024-01-01 10:00:00'
},
{
id: 2,
username: 'advisor1',
real_name: '张顾问',
email: 'advisor1@example.com',
phone: '13800138001',
role_name: '保险顾问',
status: 'active',
created_at: '2024-01-02 14:30:00'
}
]
pagination.total = 2
if (response.data && response.data.status === 'success') {
// 后端返回的数据在data.users字段中
userList.value = response.data.data.users || []
pagination.total = response.data.data.pagination?.total || 0
console.log('用户管理数据设置成功:', userList.value.length, '条')
} else {
console.log('用户管理响应格式错误:', response)
message.error(response.data?.message || '加载用户列表失败')
userList.value = []
}
} catch (error) {
message.error('加载用户列表失败')
} finally {

View File

@@ -0,0 +1,39 @@
// 测试前端API调用
const testFrontendAPI = async () => {
try {
console.log('开始测试前端API调用...');
// 模拟前端API调用
const response = await fetch('http://localhost:3000/api/policies?page=1&pageSize=10', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
console.log('API响应状态:', response.status);
if (!response.ok) {
const errorData = await response.json();
console.error('API错误:', errorData);
return;
}
const data = await response.json();
console.log('API响应数据:', data);
if (data.status === 'success' && data.data) {
console.log('✅ API调用成功返回', data.data.length, '条保单数据');
console.log('第一条保单数据:', data.data[0]);
} else {
console.log('❌ API响应格式不正确');
}
} catch (error) {
console.error('❌ API调用失败:', error);
}
};
// 在浏览器控制台中运行
console.log('请在浏览器控制台中运行: testFrontendAPI()');

View File

@@ -0,0 +1,93 @@
const { sequelize } = require('./config/database');
async function checkDatabaseStructure() {
try {
console.log('🔍 检查数据库表结构...\n');
// 检查数据库连接
await sequelize.authenticate();
console.log('✅ 数据库连接成功\n');
// 检查所有表
const [tables] = await sequelize.query('SHOW TABLES');
console.log('📋 数据库中的表:');
tables.forEach(table => {
const tableName = Object.values(table)[0];
console.log(` - ${tableName}`);
});
console.log('\n🏗 检查关键表结构:\n');
// 检查users表
try {
const [usersStructure] = await sequelize.query('DESCRIBE users');
console.log('👥 users表结构:');
usersStructure.forEach(column => {
console.log(` - ${column.Field}: ${column.Type} ${column.Null === 'NO' ? '(必填)' : '(可选)'} ${column.Key ? `[${column.Key}]` : ''}`);
});
} catch (error) {
console.log('❌ users表不存在');
}
console.log('');
// 检查permissions表
try {
const [permissionsStructure] = await sequelize.query('DESCRIBE permissions');
console.log('🔐 permissions表结构:');
permissionsStructure.forEach(column => {
console.log(` - ${column.Field}: ${column.Type} ${column.Null === 'NO' ? '(必填)' : '(可选)'} ${column.Key ? `[${column.Key}]` : ''}`);
});
} catch (error) {
console.log('❌ permissions表不存在');
}
console.log('');
// 检查roles表
try {
const [rolesStructure] = await sequelize.query('DESCRIBE roles');
console.log('👑 roles表结构:');
rolesStructure.forEach(column => {
console.log(` - ${column.Field}: ${column.Type} ${column.Null === 'NO' ? '(必填)' : '(可选)'} ${column.Key ? `[${column.Key}]` : ''}`);
});
} catch (error) {
console.log('❌ roles表不存在');
}
console.log('');
// 检查role_permissions表
try {
const [rolePermissionsStructure] = await sequelize.query('DESCRIBE role_permissions');
console.log('🔗 role_permissions表结构:');
rolePermissionsStructure.forEach(column => {
console.log(` - ${column.Field}: ${column.Type} ${column.Null === 'NO' ? '(必填)' : '(可选)'} ${column.Key ? `[${column.Key}]` : ''}`);
});
} catch (error) {
console.log('❌ role_permissions表不存在');
}
console.log('\n📊 检查数据量:');
// 检查各表数据量
const tables_to_check = ['users', 'permissions', 'roles', 'role_permissions'];
for (const tableName of tables_to_check) {
try {
const [result] = await sequelize.query(`SELECT COUNT(*) as count FROM ${tableName}`);
console.log(` - ${tableName}: ${result[0].count} 条记录`);
} catch (error) {
console.log(` - ${tableName}: 表不存在或查询失败`);
}
}
} catch (error) {
console.error('❌ 检查失败:', error.message);
} finally {
await sequelize.close();
console.log('\n🔚 检查完成');
}
}
checkDatabaseStructure();

View File

@@ -264,6 +264,42 @@ const getChartData = async (req, res) => {
date: item.dataValues.date,
value: parseInt(item.dataValues.count)
}));
} else if (type === 'claims') {
// 获取理赔数据趋势
const claims = await Claim.findAll({
where: {
created_at: {
[Op.gte]: startDate,
[Op.lte]: endDate
}
},
attributes: [
[dbSequelize.fn('DATE', dbSequelize.col('created_at')), 'date'],
[dbSequelize.fn('COUNT', dbSequelize.col('id')), 'count']
],
group: [dbSequelize.fn('DATE', dbSequelize.col('created_at'))],
order: [[dbSequelize.fn('DATE', dbSequelize.col('created_at')), 'ASC']]
});
chartData = claims.map(item => ({
date: item.dataValues.date,
value: parseInt(item.dataValues.count)
}));
} else if (type === 'policy_status') {
// 获取保单状态分布数据
const policyStatusData = await Policy.findAll({
attributes: [
'policy_status',
[dbSequelize.fn('COUNT', dbSequelize.col('id')), 'count']
],
group: ['policy_status'],
order: [[dbSequelize.fn('COUNT', dbSequelize.col('id')), 'DESC']]
});
chartData = policyStatusData.map(item => ({
status: item.dataValues.policy_status,
count: parseInt(item.dataValues.count)
}));
}
console.log(`获取到 ${chartData.length} 条图表数据`);

View File

@@ -6,32 +6,32 @@ const { Op } = require('sequelize');
const getPolicies = async (req, res) => {
try {
const {
policy_no,
customer_name,
policy_status,
payment_status,
policy_number, // 前端发送的参数名
policyholder_name, // 前端发送的参数名
insurance_type_id, // 前端发送的参数名
status, // 前端发送的参数名
page = 1,
limit = 10
pageSize = 10 // 前端发送的参数名
} = req.query;
const whereClause = {};
// 保单编号筛选
if (policy_no) {
whereClause.policy_no = { [Op.like]: `%${policy_no}%` };
if (policy_number) {
whereClause.policy_no = { [Op.like]: `%${policy_number}%` };
}
// 保单状态筛选
if (policy_status) {
whereClause.policy_status = policy_status;
if (status) {
whereClause.policy_status = status;
}
// 支付状态筛选
if (payment_status) {
whereClause.payment_status = payment_status;
// 保险类型筛选
if (insurance_type_id) {
whereClause.insurance_type_id = insurance_type_id;
}
const offset = (page - 1) * limit;
const offset = (page - 1) * pageSize;
const { count, rows } = await Policy.findAndCountAll({
where: whereClause,
@@ -52,16 +52,45 @@ const getPolicies = async (req, res) => {
model: User,
as: 'customer',
attributes: ['id', 'real_name', 'username']
},
{
model: InsuranceType,
as: 'insurance_type',
attributes: ['id', 'name']
}
],
order: [['created_at', 'DESC']],
offset,
limit: parseInt(limit)
limit: parseInt(pageSize)
});
res.json(responseFormat.pagination(rows, {
// 处理返回数据,确保前端能够正确解析
const processedRows = rows.map(row => {
const policy = row.toJSON();
return {
id: policy.id,
policy_number: policy.policy_no, // 映射字段名
policyholder_name: policy.application?.customer_name || policy.customer?.real_name || '',
insured_name: policy.application?.customer_name || policy.customer?.real_name || '',
insurance_type_id: policy.insurance_type_id,
insurance_type_name: policy.insurance_type?.name || '',
premium_amount: parseFloat(policy.premium_amount) || 0,
coverage_amount: parseFloat(policy.coverage_amount) || 0,
start_date: policy.start_date,
end_date: policy.end_date,
status: policy.policy_status, // 映射字段名
phone: policy.application?.customer_phone || '',
email: policy.customer?.email || '',
address: policy.application?.address || '',
remarks: policy.terms_and_conditions || '',
created_at: policy.created_at,
updated_at: policy.updated_at
};
});
res.json(responseFormat.pagination(processedRows, {
page: parseInt(page),
limit: parseInt(limit),
pageSize: parseInt(pageSize),
total: count
}, '获取保单列表成功'));
} catch (error) {

View File

@@ -17,17 +17,21 @@ class RolePermissionController {
order: [['id', 'ASC']]
});
const rolesData = roles.map(role => {
let permissions = [];
if (Array.isArray(role.permissions)) {
permissions = role.permissions;
} else if (typeof role.permissions === 'string') {
try {
permissions = JSON.parse(role.permissions);
} catch (e) {
permissions = [];
}
}
const rolesData = await Promise.all(roles.map(async (role) => {
// 从RolePermission表获取权限
const rolePermissions = await RolePermission.findAll({
where: {
role_id: role.id,
granted: true
},
include: [{
model: Permission,
as: 'permission',
attributes: ['id', 'name', 'code', 'description', 'module', 'type']
}]
});
const permissions = rolePermissions.map(rp => rp.permission.code);
return {
id: role.id,
@@ -37,7 +41,7 @@ class RolePermissionController {
permissions: permissions,
permissionCount: permissions.length
};
});
}));
res.json(responseFormat.success({
roles: rolesData,
@@ -71,8 +75,10 @@ class RolePermissionController {
async getRolePermissionDetail(req, res) {
try {
const { roleId } = req.params;
console.log('获取角色权限详情角色ID:', roleId);
const role = await Role.findByPk(roleId);
console.log('角色查询结果:', role ? role.name : '未找到');
if (!role) {
return res.status(404).json(responseFormat.error('角色不存在'));
@@ -83,26 +89,25 @@ class RolePermissionController {
attributes: ['id', 'name', 'code', 'description', 'module', 'type', 'parent_id'],
order: [['module', 'ASC'], ['id', 'ASC']]
});
console.log('权限查询结果:', allPermissions.length, '个权限');
// 构建权限树结构
const controller = this;
const permissionTree = controller.buildPermissionTree(allPermissions);
// 获取角色已分配的权限代码
let assignedPermissionCodes = [];
if (Array.isArray(role.permissions)) {
assignedPermissionCodes = role.permissions;
} else if (typeof role.permissions === 'string') {
try {
assignedPermissionCodes = JSON.parse(role.permissions);
} catch (e) {
assignedPermissionCodes = [];
}
}
// 标记已分配的权限
const markedPermissions = controller.markAssignedPermissionsByCode(permissionTree, assignedPermissionCodes);
// 从RolePermission表获取角色已分配的权限
const rolePermissions = await RolePermission.findAll({
where: {
role_id: roleId,
granted: true
},
include: [{
model: Permission,
as: 'permission',
attributes: ['id', 'name', 'code', 'description', 'module', 'type']
}]
});
const assignedPermissionCodes = rolePermissions.map(rp => rp.permission.code);
console.log('已分配权限代码:', assignedPermissionCodes.length, '个');
// 暂时返回简化数据,不构建权限树
res.json(responseFormat.success({
role: {
id: role.id,
@@ -111,12 +116,22 @@ class RolePermissionController {
status: role.status
},
assignedPermissions: assignedPermissionCodes,
allPermissions: markedPermissions,
allPermissions: allPermissions.map(p => ({
id: p.id,
name: p.name,
code: p.code,
description: p.description,
module: p.module,
type: p.type,
parent_id: p.parent_id,
assigned: assignedPermissionCodes.includes(p.code)
})),
assignedCount: assignedPermissionCodes.length,
totalCount: allPermissions.length
}, '获取角色权限详情成功'));
} catch (error) {
console.error('获取角色权限详情失败:', error);
console.error('错误堆栈:', error.stack);
res.status(500).json(responseFormat.error('获取角色权限详情失败'));
}
}
@@ -129,14 +144,24 @@ class RolePermissionController {
const { roleId } = req.params;
const { permissionIds, operation = 'replace' } = req.body;
console.log('=== 批量分配权限开始 ===');
console.log('角色ID:', roleId);
console.log('权限ID列表:', permissionIds);
console.log('操作类型:', operation);
console.log('请求体:', JSON.stringify(req.body, null, 2));
if (!Array.isArray(permissionIds)) {
console.log('❌ 权限ID列表格式错误');
return res.status(400).json(responseFormat.error('权限ID列表格式错误'));
}
const role = await Role.findByPk(roleId);
if (!role) {
console.log('❌ 角色不存在');
return res.status(404).json(responseFormat.error('角色不存在'));
}
console.log('找到角色:', role.name);
// 验证权限ID是否存在
const validPermissions = await Permission.findAll({
@@ -147,14 +172,21 @@ class RolePermissionController {
const validPermissionIds = validPermissions.map(p => p.id);
const invalidIds = permissionIds.filter(id => !validPermissionIds.includes(id));
console.log('有效权限ID:', validPermissionIds);
console.log('无效权限ID:', invalidIds);
if (invalidIds.length > 0) {
console.log('❌ 存在无效的权限ID');
return res.status(400).json(responseFormat.error(`无效的权限ID: ${invalidIds.join(', ')}`));
}
// 根据操作类型处理权限分配
if (operation === 'replace') {
console.log('执行替换模式权限分配');
// 替换模式:删除现有权限,添加新权限
await RolePermission.destroy({ where: { role_id: roleId } });
const deletedCount = await RolePermission.destroy({ where: { role_id: roleId } });
console.log('删除现有权限数量:', deletedCount);
if (permissionIds.length > 0) {
const rolePermissions = permissionIds.map(permissionId => ({
@@ -162,7 +194,10 @@ class RolePermissionController {
permission_id: permissionId,
granted: true
}));
await RolePermission.bulkCreate(rolePermissions);
console.log('准备创建的权限记录:', rolePermissions);
const createdPermissions = await RolePermission.bulkCreate(rolePermissions);
console.log('成功创建的权限记录数量:', createdPermissions.length);
}
} else if (operation === 'add') {
// 添加模式:只添加新权限
@@ -191,11 +226,15 @@ class RolePermissionController {
});
}
console.log('✅ 权限分配完成');
res.json(responseFormat.success(null, `${operation === 'replace' ? '替换' : operation === 'add' ? '添加' : '移除'}角色权限成功`));
} catch (error) {
console.error('批量分配角色权限失败:', error);
console.error('批量分配权限失败:', error);
console.error('错误堆栈:', error.stack);
res.status(500).json(responseFormat.error('批量分配角色权限失败'));
}
console.log('=== 批量分配权限结束 ===');
}
/**

View File

@@ -6,7 +6,7 @@ async function generateTestData() {
port: 9527,
user: 'root',
password: 'aiotAiot123!',
database: 'insurance_data'
database: 'nxxmdata'
});
try {
@@ -62,8 +62,8 @@ async function generateTestData() {
status, application_date, review_notes, reviewer_id, review_date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
app[0], app[1], app[2], app[3], app[4], app[5], app[6], app[8], app[9],
app[10], app[11], app[12], app[13], app[14]
app[0], app[1], app[2], app[3], app[4], app[5], app[6], app[7], app[8],
app[9], app[10], app[11], app[12], app[13]
]);
}

View File

@@ -133,7 +133,7 @@ const checkPermission = (resource, action) => {
// 如果JWT中没有权限信息或者JWT权限不足从数据库查询最新权限
if (permissions.length === 0 || !hasPermission) {
console.log('JWT权限不足或为空从数据库获取最新权限...');
const { Role } = require('../models');
const { Role, RolePermission, Permission } = require('../models');
const userRole = await Role.findByPk(user.role_id);
if (!userRole) {
@@ -141,21 +141,21 @@ const checkPermission = (resource, action) => {
return res.status(403).json(responseFormat.error('用户角色不存在'));
}
let rolePermissions = userRole.permissions || [];
// 从RolePermission表获取权限
const rolePermissions = await RolePermission.findAll({
where: {
role_id: user.role_id,
granted: true
},
include: [{
model: Permission,
as: 'permission',
attributes: ['code']
}]
});
// 如果permissions是字符串尝试解析为JSON
if (typeof rolePermissions === 'string') {
try {
permissions = JSON.parse(rolePermissions);
} catch (e) {
console.log('数据库权限解析失败:', e.message);
permissions = [];
}
} else if (Array.isArray(rolePermissions)) {
permissions = rolePermissions;
}
console.log('从数据库获取的最新权限:', permissions);
permissions = rolePermissions.map(rp => rp.permission.code);
console.log('从RolePermission表获取的最新权限:', permissions);
// 重新检查权限
hasPermission = permissions.includes(requiredPermission) ||

View File

@@ -172,7 +172,7 @@ router.get('/recent-activities', jwtAuth, checkPermission('dashboard', 'read'),
* name: type
* schema:
* type: string
* enum: [applications, policies, claims]
* enum: [applications, policies, claims, policy_status]
* default: applications
* description: 图表数据类型
* - in: query

View File

@@ -9,27 +9,27 @@ const {
deleteLivestockPolicy,
getLivestockPolicyStats
} = require('../controllers/livestockPolicyController');
const { authenticateToken, requirePermission } = require('../middleware/auth');
const { jwtAuth, checkPermission } = require('../middleware/auth');
// 获取生资保单列表
router.get('/', authenticateToken, requirePermission('livestock_policy:read'), getLivestockPolicies);
router.get('/', jwtAuth, checkPermission('insurance:policy', 'view'), getLivestockPolicies);
// 获取生资保单统计
router.get('/stats', authenticateToken, requirePermission('livestock_policy:read'), getLivestockPolicyStats);
router.get('/stats', jwtAuth, checkPermission('insurance:policy', 'view'), getLivestockPolicyStats);
// 获取单个生资保单详情
router.get('/:id', authenticateToken, requirePermission('livestock_policy:read'), getLivestockPolicyById);
router.get('/:id', jwtAuth, checkPermission('insurance:policy', 'view'), getLivestockPolicyById);
// 创建生资保单
router.post('/', authenticateToken, requirePermission('livestock_policy:create'), createLivestockPolicy);
router.post('/', jwtAuth, checkPermission('insurance:policy', 'create'), createLivestockPolicy);
// 更新生资保单
router.put('/:id', authenticateToken, requirePermission('livestock_policy:update'), updateLivestockPolicy);
router.put('/:id', jwtAuth, checkPermission('insurance:policy', 'edit'), updateLivestockPolicy);
// 更新生资保单状态
router.patch('/:id/status', authenticateToken, requirePermission('livestock_policy:update'), updateLivestockPolicyStatus);
router.patch('/:id/status', jwtAuth, checkPermission('insurance:policy', 'edit'), updateLivestockPolicyStatus);
// 删除生资保单
router.delete('/:id', authenticateToken, requirePermission('livestock_policy:delete'), deleteLivestockPolicy);
router.delete('/:id', jwtAuth, checkPermission('insurance:policy', 'delete'), deleteLivestockPolicy);
module.exports = router;

View File

@@ -9,27 +9,27 @@ const {
deleteLivestockType,
batchUpdateLivestockTypeStatus
} = require('../controllers/livestockTypeController');
const { authenticateToken, requirePermission } = require('../middleware/auth');
const { jwtAuth, checkPermission } = require('../middleware/auth');
// 获取牲畜类型列表
router.get('/', authenticateToken, requirePermission('livestock_type:read'), getLivestockTypes);
router.get('/', jwtAuth, checkPermission('insurance_type', 'read'), getLivestockTypes);
// 获取所有启用的牲畜类型(用于下拉选择)
router.get('/active', authenticateToken, getActiveLivestockTypes);
router.get('/active', getActiveLivestockTypes);
// 获取单个牲畜类型详情
router.get('/:id', authenticateToken, requirePermission('livestock_type:read'), getLivestockTypeById);
router.get('/:id', jwtAuth, checkPermission('insurance_type', 'read'), getLivestockTypeById);
// 创建牲畜类型
router.post('/', authenticateToken, requirePermission('livestock_type:create'), createLivestockType);
router.post('/', jwtAuth, checkPermission('insurance_type', 'create'), createLivestockType);
// 更新牲畜类型
router.put('/:id', authenticateToken, requirePermission('livestock_type:update'), updateLivestockType);
router.put('/:id', jwtAuth, checkPermission('insurance_type', 'edit'), updateLivestockType);
// 删除牲畜类型
router.delete('/:id', authenticateToken, requirePermission('livestock_type:delete'), deleteLivestockType);
router.delete('/:id', jwtAuth, checkPermission('insurance_type', 'delete'), deleteLivestockType);
// 批量更新牲畜类型状态
router.patch('/batch/status', authenticateToken, requirePermission('livestock_type:update'), batchUpdateLivestockTypeStatus);
router.patch('/batch/status', jwtAuth, checkPermission('insurance_type', 'edit'), batchUpdateLivestockTypeStatus);
module.exports = router;

View File

@@ -4,32 +4,32 @@ const policyController = require('../controllers/policyController');
const { jwtAuth, checkPermission } = require('../middleware/auth');
// 获取保单统计(必须在动态路由之前)
router.get('/stats/overview', jwtAuth, checkPermission('policy', 'read'),
router.get('/stats/overview', jwtAuth, checkPermission('insurance:policy', 'view'),
policyController.getPolicyStats
);
// 获取保单列表
router.get('/', jwtAuth, checkPermission('policy', 'read'),
router.get('/', jwtAuth, checkPermission('insurance:policy', 'view'),
policyController.getPolicies
);
// 创建保单
router.post('/', jwtAuth, checkPermission('policy', 'create'),
router.post('/', jwtAuth, checkPermission('insurance:policy', 'create'),
policyController.createPolicy
);
// 获取单个保单详情
router.get('/:id', jwtAuth, checkPermission('policy', 'read'),
router.get('/:id', jwtAuth, checkPermission('insurance:policy', 'view'),
policyController.getPolicyById
);
// 更新保单
router.put('/:id', jwtAuth, checkPermission('policy', 'update'),
router.put('/:id', jwtAuth, checkPermission('insurance:policy', 'edit'),
policyController.updatePolicy
);
// 更新保单状态
router.patch('/:id/status', jwtAuth, checkPermission('policy', 'update'),
router.patch('/:id/status', jwtAuth, checkPermission('insurance:policy', 'edit'),
policyController.updatePolicyStatus
);

View File

@@ -0,0 +1,156 @@
const { Permission, Role, RolePermission } = require('../models');
const { Op } = require('sequelize');
// 需要添加的权限列表
const missingPermissions = [
// 用户管理权限
{ code: 'user:read', name: '用户查看', description: '查看用户信息', module: 'user', type: 'operation' },
{ code: 'user:create', name: '用户创建', description: '创建新用户', module: 'user', type: 'operation' },
{ code: 'user:update', name: '用户更新', description: '更新用户信息', module: 'user', type: 'operation' },
{ code: 'user:delete', name: '用户删除', description: '删除用户', module: 'user', type: 'operation' },
// 保单管理权限
{ code: 'insurance:policy:create', name: '保单创建', description: '创建保单', module: 'insurance', type: 'operation' },
{ code: 'insurance:policy:edit', name: '保单编辑', description: '编辑保单信息', module: 'insurance', type: 'operation' },
{ code: 'insurance:policy:delete', name: '保单删除', description: '删除保单', module: 'insurance', type: 'operation' },
// 保险申请权限
{ code: 'insurance:read', name: '保险申请查看', description: '查看保险申请', module: 'insurance', type: 'operation' },
{ code: 'insurance:create', name: '保险申请创建', description: '创建保险申请', module: 'insurance', type: 'operation' },
{ code: 'insurance:update', name: '保险申请更新', description: '更新保险申请', module: 'insurance', type: 'operation' },
{ code: 'insurance:review', name: '保险申请审核', description: '审核保险申请', module: 'insurance', type: 'operation' },
{ code: 'insurance:delete', name: '保险申请删除', description: '删除保险申请', module: 'insurance', type: 'operation' },
// 系统管理权限
{ code: 'system:read', name: '系统查看', description: '查看系统信息', module: 'system', type: 'operation' },
{ code: 'system:update', name: '系统更新', description: '更新系统配置', module: 'system', type: 'operation' },
{ code: 'system:admin', name: '系统管理', description: '系统管理操作', module: 'system', type: 'operation' },
{ code: 'system:export', name: '系统导出', description: '导出系统数据', module: 'system', type: 'operation' },
// 监管任务权限
{ code: 'supervision_tasks:read', name: '监管任务查看', description: '查看监管任务', module: 'supervision', type: 'operation' },
{ code: 'supervision_tasks:create', name: '监管任务创建', description: '创建监管任务', module: 'supervision', type: 'operation' },
{ code: 'supervision_tasks:update', name: '监管任务更新', description: '更新监管任务', module: 'supervision', type: 'operation' },
{ code: 'supervision_tasks:delete', name: '监管任务删除', description: '删除监管任务', module: 'supervision', type: 'operation' },
// 监管任务完成权限
{ code: 'regulatory_task:read', name: '监管任务完成查看', description: '查看监管任务完成情况', module: 'regulatory', type: 'operation' },
{ code: 'regulatory_task:create', name: '监管任务完成创建', description: '创建监管任务完成记录', module: 'regulatory', type: 'operation' },
{ code: 'regulatory_task:update', name: '监管任务完成更新', description: '更新监管任务完成记录', module: 'regulatory', type: 'operation' },
{ code: 'regulatory_task:delete', name: '监管任务完成删除', description: '删除监管任务完成记录', module: 'regulatory', type: 'operation' },
{ code: 'regulatory_task:review', name: '监管任务完成审核', description: '审核监管任务完成记录', module: 'regulatory', type: 'operation' },
// 安装任务权限
{ code: 'installation_tasks:read', name: '安装任务查看', description: '查看安装任务', module: 'installation', type: 'operation' },
{ code: 'installation_tasks:create', name: '安装任务创建', description: '创建安装任务', module: 'installation', type: 'operation' },
{ code: 'installation_tasks:update', name: '安装任务更新', description: '更新安装任务', module: 'installation', type: 'operation' },
{ code: 'installation_tasks:delete', name: '安装任务删除', description: '删除安装任务', module: 'installation', type: 'operation' },
// 生资理赔权限
{ code: 'livestock_claim:read', name: '生资理赔查看', description: '查看生资理赔', module: 'livestock', type: 'operation' },
{ code: 'livestock_claim:create', name: '生资理赔创建', description: '创建生资理赔', module: 'livestock', type: 'operation' },
{ code: 'livestock_claim:review', name: '生资理赔审核', description: '审核生资理赔', module: 'livestock', type: 'operation' },
{ code: 'livestock_claim:payment', name: '生资理赔支付', description: '处理生资理赔支付', module: 'livestock', type: 'operation' },
// 设备管理权限
{ code: 'device:read', name: '设备查看', description: '查看设备信息', module: 'device', type: 'operation' },
{ code: 'device:create', name: '设备创建', description: '创建设备', module: 'device', type: 'operation' },
{ code: 'device:update', name: '设备更新', description: '更新设备信息', module: 'device', type: 'operation' },
{ code: 'device:delete', name: '设备删除', description: '删除设备', module: 'device', type: 'operation' },
// 设备告警权限
{ code: 'device_alerts:read', name: '设备告警查看', description: '查看设备告警', module: 'device', type: 'operation' },
{ code: 'device_alerts:create', name: '设备告警创建', description: '创建设备告警', module: 'device', type: 'operation' },
{ code: 'device_alerts:update', name: '设备告警更新', description: '更新设备告警', module: 'device', type: 'operation' },
{ code: 'device_alerts:delete', name: '设备告警删除', description: '删除设备告警', module: 'device', type: 'operation' },
// 理赔管理权限
{ code: 'claim:read', name: '理赔查看', description: '查看理赔信息', module: 'claim', type: 'operation' },
{ code: 'claim:create', name: '理赔创建', description: '创建理赔', module: 'claim', type: 'operation' },
{ code: 'claim:update', name: '理赔更新', description: '更新理赔信息', module: 'claim', type: 'operation' },
{ code: 'claim:delete', name: '理赔删除', description: '删除理赔', module: 'claim', type: 'operation' },
{ code: 'claim:review', name: '理赔审核', description: '审核理赔', module: 'claim', type: 'operation' },
// 数据仓库权限
{ code: 'data_warehouse:read', name: '数据仓库查看', description: '查看数据仓库', module: 'data', type: 'operation' },
{ code: 'data_warehouse:export', name: '数据仓库导出', description: '导出数据仓库数据', module: 'data', type: 'operation' }
];
async function addMissingPermissions() {
try {
console.log('开始添加缺失的权限...');
// 获取现有权限
const existingPermissions = await Permission.findAll({
attributes: ['code']
});
const existingCodes = existingPermissions.map(p => p.code);
// 过滤出需要添加的权限
const permissionsToAdd = missingPermissions.filter(p => !existingCodes.includes(p.code));
console.log(`找到 ${permissionsToAdd.length} 个需要添加的权限`);
if (permissionsToAdd.length === 0) {
console.log('所有权限都已存在,无需添加');
return;
}
// 批量创建权限
const createdPermissions = await Permission.bulkCreate(permissionsToAdd, {
ignoreDuplicates: true
});
console.log(`成功创建 ${createdPermissions.length} 个权限`);
// 获取admin角色
const adminRole = await Role.findOne({
where: { name: 'admin' }
});
if (!adminRole) {
console.log('未找到admin角色跳过权限分配');
return;
}
console.log(`找到admin角色ID: ${adminRole.id}`);
// 获取新创建的权限ID
const newPermissions = await Permission.findAll({
where: {
code: {
[Op.in]: permissionsToAdd.map(p => p.code)
}
}
});
// 为admin角色分配新权限
const rolePermissions = newPermissions.map(permission => ({
role_id: adminRole.id,
permission_id: permission.id,
granted: true,
granted_by: 1, // 假设用户ID为1
granted_at: new Date()
}));
await RolePermission.bulkCreate(rolePermissions, {
ignoreDuplicates: true
});
console.log(`成功为admin角色分配 ${rolePermissions.length} 个权限`);
console.log('权限添加完成!');
} catch (error) {
console.error('添加权限失败:', error);
}
}
// 运行脚本
addMissingPermissions().then(() => {
console.log('脚本执行完成');
process.exit(0);
}).catch(error => {
console.error('脚本执行失败:', error);
process.exit(1);
});

View File

@@ -1,4 +1,10 @@
require('dotenv').config({ path: require('path').join(__dirname, '../.env') });
// 设置默认环境变量
if (!process.env.JWT_SECRET) {
process.env.JWT_SECRET = 'insurance_super_secret_jwt_key_2024_very_long_and_secure';
}
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');

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' },