feat: implement JSON editor component for enhanced JSON data management in forms

This commit is contained in:
wood911010 committed 2026-03-13 23:57:14 +08:00
1 parent 7355edf999
commit 2f9a401381
10 files changed
+1710 -851

No files matched your search

+473 -6
View File
@@ -33,6 +33,8 @@
"sass": "^1.89.2",
"terser": "^5.43.1",
"typescript": "^5.8.3",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^31.0.0",
"vite": "^5.0.0",
"vite-plugin-compression": "^0.5.1",
"vitest": "^2.0.0",
@@ -730,6 +732,17 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -2328,9 +2341,9 @@
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -2648,6 +2661,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/confbox": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
"dev": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3292,6 +3312,13 @@
"node": ">=12.0.0"
}
},
"node_modules/exsolve": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3897,6 +3924,13 @@
"dev": true,
"license": "ISC"
},
"node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -3968,6 +4002,24 @@
"node": ">= 0.8.0"
}
},
"node_modules/local-pkg": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
"dev": true,
"license": "MIT",
"dependencies": {
"mlly": "^1.7.4",
"pkg-types": "^2.3.0",
"quansync": "^0.2.11"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -4022,9 +4074,9 @@
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.18",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
"integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@@ -4106,6 +4158,45 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mlly": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz",
"integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.16.0",
"pathe": "^2.0.3",
"pkg-types": "^1.3.1",
"ufo": "^1.6.3"
}
},
"node_modules/mlly/node_modules/confbox": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
"dev": true,
"license": "MIT"
},
"node_modules/mlly/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/mlly/node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.1.8",
"mlly": "^1.7.4",
"pathe": "^2.0.1"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -4172,6 +4263,17 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
],
"license": "MIT"
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -4368,6 +4470,25 @@
}
}
},
"node_modules/pkg-types": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
"dev": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.2.2",
"exsolve": "^1.0.7",
"pathe": "^2.0.3"
}
},
"node_modules/pkg-types/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -4465,6 +4586,23 @@
"node": ">=6"
}
},
"node_modules/quansync": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/antfu"
},
{
"type": "individual",
"url": "https://github.com/sponsors/sxzz"
}
],
"license": "MIT"
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -4692,6 +4830,13 @@
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/scule": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
"dev": true,
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@@ -4840,6 +4985,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-literal": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^9.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -4909,6 +5067,54 @@
"dev": true,
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tinypool": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
@@ -5011,6 +5217,13 @@
"node": ">=14.17"
}
},
"node_modules/ufo": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
"integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -5018,6 +5231,65 @@
"dev": true,
"license": "MIT"
},
"node_modules/unimport": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.7.0.tgz",
"integrity": "sha512-njnL6sp8lEA8QQbZrt+52p/g4X0rw3bnGGmUcJnt1jeG8+iiqO779aGz0PirCtydAIVcuTBRlJ52F0u46z309Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.16.0",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"local-pkg": "^1.1.2",
"magic-string": "^0.30.21",
"mlly": "^1.8.0",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
"pkg-types": "^2.3.0",
"scule": "^1.3.0",
"strip-literal": "^3.1.0",
"tinyglobby": "^0.2.15",
"unplugin": "^2.3.11",
"unplugin-utils": "^0.3.1"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/unimport/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unimport/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/unimport/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -5028,6 +5300,194 @@
"node": ">= 10.0.0"
}
},
"node_modules/unplugin": {
"version": "2.3.11",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
"integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"acorn": "^8.15.0",
"picomatch": "^4.0.3",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/unplugin-auto-import": {
"version": "21.0.0",
"resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-21.0.0.tgz",
"integrity": "sha512-vWuC8SwqJmxZFYwPojhOhOXDb5xFhNNcEVb9K/RFkyk/3VnfaOjzitWN7v+8DEKpMjSsY2AEGXNgt6I0yQrhRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"local-pkg": "^1.1.2",
"magic-string": "^0.30.21",
"picomatch": "^4.0.3",
"unimport": "^5.6.0",
"unplugin": "^2.3.11",
"unplugin-utils": "^0.3.1"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@nuxt/kit": "^4.0.0",
"@vueuse/core": "*"
},
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
},
"@vueuse/core": {
"optional": true
}
}
},
"node_modules/unplugin-auto-import/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/unplugin-utils": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
"integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
"dev": true,
"license": "MIT",
"dependencies": {
"pathe": "^2.0.3",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/unplugin-utils/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/unplugin-utils/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/unplugin-vue-components": {
"version": "31.0.0",
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-31.0.0.tgz",
"integrity": "sha512-4ULwfTZTLuWJ7+S9P7TrcStYLsSRkk6vy2jt/WTfgUEUb0nW9//xxmrfhyHUEVpZ2UKRRwfRb8Yy15PDbVZf+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^5.0.0",
"local-pkg": "^1.1.2",
"magic-string": "^0.30.21",
"mlly": "^1.8.0",
"obug": "^2.1.1",
"picomatch": "^4.0.3",
"tinyglobby": "^0.2.15",
"unplugin": "^2.3.11",
"unplugin-utils": "^0.3.1"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@nuxt/kit": "^3.2.2 || ^4.0.0",
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
}
}
},
"node_modules/unplugin-vue-components/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/unplugin-vue-components/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/unplugin-vue-components/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/unplugin/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -5432,6 +5892,13 @@
"typescript": ">=5.0.0"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -0,0 +1,505 @@
<template>
<div class="json-editor">
<!-- 操作按钮 -->
<div class="json-editor-actions">
<el-button size="small" @click="addRootEntry">
{{ t('jsonEditor.addItem') }}
</el-button>
<el-button size="small" text :disabled="!hasData" @click="clearAll">
{{ t('jsonEditor.clearAll') }}
</el-button>
<el-button size="small" text :disabled="!hasData" @click="copyJson">
{{ t('jsonEditor.copyJson') }}
</el-button>
<el-button size="small" text @click="openImportDialog">
{{ t('jsonEditor.importJson') }}
</el-button>
</div>
<!-- JSON 树形结构 -->
<div class="json-tree">
<JsonNode
v-for="(node, index) in nodes"
:key="node.id"
:node="node"
:depth="0"
@update="handleNodeUpdate"
@delete="handleNodeDelete(index)"
@add-child="handleAddChild"
@toggle-expand="handleToggleExpand"
/>
<div v-if="!nodes.length" class="json-empty">
{{ t('jsonEditor.emptyState') }}
</div>
</div>
<!-- JSON 预览 -->
<div class="json-preview">
<div class="json-preview-label">{{ t('jsonEditor.preview') }}</div>
<el-input
type="textarea"
:rows="4"
:model-value="jsonPreview"
readonly
/>
</div>
<!-- 导入弹窗 -->
<el-dialog
v-model="importDialogVisible"
:title="t('jsonEditor.importTitle')"
width="600px"
destroy-on-close
>
<p class="import-dialog-tip">
{{ t('jsonEditor.importDescription') }}
</p>
<el-input
v-model="importDialogContent"
type="textarea"
:rows="10"
:placeholder="t('jsonEditor.importPlaceholder')"
/>
<p v-if="importDialogError" class="json-editor-error">
{{ importDialogError }}
</p>
<template #footer>
<el-button @click="importDialogVisible = false">
{{ t('jsonEditor.importCancel') }}
</el-button>
<el-button type="primary" @click="handleImportConfirm">
{{ t('jsonEditor.importConfirm') }}
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
import JsonNode from './JsonNode.vue'
export type JsonValueType = 'string' | 'number' | 'boolean' | 'object' | 'array'
export interface JsonNodeData {
id: string
key: string
value: string
type: JsonValueType
children: JsonNodeData[]
expanded: boolean
keyError?: string
valueError?: string
}
const props = defineProps<{
modelValue?: Record<string, any> | string | null
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: Record<string, any> | string | null): void
}>()
const { t } = useI18n()
// 响应式数据
const nodes = ref<JsonNodeData[]>([])
const importDialogVisible = ref(false)
const importDialogContent = ref('')
const importDialogError = ref('')
// 计算属性
const hasData = computed(() => nodes.value.length > 0)
const jsonPreview = computed(() => {
const data = buildJsonData(nodes.value)
return data ? JSON.stringify(data, null, 2) : ''
})
// 生成唯一ID
const createNodeId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
// 创建空节点
const createEmptyNode = (type: JsonValueType = 'string'): JsonNodeData => ({
id: createNodeId(),
key: '',
value: type === 'object' || type === 'array' ? '' : '',
type,
children: [],
expanded: true,
keyError: '',
valueError: ''
})
// 构建 JSON 数据
const buildJsonData = (nodeList: JsonNodeData[]): Record<string, any> | null => {
if (!nodeList.length) return null
const result: Record<string, any> = {}
for (const node of nodeList) {
const trimmedKey = node.key.trim()
if (!trimmedKey) continue
let value: any
switch (node.type) {
case 'number':
const num = Number(node.value)
value = Number.isNaN(num) ? node.value : num
break
case 'boolean':
value = node.value === 'true'
break
case 'object':
value = buildJsonData(node.children)
break
case 'array':
value = node.children.map(child => {
if (child.type === 'object') {
return buildJsonData(child.children)
}
if (child.type === 'number') {
return Number(child.value)
}
if (child.type === 'boolean') {
return child.value === 'true'
}
return child.value
})
break
default:
value = node.value
}
result[trimmedKey] = value
}
return Object.keys(result).length ? result : null
}
// 解析 JSON 为节点
const parseJsonToNodes = (data: unknown): JsonNodeData[] => {
if (!data) return []
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch {
return []
}
}
if (typeof data !== 'object' || data === null) return []
if (Array.isArray(data)) {
return data.map((item, index) => {
const isObj = item !== null && typeof item === 'object'
const isArr = Array.isArray(item)
const itemType: JsonValueType = item === null ? 'string' : (isArr ? 'array' : (isObj ? 'object' : typeof item as JsonValueType))
return {
id: createNodeId(),
key: String(index),
value: isObj || isArr ? '' : String(item),
type: itemType,
children: (isObj || isArr) ? parseJsonToNodes(item) : [],
expanded: true,
keyError: '',
valueError: ''
}
})
}
return Object.entries(data as Record<string, any>).map(([key, value]) => {
let type: JsonValueType = 'string'
let children: JsonNodeData[] = []
if (value === null) {
type = 'string'
} else if (Array.isArray(value)) {
type = 'array'
children = value.map((item, index) => {
const isObj = item !== null && typeof item === 'object'
const isArr = Array.isArray(item)
const itemType: JsonValueType = item === null ? 'string' : (isArr ? 'array' : (isObj ? 'object' : typeof item as JsonValueType))
return {
id: createNodeId(),
key: String(index),
value: isObj || isArr ? '' : String(item),
type: itemType,
children: (isObj || isArr) ? parseJsonToNodes(item) : [],
expanded: true,
keyError: '',
valueError: ''
}
})
} else if (typeof value === 'object') {
type = 'object'
children = parseJsonToNodes(value)
} else if (typeof value === 'number') {
type = 'number'
} else if (typeof value === 'boolean') {
type = 'boolean'
}
return {
id: createNodeId(),
key,
value: type === 'object' || type === 'array' ? '' : String(value),
type,
children,
expanded: true,
keyError: '',
valueError: ''
}
})
}
// 初始化节点
const initializeNodes = (value: unknown) => {
nodes.value = parseJsonToNodes(value)
}
// 添加根节点
const addRootEntry = () => {
nodes.value.push(createEmptyNode('string'))
emitUpdate()
}
// 清空
const clearAll = () => {
nodes.value = []
emitUpdate()
}
// 复制 JSON
const fallbackCopy = (value: string) => {
const textarea = document.createElement('textarea')
textarea.value = value
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
const copyJson = async () => {
if (!jsonPreview.value) {
ElMessage.info(t('jsonEditor.copyEmpty'))
return
}
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(jsonPreview.value)
} else {
fallbackCopy(jsonPreview.value)
}
ElMessage.success(t('jsonEditor.copySuccess'))
} catch (error) {
console.error('复制JSON失败:', error)
ElMessage.error(t('jsonEditor.copyError'))
}
}
// 导入弹窗
const openImportDialog = () => {
importDialogContent.value = jsonPreview.value
importDialogError.value = ''
importDialogVisible.value = true
}
const handleImportConfirm = () => {
importDialogError.value = ''
const content = importDialogContent.value.trim()
if (!content) {
nodes.value = []
importDialogVisible.value = false
ElMessage.success(t('jsonEditor.importSuccess'))
emitUpdate()
return
}
try {
const parsed = JSON.parse(content)
nodes.value = parseJsonToNodes(parsed)
importDialogVisible.value = false
ElMessage.success(t('jsonEditor.importSuccess'))
emitUpdate()
} catch (error: any) {
importDialogError.value = error?.message || t('jsonEditor.importFailed')
}
}
// 节点更新
const handleNodeUpdate = () => {
// 自动触发表单更新
}
// 节点删除
const handleNodeDelete = (index: number) => {
nodes.value.splice(index, 1)
emitUpdate()
}
// 添加子节点
const handleAddChild = (parentId: string) => {
const findAndAdd = (nodeList: JsonNodeData[]): boolean => {
for (const node of nodeList) {
if (node.id === parentId) {
if (node.type === 'array') {
node.children.push(createEmptyNode('string'))
} else if (node.type === 'object') {
node.children.push(createEmptyNode('string'))
}
return true
}
if (node.children.length && findAndAdd(node.children)) {
return true
}
}
return false
}
findAndAdd(nodes.value)
}
// 切换展开
const handleToggleExpand = (nodeId: string) => {
const findAndToggle = (nodeList: JsonNodeData[]): boolean => {
for (const node of nodeList) {
if (node.id === nodeId) {
node.expanded = !node.expanded
return true
}
if (node.children.length && findAndToggle(node.children)) {
return true
}
}
return false
}
findAndToggle(nodes.value)
}
// 验证所有节点
const validateAll = (): boolean => {
let isValid = true
const existingKeys = new Map<string, number>()
const validate = (nodeList: JsonNodeData[], parentPath: string = '') => {
for (const node of nodeList) {
node.keyError = ''
node.valueError = ''
const trimmedKey = node.key.trim()
const fullPath = parentPath ? `${parentPath}.${trimmedKey}` : trimmedKey
// Key 验证
if (!trimmedKey) {
node.keyError = t('jsonEditor.keyRequired')
isValid = false
} else {
const keyCount = existingKeys.get(fullPath) || 0
if (keyCount > 0) {
node.keyError = t('jsonEditor.keyDuplicate')
isValid = false
}
existingKeys.set(fullPath, keyCount + 1)
}
// Value 验证
if (node.type === 'number') {
if (node.value && Number.isNaN(Number(node.value))) {
node.valueError = t('jsonEditor.numberInvalid')
isValid = false
}
}
// 递归验证子节点
if (node.children.length) {
validate(node.children, fullPath)
}
}
}
validate(nodes.value)
return isValid
}
// 暴露方法
defineExpose({
validate: validateAll,
getValue: () => buildJsonData(nodes.value),
setValue: initializeNodes
})
// 监听 modelValue 变化(仅用于初始化)
watch(
() => props.modelValue,
(newValue) => {
initializeNodes(newValue)
},
{ immediate: true }
)
// 手动触发更新的方法(供内部操作调用)
const emitUpdate = () => {
nextTick(() => {
const newValue = buildJsonData(nodes.value)
emit('update:modelValue', newValue)
})
}
</script>
<style scoped>
.json-editor {
width: 100%;
}
.json-editor-actions {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.json-tree {
border: 1px solid var(--app-border-color);
border-radius: 4px;
padding: 12px;
margin-bottom: 16px;
min-height: 100px;
background: var(--app-content-bg);
}
.json-empty {
text-align: center;
color: var(--app-text-secondary);
padding: 20px;
font-size: 13px;
}
.json-preview {
margin-top: 16px;
}
.json-preview-label {
font-size: 12px;
color: var(--app-text-secondary);
margin-bottom: 8px;
}
.import-dialog-tip {
font-size: 13px;
color: var(--app-text-secondary);
margin-bottom: 12px;
}
.json-editor-error {
color: #f56c6c;
font-size: 12px;
margin-top: 8px;
}
</style>
+258
View File
@@ -0,0 +1,258 @@
<template>
<div class="json-node" :class="{ 'is-child': depth > 0 }">
<!-- 节点内容 -->
<div class="json-node-content">
<!-- 展开/折叠按钮 (对象和数组) -->
<el-button
v-if="isContainer"
link
class="expand-btn"
@click="$emit('toggle-expand', node.id)"
>
<el-icon>
<ArrowRight v-if="!node.expanded" />
<ArrowDown v-else />
</el-icon>
</el-button>
<span v-else class="expand-placeholder"></span>
<!-- Key 输入 -->
<div class="node-field key-field">
<el-input
v-model="node.key"
:placeholder="t('jsonEditor.keyPlaceholder')"
size="small"
@input="node.keyError = ''"
/>
<p v-if="node.keyError" class="node-error">{{ node.keyError }}</p>
</div>
<!-- 类型选择 -->
<div class="node-field type-field">
<el-select
v-model="node.type"
size="small"
@change="handleTypeChange"
>
<el-option
v-for="type in typeOptions"
:key="type"
:label="t(`jsonEditor.types.${type}`)"
:value="type"
/>
</el-select>
</div>
<!-- Value 输入 -->
<div class="node-field value-field">
<!-- 布尔类型 -->
<el-select
v-if="node.type === 'boolean'"
v-model="node.value"
size="small"
>
<el-option :label="'true'" value="true" />
<el-option :label="'false'" value="false" />
</el-select>
<!-- 对象/数组类型 - 显示子节点 -->
<template v-else-if="isContainer">
<span class="container-label">
<template v-if="node.type === 'array'">[{{ node.children.length }}]</template>
<template v-else>{ {{ node.children.length }} }</template>
</span>
</template>
<!-- 数字类型 -->
<el-input
v-else-if="node.type === 'number'"
v-model="node.value"
:placeholder="t('jsonEditor.valuePlaceholder')"
size="small"
type="number"
@input="node.valueError = ''"
/>
<!-- 字符串类型 -->
<el-input
v-else
v-model="node.value"
:placeholder="t('jsonEditor.valuePlaceholder')"
size="small"
@input="node.valueError = ''"
/>
<p v-if="node.valueError" class="node-error">{{ node.valueError }}</p>
</div>
<!-- 操作按钮 -->
<div class="node-actions">
<el-button
v-if="isContainer"
link
type="primary"
size="small"
@click="$emit('add-child', node.id)"
>
{{ t('jsonEditor.addChild') }}
</el-button>
<el-button
v-if="depth > 0 || node.type === 'array'"
link
type="danger"
size="small"
@click="$emit('delete')"
>
{{ t('jsonEditor.remove') }}
</el-button>
</div>
</div>
<!-- 子节点容器 -->
<div v-if="isContainer && node.expanded" class="json-node-children">
<JsonNode
v-for="(child, index) in node.children"
:key="child.id"
:node="child"
:depth="depth + 1"
@update="$emit('update')"
@delete="removeChild(index)"
@add-child="(id: string) => $emit('add-child', id)"
@toggle-expand="(id: string) => $emit('toggle-expand', id)"
/>
<div v-if="!node.children.length" class="child-empty">
{{ node.type === 'array' ? t('jsonEditor.arrayEmpty') : t('jsonEditor.objectEmpty') }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick } from 'vue'
import { ArrowRight, ArrowDown } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n'
import type { JsonNodeData, JsonValueType } from './JsonEditor.vue'
const props = defineProps<{
node: JsonNodeData
depth: number
}>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'delete'): void
(e: 'add-child', id: string): void
(e: 'toggle-expand', id: string): void
}>()
const { t } = useI18n()
const typeOptions: JsonValueType[] = ['string', 'number', 'boolean', 'object', 'array']
const isContainer = computed(() => props.node.type === 'object' || props.node.type === 'array')
const handleTypeChange = (newType: JsonValueType) => {
// 清空 children 如果切换到非容器类型
if (newType !== 'object' && newType !== 'array') {
props.node.children = []
}
// 清空 value 如果切换到容器类型
if (newType === 'object' || newType === 'array') {
props.node.value = ''
}
// 初始化空 children 如果切换到容器类型
if ((newType === 'object' || newType === 'array') && !props.node.children.length) {
props.node.children = []
}
nextTick(() => {
emit('update')
})
}
const removeChild = (index: number) => {
nextTick(() => {
props.node.children.splice(index, 1)
emit('update')
})
}
</script>
<style scoped>
.json-node {
margin-bottom: 8px;
}
.json-node.is-child {
margin-left: 24px;
padding-left: 12px;
border-left: 1px dashed var(--app-border-color);
}
.json-node-content {
display: flex;
align-items: flex-start;
gap: 8px;
flex-wrap: wrap;
}
.expand-btn {
padding: 2px;
min-width: 20px;
}
.expand-placeholder {
width: 20px;
display: inline-block;
}
.node-field {
display: flex;
flex-direction: column;
}
.key-field {
flex: 1;
min-width: 120px;
}
.type-field {
flex: 0 0 100px;
}
.value-field {
flex: 1;
min-width: 120px;
}
.container-label {
display: inline-block;
padding: 4px 12px;
background: var(--app-bg-color);
border-radius: 4px;
font-size: 12px;
color: var(--app-text-secondary);
}
.node-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.node-error {
color: #f56c6c;
font-size: 11px;
margin-top: 2px;
}
.json-node-children {
margin-top: 8px;
}
.child-empty {
text-align: center;
color: var(--app-text-secondary);
font-size: 12px;
padding: 8px;
}
</style>
+38 -1
View File
@@ -813,7 +813,7 @@
"importTitleLimit": "Import Usage Limits JSON",
"importTitleCustom": "Import Custom Parameters JSON",
"importDescription": "Paste a JSON object whose values can be string, number, or boolean. Confirming will overwrite current entries.",
"importPlaceholder": "{ \"feature_dashboard\": \"enabled\" }",
"importPlaceholder": "Please enter a JSON object",
"importCancel": "Cancel",
"importConfirm": "Parse & Overwrite",
"importSuccess": "JSON imported successfully",
@@ -964,5 +964,42 @@
"days": "{days} day(s)"
}
}
},
"jsonEditor": {
"addItem": "Add Field",
"clearAll": "Clear",
"copyJson": "Copy JSON",
"importJson": "Import JSON",
"emptyState": "No data yet, add fields or import JSON",
"preview": "JSON Preview",
"copyEmpty": "Nothing to copy",
"copySuccess": "Copied successfully",
"copyError": "Copy failed",
"importTitle": "Import JSON",
"importDescription": "Please enter valid JSON string, supports nested objects and arrays",
"importPlaceholder": "Please enter a valid JSON string",
"importCancel": "Cancel",
"importConfirm": "Import",
"importSuccess": "Imported successfully",
"importFailed": "Invalid JSON format",
"keyLabel": "Key",
"keyPlaceholder": "Enter key",
"valueLabel": "Value",
"valuePlaceholder": "Enter value",
"typeLabel": "Type",
"keyRequired": "Key is required",
"keyDuplicate": "Duplicate key",
"numberInvalid": "Please enter a valid number",
"addChild": "Add Child",
"remove": "Delete",
"arrayEmpty": "Array is empty, click to add child",
"objectEmpty": "Object is empty, click to add child",
"types": {
"string": "String",
"number": "Number",
"boolean": "Boolean",
"object": "Object",
"array": "Array"
}
}
}
+38 -1
View File
@@ -812,7 +812,7 @@
"importTitleLimit": "利用制限 JSON をインポート",
"importTitleCustom": "カスタムパラメータ JSON をインポート",
"importDescription": "キーを設定名とするオブジェクト形式の JSON を貼り付けてください。値は文字列・数値・ブールをサポートします。確定すると現在のリストを上書きします。",
"importPlaceholder": "{ \"feature_dashboard\": \"enabled\" }",
"importPlaceholder": "JSONオブジェクトを入力してください",
"importCancel": "キャンセル",
"importConfirm": "解析して上書き",
"importSuccess": "JSON を取り込みました",
@@ -963,5 +963,42 @@
"days": "{days}日"
}
}
},
"jsonEditor": {
"addItem": "フィールド追加",
"clearAll": "クリア",
"copyJson": "JSONをコピー",
"importJson": "JSONをインポート",
"emptyState": "データがありません。フィールドを追加するかJSONをインポートしてください",
"preview": "JSONプレビュー",
"copyEmpty": "コピーするJSONがありません",
"copySuccess": "コピー成功",
"copyError": "コピー失敗",
"importTitle": "JSONをインポート",
"importDescription": "有効なJSON文字列を入力してください。ネストされたオブジェクトと配列をサポートします",
"importPlaceholder": "有効なJSON文字列を入力してください",
"importCancel": "キャンセル",
"importConfirm": "インポート",
"importSuccess": "インポート成功",
"importFailed": "JSON形式が無効です",
"keyLabel": "キー",
"keyPlaceholder": "キーを入力",
"valueLabel": "値",
"valuePlaceholder": "値を入力",
"typeLabel": "タイプ",
"keyRequired": "キーを入力してください",
"keyDuplicate": "キーが重複しています",
"numberInvalid": "有効な数値を入力してください",
"addChild": "子要素を追加",
"remove": "削除",
"arrayEmpty": "配列が空です。クリックして子要素を追加",
"objectEmpty": "オブジェクトが空です。クリックして子要素を追加",
"types": {
"string": "文字列",
"number": "数値",
"boolean": "真偽値",
"object": "オブジェクト",
"array": "配列"
}
}
}
+38 -1
View File
@@ -846,7 +846,7 @@
"importTitleLimit": "导入使用限制 JSON",
"importTitleCustom": "导入自定义参数 JSON",
"importDescription": "粘贴一个对象格式的 JSON,键为配置项,值支持字符串、数字或布尔类型。提交后将覆盖当前列表。",
"importPlaceholder": "{ \"feature_dashboard\": \"enabled\" }",
"importPlaceholder": "请输入 JSON 对象",
"importCancel": "取消",
"importConfirm": "解析并覆盖",
"importSuccess": "JSON 解析成功",
@@ -997,5 +997,42 @@
"days": "{days}天"
}
}
},
"jsonEditor": {
"addItem": "添加字段",
"clearAll": "清空",
"copyJson": "复制JSON",
"importJson": "导入JSON",
"emptyState": "暂无数据,请添加字段或导入JSON",
"preview": "JSON 预览",
"copyEmpty": "没有可复制的JSON",
"copySuccess": "复制成功",
"copyError": "复制失败",
"importTitle": "导入 JSON",
"importDescription": "请输入有效的 JSON 字符串,支持嵌套对象和数组",
"importPlaceholder": "请输入有效的 JSON 字符串",
"importCancel": "取消",
"importConfirm": "导入",
"importSuccess": "导入成功",
"importFailed": "JSON 格式不正确",
"keyLabel": "键名",
"keyPlaceholder": "请输入键名",
"valueLabel": "值",
"valuePlaceholder": "请输入值",
"typeLabel": "类型",
"keyRequired": "请输入键名",
"keyDuplicate": "键名重复",
"numberInvalid": "请输入有效数字",
"addChild": "添加子项",
"remove": "删除",
"arrayEmpty": "数组为空,点击添加子项",
"objectEmpty": "对象为空,点击添加子项",
"types": {
"string": "字符串",
"number": "数字",
"boolean": "布尔",
"object": "对象",
"array": "数组"
}
}
}
+31 -617
View File
@@ -213,295 +213,24 @@
<!-- 功能配置 -->
<div class="license-form">
<div class="key-value-section-header">
<h3 class="section-title">{{ t('pages.licenses.form.sections.featureConfig') }}</h3>
<div class="key-value-section-actions">
<el-button size="small" @click="addFeatureConfigEntry">
{{ t('pages.licenses.form.keyValue.addItem') }}
</el-button>
<el-button
size="small"
text
:disabled="!featureConfigEntries.length"
@click="clearFeatureConfigEntries"
>
{{ t('pages.licenses.form.keyValue.clearAll') }}
</el-button>
<el-button
size="small"
text
:disabled="!featureConfigPreview"
@click="copyJson(featureConfigPreview)"
>
{{ t('pages.licenses.form.keyValue.copyJson') }}
</el-button>
<el-button size="small" text @click="openImportDialog('feature')">
{{ t('pages.licenses.form.keyValue.importJson') }}
</el-button>
</div>
</div>
<div class="key-value-list">
<div v-for="(item, index) in featureConfigEntries" :key="item.id" class="key-value-row">
<div class="key-value-field">
<label>{{ t('pages.licenses.form.keyValue.keyLabel') }}</label>
<el-input
v-model="item.key"
:placeholder="t('pages.licenses.form.keyValue.keyPlaceholder')"
@input="item.keyError = ''"
/>
<p v-if="item.keyError" class="key-value-error">{{ item.keyError }}</p>
</div>
<div class="key-value-field type-field">
<label>{{ t('pages.licenses.form.keyValue.typeLabel') }}</label>
<el-select v-model="item.type" @change="handleEntryTypeChange(item)">
<el-option
v-for="type in typeOptions"
:key="type"
:label="t(`pages.licenses.form.keyValue.typeOptions.${type}`)"
:value="type"
/>
</el-select>
</div>
<div class="key-value-field">
<label>{{ t('pages.licenses.form.keyValue.valueLabel') }}</label>
<template v-if="item.type === 'bool'">
<el-select v-model="item.value" @change="item.valueError = ''">
<el-option
v-for="option in boolOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</template>
<el-input
v-else
v-model="item.value"
:placeholder="t('pages.licenses.form.keyValue.valuePlaceholder')"
@input="item.valueError = ''"
:inputmode="item.type === 'number' ? 'decimal' : 'text'"
/>
<p v-if="item.valueError" class="key-value-error">{{ item.valueError }}</p>
</div>
<el-button link type="danger" @click="removeFeatureConfigEntry(index)">
{{ t('pages.licenses.form.keyValue.remove') }}
</el-button>
</div>
<div v-if="!featureConfigEntries.length" class="key-value-empty">
{{ t('pages.licenses.form.keyValue.emptyState') }}
</div>
</div>
<div class="key-value-preview">
<div class="key-value-preview-label">
{{ t('pages.licenses.form.keyValue.preview') }}
</div>
<el-input type="textarea" :rows="3" :model-value="featureConfigPreview" readonly />
</div>
<JsonEditor ref="featureConfigRef" v-model="featureConfigData" />
</div>
<!-- 限制设置 -->
<div class="license-form">
<div class="key-value-section-header">
<h3 class="section-title">{{ t('pages.licenses.form.sections.usageLimits') }}</h3>
<div class="key-value-section-actions">
<el-button size="small" @click="addLimitEntry">
{{ t('pages.licenses.form.keyValue.addItem') }}
</el-button>
<el-button size="small" text :disabled="!limitEntries.length" @click="clearLimitEntries">
{{ t('pages.licenses.form.keyValue.clearAll') }}
</el-button>
<el-button
size="small"
text
:disabled="!usageJsonPreview"
@click="copyJson(usageJsonPreview)"
>
{{ t('pages.licenses.form.keyValue.copyJson') }}
</el-button>
<el-button size="small" text @click="openImportDialog('limit')">
{{ t('pages.licenses.form.keyValue.importJson') }}
</el-button>
</div>
</div>
<div class="key-value-list">
<div v-for="(item, index) in limitEntries" :key="item.id" class="key-value-row">
<div class="key-value-field">
<label>{{ t('pages.licenses.form.keyValue.keyLabel') }}</label>
<el-input
v-model="item.key"
:placeholder="t('pages.licenses.form.keyValue.keyPlaceholder')"
@input="item.keyError = ''"
/>
<p v-if="item.keyError" class="key-value-error">{{ item.keyError }}</p>
</div>
<div class="key-value-field type-field">
<label>{{ t('pages.licenses.form.keyValue.typeLabel') }}</label>
<el-select v-model="item.type" @change="handleEntryTypeChange(item)">
<el-option
v-for="type in typeOptions"
:key="type"
:label="t(`pages.licenses.form.keyValue.typeOptions.${type}`)"
:value="type"
/>
</el-select>
</div>
<div class="key-value-field">
<label>{{ t('pages.licenses.form.keyValue.valueLabel') }}</label>
<template v-if="item.type === 'bool'">
<el-select v-model="item.value" @change="item.valueError = ''">
<el-option
v-for="option in boolOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</template>
<el-input
v-else
v-model="item.value"
:placeholder="t('pages.licenses.form.keyValue.valuePlaceholder')"
@input="item.valueError = ''"
:inputmode="item.type === 'number' ? 'decimal' : 'text'"
/>
<p v-if="item.valueError" class="key-value-error">{{ item.valueError }}</p>
</div>
<el-button link type="danger" @click="removeLimitEntry(index)">
{{ t('pages.licenses.form.keyValue.remove') }}
</el-button>
</div>
<div v-if="!limitEntries.length" class="key-value-empty">
{{ t('pages.licenses.form.keyValue.emptyState') }}
</div>
</div>
<div class="key-value-preview">
<div class="key-value-preview-label">
{{ t('pages.licenses.form.keyValue.preview') }}
</div>
<el-input type="textarea" :rows="3" :model-value="usageJsonPreview" readonly />
</div>
<JsonEditor ref="usageLimitsRef" v-model="usageLimitsData" />
</div>
<!-- 自定义参数 -->
<div class="license-form">
<div class="key-value-section-header">
<h3 class="section-title">{{ t('pages.licenses.form.sections.customParameters') }}</h3>
<div class="key-value-section-actions">
<el-button size="small" @click="addCustomEntry">
{{ t('pages.licenses.form.keyValue.addItem') }}
</el-button>
<el-button size="small" text :disabled="!customEntries.length" @click="clearCustomEntries">
{{ t('pages.licenses.form.keyValue.clearAll') }}
</el-button>
<el-button
size="small"
text
:disabled="!customJsonPreview"
@click="copyJson(customJsonPreview)"
>
{{ t('pages.licenses.form.keyValue.copyJson') }}
</el-button>
<el-button size="small" text @click="openImportDialog('custom')">
{{ t('pages.licenses.form.keyValue.importJson') }}
</el-button>
</div>
</div>
<div class="key-value-list">
<div v-for="(item, index) in customEntries" :key="item.id" class="key-value-row">
<div class="key-value-field">
<label>{{ t('pages.licenses.form.keyValue.keyLabel') }}</label>
<el-input
v-model="item.key"
:placeholder="t('pages.licenses.form.keyValue.keyPlaceholder')"
@input="item.keyError = ''"
/>
<p v-if="item.keyError" class="key-value-error">{{ item.keyError }}</p>
</div>
<div class="key-value-field type-field">
<label>{{ t('pages.licenses.form.keyValue.typeLabel') }}</label>
<el-select v-model="item.type" @change="handleEntryTypeChange(item)">
<el-option
v-for="type in typeOptions"
:key="type"
:label="t(`pages.licenses.form.keyValue.typeOptions.${type}`)"
:value="type"
/>
</el-select>
</div>
<div class="key-value-field">
<label>{{ t('pages.licenses.form.keyValue.valueLabel') }}</label>
<template v-if="item.type === 'bool'">
<el-select v-model="item.value" @change="item.valueError = ''">
<el-option
v-for="option in boolOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</template>
<el-input
v-else
v-model="item.value"
:placeholder="t('pages.licenses.form.keyValue.valuePlaceholder')"
@input="item.valueError = ''"
:inputmode="item.type === 'number' ? 'decimal' : 'text'"
/>
<p v-if="item.valueError" class="key-value-error">{{ item.valueError }}</p>
</div>
<el-button link type="danger" @click="removeCustomEntry(index)">
{{ t('pages.licenses.form.keyValue.remove') }}
</el-button>
</div>
<div v-if="!customEntries.length" class="key-value-empty">
{{ t('pages.licenses.form.keyValue.emptyState') }}
</div>
</div>
<div class="key-value-preview">
<div class="key-value-preview-label">
{{ t('pages.licenses.form.keyValue.preview') }}
</div>
<el-input type="textarea" :rows="3" :model-value="customJsonPreview" readonly />
</div>
<JsonEditor ref="customParamsRef" v-model="customParamsData" />
</div>
</el-form>
</div>
</div>
<el-dialog
v-model="importDialogVisible"
:title="importDialogTitle"
width="520px"
destroy-on-close
>
<p class="import-dialog-tip">
{{ t('pages.licenses.form.keyValue.importDescription') }}
</p>
<el-input
v-model="importDialogContent"
type="textarea"
:rows="8"
:placeholder="t('pages.licenses.form.keyValue.importPlaceholder')"
/>
<p v-if="importDialogError" class="key-value-error import-error">{{ importDialogError }}</p>
<template #footer>
<el-button @click="importDialogVisible = false">
{{ t('pages.licenses.form.keyValue.importCancel') }}
</el-button>
<el-button type="primary" @click="handleImportConfirm">
{{ t('pages.licenses.form.keyValue.importConfirm') }}
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
@@ -519,58 +248,34 @@ import {
} from '@/api/license'
import { getCustomers, type Customer } from '@/api/customer'
import { getEnumOptions, type RawEnumItem } from '@/api/enum'
type KeyValueType = 'string' | 'number' | 'bool'
interface KeyValueItem {
id: string
key: string
value: string
type: KeyValueType
keyError?: string
valueError?: string
}
const KEY_PATTERN = /^[a-z0-9-]{1,32}$/
import JsonEditor from '@/components/common/JsonEditor.vue'
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const boolOptions = computed(() => [
{ label: t('pages.licenses.form.keyValue.booleanTrue'), value: 'true' },
{ label: t('pages.licenses.form.keyValue.booleanFalse'), value: 'false' }
])
// 表单引用
const formRef = ref<FormInstance>()
// JsonEditor 组件引用
const featureConfigRef = ref<InstanceType<typeof JsonEditor>>()
const usageLimitsRef = ref<InstanceType<typeof JsonEditor>>()
const customParamsRef = ref<InstanceType<typeof JsonEditor>>()
// 响应式数据
const submitting = ref(false)
const customerLoading = ref(false)
const customerOptions = ref<Customer[]>([])
const featureConfigEntries = ref<KeyValueItem[]>([])
const limitEntries = ref<KeyValueItem[]>([])
const customEntries = ref<KeyValueItem[]>([])
const importDialogVisible = ref(false)
const importDialogTarget = ref<'feature' | 'limit' | 'custom'>('feature')
const importDialogTitle = computed(() => {
if (importDialogTarget.value === 'limit') {
return t('pages.licenses.form.keyValue.importTitleLimit')
}
if (importDialogTarget.value === 'custom') {
return t('pages.licenses.form.keyValue.importTitleCustom')
}
return t('pages.licenses.form.keyValue.importTitleFeature')
})
const importDialogContent = ref('')
const importDialogError = ref('')
// JsonEditor 数据
const featureConfigData = ref<Record<string, any> | null>(null)
const usageLimitsData = ref<Record<string, any> | null>(null)
const customParamsData = ref<Record<string, any> | null>(null)
// 枚举选项
const deploymentTypeOptions = ref<RawEnumItem[]>([])
const encryptionTypeOptions = ref<RawEnumItem[]>([])
const typeOptions: KeyValueType[] = ['string', 'number', 'bool']
// 表单数据
const formData = reactive<
AuthorizationCodeCreateRequest & {
@@ -594,10 +299,6 @@ const formData = reactive<
// 独立的响应式变量
const dateRange = ref<[string, string] | null>(null)
const featureConfigPreview = computed(() => buildJsonString(featureConfigEntries.value))
const usageJsonPreview = computed(() => buildJsonString(limitEntries.value))
const customJsonPreview = computed(() => buildJsonString(customEntries.value))
// 计算属性
const isEdit = computed(() => {
return route.name === 'licenses-edit' && route.params.id
@@ -744,277 +445,6 @@ const formRules: FormRules = {
]
}
const createEntryId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const createEmptyEntry = (): KeyValueItem => ({
id: createEntryId(),
key: '',
value: '',
type: 'string',
keyError: '',
valueError: ''
})
const convertValueByType = (value: string, type: KeyValueType) => {
if (type === 'number') {
const num = Number(value)
if (Number.isNaN(num)) {
throw new Error('invalid number')
}
return num
}
if (type === 'bool') {
return value === 'true'
}
return value
}
const buildJsonRecord = (entries: KeyValueItem[]) => {
if (!entries.length) {
return null
}
const result: Record<string, string | number | boolean> = {}
entries.forEach(item => {
const trimmedKey = item.key.trim()
if (trimmedKey) {
try {
result[trimmedKey] = convertValueByType(item.value, item.type)
} catch {
// 如果转换失败,则忽略该条目,等待校验提示
}
}
})
return Object.keys(result).length ? result : null
}
const buildJsonString = (entries: KeyValueItem[]) => {
const record = buildJsonRecord(entries)
return record ? JSON.stringify(record) : ''
}
const addFeatureConfigEntry = () => {
featureConfigEntries.value.push(createEmptyEntry())
}
const addLimitEntry = () => {
limitEntries.value.push(createEmptyEntry())
}
const addCustomEntry = () => {
customEntries.value.push(createEmptyEntry())
}
const removeFeatureConfigEntry = (index: number) => {
featureConfigEntries.value.splice(index, 1)
}
const removeLimitEntry = (index: number) => {
limitEntries.value.splice(index, 1)
}
const removeCustomEntry = (index: number) => {
customEntries.value.splice(index, 1)
}
const clearFeatureConfigEntries = () => {
featureConfigEntries.value = []
}
const clearLimitEntries = () => {
limitEntries.value = []
}
const clearCustomEntries = () => {
customEntries.value = []
}
const handleEntryTypeChange = (entry: KeyValueItem) => {
if (entry.type === 'bool') {
entry.value = entry.value === 'false' ? 'false' : 'true'
} else if (entry.type === 'number') {
if (entry.value && Number.isNaN(Number(entry.value))) {
entry.value = ''
}
}
entry.valueError = ''
}
const fallbackCopy = (value: string) => {
if (typeof document === 'undefined') {
throw new Error('document is not available')
}
const textarea = document.createElement('textarea')
textarea.value = value
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
const copyJson = async (value: string) => {
if (!value) {
ElMessage.info(t('pages.licenses.form.keyValue.copyEmpty'))
return
}
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(value)
} else {
fallbackCopy(value)
}
ElMessage.success(t('pages.licenses.form.keyValue.copySuccess'))
} catch (error) {
console.error('复制JSON失败:', error)
ElMessage.error(t('pages.licenses.form.keyValue.copyError'))
}
}
const validateKeyValueEntries = (entries: KeyValueItem[]) => {
let isValid = true
const existingKeys = new Set<string>()
entries.forEach(item => {
item.keyError = ''
item.valueError = ''
const trimmedKey = item.key.trim()
if (!trimmedKey) {
item.keyError = t('pages.licenses.form.keyValue.keyRequired')
isValid = false
} else if (!KEY_PATTERN.test(trimmedKey)) {
item.keyError = t('pages.licenses.form.keyValue.keyFormat')
isValid = false
} else if (existingKeys.has(trimmedKey)) {
item.keyError = t('pages.licenses.form.keyValue.keyDuplicate')
isValid = false
} else {
existingKeys.add(trimmedKey)
}
if (item.value.trim() === '') {
item.valueError = t('pages.licenses.form.keyValue.valueRequired')
isValid = false
} else if (item.type === 'number') {
const num = Number(item.value)
if (Number.isNaN(num)) {
item.valueError = t('pages.licenses.form.keyValue.numberRequired')
isValid = false
}
} else if (item.type === 'bool') {
if (item.value !== 'true' && item.value !== 'false') {
item.valueError = t('pages.licenses.form.keyValue.boolRequired')
isValid = false
}
}
})
return isValid
}
const openImportDialog = (target: 'feature' | 'limit' | 'custom') => {
importDialogTarget.value = target
importDialogContent.value =
target === 'feature'
? featureConfigPreview.value
: target === 'limit'
? usageJsonPreview.value
: customJsonPreview.value
importDialogError.value = ''
importDialogVisible.value = true
}
const handleImportConfirm = () => {
importDialogError.value = ''
try {
const content = importDialogContent.value.trim()
if (!content) {
if (importDialogTarget.value === 'feature') {
featureConfigEntries.value = []
} else if (importDialogTarget.value === 'limit') {
limitEntries.value = []
} else {
customEntries.value = []
}
} else {
const entries = parseJsonToEntries(content, { strict: true })
if (!entries.length) {
throw new Error(t('pages.licenses.form.keyValue.importEmpty'))
}
if (importDialogTarget.value === 'feature') {
featureConfigEntries.value = entries
} else if (importDialogTarget.value === 'limit') {
limitEntries.value = entries
} else {
customEntries.value = entries
}
}
importDialogVisible.value = false
ElMessage.success(t('pages.licenses.form.keyValue.importSuccess'))
} catch (error: any) {
importDialogError.value =
error?.message || t('pages.licenses.form.keyValue.importFailed')
}
}
const mapObjectToEntries = (data: Record<string, any>): KeyValueItem[] => {
return Object.entries(data).map(([key, value]) => {
let type: KeyValueType = 'string'
let normalizedValue = ''
if (typeof value === 'number') {
type = 'number'
normalizedValue = String(value)
} else if (typeof value === 'boolean') {
type = 'bool'
normalizedValue = value ? 'true' : 'false'
} else {
normalizedValue = value === undefined || value === null ? '' : String(value)
}
return {
id: createEntryId(),
key,
value: normalizedValue,
type,
keyError: '',
valueError: ''
}
})
}
const parseJsonToEntries = (raw: unknown, options?: { strict?: boolean }): KeyValueItem[] => {
if (!raw) return []
let parsed: unknown = raw
if (typeof raw === 'string') {
if (!raw.trim()) return []
try {
parsed = JSON.parse(raw)
} catch (error) {
if (options?.strict) {
throw error
}
console.warn('解析JSON失败:', error)
return []
}
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
if (options?.strict) {
throw new Error('JSON payload must be an object with key-value pairs')
}
return []
}
return mapObjectToEntries(parsed as Record<string, any>)
}
const hydrateEntriesFromField = (raw: unknown, target: Ref<KeyValueItem[]>) => {
target.value = parseJsonToEntries(raw)
}
// 方法
const loadEnumOptions = async () => {
try {
@@ -1201,9 +631,16 @@ const loadLicenseDetail = async () => {
]
}
hydrateEntriesFromField(data.feature_config, featureConfigEntries)
hydrateEntriesFromField(data.usage_limits, limitEntries)
hydrateEntriesFromField(data.custom_parameters, customEntries)
// 使用 JsonEditor 加载数据
featureConfigData.value = typeof data.feature_config === 'string'
? JSON.parse(data.feature_config || '{}')
: (data.feature_config || {})
usageLimitsData.value = typeof data.usage_limits === 'string'
? JSON.parse(data.usage_limits || '{}')
: (data.usage_limits || {})
customParamsData.value = typeof data.custom_parameters === 'string'
? JSON.parse(data.custom_parameters || '{}')
: (data.custom_parameters || {})
} else {
throw new Error(response.message || t('pages.licenses.form.messages.loadDetailError'))
}
@@ -1226,17 +663,18 @@ const handleSubmit = async () => {
// 执行表单验证
await formRef.value.validate()
const featureConfigValid = validateKeyValueEntries(featureConfigEntries.value)
const limitValid = validateKeyValueEntries(limitEntries.value)
const customValid = validateKeyValueEntries(customEntries.value)
if (!featureConfigValid || !limitValid || !customValid) {
// 验证 JsonEditor 数据
const featureValid = featureConfigRef.value?.validate() ?? true
const limitValid = usageLimitsRef.value?.validate() ?? true
const customValid = customParamsRef.value?.validate() ?? true
if (!featureValid || !limitValid || !customValid) {
ElMessage.error(t('pages.licenses.form.keyValue.validationFailed'))
return
}
formData.feature_config = buildJsonRecord(featureConfigEntries.value) || {}
formData.usage_limits = usageJsonPreview.value
formData.custom_parameters = customJsonPreview.value
formData.feature_config = featureConfigData.value || {}
formData.usage_limits = JSON.stringify(usageLimitsData.value || {})
formData.custom_parameters = JSON.stringify(customParamsData.value || {})
submitting.value = true
@@ -1325,30 +763,6 @@ watch(
}
)
watch(
limitEntries,
() => {
formData.usage_limits = usageJsonPreview.value
},
{ deep: true }
)
watch(
featureConfigEntries,
() => {
formData.feature_config = buildJsonRecord(featureConfigEntries.value) || {}
},
{ deep: true }
)
watch(
customEntries,
() => {
formData.custom_parameters = customJsonPreview.value
},
{ deep: true }
)
// 生命周期
onMounted(async () => {
await loadEnumOptions()
@@ -176,6 +176,9 @@ const parseKeyValueData = (source: unknown): KeyValueItem[] => {
} else if (typeof value === 'boolean') {
type = 'bool'
normalizedValue = value ? 'true' : 'false'
} else if (typeof value === 'object' && value !== null) {
// 对象或数组用 JSON.stringify 处理
normalizedValue = JSON.stringify(value)
} else {
normalizedValue = value === undefined || value === null ? '' : String(value)
}
@@ -138,51 +138,15 @@
</el-form-item>
<el-form-item :label="t('pages.licenses.detail.updateDialog.fields.featureConfig')">
<div class="kv-entries">
<div v-for="(entry, idx) in featureConfigEntries" :key="entry.id" class="kv-row">
<el-input v-model="entry.key" placeholder="key" style="width:40%" />
<el-select v-model="entry.type" placeholder="type" style="width:14%;margin:0 8px">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="bool" value="bool" />
</el-select>
<el-input v-model="entry.value" placeholder="value" style="width:40%" />
<el-button type="text" icon="el-icon-delete" @click="removeFeatureConfigEntry(idx)" />
</div>
<el-button size="small" @click="addFeatureConfigEntry">{{ t('pages.licenses.form.keyValue.addItem') }}</el-button>
</div>
<JsonEditor ref="featureConfigRef" v-model="featureConfigData" />
</el-form-item>
<el-form-item :label="t('pages.licenses.detail.updateDialog.fields.usageLimits')">
<div class="kv-entries">
<div v-for="(entry, idx) in usageLimitsEntries" :key="entry.id" class="kv-row">
<el-input v-model="entry.key" placeholder="key" style="width:40%" />
<el-select v-model="entry.type" placeholder="type" style="width:14%;margin:0 8px">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="bool" value="bool" />
</el-select>
<el-input v-model="entry.value" placeholder="value" style="width:40%" />
<el-button type="text" icon="el-icon-delete" @click="removeUsageLimitEntry(idx)" />
</div>
<el-button size="small" @click="addUsageLimitEntry">{{ t('pages.licenses.form.keyValue.addItem') }}</el-button>
</div>
<JsonEditor ref="usageLimitsRef" v-model="usageLimitsData" />
</el-form-item>
<el-form-item :label="t('pages.licenses.detail.updateDialog.fields.customParameters')">
<div class="kv-entries">
<div v-for="(entry, idx) in customParametersEntries" :key="entry.id" class="kv-row">
<el-input v-model="entry.key" placeholder="key" style="width:40%" />
<el-select v-model="entry.type" placeholder="type" style="width:14%;margin:0 8px">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="bool" value="bool" />
</el-select>
<el-input v-model="entry.value" placeholder="value" style="width:40%" />
<el-button type="text" icon="el-icon-delete" @click="removeCustomParamEntry(idx)" />
</div>
<el-button size="small" @click="addCustomParamEntry">{{ t('pages.licenses.form.keyValue.addItem') }}</el-button>
</div>
<JsonEditor ref="customParamsRef" v-model="customParamsData" />
</el-form-item>
<el-form-item :label="t('pages.licenses.detail.updateDialog.fields.reason')">
@@ -223,6 +187,7 @@ import BasicInfo from './components/BasicInfo.vue'
import AuthorizationInfo from './components/AuthorizationInfo.vue'
import LicenseInfo from './components/LicenseInfo.vue'
import ChangeHistory from './components/ChangeHistory.vue'
import JsonEditor from '@/components/common/JsonEditor.vue'
const router = useRouter()
const route = useRoute()
@@ -385,12 +350,10 @@ const handleConfirmChangeValidity = async () => {
if (updateMaxActivations.value !== null && updateMaxActivations.value !== undefined) {
payload.max_activations = updateMaxActivations.value
}
const fc = buildJsonRecord(featureConfigEntries.value)
if (fc) payload.feature_config = fc
const ul = buildJsonRecord(usageLimitsEntries.value)
if (ul) payload.usage_limits = ul
const cp = buildJsonRecord(customParametersEntries.value)
if (cp) payload.custom_parameters = cp
// 使用 JsonEditor 数据
payload.feature_config = featureConfigData.value || {}
payload.usage_limits = usageLimitsData.value || {}
payload.custom_parameters = customParamsData.value || {}
payload.change_type = updateChangeType.value || payload.change_type
// if reason in update dialog provided, override
if (updateReason.value && updateReason.value.trim()) {
@@ -417,121 +380,37 @@ const handleConfirmChangeValidity = async () => {
// --- update authorization dialog ---
import type { RawEnumItem } from '@/api/enum'
type KeyValueType = 'string' | 'number' | 'bool'
interface KeyValueItem {
id: string
key: string
value: string
type: KeyValueType
}
const updateDialogVisible = ref(false)
const changeTypeOptions = ref<RawEnumItem[]>([])
const updateChangeType = ref<string>('other')
const updateMaxActivations = ref<number | null>(null)
const featureConfigEntries = ref<KeyValueItem[]>([])
const usageLimitsEntries = ref<KeyValueItem[]>([])
const customParametersEntries = ref<KeyValueItem[]>([])
const featureConfigData = ref<Record<string, any>>({})
const usageLimitsData = ref<Record<string, any>>({})
const customParamsData = ref<Record<string, any>>({})
const updateReason = ref<string>('')
const updateSubmitting = ref(false)
const createEntryId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const createEmptyEntry = (): KeyValueItem => ({
id: createEntryId(),
key: '',
value: '',
type: 'string'
})
const addFeatureConfigEntry = () => featureConfigEntries.value.push(createEmptyEntry())
const addUsageLimitEntry = () => usageLimitsEntries.value.push(createEmptyEntry())
const addCustomParamEntry = () => customParametersEntries.value.push(createEmptyEntry())
const removeFeatureConfigEntry = (i: number) => featureConfigEntries.value.splice(i, 1)
const removeUsageLimitEntry = (i: number) => usageLimitsEntries.value.splice(i, 1)
const removeCustomParamEntry = (i: number) => customParametersEntries.value.splice(i, 1)
const convertValueByType = (value: string, type: KeyValueType) => {
if (type === 'number') {
const num = Number(value)
if (Number.isNaN(num)) throw new Error('invalid number')
return num
}
if (type === 'bool') return value === 'true'
return value
}
const buildJsonRecord = (entries: KeyValueItem[]) => {
if (!entries.length) return null
const result: Record<string, any> = {}
entries.forEach(item => {
const k = item.key.trim()
if (!k) return
try {
result[k] = convertValueByType(item.value, item.type)
} catch {
// ignore here; validation will surface
}
})
return Object.keys(result).length ? result : null
}
// Validate key-value entries: ensure key present and value matches selected type
const validateKeyValueEntries = (entries: KeyValueItem[]): { ok: boolean; msg?: string } => {
for (const item of entries) {
const key = item.key?.trim()
const val = item.value
if (!key) {
return { ok: false, msg: t('pages.licenses.form.keyValue.keyRequired') }
}
if (item.type === 'number') {
if (val === '' || val === null || val === undefined || Number.isNaN(Number(val))) {
return { ok: false, msg: t('pages.licenses.form.keyValue.numberRequired') }
}
}
if (item.type === 'bool') {
const lower = String(val).toLowerCase()
if (lower !== 'true' && lower !== 'false') {
return { ok: false, msg: t('pages.licenses.form.keyValue.boolRequired') }
}
}
}
return { ok: true }
}
// JsonEditor 组件引用
const featureConfigRef = ref<InstanceType<typeof JsonEditor>>()
const usageLimitsRef = ref<InstanceType<typeof JsonEditor>>()
const customParamsRef = ref<InstanceType<typeof JsonEditor>>()
const openUpdateDialog = async () => {
updateChangeType.value = 'other'
updateReason.value = ''
updateMaxActivations.value = licenseData.value?.max_activations || null
// populate entries from existing JSON if present
featureConfigEntries.value = []
usageLimitsEntries.value = []
customParametersEntries.value = []
try {
if (licenseData.value?.feature_config) {
const obj = typeof licenseData.value.feature_config === 'string' ? JSON.parse(licenseData.value.feature_config) : licenseData.value.feature_config
for (const [k, v] of Object.entries(obj || {})) {
featureConfigEntries.value.push({ id: createEntryId(), key: k, value: String(v), type: typeof v === 'number' ? 'number' : typeof v === 'boolean' ? 'bool' : 'string' })
}
}
} catch {
// ignore parse errors
}
try {
if (licenseData.value?.usage_limits) {
const obj = typeof licenseData.value.usage_limits === 'string' ? JSON.parse(licenseData.value.usage_limits) : licenseData.value.usage_limits
for (const [k, v] of Object.entries(obj || {})) {
usageLimitsEntries.value.push({ id: createEntryId(), key: k, value: String(v), type: typeof v === 'number' ? 'number' : typeof v === 'boolean' ? 'bool' : 'string' })
}
}
} catch {}
try {
if (licenseData.value?.custom_parameters) {
const obj = typeof licenseData.value.custom_parameters === 'string' ? JSON.parse(licenseData.value.custom_parameters) : licenseData.value.custom_parameters
for (const [k, v] of Object.entries(obj || {})) {
customParametersEntries.value.push({ id: createEntryId(), key: k, value: String(v), type: typeof v === 'number' ? 'number' : typeof v === 'boolean' ? 'bool' : 'string' })
}
}
} catch {}
// 使用 JsonEditor 加载数据
featureConfigData.value = typeof licenseData.value?.feature_config === 'string'
? JSON.parse(licenseData.value?.feature_config || '{}')
: (licenseData.value?.feature_config || {})
usageLimitsData.value = typeof licenseData.value?.usage_limits === 'string'
? JSON.parse(licenseData.value?.usage_limits || '{}')
: (licenseData.value?.usage_limits || {})
customParamsData.value = typeof licenseData.value?.custom_parameters === 'string'
? JSON.parse(licenseData.value?.custom_parameters || '{}')
: (licenseData.value?.custom_parameters || {})
// load change type options from backend enum
try {
@@ -563,7 +442,17 @@ const handleConfirmUpdate = async () => {
return
}
// build payload
// 验证 JsonEditor 数据
const featureValid = featureConfigRef.value?.validate() ?? true
const limitValid = usageLimitsRef.value?.validate() ?? true
const customValid = customParamsRef.value?.validate() ?? true
if (!featureValid || !limitValid || !customValid) {
ElMessage.error(t('pages.licenses.form.keyValue.validationFailed'))
changeSubmitting.value = false
return
}
// 构建 payload
const payload: any = {
change_type: updateChangeType.value,
reason: updateReason.value
@@ -572,32 +461,10 @@ const handleConfirmUpdate = async () => {
payload.max_activations = updateMaxActivations.value
}
// build JSON object from key-value entries
const fc = buildJsonRecord(featureConfigEntries.value)
if (fc) payload.feature_config = fc
const ul = buildJsonRecord(usageLimitsEntries.value)
if (ul) payload.usage_limits = ul
const cp = buildJsonRecord(customParametersEntries.value)
if (cp) payload.custom_parameters = cp
// validate KV entries before sending
const v1 = validateKeyValueEntries(featureConfigEntries.value)
if (!v1.ok) {
ElMessage.error(v1.msg || t('pages.licenses.detail.updateDialog.validation.featureConfigInvalid'))
changeSubmitting.value = false
return
}
const v2 = validateKeyValueEntries(usageLimitsEntries.value)
if (!v2.ok) {
ElMessage.error(v2.msg || t('pages.licenses.detail.updateDialog.validation.usageLimitsInvalid'))
changeSubmitting.value = false
return
}
const v3 = validateKeyValueEntries(customParametersEntries.value)
if (!v3.ok) {
ElMessage.error(v3.msg || t('pages.licenses.detail.updateDialog.validation.customParametersInvalid'))
changeSubmitting.value = false
return
}
// 使用 JsonEditor 数据
payload.feature_config = featureConfigData.value || {}
payload.usage_limits = usageLimitsData.value || {}
payload.custom_parameters = customParamsData.value || {}
updateSubmitting.value = true
try {
+285 -51
View File
@@ -42,16 +42,16 @@
resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz"
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
"@esbuild/android-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz"
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
"@esbuild/android-arm@0.21.5":
version "0.21.5"
resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz"
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
"@esbuild/android-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz"
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
"@esbuild/android-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz"
@@ -77,16 +77,16 @@
resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz"
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
"@esbuild/linux-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz"
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
"@esbuild/linux-arm@0.21.5":
version "0.21.5"
resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz"
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
"@esbuild/linux-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz"
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
"@esbuild/linux-ia32@0.21.5":
version "0.21.5"
resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz"
@@ -257,6 +257,14 @@
"@jridgewell/sourcemap-codec" "^1.5.0"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/remapping@^2.3.5":
version "2.3.5"
resolved "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz"
integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
@@ -291,7 +299,7 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@@ -508,12 +516,12 @@
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.0.tgz"
integrity sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==
"@types/estree@1.0.8", "@types/estree@^1.0.0":
"@types/estree@^1.0.0", "@types/estree@1.0.8":
version "1.0.8"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/lodash-es@^4.17.6":
"@types/lodash-es@*", "@types/lodash-es@^4.17.6":
version "4.17.12"
resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz"
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
@@ -525,7 +533,7 @@
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz"
integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==
"@types/node@^22.0.0":
"@types/node@^18.0.0 || >=20.0.0", "@types/node@^22.0.0":
version "22.18.0"
resolved "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz"
integrity sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==
@@ -567,7 +575,7 @@
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@^7.1.1":
"@typescript-eslint/parser@^7.0.0", "@typescript-eslint/parser@^7.1.1":
version "7.18.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz"
integrity sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==
@@ -578,7 +586,7 @@
"@typescript-eslint/visitor-keys" "7.18.0"
debug "^4.3.4"
"@typescript-eslint/parser@^8.0.0":
"@typescript-eslint/parser@^8.0.0", "@typescript-eslint/parser@^8.41.0":
version "8.41.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.41.0.tgz"
integrity sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==
@@ -614,7 +622,7 @@
"@typescript-eslint/types" "8.41.0"
"@typescript-eslint/visitor-keys" "8.41.0"
"@typescript-eslint/tsconfig-utils@8.41.0", "@typescript-eslint/tsconfig-utils@^8.41.0":
"@typescript-eslint/tsconfig-utils@^8.41.0", "@typescript-eslint/tsconfig-utils@8.41.0":
version "8.41.0"
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz"
integrity sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==
@@ -640,16 +648,16 @@
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@^8.41.0", "@typescript-eslint/types@8.41.0":
version "8.41.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz"
integrity sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==
"@typescript-eslint/types@7.18.0":
version "7.18.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz"
integrity sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==
"@typescript-eslint/types@8.41.0", "@typescript-eslint/types@^8.41.0":
version "8.41.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz"
integrity sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==
"@typescript-eslint/typescript-estree@7.18.0":
version "7.18.0"
resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz"
@@ -745,7 +753,7 @@
estree-walker "^3.0.3"
magic-string "^0.30.12"
"@vitest/pretty-format@2.1.9", "@vitest/pretty-format@^2.1.9":
"@vitest/pretty-format@^2.1.9", "@vitest/pretty-format@2.1.9":
version "2.1.9"
resolved "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz"
integrity sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==
@@ -817,7 +825,7 @@
estree-walker "^2.0.2"
source-map-js "^1.2.1"
"@vue/compiler-dom@3.5.20", "@vue/compiler-dom@^3.5.0":
"@vue/compiler-dom@^3.5.0", "@vue/compiler-dom@3.5.20":
version "3.5.20"
resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.20.tgz"
integrity sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ==
@@ -899,7 +907,7 @@
dependencies:
"@vue/shared" "3.5.20"
"@vue/runtime-core@3.5.20":
"@vue/runtime-core@^3.0.0", "@vue/runtime-core@3.5.20":
version "3.5.20"
resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.20.tgz"
integrity sha512-vyQRiH5uSZlOa+4I/t4Qw/SsD/gbth0SW2J7oMeVlMFMAmsG1rwDD6ok0VMmjXY3eI0iHNSSOBilEDW98PLRKw==
@@ -925,12 +933,12 @@
"@vue/compiler-ssr" "3.5.20"
"@vue/shared" "3.5.20"
"@vue/shared@3.5.20", "@vue/shared@^3.5.0":
"@vue/shared@^3.5.0", "@vue/shared@3.5.20":
version "3.5.20"
resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.5.20.tgz"
integrity sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==
"@vueuse/core@^9.1.0":
"@vueuse/core@*", "@vueuse/core@^9.1.0":
version "9.13.0"
resolved "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz"
integrity sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==
@@ -957,10 +965,10 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn@^8.14.0, acorn@^8.15.0, acorn@^8.9.0:
version "8.15.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.14.0, acorn@^8.15.0, acorn@^8.16.0, acorn@^8.9.0:
version "8.16.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz"
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
ajv@^6.12.4:
version "6.12.6"
@@ -1109,6 +1117,13 @@ chokidar@^4.0.0:
dependencies:
readdirp "^4.0.1"
chokidar@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz"
integrity sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==
dependencies:
readdirp "^5.0.0"
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
@@ -1147,6 +1162,16 @@ concat-map@0.0.1:
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
confbox@^0.1.8:
version "0.1.8"
resolved "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz"
integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==
confbox@^0.2.2:
version "0.2.4"
resolved "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz"
integrity sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==
cross-spawn@^7.0.2:
version "7.0.6"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz"
@@ -1231,7 +1256,7 @@ dunder-proto@^1.0.1:
es-errors "^1.3.0"
gopd "^1.2.0"
echarts@^5.6.0:
echarts@^5.5.1, echarts@^5.6.0:
version "5.6.0"
resolved "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz"
integrity sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==
@@ -1346,7 +1371,12 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-prettier@^9.0.0:
escape-string-regexp@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz"
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
eslint-config-prettier@^9.0.0, "eslint-config-prettier@>= 7.0.0 <10.0.0 || >=10.1.0":
version "9.1.2"
resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz"
integrity sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==
@@ -1399,7 +1429,7 @@ eslint-visitor-keys@^4.2.0, eslint-visitor-keys@^4.2.1:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
eslint@^8.57.1:
"eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", eslint@^8.56.0, "eslint@^8.57.0 || ^9.0.0", eslint@^8.57.1, "eslint@>= 8.0.0", eslint@>=6.0.0, eslint@>=7.0.0, eslint@>=8.0.0:
version "8.57.1"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz"
integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
@@ -1502,6 +1532,11 @@ expect-type@^1.1.0:
resolved "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz"
integrity sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==
exsolve@^1.0.7:
version "1.0.8"
resolved "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz"
integrity sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
@@ -1540,6 +1575,11 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
fdir@^6.5.0:
version "6.5.0"
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
@@ -1734,7 +1774,12 @@ he@^1.2.0:
resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
ignore@^5.2.0, ignore@^5.3.1:
ignore@^5.2.0:
version "5.3.2"
resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
ignore@^5.3.1:
version "5.3.2"
resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
@@ -1819,6 +1864,11 @@ isexe@^2.0.0:
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
js-tokens@^9.0.1:
version "9.0.1"
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz"
integrity sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz"
@@ -1865,6 +1915,15 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
local-pkg@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz"
integrity sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==
dependencies:
mlly "^1.7.4"
pkg-types "^2.3.0"
quansync "^0.2.11"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz"
@@ -1872,7 +1931,7 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-es@^4.17.21:
lodash-es@*, lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
@@ -1887,7 +1946,7 @@ lodash.merge@^4.6.2:
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash@^4.17.21:
lodash@*, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -1897,10 +1956,10 @@ loupe@^3.1.0, loupe@^3.1.2:
resolved "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz"
integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==
magic-string@^0.30.12, magic-string@^0.30.17:
version "0.30.18"
resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz"
integrity sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==
magic-string@^0.30.12, magic-string@^0.30.17, magic-string@^0.30.21:
version "0.30.21"
resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz"
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
@@ -1939,7 +1998,21 @@ mime-types@^2.1.12:
dependencies:
mime-db "1.52.0"
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
minimatch@^3.0.5:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimatch@^3.1.1:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@@ -1953,6 +2026,16 @@ minimatch@^9.0.3, minimatch@^9.0.4:
dependencies:
brace-expansion "^2.0.1"
mlly@^1.7.4, mlly@^1.8.0:
version "1.8.1"
resolved "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz"
integrity sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==
dependencies:
acorn "^8.16.0"
pathe "^2.0.3"
pkg-types "^1.3.1"
ufo "^1.6.3"
ms@^2.1.3:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
@@ -1990,6 +2073,11 @@ nth-check@^2.1.1:
dependencies:
boolbase "^1.0.0"
obug@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz"
integrity sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
@@ -2069,6 +2157,11 @@ pathe@^1.1.2:
resolved "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz"
integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==
pathe@^2.0.1, pathe@^2.0.3:
version "2.0.3"
resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz"
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
pathval@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz"
@@ -2084,6 +2177,11 @@ picomatch@^2.3.1:
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
"picomatch@^3 || ^4", picomatch@^4.0.3:
version "4.0.3"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
picomatch@^4.0.2:
version "4.0.3"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
@@ -2097,6 +2195,24 @@ pinia@^2.3.1:
"@vue/devtools-api" "^6.6.3"
vue-demi "^0.14.10"
pkg-types@^1.3.1:
version "1.3.1"
resolved "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz"
integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==
dependencies:
confbox "^0.1.8"
mlly "^1.7.4"
pathe "^2.0.1"
pkg-types@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz"
integrity sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==
dependencies:
confbox "^0.2.2"
exsolve "^1.0.7"
pathe "^2.0.3"
postcss-selector-parser@^6.0.15:
version "6.1.2"
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz"
@@ -2126,7 +2242,7 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@^3.0.0:
prettier@^3.0.0, "prettier@>= 3.0.0", prettier@>=3.0.0:
version "3.6.2"
resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz"
integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
@@ -2141,6 +2257,11 @@ punycode@^2.1.0:
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
quansync@^0.2.11:
version "0.2.11"
resolved "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz"
integrity sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
@@ -2151,6 +2272,11 @@ readdirp@^4.0.1:
resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz"
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
readdirp@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz"
integrity sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
@@ -2183,7 +2309,7 @@ rollup-plugin-visualizer@^5.12.0:
source-map "^0.7.4"
yargs "^17.5.1"
rollup@^4.20.0:
rollup@^4.20.0, "rollup@2.x || 3.x || 4.x":
version "4.50.0"
resolved "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz"
integrity sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==
@@ -2220,7 +2346,7 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
sass@^1.89.2:
sass@*, sass@^1.89.2:
version "1.91.0"
resolved "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz"
integrity sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==
@@ -2231,6 +2357,11 @@ sass@^1.89.2:
optionalDependencies:
"@parcel/watcher" "^2.4.1"
scule@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz"
integrity sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==
semver@^7.3.6, semver@^7.6.0, semver@^7.6.3:
version "7.7.2"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
@@ -2258,7 +2389,7 @@ slash@^3.0.0:
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2, source-map-js@^1.2.1:
source-map-js@^1.0.2, source-map-js@^1.2.1, "source-map-js@>=0.6.2 <2.0.0":
version "1.2.1"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
@@ -2312,6 +2443,13 @@ strip-json-comments@^3.1.1:
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
strip-literal@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz"
integrity sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==
dependencies:
js-tokens "^9.0.1"
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"
@@ -2326,7 +2464,7 @@ synckit@^0.11.7:
dependencies:
"@pkgr/core" "^0.2.9"
terser@^5.43.1:
terser@^5.4.0, terser@^5.43.1:
version "5.43.1"
resolved "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz"
integrity sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==
@@ -2351,6 +2489,14 @@ tinyexec@^0.3.1:
resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz"
integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==
tinyglobby@^0.2.15:
version "0.2.15"
resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz"
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
dependencies:
fdir "^6.5.0"
picomatch "^4.0.3"
tinypool@^1.0.1:
version "1.1.1"
resolved "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz"
@@ -2400,21 +2546,91 @@ type-fest@^0.20.2:
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
typescript@^5.8.3:
typescript@*, typescript@^5.8.3, typescript@>=4.2.0, typescript@>=4.4.4, typescript@>=4.7.4, typescript@>=4.8.4, "typescript@>=4.8.4 <6.0.0", typescript@>=5.0.0:
version "5.9.2"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz"
integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==
ufo@^1.6.3:
version "1.6.3"
resolved "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz"
integrity sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==
undici-types@~6.21.0:
version "6.21.0"
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz"
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
unimport@^5.6.0:
version "5.7.0"
resolved "https://registry.npmjs.org/unimport/-/unimport-5.7.0.tgz"
integrity sha512-njnL6sp8lEA8QQbZrt+52p/g4X0rw3bnGGmUcJnt1jeG8+iiqO779aGz0PirCtydAIVcuTBRlJ52F0u46z309Q==
dependencies:
acorn "^8.16.0"
escape-string-regexp "^5.0.0"
estree-walker "^3.0.3"
local-pkg "^1.1.2"
magic-string "^0.30.21"
mlly "^1.8.0"
pathe "^2.0.3"
picomatch "^4.0.3"
pkg-types "^2.3.0"
scule "^1.3.0"
strip-literal "^3.1.0"
tinyglobby "^0.2.15"
unplugin "^2.3.11"
unplugin-utils "^0.3.1"
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
unplugin-auto-import@^21.0.0:
version "21.0.0"
resolved "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-21.0.0.tgz"
integrity sha512-vWuC8SwqJmxZFYwPojhOhOXDb5xFhNNcEVb9K/RFkyk/3VnfaOjzitWN7v+8DEKpMjSsY2AEGXNgt6I0yQrhRQ==
dependencies:
local-pkg "^1.1.2"
magic-string "^0.30.21"
picomatch "^4.0.3"
unimport "^5.6.0"
unplugin "^2.3.11"
unplugin-utils "^0.3.1"
unplugin-utils@^0.3.1:
version "0.3.1"
resolved "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz"
integrity sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==
dependencies:
pathe "^2.0.3"
picomatch "^4.0.3"
unplugin-vue-components@^31.0.0:
version "31.0.0"
resolved "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-31.0.0.tgz"
integrity sha512-4ULwfTZTLuWJ7+S9P7TrcStYLsSRkk6vy2jt/WTfgUEUb0nW9//xxmrfhyHUEVpZ2UKRRwfRb8Yy15PDbVZf+Q==
dependencies:
chokidar "^5.0.0"
local-pkg "^1.1.2"
magic-string "^0.30.21"
mlly "^1.8.0"
obug "^2.1.1"
picomatch "^4.0.3"
tinyglobby "^0.2.15"
unplugin "^2.3.11"
unplugin-utils "^0.3.1"
unplugin@^2.3.11:
version "2.3.11"
resolved "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz"
integrity sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==
dependencies:
"@jridgewell/remapping" "^2.3.5"
acorn "^8.15.0"
picomatch "^4.0.3"
webpack-virtual-modules "^0.6.2"
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"
@@ -2447,7 +2663,7 @@ vite-plugin-compression@^0.5.1:
debug "^4.3.3"
fs-extra "^10.0.0"
vite@^5.0.0:
vite@^5.0.0, "vite@^5.0.0 || ^6.0.0", vite@>=2.0.0:
version "5.4.19"
resolved "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz"
integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==
@@ -2518,7 +2734,20 @@ vue-eslint-parser@^10.0.0:
esquery "^1.6.0"
semver "^7.6.3"
vue-eslint-parser@^9.3.1, vue-eslint-parser@^9.4.3:
vue-eslint-parser@^9.3.1:
version "9.4.3"
resolved "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz"
integrity sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==
dependencies:
debug "^4.3.4"
eslint-scope "^7.1.1"
eslint-visitor-keys "^3.3.0"
espree "^9.3.1"
esquery "^1.4.0"
lodash "^4.17.21"
semver "^7.3.6"
vue-eslint-parser@^9.4.3:
version "9.4.3"
resolved "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz"
integrity sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==
@@ -2555,7 +2784,7 @@ vue-tsc@^2.0.0:
"@volar/typescript" "2.4.15"
"@vue/language-core" "2.2.12"
vue@^3.5.0:
"vue@^2.7.0 || ^3.1.1", "vue@^2.7.0 || ^3.5.11", vue@^3.0.0, "vue@^3.0.0-0 || ^2.6.0", vue@^3.2.0, vue@^3.2.25, vue@^3.5.0, vue@3.5.20:
version "3.5.20"
resolved "https://registry.npmjs.org/vue/-/vue-3.5.20.tgz"
integrity sha512-2sBz0x/wis5TkF1XZ2vH25zWq3G1bFEPOfkBcx2ikowmphoQsPH6X0V3mmPCXA2K1N/XGTnifVyDQP4GfDDeQw==
@@ -2566,6 +2795,11 @@ vue@^3.5.0:
"@vue/server-renderer" "3.5.20"
"@vue/shared" "3.5.20"
webpack-virtual-modules@^0.6.2:
version "0.6.2"
resolved "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
which@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"