This commit is contained in:
2026-02-02 16:34:10 +08:00
parent c0e2d47287
commit 544de32d80
13 changed files with 733 additions and 31 deletions

View File

@@ -17,18 +17,24 @@
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"axios": "^1.13.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"next-themes": "^0.4.6",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.13.0", "react-router-dom": "^7.13.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"uuid": "^13.0.0",
"zustand": "^5.0.10" "zustand": "^5.0.10"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/crypto-js": "^4.2.2",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^19.2.5", "@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",

243
pnpm-lock.yaml generated
View File

@@ -29,15 +29,24 @@ importers:
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)) version: 4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2))
axios:
specifier: ^1.13.4
version: 1.13.4
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
crypto-js:
specifier: ^4.2.0
version: 4.2.0
lucide-react: lucide-react:
specifier: ^0.563.0 specifier: ^0.563.0
version: 0.563.0(react@19.2.4) version: 0.563.0(react@19.2.4)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: react:
specifier: ^19.2.0 specifier: ^19.2.0
version: 19.2.4 version: 19.2.4
@@ -47,12 +56,18 @@ importers:
react-router-dom: react-router-dom:
specifier: ^7.13.0 specifier: ^7.13.0
version: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
tailwind-merge: tailwind-merge:
specifier: ^3.4.0 specifier: ^3.4.0
version: 3.4.0 version: 3.4.0
tailwindcss: tailwindcss:
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18 version: 4.1.18
uuid:
specifier: ^13.0.0
version: 13.0.0
zustand: zustand:
specifier: ^5.0.10 specifier: ^5.0.10
version: 5.0.10(@types/react@19.2.10)(react@19.2.4) version: 5.0.10(@types/react@19.2.10)(react@19.2.4)
@@ -60,6 +75,9 @@ importers:
'@eslint/js': '@eslint/js':
specifier: ^9.39.1 specifier: ^9.39.1
version: 9.39.2 version: 9.39.2
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/node': '@types/node':
specifier: ^24.10.1 specifier: ^24.10.1
version: 24.10.9 version: 24.10.9
@@ -1022,6 +1040,9 @@ packages:
'@types/babel__traverse@7.28.0': '@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/crypto-js@4.2.2':
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -1128,6 +1149,12 @@ packages:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'} engines: {node: '>=10'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.13.4:
resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==}
babel-plugin-react-compiler@1.0.0: babel-plugin-react-compiler@1.0.0:
resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
@@ -1149,6 +1176,10 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
callsites@3.1.0: callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1174,6 +1205,10 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1188,6 +1223,9 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
csstype@3.2.3: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1203,6 +1241,10 @@ packages:
deep-is@0.1.4: deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-libc@2.1.2: detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1210,6 +1252,10 @@ packages:
detect-node-es@1.1.0: detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
electron-to-chromium@1.5.283: electron-to-chromium@1.5.283:
resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==} resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==}
@@ -1217,6 +1263,22 @@ packages:
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.27.2: esbuild@0.27.2:
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1316,19 +1378,43 @@ packages:
flatted@3.3.3: flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin] os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
gensync@1.0.0-beta.2: gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-nonce@1.0.1: get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'} engines: {node: '>=6'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
glob-parent@6.0.2: glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -1341,6 +1427,10 @@ packages:
resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
graceful-fs@4.2.11: graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -1348,6 +1438,18 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hermes-estree@0.25.1: hermes-estree@0.25.1:
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
@@ -1510,6 +1612,18 @@ packages:
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
minimatch@3.1.2: minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -1528,6 +1642,12 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next-themes@0.4.6:
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
peerDependencies:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
node-releases@2.0.27: node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
@@ -1570,6 +1690,9 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1666,6 +1789,12 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'} engines: {node: '>=8'}
sonner@2.0.7:
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
peerDependencies:
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1752,6 +1881,10 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
uuid@13.0.0:
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
hasBin: true
vite@7.3.1: vite@7.3.1:
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -2598,6 +2731,8 @@ snapshots:
dependencies: dependencies:
'@babel/types': 7.28.6 '@babel/types': 7.28.6
'@types/crypto-js@4.2.2': {}
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
@@ -2740,6 +2875,16 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
asynckit@0.4.0: {}
axios@1.13.4:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
babel-plugin-react-compiler@1.0.0: babel-plugin-react-compiler@1.0.0:
dependencies: dependencies:
'@babel/types': 7.28.6 '@babel/types': 7.28.6
@@ -2765,6 +2910,11 @@ snapshots:
node-releases: 2.0.27 node-releases: 2.0.27
update-browserslist-db: 1.2.3(browserslist@4.28.1) update-browserslist-db: 1.2.3(browserslist@4.28.1)
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
callsites@3.1.0: {} callsites@3.1.0: {}
caniuse-lite@1.0.30001766: {} caniuse-lite@1.0.30001766: {}
@@ -2786,6 +2936,10 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
concat-map@0.0.1: {} concat-map@0.0.1: {}
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
@@ -2798,6 +2952,8 @@ snapshots:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
crypto-js@4.2.0: {}
csstype@3.2.3: {} csstype@3.2.3: {}
debug@4.4.3: debug@4.4.3:
@@ -2806,10 +2962,18 @@ snapshots:
deep-is@0.1.4: {} deep-is@0.1.4: {}
delayed-stream@1.0.0: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
detect-node-es@1.1.0: {} detect-node-es@1.1.0: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
electron-to-chromium@1.5.283: {} electron-to-chromium@1.5.283: {}
enhanced-resolve@5.18.4: enhanced-resolve@5.18.4:
@@ -2817,6 +2981,21 @@ snapshots:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
tapable: 2.3.0 tapable: 2.3.0
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
esbuild@0.27.2: esbuild@0.27.2:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.27.2 '@esbuild/aix-ppc64': 0.27.2
@@ -2959,13 +3138,43 @@ snapshots:
flatted@3.3.3: {} flatted@3.3.3: {}
follow-redirects@1.15.11: {}
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
function-bind@1.1.2: {}
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.2: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-nonce@1.0.1: {} get-nonce@1.0.1: {}
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
glob-parent@6.0.2: glob-parent@6.0.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -2974,10 +3183,22 @@ snapshots:
globals@16.5.0: {} globals@16.5.0: {}
gopd@1.2.0: {}
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
has-flag@4.0.0: {} has-flag@4.0.0: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
hermes-estree@0.25.1: {} hermes-estree@0.25.1: {}
hermes-parser@0.25.1: hermes-parser@0.25.1:
@@ -3097,6 +3318,14 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
math-intrinsics@1.1.0: {}
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
minimatch@3.1.2: minimatch@3.1.2:
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.12
@@ -3111,6 +3340,11 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
node-releases@2.0.27: {} node-releases@2.0.27: {}
optionator@0.9.4: optionator@0.9.4:
@@ -3150,6 +3384,8 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
proxy-from-env@1.1.0: {}
punycode@2.3.1: {} punycode@2.3.1: {}
react-dom@19.2.4(react@19.2.4): react-dom@19.2.4(react@19.2.4):
@@ -3249,6 +3485,11 @@ snapshots:
shebang-regex@3.0.0: {} shebang-regex@3.0.0: {}
sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
@@ -3320,6 +3561,8 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.10 '@types/react': 19.2.10
uuid@13.0.0: {}
vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2): vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2):
dependencies: dependencies:
esbuild: 0.27.2 esbuild: 0.27.2

View File

@@ -2,7 +2,11 @@
import { RouterProvider } from "react-router-dom"; import { RouterProvider } from "react-router-dom";
import router from "./routes.tsx"; import router from "./routes.tsx";
import "./App.module.css"; import "./App.module.css";
import useUserStore from '@/store/useUserStore';
function App() { function App() {
useUserStore()
// console.log(useUserStore());
return ( return (
<RouterProvider router={router}/> <RouterProvider router={router}/>
); );

View File

@@ -1,11 +1,13 @@
import Header from "./components/Header.tsx"; import Header from "./components/Header.tsx";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import { Toaster } from "@/components/ui/sonner"
function Layout() { function Layout() {
return ( return (
<> <>
<div className={"bg-gray-100 w-full"}> <div className={"bg-gray-100 w-full"}>
<Header /> <Header />
<Outlet /> <Outlet />
<Toaster />
</div> </div>
</> </>
); );

18
src/api/user.ts Normal file
View File

@@ -0,0 +1,18 @@
import http from '@/lib/http'
import type { Result } from '@/types/common'
import type { UserRegisterT } from '@/types/user'
import CryptoJS from 'crypto-js';
const prefix = '/user'
export const getCaptcha = (reqId:string): Promise<Result<string>>=>{
return http.get(prefix+'/captcha/'+reqId)
}
export const register = (user: UserRegisterT):Promise<Result<string>>=>{
user.password = CryptoJS.MD5(user.password as string).toString()
return http.post(prefix+'/register',user)
}
export const existUsername = (username:string):Promise<Result<boolean>>=>{
return http.get(prefix+'/exist/'+username)
}

View File

@@ -0,0 +1,38 @@
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

17
src/lib/http.ts Normal file
View File

@@ -0,0 +1,17 @@
import axios from 'axios';
const request = axios.create({
baseURL: '/api', // 指向你的 Java SpringBoot 后端地址
timeout: 10000,
});
// 响应拦截器
request.interceptors.response.use(
(response) => response.data,
(error) => {
console.error('网络请求出错:', error);
return Promise.reject(error);
}
);
export default request;

4
src/lib/keyGen.ts Normal file
View File

@@ -0,0 +1,4 @@
import { v4 as uuidv4 } from 'uuid';
export const uuid = ()=>{
return uuidv4()
}

View File

@@ -1,7 +1,238 @@
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useState, useEffect, useRef } from "react";
import { existUsername, getCaptcha, register } from "@/api/user";
import useUserStore from "@/store/useUserStore";
import type { UserRegisterT } from "@/types/user";
import { toast } from "sonner"
interface UserRegisterValidT {
username: boolean;
passwd: boolean;
email: boolean;
inviteCode: boolean;
agree: boolean;
}
interface UsernameErrorT {
min: string;
tobe: string;
}
interface PasswdErrorT {
min: string;
}
interface SepasswdErrorT {
once: string;
}
interface EmailErrorT {
pattern: string;
}
interface InviteCodeErrorT {
no: string;
invalid: string;
}
const inviteCodeError: InviteCodeErrorT = {
no: "邀请码不能为空",
invalid: "无效的邀请码",
};
const emailError: EmailErrorT = {
pattern: "邮箱格式不正确",
};
const passwdError: PasswdErrorT = {
min: "密码必须大于6位",
};
const usernameError: UsernameErrorT = {
min: "用户名不能少于4位",
tobe: "用户名已存在",
};
const sepasswdError: SepasswdErrorT = {
once: "两次输入的密码不一致",
};
function Register() { function Register() {
const [userRegister, setUserRegister] = useState<UserRegisterT>({});
const [usernameErrorMsg, setUsernameErrorMsg] = useState<string>("");
const [passwdErrorMsg, setPasswdErrorMsg] = useState<string>("");
const [sepasswdErrorMsg, setSepasswdErrorMsg] = useState<string>("");
const [emailErrorMsg, setEmailErrorMsg] = useState<string>("");
const [inviteCodeErrorMsg, setInviteCodeErrorMsg] = useState<string>("");
const [captcha, setCaptcha] = useState<string | null>(null);
const captchaId = useUserStore((state) => state.key);
const [userRegisterValid, setUserRegisterValid] =
useState<UserRegisterValidT>({
username: false,
passwd: false,
email: false,
inviteCode: false,
agree: false,
});
// 注册事件
const registerEvent = async () => {
let valid = true;
Object.entries(userRegisterValid).forEach(([, value]) => {
if (!value) {
valid = false;
}
});
if(!userRegisterValid.agree){
toast.warning('请同意用户及隐私协议',{ position: "top-center" })
}
if (valid) {
userRegister.key = captchaId
const res = await register(userRegister)
if(res.code!=0){
toast.error('注册成功',{ position: "top-center" })
}else{
toast.info(res.msg,{ position: "top-center" })
}
}
};
// 刷新验证码
// 这里的类型会自动推断为 number
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const refreshCaptcha = () => {
// 1. 先清除上一个还没执行的定时器(真正的防抖核心)
if (timerRef.current) {
clearTimeout(timerRef.current);
}
// 2. 开启新定时器,并存入 ref
timerRef.current = setTimeout(async () => {
try {
const res = await getCaptcha(captchaId);
if (res.code === 0) {
setCaptcha(res.data);
}
} finally {
timerRef.current = null; // 执行完清空引用
}
}, 500);
};
// 初始化验证码
useEffect(() => {
const fetchCaptcha = async () => {
const res = await getCaptcha(captchaId);
if (res.code == 0) {
setCaptcha(res.data);
}
};
fetchCaptcha();
}, []);
function inviteCodeChangeEvent(inviteCode: string) {
inviteCodeValidator(inviteCode);
}
// 邀请码检查
function inviteCodeValidator(inviteCode: string) {
if (!inviteCode) {
setInviteCodeErrorMsg(inviteCodeError.no);
setUserRegisterValid({ ...userRegisterValid, inviteCode: false });
} else {
setInviteCodeErrorMsg("");
setUserRegisterValid({ ...userRegisterValid, inviteCode: true });
setUserRegister({...userRegister, inviteCode})
}
}
// 邮箱变更事件
function emailChangeEvent(email: string) {
emailValidator(email);
}
// 邮箱检查
function emailValidator(email: string) {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(email)) {
setEmailErrorMsg(emailError.pattern);
setUserRegisterValid({ ...userRegisterValid, email: false });
} else {
setEmailErrorMsg("");
setUserRegisterValid({ ...userRegisterValid, email: true });
setUserRegister({...userRegister, email})
}
}
// 再次输入密码变更事件
function sepasswdChangeEvent(passwd: string) {
sepasswdValidator(passwd);
}
// 再次输入密码检查
function sepasswdValidator(passwd: string) {
if (userRegister.password != passwd) {
setSepasswdErrorMsg(sepasswdError.once);
} else {
setSepasswdErrorMsg("");
}
}
// 密码变更事件
function passwdChangeEvent(passwd: string) {
passwdValidator(passwd);
setUserRegister({ ...userRegister, password: passwd });
}
// 密码检查
function passwdValidator(username: string) {
if (username.length < 6) {
setPasswdErrorMsg(passwdError.min);
setUserRegisterValid({ ...userRegisterValid, passwd: false });
} else {
setPasswdErrorMsg("");
setUserRegisterValid({ ...userRegisterValid, passwd: true });
}
}
// 用户名变更事件
function usernameChangeEvent(username: string) {
usernameValidator(username);
setUserRegister({ ...userRegister, username });
}
// 用户名检查
function usernameValidator(username: string) {
if (username.length < 4) {
setUsernameErrorMsg(usernameError.min);
setUserRegisterValid({ ...userRegisterValid, username: false });
} else {
setUsernameErrorMsg("");
setUserRegisterValid({ ...userRegisterValid, username: true });
}
uniqueUsername(username);
}
const agreeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// 验证用户名是否唯一
const uniqueUsername = (username: string) => {
if (agreeTimerRef.current) {
clearTimeout(agreeTimerRef.current);
}
agreeTimerRef.current = setTimeout(async () => {
try {
const res = await existUsername(username);
if (res.code == 0) {
setUsernameErrorMsg(usernameError.tobe);
setUserRegisterValid({ ...userRegisterValid, username: false });
}
} finally {
agreeTimerRef.current = null;
}
},1000);
};
// 验证码变更事件
const verificationCodeEvent = (code: string)=>{
setUserRegister({...userRegister, verificationCode:code})
}
return ( return (
<div className="pb-20"> <div className="pb-20">
<div className="italic mt-20 ml-auto mr-auto w-60 h-20 text-center text-4xl"> <div className="italic mt-20 ml-auto mr-auto w-60 h-20 text-center text-4xl">
@@ -11,56 +242,159 @@ function Register() {
<div className="flex flex-col w-1/2 mr-auto ml-auto"> <div className="flex flex-col w-1/2 mr-auto ml-auto">
<div className="mb-1"> <div className="mb-1">
<div className="flex flex-row-reverse"> <div className="flex flex-row-reverse">
<Input id="username" placeholder="请输入用户名" className="mb-1 flex-3" /> <Input
<label htmlFor="username" className="mt-1.5 font-light text-sm flex-1">:</label> onChange={(e) => usernameChangeEvent(e.target.value)}
id="username"
placeholder="请输入用户名"
className="mb-1 flex-3"
/>
<label
htmlFor="username"
className="mt-1.5 font-light text-sm flex-1"
>
:
</label>
</div> </div>
<div className="text-red-400 text-xs pl-15"></div> {usernameErrorMsg != "" ? (
<div className="text-red-400 text-xs pl-15">
{usernameErrorMsg}
</div>
) : (
<div></div>
)}
</div> </div>
<div className="mb-1"> <div className="mb-1">
<div className="flex flex-row-reverse"> <div className="flex flex-row-reverse">
<Input id="passwd" placeholder="请输入密码" className="mb-1 flex-3" /> <Input
<label htmlFor="passwd" className=" mt-1.5 font-light text-sm flex-1">:</label> onChange={(e) => passwdChangeEvent(e.target.value)}
id="passwd"
placeholder="请输入密码"
className="mb-1 flex-3"
type="password"
/>
<label
htmlFor="passwd"
className=" mt-1.5 font-light text-sm flex-1"
>
:
</label>
</div> </div>
<div className="text-red-400 text-xs pl-15">6</div> {passwdErrorMsg ? (
<div className="text-red-400 text-xs pl-15">{passwdErrorMsg}</div>
) : (
""
)}
</div> </div>
<div className="mb-1"> <div className="mb-1">
<div className="flex flex-row-reverse"> <div className="flex flex-row-reverse">
<Input id="passwd1" placeholder="请输入用户名" className="mb-1 flex-3" /> <Input
<label htmlFor="passwd1" className="w-20 font-light text-sm flex-1">:</label> onChange={(e) => sepasswdChangeEvent(e.target.value)}
id="passwd1"
placeholder="再一次输入密码"
className="mb-1 flex-3"
type="password"
/>
<label
htmlFor="passwd1"
className="w-20 font-light text-sm flex-1"
>
:
</label>
</div> </div>
<div className="text-red-400 text-xs pl-15"></div> {sepasswdErrorMsg ? (
<div className="text-red-400 text-xs pl-15">
</div>
) : (
""
)}
</div> </div>
<div className="mb-1"> <div className="mb-1">
<div className="flex flex-row-reverse"> <div className="flex flex-row-reverse">
<Input id="passwd1" placeholder="请输入邮箱" className="mb-1 flex-3" /> <Input
<label htmlFor="passwd1" className=" mt-1.5 w-20 font-light text-sm flex-1">:</label> id="passwd1"
placeholder="请输入邮箱"
className="mb-1 flex-3"
onChange={(e) => emailChangeEvent(e.target.value)}
/>
<label
htmlFor="passwd1"
className=" mt-1.5 w-20 font-light text-sm flex-1"
>
:
</label>
</div> </div>
<div className="text-red-400 text-xs pl-15"></div> {emailErrorMsg ? (
<div className="text-red-400 text-xs pl-15">{emailErrorMsg}</div>
) : (
""
)}
</div> </div>
<div className="mb-1"> <div className="mb-1">
<div className="flex flex-row-reverse"> <div className="flex flex-row-reverse">
<Input id="passwd1" placeholder="请输入邀请码" className="mb-1 flex-3" /> <Input
<label htmlFor="passwd1" className=" mt-1.5 w-20 font-light text-sm flex-1">:</label> id="passwd1"
placeholder="请输入邀请码"
className="mb-1 flex-3"
onChange={(e) => inviteCodeChangeEvent(e.target.value)}
/>
<label
htmlFor="passwd1"
className=" mt-1.5 w-20 font-light text-sm flex-1"
>
:
</label>
</div> </div>
<div className="text-red-400 text-xs pl-15"></div> {inviteCodeErrorMsg ? (
<div className="text-red-400 text-xs pl-15">
{inviteCodeErrorMsg}
</div>
) : (
""
)}
</div> </div>
<div className="mb-1"> <div className="mb-1">
<div className="flex flex-row-reverse"> <div className="flex flex-row-reverse relative">
<Input id="passwd1" placeholder="请输入验证码" className="mb-1 flex-3" /> <img
<label htmlFor="passwd1" className=" mt-1.5 w-20 font-light text-sm flex-1">:</label> onClick={() => refreshCaptcha()}
className="mt-0.5 w-20 h-8 absolute left-63 hover:cursor-pointer"
src={captcha == null ? "#" : captcha}
alt="点击刷新"
/>
<Input
onChange={(e)=>verificationCodeEvent(e.target.value)}
id="passwd1"
placeholder="请输入验证码"
className="mb-1 flex-3"
/>
<label
htmlFor="passwd1"
className=" mt-1.5 w-20 font-light text-sm flex-1"
>
:
</label>
</div> </div>
<div className="text-red-400 text-xs pl-15"></div> {/* <div className="text-red-400 text-xs pl-15">无效的验证码</div> */}
</div> </div>
<div className="flex align-middle"> <div className="flex align-middle">
<Checkbox /><span className="ml-1 text-xs hover:text-gray-500 hover:underline hover:cursor-pointer"></span> <Checkbox
onCheckedChange={(check: boolean) =>
setUserRegisterValid({ ...userRegisterValid, agree: check })
}
/>
<span className="ml-1 text-xs hover:text-gray-500 hover:underline hover:cursor-pointer">
</span>
</div> </div>
<div className="mt-2"></div> <div className="mt-2"></div>
<hr /> <hr />
<div> <div>
<Button className="w-full mt-4 mb-4" ></Button> <Button
onClick={() => registerEvent()}
className="w-full mt-4 mb-4"
>
</Button>
</div> </div>
</div> </div>
</div> </div>

12
src/store/useUserStore.ts Normal file
View File

@@ -0,0 +1,12 @@
import { uuid } from './../lib/keyGen';
import { create } from 'zustand';
interface UserState {
key: string
}
const useUserStore = create<UserState>(() => ({
key: uuid().replaceAll('-','')
}));
export default useUserStore;

5
src/types/common.ts Normal file
View File

@@ -0,0 +1,5 @@
export interface Result<T> {
code: number
msg:string
data: T
}

8
src/types/user.ts Normal file
View File

@@ -0,0 +1,8 @@
export interface UserRegisterT {
username?: string;
password?: string;
email?: string;
inviteCode?: string;
verificationCode?: string;
key?:string
}

View File

@@ -4,6 +4,17 @@ import path from "path"
import tailwindcss from "@tailwindcss/vite" import tailwindcss from "@tailwindcss/vite"
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
server: {
proxy: {
// 1. 匹配所有以 /api 开头的请求
'/api': {
target: 'http://localhost', // 你的 Java 后端地址
changeOrigin: true, // 允许跨域
// 2. 路径重写:如果后端接口没有 /api 前缀,就把它删掉
rewrite: (path) => path.replace(/^\/api/, '')
},
}
},
plugins: [ plugins: [
tailwindcss(), tailwindcss(),
react({ react({