完善认证功能,添加实名认证和回调处理逻辑

This commit is contained in:
2025-04-16 09:43:12 +08:00
parent 1f1f603523
commit d435d98887
8 changed files with 502 additions and 10 deletions

View File

@@ -29,6 +29,7 @@
"motion": "^12.5.0",
"next": "15.2.1",
"next-themes": "^0.4.6",
"qrcode": "^1.5.4",
"react": "^19.0.0",
"react-day-picker": "8.10.1",
"react-dom": "^19.0.0",
@@ -44,6 +45,7 @@
"@stylistic/eslint-plugin": "^4.2.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/qrcode": "^1.5.5",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",

199
pnpm-lock.yaml generated
View File

@@ -68,6 +68,9 @@ importers:
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
qrcode:
specifier: ^1.5.4
version: 1.5.4
react:
specifier: ^19.0.0
version: 19.0.0
@@ -108,6 +111,9 @@ importers:
'@types/node':
specifier: ^20
version: 20.17.23
'@types/qrcode':
specifier: ^1.5.5
version: 1.5.5
'@types/react':
specifier: ^19
version: 19.0.10
@@ -1084,6 +1090,9 @@ packages:
'@types/node@20.17.23':
resolution: {integrity: sha512-8PCGZ1ZJbEZuYNTMqywO+Sj4vSKjSjT6Ua+6RFOYlEvIvKQABPtrNkoVSLSKDb4obYcMhspVKmsw8Cm10NFRUg==}
'@types/qrcode@1.5.5':
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
'@types/react-dom@19.0.4':
resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==}
peerDependencies:
@@ -1152,6 +1161,10 @@ packages:
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@@ -1260,6 +1273,10 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
caniuse-lite@1.0.30001701:
resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==}
@@ -1280,6 +1297,9 @@ packages:
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -1349,6 +1369,10 @@ packages:
supports-color:
optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@@ -1380,6 +1404,9 @@ packages:
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
@@ -1388,6 +1415,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
@@ -1590,6 +1620,10 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -1632,6 +1666,10 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -1778,6 +1816,10 @@ packages:
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
engines: {node: '>= 0.4'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-generator-function@1.1.0:
resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==}
engines: {node: '>= 0.4'}
@@ -1954,6 +1996,10 @@ packages:
resolution: {integrity: sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==}
engines: {node: '>= 12.0.0'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -2110,14 +2156,26 @@ packages:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -2144,6 +2202,10 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -2175,6 +2237,11 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -2248,6 +2315,13 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -2298,6 +2372,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -2364,6 +2441,10 @@ packages:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string.prototype.includes@2.0.1:
resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==}
engines: {node: '>= 0.4'}
@@ -2390,6 +2471,10 @@ packages:
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
@@ -2538,6 +2623,9 @@ packages:
resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
engines: {node: '>= 0.4'}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which-typed-array@1.1.18:
resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==}
engines: {node: '>= 0.4'}
@@ -2551,9 +2639,24 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@@ -3375,6 +3478,10 @@ snapshots:
dependencies:
undici-types: 6.19.8
'@types/qrcode@1.5.5':
dependencies:
'@types/node': 20.17.23
'@types/react-dom@19.0.4(@types/react@19.0.10)':
dependencies:
'@types/react': 19.0.10
@@ -3473,6 +3580,8 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
@@ -3612,6 +3721,8 @@ snapshots:
callsites@3.1.0: {}
camelcase@5.3.1: {}
caniuse-lite@1.0.30001701: {}
canvas@3.1.0:
@@ -3632,6 +3743,12 @@ snapshots:
client-only@0.0.1: {}
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
clsx@2.1.1: {}
cmdk@1.1.1(@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):
@@ -3704,6 +3821,8 @@ snapshots:
dependencies:
ms: 2.1.3
decamelize@1.2.0: {}
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
@@ -3730,6 +3849,8 @@ snapshots:
detect-node-es@1.1.0: {}
dijkstrajs@1.0.3: {}
doctrine@2.1.0:
dependencies:
esutils: 2.0.3
@@ -3740,6 +3861,8 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
end-of-stream@1.4.4:
@@ -4087,6 +4210,11 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -4127,6 +4255,8 @@ snapshots:
functions-have-names@1.2.3: {}
get-caller-file@2.0.5: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -4276,6 +4406,8 @@ snapshots:
dependencies:
call-bound: 1.0.4
is-fullwidth-code-point@3.0.0: {}
is-generator-function@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -4433,6 +4565,10 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.29.1
lightningcss-win32-x64-msvc: 1.29.1
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -4588,14 +4724,24 @@ snapshots:
object-keys: 1.1.1
safe-push-apply: 1.0.0
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
p-try@2.2.0: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -4612,6 +4758,8 @@ snapshots:
picomatch@4.0.2: {}
pngjs@5.0.0: {}
possible-typed-array-names@1.1.0: {}
postcss@8.4.31:
@@ -4656,6 +4804,12 @@ snapshots:
punycode@2.3.1: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
queue-microtask@1.2.3: {}
rc@1.2.8:
@@ -4736,6 +4890,10 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
require-directory@2.1.1: {}
require-main-filename@2.0.0: {}
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -4785,6 +4943,8 @@ snapshots:
semver@7.7.1: {}
set-blocking@2.0.0: {}
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -4892,6 +5052,12 @@ snapshots:
streamsearch@1.1.0: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string.prototype.includes@2.0.1:
dependencies:
call-bind: 1.0.8
@@ -4946,6 +5112,10 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-bom@3.0.0: {}
strip-json-comments@2.0.1: {}
@@ -5114,6 +5284,8 @@ snapshots:
is-weakmap: 2.0.2
is-weakset: 2.0.4
which-module@2.0.1: {}
which-typed-array@1.1.18:
dependencies:
available-typed-arrays: 1.0.7
@@ -5129,8 +5301,35 @@ snapshots:
word-wrap@1.2.5: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrappy@1.0.2: {}
y18n@4.0.3: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
yocto-queue@0.1.0: {}
zod@3.24.2: {}

View File

@@ -0,0 +1,21 @@
import {callByUser, callPublic} from '@/actions/base'
export async function Identify(props: {
type: number
name: string
iden_no: string
}) {
return await callByUser<{
identified: boolean
target: string
}>('/api/user/identify', props)
}
export async function IdentifyCallback(props: {
id: string
}) {
return await callPublic<{
success: boolean
message: string
}>('/api/user/identify/callback', props)
}

View File

@@ -238,6 +238,48 @@ async function callByUser<R = undefined>(
// endregion
// ======================
// region no token
// ======================
// 不需要令牌的公共API调用函数
async function callPublic<R = undefined>(
endpoint: string,
data?: unknown,
): Promise<ApiResponse<R>> {
try {
// 发送请求
const requestOptions: RequestInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, requestOptions)
// 检查响应状态
if (!response.ok) {
console.log('公共接口响应不成功', `status=${response.status}`, await response.text())
return {
status: response.status,
success: false,
message: response.status >= 500 ? '服务器错误' : '请求失败',
}
}
return handleResponse(response)
}
catch (e) {
console.error('Public API call failed:', e)
throw new Error('服务调用失败', { cause: e })
}
}
// endregion
// 统一响应解析
async function handleResponse<R = undefined>(response: Response): Promise<ApiResponse<R>> {
@@ -286,4 +328,5 @@ export {
getUserToken,
callByDevice,
callByUser,
callPublic,
}

View File

@@ -0,0 +1,76 @@
'use client'
import {useEffect, useState} from 'react'
import {useSearchParams} from 'next/navigation'
import {IdentifyCallback} from '@/actions/auth/identify'
import {Card, CardContent} from '@/components/ui/card'
import {CheckCircle, AlertCircle, Loader2} from 'lucide-react'
export type PageProps = {}
export default function Page(props: PageProps) {
const params = useSearchParams()
const success = params.get('success') === 'true'
const id = params.get('id') || ''
const [result, setResult] = useState({
status: 'load',
message: '处理中',
})
useEffect(() => {
if (success) {
IdentifyCallback({id}).then(resp => {
if (!resp.success) {
setResult({
status: 'fail',
message: resp.message || '获取活体检测结果失败',
})
}
else {
const data = resp.data
setResult({
status: data.success ? 'done' : 'fail',
message: data.message || data.success
? '认证已完成'
: '认证失败',
})
}
})
}
else {
setResult({
status: 'fail',
message: '未完成认证',
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<div className={`w-full min-h-screen flex justify-center items-center bg-blue-50`}>
<Card className="w-full max-w-xs border-none shadow-none bg-white -translate-y-1/3">
<CardContent className="flex flex-col items-center gap-4 py-6">
{result.status === 'load' ? (<>
<Loader2 className="w-16 h-16 text-primary animate-spin"/>
<p className={`text-primary text-xl`}>{result.message}</p>
<p className={`text-weak text-sm`}>
</p>
</>) : result.status === 'done' ? (<>
<CheckCircle className="w-16 h-16 text-done"/>
<p className={`text-done text-xl`}>{result.message}</p>
<p className={`text-weak text-sm`}>
</p>
</>) : (<>
<AlertCircle className="w-16 h-16 text-fail"/>
<p className={`text-fail text-xl`}>{result.message}</p>
<p className={`text-weak text-sm`}>
</p>
</>)}
</CardContent>
</Card>
</div>
)
}

View File

@@ -1,12 +1,102 @@
'use client'
import {Button} from '@/components/ui/button'
import banner from './_assets/banner.webp'
import personal from './_assets/personal.webp'
import Image from 'next/image'
import Page from '@/components/page'
import {Dialog, DialogContent, DialogFooter, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
import {Form, FormField} from '@/components/ui/form'
import {useForm} from 'react-hook-form'
import zod from 'zod'
import {zodResolver} from '@hookform/resolvers/zod'
import {Identify} from '@/actions/auth/identify'
import {toast} from 'sonner'
import {useContext, useEffect, useRef, useState} from 'react'
import * as qrcode from 'qrcode'
import {AuthContext} from '@/components/providers/AuthProvider'
export type IdentifyPageProps = {}
export default async function IdentifyPage(props: IdentifyPageProps) {
export default function IdentifyPage(props: IdentifyPageProps) {
// ======================
// 填写信息
// ======================
const schema = zod.object({
type: zod.enum([`personal`, `enterprise`], {errorMap: () => ({message: `请选择认证类型`})}).default('personal'),
name: zod.string().min(2, {message: `姓名至少2个字符`}),
iden_no: zod.string().length(18, {message: `身份证号码必须为18位`}),
})
type Schema = zod.infer<typeof schema>
const form = useForm<Schema>({
resolver: zodResolver(schema),
defaultValues: {
type: `personal`,
name: ``,
iden_no: ``,
},
})
const handler = form.handleSubmit(
async (value) => {
const resp = await Identify({
type: value.type === `personal` ? 1 : 2,
name: value.name,
iden_no: value.iden_no,
})
if (resp.success) {
form.reset()
if (!resp.data?.identified) {
setStep('scan')
setTarget(resp.data.target)
}
else {
toast.success('认证已完成')
}
}
else {
console.log(resp.message)
toast.error(`认证信息提交失败:请稍后重试`)
}
},
)
// ======================
// 扫码认证
// ======================
const [step, setStep] = useState<'form' | 'scan'>('form')
const [target, setTarget] = useState('')
const [openDialog, setOpenDialog] = useState(false)
const canvas = useRef<HTMLCanvasElement>(null)
useEffect(() => {
if (canvas.current && target) {
qrcode.toCanvas(canvas.current, target, {
width: 256,
margin: 0,
}, (error) => {
if (error) {
console.error(error)
toast.error(`二维码生成失败:请稍后重试`)
}
})
}
}, [target])
// ======================
// 用户数据
// ======================
const ctx = useContext(AuthContext)
console.log('render identify page')
// ======================
// render
// ======================
return (
<Page mode={`blank`}>
<div className={`flex-3/4 flex flex-col bg-white rounded-lg overflow-hidden gap-16`}>
@@ -26,7 +116,62 @@ export default async function IdentifyPage(props: IdentifyPageProps) {
<h3 className={`text-center text-lg font-bold`}></h3>
<p className={`text-sm text-gray-600`}></p>
</div>
<Button className={`w-full`}></Button>
{ctx.profile?.id_token ? (
<div className={`flex flex-col gap-4`}>
<p className={`text-sm text-gray-600`}></p>
</div>
) : (
<Dialog open={openDialog} onOpenChange={setOpenDialog}>
<DialogTrigger asChild>
<Button className={`w-full`}></Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>
{step === 'form' ? `实名认证` : `扫码完成认证`}
</DialogTitle>
{step === 'form' && (
<Form form={form} handler={handler} className={`flex flex-col gap-4`}>
<FormField<Schema> name={`name`} label={`姓名`}>
{({id, field}) => (
<input
{...field}
id={id}
placeholder={`请输入姓名`}
className={`border rounded p-2 w-full`}
autoComplete={`name`}
/>
)}
</FormField>
<FormField<Schema> name={`iden_no`} label={`身份证号`}>
{({id, field}) => (
<input
{...field}
id={id}
placeholder={`请输入身份证号`}
className={`border rounded p-2 w-full`}
/>
)}
</FormField>
<DialogFooter>
<Button type={`submit`} className={`w-full mt-4`}></Button>
</DialogFooter>
</Form>
)}
{step === 'scan' && (
<div className={`flex flex-col gap-4 items-center`}>
<canvas ref={canvas} width={256} height={256}/>
<p className={`text-sm text-gray-600`}></p>
<Button onClick={async () => {
await ctx.refreshProfile()
setOpenDialog(false)
}}>
</Button>
</div>
)}
</DialogContent>
</Dialog>
)}
</section>
</div>
</div>

View File

@@ -32,16 +32,16 @@ export default async function DashboardLayout(props: DashboardLayoutProps) {
<header className={`flex-none basis-20 flex items-stretch`}>
{/* logo */}
<div className={`flex-none basis-60 flex items-center justify-center`}>
<Image src={logo} alt={`logo`} height={40}/>
<Link href={'/'}><Image src={logo} alt={`logo`} height={40}/></Link>
</div>
{/* title */}
<div className={`flex-auto overflow-hidden flex items-center`}>
<div className={`flex-auto overflow-hidden flex items-center pl-4`}>
</div>
{/* profile */}
<div className={`flex-none basis-80 flex items-center justify-end`}>
<div className={`flex-none basis-80 flex items-center justify-end pr-4`}>
<Profile/>
</div>
</header>

View File

@@ -14,25 +14,31 @@ import {
import {merge} from '@/lib/utils'
import {Label} from '@/components/ui/label'
import {BaseSyntheticEvent, ComponentProps, createContext, ReactNode, useContext, useId} from 'react'
import React, {BaseSyntheticEvent, ComponentProps, createContext, ReactNode, useContext, useId} from 'react'
type FormProps<T extends FieldValues> = {
form: UseFormReturn<T>
onSubmit: SubmitHandler<T>
onSubmit?: SubmitHandler<T>
onError?: SubmitErrorHandler<T>
handler?: (e?: React.BaseSyntheticEvent) => Promise<void>
} & Omit<ComponentProps<'form'>, 'onSubmit' | 'onError'>
function Form<T extends FieldValues>(rawProps: FormProps<T>) {
const {children, onSubmit, onError, ...props} = rawProps
const {children, onSubmit, onError, handler, ...props} = rawProps
const form = props.form
const handle = handler || form.handleSubmit(
onSubmit || (_ => {}),
onError,
)
return (
<FormProvider {...form}>
<form {...props} onSubmit={event => {
<form {...props} onSubmit={async event => {
event.preventDefault()
form.handleSubmit(onSubmit, onError)(event)
event.stopPropagation()
await handle(event)
}}>
{children}
</form>