平台配置页改造
This commit is contained in:
Generated
+420
@@ -19,6 +19,7 @@
|
||||
"@types/node": "^24.12.0",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vue/tsconfig": "^0.9.0",
|
||||
"sass": "^1.99.0",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^32.0.0",
|
||||
@@ -230,6 +231,334 @@
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
|
||||
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"node-addon-api": "^7.0.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.6",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.6",
|
||||
"@parcel/watcher-darwin-x64": "2.5.6",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.6",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.6",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.6",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.6",
|
||||
"@parcel/watcher-win32-arm64": "2.5.6",
|
||||
"@parcel/watcher-win32-ia32": "2.5.6",
|
||||
"@parcel/watcher-win32-x64": "2.5.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
|
||||
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"name": "@sxzz/popperjs-es",
|
||||
"version": "2.11.8",
|
||||
@@ -1335,6 +1664,24 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
|
||||
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"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,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
@@ -1344,6 +1691,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"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,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
@@ -1767,6 +2128,14 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/normalize-wheel-es": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||
@@ -2024,6 +2393,57 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.99.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz",
|
||||
"integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.1.5",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/scule": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@types/node": "^24.12.0",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vue/tsconfig": "^0.9.0",
|
||||
"sass": "^1.99.0",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^32.0.0",
|
||||
|
||||
Vendored
+20
-1
@@ -19,23 +19,42 @@ declare module 'vue' {
|
||||
AdminPlatformNotificationSection: typeof import('./components/admin/AdminPlatformNotificationSection.vue')['default']
|
||||
AdminResultCard: typeof import('./components/admin/AdminResultCard.vue')['default']
|
||||
AdminStatusTag: typeof import('./components/admin/AdminStatusTag.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElResult: typeof import('element-plus/es')['ElResult']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
TencentAuthCard: typeof import('./components/tencent/TencentAuthCard.vue')['default']
|
||||
TencentRedeemPanel: typeof import('./components/tencent/TencentRedeemPanel.vue')['default']
|
||||
TencentResultPanel: typeof import('./components/tencent/TencentResultPanel.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,161 +24,252 @@ defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="meta-card">
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">配置文件</span>
|
||||
<code>{{ agisoFilePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">职责</span>
|
||||
<span>管理 Agiso 默认消息模板、店铺 AccessToken 以及店铺级模板覆盖。</span>
|
||||
</div>
|
||||
</section>
|
||||
<div class="platform-section">
|
||||
<!-- 配置信息 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">配置信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="配置文件">
|
||||
<code>{{ agisoFilePath || '-' }}</code>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="职责">
|
||||
管理 Agiso 默认消息模板、店铺 AccessToken 以及店铺级模板覆盖。
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>默认消息模板</h3>
|
||||
<p>Agiso 店铺未单独配置时,统一使用这里的领取消息与自动发货消息模板。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="field-block field-wide">
|
||||
<span>默认领取链接消息模板</span>
|
||||
<textarea
|
||||
v-model="defaults.messageTemplate"
|
||||
class="text-area"
|
||||
placeholder="例如:您的订单 {platformOrderId} 已创建领取链接,请尽快打开并完成领取:{claimUrl}"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>默认自动发货消息模板</span>
|
||||
<textarea
|
||||
v-model="defaults.autoDeliveryMessageTemplate"
|
||||
class="text-area"
|
||||
placeholder="例如:您的订单 {platformOrderId} 已完成自动发货,请注意查收。"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>Agiso 店铺列表</h3>
|
||||
<p>每家店铺可单独配置 AccessToken 和模板覆盖。建议先用“近期识别到的店铺”导入,再补 AccessToken。</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :disabled="shops.length === 0" text @click="expandAllShops">全部展开</el-button>
|
||||
<el-button :disabled="shops.length === 0" text @click="collapseAllShops">全部折叠</el-button>
|
||||
<el-button :loading="saving" round type="primary" @click="saveAgisoConfigs">保存 Agiso 配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="shops.length === 0" class="empty-inline">当前还没有 Agiso 店铺配置,先新增一条。</div>
|
||||
|
||||
<div v-for="shop in shops" :key="shop.id" class="shop-card">
|
||||
<button type="button" class="shop-summary" @click="toggleShop(shop.id)">
|
||||
<div class="shop-summary-main">
|
||||
<strong>{{ shop.shopName || shop.shopId || '未命名店铺' }}</strong>
|
||||
<span class="cell-subtle">ID: {{ shop.shopId || '未填写' }}</span>
|
||||
<!-- 默认消息模板 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">默认消息模板</span>
|
||||
<p class="card-desc">Agiso 店铺未单独配置时,统一使用这里的领取消息与自动发货消息模板。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shop-summary-side">
|
||||
<span class="shop-status" :class="{ 'is-disabled': !shop.enabled }">
|
||||
{{ shop.enabled ? '已启用' : '已停用' }}
|
||||
</span>
|
||||
<span class="shop-summary-copy">{{ describeShop(shop) }}</span>
|
||||
<span class="shop-toggle">{{ isShopExpanded(shop.id) ? '收起' : '展开' }}</span>
|
||||
</template>
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item label="默认领取链接消息模板">
|
||||
<el-input
|
||||
v-model="defaults.messageTemplate"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="例如:您的订单 {platformOrderId} 已创建领取链接,请尽快打开并完成领取:{claimUrl}"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="默认自动发货消息模板">
|
||||
<el-input
|
||||
v-model="defaults.autoDeliveryMessageTemplate"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="例如:您的订单 {platformOrderId} 已完成自动发货,请注意查收。"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 店铺列表 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">Agiso 店铺列表</span>
|
||||
<p class="card-desc">每家店铺可单独配置 AccessToken 和模板覆盖。建议先用"近期识别到的店铺"导入,再补 AccessToken。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button :disabled="shops.length === 0" @click="expandAllShops">全部展开</el-button>
|
||||
<el-button :disabled="shops.length === 0" @click="collapseAllShops">全部折叠</el-button>
|
||||
<el-button :loading="saving" type="primary" @click="saveAgisoConfigs">保存 Agiso 配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div v-if="isShopExpanded(shop.id)" class="shop-body">
|
||||
<div class="form-grid">
|
||||
<label class="field-block">
|
||||
<span>店铺 ID</span>
|
||||
<input v-model="shop.shopId" class="text-input" placeholder="例如 693760716" />
|
||||
</label>
|
||||
<el-empty v-if="shops.length === 0" description="当前还没有 Agiso 店铺配置,先新增一条。" />
|
||||
|
||||
<label class="field-block">
|
||||
<span>店铺名称</span>
|
||||
<input v-model="shop.shopName" class="text-input" placeholder="例如 大锤商行" />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>AccessToken</span>
|
||||
<input v-model="shop.accessToken" class="text-input" placeholder="AldsIdle..." />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>领取链接消息模板</span>
|
||||
<textarea
|
||||
v-model="shop.messageTemplate"
|
||||
class="text-area"
|
||||
placeholder="留空则沿用默认领取链接模板"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>自动发货消息模板</span>
|
||||
<textarea
|
||||
v-model="shop.autoDeliveryMessageTemplate"
|
||||
class="text-area"
|
||||
placeholder="留空则沿用默认自动发货模板"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="shop-actions">
|
||||
<label class="checkbox-line">
|
||||
<input v-model="shop.enabled" type="checkbox" />
|
||||
<span>启用店铺级覆盖</span>
|
||||
</label>
|
||||
<el-button link type="danger" @click="removeShop(shop.id)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>近期识别到的 Agiso 店铺</h3>
|
||||
<p>从 webhook 自动识别出的店铺,可一键导入到上方店铺列表。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>店铺</th>
|
||||
<th>Webhook 次数</th>
|
||||
<th>最近出现时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in observedShops" :key="item.shopId">
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.displayShopName || item.shopId }}</strong>
|
||||
<span class="cell-subtle">ID: {{ item.shopId }}</span>
|
||||
<el-collapse
|
||||
v-else
|
||||
:model-value="shops.filter(s => isShopExpanded(s.id)).map(s => s.id)"
|
||||
>
|
||||
<el-collapse-item
|
||||
v-for="shop in shops"
|
||||
:key="shop.id"
|
||||
:name="shop.id"
|
||||
>
|
||||
<template #title>
|
||||
<div class="shop-title">
|
||||
<strong>{{ shop.shopName || shop.shopId || '未命名店铺' }}</strong>
|
||||
<span class="shop-subtitle">ID: {{ shop.shopId || '未填写' }}</span>
|
||||
<el-tag
|
||||
:type="shop.enabled ? 'success' : 'info'"
|
||||
size="small"
|
||||
style="margin-left: auto; margin-right: 12px"
|
||||
>
|
||||
{{ shop.enabled ? '已启用' : '已停用' }}
|
||||
</el-tag>
|
||||
<span class="shop-brief">{{ describeShop(shop) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.webhookEventCount }}</td>
|
||||
<td>{{ formatAdminDateTime(item.latestSeenAt) }}</td>
|
||||
<td>{{ item.configured ? '已配置' : '未配置' }}</td>
|
||||
<td>
|
||||
<el-button v-if="!item.configured" link type="primary" @click="importObservedShop(item)">导入到店铺</el-button>
|
||||
<span v-else class="cell-subtle">已存在</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="observedShops.length === 0">
|
||||
<td colspan="5" class="empty-inline">最近还没有识别到可用店铺。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="店铺 ID">
|
||||
<el-input v-model="shop.shopId" placeholder="例如 693760716" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="店铺名称">
|
||||
<el-input v-model="shop.shopName" placeholder="例如 大锤商行" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="AccessToken">
|
||||
<el-input v-model="shop.accessToken" placeholder="AldsIdle..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="领取链接消息模板">
|
||||
<el-input
|
||||
v-model="shop.messageTemplate"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="留空则沿用默认领取链接模板"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="自动发货消息模板">
|
||||
<el-input
|
||||
v-model="shop.autoDeliveryMessageTemplate"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="留空则沿用默认自动发货模板"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="shop-footer">
|
||||
<el-switch v-model="shop.enabled" active-text="启用店铺级覆盖" />
|
||||
<el-button type="danger" size="small" @click="removeShop(shop.id)">删除</el-button>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
|
||||
<!-- 近期识别到的店铺 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">近期识别到的 Agiso 店铺</span>
|
||||
<p class="card-desc">从 webhook 自动识别出的店铺,可一键导入到上方店铺列表。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="observedShops" stripe size="small">
|
||||
<el-table-column label="店铺" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<div>{{ row.displayShopName || row.shopId }}</div>
|
||||
<div style="font-size: var(--text-xs); color: var(--text-muted)">ID: {{ row.shopId }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="webhookEventCount" label="Webhook 次数" width="120" align="center" />
|
||||
<el-table-column label="最近出现时间" width="180">
|
||||
<template #default="{ row }">{{ formatAdminDateTime(row.latestSeenAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.configured ? 'success' : 'warning'" size="small">
|
||||
{{ row.configured ? '已配置' : '未配置' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="!row.configured" size="small" type="primary" @click="importObservedShop(row)">
|
||||
导入到店铺
|
||||
</el-button>
|
||||
<span v-else class="text-muted">已存在</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="最近还没有识别到可用店铺。" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.shop-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: var(--space-4);
|
||||
}
|
||||
|
||||
.shop-title strong {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shop-subtitle {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.shop-brief {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.shop-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: var(--space-3);
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -69,183 +69,265 @@ defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="meta-card">
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">配置文件</span>
|
||||
<code>{{ cloudtentaclesFilePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">会话文件</span>
|
||||
<code>{{ cloudtentaclesSessionFilePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">职责</span>
|
||||
<span>管理 cloudtentacles 外部履约平台的账号、短信验证码登录与 token 探活。该区域不处理订单来源同步。</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">已保存 token</span>
|
||||
<span>
|
||||
{{ cloudtentaclesPersistedSession?.hasToken ? `${cloudtentaclesPersistedSession.tokenMasked} / ${formatAdminDateTime(cloudtentaclesPersistedSession.loggedInAt)}` : '未保存' }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<div class="platform-section">
|
||||
<!-- 配置信息 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">配置信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="配置文件">
|
||||
<code>{{ cloudtentaclesFilePath || '-' }}</code>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="会话文件">
|
||||
<code>{{ cloudtentaclesSessionFilePath || '-' }}</code>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="职责">
|
||||
管理 cloudtentacles 外部履约平台的账号、短信验证码登录与 token 探活。该区域不处理订单来源同步。
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已保存 token">
|
||||
{{ cloudtentaclesPersistedSession?.hasToken ? `${cloudtentaclesPersistedSession.tokenMasked} / ${formatAdminDateTime(cloudtentaclesPersistedSession.loggedInAt)}` : '未保存' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>cloudtentacles 履约平台配置</h3>
|
||||
<p>先保存账号、密码、手机号和设备头,再发送短信验证码,最后执行登录测试或手动校验 token。</p>
|
||||
<!-- 履约平台配置 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 履约平台配置</span>
|
||||
<p class="card-desc">先保存账号、密码、手机号和设备头,再发送短信验证码,最后执行登录测试或手动校验 token。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button :loading="cloudtentaclesSaving" @click="handleCloudtentaclesSaveSource">保存履约配置</el-button>
|
||||
<el-button :loading="cloudtentaclesSendingSms" @click="handleCloudtentaclesSendSmsCode">发送短信验证码</el-button>
|
||||
<el-button :loading="cloudtentaclesTesting" type="primary" @click="handleCloudtentaclesTestLogin">测试登录</el-button>
|
||||
<el-button :loading="cloudtentaclesValidating" @click="handleCloudtentaclesValidateSession">校验 token</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item label="Base URL">
|
||||
<el-input v-model="cloudtentaclesForm.baseUrl" placeholder="https://123.207.217.176" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="账号">
|
||||
<el-input v-model="cloudtentaclesForm.username" placeholder="cloudtentacles 登录账号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="cloudtentaclesForm.password" type="password" show-password placeholder="cloudtentacles 登录密码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="cloudtentaclesForm.phone" placeholder="接收短信验证码的手机号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="短信验证码">
|
||||
<el-input v-model="cloudtentaclesForm.code" placeholder="收到验证码后填写" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="VN Key">
|
||||
<el-input v-model="cloudtentaclesForm.vnKey" placeholder="例如 1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="VN ID">
|
||||
<el-input-number v-model="cloudtentaclesForm.vnId" :min="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="SKU ID">
|
||||
<el-input-number v-model="cloudtentaclesForm.skuId" :min="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="购买数量">
|
||||
<el-input-number v-model="cloudtentaclesForm.skuCount" :min="1" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="deviceId">
|
||||
<el-input v-model="cloudtentaclesForm.deviceId" placeholder="-" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="deviceType">
|
||||
<el-input-number v-model="cloudtentaclesForm.deviceType" :min="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="token(可手动粘贴)">
|
||||
<el-input v-model="cloudtentaclesForm.token" placeholder="登录成功后可自动回填,也可手动粘贴" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<el-alert
|
||||
v-if="cloudtentaclesResultError"
|
||||
:title="cloudtentaclesResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-top: 16px"
|
||||
/>
|
||||
|
||||
<div class="result-grid" style="margin-top: 16px">
|
||||
<AdminResultCard label="短信发送" :value="cloudtentaclesSmsResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesSmsResult">
|
||||
{{ cloudtentaclesSmsResult.phoneMasked }},{{ formatAdminDateTime(cloudtentaclesSmsResult.sentAt) }}
|
||||
</p>
|
||||
<p v-else>先发送短信验证码,确认手机号可用。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard label="登录结果" :value="cloudtentaclesLoginResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesLoginResult">
|
||||
{{ cloudtentaclesLoginResult.phoneMasked }},{{ formatAdminDateTime(cloudtentaclesLoginResult.loggedInAt) }}
|
||||
</p>
|
||||
<p v-else>填写短信验证码后可执行测试登录。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard label="会话校验" :value="cloudtentaclesValidateResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesValidateResult">
|
||||
权限 {{ cloudtentaclesValidateResult.session.permissionCount }} 项,余额 {{ cloudtentaclesValidateResult.asset }}
|
||||
</p>
|
||||
<p v-else>登录成功后可继续校验 token 会话。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :loading="cloudtentaclesSaving" round @click="handleCloudtentaclesSaveSource">保存履约配置</el-button>
|
||||
<el-button :loading="cloudtentaclesSendingSms" round @click="handleCloudtentaclesSendSmsCode">发送短信验证码</el-button>
|
||||
<el-button :loading="cloudtentaclesTesting" round type="primary" @click="handleCloudtentaclesTestLogin">测试登录</el-button>
|
||||
<el-button :loading="cloudtentaclesValidating" round @click="handleCloudtentaclesValidateSession">校验 token</el-button>
|
||||
</el-card>
|
||||
|
||||
<!-- 开发调试工具 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 开发调试工具</span>
|
||||
<p class="card-desc">仅开发模式展示。用于调试分类、SKU、购买、发货、虚拟号、兑换链接和结束退还链路。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="debug-buttons">
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchAsset">余额</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchCategories">分类</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchSkuList">SKU</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesBuySku">购买</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" type="primary" @click="handleCloudtentaclesUseSku">发货</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchKnapsack">背包</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchVnList">虚拟号列表</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesAppointVn">申请虚拟号</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesGenerateVnLoginCode">生成登录码</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchVnCode">获取验证码</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesVerifyVnCode">校验验证码</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchBindUrl">兑换链接</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" type="warning" @click="handleCloudtentaclesBackVn">退还号码</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" type="success" @click="handleCloudtentaclesRunFullFlow">完整流程</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="field-block field-wide">
|
||||
<span>Base URL</span>
|
||||
<input v-model="cloudtentaclesForm.baseUrl" class="text-input" placeholder="https://123.207.217.176" />
|
||||
</label>
|
||||
<pre v-if="cloudtentaclesDebugResult" class="debug-pre" style="margin-top: 16px">{{ cloudtentaclesDebugResult }}</pre>
|
||||
<el-empty v-else description="执行一次调试动作后,这里会显示原始返回数据。" :image-size="60" />
|
||||
</el-card>
|
||||
|
||||
<label class="field-block">
|
||||
<span>账号</span>
|
||||
<input v-model="cloudtentaclesForm.username" class="text-input" placeholder="cloudtentacles 登录账号" />
|
||||
</label>
|
||||
<!-- 调试结果 -->
|
||||
<el-card v-if="cloudtentaclesLoginResult || cloudtentaclesValidateResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 调试结果</span>
|
||||
<p class="card-desc">用于确认当前账号会话、权限和用户信息,后续发货执行器会复用这里的登录能力。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<label class="field-block">
|
||||
<span>密码</span>
|
||||
<input v-model="cloudtentaclesForm.password" class="text-input" type="password" placeholder="cloudtentacles 登录密码" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>手机号</span>
|
||||
<input v-model="cloudtentaclesForm.phone" class="text-input" placeholder="接收短信验证码的手机号" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>短信验证码</span>
|
||||
<input v-model="cloudtentaclesForm.code" class="text-input" placeholder="收到验证码后填写" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>VN Key</span>
|
||||
<input v-model="cloudtentaclesForm.vnKey" class="text-input" placeholder="例如 1" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>VN ID</span>
|
||||
<input v-model.number="cloudtentaclesForm.vnId" class="text-input" type="number" min="0" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>SKU ID</span>
|
||||
<input v-model.number="cloudtentaclesForm.skuId" class="text-input" type="number" min="0" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>购买数量</span>
|
||||
<input v-model.number="cloudtentaclesForm.skuCount" class="text-input" type="number" min="1" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>deviceId</span>
|
||||
<input v-model="cloudtentaclesForm.deviceId" class="text-input" placeholder="-" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>deviceType</span>
|
||||
<input v-model.number="cloudtentaclesForm.deviceType" class="text-input" type="number" />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>token(可手动粘贴)</span>
|
||||
<input v-model="cloudtentaclesForm.token" class="text-input" placeholder="登录成功后可自动回填,也可手动粘贴" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p v-if="cloudtentaclesResultError" class="error-copy">{{ cloudtentaclesResultError }}</p>
|
||||
|
||||
<div class="result-grid">
|
||||
<AdminResultCard label="短信发送" :value="cloudtentaclesSmsResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesSmsResult">
|
||||
{{ cloudtentaclesSmsResult.phoneMasked }},{{ formatAdminDateTime(cloudtentaclesSmsResult.sentAt) }}
|
||||
</p>
|
||||
<p v-else>先发送短信验证码,确认手机号可用。</p>
|
||||
</AdminResultCard>
|
||||
|
||||
<AdminResultCard label="登录结果" :value="cloudtentaclesLoginResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesLoginResult">
|
||||
{{ cloudtentaclesLoginResult.phoneMasked }},{{ formatAdminDateTime(cloudtentaclesLoginResult.loggedInAt) }}
|
||||
</p>
|
||||
<p v-else>填写短信验证码后可执行测试登录。</p>
|
||||
</AdminResultCard>
|
||||
|
||||
<AdminResultCard label="会话校验" :value="cloudtentaclesValidateResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesValidateResult">
|
||||
权限 {{ cloudtentaclesValidateResult.session.permissionCount }} 项,余额 {{ cloudtentaclesValidateResult.asset }}
|
||||
</p>
|
||||
<p v-else>登录成功后可继续校验 token 会话。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>cloudtentacles 开发调试工具</h3>
|
||||
<p>仅开发模式展示。用于调试分类、SKU、购买、发货、虚拟号、兑换链接和结束退还链路。</p>
|
||||
<div class="result-grid">
|
||||
<AdminResultCard
|
||||
label="当前 token"
|
||||
:value="cloudtentaclesValidateResult?.session.tokenMasked || cloudtentaclesLoginResult?.session.tokenMasked || '-'"
|
||||
>
|
||||
<p>原始 token 已写入上方输入框,可继续用于校验或后续调试。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="权限"
|
||||
:value="`${cloudtentaclesValidateResult?.session.permissionCount || cloudtentaclesLoginResult?.session.permissionCount || 0} 项`"
|
||||
>
|
||||
<p>{{ (cloudtentaclesValidateResult?.session.permissions || cloudtentaclesLoginResult?.session.permissions || []).join('、') || '暂无' }}</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard label="余额" :value="cloudtentaclesValidateResult?.asset || cloudtentaclesLoginResult?.asset || 0">
|
||||
<p>可用于确认当前履约账号状态。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round @click="handleCloudtentaclesFetchAsset">余额</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round @click="handleCloudtentaclesFetchCategories">分类</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round @click="handleCloudtentaclesFetchSkuList">SKU</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round @click="handleCloudtentaclesBuySku">购买</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round type="primary" @click="handleCloudtentaclesUseSku">发货</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round @click="handleCloudtentaclesFetchKnapsack">背包</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round @click="handleCloudtentaclesFetchVnList">虚拟号列表</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round @click="handleCloudtentaclesAppointVn">申请虚拟号</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round @click="handleCloudtentaclesGenerateVnLoginCode">生成登录码</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round @click="handleCloudtentaclesFetchVnCode">获取验证码</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round @click="handleCloudtentaclesVerifyVnCode">校验验证码</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round @click="handleCloudtentaclesFetchBindUrl">兑换链接</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round type="warning" @click="handleCloudtentaclesBackVn">退还号码</el-button>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" round type="success" @click="handleCloudtentaclesRunFullFlow">完整流程</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre v-if="cloudtentaclesDebugResult" class="debug-result">{{ cloudtentaclesDebugResult }}</pre>
|
||||
<div v-else class="empty-inline">执行一次调试动作后,这里会显示原始返回数据。</div>
|
||||
</section>
|
||||
|
||||
<section v-if="cloudtentaclesLoginResult || cloudtentaclesValidateResult" class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>cloudtentacles 调试结果</h3>
|
||||
<p>用于确认当前账号会话、权限和用户信息,后续发货执行器会复用这里的登录能力。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-grid">
|
||||
<AdminResultCard
|
||||
label="当前 token"
|
||||
:value="cloudtentaclesValidateResult?.session.tokenMasked || cloudtentaclesLoginResult?.session.tokenMasked || '-'"
|
||||
>
|
||||
<p>原始 token 已写入上方输入框,可继续用于校验或后续调试。</p>
|
||||
</AdminResultCard>
|
||||
|
||||
<AdminResultCard
|
||||
label="权限"
|
||||
:value="`${cloudtentaclesValidateResult?.session.permissionCount || cloudtentaclesLoginResult?.session.permissionCount || 0} 项`"
|
||||
>
|
||||
<p>{{ (cloudtentaclesValidateResult?.session.permissions || cloudtentaclesLoginResult?.session.permissions || []).join('、') || '暂无' }}</p>
|
||||
</AdminResultCard>
|
||||
|
||||
<AdminResultCard label="余额" :value="cloudtentaclesValidateResult?.asset || cloudtentaclesLoginResult?.asset || 0">
|
||||
<p>可用于确认当前履约账号状态。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</section>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.debug-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.debug-pre {
|
||||
margin: 0;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import AdminResultCard from '@/components/admin/AdminResultCard.vue'
|
||||
import type { EditableKuaishouEticketShop } from '@/composables/admin/platform-shops/types'
|
||||
import type {
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
} from '@/types/admin'
|
||||
import AdminResultCard from '@/components/admin/AdminResultCard.vue'
|
||||
|
||||
type KuaishouEticketForm = {
|
||||
baseUrl: string
|
||||
@@ -59,276 +59,457 @@ defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="meta-card">
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">配置文件</span>
|
||||
<code>{{ kuaishouEticketFilePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">职责</span>
|
||||
<span>这是独立的新流程,只负责快手小店券核销调试与配置,不接入当前已完成的订单来源和履约主链路。</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">建议流程</span>
|
||||
<span>先为每个店铺录入 `kshopName + Cookie`,可用 info 接口自动识别店铺名与店铺 ID;保存后选择目标店铺,再查询券码并执行核销。</span>
|
||||
</div>
|
||||
</section>
|
||||
<div class="platform-section">
|
||||
<!-- 说明信息 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">说明信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="配置文件">{{ kuaishouEticketFilePath || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职责">
|
||||
这是独立的新流程,只负责快手小店券核销调试与配置,不接入当前已完成的订单来源和履约主链路。
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="建议流程">
|
||||
先为每个店铺录入 kshopName + Cookie,可用 info 接口自动识别店铺名与店铺 ID;保存后选择目标店铺,再查询券码并执行核销。
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>快手小店店铺配置</h3>
|
||||
<p>支持多店铺配置,建议先粘贴 Cookie,再用 info 接口自动识别 `kshopName / shopId`,最后统一保存到文件。</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :disabled="kuaishouEticketShops.length === 0" text @click="expandAllKuaishouEticketShops">全部展开</el-button>
|
||||
<el-button :disabled="kuaishouEticketShops.length === 0" text @click="collapseAllKuaishouEticketShops">全部折叠</el-button>
|
||||
<el-button round @click="addKuaishouEticketShop">新增店铺</el-button>
|
||||
<el-button :loading="kuaishouEticketSaving" round type="primary" @click="handleKuaishouEticketSaveSource">保存配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuaishou-workspace">
|
||||
<div class="kuaishou-shop-stack">
|
||||
<div class="form-grid kuaishou-base-grid">
|
||||
<label class="field-block field-wide">
|
||||
<span>Base URL</span>
|
||||
<input v-model="kuaishouEticketForm.baseUrl" class="text-input" placeholder="https://s.kwaixiaodian.com" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="kuaishouEticketShops.length === 0" class="empty-inline">当前还没有快手小店店铺配置,先新增一条。</div>
|
||||
|
||||
<article
|
||||
v-for="shop in kuaishouEticketShops"
|
||||
:key="shop.id"
|
||||
:class="['shop-card', 'kuaishou-shop-card', {
|
||||
'is-active': kuaishouEticketForm.selectedShopId === shop.id,
|
||||
'is-expanded': isKuaishouEticketShopExpanded(shop.id),
|
||||
}]"
|
||||
>
|
||||
<button type="button" class="shop-summary kuaishou-shop-summary" @click="toggleKuaishouEticketShop(shop.id)">
|
||||
<div class="kuaishou-shop-head">
|
||||
<div class="shop-avatar">
|
||||
<img v-if="shop.userAvatar" :src="shop.userAvatar" :alt="shop.kshopName || shop.shopId || '店铺头像'" />
|
||||
<span v-else>{{ getKuaishouEticketShopInitial(shop) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="shop-summary-main">
|
||||
<div class="shop-title-row">
|
||||
<strong>{{ shop.kshopName || shop.shopId || '未命名店铺' }}</strong>
|
||||
<span class="shop-status" :class="{ 'is-disabled': !shop.enabled }">
|
||||
{{ shop.enabled ? '已启用' : '已停用' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="cell-subtle">店铺 ID:{{ shop.shopId || '等待 info 自动识别' }}</span>
|
||||
<div class="shop-tag-row">
|
||||
<span class="shop-mini-tag" :class="{ 'is-ready': Boolean(shop.cookie.trim()) }">
|
||||
{{ shop.cookie.trim() ? 'Cookie 已粘贴' : '待补 Cookie' }}
|
||||
</span>
|
||||
<span class="shop-mini-tag" :class="{ 'is-ready': Boolean(shop.userAvatar || shop.shopId.trim()) }">
|
||||
{{ shop.userAvatar || shop.shopId.trim() ? '店铺信息已识别' : '未读取店铺信息' }}
|
||||
</span>
|
||||
<span v-if="kuaishouEticketForm.selectedShopId === shop.id" class="shop-mini-tag is-highlight">当前调试店铺</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shop-summary-side">
|
||||
<span class="shop-summary-copy">{{ describeKuaishouEticketShop(shop) }}</span>
|
||||
<span class="shop-toggle">{{ isKuaishouEticketShopExpanded(shop.id) ? '收起详情' : '展开配置' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="isKuaishouEticketShopExpanded(shop.id)" class="shop-body kuaishou-shop-body">
|
||||
<div class="form-grid">
|
||||
<label class="field-block">
|
||||
<span>kshopName</span>
|
||||
<input v-model="shop.kshopName" class="text-input" placeholder="例如 稚嫩游戏交易店" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>店铺 ID</span>
|
||||
<input v-model="shop.shopId" class="text-input" readonly placeholder="通过 info 接口自动识别" />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>Cookie</span>
|
||||
<textarea
|
||||
v-model="shop.cookie"
|
||||
class="text-area kuaishou-cookie-area"
|
||||
placeholder="直接粘贴该店铺请求头中的完整 Cookie"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="shop-actions kuaishou-shop-actions">
|
||||
<label class="checkbox-line">
|
||||
<input v-model="shop.enabled" type="checkbox" />
|
||||
<span>启用该店铺</span>
|
||||
</label>
|
||||
<span class="cell-subtle">{{ shop.userAvatar ? '已识别头像,可直接作为调试店铺' : '建议先读取店铺信息确认 Cookie 归属' }}</span>
|
||||
<el-button
|
||||
:loading="kuaishouEticketResolvingShopId === shop.id"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleKuaishouEticketResolveShopInfo(shop)"
|
||||
>
|
||||
读取店铺信息
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="kuaishouEticketForm.selectedShopId === shop.id ? 'success' : 'primary'"
|
||||
@click="setKuaishouEticketDebugShop(shop.id)"
|
||||
>
|
||||
{{ kuaishouEticketForm.selectedShopId === shop.id ? '当前调试店铺' : '设为调试店铺' }}
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="removeKuaishouEticketShop(shop.id)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<aside class="kuaishou-side-panel">
|
||||
<AdminResultCard label="店铺总数" :value="kuaishouEticketStats.configuredShopCount" class="kuaishou-side-card">
|
||||
<p>其中 Cookie 就绪 {{ kuaishouEticketStats.cookieReadyCount }} 家。</p>
|
||||
</AdminResultCard>
|
||||
|
||||
<AdminResultCard
|
||||
label="当前调试店铺"
|
||||
:value="activeKuaishouEticketShop?.kshopName || activeKuaishouEticketShop?.shopId || '未选择'"
|
||||
class="kuaishou-side-card"
|
||||
>
|
||||
<p>{{ kuaishouEticketStats.selectedShopReady ? '已具备调试条件,可直接查询核销信息。' : '请选择店铺并补全 Cookie。' }}</p>
|
||||
</AdminResultCard>
|
||||
|
||||
<AdminResultCard
|
||||
label="最近识别结果"
|
||||
:value="kuaishouEticketShopInfoResult?.shop.kshopName || kuaishouEticketShopInfoResult?.shop.shopId || '未执行'"
|
||||
class="kuaishou-side-card"
|
||||
>
|
||||
<p v-if="kuaishouEticketShopInfoResult">结算状态 {{ kuaishouEticketShopInfoResult.shop.settleStatus }}</p>
|
||||
<p v-else>可用“读取店铺信息”校验 Cookie 是否属于预期店铺。</p>
|
||||
</AdminResultCard>
|
||||
|
||||
<AdminResultCard label="建议顺序" value="先配置,再调试" class="kuaishou-side-card kuaishou-guide-card">
|
||||
<p>1. 新增店铺并粘贴 Cookie</p>
|
||||
<p>2. 读取店铺信息确认 kshopName / shopId</p>
|
||||
<p>3. 设为调试店铺后查询券码并执行核销</p>
|
||||
</AdminResultCard>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<p v-if="kuaishouEticketResultError" class="error-copy">{{ kuaishouEticketResultError }}</p>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>快手小店核销调试</h3>
|
||||
<p>这里使用“当前调试店铺”的 Cookie 执行查询与核销;`eTicketId / oid / formToken` 都来自查询结果。</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :loading="kuaishouEticketDetailing" round @click="handleKuaishouEticketQueryDetail">查询核销信息</el-button>
|
||||
<el-button :loading="kuaishouEticketConsuming" round type="primary" @click="handleKuaishouEticketConsume">执行核销</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuaishou-debug-layout">
|
||||
<div class="kuaishou-debug-panel">
|
||||
<div class="form-grid">
|
||||
<label class="field-block">
|
||||
<span>调试店铺</span>
|
||||
<select v-model="kuaishouEticketForm.selectedShopId" class="text-input">
|
||||
<option value="">请选择店铺</option>
|
||||
<option v-for="shop in kuaishouEticketShops" :key="shop.id" :value="shop.id">
|
||||
{{ shop.kshopName || shop.shopId || '未命名店铺' }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>查询券码</span>
|
||||
<input v-model="kuaishouEticketForm.queryTicketCode" class="text-input" placeholder="用于查询核销信息" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>核销数量</span>
|
||||
<input v-model.number="kuaishouEticketForm.num" class="text-input" type="number" min="1" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>storeId</span>
|
||||
<input v-model="kuaishouEticketForm.storeId" class="text-input" placeholder="默认 0" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuaishou-debug-panel kuaishou-debug-panel-soft">
|
||||
<div class="kuaishou-debug-head">
|
||||
<!-- 店铺配置 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="result-label">自动回填参数</span>
|
||||
<strong>查询后生成</strong>
|
||||
<span class="card-title">快手小店店铺配置</span>
|
||||
<p class="card-desc">支持多店铺配置,建议先粘贴 Cookie,再用 info 接口自动识别 kshopName / shopId,最后统一保存到文件。</p>
|
||||
</div>
|
||||
<span class="cell-subtle">这些字段不需要手动填写</span>
|
||||
<div class="card-actions">
|
||||
<el-button :disabled="kuaishouEticketShops.length === 0" @click="expandAllKuaishouEticketShops">全部展开</el-button>
|
||||
<el-button :disabled="kuaishouEticketShops.length === 0" @click="collapseAllKuaishouEticketShops">全部折叠</el-button>
|
||||
<el-button @click="addKuaishouEticketShop">新增店铺</el-button>
|
||||
<el-button :loading="kuaishouEticketSaving" type="primary" @click="handleKuaishouEticketSaveSource">保存配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item label="Base URL">
|
||||
<el-input v-model="kuaishouEticketForm.baseUrl" placeholder="https://s.kwaixiaodian.com" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-empty v-if="kuaishouEticketShops.length === 0" description="当前还没有快手小店店铺配置,先新增一条。" />
|
||||
|
||||
<div v-else class="shop-panels">
|
||||
<div class="shop-list">
|
||||
<el-collapse
|
||||
:model-value="kuaishouEticketShops.filter(s => isKuaishouEticketShopExpanded(s.id)).map(s => s.id)"
|
||||
>
|
||||
<el-collapse-item
|
||||
v-for="shop in kuaishouEticketShops"
|
||||
:key="shop.id"
|
||||
:name="shop.id"
|
||||
:class="['shop-panel', {
|
||||
'is-debug': kuaishouEticketForm.selectedShopId === shop.id,
|
||||
}]"
|
||||
>
|
||||
<template #title>
|
||||
<div class="shop-title-bar">
|
||||
<div class="shop-avatar">
|
||||
<img v-if="shop.userAvatar" :src="shop.userAvatar" :alt="shop.kshopName || shop.shopId || '店铺头像'" />
|
||||
<span v-else>{{ getKuaishouEticketShopInitial(shop) }}</span>
|
||||
</div>
|
||||
<div class="shop-info">
|
||||
<div class="shop-name-row">
|
||||
<strong>{{ shop.kshopName || shop.shopId || '未命名店铺' }}</strong>
|
||||
<el-tag :type="shop.enabled ? 'success' : 'info'" size="small">{{ shop.enabled ? '已启用' : '已停用' }}</el-tag>
|
||||
<el-tag v-if="kuaishouEticketForm.selectedShopId === shop.id" type="primary" size="small">当前调试</el-tag>
|
||||
</div>
|
||||
<div class="shop-meta">
|
||||
<span>店铺 ID:{{ shop.shopId || '等待 info 自动识别' }}</span>
|
||||
<el-tag :type="Boolean(shop.cookie.trim()) ? 'success' : 'warning'" size="small">
|
||||
{{ shop.cookie.trim() ? 'Cookie 已粘贴' : '待补 Cookie' }}
|
||||
</el-tag>
|
||||
<el-tag :type="Boolean(shop.userAvatar || shop.shopId.trim()) ? 'success' : 'info'" size="small">
|
||||
{{ shop.userAvatar || shop.shopId.trim() ? '信息已识别' : '未读取信息' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="kshopName">
|
||||
<el-input v-model="shop.kshopName" placeholder="例如 稚嫩游戏交易店" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="店铺 ID">
|
||||
<el-input v-model="shop.shopId" readonly placeholder="通过 info 接口自动识别" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="Cookie">
|
||||
<el-input
|
||||
v-model="shop.cookie"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="直接粘贴该店铺请求头中的完整 Cookie"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="shop-footer">
|
||||
<div class="footer-left">
|
||||
<el-switch v-model="shop.enabled" active-text="启用该店铺" />
|
||||
<span v-if="shop.userAvatar" class="text-muted">已识别头像,可直接作为调试店铺</span>
|
||||
<span v-else class="text-muted">建议先读取店铺信息确认 Cookie 归属</span>
|
||||
</div>
|
||||
<div class="footer-actions">
|
||||
<el-button
|
||||
:loading="kuaishouEticketResolvingShopId === shop.id"
|
||||
size="small"
|
||||
@click="handleKuaishouEticketResolveShopInfo(shop)"
|
||||
>
|
||||
读取店铺信息
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="kuaishouEticketForm.selectedShopId === shop.id ? 'success' : 'primary'"
|
||||
@click="setKuaishouEticketDebugShop(shop.id)"
|
||||
>
|
||||
{{ kuaishouEticketForm.selectedShopId === shop.id ? '当前调试店铺' : '设为调试店铺' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="removeKuaishouEticketShop(shop.id)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="field-block">
|
||||
<span>eTicketId</span>
|
||||
<input :value="kuaishouEticketForm.eTicketId" class="text-input" readonly placeholder="查询后自动回填" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>oid</span>
|
||||
<input :value="kuaishouEticketForm.oid" class="text-input" readonly placeholder="查询后自动回填" />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>formToken</span>
|
||||
<input :value="kuaishouEticketForm.formToken" class="text-input" readonly placeholder="查询后自动回填" />
|
||||
</label>
|
||||
<div class="side-panel">
|
||||
<AdminResultCard label="店铺总数" :value="kuaishouEticketStats.configuredShopCount">
|
||||
<p>其中 Cookie 就绪 {{ kuaishouEticketStats.cookieReadyCount }} 家。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="当前调试店铺"
|
||||
:value="activeKuaishouEticketShop?.kshopName || activeKuaishouEticketShop?.shopId || '未选择'"
|
||||
>
|
||||
<p>{{ kuaishouEticketStats.selectedShopReady ? '已具备调试条件,可直接查询核销信息。' : '请选择店铺并补全 Cookie。' }}</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="最近识别结果"
|
||||
:value="kuaishouEticketShopInfoResult?.shop.kshopName || kuaishouEticketShopInfoResult?.shop.shopId || '未执行'"
|
||||
>
|
||||
<p v-if="kuaishouEticketShopInfoResult">结算状态 {{ kuaishouEticketShopInfoResult.shop.settleStatus }}</p>
|
||||
<p v-else>可用"读取店铺信息"校验 Cookie 是否属于预期店铺。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard label="建议顺序" value="先配置,再调试">
|
||||
<p>1. 新增店铺并粘贴 Cookie</p>
|
||||
<p>2. 读取店铺信息确认 kshopName / shopId</p>
|
||||
<p>3. 设为调试店铺后查询券码并执行核销</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-grid">
|
||||
<AdminResultCard label="详情查询" :value="kuaishouEticketDetailResult ? (kuaishouEticketDetailResult.ok ? '成功' : '已返回') : '未执行'">
|
||||
<p v-if="kuaishouEticketDetailResult?.detail">
|
||||
{{ kuaishouEticketDetailResult.goods?.itemTitle || kuaishouEticketDetailResult.goods?.skuDesc || '-' }},
|
||||
剩余 {{ kuaishouEticketDetailResult.detail.leftCount }} / {{ kuaishouEticketDetailResult.detail.totalCount }}
|
||||
</p>
|
||||
<p v-else>先通过查询券码获取核销详情并自动带出核销参数。</p>
|
||||
</AdminResultCard>
|
||||
<el-alert
|
||||
v-if="kuaishouEticketResultError"
|
||||
:title="kuaishouEticketResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-top: 16px"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<AdminResultCard label="核销执行" :value="kuaishouEticketConsumeResult ? (kuaishouEticketConsumeResult.consumed ? '已处理' : '已返回') : '未执行'">
|
||||
<p v-if="kuaishouEticketConsumeResult">
|
||||
{{ kuaishouEticketConsumeResult.errorMessage || (kuaishouEticketConsumeResult.ok ? '核销成功' : '接口已返回结果') }}
|
||||
</p>
|
||||
<p v-else>查询详情后再执行核销会更稳妥。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 核销调试 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">快手小店核销调试</span>
|
||||
<p class="card-desc">这里使用"当前调试店铺"的 Cookie 执行查询与核销;eTicketId / oid / formToken 都来自查询结果。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button :loading="kuaishouEticketDetailing" @click="handleKuaishouEticketQueryDetail">查询核销信息</el-button>
|
||||
<el-button :loading="kuaishouEticketConsuming" type="primary" @click="handleKuaishouEticketConsume">执行核销</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<section v-if="kuaishouEticketDetailResult" class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>核销详情返回</h3>
|
||||
<p>成功查询后会自动回填 `eTicketId`、`oid`、`formToken` 和建议核销数量。</p>
|
||||
<div class="debug-layout">
|
||||
<div class="debug-main">
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item label="调试店铺">
|
||||
<el-select v-model="kuaishouEticketForm.selectedShopId" placeholder="请选择店铺" style="width: 100%">
|
||||
<el-option v-for="shop in kuaishouEticketShops" :key="shop.id" :label="shop.kshopName || shop.shopId || '未命名店铺'" :value="shop.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="查询券码">
|
||||
<el-input v-model="kuaishouEticketForm.queryTicketCode" placeholder="用于查询核销信息" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="核销数量">
|
||||
<el-input-number v-model="kuaishouEticketForm.num" :min="1" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="storeId">
|
||||
<el-input v-model="kuaishouEticketForm.storeId" placeholder="默认 0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" class="auto-fill-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">自动回填参数</span>
|
||||
<span class="text-muted" style="margin-left: 8px">查询后生成</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="eTicketId">
|
||||
<el-input :model-value="kuaishouEticketForm.eTicketId" readonly placeholder="查询后自动回填" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="oid">
|
||||
<el-input :model-value="kuaishouEticketForm.oid" readonly placeholder="查询后自动回填" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="formToken">
|
||||
<el-input :model-value="kuaishouEticketForm.formToken" readonly placeholder="查询后自动回填" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre class="debug-result">{{ JSON.stringify(kuaishouEticketDetailResult.raw, null, 2) }}</pre>
|
||||
</section>
|
||||
|
||||
<section v-if="kuaishouEticketConsumeResult" class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>核销执行返回</h3>
|
||||
<p>这里保留接口原始响应,方便核对 `result`、`error_msg` 和 `requestId`。</p>
|
||||
<div class="result-grid" style="margin-top: 16px">
|
||||
<AdminResultCard label="详情查询" :value="kuaishouEticketDetailResult ? (kuaishouEticketDetailResult.ok ? '成功' : '已返回') : '未执行'">
|
||||
<p v-if="kuaishouEticketDetailResult?.detail">
|
||||
{{ kuaishouEticketDetailResult.goods?.itemTitle || kuaishouEticketDetailResult.goods?.skuDesc || '-' }},
|
||||
剩余 {{ kuaishouEticketDetailResult.detail.leftCount }} / {{ kuaishouEticketDetailResult.detail.totalCount }}
|
||||
</p>
|
||||
<p v-else>先通过查询券码获取核销详情并自动带出核销参数。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard label="核销执行" :value="kuaishouEticketConsumeResult ? (kuaishouEticketConsumeResult.consumed ? '已处理' : '已返回') : '未执行'">
|
||||
<p v-if="kuaishouEticketConsumeResult">
|
||||
{{ kuaishouEticketConsumeResult.errorMessage || (kuaishouEticketConsumeResult.ok ? '核销成功' : '接口已返回结果') }}
|
||||
</p>
|
||||
<p v-else>查询详情后再执行核销会更稳妥。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<pre class="debug-result">{{ JSON.stringify(kuaishouEticketConsumeResult.raw, null, 2) }}</pre>
|
||||
</section>
|
||||
<!-- 详情结果 -->
|
||||
<el-card v-if="kuaishouEticketDetailResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">核销详情返回</span>
|
||||
<p class="card-desc">成功查询后会自动回填 eTicketId、oid、formToken 和建议核销数量。</p>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="debug-pre">{{ JSON.stringify(kuaishouEticketDetailResult.raw, null, 2) }}</pre>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="kuaishouEticketConsumeResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">核销执行返回</span>
|
||||
<p class="card-desc">这里保留接口原始响应,方便核对 result、error_msg 和 requestId。</p>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="debug-pre">{{ JSON.stringify(kuaishouEticketConsumeResult.raw, null, 2) }}</pre>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.shop-panels {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(260px, 0.9fr);
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.shop-list {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.shop-panel.is-debug {
|
||||
border: 1px solid var(--border-hover);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.shop-title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: var(--space-4);
|
||||
}
|
||||
|
||||
.shop-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #dbeafe, #eff6ff);
|
||||
color: var(--color-primary-dark);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--border-hover);
|
||||
}
|
||||
|
||||
.shop-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.shop-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shop-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.shop-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.shop-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-3);
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.debug-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(260px, 0.9fr);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.auto-fill-card {
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.debug-pre {
|
||||
margin: 0;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.shop-panels,
|
||||
.debug-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,95 +25,155 @@ defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="meta-card">
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">职责</span>
|
||||
<span>接收 91卡券推送订单,未命中履约配置时先进入待补全队列,补完商品规则后可手动重试生成任务。</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">处理建议</span>
|
||||
<span>先到“快手 Cloud 新履约”页为 91 的 productNo 配置规则,再回到这里点击重试。</span>
|
||||
</div>
|
||||
</section>
|
||||
<div class="platform-section">
|
||||
<!-- 说明信息 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">说明信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="职责">
|
||||
接收 91卡券推送订单,未命中履约配置时先进入待补全队列,补完商品规则后可手动重试生成任务。
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处理建议">
|
||||
先到"快手 Cloud 新履约"页为 91 的 productNo 配置规则,再回到这里点击重试。
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>91卡券订单队列</h3>
|
||||
<p>当前筛选 {{ ninetyoneStats.currentCount }} 条,总计 {{ ninetyoneStats.total }} 条,已生成任务 {{ ninetyoneStats.taskReadyCount }} 条。</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button-group>
|
||||
<el-button :type="ninetyoneStatus === 'pending_config' ? 'primary' : 'default'" round @click="handleNinetyoneStatusChange('pending_config')">待补全</el-button>
|
||||
<el-button :type="ninetyoneStatus === 'manual_failed' ? 'primary' : 'default'" round @click="handleNinetyoneStatusChange('manual_failed')">失败</el-button>
|
||||
<el-button :type="ninetyoneStatus === 'all' ? 'primary' : 'default'" round @click="handleNinetyoneStatusChange('all')">全部</el-button>
|
||||
</el-button-group>
|
||||
<el-button :loading="ninetyoneLoading" round @click="loadNinetyoneOrders(ninetyoneOrders.page)">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 订单队列 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">91卡券订单队列</span>
|
||||
<p class="card-desc">
|
||||
当前筛选 {{ ninetyoneStats.currentCount }} 条,总计 {{ ninetyoneStats.total }} 条,已生成任务 {{ ninetyoneStats.taskReadyCount }} 条。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-radio-group
|
||||
:model-value="ninetyoneStatus"
|
||||
size="small"
|
||||
@update:model-value="(v: string | number | boolean | undefined) => handleNinetyoneStatusChange(v as 'pending_config' | 'all' | 'manual_failed')"
|
||||
>
|
||||
<el-radio-button value="pending_config">待补全</el-radio-button>
|
||||
<el-radio-button value="manual_failed">失败</el-radio-button>
|
||||
<el-radio-button value="all">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button :loading="ninetyoneLoading" @click="loadNinetyoneOrders(ninetyoneOrders.page)">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p v-if="ninetyoneResultError" class="error-copy">{{ ninetyoneResultError }}</p>
|
||||
<div v-if="ninetyoneLoading" class="empty-inline">正在读取 91卡券订单…</div>
|
||||
<el-alert
|
||||
v-if="ninetyoneResultError"
|
||||
:title="ninetyoneResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<table v-else class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单</th>
|
||||
<th>商品</th>
|
||||
<th>状态</th>
|
||||
<th>时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in ninetyoneOrders.items" :key="item.orderId">
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.orderNo }}</strong>
|
||||
<span class="cell-subtle">{{ item.outTradeNo || '-' }}</span>
|
||||
<el-table
|
||||
:data="ninetyoneOrders.items"
|
||||
stripe
|
||||
size="small"
|
||||
v-loading="ninetyoneLoading"
|
||||
element-loading-text="正在读取 91卡券订单..."
|
||||
>
|
||||
<el-table-column label="订单" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<div style="font-weight: 600">{{ row.orderNo }}</div>
|
||||
<div style="font-size: var(--text-xs); color: var(--text-muted)">{{ row.outTradeNo || '-' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.productName || item.productNo || '-' }}</strong>
|
||||
<span class="cell-subtle">productNo: {{ item.productNo || '-' }}</span>
|
||||
<span class="cell-subtle">数量:{{ item.buyNum || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<div style="font-weight: 600">{{ row.productName || row.productNo || '-' }}</div>
|
||||
<div style="font-size: var(--text-xs); color: var(--text-muted)">productNo: {{ row.productNo || '-' }}</div>
|
||||
<div style="font-size: var(--text-xs); color: var(--text-muted)">数量:{{ row.buyNum || 0 }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.orderStatus }}</strong>
|
||||
<span class="cell-subtle">任务 {{ item.taskCount }}</span>
|
||||
<span v-if="item.failReason" class="cell-subtle">{{ item.failReason }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<el-tag size="small">{{ row.orderStatus }}</el-tag>
|
||||
<div style="font-size: var(--text-xs); color: var(--text-muted); margin-top: 4px">任务 {{ row.taskCount }}</div>
|
||||
<div v-if="row.failReason" style="font-size: var(--text-xs); color: var(--color-danger)">{{ row.failReason }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ formatAdminDateTime(item.updatedAt || item.createdAt) }}</td>
|
||||
<td>
|
||||
<div class="row-actions">
|
||||
<el-button
|
||||
:loading="ninetyoneActionLoadingId === item.orderId"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleNinetyoneRetryOrder(item)"
|
||||
>
|
||||
重试生成任务
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.orderStatus !== 'manual_failed'"
|
||||
:loading="ninetyoneActionLoadingId === item.orderId"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleNinetyoneFailOrder(item)"
|
||||
>
|
||||
标记失败
|
||||
</el-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="ninetyoneOrders.items.length === 0">
|
||||
<td colspan="5" class="empty-inline">当前没有 91卡券订单。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间" width="180">
|
||||
<template #default="{ row }">{{ formatAdminDateTime(row.updatedAt || row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
:loading="ninetyoneActionLoadingId === row.orderId"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleNinetyoneRetryOrder(row)"
|
||||
>
|
||||
重试生成任务
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.orderStatus !== 'manual_failed'"
|
||||
:loading="ninetyoneActionLoadingId === row.orderId"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleNinetyoneFailOrder(row)"
|
||||
>
|
||||
标记失败
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="当前没有 91卡券订单。" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,10 +53,7 @@ const timeUnitOptions = [
|
||||
]
|
||||
|
||||
function formatJobType(type: string) {
|
||||
if (type === 'cloudtentacles_health') {
|
||||
return 'cloudtentacles 健康检查'
|
||||
}
|
||||
|
||||
if (type === 'cloudtentacles_health') return 'cloudtentacles 健康检查'
|
||||
return type || '-'
|
||||
}
|
||||
|
||||
@@ -65,11 +62,9 @@ function resolveTimeValue(seconds: number) {
|
||||
if (normalized >= 3600 && normalized % 3600 === 0) {
|
||||
return { amount: normalized / 3600, unit: 3600 }
|
||||
}
|
||||
|
||||
if (normalized >= 60 && normalized % 60 === 0) {
|
||||
return { amount: normalized / 60, unit: 60 }
|
||||
}
|
||||
|
||||
return { amount: normalized || 1, unit: 1 }
|
||||
}
|
||||
|
||||
@@ -89,7 +84,7 @@ function bindDuration(job: EditableScheduledJob, key: 'intervalSeconds' | 'coold
|
||||
;(job[unitKey] as number) = resolved.unit
|
||||
}
|
||||
|
||||
return computed({
|
||||
return computed<number>({
|
||||
get: () => Number(job[amountKey] || 1),
|
||||
set: (value: number) => {
|
||||
;(job[amountKey] as number) = Math.max(1, Number(value || 1))
|
||||
@@ -108,7 +103,7 @@ function bindDurationUnit(job: EditableScheduledJob, key: 'intervalSeconds' | 'c
|
||||
;(job[unitKey] as number) = resolved.unit
|
||||
}
|
||||
|
||||
return computed({
|
||||
return computed<number>({
|
||||
get: () => Number(job[unitKey] || 1),
|
||||
set: (value: number) => {
|
||||
;(job[unitKey] as number) = Math.max(1, Number(value || 1))
|
||||
@@ -119,235 +114,350 @@ function bindDurationUnit(job: EditableScheduledJob, key: 'intervalSeconds' | 'c
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="meta-card">
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">职责</span>
|
||||
<span>内部运营通知,仅发送给后台管理员、客服或值班人员。</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">配置文件</span>
|
||||
<span>{{ notificationFilePath || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">监控任务</span>
|
||||
<span>{{ scheduledJobsFilePath || '-' }}</span>
|
||||
</div>
|
||||
</section>
|
||||
<div class="platform-section">
|
||||
<!-- 配置信息 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">配置信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="职责">内部运营通知,仅发送给后台管理员、客服或值班人员。</el-descriptions-item>
|
||||
<el-descriptions-item label="配置文件">{{ notificationFilePath || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="监控任务">{{ scheduledJobsFilePath || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>Bark 通知</h3>
|
||||
<p>已配置 {{ notificationStats.configuredRecipientCount }} 人,启用 {{ notificationStats.enabledRecipientCount }} 人。</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button round @click="addNotificationRecipient">新增接收人</el-button>
|
||||
<el-button :loading="notificationSaving" round type="primary" @click="handleNotificationSaveConfig">保存配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bark 通知配置 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">Bark 通知</span>
|
||||
<p class="card-desc">已配置 {{ notificationStats.configuredRecipientCount }} 人,启用 {{ notificationStats.enabledRecipientCount }} 人。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button @click="addNotificationRecipient">新增接收人</el-button>
|
||||
<el-button :loading="notificationSaving" type="primary" @click="handleNotificationSaveConfig">保存配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p v-if="notificationResultError" class="error-copy">{{ notificationResultError }}</p>
|
||||
<el-alert
|
||||
v-if="notificationResultError"
|
||||
:title="notificationResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="checkbox-line">
|
||||
<input v-model="notificationForm.enabled" class="checkbox-box" type="checkbox" />
|
||||
<span>启用内部通知</span>
|
||||
</label>
|
||||
<label class="checkbox-line">
|
||||
<input v-model="notificationForm.barkEnabled" class="checkbox-box" type="checkbox" />
|
||||
<span>启用 Bark 通道</span>
|
||||
</label>
|
||||
<label class="field-block field-wide">
|
||||
<span>Bark 服务地址</span>
|
||||
<input v-model="notificationForm.barkServerUrl" class="text-input" placeholder="https://api.day.app" />
|
||||
</label>
|
||||
</div>
|
||||
<el-form label-width="auto" label-position="top" inline>
|
||||
<el-form-item><el-switch v-model="notificationForm.enabled" active-text="启用内部通知" /></el-form-item>
|
||||
<el-form-item><el-switch v-model="notificationForm.barkEnabled" active-text="启用 Bark 通道" /></el-form-item>
|
||||
<el-form-item label="Bark 服务地址" style="width: 100%">
|
||||
<el-input v-model="notificationForm.barkServerUrl" placeholder="https://api.day.app" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>接收人</th>
|
||||
<th>Device Key</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in notificationRecipients" :key="item.id">
|
||||
<td>
|
||||
<input v-model="item.name" class="text-input" placeholder="例如:客服A" />
|
||||
</td>
|
||||
<td>
|
||||
<input v-model="item.deviceKey" class="text-input" placeholder="Bark device key" />
|
||||
</td>
|
||||
<td>
|
||||
<label class="checkbox-line">
|
||||
<input v-model="item.enabled" class="checkbox-box" type="checkbox" />
|
||||
<span>{{ item.enabled ? '启用' : '停用' }}</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<el-button link type="danger" @click="removeNotificationRecipient(item.id)">删除</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="notificationRecipients.length === 0">
|
||||
<td colspan="4" class="empty-inline">还没有 Bark 接收人。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<el-table :data="notificationRecipients" stripe size="small" style="margin-top: 16px">
|
||||
<el-table-column label="接收人" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.name" placeholder="例如:客服A" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Device Key" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.deviceKey" placeholder="Bark device key" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.enabled" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="danger" text @click="removeNotificationRecipient(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="还没有 Bark 接收人。" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>监控任务</h3>
|
||||
<p>间隔表示多久检查一次;冷却表示同一个异常在这段时间内最多通知一次。</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :loading="scheduledJobsSaving" round type="primary" @click="handleScheduledJobsSaveConfig">保存任务</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 监控任务 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">监控任务</span>
|
||||
<p class="card-desc">间隔表示多久检查一次;冷却表示同一个异常在这段时间内最多通知一次。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button :loading="scheduledJobsSaving" type="primary" @click="handleScheduledJobsSaveConfig">保存任务</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="checkbox-line">
|
||||
<input v-model="scheduledJobsForm.enabled" class="checkbox-box" type="checkbox" />
|
||||
<span>启用监控任务系统</span>
|
||||
</label>
|
||||
</div>
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item>
|
||||
<el-switch v-model="scheduledJobsForm.enabled" active-text="启用监控任务系统" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>任务</th>
|
||||
<th>参数</th>
|
||||
<th>最近状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="job in scheduledJobs" :key="job.id">
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ formatJobType(job.type) }}</strong>
|
||||
<label class="checkbox-line">
|
||||
<input v-model="job.enabled" class="checkbox-box" type="checkbox" />
|
||||
<span>{{ job.enabled ? '启用' : '停用' }}</span>
|
||||
</label>
|
||||
<el-table :data="scheduledJobs" stripe size="small" style="margin-top: 8px">
|
||||
<el-table-column label="任务" min-width="200">
|
||||
<template #default="{ row: job }">
|
||||
<div>
|
||||
<div style="font-weight: 600">{{ formatJobType(job.type) }}</div>
|
||||
<el-switch v-model="job.enabled" size="small" active-text="启用" style="margin-top: 4px" />
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-grid">
|
||||
<label class="field-block">
|
||||
<span>检查间隔</span>
|
||||
<div class="time-input-row">
|
||||
<input v-model.number="bindDuration(job, 'intervalSeconds').value" class="text-input" type="number" min="1" />
|
||||
<select v-model.number="bindDurationUnit(job, 'intervalSeconds').value" class="text-input time-unit-select">
|
||||
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">{{ unit.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<span class="cell-subtle">每 {{ formatDuration(job.intervalSeconds) }} 检查一次</span>
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>通知冷却</span>
|
||||
<div class="time-input-row">
|
||||
<input v-model.number="bindDuration(job, 'cooldownSeconds').value" class="text-input" type="number" min="1" />
|
||||
<select v-model.number="bindDurationUnit(job, 'cooldownSeconds').value" class="text-input time-unit-select">
|
||||
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">{{ unit.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<span class="cell-subtle">同类异常 {{ formatDuration(job.cooldownSeconds) }} 内只通知一次</span>
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>余额阈值</span>
|
||||
<input v-model.number="job.assetThreshold" class="text-input" type="number" min="0" />
|
||||
<span class="cell-subtle">低于该值时通知</span>
|
||||
</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数" min-width="320">
|
||||
<template #default="{ row: job }">
|
||||
<div class="job-params">
|
||||
<div class="param-row">
|
||||
<span class="param-label">检查间隔</span>
|
||||
<input v-model.number="bindDuration(job, 'intervalSeconds').value" type="number" min="1" class="mini-input" />
|
||||
<select v-model.number="bindDurationUnit(job, 'intervalSeconds').value" class="mini-select">
|
||||
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">{{ unit.label }}</option>
|
||||
</select>
|
||||
<span class="param-hint">每 {{ formatDuration(job.intervalSeconds) }} 检查一次</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">通知冷却</span>
|
||||
<input v-model.number="bindDuration(job, 'cooldownSeconds').value" type="number" min="1" class="mini-input" />
|
||||
<select v-model.number="bindDurationUnit(job, 'cooldownSeconds').value" class="mini-select">
|
||||
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">{{ unit.label }}</option>
|
||||
</select>
|
||||
<span class="param-hint">同类异常 {{ formatDuration(job.cooldownSeconds) }} 内只通知一次</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">余额阈值</span>
|
||||
<input v-model.number="job.assetThreshold" type="number" min="0" class="mini-input" />
|
||||
<span class="param-hint">低于该值时通知</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ getScheduledJobRuntime(job.id)?.lastStatus || 'pending' }}</strong>
|
||||
<span class="cell-subtle">{{ getScheduledJobRuntime(job.id)?.lastMessage || '-' }}</span>
|
||||
<span class="cell-subtle">余额:{{ getScheduledJobRuntime(job.id)?.lastAsset ?? '-' }}</span>
|
||||
<span class="cell-subtle">上次:{{ formatAdminDateTime(getScheduledJobRuntime(job.id)?.lastRunAt || '') }}</span>
|
||||
<span class="cell-subtle">下次:{{ formatAdminDateTime(getScheduledJobRuntime(job.id)?.nextRunAt || '') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近状态" min-width="160">
|
||||
<template #default="{ row: job }">
|
||||
<div class="runtime-info">
|
||||
<el-tag
|
||||
:type="getScheduledJobRuntime(job.id)?.lastStatus === 'success' ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ getScheduledJobRuntime(job.id)?.lastStatus || 'pending' }}
|
||||
</el-tag>
|
||||
<span class="param-hint">{{ getScheduledJobRuntime(job.id)?.lastMessage || '-' }}</span>
|
||||
<span class="param-hint">余额:{{ getScheduledJobRuntime(job.id)?.lastAsset ?? '-' }}</span>
|
||||
<span class="param-hint">上次:{{ formatAdminDateTime(getScheduledJobRuntime(job.id)?.lastRunAt || '') }}</span>
|
||||
<span class="param-hint">下次:{{ formatAdminDateTime(getScheduledJobRuntime(job.id)?.nextRunAt || '') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" align="center">
|
||||
<template #default="{ row: job }">
|
||||
<el-button
|
||||
:loading="scheduledJobRunningId === job.id"
|
||||
round
|
||||
size="small"
|
||||
@click="handleScheduledJobRunNow(job.id)"
|
||||
>
|
||||
立即检查
|
||||
</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="scheduledJobs.length === 0">
|
||||
<td colspan="4" class="empty-inline">暂无可配置的监控任务。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无可配置的监控任务。" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>测试发送</h3>
|
||||
<p>测试会发送给所有已启用且填写了 Device Key 的接收人。</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :disabled="!notificationStats.barkReady" :loading="notificationTesting" round type="primary" @click="handleNotificationTest">发送测试</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 测试发送 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">测试发送</span>
|
||||
<p class="card-desc">测试会发送给所有已启用且填写了 Device Key 的接收人。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
:disabled="!notificationStats.barkReady"
|
||||
:loading="notificationTesting"
|
||||
type="primary"
|
||||
@click="handleNotificationTest"
|
||||
>
|
||||
发送测试
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="field-block">
|
||||
<span>标题</span>
|
||||
<input v-model="notificationForm.testTitle" class="text-input" />
|
||||
</label>
|
||||
<label class="field-block field-wide">
|
||||
<span>内容</span>
|
||||
<input v-model="notificationForm.testBody" class="text-input" />
|
||||
</label>
|
||||
<label class="field-block field-wide">
|
||||
<span>跳转链接</span>
|
||||
<input v-model="notificationForm.testUrl" class="text-input" placeholder="可选" />
|
||||
</label>
|
||||
</div>
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="notificationForm.testTitle" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="内容">
|
||||
<el-input v-model="notificationForm.testBody" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="跳转链接">
|
||||
<el-input v-model="notificationForm.testUrl" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<div v-if="notificationTestResult" class="result-grid">
|
||||
<div class="result-card">
|
||||
<span class="result-label">成功</span>
|
||||
<strong>{{ notificationTestResult.successCount }}</strong>
|
||||
<div v-if="notificationTestResult" class="result-grid">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">成功</div>
|
||||
<div class="stat-value">{{ notificationTestResult.successCount }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">失败</div>
|
||||
<div class="stat-value">{{ notificationTestResult.failedCount }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">通道</div>
|
||||
<div class="stat-value">{{ notificationTestResult.channel || '-' }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="result-card">
|
||||
<span class="result-label">失败</span>
|
||||
<strong>{{ notificationTestResult.failedCount }}</strong>
|
||||
</div>
|
||||
<div class="result-card">
|
||||
<span class="result-label">通道</span>
|
||||
<strong>{{ notificationTestResult.channel || '-' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table v-if="notificationTestResult" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>接收人</th>
|
||||
<th>结果</th>
|
||||
<th>状态码</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in notificationTestResult.results" :key="item.recipientId || item.recipientKeyMasked">
|
||||
<td>{{ item.recipientName || item.recipientKeyMasked || '-' }}</td>
|
||||
<td>{{ item.ok ? '成功' : '失败' }}</td>
|
||||
<td>{{ item.status || '-' }}</td>
|
||||
<td>{{ item.errorMessage || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<el-table v-if="notificationTestResult" :data="notificationTestResult.results" stripe size="small" style="margin-top: 16px">
|
||||
<el-table-column label="接收人">
|
||||
<template #default="{ row }">{{ row.recipientName || row.recipientKeyMasked || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.ok ? 'success' : 'danger'" size="small">{{ row.ok ? '成功' : '失败' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态码" width="80" align="center" />
|
||||
<el-table-column label="说明">
|
||||
<template #default="{ row }">{{ row.errorMessage || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.job-params {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.param-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.mini-input {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-surface);
|
||||
font-size: var(--text-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mini-select {
|
||||
width: 80px;
|
||||
height: 32px;
|
||||
padding: 0 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-surface);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.param-hint {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.runtime-info {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,3 +12,31 @@ defineProps<{
|
||||
<slot />
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.result-card {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-subtle);
|
||||
border: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.result-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.result-card strong {
|
||||
color: var(--text-primary);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.result-card :deep(p) {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,9 @@ import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/tokens.css'
|
||||
import './styles/main.css'
|
||||
import './styles/element-overrides.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
|
||||
@@ -1,571 +0,0 @@
|
||||
.admin-platform-shops {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .hero-card,
|
||||
.admin-platform-shops .meta-card,
|
||||
.admin-platform-shops .table-card,
|
||||
.admin-platform-shops .empty-block,
|
||||
.admin-platform-shops .error-copy {
|
||||
padding: 20px;
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(86, 108, 138, 0.1);
|
||||
}
|
||||
|
||||
.admin-platform-shops .hero-card,
|
||||
.admin-platform-shops .section-title-row,
|
||||
.admin-platform-shops .hero-actions,
|
||||
.admin-platform-shops .section-actions,
|
||||
.admin-platform-shops .platform-head,
|
||||
.admin-platform-shops .shop-summary,
|
||||
.admin-platform-shops .shop-actions,
|
||||
.admin-platform-shops .platform-switcher,
|
||||
.admin-platform-shops .overview-grid,
|
||||
.admin-platform-shops .result-grid {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .hero-card,
|
||||
.admin-platform-shops .section-title-row,
|
||||
.admin-platform-shops .shop-summary,
|
||||
.admin-platform-shops .shop-actions {
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.admin-platform-shops .hero-copy h1,
|
||||
.admin-platform-shops .section-title-row h3 {
|
||||
margin: 0;
|
||||
color: #1d3555;
|
||||
}
|
||||
|
||||
.admin-platform-shops .hero-copy p,
|
||||
.admin-platform-shops .section-title-row p {
|
||||
margin: 8px 0 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.admin-platform-shops .hero-actions,
|
||||
.admin-platform-shops .section-actions,
|
||||
.admin-platform-shops .platform-switcher {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-platform-shops .row-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-platform-shops .overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .platform-overview-card {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border: 1px solid rgba(86, 108, 138, 0.12);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-radius: 20px;
|
||||
padding: 18px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
transition: 0.2s ease;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.admin-platform-shops .platform-overview-card:hover,
|
||||
.admin-platform-shops .platform-overview-card.is-active {
|
||||
border-color: rgba(37, 99, 235, 0.28);
|
||||
box-shadow: 0 12px 32px rgba(37, 99, 235, 0.08);
|
||||
}
|
||||
|
||||
.admin-platform-shops .platform-badge,
|
||||
.admin-platform-shops .platform-tag,
|
||||
.admin-platform-shops .switch-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-platform-shops .platform-badge {
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
color: #2559a7;
|
||||
}
|
||||
|
||||
.admin-platform-shops .platform-tag {
|
||||
background: rgba(15, 23, 42, 0.05);
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
.admin-platform-shops .platform-overview-card strong {
|
||||
color: #1d3555;
|
||||
font-size: 26px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.admin-platform-shops .platform-overview-card > span,
|
||||
.admin-platform-shops .platform-metrics {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.admin-platform-shops .platform-metrics {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .switch-chip {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border: 1px solid rgba(86, 108, 138, 0.12);
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
color: #475467;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.admin-platform-shops .switch-chip.is-active {
|
||||
background: #2563eb;
|
||||
border-color: #2563eb;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.admin-platform-shops .meta-card,
|
||||
.admin-platform-shops .meta-line,
|
||||
.admin-platform-shops .cell-stack {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .meta-label {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-platform-shops .form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .field-block {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .field-block span {
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-platform-shops .field-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-workspace,
|
||||
.admin-platform-shops .kuaishou-debug-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.9fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-shop-stack,
|
||||
.admin-platform-shops .kuaishou-side-panel,
|
||||
.admin-platform-shops .kuaishou-debug-panel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-base-grid {
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
background:
|
||||
.admin-platform-shops radial-gradient(circle at top left, rgba(59, 130, 246, 0.08), transparent 38%),
|
||||
linear-gradient(180deg, rgba(239, 246, 255, 0.86), rgba(248, 250, 252, 0.96));
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.admin-platform-shops .text-input,
|
||||
.admin-platform-shops .text-area {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(86, 108, 138, 0.18);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.admin-platform-shops .text-area {
|
||||
min-height: 96px;
|
||||
padding: 12px 14px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-card {
|
||||
margin-top: 14px;
|
||||
border-radius: 18px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-card:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-summary {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 16px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-summary-main,
|
||||
.admin-platform-shops .shop-summary-side {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-summary-side {
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-summary-main strong,
|
||||
.admin-platform-shops .shop-toggle {
|
||||
color: #1d3555;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-summary-copy,
|
||||
.admin-platform-shops .cell-subtle {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-shop-card {
|
||||
margin-top: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.98));
|
||||
border-color: rgba(96, 165, 250, 0.18);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-shop-card.is-active {
|
||||
border-color: rgba(37, 99, 235, 0.34);
|
||||
box-shadow: 0 18px 44px rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-shop-card.is-expanded {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-shop-summary {
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-shop-head,
|
||||
.admin-platform-shops .shop-title-row,
|
||||
.admin-platform-shops .shop-tag-row,
|
||||
.admin-platform-shops .kuaishou-debug-head {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-shop-head {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-title-row,
|
||||
.admin-platform-shops .shop-tag-row,
|
||||
.admin-platform-shops .kuaishou-debug-head {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-title-row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-avatar {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #dbeafe, #eff6ff);
|
||||
color: #1d4ed8;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(37, 99, 235, 0.14);
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-mini-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.05);
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-mini-tag.is-ready {
|
||||
background: rgba(22, 163, 74, 0.12);
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-mini-tag.is-highlight {
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-shop-body {
|
||||
padding-top: 16px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.74), rgba(248, 250, 252, 0.96));
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-shop-actions {
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-cookie-area {
|
||||
min-height: 128px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-side-panel {
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-side-card {
|
||||
min-height: 132px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-guide-card {
|
||||
background:
|
||||
.admin-platform-shops radial-gradient(circle at top right, rgba(37, 99, 235, 0.08), transparent 35%),
|
||||
linear-gradient(180deg, rgba(239, 246, 255, 0.9), rgba(248, 250, 252, 0.98));
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-guide-card p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-debug-panel {
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
background: rgba(248, 250, 252, 0.76);
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-debug-panel-soft {
|
||||
background:
|
||||
.admin-platform-shops radial-gradient(circle at top left, rgba(59, 130, 246, 0.08), transparent 34%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.98));
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-debug-head {
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-debug-head strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #1d3555;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(22, 163, 74, 0.12);
|
||||
color: #166534;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-status.is-disabled {
|
||||
background: rgba(148, 163, 184, 0.14);
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-body {
|
||||
padding: 0 16px 16px;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.admin-platform-shops .checkbox-line {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
.admin-platform-shops .checkbox-box {
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(86, 108, 138, 0.18);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.admin-platform-shops .result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .result-card {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
|
||||
.admin-platform-shops .result-label {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-platform-shops .result-card strong {
|
||||
color: #1d3555;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .result-card p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.admin-platform-shops .compact-meta {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .data-table th,
|
||||
.admin-platform-shops .data-table td {
|
||||
padding: 12px 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(86, 108, 138, 0.08);
|
||||
color: #334155;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.admin-platform-shops .data-table th {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .time-input-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(96px, 1fr) 96px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-platform-shops .time-unit-select {
|
||||
min-width: 96px;
|
||||
}
|
||||
|
||||
.admin-platform-shops .empty-block,
|
||||
.admin-platform-shops .empty-inline {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.admin-platform-shops .error-copy {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.admin-platform-shops .debug-result {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.admin-platform-shops .overview-grid,
|
||||
.admin-platform-shops .result-grid,
|
||||
.admin-platform-shops .form-grid,
|
||||
.admin-platform-shops .kuaishou-workspace,
|
||||
.admin-platform-shops .kuaishou-debug-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.admin-platform-shops .hero-card,
|
||||
.admin-platform-shops .section-title-row,
|
||||
.admin-platform-shops .shop-actions,
|
||||
.admin-platform-shops .shop-summary {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.admin-platform-shops .kuaishou-shop-head,
|
||||
.admin-platform-shops .shop-title-row,
|
||||
.admin-platform-shops .shop-tag-row,
|
||||
.admin-platform-shops .kuaishou-debug-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.admin-platform-shops .shop-summary-side {
|
||||
justify-items: start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/* ===== Element Plus Global Overrides =====
|
||||
Single place for all Element Plus style normalization.
|
||||
Eliminates the need for scattered :deep() overrides in components.
|
||||
*/
|
||||
|
||||
/* ---- Buttons ---- */
|
||||
.el-button {
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
--el-button-bg-color: var(--color-primary);
|
||||
--el-button-border-color: var(--color-primary);
|
||||
--el-button-hover-bg-color: var(--color-primary-dark);
|
||||
--el-button-hover-border-color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
/* ---- Dialogs ---- */
|
||||
.el-dialog {
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
/* ---- Cards ---- */
|
||||
.el-card {
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-surface);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* ---- Tables ---- */
|
||||
.el-table {
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-table th.el-table__cell {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- Tags ---- */
|
||||
.el-tag {
|
||||
border-radius: var(--radius-full);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- Menus ---- */
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ---- Inputs & Selects ---- */
|
||||
.el-input .el-input__wrapper,
|
||||
.el-select .el-select__wrapper {
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.el-input .el-input__wrapper:hover,
|
||||
.el-select .el-select__wrapper:hover {
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.el-input.is-focus .el-input__wrapper,
|
||||
.el-select.is-focus .el-select__wrapper {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 1px var(--color-primary) inset;
|
||||
}
|
||||
|
||||
/* ---- Pagination ---- */
|
||||
.el-pagination {
|
||||
justify-content: center;
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
/* ---- Tabs ---- */
|
||||
.el-tabs__header {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
/* ---- Switch ---- */
|
||||
.el-switch.is-checked .el-switch__core {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ---- Skeleton ---- */
|
||||
.el-skeleton {
|
||||
--el-skeleton-color: var(--bg-subtle);
|
||||
}
|
||||
|
||||
/* ---- Empty ---- */
|
||||
.el-empty__description {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
@@ -1,19 +1,9 @@
|
||||
:root {
|
||||
--color-bg: #eef4ff;
|
||||
--color-bg-accent: #dfe9ff;
|
||||
--color-surface: rgba(255, 255, 255, 0.92);
|
||||
--color-panel-soft: rgba(15, 93, 216, 0.06);
|
||||
--color-text: #152238;
|
||||
--color-muted: #5f708a;
|
||||
--color-accent: #0f5dd8;
|
||||
--color-border: rgba(20, 47, 95, 0.08);
|
||||
--font-sans: 'PingFang SC', 'SF Pro Display', 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'SFMono-Regular', ui-monospace, monospace;
|
||||
color: var(--color-text);
|
||||
color: var(--text-primary);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(44, 133, 247, 0.22), transparent 32%),
|
||||
radial-gradient(circle at top right, rgba(114, 184, 255, 0.18), transparent 26%),
|
||||
linear-gradient(180deg, var(--color-bg) 0%, #f7fbff 100%);
|
||||
linear-gradient(180deg, #eef4ff 0%, #f7fbff 100%);
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/* ===== Design Tokens =====
|
||||
Single source of truth for all visual values.
|
||||
Every component/view must reference these variables instead of hardcoded values.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ---- Color Palette ---- */
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-light: #eff6ff;
|
||||
--color-primary-dark: #1d4ed8;
|
||||
|
||||
--color-success: #22c55e;
|
||||
--color-success-light: #f0fdf4;
|
||||
--color-success-dark: #15803d;
|
||||
|
||||
--color-warning: #f59e0b;
|
||||
--color-warning-light: #fffbeb;
|
||||
--color-warning-dark: #b45309;
|
||||
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-light: #fef2f2;
|
||||
--color-danger-dark: #b91c1c;
|
||||
|
||||
/* ---- Surfaces & Backgrounds ---- */
|
||||
--bg-page: #f3f7fc;
|
||||
--bg-surface: rgba(255, 255, 255, 0.94);
|
||||
--bg-sidebar: rgba(255, 255, 255, 0.92);
|
||||
--bg-subtle: rgba(15, 93, 216, 0.06);
|
||||
|
||||
/* ---- Text ---- */
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--text-muted: #94a3b8;
|
||||
|
||||
/* ---- Borders ---- */
|
||||
--border-default: rgba(86, 108, 138, 0.12);
|
||||
--border-hover: rgba(37, 99, 235, 0.28);
|
||||
|
||||
/* ---- Spacing (4px base) ---- */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
|
||||
/* ---- Border Radius ---- */
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 20px;
|
||||
--radius-full: 999px;
|
||||
|
||||
/* ---- Shadows ---- */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.07);
|
||||
--shadow-lg: 0 10px 30px rgba(30, 49, 78, 0.08);
|
||||
|
||||
/* ---- Typography ---- */
|
||||
--font-sans: 'PingFang SC', 'SF Pro Display', 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'SFMono-Regular', ui-monospace, monospace;
|
||||
|
||||
--text-xs: 12px;
|
||||
--text-sm: 13px;
|
||||
--text-base: 14px;
|
||||
--text-lg: 16px;
|
||||
--text-xl: 20px;
|
||||
--text-2xl: 24px;
|
||||
--text-3xl: 28px;
|
||||
|
||||
/* ---- Breakpoints (for reference, not used as CSS variables directly) ---- */
|
||||
--bp-lg: 1200px;
|
||||
--bp-md: 900px;
|
||||
--bp-sm: 640px;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { RefreshRight } from '@element-plus/icons-vue'
|
||||
|
||||
import AdminPlatformAgisoSection from '@/components/admin/AdminPlatformAgisoSection.vue'
|
||||
import AdminPlatformCloudtentaclesSection from '@/components/admin/AdminPlatformCloudtentaclesSection.vue'
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
fetchAdminScheduledJobsConfig,
|
||||
} from '@/services/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import '@/styles/admin-platform-shops.css'
|
||||
|
||||
const loading = ref(true)
|
||||
const isDevMode = import.meta.env.DEV
|
||||
@@ -189,287 +189,317 @@ async function loadConfigs() {
|
||||
}
|
||||
}
|
||||
|
||||
function switchPlatform(platform: PlatformTab) {
|
||||
activePlatform.value = platform
|
||||
}
|
||||
|
||||
async function saveAgisoConfigs() {
|
||||
await persistAgisoConfigs(loadConfigs)
|
||||
}
|
||||
|
||||
function switchTab(tab: string) {
|
||||
activePlatform.value = tab as PlatformTab
|
||||
}
|
||||
|
||||
onMounted(loadConfigs)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-panel admin-platform-shops">
|
||||
<header class="hero-card">
|
||||
<div class="hero-copy">
|
||||
<div class="platform-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>平台配置</h1>
|
||||
<p>按平台分别管理来源接入、凭据与店铺能力。Agiso 负责 webhook / 店铺消息配置,91卡券负责新订单来源,快手小店核销负责独立券核销调试,cloudtentacles 负责外部履约平台登录。</p>
|
||||
<p>按平台分别管理来源接入、凭据与店铺能力。</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<el-button round @click="loadConfigs">刷新全部配置</el-button>
|
||||
<el-button v-if="activePlatform === 'agiso'" round type="primary" @click="addShop">新增 Agiso 店铺</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button circle title="刷新全部配置" @click="loadConfigs">
|
||||
<template #icon><el-icon><RefreshRight /></el-icon></template>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="activePlatform === 'agiso'"
|
||||
type="primary"
|
||||
@click="addShop"
|
||||
>
|
||||
新增 Agiso 店铺
|
||||
</el-button>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasAdminRole('admin')" class="empty-block">仅管理员可以维护平台配置。</div>
|
||||
<!-- 权限不足 -->
|
||||
<el-result
|
||||
v-if="!hasAdminRole('admin')"
|
||||
icon="warning"
|
||||
title="仅管理员可以维护平台配置"
|
||||
sub-title="请切换管理员账号登录"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
|
||||
<div v-if="loading" class="empty-block">平台配置加载中</div>
|
||||
<!-- 错误提示 -->
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<!-- 加载态 -->
|
||||
<el-skeleton v-if="loading" :rows="8" animated />
|
||||
|
||||
<template v-else>
|
||||
<section class="overview-grid">
|
||||
<button
|
||||
type="button"
|
||||
:class="['platform-overview-card', { 'is-active': activePlatform === 'agiso' }]"
|
||||
@click="switchPlatform('agiso')"
|
||||
<!-- 概览卡片 -->
|
||||
<div class="overview-grid">
|
||||
<el-card
|
||||
v-for="card in [
|
||||
{ key: 'agiso', label: 'Agiso', tag: 'Webhook / 店铺', value: agisoStats.configuredShopCount, unit: '已配置店铺', meta: `识别到 ${agisoStats.observedShopCount} 家 · 待补 ${agisoStats.pendingObservedCount} 家` },
|
||||
{ key: 'notifications', label: '内部通知', tag: 'Bark / 值班', value: notificationStats.enabledRecipientCount, unit: '启用接收人', meta: `配置 ${notificationStats.configuredRecipientCount} 人 · 最近成功 ${notificationStats.lastSuccessCount} 个` },
|
||||
{ key: 'ninetyone', label: '91卡券', tag: '接入 / 待补全', value: ninetyoneStats.pendingCount, unit: '待补全订单', meta: `当前 ${ninetyoneStats.currentCount} 条 · 已生成任务 ${ninetyoneStats.taskReadyCount} 条` },
|
||||
{ key: 'kuaishouEticket', label: '快手小店核销', tag: 'Cookie / 券核销', value: kuaishouEticketStats.configuredShopCount, unit: '已配置店铺', meta: `Cookie 就绪 ${kuaishouEticketStats.cookieReadyCount} 家 · 当前店铺 ${kuaishouEticketStats.selectedShopReady ? '可调试' : '待补全'}` },
|
||||
...(isDevMode ? [{ key: 'cloudtentacles', label: 'cloudtentacles', tag: '履约 / 登录', value: cloudtentaclesStats.hasCredential ? '已配置' : '未配置', unit: '履约凭据', meta: `短信 ${cloudtentaclesStats.smsSent ? '已发送' : '未发送'} · 会话 ${cloudtentaclesStats.validated ? '已验证' : '未验证'}` }] : []),
|
||||
]"
|
||||
:key="card.key"
|
||||
:class="['overview-card', { 'is-active': activePlatform === card.key }]"
|
||||
:body-style="{ padding: '18px', cursor: 'pointer' }"
|
||||
shadow="hover"
|
||||
@click="switchTab(card.key)"
|
||||
>
|
||||
<div class="platform-head">
|
||||
<span class="platform-badge">Agiso</span>
|
||||
<span class="platform-tag">Webhook / 店铺</span>
|
||||
<div class="card-head">
|
||||
<el-tag size="small" type="primary">{{ card.label }}</el-tag>
|
||||
<el-tag size="small">{{ card.tag }}</el-tag>
|
||||
</div>
|
||||
<strong>{{ agisoStats.configuredShopCount }}</strong>
|
||||
<span>已配置店铺</span>
|
||||
<div class="platform-metrics">
|
||||
<span>识别到 {{ agisoStats.observedShopCount }} 家</span>
|
||||
<span>待补 {{ agisoStats.pendingObservedCount }} 家</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="card-value">{{ card.value }}</div>
|
||||
<div class="card-unit">{{ card.unit }}</div>
|
||||
<div class="card-meta">{{ card.meta }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
:class="['platform-overview-card', { 'is-active': activePlatform === 'notifications' }]"
|
||||
@click="switchPlatform('notifications')"
|
||||
>
|
||||
<div class="platform-head">
|
||||
<span class="platform-badge">内部通知</span>
|
||||
<span class="platform-tag">Bark / 值班</span>
|
||||
</div>
|
||||
<strong>{{ notificationStats.enabledRecipientCount }}</strong>
|
||||
<span>启用接收人</span>
|
||||
<div class="platform-metrics">
|
||||
<span>配置 {{ notificationStats.configuredRecipientCount }} 人</span>
|
||||
<span>最近成功 {{ notificationStats.lastSuccessCount }} 个</span>
|
||||
</div>
|
||||
</button>
|
||||
<!-- 平台切换 Tab -->
|
||||
<el-tabs v-model="activePlatform" type="card" class="platform-tabs">
|
||||
<el-tab-pane label="Agiso 店铺与消息" name="agiso">
|
||||
<AdminPlatformAgisoSection
|
||||
:agiso-file-path="agisoFilePath"
|
||||
:defaults="defaults"
|
||||
:shops="shops"
|
||||
:observed-shops="observedShops"
|
||||
:saving="saving"
|
||||
:is-shop-expanded="isShopExpanded"
|
||||
:toggle-shop="toggleShop"
|
||||
:expand-all-shops="expandAllShops"
|
||||
:collapse-all-shops="collapseAllShops"
|
||||
:describe-shop="describeShop"
|
||||
:remove-shop="removeShop"
|
||||
:import-observed-shop="importObservedShop"
|
||||
:save-agiso-configs="saveAgisoConfigs"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
:class="['platform-overview-card', { 'is-active': activePlatform === 'ninetyone' }]"
|
||||
@click="switchPlatform('ninetyone')"
|
||||
>
|
||||
<div class="platform-head">
|
||||
<span class="platform-badge">91卡券</span>
|
||||
<span class="platform-tag">接入 / 待补全</span>
|
||||
</div>
|
||||
<strong>{{ ninetyoneStats.pendingCount }}</strong>
|
||||
<span>待补全订单</span>
|
||||
<div class="platform-metrics">
|
||||
<span>当前 {{ ninetyoneStats.currentCount }} 条</span>
|
||||
<span>已生成任务 {{ ninetyoneStats.taskReadyCount }} 条</span>
|
||||
</div>
|
||||
</button>
|
||||
<el-tab-pane label="91卡券接入" name="ninetyone">
|
||||
<AdminPlatformNinetyoneSection
|
||||
:ninetyone-loading="ninetyoneLoading"
|
||||
:ninetyone-action-loading-id="ninetyoneActionLoadingId"
|
||||
:ninetyone-result-error="ninetyoneResultError"
|
||||
:ninetyone-status="ninetyoneStatus"
|
||||
:ninetyone-orders="ninetyoneOrders"
|
||||
:ninetyone-stats="ninetyoneStats"
|
||||
:load-ninetyone-orders="loadNinetyoneOrders"
|
||||
:handle-ninetyone-status-change="handleNinetyoneStatusChange"
|
||||
:handle-ninetyone-retry-order="handleNinetyoneRetryOrder"
|
||||
:handle-ninetyone-fail-order="handleNinetyoneFailOrder"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
:class="['platform-overview-card', { 'is-active': activePlatform === 'kuaishouEticket' }]"
|
||||
@click="switchPlatform('kuaishouEticket')"
|
||||
>
|
||||
<div class="platform-head">
|
||||
<span class="platform-badge">快手小店核销</span>
|
||||
<span class="platform-tag">Cookie / 券核销</span>
|
||||
</div>
|
||||
<strong>{{ kuaishouEticketStats.configuredShopCount }}</strong>
|
||||
<span>已配置店铺</span>
|
||||
<div class="platform-metrics">
|
||||
<span>Cookie 就绪 {{ kuaishouEticketStats.cookieReadyCount }} 家</span>
|
||||
<span>当前店铺 {{ kuaishouEticketStats.selectedShopReady ? '可调试' : '待补全' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<el-tab-pane label="内部通知" name="notifications">
|
||||
<AdminPlatformNotificationSection
|
||||
:notification-file-path="notificationFilePath"
|
||||
:notification-form="notificationForm"
|
||||
:notification-recipients="notificationRecipients"
|
||||
:notification-saving="notificationSaving"
|
||||
:notification-testing="notificationTesting"
|
||||
:notification-result-error="notificationResultError"
|
||||
:notification-test-result="notificationTestResult"
|
||||
:scheduled-jobs-file-path="scheduledJobsFilePath"
|
||||
:scheduled-jobs-form="scheduledJobsForm"
|
||||
:scheduled-jobs="scheduledJobs"
|
||||
:scheduled-job-runtime="scheduledJobRuntime"
|
||||
:scheduled-jobs-saving="scheduledJobsSaving"
|
||||
:scheduled-job-running-id="scheduledJobRunningId"
|
||||
:notification-stats="notificationStats"
|
||||
:add-notification-recipient="addNotificationRecipient"
|
||||
:remove-notification-recipient="removeNotificationRecipient"
|
||||
:handle-notification-save-config="handleNotificationSaveConfig"
|
||||
:handle-notification-test="handleNotificationTest"
|
||||
:handle-scheduled-jobs-save-config="handleScheduledJobsSaveConfig"
|
||||
:handle-scheduled-job-run-now="handleScheduledJobRunNow"
|
||||
:get-scheduled-job-runtime="getScheduledJobRuntime"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
:class="['platform-overview-card', { 'is-active': activePlatform === 'cloudtentacles' }]"
|
||||
@click="switchPlatform('cloudtentacles')"
|
||||
<el-tab-pane label="快手小店核销" name="kuaishouEticket">
|
||||
<AdminPlatformKuaishouEticketSection
|
||||
:kuaishou-eticket-file-path="kuaishouEticketFilePath"
|
||||
:kuaishou-eticket-shops="kuaishouEticketShops"
|
||||
:kuaishou-eticket-form="kuaishouEticketForm"
|
||||
:kuaishou-eticket-saving="kuaishouEticketSaving"
|
||||
:kuaishou-eticket-resolving-shop-id="kuaishouEticketResolvingShopId"
|
||||
:kuaishou-eticket-detailing="kuaishouEticketDetailing"
|
||||
:kuaishou-eticket-consuming="kuaishouEticketConsuming"
|
||||
:kuaishou-eticket-result-error="kuaishouEticketResultError"
|
||||
:kuaishou-eticket-shop-info-result="kuaishouEticketShopInfoResult"
|
||||
:kuaishou-eticket-detail-result="kuaishouEticketDetailResult"
|
||||
:kuaishou-eticket-consume-result="kuaishouEticketConsumeResult"
|
||||
:active-kuaishou-eticket-shop="activeKuaishouEticketShop"
|
||||
:kuaishou-eticket-stats="kuaishouEticketStats"
|
||||
:add-kuaishou-eticket-shop="addKuaishouEticketShop"
|
||||
:remove-kuaishou-eticket-shop="removeKuaishouEticketShop"
|
||||
:is-kuaishou-eticket-shop-expanded="isKuaishouEticketShopExpanded"
|
||||
:toggle-kuaishou-eticket-shop="toggleKuaishouEticketShop"
|
||||
:expand-all-kuaishou-eticket-shops="expandAllKuaishouEticketShops"
|
||||
:collapse-all-kuaishou-eticket-shops="collapseAllKuaishouEticketShops"
|
||||
:set-kuaishou-eticket-debug-shop="setKuaishouEticketDebugShop"
|
||||
:describe-kuaishou-eticket-shop="describeKuaishouEticketShop"
|
||||
:get-kuaishou-eticket-shop-initial="getKuaishouEticketShopInitial"
|
||||
:handle-kuaishou-eticket-resolve-shop-info="handleKuaishouEticketResolveShopInfo"
|
||||
:handle-kuaishou-eticket-save-source="handleKuaishouEticketSaveSource"
|
||||
:handle-kuaishou-eticket-query-detail="handleKuaishouEticketQueryDetail"
|
||||
:handle-kuaishou-eticket-consume="handleKuaishouEticketConsume"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane
|
||||
v-if="isDevMode"
|
||||
label="cloudtentacles 履约平台"
|
||||
name="cloudtentacles"
|
||||
>
|
||||
<div class="platform-head">
|
||||
<span class="platform-badge">cloudtentacles</span>
|
||||
<span class="platform-tag">履约 / 登录</span>
|
||||
</div>
|
||||
<strong>{{ cloudtentaclesStats.hasCredential ? '已配置' : '未配置' }}</strong>
|
||||
<span>履约凭据</span>
|
||||
<div class="platform-metrics">
|
||||
<span>短信 {{ cloudtentaclesStats.smsSent ? '已发送' : '未发送' }}</span>
|
||||
<span>会话 {{ cloudtentaclesStats.validated ? '已验证' : '未验证' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="platform-switcher">
|
||||
<button
|
||||
type="button"
|
||||
:class="['switch-chip', { 'is-active': activePlatform === 'agiso' }]"
|
||||
@click="switchPlatform('agiso')"
|
||||
>
|
||||
Agiso 店铺与消息
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['switch-chip', { 'is-active': activePlatform === 'ninetyone' }]"
|
||||
@click="switchPlatform('ninetyone')"
|
||||
>
|
||||
91卡券接入
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['switch-chip', { 'is-active': activePlatform === 'notifications' }]"
|
||||
@click="switchPlatform('notifications')"
|
||||
>
|
||||
内部通知
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['switch-chip', { 'is-active': activePlatform === 'kuaishouEticket' }]"
|
||||
@click="switchPlatform('kuaishouEticket')"
|
||||
>
|
||||
快手小店核销
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['switch-chip', { 'is-active': activePlatform === 'cloudtentacles' }]"
|
||||
@click="switchPlatform('cloudtentacles')"
|
||||
v-if="isDevMode"
|
||||
>
|
||||
cloudtentacles 履约平台
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<AdminPlatformAgisoSection
|
||||
v-if="activePlatform === 'agiso'"
|
||||
:agiso-file-path="agisoFilePath"
|
||||
:defaults="defaults"
|
||||
:shops="shops"
|
||||
:observed-shops="observedShops"
|
||||
:saving="saving"
|
||||
:is-shop-expanded="isShopExpanded"
|
||||
:toggle-shop="toggleShop"
|
||||
:expand-all-shops="expandAllShops"
|
||||
:collapse-all-shops="collapseAllShops"
|
||||
:describe-shop="describeShop"
|
||||
:remove-shop="removeShop"
|
||||
:import-observed-shop="importObservedShop"
|
||||
:save-agiso-configs="saveAgisoConfigs"
|
||||
/>
|
||||
|
||||
<AdminPlatformNinetyoneSection
|
||||
v-else-if="activePlatform === 'ninetyone'"
|
||||
:ninetyone-loading="ninetyoneLoading"
|
||||
:ninetyone-action-loading-id="ninetyoneActionLoadingId"
|
||||
:ninetyone-result-error="ninetyoneResultError"
|
||||
:ninetyone-status="ninetyoneStatus"
|
||||
:ninetyone-orders="ninetyoneOrders"
|
||||
:ninetyone-stats="ninetyoneStats"
|
||||
:load-ninetyone-orders="loadNinetyoneOrders"
|
||||
:handle-ninetyone-status-change="handleNinetyoneStatusChange"
|
||||
:handle-ninetyone-retry-order="handleNinetyoneRetryOrder"
|
||||
:handle-ninetyone-fail-order="handleNinetyoneFailOrder"
|
||||
/>
|
||||
|
||||
<AdminPlatformNotificationSection
|
||||
v-else-if="activePlatform === 'notifications'"
|
||||
:notification-file-path="notificationFilePath"
|
||||
:notification-form="notificationForm"
|
||||
:notification-recipients="notificationRecipients"
|
||||
:notification-saving="notificationSaving"
|
||||
:notification-testing="notificationTesting"
|
||||
:notification-result-error="notificationResultError"
|
||||
:notification-test-result="notificationTestResult"
|
||||
:scheduled-jobs-file-path="scheduledJobsFilePath"
|
||||
:scheduled-jobs-form="scheduledJobsForm"
|
||||
:scheduled-jobs="scheduledJobs"
|
||||
:scheduled-job-runtime="scheduledJobRuntime"
|
||||
:scheduled-jobs-saving="scheduledJobsSaving"
|
||||
:scheduled-job-running-id="scheduledJobRunningId"
|
||||
:notification-stats="notificationStats"
|
||||
:add-notification-recipient="addNotificationRecipient"
|
||||
:remove-notification-recipient="removeNotificationRecipient"
|
||||
:handle-notification-save-config="handleNotificationSaveConfig"
|
||||
:handle-notification-test="handleNotificationTest"
|
||||
:handle-scheduled-jobs-save-config="handleScheduledJobsSaveConfig"
|
||||
:handle-scheduled-job-run-now="handleScheduledJobRunNow"
|
||||
:get-scheduled-job-runtime="getScheduledJobRuntime"
|
||||
/>
|
||||
|
||||
<AdminPlatformKuaishouEticketSection
|
||||
v-else-if="activePlatform === 'kuaishouEticket'"
|
||||
:kuaishou-eticket-file-path="kuaishouEticketFilePath"
|
||||
:kuaishou-eticket-shops="kuaishouEticketShops"
|
||||
:kuaishou-eticket-form="kuaishouEticketForm"
|
||||
:kuaishou-eticket-saving="kuaishouEticketSaving"
|
||||
:kuaishou-eticket-resolving-shop-id="kuaishouEticketResolvingShopId"
|
||||
:kuaishou-eticket-detailing="kuaishouEticketDetailing"
|
||||
:kuaishou-eticket-consuming="kuaishouEticketConsuming"
|
||||
:kuaishou-eticket-result-error="kuaishouEticketResultError"
|
||||
:kuaishou-eticket-shop-info-result="kuaishouEticketShopInfoResult"
|
||||
:kuaishou-eticket-detail-result="kuaishouEticketDetailResult"
|
||||
:kuaishou-eticket-consume-result="kuaishouEticketConsumeResult"
|
||||
:active-kuaishou-eticket-shop="activeKuaishouEticketShop"
|
||||
:kuaishou-eticket-stats="kuaishouEticketStats"
|
||||
:add-kuaishou-eticket-shop="addKuaishouEticketShop"
|
||||
:remove-kuaishou-eticket-shop="removeKuaishouEticketShop"
|
||||
:is-kuaishou-eticket-shop-expanded="isKuaishouEticketShopExpanded"
|
||||
:toggle-kuaishou-eticket-shop="toggleKuaishouEticketShop"
|
||||
:expand-all-kuaishou-eticket-shops="expandAllKuaishouEticketShops"
|
||||
:collapse-all-kuaishou-eticket-shops="collapseAllKuaishouEticketShops"
|
||||
:set-kuaishou-eticket-debug-shop="setKuaishouEticketDebugShop"
|
||||
:describe-kuaishou-eticket-shop="describeKuaishouEticketShop"
|
||||
:get-kuaishou-eticket-shop-initial="getKuaishouEticketShopInitial"
|
||||
:handle-kuaishou-eticket-resolve-shop-info="handleKuaishouEticketResolveShopInfo"
|
||||
:handle-kuaishou-eticket-save-source="handleKuaishouEticketSaveSource"
|
||||
:handle-kuaishou-eticket-query-detail="handleKuaishouEticketQueryDetail"
|
||||
:handle-kuaishou-eticket-consume="handleKuaishouEticketConsume"
|
||||
/>
|
||||
|
||||
<AdminPlatformCloudtentaclesSection
|
||||
v-else-if="isDevMode"
|
||||
:cloudtentacles-file-path="cloudtentaclesFilePath"
|
||||
:cloudtentacles-session-file-path="cloudtentaclesSessionFilePath"
|
||||
:cloudtentacles-form="cloudtentaclesForm"
|
||||
:cloudtentacles-saving="cloudtentaclesSaving"
|
||||
:cloudtentacles-sending-sms="cloudtentaclesSendingSms"
|
||||
:cloudtentacles-testing="cloudtentaclesTesting"
|
||||
:cloudtentacles-validating="cloudtentaclesValidating"
|
||||
:cloudtentacles-result-error="cloudtentaclesResultError"
|
||||
:cloudtentacles-sms-result="cloudtentaclesSmsResult"
|
||||
:cloudtentacles-login-result="cloudtentaclesLoginResult"
|
||||
:cloudtentacles-validate-result="cloudtentaclesValidateResult"
|
||||
:cloudtentacles-debug-loading="cloudtentaclesDebugLoading"
|
||||
:cloudtentacles-debug-result="cloudtentaclesDebugResult"
|
||||
:cloudtentacles-persisted-session="cloudtentaclesPersistedSession"
|
||||
:cloudtentacles-stats="cloudtentaclesStats"
|
||||
:handle-cloudtentacles-save-source="handleCloudtentaclesSaveSource"
|
||||
:handle-cloudtentacles-send-sms-code="handleCloudtentaclesSendSmsCode"
|
||||
:handle-cloudtentacles-test-login="handleCloudtentaclesTestLogin"
|
||||
:handle-cloudtentacles-validate-session="handleCloudtentaclesValidateSession"
|
||||
:handle-cloudtentacles-fetch-asset="handleCloudtentaclesFetchAsset"
|
||||
:handle-cloudtentacles-fetch-categories="handleCloudtentaclesFetchCategories"
|
||||
:handle-cloudtentacles-fetch-sku-list="handleCloudtentaclesFetchSkuList"
|
||||
:handle-cloudtentacles-buy-sku="handleCloudtentaclesBuySku"
|
||||
:handle-cloudtentacles-use-sku="handleCloudtentaclesUseSku"
|
||||
:handle-cloudtentacles-fetch-knapsack="handleCloudtentaclesFetchKnapsack"
|
||||
:handle-cloudtentacles-fetch-vn-list="handleCloudtentaclesFetchVnList"
|
||||
:handle-cloudtentacles-appoint-vn="handleCloudtentaclesAppointVn"
|
||||
:handle-cloudtentacles-generate-vn-login-code="handleCloudtentaclesGenerateVnLoginCode"
|
||||
:handle-cloudtentacles-fetch-vn-code="handleCloudtentaclesFetchVnCode"
|
||||
:handle-cloudtentacles-verify-vn-code="handleCloudtentaclesVerifyVnCode"
|
||||
:handle-cloudtentacles-fetch-bind-url="handleCloudtentaclesFetchBindUrl"
|
||||
:handle-cloudtentacles-back-vn="handleCloudtentaclesBackVn"
|
||||
:handle-cloudtentacles-run-full-flow="handleCloudtentaclesRunFullFlow"
|
||||
/>
|
||||
<AdminPlatformCloudtentaclesSection
|
||||
:cloudtentacles-file-path="cloudtentaclesFilePath"
|
||||
:cloudtentacles-session-file-path="cloudtentaclesSessionFilePath"
|
||||
:cloudtentacles-form="cloudtentaclesForm"
|
||||
:cloudtentacles-saving="cloudtentaclesSaving"
|
||||
:cloudtentacles-sending-sms="cloudtentaclesSendingSms"
|
||||
:cloudtentacles-testing="cloudtentaclesTesting"
|
||||
:cloudtentacles-validating="cloudtentaclesValidating"
|
||||
:cloudtentacles-result-error="cloudtentaclesResultError"
|
||||
:cloudtentacles-sms-result="cloudtentaclesSmsResult"
|
||||
:cloudtentacles-login-result="cloudtentaclesLoginResult"
|
||||
:cloudtentacles-validate-result="cloudtentaclesValidateResult"
|
||||
:cloudtentacles-debug-loading="cloudtentaclesDebugLoading"
|
||||
:cloudtentacles-debug-result="cloudtentaclesDebugResult"
|
||||
:cloudtentacles-persisted-session="cloudtentaclesPersistedSession"
|
||||
:cloudtentacles-stats="cloudtentaclesStats"
|
||||
:handle-cloudtentacles-save-source="handleCloudtentaclesSaveSource"
|
||||
:handle-cloudtentacles-send-sms-code="handleCloudtentaclesSendSmsCode"
|
||||
:handle-cloudtentacles-test-login="handleCloudtentaclesTestLogin"
|
||||
:handle-cloudtentacles-validate-session="handleCloudtentaclesValidateSession"
|
||||
:handle-cloudtentacles-fetch-asset="handleCloudtentaclesFetchAsset"
|
||||
:handle-cloudtentacles-fetch-categories="handleCloudtentaclesFetchCategories"
|
||||
:handle-cloudtentacles-fetch-sku-list="handleCloudtentaclesFetchSkuList"
|
||||
:handle-cloudtentacles-buy-sku="handleCloudtentaclesBuySku"
|
||||
:handle-cloudtentacles-use-sku="handleCloudtentaclesUseSku"
|
||||
:handle-cloudtentacles-fetch-knapsack="handleCloudtentaclesFetchKnapsack"
|
||||
:handle-cloudtentacles-fetch-vn-list="handleCloudtentaclesFetchVnList"
|
||||
:handle-cloudtentacles-appoint-vn="handleCloudtentaclesAppointVn"
|
||||
:handle-cloudtentacles-generate-vn-login-code="handleCloudtentaclesGenerateVnLoginCode"
|
||||
:handle-cloudtentacles-fetch-vn-code="handleCloudtentaclesFetchVnCode"
|
||||
:handle-cloudtentacles-verify-vn-code="handleCloudtentaclesVerifyVnCode"
|
||||
:handle-cloudtentacles-fetch-bind-url="handleCloudtentaclesFetchBindUrl"
|
||||
:handle-cloudtentacles-back-vn="handleCloudtentaclesBackVn"
|
||||
:handle-cloudtentacles-run-full-flow="handleCloudtentaclesRunFullFlow"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-page {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: var(--text-3xl);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 概览卡片网格 */
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.overview-card.is-active {
|
||||
border-color: var(--border-hover);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.card-unit {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-sm);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
/* Tab 切换 */
|
||||
.platform-tabs {
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.overview-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user