feat(dashboard): 添加首页地图展示功能

- 在 Dashboard 组件中集成锡林郭勒盟区域地图
- 实现地图数据接口和区域详情接口
- 添加地图交互功能,支持点击和悬停事件
- 更新开发计划和需求文档,增加地图展示功能
This commit is contained in:
2025-08-20 20:34:52 +08:00
parent fdc58aa3a2
commit 5ff7d38904
12 changed files with 1537 additions and 57 deletions

1
.gitignore vendored
View File

@@ -24087,3 +24087,4 @@
/frontend/dashboard/node_modules/zrender/README.md
/frontend/dashboard/node_modules/.package-lock.json
/frontend/dashboard/node_modules/
/backend/api/node_modules/

831
backend/api/package-lock.json generated Normal file
View File

@@ -0,0 +1,831 @@
{
"name": "xlxumu-api",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "xlxumu-api",
"version": "1.0.0",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"express": "^4.21.2",
"express-rate-limit": "^8.0.1",
"helmet": "^8.1.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dotenv": {
"version": "17.2.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express-rate-limit": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz",
"integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==",
"dependencies": {
"ip-address": "10.0.1"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/helmet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"engines": {
"node": ">= 0.8"
}
}
}
}

View File

@@ -8,6 +8,10 @@
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2"
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"express": "^4.21.2",
"express-rate-limit": "^8.0.1",
"helmet": "^8.1.0"
}
}

View File

@@ -41,6 +41,117 @@ app.get('/health', (req, res) => {
});
});
// 大屏可视化地图数据接口
app.get('/api/v1/dashboard/map/regions', (req, res) => {
// 模拟锡林郭勒盟各区域数据
const regions = [
{
id: 'xlg',
name: '锡林浩特市',
coordinates: [116.093, 43.946],
cattle_count: 25600,
farm_count: 120,
output_value: 650000000
},
{
id: 'dwq',
name: '东乌旗',
coordinates: [116.980, 45.514],
cattle_count: 18500,
farm_count: 95,
output_value: 480000000
},
{
id: 'xwq',
name: '西乌旗',
coordinates: [117.615, 44.587],
cattle_count: 21200,
farm_count: 108,
output_value: 520000000
},
{
id: 'abg',
name: '阿巴嘎旗',
coordinates: [114.971, 44.022],
cattle_count: 16800,
farm_count: 86,
output_value: 420000000
},
{
id: 'snz',
name: '苏尼特左旗',
coordinates: [113.653, 43.859],
cattle_count: 12400,
farm_count: 65,
output_value: 310000000
}
];
res.json({ regions });
});
app.get('/api/v1/dashboard/map/region/:regionId', (req, res) => {
const { regionId } = req.params;
// 模拟各区域详细数据
const regionDetails = {
'xlg': {
region: {
id: 'xlg',
name: '锡林浩特市',
coordinates: [116.093, 43.946],
cattle_count: 25600,
farm_count: 120,
output_value: 650000000,
trend: 'up'
},
farms: [
{
id: 'FARM001',
name: '锡林浩特市第一牧场',
coordinates: [116.120, 43.950],
cattle_count: 2450,
output_value: 62000000
},
{
id: 'FARM002',
name: '锡林浩特市第二牧场',
coordinates: [116.080, 43.930],
cattle_count: 2100,
output_value: 53000000
}
]
},
'dwq': {
region: {
id: 'dwq',
name: '东乌旗',
coordinates: [116.980, 45.514],
cattle_count: 18500,
farm_count: 95,
output_value: 480000000,
trend: 'up'
},
farms: [
{
id: 'FARM003',
name: '东乌旗牧场A',
coordinates: [116.990, 45.520],
cattle_count: 1950,
output_value: 49000000
}
]
}
};
const detail = regionDetails[regionId];
if (detail) {
res.json(detail);
} else {
res.status(404).json({ error: '区域未找到' });
}
});
// 启动服务器
app.listen(PORT, () => {
console.log(`API服务器正在端口 ${PORT} 上运行`);

View File

@@ -108,7 +108,97 @@ GET /api/v1/dashboard/history
}
```
### 4.3 配置接口
### 4.3 首页地图数据接口
#### 获取锡林郭勒盟区域地图数据
```
GET /api/v1/dashboard/map/regions
```
**请求参数**:
-
**响应示例**:
```json
{
"regions": [
{
"id": "xlg",
"name": "锡林浩特市",
"coordinates": [116.093, 43.946],
"cattle_count": 25600,
"farm_count": 120,
"output_value": 650000000
},
{
"id": "dwq",
"name": "东乌旗",
"coordinates": [116.980, 45.514],
"cattle_count": 18500,
"farm_count": 95,
"output_value": 480000000
},
{
"id": "xwq",
"name": "西乌旗",
"coordinates": [117.615, 44.587],
"cattle_count": 21200,
"farm_count": 108,
"output_value": 520000000
},
{
"id": "abg",
"name": "阿巴嘎旗",
"coordinates": [114.971, 44.022],
"cattle_count": 16800,
"farm_count": 86,
"output_value": 420000000
},
{
"id": "snz",
"name": "苏尼特左旗",
"coordinates": [113.653, 43.859],
"cattle_count": 12400,
"farm_count": 65,
"output_value": 310000000
}
]
}
```
#### 获取指定区域详细数据
```
GET /api/v1/dashboard/map/region/{regionId}
```
**请求参数**:
- `regionId` (string, required): 区域ID
**响应示例**:
```json
{
"region": {
"id": "xlg",
"name": "锡林浩特市",
"coordinates": [116.093, 43.946],
"cattle_count": 25600,
"farm_count": 120,
"output_value": 650000000,
"trend": "up"
},
"farms": [
{
"id": "FARM001",
"name": "锡林浩特市第一牧场",
"coordinates": [116.120, 43.950],
"cattle_count": 2450,
"output_value": 62000000
}
]
}
```
### 4.4 配置接口
#### 获取可视化配置
```

View File

@@ -17,7 +17,7 @@
## 3. 功能模块详细计划
### 3.1 产业概览模块 (3天)
### 3.1 产业概览模块 (4天)
- 第1天
- 整体产业规模展示(牛只总数、牧场数量等关键指标)
- 产值和增长率关键指标(年度产值、增长率趋势图)
@@ -27,6 +27,8 @@
- 第3天
- 数据钻取功能实现(点击图表可查看详细数据)
- 多维度数据展示(按时间、区域、品种等维度筛选)
- 第4天
- 首页地图展示功能开发(集成锡林郭勒盟区域地图,展示各区域牛只分布、牧场位置)
### 3.2 养殖监控模块 (3天)
- 第1天
@@ -98,10 +100,11 @@
- 结合 DataV 图表实现丰富的数据可视化
- 使用自适应容器确保不同分辨率下的正常显示
- 添加窗口大小改变时的重绘功能
- 集成地图组件展示锡林郭勒盟区域数据分布
## 5. 里程碑
- **里程碑1**:完成产业概览模块和养殖监控模块(6天)
- **里程碑1**:完成产业概览模块和养殖监控模块(7天)
- **里程碑2**完成金融服务模块和交易统计模块4天
- **里程碑3**完成运输跟踪模块和风险预警模块4天
- **里程碑4**完成生态指标模块和政府监管模块4天

View File

@@ -13,6 +13,7 @@
- **实时数据更新机制**:通过 WebSocket 实现数据实时更新(`ws://<host>/api/v1/dashboard/realtime`
- **数据钻取功能**:支持点击图表查看详细数据(弹窗展示,含数据导出按钮)
- **多维度数据筛选**:支持按时间、区域、品种等维度筛选(交互:下拉选择器 + 确认按钮)
- **首页地图展示**:在首页集成锡林郭勒盟区域地图,展示各区域牛只分布、牧场位置、产业热点等信息(交互:点击区域查看详细数据)
### 2.2 养殖监控模块
- **各牧场养殖情况展示**:通过 DataV 地图组件展示各牧场位置和规模(数据来源:`/api/v1/dashboard/farms`,数据库表:`farm_locations`

View File

@@ -1,6 +1,6 @@
import {
__export
} from "./chunk-2GTGKKMZ.js";
} from "./chunk-5WWUZCGV.js";
// node_modules/tslib/tslib.es6.js
var extendStatics = function(d, b) {

View File

@@ -0,0 +1,247 @@
<template>
<div class="region-detail-overlay" v-if="visible" @click="closeOverlay">
<div class="region-detail-card" @click.stop>
<div class="card-header">
<h2>{{ regionData.region?.name }} 详情</h2>
<button class="close-button" @click="closeOverlay">×</button>
</div>
<div class="card-content">
<div class="region-stats">
<div class="stat-item">
<div class="stat-label">牛只数量</div>
<div class="stat-value">{{ regionData.region?.cattle_count || 0 }}</div>
</div>
<div class="stat-item">
<div class="stat-label">牧场数量</div>
<div class="stat-value">{{ regionData.region?.farm_count || 0 }}</div>
</div>
<div class="stat-item">
<div class="stat-label">产值</div>
<div class="stat-value">¥{{ formatNumber(regionData.region?.output_value || 0) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">趋势</div>
<div class="stat-value" :class="regionData.region?.trend">
{{ regionData.region?.trend === 'up' ? '↑ 上升' : '↓ 下降' }}
</div>
</div>
</div>
<div class="farms-section" v-if="regionData.farms && regionData.farms.length > 0">
<h3>牧场列表</h3>
<div class="farms-list">
<div
class="farm-item"
v-for="farm in regionData.farms"
:key="farm.id"
>
<div class="farm-name">{{ farm.name }}</div>
<div class="farm-details">
<span>牛只: {{ farm.cattle_count }}</span>
<span>产值: ¥{{ formatNumber(farm.output_value) }}</span>
</div>
</div>
</div>
</div>
<div class="no-farms" v-else>
<p>暂无牧场数据</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, defineProps, defineEmits } from 'vue';
export default {
name: 'RegionDetail',
props: {
visible: {
type: Boolean,
default: false
},
regionData: {
type: Object,
default: () => ({})
}
},
emits: ['close'],
setup(props, { emit }) {
const closeOverlay = () => {
emit('close');
};
const formatNumber = (num) => {
if (num >= 100000000) {
return (num / 100000000).toFixed(2) + '亿';
}
if (num >= 10000) {
return (num / 10000).toFixed(2) + '万';
}
return num.toLocaleString();
};
return {
closeOverlay,
formatNumber
};
}
};
</script>
<style scoped>
.region-detail-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.region-detail-card {
background: linear-gradient(135deg, #0f2027, #20555d);
border-radius: 12px;
width: 600px;
max-width: 90%;
max-height: 90%;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.card-header {
padding: 20px;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.card-header h2 {
margin: 0;
font-size: 24px;
color: #4CAF50;
}
.close-button {
background: none;
border: none;
color: #fff;
font-size: 28px;
cursor: pointer;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.3s;
}
.close-button:hover {
background: rgba(255, 255, 255, 0.1);
}
.card-content {
padding: 20px;
overflow-y: auto;
max-height: calc(90vh - 100px);
}
.region-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.stat-item {
background: rgba(255, 255, 255, 0.05);
padding: 15px;
border-radius: 8px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-label {
font-size: 14px;
color: #ccc;
margin-bottom: 8px;
}
.stat-value {
font-size: 20px;
font-weight: bold;
color: #4CAF50;
}
.stat-value.up {
color: #4CAF50;
}
.stat-value.down {
color: #F44336;
}
.farms-section h3 {
color: #4CAF50;
margin-top: 0;
margin-bottom: 15px;
font-size: 18px;
}
.farms-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.farm-item {
background: rgba(255, 255, 255, 0.05);
padding: 15px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.farm-name {
font-weight: bold;
margin-bottom: 8px;
color: #fff;
}
.farm-details {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #ccc;
}
.no-farms {
text-align: center;
color: #ccc;
padding: 30px 0;
}
@media (max-width: 768px) {
.region-stats {
grid-template-columns: 1fr;
}
.farm-details {
flex-direction: column;
gap: 5px;
}
}
</style>

View File

@@ -5,18 +5,27 @@
class="map-canvas"
:style="{ width: '100%', height: height + 'px' }"
></div>
<RegionDetail
:visible="showRegionDetail"
:region-data="selectedRegionData"
@close="closeRegionDetail"
/>
</div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import * as turf from '@turf/turf';
import RegionDetail from './RegionDetail.vue';
import { fetchRegionDetail } from '@/services/dashboard.js';
export default {
name: 'ThreeDMap',
components: {
RegionDetail
},
props: {
height: {
type: Number,
@@ -35,8 +44,12 @@ export default {
},
setup(props) {
const mapContainer = ref(null);
const showRegionDetail = ref(false);
const selectedRegionData = ref({});
let scene, camera, renderer, controls;
let animationId = null;
let landmarks = [];
let raycaster, mouse;
// 初始化3D场景
const initScene = () => {
@@ -78,6 +91,14 @@ export default {
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// 初始化射线检测器
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
// 添加事件监听
renderer.domElement.addEventListener('click', onMouseClick, false);
renderer.domElement.addEventListener('mousemove', onMouseMove, false);
// 创建地形和地标
createTerrain();
createLandmarks();
@@ -91,7 +112,7 @@ export default {
// 创建一个简单的平面作为地形基础
const geometry = new THREE.PlaneGeometry(2000, 2000, 50, 50);
const material = new THREE.MeshStandardMaterial({
color: 0x4CAF50,
color: 0x2c5364,
wireframe: false,
transparent: true,
opacity: 0.7
@@ -99,7 +120,7 @@ export default {
const terrain = new THREE.Mesh(geometry, material);
terrain.rotation.x = -Math.PI / 2;
terrain.position.y = -10;
terrain.position.y = -20;
terrain.receiveShadow = true;
scene.add(terrain);
@@ -115,53 +136,155 @@ export default {
// 创建地标
const createLandmarks = () => {
// 创建锡林郭勒盟主要旗县的地标
const locations = [
{ name: '锡林浩特市', position: [0, 0, 0], color: 0x2196F3 },
{ name: '东乌珠穆沁旗', position: [-200, 0, 100], color: 0xFF9800 },
{ name: '西乌珠穆沁旗', position: [200, 0, 100], color: 0xF44336 },
{ name: '镶黄旗', position: [-100, 0, -150], color: 0x9C27B0 },
{ name: '正镶白旗', position: [100, 0, -150], color: 0x4CAF50 }
];
// 清除现有的地标
landmarks.forEach(landmark => {
scene.remove(landmark.mesh);
scene.remove(landmark.label);
});
landmarks = [];
locations.forEach(location => {
// 根据传入的地图数据创建地标
props.mapData.forEach((location, index) => {
// 计算位置(基于锡林浩特市为中心点)
const x = (location.coordinates[0] - props.center[0]) * 5000;
const z = (props.center[1] - location.coordinates[1]) * 5000;
// 根据牛只数量确定地标大小
const size = Math.max(20, Math.min(50, (location.cattle_count || location.cattleCount) / 1000));
const height = Math.max(30, Math.min(100, (location.cattle_count || location.cattleCount) / 500));
// 创建地标圆柱体
const geometry = new THREE.CylinderGeometry(20, 20, 50, 32);
const geometry = new THREE.CylinderGeometry(size, size, height, 32);
const material = new THREE.MeshStandardMaterial({
color: location.color,
color: getColorByCattleCount(location.cattle_count || location.cattleCount),
transparent: true,
opacity: 0.8
});
const cylinder = new THREE.Mesh(geometry, material);
cylinder.position.set(...location.position);
cylinder.position.y = 25;
cylinder.position.set(x, height/2 - 20, z);
cylinder.castShadow = true;
cylinder.userData = { location }; // 保存位置信息用于点击检测
scene.add(cylinder);
// 添加地标名称
addLabel(location.name, location.position, location.color);
// 添加地标名称和数据
const label = addLabel(
`${location.name}\n牛只: ${location.cattle_count || location.cattleCount}\n牧场: ${location.farm_count || location.farmCount}`,
[x, height + 20, z],
getColorByCattleCount(location.cattle_count || location.cattleCount)
);
landmarks.push({
mesh: cylinder,
label: label,
data: location
});
});
};
// 根据牛只数量获取颜色
const getColorByCattleCount = (count) => {
if (count > 20000) return 0x4CAF50; // 绿色 - 数量多
if (count > 15000) return 0xFFEB3B; // 黄色 - 数量中等
return 0xF44336; // 红色 - 数量较少
};
// 添加标签
const addLabel = (text, position, color) => {
// 创建简单的文本标签(在实际项目中可以使用更复杂的文本渲染)
// 创建简单的文本标签
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = 256;
canvas.height = 128;
canvas.width = 512;
canvas.height = 256;
context.fillStyle = `#${color.toString(16).padStart(6, '0')}`;
context.font = '24px Arial';
context.fillStyle = `#${new THREE.Color(color).getHexString()}`;
context.font = '24px Microsoft YaHei';
context.textAlign = 'center';
context.fillText(text, 128, 64);
context.textBaseline = 'middle';
// 支持多行文本
const lines = text.split('\n');
lines.forEach((line, i) => {
context.fillText(line, 256, 128 + (i - (lines.length-1)/2) * 30);
});
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.SpriteMaterial({ map: texture });
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
const sprite = new THREE.Sprite(material);
sprite.position.set(position[0], 80, position[2]);
sprite.scale.set(100, 50, 1);
sprite.position.set(position[0], position[1], position[2]);
sprite.scale.set(200, 100, 1);
scene.add(sprite);
return sprite;
};
// 鼠标点击事件处理
const onMouseClick = async (event) => {
// 计算鼠标位置标准化设备坐标 (-1 到 +1)
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// 通过摄像机和鼠标位置更新射线
raycaster.setFromCamera(mouse, camera);
// 计算物体和射线的交点
const intersects = raycaster.intersectObjects(scene.children);
for (let i = 0; i < intersects.length; i++) {
if (intersects[i].object.userData.location) {
const region = intersects[i].object.userData.location;
selectedRegionData.value = await fetchRegionDetail(region.id);
showRegionDetail.value = true;
highlightRegion(intersects[i].object);
break;
}
}
};
// 鼠标移动事件处理(悬停效果)
const onMouseMove = (event) => {
// 计算鼠标位置标准化设备坐标 (-1 到 +1)
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// 通过摄像机和鼠标位置更新射线
raycaster.setFromCamera(mouse, camera);
// 计算物体和射线的交点
const intersects = raycaster.intersectObjects(scene.children);
// 重置所有地标的颜色
resetRegionColors();
// 高亮显示悬停的地標
for (let i = 0; i < intersects.length; i++) {
if (intersects[i].object.userData.location) {
intersects[i].object.material.emissive = new THREE.Color(0x222222);
break;
}
}
};
// 高亮选中区域
const highlightRegion = (mesh) => {
resetRegionColors();
mesh.material.emissive = new THREE.Color(0x333333);
};
// 重置区域颜色
const resetRegionColors = () => {
scene.children.forEach(child => {
if (child.isMesh && child.userData.location) {
child.material.emissive = new THREE.Color(0x000000);
}
});
};
// 关闭区域详情
const closeRegionDetail = () => {
showRegionDetail.value = false;
selectedRegionData.value = {};
resetRegionColors();
};
// 动画循环
@@ -187,6 +310,13 @@ export default {
}
};
// 监听地图数据变化
watch(() => props.mapData, () => {
if (scene) {
createLandmarks();
}
}, { deep: true });
// 清理资源
const cleanup = () => {
if (animationId) {
@@ -194,6 +324,8 @@ export default {
}
if (renderer) {
renderer.domElement.removeEventListener('click', onMouseClick, false);
renderer.domElement.removeEventListener('mousemove', onMouseMove, false);
mapContainer.value.removeChild(renderer.domElement);
renderer.dispose();
}
@@ -221,7 +353,10 @@ export default {
});
return {
mapContainer
mapContainer,
showRegionDetail,
selectedRegionData,
closeRegionDetail
};
}
};
@@ -231,6 +366,7 @@ export default {
.three-d-map-container {
width: 100%;
height: 100%;
position: relative;
}
.map-canvas {

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
const API_BASE_URL = 'http://localhost:3000/api/v1/dashboard';
const API_BASE_URL = 'http://localhost:8000/api/v1/dashboard';
export const fetchOverviewData = async () => {
try {
@@ -50,4 +50,24 @@ export const fetchFinanceData = async (type) => {
console.error('Error fetching finance data:', error);
return [];
}
};
export const fetchMapData = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/map/regions`);
return response.data;
} catch (error) {
console.error('Error fetching map data:', error);
return [];
}
};
export const fetchRegionDetail = async (regionId) => {
try {
const response = await axios.get(`${API_BASE_URL}/map/region/${regionId}`);
return response.data;
} catch (error) {
console.error('Error fetching region detail:', error);
return {};
}
};

View File

@@ -53,25 +53,16 @@
<!-- 中间区域 -->
<div class="center-section">
<!-- 产业概览 -->
<!-- 产业概览地图 -->
<div class="center-top">
<div class="center-border card">
<h2>产业概览</h2>
<div class="center-content">
<div class="total-count">
<div class="count-title">牛只总数</div>
<div class="count-value">128,456</div>
<div class="count-unit"></div>
</div>
<div class="total-count">
<div class="count-title">牧场数量</div>
<div class="count-value">1,245</div>
<div class="count-unit"></div>
</div>
<div class="growth-rate">
<div class="rate-title">同比增长</div>
<div class="rate-value positive">5.2%</div>
</div>
<h2>锡林郭勒盟产业分布</h2>
<div class="map-container">
<ThreeDMap
:height="300"
:map-data="mapData"
ref="threeDMap"
/>
</div>
</div>
</div>
@@ -126,15 +117,21 @@
<script>
import * as echarts from 'echarts'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import ThreeDMap from '@/components/map/ThreeDMap.vue'
import { fetchMapData } from '@/services/dashboard.js'
export default {
name: 'Dashboard',
components: {
ThreeDMap
},
setup() {
const currentTime = ref(new Date().toLocaleString())
const breedingChart = ref(null)
const transactionChart = ref(null)
const regionChart = ref(null)
const riskRadarChart = ref(null)
const threeDMap = ref(null)
let breedingChartInstance = null
let transactionChartInstance = null
@@ -158,6 +155,9 @@ export default {
{ time: '08-18 16:15', type: '运输风险', desc: '运输路线受阻', status: '已处理' }
])
// 地图数据
const mapData = ref([])
// 初始化图表
const initCharts = () => {
// 确保 DOM 元素已正确绑定
@@ -255,6 +255,35 @@ export default {
}
}
// 获取地图数据
const loadMapData = async () => {
try {
const data = await fetchMapData()
if (data.regions && data.regions.length > 0) {
mapData.value = data.regions
} else {
// 使用默认数据
mapData.value = [
{ id: 'xlg', name: '锡林浩特市', cattle_count: 25600, farm_count: 120, coordinates: [116.093, 43.946] },
{ id: 'dwq', name: '东乌旗', cattle_count: 18500, farm_count: 95, coordinates: [116.980, 45.514] },
{ id: 'xwq', name: '西乌旗', cattle_count: 21200, farm_count: 108, coordinates: [117.615, 44.587] },
{ id: 'abg', name: '阿巴嘎旗', cattle_count: 16800, farm_count: 86, coordinates: [114.971, 44.022] },
{ id: 'snz', name: '苏尼特左旗', cattle_count: 12400, farm_count: 65, coordinates: [113.653, 43.859] }
]
}
} catch (error) {
console.error('获取地图数据失败:', error)
// 使用默认数据
mapData.value = [
{ id: 'xlg', name: '锡林浩特市', cattle_count: 25600, farm_count: 120, coordinates: [116.093, 43.946] },
{ id: 'dwq', name: '东乌旗', cattle_count: 18500, farm_count: 95, coordinates: [116.980, 45.514] },
{ id: 'xwq', name: '西乌旗', cattle_count: 21200, farm_count: 108, coordinates: [117.615, 44.587] },
{ id: 'abg', name: '阿巴嘎旗', cattle_count: 16800, farm_count: 86, coordinates: [114.971, 44.022] },
{ id: 'snz', name: '苏尼特左旗', cattle_count: 12400, farm_count: 65, coordinates: [113.653, 43.859] }
]
}
}
// 更新时间
const updateTime = () => {
currentTime.value = new Date().toLocaleString()
@@ -270,6 +299,7 @@ export default {
onMounted(() => {
initCharts()
loadMapData()
timer = setInterval(updateTime, 1000)
window.addEventListener('resize', resizeCharts)
})
@@ -287,10 +317,12 @@ export default {
currentTime,
keyMetrics,
riskData,
mapData,
breedingChart,
transactionChart,
regionChart,
riskRadarChart
riskRadarChart,
threeDMap
}
}
}
@@ -425,15 +457,15 @@ export default {
}
.center-top {
height: 20%;
height: 40%;
}
.center-middle {
height: 50%;
height: 35%;
}
.center-bottom {
height: 30%;
height: 25%;
}
.center-border, .center-chart-border {
@@ -442,6 +474,10 @@ export default {
padding: 20px;
}
.map-container {
height: calc(100% - 40px);
}
.center-content {
height: 100%;
display: flex;