完善保险项目和养殖端小程序
This commit is contained in:
@@ -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/ {
|
||||
|
||||
217
admin-system/package-lock.json
generated
217
admin-system/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
341
backend/package-lock.json
generated
341
backend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -6,7 +6,6 @@ const multer = require('multer');
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
const swaggerSpec = require('./config/swagger');
|
||||
const { sequelize } = require('./config/database-simple');
|
||||
const webSocketManager = require('./utils/websocket');
|
||||
const logger = require('./utils/logger');
|
||||
const {
|
||||
apiRateLimiter,
|
||||
@@ -264,24 +263,23 @@ app.use((err, req, res, next) => {
|
||||
});
|
||||
|
||||
// 初始化WebSocket
|
||||
webSocketManager.init(server);
|
||||
// webSocketManager.init(server);
|
||||
|
||||
// 初始化实时数据推送服务
|
||||
const realtimeService = require('./services/realtimeService');
|
||||
// const realtimeService = require('./services/realtimeService');
|
||||
|
||||
// 启动服务器
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`服务器运行在端口 ${PORT}`);
|
||||
console.log(`服务器监听所有网络接口 (0.0.0.0:${PORT})`);
|
||||
console.log(`API 文档地址: http://localhost:${PORT}/api-docs`);
|
||||
console.log(`WebSocket 服务已启动`);
|
||||
|
||||
// 启动实时数据推送服务
|
||||
realtimeService.start();
|
||||
console.log(`实时数据推送服务已启动`);
|
||||
// realtimeService.start();
|
||||
// console.log(`实时数据推送服务已启动`);
|
||||
|
||||
logger.info(`宁夏智慧养殖监管平台服务器启动成功,端口: ${PORT}`);
|
||||
});
|
||||
|
||||
// 导出app和webSocketManager供其他模块使用
|
||||
module.exports = { app, webSocketManager };
|
||||
// 导出app供其他模块使用
|
||||
module.exports = { app };
|
||||
@@ -1,364 +0,0 @@
|
||||
/**
|
||||
* 实时数据推送服务
|
||||
* @file realtimeService.js
|
||||
* @description 定期检查数据变化并通过WebSocket推送给客户端
|
||||
*/
|
||||
const cron = require('node-cron');
|
||||
const { Device, Alert, Animal, Farm } = require('../models');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
const webSocketManager = require('../utils/websocket');
|
||||
const notificationService = require('./notificationService');
|
||||
const logger = require('../utils/logger');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
class RealtimeService {
|
||||
constructor() {
|
||||
this.isRunning = false;
|
||||
this.lastUpdateTimes = {
|
||||
devices: null,
|
||||
alerts: null,
|
||||
animals: null,
|
||||
stats: null
|
||||
};
|
||||
this.updateInterval = 30; // 30秒更新间隔,符合文档要求
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动实时数据推送服务
|
||||
*/
|
||||
start() {
|
||||
if (this.isRunning) {
|
||||
logger.warn('实时数据推送服务已在运行中');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
|
||||
// 设备状态监控 - 每30秒检查一次
|
||||
cron.schedule(`*/${this.updateInterval} * * * * *`, () => {
|
||||
this.checkDeviceUpdates();
|
||||
});
|
||||
|
||||
// 预警监控 - 每10秒检查一次(预警更紧急)
|
||||
cron.schedule('*/10 * * * * *', () => {
|
||||
this.checkAlertUpdates();
|
||||
});
|
||||
|
||||
// 动物健康状态监控 - 每60秒检查一次
|
||||
cron.schedule('*/60 * * * * *', () => {
|
||||
this.checkAnimalUpdates();
|
||||
});
|
||||
|
||||
// 系统统计数据更新 - 每2分钟更新一次
|
||||
cron.schedule('*/120 * * * * *', () => {
|
||||
this.updateSystemStats();
|
||||
});
|
||||
|
||||
logger.info('实时数据推送服务已启动');
|
||||
console.log(`实时数据推送服务已启动,更新间隔: ${this.updateInterval}秒`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止实时数据推送服务
|
||||
*/
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
logger.info('实时数据推送服务已停止');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备状态更新
|
||||
*/
|
||||
async checkDeviceUpdates() {
|
||||
try {
|
||||
const lastCheck = this.lastUpdateTimes.devices || new Date(Date.now() - 60000); // 默认检查最近1分钟
|
||||
|
||||
const updatedDevices = await Device.findAll({
|
||||
where: {
|
||||
updated_at: {
|
||||
[Op.gt]: lastCheck
|
||||
}
|
||||
},
|
||||
include: [{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}],
|
||||
order: [['updated_at', 'DESC']]
|
||||
});
|
||||
|
||||
if (updatedDevices.length > 0) {
|
||||
logger.info(`检测到 ${updatedDevices.length} 个设备状态更新`);
|
||||
|
||||
// 为每个更新的设备推送数据
|
||||
for (const device of updatedDevices) {
|
||||
webSocketManager.broadcastDeviceUpdate({
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
type: device.type,
|
||||
status: device.status,
|
||||
farm_id: device.farm_id,
|
||||
farm_name: device.farm?.name,
|
||||
last_maintenance: device.last_maintenance,
|
||||
metrics: device.metrics,
|
||||
updated_at: device.updated_at
|
||||
});
|
||||
}
|
||||
|
||||
this.lastUpdateTimes.devices = new Date();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('检查设备更新失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查预警更新
|
||||
*/
|
||||
async checkAlertUpdates() {
|
||||
try {
|
||||
const lastCheck = this.lastUpdateTimes.alerts || new Date(Date.now() - 30000); // 默认检查最近30秒
|
||||
|
||||
const newAlerts = await Alert.findAll({
|
||||
where: {
|
||||
created_at: {
|
||||
[Op.gt]: lastCheck
|
||||
}
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name', 'contact', 'phone']
|
||||
},
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['id', 'name', 'type']
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
if (newAlerts.length > 0) {
|
||||
logger.info(`检测到 ${newAlerts.length} 个新预警`);
|
||||
|
||||
// 推送新预警
|
||||
for (const alert of newAlerts) {
|
||||
webSocketManager.broadcastAlert({
|
||||
id: alert.id,
|
||||
type: alert.type,
|
||||
level: alert.level,
|
||||
message: alert.message,
|
||||
status: alert.status,
|
||||
farm_id: alert.farm_id,
|
||||
farm_name: alert.farm?.name,
|
||||
device_id: alert.device_id,
|
||||
device_name: alert.device?.name,
|
||||
created_at: alert.created_at
|
||||
});
|
||||
|
||||
// 如果是高级或紧急预警,立即发送通知
|
||||
if (alert.level === 'high' || alert.level === 'critical') {
|
||||
await this.sendUrgentNotification(alert);
|
||||
}
|
||||
}
|
||||
|
||||
this.lastUpdateTimes.alerts = new Date();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('检查预警更新失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查动物健康状态更新
|
||||
*/
|
||||
async checkAnimalUpdates() {
|
||||
try {
|
||||
const lastCheck = this.lastUpdateTimes.animals || new Date(Date.now() - 120000); // 默认检查最近2分钟
|
||||
|
||||
const updatedAnimals = await Animal.findAll({
|
||||
where: {
|
||||
updated_at: {
|
||||
[Op.gt]: lastCheck
|
||||
}
|
||||
},
|
||||
include: [{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}],
|
||||
order: [['updated_at', 'DESC']]
|
||||
});
|
||||
|
||||
if (updatedAnimals.length > 0) {
|
||||
logger.info(`检测到 ${updatedAnimals.length} 个动物健康状态更新`);
|
||||
|
||||
for (const animal of updatedAnimals) {
|
||||
webSocketManager.broadcastAnimalUpdate({
|
||||
id: animal.id,
|
||||
type: animal.type,
|
||||
count: animal.count,
|
||||
health_status: animal.health_status,
|
||||
farm_id: animal.farm_id,
|
||||
farm_name: animal.farm?.name,
|
||||
last_inspection: animal.last_inspection,
|
||||
updated_at: animal.updated_at
|
||||
});
|
||||
}
|
||||
|
||||
this.lastUpdateTimes.animals = new Date();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('检查动物更新失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新系统统计数据
|
||||
*/
|
||||
async updateSystemStats() {
|
||||
try {
|
||||
const stats = await this.getSystemStats();
|
||||
webSocketManager.broadcastStatsUpdate(stats);
|
||||
|
||||
this.lastUpdateTimes.stats = new Date();
|
||||
logger.info('系统统计数据已推送');
|
||||
} catch (error) {
|
||||
logger.error('更新系统统计失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统统计数据
|
||||
* @returns {Promise<Object>} 统计数据
|
||||
*/
|
||||
async getSystemStats() {
|
||||
try {
|
||||
const [farmCount, deviceCount, animalCount, alertCount] = await Promise.all([
|
||||
Farm.count(),
|
||||
Device.count(),
|
||||
Animal.sum('count'),
|
||||
Alert.count({ where: { status: 'active' } })
|
||||
]);
|
||||
|
||||
const deviceStatusStats = await Device.findAll({
|
||||
attributes: [
|
||||
'status',
|
||||
[sequelize.fn('COUNT', sequelize.col('status')), 'count']
|
||||
],
|
||||
group: ['status']
|
||||
});
|
||||
|
||||
const alertLevelStats = await Alert.findAll({
|
||||
where: { status: 'active' },
|
||||
attributes: [
|
||||
'level',
|
||||
[sequelize.fn('COUNT', sequelize.col('level')), 'count']
|
||||
],
|
||||
group: ['level']
|
||||
});
|
||||
|
||||
return {
|
||||
farmCount: farmCount || 0,
|
||||
deviceCount: deviceCount || 0,
|
||||
animalCount: animalCount || 0,
|
||||
alertCount: alertCount || 0,
|
||||
deviceStatus: deviceStatusStats.reduce((acc, item) => {
|
||||
acc[item.status] = parseInt(item.dataValues.count);
|
||||
return acc;
|
||||
}, {}),
|
||||
alertLevels: alertLevelStats.reduce((acc, item) => {
|
||||
acc[item.level] = parseInt(item.dataValues.count);
|
||||
return acc;
|
||||
}, {}),
|
||||
timestamp: new Date()
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('获取系统统计数据失败:', error);
|
||||
return {
|
||||
error: '获取统计数据失败',
|
||||
timestamp: new Date()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送紧急预警通知
|
||||
* @param {Object} alert 预警对象
|
||||
*/
|
||||
async sendUrgentNotification(alert) {
|
||||
try {
|
||||
logger.warn(`紧急预警: ${alert.message} (级别: ${alert.level})`);
|
||||
|
||||
// 发送实时WebSocket通知给管理员
|
||||
webSocketManager.broadcastAlert({
|
||||
id: alert.id,
|
||||
type: alert.type,
|
||||
level: alert.level,
|
||||
message: alert.message,
|
||||
farm_id: alert.farm_id,
|
||||
farm_name: alert.farm?.name,
|
||||
created_at: alert.created_at
|
||||
});
|
||||
|
||||
// 发送邮件/短信通知
|
||||
const isUrgent = alert.level === 'critical' || alert.level === 'high';
|
||||
await notificationService.sendAlertNotification(alert, [], {
|
||||
urgent: isUrgent,
|
||||
includeSMS: alert.level === 'critical', // 仅紧急预警发送短信
|
||||
maxResponseTime: 300000 // 5分钟响应时间
|
||||
});
|
||||
|
||||
logger.info(`预警通知已发送: 预警ID ${alert.id}, 紧急程度: ${isUrgent}`);
|
||||
} catch (error) {
|
||||
logger.error('发送紧急预警通知失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟设备数据变化(用于演示)
|
||||
*/
|
||||
async simulateDeviceChange(deviceId) {
|
||||
try {
|
||||
const device = await Device.findByPk(deviceId);
|
||||
if (!device) return;
|
||||
|
||||
// 随机改变设备状态
|
||||
const statuses = ['online', 'offline', 'maintenance'];
|
||||
const randomStatus = statuses[Math.floor(Math.random() * statuses.length)];
|
||||
|
||||
await device.update({
|
||||
status: randomStatus,
|
||||
metrics: {
|
||||
temperature: Math.random() * 10 + 20, // 20-30度
|
||||
humidity: Math.random() * 20 + 50, // 50-70%
|
||||
lastUpdate: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`模拟设备 ${deviceId} 状态变化为: ${randomStatus}`);
|
||||
} catch (error) {
|
||||
logger.error('模拟设备变化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务状态
|
||||
* @returns {Object} 服务状态
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
lastUpdateTimes: this.lastUpdateTimes,
|
||||
updateInterval: this.updateInterval,
|
||||
connectedClients: webSocketManager.getConnectionStats()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const realtimeService = new RealtimeService();
|
||||
|
||||
module.exports = realtimeService;
|
||||
@@ -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;
|
||||
8
insurance_admin-system/package-lock.json
generated
8
insurance_admin-system/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -298,24 +298,34 @@ const isModuleIndeterminate = (module) => {
|
||||
}
|
||||
|
||||
// 获取所有角色和权限数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
const loadData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const [rolesResponse, permissionsResponse] = await Promise.all([
|
||||
rolePermissionAPI.getAllRolesWithPermissions(),
|
||||
rolePermissionAPI.getAllPermissions()
|
||||
])
|
||||
|
||||
if (rolesResponse.success) {
|
||||
roles.value = rolesResponse.data
|
||||
// 处理角色数据
|
||||
if (rolesResponse.data && rolesResponse.data.status === 'success') {
|
||||
roles.value = rolesResponse.data.data.roles || []
|
||||
console.log('加载角色数据成功:', roles.value.length, '个角色')
|
||||
} else {
|
||||
console.error('角色数据响应格式错误:', rolesResponse)
|
||||
message.error('加载角色数据失败: 响应格式错误')
|
||||
}
|
||||
|
||||
if (permissionsResponse.success) {
|
||||
permissions.value = permissionsResponse.data
|
||||
// 处理权限数据
|
||||
if (permissionsResponse.data && permissionsResponse.data.status === 'success') {
|
||||
permissions.value = permissionsResponse.data.data || []
|
||||
console.log('加载权限数据成功:', permissions.value.length, '个权限')
|
||||
} else {
|
||||
console.error('权限数据响应格式错误:', permissionsResponse)
|
||||
message.error('加载权限数据失败: 响应格式错误')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
message.error('获取数据失败')
|
||||
console.error('加载数据失败:', error)
|
||||
message.error('加载数据失败: ' + error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -325,26 +335,47 @@ const fetchData = async () => {
|
||||
const handleRoleChange = async (roleId) => {
|
||||
if (!roleId) return
|
||||
|
||||
console.log('=== 角色变化处理开始 ===');
|
||||
console.log('选择的角色ID:', roleId);
|
||||
|
||||
try {
|
||||
const response = await rolePermissionAPI.getRolePermissions(roleId)
|
||||
if (response.success) {
|
||||
// 重置选择状态
|
||||
console.log('获取角色权限API响应:', JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.data && response.data.status === 'success') {
|
||||
// 清空之前的选择
|
||||
console.log('清空前的权限状态:', JSON.stringify(selectedPermissions, null, 2));
|
||||
Object.keys(selectedPermissions).forEach(key => {
|
||||
selectedPermissions[key] = false
|
||||
})
|
||||
console.log('清空后的权限状态:', JSON.stringify(selectedPermissions, null, 2));
|
||||
|
||||
// 设置当前角色的权限
|
||||
response.data.permissions.forEach(permission => {
|
||||
selectedPermissions[permission.id] = true
|
||||
})
|
||||
if (response.data.data && response.data.data.allPermissions) {
|
||||
const assignedPermissions = response.data.data.allPermissions.filter(p => p.assigned);
|
||||
console.log('角色已分配的权限:', assignedPermissions.length, '个');
|
||||
console.log('已分配权限详情:', assignedPermissions.map(p => ({ id: p.id, name: p.name, code: p.code })));
|
||||
|
||||
assignedPermissions.forEach(permission => {
|
||||
selectedPermissions[permission.id] = true
|
||||
})
|
||||
|
||||
console.log('设置权限后的状态:', JSON.stringify(selectedPermissions, null, 2));
|
||||
}
|
||||
|
||||
// 默认展开所有模块
|
||||
// 展开所有模块
|
||||
activeModules.value = permissionModules.value.map(m => m.module)
|
||||
console.log('展开的模块:', activeModules.value);
|
||||
} else {
|
||||
console.error('获取角色权限详情失败:', response)
|
||||
message.error('获取角色权限详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取角色权限失败:', error)
|
||||
message.error('获取角色权限失败')
|
||||
}
|
||||
|
||||
console.log('=== 角色变化处理结束 ===');
|
||||
}
|
||||
|
||||
// 权限变化处理
|
||||
@@ -364,9 +395,17 @@ const handleModuleCheckChange = (e, module) => {
|
||||
|
||||
// 全选处理
|
||||
const handleSelectAll = () => {
|
||||
console.log('=== 全选操作开始 ===');
|
||||
console.log('全选前的权限状态:', JSON.stringify(selectedPermissions, null, 2));
|
||||
console.log('当前权限列表长度:', permissions.value.length);
|
||||
|
||||
permissions.value.forEach(permission => {
|
||||
selectedPermissions[permission.id] = true
|
||||
})
|
||||
|
||||
console.log('全选后的权限状态:', JSON.stringify(selectedPermissions, null, 2));
|
||||
console.log('全选操作完成,选中权限数量:', Object.values(selectedPermissions).filter(Boolean).length);
|
||||
console.log('=== 全选操作结束 ===');
|
||||
}
|
||||
|
||||
// 全不选处理
|
||||
@@ -405,27 +444,43 @@ const handleSavePermissions = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('=== 保存权限设置开始 ===');
|
||||
console.log('当前选择的角色ID:', selectedRoleId.value);
|
||||
console.log('保存前的权限状态:', JSON.stringify(selectedPermissions, null, 2));
|
||||
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const permissionIds = Object.keys(selectedPermissions)
|
||||
.filter(id => selectedPermissions[id])
|
||||
.map(id => parseInt(id))
|
||||
|
||||
const response = await rolePermissionAPI.assignRolePermissions(selectedRoleId.value, {
|
||||
console.log('准备保存的权限ID列表:', permissionIds);
|
||||
console.log('权限ID数量:', permissionIds.length);
|
||||
|
||||
const requestData = {
|
||||
permissionIds,
|
||||
mode: 'replace'
|
||||
})
|
||||
};
|
||||
console.log('发送的请求数据:', JSON.stringify(requestData, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
const response = await rolePermissionAPI.assignRolePermissions(selectedRoleId.value, requestData)
|
||||
|
||||
console.log('保存权限API响应:', JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.data && response.data.status === 'success') {
|
||||
message.success('权限设置保存成功')
|
||||
console.log('✅ 权限保存成功');
|
||||
} else {
|
||||
message.error(response.message || '保存失败')
|
||||
console.error('❌ 保存权限设置失败:', response)
|
||||
message.error('保存权限设置失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存权限设置失败:', error)
|
||||
console.error('❌ 保存权限设置异常:', error)
|
||||
console.error('错误详情:', error.response?.data || error.message)
|
||||
message.error('保存权限设置失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
console.log('=== 保存权限设置结束 ===');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,13 +505,13 @@ const handleConfirmCopy = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await rolePermissionAPI.copyRolePermissions(
|
||||
copyForm.sourceRoleId,
|
||||
copyForm.targetRoleId,
|
||||
copyForm.mode
|
||||
)
|
||||
const response = await rolePermissionAPI.copyRolePermissions({
|
||||
sourceRoleId: copyForm.sourceRoleId,
|
||||
targetRoleId: copyForm.targetRoleId,
|
||||
mode: copyForm.mode
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
if (response.data && response.data.status === 'success') {
|
||||
message.success('权限复制成功')
|
||||
copyModalVisible.value = false
|
||||
|
||||
@@ -465,7 +520,8 @@ const handleConfirmCopy = async () => {
|
||||
handleRoleChange(selectedRoleId.value)
|
||||
}
|
||||
} else {
|
||||
message.error(response.message || '复制失败')
|
||||
console.error('复制权限失败:', response)
|
||||
message.error('复制权限失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('复制权限失败:', error)
|
||||
@@ -475,7 +531,7 @@ const handleConfirmCopy = async () => {
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = () => {
|
||||
fetchData()
|
||||
loadData()
|
||||
if (selectedRoleId.value) {
|
||||
handleRoleChange(selectedRoleId.value)
|
||||
}
|
||||
@@ -492,7 +548,7 @@ watch(permissions, (newPermissions) => {
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<a-range-picker
|
||||
v-model:value="searchForm.dateRange"
|
||||
style="width: 100%"
|
||||
placeholder="['开始日期', '结束日期']"
|
||||
:placeholder="['开始日期', '结束日期']"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
|
||||
@@ -209,25 +209,33 @@ const loadDashboardData = async () => {
|
||||
try {
|
||||
// 获取统计信息
|
||||
const statsResponse = await dashboardAPI.getStats()
|
||||
if (statsResponse.status === 'success') {
|
||||
console.log('统计数据响应:', statsResponse)
|
||||
if (statsResponse.data && statsResponse.data.status === 'success') {
|
||||
stats.value = {
|
||||
totalUsers: statsResponse.data.totalUsers || 0,
|
||||
totalApplications: statsResponse.data.totalApplications || 0,
|
||||
totalPolicies: statsResponse.data.totalPolicies || 0,
|
||||
totalClaims: statsResponse.data.totalClaims || 0
|
||||
totalUsers: statsResponse.data.data.totalUsers || 0,
|
||||
totalApplications: statsResponse.data.data.totalApplications || 0,
|
||||
totalPolicies: statsResponse.data.data.totalPolicies || 0,
|
||||
totalClaims: statsResponse.data.data.totalClaims || 0
|
||||
}
|
||||
console.log('统计数据设置成功:', stats.value)
|
||||
} else {
|
||||
console.log('统计数据响应格式错误:', statsResponse)
|
||||
}
|
||||
|
||||
// 获取最近活动
|
||||
const activitiesResponse = await dashboardAPI.getRecentActivities()
|
||||
if (activitiesResponse.status === 'success') {
|
||||
recentActivities.value = activitiesResponse.data.map(activity => ({
|
||||
console.log('最近活动响应:', activitiesResponse)
|
||||
if (activitiesResponse.data && activitiesResponse.data.status === 'success') {
|
||||
recentActivities.value = activitiesResponse.data.data.map(activity => ({
|
||||
id: activity.id,
|
||||
type: getLogType(activity.action),
|
||||
title: getLogTitle('info', activity.action),
|
||||
description: activity.action,
|
||||
created_at: activity.createdAt
|
||||
type: getLogType(activity.type),
|
||||
title: activity.title,
|
||||
description: activity.description,
|
||||
created_at: activity.timestamp || new Date().toISOString()
|
||||
})) || []
|
||||
console.log('最近活动设置成功:', recentActivities.value)
|
||||
} else {
|
||||
console.log('最近活动响应格式错误:', activitiesResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载仪表板数据失败:', error)
|
||||
@@ -281,68 +289,80 @@ const loadDashboardData = async () => {
|
||||
|
||||
// 加载图表数据
|
||||
const loadChartData = async () => {
|
||||
console.log('开始加载图表数据...')
|
||||
chartLoading.value = true
|
||||
try {
|
||||
// 获取申请趋势数据
|
||||
console.log('正在获取申请趋势数据...')
|
||||
const applicationTrendResponse = await dashboardAPI.getChartData({
|
||||
type: 'applications',
|
||||
period: '7d'
|
||||
})
|
||||
|
||||
if (applicationTrendResponse.status === 'success') {
|
||||
setupApplicationTrendChart(applicationTrendResponse.data)
|
||||
console.log('申请趋势响应:', applicationTrendResponse)
|
||||
if (applicationTrendResponse.data && applicationTrendResponse.data.status === 'success') {
|
||||
console.log('设置申请趋势图表,数据:', applicationTrendResponse.data.data)
|
||||
setupApplicationTrendChart(applicationTrendResponse.data.data)
|
||||
} else {
|
||||
console.log('申请趋势响应格式错误:', applicationTrendResponse)
|
||||
}
|
||||
|
||||
// 获取保单分布数据
|
||||
// 获取保单状态分布数据
|
||||
console.log('正在获取保单状态分布数据...')
|
||||
const policyDistributionResponse = await dashboardAPI.getChartData({
|
||||
type: 'policies',
|
||||
period: '30d'
|
||||
type: 'policy_status'
|
||||
})
|
||||
|
||||
if (policyDistributionResponse.status === 'success') {
|
||||
setupPolicyDistributionChart(policyDistributionResponse.data)
|
||||
console.log('保单状态分布响应:', policyDistributionResponse)
|
||||
if (policyDistributionResponse.data && policyDistributionResponse.data.status === 'success') {
|
||||
console.log('设置保单状态分布图表,数据:', policyDistributionResponse.data.data)
|
||||
setupPolicyDistributionChart(policyDistributionResponse.data.data)
|
||||
} else {
|
||||
console.log('保单状态分布响应格式错误:', policyDistributionResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载图表数据失败:', error)
|
||||
// 使用模拟数据
|
||||
setupApplicationTrendChart([
|
||||
{ date: '2024-01-01', count: 5 },
|
||||
{ date: '2024-01-02', count: 8 },
|
||||
{ date: '2024-01-03', count: 12 },
|
||||
{ date: '2024-01-04', count: 7 },
|
||||
{ date: '2024-01-05', count: 15 },
|
||||
{ date: '2024-01-06', count: 10 },
|
||||
{ date: '2024-01-07', count: 18 }
|
||||
])
|
||||
|
||||
setupPolicyDistributionChart([
|
||||
{ date: '2024-01-01', count: 3 },
|
||||
{ date: '2024-01-02', count: 5 },
|
||||
{ date: '2024-01-03', count: 8 },
|
||||
{ date: '2024-01-04', count: 4 },
|
||||
{ date: '2024-01-05', count: 12 }
|
||||
])
|
||||
console.error('错误详情:', error.response?.data || error.message)
|
||||
message.error('加载图表数据失败')
|
||||
} finally {
|
||||
chartLoading.value = false
|
||||
console.log('图表数据加载完成')
|
||||
}
|
||||
}
|
||||
|
||||
// 设置申请趋势图表
|
||||
const setupApplicationTrendChart = (data) => {
|
||||
console.log('设置申请趋势图表,接收数据:', data)
|
||||
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||
console.warn('申请趋势数据为空或格式错误')
|
||||
return
|
||||
}
|
||||
|
||||
const dates = data.map(item => item.date)
|
||||
const counts = data.map(item => item.count)
|
||||
const counts = data.map(item => item.value || item.count)
|
||||
console.log('处理后的数据 - 日期:', dates, '数量:', counts)
|
||||
|
||||
applicationTrendOption.value = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: '#ccc',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
@@ -350,15 +370,29 @@ const setupApplicationTrendChart = (data) => {
|
||||
data: dates,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#8c8c8c'
|
||||
color: '#d9d9d9'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
alignWithLabel: true
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#8c8c8c'
|
||||
color: '#d9d9d9'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#666'
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -367,12 +401,25 @@ const setupApplicationTrendChart = (data) => {
|
||||
name: '申请数量',
|
||||
type: 'bar',
|
||||
data: counts,
|
||||
barWidth: '60%',
|
||||
itemStyle: {
|
||||
color: '#1890ff'
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: '#1890ff'
|
||||
}, {
|
||||
offset: 1, color: '#40a9ff'
|
||||
}]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: '#40a9ff'
|
||||
color: '#0050b3'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,27 +427,47 @@ const setupApplicationTrendChart = (data) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置保单分布图表
|
||||
// 设置保单状态分布图表
|
||||
const setupPolicyDistributionChart = (data) => {
|
||||
console.log('设置保单状态分布图表,接收数据:', data)
|
||||
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||
console.warn('保单状态分布数据为空或格式错误')
|
||||
return
|
||||
}
|
||||
|
||||
const chartData = data.map(item => ({
|
||||
name: item.date,
|
||||
name: getPolicyStatusLabel(item.status),
|
||||
value: item.count
|
||||
}))
|
||||
console.log('处理后的图表数据:', chartData)
|
||||
|
||||
policyDistributionOption.value = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: '#ccc',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
left: 'left',
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
color: '#666',
|
||||
fontSize: 12
|
||||
},
|
||||
itemGap: 15
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '保单数量',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
radius: ['45%', '75%'],
|
||||
center: ['65%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
@@ -409,8 +476,14 @@ const setupPolicyDistributionChart = (data) => {
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
fontSize: '16',
|
||||
fontWeight: 'bold',
|
||||
color: '#333'
|
||||
},
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
@@ -418,17 +491,56 @@ const setupPolicyDistributionChart = (data) => {
|
||||
},
|
||||
data: chartData,
|
||||
itemStyle: {
|
||||
color: function(params) {
|
||||
const colors = ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1']
|
||||
return colors[params.dataIndex % colors.length]
|
||||
}
|
||||
borderRadius: 8,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 获取保单状态标签
|
||||
const getPolicyStatusLabel = (status) => {
|
||||
const statusMap = {
|
||||
'active': '有效',
|
||||
'expired': '已过期',
|
||||
'cancelled': '已取消',
|
||||
'suspended': '暂停'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('Dashboard组件已挂载')
|
||||
console.log('图表选项初始状态:', {
|
||||
applicationTrendOption: applicationTrendOption.value,
|
||||
policyDistributionOption: policyDistributionOption.value
|
||||
})
|
||||
|
||||
// 设置测试图表数据
|
||||
console.log('设置测试图表数据...')
|
||||
applicationTrendOption.value = {
|
||||
title: { text: '测试图表' },
|
||||
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
|
||||
yAxis: { type: 'value' },
|
||||
series: [{ data: [120, 200, 150], type: 'bar' }]
|
||||
}
|
||||
|
||||
policyDistributionOption.value = {
|
||||
title: { text: '测试饼图' },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
data: [
|
||||
{ value: 1048, name: '搜索引擎' },
|
||||
{ value: 735, name: '直接访问' },
|
||||
{ value: 580, name: '邮件营销' }
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
||||
console.log('测试图表设置完成')
|
||||
|
||||
loadDashboardData()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, getCurrentInstance, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { dataWarehouseAPI } from '@/utils/api';
|
||||
@@ -192,13 +192,20 @@ const fetchTypeDistribution = async () => {
|
||||
const response = await dataWarehouseAPI.getInsuranceTypeDistribution();
|
||||
console.log('保险类型分布响应:', response);
|
||||
if (response.data && response.data.success) {
|
||||
renderTypeDistributionChart(response.data.data);
|
||||
console.log('保险类型分布数据:', response.data.data);
|
||||
await nextTick(); // 确保DOM更新
|
||||
ensureChartReady(typeDistributionChart, () => renderTypeDistributionChart(response.data.data));
|
||||
} else {
|
||||
console.log('保险类型分布响应格式错误:', response);
|
||||
message.error('获取保险类型分布数据失败');
|
||||
await nextTick();
|
||||
ensureChartReady(typeDistributionChart, () => renderTypeDistributionChart([]));
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取保险类型分布数据失败');
|
||||
console.error('获取保险类型分布数据错误:', error);
|
||||
await nextTick();
|
||||
ensureChartReady(typeDistributionChart, () => renderTypeDistributionChart([]));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -208,8 +215,11 @@ const fetchStatusDistribution = async () => {
|
||||
const response = await dataWarehouseAPI.getApplicationStatusDistribution();
|
||||
console.log('申请状态分布响应:', response);
|
||||
if (response.data && response.data.success) {
|
||||
console.log('申请状态分布数据:', response.data.data);
|
||||
await nextTick(); // 确保DOM更新
|
||||
renderStatusDistributionChart(response.data.data);
|
||||
} else {
|
||||
console.log('申请状态分布响应格式错误:', response);
|
||||
message.error('获取申请状态分布数据失败');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -224,8 +234,10 @@ const fetchTrendData = async () => {
|
||||
const response = await dataWarehouseAPI.getTrendData();
|
||||
console.log('趋势数据响应:', response);
|
||||
if (response.data && response.data.success) {
|
||||
console.log('趋势数据:', response.data.data);
|
||||
renderTrendChart(response.data.data);
|
||||
} else {
|
||||
console.log('趋势数据响应格式错误:', response);
|
||||
message.error('获取趋势数据失败');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -240,9 +252,10 @@ const fetchClaimStats = async () => {
|
||||
const response = await dataWarehouseAPI.getClaimStats();
|
||||
console.log('赔付统计响应:', response);
|
||||
if (response && response.data && response.data.success) {
|
||||
console.log('赔付统计数据:', response.data.data);
|
||||
renderClaimStatsChart(response.data.data || { statusDistribution: [], monthlyTrend: [] });
|
||||
} else {
|
||||
console.warn('赔付统计数据响应格式不正确:', response);
|
||||
console.log('赔付统计数据响应格式错误:', response);
|
||||
message.error('获取赔付统计数据失败');
|
||||
// 使用空数据渲染图表,避免显示错误
|
||||
renderClaimStatsChart({ statusDistribution: [], monthlyTrend: [] });
|
||||
@@ -272,6 +285,18 @@ const refreshData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 确保图表容器准备就绪后初始化
|
||||
const ensureChartReady = (chartRef, initCallback) => {
|
||||
const checkReady = () => {
|
||||
if (chartRef.value && chartRef.value.clientWidth > 0 && chartRef.value.clientHeight > 0) {
|
||||
initCallback();
|
||||
} else {
|
||||
setTimeout(checkReady, 50);
|
||||
}
|
||||
};
|
||||
checkReady();
|
||||
};
|
||||
|
||||
// 处理日期范围变化
|
||||
const handleDateChange = (dates) => {
|
||||
dateRange.value = dates;
|
||||
@@ -281,11 +306,40 @@ const handleDateChange = (dates) => {
|
||||
|
||||
// 渲染保险类型分布图表
|
||||
const renderTypeDistributionChart = (data) => {
|
||||
if (!typeChartInstance) {
|
||||
typeChartInstance = echarts.init(typeDistributionChart.value);
|
||||
console.log('=== 开始渲染保险类型分布图表 ===');
|
||||
console.log('接收到的数据:', data);
|
||||
console.log('数据类型:', typeof data);
|
||||
console.log('是否为数组:', Array.isArray(data));
|
||||
console.log('数据长度:', data ? data.length : 'N/A');
|
||||
|
||||
// 检查DOM元素是否存在
|
||||
if (!typeDistributionChart.value) {
|
||||
console.log('DOM元素不存在,延迟渲染');
|
||||
setTimeout(() => renderTypeDistributionChart(data), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('保险类型分布数据:', data);
|
||||
console.log('DOM元素存在,开始渲染。尺寸信息:', {
|
||||
clientWidth: typeDistributionChart.value.clientWidth,
|
||||
clientHeight: typeDistributionChart.value.clientHeight,
|
||||
offsetWidth: typeDistributionChart.value.offsetWidth,
|
||||
offsetHeight: typeDistributionChart.value.offsetHeight
|
||||
});
|
||||
|
||||
console.log('DOM元素已准备好,开始初始化图表');
|
||||
|
||||
if (!typeChartInstance) {
|
||||
typeChartInstance = echarts.init(typeDistributionChart.value);
|
||||
console.log('图表实例已创建');
|
||||
}
|
||||
|
||||
// 验证数据格式
|
||||
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||
console.warn('保险类型分布数据为空或格式错误:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('保险类型分布数据验证通过:', data);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
@@ -318,16 +372,46 @@ const renderTypeDistributionChart = (data) => {
|
||||
]
|
||||
};
|
||||
|
||||
console.log('设置图表选项:', option);
|
||||
typeChartInstance.setOption(option);
|
||||
console.log('保险类型分布图表渲染完成');
|
||||
};
|
||||
|
||||
// 渲染申请状态分布图表
|
||||
const renderStatusDistributionChart = (data) => {
|
||||
if (!statusChartInstance) {
|
||||
statusChartInstance = echarts.init(statusDistributionChart.value);
|
||||
console.log('=== 开始渲染申请状态分布图表 ===');
|
||||
console.log('接收到的数据:', data);
|
||||
console.log('数据类型:', typeof data);
|
||||
console.log('是否为数组:', Array.isArray(data));
|
||||
console.log('数据长度:', data ? data.length : 'N/A');
|
||||
|
||||
// 检查DOM元素是否存在且有尺寸
|
||||
if (!statusDistributionChart.value || statusDistributionChart.value.clientWidth === 0 || statusDistributionChart.value.clientHeight === 0) {
|
||||
console.log('DOM元素未准备好,延迟渲染。当前状态:', {
|
||||
element: statusDistributionChart.value,
|
||||
clientWidth: statusDistributionChart.value?.clientWidth,
|
||||
clientHeight: statusDistributionChart.value?.clientHeight,
|
||||
offsetWidth: statusDistributionChart.value?.offsetWidth,
|
||||
offsetHeight: statusDistributionChart.value?.offsetHeight
|
||||
});
|
||||
setTimeout(() => renderStatusDistributionChart(data), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('申请状态分布数据:', data);
|
||||
console.log('DOM元素已准备好,开始初始化图表');
|
||||
|
||||
if (!statusChartInstance) {
|
||||
statusChartInstance = echarts.init(statusDistributionChart.value);
|
||||
console.log('图表实例已创建');
|
||||
}
|
||||
|
||||
// 验证数据格式
|
||||
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||
console.warn('申请状态分布数据为空或格式错误:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('申请状态分布数据验证通过:', data);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
@@ -360,11 +444,19 @@ const renderStatusDistributionChart = (data) => {
|
||||
]
|
||||
};
|
||||
|
||||
console.log('设置图表选项:', option);
|
||||
statusChartInstance.setOption(option);
|
||||
console.log('申请状态分布图表渲染完成');
|
||||
};
|
||||
|
||||
// 渲染趋势图表
|
||||
const renderTrendChart = (data) => {
|
||||
// 检查DOM元素是否存在且有尺寸
|
||||
if (!trendChart.value || trendChart.value.clientWidth === 0 || trendChart.value.clientHeight === 0) {
|
||||
setTimeout(() => renderTrendChart(data), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trendChartInstance) {
|
||||
trendChartInstance = echarts.init(trendChart.value);
|
||||
}
|
||||
@@ -465,6 +557,12 @@ const renderTrendChart = (data) => {
|
||||
|
||||
// 渲染赔付统计图表
|
||||
const renderClaimStatsChart = (data) => {
|
||||
// 检查DOM元素是否存在且有尺寸
|
||||
if (!claimStatsChart.value || claimStatsChart.value.clientWidth === 0 || claimStatsChart.value.clientHeight === 0) {
|
||||
setTimeout(() => renderClaimStatsChart(data), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!claimChartInstance) {
|
||||
claimChartInstance = echarts.init(claimStatsChart.value);
|
||||
}
|
||||
@@ -539,7 +637,30 @@ const handleResize = () => {
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
refreshData();
|
||||
console.log('=== DataWarehouse组件已挂载 ===');
|
||||
console.log('图表容器引用:', {
|
||||
typeDistributionChart: typeDistributionChart.value,
|
||||
statusDistributionChart: statusDistributionChart.value,
|
||||
trendChart: trendChart.value,
|
||||
claimStatsChart: claimStatsChart.value
|
||||
});
|
||||
|
||||
// 延迟一点时间确保DOM完全渲染
|
||||
setTimeout(() => {
|
||||
console.log('延迟检查图表容器尺寸:', {
|
||||
typeDistributionChart: typeDistributionChart.value ? {
|
||||
clientWidth: typeDistributionChart.value.clientWidth,
|
||||
clientHeight: typeDistributionChart.value.clientHeight
|
||||
} : 'null',
|
||||
statusDistributionChart: statusDistributionChart.value ? {
|
||||
clientWidth: statusDistributionChart.value.clientWidth,
|
||||
clientHeight: statusDistributionChart.value.clientHeight
|
||||
} : 'null'
|
||||
});
|
||||
|
||||
refreshData();
|
||||
}, 100);
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
@@ -626,6 +747,8 @@ onUnmounted(() => {
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
min-height: 300px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
|
||||
@@ -239,12 +239,15 @@ const fetchInstallationTasks = async () => {
|
||||
};
|
||||
|
||||
const response = await installationTaskApi.getInstallationTasks(params);
|
||||
console.log('安装任务API响应:', response);
|
||||
|
||||
if (response.code === 200) {
|
||||
tableData.value = response.data.rows || response.data.list || [];
|
||||
pagination.total = response.data.total || 0;
|
||||
if (response.data && response.data.status === 'success') {
|
||||
tableData.value = response.data.data.list || [];
|
||||
pagination.total = response.data.data.total || 0;
|
||||
console.log('安装任务数据设置成功:', tableData.value.length, '条');
|
||||
} else {
|
||||
message.error(response.message || '获取安装任务列表失败');
|
||||
console.log('安装任务响应格式错误:', response);
|
||||
message.error(response.data?.message || '获取安装任务列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取安装任务列表失败:', error);
|
||||
|
||||
@@ -365,12 +365,16 @@ const loadInsuranceTypes = async () => {
|
||||
}
|
||||
|
||||
const response = await insuranceTypeAPI.getList(params)
|
||||
console.log('险种管理API响应:', response)
|
||||
|
||||
if (response.status === 'success') {
|
||||
typeList.value = response.data.list
|
||||
pagination.total = response.data.total
|
||||
if (response.data && response.data.status === 'success') {
|
||||
// API返回的数据直接是数组格式
|
||||
typeList.value = response.data.data || []
|
||||
pagination.total = response.data.pagination?.total || 0
|
||||
console.log('险种管理数据设置成功:', typeList.value.length, '个')
|
||||
} else {
|
||||
message.error(response.message || '加载险种列表失败')
|
||||
console.log('险种管理响应格式错误:', response)
|
||||
message.error(response.data?.message || '加载险种列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载险种列表失败')
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<a-col :span="8">
|
||||
<a-range-picker
|
||||
v-model:value="searchForm.dateRange"
|
||||
placeholder="['开始日期', '结束日期']"
|
||||
:placeholder="['开始日期', '结束日期']"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-col>
|
||||
@@ -474,10 +474,21 @@ const fetchData = async () => {
|
||||
}
|
||||
delete params.dateRange
|
||||
|
||||
console.log('生资保单API请求参数:', params)
|
||||
const response = await livestockPolicyApi.getList(params)
|
||||
console.log('生资保单API响应:', response)
|
||||
console.log('响应数据类型:', typeof response)
|
||||
console.log('响应code:', response.code)
|
||||
console.log('响应data:', response.data)
|
||||
|
||||
if (response.code === 200) {
|
||||
tableData.value = response.data.list
|
||||
pagination.total = response.data.total
|
||||
// 后端返回的数据直接是数组格式,不是{list: [], total: 18}格式
|
||||
tableData.value = response.data || []
|
||||
pagination.total = response.pagination?.total || 0
|
||||
console.log('生资保单数据设置成功:', tableData.value.length, '条')
|
||||
} else {
|
||||
console.log('生资保单响应格式错误:', response)
|
||||
message.error('获取数据失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取数据失败')
|
||||
|
||||
@@ -107,9 +107,8 @@ const onFinish = async (values) => {
|
||||
userStore.setAuthData({
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
accessTokenExpiresAt: data.accessTokenExpiresAt,
|
||||
refreshTokenExpiresAt: data.refreshTokenExpiresAt,
|
||||
user: data.user || data.userInfo // 修复:用户信息在data.user中
|
||||
user: data.user || data.userInfo,
|
||||
expiresIn: data.accessTokenExpiresIn || 900 // 默认15分钟
|
||||
})
|
||||
} else if (data.token) {
|
||||
// 兼容旧的单Token格式
|
||||
|
||||
@@ -1,122 +1,247 @@
|
||||
<template>
|
||||
<div style="padding: 20px;">
|
||||
<h2>登录和API测试</h2>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h3>当前状态</h3>
|
||||
<p>Token: {{ userStore.token ? '已设置' : '未设置' }}</p>
|
||||
<p>用户信息: {{ JSON.stringify(userStore.userInfo) }}</p>
|
||||
</div>
|
||||
<div class="login-test">
|
||||
<a-card title="登录测试" style="margin: 20px;">
|
||||
<a-space direction="vertical" style="width: 100%;">
|
||||
<div>
|
||||
<h3>快速登录测试</h3>
|
||||
<a-form :model="loginForm" @finish="handleLogin">
|
||||
<a-form-item>
|
||||
<a-input v-model:value="loginForm.username" placeholder="用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password v-model:value="loginForm.password" placeholder="密码" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit" :loading="loginLoading" block>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h3>快速登录</h3>
|
||||
<a-button type="primary" @click="quickLogin" :loading="loginLoading">
|
||||
使用 admin/123456 登录
|
||||
</a-button>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Token状态检查</h3>
|
||||
<a-descriptions bordered :column="1">
|
||||
<a-descriptions-item label="Store中的Access Token">
|
||||
<span v-if="userStore.accessToken">
|
||||
{{ userStore.accessToken.substring(0, 50) }}...
|
||||
<br>
|
||||
<small>长度: {{ userStore.accessToken.length }}</small>
|
||||
</span>
|
||||
<span v-else style="color: red;">未找到</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="LocalStorage中的Access Token">
|
||||
<span v-if="localStorageToken">
|
||||
{{ localStorageToken.substring(0, 50) }}...
|
||||
<br>
|
||||
<small>长度: {{ localStorageToken.length }}</small>
|
||||
</span>
|
||||
<span v-else style="color: red;">未找到</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="是否已登录">
|
||||
<span :style="{ color: userStore.isLoggedIn ? 'green' : 'red' }">
|
||||
{{ userStore.isLoggedIn ? '是' : '否' }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="Token是否过期">
|
||||
<span :style="{ color: userStore.isTokenExpired ? 'red' : 'green' }">
|
||||
{{ userStore.isTokenExpired ? '是' : '否' }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h3>测试API调用</h3>
|
||||
<a-button @click="testStatsAPI" :loading="statsLoading" style="margin-right: 10px;">
|
||||
测试系统统计API
|
||||
</a-button>
|
||||
<a-button @click="testAuthAPI" :loading="authLoading">
|
||||
测试认证API
|
||||
</a-button>
|
||||
</div>
|
||||
<div>
|
||||
<h3>API测试</h3>
|
||||
<a-space>
|
||||
<a-button @click="testPermissionsAPI" :loading="permissionsLoading">
|
||||
测试权限API
|
||||
</a-button>
|
||||
<a-button @click="testRolesAPI" :loading="rolesLoading">
|
||||
测试角色API
|
||||
</a-button>
|
||||
<a-button @click="refreshStatus">
|
||||
刷新状态
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div v-if="apiResults.length">
|
||||
<h3>API调用结果</h3>
|
||||
<div v-for="(result, index) in apiResults" :key="index" style="margin-bottom: 10px; padding: 10px; border: 1px solid #ddd;">
|
||||
<strong>{{ result.name }}:</strong>
|
||||
<pre>{{ result.data }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="testResults.length > 0">
|
||||
<h3>测试结果</h3>
|
||||
<a-list :data-source="testResults" bordered>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<span :style="{ color: item.success ? 'green' : 'red' }">
|
||||
{{ item.name }} - {{ item.success ? '成功' : '失败' }}
|
||||
</span>
|
||||
</template>
|
||||
<template #description>
|
||||
<div>
|
||||
<p><strong>状态:</strong> {{ item.status }}</p>
|
||||
<p><strong>响应:</strong></p>
|
||||
<pre style="background: #f5f5f5; padding: 8px; border-radius: 4px; font-size: 12px;">{{ item.response }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { authAPI, dashboardAPI } from '@/utils/api'
|
||||
import { authAPI, rolePermissionAPI } from '@/utils/api'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const loginLoading = ref(false)
|
||||
const statsLoading = ref(false)
|
||||
const authLoading = ref(false)
|
||||
const apiResults = ref([])
|
||||
const permissionsLoading = ref(false)
|
||||
const rolesLoading = ref(false)
|
||||
const testResults = ref([])
|
||||
|
||||
const quickLogin = async () => {
|
||||
const loginForm = reactive({
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
})
|
||||
|
||||
const localStorageToken = ref('')
|
||||
|
||||
const refreshStatus = () => {
|
||||
localStorageToken.value = localStorage.getItem('accessToken') || ''
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
loginLoading.value = true
|
||||
try {
|
||||
const response = await authAPI.login({
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
})
|
||||
const response = await authAPI.login(loginForm)
|
||||
|
||||
if (response.status === 'success') {
|
||||
userStore.setToken(response.data.token)
|
||||
userStore.setUserInfo(response.data.user)
|
||||
message.success('登录成功!')
|
||||
if (response.data && (response.data.status === 'success' || response.data.code === 200)) {
|
||||
const data = response.data.data
|
||||
|
||||
apiResults.value.unshift({
|
||||
name: '登录API',
|
||||
data: JSON.stringify(response, null, 2)
|
||||
})
|
||||
if (data.accessToken && data.refreshToken) {
|
||||
userStore.setAuthData({
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
user: data.user || data.userInfo,
|
||||
expiresIn: data.accessTokenExpiresIn || 900
|
||||
})
|
||||
|
||||
testResults.value.unshift({
|
||||
name: '登录测试',
|
||||
success: true,
|
||||
status: '成功',
|
||||
response: JSON.stringify({
|
||||
message: '登录成功',
|
||||
tokenLength: data.accessToken.length,
|
||||
user: data.user?.username
|
||||
}, null, 2)
|
||||
})
|
||||
|
||||
message.success('登录成功')
|
||||
refreshStatus()
|
||||
} else {
|
||||
throw new Error('响应数据格式不正确:缺少token信息')
|
||||
}
|
||||
} else {
|
||||
message.error(response.message || '登录失败')
|
||||
throw new Error(response.data?.message || '登录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.response?.data?.message || '登录失败')
|
||||
apiResults.value.unshift({
|
||||
name: '登录API (错误)',
|
||||
data: JSON.stringify(error.response?.data || error.message, null, 2)
|
||||
testResults.value.unshift({
|
||||
name: '登录测试',
|
||||
success: false,
|
||||
status: '失败',
|
||||
response: JSON.stringify(error.response?.data || error.message, null, 2)
|
||||
})
|
||||
|
||||
message.error('登录失败: ' + (error.response?.data?.message || error.message))
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testStatsAPI = async () => {
|
||||
statsLoading.value = true
|
||||
const testPermissionsAPI = async () => {
|
||||
permissionsLoading.value = true
|
||||
try {
|
||||
const response = await dashboardAPI.getStats()
|
||||
message.success('系统统计API调用成功!')
|
||||
const response = await rolePermissionAPI.getAllPermissions()
|
||||
|
||||
apiResults.value.unshift({
|
||||
name: '系统统计API',
|
||||
data: JSON.stringify(response, null, 2)
|
||||
testResults.value.unshift({
|
||||
name: '权限API测试',
|
||||
success: response.success,
|
||||
status: response.success ? '成功' : '失败',
|
||||
response: JSON.stringify(response, null, 2)
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
message.success('权限API测试成功')
|
||||
} else {
|
||||
message.error('权限API测试失败: ' + response.error)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('系统统计API调用失败')
|
||||
apiResults.value.unshift({
|
||||
name: '系统统计API (错误)',
|
||||
data: JSON.stringify(error.response?.data || error.message, null, 2)
|
||||
testResults.value.unshift({
|
||||
name: '权限API测试',
|
||||
success: false,
|
||||
status: '失败',
|
||||
response: JSON.stringify(error.message, null, 2)
|
||||
})
|
||||
|
||||
message.error('权限API测试失败: ' + error.message)
|
||||
} finally {
|
||||
statsLoading.value = false
|
||||
permissionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testAuthAPI = async () => {
|
||||
authLoading.value = true
|
||||
const testRolesAPI = async () => {
|
||||
rolesLoading.value = true
|
||||
try {
|
||||
const response = await authAPI.getProfile()
|
||||
message.success('认证API调用成功!')
|
||||
const response = await rolePermissionAPI.getAllRolesWithPermissions()
|
||||
|
||||
apiResults.value.unshift({
|
||||
name: '用户资料API',
|
||||
data: JSON.stringify(response, null, 2)
|
||||
testResults.value.unshift({
|
||||
name: '角色API测试',
|
||||
success: response.success,
|
||||
status: response.success ? '成功' : '失败',
|
||||
response: JSON.stringify(response, null, 2)
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
message.success('角色API测试成功')
|
||||
} else {
|
||||
message.error('角色API测试失败: ' + response.error)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('认证API调用失败')
|
||||
apiResults.value.unshift({
|
||||
name: '用户资料API (错误)',
|
||||
data: JSON.stringify(error.response?.data || error.message, null, 2)
|
||||
testResults.value.unshift({
|
||||
name: '角色API测试',
|
||||
success: false,
|
||||
status: '失败',
|
||||
response: JSON.stringify(error.message, null, 2)
|
||||
})
|
||||
|
||||
message.error('角色API测试失败: ' + error.message)
|
||||
} finally {
|
||||
authLoading.value = false
|
||||
rolesLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
onMounted(() => {
|
||||
refreshStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-test {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
260
insurance_admin-system/src/views/TokenDebug.vue
Normal file
260
insurance_admin-system/src/views/TokenDebug.vue
Normal file
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<div class="token-debug">
|
||||
<a-card title="Token调试信息" style="margin: 20px;">
|
||||
<a-space direction="vertical" style="width: 100%;">
|
||||
<div>
|
||||
<h3>LocalStorage中的Token信息:</h3>
|
||||
<a-descriptions bordered :column="1">
|
||||
<a-descriptions-item label="Access Token">
|
||||
<span v-if="tokenInfo.accessToken">
|
||||
{{ tokenInfo.accessToken.substring(0, 50) }}...
|
||||
<br>
|
||||
<small>长度: {{ tokenInfo.accessToken.length }}</small>
|
||||
</span>
|
||||
<span v-else style="color: red;">未找到</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="Refresh Token">
|
||||
<span v-if="tokenInfo.refreshToken">
|
||||
{{ tokenInfo.refreshToken.substring(0, 50) }}...
|
||||
<br>
|
||||
<small>长度: {{ tokenInfo.refreshToken.length }}</small>
|
||||
</span>
|
||||
<span v-else style="color: red;">未找到</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="User Info">
|
||||
<pre v-if="tokenInfo.userInfo">{{ JSON.stringify(tokenInfo.userInfo, null, 2) }}</pre>
|
||||
<span v-else style="color: red;">未找到</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="Token Expires At">
|
||||
<span v-if="tokenInfo.tokenExpiresAt">
|
||||
{{ new Date(tokenInfo.tokenExpiresAt).toLocaleString() }}
|
||||
<br>
|
||||
<small :style="{ color: tokenInfo.isExpired ? 'red' : 'green' }">
|
||||
{{ tokenInfo.isExpired ? '已过期' : '未过期' }}
|
||||
</small>
|
||||
</span>
|
||||
<span v-else style="color: red;">未找到</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Store中的Token信息:</h3>
|
||||
<a-descriptions bordered :column="1">
|
||||
<a-descriptions-item label="Access Token">
|
||||
<span v-if="userStore.accessToken">
|
||||
{{ userStore.accessToken.substring(0, 50) }}...
|
||||
<br>
|
||||
<small>长度: {{ userStore.accessToken.length }}</small>
|
||||
</span>
|
||||
<span v-else style="color: red;">未找到</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="Refresh Token">
|
||||
<span v-if="userStore.refreshToken">
|
||||
{{ userStore.refreshToken.substring(0, 50) }}...
|
||||
<br>
|
||||
<small>长度: {{ userStore.refreshToken.length }}</small>
|
||||
</span>
|
||||
<span v-else style="color: red;">未找到</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="User Info">
|
||||
<pre v-if="userStore.userInfo">{{ JSON.stringify(userStore.userInfo, null, 2) }}</pre>
|
||||
<span v-else style="color: red;">未找到</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="Is Logged In">
|
||||
<span :style="{ color: userStore.isLoggedIn ? 'green' : 'red' }">
|
||||
{{ userStore.isLoggedIn ? '是' : '否' }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="Is Token Expired">
|
||||
<span :style="{ color: userStore.isTokenExpired ? 'red' : 'green' }">
|
||||
{{ userStore.isTokenExpired ? '是' : '否' }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>API测试:</h3>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="testLogin" :loading="loginLoading">
|
||||
测试登录
|
||||
</a-button>
|
||||
<a-button @click="testPermissionsAPI" :loading="permissionsLoading">
|
||||
测试权限API
|
||||
</a-button>
|
||||
<a-button @click="testRolesAPI" :loading="rolesLoading">
|
||||
测试角色API
|
||||
</a-button>
|
||||
<a-button @click="refreshTokenInfo">
|
||||
刷新Token信息
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div v-if="apiTestResults.length > 0">
|
||||
<h3>API测试结果:</h3>
|
||||
<a-list :data-source="apiTestResults" bordered>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<span :style="{ color: item.success ? 'green' : 'red' }">
|
||||
{{ item.name }} - {{ item.success ? '成功' : '失败' }}
|
||||
</span>
|
||||
</template>
|
||||
<template #description>
|
||||
<div>
|
||||
<p><strong>状态码:</strong> {{ item.status }}</p>
|
||||
<p><strong>响应:</strong></p>
|
||||
<pre style="background: #f5f5f5; padding: 8px; border-radius: 4px; font-size: 12px;">{{ item.response }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { authAPI, rolePermissionAPI } from '@/utils/api'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const loginLoading = ref(false)
|
||||
const permissionsLoading = ref(false)
|
||||
const rolesLoading = ref(false)
|
||||
const apiTestResults = ref([])
|
||||
|
||||
const tokenInfo = reactive({
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
userInfo: null,
|
||||
tokenExpiresAt: null,
|
||||
isExpired: false
|
||||
})
|
||||
|
||||
const refreshTokenInfo = () => {
|
||||
tokenInfo.accessToken = localStorage.getItem('accessToken')
|
||||
tokenInfo.refreshToken = localStorage.getItem('refreshToken')
|
||||
tokenInfo.userInfo = JSON.parse(localStorage.getItem('userInfo') || 'null')
|
||||
tokenInfo.tokenExpiresAt = parseInt(localStorage.getItem('tokenExpiresAt') || '0')
|
||||
tokenInfo.isExpired = tokenInfo.tokenExpiresAt ? Date.now() >= tokenInfo.tokenExpiresAt : true
|
||||
}
|
||||
|
||||
const testLogin = async () => {
|
||||
loginLoading.value = true
|
||||
try {
|
||||
const response = await authAPI.login({
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
})
|
||||
|
||||
apiTestResults.value.unshift({
|
||||
name: '登录测试',
|
||||
success: true,
|
||||
status: response.status || '200',
|
||||
response: JSON.stringify(response.data, null, 2)
|
||||
})
|
||||
|
||||
message.success('登录测试成功')
|
||||
refreshTokenInfo()
|
||||
} catch (error) {
|
||||
apiTestResults.value.unshift({
|
||||
name: '登录测试',
|
||||
success: false,
|
||||
status: error.response?.status || 'Error',
|
||||
response: JSON.stringify(error.response?.data || error.message, null, 2)
|
||||
})
|
||||
|
||||
message.error('登录测试失败')
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testPermissionsAPI = async () => {
|
||||
permissionsLoading.value = true
|
||||
try {
|
||||
const response = await rolePermissionAPI.getAllPermissions()
|
||||
|
||||
apiTestResults.value.unshift({
|
||||
name: '权限API测试',
|
||||
success: response.success,
|
||||
status: response.success ? '200' : 'Error',
|
||||
response: JSON.stringify(response, null, 2)
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
message.success('权限API测试成功')
|
||||
} else {
|
||||
message.error('权限API测试失败')
|
||||
}
|
||||
} catch (error) {
|
||||
apiTestResults.value.unshift({
|
||||
name: '权限API测试',
|
||||
success: false,
|
||||
status: 'Error',
|
||||
response: JSON.stringify(error.message, null, 2)
|
||||
})
|
||||
|
||||
message.error('权限API测试失败')
|
||||
} finally {
|
||||
permissionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testRolesAPI = async () => {
|
||||
rolesLoading.value = true
|
||||
try {
|
||||
const response = await rolePermissionAPI.getAllRolesWithPermissions()
|
||||
|
||||
apiTestResults.value.unshift({
|
||||
name: '角色API测试',
|
||||
success: response.success,
|
||||
status: response.success ? '200' : 'Error',
|
||||
response: JSON.stringify(response, null, 2)
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
message.success('角色API测试成功')
|
||||
} else {
|
||||
message.error('角色API测试失败')
|
||||
}
|
||||
} catch (error) {
|
||||
apiTestResults.value.unshift({
|
||||
name: '角色API测试',
|
||||
success: false,
|
||||
status: 'Error',
|
||||
response: JSON.stringify(error.message, null, 2)
|
||||
})
|
||||
|
||||
message.error('角色API测试失败')
|
||||
} finally {
|
||||
rolesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshTokenInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.token-debug {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -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 {
|
||||
|
||||
39
insurance_admin-system/test-frontend-api.js
Normal file
39
insurance_admin-system/test-frontend-api.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// 测试前端API调用
|
||||
const testFrontendAPI = async () => {
|
||||
try {
|
||||
console.log('开始测试前端API调用...');
|
||||
|
||||
// 模拟前端API调用
|
||||
const response = await fetch('http://localhost:3000/api/policies?page=1&pageSize=10', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('API响应状态:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('API错误:', errorData);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('API响应数据:', data);
|
||||
|
||||
if (data.status === 'success' && data.data) {
|
||||
console.log('✅ API调用成功,返回', data.data.length, '条保单数据');
|
||||
console.log('第一条保单数据:', data.data[0]);
|
||||
} else {
|
||||
console.log('❌ API响应格式不正确');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ API调用失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 在浏览器控制台中运行
|
||||
console.log('请在浏览器控制台中运行: testFrontendAPI()');
|
||||
93
insurance_backend/check_db_structure.js
Normal file
93
insurance_backend/check_db_structure.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const { sequelize } = require('./config/database');
|
||||
|
||||
async function checkDatabaseStructure() {
|
||||
try {
|
||||
console.log('🔍 检查数据库表结构...\n');
|
||||
|
||||
// 检查数据库连接
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 数据库连接成功\n');
|
||||
|
||||
// 检查所有表
|
||||
const [tables] = await sequelize.query('SHOW TABLES');
|
||||
console.log('📋 数据库中的表:');
|
||||
tables.forEach(table => {
|
||||
const tableName = Object.values(table)[0];
|
||||
console.log(` - ${tableName}`);
|
||||
});
|
||||
|
||||
console.log('\n🏗️ 检查关键表结构:\n');
|
||||
|
||||
// 检查users表
|
||||
try {
|
||||
const [usersStructure] = await sequelize.query('DESCRIBE users');
|
||||
console.log('👥 users表结构:');
|
||||
usersStructure.forEach(column => {
|
||||
console.log(` - ${column.Field}: ${column.Type} ${column.Null === 'NO' ? '(必填)' : '(可选)'} ${column.Key ? `[${column.Key}]` : ''}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('❌ users表不存在');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// 检查permissions表
|
||||
try {
|
||||
const [permissionsStructure] = await sequelize.query('DESCRIBE permissions');
|
||||
console.log('🔐 permissions表结构:');
|
||||
permissionsStructure.forEach(column => {
|
||||
console.log(` - ${column.Field}: ${column.Type} ${column.Null === 'NO' ? '(必填)' : '(可选)'} ${column.Key ? `[${column.Key}]` : ''}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('❌ permissions表不存在');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// 检查roles表
|
||||
try {
|
||||
const [rolesStructure] = await sequelize.query('DESCRIBE roles');
|
||||
console.log('👑 roles表结构:');
|
||||
rolesStructure.forEach(column => {
|
||||
console.log(` - ${column.Field}: ${column.Type} ${column.Null === 'NO' ? '(必填)' : '(可选)'} ${column.Key ? `[${column.Key}]` : ''}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('❌ roles表不存在');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// 检查role_permissions表
|
||||
try {
|
||||
const [rolePermissionsStructure] = await sequelize.query('DESCRIBE role_permissions');
|
||||
console.log('🔗 role_permissions表结构:');
|
||||
rolePermissionsStructure.forEach(column => {
|
||||
console.log(` - ${column.Field}: ${column.Type} ${column.Null === 'NO' ? '(必填)' : '(可选)'} ${column.Key ? `[${column.Key}]` : ''}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('❌ role_permissions表不存在');
|
||||
}
|
||||
|
||||
console.log('\n📊 检查数据量:');
|
||||
|
||||
// 检查各表数据量
|
||||
const tables_to_check = ['users', 'permissions', 'roles', 'role_permissions'];
|
||||
|
||||
for (const tableName of tables_to_check) {
|
||||
try {
|
||||
const [result] = await sequelize.query(`SELECT COUNT(*) as count FROM ${tableName}`);
|
||||
console.log(` - ${tableName}: ${result[0].count} 条记录`);
|
||||
} catch (error) {
|
||||
console.log(` - ${tableName}: 表不存在或查询失败`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 检查失败:', error.message);
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
console.log('\n🔚 检查完成');
|
||||
}
|
||||
}
|
||||
|
||||
checkDatabaseStructure();
|
||||
@@ -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} 条图表数据`);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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('=== 批量分配权限结束 ===');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
156
insurance_backend/scripts/add_missing_permissions.js
Normal file
156
insurance_backend/scripts/add_missing_permissions.js
Normal file
@@ -0,0 +1,156 @@
|
||||
const { Permission, Role, RolePermission } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// 需要添加的权限列表
|
||||
const missingPermissions = [
|
||||
// 用户管理权限
|
||||
{ code: 'user:read', name: '用户查看', description: '查看用户信息', module: 'user', type: 'operation' },
|
||||
{ code: 'user:create', name: '用户创建', description: '创建新用户', module: 'user', type: 'operation' },
|
||||
{ code: 'user:update', name: '用户更新', description: '更新用户信息', module: 'user', type: 'operation' },
|
||||
{ code: 'user:delete', name: '用户删除', description: '删除用户', module: 'user', type: 'operation' },
|
||||
|
||||
// 保单管理权限
|
||||
{ code: 'insurance:policy:create', name: '保单创建', description: '创建保单', module: 'insurance', type: 'operation' },
|
||||
{ code: 'insurance:policy:edit', name: '保单编辑', description: '编辑保单信息', module: 'insurance', type: 'operation' },
|
||||
{ code: 'insurance:policy:delete', name: '保单删除', description: '删除保单', module: 'insurance', type: 'operation' },
|
||||
|
||||
// 保险申请权限
|
||||
{ code: 'insurance:read', name: '保险申请查看', description: '查看保险申请', module: 'insurance', type: 'operation' },
|
||||
{ code: 'insurance:create', name: '保险申请创建', description: '创建保险申请', module: 'insurance', type: 'operation' },
|
||||
{ code: 'insurance:update', name: '保险申请更新', description: '更新保险申请', module: 'insurance', type: 'operation' },
|
||||
{ code: 'insurance:review', name: '保险申请审核', description: '审核保险申请', module: 'insurance', type: 'operation' },
|
||||
{ code: 'insurance:delete', name: '保险申请删除', description: '删除保险申请', module: 'insurance', type: 'operation' },
|
||||
|
||||
// 系统管理权限
|
||||
{ code: 'system:read', name: '系统查看', description: '查看系统信息', module: 'system', type: 'operation' },
|
||||
{ code: 'system:update', name: '系统更新', description: '更新系统配置', module: 'system', type: 'operation' },
|
||||
{ code: 'system:admin', name: '系统管理', description: '系统管理操作', module: 'system', type: 'operation' },
|
||||
{ code: 'system:export', name: '系统导出', description: '导出系统数据', module: 'system', type: 'operation' },
|
||||
|
||||
// 监管任务权限
|
||||
{ code: 'supervision_tasks:read', name: '监管任务查看', description: '查看监管任务', module: 'supervision', type: 'operation' },
|
||||
{ code: 'supervision_tasks:create', name: '监管任务创建', description: '创建监管任务', module: 'supervision', type: 'operation' },
|
||||
{ code: 'supervision_tasks:update', name: '监管任务更新', description: '更新监管任务', module: 'supervision', type: 'operation' },
|
||||
{ code: 'supervision_tasks:delete', name: '监管任务删除', description: '删除监管任务', module: 'supervision', type: 'operation' },
|
||||
|
||||
// 监管任务完成权限
|
||||
{ code: 'regulatory_task:read', name: '监管任务完成查看', description: '查看监管任务完成情况', module: 'regulatory', type: 'operation' },
|
||||
{ code: 'regulatory_task:create', name: '监管任务完成创建', description: '创建监管任务完成记录', module: 'regulatory', type: 'operation' },
|
||||
{ code: 'regulatory_task:update', name: '监管任务完成更新', description: '更新监管任务完成记录', module: 'regulatory', type: 'operation' },
|
||||
{ code: 'regulatory_task:delete', name: '监管任务完成删除', description: '删除监管任务完成记录', module: 'regulatory', type: 'operation' },
|
||||
{ code: 'regulatory_task:review', name: '监管任务完成审核', description: '审核监管任务完成记录', module: 'regulatory', type: 'operation' },
|
||||
|
||||
// 安装任务权限
|
||||
{ code: 'installation_tasks:read', name: '安装任务查看', description: '查看安装任务', module: 'installation', type: 'operation' },
|
||||
{ code: 'installation_tasks:create', name: '安装任务创建', description: '创建安装任务', module: 'installation', type: 'operation' },
|
||||
{ code: 'installation_tasks:update', name: '安装任务更新', description: '更新安装任务', module: 'installation', type: 'operation' },
|
||||
{ code: 'installation_tasks:delete', name: '安装任务删除', description: '删除安装任务', module: 'installation', type: 'operation' },
|
||||
|
||||
// 生资理赔权限
|
||||
{ code: 'livestock_claim:read', name: '生资理赔查看', description: '查看生资理赔', module: 'livestock', type: 'operation' },
|
||||
{ code: 'livestock_claim:create', name: '生资理赔创建', description: '创建生资理赔', module: 'livestock', type: 'operation' },
|
||||
{ code: 'livestock_claim:review', name: '生资理赔审核', description: '审核生资理赔', module: 'livestock', type: 'operation' },
|
||||
{ code: 'livestock_claim:payment', name: '生资理赔支付', description: '处理生资理赔支付', module: 'livestock', type: 'operation' },
|
||||
|
||||
// 设备管理权限
|
||||
{ code: 'device:read', name: '设备查看', description: '查看设备信息', module: 'device', type: 'operation' },
|
||||
{ code: 'device:create', name: '设备创建', description: '创建设备', module: 'device', type: 'operation' },
|
||||
{ code: 'device:update', name: '设备更新', description: '更新设备信息', module: 'device', type: 'operation' },
|
||||
{ code: 'device:delete', name: '设备删除', description: '删除设备', module: 'device', type: 'operation' },
|
||||
|
||||
// 设备告警权限
|
||||
{ code: 'device_alerts:read', name: '设备告警查看', description: '查看设备告警', module: 'device', type: 'operation' },
|
||||
{ code: 'device_alerts:create', name: '设备告警创建', description: '创建设备告警', module: 'device', type: 'operation' },
|
||||
{ code: 'device_alerts:update', name: '设备告警更新', description: '更新设备告警', module: 'device', type: 'operation' },
|
||||
{ code: 'device_alerts:delete', name: '设备告警删除', description: '删除设备告警', module: 'device', type: 'operation' },
|
||||
|
||||
// 理赔管理权限
|
||||
{ code: 'claim:read', name: '理赔查看', description: '查看理赔信息', module: 'claim', type: 'operation' },
|
||||
{ code: 'claim:create', name: '理赔创建', description: '创建理赔', module: 'claim', type: 'operation' },
|
||||
{ code: 'claim:update', name: '理赔更新', description: '更新理赔信息', module: 'claim', type: 'operation' },
|
||||
{ code: 'claim:delete', name: '理赔删除', description: '删除理赔', module: 'claim', type: 'operation' },
|
||||
{ code: 'claim:review', name: '理赔审核', description: '审核理赔', module: 'claim', type: 'operation' },
|
||||
|
||||
// 数据仓库权限
|
||||
{ code: 'data_warehouse:read', name: '数据仓库查看', description: '查看数据仓库', module: 'data', type: 'operation' },
|
||||
{ code: 'data_warehouse:export', name: '数据仓库导出', description: '导出数据仓库数据', module: 'data', type: 'operation' }
|
||||
];
|
||||
|
||||
async function addMissingPermissions() {
|
||||
try {
|
||||
console.log('开始添加缺失的权限...');
|
||||
|
||||
// 获取现有权限
|
||||
const existingPermissions = await Permission.findAll({
|
||||
attributes: ['code']
|
||||
});
|
||||
const existingCodes = existingPermissions.map(p => p.code);
|
||||
|
||||
// 过滤出需要添加的权限
|
||||
const permissionsToAdd = missingPermissions.filter(p => !existingCodes.includes(p.code));
|
||||
|
||||
console.log(`找到 ${permissionsToAdd.length} 个需要添加的权限`);
|
||||
|
||||
if (permissionsToAdd.length === 0) {
|
||||
console.log('所有权限都已存在,无需添加');
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量创建权限
|
||||
const createdPermissions = await Permission.bulkCreate(permissionsToAdd, {
|
||||
ignoreDuplicates: true
|
||||
});
|
||||
|
||||
console.log(`成功创建 ${createdPermissions.length} 个权限`);
|
||||
|
||||
// 获取admin角色
|
||||
const adminRole = await Role.findOne({
|
||||
where: { name: 'admin' }
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
console.log('未找到admin角色,跳过权限分配');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`找到admin角色,ID: ${adminRole.id}`);
|
||||
|
||||
// 获取新创建的权限ID
|
||||
const newPermissions = await Permission.findAll({
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: permissionsToAdd.map(p => p.code)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 为admin角色分配新权限
|
||||
const rolePermissions = newPermissions.map(permission => ({
|
||||
role_id: adminRole.id,
|
||||
permission_id: permission.id,
|
||||
granted: true,
|
||||
granted_by: 1, // 假设用户ID为1
|
||||
granted_at: new Date()
|
||||
}));
|
||||
|
||||
await RolePermission.bulkCreate(rolePermissions, {
|
||||
ignoreDuplicates: true
|
||||
});
|
||||
|
||||
console.log(`成功为admin角色分配 ${rolePermissions.length} 个权限`);
|
||||
|
||||
console.log('权限添加完成!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('添加权限失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行脚本
|
||||
addMissingPermissions().then(() => {
|
||||
console.log('脚本执行完成');
|
||||
process.exit(0);
|
||||
}).catch(error => {
|
||||
console.error('脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
// 分页切换
|
||||
|
||||
@@ -7,31 +7,211 @@
|
||||
bindinput="onSearchInput"
|
||||
value="{{searchValue}}"
|
||||
/>
|
||||
<button bindtap="onSearch">查询</button>
|
||||
<button bindtap="onSearch" class="search-btn">查询</button>
|
||||
<button wx:if="{{isSearching}}" bindtap="clearSearch" class="clear-btn">清除</button>
|
||||
</view>
|
||||
|
||||
<!-- 搜索状态提示 -->
|
||||
<view wx:if="{{isSearching}}" class="search-status">
|
||||
<text>搜索项圈编号: {{searchValue}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<view wx:if="{{isSearching && searchResult}}" class="search-result">
|
||||
<view class="result-header">
|
||||
<text class="result-title">搜索结果</text>
|
||||
<text class="result-subtitle">找到匹配的设备</text>
|
||||
</view>
|
||||
<view class="item search-item" bindtap="viewCollarDetail" data-id="{{searchResult.id}}">
|
||||
<!-- 设备基本信息 -->
|
||||
<view class="item-header">
|
||||
<text class="device-sn">{{searchResult.snText}}</text>
|
||||
<text class="device-status {{searchResult.status === '在线' ? 'online' : 'offline'}}">{{searchResult.statusText}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 设备详细信息 -->
|
||||
<view class="item-content">
|
||||
<view class="info-row">
|
||||
<text class="label">项圈编号:</text>
|
||||
<text class="value">{{searchResult.sn}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">佩戴状态:</text>
|
||||
<text class="value {{searchResult.is_wear === 1 ? 'wear-on' : 'wear-off'}}">{{searchResult.wearStatusText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">连接状态:</text>
|
||||
<text class="value {{searchResult.is_connect === 1 ? 'connect-on' : 'connect-off'}}">{{searchResult.connectStatusText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">电池电量:</text>
|
||||
<text class="value battery-{{searchResult.batteryPercent > 50 ? 'high' : searchResult.batteryPercent > 20 ? 'medium' : 'low'}}">{{searchResult.batteryText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">体温:</text>
|
||||
<text class="value">{{searchResult.temperatureText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">步数:</text>
|
||||
<text class="value">{{searchResult.stepsText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">信号强度:</text>
|
||||
<text class="value">{{searchResult.signalText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">GPS信号:</text>
|
||||
<text class="value">{{searchResult.gpsText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">位置状态:</text>
|
||||
<text class="value">{{searchResult.locationText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">最后更新:</text>
|
||||
<text class="value">{{searchResult.lastUpdateText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">更新间隔:</text>
|
||||
<text class="value">{{searchResult.updateIntervalText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="item-actions">
|
||||
<button class="btn-detail" size="mini" bindtap="viewCollarDetail" data-id="{{searchResult.id}}" catchtap="true">查看详情</button>
|
||||
<button class="btn-edit" size="mini" bindtap="editCollar" data-id="{{searchResult.id}}" catchtap="true">编辑</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 无搜索结果 -->
|
||||
<view wx:if="{{isSearching && !searchResult}}" class="no-result">
|
||||
<view class="no-result-icon">🔍</view>
|
||||
<text class="no-result-text">未找到项圈编号为 "{{searchValue}}" 的设备</text>
|
||||
<button class="retry-btn" bindtap="clearSearch">重新搜索</button>
|
||||
</view>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<view class="list">
|
||||
<view wx:if="{{!isSearching}}" class="list">
|
||||
<block wx:for="{{list}}" wx:key="id">
|
||||
<view class="item">
|
||||
<text>项圈编号: {{item.deviceId}}</text>
|
||||
<text>状态: {{item.status | statusText}}</text>
|
||||
<text>电量: {{item.battery}}%</text>
|
||||
<text>最后在线: {{item.lastOnlineTime}}</text>
|
||||
<view class="item" bindtap="viewCollarDetail" data-id="{{item.id}}">
|
||||
<!-- 设备基本信息 -->
|
||||
<view class="item-header">
|
||||
<text class="device-sn">{{item.snText}}</text>
|
||||
<text class="device-status {{item.status === '在线' ? 'online' : 'offline'}}">{{item.statusText}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 设备详细信息 -->
|
||||
<view class="item-content">
|
||||
<view class="info-row">
|
||||
<text class="label">项圈编号:</text>
|
||||
<text class="value">{{item.sn}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">佩戴状态:</text>
|
||||
<text class="value {{item.is_wear === 1 ? 'wear-on' : 'wear-off'}}">{{item.wearStatusText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">连接状态:</text>
|
||||
<text class="value {{item.is_connect === 1 ? 'connect-on' : 'connect-off'}}">{{item.connectStatusText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">电池电量:</text>
|
||||
<text class="value battery-{{item.batteryPercent > 50 ? 'high' : item.batteryPercent > 20 ? 'medium' : 'low'}}">{{item.batteryText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">体温:</text>
|
||||
<text class="value">{{item.temperatureText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">步数:</text>
|
||||
<text class="value">{{item.stepsText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">信号强度:</text>
|
||||
<text class="value">{{item.signalText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">GPS信号:</text>
|
||||
<text class="value">{{item.gpsText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">位置状态:</text>
|
||||
<text class="value">{{item.locationText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">最后更新:</text>
|
||||
<text class="value">{{item.lastUpdateText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="label">更新间隔:</text>
|
||||
<text class="value">{{item.updateIntervalText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="item-actions">
|
||||
<button class="btn-detail" size="mini" bindtap="viewCollarDetail" data-id="{{item.id}}" catchtap="true">查看详情</button>
|
||||
<button class="btn-edit" size="mini" bindtap="editCollar" data-id="{{item.id}}" catchtap="true">编辑</button>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<view class="pagination">
|
||||
<block wx:for="{{pages}}" wx:key="index">
|
||||
<text
|
||||
class="{{currentPage === item ? 'active' : ''}}"
|
||||
bindtap="onPageChange"
|
||||
data-page="{{item}}"
|
||||
<view class="pagination" wx:if="{{!isSearching && totalPages > 1}}">
|
||||
<view class="pagination-info">
|
||||
<text>共 {{total}} 条数据,第 {{currentPage}} / {{totalPages}} 页</text>
|
||||
</view>
|
||||
<view class="pagination-buttons">
|
||||
<button
|
||||
class="page-btn prev-btn {{currentPage <= 1 ? 'disabled' : ''}}"
|
||||
bindtap="onPrevPage"
|
||||
disabled="{{currentPage <= 1}}"
|
||||
>
|
||||
{{item}}
|
||||
</text>
|
||||
</block>
|
||||
上一页
|
||||
</button>
|
||||
|
||||
<view class="page-numbers">
|
||||
<block wx:for="{{pageNumbers}}" wx:key="index">
|
||||
<text
|
||||
class="page-number {{currentPage === item ? 'active' : ''}}"
|
||||
bindtap="onPageChange"
|
||||
data-page="{{item}}"
|
||||
>
|
||||
{{item}}
|
||||
</text>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="page-btn next-btn {{currentPage >= totalPages ? 'disabled' : ''}}"
|
||||
bindtap="onNextPage"
|
||||
disabled="{{currentPage >= totalPages}}"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -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);
|
||||
}
|
||||
842
mini_program/farm-monitor-dashboard/pages/device/fence/fence.js
Normal file
842
mini_program/farm-monitor-dashboard/pages/device/fence/fence.js
Normal file
@@ -0,0 +1,842 @@
|
||||
Page({
|
||||
data: {
|
||||
// 围栏数据
|
||||
fenceList: [],
|
||||
loading: false,
|
||||
|
||||
// 设备统计
|
||||
stats: {
|
||||
smartCollector: 0,
|
||||
smartDevice: 0
|
||||
},
|
||||
|
||||
// 地图相关
|
||||
mapCenter: {
|
||||
lng: 106.2751866,
|
||||
lat: 38.4689544
|
||||
},
|
||||
mapZoom: 15,
|
||||
mapLocked: true, // 地图位置锁定
|
||||
lastMapCenter: null, // 上次地图中心位置
|
||||
|
||||
// 当前选中的围栏
|
||||
selectedFence: null,
|
||||
selectedFenceIndex: 0, // 当前选中的围栏索引
|
||||
|
||||
// 显示控制
|
||||
showPasture: true,
|
||||
mapType: 'normal', // normal, satellite
|
||||
|
||||
// 地图标记和多边形
|
||||
fenceMarkers: [],
|
||||
fencePolygons: [],
|
||||
|
||||
// 缓存数据
|
||||
cachedFenceData: null,
|
||||
isOfflineMode: false,
|
||||
|
||||
// 地图锁定相关
|
||||
includePoints: [], // 用于强制锁定地图位置的点
|
||||
mapLockTimer: null, // 地图锁定监控定时器
|
||||
|
||||
// 围栏类型配置
|
||||
fenceTypes: {
|
||||
'grazing': { name: '放牧围栏', color: '#52c41a', icon: '🌿' },
|
||||
'safety': { name: '安全围栏', color: '#1890ff', icon: '🛡️' },
|
||||
'restricted': { name: '限制围栏', color: '#ff4d4f', icon: '⚠️' },
|
||||
'collector': { name: '收集围栏', color: '#fa8c16', icon: '📦' }
|
||||
},
|
||||
|
||||
// 搜索和过滤
|
||||
searchValue: '',
|
||||
selectedFenceType: '',
|
||||
filteredFenceList: []
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
console.log('电子围栏页面加载')
|
||||
this.checkLoginStatus()
|
||||
|
||||
// 启动地图锁定监控定时器
|
||||
this.startMapLockTimer()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
// 清除定时器
|
||||
if (this.data.mapLockTimer) {
|
||||
clearInterval(this.data.mapLockTimer)
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 先尝试加载缓存数据,如果没有缓存再请求API
|
||||
if (!this.loadCachedData()) {
|
||||
this.loadFenceData()
|
||||
}
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
const token = wx.getStorageSync('token')
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
|
||||
if (!token || !userInfo) {
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
// 加载围栏数据
|
||||
loadFenceData() {
|
||||
if (!this.checkLoginStatus()) return
|
||||
|
||||
const token = wx.getStorageSync('token')
|
||||
const url = `https://ad.ningmuyun.com/api/electronic-fence?page=1&limit=100&_t=${Date.now()}`
|
||||
|
||||
this.setData({ loading: true })
|
||||
wx.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
timeout: 30000,
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('围栏API响应:', res)
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const response = res.data
|
||||
|
||||
if (response.success && response.data) {
|
||||
const fenceList = response.data.map(fence => this.formatFenceData(fence))
|
||||
|
||||
// 生成地图标记和多边形数据
|
||||
const fenceMarkers = this.generateFenceMarkers(fenceList)
|
||||
const fencePolygons = this.generateFencePolygons(fenceList)
|
||||
|
||||
// 缓存数据
|
||||
const cacheData = {
|
||||
fenceList: fenceList,
|
||||
fenceMarkers: fenceMarkers,
|
||||
fencePolygons: fencePolygons,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
wx.setStorageSync('fenceCache', cacheData)
|
||||
|
||||
this.setData({
|
||||
fenceList: fenceList,
|
||||
fenceMarkers: fenceMarkers,
|
||||
fencePolygons: fencePolygons,
|
||||
cachedFenceData: cacheData,
|
||||
isOfflineMode: false,
|
||||
stats: {
|
||||
smartCollector: 2, // 从API获取或硬编码
|
||||
smartDevice: 4 // 从API获取或硬编码
|
||||
}
|
||||
})
|
||||
|
||||
// 如果有围栏数据,设置默认选中第一个围栏
|
||||
if (fenceList.length > 0) {
|
||||
const firstFence = fenceList[0]
|
||||
const centerLng = parseFloat(firstFence.center.lng)
|
||||
const centerLat = parseFloat(firstFence.center.lat)
|
||||
|
||||
this.setData({
|
||||
selectedFence: firstFence,
|
||||
selectedFenceIndex: 0,
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
},
|
||||
mapLocked: true, // 初始化后锁定地图
|
||||
lastMapCenter: {
|
||||
latitude: centerLat,
|
||||
longitude: centerLng
|
||||
}
|
||||
})
|
||||
|
||||
// 立即更新include-points来强制锁定
|
||||
this.updateIncludePoints()
|
||||
|
||||
// 多次强制锁定,确保地图不会移动
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
}
|
||||
})
|
||||
this.updateIncludePoints()
|
||||
console.log('第一次延迟强制锁定:', centerLng, centerLat)
|
||||
}, 500)
|
||||
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
}
|
||||
})
|
||||
this.updateIncludePoints()
|
||||
console.log('第二次延迟强制锁定:', centerLng, centerLat)
|
||||
}, 1000)
|
||||
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
}
|
||||
})
|
||||
this.updateIncludePoints()
|
||||
console.log('第三次延迟强制锁定:', centerLng, centerLat)
|
||||
}, 2000)
|
||||
}
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '数据加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
} else if (res.statusCode === 502) {
|
||||
wx.showModal({
|
||||
title: '服务器错误',
|
||||
content: '服务器暂时不可用(502错误),请稍后重试',
|
||||
confirmText: '重试',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
setTimeout(() => {
|
||||
this.loadFenceData()
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (res.statusCode >= 500) {
|
||||
wx.showModal({
|
||||
title: '服务器错误',
|
||||
content: `服务器错误(${res.statusCode}),请稍后重试`,
|
||||
confirmText: '重试',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
setTimeout(() => {
|
||||
this.loadFenceData()
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: res.data?.message || `请求失败(${res.statusCode})`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('请求失败:', err)
|
||||
|
||||
// 根据错误类型显示不同的提示和处理方式
|
||||
let errorMessage = '网络请求失败'
|
||||
let errorTitle = '请求失败'
|
||||
let showRetry = true
|
||||
|
||||
if (err.errMsg && err.errMsg.includes('timeout')) {
|
||||
errorMessage = '请求超时,服务器响应较慢'
|
||||
errorTitle = '请求超时'
|
||||
} else if (err.errMsg && err.errMsg.includes('fail')) {
|
||||
if (err.errMsg.includes('502')) {
|
||||
errorMessage = '服务器网关错误(502),服务暂时不可用'
|
||||
errorTitle = '服务器错误'
|
||||
} else if (err.errMsg.includes('503')) {
|
||||
errorMessage = '服务器维护中(503),请稍后重试'
|
||||
errorTitle = '服务维护'
|
||||
} else if (err.errMsg.includes('504')) {
|
||||
errorMessage = '服务器响应超时(504),请重试'
|
||||
errorTitle = '服务器超时'
|
||||
} else {
|
||||
errorMessage = '网络连接失败,请检查网络设置'
|
||||
errorTitle = '网络错误'
|
||||
}
|
||||
} else if (err.errMsg && err.errMsg.includes('ssl')) {
|
||||
errorMessage = 'SSL证书错误,请检查网络环境'
|
||||
errorTitle = '安全连接错误'
|
||||
} else if (err.errMsg && err.errMsg.includes('dns')) {
|
||||
errorMessage = 'DNS解析失败,请检查网络连接'
|
||||
errorTitle = '网络解析错误'
|
||||
}
|
||||
|
||||
// 尝试加载缓存数据
|
||||
this.loadCachedData()
|
||||
|
||||
if (showRetry) {
|
||||
wx.showModal({
|
||||
title: errorTitle,
|
||||
content: errorMessage + ',是否重试?',
|
||||
confirmText: '重试',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 显示重试提示
|
||||
wx.showLoading({
|
||||
title: '重试中...',
|
||||
mask: true
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.hideLoading()
|
||||
this.loadFenceData()
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 格式化围栏数据
|
||||
formatFenceData(fence) {
|
||||
// 处理围栏类型映射
|
||||
let fenceType = fence.type || 'grazing'
|
||||
if (fenceType === '放牧围栏') fenceType = 'grazing'
|
||||
else if (fenceType === '安全围栏') fenceType = 'safety'
|
||||
else if (fenceType === '限制围栏') fenceType = 'restricted'
|
||||
else if (fenceType === '收集围栏') fenceType = 'collector'
|
||||
|
||||
return {
|
||||
id: fence.id,
|
||||
name: fence.name,
|
||||
type: fenceType,
|
||||
typeName: this.data.fenceTypes[fenceType]?.name || fenceType,
|
||||
typeColor: this.data.fenceTypes[fenceType]?.color || '#666',
|
||||
typeIcon: this.data.fenceTypes[fenceType]?.icon || '📍',
|
||||
description: fence.description,
|
||||
coordinates: fence.coordinates || [],
|
||||
center: fence.center,
|
||||
area: fence.area,
|
||||
grazingStatus: fence.grazingStatus,
|
||||
insideCount: fence.insideCount,
|
||||
outsideCount: fence.outsideCount,
|
||||
isActive: fence.isActive,
|
||||
createdAt: fence.createdAt,
|
||||
updatedAt: fence.updatedAt
|
||||
}
|
||||
},
|
||||
|
||||
// 加载缓存数据
|
||||
loadCachedData() {
|
||||
try {
|
||||
const cachedData = wx.getStorageSync('fenceCache')
|
||||
if (cachedData && cachedData.timestamp) {
|
||||
const cacheAge = Date.now() - cachedData.timestamp
|
||||
const maxCacheAge = 24 * 60 * 60 * 1000 // 24小时
|
||||
|
||||
if (cacheAge < maxCacheAge) {
|
||||
console.log('加载缓存数据,缓存时间:', new Date(cachedData.timestamp))
|
||||
|
||||
this.setData({
|
||||
fenceList: cachedData.fenceList || [],
|
||||
fenceMarkers: cachedData.fenceMarkers || [],
|
||||
fencePolygons: cachedData.fencePolygons || [],
|
||||
isOfflineMode: true,
|
||||
stats: {
|
||||
smartCollector: 2,
|
||||
smartDevice: 4
|
||||
}
|
||||
})
|
||||
|
||||
// 如果有围栏数据,设置默认选中第一个围栏
|
||||
if (cachedData.fenceList && cachedData.fenceList.length > 0) {
|
||||
const firstFence = cachedData.fenceList[0]
|
||||
const centerLng = parseFloat(firstFence.center.lng)
|
||||
const centerLat = parseFloat(firstFence.center.lat)
|
||||
|
||||
this.setData({
|
||||
selectedFence: firstFence,
|
||||
selectedFenceIndex: 0,
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
},
|
||||
mapLocked: true,
|
||||
lastMapCenter: {
|
||||
latitude: centerLat,
|
||||
longitude: centerLng
|
||||
}
|
||||
})
|
||||
|
||||
// 立即更新include-points来强制锁定
|
||||
this.updateIncludePoints()
|
||||
|
||||
// 多次强制锁定,确保地图不会移动
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
}
|
||||
})
|
||||
this.updateIncludePoints()
|
||||
console.log('缓存数据第一次延迟强制锁定:', centerLng, centerLat)
|
||||
}, 500)
|
||||
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
}
|
||||
})
|
||||
this.updateIncludePoints()
|
||||
console.log('缓存数据第二次延迟强制锁定:', centerLng, centerLat)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 显示离线模式提示
|
||||
wx.showToast({
|
||||
title: '已加载缓存数据(离线模式)',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
return true
|
||||
} else {
|
||||
console.log('缓存数据已过期,清除缓存')
|
||||
wx.removeStorageSync('fenceCache')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载缓存数据失败:', error)
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
// 显示设置菜单
|
||||
onShowMenu() {
|
||||
const menuItems = ['围栏设置', '设备管理', '历史记录']
|
||||
|
||||
// 添加地图锁定/解锁选项
|
||||
const lockText = this.data.mapLocked ? '解锁地图' : '锁定地图'
|
||||
menuItems.unshift(lockText)
|
||||
|
||||
// 如果有多个围栏,添加围栏切换选项
|
||||
if (this.data.fenceList.length > 1) {
|
||||
menuItems.unshift('切换围栏')
|
||||
}
|
||||
|
||||
wx.showActionSheet({
|
||||
itemList: menuItems,
|
||||
success: (res) => {
|
||||
const tapIndex = res.tapIndex
|
||||
console.log('选择了:', tapIndex)
|
||||
|
||||
let actualIndex = tapIndex
|
||||
|
||||
// 处理围栏切换
|
||||
if (this.data.fenceList.length > 1 && tapIndex === 0) {
|
||||
this.showFenceSelector()
|
||||
return
|
||||
} else if (this.data.fenceList.length > 1) {
|
||||
actualIndex = tapIndex - 1
|
||||
}
|
||||
|
||||
// 处理地图锁定/解锁
|
||||
if (actualIndex === 0) {
|
||||
this.toggleMapLock()
|
||||
} else {
|
||||
// 其他菜单项
|
||||
const menuIndex = this.data.fenceList.length > 1 ? actualIndex - 1 : actualIndex
|
||||
switch (menuIndex) {
|
||||
case 0: // 围栏设置
|
||||
wx.showToast({ title: '围栏设置功能开发中', icon: 'none' })
|
||||
break
|
||||
case 1: // 设备管理
|
||||
wx.showToast({ title: '设备管理功能开发中', icon: 'none' })
|
||||
break
|
||||
case 2: // 历史记录
|
||||
wx.showToast({ title: '历史记录功能开发中', icon: 'none' })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 显示围栏选择器
|
||||
showFenceSelector() {
|
||||
const fenceNames = this.data.fenceList.map(fence => fence.name)
|
||||
|
||||
wx.showActionSheet({
|
||||
itemList: fenceNames,
|
||||
success: (res) => {
|
||||
const selectedIndex = res.tapIndex
|
||||
const selectedFence = this.data.fenceList[selectedIndex]
|
||||
|
||||
this.setData({
|
||||
selectedFence: selectedFence,
|
||||
selectedFenceIndex: selectedIndex
|
||||
})
|
||||
|
||||
// 自动定位到选中的围栏
|
||||
this.locateToSelectedFence()
|
||||
|
||||
wx.showToast({
|
||||
title: `已切换到${selectedFence.name}`,
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 切换牧场显示
|
||||
onTogglePasture() {
|
||||
const newShowPasture = !this.data.showPasture
|
||||
this.setData({
|
||||
showPasture: newShowPasture
|
||||
})
|
||||
|
||||
if (newShowPasture && this.data.selectedFence) {
|
||||
// 如果显示牧场,定位到选中的围栏
|
||||
this.locateToSelectedFence()
|
||||
}
|
||||
},
|
||||
|
||||
// 定位到选中的围栏
|
||||
locateToSelectedFence() {
|
||||
if (!this.data.selectedFence) {
|
||||
wx.showToast({
|
||||
title: '没有选中的围栏',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const fence = this.data.selectedFence
|
||||
|
||||
// 计算围栏的边界,用于设置合适的地图视野
|
||||
const coordinates = fence.coordinates
|
||||
if (coordinates && coordinates.length > 0) {
|
||||
let minLat = coordinates[0].lat
|
||||
let maxLat = coordinates[0].lat
|
||||
let minLng = coordinates[0].lng
|
||||
let maxLng = coordinates[0].lng
|
||||
|
||||
coordinates.forEach(coord => {
|
||||
minLat = Math.min(minLat, coord.lat)
|
||||
maxLat = Math.max(maxLat, coord.lat)
|
||||
minLng = Math.min(minLng, coord.lng)
|
||||
maxLng = Math.max(maxLng, coord.lng)
|
||||
})
|
||||
|
||||
// 计算中心点和合适的缩放级别
|
||||
const centerLat = (minLat + maxLat) / 2
|
||||
const centerLng = (minLng + maxLng) / 2
|
||||
|
||||
// 根据围栏大小调整缩放级别
|
||||
const latDiff = maxLat - minLat
|
||||
const lngDiff = maxLng - minLng
|
||||
const maxDiff = Math.max(latDiff, lngDiff)
|
||||
|
||||
let zoom = 15
|
||||
if (maxDiff > 0.01) zoom = 12
|
||||
else if (maxDiff > 0.005) zoom = 14
|
||||
else if (maxDiff > 0.002) zoom = 16
|
||||
else zoom = 18
|
||||
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
},
|
||||
mapZoom: zoom,
|
||||
mapLocked: true, // 定位后锁定地图
|
||||
lastMapCenter: {
|
||||
latitude: centerLat,
|
||||
longitude: centerLng
|
||||
}
|
||||
})
|
||||
|
||||
// 立即更新include-points来强制锁定
|
||||
setTimeout(() => {
|
||||
this.updateIncludePoints()
|
||||
}, 100)
|
||||
|
||||
wx.showToast({
|
||||
title: `已定位到${fence.name}`,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
} else {
|
||||
// 如果没有坐标点,使用中心点定位
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: parseFloat(fence.center.lng),
|
||||
lat: parseFloat(fence.center.lat)
|
||||
},
|
||||
mapZoom: 15,
|
||||
mapLocked: true, // 定位后锁定地图
|
||||
lastMapCenter: {
|
||||
latitude: parseFloat(fence.center.lat),
|
||||
longitude: parseFloat(fence.center.lng)
|
||||
}
|
||||
})
|
||||
|
||||
// 立即更新include-points来强制锁定
|
||||
setTimeout(() => {
|
||||
this.updateIncludePoints()
|
||||
}, 100)
|
||||
|
||||
wx.showToast({
|
||||
title: `已定位到${fence.name}`,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 切换地图类型
|
||||
onSwitchMap() {
|
||||
const mapType = this.data.mapType === 'normal' ? 'satellite' : 'normal'
|
||||
this.setData({
|
||||
mapType: mapType
|
||||
})
|
||||
|
||||
wx.showToast({
|
||||
title: mapType === 'normal' ? '切换到普通地图' : '切换到卫星地图',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
// 解锁/锁定地图
|
||||
toggleMapLock() {
|
||||
const newLocked = !this.data.mapLocked
|
||||
this.setData({
|
||||
mapLocked: newLocked
|
||||
})
|
||||
|
||||
// 更新include-points
|
||||
this.updateIncludePoints()
|
||||
|
||||
wx.showToast({
|
||||
title: newLocked ? '地图已锁定' : '地图已解锁',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
},
|
||||
|
||||
// 地图标记点击事件
|
||||
onMarkerTap(e) {
|
||||
const markerId = e.detail.markerId
|
||||
const fence = this.data.fenceList.find(f => f.id === markerId)
|
||||
|
||||
if (fence) {
|
||||
// 选中该围栏
|
||||
this.setData({
|
||||
selectedFence: fence,
|
||||
selectedFenceIndex: this.data.fenceList.findIndex(f => f.id === markerId)
|
||||
})
|
||||
|
||||
wx.showModal({
|
||||
title: `${fence.typeIcon} ${fence.name}`,
|
||||
content: `类型: ${fence.typeName}\n状态: ${fence.grazingStatus}\n面积: ${fence.area}平方米\n坐标点: ${fence.coordinates.length}个\n描述: ${fence.description || '无描述'}`,
|
||||
confirmText: '定位到此围栏',
|
||||
cancelText: '关闭',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 定位到选中的围栏
|
||||
this.locateToSelectedFence()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 地图区域变化
|
||||
onRegionChange(e) {
|
||||
console.log('地图区域变化:', e.detail)
|
||||
|
||||
// 强制锁定地图 - 无论什么情况都恢复位置
|
||||
if (this.data.lastMapCenter) {
|
||||
console.log('强制锁定地图位置')
|
||||
|
||||
// 立即恢复地图位置
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: this.data.lastMapCenter.longitude,
|
||||
lat: this.data.lastMapCenter.latitude
|
||||
}
|
||||
})
|
||||
|
||||
// 更新include-points来强制锁定
|
||||
this.updateIncludePoints()
|
||||
}
|
||||
},
|
||||
|
||||
// 更新include-points来强制锁定地图
|
||||
updateIncludePoints() {
|
||||
if (this.data.lastMapCenter) {
|
||||
const center = this.data.lastMapCenter
|
||||
|
||||
// 创建更紧密的四个点来强制锁定地图视野
|
||||
const offset = 0.0005 // 减小偏移量,使锁定更紧密
|
||||
const points = [
|
||||
{ latitude: center.latitude - offset, longitude: center.longitude - offset },
|
||||
{ latitude: center.latitude + offset, longitude: center.longitude - offset },
|
||||
{ latitude: center.latitude + offset, longitude: center.longitude + offset },
|
||||
{ latitude: center.latitude - offset, longitude: center.longitude + offset }
|
||||
]
|
||||
|
||||
this.setData({
|
||||
includePoints: points
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
includePoints: []
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 启动地图锁定监控定时器
|
||||
startMapLockTimer() {
|
||||
if (this.data.mapLockTimer) {
|
||||
clearInterval(this.data.mapLockTimer)
|
||||
}
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (this.data.lastMapCenter) {
|
||||
// 强制更新地图位置 - 无论锁定状态如何
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: this.data.lastMapCenter.longitude,
|
||||
lat: this.data.lastMapCenter.latitude
|
||||
}
|
||||
})
|
||||
|
||||
// 更新include-points
|
||||
this.updateIncludePoints()
|
||||
|
||||
console.log('定时器强制锁定地图位置:', this.data.lastMapCenter)
|
||||
}
|
||||
}, 500) // 每500毫秒检查一次,更频繁
|
||||
|
||||
this.setData({
|
||||
mapLockTimer: timer
|
||||
})
|
||||
},
|
||||
|
||||
// 地图点击事件
|
||||
onMapTap(e) {
|
||||
console.log('地图点击:', e.detail)
|
||||
},
|
||||
|
||||
// 关闭围栏信息面板
|
||||
onCloseFenceInfo() {
|
||||
this.setData({
|
||||
selectedFence: null
|
||||
})
|
||||
},
|
||||
|
||||
// 定位围栏
|
||||
onLocateFence() {
|
||||
if (this.data.selectedFence) {
|
||||
this.locateToSelectedFence()
|
||||
}
|
||||
},
|
||||
|
||||
// 查看围栏详情
|
||||
onViewFenceDetails() {
|
||||
if (this.data.selectedFence) {
|
||||
wx.showModal({
|
||||
title: `${this.data.selectedFence.typeIcon} ${this.data.selectedFence.name}`,
|
||||
content: `围栏ID: ${this.data.selectedFence.id}\n类型: ${this.data.selectedFence.typeName}\n状态: ${this.data.selectedFence.grazingStatus}\n面积: ${this.data.selectedFence.area}平方米\n坐标点: ${this.data.selectedFence.coordinates.length}个\n内部设备: ${this.data.selectedFence.insideCount}个\n外部设备: ${this.data.selectedFence.outsideCount}个\n创建时间: ${this.data.selectedFence.createdAt}\n更新时间: ${this.data.selectedFence.updatedAt}\n描述: ${this.data.selectedFence.description || '无描述'}`,
|
||||
showCancel: false,
|
||||
confirmText: '确定'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 生成围栏标记
|
||||
generateFenceMarkers(fenceList) {
|
||||
return fenceList.map((fence, index) => {
|
||||
return {
|
||||
id: fence.id,
|
||||
latitude: parseFloat(fence.center.lat),
|
||||
longitude: parseFloat(fence.center.lng),
|
||||
iconPath: '', // 使用默认图标
|
||||
width: 30,
|
||||
height: 30,
|
||||
title: fence.name,
|
||||
callout: {
|
||||
content: `${fence.typeIcon} ${fence.name}\n${fence.typeName}\n${fence.grazingStatus}\n${fence.coordinates.length}个坐标点`,
|
||||
color: '#333',
|
||||
fontSize: 12,
|
||||
borderRadius: 8,
|
||||
bgColor: '#fff',
|
||||
padding: 12,
|
||||
display: 'BYCLICK',
|
||||
borderWidth: 1,
|
||||
borderColor: fence.typeColor || '#3cc51f'
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 生成围栏多边形
|
||||
generateFencePolygons(fenceList) {
|
||||
return fenceList.map((fence, index) => {
|
||||
const points = fence.coordinates.map(coord => ({
|
||||
latitude: coord.lat,
|
||||
longitude: coord.lng
|
||||
}))
|
||||
|
||||
// 根据围栏类型设置颜色
|
||||
const strokeColor = fence.typeColor || (fence.isActive ? '#3cc51f' : '#ff6b6b')
|
||||
const fillColor = fence.typeColor ?
|
||||
`${fence.typeColor}33` : // 添加透明度
|
||||
(fence.isActive ? 'rgba(60, 197, 31, 0.2)' : 'rgba(255, 107, 107, 0.2)')
|
||||
|
||||
return {
|
||||
points: points,
|
||||
strokeWidth: 3,
|
||||
strokeColor: strokeColor,
|
||||
fillColor: fillColor
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "电子围栏",
|
||||
"navigationBarBackgroundColor": "#3cc51f",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#f5f5f5"
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<!-- 电子围栏页面 -->
|
||||
<view class="fence-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="header">
|
||||
<view class="header-left" bindtap="onBack">
|
||||
<text class="back-icon">‹</text>
|
||||
</view>
|
||||
<view class="header-title">电子围栏</view>
|
||||
<view class="header-right">
|
||||
<text class="menu-icon" bindtap="onShowMenu">⋯</text>
|
||||
<text class="minimize-icon" bindtap="onShowMenu">−</text>
|
||||
<text class="target-icon" bindtap="onShowMenu">◎</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 离线模式提示 -->
|
||||
<view wx:if="{{isOfflineMode}}" class="offline-notice">
|
||||
<text class="offline-icon">📡</text>
|
||||
<text class="offline-text">离线模式 - 显示缓存数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 地图锁定提示 -->
|
||||
<view wx:if="{{mapLocked}}" class="map-lock-notice">
|
||||
<text class="lock-icon">🔒</text>
|
||||
<text class="lock-text">地图已锁定 - 防止自动移动</text>
|
||||
</view>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<view class="control-panel">
|
||||
<!-- 左侧控制区 -->
|
||||
<view class="left-controls">
|
||||
<!-- 设置按钮 -->
|
||||
<view class="settings-btn" bindtap="onShowMenu">
|
||||
<text class="settings-icon">⚙</text>
|
||||
</view>
|
||||
|
||||
<!-- 显示牧场按钮 -->
|
||||
<view class="pasture-btn {{showPasture ? 'active' : ''}}" bindtap="onTogglePasture">
|
||||
<text>显示牧场</text>
|
||||
</view>
|
||||
|
||||
<!-- 设备统计信息 -->
|
||||
<view class="device-stats">
|
||||
<view class="stats-item">
|
||||
<text class="stats-label">智能采集器:</text>
|
||||
<text class="stats-value">{{stats.smartCollector}}</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="stats-label">智能设备:</text>
|
||||
<text class="stats-value">{{stats.smartDevice}}</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="stats-label">围栏总数:</text>
|
||||
<text class="stats-value">{{fenceList.length}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 牧场名称 -->
|
||||
<view class="pasture-name">各德</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧控制区 -->
|
||||
<view class="right-controls">
|
||||
<view class="switch-map-btn" bindtap="onSwitchMap">
|
||||
<text>切换地图</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 围栏信息面板 -->
|
||||
<view wx:if="{{selectedFence}}" class="fence-info-panel">
|
||||
<view class="panel-header">
|
||||
<view class="fence-title">
|
||||
<text class="fence-icon">{{selectedFence.typeIcon}}</text>
|
||||
<text class="fence-name">{{selectedFence.name}}</text>
|
||||
</view>
|
||||
<view class="close-btn" bindtap="onCloseFenceInfo">
|
||||
<text>✕</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel-content">
|
||||
<view class="info-row">
|
||||
<text class="info-label">围栏类型:</text>
|
||||
<text class="info-value" style="color: {{selectedFence.typeColor}}">{{selectedFence.typeName}}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">放牧状态:</text>
|
||||
<text class="info-value">{{selectedFence.grazingStatus}}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">围栏面积:</text>
|
||||
<text class="info-value">{{selectedFence.area}}平方米</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">坐标点数:</text>
|
||||
<text class="info-value">{{selectedFence.coordinates.length}}个</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">围栏描述:</text>
|
||||
<text class="info-value">{{selectedFence.description || '无描述'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel-actions">
|
||||
<view class="action-btn primary" bindtap="onLocateFence">
|
||||
<text>定位围栏</text>
|
||||
</view>
|
||||
<view class="action-btn secondary" bindtap="onViewFenceDetails">
|
||||
<text>查看详情</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 地图区域 -->
|
||||
<view class="map-container">
|
||||
<map
|
||||
id="fenceMap"
|
||||
class="fence-map"
|
||||
longitude="{{mapCenter.lng}}"
|
||||
latitude="{{mapCenter.lat}}"
|
||||
scale="{{mapZoom}}"
|
||||
markers="{{fenceMarkers}}"
|
||||
polygons="{{fencePolygons}}"
|
||||
show-location="{{false}}"
|
||||
enable-scroll="{{false}}"
|
||||
enable-zoom="{{false}}"
|
||||
enable-rotate="{{false}}"
|
||||
enable-overlooking="{{false}}"
|
||||
enable-satellite="{{false}}"
|
||||
enable-traffic="{{false}}"
|
||||
enable-3D="{{false}}"
|
||||
enable-compass="{{false}}"
|
||||
enable-scale="{{false}}"
|
||||
enable-poi="{{false}}"
|
||||
enable-building="{{false}}"
|
||||
include-points="{{includePoints}}"
|
||||
bindmarkertap="onMarkerTap"
|
||||
bindregionchange="onRegionChange"
|
||||
bindtap="onMapTap"
|
||||
>
|
||||
<!-- 地图加载中 -->
|
||||
<view wx:if="{{loading}}" class="map-loading">
|
||||
<text>地图加载中...</text>
|
||||
</view>
|
||||
</map>
|
||||
</view>
|
||||
</view>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
477
mini_program/farm-monitor-dashboard/pages/device/host/host.js
Normal file
477
mini_program/farm-monitor-dashboard/pages/device/host/host.js
Normal file
@@ -0,0 +1,477 @@
|
||||
Page({
|
||||
data: {
|
||||
list: [],
|
||||
searchValue: '',
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
totalPages: 0,
|
||||
pageNumbers: [],
|
||||
paginationList: [], // 分页页码列表
|
||||
stats: {
|
||||
total: 0,
|
||||
online: 0,
|
||||
offline: 0
|
||||
},
|
||||
loading: false,
|
||||
isSearching: false,
|
||||
searchResult: null
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
console.log('智能主机页面加载')
|
||||
this.checkLoginStatus()
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadData().then(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
const token = wx.getStorageSync('token')
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
|
||||
if (!token || !userInfo) {
|
||||
console.log('用户未登录,跳转到登录页')
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录后再使用',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
console.log('用户已登录:', userInfo.username)
|
||||
return true
|
||||
},
|
||||
|
||||
// 加载数据
|
||||
loadData() {
|
||||
const { currentPage, pageSize } = this.data
|
||||
const url = `https://ad.ningmuyun.com/api/smart-devices/hosts?page=${currentPage}&limit=${pageSize}&_t=${Date.now()}&refresh=true`
|
||||
|
||||
const token = wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ loading: true })
|
||||
wx.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
timeout: 30000, // 设置30秒超时
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('API响应:', res)
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const response = res.data
|
||||
|
||||
if (response.success && response.data) {
|
||||
const data = response.data
|
||||
const total = response.total || 0
|
||||
const totalPages = Math.ceil(total / this.data.pageSize)
|
||||
const paginationList = this.generatePaginationList(this.data.currentPage, totalPages)
|
||||
|
||||
this.setData({
|
||||
list: Array.isArray(data) ? data.map(item => this.formatItemData(item)) : [],
|
||||
total: total,
|
||||
totalPages: totalPages,
|
||||
paginationList: paginationList,
|
||||
stats: response.stats || { total: 0, online: 0, offline: 0 }
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '数据加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: res.data?.message || '数据加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('请求失败:', err)
|
||||
|
||||
// 根据错误类型显示不同的提示
|
||||
let errorMessage = '网络请求失败'
|
||||
if (err.errMsg && err.errMsg.includes('timeout')) {
|
||||
errorMessage = '请求超时,请检查网络连接'
|
||||
} else if (err.errMsg && err.errMsg.includes('fail')) {
|
||||
errorMessage = '网络连接失败,请重试'
|
||||
} else if (err.errMsg && err.errMsg.includes('401')) {
|
||||
errorMessage = '请登录后重试'
|
||||
}
|
||||
|
||||
wx.showModal({
|
||||
title: '请求失败',
|
||||
content: errorMessage + ',是否重试?',
|
||||
confirmText: '重试',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户选择重试
|
||||
setTimeout(() => {
|
||||
this.loadData()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
complete: () => {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 格式化单个设备数据
|
||||
formatItemData(item) {
|
||||
return {
|
||||
...item,
|
||||
statusText: item.networkStatus || '未知',
|
||||
signalText: item.signalValue || '未知',
|
||||
batteryText: `${item.battery || 0}%`,
|
||||
temperatureText: `${item.temperature || 0}°C`,
|
||||
deviceNumberText: item.deviceNumber || '未知',
|
||||
updateTimeText: item.updateTime || '未知'
|
||||
}
|
||||
},
|
||||
|
||||
// 生成分页页码列表
|
||||
generatePaginationList(currentPage, totalPages) {
|
||||
const paginationList = []
|
||||
const maxVisiblePages = 5 // 最多显示5个页码
|
||||
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
// 总页数少于等于5页,显示所有页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 总页数大于5页,显示省略号
|
||||
if (currentPage <= 3) {
|
||||
// 当前页在前3页
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
paginationList.push({
|
||||
page: totalPages,
|
||||
active: false,
|
||||
text: totalPages.toString()
|
||||
})
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
// 当前页在后3页
|
||||
paginationList.push({
|
||||
page: 1,
|
||||
active: false,
|
||||
text: '1'
|
||||
})
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
for (let i = totalPages - 3; i <= totalPages; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 当前页在中间
|
||||
paginationList.push({
|
||||
page: 1,
|
||||
active: false,
|
||||
text: '1'
|
||||
})
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
paginationList.push({
|
||||
page: totalPages,
|
||||
active: false,
|
||||
text: totalPages.toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return paginationList
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({ searchValue: e.detail.value.trim() })
|
||||
},
|
||||
|
||||
// 执行搜索
|
||||
onSearch() {
|
||||
const searchValue = this.data.searchValue.trim()
|
||||
if (!searchValue) {
|
||||
wx.showToast({
|
||||
title: '请输入主机编号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证主机编号格式(数字和字母)
|
||||
if (!/^[A-Za-z0-9]+$/.test(searchValue)) {
|
||||
wx.showToast({
|
||||
title: '主机编号只能包含数字和字母',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置搜索状态
|
||||
this.setData({
|
||||
isSearching: true,
|
||||
searchResult: null,
|
||||
currentPage: 1
|
||||
})
|
||||
|
||||
// 执行精确搜索
|
||||
this.performExactSearch(searchValue)
|
||||
},
|
||||
|
||||
// 执行精确搜索
|
||||
performExactSearch(searchValue) {
|
||||
const url = `https://ad.ningmuyun.com/api/smart-devices/hosts?page=1&limit=1&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
|
||||
const token = wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '搜索中...' })
|
||||
wx.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
timeout: 30000, // 设置30秒超时
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('搜索API响应:', res)
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const response = res.data
|
||||
|
||||
if (response.success && response.data) {
|
||||
const data = response.data
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
// 找到匹配的设备
|
||||
const device = this.formatItemData(data[0])
|
||||
this.setData({
|
||||
searchResult: device,
|
||||
list: [], // 清空列表显示
|
||||
total: 1,
|
||||
totalPages: 1,
|
||||
paginationList: [{ page: 1, active: true, text: '1' }]
|
||||
})
|
||||
wx.showToast({
|
||||
title: '搜索成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
// 没有找到匹配的设备
|
||||
this.setData({
|
||||
searchResult: null,
|
||||
list: [],
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
paginationList: []
|
||||
})
|
||||
wx.showToast({
|
||||
title: '未找到该设备',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '搜索失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '搜索失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('搜索请求失败:', err)
|
||||
|
||||
// 根据错误类型显示不同的提示
|
||||
let errorMessage = '网络请求失败'
|
||||
if (err.errMsg && err.errMsg.includes('timeout')) {
|
||||
errorMessage = '搜索超时,请检查网络连接'
|
||||
} else if (err.errMsg && err.errMsg.includes('fail')) {
|
||||
errorMessage = '网络连接失败,请重试'
|
||||
}
|
||||
|
||||
wx.showModal({
|
||||
title: '搜索失败',
|
||||
content: errorMessage + ',是否重试?',
|
||||
confirmText: '重试',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户选择重试
|
||||
setTimeout(() => {
|
||||
this.performExactSearch(this.data.searchValue)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
complete: () => {
|
||||
wx.hideLoading()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 清除搜索
|
||||
clearSearch() {
|
||||
this.setData({
|
||||
searchValue: '',
|
||||
isSearching: false,
|
||||
searchResult: null,
|
||||
currentPage: 1
|
||||
})
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
// 上一页
|
||||
onPrevPage() {
|
||||
if (this.data.currentPage > 1) {
|
||||
this.setData({
|
||||
currentPage: this.data.currentPage - 1
|
||||
}, () => {
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 下一页
|
||||
onNextPage() {
|
||||
if (this.data.currentPage < this.data.totalPages) {
|
||||
this.setData({
|
||||
currentPage: this.data.currentPage + 1
|
||||
}, () => {
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 分页切换
|
||||
onPageChange(e) {
|
||||
const page = parseInt(e.currentTarget.dataset.page)
|
||||
if (page > 0 && page !== this.data.currentPage) {
|
||||
this.setData({
|
||||
currentPage: page
|
||||
}, () => {
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 查看主机详情
|
||||
viewHostDetail(e) {
|
||||
const hostId = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/device/host-detail/host-detail?id=${hostId}`
|
||||
})
|
||||
},
|
||||
|
||||
// 主机定位总览
|
||||
onLocationOverview() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/device/host-location/host-location'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "智能主机",
|
||||
"enablePullDownRefresh": true,
|
||||
"onReachBottomDistance": 50
|
||||
}
|
||||
177
mini_program/farm-monitor-dashboard/pages/device/host/host.wxml
Normal file
177
mini_program/farm-monitor-dashboard/pages/device/host/host.wxml
Normal file
@@ -0,0 +1,177 @@
|
||||
<view class="container">
|
||||
<!-- 搜索区域 -->
|
||||
<view class="search-section">
|
||||
<view class="search-box">
|
||||
<view class="search-icon">🔍</view>
|
||||
<input
|
||||
placeholder="搜索"
|
||||
bindinput="onSearchInput"
|
||||
value="{{searchValue}}"
|
||||
class="search-input"
|
||||
/>
|
||||
<button bindtap="onSearch" class="search-btn">查询</button>
|
||||
<button wx:if="{{isSearching}}" bindtap="clearSearch" class="clear-btn">清除</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索状态提示 -->
|
||||
<view wx:if="{{isSearching}}" class="search-status">
|
||||
<text>搜索主机编号: {{searchValue}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 统计卡片区域 -->
|
||||
<view class="stats-section">
|
||||
<view class="stats-card">
|
||||
<text class="stats-label">主机总数</text>
|
||||
<text class="stats-value">{{stats.total}}</text>
|
||||
</view>
|
||||
<view class="stats-card">
|
||||
<text class="stats-label">联网数量</text>
|
||||
<text class="stats-value">{{stats.online}}</text>
|
||||
</view>
|
||||
<view class="stats-card">
|
||||
<text class="stats-label">断网数量</text>
|
||||
<text class="stats-value">{{stats.offline}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<view wx:if="{{isSearching && searchResult}}" class="search-result">
|
||||
<view class="result-header">
|
||||
<text class="result-title">搜索结果</text>
|
||||
<text class="result-subtitle">找到匹配的设备</text>
|
||||
</view>
|
||||
<view class="host-item search-item" bindtap="viewHostDetail" data-id="{{searchResult.id}}">
|
||||
<!-- 主机编号和状态 -->
|
||||
<view class="host-header">
|
||||
<text class="host-number">主机编号: {{searchResult.deviceNumberText}}</text>
|
||||
<view class="status-btn {{searchResult.networkStatus === '已联网' ? 'online' : 'offline'}}">
|
||||
{{searchResult.statusText}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设备详细信息 -->
|
||||
<view class="host-details">
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">设备电量:</text>
|
||||
<text class="detail-value">{{searchResult.batteryText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">设备信号值:</text>
|
||||
<text class="detail-value">{{searchResult.signalText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">设备温度:</text>
|
||||
<text class="detail-value">{{searchResult.temperatureText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">绑带状态:</text>
|
||||
<text class="detail-value">{{searchResult.statusText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">数据更新时间:</text>
|
||||
<text class="detail-value">{{searchResult.updateTimeText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 无搜索结果 -->
|
||||
<view wx:if="{{isSearching && !searchResult}}" class="no-result">
|
||||
<view class="no-result-icon">🔍</view>
|
||||
<text class="no-result-text">未找到主机编号为 "{{searchValue}}" 的设备</text>
|
||||
<button class="retry-btn" bindtap="clearSearch">重新搜索</button>
|
||||
</view>
|
||||
|
||||
<!-- 主机设备列表 -->
|
||||
<view wx:if="{{!isSearching}}" class="host-list">
|
||||
<block wx:for="{{list}}" wx:key="id">
|
||||
<view class="host-item" bindtap="viewHostDetail" data-id="{{item.id}}">
|
||||
<!-- 主机编号和状态 -->
|
||||
<view class="host-header">
|
||||
<text class="host-number">主机编号: {{item.deviceNumberText}}</text>
|
||||
<view class="status-btn {{item.networkStatus === '已联网' ? 'online' : 'offline'}}">
|
||||
{{item.statusText}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设备详细信息 -->
|
||||
<view class="host-details">
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">设备电量:</text>
|
||||
<text class="detail-value">{{item.batteryText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">设备信号值:</text>
|
||||
<text class="detail-value">{{item.signalText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">设备温度:</text>
|
||||
<text class="detail-value">{{item.temperatureText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">绑带状态:</text>
|
||||
<text class="detail-value">{{item.statusText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">数据更新时间:</text>
|
||||
<text class="detail-value">{{item.updateTimeText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 分页组件 -->
|
||||
<view wx:if="{{!isSearching && totalPages > 1}}" class="pagination-container">
|
||||
<view class="pagination-info">
|
||||
<text>共 {{total}} 条记录,第 {{currentPage}}/{{totalPages}} 页</text>
|
||||
</view>
|
||||
|
||||
<view class="pagination-controls">
|
||||
<!-- 上一页按钮 -->
|
||||
<view
|
||||
class="pagination-btn {{currentPage === 1 ? 'disabled' : ''}}"
|
||||
bindtap="onPrevPage"
|
||||
>
|
||||
上一页
|
||||
</view>
|
||||
|
||||
<!-- 页码列表 -->
|
||||
<view class="pagination-pages">
|
||||
<view
|
||||
wx:for="{{paginationList}}"
|
||||
wx:key="index"
|
||||
class="pagination-page {{item.active ? 'active' : ''}} {{item.page === -1 ? 'ellipsis' : ''}}"
|
||||
bindtap="onPageChange"
|
||||
data-page="{{item.page}}"
|
||||
>
|
||||
{{item.text}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下一页按钮 -->
|
||||
<view
|
||||
class="pagination-btn {{currentPage === totalPages ? 'disabled' : ''}}"
|
||||
bindtap="onNextPage"
|
||||
>
|
||||
下一页
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="footer-action">
|
||||
<button class="location-btn" bindtap="onLocationOverview">
|
||||
主机定位总览
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
370
mini_program/farm-monitor-dashboard/pages/device/host/host.wxss
Normal file
370
mini_program/farm-monitor-dashboard/pages/device/host/host.wxss
Normal file
@@ -0,0 +1,370 @@
|
||||
/* 智能主机页面样式 */
|
||||
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding-bottom: 120rpx; /* 为底部按钮留出空间 */
|
||||
}
|
||||
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
background: #52c41a;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 25rpx;
|
||||
padding: 15rpx 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 32rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: linear-gradient(135deg, #52c41a, #73d13d);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 20rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
background: linear-gradient(135deg, #ff4d4f, #ff7875);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 20rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
|
||||
}
|
||||
|
||||
.search-status {
|
||||
background: linear-gradient(135deg, #e6f7ff, #bae7ff);
|
||||
border: 2rpx solid #91d5ff;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin: 0 20rpx 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.search-status text {
|
||||
color: #1890ff;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.search-result {
|
||||
margin: 0 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
background: linear-gradient(135deg, #f6ffed, #d9f7be);
|
||||
border: 2rpx solid #b7eb8f;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #52c41a;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.result-subtitle {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #73d13d;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
border: 2rpx solid #52c41a;
|
||||
box-shadow: 0 4rpx 16rpx rgba(82, 196, 26, 0.2);
|
||||
}
|
||||
|
||||
.no-result {
|
||||
text-align: center;
|
||||
padding: 80rpx 40rpx;
|
||||
background: #fafafa;
|
||||
border-radius: 12rpx;
|
||||
margin: 0 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.no-result-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.no-result-text {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 40rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: linear-gradient(135deg, #52c41a, #73d13d);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 25rpx;
|
||||
padding: 20rpx 40rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
|
||||
/* 统计卡片区域 */
|
||||
.stats-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
flex: 1;
|
||||
background: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
border: 1rpx solid #e8e8e8;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 主机设备列表 */
|
||||
.host-list {
|
||||
padding: 0 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.host-item {
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
padding: 25rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
border: 1rpx solid #e8e8e8;
|
||||
}
|
||||
|
||||
.host-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 15rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.host-number {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-btn {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 6rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
border: 2rpx solid;
|
||||
}
|
||||
|
||||
.status-btn.online {
|
||||
color: #1890ff;
|
||||
border-color: #1890ff;
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-btn.offline {
|
||||
color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
background: rgba(255, 77, 79, 0.1);
|
||||
}
|
||||
|
||||
/* 设备详细信息 */
|
||||
.host-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 分页组件样式 */
|
||||
.pagination-container {
|
||||
padding: 32rpx;
|
||||
background: #ffffff;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
text-align: center;
|
||||
margin-bottom: 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
padding: 16rpx 24rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.pagination-btn.disabled {
|
||||
background: #f0f0f0;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.pagination-pages {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.pagination-page {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pagination-page.active {
|
||||
background: #3cc51f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.pagination-page.ellipsis {
|
||||
background: transparent;
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 底部操作按钮 */
|
||||
.footer-action {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-top: 1rpx solid #e8e8e8;
|
||||
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.location-btn {
|
||||
width: 100%;
|
||||
background: #52c41a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
padding: 25rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.location-btn:active {
|
||||
background: #389e0d;
|
||||
transform: translateY(2rpx);
|
||||
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80rpx 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state .empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-state .empty-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user