diff --git a/admin-system/default.conf b/admin-system/default.conf index 8bd2663..9150bbc 100644 --- a/admin-system/default.conf +++ b/admin-system/default.conf @@ -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/ { diff --git a/admin-system/package-lock.json b/admin-system/package-lock.json index ad8f87b..06b7e74 100644 --- a/admin-system/package-lock.json +++ b/admin-system/package-lock.json @@ -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", diff --git a/admin-system/package.json b/admin-system/package.json index f77b987..8bd29bd 100644 --- a/admin-system/package.json +++ b/admin-system/package.json @@ -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", diff --git a/admin-system/src/stores/data.js b/admin-system/src/stores/data.js index dbee447..8bc2d40 100644 --- a/admin-system/src/stores/data.js +++ b/admin-system/src/stores/data.js @@ -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 } }) \ No newline at end of file diff --git a/admin-system/src/stores/user.js b/admin-system/src/stores/user.js index 4604f17..7936f44 100644 --- a/admin-system/src/stores/user.js +++ b/admin-system/src/stores/user.js @@ -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, diff --git a/admin-system/src/utils/websocketService.js b/admin-system/src/utils/websocketService.js deleted file mode 100644 index de6c918..0000000 --- a/admin-system/src/utils/websocketService.js +++ /dev/null @@ -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; diff --git a/backend/package-lock.json b/backend/package-lock.json index 7c288e1..38e4519 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index eb47d8c..6094824 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/server.js b/backend/server.js index 7f8cb32..8507a5f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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 }; \ No newline at end of file +// 导出app供其他模块使用 +module.exports = { app }; \ No newline at end of file diff --git a/backend/services/realtimeService.js b/backend/services/realtimeService.js deleted file mode 100644 index 25f2de1..0000000 --- a/backend/services/realtimeService.js +++ /dev/null @@ -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} 统计数据 - */ - 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; diff --git a/backend/utils/websocket.js b/backend/utils/websocket.js deleted file mode 100644 index bb30191..0000000 --- a/backend/utils/websocket.js +++ /dev/null @@ -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; diff --git a/insurance_admin-system/package-lock.json b/insurance_admin-system/package-lock.json index 5c49b94..b494018 100644 --- a/insurance_admin-system/package-lock.json +++ b/insurance_admin-system/package-lock.json @@ -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": { diff --git a/insurance_admin-system/src/components/RolePermissionManagement.vue b/insurance_admin-system/src/components/RolePermissionManagement.vue index 830292f..b6cbd04 100644 --- a/insurance_admin-system/src/components/RolePermissionManagement.vue +++ b/insurance_admin-system/src/components/RolePermissionManagement.vue @@ -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() }) diff --git a/insurance_admin-system/src/router/index.js b/insurance_admin-system/src/router/index.js index ac83408..24e4bd7 100644 --- a/insurance_admin-system/src/router/index.js +++ b/insurance_admin-system/src/router/index.js @@ -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() diff --git a/insurance_admin-system/src/stores/user.js b/insurance_admin-system/src/stores/user.js index 8e69cd5..19bec89 100644 --- a/insurance_admin-system/src/stores/user.js +++ b/insurance_admin-system/src/stores/user.js @@ -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, diff --git a/insurance_admin-system/src/utils/api.js b/insurance_admin-system/src/utils/api.js index 46945f7..7d1ed54 100644 --- a/insurance_admin-system/src/utils/api.js +++ b/insurance_admin-system/src/utils/api.js @@ -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') diff --git a/insurance_admin-system/src/views/ApplicationManagement.vue b/insurance_admin-system/src/views/ApplicationManagement.vue index 272e291..9d4d567 100644 --- a/insurance_admin-system/src/views/ApplicationManagement.vue +++ b/insurance_admin-system/src/views/ApplicationManagement.vue @@ -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 = [] } } diff --git a/insurance_admin-system/src/views/ClaimManagement.vue b/insurance_admin-system/src/views/ClaimManagement.vue index d290be8..2184a78 100644 --- a/insurance_admin-system/src/views/ClaimManagement.vue +++ b/insurance_admin-system/src/views/ClaimManagement.vue @@ -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 { diff --git a/insurance_admin-system/src/views/CompletedTaskManagement.vue b/insurance_admin-system/src/views/CompletedTaskManagement.vue index 1fedc69..886e0ae 100644 --- a/insurance_admin-system/src/views/CompletedTaskManagement.vue +++ b/insurance_admin-system/src/views/CompletedTaskManagement.vue @@ -34,7 +34,7 @@ diff --git a/insurance_admin-system/src/views/Dashboard.vue b/insurance_admin-system/src/views/Dashboard.vue index 046769c..9f27ccd 100644 --- a/insurance_admin-system/src/views/Dashboard.vue +++ b/insurance_admin-system/src/views/Dashboard.vue @@ -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}
{b}: {c} ({d}%)' + formatter: '{a}
{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() }) diff --git a/insurance_admin-system/src/views/DataWarehouse.vue b/insurance_admin-system/src/views/DataWarehouse.vue index eaa3b05..6fce57d 100644 --- a/insurance_admin-system/src/views/DataWarehouse.vue +++ b/insurance_admin-system/src/views/DataWarehouse.vue @@ -78,7 +78,7 @@ \ No newline at end of file + +onMounted(() => { + refreshStatus() +}) + + + \ No newline at end of file diff --git a/insurance_admin-system/src/views/PolicyManagement.vue b/insurance_admin-system/src/views/PolicyManagement.vue index 4e3294a..41fb98e 100644 --- a/insurance_admin-system/src/views/PolicyManagement.vue +++ b/insurance_admin-system/src/views/PolicyManagement.vue @@ -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 { diff --git a/insurance_admin-system/src/views/SupervisionTaskManagement.vue b/insurance_admin-system/src/views/SupervisionTaskManagement.vue index ac7d040..b83406a 100644 --- a/insurance_admin-system/src/views/SupervisionTaskManagement.vue +++ b/insurance_admin-system/src/views/SupervisionTaskManagement.vue @@ -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) diff --git a/insurance_admin-system/src/views/SupervisoryTaskManagement.vue b/insurance_admin-system/src/views/SupervisoryTaskManagement.vue index 876bc03..893c1ac 100644 --- a/insurance_admin-system/src/views/SupervisoryTaskManagement.vue +++ b/insurance_admin-system/src/views/SupervisoryTaskManagement.vue @@ -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) diff --git a/insurance_admin-system/src/views/SystemLogs.vue b/insurance_admin-system/src/views/SystemLogs.vue index cf3d62e..b1082ac 100644 --- a/insurance_admin-system/src/views/SystemLogs.vue +++ b/insurance_admin-system/src/views/SystemLogs.vue @@ -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 { diff --git a/insurance_admin-system/src/views/TokenDebug.vue b/insurance_admin-system/src/views/TokenDebug.vue new file mode 100644 index 0000000..b211a4d --- /dev/null +++ b/insurance_admin-system/src/views/TokenDebug.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/insurance_admin-system/src/views/UserManagement.vue b/insurance_admin-system/src/views/UserManagement.vue index 422be0f..48211ac 100644 --- a/insurance_admin-system/src/views/UserManagement.vue +++ b/insurance_admin-system/src/views/UserManagement.vue @@ -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 { diff --git a/insurance_admin-system/test-frontend-api.js b/insurance_admin-system/test-frontend-api.js new file mode 100644 index 0000000..b3d004b --- /dev/null +++ b/insurance_admin-system/test-frontend-api.js @@ -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()'); diff --git a/insurance_backend/check_db_structure.js b/insurance_backend/check_db_structure.js new file mode 100644 index 0000000..2f8817d --- /dev/null +++ b/insurance_backend/check_db_structure.js @@ -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(); \ No newline at end of file diff --git a/insurance_backend/controllers/dashboardController.js b/insurance_backend/controllers/dashboardController.js index c92a810..2e593e0 100644 --- a/insurance_backend/controllers/dashboardController.js +++ b/insurance_backend/controllers/dashboardController.js @@ -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} 条图表数据`); diff --git a/insurance_backend/controllers/policyController.js b/insurance_backend/controllers/policyController.js index a3032e3..de45db6 100644 --- a/insurance_backend/controllers/policyController.js +++ b/insurance_backend/controllers/policyController.js @@ -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) { diff --git a/insurance_backend/controllers/rolePermissionController.js b/insurance_backend/controllers/rolePermissionController.js index 659bf6f..f197634 100644 --- a/insurance_backend/controllers/rolePermissionController.js +++ b/insurance_backend/controllers/rolePermissionController.js @@ -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('=== 批量分配权限结束 ==='); } /** diff --git a/insurance_backend/generate_test_data.js b/insurance_backend/generate_test_data.js index 7ce8a23..1fba110 100644 --- a/insurance_backend/generate_test_data.js +++ b/insurance_backend/generate_test_data.js @@ -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] ]); } diff --git a/insurance_backend/middleware/auth.js b/insurance_backend/middleware/auth.js index c10262d..aa7e90a 100644 --- a/insurance_backend/middleware/auth.js +++ b/insurance_backend/middleware/auth.js @@ -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) || diff --git a/insurance_backend/routes/dashboard.js b/insurance_backend/routes/dashboard.js index e065a99..1987a63 100644 --- a/insurance_backend/routes/dashboard.js +++ b/insurance_backend/routes/dashboard.js @@ -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 diff --git a/insurance_backend/routes/livestockPolicies.js b/insurance_backend/routes/livestockPolicies.js index a0c3edc..e4aec37 100644 --- a/insurance_backend/routes/livestockPolicies.js +++ b/insurance_backend/routes/livestockPolicies.js @@ -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; \ No newline at end of file diff --git a/insurance_backend/routes/livestockTypes.js b/insurance_backend/routes/livestockTypes.js index ee631b0..62ead02 100644 --- a/insurance_backend/routes/livestockTypes.js +++ b/insurance_backend/routes/livestockTypes.js @@ -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; \ No newline at end of file diff --git a/insurance_backend/routes/policies.js b/insurance_backend/routes/policies.js index de80e48..9c09b5e 100644 --- a/insurance_backend/routes/policies.js +++ b/insurance_backend/routes/policies.js @@ -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 ); diff --git a/insurance_backend/scripts/add_missing_permissions.js b/insurance_backend/scripts/add_missing_permissions.js new file mode 100644 index 0000000..0c078da --- /dev/null +++ b/insurance_backend/scripts/add_missing_permissions.js @@ -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); +}); diff --git a/insurance_backend/src/app.js b/insurance_backend/src/app.js index 9edc892..f95eaa6 100644 --- a/insurance_backend/src/app.js +++ b/insurance_backend/src/app.js @@ -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'); diff --git a/mini_program/farm-monitor-dashboard/app.json b/mini_program/farm-monitor-dashboard/app.json index cd05e95..fc2ac6a 100644 --- a/mini_program/farm-monitor-dashboard/app.json +++ b/mini_program/farm-monitor-dashboard/app.json @@ -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" diff --git a/mini_program/farm-monitor-dashboard/pages/device/collar/collar.js b/mini_program/farm-monitor-dashboard/pages/device/collar/collar.js index e33c7c9..45fef18 100644 --- a/mini_program/farm-monitor-dashboard/pages/device/collar/collar.js +++ b/mini_program/farm-monitor-dashboard/pages/device/collar/collar.js @@ -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() }, // 分页切换 diff --git a/mini_program/farm-monitor-dashboard/pages/device/collar/collar.wxml b/mini_program/farm-monitor-dashboard/pages/device/collar/collar.wxml index eda51a3..d911154 100644 --- a/mini_program/farm-monitor-dashboard/pages/device/collar/collar.wxml +++ b/mini_program/farm-monitor-dashboard/pages/device/collar/collar.wxml @@ -7,31 +7,211 @@ bindinput="onSearchInput" value="{{searchValue}}" /> - + + + + + + + 搜索项圈编号: {{searchValue}} + + + + + + 搜索结果 + 找到匹配的设备 + + + + + {{searchResult.snText}} + {{searchResult.statusText}} + + + + + + 项圈编号: + {{searchResult.sn}} + + + + 佩戴状态: + {{searchResult.wearStatusText}} + + + + 连接状态: + {{searchResult.connectStatusText}} + + + + 电池电量: + {{searchResult.batteryText}} + + + + 体温: + {{searchResult.temperatureText}} + + + + 步数: + {{searchResult.stepsText}} + + + + 信号强度: + {{searchResult.signalText}} + + + + GPS信号: + {{searchResult.gpsText}} + + + + 位置状态: + {{searchResult.locationText}} + + + + 最后更新: + {{searchResult.lastUpdateText}} + + + + 更新间隔: + {{searchResult.updateIntervalText}} + + + + + + + + + + + + + + 🔍 + 未找到项圈编号为 "{{searchValue}}" 的设备 + - + - - 项圈编号: {{item.deviceId}} - 状态: {{item.status | statusText}} - 电量: {{item.battery}}% - 最后在线: {{item.lastOnlineTime}} + + + + {{item.snText}} + {{item.statusText}} + + + + + + 项圈编号: + {{item.sn}} + + + + 佩戴状态: + {{item.wearStatusText}} + + + + 连接状态: + {{item.connectStatusText}} + + + + 电池电量: + {{item.batteryText}} + + + + 体温: + {{item.temperatureText}} + + + + 步数: + {{item.stepsText}} + + + + 信号强度: + {{item.signalText}} + + + + GPS信号: + {{item.gpsText}} + + + + 位置状态: + {{item.locationText}} + + + + 最后更新: + {{item.lastUpdateText}} + + + + 更新间隔: + {{item.updateIntervalText}} + + + + + + + + - - - + + 共 {{total}} 条数据,第 {{currentPage}} / {{totalPages}} 页 + + + + + + + + {{item}} + + + + + + \ No newline at end of file diff --git a/mini_program/farm-monitor-dashboard/pages/device/collar/collar.wxss b/mini_program/farm-monitor-dashboard/pages/device/collar/collar.wxss index 55eff87..ff7e411 100644 --- a/mini_program/farm-monitor-dashboard/pages/device/collar/collar.wxss +++ b/mini_program/farm-monitor-dashboard/pages/device/collar/collar.wxss @@ -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); } \ No newline at end of file diff --git a/mini_program/farm-monitor-dashboard/pages/device/fence/fence.js b/mini_program/farm-monitor-dashboard/pages/device/fence/fence.js new file mode 100644 index 0000000..3a98f53 --- /dev/null +++ b/mini_program/farm-monitor-dashboard/pages/device/fence/fence.js @@ -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 + } + }) + }, +}) diff --git a/mini_program/farm-monitor-dashboard/pages/device/fence/fence.json b/mini_program/farm-monitor-dashboard/pages/device/fence/fence.json new file mode 100644 index 0000000..f711532 --- /dev/null +++ b/mini_program/farm-monitor-dashboard/pages/device/fence/fence.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "电子围栏", + "navigationBarBackgroundColor": "#3cc51f", + "navigationBarTextStyle": "white", + "backgroundColor": "#f5f5f5" +} diff --git a/mini_program/farm-monitor-dashboard/pages/device/fence/fence.wxml b/mini_program/farm-monitor-dashboard/pages/device/fence/fence.wxml new file mode 100644 index 0000000..f871c30 --- /dev/null +++ b/mini_program/farm-monitor-dashboard/pages/device/fence/fence.wxml @@ -0,0 +1,148 @@ + + + + + + + + 电子围栏 + + + + + + + + + + 📡 + 离线模式 - 显示缓存数据 + + + + + 🔒 + 地图已锁定 - 防止自动移动 + + + + + + + + + + + + + + 显示牧场 + + + + + + 智能采集器: + {{stats.smartCollector}} + + + 智能设备: + {{stats.smartDevice}} + + + 围栏总数: + {{fenceList.length}} + + + + + 各德 + + + + + + 切换地图 + + + + + + + + + {{selectedFence.typeIcon}} + {{selectedFence.name}} + + + + + + + + + 围栏类型: + {{selectedFence.typeName}} + + + 放牧状态: + {{selectedFence.grazingStatus}} + + + 围栏面积: + {{selectedFence.area}}平方米 + + + 坐标点数: + {{selectedFence.coordinates.length}}个 + + + 围栏描述: + {{selectedFence.description || '无描述'}} + + + + + + 定位围栏 + + + 查看详情 + + + + + + + + + + 地图加载中... + + + + diff --git a/mini_program/farm-monitor-dashboard/pages/device/fence/fence.wxss b/mini_program/farm-monitor-dashboard/pages/device/fence/fence.wxss new file mode 100644 index 0000000..afdcef8 --- /dev/null +++ b/mini_program/farm-monitor-dashboard/pages/device/fence/fence.wxss @@ -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; + } +} diff --git a/mini_program/farm-monitor-dashboard/pages/device/host/host.js b/mini_program/farm-monitor-dashboard/pages/device/host/host.js new file mode 100644 index 0000000..95fd7ce --- /dev/null +++ b/mini_program/farm-monitor-dashboard/pages/device/host/host.js @@ -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' + }) + } +}) \ No newline at end of file diff --git a/mini_program/farm-monitor-dashboard/pages/device/host/host.json b/mini_program/farm-monitor-dashboard/pages/device/host/host.json new file mode 100644 index 0000000..c4fcfee --- /dev/null +++ b/mini_program/farm-monitor-dashboard/pages/device/host/host.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "智能主机", + "enablePullDownRefresh": true, + "onReachBottomDistance": 50 +} diff --git a/mini_program/farm-monitor-dashboard/pages/device/host/host.wxml b/mini_program/farm-monitor-dashboard/pages/device/host/host.wxml new file mode 100644 index 0000000..54d9b58 --- /dev/null +++ b/mini_program/farm-monitor-dashboard/pages/device/host/host.wxml @@ -0,0 +1,177 @@ + + + + + 🔍 + + + + + + + + + 搜索主机编号: {{searchValue}} + + + + + + 主机总数 + {{stats.total}} + + + 联网数量 + {{stats.online}} + + + 断网数量 + {{stats.offline}} + + + + + + + 搜索结果 + 找到匹配的设备 + + + + + 主机编号: {{searchResult.deviceNumberText}} + + {{searchResult.statusText}} + + + + + + + 设备电量: + {{searchResult.batteryText}} + + + + 设备信号值: + {{searchResult.signalText}} + + + + 设备温度: + {{searchResult.temperatureText}} + + + + 绑带状态: + {{searchResult.statusText}} + + + + 数据更新时间: + {{searchResult.updateTimeText}} + + + + + + + + 🔍 + 未找到主机编号为 "{{searchValue}}" 的设备 + + + + + + + + + + 主机编号: {{item.deviceNumberText}} + + {{item.statusText}} + + + + + + + 设备电量: + {{item.batteryText}} + + + + 设备信号值: + {{item.signalText}} + + + + 设备温度: + {{item.temperatureText}} + + + + 绑带状态: + {{item.statusText}} + + + + 数据更新时间: + {{item.updateTimeText}} + + + + + + + + + + 共 {{total}} 条记录,第 {{currentPage}}/{{totalPages}} 页 + + + + + + 上一页 + + + + + + {{item.text}} + + + + + + 下一页 + + + + + + + + + \ No newline at end of file diff --git a/mini_program/farm-monitor-dashboard/pages/device/host/host.wxss b/mini_program/farm-monitor-dashboard/pages/device/host/host.wxss new file mode 100644 index 0000000..6305ed9 --- /dev/null +++ b/mini_program/farm-monitor-dashboard/pages/device/host/host.wxss @@ -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; +} diff --git a/mini_program/farm-monitor-dashboard/pages/home/home.js b/mini_program/farm-monitor-dashboard/pages/home/home.js index c643dfe..1cb38aa 100644 --- a/mini_program/farm-monitor-dashboard/pages/home/home.js +++ b/mini_program/farm-monitor-dashboard/pages/home/home.js @@ -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' },