diff --git a/admin/package-lock.json b/admin/package-lock.json index 4e8c68f..2913c8c 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -10,11 +10,13 @@ "dependencies": { "@lucide/vue": "^1.22.0", "element-plus": "^2.10.4", - "vue": "^3.5.17" + "vue": "^3.5.17", + "vue-router": "^4.5.1" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.4", "typescript": "^5.6.3", + "unplugin-vue-components": "^28.8.0", "vite": "^6.4.3", "vue-tsc": "^2.2.12" } @@ -515,11 +517,50 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==" }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@lucide/vue": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.22.0.tgz", @@ -982,6 +1023,11 @@ "he": "^1.2.0" } }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, "node_modules/@vue/language-core": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", @@ -1086,12 +1132,49 @@ "vue": "^3.5.0" } }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/alien-signals": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", "dev": true }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/async-validator": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", @@ -1103,15 +1186,69 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1128,6 +1265,23 @@ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", "dev": true }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/element-plus": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.2.tgz", @@ -1210,6 +1364,12 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "dev": true + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1227,6 +1387,18 @@ } } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1241,6 +1413,18 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1250,6 +1434,65 @@ "he": "bin/he" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "dev": true, + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -1298,6 +1541,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", @@ -1305,9 +1583,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -1321,6 +1599,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-wheel-es": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", @@ -1332,6 +1619,12 @@ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "dev": true }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1349,10 +1642,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "dev": true, + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -1368,7 +1672,7 @@ } ], "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1376,6 +1680,46 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ] + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", @@ -1444,6 +1788,18 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1457,6 +1813,78 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true + }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz", + "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==", + "dev": true, + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unplugin-vue-components": { + "version": "28.8.0", + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-28.8.0.tgz", + "integrity": "sha512-2Q6ZongpoQzuXDK0ZsVzMoshH0MWZQ1pzVL538G7oIDKRTVzHjppBDS8aB99SADGHN3lpGU7frraCG6yWNoL5Q==", + "dev": true, + "dependencies": { + "chokidar": "^3.6.0", + "debug": "^4.4.1", + "local-pkg": "^1.1.1", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "tinyglobby": "^0.2.14", + "unplugin": "^2.3.5", + "unplugin-utils": "^0.2.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2 || ^4.0.0", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", @@ -1562,6 +1990,20 @@ "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.5.tgz", "integrity": "sha512-Fe1jyPJoUGpJOYKOri44jduR7My4yYINOMJISuMAbmrs+L5LbIDUc8NTWZYY3EJLK0yPLuCmcd5zoCsE4k2/KA==" }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/vue-tsc": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", @@ -1577,6 +2019,12 @@ "peerDependencies": { "typescript": ">=5.0.0" } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true } } } diff --git a/admin/package.json b/admin/package.json index 66f79e4..fb64b46 100644 --- a/admin/package.json +++ b/admin/package.json @@ -6,16 +6,20 @@ "scripts": { "dev": "vite --host 0.0.0.0", "build": "vue-tsc --noEmit && vite build", + "test": "node tests/navigation.test.mjs && node ../scripts/check-admin-m08-d-r1.mjs --source-only", + "verify:r1": "npm run build && npm test && node ../scripts/check-admin-m08-d-r1.mjs --dist-only", "preview": "vite preview --host 0.0.0.0" }, "dependencies": { "@lucide/vue": "^1.22.0", "element-plus": "^2.10.4", - "vue": "^3.5.17" + "vue": "^3.5.17", + "vue-router": "^4.5.1" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.4", "typescript": "^5.6.3", + "unplugin-vue-components": "^28.8.0", "vite": "^6.4.3", "vue-tsc": "^2.2.12" } diff --git a/admin/src/App.vue b/admin/src/App.vue index eb3c12f..7cf89fd 100644 --- a/admin/src/App.vue +++ b/admin/src/App.vue @@ -1,7 +1,11 @@ diff --git a/admin/src/components/CleaningWorkspace.vue b/admin/src/components/CleaningWorkspace.vue new file mode 100644 index 0000000..0b94ff0 --- /dev/null +++ b/admin/src/components/CleaningWorkspace.vue @@ -0,0 +1,615 @@ + + + diff --git a/admin/src/main.ts b/admin/src/main.ts index ca7e2d1..39e789b 100644 --- a/admin/src/main.ts +++ b/admin/src/main.ts @@ -1,7 +1,6 @@ import { createApp } from 'vue'; -import ElementPlus from 'element-plus'; -import 'element-plus/dist/index.css'; import './styles.css'; import App from './App.vue'; +import { router } from './router'; -createApp(App).use(ElementPlus).mount('#app'); +createApp(App).use(router).mount('#app'); diff --git a/admin/src/navigation.d.mts b/admin/src/navigation.d.mts new file mode 100644 index 0000000..bbb92e1 --- /dev/null +++ b/admin/src/navigation.d.mts @@ -0,0 +1,39 @@ +export type AdminMenuKey = + | 'overview' + | 'stores' + | 'orders' + | 'payments' + | 'thirdParty' + | 'people' + | 'cleaning' + | 'devices' + | 'platformApps' + | 'content' + | 'franchise' + | 'system'; + +export interface AdminNavigationItem { + menuKey: AdminMenuKey; + path: string; + stage: string; + title: string; + label: string; + icon: string; + capabilities: readonly string[]; + roles: readonly string[]; +} + +export interface AdminNavigationAccess { + menus: readonly string[]; + capabilities: readonly string[]; + roles: readonly string[]; +} + +export const ADMIN_NAVIGATION: readonly AdminNavigationItem[]; +export function isMenuAllowed(menus: readonly string[], menuKey: AdminMenuKey): boolean; +export function pathForMenu(menuKey: AdminMenuKey): string | null; +export function firstAllowedPath(menus: readonly string[]): string | null; +export function resolveProtectedPath(menus: readonly string[], requestedMenu: AdminMenuKey): string | null; +export function isNavigationAllowed(access: AdminNavigationAccess, item: AdminNavigationItem): boolean; +export function firstAuthorizedPath(access: AdminNavigationAccess): string | null; +export function resolveAuthorizedPath(access: AdminNavigationAccess, requestedMenu: AdminMenuKey): string | null; diff --git a/admin/src/navigation.mjs b/admin/src/navigation.mjs new file mode 100644 index 0000000..a40054e --- /dev/null +++ b/admin/src/navigation.mjs @@ -0,0 +1,49 @@ +export const ADMIN_NAVIGATION = Object.freeze([ + { menuKey: 'overview', path: '/overview', stage: 'M08-D', title: '平台运营总览', label: '运营总览', icon: 'LayoutDashboard', capabilities: ['store.operation.read', 'tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] }, + { menuKey: 'stores', path: '/stores', stage: 'M08-D', title: '门店与房间管理', label: '门店房间', icon: 'Store', capabilities: ['store.operation.read', 'tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] }, + { menuKey: 'orders', path: '/orders', stage: 'M08-D', title: '订单筛选与运营处置', label: '订单运营', icon: 'ClipboardList', capabilities: ['store.operation.read', 'tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] }, + { menuKey: 'payments', path: '/payments', stage: 'M08-D', title: '支付、退款与分账', label: '支付分账', icon: 'WalletCards', capabilities: ['tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] }, + { menuKey: 'thirdParty', path: '/third-party', stage: 'M08-D', title: '团购与第三方平台运营', label: '团购平台', icon: 'TicketCheck', capabilities: ['store.operation.read', 'tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] }, + { menuKey: 'people', path: '/people', stage: 'M08-D', title: '会员与员工用户管理', label: '会员员工', icon: 'UsersRound', capabilities: ['tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] }, + { menuKey: 'cleaning', path: '/cleaning', stage: 'M08-B/M09', title: '保洁任务与结算', label: '保洁运营', icon: 'Sparkles', capabilities: ['cleaning.task.read', 'tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] }, + { menuKey: 'devices', path: '/devices', stage: 'M08-D', title: '设备资产、拓扑与控制', label: '设备', icon: 'RadioTower', capabilities: ['device.read', 'tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] }, + { menuKey: 'platformApps', path: '/apps', stage: 'M08-D', title: '多小程序与租户品牌配置', label: '小程序租户', icon: 'PanelsTopLeft', capabilities: ['tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] }, + { menuKey: 'content', path: '/content', stage: 'M08-D', title: '广告投放与门店装修', label: '广告装修', icon: 'Images', capabilities: ['tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] }, + { menuKey: 'franchise', path: '/franchise', stage: 'M08-D', title: '加盟申请与跟进运营', label: '加盟跟进', icon: 'Handshake', capabilities: ['tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] }, + { menuKey: 'system', path: '/system', stage: 'M08-D', title: '安全审计与系统配置', label: '日志配置', icon: 'ScrollText', capabilities: ['tenant.manage', 'platform.manage'], roles: ['PLATFORM_ADMIN'] } +]); + +export function isMenuAllowed(menus, menuKey) { + return menus.includes(menuKey); +} + +export function pathForMenu(menuKey) { + return ADMIN_NAVIGATION.find((item) => item.menuKey === menuKey)?.path ?? null; +} + +export function firstAllowedPath(menus) { + return ADMIN_NAVIGATION.find((item) => menus.includes(item.menuKey))?.path ?? null; +} + +export function resolveProtectedPath(menus, requestedMenu) { + return isMenuAllowed(menus, requestedMenu) + ? pathForMenu(requestedMenu) + : firstAllowedPath(menus); +} + +export function isNavigationAllowed(access, item) { + if (!isMenuAllowed(access.menus, item.menuKey)) return false; + return item.capabilities.some((capability) => access.capabilities.includes(capability)) + || item.roles.some((role) => access.roles.includes(role)); +} + +export function firstAuthorizedPath(access) { + return ADMIN_NAVIGATION.find((item) => isNavigationAllowed(access, item))?.path ?? null; +} + +export function resolveAuthorizedPath(access, requestedMenu) { + const requested = ADMIN_NAVIGATION.find((item) => item.menuKey === requestedMenu); + return requested && isNavigationAllowed(access, requested) + ? requested.path + : firstAuthorizedPath(access); +} diff --git a/admin/src/router.ts b/admin/src/router.ts new file mode 100644 index 0000000..7a65532 --- /dev/null +++ b/admin/src/router.ts @@ -0,0 +1,53 @@ +import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'; +import { + ADMIN_NAVIGATION, + firstAuthorizedPath, + isNavigationAllowed, + type AdminMenuKey +} from './navigation.mjs'; +import { adminSessionState } from './session'; + +const routeComponents = { + overview: () => import('./components/OperationsOverviewPanel.vue'), + stores: () => import('./components/StoresRoomsPanel.vue'), + orders: () => import('./components/OrdersPanel.vue'), + payments: () => import('./components/PaymentsPanel.vue'), + thirdParty: () => import('./components/ThirdPartyPanel.vue'), + people: () => import('./components/MembersStaffPanel.vue'), + cleaning: () => import('./components/CleaningWorkspace.vue'), + devices: () => import('./components/DevicesPanel.vue'), + platformApps: () => import('./components/PlatformAppsPanel.vue'), + content: () => import('./components/ContentManagementPanel.vue'), + franchise: () => import('./components/FranchisePanel.vue'), + system: () => import('./components/LogsSystemPanel.vue') +} satisfies Record Promise>; + +const businessRoutes = ADMIN_NAVIGATION.map((item) => ({ + path: item.path, + name: item.menuKey, + component: routeComponents[item.menuKey], + meta: { + menuKey: item.menuKey, + stage: item.stage, + title: item.title + } +})); + +export const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { path: '/', redirect: '/overview' }, + ...businessRoutes, + { path: '/:pathMatch(.*)*', redirect: '/overview' } + ], + scrollBehavior: () => ({ top: 0 }) +}); + +router.beforeEach((to) => { + const menuKey = to.meta.menuKey as AdminMenuKey | undefined; + if (!menuKey || !adminSessionState.token || !adminSessionState.ready) return true; + const navigation = ADMIN_NAVIGATION.find((item) => item.menuKey === menuKey); + if (navigation && isNavigationAllowed(adminSessionState.access, navigation)) return true; + const fallback = firstAuthorizedPath(adminSessionState.access); + return fallback ? { path: fallback, replace: true } : false; +}); diff --git a/admin/src/session.ts b/admin/src/session.ts new file mode 100644 index 0000000..198d713 --- /dev/null +++ b/admin/src/session.ts @@ -0,0 +1,36 @@ +import { reactive } from 'vue'; +import type { AdminAccess, AdminIdentity, AdminSessionPayload } from './types'; + +const emptyAccess = (): AdminAccess => ({ roles: [], capabilities: [], storeIds: [], menus: [] }); + +export const adminSessionState = reactive<{ + token: string; + user: AdminIdentity | null; + access: AdminAccess; + ready: boolean; +}>({ + token: localStorage.getItem('qipai.admin.token') || '', + user: null, + access: emptyAccess(), + ready: !localStorage.getItem('qipai.admin.token') +}); + +export function applyAdminSession(payload: AdminSessionPayload) { + adminSessionState.token = payload.accessToken; + adminSessionState.user = payload.user; + adminSessionState.access = payload.access; + adminSessionState.ready = true; +} + +export function applyAdminIdentity(user: AdminIdentity, access: AdminAccess) { + adminSessionState.user = user; + adminSessionState.access = access; + adminSessionState.ready = true; +} + +export function clearAdminSessionState() { + adminSessionState.token = ''; + adminSessionState.user = null; + adminSessionState.access = emptyAccess(); + adminSessionState.ready = true; +} diff --git a/admin/src/styles.css b/admin/src/styles.css index 8743f14..4a6aa6c 100644 --- a/admin/src/styles.css +++ b/admin/src/styles.css @@ -678,6 +678,46 @@ textarea { font-size: 24px; } +.route-alert, +.route-state, +.session-recovery-card { + margin-top: 20px; +} + +.route-state, +.session-recovery-card { + padding: 24px; + border: 1px solid #d8e0ea; + border-radius: 12px; + background: #fff; +} + +.session-recovery { + display: grid; + min-height: 100vh; + padding: 24px; + place-items: center; +} + +.session-recovery-card { + width: min(100%, 520px); + box-sizing: border-box; +} + +.session-recovery-card h1 { + margin: 0 0 12px; + font-size: 24px; +} + +.session-recovery-card p { + color: #60708a; + line-height: 1.7; +} + +.cleaning-workspace { + min-width: 0; +} + .session-box, .session-user { display: flex; @@ -2413,6 +2453,96 @@ textarea { } } +/* M08-D-R1: 768px tablet and 375px phone share a horizontal-scroll-free core layout. */ +@media (max-width: 768px) { + .workspace { + max-width: 100vw; + overflow-x: clip; + } + + .workspace :where(.el-form, .el-form-item, .el-input, .el-select, .el-date-editor), + .workspace :where(.el-dialog, .el-drawer) { + min-width: 0; + max-width: 100%; + } + + .workspace .el-dialog { + width: calc(100vw - 32px) !important; + margin: 16px auto !important; + } + + .workspace .el-drawer { + width: min(92vw, 520px) !important; + } + + .workspace .data-table { + width: 100% !important; + background: transparent; + } + + .workspace .data-table .el-table__inner-wrapper::before, + .workspace .data-table .el-table__header-wrapper { + display: none; + } + + .workspace .data-table .el-table__body-wrapper, + .workspace .data-table .el-scrollbar__view, + .workspace .data-table .el-table__body, + .workspace .data-table .el-table__body tbody { + display: block; + width: 100% !important; + } + + .workspace .data-table .el-table__body colgroup { + display: none; + } + + .workspace .data-table .el-table__body tbody { + display: grid; + gap: 12px; + } + + .workspace .data-table .el-table__row { + display: grid; + width: 100%; + padding: 10px 12px; + border: 1px solid #d8e0ea; + border-radius: 10px; + box-sizing: border-box; + background: #fff; + } + + .workspace .data-table .el-table__cell { + position: static !important; + display: block; + width: 100% !important; + padding: 6px 0; + border: 0; + box-sizing: border-box; + } + + .workspace .data-table .cell { + padding: 0; + overflow: visible; + white-space: normal; + } + + .workspace .data-table .row-actions { + flex-wrap: wrap; + } + + .asset-form, + .member-form, + .detail-grid, + .timeline-grid, + .stats-filter, + .handoff-session .el-form, + .handoff-execution-form, + .handoff-category-progress { + grid-template-columns: 1fr; + } +} + @media (max-width: 560px) { .login-shell { grid-template-columns: 1fr; diff --git a/admin/tests/navigation.test.mjs b/admin/tests/navigation.test.mjs new file mode 100644 index 0000000..00ae68b --- /dev/null +++ b/admin/tests/navigation.test.mjs @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import { + ADMIN_NAVIGATION, + firstAuthorizedPath, + isNavigationAllowed, + pathForMenu, + resolveAuthorizedPath +} from '../src/navigation.mjs'; + +assert.deepEqual( + ADMIN_NAVIGATION.map(({ path }) => path), + ['/overview', '/stores', '/orders', '/payments', '/third-party', '/people', + '/cleaning', '/devices', '/apps', '/content', '/franchise', '/system'] +); +assert.equal(pathForMenu('platformApps'), '/apps'); +assert.equal(pathForMenu('thirdParty'), '/third-party'); + +const storeOperator = { + roles: ['STORE_ADMIN'], + capabilities: ['store.operation.read'], + menus: ['overview', 'stores', 'orders', 'thirdParty'] +}; +assert.equal(resolveAuthorizedPath(storeOperator, 'orders'), '/orders'); +assert.equal(resolveAuthorizedPath(storeOperator, 'system'), '/overview'); +assert.equal(firstAuthorizedPath(storeOperator), '/overview'); + +const forgedSystemMenu = { + roles: ['STORE_ADMIN'], + capabilities: ['store.operation.read'], + menus: ['system'] +}; +const systemNavigation = ADMIN_NAVIGATION.find(({ menuKey }) => menuKey === 'system'); +assert.ok(systemNavigation); +assert.equal(isNavigationAllowed(forgedSystemMenu, systemNavigation), false); +assert.equal(firstAuthorizedPath(forgedSystemMenu), null); + +const cleaner = { + roles: ['CLEANER'], + capabilities: ['cleaning.task.read'], + menus: ['cleaning'] +}; +assert.equal(resolveAuthorizedPath(cleaner, 'cleaning'), '/cleaning'); +assert.equal(resolveAuthorizedPath(cleaner, 'devices'), '/cleaning'); + +const platformAdmin = { + roles: ['PLATFORM_ADMIN'], + capabilities: [], + menus: ADMIN_NAVIGATION.map(({ menuKey }) => menuKey) +}; +assert.equal(resolveAuthorizedPath(platformAdmin, 'system'), '/system'); + +console.log('PASS: M08-D-R1 deep links, menu/capability guards and refresh fallbacks are deterministic.'); diff --git a/admin/tsconfig.json b/admin/tsconfig.json index 4dd26a5..a9e2c46 100644 --- a/admin/tsconfig.json +++ b/admin/tsconfig.json @@ -16,5 +16,5 @@ "noUnusedParameters": true, "types": ["vite/client"] }, - "include": ["src/**/*.ts", "src/**/*.vue"] + "include": ["src/**/*.ts", "src/**/*.mts", "src/**/*.vue"] } diff --git a/admin/vite.config.ts b/admin/vite.config.ts index 1b851c2..8e9c45f 100644 --- a/admin/vite.config.ts +++ b/admin/vite.config.ts @@ -1,15 +1,22 @@ import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; +import Components from 'unplugin-vue-components/vite'; +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; export default defineConfig({ - plugins: [vue()], + plugins: [ + vue(), + Components({ + dts: false, + resolvers: [ElementPlusResolver({ importStyle: 'css' })] + }) + ], base: '/admin/', build: { rollupOptions: { output: { manualChunks: { - vue: ['vue'], - element: ['element-plus'], + vue: ['vue', 'vue-router'], icons: ['@lucide/vue'] } } diff --git a/scripts/check-admin-m08-b.mjs b/scripts/check-admin-m08-b.mjs index 23ba329..85906de 100644 --- a/scripts/check-admin-m08-b.mjs +++ b/scripts/check-admin-m08-b.mjs @@ -19,7 +19,10 @@ const vite = read('admin/vite.config.ts'); assert.match(vite, /base: '\/admin\/'/); assert.match(vite, /'\/admin-api'/); -const app = read('admin/src/App.vue'); +const app = [ + read('admin/src/App.vue'), + read('admin/src/components/CleaningWorkspace.vue') +].join('\n'); for (const pattern of [ 'CleaningTasksPanel', 'CleaningSettlementsPanel', diff --git a/scripts/check-admin-m08-d-r1.mjs b/scripts/check-admin-m08-d-r1.mjs new file mode 100644 index 0000000..bd6a194 --- /dev/null +++ b/scripts/check-admin-m08-d-r1.mjs @@ -0,0 +1,73 @@ +import assert from 'node:assert/strict'; +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; +import { gzipSync } from 'node:zlib'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('..', import.meta.url)); +const read = (path) => readFileSync(join(root, path), 'utf8'); +const sourceOnly = process.argv.includes('--source-only'); +const distOnly = process.argv.includes('--dist-only'); + +if (!distOnly) { + const packageJson = JSON.parse(read('admin/package.json')); + assert.ok(packageJson.dependencies['vue-router']); + assert.ok(packageJson.devDependencies['unplugin-vue-components']); + + const main = read('admin/src/main.ts'); + assert.match(main, /use\(router\)/); + assert.doesNotMatch(main, /ElementPlus|element-plus\/dist\/index\.css/); + + const app = read('admin/src/App.vue'); + for (const pattern of ['RouterLink', 'RouterView', 'Suspense', 'routeLoadError', + 'retryLazyRoute', 'restoreSession', 'qipai:admin-refreshed']) assert.match(app, new RegExp(pattern)); + assert.doesNotMatch(app, /import\s+(?!AdminLoginPanel\b)\w+Panel\s+from/); + + const router = read('admin/src/router.ts'); + for (const path of ['/overview', '/stores', '/orders', '/payments', '/third-party', + '/people', '/cleaning', '/devices', '/apps', '/content', '/franchise', '/system']) { + assert.match(read('admin/src/navigation.mjs'), new RegExp(`path: '${path.replace('/', '\\/')}'`)); + } + assert.equal((router.match(/\(\) => import\('/g) || []).length, 12); + assert.match(router, /router\.beforeEach/); + assert.match(router, /isNavigationAllowed/); + + const api = read('admin/src/api.ts'); + assert.match(api, /let refreshInFlight: Promise \| null = null/); + assert.match(api, /if \(!refreshInFlight\)/); + assert.match(api, /refreshInFlight = publicRequest\('\/auth\/refresh'/); + assert.match(api, /\.finally\(\(\) => \{ refreshInFlight = null; \}\)/); + assert.match(api, /return refreshInFlight/); + + const vite = read('admin/vite.config.ts'); + assert.match(vite, /ElementPlusResolver/); + assert.doesNotMatch(vite, /chunkSizeWarningLimit/); + assert.doesNotMatch(vite, /element:\s*\['element-plus'\]/); + + const styles = read('admin/src/styles.css'); + for (const pattern of ['@media (max-width: 980px)', '@media (max-width: 768px)', + '@media (max-width: 560px)', '.workspace .data-table .el-table__row', + '.workspace .el-dialog', '.session-recovery', '.route-state']) assert.ok(styles.includes(pattern)); +} + +if (!sourceOnly) { + const dist = join(root, 'admin/dist'); + assert.ok(existsSync(join(dist, 'index.html')), 'admin production build is missing'); + const assets = join(dist, 'assets'); + const javascript = readdirSync(assets).filter((name) => name.endsWith('.js')); + const largest = javascript + .map((name) => ({ name, bytes: statSync(join(assets, name)).size })) + .sort((left, right) => right.bytes - left.bytes)[0]; + assert.ok(largest.bytes < 500 * 1024, `chunk ${largest.name} exceeds 500 KiB`); + for (const chunk of ['OperationsOverviewPanel', 'StoresRoomsPanel', 'OrdersPanel', + 'PaymentsPanel', 'ThirdPartyPanel', 'MembersStaffPanel', 'CleaningWorkspace', + 'DevicesPanel', 'PlatformAppsPanel', 'ContentManagementPanel', 'FranchisePanel', + 'LogsSystemPanel']) assert.ok(javascript.some((name) => name.startsWith(`${chunk}-`)), `${chunk} is not lazy`); + const entry = javascript.find((name) => name.startsWith('index-')); + assert.ok(entry); + assert.ok(gzipSync(readFileSync(join(assets, entry))).length < 120 * 1024, 'entry gzip exceeds 120 KiB'); + assert.doesNotMatch(read('admin/dist/index.html'), /CleaningWorkspace|StoresRoomsPanel|OrdersPanel/); + console.log(`PASS: M08-D-R1 lazy chunks are below 500 KiB; largest=${largest.name}:${largest.bytes}.`); +} else { + console.log('PASS: M08-D-R1 route, permission, recovery and responsive source gates are present.'); +} diff --git a/scripts/check-admin-m08-d.mjs b/scripts/check-admin-m08-d.mjs index 7d13f04..e2029f2 100644 --- a/scripts/check-admin-m08-d.mjs +++ b/scripts/check-admin-m08-d.mjs @@ -8,39 +8,31 @@ const read = (path) => readFileSync(join(root, path), 'utf8'); const app = read('admin/src/App.vue'); for (const pattern of [ - 'OperationsOverviewPanel', - "activeModule = 'overview'", - "activeModule = 'cleaning'", - "activeModule = 'stores'", - "activeModule = 'orders'", - "activeModule = 'payments'", - "activeModule = 'thirdParty'", - "activeModule = 'people'", - "activeModule = 'devices'", - "activeModule = 'platformApps'", - "activeModule = 'content'", - "activeModule = 'franchise'", - "activeModule = 'system'", - '平台运营总览', - 'StoresRoomsPanel', - 'OrdersPanel', - 'PaymentsPanel', - 'ThirdPartyPanel', - 'MembersStaffPanel', - 'DevicesPanel', - 'PlatformAppsPanel', - 'ContentManagementPanel', - 'FranchisePanel', - 'LogsSystemPanel', + 'RouterLink', + 'RouterView', + 'visibleNavigation', + 'isNavigationAllowed', + 'restoreSession', + 'routeLoadError', 'AdminLoginPanel', - '运营总览', - 'savedToken', 'handleAuthenticated', 'handleLogout', - 'loadCleaningWorkspace' + 'openCleaning' ]) { assert.match(app, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } +assert.doesNotMatch(app, /import\s+(?!AdminLoginPanel\b)\w+Panel\s+from/); + +const router = read('admin/src/router.ts'); +assert.equal((router.match(/\(\) => import\('/g) || []).length, 12); +assert.match(router, /router\.beforeEach/); +const navigation = read('admin/src/navigation.mjs'); +for (const path of ['/overview', '/stores', '/orders', '/payments', '/third-party', '/people', + '/cleaning', '/devices', '/apps', '/content', '/franchise', '/system']) assert.ok(navigation.includes(`path: '${path}'`)); + +const cleaningWorkspace = read('admin/src/components/CleaningWorkspace.vue'); +for (const pattern of ['CleaningTasksPanel', 'CleaningSettlementsPanel', 'CleaningStatisticsPanel', + 'CleanersPanel', 'CleaningFieldHandoffPanel', 'loadCleaningWorkspace']) assert.match(cleaningWorkspace, new RegExp(pattern)); const overview = read('admin/src/components/OperationsOverviewPanel.vue'); for (const pattern of [ @@ -301,10 +293,13 @@ for (const pattern of [ '.audit-filters', '.login-shell', '.session-box', + '.session-recovery', + '.route-state', '@media (max-width: 980px)', + '@media (max-width: 768px)', '@media (max-width: 560px)' ]) { assert.match(styles, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } -console.log('PASS: M08-D admin operations overview and scoped business statistics are present.'); +console.log('PASS: M08-D admin operations and M08-D-R1 route architecture are present.'); diff --git a/scripts/dev/windows/test-all.ps1 b/scripts/dev/windows/test-all.ps1 index 19865d4..32c619e 100644 --- a/scripts/dev/windows/test-all.ps1 +++ b/scripts/dev/windows/test-all.ps1 @@ -1,28 +1,59 @@ $ErrorActionPreference = "Stop" +function Assert-NativeSuccess([string]$Step) { + if ($LASTEXITCODE -ne 0) { + throw "$Step failed with exit code $LASTEXITCODE." + } +} + & powershell -ExecutionPolicy Bypass -File scripts/dev/windows/check-workspace.ps1 +Assert-NativeSuccess "check-workspace" & powershell -ExecutionPolicy Bypass -File scripts/dev/windows/check-reference.ps1 +Assert-NativeSuccess "check-reference" & powershell -ExecutionPolicy Bypass -File scripts/dev/windows/check-line-endings.ps1 +Assert-NativeSuccess "check-line-endings" & powershell -ExecutionPolicy Bypass -File scripts/dev/windows/check-large-files.ps1 +Assert-NativeSuccess "check-large-files" & powershell -ExecutionPolicy Bypass -File scripts/dev/windows/check-repo-completeness.ps1 +Assert-NativeSuccess "check-repo-completeness" & powershell -ExecutionPolicy Bypass -File scripts/dev/windows/check-release-manifest.ps1 +Assert-NativeSuccess "check-release-manifest" & powershell -ExecutionPolicy Bypass -File scripts/dev/windows/check-status-docs.ps1 +Assert-NativeSuccess "check-status-docs" & powershell -ExecutionPolicy Bypass -File scripts/dev/windows/check-readme-config.ps1 +Assert-NativeSuccess "check-readme-config" & powershell -ExecutionPolicy Bypass -File scripts/dev/windows/check-backend.ps1 +Assert-NativeSuccess "check-backend" node scripts/check-admin-m08-b.mjs +Assert-NativeSuccess "check-admin-m08-b" node scripts/check-admin-m08-d.mjs +Assert-NativeSuccess "check-admin-m08-d" +node scripts/check-admin-m08-d-r1.mjs --source-only +Assert-NativeSuccess "check-admin-m08-d-r1 source" node scripts/check-miniapp-m08-c.mjs +Assert-NativeSuccess "check-miniapp-m08-c" if (Test-Path "admin/package.json") { Push-Location admin - npm run build - Pop-Location + try { + npm run build + Assert-NativeSuccess "admin build" + npm test + Assert-NativeSuccess "admin test" + node ../scripts/check-admin-m08-d-r1.mjs --dist-only + Assert-NativeSuccess "check-admin-m08-d-r1 dist" + } finally { + Pop-Location + } } if (Test-Path "package.json") { npm run lint --if-present + Assert-NativeSuccess "root lint" npm test --if-present + Assert-NativeSuccess "root test" npm run build --if-present + Assert-NativeSuccess "root build" } Write-Host "PASS: M00 local checks completed."