refactor(M08-D-R1): 后台改用权限路由与懒加载
This commit is contained in:
Generated
+459
-11
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -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"
|
||||
}
|
||||
|
||||
+186
-766
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,615 @@
|
||||
<template>
|
||||
<section class="cleaning-workspace">
|
||||
<section class="metric-grid" aria-label="保洁概览">
|
||||
<div class="metric">
|
||||
<span>待验收</span>
|
||||
<strong>{{ statistics.summary.pendingReview }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>已完成</span>
|
||||
<strong>{{ statistics.summary.completed }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>待发放</span>
|
||||
<strong>{{ money(statistics.summary.confirmedSettlementCents) }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>待结算</span>
|
||||
<strong>{{ money(statistics.summary.pendingSettlementCents) }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="action-board" aria-label="运营待处理">
|
||||
<button class="action-tile" type="button" @click="jumpToTaskStatus('SUBMITTED')">
|
||||
<ClipboardCheck :size="20" />
|
||||
<span>待验收任务</span>
|
||||
<strong>{{ statistics.summary.pendingReview }}</strong>
|
||||
</button>
|
||||
<button class="action-tile" type="button" @click="jumpToTaskStatus('REJECTED')">
|
||||
<RotateCcw :size="20" />
|
||||
<span>驳回补做</span>
|
||||
<strong>{{ statistics.summary.rejected }}</strong>
|
||||
</button>
|
||||
<button class="action-tile" type="button" @click="jumpToSettlementStatus('DRAFT')">
|
||||
<ListTodo :size="20" />
|
||||
<span>待确认结算</span>
|
||||
<strong>{{ draftSettlementTotal }}</strong>
|
||||
</button>
|
||||
<button class="action-tile" type="button" @click="jumpToSettlementStatus('CONFIRMED')">
|
||||
<Send :size="20" />
|
||||
<span>待发放金额</span>
|
||||
<strong>{{ money(statistics.summary.confirmedSettlementCents) }}</strong>
|
||||
</button>
|
||||
<button class="action-tile" type="button" @click="exportOperationalQueue">
|
||||
<Download :size="20" />
|
||||
<span>导出待处理</span>
|
||||
<strong>{{ operationalQueueTotal }}</strong>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<el-alert
|
||||
v-if="lastError"
|
||||
class="alert-line"
|
||||
:title="lastError"
|
||||
type="error"
|
||||
show-icon
|
||||
closable
|
||||
@close="lastError = ''"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="lastMessage"
|
||||
class="alert-line"
|
||||
:title="lastMessage"
|
||||
type="success"
|
||||
show-icon
|
||||
closable
|
||||
@close="lastMessage = ''"
|
||||
/>
|
||||
|
||||
<el-tabs v-model="activeTab" class="work-tabs">
|
||||
<el-tab-pane label="任务" name="tasks">
|
||||
<CleaningTasksPanel
|
||||
:session="session"
|
||||
:loading="loading.tasks"
|
||||
:items="tasks.items"
|
||||
:cleaners="cleaners.items"
|
||||
:total="tasks.total"
|
||||
:page="tasks.page"
|
||||
:page-size="tasks.pageSize"
|
||||
:status="taskStatus"
|
||||
:filters="taskFilters"
|
||||
@refresh="loadTasks"
|
||||
@status-change="changeTaskStatus"
|
||||
@filter="changeTaskFilters"
|
||||
@page-change="changeTaskPage"
|
||||
@assign="handleAssign"
|
||||
@complete="handleComplete"
|
||||
@complete-many="handleCompleteMany"
|
||||
@reject="handleReject"
|
||||
@reject-many="handleRejectMany"
|
||||
@exempt="handleExempt"
|
||||
@exempt-many="handleExemptMany"
|
||||
@reclaim="handleReclaim"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="结算" name="settlements">
|
||||
<CleaningSettlementsPanel
|
||||
:session="session"
|
||||
:loading="loading.settlements"
|
||||
:items="settlements.items"
|
||||
:cleaners="cleaners.items"
|
||||
:total="settlements.total"
|
||||
:page="settlements.page"
|
||||
:page-size="settlements.pageSize"
|
||||
:status="settlementStatus"
|
||||
:filters="settlementFilters"
|
||||
@refresh="loadSettlements"
|
||||
@status-change="changeSettlementStatus"
|
||||
@filter="changeSettlementFilters"
|
||||
@page-change="changeSettlementPage"
|
||||
@generate="handleGenerateSettlement"
|
||||
@confirm="handleConfirmSettlement"
|
||||
@transfer="handleTransfer"
|
||||
@sync-transfer="handleSyncTransfer"
|
||||
@sync-transfer-many="handleSyncTransferMany"
|
||||
@failure="handleFailure"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="统计" name="statistics">
|
||||
<CleaningStatisticsPanel
|
||||
:loading="loading.statistics"
|
||||
:statistics="statistics"
|
||||
:filters="statisticsFilters"
|
||||
:cleaners="cleaners.items"
|
||||
@refresh="loadStatistics"
|
||||
@filter="changeStatisticsFilters"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="保洁员" name="cleaners">
|
||||
<CleanersPanel
|
||||
:loading="loading.cleaners"
|
||||
:items="cleaners.items"
|
||||
:total="cleaners.total"
|
||||
:page="cleaners.page"
|
||||
:page-size="cleaners.pageSize"
|
||||
:status="cleanerStatus"
|
||||
:search="cleanerSearch"
|
||||
@refresh="loadCleaners"
|
||||
@status-change="changeCleanerStatus"
|
||||
@search-change="changeCleanerSearch"
|
||||
@page-change="changeCleanerPage"
|
||||
@create="handleCreateCleaner"
|
||||
@update="handleUpdateCleaner"
|
||||
@status-toggle="handleToggleCleanerStatus"
|
||||
@reset-sessions="handleResetCleanerSessions"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="联调" name="handoff">
|
||||
<CleaningFieldHandoffPanel />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, toRef } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ClipboardCheck, Download, ListTodo, RotateCcw, Send } from '@lucide/vue';
|
||||
import CleaningTasksPanel from './CleaningTasksPanel.vue';
|
||||
import CleaningSettlementsPanel from './CleaningSettlementsPanel.vue';
|
||||
import CleaningStatisticsPanel from './CleaningStatisticsPanel.vue';
|
||||
import CleanersPanel from './CleanersPanel.vue';
|
||||
import CleaningFieldHandoffPanel from './CleaningFieldHandoffPanel.vue';
|
||||
import {
|
||||
ApiError,
|
||||
assignCleaningTask,
|
||||
completeCleaningTask,
|
||||
confirmCleaningSettlement,
|
||||
createStaffUser,
|
||||
exemptCleaningTask,
|
||||
executeWechatTransfer,
|
||||
generateCleaningSettlement,
|
||||
getCleaningStatistics,
|
||||
listCleaningSettlements,
|
||||
listCleaningTasks,
|
||||
listStaffUsers,
|
||||
reclaimCleaningTimeouts,
|
||||
recordPayoutFailure,
|
||||
rejectCleaningTask,
|
||||
resetStaffSessions,
|
||||
syncWechatTransfer,
|
||||
updateStaffUser,
|
||||
type ApiSession
|
||||
} from '../api';
|
||||
import type {
|
||||
CleaningSettlement,
|
||||
CleaningStatistics,
|
||||
CleaningTask,
|
||||
ManagedUser,
|
||||
PageResult,
|
||||
PayoutStateFilter,
|
||||
SettlementStatus,
|
||||
TaskStatus,
|
||||
TransferMode,
|
||||
UserStatus
|
||||
} from '../types';
|
||||
import { money } from '../format';
|
||||
|
||||
const props = defineProps<{ session: ApiSession }>();
|
||||
const session = toRef(props, 'session');
|
||||
const activeTab = ref('tasks');
|
||||
const lastError = ref('');
|
||||
const lastMessage = ref('');
|
||||
const taskStatus = ref<TaskStatus | ''>('SUBMITTED');
|
||||
const settlementStatus = ref<SettlementStatus | ''>('CONFIRMED');
|
||||
const cleanerStatus = ref<UserStatus | ''>('ACTIVE');
|
||||
const cleanerSearch = ref('');
|
||||
const loading = reactive({ tasks: false, settlements: false, statistics: false, cleaners: false });
|
||||
const tasks = reactive<PageResult<CleaningTask>>({ items: [], total: 0, page: 1, pageSize: 20 });
|
||||
const settlements = reactive<PageResult<CleaningSettlement>>({
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
const cleaners = reactive<PageResult<ManagedUser>>({ items: [], total: 0, page: 1, pageSize: 20 });
|
||||
const taskFilters = reactive({ storeId: '', cleanerUserId: '' });
|
||||
const settlementFilters = reactive<{
|
||||
storeId: string;
|
||||
cleanerUserId: string;
|
||||
payoutState: PayoutStateFilter | '';
|
||||
}>({ storeId: '', cleanerUserId: '', payoutState: '' });
|
||||
const statisticsFilters = reactive({ from: '', to: '', storeId: '', cleanerUserId: '' });
|
||||
const statistics = reactive<CleaningStatistics>({
|
||||
summary: {
|
||||
taskTotal: 0,
|
||||
pendingReview: 0,
|
||||
active: 0,
|
||||
rejected: 0,
|
||||
exempted: 0,
|
||||
completed: 0,
|
||||
rewardCents: 0,
|
||||
pendingSettlementCents: 0,
|
||||
confirmedSettlementCents: 0,
|
||||
paidSettlementCents: 0
|
||||
},
|
||||
byStatus: [],
|
||||
byStore: [],
|
||||
settlements: [],
|
||||
members: [],
|
||||
trend: []
|
||||
});
|
||||
const draftSettlementTotal = computed(() => statistics.settlements
|
||||
.find((item) => item.status === 'DRAFT')?.total || 0);
|
||||
const operationalQueueTotal = computed(() => (
|
||||
statistics.summary.pendingReview
|
||||
+ statistics.summary.rejected
|
||||
+ draftSettlementTotal.value
|
||||
));
|
||||
|
||||
function loadCleaningWorkspace() {
|
||||
return Promise.all([loadTasks(), loadSettlements(), loadStatistics(), loadCleaners()]);
|
||||
}
|
||||
|
||||
async function runAction(work: () => Promise<void>, success: string) {
|
||||
lastError.value = '';
|
||||
lastMessage.value = '';
|
||||
try {
|
||||
await work();
|
||||
lastMessage.value = success;
|
||||
ElMessage.success(success);
|
||||
} catch (error) {
|
||||
lastError.value = error instanceof ApiError
|
||||
? `${error.code}${error.traceId ? ` · ${error.traceId}` : ''}`
|
||||
: error instanceof Error ? error.message : '操作失败';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTasks() {
|
||||
loading.tasks = true;
|
||||
await runAction(async () => {
|
||||
Object.assign(tasks, await listCleaningTasks(session.value, {
|
||||
page: tasks.page,
|
||||
pageSize: tasks.pageSize,
|
||||
status: taskStatus.value || undefined,
|
||||
storeId: taskFilters.storeId || undefined,
|
||||
cleanerUserId: taskFilters.cleanerUserId || undefined
|
||||
}));
|
||||
}, '任务已刷新');
|
||||
loading.tasks = false;
|
||||
}
|
||||
|
||||
async function loadSettlements() {
|
||||
loading.settlements = true;
|
||||
await runAction(async () => {
|
||||
Object.assign(settlements, await listCleaningSettlements(session.value, {
|
||||
page: settlements.page,
|
||||
pageSize: settlements.pageSize,
|
||||
status: settlementStatus.value || undefined,
|
||||
payoutState: settlementFilters.payoutState || undefined,
|
||||
storeId: settlementFilters.storeId || undefined,
|
||||
cleanerUserId: settlementFilters.cleanerUserId || undefined
|
||||
}));
|
||||
}, '结算已刷新');
|
||||
loading.settlements = false;
|
||||
}
|
||||
|
||||
async function loadStatistics() {
|
||||
loading.statistics = true;
|
||||
await runAction(async () => {
|
||||
const result = await getCleaningStatistics(session.value, {
|
||||
from: statisticsFilters.from || undefined,
|
||||
to: statisticsFilters.to || undefined,
|
||||
storeId: statisticsFilters.storeId || undefined,
|
||||
cleanerUserId: statisticsFilters.cleanerUserId || undefined
|
||||
});
|
||||
Object.assign(statistics.summary, result.summary);
|
||||
statistics.byStatus = result.byStatus;
|
||||
statistics.byStore = result.byStore;
|
||||
statistics.settlements = result.settlements;
|
||||
statistics.members = result.members;
|
||||
statistics.trend = result.trend;
|
||||
}, '统计已刷新');
|
||||
loading.statistics = false;
|
||||
}
|
||||
|
||||
async function loadCleaners() {
|
||||
loading.cleaners = true;
|
||||
await runAction(async () => {
|
||||
const result = await listStaffUsers(session.value, {
|
||||
page: cleaners.page,
|
||||
pageSize: cleaners.pageSize,
|
||||
role: 'CLEANER',
|
||||
status: cleanerStatus.value || undefined,
|
||||
search: cleanerSearch.value || undefined
|
||||
});
|
||||
Object.assign(cleaners, {
|
||||
items: result.items,
|
||||
total: result.total,
|
||||
page: cleaners.page,
|
||||
pageSize: cleaners.pageSize
|
||||
});
|
||||
}, '保洁员已刷新');
|
||||
loading.cleaners = false;
|
||||
}
|
||||
|
||||
function changeTaskStatus(status: TaskStatus | '') {
|
||||
taskStatus.value = status;
|
||||
tasks.page = 1;
|
||||
void loadTasks();
|
||||
}
|
||||
|
||||
function jumpToTaskStatus(status: TaskStatus) {
|
||||
activeTab.value = 'tasks';
|
||||
taskStatus.value = status;
|
||||
tasks.page = 1;
|
||||
void loadTasks();
|
||||
}
|
||||
|
||||
function changeTaskFilters(input: { storeId: string; cleanerUserId: string }) {
|
||||
Object.assign(taskFilters, input);
|
||||
tasks.page = 1;
|
||||
void loadTasks();
|
||||
}
|
||||
|
||||
function changeSettlementStatus(status: SettlementStatus | '') {
|
||||
settlementStatus.value = status;
|
||||
settlements.page = 1;
|
||||
void loadSettlements();
|
||||
}
|
||||
|
||||
function jumpToSettlementStatus(status: SettlementStatus) {
|
||||
activeTab.value = 'settlements';
|
||||
settlementStatus.value = status;
|
||||
settlementFilters.payoutState = '';
|
||||
settlements.page = 1;
|
||||
void loadSettlements();
|
||||
}
|
||||
|
||||
function exportOperationalQueue() {
|
||||
downloadCsv(`cleaning-operational-queue-${Date.now()}.csv`, [
|
||||
['队列', '数量', '金额', '入口'],
|
||||
['待验收任务', String(statistics.summary.pendingReview), '', '任务/SUBMITTED'],
|
||||
['驳回补做', String(statistics.summary.rejected), '', '任务/REJECTED'],
|
||||
['待确认结算', String(draftSettlementTotal.value), '', '结算/DRAFT'],
|
||||
['待发放金额', '', money(statistics.summary.confirmedSettlementCents), '结算/CONFIRMED'],
|
||||
['待结算金额', '', money(statistics.summary.pendingSettlementCents), '统计/待结算'],
|
||||
['已完成任务', String(statistics.summary.completed), '', '统计/已完成']
|
||||
]);
|
||||
}
|
||||
|
||||
function downloadCsv(filename: string, rows: string[][]) {
|
||||
const content = rows.map((row) => row.map(csvCell).join(',')).join('\r\n');
|
||||
const blob = new Blob([`\uFEFF${content}`], { type: 'text/csv;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function csvCell(value: string) {
|
||||
const normalized = value.replace(/\r?\n/g, ' ');
|
||||
return /[",\r\n]/.test(normalized) ? `"${normalized.replace(/"/g, '""')}"` : normalized;
|
||||
}
|
||||
|
||||
function changeSettlementFilters(input: {
|
||||
storeId: string;
|
||||
cleanerUserId: string;
|
||||
payoutState: PayoutStateFilter | '';
|
||||
}) {
|
||||
Object.assign(settlementFilters, input);
|
||||
settlements.page = 1;
|
||||
void loadSettlements();
|
||||
}
|
||||
|
||||
function changeTaskPage(page: number) {
|
||||
tasks.page = page;
|
||||
void loadTasks();
|
||||
}
|
||||
|
||||
function changeSettlementPage(page: number) {
|
||||
settlements.page = page;
|
||||
void loadSettlements();
|
||||
}
|
||||
|
||||
function changeStatisticsFilters(input: {
|
||||
from: string;
|
||||
to: string;
|
||||
storeId: string;
|
||||
cleanerUserId: string;
|
||||
}) {
|
||||
Object.assign(statisticsFilters, input);
|
||||
void loadStatistics();
|
||||
}
|
||||
|
||||
function changeCleanerStatus(status: UserStatus | '') {
|
||||
cleanerStatus.value = status;
|
||||
cleaners.page = 1;
|
||||
void loadCleaners();
|
||||
}
|
||||
|
||||
function changeCleanerSearch(search: string) {
|
||||
cleanerSearch.value = search;
|
||||
cleaners.page = 1;
|
||||
void loadCleaners();
|
||||
}
|
||||
|
||||
function changeCleanerPage(page: number) {
|
||||
cleaners.page = page;
|
||||
void loadCleaners();
|
||||
}
|
||||
|
||||
async function handleAssign(input: { taskId: string; cleanerUserId: string; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await assignCleaningTask(session.value, input.taskId, input);
|
||||
await Promise.all([loadTasks(), loadStatistics()]);
|
||||
}, '已指派');
|
||||
}
|
||||
|
||||
async function handleComplete(input: { taskId: string; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await completeCleaningTask(session.value, input.taskId, input.note);
|
||||
await Promise.all([loadTasks(), loadStatistics()]);
|
||||
}, '已验收');
|
||||
}
|
||||
|
||||
async function handleCompleteMany(input: { taskIds: string[]; note?: string }) {
|
||||
await runAction(async () => {
|
||||
for (const taskId of input.taskIds) await completeCleaningTask(session.value, taskId, input.note);
|
||||
await Promise.all([loadTasks(), loadStatistics()]);
|
||||
}, `已批量验收 ${input.taskIds.length} 个任务`);
|
||||
}
|
||||
|
||||
async function handleReject(input: { taskId: string; reason: string }) {
|
||||
await runAction(async () => {
|
||||
await rejectCleaningTask(session.value, input.taskId, input.reason);
|
||||
await Promise.all([loadTasks(), loadStatistics()]);
|
||||
}, '已驳回');
|
||||
}
|
||||
|
||||
async function handleRejectMany(input: { taskIds: string[]; reason: string }) {
|
||||
await runAction(async () => {
|
||||
for (const taskId of input.taskIds) await rejectCleaningTask(session.value, taskId, input.reason);
|
||||
await Promise.all([loadTasks(), loadStatistics()]);
|
||||
}, `已批量驳回 ${input.taskIds.length} 个任务`);
|
||||
}
|
||||
|
||||
async function handleExempt(input: { taskId: string; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await exemptCleaningTask(session.value, input.taskId, input.note);
|
||||
await Promise.all([loadTasks(), loadStatistics()]);
|
||||
}, '已标记免清洁');
|
||||
}
|
||||
|
||||
async function handleExemptMany(input: { taskIds: string[]; note?: string }) {
|
||||
await runAction(async () => {
|
||||
for (const taskId of input.taskIds) await exemptCleaningTask(session.value, taskId, input.note);
|
||||
await Promise.all([loadTasks(), loadStatistics()]);
|
||||
}, `已批量标记免清洁 ${input.taskIds.length} 个任务`);
|
||||
}
|
||||
|
||||
async function handleReclaim(input: { olderThanMinutes: number; limit: number }) {
|
||||
await runAction(async () => {
|
||||
await reclaimCleaningTimeouts(session.value, input);
|
||||
await Promise.all([loadTasks(), loadStatistics()]);
|
||||
}, '已回收超时任务');
|
||||
}
|
||||
|
||||
async function handleGenerateSettlement(input: {
|
||||
cleanerUserId: string;
|
||||
storeId?: string;
|
||||
note?: string;
|
||||
}) {
|
||||
await runAction(async () => {
|
||||
await generateCleaningSettlement(session.value, input);
|
||||
await Promise.all([loadTasks(), loadSettlements(), loadStatistics()]);
|
||||
}, '已生成结算单');
|
||||
}
|
||||
|
||||
async function handleConfirmSettlement(input: { settlementId: string; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await confirmCleaningSettlement(session.value, input.settlementId, input.note);
|
||||
await Promise.all([loadSettlements(), loadStatistics()]);
|
||||
}, '已确认结算单');
|
||||
}
|
||||
|
||||
async function handleTransfer(input: { settlementId: string; mode: TransferMode; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await executeWechatTransfer(session.value, input.settlementId, {
|
||||
mode: input.mode,
|
||||
note: input.note
|
||||
});
|
||||
await Promise.all([loadSettlements(), loadStatistics()]);
|
||||
}, '已提交微信转账');
|
||||
}
|
||||
|
||||
async function handleSyncTransfer(input: { settlementId: string; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await syncWechatTransfer(session.value, input.settlementId, input.note);
|
||||
await Promise.all([loadSettlements(), loadStatistics()]);
|
||||
}, '已同步微信状态');
|
||||
}
|
||||
|
||||
async function handleSyncTransferMany(input: { settlementIds: string[]; note?: string }) {
|
||||
await runAction(async () => {
|
||||
for (const settlementId of input.settlementIds) {
|
||||
await syncWechatTransfer(session.value, settlementId, input.note);
|
||||
}
|
||||
await Promise.all([loadSettlements(), loadStatistics()]);
|
||||
}, `已批量同步 ${input.settlementIds.length} 个结算单`);
|
||||
}
|
||||
|
||||
async function handleFailure(input: {
|
||||
settlementId: string;
|
||||
error: string;
|
||||
payoutReference?: string;
|
||||
note?: string;
|
||||
}) {
|
||||
await runAction(async () => {
|
||||
await recordPayoutFailure(session.value, input.settlementId, {
|
||||
error: input.error,
|
||||
payoutChannel: 'WECHAT_TRANSFER',
|
||||
payoutReference: input.payoutReference,
|
||||
note: input.note
|
||||
});
|
||||
await Promise.all([loadSettlements(), loadStatistics()]);
|
||||
}, '已记录失败');
|
||||
}
|
||||
|
||||
async function handleCreateCleaner(input: {
|
||||
nickname: string;
|
||||
phone: string;
|
||||
storeIds: string[];
|
||||
note?: string;
|
||||
}) {
|
||||
await runAction(async () => {
|
||||
await createStaffUser(session.value, {
|
||||
nickname: input.nickname,
|
||||
phone: input.phone,
|
||||
roles: ['CLEANER'],
|
||||
storeIds: input.storeIds,
|
||||
note: input.note
|
||||
});
|
||||
await loadCleaners();
|
||||
}, '已新增保洁员');
|
||||
}
|
||||
|
||||
async function handleUpdateCleaner(input: {
|
||||
userId: string;
|
||||
nickname: string;
|
||||
phone?: string;
|
||||
storeIds: string[];
|
||||
note?: string;
|
||||
}) {
|
||||
await runAction(async () => {
|
||||
await updateStaffUser(session.value, input.userId, {
|
||||
nickname: input.nickname,
|
||||
phone: input.phone,
|
||||
roles: ['CLEANER'],
|
||||
storeIds: input.storeIds,
|
||||
note: input.note
|
||||
});
|
||||
await loadCleaners();
|
||||
}, '已更新保洁员');
|
||||
}
|
||||
|
||||
async function handleToggleCleanerStatus(input: { userId: string; status: UserStatus }) {
|
||||
await runAction(async () => {
|
||||
await updateStaffUser(session.value, input.userId, { status: input.status });
|
||||
await loadCleaners();
|
||||
}, input.status === 'ACTIVE' ? '已启用保洁员' : '已停用保洁员');
|
||||
}
|
||||
|
||||
async function handleResetCleanerSessions(userId: string) {
|
||||
await runAction(async () => {
|
||||
await resetStaffSessions(session.value, userId);
|
||||
await loadCleaners();
|
||||
}, '已重置保洁员会话');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (session.value.token) void loadCleaningWorkspace();
|
||||
});
|
||||
</script>
|
||||
+2
-3
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<AdminMenuKey, () => Promise<unknown>>;
|
||||
|
||||
const businessRoutes = ADMIN_NAVIGATION.map<RouteRecordRaw>((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;
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.');
|
||||
+1
-1
@@ -16,5 +16,5 @@
|
||||
"noUnusedParameters": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||
"include": ["src/**/*.ts", "src/**/*.mts", "src/**/*.vue"]
|
||||
}
|
||||
|
||||
+10
-3
@@ -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']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<AdminSessionPayload> \| null = null/);
|
||||
assert.match(api, /if \(!refreshInFlight\)/);
|
||||
assert.match(api, /refreshInFlight = publicRequest<AdminSessionPayload>\('\/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.');
|
||||
}
|
||||
@@ -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.');
|
||||
|
||||
@@ -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
|
||||
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."
|
||||
|
||||
Reference in New Issue
Block a user