登录页面与组件样式调整
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,3 +42,4 @@ next-env.d.ts
|
||||
|
||||
# editor
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -15,6 +15,7 @@ const eslintConfig = [
|
||||
rules: {
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'semi': ['error', 'never'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
import type { NextConfig } from "next"
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
}
|
||||
|
||||
export default nextConfig;
|
||||
export default nextConfig
|
||||
|
||||
31
package.json
31
package.json
@@ -9,27 +9,27 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "15.2.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"lucide-react": "^0.479.0",
|
||||
|
||||
"canvas": "^3.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
|
||||
"lucide-react": "^0.479.0",
|
||||
"motion": "^12.5.0",
|
||||
"next": "15.2.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"zod": "^3.24.2",
|
||||
|
||||
"sonner": "^2.0.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"motion": "^12.5.0"
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@@ -44,5 +44,10 @@
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b"
|
||||
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"canvas"
|
||||
]
|
||||
}
|
||||
}
|
||||
316
pnpm-lock.yaml
generated
316
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@radix-ui/react-checkbox':
|
||||
specifier: ^1.1.4
|
||||
version: 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@@ -26,6 +29,9 @@ importers:
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/react@19.0.10)(react@19.0.0)
|
||||
canvas:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -41,6 +47,9 @@ importers:
|
||||
next:
|
||||
specifier: 15.2.1
|
||||
version: 15.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0
|
||||
@@ -50,6 +59,9 @@ importers:
|
||||
react-hook-form:
|
||||
specifier: ^7.54.2
|
||||
version: 7.54.2(react@19.0.0)
|
||||
sonner:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
tailwind-merge:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
@@ -431,6 +443,19 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-dialog@1.1.6':
|
||||
resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-direction@1.1.0':
|
||||
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
|
||||
peerDependencies:
|
||||
@@ -920,6 +945,12 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
brace-expansion@1.1.11:
|
||||
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
||||
|
||||
@@ -930,6 +961,9 @@ packages:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
buffer@5.7.1:
|
||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||
|
||||
busboy@1.6.0:
|
||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
@@ -953,10 +987,17 @@ packages:
|
||||
caniuse-lite@1.0.30001701:
|
||||
resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==}
|
||||
|
||||
canvas@3.1.0:
|
||||
resolution: {integrity: sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==}
|
||||
engines: {node: ^18.12.0 || >= 20.9.0}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
@@ -1023,6 +1064,14 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decompress-response@6.0.0:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
deep-extend@0.6.0:
|
||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
@@ -1057,6 +1106,9 @@ packages:
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
end-of-stream@1.4.4:
|
||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -1213,6 +1265,10 @@ packages:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
expand-template@2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -1278,6 +1334,9 @@ packages:
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
@@ -1307,6 +1366,9 @@ packages:
|
||||
get-tsconfig@4.10.0:
|
||||
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
|
||||
|
||||
github-from-package@0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1360,6 +1422,9 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -1372,6 +1437,12 @@ packages:
|
||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||
engines: {node: '>=0.8.19'}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
|
||||
internal-slot@1.1.0:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1626,6 +1697,10 @@ packages:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
@@ -1636,6 +1711,9 @@ packages:
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
|
||||
motion-dom@12.5.0:
|
||||
resolution: {integrity: sha512-uH2PETDh7m+Hjd1UQQ56yHqwn83SAwNjimNPE/kC+Kds0t4Yh7+29rfo5wezVFpPOv57U4IuWved5d1x0kNhbQ==}
|
||||
|
||||
@@ -1664,9 +1742,18 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
napi-build-utils@2.0.0:
|
||||
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
||||
|
||||
natural-compare@1.4.0:
|
||||
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
|
||||
|
||||
next@15.2.1:
|
||||
resolution: {integrity: sha512-zxbsdQv3OqWXybK5tMkPCBKyhIz63RstJ+NvlfkaLMc/m5MwXgz2e92k+hSKcyBpyADhMk2C31RIiaDjUZae7g==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
@@ -1688,6 +1775,13 @@ packages:
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
node-abi@3.74.0:
|
||||
resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1720,6 +1814,9 @@ packages:
|
||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1774,6 +1871,11 @@ packages:
|
||||
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1781,6 +1883,9 @@ packages:
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
pump@3.0.2:
|
||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1788,6 +1893,10 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
rc@1.2.8:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
|
||||
react-dom@19.0.0:
|
||||
resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==}
|
||||
peerDependencies:
|
||||
@@ -1836,6 +1945,10 @@ packages:
|
||||
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1871,6 +1984,9 @@ packages:
|
||||
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
safe-push-apply@1.0.0:
|
||||
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1931,9 +2047,21 @@ packages:
|
||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
simple-concat@1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
|
||||
simple-get@4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
|
||||
simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
|
||||
sonner@2.0.1:
|
||||
resolution: {integrity: sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1968,10 +2096,17 @@ packages:
|
||||
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
strip-bom@3.0.0:
|
||||
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
strip-json-comments@2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
strip-json-comments@3.1.1:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2012,6 +2147,13 @@ packages:
|
||||
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tar-fs@2.1.2:
|
||||
resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==}
|
||||
|
||||
tar-stream@2.2.0:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tinyglobby@0.2.12:
|
||||
resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -2032,6 +2174,9 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2087,6 +2232,9 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2112,6 +2260,9 @@ packages:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2377,6 +2528,28 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@radix-ui/react-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0)
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.10)(react@19.0.0)
|
||||
'@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@radix-ui/react-id': 1.1.0(@types/react@19.0.10)(react@19.0.0)
|
||||
'@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0)
|
||||
aria-hidden: 1.2.4
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
react-remove-scroll: 2.6.3(@types/react@19.0.10)(react@19.0.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
'@types/react-dom': 19.0.4(@types/react@19.0.10)
|
||||
|
||||
'@radix-ui/react-direction@1.1.0(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
@@ -2876,6 +3049,14 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
bl@4.1.0:
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
brace-expansion@1.1.11:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
@@ -2889,6 +3070,11 @@ snapshots:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
buffer@5.7.1:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
busboy@1.6.0:
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
@@ -2914,11 +3100,18 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001701: {}
|
||||
|
||||
canvas@3.1.0:
|
||||
dependencies:
|
||||
node-addon-api: 7.1.1
|
||||
prebuild-install: 7.1.3
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chownr@1.1.4: {}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
@@ -2983,6 +3176,12 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
|
||||
deep-extend@0.6.0: {}
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
define-data-property@1.1.4:
|
||||
@@ -2999,8 +3198,7 @@ snapshots:
|
||||
|
||||
detect-libc@1.0.3: {}
|
||||
|
||||
detect-libc@2.0.3:
|
||||
optional: true
|
||||
detect-libc@2.0.3: {}
|
||||
|
||||
detect-node-es@1.1.0: {}
|
||||
|
||||
@@ -3016,6 +3214,10 @@ snapshots:
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
end-of-stream@1.4.4:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -3317,6 +3519,8 @@ snapshots:
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
expand-template@2.0.3: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-glob@3.3.1:
|
||||
@@ -3380,6 +3584,8 @@ snapshots:
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
function.prototype.name@1.1.8:
|
||||
@@ -3423,6 +3629,8 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
github-from-package@0.0.0: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -3466,6 +3674,8 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
@@ -3475,6 +3685,10 @@ snapshots:
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ini@1.3.8: {}
|
||||
|
||||
internal-slot@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -3714,6 +3928,8 @@ snapshots:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
@@ -3724,6 +3940,8 @@ snapshots:
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
mkdirp-classic@0.5.3: {}
|
||||
|
||||
motion-dom@12.5.0:
|
||||
dependencies:
|
||||
motion-utils: 12.5.0
|
||||
@@ -3742,8 +3960,15 @@ snapshots:
|
||||
|
||||
nanoid@3.3.8: {}
|
||||
|
||||
napi-build-utils@2.0.0: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
next-themes@0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
next@15.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
'@next/env': 15.2.1
|
||||
@@ -3769,6 +3994,12 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
node-abi@3.74.0:
|
||||
dependencies:
|
||||
semver: 7.7.1
|
||||
|
||||
node-addon-api@7.1.1: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
@@ -3810,6 +4041,10 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -3863,6 +4098,21 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
dependencies:
|
||||
detect-libc: 2.0.3
|
||||
expand-template: 2.0.3
|
||||
github-from-package: 0.0.0
|
||||
minimist: 1.2.8
|
||||
mkdirp-classic: 0.5.3
|
||||
napi-build-utils: 2.0.0
|
||||
node-abi: 3.74.0
|
||||
pump: 3.0.2
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.2
|
||||
tunnel-agent: 0.6.0
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prop-types@15.8.1:
|
||||
@@ -3871,10 +4121,22 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
pump@3.0.2:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.4
|
||||
once: 1.4.0
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
rc@1.2.8:
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
ini: 1.3.8
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
|
||||
react-dom@19.0.0(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
@@ -3915,6 +4177,12 @@ snapshots:
|
||||
|
||||
react@19.0.0: {}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -3965,6 +4233,8 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
isarray: 2.0.5
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safe-push-apply@1.0.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -4065,11 +4335,24 @@ snapshots:
|
||||
side-channel-map: 1.0.1
|
||||
side-channel-weakmap: 1.0.2
|
||||
|
||||
simple-concat@1.0.1: {}
|
||||
|
||||
simple-get@4.0.1:
|
||||
dependencies:
|
||||
decompress-response: 6.0.0
|
||||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
|
||||
simple-swizzle@0.2.2:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
optional: true
|
||||
|
||||
sonner@2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
stable-hash@0.0.4: {}
|
||||
@@ -4126,8 +4409,14 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
string_decoder@1.3.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
strip-bom@3.0.0: {}
|
||||
|
||||
strip-json-comments@2.0.1: {}
|
||||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
styled-jsx@5.1.6(react@19.0.0):
|
||||
@@ -4151,6 +4440,21 @@ snapshots:
|
||||
|
||||
tapable@2.2.1: {}
|
||||
|
||||
tar-fs@2.1.2:
|
||||
dependencies:
|
||||
chownr: 1.1.4
|
||||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.2
|
||||
tar-stream: 2.2.0
|
||||
|
||||
tar-stream@2.2.0:
|
||||
dependencies:
|
||||
bl: 4.1.0
|
||||
end-of-stream: 1.4.4
|
||||
fs-constants: 1.0.0
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
tinyglobby@0.2.12:
|
||||
dependencies:
|
||||
fdir: 6.4.3(picomatch@4.0.2)
|
||||
@@ -4173,6 +4477,10 @@ snapshots:
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -4240,6 +4548,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
dependencies:
|
||||
is-bigint: 1.1.0
|
||||
@@ -4286,6 +4596,8 @@ snapshots:
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod@3.24.2: {}
|
||||
|
||||
52
src/actions/auth/login.ts
Normal file
52
src/actions/auth/login.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
'use server'
|
||||
import {cookies} from 'next/headers'
|
||||
import {ApiResponse, call} from '@/lib/api'
|
||||
|
||||
export interface LoginParams {
|
||||
username: string;
|
||||
password: string;
|
||||
remember?: boolean;
|
||||
}
|
||||
|
||||
type LoginResp = {
|
||||
token: string;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
export async function login(props: LoginParams): Promise<ApiResponse> {
|
||||
try {
|
||||
// 尝试登录
|
||||
const result = await call<LoginResp>('/api/auth/login/sms', {
|
||||
username: props.username,
|
||||
password: props.password,
|
||||
remember: props.remember ?? false,
|
||||
})
|
||||
if (!result.success) {
|
||||
return result
|
||||
}
|
||||
|
||||
const data = result.data
|
||||
console.log('login', data)
|
||||
|
||||
// 计算过期时间
|
||||
const current = Math.floor(Date.now() / 1000)
|
||||
const future = data.expires - current
|
||||
|
||||
// 保存到 cookies
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set('auth_token', data.token, {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: Math.max(future, 0),
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error('请求登陆失败', {cause: e})
|
||||
}
|
||||
}
|
||||
69
src/actions/auth/verify.ts
Normal file
69
src/actions/auth/verify.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
'use server'
|
||||
// 验证验证码函数
|
||||
import {cookies} from 'next/headers'
|
||||
import crypto from 'crypto'
|
||||
import {ApiResponse, call} from '@/lib/api'
|
||||
|
||||
|
||||
export interface VerifyParams {
|
||||
phone: string;
|
||||
captcha: string; // 添加验证码字段
|
||||
}
|
||||
|
||||
export default async function verify(props: VerifyParams): Promise<ApiResponse> {
|
||||
try {
|
||||
// 人机验证
|
||||
if (!props.captcha?.length) {
|
||||
return {
|
||||
success: false,
|
||||
status: 400,
|
||||
message: '请输入验证码',
|
||||
}
|
||||
}
|
||||
const valid = await verifyCaptcha(props.captcha)
|
||||
if (!valid) {
|
||||
return {
|
||||
success: false,
|
||||
status: 400,
|
||||
message: '验证码错误或已过期',
|
||||
}
|
||||
}
|
||||
|
||||
// 请求发送短信
|
||||
return await call('/api/auth/verify/sms', {
|
||||
phone: props.phone,
|
||||
purpose: 0,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error('验证码验证失败', {cause: error})
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyCaptcha(userInput: string): Promise<boolean> {
|
||||
const cookieStore = await cookies()
|
||||
const hash = cookieStore.get('captcha_hash')?.value
|
||||
const salt = cookieStore.get('captcha_salt')?.value
|
||||
|
||||
// 如果没有找到验证码cookie,验证失败
|
||||
if (!hash || !salt) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 使用相同的方法哈希用户输入的验证码
|
||||
const userInputHash = crypto
|
||||
.createHmac('sha256', salt)
|
||||
.update(userInput.toLowerCase())
|
||||
.digest('hex')
|
||||
|
||||
// 比较哈希值
|
||||
const isValid = hash === userInputHash
|
||||
|
||||
// 验证后删除验证码cookie,防止重复使用
|
||||
if (isValid) {
|
||||
cookieStore.delete('captcha_hash')
|
||||
cookieStore.delete('captcha_salt')
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
92
src/app/(auth)/captcha/route.ts
Normal file
92
src/app/(auth)/captcha/route.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
'use server'
|
||||
import {createCanvas} from 'canvas'
|
||||
import crypto from 'crypto'
|
||||
import {cookies} from 'next/headers'
|
||||
|
||||
// 生成随机验证码
|
||||
function generateCaptchaText(length: number = 4): string {
|
||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
let result = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars[Math.floor(Math.random() * chars.length)]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 哈希验证码文本并使用随机盐值
|
||||
function hashCaptcha(text: string): { hash: string; salt: string } {
|
||||
const salt = crypto.randomBytes(16).toString('hex')
|
||||
const hash = crypto
|
||||
.createHmac('sha256', salt)
|
||||
.update(text.toLowerCase())
|
||||
.digest('hex')
|
||||
return {hash, salt}
|
||||
}
|
||||
|
||||
// 生成验证码图片
|
||||
function generateCaptchaImage(text: string) {
|
||||
const canvas = createCanvas(180, 50)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// 设置背景色
|
||||
ctx.fillStyle = '#f3f4f6'
|
||||
ctx.fillRect(0, 0, 180, 50)
|
||||
|
||||
// 绘制干扰线
|
||||
for (let i = 0; i < 3; i++) {
|
||||
ctx.strokeStyle = `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255})`
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(Math.random() * 180, Math.random() * 50)
|
||||
ctx.lineTo(Math.random() * 180, Math.random() * 50)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// 绘制文本
|
||||
ctx.font = '28px Arial'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
// 随机文本颜色和位置
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
ctx.fillStyle = `rgb(${Math.random() * 100}, ${Math.random() * 100}, ${Math.random() * 100})`
|
||||
ctx.fillText(
|
||||
text[i],
|
||||
(180 / text.length) * (i + 0.5), // 均匀分布
|
||||
25 + Math.random() * 10 - 5, // 中间位置上下浮动
|
||||
)
|
||||
}
|
||||
|
||||
return canvas.toBuffer('image/png')
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const captchaText = generateCaptchaText()
|
||||
|
||||
// 生成验证码图像
|
||||
const captchaImage = generateCaptchaImage(captchaText)
|
||||
|
||||
// 生成验证码哈希和盐值
|
||||
const {hash, salt} = hashCaptcha(captchaText)
|
||||
const store = await cookies()
|
||||
const coo = store
|
||||
.set('captcha_hash', hash, {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60,
|
||||
})
|
||||
.set('captcha_salt', salt, {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60,
|
||||
})
|
||||
|
||||
return new Response(captchaImage, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'no-store',
|
||||
'Set-Cookie': `${coo}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
87
src/app/(auth)/login/captcha.tsx
Normal file
87
src/app/(auth)/login/captcha.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import {useCallback, useEffect, useState} from 'react'
|
||||
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {Input} from '@/components/ui/input'
|
||||
|
||||
export type CaptchaProps = {
|
||||
showCaptcha: boolean
|
||||
setShowCaptcha: (show: boolean) => void
|
||||
handleSendCode: (captchaCode: string) => boolean | Promise<boolean>
|
||||
}
|
||||
|
||||
export default function Captcha(props: CaptchaProps) {
|
||||
const {showCaptcha, setShowCaptcha, handleSendCode} = props
|
||||
const [captchaImage, setCaptchaImage] = useState('/captcha?t=' + Date.now())
|
||||
const [captchaCode, setCaptchaCode] = useState('')
|
||||
|
||||
// 刷新图形验证码
|
||||
const refreshCaptcha = useCallback(() => {
|
||||
setCaptchaImage('/captcha?t=' + Date.now())
|
||||
setCaptchaCode('')
|
||||
}, [])
|
||||
|
||||
const handleVerifyCaptcha = useCallback(async () => {
|
||||
let refresh = handleSendCode(captchaCode)
|
||||
if (refresh instanceof Promise) {
|
||||
refresh = await refresh
|
||||
}
|
||||
if (refresh) {
|
||||
refreshCaptcha()
|
||||
}
|
||||
}, [captchaCode, handleSendCode, refreshCaptcha])
|
||||
|
||||
useEffect(() => {
|
||||
if (showCaptcha) {
|
||||
refreshCaptcha()
|
||||
}
|
||||
}, [showCaptcha, refreshCaptcha])
|
||||
|
||||
return (
|
||||
<Dialog open={showCaptcha} onOpenChange={setShowCaptcha}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>请完成图形验证</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<img
|
||||
src={captchaImage}
|
||||
alt="验证码"
|
||||
width={180}
|
||||
height={50}
|
||||
className="border cursor-pointer"
|
||||
onClick={refreshCaptcha}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={refreshCaptcha}
|
||||
className="text-sm"
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="请输入图形验证码"
|
||||
value={captchaCode}
|
||||
onChange={(e) => setCaptchaCode(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowCaptcha(false)}
|
||||
className="mr-2"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleVerifyCaptcha()}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,101 +1,301 @@
|
||||
'use client'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import {useState, useCallback, useRef} from 'react'
|
||||
import {Input} from '@/components/ui/input'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {Checkbox} from '@/components/ui/checkbox'
|
||||
import {merge} from '@/lib/utils'
|
||||
import Image from 'next/image'
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import logo from '@/assets/logo.webp'
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardContent,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import zod from 'zod'
|
||||
import Captcha from './captcha'
|
||||
import verify from '@/actions/auth/verify'
|
||||
import {login} from '@/actions/auth/login'
|
||||
import {useRouter} from 'next/navigation'
|
||||
import {toast} from 'sonner'
|
||||
import {ApiResponse} from '@/lib/api'
|
||||
|
||||
export type LoginPageProps = {}
|
||||
|
||||
export default function LoginPage(props: LoginPageProps) {
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
// 定义表单验证模式
|
||||
const formSchema = zod.object({
|
||||
username: zod.string().min(11, '请输入正确的手机号码').max(11, '请输入正确的手机号码'),
|
||||
password: zod.string().min(1, '请输入验证码'),
|
||||
remember: zod.boolean().default(false),
|
||||
})
|
||||
type FormValues = zod.infer<typeof formSchema>
|
||||
|
||||
const handleSendCode = () => {
|
||||
// 这里实现发送验证码的逻辑
|
||||
setCountdown(60);
|
||||
const timer = setInterval(() => {
|
||||
export default function LoginPage(props: LoginPageProps) {
|
||||
const router = useRouter()
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [countdown, setCountdown] = useState(0)
|
||||
const [showCaptcha, setShowCaptcha] = useState(false)
|
||||
const timerRef = useRef<NodeJS.Timeout>(undefined)
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
},
|
||||
})
|
||||
|
||||
// 获取表单值的快捷方式
|
||||
const username = form.watch('username')
|
||||
|
||||
// 处理短信验证码发送前的验证
|
||||
const checkUsername = useCallback(() => {
|
||||
if (!username || username.length !== 11) {
|
||||
form.setError('username', {
|
||||
type: 'manual',
|
||||
message: '请输入正确的手机号码',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 显示图形验证码
|
||||
setShowCaptcha(true)
|
||||
}, [username, form])
|
||||
|
||||
// 验证图形验证码并发送短信验证码
|
||||
const sendCode = useCallback(async (captchaCode: string) => {
|
||||
if (!captchaCode) {
|
||||
toast.error('请输入图形验证码')
|
||||
return false
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
const resp = await verify({
|
||||
phone: username,
|
||||
captcha: captchaCode,
|
||||
})
|
||||
|
||||
// 处理验证码发送结果
|
||||
let waiting = 60
|
||||
if (!resp.success) {
|
||||
if (resp.status == 429) {
|
||||
setShowCaptcha(false)
|
||||
waiting = parseInt(resp.message)
|
||||
console.log(resp.message)
|
||||
toast.error('发送频率过快', {
|
||||
description: '请稍后再试',
|
||||
})
|
||||
}
|
||||
else {
|
||||
toast.error(resp.message)
|
||||
return true
|
||||
}
|
||||
}
|
||||
else {
|
||||
setShowCaptcha(false)
|
||||
toast.success('验证码已发送', {
|
||||
description: '请注意查收短信',
|
||||
})
|
||||
}
|
||||
|
||||
// 开始倒计时
|
||||
setCountdown(waiting)
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current)
|
||||
}
|
||||
timerRef.current = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(timer);
|
||||
return 0;
|
||||
clearInterval(timerRef.current)
|
||||
return 0
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return false
|
||||
}, [username])
|
||||
|
||||
const setWaiting = (resp: ApiResponse<undefined>) => {
|
||||
|
||||
}
|
||||
|
||||
// 处理表单提交
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
try {
|
||||
setSubmitting(true)
|
||||
|
||||
// 验证表单数据
|
||||
if (values.username?.length !== 11) {
|
||||
form.setError('username', {
|
||||
type: 'manual',
|
||||
message: '请输入有效的手机号码',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!values.password) {
|
||||
form.setError('password', {
|
||||
type: 'manual',
|
||||
message: '请输入验证码',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用登录函数
|
||||
const result = await login({
|
||||
username: values.username,
|
||||
password: values.password, // 使用验证码作为密码
|
||||
remember: values.remember,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
// 登录成功
|
||||
toast.success('登陆成功', {
|
||||
description: '欢迎回来!',
|
||||
})
|
||||
|
||||
// 跳转到首页或用户仪表板
|
||||
router.push('/')
|
||||
router.refresh() // 刷新页面状态
|
||||
}
|
||||
else {
|
||||
// 登录失败
|
||||
toast.error(result.message, {
|
||||
description: '请检查您的手机号码和验证码',
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
toast.error('服务器错误', {
|
||||
description: '请稍后再试',
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<main className="h-screen w-screen lg:pr-80 bg-[url(/login/bg.webp)] bg-cover bg-left flex justify-center lg:justify-end items-center">
|
||||
<main className={merge(
|
||||
`relative`,
|
||||
`h-screen w-screen xl:pr-64 bg-[url(/login/bg.webp)] bg-cover bg-left`,
|
||||
`flex justify-center xl:justify-end items-center`,
|
||||
)}>
|
||||
<Image src={logo} alt={`logo`} height={64} className={`absolute top-8 left-8`}/>
|
||||
|
||||
{/* 登录表单 */}
|
||||
<div className="w-96 mx-4 p-8 lg:p-12 bg-white rounded-lg flex items-center justify-center">
|
||||
<div className="w-full space-y-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl text-gray-900">
|
||||
登录/注册
|
||||
</h2>
|
||||
</div>
|
||||
<Card className="w-96 mx-4 shadow-lg">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-2xl">登录/注册</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<form className="mt-8 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">手机号码</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
placeholder="请输入手机号码"
|
||||
autoComplete="tel"
|
||||
required
|
||||
/>
|
||||
<CardContent className={`px-8`}>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>手机号码</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="tel"
|
||||
placeholder="请输入手机号码"
|
||||
autoComplete="tel-national"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>验证码</FormLabel>
|
||||
<div className="flex space-x-4">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="h-12"
|
||||
placeholder="请输入验证码"
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="whitespace-nowrap h-12"
|
||||
onClick={checkUsername}
|
||||
disabled={countdown > 0}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remember"
|
||||
render={({field}) => (
|
||||
<FormItem className="flex flex-row items-start space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>保持登录</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
className="w-full h-12 text-lg"
|
||||
type="submit"
|
||||
variant="gradient"
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? '登录中...' : '注册 / 登录'}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-center text-gray-500">
|
||||
登录即表示您同意<a href="#" className="text-blue-600 hover:text-blue-500">《用户协议》</a>和<a href="#" className="text-blue-600 hover:text-blue-500">《隐私政策》</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="verificationCode">验证码</Label>
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
id="verificationCode"
|
||||
name="verificationCode"
|
||||
type="text"
|
||||
placeholder="请输入验证码"
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="whitespace-nowrap"
|
||||
onClick={handleSendCode}
|
||||
disabled={countdown > 0}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 图形验证码弹窗 */}
|
||||
<Captcha
|
||||
showCaptcha={showCaptcha}
|
||||
setShowCaptcha={setShowCaptcha}
|
||||
handleSendCode={sendCode}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="remember-me" name="remember-me" />
|
||||
<label htmlFor="remember-me" className="text-sm text-gray-900">
|
||||
保持登录
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex flex-col gap-2`}>
|
||||
<Button type="submit" className="w-full">
|
||||
注册 / 登录
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-center text-gray-500">
|
||||
登录即表示您同意<a href="#" className="text-blue-600 hover:text-blue-500">《用户协议》</a>和<a href="#" className="text-blue-600 hover:text-blue-500">《隐私政策》</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { LinkItem, MenuItem } from './_server/navs'
|
||||
import {LinkItem, MenuItem} from './_server/navs'
|
||||
import SolutionMenu from './_client/solution'
|
||||
import ProductMenu from './_client/product'
|
||||
import HelpMenu from './_client/help'
|
||||
@@ -39,9 +39,9 @@ export default function Header(props: HeaderProps) {
|
||||
const [menu, setMenu] = useState(false)
|
||||
const [page, setPage] = useState(0)
|
||||
const pages = useMemo(() => [
|
||||
<ProductMenu key={`product`} />,
|
||||
<SolutionMenu key={`solution`} />,
|
||||
<HelpMenu key={`help`} />,
|
||||
<ProductMenu key={`product`}/>,
|
||||
<SolutionMenu key={`solution`}/>,
|
||||
<HelpMenu key={`help`}/>,
|
||||
], [])
|
||||
|
||||
// ======================
|
||||
@@ -53,7 +53,6 @@ export default function Header(props: HeaderProps) {
|
||||
`fixed top-0 w-full z-10`,
|
||||
].join(' ')}>
|
||||
<div className={[
|
||||
``,
|
||||
`transition-[background, shadow] duration-200 ease-in-out`,
|
||||
menu
|
||||
? `bg-[#fffe] backdrop-blur-sm`
|
||||
@@ -65,13 +64,13 @@ export default function Header(props: HeaderProps) {
|
||||
<div className="flex justify-between gap-8">
|
||||
{/* logo */}
|
||||
<Link href="/public" className={`flex items-center`}>
|
||||
<Image src={logo} alt={`logo`} className={`w-16 max-md:w-12 h-16 max-md:h-12 rounded-full`} />
|
||||
<Image src={logo} alt={`logo`} className={`w-16 max-md:w-12 h-16 max-md:h-12 rounded-full`}/>
|
||||
</Link>
|
||||
|
||||
{/* 菜单 */}
|
||||
<nav>
|
||||
<ul className="h-full flex items-stretch max-lg:hidden">
|
||||
<LinkItem text={`首页`} href={`/`} />
|
||||
<LinkItem text={`首页`} href={`/`}/>
|
||||
<MenuItem
|
||||
text={`产品订购`}
|
||||
active={menu && page === 0}
|
||||
@@ -106,9 +105,9 @@ export default function Header(props: HeaderProps) {
|
||||
}}
|
||||
/>
|
||||
<LinkItem
|
||||
text={`企业服务`} href={`#`} />
|
||||
text={`企业服务`} href={`#`}/>
|
||||
<LinkItem
|
||||
text={`推广返利`} href={`#`} />
|
||||
text={`推广返利`} href={`#`}/>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -156,4 +155,3 @@ export default function Header(props: HeaderProps) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -4,10 +4,6 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
body {
|
||||
color: hsl(0, 0%, 10%);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
@@ -124,3 +120,7 @@ body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: hsl(0, 0%, 10%);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import {ReactNode} from 'react'
|
||||
import {Metadata} from 'next'
|
||||
import './globals.css'
|
||||
import localFont from 'next/font/local'
|
||||
import {Toaster} from '@/components/ui/sonner'
|
||||
|
||||
const font = localFont({
|
||||
src: './NotoSansSC-VariableFont_wght.ttf',
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
@@ -15,8 +20,9 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-Cn">
|
||||
<body className={`bg-blue-50`}>
|
||||
<body className={`${font.className} bg-blue-50`}>
|
||||
{children}
|
||||
<Toaster position={'top-center'}/>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
12
src/app/test/route.ts
Normal file
12
src/app/test/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {NextRequest, NextResponse} from 'next/server'
|
||||
import {cookies} from 'next/headers'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
|
||||
const store = await cookies()
|
||||
store.set('test','test')
|
||||
|
||||
return NextResponse.json(JSON.stringify({
|
||||
'test': 'value',
|
||||
}))
|
||||
}
|
||||
@@ -1,64 +1,35 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from 'react'
|
||||
import {Slot} from '@radix-ui/react-slot'
|
||||
import {merge} from '@/lib/utils'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
type ButtonProps = React.ComponentProps<'button'> & {
|
||||
variant?: 'default' | 'outline' | 'gradient'
|
||||
}
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 " +
|
||||
"whitespace-nowrap rounded-md text-sm transition-all " +
|
||||
"disabled:pointer-events-none disabled:opacity-50 " +
|
||||
"[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 " +
|
||||
"outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] " +
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
function Button(rawProps: ButtonProps) {
|
||||
const {className, variant, ...props} = rawProps
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
<button
|
||||
className={merge(
|
||||
`transition-all duration-200 ease-in-out`,
|
||||
`h-10 px-4 rounded-md cursor-pointer`,
|
||||
'whitespace-nowrap',
|
||||
'inline-flex items-center justify-center gap-2',
|
||||
'disabled:pointer-events-none disabled:opacity-50 ',
|
||||
'[&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 ',
|
||||
'outline-none focus-visible:ring-4 ring-blue-200',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
{
|
||||
gradient: 'bg-gradient-to-r from-blue-400 to-cyan-300 text-white ring-offset-2',
|
||||
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
}[variant ?? 'default'],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export {Button}
|
||||
|
||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { merge } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={merge(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={merge(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-[data-slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={merge("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={merge("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={merge(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={merge("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={merge("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { merge } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
@@ -13,7 +13,7 @@ function Checkbox({
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
className={merge(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
|
||||
135
src/components/ui/dialog.tsx
Normal file
135
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { merge } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={merge(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={merge(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={merge("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={merge(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={merge("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={merge("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type FieldValues,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { merge } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
@@ -80,7 +80,7 @@ function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
className={merge("grid gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
@@ -97,7 +97,7 @@ function FormLabel({
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
className={merge("data-[error=true]:text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
@@ -129,7 +129,7 @@ function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={merge("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -147,7 +147,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
className={merge("text-destructive text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {merge} from '@/lib/utils'
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
function Input({className, type, ...props}: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
className={merge(
|
||||
`transition-[color,box-shadow] duration-200 ease-in-out`,
|
||||
`h-10 min-w-0 w-full`,
|
||||
' placeholder:text-muted-foreground',
|
||||
'selection:bg-primary selection:text-primary-foreground',
|
||||
'flex rounded-md border bg-transparent px-3 py-1 text-base shadow-xs',
|
||||
'outline-none focus-visible:ring-4 ring-blue-200',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40 dark:bg-input/30',
|
||||
'file:text-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:disabled:pointer-events-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
export {Input}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { merge } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
@@ -12,8 +12,8 @@ function Label({
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className={merge(
|
||||
"flex items-center gap-2 leading-none select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { merge } from "@/lib/utils"
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
@@ -13,7 +13,7 @@ function RadioGroup({
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
className={merge("grid gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -26,7 +26,7 @@ function RadioGroupItem({
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
className={merge(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { merge } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
@@ -36,7 +36,7 @@ function SelectTrigger({
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
className={merge(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
@@ -60,7 +60,7 @@ function SelectContent({
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
className={merge(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
@@ -71,7 +71,7 @@ function SelectContent({
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
className={merge(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
@@ -92,7 +92,7 @@ function SelectLabel({
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("px-2 py-1.5 text-sm", className)}
|
||||
className={merge("px-2 py-1.5 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -106,7 +106,7 @@ function SelectItem({
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
className={merge(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
@@ -129,7 +129,7 @@ function SelectSeparator({
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
className={merge("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -142,7 +142,7 @@ function SelectScrollUpButton({
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
className={merge(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
@@ -160,7 +160,7 @@ function SelectScrollDownButton({
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
className={merge(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
|
||||
25
src/components/ui/sonner.tsx
Normal file
25
src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
135
src/lib/api.ts
Normal file
135
src/lib/api.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
// API工具函数
|
||||
|
||||
// 定义后端服务URL和OAuth2配置
|
||||
const API_BASE_URL = process.env.API_BASE_URL
|
||||
const CLIENT_ID = process.env.CLIENT_ID
|
||||
const CLIENT_SECRET = process.env.CLIENT_SECRET
|
||||
|
||||
// OAuth令牌缓存
|
||||
interface TokenCache {
|
||||
token: string
|
||||
expires: number // 过期时间戳
|
||||
}
|
||||
|
||||
let tokenCache: TokenCache | null = null
|
||||
|
||||
// 获取OAuth2访问令牌
|
||||
export async function getAccessToken(forceRefresh = false): Promise<string> {
|
||||
try {
|
||||
// 检查缓存的令牌是否可用
|
||||
if (!forceRefresh && tokenCache && tokenCache.expires > Date.now()) {
|
||||
return tokenCache.token
|
||||
}
|
||||
|
||||
const addr = `http://${API_BASE_URL}/api/auth/token`
|
||||
const body = {
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
grant_type: 'client_credentials',
|
||||
}
|
||||
|
||||
const response = await fetch(addr, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OAuth token request failed: ${response.status} ${await response.text()}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// 缓存令牌和过期时间
|
||||
// 通常后端会返回expires_in(秒为单位)
|
||||
tokenCache = {
|
||||
token: data.access_token,
|
||||
expires: Date.now() + data.expires_in * 1000,
|
||||
}
|
||||
|
||||
return tokenCache.token
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to get access token:', error)
|
||||
throw new Error('认证服务暂时不可用')
|
||||
}
|
||||
}
|
||||
|
||||
// 通用的API调用函数
|
||||
export async function call<R = undefined>(endpoint: string, data: unknown): Promise<ApiResponse<R>> {
|
||||
try {
|
||||
// 发送请求
|
||||
let accessToken = getAccessToken()
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${await accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
let response = await fetch(`http://${API_BASE_URL}${endpoint}`, requestOptions)
|
||||
|
||||
// 如果返回401未授权,尝试刷新令牌并重试一次
|
||||
if (response.status === 401) {
|
||||
accessToken = getAccessToken(true) // 强制刷新令牌
|
||||
|
||||
// 使用新令牌重试请求
|
||||
requestOptions.headers['Authorization'] = `Bearer ${await accessToken}`
|
||||
response = await fetch(`http://${API_BASE_URL}${endpoint}`, requestOptions)
|
||||
}
|
||||
|
||||
// 解析响应数据
|
||||
const type = response.headers.get('Content-Type') ?? 'text/plain'
|
||||
if (type.indexOf('application/json') !== -1) {
|
||||
const json = await response.json()
|
||||
if (!response.ok) {
|
||||
console.log('响应不成功', `status=${response.status}`, json)
|
||||
return {
|
||||
success: false,
|
||||
status: response.status,
|
||||
message: json.message || '请求失败',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: json,
|
||||
}
|
||||
}
|
||||
else if (type.indexOf('text/plain') !== -1) {
|
||||
const text = await response.text()
|
||||
if (!response.ok) {
|
||||
console.log('响应不成功', `status=${response.status}`, text)
|
||||
return {
|
||||
success: false,
|
||||
status: response.status,
|
||||
message: text || '请求失败',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: undefined as unknown as R, // 强转类型,考虑优化
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error(`无法解析响应数据,未处理的 Content-Type: ${type}`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('API call failed:', e)
|
||||
throw new Error('服务调用失败', {cause: e})
|
||||
}
|
||||
}
|
||||
|
||||
// 统一的API响应类型
|
||||
export type ApiResponse<T = undefined> = {
|
||||
success: false
|
||||
status: number
|
||||
message: string
|
||||
} | {
|
||||
success: true
|
||||
data: T
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import {ClassNameValue, twMerge} from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
export function merge(...inputs: ClassNameValue[]) {
|
||||
return twMerge(inputs)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user