修复一些已知的BUG

This commit is contained in:
wzclm 2025-03-08 15:42:36 +08:00
parent 9572d9000f
commit d0eca2f2ca
13 changed files with 1960 additions and 461 deletions

477
package-lock.json generated
View File

@ -11,6 +11,8 @@
"@dataview/datav-vue3": "^0.0.0-test.1672506674342",
"@element-plus/icons-vue": "^2.3.1",
"@kjgl77/datav-vue3": "^1.7.4",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.7.9",
"datav-vue3": "^1.0.0",
"echarts": "^5.6.0",
@ -756,11 +758,21 @@
"node": ">=12.20"
}
},
"node_modules/@transloadit/prettier-bytes": {
"version": "0.0.7",
"resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
"integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA=="
},
"node_modules/@types/estree": {
"version": "1.0.6",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true
},
"node_modules/@types/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ=="
},
"node_modules/@types/lodash": {
"version": "4.17.15",
"integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw=="
@ -780,6 +792,56 @@
"undici-types": "~6.20.0"
}
},
"node_modules/@uppy/companion-client": {
"version": "2.2.2",
"resolved": "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz",
"integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==",
"dependencies": {
"@uppy/utils": "^4.1.2",
"namespace-emitter": "^2.0.1"
}
},
"node_modules/@uppy/core": {
"version": "2.3.4",
"resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
"dependencies": {
"@transloadit/prettier-bytes": "0.0.7",
"@uppy/store-default": "^2.1.1",
"@uppy/utils": "^4.1.3",
"lodash.throttle": "^4.1.1",
"mime-match": "^1.0.2",
"namespace-emitter": "^2.0.1",
"nanoid": "^3.1.25",
"preact": "^10.5.13"
}
},
"node_modules/@uppy/store-default": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz",
"integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ=="
},
"node_modules/@uppy/utils": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz",
"integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==",
"dependencies": {
"lodash.throttle": "^4.1.1"
}
},
"node_modules/@uppy/xhr-upload": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
"dependencies": {
"@uppy/companion-client": "^2.2.2",
"@uppy/utils": "^4.1.2",
"nanoid": "^3.1.25"
},
"peerDependencies": {
"@uppy/core": "^2.3.3"
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.1",
"integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==",
@ -950,6 +1012,156 @@
}
}
},
"node_modules/@wangeditor/basic-modules": {
"version": "1.1.7",
"resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
"dependencies": {
"is-url": "^1.2.4"
},
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"lodash.throttle": "^4.1.1",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/code-highlight": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz",
"integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==",
"dependencies": {
"prismjs": "^1.23.0"
},
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/core": {
"version": "1.1.19",
"resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
"dependencies": {
"@types/event-emitter": "^0.3.3",
"event-emitter": "^0.3.5",
"html-void-elements": "^2.0.0",
"i18next": "^20.4.0",
"scroll-into-view-if-needed": "^2.2.28",
"slate-history": "^0.66.0"
},
"peerDependencies": {
"@uppy/core": "^2.1.1",
"@uppy/xhr-upload": "^2.0.3",
"dom7": "^3.0.0",
"is-hotkey": "^0.2.0",
"lodash.camelcase": "^4.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.foreach": "^4.5.0",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"lodash.toarray": "^4.4.0",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/editor": {
"version": "5.1.23",
"resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz",
"integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
"dependencies": {
"@uppy/core": "^2.1.1",
"@uppy/xhr-upload": "^2.0.3",
"@wangeditor/basic-modules": "^1.1.7",
"@wangeditor/code-highlight": "^1.0.3",
"@wangeditor/core": "^1.1.19",
"@wangeditor/list-module": "^1.0.5",
"@wangeditor/table-module": "^1.1.4",
"@wangeditor/upload-image-module": "^1.0.2",
"@wangeditor/video-module": "^1.1.4",
"dom7": "^3.0.0",
"is-hotkey": "^0.2.0",
"lodash.camelcase": "^4.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.foreach": "^4.5.0",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"lodash.toarray": "^4.4.0",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/editor-for-vue": {
"version": "5.1.12",
"resolved": "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz",
"integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==",
"peerDependencies": {
"@wangeditor/editor": ">=5.1.0",
"vue": "^3.0.5"
}
},
"node_modules/@wangeditor/list-module": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz",
"integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==",
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/table-module": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz",
"integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==",
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/upload-image-module": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz",
"integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==",
"peerDependencies": {
"@uppy/core": "^2.0.3",
"@uppy/xhr-upload": "^2.0.3",
"@wangeditor/basic-modules": "1.x",
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"lodash.foreach": "^4.5.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/video-module": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz",
"integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
"peerDependencies": {
"@uppy/core": "^2.1.4",
"@uppy/xhr-upload": "^2.0.7",
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/alien-signals": {
"version": "0.4.14",
"integrity": "sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==",
@ -1038,10 +1250,27 @@
"node": ">= 0.8"
}
},
"node_modules/compute-scroll-into-view": {
"version": "1.0.20",
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="
},
"node_modules/csstype": {
"version": "3.1.3",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/d": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/d/-/d-1.0.2.tgz",
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"dependencies": {
"es5-ext": "^0.10.64",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/datav-vue3": {
"version": "1.0.0",
"integrity": "sha512-ehQgoAxyZHZwLtZBJ8mlEe41bOjGH816bPH0XhtjR6saVYUgt/HUslcE2M18lm0FfmTlLOcvPtvoDBlitIyehg==",
@ -1087,6 +1316,14 @@
"node": ">=0.10"
}
},
"node_modules/dom7": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz",
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
"dependencies": {
"ssr-window": "^3.0.0-alpha.1"
}
},
"node_modules/dot-prop": {
"version": "9.0.0",
"integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==",
@ -1195,10 +1432,47 @@
"node": ">=12.x"
}
},
"node_modules/es5-ext": {
"version": "0.10.64",
"resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.64.tgz",
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"hasInstallScript": true,
"dependencies": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
"esniff": "^2.0.1",
"next-tick": "^1.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
"dependencies": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"node_modules/es6-promise": {
"version": "4.2.8",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"node_modules/es6-symbol": {
"version": "3.1.4",
"resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz",
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
"dependencies": {
"d": "^1.0.2",
"ext": "^1.7.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/esbuild": {
"version": "0.24.2",
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
@ -1626,6 +1900,20 @@
"version": "1.0.3",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/esniff": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/esniff/-/esniff-2.0.1.tgz",
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
"dependencies": {
"d": "^1.0.1",
"es5-ext": "^0.10.62",
"event-emitter": "^0.3.5",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
@ -1640,6 +1928,23 @@
"url": "https://github.com/eta-dev/eta?sponsor=1"
}
},
"node_modules/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
"dependencies": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"node_modules/ext": {
"version": "1.7.0",
"resolved": "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz",
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
"dependencies": {
"type": "^2.7.2"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
@ -1719,10 +2024,36 @@
"node": ">=12.22.0"
}
},
"node_modules/html-void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz",
"integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/http-status-emojis": {
"version": "2.2.0",
"integrity": "sha512-ompKtgwpx8ff0hsbpIB7oE4ax1LXoHmftsHHStMELX56ivG3GhofTX8ZHWlUaFKfGjcGjw6G3rPk7dJRXMmbbg=="
},
"node_modules/i18next": {
"version": "20.6.1",
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz",
"integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==",
"dependencies": {
"@babel/runtime": "^7.12.0"
}
},
"node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "5.0.3",
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
@ -1763,6 +2094,11 @@
"node": ">=0.10.0"
}
},
"node_modules/is-hotkey": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw=="
},
"node_modules/is-number": {
"version": "7.0.0",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
@ -1772,6 +2108,19 @@
"node": ">=0.12.0"
}
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
},
"node_modules/json-server": {
"version": "1.0.0-beta.3",
"integrity": "sha512-DwE69Ep5ccwIJZBUIWEENC30Yj8bwr4Ax9W9VoIWAYnB8Sj4ReptscO8/DRHv/nXwVlmb3Bk73Ls86+VZdYkkA==",
@ -1824,6 +2173,42 @@
"lodash-es": "*"
}
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"node_modules/lodash.foreach": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead."
},
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
},
"node_modules/lodash.toarray": {
"version": "4.4.0",
"resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw=="
},
"node_modules/lowdb": {
"version": "7.0.1",
"integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==",
@ -1888,6 +2273,14 @@
"node": ">= 0.6"
}
},
"node_modules/mime-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz",
"integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
"dependencies": {
"wildcard": "^1.1.0"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
@ -1924,6 +2317,11 @@
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
"dev": true
},
"node_modules/namespace-emitter": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
"integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g=="
},
"node_modules/nanoid": {
"version": "3.3.8",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
@ -1947,6 +2345,11 @@
"node": ">= 0.6"
}
},
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
@ -2024,6 +2427,23 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/preact": {
"version": "10.26.4",
"resolved": "https://registry.npmmirror.com/preact/-/preact-10.26.4.tgz",
"integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prismjs": {
"version": "1.29.0",
"resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.29.0.tgz",
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
"engines": {
"node": ">=6"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
@ -2340,6 +2760,14 @@
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/scroll-into-view-if-needed": {
"version": "2.2.31",
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
"dependencies": {
"compute-scroll-into-view": "^1.0.20"
}
},
"node_modules/sirv": {
"version": "2.0.4",
"integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
@ -2352,6 +2780,35 @@
"node": ">= 10"
}
},
"node_modules/slate": {
"version": "0.72.8",
"resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz",
"integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
"dependencies": {
"immer": "^9.0.6",
"is-plain-object": "^5.0.0",
"tiny-warning": "^1.0.3"
}
},
"node_modules/slate-history": {
"version": "0.66.0",
"resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz",
"integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==",
"dependencies": {
"is-plain-object": "^5.0.0"
},
"peerDependencies": {
"slate": ">=0.65.3"
}
},
"node_modules/snabbdom": {
"version": "3.6.2",
"resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.6.2.tgz",
"integrity": "sha512-ig5qOnCDbugFntKi6c7Xlib8bA6xiJVk8O+WdFrV3wxbMqeHO0hXFQC4nAhPVWfZfi8255lcZkNhtIBINCc4+Q==",
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/sort-on": {
"version": "6.1.0",
"integrity": "sha512-WTECP0nYNWO1n2g5bpsV0yZN9cBmZsF8ThHFbOqVN0HBFRoaQZLLEMvMmJlKHNPYQeVngeI5+jJzIfFqOIo1OA==",
@ -2372,6 +2829,11 @@
"node": ">=0.10.0"
}
},
"node_modules/ssr-window": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz",
"integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA=="
},
"node_modules/steno": {
"version": "4.0.2",
"integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==",
@ -2382,6 +2844,11 @@
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
@ -2405,6 +2872,11 @@
"version": "2.3.0",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
},
"node_modules/type": {
"version": "2.7.3",
"resolved": "https://registry.npmmirror.com/type/-/type-2.7.3.tgz",
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
},
"node_modules/type-fest": {
"version": "4.33.0",
"integrity": "sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==",
@ -2586,6 +3058,11 @@
"resolved": "",
"link": true
},
"node_modules/wildcard": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz",
"integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng=="
},
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz",

View File

@ -12,6 +12,8 @@
"@dataview/datav-vue3": "^0.0.0-test.1672506674342",
"@element-plus/icons-vue": "^2.3.1",
"@kjgl77/datav-vue3": "^1.7.4",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.7.9",
"datav-vue3": "^1.0.0",
"echarts": "^5.6.0",

View File

@ -0,0 +1,67 @@
import request from '@/utils/request'
/**
* 获取游园需知列表
* @param {Object} params - 查询参数
* @returns {Promise} 返回游园需知列表数据
*/
export function getParkGuidelinesList(params = {}) {
return request.get('/api/admin/park-guidelines', { params })
}
/**
* 获取游园需知详情
* @param {string|number} id - 游园需知ID
* @returns {Promise} 返回游园需知详情
*/
export function getParkGuidelinesDetail(id) {
return request.get(`/api/admin/park-guidelines/${id}`)
}
/**
* 添加游园需知
* @param {Object} data - 游园需知数据
* @returns {Promise} 返回添加结果
*/
export function addParkGuidelines(data) {
return request.post('/api/admin/park-guidelines', data)
}
/**
* 更新游园需知
* @param {string|number} id - 游园需知ID
* @param {Object} data - 更新数据
* @returns {Promise} 返回更新结果
*/
export function updateParkGuidelines(id, data) {
return request.put(`/api/admin/park-guidelines/${id}`, data)
}
/**
* 删除游园需知
* @param {string|number} id - 游园需知ID
* @returns {Promise} 返回删除结果
*/
export function deleteParkGuidelines(id) {
return request.delete(`/api/admin/park-guidelines/${id}`)
}
/**
* 更新游园需知状态
* @param {string|number} id - 游园需知ID
* @param {Object} data - 状态数据 { status: number }
* @returns {Promise} 返回更新结果
*/
export function updateParkGuidelinesStatus(id, data) {
return request.put(`/api/admin/park-guidelines/${id}/status`, data)
}
/**
* 更新游园需知排序
* @param {string|number} id - 游园需知ID
* @param {Object} data - 排序数据 { sort_order: number }
* @returns {Promise} 返回更新结果
*/
export function updateParkGuidelinesSort(id, data) {
return request.put(`/api/admin/park-guidelines/${id}/sort`, data)
}

View File

@ -6,7 +6,11 @@ import request from '@/utils/request'
* @returns {Promise} 返回创建结果
*/
export function createActivity(data) {
return request.post('/api/education/activities', data)
return request.post('/api/education/activities', data, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
@ -51,7 +55,11 @@ export function batchCancelActivities(ids) {
* @returns {Promise} 返回更新结果
*/
export function updateActivity(id, data) {
return request.put(`/api/education/activities/${id}`, data)
return request.put(`/api/education/activities/${id}`, data, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
@ -88,4 +96,23 @@ export function getCategoryActivities(params = {}) {
...params
}
})
}
/**
* 获取活动报名记录
* @param {string|number} activityId - 活动ID
* @returns {Promise} 返回报名记录列表
*/
export function getActivityEnrollments(activityId) {
return request.get(`/api/admin/activity-enrollments/activities/${activityId}/enrollments`)
}
/**
* 更新报名状态
* @param {string|number} enrollmentId - 报名ID
* @param {number} status - 状态0-已取消 1-已报名 2-已完成
* @returns {Promise} 返回更新结果
*/
export function updateEnrollmentStatus(enrollmentId, status) {
return request.put(`/api/admin/activity-enrollments/enrollments/${enrollmentId}/status`, { status })
}

View File

@ -5,7 +5,7 @@ import request from '@/utils/request'
* @param {Object} params - 查询参数
* @param {number} [params.page=1] - 页码
* @param {number} [params.page_size=10] - 每页条数
* @param {string} [params.device_type] - 设备类型10000-摄像头10001-无人机
* @param {string} [params.device_type] - 设备类型10000-摄像头10001-无人机0-传感器
* @returns {Promise} 返回设备列表数据
*/
export function getDeviceList(params = {}) {

View File

@ -189,9 +189,10 @@ const handleLogout = () => {
<el-sub-menu index="projects">
<template #title>
<el-icon><component :is="icons.DataLine" /></el-icon>
<span>项目简介</span>
<span>关于我们</span>
</template>
<el-menu-item index="/projects">项目简介</el-menu-item>
<el-menu-item index="/about/projects">项目简介</el-menu-item>
<el-menu-item index="/about/needToKnow">游园需知</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>

View File

@ -188,10 +188,16 @@ const router = createRouter({
meta: { title: '数据管理', icon: 'data' }
},
{
path: 'projects',
path: 'about/projects',
name: 'Projects',
component: () => import('@/views/projects/index.vue'),
meta: { title: '项目简介管理', icon: 'data' }
component: () => import('@/views/about/projects/index.vue'),
meta: { title: '项目简介', icon: 'data' }
},
{
path: 'about/needToKnow',
name: 'NeedToKnow',
component: () => import('@/views/about/needToKnow/index.vue'),
meta: { title: '游园需知', icon: 'data' }
}
]
}

View File

@ -1,226 +1,265 @@
<script setup>
import { ref } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import { ref, onMounted } from 'vue'
import { Plus, Monitor, Timer } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getDeviceList, addDevice, updateDevice, deleteDevice } from '@/api/device'
//
const sensorList = ref([
{
id: 1,
name: '水质传感器01',
type: 'water',
status: 'online',
data: {
temperature: '25.6°C',
ph: '7.2',
oxygen: '6.8mg/L'
//
const loading = ref(false)
//
const sensorList = ref([])
//
const pagination = ref({
page: 1,
page_size: 10,
total: 0
})
//
const getSensorList = async () => {
loading.value = true
try {
const res = await getDeviceList({
page: pagination.value.page,
page_size: pagination.value.page_size,
device_type: 0 // 0
})
if (res.success) {
sensorList.value = res.data.list || []
if (res.data.pagination) {
pagination.value.total = res.data.pagination.total
}
} else {
ElMessage.error(res.message || '获取传感器列表失败')
}
},
{
id: 2,
name: '空气传感器01',
type: 'air',
status: 'online',
data: {
temperature: '28.3°C',
humidity: '65%',
pm25: '35μg/m³'
} catch (error) {
console.error('获取传感器列表失败:', error)
ElMessage.error('获取传感器列表失败')
} finally {
loading.value = false
}
}
//
const handleCurrentChange = (page) => {
pagination.value.page = page
getSensorList()
}
//
const handleSizeChange = (size) => {
pagination.value.page_size = size
pagination.value.page = 1
getSensorList()
}
//
const handleDelete = async (id) => {
try {
await ElMessageBox.confirm('确定要删除该传感器吗?', '提示', {
type: 'warning'
})
const res = await deleteDevice(id)
if (res.success) {
ElMessage.success('删除成功')
getSensorList()
} else {
ElMessage.error(res.message || '删除失败')
}
},
{
id: 3,
name: '土壤传感器01',
type: 'soil',
status: 'offline',
data: {
moisture: '42%',
ph: '6.8',
nutrients: '中等'
}
},
{
id: 4,
name: '水质传感器02',
type: 'water',
status: 'online',
data: {
temperature: '26.1°C',
ph: '7.4',
oxygen: '7.1mg/L'
}
},
{
id: 5,
name: '空气传感器02',
type: 'air',
status: 'error',
data: {
temperature: '27.8°C',
humidity: '58%',
pm25: '42μg/m³'
} catch (error) {
if (error !== 'cancel') {
console.error('删除传感器失败:', error)
ElMessage.error('删除失败')
}
}
])
}
//
const getStatusStyle = (status) => {
if (!status) return { color: '#909399', text: '未知' }
return {
online: {
1: {
color: '#67C23A',
text: '在线'
},
offline: {
0: {
color: '#909399',
text: '离线'
},
error: {
2: {
color: '#F56C6C',
text: '异常'
}
}[status]
}[status.code] || { color: '#909399', text: status.text || '未知' }
}
//
const getTypeInfo = (type) => {
return {
water: {
icon: 'WaterMeter',
text: '水质传感器',
0: {
icon: 'Monitor',
text: '传感器',
color: '#409EFF'
},
air: {
icon: 'Sunny',
text: '空气传感器',
color: '#67C23A'
},
soil: {
icon: 'Plant',
text: '土壤传感器',
color: '#E6A23C'
}
}[type]
}[type] || {
icon: 'Monitor',
text: '传感器',
color: '#409EFF'
}
}
//
const formatValue = (value, unit = '') => {
if (value === null || value === undefined) return '暂无数据'
return `${value}${unit}`
}
//
onMounted(() => {
getSensorList()
})
</script>
<template>
<div class="sensor-management">
<div class="page-header">
<div class="header-title">传感器管理</div>
<div class="header-title">
<el-icon><Monitor /></el-icon>
传感器管理
</div>
<el-button type="primary" :icon="Plus">添加传感器</el-button>
</div>
<div class="sensor-container">
<div class="sensor-container" v-loading="loading">
<div
v-for="sensor in sensorList"
:key="sensor.id"
class="sensor-card"
:class="{ 'offline': sensor.status === 'offline' }"
:class="{ 'offline': sensor.status?.code === 0 }"
>
<div class="card-header">
<div class="sensor-info">
<el-icon :class="sensor.type">
<component :is="getTypeInfo(sensor.type).icon" />
<el-icon :class="sensor.device_type">
<component :is="getTypeInfo(sensor.device_type).icon" />
</el-icon>
<span class="sensor-name">{{ sensor.name }}</span>
<el-tag
size="small"
:type="sensor.status === 'online' ? 'success' : sensor.status === 'error' ? 'danger' : 'info'"
>
{{ getStatusStyle(sensor.status).text }}
</el-tag>
</div>
<div class="sensor-actions">
<el-button type="primary" link>编辑</el-button>
<el-button type="danger" link>删除</el-button>
<span class="sensor-name">{{ sensor.device_name }}</span>
</div>
<el-tag
size="small"
:type="sensor.status?.code === 1 ? 'success' : sensor.status?.code === 2 ? 'danger' : 'info'"
>
{{ getStatusStyle(sensor.status).text }}
</el-tag>
</div>
<div class="card-content">
<template v-if="sensor.type === 'water'">
<div class="data-item">
<span class="label">温度</span>
<span class="value">{{ sensor.data.temperature }}</span>
<div class="info-section">
<div class="info-item">
<span class="label">设备编号</span>
<span class="value">{{ sensor.device_code }}</span>
</div>
<div class="data-item">
<span class="label">pH值</span>
<span class="value">{{ sensor.data.ph }}</span>
<div class="info-item">
<span class="label">安装位置</span>
<span class="value">{{ sensor.install_location || '暂无数据' }}</span>
</div>
<div class="data-item">
<span class="label">溶解氧</span>
<span class="value">{{ sensor.data.oxygen }}</span>
</div>
<div class="data-section">
<div class="data-grid">
<div class="data-item">
<div class="data-value">{{ formatValue(sensor.data?.temp, '°C') }}</div>
<div class="data-label">温度</div>
</div>
<div class="data-item">
<div class="data-value">{{ formatValue(sensor.data?.humi, '%') }}</div>
<div class="data-label">湿度</div>
</div>
<div class="data-item">
<div class="data-value">{{ formatValue(sensor.data?.light_adc) }}</div>
<div class="data-label">光照强度</div>
</div>
<div class="data-item">
<div class="data-value">{{ formatValue(sensor.data?.soil_adc) }}</div>
<div class="data-label">土壤湿度</div>
</div>
</div>
</template>
<template v-else-if="sensor.type === 'air'">
<div class="data-item">
<span class="label">温度</span>
<span class="value">{{ sensor.data.temperature }}</span>
</div>
<div class="footer-section">
<div class="update-time">
<el-icon><Timer /></el-icon>
{{ sensor.last_update_time ? new Date(sensor.last_update_time).toLocaleString() : '暂无数据' }}
</div>
<div class="data-item">
<span class="label">湿度</span>
<span class="value">{{ sensor.data.humidity }}</span>
<div class="actions">
<el-button type="primary" link>编辑</el-button>
<el-button type="danger" link @click="handleDelete(sensor.id)">删除</el-button>
</div>
<div class="data-item">
<span class="label">PM2.5</span>
<span class="value">{{ sensor.data.pm25 }}</span>
</div>
</template>
<template v-else>
<div class="data-item">
<span class="label">湿度</span>
<span class="value">{{ sensor.data.moisture }}</span>
</div>
<div class="data-item">
<span class="label">pH值</span>
<span class="value">{{ sensor.data.ph }}</span>
</div>
<div class="data-item">
<span class="label">养分</span>
<span class="value">{{ sensor.data.nutrients }}</span>
</div>
</template>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-container" v-if="pagination.total > 10">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.page_size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.sensor-management {
padding: 16px;
padding: 20px;
height: 100%;
box-sizing: border-box;
background: #f5f7fa;
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 0 4px;
.header-title {
font-size: 20px;
font-size: 22px;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 8px;
.el-icon {
font-size: 24px;
color: #409EFF;
}
}
}
.sensor-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
padding: 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 20px;
padding: 20px 0;
position: relative;
min-height: 200px;
.sensor-card {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
border-radius: 12px;
overflow: hidden;
transition: all 0.3s;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
&.offline {
@ -229,35 +268,28 @@ const getTypeInfo = (type) => {
}
.card-header {
padding: 16px;
border-bottom: 1px solid #e4e7ed;
padding: 16px 20px;
border-bottom: 1px solid #f0f2f5;
display: flex;
justify-content: space-between;
align-items: center;
background: #fafafa;
.sensor-info {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
.el-icon {
font-size: 20px;
&.water {
color: #409EFF;
}
&.air {
color: #67C23A;
}
&.soil {
color: #E6A23C;
}
color: #409EFF;
background: rgba(64, 158, 255, 0.1);
padding: 8px;
border-radius: 8px;
}
.sensor-name {
font-size: 14px;
font-size: 15px;
font-weight: 500;
color: #333;
}
@ -265,31 +297,99 @@ const getTypeInfo = (type) => {
}
.card-content {
padding: 16px;
padding: 16px 20px;
.data-item {
.info-section {
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px dashed #f0f2f5;
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
.label {
color: #909399;
font-size: 13px;
}
.value {
color: #606266;
font-size: 13px;
font-family: monospace;
}
}
}
.data-section {
margin-bottom: 16px;
.data-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
.data-item {
background: #f8f9fb;
padding: 12px;
border-radius: 8px;
text-align: center;
.data-value {
font-size: 20px;
font-weight: 600;
color: #409EFF;
margin-bottom: 4px;
font-family: 'DIN Alternate', sans-serif;
}
.data-label {
font-size: 12px;
color: #909399;
}
}
}
}
.footer-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-top: 16px;
border-top: 1px dashed #f0f2f5;
&:last-child {
margin-bottom: 0;
.update-time {
color: #909399;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
.el-icon {
font-size: 14px;
}
}
.label {
color: #666;
font-size: 14px;
}
.value {
color: #333;
font-weight: 500;
font-size: 14px;
.actions {
display: flex;
gap: 12px;
}
}
}
}
}
.pagination-container {
padding: 16px 0;
display: flex;
justify-content: flex-end;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,515 @@
<script setup>
import { ref, reactive, onMounted, computed, shallowRef, onBeforeUnmount, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
import { getParkGuidelinesList, addParkGuidelines, updateParkGuidelines, deleteParkGuidelines, updateParkGuidelinesStatus, updateParkGuidelinesSort } from '@/api/about/parkGuidelines'
import { formatDateTime } from '@/utils/format'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
//
const queryParams = reactive({
page: 1,
page_size: 10,
title: '',
category: undefined,
status: undefined
})
//
const loading = ref(false)
const guidelinesList = ref([])
const pagination = reactive({
total: 0,
page: 1,
pageSize: 10
})
//
const categoryOptions = [
{ label: '注意事项', value: 'notice' },
{ label: '园区规则', value: 'rules' },
{ label: '设施说明', value: 'facilities' }
]
//
const dialogVisible = ref(false)
const dialogType = ref('add')
const dialogTitle = ref('添加游园需知')
// shallowRef
const editorRef = shallowRef()
// HTML
const valueHtml = ref('<p>请输入内容</p>')
//
const toolbarConfig = {
excludeKeys: [
'uploadImage',
'uploadVideo',
'insertTable',
'group-video',
'group-image',
'insertTable'
]
}
//
const editorConfig = {
placeholder: '请输入内容...',
autoFocus: false,
MENU_CONF: {}
}
//
const handleCreated = (editor) => {
editorRef.value = editor
}
//
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
//
const formRef = ref()
const formData = ref({
title: '',
content: '',
category: '',
sort_order: 0,
status: 1
})
//
watch(valueHtml, (html) => {
formData.value.content = html
})
//
const rules = {
title: [
{ required: true, message: '请输入标题', trigger: 'blur' },
{ max: 200, message: '长度不能超过200个字符', trigger: 'blur' }
],
content: [
{
required: true,
message: '请输入内容',
trigger: 'change',
validator: (rule, value, callback) => {
if (!valueHtml.value || valueHtml.value === '<p>请输入内容</p>' || valueHtml.value === '<p><br></p>') {
callback(new Error('请输入内容'))
} else {
callback()
}
}
}
],
category: [
{ required: true, message: '请选择类别', trigger: 'change' }
],
sort_order: [
{ required: true, message: '请输入排序顺序', trigger: 'blur' },
{ type: 'number', message: '排序顺序必须为数字', trigger: 'blur' }
]
}
//
const getList = async () => {
loading.value = true
try {
//
const params = {
...queryParams,
category: queryParams.category || undefined,
status: queryParams.status === '' ? undefined : queryParams.status
}
// undefined
Object.keys(params).forEach(key =>
params[key] === undefined && delete params[key]
)
const res = await getParkGuidelinesList(params)
if (res.success && res.data) {
//
let list = Array.isArray(res.data.list) ? [...res.data.list] : []
//
const searchText = queryParams.title?.trim().toLowerCase()
if (searchText) {
list = list.filter(item =>
item.title?.toLowerCase().includes(searchText)
)
}
guidelinesList.value = list
//
if (res.data.pagination) {
pagination.total = searchText ? list.length : Number(res.data.pagination.total) || 0
pagination.page = Number(res.data.pagination.current) || 1
pagination.pageSize = Number(res.data.pagination.page_size) || 10
}
} else {
guidelinesList.value = []
ElMessage.error(res.message || '获取数据失败')
}
} catch (error) {
console.error('获取游园需知列表失败:', error)
guidelinesList.value = []
ElMessage.error('获取游园需知列表失败')
} finally {
loading.value = false
}
}
//
const resetQuery = () => {
queryParams.title = ''
queryParams.category = undefined
queryParams.status = undefined
getList()
}
// /
const handleAddOrEdit = (type, row) => {
dialogType.value = type
dialogTitle.value = type === 'add' ? '添加游园需知' : '编辑游园需知'
dialogVisible.value = true
if (type === 'edit' && row) {
formData.value = { ...row }
valueHtml.value = row.content || '<p><br></p>'
} else {
formData.value = {
title: '',
content: '',
category: '',
sort_order: 0,
status: 1
}
valueHtml.value = '<p><br></p>'
}
}
//
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
const api = dialogType.value === 'add' ? addParkGuidelines : updateParkGuidelines
//
const submitData = {
title: formData.value.title,
content: valueHtml.value,
category: formData.value.category,
sort_order: formData.value.sort_order,
status: formData.value.status
}
const res = await api(
dialogType.value === 'add' ? submitData : formData.value.id,
submitData
)
if (res.success) {
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '修改成功')
dialogVisible.value = false
getList()
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error) {
console.error('提交失败:', error)
ElMessage.error('提交失败')
}
}
})
}
//
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该条游园需知吗?', '提示', {
type: 'warning'
})
const res = await deleteParkGuidelines(row.id)
if (res.success) {
ElMessage.success('删除成功')
getList()
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
}
}
//
const handleCurrentChange = (val) => {
queryParams.page = val
getList()
}
//
const handleSizeChange = (val) => {
queryParams.page_size = val
queryParams.page = 1
getList()
}
// HTML
const getPlainText = (html) => {
if (!html) return ''
const temp = document.createElement('div')
temp.innerHTML = html
return temp.textContent || temp.innerText || ''
}
onMounted(() => {
getList()
})
</script>
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-card class="search-container">
<el-form :model="queryParams" ref="queryForm" :inline="true">
<el-form-item label="标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入标题"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="类别" prop="category">
<el-select
v-model="queryParams.category"
placeholder="请选择类别"
clearable
style="width: 200px"
>
<el-option
v-for="item in categoryOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
clearable
style="width: 200px"
>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作按钮区域 -->
<el-card class="table-container">
<template #header>
<el-button type="primary" :icon="Plus" @click="handleAddOrEdit('add')">新增</el-button>
</template>
<!-- 表格区域 -->
<el-table
v-loading="loading"
:data="guidelinesList"
border
style="width: 100%"
row-key="id"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip align="center"/>
<el-table-column prop="category" label="类别" width="120" align="center">
<template #default="{ row }">
{{ categoryOptions.find(item => item.value === row.category)?.label || '-' }}
</template>
</el-table-column>
<el-table-column prop="content" label="内容" min-width="300" show-overflow-tooltip align="center">
<template #default="{ row }">
{{ getPlainText(row.content) }}
</template>
</el-table-column>
<el-table-column prop="sort_order" label="排序" width="80" align="center">
<template #default="{ row }">
<span>{{ row.sort_order }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180" align="center">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column prop="creator_name" label="创建人" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="updated_at" label="更新时间" width="180" align="center">
<template #default="{ row }">
{{ formatDateTime(row.updated_at) }}
</template>
</el-table-column>
<el-table-column prop="updater_name" label="更新人" width="100" align="center" show-overflow-tooltip />
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="handleAddOrEdit('edit', row)">编辑</el-button>
<el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
v-if="pagination.total > 10"
/>
</div>
</el-card>
<!-- 添加/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="800px"
append-to-body
destroy-on-close
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="100px"
>
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="类别" prop="category">
<el-select v-model="formData.category" placeholder="请选择类别" style="width: 100%">
<el-option label="注意事项" value="notice" />
<el-option label="园区规则" value="rules" />
<el-option label="设施说明" value="facilities" />
</el-select>
</el-form-item>
<el-form-item label="内容" prop="content">
<div class="editor-container">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:defaultConfig="toolbarConfig"
mode="default"
/>
<Editor
style="height: 300px"
v-model="valueHtml"
:defaultConfig="editorConfig"
mode="default"
@onCreated="handleCreated"
/>
</div>
</el-form-item>
<el-form-item label="排序顺序" prop="sort_order">
<el-input-number
v-model="formData.sort_order"
:min="0"
:max="9999"
controls-position="right"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitForm"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
.app-container {
padding: 20px;
.search-container {
margin-bottom: 20px;
}
.table-container {
margin-bottom: 20px;
:deep(.el-card__header) {
padding: 12px 20px;
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.dialog-footer {
text-align: right;
padding-top: 20px;
}
}
.editor-container {
border: 1px solid #ccc;
z-index: 100;
:deep(.w-e-text-container) {
min-height: 300px;
}
}
//
:deep(.w-e-toolbar) {
z-index: 2 !important;
}
:deep(.w-e-text-container) {
z-index: 1 !important;
}
</style>

View File

@ -1,8 +1,9 @@
<script setup>
import { ref, onMounted, watch } from 'vue'
import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
import { Plus, Edit, Delete, Search, Refresh } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/format'
import { reverseArray } from '@/utils/sort'
import {
getActivityList,
createActivity,
@ -10,32 +11,31 @@ import {
cancelActivity,
batchCancelActivities,
updateActivityStatus,
checkActivityCapacity
checkActivityCapacity,
getActivityEnrollments,
updateEnrollmentStatus
} from '@/api/activity/study'
import { useUserStore } from '@/stores/user'
//
const formatDate = (date) => {
if (!date) return ''
const d = new Date(date)
const pad = (num) => (num < 10 ? `0${num}` : num)
const year = d.getFullYear()
const month = pad(d.getMonth() + 1)
const day = pad(d.getDate())
const hours = pad(d.getHours())
const minutes = pad(d.getMinutes())
const seconds = pad(d.getSeconds())
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
//
const searchForm = ref({
//
const queryParams = reactive({
page: 1,
page_size: 10,
title: '',
category: '',
status: ''
category: undefined,
status: undefined
})
//
const loading = ref(false)
const tableData = ref([])
const selectedRows = ref([])
//
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
//
const categoryOptions = [
{ label: '实地考察', value: 'field_study' },
@ -51,25 +51,24 @@ const statusOptions = [
{ label: '已结束', value: 0 }
]
//
const tableData = ref([])
const allData = ref([]) //
const loading = ref(false)
const selectedRows = ref([])
//
const searchForm = ref({
title: '',
category: '',
status: ''
})
//
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
// store
const userStore = useUserStore()
//
//
const getList = async () => {
loading.value = true
try {
const res = await getActivityList()
if (res.success) {
//
allData.value = res.data.map(item => ({
//
let allData = res.data.map(item => ({
id: item.id,
title: item.title,
category: item.category,
@ -84,64 +83,67 @@ const getList = async () => {
status: item.status,
created_at: item.created_at,
updated_at: item.updated_at,
activity_code: item.activity_code
activity_code: item.activity_code,
image: item.image
}))
//
filterAndPaginateData()
//
allData = reverseArray(allData)
//
if (searchForm.value.title) {
const keyword = searchForm.value.title.toLowerCase()
allData = allData.filter(item =>
item.title.toLowerCase().includes(keyword)
)
}
if (searchForm.value.category) {
allData = allData.filter(item =>
item.category === searchForm.value.category
)
}
if (searchForm.value.status !== '') {
allData = allData.filter(item =>
item.status === Number(searchForm.value.status)
)
}
//
total.value = allData.length
//
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
tableData.value = allData.slice(start, end)
} else {
ElMessage.error(res.message || '获取研学活动列表失败')
}
} catch (error) {
console.error('获取研学活动列表错误:', error)
ElMessage.error('获取研学活动列表失败')
} finally {
loading.value = false
}
}
//
const filterAndPaginateData = () => {
// 1.
let filteredData = [...allData.value]
//
const handleCurrentChange = (val) => {
currentPage.value = val
getList()
}
//
if (searchForm.value.title) {
const keyword = searchForm.value.title.toLowerCase()
filteredData = filteredData.filter(item =>
item.title.toLowerCase().includes(keyword)
)
}
//
if (searchForm.value.category) {
filteredData = filteredData.filter(item =>
item.category === searchForm.value.category
)
}
// -
if (searchForm.value.status !== '') {
filteredData = filteredData.filter(item =>
item.status === Number(searchForm.value.status)
)
}
// id
filteredData.sort((a, b) => a.id - b.id)
// 2.
total.value = filteredData.length
// 3.
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
tableData.value = filteredData.slice(start, end)
//
const handleSizeChange = (val) => {
pageSize.value = val
currentPage.value = 1
getList()
}
//
const handleSearch = () => {
currentPage.value = 1 //
filterAndPaginateData()
getList()
}
//
@ -152,7 +154,7 @@ const resetSearch = () => {
status: ''
}
currentPage.value = 1
filterAndPaginateData()
getList()
}
//
@ -163,23 +165,16 @@ const handleClear = (field) => {
//
watch([currentPage, pageSize], () => {
filterAndPaginateData()
getList()
})
//
watch(searchForm, (newVal, oldVal) => {
//
if (oldVal.status !== '' && newVal.status === '') {
//
currentPage.value = 1
//
filterAndPaginateData()
return
}
//
currentPage.value = 1
filterAndPaginateData()
getList()
}, { deep: true })
//
@ -191,6 +186,7 @@ const handleRefresh = () => {
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formMode = ref('create')
const formRef = ref()
const form = ref({
title: '',
category: 'field_study',
@ -201,7 +197,9 @@ const form = ref({
description: '',
requirements: '',
cost: 50.00,
status: 1
status: 1,
image_url: null,
imageUrl: ''
})
//
@ -226,9 +224,48 @@ const formRules = {
cost: [
{ required: true, message: '请输入活动费用', trigger: 'blur' },
{ type: 'number', min: 0, message: '费用不能小于0', trigger: 'blur' }
],
image_url: [
{ required: true, message: '请上传活动图片', trigger: 'change' }
]
}
//
const beforeImageUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB')
return false
}
return true
}
//
const handleImageChange = (uploadFile) => {
const file = uploadFile.raw
if (!file) return
form.value.image_url = file
form.value.imageUrl = URL.createObjectURL(file)
}
//
const handleImageRemove = () => {
form.value.imageUrl = ''
form.value.image_url = null
//
if (formRef.value) {
formRef.value.validateField('image_url')
}
}
//
const handleAdd = () => {
formMode.value = 'create'
@ -243,7 +280,9 @@ const handleAdd = () => {
description: '',
requirements: '',
cost: 50.00,
status: 1
status: 1,
image_url: null,
imageUrl: ''
}
dialogVisible.value = true
}
@ -254,42 +293,68 @@ const handleEdit = (row) => {
dialogTitle.value = '编辑研学活动'
form.value = {
...row,
category: row.category || 'field_study'
category: row.category || 'field_study',
imageUrl: row.image || '',
image_url: null //
}
dialogVisible.value = true
}
//
const formRef = ref(null)
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
//
const submitData = {
title: form.value.title.trim(),
category: form.value.category,
start_time: form.value.start_time ? formatDate(new Date(form.value.start_time)) : '',
end_time: form.value.end_time ? formatDate(new Date(form.value.end_time)) : '',
location: form.value.location.trim(),
capacity: Number(form.value.capacity),
description: (form.value.description || '').trim(),
requirements: (form.value.requirements || '').trim(),
cost: Number(form.value.cost),
status: Number(form.value.status)
// FormData
const formData = new FormData()
//
formData.append('title', form.value.title.trim())
formData.append('category', form.value.category)
// - 使MySQL
if (form.value.start_time) {
const startDate = new Date(form.value.start_time)
const formattedStartTime = formatDate(startDate)
formData.append('start_time', formattedStartTime)
}
if (form.value.end_time) {
const endDate = new Date(form.value.end_time)
const formattedEndTime = formatDate(endDate)
formData.append('end_time', formattedEndTime)
}
//
formData.append('capacity', form.value.capacity.toString())
formData.append('cost', form.value.cost.toString())
formData.append('status', form.value.status.toString())
//
formData.append('location', form.value.location.trim())
if (form.value.description) {
formData.append('description', form.value.description.trim())
}
if (form.value.requirements) {
formData.append('requirements', form.value.requirements.trim())
}
//
if (form.value.image_url instanceof File) {
formData.append('image_url', form.value.image_url)
}
//
if (!submitData.title || !submitData.start_time || !submitData.end_time || !submitData.location) {
if (!formData.get('title') || !formData.get('start_time') || !formData.get('end_time') || !formData.get('location')) {
ElMessage.error('请填写必填字段')
return
}
//
const startTime = new Date(submitData.start_time)
const endTime = new Date(submitData.end_time)
const startTime = new Date(formData.get('start_time'))
const endTime = new Date(formData.get('end_time'))
const now = new Date()
//
@ -298,24 +363,12 @@ const handleSubmit = async () => {
return
}
//
if (formMode.value === 'edit') {
//
const hoursUntilEnd = (endTime - now) / (1000 * 60 * 60)
// 2
if (endTime < now || hoursUntilEnd <= 2) {
ElMessage.warning(endTime < now ? '活动已结束,无法更新活动信息' : '距离活动结束不足2小时无法更新活动信息')
return
}
}
let res
if (formMode.value === 'create') {
res = await createActivity(submitData)
res = await createActivity(formData)
} else {
const id = form.value.id
res = await updateActivity(id, submitData)
res = await updateActivity(id, formData)
}
if (res.success) {
@ -326,8 +379,11 @@ const handleSubmit = async () => {
ElMessage.error(res.message || `${formMode.value === 'create' ? '新增' : '编辑'}失败`)
}
} catch (error) {
console.error(`${formMode.value === 'create' ? '新增' : '编辑'}研学活动错误:`, error)
ElMessage.error('操作失败,请检查输入是否正确')
if (error.response) {
ElMessage.error(error.response.data.message || '操作失败,请检查输入是否正确')
} else {
ElMessage.error('操作失败,请检查网络连接')
}
}
}
})
@ -419,26 +475,110 @@ const handleStatusChange = async (row) => {
}
}
//
const checkCapacity = async (row) => {
//
const enrollmentDialogVisible = ref(false)
const enrollmentLoading = ref(false)
const enrollmentList = ref([])
const currentActivity = ref(null)
//
const getEnrollmentDetails = async (row) => {
if (!row || !row.id) {
ElMessage.error('无效的活动ID')
return
}
enrollmentLoading.value = true
currentActivity.value = row
enrollmentDialogVisible.value = true
try {
const res = await checkActivityCapacity(row.id)
const activityId = row.id
const res = await getActivityEnrollments(activityId)
if (res.success) {
ElMessage.success(`当前报名人数:${res.data.currentParticipants}/${res.data.maxParticipants}`)
// enrollmentList
enrollmentList.value = Array.isArray(res.data.list) ? res.data.list : []
} else {
ElMessage.error(res.message || '检查容量失败')
ElMessage.error(res.message || '获取报名详情失败')
enrollmentList.value = [] //
}
} catch (error) {
console.error('检查研学活动容量错误:', error)
ElMessage.error('检查容量失败')
console.error('获取报名详情失败:', error)
ElMessage.error('获取报名详情失败')
enrollmentList.value = [] //
} finally {
enrollmentLoading.value = false
}
}
//
const getEnrollmentStatusType = (status) => {
const statusMap = {
0: 'info',
1: 'success',
2: 'warning'
}
return statusMap[status] || 'info'
}
//
const getEnrollmentStatusText = (status) => {
const statusMap = {
0: '已取消',
1: '已报名',
2: '已完成'
}
return statusMap[status] || '未知'
}
// checkCapacity
const checkCapacity = async (row) => {
if (!row || !row.id) {
ElMessage.error('无效的活动ID')
return
}
//
getEnrollmentDetails(row)
}
//
const handleSelectionChange = (rows) => {
selectedRows.value = rows
}
//
const formatDate = (date) => {
if (!date) return ''
const d = new Date(date)
const pad = (num) => (num < 10 ? `0${num}` : num)
const year = d.getFullYear()
const month = pad(d.getMonth() + 1)
const day = pad(d.getDate())
const hours = pad(d.getHours())
const minutes = pad(d.getMinutes())
const seconds = pad(d.getSeconds())
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
//
const handleEnrollmentStatusChange = async (row) => {
try {
const res = await updateEnrollmentStatus(row.id, row.status)
if (res.success) {
ElMessage.success('状态更新成功')
//
getEnrollmentDetails(currentActivity.value)
} else {
ElMessage.error(res.message || '状态更新失败')
}
} catch (error) {
console.error('更新报名状态失败:', error)
ElMessage.error('更新报名状态失败')
}
}
onMounted(() => {
getList()
})
@ -534,7 +674,7 @@ onMounted(() => {
<el-table-column prop="enrolled" label="已报名" width="100" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="checkCapacity(row)">
{{ row.enrolled }}/{{ row.capacity }}
{{ row.enrolled ?? 0 }}/{{ row.capacity ?? 0 }}
</el-button>
</template>
</el-table-column>
@ -563,6 +703,11 @@ onMounted(() => {
</el-tag>
</template>
</el-table-column>
<el-table-column prop="user_id" label="用户昵称" min-width="120" align="center">
<template #default="{ row }">
{{ userStore.userInfo?.nickname || userStore.userInfo?.username || row.user_id }}
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right" align="center">
<template #default="{ row }">
<el-button
@ -688,6 +833,24 @@ onMounted(() => {
/>
</el-select>
</el-form-item>
<el-form-item label="活动图片" prop="image_url" required>
<el-upload
class="activity-image-uploader"
action="#"
:show-file-list="false"
:on-change="handleImageChange"
:before-upload="beforeImageUpload"
:auto-upload="false"
accept="image/*"
>
<img v-if="form.imageUrl" :src="form.imageUrl" class="activity-image" />
<el-icon v-else class="activity-image-uploader-icon"><Plus /></el-icon>
</el-upload>
<div v-if="form.imageUrl" class="image-actions">
<el-button type="danger" link @click="handleImageRemove">移除图片</el-button>
</div>
<div class="image-tip">建议尺寸750x422px格式JPGPNG大小不超过2MB</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
@ -696,6 +859,87 @@ onMounted(() => {
</span>
</template>
</el-dialog>
<!-- 报名详情对话框 -->
<el-dialog
v-model="enrollmentDialogVisible"
:title="currentActivity?.title ? `报名详情 - ${currentActivity.title}` : '报名详情'"
width="900px"
destroy-on-close
>
<div class="enrollment-dialog-content" v-loading="enrollmentLoading">
<div class="enrollment-summary">
<div class="summary-item">
<span class="label">总人数上限</span>
<span class="value">{{ currentActivity?.capacity ?? 0 }}</span>
</div>
<div class="summary-item">
<span class="label">已报名人数</span>
<span class="value">{{ currentActivity?.enrolled ?? 0 }}</span>
</div>
<div class="summary-item">
<span class="label">剩余名额</span>
<span class="value">{{ (currentActivity?.capacity ?? 0) - (currentActivity?.enrolled ?? 0) }}</span>
</div>
</div>
<el-table :data="enrollmentList" style="width: 100%" border>
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column prop="user_id" label="用户昵称" min-width="120" align="center">
<template #default="{ row }">
{{ userStore.userInfo?.nickname || userStore.userInfo?.username || row.user_id }}
</template>
</el-table-column>
<el-table-column prop="enrollment_time" label="报名时间" min-width="180" align="center">
<template #default="{ row }">
{{ formatDateTime(row.enrollment_time) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120" align="center">
<template #default="{ row }">
<el-select
v-model="row.status"
placeholder="请选择状态"
size="small"
style="width: 100px"
@change="() => handleEnrollmentStatusChange(row)"
>
<el-option label="已取消" :value="0" />
<el-option label="已报名" :value="1" />
<el-option label="已完成" :value="2" />
</el-select>
</template>
</el-table-column>
<el-table-column prop="feedback" label="活动反馈" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<el-popover
placement="top-start"
trigger="hover"
:width="300"
v-if="row.feedback"
>
<template #default>
<div style="max-height: 200px; overflow-y: auto;">{{ row.feedback }}</div>
</template>
<template #reference>
<span>{{ row.feedback }}</span>
</template>
</el-popover>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="feedback_time" label="反馈时间" min-width="180" align="center">
<template #default="{ row }">
{{ row.feedback_time ? formatDateTime(row.feedback_time) : '-' }}
</template>
</el-table-column>
</el-table>
<div class="enrollment-empty" v-if="!enrollmentLoading && (!enrollmentList || enrollmentList.length === 0)">
<el-empty description="暂无报名数据" />
</div>
</div>
</el-dialog>
</div>
</template>
@ -753,5 +997,81 @@ onMounted(() => {
.text-center {
text-align: center;
}
.activity-image-uploader {
:deep(.el-upload) {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color 0.3s;
&:hover {
border-color: #409EFF;
}
}
}
.activity-image-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
line-height: 178px;
}
.activity-image {
width: 178px;
height: 178px;
display: block;
object-fit: cover;
}
.image-tip {
font-size: 12px;
color: #909399;
margin-top: 8px;
line-height: 1.4;
}
.image-actions {
margin-top: 8px;
display: flex;
justify-content: flex-start;
}
.enrollment-dialog-content {
.enrollment-summary {
display: flex;
gap: 40px;
margin-bottom: 20px;
padding: 15px 20px;
background-color: #f8f9fa;
border-radius: 8px;
.summary-item {
display: flex;
align-items: center;
gap: 8px;
.label {
color: #606266;
font-size: 14px;
}
.value {
color: #409EFF;
font-size: 16px;
font-weight: 600;
}
}
}
.enrollment-empty {
padding: 40px 0;
}
}
}
</style>

View File

@ -31,9 +31,19 @@ const categoryOptions = [
{ label: '植物', value: 'plant' }
]
//
const protectionLevelOptions = [
{ label: '国家一级', value: 'national_first' },
{ label: '国家二级', value: 'national_second' },
{ label: '省级', value: 'provincial' },
{ label: '普通', value: 'normal' }
]
//
const categoryChartRef = ref(null)
let categoryChart = null
const protectionChartRef = ref(null)
let protectionChart = null
//
const statsCards = ref([
@ -267,6 +277,96 @@ const updateDistributionChart = (data) => {
})
}
//
const initProtectionChart = () => {
if (!protectionChartRef.value) return
protectionChart = echarts.init(protectionChartRef.value)
const option = {
title: {
text: '保护等级统计',
textStyle: {
fontSize: 16,
fontWeight: 500,
color: '#303133'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: '{b}: {c}种'
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: [],
axisLabel: {
interval: 0,
rotate: 30
}
},
yAxis: {
type: 'value',
name: '物种数量',
minInterval: 1
},
series: [
{
name: '物种数量',
type: 'bar',
barWidth: '40%',
data: [],
label: {
show: true,
position: 'top',
formatter: '{c}种'
}
}
]
}
protectionChart.setOption(option)
}
//
const updateProtectionChart = (data) => {
if (!protectionChart) return
//
const protectionData = Object.entries(data.protection_levels)
.filter(([_, count]) => count > 0)
.map(([level, count]) => ({
name: protectionLevelOptions.find(item => item.value === level)?.label || level,
value: count
}))
.sort((a, b) => b.value - a.value)
protectionChart.setOption({
xAxis: {
data: protectionData.map(item => item.name)
},
series: [{
data: protectionData.map(item => ({
value: item.value,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#409EFF' },
{ offset: 1, color: '#2c76c5' }
])
}
}))
}]
})
}
//
const initData = async () => {
try {
@ -551,6 +651,9 @@ const fetchStatisticsData = async () => {
//
updateCategoryChart(res.data)
//
updateProtectionChart(res.data)
}
} catch (error) {
console.error('获取统计数据失败:', error)
@ -560,11 +663,13 @@ const fetchStatisticsData = async () => {
onMounted(() => {
initData()
startAutoRefresh()
initTrendChart();
initDistributionChart();
initTrendChart()
initDistributionChart()
initCategoryChart()
initProtectionChart()
window.addEventListener('resize', () => {
categoryChart?.resize()
protectionChart?.resize()
})
});
@ -576,8 +681,13 @@ onUnmounted(() => {
categoryChart.dispose()
categoryChart = null
}
if (protectionChart) {
protectionChart.dispose()
protectionChart = null
}
window.removeEventListener('resize', () => {
categoryChart?.resize()
protectionChart?.resize()
})
})
</script>
@ -629,6 +739,10 @@ onUnmounted(() => {
<div class="chart-title">趋势统计</div>
<div id="trendChart" class="chart-content"></div>
</div>
<div class="chart-item">
<div class="chart-title">保护等级统计</div>
<div ref="protectionChartRef" class="chart-content"></div>
</div>
</div>
</div>
</template>
@ -791,18 +905,18 @@ onUnmounted(() => {
}
.charts-container {
display: flex;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-top: 20px;
margin-bottom: 20px;
.chart-item {
flex: 1;
.chart-title {
font-size: 16px;
font-weight: 500;
color: v.$text-primary;
margin-bottom: 8px;
margin-bottom: 16px;
}
.chart-content {

View File

@ -93,115 +93,6 @@ const rules = {
]
}
//
const statistics = ref({
categories: {},
protection_levels: {}
})
//
const protectionChartRef = ref(null)
let protectionChart = null
// URL
const baseUrl = computed(() => import.meta.env.VITE_API_BASE_URL || '')
// URL
const getFullImageUrl = (url) => {
if (!url) return ''
if (url.startsWith('http')) return url
if (url.startsWith('data:')) return url
if (url.startsWith('blob:')) return url
// URL
const cleanUrl = url.startsWith('/') ? url.slice(1) : url
// uploads
if (cleanUrl.startsWith('uploads/')) {
return `${baseUrl.value}/${cleanUrl}`
}
return `${baseUrl.value}/uploads/${cleanUrl}`
}
//
const initCharts = () => {
if (protectionChartRef.value) {
protectionChart = echarts.init(protectionChartRef.value)
}
}
//
const updateCharts = () => {
//
const protectionData = Object.entries(statistics.value.protection_levels)
.filter(([_, count]) => count > 0)
.map(([level, count]) => ({
name: protectionLevelOptions.find(item => item.value === level)?.label || level,
value: count
}))
.sort((a, b) => b.value - a.value)
//
protectionChart?.setOption({
title: {
text: '保护等级统计',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: '{b}: {c}种'
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: protectionData.map(item => item.name),
axisLabel: {
interval: 0,
rotate: 30
}
},
yAxis: {
type: 'value',
name: '物种数量',
minInterval: 1
},
series: [
{
name: '物种数量',
type: 'bar',
barWidth: '40%',
data: protectionData.map(item => ({
value: item.value,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#409EFF' },
{ offset: 1, color: '#2c76c5' }
])
}
})),
label: {
show: true,
position: 'top',
formatter: '{c}种'
}
}
]
})
}
//
const handleResize = () => {
protectionChart?.resize()
}
//
const getList = async () => {
loading.value = true
@ -401,8 +292,6 @@ const getStatistics = async () => {
try {
const res = await getSpeciesStatistics()
statistics.value = res.data
//
updateCharts()
} catch (error) {
console.error('获取统计信息失败:', error)
}
@ -443,32 +332,37 @@ const handleSortChange = ({ prop, order }) => {
}
}
// URL
const baseUrl = computed(() => import.meta.env.VITE_API_BASE_URL || '')
// URL
const getFullImageUrl = (url) => {
if (!url) return ''
if (url.startsWith('http')) return url
if (url.startsWith('data:')) return url
if (url.startsWith('blob:')) return url
// URL
const cleanUrl = url.startsWith('/') ? url.slice(1) : url
// uploads
if (cleanUrl.startsWith('uploads/')) {
return `${baseUrl.value}/${cleanUrl}`
}
return `${baseUrl.value}/uploads/${cleanUrl}`
}
onMounted(() => {
getList()
//
initCharts()
getStatistics()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
//
protectionChart?.dispose()
window.removeEventListener('resize', handleResize)
//
})
</script>
<template>
<div class="app-container">
<!-- 统计信息展示 -->
<el-row :gutter="20" class="statistics-container">
<el-col :span="24">
<el-card>
<div ref="protectionChartRef" style="height: 400px"></div>
</el-card>
</el-col>
</el-row>
<!-- 搜索区域 -->
<el-card class="search-container">
<el-form :model="queryParams" ref="queryForm" :inline="true">
@ -732,30 +626,6 @@ onUnmounted(() => {
padding: 10px 20px;
}
.statistics-container {
margin-bottom: 20px;
.card-header {
font-weight: bold;
}
.statistics-content {
.statistics-item {
display: flex;
align-items: center;
margin-bottom: 10px;
span {
margin-right: 10px;
}
&:last-child {
margin-bottom: 0;
}
}
}
}
.species-image {
width: 60px;
height: 60px;