Commit 8bf47f2e by Performance System

1

parent 2611dc34
Pipeline #3155 passed with stage
in 55 seconds
......@@ -5,7 +5,7 @@ import os
import sys
from pathlib import Path
PORT = 5174
PORT = 4001
class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
......
......@@ -162,8 +162,8 @@
<h3>🔧 解决方案</h3>
<ol>
<li><strong>推荐方式</strong>:运行 <code>启动v86优化版本.bat</code> 脚本</li>
<li>或者在命令行运行:<code>python -m http.server 5174</code></li>
<li>然后访问:<code>http://localhost:5174</code></li>
<li>或者在命令行运行:<code>python -m http.server 4001</code></li>
<li>然后访问:<code>http://localhost:4001</code></li>
<li>如果没有Python,可以安装Node.js后运行:<code>npx serve .</code></li>
</ol>
</div>
......
......@@ -12,10 +12,12 @@
"cors": "^2.8.5",
"element-plus": "^2.4.4",
"express": "^5.1.0",
"file-saver": "^2.0.5",
"html2canvas": "^1.4.1",
"jspdf": "^3.0.1",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"uuid": "^11.1.0",
"vue": "^3.3.8",
"vue-router": "^4.2.5",
......@@ -60,6 +62,15 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
......@@ -804,21 +815,6 @@
"win32"
]
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
......@@ -845,11 +841,28 @@
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~7.10.0"
}
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
......@@ -1066,13 +1079,25 @@
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT"
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": "^4.5.0 || >= 5.9"
"node": ">= 0.6.0"
}
},
"node_modules/body-parser": {
......@@ -1095,6 +1120,18 @@
"node": ">=18"
}
},
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
......@@ -1133,6 +1170,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
......@@ -1285,6 +1342,24 @@
"node": ">=6.6.0"
}
},
"node_modules/core-js": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz",
"integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
......@@ -1310,6 +1385,15 @@
"node": ">=0.8"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
......@@ -1348,6 +1432,16 @@
"node": ">= 0.8"
}
},
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
......@@ -1410,125 +1504,6 @@
"node": ">= 0.8"
}
},
"node_modules/engine.io": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/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==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io/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==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/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==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/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==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
......@@ -1683,6 +1658,18 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
"license": "MIT"
},
"node_modules/finalhandler": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
......@@ -1844,6 +1831,19 @@
"node": ">= 0.4"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
......@@ -1881,6 +1881,12 @@
"node": ">=0.10.0"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
......@@ -1912,6 +1918,51 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jspdf": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz",
"integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.7",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
......@@ -2082,6 +2133,12 @@
"wrappy": "1"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
......@@ -2100,6 +2157,13 @@
"node": ">=16"
}
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
......@@ -2156,6 +2220,12 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
......@@ -2184,6 +2254,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
......@@ -2208,6 +2288,34 @@
"node": ">= 0.8"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
......@@ -2218,6 +2326,16 @@
"node": ">=0.10.0"
}
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rollup": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
......@@ -2347,6 +2465,12 @@
"node": ">= 18"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
......@@ -2438,173 +2562,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/socket.io": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"license": "MIT",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/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==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/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==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/socket.io/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==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/socket.io/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==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
......@@ -2626,6 +2583,16 @@
"node": ">=0.8"
}
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
......@@ -2635,6 +2602,21 @@
"node": ">= 0.8"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
......@@ -2679,6 +2661,25 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
......@@ -2723,7 +2724,10 @@
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"license": "MIT"
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/unpipe": {
"version": "1.0.0",
......@@ -2734,6 +2738,21 @@
"node": ">= 0.8"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
......@@ -2920,27 +2939,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
......@@ -2962,14 +2960,6 @@
"node": ">=0.8"
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
......
......@@ -14,6 +14,10 @@
"cors": "^2.8.5",
"element-plus": "^2.4.4",
"express": "^5.1.0",
"file-saver": "^2.0.5",
"html2canvas": "^1.4.1",
"jspdf": "^3.0.1",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"uuid": "^11.1.0",
......
......@@ -2,7 +2,7 @@ const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 5174;
const PORT = 4001;
// MIME类型映射
const mimeTypes = {
......
......@@ -2118,6 +2118,110 @@ export const useDataStore = defineStore('data', () => {
}
/**
* 月度重置功能 - 每月1日自动执行
*/
const performMonthlyReset = () => {
try {
const currentDate = new Date()
const lastResetKey = 'last_monthly_reset'
const lastReset = localStorage.getItem(lastResetKey)
const currentMonthKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`
// 检查是否需要重置(每月只重置一次)
if (!lastReset || lastReset !== currentMonthKey) {
console.log('🔄 执行月度重置...')
// 1. 先保存当前月份的统计数据
const saveSuccess = saveCurrentMonthStats()
if (!saveSuccess) {
console.warn('⚠️ 保存月度统计失败,但继续执行重置')
}
// 2. 清空所有机构的图片上传记录
institutions.value.forEach(institution => {
if (institution.images && institution.images.length > 0) {
console.log(`清空机构 ${institution.name}${institution.images.length} 张图片`)
institution.images = []
}
})
// 3. 保存重置后的数据
saveToStorage()
// 4. 记录重置时间
localStorage.setItem(lastResetKey, currentMonthKey)
localStorage.setItem('last_reset_time', new Date().toISOString())
console.log(`✅ ${currentMonthKey} 月度重置完成`)
console.log('- 已保存上月统计数据到历史记录')
console.log('- 已清空所有机构的图片上传记录')
console.log('- 用户分数将自动重新计算')
return true
}
return false // 本月已重置过
} catch (error) {
console.error('月度重置失败:', error)
return false
}
}
/**
* 检查并执行月度重置(系统启动时调用)
*/
const checkAndPerformMonthlyReset = () => {
const currentDate = new Date()
// 只在每月1-3日检查是否需要重置(给一些缓冲时间)
if (currentDate.getDate() <= 3) {
console.log('🔍 检查是否需要执行月度重置...')
return performMonthlyReset()
}
return false
}
/**
* 手动执行月度重置(管理员功能)
*/
const manualMonthlyReset = () => {
try {
console.log('🔄 手动执行月度重置...')
// 1. 保存当前统计数据
const saveSuccess = saveCurrentMonthStats()
if (!saveSuccess) {
throw new Error('保存月度统计失败')
}
// 2. 清空所有机构的图片
let clearedCount = 0
institutions.value.forEach(institution => {
if (institution.images && institution.images.length > 0) {
clearedCount += institution.images.length
institution.images = []
}
})
// 3. 保存数据
saveToStorage()
// 4. 更新重置记录
const currentDate = new Date()
const currentMonthKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`
localStorage.setItem('last_monthly_reset', currentMonthKey)
localStorage.setItem('last_reset_time', new Date().toISOString())
console.log(`✅ 手动月度重置完成,清空了 ${clearedCount} 张图片`)
return { success: true, clearedCount }
} catch (error) {
console.error('手动月度重置失败:', error)
return { success: false, error: error.message }
}
}
/**
* 导出数据(用于备份)
*/
const exportData = () => {
......@@ -2307,6 +2411,9 @@ export const useDataStore = defineStore('data', () => {
getMonthStats,
deleteMonthStats,
clearAllHistoryStats,
autoSaveMonthlyStats
autoSaveMonthlyStats,
performMonthlyReset,
checkAndPerformMonthlyReset,
manualMonthlyReset
}
})
\ No newline at end of file
......@@ -360,6 +360,29 @@
<p class="history-subtitle">查看历史月份各用户的绩效数据</p>
</div>
<div class="header-actions">
<el-dropdown
@command="handleHistoryExportCommand"
:disabled="!selectedMonthData"
split-button
type="success"
@click="exportHistoryData('csv')"
:loading="exportHistoryLoading"
>
<el-icon><Download /></el-icon>
导出历史数据
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="csv">
<el-icon><List /></el-icon>
CSV格式
</el-dropdown-item>
<el-dropdown-item command="zip">
<el-icon><FolderOpened /></el-icon>
ZIP图片包
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button type="primary" @click="saveCurrentMonthStats" :loading="saveStatsLoading">
<el-icon><Download /></el-icon>
保存当前月份
......@@ -494,54 +517,7 @@
</div>
<el-row :gutter="16">
<!-- 数据修复 -->
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<div class="data-management-card repair">
<div class="card-header">
<el-icon class="card-icon repair"><Tools /></el-icon>
<h4>数据修复</h4>
</div>
<div class="card-content">
<p>修复图片归属错误和机构权限验证问题。</p>
<div class="repair-options">
<div class="repair-item">
<span class="label">图片归属修复:</span>
<span class="description">修复图片显示在错误机构的问题</span>
</div>
<div class="repair-item">
<span class="label">权限验证修复:</span>
<span class="description">修复机构权限验证错误</span>
</div>
<div class="repair-item">
<span class="label">权限测试:</span>
<span class="description">测试图片上传权限验证</span>
</div>
<div class="repair-item">
<span class="label">ID修复:</span>
<span class="description">修复虚拟/真实机构ID问题</span>
</div>
</div>
</div>
<div class="card-actions">
<el-button type="warning" @click="fixImageOwnership" :loading="fixImageLoading">
<el-icon><Tools /></el-icon>
修复图片归属
</el-button>
<el-button type="info" @click="fixPermissionErrors" :loading="fixPermissionLoading">
<el-icon><Lock /></el-icon>
修复权限验证
</el-button>
<el-button type="primary" @click="testPermissions" :loading="testPermissionLoading" size="small">
<el-icon><Search /></el-icon>
测试权限
</el-button>
<el-button type="success" @click="fixVirtualRealIds" :loading="fixVirtualRealLoading" size="small">
<el-icon><Setting /></el-icon>
修复ID问题
</el-button>
</div>
</div>
</el-col>
<!-- 数据备份 -->
<el-col :xs="24" :sm="24" :md="6" :lg="6" :xl="6">
......@@ -616,24 +592,32 @@
</div>
</el-col>
<!-- 数据重置 -->
<!-- 月度重置 -->
<el-col :xs="24" :sm="24" :md="6" :lg="6" :xl="6">
<div class="data-management-card danger">
<div class="data-management-card warning">
<div class="card-header">
<el-icon class="card-icon reset"><RefreshLeft /></el-icon>
<h4>数据重置</h4>
<el-icon class="card-icon monthly-reset"><Calendar /></el-icon>
<h4>月度重置</h4>
</div>
<div class="card-content">
<p>将系统重置为初始状态,恢复默认用户和清空所有机构数据。</p>
<div class="warning-notice">
<el-icon><WarningFilled /></el-icon>
<span>此操作不可逆,请谨慎操作!</span>
<p>手动执行月度重置,保存当前统计数据并清空所有图片上传记录。</p>
<div class="info-notice">
<el-icon><InfoFilled /></el-icon>
<span>仅支持手动执行,请根据需要进行重置</span>
</div>
<div class="reset-info">
<div class="info-item">
<span class="label">上次重置:</span>
<span class="value">{{ lastResetTime || '未重置' }}</span>
</div>
</div>
</div>
<div class="card-actions">
<el-button type="danger" @click="showResetConfirm" :loading="resetLoading">
<el-icon><RefreshLeft /></el-icon>
重置系统
<el-button type="warning" @click="showMonthlyResetConfirm" :loading="monthlyResetLoading">
<el-icon><Calendar /></el-icon>
执行月度重置
</el-button>
</div>
</div>
......@@ -891,14 +875,43 @@
</div>
</div>
<template #footer>
<el-button @click="userViewDialogVisible = false">取消</el-button>
<el-button
type="primary"
:disabled="!selectedViewUserId"
@click="switchToUserView"
>
切换视图
</el-button>
<div class="dialog-footer">
<div class="footer-left">
<el-dropdown
@command="handleUserExportCommand"
:disabled="!selectedViewUserId"
split-button
type="success"
@click="exportUserData('csv')"
:loading="exportUserLoading"
>
<el-icon><Download /></el-icon>
导出用户数据
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="csv">
<el-icon><List /></el-icon>
CSV格式
</el-dropdown-item>
<el-dropdown-item command="zip">
<el-icon><FolderOpened /></el-icon>
ZIP图片包
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="footer-right">
<el-button @click="userViewDialogVisible = false">取消</el-button>
<el-button
type="primary"
:disabled="!selectedViewUserId"
@click="switchToUserView"
>
切换视图
</el-button>
</div>
</div>
</template>
</el-dialog>
......@@ -990,10 +1003,20 @@ import {
Tools,
Lock,
Setting,
Delete
Delete,
Calendar,
InfoFilled,
Document,
Grid,
List,
Printer
} from '@element-plus/icons-vue'
import { useAuthStore } from '@/store/auth'
import { useDataStore } from '@/store/data'
import * as XLSX from 'xlsx'
import jsPDF from 'jspdf'
import JSZip from 'jszip'
import { saveAs } from 'file-saver'
/**
* 管理员控制面板组件
......@@ -1049,12 +1072,7 @@ const selectedInstitutionDetail = ref(null)
// 数据管理相关
const exportLoading = ref(false)
const importLoading = ref(false)
const resetLoading = ref(false)
const selectedFile = ref(null)
const fixImageLoading = ref(false)
const fixPermissionLoading = ref(false)
const testPermissionLoading = ref(false)
const fixVirtualRealLoading = ref(false)
// 历史统计相关变量
const selectedHistoryMonth = ref('')
......@@ -1062,6 +1080,14 @@ const selectedMonthData = ref(null)
const availableMonths = ref([])
const saveStatsLoading = ref(false)
const clearHistoryLoading = ref(false)
const exportHistoryLoading = ref(false)
// 用户视图导出相关变量
const exportUserLoading = ref(false)
// 月度重置相关变量
const monthlyResetLoading = ref(false)
const lastResetTime = ref('')
// 表单引用
const addUserFormRef = ref()
......@@ -1969,6 +1995,419 @@ const switchToUserView = () => {
}
/**
* 处理用户导出命令
*/
const handleUserExportCommand = (command) => {
exportUserData(command)
}
/**
* 导出选中用户的数据
*/
const exportUserData = async (format = 'json') => {
if (!selectedViewUserId.value || !selectedViewUser.value) {
ElMessage.error('请先选择用户')
return
}
try {
exportUserLoading.value = true
// 获取当前月份
const currentDate = new Date()
const currentMonth = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`
// 准备导出数据
const exportData = {
exportInfo: {
exportTime: new Date().toISOString(),
exportMonth: currentMonth,
exportType: '用户数据导出',
userName: selectedViewUser.value.name,
userId: selectedViewUser.value.id,
format: format
},
userData: {
id: selectedViewUser.value.id,
name: selectedViewUser.value.name,
phone: selectedViewUser.value.phone,
institutionCount: selectedUserInstitutions.value.length,
interactionScore: dataStore.calculateInteractionScore(selectedViewUser.value.id),
performanceScore: dataStore.calculatePerformanceScore(selectedViewUser.value.id)
},
institutions: selectedUserInstitutions.value.map(inst => ({
id: inst.id,
institutionId: inst.institutionId,
name: inst.name,
imageCount: inst.images ? inst.images.length : 0,
images: inst.images ? inst.images.map(img => ({
id: img.id,
name: img.name,
uploadTime: img.uploadTime,
size: img.size
})) : []
}))
}
// 根据格式导出
switch (format) {
case 'csv':
await exportUserDataAsCSV(exportData, selectedViewUser.value.name, currentMonth)
break
case 'zip':
await exportUserDataAsZIP(exportData, selectedViewUser.value.name, currentMonth)
break
default:
throw new Error(`不支持的导出格式: ${format}`)
}
ElMessage.success(`${selectedViewUser.value.name}${format.toUpperCase()}数据导出成功!`)
} catch (error) {
console.error('导出用户数据失败:', error)
ElMessage.error(`导出用户数据失败:${error.message}`)
} finally {
exportUserLoading.value = false
}
}
/**
* 导出用户数据为JSON格式
*/
const exportUserDataAsJSON = async (exportData, userName, currentMonth) => {
const jsonString = JSON.stringify(exportData, null, 2)
const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${userName}_${currentMonth}_数据导出.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
/**
* 导出用户数据为Excel格式
*/
const exportUserDataAsExcel = async (exportData, userName, currentMonth) => {
const workbook = XLSX.utils.book_new()
// 用户信息工作表
const userInfoData = [
['用户信息', ''],
['用户ID', exportData.userData.id],
['姓名', exportData.userData.name],
['手机号', exportData.userData.phone],
['负责机构数', exportData.userData.institutionCount],
['互动得分', exportData.userData.interactionScore],
['绩效得分', exportData.userData.performanceScore],
['', ''],
['导出信息', ''],
['导出时间', exportData.exportInfo.exportTime],
['导出月份', exportData.exportInfo.exportMonth],
['导出类型', exportData.exportInfo.exportType]
]
const userInfoSheet = XLSX.utils.aoa_to_sheet(userInfoData)
XLSX.utils.book_append_sheet(workbook, userInfoSheet, '用户信息')
// 机构详情工作表
const institutionHeaders = ['机构ID', '机构名称', '图片数量', '得分']
const institutionData = [institutionHeaders]
exportData.institutions.forEach(inst => {
const score = inst.imageCount === 0 ? 0 : inst.imageCount === 1 ? 0.5 : 1
institutionData.push([
inst.institutionId,
inst.name,
inst.imageCount,
score
])
})
const institutionSheet = XLSX.utils.aoa_to_sheet(institutionData)
XLSX.utils.book_append_sheet(workbook, institutionSheet, '机构详情')
// 图片详情工作表
const imageHeaders = ['机构名称', '图片名称', '上传时间', '文件大小']
const imageData = [imageHeaders]
exportData.institutions.forEach(inst => {
if (inst.images && inst.images.length > 0) {
inst.images.forEach(img => {
imageData.push([
inst.name,
img.name,
img.uploadTime,
img.size
])
})
}
})
const imageSheet = XLSX.utils.aoa_to_sheet(imageData)
XLSX.utils.book_append_sheet(workbook, imageSheet, '图片详情')
// 图片缩略图工作表
await addImageThumbnailsToWorkbook(workbook, exportData.institutions, '图片缩略图')
// 导出文件
XLSX.writeFile(workbook, `${userName}_${currentMonth}_数据导出.xlsx`)
}
/**
* 导出用户数据为CSV格式
*/
const exportUserDataAsCSV = async (exportData, userName, currentMonth) => {
const csvContent = []
// 用户信息部分
csvContent.push('用户信息')
csvContent.push(`用户ID,${exportData.userData.id}`)
csvContent.push(`姓名,${exportData.userData.name}`)
csvContent.push(`手机号,${exportData.userData.phone}`)
csvContent.push(`负责机构数,${exportData.userData.institutionCount}`)
csvContent.push(`互动得分,${exportData.userData.interactionScore}`)
csvContent.push(`绩效得分,${exportData.userData.performanceScore}`)
csvContent.push('')
// 机构详情部分
csvContent.push('机构详情')
csvContent.push('机构ID,机构名称,图片数量,得分')
exportData.institutions.forEach(inst => {
const score = inst.imageCount === 0 ? 0 : inst.imageCount === 1 ? 0.5 : 1
csvContent.push(`${inst.institutionId},${inst.name},${inst.imageCount},${score}`)
})
csvContent.push('')
// 图片详情部分
csvContent.push('图片详情')
csvContent.push('机构名称,图片名称,上传时间,文件大小')
exportData.institutions.forEach(inst => {
if (inst.images && inst.images.length > 0) {
inst.images.forEach(img => {
csvContent.push(`${inst.name},${img.name},${img.uploadTime},${img.size}`)
})
}
})
// 添加BOM以支持中文
const BOM = '\uFEFF'
const csvString = BOM + csvContent.join('\n')
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${userName}_${currentMonth}_数据导出.csv`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
/**
* 导出用户数据为PDF格式
*/
const exportUserDataAsPDF = async (exportData, userName, currentMonth) => {
const pdf = new jsPDF()
// 设置中文字体(使用默认字体,可能不支持中文,但保持功能完整)
pdf.setFont('helvetica')
let yPosition = 20
const lineHeight = 10
const pageHeight = pdf.internal.pageSize.height
// 添加标题
pdf.setFontSize(16)
pdf.text(`User Data Export - ${userName}`, 20, yPosition)
yPosition += lineHeight * 2
// 用户信息
pdf.setFontSize(14)
pdf.text('User Information:', 20, yPosition)
yPosition += lineHeight
pdf.setFontSize(12)
const userInfo = [
`User ID: ${exportData.userData.id}`,
`Name: ${exportData.userData.name}`,
`Phone: ${exportData.userData.phone}`,
`Institution Count: ${exportData.userData.institutionCount}`,
`Interaction Score: ${exportData.userData.interactionScore}`,
`Performance Score: ${exportData.userData.performanceScore}`
]
userInfo.forEach(info => {
if (yPosition > pageHeight - 20) {
pdf.addPage()
yPosition = 20
}
pdf.text(info, 20, yPosition)
yPosition += lineHeight
})
yPosition += lineHeight
// 机构详情
pdf.setFontSize(14)
if (yPosition > pageHeight - 20) {
pdf.addPage()
yPosition = 20
}
pdf.text('Institution Details:', 20, yPosition)
yPosition += lineHeight
pdf.setFontSize(12)
exportData.institutions.forEach((inst, index) => {
if (yPosition > pageHeight - 30) {
pdf.addPage()
yPosition = 20
}
const score = inst.imageCount === 0 ? 0 : inst.imageCount === 1 ? 0.5 : 1
pdf.text(`${index + 1}. ${inst.name} (ID: ${inst.institutionId})`, 20, yPosition)
yPosition += lineHeight
pdf.text(` Images: ${inst.imageCount}, Score: ${score}`, 20, yPosition)
yPosition += lineHeight
})
// 导出信息
yPosition += lineHeight
pdf.setFontSize(14)
if (yPosition > pageHeight - 20) {
pdf.addPage()
yPosition = 20
}
pdf.text('Export Information:', 20, yPosition)
yPosition += lineHeight
pdf.setFontSize(12)
pdf.text(`Export Time: ${exportData.exportInfo.exportTime}`, 20, yPosition)
yPosition += lineHeight
pdf.text(`Export Month: ${exportData.exportInfo.exportMonth}`, 20, yPosition)
// 保存PDF
pdf.save(`${userName}_${currentMonth}_数据导出.pdf`)
}
/**
* 导出用户数据为ZIP压缩包格式
*/
const exportUserDataAsZIP = async (exportData, userName, currentMonth) => {
try {
const zip = new JSZip()
// 创建用户数据摘要文件
const summaryData = {
exportInfo: exportData.exportInfo,
userData: exportData.userData,
institutionSummary: exportData.institutions.map(inst => ({
id: inst.id,
institutionId: inst.institutionId,
name: inst.name,
imageCount: inst.imageCount
}))
}
zip.file(`${userName}_数据摘要.json`, JSON.stringify(summaryData, null, 2))
// 按机构创建文件夹并添加图片
for (const institution of exportData.institutions) {
if (institution.images && institution.images.length > 0) {
const folderName = `${institution.name}_${institution.institutionId}`
for (const image of institution.images) {
try {
// 从Base64数据中提取图片数据
const base64Data = image.url.split(',')[1]
if (base64Data) {
// 获取文件扩展名
const mimeType = image.url.split(';')[0].split(':')[1]
const extension = mimeType.split('/')[1] || 'jpg'
const fileName = `${image.name.replace(/\.[^/.]+$/, '')}.${extension}`
zip.file(`${folderName}/${fileName}`, base64Data, { base64: true })
}
} catch (error) {
console.warn(`处理图片失败: ${image.name}`, error)
}
}
}
}
// 生成并下载ZIP文件
const content = await zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
})
saveAs(content, `${userName}_${currentMonth}_图片数据包.zip`)
} catch (error) {
console.error('生成ZIP文件失败:', error)
throw new Error('生成ZIP文件失败')
}
}
/**
* 为Excel工作簿添加图片缩略图工作表
*/
const addImageThumbnailsToWorkbook = async (workbook, institutions, sheetName) => {
try {
const imageData = [['用户姓名', '机构名称', '图片名称', '上传时间', '文件大小', '图片预览']]
institutions.forEach(inst => {
// 处理当前用户数据导出的情况
if (inst.images && inst.images.length > 0) {
inst.images.forEach(img => {
imageData.push([
inst.userName || '当前用户', // 用户姓名
inst.name,
img.name,
img.uploadTime,
img.size,
'图片数据' // 在实际实现中,这里可以是图片的缩略图
])
})
}
// 处理历史数据导出的情况(可能没有完整的图片信息)
else if (inst.imageCount > 0) {
imageData.push([
inst.userName || '历史用户',
inst.name,
`${inst.imageCount}张图片`,
'历史数据',
'历史数据',
'历史图片数据'
])
}
})
const imageSheet = XLSX.utils.aoa_to_sheet(imageData)
// 设置列宽
imageSheet['!cols'] = [
{ width: 15 }, // 用户姓名
{ width: 20 }, // 机构名称
{ width: 30 }, // 图片名称
{ width: 20 }, // 上传时间
{ width: 15 }, // 文件大小
{ width: 20 } // 图片预览
]
XLSX.utils.book_append_sheet(workbook, imageSheet, sheetName)
} catch (error) {
console.warn('添加图片缩略图工作表失败:', error)
}
}
/**
* 显示机构详情
*/
const showInstitutionDetail = (institution) => {
......@@ -2248,280 +2687,70 @@ const handleImportData = async () => {
}
/**
* 显示重置确认对话框
* 显示月度重置确认对话框
*/
const showResetConfirm = async () => {
const showMonthlyResetConfirm = async () => {
try {
await ElMessageBox.confirm(
'重置系统将清空所有用户数据和机构信息,恢复为初始状态。此操作不可逆!确定要继续吗?',
'确认重置系统',
'月度重置将执行以下操作:\n' +
'• 保存当前月份的统计数据到历史记录\n' +
'• 清空所有机构的图片上传记录\n' +
'• 重置所有用户的互动分数和绩效分数\n\n' +
'此操作不可逆!确定要继续吗?',
'确认月度重置',
{
type: 'error',
type: 'warning',
confirmButtonText: '确定重置',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--danger'
confirmButtonClass: 'el-button--warning'
}
)
resetLoading.value = true
monthlyResetLoading.value = true
// 执行重置
const success = dataStore.resetToDefault()
// 执行月度重置
const result = dataStore.manualMonthlyReset()
if (success) {
ElMessage.success('系统重置成功!页面将刷新。')
// 刷新页面
setTimeout(() => {
window.location.reload()
}, 1500)
if (result.success) {
ElMessage.success(`月度重置成功!已清空 ${result.clearedCount} 张图片`)
// 刷新页面数据
refreshData()
// 更新上次重置时间显示
loadLastResetTime()
} else {
ElMessage.error('系统重置失败!')
ElMessage.error(`月度重置失败:${result.error}`)
}
} catch (error) {
if (error !== 'cancel') {
console.error('重置系统失败:', error)
ElMessage.error('系统重置失败!')
console.error('月度重置失败:', error)
ElMessage.error('月度重置失败!')
}
} finally {
resetLoading.value = false
monthlyResetLoading.value = false
}
}
/**
* 修复图片归属错误
* 加载上次重置时间
*/
const fixImageOwnership = async () => {
try {
await ElMessageBox.confirm(
'此操作将修复图片归属错误问题,包括:\n' +
'• 修复图片显示在错误机构的问题\n' +
'• 合并相似机构的图片到正确位置\n\n' +
'确定要继续吗?',
'修复图片归属',
{
type: 'warning',
confirmButtonText: '开始修复',
cancelButtonText: '取消'
}
)
fixImageLoading.value = true
// 执行图片归属修复
const result = dataStore.fixImageOwnershipIssues()
if (result.fixed > 0) {
ElMessage.success(`图片归属修复完成!共修复了 ${result.fixed} 个问题`)
if (result.issues.length > 0) {
console.log('修复详情:', result.issues)
ElMessage.info(`修复详情:${result.issues.slice(0, 3).join(';')}${result.issues.length > 3 ? '...' : ''}`)
}
} else {
ElMessage.info('未发现图片归属问题')
}
} catch (error) {
if (error !== 'cancel') {
console.error('修复图片归属失败:', error)
ElMessage.error('修复图片归属失败!')
}
} finally {
fixImageLoading.value = false
const loadLastResetTime = () => {
const lastReset = localStorage.getItem('last_reset_time')
if (lastReset) {
const date = new Date(lastReset)
lastResetTime.value = date.toLocaleString('zh-CN')
} else {
lastResetTime.value = '未重置'
}
}
/**
* 修复机构权限验证错误
*/
const fixPermissionErrors = async () => {
try {
await ElMessageBox.confirm(
'此操作将修复机构权限验证错误,包括:\n' +
'• 修复重复的机构ID\n' +
'• 检查跨地区机构归属问题\n' +
'• 验证用户权限映射\n\n' +
'确定要继续吗?',
'修复权限验证',
{
type: 'info',
confirmButtonText: '开始修复',
cancelButtonText: '取消'
}
)
fixPermissionLoading.value = true
// 执行权限验证修复
const result = dataStore.fixInstitutionPermissionErrors()
if (result.fixed > 0) {
ElMessage.success(`权限验证修复完成!共修复了 ${result.fixed} 个问题`)
if (result.issues.length > 0) {
console.log('修复详情:', result.issues)
ElMessage.info(`修复详情:${result.issues.slice(0, 3).join(';')}${result.issues.length > 3 ? '...' : ''}`)
}
} else {
ElMessage.info('未发现权限验证问题')
}
} catch (error) {
if (error !== 'cancel') {
console.error('修复权限验证失败:', error)
ElMessage.error('修复权限验证失败!')
}
} finally {
fixPermissionLoading.value = false
}
}
/**
* 测试图片上传权限验证
*/
const testPermissions = async () => {
try {
await ElMessageBox.confirm(
'此操作将测试所有用户的图片上传权限验证,包括:\n' +
'• 测试用户对自己机构的权限\n' +
'• 测试用户对其他机构的权限(应该被拒绝)\n' +
'• 检查权限验证逻辑的完整性\n\n' +
'确定要开始测试吗?',
'测试权限验证',
{
type: 'info',
confirmButtonText: '开始测试',
cancelButtonText: '取消'
}
)
testPermissionLoading.value = true
// 获取所有普通用户
const normalUsers = dataStore.getUsers().filter(user => user.role === 'user')
if (normalUsers.length === 0) {
ElMessage.warning('没有找到普通用户,无法进行权限测试')
return
}
console.log(`开始测试 ${normalUsers.length} 个用户的权限...`)
const allResults = []
let totalTests = 0
let passedTests = 0
for (const user of normalUsers) {
console.log(`\n测试用户: ${user.name}`)
const result = dataStore.testImageUploadPermissions(user.id)
allResults.push(result)
totalTests += result.totalTests
passedTests += result.passedTests
if (!result.success) {
console.warn(`用户 ${user.name} 的权限测试失败`)
result.results.forEach(r => {
if (!r.success) {
console.warn(` - ${r.institutionName}: ${r.error}`)
}
})
}
}
// 显示测试结果
const overallSuccess = passedTests === totalTests
if (overallSuccess) {
ElMessage.success(`权限验证测试完成!所有 ${totalTests} 项测试都通过了`)
} else {
ElMessage.warning(`权限验证测试完成!${passedTests}/${totalTests} 项测试通过`)
}
// 显示详细结果
console.log('\n=== 权限测试总结 ===')
console.log(`总测试数: ${totalTests}`)
console.log(`通过测试: ${passedTests}`)
console.log(`失败测试: ${totalTests - passedTests}`)
allResults.forEach(result => {
console.log(`\n用户 ${result.userName}:`)
console.log(` 测试数: ${result.totalTests}`)
console.log(` 通过数: ${result.passedTests}`)
if (!result.success) {
console.log(` 失败原因:`)
result.results.filter(r => !r.success).forEach(r => {
console.log(` - ${r.institutionName}: ${r.error}`)
})
}
})
// 如果有失败的测试,提供修复建议
if (!overallSuccess) {
ElMessage.info('发现权限验证问题,建议执行"修复权限验证"功能')
}
} catch (error) {
if (error !== 'cancel') {
console.error('权限测试失败:', error)
ElMessage.error('权限测试失败!')
}
} finally {
testPermissionLoading.value = false
}
}
/**
* 修复虚拟/真实机构ID问题
*/
const fixVirtualRealIds = async () => {
try {
await ElMessageBox.confirm(
'此操作将修复虚拟机构和真实机构的ID问题,包括:\n' +
'• 为缺失ID的机构生成新ID\n' +
'• 修复ID格式问题\n' +
'• 检查并修复负责人关联\n' +
'• 验证数据结构完整性\n\n' +
'确定要开始修复吗?',
'修复机构ID问题',
{
type: 'warning',
confirmButtonText: '开始修复',
cancelButtonText: '取消'
}
)
fixVirtualRealLoading.value = true
// 执行虚拟/真实机构ID修复
const result = dataStore.fixVirtualRealInstitutionIds()
if (result.fixed > 0) {
ElMessage.success(`机构ID修复完成!共修复了 ${result.fixed} 个问题`)
if (result.issues.length > 0) {
console.log('修复详情:', result.issues)
ElMessage.info(`修复详情:${result.issues.slice(0, 3).join(';')}${result.issues.length > 3 ? '...' : ''}`)
}
// 建议用户测试图片上传功能
setTimeout(() => {
ElMessage.info('建议现在测试图片上传功能是否正常')
}, 2000)
} else {
ElMessage.info('未发现需要修复的机构ID问题')
}
} catch (error) {
if (error !== 'cancel') {
console.error('修复机构ID失败:', error)
ElMessage.error('修复机构ID失败!')
}
} finally {
fixVirtualRealLoading.value = false
}
}
/**
* 历史统计相关方法
......@@ -2662,6 +2891,397 @@ const clearHistoryStats = async () => {
}
/**
* 处理历史数据导出命令
*/
const handleHistoryExportCommand = (command) => {
exportHistoryData(command)
}
/**
* 导出历史数据
*/
const exportHistoryData = async (format = 'json') => {
if (!selectedMonthData.value) {
ElMessage.error('请先选择要导出的历史月份')
return
}
try {
exportHistoryLoading.value = true
const monthData = selectedMonthData.value
// 准备导出数据
const exportData = {
exportInfo: {
exportTime: new Date().toISOString(),
exportType: '历史数据分析导出',
month: monthData.month,
monthLabel: formatMonthLabel(monthData.month),
saveTime: monthData.saveTime,
format: format
},
summary: {
month: monthData.month,
totalUsers: monthData.totalUsers,
totalInstitutions: monthData.totalInstitutions,
totalImages: monthData.totalImages,
saveTime: monthData.saveTime
},
userDetails: monthData.userStats.map(user => ({
userId: user.userId,
userName: user.userName,
institutionCount: user.institutionCount,
interactionScore: user.interactionScore,
performanceScore: user.performanceScore,
institutions: user.institutions.map(inst => ({
id: inst.id,
name: inst.name,
imageCount: inst.imageCount
}))
}))
}
// 根据格式导出
switch (format) {
case 'csv':
await exportHistoryDataAsCSV(exportData, monthData.month)
break
case 'zip':
await exportHistoryDataAsZIP(exportData, monthData.month)
break
default:
throw new Error(`不支持的导出格式: ${format}`)
}
ElMessage.success(`${formatMonthLabel(monthData.month)} 历史数据${format.toUpperCase()}导出成功!`)
} catch (error) {
console.error('导出历史数据失败:', error)
ElMessage.error(`导出历史数据失败:${error.message}`)
} finally {
exportHistoryLoading.value = false
}
}
/**
* 导出历史数据为JSON格式
*/
const exportHistoryDataAsJSON = async (exportData, month) => {
const jsonString = JSON.stringify(exportData, null, 2)
const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `历史数据分析_${month}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
/**
* 导出历史数据为Excel格式
*/
const exportHistoryDataAsExcel = async (exportData, month) => {
const workbook = XLSX.utils.book_new()
// 概览信息工作表
const summaryData = [
['历史数据概览', ''],
['月份', exportData.summary.month],
['总用户数', exportData.summary.totalUsers],
['总机构数', exportData.summary.totalInstitutions],
['总图片数', exportData.summary.totalImages],
['保存时间', exportData.summary.saveTime],
['', ''],
['导出信息', ''],
['导出时间', exportData.exportInfo.exportTime],
['导出类型', exportData.exportInfo.exportType]
]
const summarySheet = XLSX.utils.aoa_to_sheet(summaryData)
XLSX.utils.book_append_sheet(workbook, summarySheet, '概览信息')
// 用户绩效工作表
const userHeaders = ['用户ID', '用户姓名', '负责机构数', '互动得分', '绩效得分']
const userData = [userHeaders]
exportData.userDetails.forEach(user => {
userData.push([
user.userId,
user.userName,
user.institutionCount,
user.interactionScore,
user.performanceScore
])
})
const userSheet = XLSX.utils.aoa_to_sheet(userData)
XLSX.utils.book_append_sheet(workbook, userSheet, '用户绩效')
// 机构详情工作表
const institutionHeaders = ['用户姓名', '机构ID', '机构名称', '图片数量']
const institutionData = [institutionHeaders]
exportData.userDetails.forEach(user => {
user.institutions.forEach(inst => {
institutionData.push([
user.userName,
inst.id,
inst.name,
inst.imageCount
])
})
})
const institutionSheet = XLSX.utils.aoa_to_sheet(institutionData)
XLSX.utils.book_append_sheet(workbook, institutionSheet, '机构详情')
// 图片缩略图工作表
const allInstitutions = []
exportData.userDetails.forEach(user => {
user.institutions.forEach(inst => {
allInstitutions.push({
...inst,
userName: user.userName
})
})
})
await addImageThumbnailsToWorkbook(workbook, allInstitutions, '图片缩略图')
// 导出文件
XLSX.writeFile(workbook, `历史数据分析_${month}.xlsx`)
}
/**
* 导出历史数据为CSV格式
*/
const exportHistoryDataAsCSV = async (exportData, month) => {
const csvContent = []
// 概览信息部分
csvContent.push('历史数据概览')
csvContent.push(`月份,${exportData.summary.month}`)
csvContent.push(`总用户数,${exportData.summary.totalUsers}`)
csvContent.push(`总机构数,${exportData.summary.totalInstitutions}`)
csvContent.push(`总图片数,${exportData.summary.totalImages}`)
csvContent.push(`保存时间,${exportData.summary.saveTime}`)
csvContent.push('')
// 用户绩效部分
csvContent.push('用户绩效数据')
csvContent.push('用户ID,用户姓名,负责机构数,互动得分,绩效得分')
exportData.userDetails.forEach(user => {
csvContent.push(`${user.userId},${user.userName},${user.institutionCount},${user.interactionScore},${user.performanceScore}`)
})
csvContent.push('')
// 机构详情部分
csvContent.push('机构详情数据')
csvContent.push('用户姓名,机构ID,机构名称,图片数量')
exportData.userDetails.forEach(user => {
user.institutions.forEach(inst => {
csvContent.push(`${user.userName},${inst.id},${inst.name},${inst.imageCount}`)
})
})
// 添加BOM以支持中文
const BOM = '\uFEFF'
const csvString = BOM + csvContent.join('\n')
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `历史数据分析_${month}.csv`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
/**
* 导出历史数据为PDF格式
*/
const exportHistoryDataAsPDF = async (exportData, month) => {
const pdf = new jsPDF()
pdf.setFont('helvetica')
let yPosition = 20
const lineHeight = 10
const pageHeight = pdf.internal.pageSize.height
// 添加标题
pdf.setFontSize(16)
pdf.text(`Historical Data Analysis - ${month}`, 20, yPosition)
yPosition += lineHeight * 2
// 概览信息
pdf.setFontSize(14)
pdf.text('Summary Information:', 20, yPosition)
yPosition += lineHeight
pdf.setFontSize(12)
const summaryInfo = [
`Month: ${exportData.summary.month}`,
`Total Users: ${exportData.summary.totalUsers}`,
`Total Institutions: ${exportData.summary.totalInstitutions}`,
`Total Images: ${exportData.summary.totalImages}`,
`Save Time: ${exportData.summary.saveTime}`
]
summaryInfo.forEach(info => {
if (yPosition > pageHeight - 20) {
pdf.addPage()
yPosition = 20
}
pdf.text(info, 20, yPosition)
yPosition += lineHeight
})
yPosition += lineHeight
// 用户绩效数据
pdf.setFontSize(14)
if (yPosition > pageHeight - 20) {
pdf.addPage()
yPosition = 20
}
pdf.text('User Performance Data:', 20, yPosition)
yPosition += lineHeight
pdf.setFontSize(12)
exportData.userDetails.forEach((user, index) => {
if (yPosition > pageHeight - 40) {
pdf.addPage()
yPosition = 20
}
pdf.text(`${index + 1}. ${user.userName}`, 20, yPosition)
yPosition += lineHeight
pdf.text(` Institutions: ${user.institutionCount}, Interaction: ${user.interactionScore}, Performance: ${user.performanceScore}`, 20, yPosition)
yPosition += lineHeight
// 显示机构详情
user.institutions.forEach(inst => {
if (yPosition > pageHeight - 20) {
pdf.addPage()
yPosition = 20
}
pdf.text(` - ${inst.name}: ${inst.imageCount} images`, 20, yPosition)
yPosition += lineHeight
})
yPosition += 5
})
// 导出信息
yPosition += lineHeight
pdf.setFontSize(14)
if (yPosition > pageHeight - 20) {
pdf.addPage()
yPosition = 20
}
pdf.text('Export Information:', 20, yPosition)
yPosition += lineHeight
pdf.setFontSize(12)
pdf.text(`Export Time: ${exportData.exportInfo.exportTime}`, 20, yPosition)
yPosition += lineHeight
pdf.text(`Export Type: ${exportData.exportInfo.exportType}`, 20, yPosition)
// 保存PDF
pdf.save(`历史数据分析_${month}.pdf`)
}
/**
* 导出历史数据为ZIP压缩包格式
*/
const exportHistoryDataAsZIP = async (exportData, month) => {
try {
const zip = new JSZip()
// 创建历史数据摘要文件
const summaryData = {
exportInfo: exportData.exportInfo,
summary: exportData.summary,
userSummary: exportData.userDetails.map(user => ({
userId: user.userId,
userName: user.userName,
institutionCount: user.institutionCount,
interactionScore: user.interactionScore,
performanceScore: user.performanceScore
}))
}
zip.file(`历史数据摘要_${month}.json`, JSON.stringify(summaryData, null, 2))
// 创建用户绩效统计文件
const performanceData = exportData.userDetails.map(user => ({
用户姓名: user.userName,
负责机构数: user.institutionCount,
互动得分: user.interactionScore,
绩效得分: user.performanceScore,
机构详情: user.institutions.map(inst => `${inst.name}(${inst.imageCount}张图片)`).join(', ')
}))
zip.file(`用户绩效统计_${month}.json`, JSON.stringify(performanceData, null, 2))
// 按用户和机构创建多级文件夹并添加图片
// 注意:历史数据可能没有完整的图片信息,需要从当前数据中获取
for (const user of exportData.userDetails) {
const userFolderName = user.userName
// 获取当前用户的机构数据以获取图片
const currentUser = dataStore.getUserById(user.userId)
if (currentUser) {
const userInstitutions = dataStore.getInstitutionsByUserId(user.userId)
for (const institution of userInstitutions) {
if (institution.images && institution.images.length > 0) {
const institutionFolderName = `${institution.name}_${institution.institutionId}`
for (const image of institution.images) {
try {
// 从Base64数据中提取图片数据
const base64Data = image.url.split(',')[1]
if (base64Data) {
// 获取文件扩展名
const mimeType = image.url.split(';')[0].split(':')[1]
const extension = mimeType.split('/')[1] || 'jpg'
const fileName = `${image.name.replace(/\.[^/.]+$/, '')}.${extension}`
zip.file(`${userFolderName}/${institutionFolderName}/${fileName}`, base64Data, { base64: true })
}
} catch (error) {
console.warn(`处理图片失败: ${image.name}`, error)
}
}
}
}
}
}
// 生成并下载ZIP文件
const content = await zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
})
saveAs(content, `历史数据_${month}_完整图片包.zip`)
} catch (error) {
console.error('生成历史数据ZIP文件失败:', error)
throw new Error('生成历史数据ZIP文件失败')
}
}
/**
* 格式化月份标签
*/
const formatMonthLabel = (monthKey) => {
......@@ -2701,6 +3321,9 @@ onMounted(() => {
// 加载历史统计数据
loadAvailableMonths()
// 加载上次重置时间
loadLastResetTime()
// 自动保存月度统计(如果需要)
dataStore.autoSaveMonthlyStats()
})
......@@ -4083,4 +4706,64 @@ const iconComponents = {
font-size: 24px;
}
}
/* 对话框footer样式 */
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.footer-left {
flex: 1;
}
.footer-right {
display: flex;
gap: 10px;
}
/* 月度重置样式 */
.data-management-card.warning {
border-left: 4px solid #e6a23c;
}
.data-management-card.warning .card-icon.monthly-reset {
color: #e6a23c;
}
.info-notice {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
margin: 10px 0;
font-size: 12px;
color: #409eff;
}
.reset-info {
margin-top: 10px;
}
.reset-info .info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
font-size: 12px;
}
.reset-info .label {
color: #909399;
}
.reset-info .value {
color: #303133;
font-weight: 500;
}
</style>
\ No newline at end of file
......@@ -7,6 +7,7 @@
<div class="user-info">
<h2>{{ authStore.currentUser.name }} 的工作台</h2>
<p>负责机构:{{ userInstitutions.length }}</p>
<p class="period-info">当前统计周期:{{ currentPeriod }}</p>
</div>
<div class="header-actions">
<el-button @click="handleLogout">退出登录</el-button>
......@@ -16,21 +17,21 @@
</div>
<div class="container">
<!-- 得分统计 -->
<!-- 统计信息 -->
<div class="score-section">
<el-row :gutter="20">
<el-col :span="12">
<div class="score-card">
<div class="score-title">互动得分</div>
<div class="score-value">{{ interactionScore.toFixed(1) }}</div>
<div class="score-desc">每机构最多1分</div>
<div class="score-title">已传机构数</div>
<div class="score-value">{{ uploadedInstitutionsCount }}</div>
<div class="score-desc">已上传图片的机构数量</div>
</div>
</el-col>
<el-col :span="12">
<div class="score-card performance">
<div class="score-title">绩效得分</div>
<div class="score-value">{{ performanceScore.toFixed(1) }}</div>
<div class="score-desc">满分10分</div>
<div class="score-title">完成率</div>
<div class="score-value">{{ completionRate }}%</div>
<div class="score-desc">已传机构数/总负责机构数</div>
</div>
</el-col>
</el-row>
......@@ -270,17 +271,44 @@ const filteredInstitutions = computed(() => {
})
/**
* 计算属性:互动得分
* 计算属性:已传机构数
*/
const interactionScore = computed(() => {
return dataStore.calculateInteractionScore(authStore.currentUser.id)
const uploadedInstitutionsCount = computed(() => {
return userInstitutions.value.filter(inst => inst.images && inst.images.length > 0).length
})
/**
* 计算属性:绩效得分
* 计算属性:完成率
*/
const performanceScore = computed(() => {
return dataStore.calculatePerformanceScore(authStore.currentUser.id)
const completionRate = computed(() => {
const totalInstitutions = userInstitutions.value.length
if (totalInstitutions === 0) return 0
return Math.round((uploadedInstitutionsCount.value / totalInstitutions) * 100)
})
/**
* 计算属性:当前统计周期
*/
const currentPeriod = computed(() => {
const currentDate = new Date()
const year = currentDate.getFullYear()
const month = currentDate.getMonth() + 1
// 获取上次重置时间
const lastResetTime = localStorage.getItem('last_reset_time')
if (lastResetTime) {
const resetDate = new Date(lastResetTime)
const resetMonth = resetDate.getMonth() + 1
const resetYear = resetDate.getFullYear()
// 如果是同一年同一月,显示重置后的天数
if (resetYear === year && resetMonth === month) {
const daysSinceReset = Math.floor((currentDate - resetDate) / (1000 * 60 * 60 * 24))
return `${year}${month}月(重置后第${daysSinceReset + 1}天)`
}
}
return `${year}${month}月`
})
/**
......@@ -1096,4 +1124,11 @@ onMounted(() => {
padding: 15px;
}
}
</style>
\ No newline at end of file
/* 周期信息样式 */
.period-info {
font-size: 12px;
color: #909399;
margin: 4px 0 0 0;
}
</style>
\ No newline at end of file
......@@ -54,6 +54,6 @@ echo ========================================
echo.
:: 直接调用vite
.\node_modules\.bin\vite.cmd --host 127.0.0.1 --port 5174
.\node_modules\.bin\vite.cmd --host 127.0.0.1 --port 4001
pause
......@@ -8,5 +8,9 @@ export default defineConfig({
alias: {
'@': '/src'
}
},
server: {
port: 4001,
host: '0.0.0.0'
}
})
\ No newline at end of file
})
\ No newline at end of file
@echo off
@echo off
......@@ -62,7 +62,7 @@ if not exist "node_modules" (
echo 🚀 正在启动开发服务器...
echo.
echo 启动成功后,请在浏览器中访问显示的地址
echo 通常是: http://localhost:5173 或 http://localhost:5174
echo 通常是: http://localhost:4001
echo.
echo 默认登录账号:
echo - 管理员: admin / admin123
......
# 🚀 新功能演示指南
# 🚀 新功能演示指南
......@@ -196,7 +196,7 @@ const calculateImageHash = (imageUrl) => {
现在您可以开始体验这些新功能了!
1. 访问 **http://localhost:5174/** 体验重复图片检测
1. 访问 **http://localhost:4001/** 体验重复图片检测
2. 使用管理员账户体验历史统计功能
3. 使用测试工具验证功能正确性
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment