mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-02 15:52:26 +08:00
feat: 实现邮箱验证注册功能
添加完整的邮箱验证注册系统,包括验证码发送、验证和限流控制: - 新增邮箱验证服务模块(email_sender, email_template, email_verification) - 更新认证API支持邮箱验证注册流程 - 添加注册对话框和验证码输入组件 - 完善IP限流器支持邮箱验证场景 - 修复前端类型定义问题,升级esbuild依赖 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
522
frontend/package-lock.json
generated
522
frontend/package-lock.json
generated
@@ -262,6 +262,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -305,6 +306,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -316,9 +318,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
|
||||
"integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -333,9 +335,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
|
||||
"integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -350,9 +352,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -367,9 +369,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -384,9 +386,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -401,9 +403,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -418,9 +420,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -435,9 +437,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -452,9 +454,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
|
||||
"integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -469,9 +471,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -486,9 +488,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
|
||||
"integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -503,9 +505,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
|
||||
"integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
|
||||
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -520,9 +522,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
|
||||
"integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
|
||||
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -537,9 +539,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
|
||||
"integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -554,9 +556,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
|
||||
"integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
|
||||
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -571,9 +573,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
|
||||
"integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
|
||||
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -588,9 +590,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -605,9 +607,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -622,9 +624,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -639,9 +641,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -656,9 +658,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -673,9 +675,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -690,9 +692,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -707,9 +709,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -724,9 +726,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
|
||||
"integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -741,9 +743,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1598,6 +1600,7 @@
|
||||
"integrity": "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
@@ -1676,6 +1679,7 @@
|
||||
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.49.0",
|
||||
"@typescript-eslint/types": "8.49.0",
|
||||
@@ -2004,6 +2008,7 @@
|
||||
"integrity": "sha512-oWtNM89Np+YsQO3ttT5i1Aer/0xbzQzp66NzuJn/U16bB7MnvSzdLKXgk1kkMLYyKSSzA2ajzqMkYheaE9opuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.10",
|
||||
"fflate": "^0.8.2",
|
||||
@@ -2301,6 +2306,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2602,6 +2608,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.2",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -2718,6 +2725,7 @@
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
|
||||
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
@@ -2940,6 +2948,7 @@
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
@@ -2999,18 +3008,6 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@@ -3134,9 +3131,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
|
||||
"integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -3147,32 +3144,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.9",
|
||||
"@esbuild/android-arm": "0.25.9",
|
||||
"@esbuild/android-arm64": "0.25.9",
|
||||
"@esbuild/android-x64": "0.25.9",
|
||||
"@esbuild/darwin-arm64": "0.25.9",
|
||||
"@esbuild/darwin-x64": "0.25.9",
|
||||
"@esbuild/freebsd-arm64": "0.25.9",
|
||||
"@esbuild/freebsd-x64": "0.25.9",
|
||||
"@esbuild/linux-arm": "0.25.9",
|
||||
"@esbuild/linux-arm64": "0.25.9",
|
||||
"@esbuild/linux-ia32": "0.25.9",
|
||||
"@esbuild/linux-loong64": "0.25.9",
|
||||
"@esbuild/linux-mips64el": "0.25.9",
|
||||
"@esbuild/linux-ppc64": "0.25.9",
|
||||
"@esbuild/linux-riscv64": "0.25.9",
|
||||
"@esbuild/linux-s390x": "0.25.9",
|
||||
"@esbuild/linux-x64": "0.25.9",
|
||||
"@esbuild/netbsd-arm64": "0.25.9",
|
||||
"@esbuild/netbsd-x64": "0.25.9",
|
||||
"@esbuild/openbsd-arm64": "0.25.9",
|
||||
"@esbuild/openbsd-x64": "0.25.9",
|
||||
"@esbuild/openharmony-arm64": "0.25.9",
|
||||
"@esbuild/sunos-x64": "0.25.9",
|
||||
"@esbuild/win32-arm64": "0.25.9",
|
||||
"@esbuild/win32-ia32": "0.25.9",
|
||||
"@esbuild/win32-x64": "0.25.9"
|
||||
"@esbuild/aix-ppc64": "0.27.2",
|
||||
"@esbuild/android-arm": "0.27.2",
|
||||
"@esbuild/android-arm64": "0.27.2",
|
||||
"@esbuild/android-x64": "0.27.2",
|
||||
"@esbuild/darwin-arm64": "0.27.2",
|
||||
"@esbuild/darwin-x64": "0.27.2",
|
||||
"@esbuild/freebsd-arm64": "0.27.2",
|
||||
"@esbuild/freebsd-x64": "0.27.2",
|
||||
"@esbuild/linux-arm": "0.27.2",
|
||||
"@esbuild/linux-arm64": "0.27.2",
|
||||
"@esbuild/linux-ia32": "0.27.2",
|
||||
"@esbuild/linux-loong64": "0.27.2",
|
||||
"@esbuild/linux-mips64el": "0.27.2",
|
||||
"@esbuild/linux-ppc64": "0.27.2",
|
||||
"@esbuild/linux-riscv64": "0.27.2",
|
||||
"@esbuild/linux-s390x": "0.27.2",
|
||||
"@esbuild/linux-x64": "0.27.2",
|
||||
"@esbuild/netbsd-arm64": "0.27.2",
|
||||
"@esbuild/netbsd-x64": "0.27.2",
|
||||
"@esbuild/openbsd-arm64": "0.27.2",
|
||||
"@esbuild/openbsd-x64": "0.27.2",
|
||||
"@esbuild/openharmony-arm64": "0.27.2",
|
||||
"@esbuild/sunos-x64": "0.27.2",
|
||||
"@esbuild/win32-arm64": "0.27.2",
|
||||
"@esbuild/win32-ia32": "0.27.2",
|
||||
"@esbuild/win32-x64": "0.27.2"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@@ -3204,6 +3201,7 @@
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3747,9 +3745,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -4084,18 +4082,6 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
@@ -4115,6 +4101,7 @@
|
||||
"integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.23",
|
||||
"@asamuzakjp/dom-selector": "^6.7.4",
|
||||
@@ -4194,257 +4181,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-darwin-arm64": "1.30.1",
|
||||
"lightningcss-darwin-x64": "1.30.1",
|
||||
"lightningcss-freebsd-x64": "1.30.1",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.30.1",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.1",
|
||||
"lightningcss-linux-arm64-musl": "1.30.1",
|
||||
"lightningcss-linux-x64-gnu": "1.30.1",
|
||||
"lightningcss-linux-x64-musl": "1.30.1",
|
||||
"lightningcss-win32-arm64-msvc": "1.30.1",
|
||||
"lightningcss-win32-x64-msvc": "1.30.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
|
||||
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
|
||||
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
|
||||
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
|
||||
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
|
||||
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
|
||||
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
|
||||
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
|
||||
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
|
||||
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
|
||||
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
@@ -4930,6 +4666,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4997,6 +4734,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -6027,6 +5765,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -6115,13 +5854,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
|
||||
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
@@ -6195,6 +5935,7 @@
|
||||
"integrity": "sha512-2Fqty3MM9CDwOVet/jaQalYlbcjATZwPYGcqpiYQqgQ/dLC7GuHdISKgTYIVF/kaishKxLzleKWWfbSDklyIKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.10",
|
||||
"@vitest/mocker": "4.0.10",
|
||||
@@ -6279,6 +6020,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz",
|
||||
"integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.21",
|
||||
"@vue/compiler-sfc": "3.5.21",
|
||||
@@ -6311,7 +6053,6 @@
|
||||
"integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"eslint-scope": "^8.2.0",
|
||||
@@ -6336,7 +6077,6 @@
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
|
||||
@@ -386,5 +386,14 @@ export const adminApi = {
|
||||
{ provider_id: providerId, api_key_id: apiKeyId }
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 测试 SMTP 连接,支持传入未保存的配置
|
||||
async testSmtpConnection(config: Record<string, any> = {}): Promise<{ success: boolean; message: string }> {
|
||||
const response = await apiClient.post<{ success: boolean; message: string }>(
|
||||
'/api/admin/system/smtp/test',
|
||||
config
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,45 @@ export interface UserStats {
|
||||
[key: string]: unknown // 允许扩展其他统计数据
|
||||
}
|
||||
|
||||
export interface SendVerificationCodeRequest {
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface SendVerificationCodeResponse {
|
||||
message: string
|
||||
success: boolean
|
||||
expire_minutes?: number
|
||||
}
|
||||
|
||||
export interface VerifyEmailRequest {
|
||||
email: string
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface VerifyEmailResponse {
|
||||
message: string
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
email: string
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
user_id: string
|
||||
email: string
|
||||
username: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface RegistrationSettingsResponse {
|
||||
enable_registration: boolean
|
||||
require_email_verification: boolean
|
||||
verification_code_expire_minutes?: number
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string // UUID
|
||||
username: string
|
||||
@@ -87,5 +126,33 @@ export const authApi = {
|
||||
localStorage.setItem('refresh_token', response.data.refresh_token)
|
||||
}
|
||||
return response.data
|
||||
},
|
||||
|
||||
async sendVerificationCode(email: string): Promise<SendVerificationCodeResponse> {
|
||||
const response = await apiClient.post<SendVerificationCodeResponse>(
|
||||
'/api/auth/send-verification-code',
|
||||
{ email }
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async verifyEmail(email: string, code: string): Promise<VerifyEmailResponse> {
|
||||
const response = await apiClient.post<VerifyEmailResponse>(
|
||||
'/api/auth/verify-email',
|
||||
{ email, code }
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async register(data: RegisterRequest): Promise<RegisterResponse> {
|
||||
const response = await apiClient.post<RegisterResponse>('/api/auth/register', data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getRegistrationSettings(): Promise<RegistrationSettingsResponse> {
|
||||
const response = await apiClient.get<RegistrationSettingsResponse>(
|
||||
'/api/auth/registration-settings'
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
192
frontend/src/components/VerificationCodeInput.vue
Normal file
192
frontend/src/components/VerificationCodeInput.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="verification-code-input">
|
||||
<div class="code-inputs flex gap-2">
|
||||
<input
|
||||
v-for="(digit, index) in digits"
|
||||
:key="index"
|
||||
:ref="(el) => (inputRefs[index] = el as HTMLInputElement)"
|
||||
v-model="digits[index]"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
maxlength="1"
|
||||
class="code-digit"
|
||||
:class="{ error: hasError }"
|
||||
@input="handleInput(index, $event)"
|
||||
@keydown="handleKeyDown(index, $event)"
|
||||
@paste="handlePaste"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
length?: number
|
||||
hasError?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'complete', value: string): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
length: 6,
|
||||
hasError: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const digits = ref<string[]>(Array(props.length).fill(''))
|
||||
const inputRefs = ref<HTMLInputElement[]>([])
|
||||
|
||||
// Watch modelValue changes from parent
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue.length <= props.length) {
|
||||
digits.value = newValue.split('').concat(Array(props.length - newValue.length).fill(''))
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const updateValue = () => {
|
||||
const value = digits.value.join('')
|
||||
emit('update:modelValue', value)
|
||||
|
||||
// Emit complete event when all digits are filled
|
||||
if (value.length === props.length && /^\d+$/.test(value)) {
|
||||
emit('complete', value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInput = (index: number, event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const value = input.value
|
||||
|
||||
// Only allow digits
|
||||
if (!/^\d*$/.test(value)) {
|
||||
input.value = digits.value[index]
|
||||
return
|
||||
}
|
||||
|
||||
digits.value[index] = value
|
||||
|
||||
// Auto-focus next input
|
||||
if (value && index < props.length - 1) {
|
||||
inputRefs.value[index + 1]?.focus()
|
||||
}
|
||||
|
||||
updateValue()
|
||||
}
|
||||
|
||||
const handleKeyDown = (index: number, event: KeyboardEvent) => {
|
||||
// Handle backspace
|
||||
if (event.key === 'Backspace') {
|
||||
if (!digits.value[index] && index > 0) {
|
||||
// If current input is empty, move to previous and clear it
|
||||
inputRefs.value[index - 1]?.focus()
|
||||
digits.value[index - 1] = ''
|
||||
updateValue()
|
||||
} else {
|
||||
// Clear current input
|
||||
digits.value[index] = ''
|
||||
updateValue()
|
||||
}
|
||||
}
|
||||
// Handle arrow keys
|
||||
else if (event.key === 'ArrowLeft' && index > 0) {
|
||||
inputRefs.value[index - 1]?.focus()
|
||||
} else if (event.key === 'ArrowRight' && index < props.length - 1) {
|
||||
inputRefs.value[index + 1]?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePaste = (event: ClipboardEvent) => {
|
||||
event.preventDefault()
|
||||
const pastedData = event.clipboardData?.getData('text') || ''
|
||||
const cleanedData = pastedData.replace(/\D/g, '').slice(0, props.length)
|
||||
|
||||
if (cleanedData) {
|
||||
digits.value = cleanedData.split('').concat(Array(props.length - cleanedData.length).fill(''))
|
||||
updateValue()
|
||||
|
||||
// Focus the next empty input or the last input
|
||||
const nextEmptyIndex = digits.value.findIndex((d) => !d)
|
||||
const focusIndex = nextEmptyIndex >= 0 ? nextEmptyIndex : props.length - 1
|
||||
inputRefs.value[focusIndex]?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Expose method to clear inputs
|
||||
const clear = () => {
|
||||
digits.value = Array(props.length).fill('')
|
||||
inputRefs.value[0]?.focus()
|
||||
updateValue()
|
||||
}
|
||||
|
||||
// Expose method to focus first input
|
||||
const focus = () => {
|
||||
inputRefs.value[0]?.focus()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
clear,
|
||||
focus
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.code-inputs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.code-digit {
|
||||
width: 3rem;
|
||||
height: 3.5rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
border: 2px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.code-digit:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 3px hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
.code-digit:hover:not(:focus) {
|
||||
border-color: hsl(var(--primary) / 0.5);
|
||||
}
|
||||
|
||||
.code-digit.error {
|
||||
border-color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.code-digit.error:focus {
|
||||
box-shadow: 0 0 0 3px hsl(var(--destructive) / 0.1);
|
||||
}
|
||||
|
||||
/* Prevent spinner buttons on number inputs */
|
||||
.code-digit::-webkit-outer-spin-button,
|
||||
.code-digit::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-digit[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
@@ -98,12 +98,27 @@
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<p
|
||||
v-if="!isDemo"
|
||||
v-if="!isDemo && !allowRegistration"
|
||||
class="text-xs text-slate-400 dark:text-muted-foreground/80"
|
||||
>
|
||||
如需开通账户,请联系管理员配置访问权限
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<!-- 注册链接 -->
|
||||
<div
|
||||
v-if="allowRegistration"
|
||||
class="mt-4 text-center text-sm"
|
||||
>
|
||||
还没有账户?
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-auto p-0"
|
||||
@click="handleSwitchToRegister"
|
||||
>
|
||||
立即注册
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -124,10 +139,18 @@
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Register Dialog -->
|
||||
<RegisterDialog
|
||||
v-model:open="showRegisterDialog"
|
||||
:require-email-verification="requireEmailVerification"
|
||||
@success="handleRegisterSuccess"
|
||||
@switch-to-login="handleSwitchToLogin"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ref, watch, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Dialog } from '@/components/ui'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
@@ -136,6 +159,8 @@ import Label from '@/components/ui/label.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { isDemoMode, DEMO_ACCOUNTS } from '@/config/demo'
|
||||
import RegisterDialog from './RegisterDialog.vue'
|
||||
import { authApi } from '@/api/auth'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
@@ -151,6 +176,9 @@ const { success: showSuccess, warning: showWarning, error: showError } = useToas
|
||||
|
||||
const isOpen = ref(props.modelValue)
|
||||
const isDemo = computed(() => isDemoMode())
|
||||
const showRegisterDialog = ref(false)
|
||||
const requireEmailVerification = ref(false)
|
||||
const allowRegistration = ref(false) // 由系统配置控制,默认关闭
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
isOpen.value = val
|
||||
@@ -201,4 +229,33 @@ async function handleLogin() {
|
||||
showError(authStore.error || '登录失败,请检查邮箱和密码')
|
||||
}
|
||||
}
|
||||
|
||||
function handleSwitchToRegister() {
|
||||
isOpen.value = false
|
||||
showRegisterDialog.value = true
|
||||
}
|
||||
|
||||
function handleRegisterSuccess() {
|
||||
showRegisterDialog.value = false
|
||||
showSuccess('注册成功!请登录')
|
||||
isOpen.value = true
|
||||
}
|
||||
|
||||
function handleSwitchToLogin() {
|
||||
showRegisterDialog.value = false
|
||||
isOpen.value = true
|
||||
}
|
||||
|
||||
// Load registration settings on mount
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const settings = await authApi.getRegistrationSettings()
|
||||
allowRegistration.value = !!settings.enable_registration
|
||||
requireEmailVerification.value = !!settings.require_email_verification
|
||||
} catch (error) {
|
||||
// If获取失败,保持默认:关闭注册 & 关闭邮箱验证
|
||||
allowRegistration.value = false
|
||||
requireEmailVerification.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
443
frontend/src/features/auth/components/RegisterDialog.vue
Normal file
443
frontend/src/features/auth/components/RegisterDialog.vue
Normal file
@@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:open="isOpen"
|
||||
size="lg"
|
||||
>
|
||||
<DialogContent>
|
||||
<!-- Logo -->
|
||||
<div class="flex justify-center mb-6">
|
||||
<div
|
||||
class="w-16 h-16 rounded-full border-2 border-primary/20 flex items-center justify-center bg-primary/5"
|
||||
>
|
||||
<img
|
||||
src="@/assets/logo.svg"
|
||||
alt="Logo"
|
||||
class="w-10 h-10"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle class="text-center text-2xl">
|
||||
注册新账户
|
||||
</DialogTitle>
|
||||
<DialogDescription class="text-center">
|
||||
请填写您的邮箱和个人信息完成注册
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form
|
||||
class="space-y-4 mt-4"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<!-- Email -->
|
||||
<div class="space-y-2">
|
||||
<Label for="register-email">邮箱</Label>
|
||||
<Input
|
||||
id="register-email"
|
||||
v-model="formData.email"
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
:disabled="isLoading || emailVerified"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Verification Code Section -->
|
||||
<div
|
||||
v-if="requireEmailVerification"
|
||||
class="space-y-2"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="verification-code">验证码</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="h-auto p-0 text-xs"
|
||||
:disabled="isLoading || !canSendCode || emailVerified"
|
||||
@click="handleSendCode"
|
||||
>
|
||||
{{ sendCodeButtonText }}
|
||||
</Button>
|
||||
</div>
|
||||
<VerificationCodeInput
|
||||
ref="codeInputRef"
|
||||
v-model="formData.verificationCode"
|
||||
:has-error="verificationError"
|
||||
:length="6"
|
||||
@complete="handleCodeComplete"
|
||||
/>
|
||||
<p
|
||||
v-if="verificationError"
|
||||
class="text-xs text-destructive"
|
||||
>
|
||||
验证码错误,请重新输入
|
||||
</p>
|
||||
<p
|
||||
v-if="emailVerified"
|
||||
class="text-xs text-green-600"
|
||||
>
|
||||
✓ 邮箱验证成功
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Username -->
|
||||
<div class="space-y-2">
|
||||
<Label for="register-username">用户名</Label>
|
||||
<Input
|
||||
id="register-username"
|
||||
v-model="formData.username"
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
required
|
||||
:disabled="isLoading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="space-y-2">
|
||||
<Label for="register-password">密码</Label>
|
||||
<Input
|
||||
id="register-password"
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
placeholder="至少 8 位字符"
|
||||
required
|
||||
:disabled="isLoading"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
密码长度至少 8 位
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="space-y-2">
|
||||
<Label for="register-confirm-password">确认密码</Label>
|
||||
<Input
|
||||
id="register-confirm-password"
|
||||
v-model="formData.confirmPassword"
|
||||
type="password"
|
||||
placeholder="再次输入密码"
|
||||
required
|
||||
:disabled="isLoading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter class="gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
:disabled="isLoading"
|
||||
@click="handleCancel"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="isLoading || !canSubmit"
|
||||
>
|
||||
<span
|
||||
v-if="isLoading"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<span class="animate-spin">⏳</span>
|
||||
{{ loadingText }}
|
||||
</span>
|
||||
<span v-else>注册</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 text-center text-sm">
|
||||
已有账户?
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-auto p-0"
|
||||
@click="handleSwitchToLogin"
|
||||
>
|
||||
立即登录
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Button,
|
||||
Input,
|
||||
Label
|
||||
} from '@/components/ui'
|
||||
import VerificationCodeInput from '@/components/VerificationCodeInput.vue'
|
||||
|
||||
interface Props {
|
||||
open?: boolean
|
||||
requireEmailVerification?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:open', value: boolean): void
|
||||
(e: 'success'): void
|
||||
(e: 'switchToLogin'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
requireEmailVerification: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const { showToast, success, error: showError } = useToast()
|
||||
|
||||
const codeInputRef = ref<InstanceType<typeof VerificationCodeInput> | null>(null)
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.open,
|
||||
set: (value) => emit('update:open', value)
|
||||
})
|
||||
|
||||
const formData = ref({
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
verificationCode: ''
|
||||
})
|
||||
|
||||
const isLoading = ref(false)
|
||||
const loadingText = ref('注册中...')
|
||||
const emailVerified = ref(false)
|
||||
const verificationError = ref(false)
|
||||
const codeSentAt = ref<number | null>(null)
|
||||
const cooldownSeconds = ref(0)
|
||||
const expireMinutes = ref(30)
|
||||
const cooldownTimer = ref<number | null>(null)
|
||||
|
||||
// Send code cooldown timer
|
||||
const canSendCode = computed(() => {
|
||||
if (!formData.value.email) return false
|
||||
if (cooldownSeconds.value > 0) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const sendCodeButtonText = computed(() => {
|
||||
if (emailVerified.value) return '已验证'
|
||||
if (cooldownSeconds.value > 0) return `${cooldownSeconds.value}秒后重试`
|
||||
if (codeSentAt.value) return '重新发送验证码'
|
||||
return '发送验证码'
|
||||
})
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
const hasBasicInfo =
|
||||
formData.value.email &&
|
||||
formData.value.username &&
|
||||
formData.value.password &&
|
||||
formData.value.confirmPassword
|
||||
|
||||
if (!hasBasicInfo) return false
|
||||
|
||||
// If email verification is required, check if verified
|
||||
if (props.requireEmailVerification && !emailVerified.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check password match
|
||||
if (formData.value.password !== formData.value.confirmPassword) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check password length
|
||||
if (formData.value.password.length < 8) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Reset form when dialog opens
|
||||
watch(isOpen, (newValue) => {
|
||||
if (newValue) {
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
// Start cooldown timer
|
||||
const startCooldown = (seconds: number) => {
|
||||
// Clear existing timer if any
|
||||
if (cooldownTimer.value !== null) {
|
||||
clearInterval(cooldownTimer.value)
|
||||
}
|
||||
|
||||
cooldownSeconds.value = seconds
|
||||
cooldownTimer.value = window.setInterval(() => {
|
||||
cooldownSeconds.value--
|
||||
if (cooldownSeconds.value <= 0) {
|
||||
if (cooldownTimer.value !== null) {
|
||||
clearInterval(cooldownTimer.value)
|
||||
cooldownTimer.value = null
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Cleanup timer on unmount
|
||||
onUnmounted(() => {
|
||||
if (cooldownTimer.value !== null) {
|
||||
clearInterval(cooldownTimer.value)
|
||||
}
|
||||
})
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
verificationCode: ''
|
||||
}
|
||||
emailVerified.value = false
|
||||
verificationError.value = false
|
||||
codeSentAt.value = null
|
||||
cooldownSeconds.value = 0
|
||||
|
||||
// Clear timer
|
||||
if (cooldownTimer.value !== null) {
|
||||
clearInterval(cooldownTimer.value)
|
||||
cooldownTimer.value = null
|
||||
}
|
||||
|
||||
codeInputRef.value?.clear()
|
||||
}
|
||||
|
||||
const handleSendCode = async () => {
|
||||
if (!formData.value.email) {
|
||||
showError('请输入邮箱')
|
||||
return
|
||||
}
|
||||
|
||||
// Basic email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(formData.value.email)) {
|
||||
showError('请输入有效的邮箱地址', '邮箱格式错误')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
loadingText.value = '发送中...'
|
||||
|
||||
try {
|
||||
const response = await authApi.sendVerificationCode(formData.value.email)
|
||||
|
||||
if (response.success) {
|
||||
codeSentAt.value = Date.now()
|
||||
if (response.expire_minutes) {
|
||||
expireMinutes.value = response.expire_minutes
|
||||
}
|
||||
|
||||
success(`请查收邮件,验证码有效期 ${expireMinutes.value} 分钟`, '验证码已发送')
|
||||
|
||||
// Start 60 second cooldown
|
||||
startCooldown(60)
|
||||
|
||||
// Focus the verification code input
|
||||
setTimeout(() => {
|
||||
codeInputRef.value?.focus()
|
||||
}, 100)
|
||||
} else {
|
||||
showError(response.message || '请稍后重试', '发送失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || error.message || '网络错误,请重试', '发送失败')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCodeComplete = async (code: string) => {
|
||||
if (!formData.value.email || code.length !== 6) return
|
||||
|
||||
isLoading.value = true
|
||||
loadingText.value = '验证中...'
|
||||
verificationError.value = false
|
||||
|
||||
try {
|
||||
const response = await authApi.verifyEmail(formData.value.email, code)
|
||||
|
||||
if (response.success) {
|
||||
emailVerified.value = true
|
||||
success('邮箱验证通过,请继续完成注册', '验证成功')
|
||||
} else {
|
||||
verificationError.value = true
|
||||
showError(response.message || '验证码错误', '验证失败')
|
||||
// Clear the code input
|
||||
codeInputRef.value?.clear()
|
||||
}
|
||||
} catch (error: any) {
|
||||
verificationError.value = true
|
||||
showError(error.response?.data?.detail || error.message || '验证码错误,请重试', '验证失败')
|
||||
// Clear the code input
|
||||
codeInputRef.value?.clear()
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Validate password match
|
||||
if (formData.value.password !== formData.value.confirmPassword) {
|
||||
showError('两次输入的密码不一致', '密码不匹配')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate password length
|
||||
if (formData.value.password.length < 8) {
|
||||
showError('密码长度至少 8 位', '密码过短')
|
||||
return
|
||||
}
|
||||
|
||||
// Check email verification if required
|
||||
if (props.requireEmailVerification && !emailVerified.value) {
|
||||
showError('请先完成邮箱验证')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
loadingText.value = '注册中...'
|
||||
|
||||
try {
|
||||
const response = await authApi.register({
|
||||
email: formData.value.email,
|
||||
username: formData.value.username,
|
||||
password: formData.value.password
|
||||
})
|
||||
|
||||
success(response.message || '欢迎加入!请登录以继续', '注册成功')
|
||||
|
||||
emit('success')
|
||||
isOpen.value = false
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || error.message || '注册失败,请重试', '注册失败')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const handleSwitchToLogin = () => {
|
||||
emit('switchToLogin')
|
||||
isOpen.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -374,8 +374,6 @@ import {
|
||||
} from '@/api/endpoints'
|
||||
import { useUpstreamModelsCache, type UpstreamModel } from '../composables/useUpstreamModelsCache'
|
||||
|
||||
const { fetchModels: fetchCachedModels, clearCache, getCachedModels } = useUpstreamModelsCache()
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
providerId: string
|
||||
@@ -388,6 +386,8 @@ const emit = defineEmits<{
|
||||
'changed': []
|
||||
}>()
|
||||
|
||||
const { fetchModels: fetchCachedModels, clearCache, getCachedModels } = useUpstreamModelsCache()
|
||||
|
||||
const { error: showError, success } = useToast()
|
||||
|
||||
// 状态
|
||||
|
||||
@@ -177,8 +177,8 @@
|
||||
<Label for="proxy_user">用户名(可选)</Label>
|
||||
<Input
|
||||
:id="`proxy_user_${formId}`"
|
||||
:name="`proxy_user_${formId}`"
|
||||
v-model="form.proxy_username"
|
||||
:name="`proxy_user_${formId}`"
|
||||
placeholder="代理认证用户名"
|
||||
autocomplete="off"
|
||||
data-form-type="other"
|
||||
@@ -191,8 +191,8 @@
|
||||
<Label :for="`proxy_pass_${formId}`">密码(可选)</Label>
|
||||
<Input
|
||||
:id="`proxy_pass_${formId}`"
|
||||
:name="`proxy_pass_${formId}`"
|
||||
v-model="form.proxy_password"
|
||||
:name="`proxy_pass_${formId}`"
|
||||
type="text"
|
||||
:placeholder="passwordPlaceholder"
|
||||
autocomplete="off"
|
||||
|
||||
@@ -126,8 +126,14 @@
|
||||
:disabled="testingModelName === model.global_model_name"
|
||||
@click.stop="testModelConnection(model)"
|
||||
>
|
||||
<Loader2 v-if="testingModelName === model.global_model_name" class="w-3.5 h-3.5 animate-spin" />
|
||||
<Play v-else class="w-3.5 h-3.5" />
|
||||
<Loader2
|
||||
v-if="testingModelName === model.global_model_name"
|
||||
class="w-3.5 h-3.5 animate-spin"
|
||||
/>
|
||||
<Play
|
||||
v-else
|
||||
class="w-3.5 h-3.5"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,8 +131,14 @@
|
||||
:disabled="testingMapping === `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`"
|
||||
@click="testMapping(group, mapping)"
|
||||
>
|
||||
<Loader2 v-if="testingMapping === `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`" class="w-3 h-3 animate-spin" />
|
||||
<Play v-else class="w-3 h-3" />
|
||||
<Loader2
|
||||
v-if="testingMapping === `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`"
|
||||
class="w-3 h-3 animate-spin"
|
||||
/>
|
||||
<Play
|
||||
v-else
|
||||
class="w-3 h-3"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1057,7 +1057,10 @@ onBeforeUnmount(() => {
|
||||
<span class="text-xs text-muted-foreground hidden sm:inline">分析用户请求间隔,推荐合适的缓存 TTL</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Select v-model="analysisHours" v-model:open="analysisHoursSelectOpen">
|
||||
<Select
|
||||
v-model="analysisHours"
|
||||
v-model:open="analysisHoursSelectOpen"
|
||||
>
|
||||
<SelectTrigger class="w-24 sm:w-28 h-8">
|
||||
<SelectValue placeholder="时间段" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -185,6 +185,218 @@
|
||||
</div>
|
||||
</CardSection>
|
||||
|
||||
<!-- SMTP 邮件配置 -->
|
||||
<CardSection
|
||||
title="SMTP 邮件配置"
|
||||
description="配置 SMTP 服务用于发送验证码邮件"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label
|
||||
for="smtp-host"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
SMTP 服务器地址
|
||||
</Label>
|
||||
<Input
|
||||
id="smtp-host"
|
||||
v-model="systemConfig.smtp_host"
|
||||
type="text"
|
||||
placeholder="smtp.gmail.com"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
邮件服务器地址
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="smtp-port"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
SMTP 端口
|
||||
</Label>
|
||||
<Input
|
||||
id="smtp-port"
|
||||
v-model.number="systemConfig.smtp_port"
|
||||
type="number"
|
||||
placeholder="587"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
常用端口: 587 (TLS), 465 (SSL), 25 (非加密)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="smtp-user"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
SMTP 用户名
|
||||
</Label>
|
||||
<Input
|
||||
id="smtp-user"
|
||||
v-model="systemConfig.smtp_user"
|
||||
type="text"
|
||||
placeholder="your-email@example.com"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
通常是您的邮箱地址
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="smtp-password"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
SMTP 密码
|
||||
</Label>
|
||||
<Input
|
||||
id="smtp-password"
|
||||
v-model="systemConfig.smtp_password"
|
||||
type="password"
|
||||
placeholder="********"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
邮箱密码或应用专用密码
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="smtp-from-email"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
发件人邮箱
|
||||
</Label>
|
||||
<Input
|
||||
id="smtp-from-email"
|
||||
v-model="systemConfig.smtp_from_email"
|
||||
type="email"
|
||||
placeholder="noreply@example.com"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
显示为发件人的邮箱地址
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="smtp-from-name"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
发件人名称
|
||||
</Label>
|
||||
<Input
|
||||
id="smtp-from-name"
|
||||
v-model="systemConfig.smtp_from_name"
|
||||
type="text"
|
||||
placeholder="Aether"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
显示为发件人的名称
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="verification-code-expire"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
验证码有效期(分钟)
|
||||
</Label>
|
||||
<Input
|
||||
id="verification-code-expire"
|
||||
v-model.number="systemConfig.verification_code_expire_minutes"
|
||||
type="number"
|
||||
placeholder="30"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
验证码的有效时间
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center h-full">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="smtp-use-tls"
|
||||
v-model:checked="systemConfig.smtp_use_tls"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
for="smtp-use-tls"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
使用 TLS 加密
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
推荐开启以提高安全性
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center h-full">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="smtp-use-ssl"
|
||||
v-model:checked="systemConfig.smtp_use_ssl"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
for="smtp-use-ssl"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
使用 SSL 加密 (465)
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
部分服务需要隐式 SSL,一般使用端口 465
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
:disabled="testSmtpLoading"
|
||||
@click="handleTestSmtp"
|
||||
>
|
||||
{{ testSmtpLoading ? '测试中...' : '测试 SMTP 连接' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="smtpTestResult"
|
||||
class="mt-4 p-4 rounded-lg"
|
||||
:class="smtpTestResult.success ? 'bg-green-50 dark:bg-green-950' : 'bg-destructive/10'"
|
||||
>
|
||||
<p
|
||||
class="text-sm font-medium"
|
||||
:class="smtpTestResult.success ? 'text-green-700 dark:text-green-300' : 'text-destructive'"
|
||||
>
|
||||
{{ smtpTestResult.success ? '✓ SMTP 连接测试成功' : '✗ SMTP 连接测试失败' }}
|
||||
</p>
|
||||
<p
|
||||
v-if="smtpTestResult.message"
|
||||
class="text-xs mt-1"
|
||||
:class="smtpTestResult.success ? 'text-green-600 dark:text-green-400' : 'text-destructive'"
|
||||
>
|
||||
{{ smtpTestResult.message }}
|
||||
</p>
|
||||
</div>
|
||||
</CardSection>
|
||||
|
||||
<!-- 独立余额 Key 过期管理 -->
|
||||
<CardSection
|
||||
title="独立余额 Key 过期管理"
|
||||
@@ -464,7 +676,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</CardSection>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 导入配置对话框 -->
|
||||
@@ -798,6 +1009,16 @@ interface SystemConfig {
|
||||
// 用户注册
|
||||
enable_registration: boolean
|
||||
require_email_verification: boolean
|
||||
// SMTP 邮件配置
|
||||
smtp_host: string | null
|
||||
smtp_port: number
|
||||
smtp_user: string | null
|
||||
smtp_password: string | null
|
||||
smtp_use_tls: boolean
|
||||
smtp_use_ssl: boolean
|
||||
smtp_from_email: string | null
|
||||
smtp_from_name: string
|
||||
verification_code_expire_minutes: number
|
||||
// 独立余额 Key 过期管理
|
||||
auto_delete_expired_keys: boolean
|
||||
// 日志记录
|
||||
@@ -817,6 +1038,8 @@ interface SystemConfig {
|
||||
|
||||
const loading = ref(false)
|
||||
const logLevelSelectOpen = ref(false)
|
||||
const testSmtpLoading = ref(false)
|
||||
const smtpTestResult = ref<{ success: boolean; message?: string } | null>(null)
|
||||
|
||||
// 导出/导入相关
|
||||
const exportLoading = ref(false)
|
||||
@@ -847,6 +1070,16 @@ const systemConfig = ref<SystemConfig>({
|
||||
// 用户注册
|
||||
enable_registration: false,
|
||||
require_email_verification: false,
|
||||
// SMTP 邮件配置
|
||||
smtp_host: null,
|
||||
smtp_port: 587,
|
||||
smtp_user: null,
|
||||
smtp_password: null,
|
||||
smtp_use_tls: true,
|
||||
smtp_use_ssl: false,
|
||||
smtp_from_email: null,
|
||||
smtp_from_name: 'Aether',
|
||||
verification_code_expire_minutes: 30,
|
||||
// 独立余额 Key 过期管理
|
||||
auto_delete_expired_keys: false,
|
||||
// 日志记录
|
||||
@@ -903,6 +1136,16 @@ async function loadSystemConfig() {
|
||||
// 用户注册
|
||||
'enable_registration',
|
||||
'require_email_verification',
|
||||
// SMTP 邮件配置
|
||||
'smtp_host',
|
||||
'smtp_port',
|
||||
'smtp_user',
|
||||
'smtp_password',
|
||||
'smtp_use_tls',
|
||||
'smtp_use_ssl',
|
||||
'smtp_from_email',
|
||||
'smtp_from_name',
|
||||
'verification_code_expire_minutes',
|
||||
// 独立余额 Key 过期管理
|
||||
'auto_delete_expired_keys',
|
||||
// 日志记录
|
||||
@@ -962,6 +1205,52 @@ async function saveSystemConfig() {
|
||||
value: systemConfig.value.require_email_verification,
|
||||
description: '是否需要邮箱验证'
|
||||
},
|
||||
// SMTP 邮件配置
|
||||
{
|
||||
key: 'smtp_host',
|
||||
value: systemConfig.value.smtp_host,
|
||||
description: 'SMTP 服务器地址'
|
||||
},
|
||||
{
|
||||
key: 'smtp_port',
|
||||
value: systemConfig.value.smtp_port,
|
||||
description: 'SMTP 端口'
|
||||
},
|
||||
{
|
||||
key: 'smtp_user',
|
||||
value: systemConfig.value.smtp_user,
|
||||
description: 'SMTP 用户名'
|
||||
},
|
||||
{
|
||||
key: 'smtp_password',
|
||||
value: systemConfig.value.smtp_password,
|
||||
description: 'SMTP 密码'
|
||||
},
|
||||
{
|
||||
key: 'smtp_use_tls',
|
||||
value: systemConfig.value.smtp_use_tls,
|
||||
description: '是否使用 TLS 加密'
|
||||
},
|
||||
{
|
||||
key: 'smtp_use_ssl',
|
||||
value: systemConfig.value.smtp_use_ssl,
|
||||
description: '是否使用 SSL 加密'
|
||||
},
|
||||
{
|
||||
key: 'smtp_from_email',
|
||||
value: systemConfig.value.smtp_from_email,
|
||||
description: '发件人邮箱'
|
||||
},
|
||||
{
|
||||
key: 'smtp_from_name',
|
||||
value: systemConfig.value.smtp_from_name,
|
||||
description: '发件人名称'
|
||||
},
|
||||
{
|
||||
key: 'verification_code_expire_minutes',
|
||||
value: systemConfig.value.verification_code_expire_minutes,
|
||||
description: '验证码有效期(分钟)'
|
||||
},
|
||||
// 独立余额 Key 过期管理
|
||||
{
|
||||
key: 'auto_delete_expired_keys',
|
||||
@@ -1041,6 +1330,41 @@ async function saveSystemConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// 测试 SMTP 连接
|
||||
async function handleTestSmtp() {
|
||||
testSmtpLoading.value = true
|
||||
smtpTestResult.value = null
|
||||
|
||||
try {
|
||||
const result = await adminApi.testSmtpConnection({
|
||||
smtp_host: systemConfig.value.smtp_host,
|
||||
smtp_port: systemConfig.value.smtp_port,
|
||||
smtp_user: systemConfig.value.smtp_user,
|
||||
smtp_password: systemConfig.value.smtp_password,
|
||||
smtp_use_tls: systemConfig.value.smtp_use_tls,
|
||||
smtp_use_ssl: systemConfig.value.smtp_use_ssl,
|
||||
smtp_from_email: systemConfig.value.smtp_from_email,
|
||||
smtp_from_name: systemConfig.value.smtp_from_name
|
||||
})
|
||||
smtpTestResult.value = result
|
||||
|
||||
if (result.success) {
|
||||
success('SMTP 连接测试成功')
|
||||
} else {
|
||||
error('SMTP 连接测试失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
log.error('SMTP 连接测试失败:', err)
|
||||
smtpTestResult.value = {
|
||||
success: false,
|
||||
message: err.response?.data?.detail || err.message || 'SMTP 连接测试失败'
|
||||
}
|
||||
error('SMTP 连接测试失败')
|
||||
} finally {
|
||||
testSmtpLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导出配置
|
||||
async function handleExportConfig() {
|
||||
exportLoading.value = true
|
||||
|
||||
@@ -179,8 +179,8 @@
|
||||
</Badge>
|
||||
</div>
|
||||
<div
|
||||
class="grid gap-2 sm:gap-3"
|
||||
:class="[
|
||||
'grid gap-2 sm:gap-3',
|
||||
hasCacheData ? 'grid-cols-2 xl:grid-cols-4' : 'grid-cols-1 max-w-xs'
|
||||
]"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user