feat(core): 增加面具配置和语言切换
This commit is contained in:
parent
46c30795c3
commit
d571477a0e
1
.env
1
.env
|
|
@ -12,6 +12,7 @@ NEXT_PUBLIC_CHAT_API_URL=http://54.223.196.180
|
||||||
|
|
||||||
# 三方登录
|
# 三方登录
|
||||||
NEXT_PUBLIC_DISCORD_CLIENT_ID=1448143535609217076
|
NEXT_PUBLIC_DISCORD_CLIENT_ID=1448143535609217076
|
||||||
|
NEXT_PUBLIC_GOOGLE_CLIENT_ID=754103853450-vn4af37tmguerut5a7abo54jr87mv09k.apps.googleusercontent.com
|
||||||
|
|
||||||
# 前端回调地址
|
# 前端回调地址
|
||||||
NEXT_PUBLIC_APP_URL=http://54.223.196.180:3000
|
NEXT_PUBLIC_APP_URL=http://54.223.196.180:3000
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
"lamejs": "^1.2.1",
|
"lamejs": "^1.2.1",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "16.0.8",
|
"next": "16.0.8",
|
||||||
|
"next-intl": "^4.6.1",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"qs": "^6.14.0",
|
"qs": "^6.14.0",
|
||||||
|
|
@ -63,7 +64,6 @@
|
||||||
"react-virtuoso": "^4.17.0",
|
"react-virtuoso": "^4.17.0",
|
||||||
"sonner": "^2.0.6",
|
"sonner": "^2.0.6",
|
||||||
"stream-chat": "^9.27.0",
|
"stream-chat": "^9.27.0",
|
||||||
"swiper": "^12.0.3",
|
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.0.5",
|
"zod": "^4.0.5",
|
||||||
|
|
|
||||||
416
pnpm-lock.yaml
416
pnpm-lock.yaml
|
|
@ -122,6 +122,9 @@ importers:
|
||||||
next:
|
next:
|
||||||
specifier: 16.0.8
|
specifier: 16.0.8
|
||||||
version: 16.0.8(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
version: 16.0.8(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
||||||
|
next-intl:
|
||||||
|
specifier: ^4.6.1
|
||||||
|
version: 4.6.1(next@16.0.8(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.8.3)
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
version: 0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
||||||
|
|
@ -158,9 +161,6 @@ importers:
|
||||||
stream-chat:
|
stream-chat:
|
||||||
specifier: ^9.27.0
|
specifier: ^9.27.0
|
||||||
version: 9.27.0
|
version: 9.27.0
|
||||||
swiper:
|
|
||||||
specifier: ^12.0.3
|
|
||||||
version: 12.0.3
|
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
version: 3.3.1
|
version: 3.3.1
|
||||||
|
|
@ -242,7 +242,7 @@ importers:
|
||||||
version: 27.0.2
|
version: 27.0.2
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.2
|
specifier: ^10.9.2
|
||||||
version: 10.9.2(@types/node@20.19.8)(typescript@5.8.3)
|
version: 10.9.2(@swc/core@1.15.7)(@types/node@20.19.8)(typescript@5.8.3)
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.20.6
|
specifier: ^4.20.6
|
||||||
version: 4.20.6
|
version: 4.20.6
|
||||||
|
|
@ -793,6 +793,24 @@ packages:
|
||||||
'@floating-ui/utils@0.2.10':
|
'@floating-ui/utils@0.2.10':
|
||||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||||
|
|
||||||
|
'@formatjs/ecma402-abstract@2.3.6':
|
||||||
|
resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==}
|
||||||
|
|
||||||
|
'@formatjs/fast-memoize@2.2.7':
|
||||||
|
resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==}
|
||||||
|
|
||||||
|
'@formatjs/icu-messageformat-parser@2.11.4':
|
||||||
|
resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==}
|
||||||
|
|
||||||
|
'@formatjs/icu-skeleton-parser@1.8.16':
|
||||||
|
resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==}
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.5.10':
|
||||||
|
resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.6.2':
|
||||||
|
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
|
||||||
|
|
||||||
'@gulpjs/to-absolute-glob@4.0.0':
|
'@gulpjs/to-absolute-glob@4.0.0':
|
||||||
resolution: {integrity: sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==}
|
resolution: {integrity: sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
@ -1107,6 +1125,88 @@ packages:
|
||||||
'@open-draft/until@2.1.0':
|
'@open-draft/until@2.1.0':
|
||||||
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
||||||
|
|
||||||
|
'@parcel/watcher-android-arm64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-arm64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-x64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@parcel/watcher-freebsd-x64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-glibc@2.5.1':
|
||||||
|
resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||||
|
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||||
|
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||||
|
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||||
|
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||||
|
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-arm64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-ia32@2.5.1':
|
||||||
|
resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-x64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher@2.5.1':
|
||||||
|
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
'@pkgr/core@0.2.9':
|
'@pkgr/core@0.2.9':
|
||||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
@ -1570,6 +1670,9 @@ packages:
|
||||||
'@rtsao/scc@1.1.0':
|
'@rtsao/scc@1.1.0':
|
||||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||||
|
|
||||||
|
'@schummar/icu-type-parser@1.21.5':
|
||||||
|
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
|
||||||
|
|
||||||
'@sindresorhus/merge-streams@4.0.0':
|
'@sindresorhus/merge-streams@4.0.0':
|
||||||
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -1789,9 +1892,84 @@ packages:
|
||||||
'@standard-schema/utils@0.3.0':
|
'@standard-schema/utils@0.3.0':
|
||||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||||
|
|
||||||
|
'@swc/core-darwin-arm64@1.15.7':
|
||||||
|
resolution: {integrity: sha512-+hNVUfezUid7LeSHqnhoC6Gh3BROABxjlDNInuZ/fie1RUxaEX4qzDwdTgozJELgHhvYxyPIg1ro8ibnKtgO4g==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@swc/core-darwin-x64@1.15.7':
|
||||||
|
resolution: {integrity: sha512-ZAFuvtSYZTuXPcrhanaD5eyp27H8LlDzx2NAeVyH0FchYcuXf0h5/k3GL9ZU6Jw9eQ63R1E8KBgpXEJlgRwZUQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@swc/core-linux-arm-gnueabihf@1.15.7':
|
||||||
|
resolution: {integrity: sha512-K3HTYocpqnOw8KcD8SBFxiDHjIma7G/X+bLdfWqf+qzETNBrzOub/IEkq9UaeupaJiZJkPptr/2EhEXXWryS/A==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@swc/core-linux-arm64-gnu@1.15.7':
|
||||||
|
resolution: {integrity: sha512-HCnVIlsLnCtQ3uXcXgWrvQ6SAraskLA9QJo9ykTnqTH6TvUYqEta+TdTdGjzngD6TOE7XjlAiUs/RBtU8Z0t+Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@swc/core-linux-arm64-musl@1.15.7':
|
||||||
|
resolution: {integrity: sha512-/OOp9UZBg4v2q9+x/U21Jtld0Wb8ghzBScwhscI7YvoSh4E8RALaJ1msV8V8AKkBkZH7FUAFB7Vbv0oVzZsezA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@swc/core-linux-x64-gnu@1.15.7':
|
||||||
|
resolution: {integrity: sha512-VBbs4gtD4XQxrHuQ2/2+TDZpPQQgrOHYRnS6SyJW+dw0Nj/OomRqH+n5Z4e/TgKRRbieufipeIGvADYC/90PYQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@swc/core-linux-x64-musl@1.15.7':
|
||||||
|
resolution: {integrity: sha512-kVuy2unodso6p0rMauS2zby8/bhzoGRYxBDyD6i2tls/fEYAE74oP0VPFzxIyHaIjK1SN6u5TgvV9MpyJ5xVug==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@swc/core-win32-arm64-msvc@1.15.7':
|
||||||
|
resolution: {integrity: sha512-uddYoo5Xmo1XKLhAnh4NBIyy5d0xk33x1sX3nIJboFySLNz878ksCFCZ3IBqrt1Za0gaoIWoOSSSk0eNhAc/sw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@swc/core-win32-ia32-msvc@1.15.7':
|
||||||
|
resolution: {integrity: sha512-rqq8JjNMLx3QNlh0aPTtN/4+BGLEHC94rj9mkH1stoNRf3ra6IksNHMHy+V1HUqElEgcZyx+0yeXx3eLOTcoFw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@swc/core-win32-x64-msvc@1.15.7':
|
||||||
|
resolution: {integrity: sha512-4BK06EGdPnuplgcNhmSbOIiLdRgHYX3v1nl4HXo5uo4GZMfllXaCyBUes+0ePRfwbn9OFgVhCWPcYYjMT6hycQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@swc/core@1.15.7':
|
||||||
|
resolution: {integrity: sha512-kTGB8XI7P+pTKW83tnUEDVP4zduF951u3UAOn5eTi0vyW6MvL56A3+ggMdfuVFtDI0/DsbSzf5z34HVBbuScWw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@swc/helpers': '>=0.5.17'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@swc/helpers':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@swc/counter@0.1.3':
|
||||||
|
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||||
|
|
||||||
|
'@swc/types@0.1.25':
|
||||||
|
resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
|
||||||
|
|
||||||
'@tailwindcss/node@4.1.11':
|
'@tailwindcss/node@4.1.11':
|
||||||
resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==}
|
resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==}
|
||||||
|
|
||||||
|
|
@ -2500,6 +2678,11 @@ packages:
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
|
detect-libc@1.0.3:
|
||||||
|
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
||||||
|
engines: {node: '>=0.10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
detect-libc@2.1.2:
|
detect-libc@2.1.2:
|
||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
@ -3003,6 +3186,9 @@ packages:
|
||||||
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
|
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
|
||||||
deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.
|
deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.
|
||||||
|
|
||||||
|
intl-messageformat@10.7.18:
|
||||||
|
resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==}
|
||||||
|
|
||||||
is-array-buffer@3.0.5:
|
is-array-buffer@3.0.5:
|
||||||
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -3435,6 +3621,23 @@ packages:
|
||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
negotiator@1.0.0:
|
||||||
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
next-intl-swc-plugin-extractor@4.6.1:
|
||||||
|
resolution: {integrity: sha512-+HHNeVERfSvuPDF7LYVn3pxst5Rf7EYdUTw7C7WIrYhcLaKiZ1b9oSRkTQddAN3mifDMCfHqO4kAQ/pcKiBl3A==}
|
||||||
|
|
||||||
|
next-intl@4.6.1:
|
||||||
|
resolution: {integrity: sha512-KlWgWtKLBPUsTPgxqwyjws1wCMD2QKxLlVjeeGj53DC1JWfKmBShKOrhIP0NznZrRQ0GleeoDUeHSETmyyIFeA==}
|
||||||
|
peerDependencies:
|
||||||
|
next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||||
|
typescript: ^5.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
next-themes@0.4.6:
|
next-themes@0.4.6:
|
||||||
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -3462,6 +3665,9 @@ packages:
|
||||||
sass:
|
sass:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
node-addon-api@7.1.1:
|
||||||
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
node-releases@2.0.27:
|
node-releases@2.0.27:
|
||||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||||
|
|
||||||
|
|
@ -3575,6 +3781,9 @@ packages:
|
||||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
po-parser@2.0.0:
|
||||||
|
resolution: {integrity: sha512-SZvoKi3PoI/hHa2V9je9CW7Xgxl4dvO74cvaa6tWShIHT51FkPxje6pt0gTJznJrU67ix91nDaQp2hUxkOYhKA==}
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0:
|
possible-typed-array-names@1.1.0:
|
||||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -4029,10 +4238,6 @@ packages:
|
||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
swiper@12.0.3:
|
|
||||||
resolution: {integrity: sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==}
|
|
||||||
engines: {node: '>= 4.7.0'}
|
|
||||||
|
|
||||||
synckit@0.11.11:
|
synckit@0.11.11:
|
||||||
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
|
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
@ -4196,6 +4401,11 @@ packages:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
use-intl@4.6.1:
|
||||||
|
resolution: {integrity: sha512-mUIj6QvJZ7Rk33mLDxRziz1YiBBAnIji8YW4TXXMdYHtaPEbVucrXD3iKQGAqJhbVn0VnjrEtIKYO1B18mfSJw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||||
|
|
||||||
use-sidecar@1.1.3:
|
use-sidecar@1.1.3:
|
||||||
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -5253,6 +5463,36 @@ snapshots:
|
||||||
|
|
||||||
'@floating-ui/utils@0.2.10': {}
|
'@floating-ui/utils@0.2.10': {}
|
||||||
|
|
||||||
|
'@formatjs/ecma402-abstract@2.3.6':
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/fast-memoize': 2.2.7
|
||||||
|
'@formatjs/intl-localematcher': 0.6.2
|
||||||
|
decimal.js: 10.6.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/fast-memoize@2.2.7':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/icu-messageformat-parser@2.11.4':
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/ecma402-abstract': 2.3.6
|
||||||
|
'@formatjs/icu-skeleton-parser': 1.8.16
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/icu-skeleton-parser@1.8.16':
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/ecma402-abstract': 2.3.6
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.5.10':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.6.2':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@gulpjs/to-absolute-glob@4.0.0':
|
'@gulpjs/to-absolute-glob@4.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
is-negated-glob: 1.0.0
|
is-negated-glob: 1.0.0
|
||||||
|
|
@ -5501,6 +5741,66 @@ snapshots:
|
||||||
|
|
||||||
'@open-draft/until@2.1.0': {}
|
'@open-draft/until@2.1.0': {}
|
||||||
|
|
||||||
|
'@parcel/watcher-android-arm64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-arm64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-x64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-freebsd-x64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-glibc@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-arm64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-ia32@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-x64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher@2.5.1':
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 1.0.3
|
||||||
|
is-glob: 4.0.3
|
||||||
|
micromatch: 4.0.8
|
||||||
|
node-addon-api: 7.1.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@parcel/watcher-android-arm64': 2.5.1
|
||||||
|
'@parcel/watcher-darwin-arm64': 2.5.1
|
||||||
|
'@parcel/watcher-darwin-x64': 2.5.1
|
||||||
|
'@parcel/watcher-freebsd-x64': 2.5.1
|
||||||
|
'@parcel/watcher-linux-arm-glibc': 2.5.1
|
||||||
|
'@parcel/watcher-linux-arm-musl': 2.5.1
|
||||||
|
'@parcel/watcher-linux-arm64-glibc': 2.5.1
|
||||||
|
'@parcel/watcher-linux-arm64-musl': 2.5.1
|
||||||
|
'@parcel/watcher-linux-x64-glibc': 2.5.1
|
||||||
|
'@parcel/watcher-linux-x64-musl': 2.5.1
|
||||||
|
'@parcel/watcher-win32-arm64': 2.5.1
|
||||||
|
'@parcel/watcher-win32-ia32': 2.5.1
|
||||||
|
'@parcel/watcher-win32-x64': 2.5.1
|
||||||
|
|
||||||
'@pkgr/core@0.2.9': {}
|
'@pkgr/core@0.2.9': {}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1': {}
|
'@radix-ui/number@1.1.1': {}
|
||||||
|
|
@ -5959,6 +6259,8 @@ snapshots:
|
||||||
|
|
||||||
'@rtsao/scc@1.1.0': {}
|
'@rtsao/scc@1.1.0': {}
|
||||||
|
|
||||||
|
'@schummar/icu-type-parser@1.21.5': {}
|
||||||
|
|
||||||
'@sindresorhus/merge-streams@4.0.0': {}
|
'@sindresorhus/merge-streams@4.0.0': {}
|
||||||
|
|
||||||
'@smithy/abort-controller@4.0.4':
|
'@smithy/abort-controller@4.0.4':
|
||||||
|
|
@ -6296,10 +6598,62 @@ snapshots:
|
||||||
|
|
||||||
'@standard-schema/utils@0.3.0': {}
|
'@standard-schema/utils@0.3.0': {}
|
||||||
|
|
||||||
|
'@swc/core-darwin-arm64@1.15.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@swc/core-darwin-x64@1.15.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@swc/core-linux-arm-gnueabihf@1.15.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@swc/core-linux-arm64-gnu@1.15.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@swc/core-linux-arm64-musl@1.15.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@swc/core-linux-x64-gnu@1.15.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@swc/core-linux-x64-musl@1.15.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@swc/core-win32-arm64-msvc@1.15.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@swc/core-win32-ia32-msvc@1.15.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@swc/core-win32-x64-msvc@1.15.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@swc/core@1.15.7':
|
||||||
|
dependencies:
|
||||||
|
'@swc/counter': 0.1.3
|
||||||
|
'@swc/types': 0.1.25
|
||||||
|
optionalDependencies:
|
||||||
|
'@swc/core-darwin-arm64': 1.15.7
|
||||||
|
'@swc/core-darwin-x64': 1.15.7
|
||||||
|
'@swc/core-linux-arm-gnueabihf': 1.15.7
|
||||||
|
'@swc/core-linux-arm64-gnu': 1.15.7
|
||||||
|
'@swc/core-linux-arm64-musl': 1.15.7
|
||||||
|
'@swc/core-linux-x64-gnu': 1.15.7
|
||||||
|
'@swc/core-linux-x64-musl': 1.15.7
|
||||||
|
'@swc/core-win32-arm64-msvc': 1.15.7
|
||||||
|
'@swc/core-win32-ia32-msvc': 1.15.7
|
||||||
|
'@swc/core-win32-x64-msvc': 1.15.7
|
||||||
|
|
||||||
|
'@swc/counter@0.1.3': {}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@swc/types@0.1.25':
|
||||||
|
dependencies:
|
||||||
|
'@swc/counter': 0.1.3
|
||||||
|
|
||||||
'@tailwindcss/node@4.1.11':
|
'@tailwindcss/node@4.1.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
|
|
@ -6984,6 +7338,8 @@ snapshots:
|
||||||
|
|
||||||
delayed-stream@1.0.0: {}
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
|
detect-libc@1.0.3: {}
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
detect-node-es@1.1.0: {}
|
detect-node-es@1.1.0: {}
|
||||||
|
|
@ -7669,6 +8025,13 @@ snapshots:
|
||||||
|
|
||||||
intersection-observer@0.12.2: {}
|
intersection-observer@0.12.2: {}
|
||||||
|
|
||||||
|
intl-messageformat@10.7.18:
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/ecma402-abstract': 2.3.6
|
||||||
|
'@formatjs/fast-memoize': 2.2.7
|
||||||
|
'@formatjs/icu-messageformat-parser': 2.11.4
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
is-array-buffer@3.0.5:
|
is-array-buffer@3.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
|
|
@ -8057,6 +8420,26 @@ snapshots:
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
|
next-intl-swc-plugin-extractor@4.6.1: {}
|
||||||
|
|
||||||
|
next-intl@4.6.1(next@16.0.8(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.8.3):
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/intl-localematcher': 0.5.10
|
||||||
|
'@parcel/watcher': 2.5.1
|
||||||
|
'@swc/core': 1.15.7
|
||||||
|
negotiator: 1.0.0
|
||||||
|
next: 16.0.8(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
||||||
|
next-intl-swc-plugin-extractor: 4.6.1
|
||||||
|
po-parser: 2.0.0
|
||||||
|
react: 19.2.1
|
||||||
|
use-intl: 4.6.1(react@19.2.1)
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.8.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@swc/helpers'
|
||||||
|
|
||||||
next-themes@0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
|
next-themes@0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.1
|
react: 19.2.1
|
||||||
|
|
@ -8085,6 +8468,8 @@ snapshots:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
|
node-addon-api@7.1.1: {}
|
||||||
|
|
||||||
node-releases@2.0.27: {}
|
node-releases@2.0.27: {}
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
@ -8194,6 +8579,8 @@ snapshots:
|
||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.3: {}
|
||||||
|
|
||||||
|
po-parser@2.0.0: {}
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
postcss@8.4.31:
|
postcss@8.4.31:
|
||||||
|
|
@ -8669,8 +9056,6 @@ snapshots:
|
||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
swiper@12.0.3: {}
|
|
||||||
|
|
||||||
synckit@0.11.11:
|
synckit@0.11.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@pkgr/core': 0.2.9
|
'@pkgr/core': 0.2.9
|
||||||
|
|
@ -8744,7 +9129,7 @@ snapshots:
|
||||||
'@ts-morph/common': 0.28.1
|
'@ts-morph/common': 0.28.1
|
||||||
code-block-writer: 13.0.3
|
code-block-writer: 13.0.3
|
||||||
|
|
||||||
ts-node@10.9.2(@types/node@20.19.8)(typescript@5.8.3):
|
ts-node@10.9.2(@swc/core@1.15.7)(@types/node@20.19.8)(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cspotcode/source-map-support': 0.8.1
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
'@tsconfig/node10': 1.0.11
|
'@tsconfig/node10': 1.0.11
|
||||||
|
|
@ -8761,6 +9146,8 @@ snapshots:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
v8-compile-cache-lib: 3.0.1
|
v8-compile-cache-lib: 3.0.1
|
||||||
yn: 3.1.1
|
yn: 3.1.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@swc/core': 1.15.7
|
||||||
|
|
||||||
tsconfig-paths@3.15.0:
|
tsconfig-paths@3.15.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -8893,6 +9280,13 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.7
|
'@types/react': 19.2.7
|
||||||
|
|
||||||
|
use-intl@4.6.1(react@19.2.1):
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/fast-memoize': 2.2.7
|
||||||
|
'@schummar/icu-type-parser': 1.21.5
|
||||||
|
intl-messageformat: 10.7.18
|
||||||
|
react: 19.2.1
|
||||||
|
|
||||||
use-sidecar@1.1.3(@types/react@19.2.7)(react@19.2.1):
|
use-sidecar@1.1.3(@types/react@19.2.7)(react@19.2.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-node-es: 1.1.0
|
detect-node-es: 1.1.0
|
||||||
|
|
|
||||||
|
|
@ -1,539 +0,0 @@
|
||||||
/* Logo 字体 */
|
|
||||||
@font-face {
|
|
||||||
font-family: "iconfont logo";
|
|
||||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
|
|
||||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
|
|
||||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
|
|
||||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
|
|
||||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-family: "iconfont logo";
|
|
||||||
font-size: 160px;
|
|
||||||
font-style: normal;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tabs */
|
|
||||||
.nav-tabs {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs .nav-more {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: 42px;
|
|
||||||
line-height: 42px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tabs {
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tabs li {
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100px;
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
margin-bottom: -1px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#tabs .active {
|
|
||||||
border-bottom-color: #f00;
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-container .content {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 页面布局 */
|
|
||||||
.main {
|
|
||||||
padding: 30px 100px;
|
|
||||||
width: 960px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main .logo {
|
|
||||||
color: #333;
|
|
||||||
text-align: left;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
line-height: 1;
|
|
||||||
height: 110px;
|
|
||||||
margin-top: -50px;
|
|
||||||
overflow: hidden;
|
|
||||||
*zoom: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main .logo a {
|
|
||||||
font-size: 160px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.helps {
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.helps pre {
|
|
||||||
padding: 20px;
|
|
||||||
margin: 10px 0;
|
|
||||||
border: solid 1px #e7e1cd;
|
|
||||||
background-color: #fffdef;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon_lists {
|
|
||||||
width: 100% !important;
|
|
||||||
overflow: hidden;
|
|
||||||
*zoom: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon_lists li {
|
|
||||||
width: 100px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-right: 20px;
|
|
||||||
text-align: center;
|
|
||||||
list-style: none !important;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon_lists li .code-name {
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon_lists .icon {
|
|
||||||
display: block;
|
|
||||||
height: 100px;
|
|
||||||
line-height: 100px;
|
|
||||||
font-size: 42px;
|
|
||||||
margin: 10px auto;
|
|
||||||
color: #333;
|
|
||||||
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
|
|
||||||
-moz-transition: font-size 0.25s linear, width 0.25s linear;
|
|
||||||
transition: font-size 0.25s linear, width 0.25s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon_lists .icon:hover {
|
|
||||||
font-size: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon_lists .svg-icon {
|
|
||||||
/* 通过设置 font-size 来改变图标大小 */
|
|
||||||
width: 1em;
|
|
||||||
/* 图标和文字相邻时,垂直对齐 */
|
|
||||||
vertical-align: -0.15em;
|
|
||||||
/* 通过设置 color 来改变 SVG 的颜色/fill */
|
|
||||||
fill: currentColor;
|
|
||||||
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
|
|
||||||
normalize.css 中也包含这行 */
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon_lists li .name,
|
|
||||||
.icon_lists li .code-name {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* markdown 样式 */
|
|
||||||
.markdown {
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown img {
|
|
||||||
vertical-align: middle;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown h1 {
|
|
||||||
color: #404040;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 40px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown h2,
|
|
||||||
.markdown h3,
|
|
||||||
.markdown h4,
|
|
||||||
.markdown h5,
|
|
||||||
.markdown h6 {
|
|
||||||
color: #404040;
|
|
||||||
margin: 1.6em 0 0.6em 0;
|
|
||||||
font-weight: 500;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown h1 {
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown h2 {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown h4 {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown h5 {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown h6 {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown hr {
|
|
||||||
height: 1px;
|
|
||||||
border: 0;
|
|
||||||
background: #e9e9e9;
|
|
||||||
margin: 16px 0;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown p {
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown>p,
|
|
||||||
.markdown>blockquote,
|
|
||||||
.markdown>.highlight,
|
|
||||||
.markdown>ol,
|
|
||||||
.markdown>ul {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown ul>li {
|
|
||||||
list-style: circle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown>ul li,
|
|
||||||
.markdown blockquote ul>li {
|
|
||||||
margin-left: 20px;
|
|
||||||
padding-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown>ul li p,
|
|
||||||
.markdown>ol li p {
|
|
||||||
margin: 0.6em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown ol>li {
|
|
||||||
list-style: decimal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown>ol li,
|
|
||||||
.markdown blockquote ol>li {
|
|
||||||
margin-left: 20px;
|
|
||||||
padding-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown code {
|
|
||||||
margin: 0 3px;
|
|
||||||
padding: 0 5px;
|
|
||||||
background: #eee;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown strong,
|
|
||||||
.markdown b {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown>table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0px;
|
|
||||||
empty-cells: show;
|
|
||||||
border: 1px solid #e9e9e9;
|
|
||||||
width: 95%;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown>table th {
|
|
||||||
white-space: nowrap;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown>table th,
|
|
||||||
.markdown>table td {
|
|
||||||
border: 1px solid #e9e9e9;
|
|
||||||
padding: 8px 16px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown>table th {
|
|
||||||
background: #F7F7F7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown blockquote {
|
|
||||||
font-size: 90%;
|
|
||||||
color: #999;
|
|
||||||
border-left: 4px solid #e9e9e9;
|
|
||||||
padding-left: 0.8em;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown blockquote p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown .anchor {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown .waiting {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown h1:hover .anchor,
|
|
||||||
.markdown h2:hover .anchor,
|
|
||||||
.markdown h3:hover .anchor,
|
|
||||||
.markdown h4:hover .anchor,
|
|
||||||
.markdown h5:hover .anchor,
|
|
||||||
.markdown h6:hover .anchor {
|
|
||||||
opacity: 1;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown>br,
|
|
||||||
.markdown>p>br {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.hljs {
|
|
||||||
display: block;
|
|
||||||
background: white;
|
|
||||||
padding: 0.5em;
|
|
||||||
color: #333333;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-comment,
|
|
||||||
.hljs-meta {
|
|
||||||
color: #969896;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-string,
|
|
||||||
.hljs-variable,
|
|
||||||
.hljs-template-variable,
|
|
||||||
.hljs-strong,
|
|
||||||
.hljs-emphasis,
|
|
||||||
.hljs-quote {
|
|
||||||
color: #df5000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-keyword,
|
|
||||||
.hljs-selector-tag,
|
|
||||||
.hljs-type {
|
|
||||||
color: #a71d5d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-literal,
|
|
||||||
.hljs-symbol,
|
|
||||||
.hljs-bullet,
|
|
||||||
.hljs-attribute {
|
|
||||||
color: #0086b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-section,
|
|
||||||
.hljs-name {
|
|
||||||
color: #63a35c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-tag {
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-title,
|
|
||||||
.hljs-attr,
|
|
||||||
.hljs-selector-id,
|
|
||||||
.hljs-selector-class,
|
|
||||||
.hljs-selector-attr,
|
|
||||||
.hljs-selector-pseudo {
|
|
||||||
color: #795da3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-addition {
|
|
||||||
color: #55a532;
|
|
||||||
background-color: #eaffea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-deletion {
|
|
||||||
color: #bd2c00;
|
|
||||||
background-color: #ffecec;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-link {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 代码高亮 */
|
|
||||||
/* PrismJS 1.15.0
|
|
||||||
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
|
|
||||||
/**
|
|
||||||
* prism.js default theme for JavaScript, CSS and HTML
|
|
||||||
* Based on dabblet (http://dabblet.com)
|
|
||||||
* @author Lea Verou
|
|
||||||
*/
|
|
||||||
code[class*="language-"],
|
|
||||||
pre[class*="language-"] {
|
|
||||||
color: black;
|
|
||||||
background: none;
|
|
||||||
text-shadow: 0 1px white;
|
|
||||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
|
||||||
text-align: left;
|
|
||||||
white-space: pre;
|
|
||||||
word-spacing: normal;
|
|
||||||
word-break: normal;
|
|
||||||
word-wrap: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
-moz-tab-size: 4;
|
|
||||||
-o-tab-size: 4;
|
|
||||||
tab-size: 4;
|
|
||||||
|
|
||||||
-webkit-hyphens: none;
|
|
||||||
-moz-hyphens: none;
|
|
||||||
-ms-hyphens: none;
|
|
||||||
hyphens: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre[class*="language-"]::-moz-selection,
|
|
||||||
pre[class*="language-"] ::-moz-selection,
|
|
||||||
code[class*="language-"]::-moz-selection,
|
|
||||||
code[class*="language-"] ::-moz-selection {
|
|
||||||
text-shadow: none;
|
|
||||||
background: #b3d4fc;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre[class*="language-"]::selection,
|
|
||||||
pre[class*="language-"] ::selection,
|
|
||||||
code[class*="language-"]::selection,
|
|
||||||
code[class*="language-"] ::selection {
|
|
||||||
text-shadow: none;
|
|
||||||
background: #b3d4fc;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
|
|
||||||
code[class*="language-"],
|
|
||||||
pre[class*="language-"] {
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
pre[class*="language-"] {
|
|
||||||
padding: 1em;
|
|
||||||
margin: .5em 0;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(pre)>code[class*="language-"],
|
|
||||||
pre[class*="language-"] {
|
|
||||||
background: #f5f2f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline code */
|
|
||||||
:not(pre)>code[class*="language-"] {
|
|
||||||
padding: .1em;
|
|
||||||
border-radius: .3em;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.comment,
|
|
||||||
.token.prolog,
|
|
||||||
.token.doctype,
|
|
||||||
.token.cdata {
|
|
||||||
color: slategray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.punctuation {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.namespace {
|
|
||||||
opacity: .7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.property,
|
|
||||||
.token.tag,
|
|
||||||
.token.boolean,
|
|
||||||
.token.number,
|
|
||||||
.token.constant,
|
|
||||||
.token.symbol,
|
|
||||||
.token.deleted {
|
|
||||||
color: #905;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.selector,
|
|
||||||
.token.attr-name,
|
|
||||||
.token.string,
|
|
||||||
.token.char,
|
|
||||||
.token.builtin,
|
|
||||||
.token.inserted {
|
|
||||||
color: #690;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.operator,
|
|
||||||
.token.entity,
|
|
||||||
.token.url,
|
|
||||||
.language-css .token.string,
|
|
||||||
.style .token.string {
|
|
||||||
color: #9a6e3a;
|
|
||||||
background: hsla(0, 0%, 100%, .5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.atrule,
|
|
||||||
.token.attr-value,
|
|
||||||
.token.keyword {
|
|
||||||
color: #07a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.function,
|
|
||||||
.token.class-name {
|
|
||||||
color: #DD4A68;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.regex,
|
|
||||||
.token.important,
|
|
||||||
.token.variable {
|
|
||||||
color: #e90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.important,
|
|
||||||
.token.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.entity {
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
|
|
@ -1,674 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<title>iconfont Demo</title>
|
|
||||||
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
|
|
||||||
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
|
|
||||||
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
|
|
||||||
<link rel="stylesheet" href="demo.css">
|
|
||||||
<link rel="stylesheet" href="iconfont.css">
|
|
||||||
<script src="iconfont.js"></script>
|
|
||||||
<!-- jQuery -->
|
|
||||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
|
|
||||||
<!-- 代码高亮 -->
|
|
||||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
|
|
||||||
<style>
|
|
||||||
.main .logo {
|
|
||||||
margin-top: 0;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main .logo a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main .logo .sub-title {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
font-size: 22px;
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(-45deg, #3967FF, #B500FE);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="main">
|
|
||||||
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
|
|
||||||
<img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
|
|
||||||
|
|
||||||
</a></h1>
|
|
||||||
<div class="nav-tabs">
|
|
||||||
<ul id="tabs" class="dib-box">
|
|
||||||
<li class="dib active"><span>Unicode</span></li>
|
|
||||||
<li class="dib"><span>Font class</span></li>
|
|
||||||
<li class="dib"><span>Symbol</span></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=5076160" target="_blank" class="nav-more">查看项目</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="tab-container">
|
|
||||||
<div class="content unicode" style="display: block;">
|
|
||||||
<ul class="icon_lists dib-box">
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">Logo</div>
|
|
||||||
<div class="code-name">&#xe624;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">12</div>
|
|
||||||
<div class="code-name">&#xe622;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">群聊</div>
|
|
||||||
<div class="code-name">&#xe621;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">挂断电话</div>
|
|
||||||
<div class="code-name">&#xe620;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">电话</div>
|
|
||||||
<div class="code-name">&#xe61f;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">艾特</div>
|
|
||||||
<div class="code-name">&#xe61d;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">性别</div>
|
|
||||||
<div class="code-name">&#xe61e;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">Frame 247</div>
|
|
||||||
<div class="code-name">&#xe61c;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">编辑</div>
|
|
||||||
<div class="code-name">&#xe61b;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">展开</div>
|
|
||||||
<div class="code-name">&#xe615;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">16-右</div>
|
|
||||||
<div class="code-name">&#xe616;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">展开-1</div>
|
|
||||||
<div class="code-name">&#xe617;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">生成</div>
|
|
||||||
<div class="code-name">&#xe618;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">复制</div>
|
|
||||||
<div class="code-name">&#xe619;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">16-左</div>
|
|
||||||
<div class="code-name">&#xe61a;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">gender-female-line</div>
|
|
||||||
<div class="code-name">&#xe614;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">gender-male-line</div>
|
|
||||||
<div class="code-name">&#xe613;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">刷新</div>
|
|
||||||
<div class="code-name">&#xe612;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">箭头</div>
|
|
||||||
<div class="code-name">&#xe610;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">关闭</div>
|
|
||||||
<div class="code-name">&#xe611;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">搜索</div>
|
|
||||||
<div class="code-name">&#xe60d;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
<div class="article markdown">
|
|
||||||
<h2 id="unicode-">Unicode 引用</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
|
|
||||||
<ul>
|
|
||||||
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
|
|
||||||
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
|
|
||||||
</ul>
|
|
||||||
<blockquote>
|
|
||||||
<p>注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
|
|
||||||
</blockquote>
|
|
||||||
<p>Unicode 使用步骤如下:</p>
|
|
||||||
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
|
|
||||||
<pre><code class="language-css"
|
|
||||||
>@font-face {
|
|
||||||
font-family: 'iconfont';
|
|
||||||
src: url('iconfont.eot?t=1766052523135'); /* IE9 */
|
|
||||||
src: url('iconfont.eot?t=1766052523135#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
|
||||||
url('iconfont.woff2?t=1766052523135') format('woff2'),
|
|
||||||
url('iconfont.woff?t=1766052523135') format('woff'),
|
|
||||||
url('iconfont.ttf?t=1766052523135') format('truetype'),
|
|
||||||
url('iconfont.svg?t=1766052523135#iconfont') format('svg');
|
|
||||||
}
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
|
||||||
<pre><code class="language-css"
|
|
||||||
>.iconfont {
|
|
||||||
font-family: "iconfont" !important;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
|
|
||||||
<pre>
|
|
||||||
<code class="language-html"
|
|
||||||
><span class="iconfont">&#x33;</span>
|
|
||||||
</code></pre>
|
|
||||||
<blockquote>
|
|
||||||
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
|
||||||
</blockquote>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content font-class">
|
|
||||||
<ul class="icon_lists dib-box">
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-Logo"></span>
|
|
||||||
<div class="name">
|
|
||||||
Logo
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-Logo
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-wodejiemianqianwang"></span>
|
|
||||||
<div class="name">
|
|
||||||
12
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-wodejiemianqianwang
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-qunliao"></span>
|
|
||||||
<div class="name">
|
|
||||||
群聊
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-qunliao
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-guaduandianhua"></span>
|
|
||||||
<div class="name">
|
|
||||||
挂断电话
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-guaduandianhua
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-dianhua"></span>
|
|
||||||
<div class="name">
|
|
||||||
电话
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-dianhua
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-aite"></span>
|
|
||||||
<div class="name">
|
|
||||||
艾特
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-aite
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-xingbie"></span>
|
|
||||||
<div class="name">
|
|
||||||
性别
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-xingbie
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-a-Frame247"></span>
|
|
||||||
<div class="name">
|
|
||||||
Frame 247
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-a-Frame247
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-bianji"></span>
|
|
||||||
<div class="name">
|
|
||||||
编辑
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-bianji
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-zhankai1"></span>
|
|
||||||
<div class="name">
|
|
||||||
展开
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-zhankai1
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-a-Frame195"></span>
|
|
||||||
<div class="name">
|
|
||||||
16-右
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-a-Frame195
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-zhankai-1"></span>
|
|
||||||
<div class="name">
|
|
||||||
展开-1
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-zhankai-1
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-shengcheng"></span>
|
|
||||||
<div class="name">
|
|
||||||
生成
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-shengcheng
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-fuzhi"></span>
|
|
||||||
<div class="name">
|
|
||||||
复制
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-fuzhi
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-a-Frame194"></span>
|
|
||||||
<div class="name">
|
|
||||||
16-左
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-a-Frame194
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-gender-female-line"></span>
|
|
||||||
<div class="name">
|
|
||||||
gender-female-line
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-gender-female-line
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-gender-male-line"></span>
|
|
||||||
<div class="name">
|
|
||||||
gender-male-line
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-gender-male-line
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-shuaxin"></span>
|
|
||||||
<div class="name">
|
|
||||||
刷新
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-shuaxin
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-jiantou"></span>
|
|
||||||
<div class="name">
|
|
||||||
箭头
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-jiantou
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-guanbi"></span>
|
|
||||||
<div class="name">
|
|
||||||
关闭
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-guanbi
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-sousuo"></span>
|
|
||||||
<div class="name">
|
|
||||||
搜索
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-sousuo
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
<div class="article markdown">
|
|
||||||
<h2 id="font-class-">font-class 引用</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
|
|
||||||
<p>与 Unicode 使用方式相比,具有如下特点:</p>
|
|
||||||
<ul>
|
|
||||||
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
|
|
||||||
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
|
|
||||||
</ul>
|
|
||||||
<p>使用步骤如下:</p>
|
|
||||||
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
|
|
||||||
<pre><code class="language-html"><link rel="stylesheet" href="./iconfont.css">
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
|
|
||||||
<pre><code class="language-html"><span class="iconfont icon-xxx"></span>
|
|
||||||
</code></pre>
|
|
||||||
<blockquote>
|
|
||||||
<p>"
|
|
||||||
iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
|
||||||
</blockquote>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content symbol">
|
|
||||||
<ul class="icon_lists dib-box">
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-Logo"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">Logo</div>
|
|
||||||
<div class="code-name">#icon-Logo</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-wodejiemianqianwang"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">12</div>
|
|
||||||
<div class="code-name">#icon-wodejiemianqianwang</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-qunliao"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">群聊</div>
|
|
||||||
<div class="code-name">#icon-qunliao</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-guaduandianhua"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">挂断电话</div>
|
|
||||||
<div class="code-name">#icon-guaduandianhua</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-dianhua"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">电话</div>
|
|
||||||
<div class="code-name">#icon-dianhua</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-aite"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">艾特</div>
|
|
||||||
<div class="code-name">#icon-aite</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-xingbie"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">性别</div>
|
|
||||||
<div class="code-name">#icon-xingbie</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-a-Frame247"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">Frame 247</div>
|
|
||||||
<div class="code-name">#icon-a-Frame247</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-bianji"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">编辑</div>
|
|
||||||
<div class="code-name">#icon-bianji</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-zhankai1"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">展开</div>
|
|
||||||
<div class="code-name">#icon-zhankai1</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-a-Frame195"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">16-右</div>
|
|
||||||
<div class="code-name">#icon-a-Frame195</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-zhankai-1"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">展开-1</div>
|
|
||||||
<div class="code-name">#icon-zhankai-1</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-shengcheng"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">生成</div>
|
|
||||||
<div class="code-name">#icon-shengcheng</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-fuzhi"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">复制</div>
|
|
||||||
<div class="code-name">#icon-fuzhi</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-a-Frame194"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">16-左</div>
|
|
||||||
<div class="code-name">#icon-a-Frame194</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-gender-female-line"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">gender-female-line</div>
|
|
||||||
<div class="code-name">#icon-gender-female-line</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-gender-male-line"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">gender-male-line</div>
|
|
||||||
<div class="code-name">#icon-gender-male-line</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-shuaxin"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">刷新</div>
|
|
||||||
<div class="code-name">#icon-shuaxin</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-jiantou"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">箭头</div>
|
|
||||||
<div class="code-name">#icon-jiantou</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-guanbi"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">关闭</div>
|
|
||||||
<div class="code-name">#icon-guanbi</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-sousuo"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">搜索</div>
|
|
||||||
<div class="code-name">#icon-sousuo</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
<div class="article markdown">
|
|
||||||
<h2 id="symbol-">Symbol 引用</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
|
|
||||||
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
|
|
||||||
<ul>
|
|
||||||
<li>支持多色图标了,不再受单色限制。</li>
|
|
||||||
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
|
|
||||||
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
|
|
||||||
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
|
|
||||||
</ul>
|
|
||||||
<p>使用步骤如下:</p>
|
|
||||||
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
|
|
||||||
<pre><code class="language-html"><script src="./iconfont.js"></script>
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
|
|
||||||
<pre><code class="language-html"><style>
|
|
||||||
.icon {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
vertical-align: -0.15em;
|
|
||||||
fill: currentColor;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
|
|
||||||
<pre><code class="language-html"><svg class="icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-xxx"></use>
|
|
||||||
</svg>
|
|
||||||
</code></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
$(document).ready(function () {
|
|
||||||
$('.tab-container .content:first').show()
|
|
||||||
|
|
||||||
$('#tabs li').click(function (e) {
|
|
||||||
var tabContent = $('.tab-container .content')
|
|
||||||
var index = $(this).index()
|
|
||||||
|
|
||||||
if ($(this).hasClass('active')) {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
$('#tabs li').removeClass('active')
|
|
||||||
$(this).addClass('active')
|
|
||||||
|
|
||||||
tabContent.hide().eq(index).fadeIn()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
@font-face {
|
|
||||||
font-family: "iconfont"; /* Project id 5076160 */
|
|
||||||
src: url('iconfont.eot?t=1766052523135'); /* IE9 */
|
|
||||||
src: url('iconfont.eot?t=1766052523135#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
|
||||||
url('iconfont.woff2?t=1766052523135') format('woff2'),
|
|
||||||
url('iconfont.woff?t=1766052523135') format('woff'),
|
|
||||||
url('iconfont.ttf?t=1766052523135') format('truetype'),
|
|
||||||
url('iconfont.svg?t=1766052523135#iconfont') format('svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
font-family: "iconfont" !important;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-Logo:before {
|
|
||||||
content: "\e624";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-wodejiemianqianwang:before {
|
|
||||||
content: "\e622";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-qunliao:before {
|
|
||||||
content: "\e621";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-guaduandianhua:before {
|
|
||||||
content: "\e620";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-dianhua:before {
|
|
||||||
content: "\e61f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-aite:before {
|
|
||||||
content: "\e61d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-xingbie:before {
|
|
||||||
content: "\e61e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-a-Frame247:before {
|
|
||||||
content: "\e61c";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-bianji:before {
|
|
||||||
content: "\e61b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-zhankai1:before {
|
|
||||||
content: "\e615";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-a-Frame195:before {
|
|
||||||
content: "\e616";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-zhankai-1:before {
|
|
||||||
content: "\e617";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-shengcheng:before {
|
|
||||||
content: "\e618";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-fuzhi:before {
|
|
||||||
content: "\e619";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-a-Frame194:before {
|
|
||||||
content: "\e61a";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-gender-female-line:before {
|
|
||||||
content: "\e614";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-gender-male-line:before {
|
|
||||||
content: "\e613";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-shuaxin:before {
|
|
||||||
content: "\e612";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-jiantou:before {
|
|
||||||
content: "\e610";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-guanbi:before {
|
|
||||||
content: "\e611";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sousuo:before {
|
|
||||||
content: "\e60d";
|
|
||||||
}
|
|
||||||
|
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
|
|
@ -1,156 +0,0 @@
|
||||||
{
|
|
||||||
"id": "5076160",
|
|
||||||
"name": "spicyxx.ai",
|
|
||||||
"font_family": "iconfont",
|
|
||||||
"css_prefix_text": "icon-",
|
|
||||||
"description": "",
|
|
||||||
"glyphs": [
|
|
||||||
{
|
|
||||||
"icon_id": "46381406",
|
|
||||||
"name": "Logo",
|
|
||||||
"font_class": "Logo",
|
|
||||||
"unicode": "e624",
|
|
||||||
"unicode_decimal": 58916
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46339903",
|
|
||||||
"name": "12",
|
|
||||||
"font_class": "wodejiemianqianwang",
|
|
||||||
"unicode": "e622",
|
|
||||||
"unicode_decimal": 58914
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46337600",
|
|
||||||
"name": "群聊",
|
|
||||||
"font_class": "qunliao",
|
|
||||||
"unicode": "e621",
|
|
||||||
"unicode_decimal": 58913
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46281959",
|
|
||||||
"name": "挂断电话",
|
|
||||||
"font_class": "guaduandianhua",
|
|
||||||
"unicode": "e620",
|
|
||||||
"unicode_decimal": 58912
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46281954",
|
|
||||||
"name": "电话",
|
|
||||||
"font_class": "dianhua",
|
|
||||||
"unicode": "e61f",
|
|
||||||
"unicode_decimal": 58911
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46261887",
|
|
||||||
"name": "艾特",
|
|
||||||
"font_class": "aite",
|
|
||||||
"unicode": "e61d",
|
|
||||||
"unicode_decimal": 58909
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46261886",
|
|
||||||
"name": "性别",
|
|
||||||
"font_class": "xingbie",
|
|
||||||
"unicode": "e61e",
|
|
||||||
"unicode_decimal": 58910
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46252831",
|
|
||||||
"name": "Frame 247",
|
|
||||||
"font_class": "a-Frame247",
|
|
||||||
"unicode": "e61c",
|
|
||||||
"unicode_decimal": 58908
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46252115",
|
|
||||||
"name": "编辑",
|
|
||||||
"font_class": "bianji",
|
|
||||||
"unicode": "e61b",
|
|
||||||
"unicode_decimal": 58907
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46252119",
|
|
||||||
"name": "展开",
|
|
||||||
"font_class": "zhankai1",
|
|
||||||
"unicode": "e615",
|
|
||||||
"unicode_decimal": 58901
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46252114",
|
|
||||||
"name": "16-右",
|
|
||||||
"font_class": "a-Frame195",
|
|
||||||
"unicode": "e616",
|
|
||||||
"unicode_decimal": 58902
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46252118",
|
|
||||||
"name": "展开-1",
|
|
||||||
"font_class": "zhankai-1",
|
|
||||||
"unicode": "e617",
|
|
||||||
"unicode_decimal": 58903
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46252117",
|
|
||||||
"name": "生成",
|
|
||||||
"font_class": "shengcheng",
|
|
||||||
"unicode": "e618",
|
|
||||||
"unicode_decimal": 58904
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46252116",
|
|
||||||
"name": "复制",
|
|
||||||
"font_class": "fuzhi",
|
|
||||||
"unicode": "e619",
|
|
||||||
"unicode_decimal": 58905
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46252113",
|
|
||||||
"name": "16-左",
|
|
||||||
"font_class": "a-Frame194",
|
|
||||||
"unicode": "e61a",
|
|
||||||
"unicode_decimal": 58906
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46234515",
|
|
||||||
"name": "gender-female-line",
|
|
||||||
"font_class": "gender-female-line",
|
|
||||||
"unicode": "e614",
|
|
||||||
"unicode_decimal": 58900
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46234139",
|
|
||||||
"name": "gender-male-line",
|
|
||||||
"font_class": "gender-male-line",
|
|
||||||
"unicode": "e613",
|
|
||||||
"unicode_decimal": 58899
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46211262",
|
|
||||||
"name": "刷新",
|
|
||||||
"font_class": "shuaxin",
|
|
||||||
"unicode": "e612",
|
|
||||||
"unicode_decimal": 58898
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46211223",
|
|
||||||
"name": "箭头",
|
|
||||||
"font_class": "jiantou",
|
|
||||||
"unicode": "e610",
|
|
||||||
"unicode_decimal": 58896
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46211222",
|
|
||||||
"name": "关闭",
|
|
||||||
"font_class": "guanbi",
|
|
||||||
"unicode": "e611",
|
|
||||||
"unicode_decimal": 58897
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "46211227",
|
|
||||||
"name": "搜索",
|
|
||||||
"font_class": "sousuo",
|
|
||||||
"unicode": "e60d",
|
|
||||||
"unicode_decimal": 58893
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 23 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,11 +1,11 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useMedia } from '@/hooks/tools';
|
import ChatSidebar from '@/layout/BasicLayout/components/ChatSidebar';
|
||||||
import ChatSidebar from '@/layout/components/ChatSidebar';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useLayoutStore } from '@/stores';
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const response = useMedia();
|
const response = useLayoutStore((s) => s.response);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ function Background({ imageUrl }: { imageUrl: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background-default absolute top-0 right-0 bottom-0 left-0 overflow-hidden">
|
<div className="bg-background-default absolute top-0 right-0 bottom-0 left-0 overflow-hidden">
|
||||||
<div className="absolute top-0 bottom-0 left-1/2 w-[752px] -translate-x-1/2">
|
<div className="absolute top-0 bottom-0 left-1/2 max-w-[752px] w-full -translate-x-1/2">
|
||||||
{imageUrl && (
|
{imageUrl && (
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
|
|
@ -21,13 +21,13 @@ function Background({ imageUrl }: { imageUrl: string }) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className="absolute top-0 bottom-0 left-0 w-120"
|
className="absolute top-0 bottom-0 left-0 w-[40%]"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(to right, rgba(6, 3, 24, 1), transparent)',
|
background: 'linear-gradient(to right, rgba(6, 3, 24, 1), transparent)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="absolute top-0 bottom-0 right-0 w-120"
|
className="absolute top-0 bottom-0 right-0 w-[40%]"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(to left, rgba(6, 3, 24, 1), transparent)',
|
background: 'linear-gradient(to left, rgba(6, 3, 24, 1), transparent)',
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export const CharacterAvatorAndName = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChatMessageUserHeader() {
|
const ChatMessageUserHeader = React.memo(() => {
|
||||||
const [isFullIntroduction, setIsFullIntroduction] = useState(false);
|
const [isFullIntroduction, setIsFullIntroduction] = useState(false);
|
||||||
const [shouldShowExpandButton, setShouldShowExpandButton] = useState(false);
|
const [shouldShowExpandButton, setShouldShowExpandButton] = useState(false);
|
||||||
const textRef = useRef<HTMLDivElement>(null);
|
const textRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -50,7 +50,7 @@ function ChatMessageUserHeader() {
|
||||||
}, [character.description]);
|
}, [character.description]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-6">
|
<div className="flex flex-col mt-4 items-center gap-6">
|
||||||
<CharacterAvatorAndName
|
<CharacterAvatorAndName
|
||||||
id={characterId}
|
id={characterId}
|
||||||
name={character.name || '-'}
|
name={character.name || '-'}
|
||||||
|
|
@ -97,6 +97,6 @@ function ChatMessageUserHeader() {
|
||||||
<div className="txt-body-m text-txt-secondary-normal">Content generated by AI</div>
|
<div className="txt-body-m text-txt-secondary-normal">Content generated by AI</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default React.memo(ChatMessageUserHeader);
|
export default ChatMessageUserHeader;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { MaskForm } from '@/app/(main)/profile/mask/MaskForm';
|
||||||
|
import { ActiveTabType } from '.';
|
||||||
|
|
||||||
|
export default function MaskCreate({ onActiveTab }: { onActiveTab: (tab: ActiveTabType) => void }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MaskForm onSubmitSuccess={() => onActiveTab('mask')} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import MaskList from '@/app/(main)/profile/mask/MaskList';
|
||||||
|
import { ActiveTabType } from '.';
|
||||||
|
|
||||||
|
export default function Personal({ onActiveTab }: { onActiveTab: (tab: ActiveTabType) => void }) {
|
||||||
|
// const router = useRouter();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MaskList selectAble onAdd={() => onActiveTab('mask_create')} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,308 +0,0 @@
|
||||||
'use client';
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from '@/components/ui/form';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { Gender } from '@/types/user';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { calculateAge, getDaysInMonth } from '@/lib/utils';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from '@/components/ui/alert-dialog';
|
|
||||||
|
|
||||||
const currentYear = dayjs().year();
|
|
||||||
const years = Array.from({ length: currentYear - 1950 + 1 }, (_, i) => `${1950 + i}`);
|
|
||||||
const months = Array.from({ length: 12 }, (_, i) => `${i + 1}`.padStart(2, '0'));
|
|
||||||
const monthTexts = Array.from({ length: 12 }, (_, i) => dayjs().month(i).format('MMM'));
|
|
||||||
|
|
||||||
const characterFormSchema = z
|
|
||||||
.object({
|
|
||||||
nickname: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1, 'Please Enter nickname')
|
|
||||||
.min(2, 'Nickname must be between 2 and 20 characters')
|
|
||||||
.max(20, 'Nickname must be less than 20 characters'),
|
|
||||||
sex: z.enum(Gender, { message: 'Please select gender' }),
|
|
||||||
year: z.string().min(1, 'Please select year'),
|
|
||||||
month: z.string().min(1, 'Please select month'),
|
|
||||||
day: z.string().min(1, 'Please select day'),
|
|
||||||
profile: z.string().trim().optional(),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const age = calculateAge(data.year, data.month, data.day);
|
|
||||||
return age >= 18;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Character age must be at least 18 years old',
|
|
||||||
path: ['year'],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
if (data.profile) {
|
|
||||||
if (data.profile.trim().length > 300) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return data.profile.trim().length >= 10;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'At least 10 characters',
|
|
||||||
path: ['profile'],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function Personal() {
|
|
||||||
// 静态数据,模拟从接口获取的数据
|
|
||||||
const chatSettingData = {
|
|
||||||
nickname: 'John',
|
|
||||||
sex: Gender.MALE,
|
|
||||||
birthday: dayjs('1995-06-15').valueOf(),
|
|
||||||
whoAmI: 'A creative and passionate developer',
|
|
||||||
};
|
|
||||||
|
|
||||||
const birthday = chatSettingData?.birthday ? dayjs(chatSettingData.birthday) : undefined;
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof characterFormSchema>>({
|
|
||||||
resolver: zodResolver(characterFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
nickname: chatSettingData?.nickname || '',
|
|
||||||
sex: chatSettingData?.sex,
|
|
||||||
year: birthday?.year().toString() || undefined,
|
|
||||||
month:
|
|
||||||
birthday?.month() !== undefined
|
|
||||||
? (birthday.month() + 1).toString().padStart(2, '0')
|
|
||||||
: undefined,
|
|
||||||
day: birthday?.date().toString().padStart(2, '0') || undefined,
|
|
||||||
profile: chatSettingData?.whoAmI || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedYear = form.watch('year');
|
|
||||||
const selectedMonth = form.watch('month');
|
|
||||||
const days = selectedYear && selectedMonth ? getDaysInMonth(selectedYear, selectedMonth) : [];
|
|
||||||
|
|
||||||
const genderTexts = [
|
|
||||||
{
|
|
||||||
value: Gender.MALE,
|
|
||||||
label: 'Male',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: Gender.FEMALE,
|
|
||||||
label: 'Female',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: Gender.OTHER,
|
|
||||||
label: 'Other',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const gender = form.watch('sex');
|
|
||||||
const genderText = genderTexts.find((text) => text.value === gender)?.label;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<Form {...form}>
|
|
||||||
<form className="space-y-8">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="nickname"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Nickname</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter nickname"
|
|
||||||
maxLength={20}
|
|
||||||
error={!!form.formState.errors.nickname}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="">
|
|
||||||
<div className="txt-label-m text-txt-secondary-normal">Gender</div>
|
|
||||||
|
|
||||||
<div className="mt-3">
|
|
||||||
<div className="bg-surface-element-normal rounded-m txt-body-l text-txt-secondary-disabled flex h-12 items-center px-4 py-3">
|
|
||||||
{genderText}
|
|
||||||
</div>
|
|
||||||
<div className="txt-body-s text-txt-secondary-disabled mt-1">
|
|
||||||
Please note: gender cannot be changed after setting
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label className="txt-label-m mb-3 block">Birthday</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="year"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Select onValueChange={field.onChange} value={field.value}>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Year" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{years.map((year) => (
|
|
||||||
<SelectItem key={year} value={year}>
|
|
||||||
{year}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="month"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Select onValueChange={field.onChange} value={field.value}>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Month" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{months.map((m, index) => (
|
|
||||||
<SelectItem key={m} value={m}>
|
|
||||||
{monthTexts[index]}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="day"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Select onValueChange={field.onChange} value={field.value}>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Day" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{days.map((d) => (
|
|
||||||
<SelectItem key={d} value={d}>
|
|
||||||
{d}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<FormMessage>
|
|
||||||
{form.formState.errors.year?.message ||
|
|
||||||
form.formState.errors.month?.message ||
|
|
||||||
form.formState.errors.day?.message}
|
|
||||||
</FormMessage>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="profile"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
My Persona
|
|
||||||
<span className="txt-label-m text-txt-secondary-normal">(Optional)</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
{...field}
|
|
||||||
maxLength={300}
|
|
||||||
error={!!form.formState.errors.profile}
|
|
||||||
placeholder="Set your own persona in CrushLevel"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
{/* <div className="flex gap-3">
|
|
||||||
<Button variant="tertiary" size="large" className="flex-1" onClick={handleGoBack}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
size="large"
|
|
||||||
className="flex-1"
|
|
||||||
onClick={form.handleSubmit(onSubmit)}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 确认放弃修改的对话框 */}
|
|
||||||
{/* <AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Unsaved Edits</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
The edited content will not be saved after exiting. Please confirm whether to continue
|
|
||||||
exiting?
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel onClick={() => setShowConfirmDialog(false)}>
|
|
||||||
Cancel
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction variant="destructive" onClick={handleConfirmDiscard}>
|
|
||||||
Exit
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog> */}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -29,10 +29,10 @@ const ChatProfilePersona = React.memo(({ onActiveTab }: ProfileProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="txt-title-s">My Chat Persona</div>
|
<div className="txt-title-s">Masked Identity Mode</div>
|
||||||
<div
|
<div
|
||||||
className="txt-label-m text-primary-variant-normal cursor-pointer"
|
className="txt-label-m text-primary-variant-normal cursor-pointer"
|
||||||
onClick={() => onActiveTab('personal')}
|
onClick={() => onActiveTab('mask')}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import Profile from './Profile';
|
import Profile from './Profile';
|
||||||
import Personal from './Personal';
|
import MaskList from './MaskList';
|
||||||
import VoiceActor from './VoiceActor';
|
import VoiceActor from './VoiceActor';
|
||||||
import Font from './Font';
|
import Font from './Font';
|
||||||
import MaxToken from './MaxToken';
|
import MaxToken from './MaxToken';
|
||||||
|
|
@ -19,6 +19,8 @@ import {
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { useStreamChatStore } from '../stream-chat';
|
import { useStreamChatStore } from '../stream-chat';
|
||||||
|
import IconFont from '@/components/ui/iconFont';
|
||||||
|
import MaskCreate from './MaskCreate';
|
||||||
|
|
||||||
type SettingProps = {
|
type SettingProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -26,7 +28,8 @@ type SettingProps = {
|
||||||
};
|
};
|
||||||
export type ActiveTabType =
|
export type ActiveTabType =
|
||||||
| 'profile'
|
| 'profile'
|
||||||
| 'personal'
|
| 'mask'
|
||||||
|
| 'mask_create'
|
||||||
| 'history'
|
| 'history'
|
||||||
| 'voice_actor'
|
| 'voice_actor'
|
||||||
| 'font'
|
| 'font'
|
||||||
|
|
@ -35,7 +38,8 @@ export type ActiveTabType =
|
||||||
| 'model';
|
| 'model';
|
||||||
|
|
||||||
const titleMap = {
|
const titleMap = {
|
||||||
personal: 'Personal',
|
mask: 'Masked Identity Mode',
|
||||||
|
mask_create: 'Create Mask',
|
||||||
history: 'History',
|
history: 'History',
|
||||||
voice_actor: 'Voice Actor',
|
voice_actor: 'Voice Actor',
|
||||||
font: 'Font',
|
font: 'Font',
|
||||||
|
|
@ -44,6 +48,17 @@ const titleMap = {
|
||||||
model: 'Chat Model',
|
model: 'Chat Model',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const backMap = {
|
||||||
|
mask: 'profile',
|
||||||
|
mask_create: 'mask',
|
||||||
|
history: 'profile',
|
||||||
|
voice_actor: 'profile',
|
||||||
|
font: 'profile',
|
||||||
|
max_token: 'profile',
|
||||||
|
background: 'profile',
|
||||||
|
model: 'profile',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default function SettingDialog({ open, onOpenChange }: SettingProps) {
|
export default function SettingDialog({ open, onOpenChange }: SettingProps) {
|
||||||
const [activeTab, setActiveTab] = useState<ActiveTabType>('profile');
|
const [activeTab, setActiveTab] = useState<ActiveTabType>('profile');
|
||||||
const updateUserChatSetting = useStreamChatStore((store) => store.updateUserChatSetting);
|
const updateUserChatSetting = useStreamChatStore((store) => store.updateUserChatSetting);
|
||||||
|
|
@ -65,14 +80,19 @@ export default function SettingDialog({ open, onOpenChange }: SettingProps) {
|
||||||
titleMap[activeTab]
|
titleMap[activeTab]
|
||||||
)}
|
)}
|
||||||
{activeTab !== 'profile' && (
|
{activeTab !== 'profile' && (
|
||||||
<IconButton variant="tertiary" size="small" onClick={() => setActiveTab('profile')}>
|
<IconButton
|
||||||
<i className="iconfont-v2 iconv2-jiantou" />
|
variant="tertiary"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setActiveTab(backMap[activeTab])}
|
||||||
|
>
|
||||||
|
<IconFont size={24} className="text-white cursor-pointer" type="icon-jiantou" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<div className="w-full h-[calc(100vh-160px)] pr-1 mt-4">
|
<div className="w-full h-[calc(100vh-160px)] pr-1 mt-4">
|
||||||
{activeTab === 'profile' && <Profile onActiveTab={setActiveTab} />}
|
{activeTab === 'profile' && <Profile onActiveTab={setActiveTab} />}
|
||||||
{activeTab === 'personal' && <Personal />}
|
{activeTab === 'mask' && <MaskList onActiveTab={setActiveTab} />}
|
||||||
|
{activeTab === 'mask_create' && <MaskCreate onActiveTab={setActiveTab} />}
|
||||||
{activeTab === 'voice_actor' && <VoiceActor />}
|
{activeTab === 'voice_actor' && <VoiceActor />}
|
||||||
{activeTab === 'font' && <Font />}
|
{activeTab === 'font' && <Font />}
|
||||||
{activeTab === 'max_token' && <MaxToken />}
|
{activeTab === 'max_token' && <MaxToken />}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useAsyncFn } from '@/hooks/tools';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useStreamChatStore } from '@/app/(main)/chat/[id]/stream-chat';
|
import { useStreamChatStore } from '@/app/(main)/chat/[id]/stream-chat';
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import IconFont from '@/components/ui/iconFont';
|
||||||
|
|
||||||
const AuthHeightTextarea = (
|
const AuthHeightTextarea = (
|
||||||
props: React.ComponentProps<'textarea'> & {
|
props: React.ComponentProps<'textarea'> & {
|
||||||
|
|
@ -90,7 +91,9 @@ export default function Input() {
|
||||||
<div></div>
|
<div></div>
|
||||||
<div className="flex w-full items-end gap-4">
|
<div className="flex w-full items-end gap-4">
|
||||||
{/* 打电话按钮 */}
|
{/* 打电话按钮 */}
|
||||||
<IconButton onClick={() => {}} iconfont="icon-gift-border" />
|
<IconButton onClick={() => {}}>
|
||||||
|
<IconFont type="icon-dianhua" size={30} />
|
||||||
|
</IconButton>
|
||||||
<div className="flex-1 flex items-end gap-2 min-h-12 py-2 px-2 bg-white/15 rounded-3xl">
|
<div className="flex-1 flex items-end gap-2 min-h-12 py-2 px-2 bg-white/15 rounded-3xl">
|
||||||
{/* 语音录制按钮 */}
|
{/* 语音录制按钮 */}
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { useParams } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useCharacter } from '@/hooks/services/character';
|
import { useCharacter } from '@/hooks/services/character';
|
||||||
import Background from './Background';
|
import Background from './Background';
|
||||||
|
import IconFont from '@/components/ui/iconFont';
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
@ -34,11 +35,11 @@ export default function ChatPage() {
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => setSettingOpen(!settingOpen)}
|
onClick={() => setSettingOpen(!settingOpen)}
|
||||||
className="absolute top-2 right-2"
|
className="absolute z-10 top-2 right-2"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
<i className="iconfont-v2 iconv2-zhankai-1" />
|
<IconFont size={24} className="text-white cursor-pointer" type="icon-zhankai-1" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<SettingDialog open={settingOpen} onOpenChange={setSettingOpen} />
|
<SettingDialog open={settingOpen} onOpenChange={setSettingOpen} />
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ const Character = () => {
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<InfiniteScrollList<any>
|
<InfiniteScrollList<any>
|
||||||
items={dataSource}
|
items={dataSource}
|
||||||
|
enableLazyRender
|
||||||
|
lazyRenderMargin="500px"
|
||||||
columns={(width) => {
|
columns={(width) => {
|
||||||
const cardWidth = width > 1200 ? 256 : width > 588 ? 200 : width > 375 ? 170 : 150;
|
const cardWidth = width > 1200 ? 256 : width > 588 ? 200 : width > 375 ? 170 : 150;
|
||||||
return Math.floor(width / cardWidth);
|
return Math.floor(width / cardWidth);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { useHomeStore } from '../store';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { fetchCharacterTags } from '@/services/editor';
|
import { fetchCharacterTags } from '@/services/editor';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const Filter = () => {
|
const Filter = () => {
|
||||||
const tab = useHomeStore((state) => state.tab);
|
const tab = useHomeStore((state) => state.tab);
|
||||||
|
|
@ -14,6 +15,7 @@ const Filter = () => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const selectedTags = useHomeStore((state) => state.selectedTags);
|
const selectedTags = useHomeStore((state) => state.selectedTags);
|
||||||
const setSelectedTags = useHomeStore((state) => state.setSelectedTags);
|
const setSelectedTags = useHomeStore((state) => state.setSelectedTags);
|
||||||
|
const t = useTranslations('home');
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// const mainContent = document.getElementById('main-content');
|
// const mainContent = document.getElementById('main-content');
|
||||||
|
|
@ -48,13 +50,13 @@ const Filter = () => {
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: 'Story',
|
label: t('story'),
|
||||||
value: 'story',
|
value: 'story',
|
||||||
icon: 'icon-story',
|
icon: 'icon-story',
|
||||||
activeIcon: 'icon-story-active',
|
activeIcon: 'icon-story-active',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Character',
|
label: t('character'),
|
||||||
value: 'character',
|
value: 'character',
|
||||||
icon: 'icon-character',
|
icon: 'icon-character',
|
||||||
activeIcon: 'icon-character-active',
|
activeIcon: 'icon-character-active',
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import Image from 'next/image';
|
||||||
import { IconButton } from '@/components/ui/button';
|
import { IconButton } from '@/components/ui/button';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useMedia } from '@/hooks/tools';
|
import { useLayoutStore } from '@/stores';
|
||||||
|
|
||||||
const Header = React.memo(() => {
|
const Header = React.memo(() => {
|
||||||
const response = useMedia();
|
const response = useLayoutStore((s) => s.response);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <Link href="/crushcoin">
|
// <Link href="/crushcoin">
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ import Story from './components/Story';
|
||||||
import Character from './components/Character';
|
import Character from './components/Character';
|
||||||
import Filter from './components/Filter';
|
import Filter from './components/Filter';
|
||||||
import { useHomeStore } from './store';
|
import { useHomeStore } from './store';
|
||||||
import { useMedia } from '@/hooks/tools';
|
import { useLayoutStore } from '@/stores';
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
const tab = useHomeStore((state) => state.tab);
|
const tab = useHomeStore((state) => state.tab);
|
||||||
const response = useMedia();
|
const response = useLayoutStore((s) => s.response);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { IconButton, Button } from '@/components/ui/button';
|
import { IconButton, Button } from '@/components/ui/button';
|
||||||
import { useCurrentUser, useDeleteUser } from '@/hooks/auth';
|
import { useCurrentUser, useDeleteUser } from '@/hooks/auth';
|
||||||
|
import ProfileLayout from '@/layout/ProfileLayout';
|
||||||
import { ThirdType } from '@/services/auth';
|
import { ThirdType } from '@/services/auth';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
@ -25,10 +26,6 @@ const AccountPage = () => {
|
||||||
const [isDisabling, setIsDisabling] = useState(false);
|
const [isDisabling, setIsDisabling] = useState(false);
|
||||||
const { mutateAsync: deleteUser } = useDeleteUser();
|
const { mutateAsync: deleteUser } = useDeleteUser();
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
router.back();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisableAccount = async () => {
|
const handleDisableAccount = async () => {
|
||||||
setIsDisabling(true);
|
setIsDisabling(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -56,15 +53,7 @@ const AccountPage = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto px-4 max-w-[752px] pt-6 pb-6">
|
<ProfileLayout title="Account">
|
||||||
{/* 标题栏 */}
|
|
||||||
<div className="mb-6 flex items-center gap-2">
|
|
||||||
<IconButton variant="ghost" size="large" onClick={handleBack} className="p-2">
|
|
||||||
<i className="iconfont icon-arrow-left !text-[16px]" />
|
|
||||||
</IconButton>
|
|
||||||
<h1 className="txt-title-l text-txt-primary-normal">Account</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 账户信息容器 */}
|
{/* 账户信息容器 */}
|
||||||
<div className="bg-surface-base-normal rounded-2xl p-6">
|
<div className="bg-surface-base-normal rounded-2xl p-6">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -114,7 +103,7 @@ const AccountPage = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ProfileLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
import { Tag } from '@/components/ui/tag'
|
|
||||||
import { cn, formatNumberToKMB } from '@/lib/utils'
|
|
||||||
import { AIPermission, AiUserBaseListOutput } from '@/services/create'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
const CharacterCard = ({
|
|
||||||
character,
|
|
||||||
isHovered,
|
|
||||||
onHover,
|
|
||||||
}: {
|
|
||||||
character: AiUserBaseListOutput
|
|
||||||
isHovered: boolean
|
|
||||||
onHover: (hovered: boolean) => void
|
|
||||||
}) => {
|
|
||||||
// 根据权限判断是否私密
|
|
||||||
const isPrivate = character.permission === AIPermission.Private
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link href={`/@${character.aiId}`}>
|
|
||||||
<div
|
|
||||||
className="group flex w-full min-w-[200px] grow basis-0 flex-col gap-3"
|
|
||||||
onMouseEnter={() => onHover(true)}
|
|
||||||
onMouseLeave={() => onHover(false)}
|
|
||||||
>
|
|
||||||
{/* 角色图片 */}
|
|
||||||
<div
|
|
||||||
className="relative overflow-hidden rounded-lg bg-cover bg-center bg-no-repeat pb-[133%]"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
// backgroundImage: character.homeImageUrl ? `url('${character.homeImageUrl}')` : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={character.homeImageUrl ?? ''}
|
|
||||||
alt={character.nickname ?? ''}
|
|
||||||
fill
|
|
||||||
className="object-cover object-top"
|
|
||||||
sizes="100%"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 私密标识 */}
|
|
||||||
<div className="bg-surface-element-dark-normal absolute top-2 right-2 z-[1] flex h-6 w-6 items-center justify-center rounded-xs backdrop-blur-xs">
|
|
||||||
<i
|
|
||||||
className={cn('iconfont !text-[12px]', {
|
|
||||||
'icon-eye-off': isPrivate,
|
|
||||||
'icon-eye-on': !isPrivate,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 底部遮罩层 */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'absolute right-0 bottom-0 left-0 flex flex-col justify-end bg-gradient-to-b from-transparent to-black px-3 pb-3',
|
|
||||||
{
|
|
||||||
'pt-6': !isHovered,
|
|
||||||
'h-full pt-12': isHovered,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* 描述文字 - hover时显示,带过渡动效 */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'txt-body-m mb-2 line-clamp-6 transform overflow-hidden text-white transition-all duration-300 ease-in-out',
|
|
||||||
{
|
|
||||||
'max-h-0 opacity-0': !isHovered,
|
|
||||||
'h-full max-h-max opacity-100': isHovered,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{character.introduction || ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 点赞数 - 暂时用固定值,实际应该从API获取 */}
|
|
||||||
<div className="flex h-6 items-center gap-1 px-1 py-0.5">
|
|
||||||
{/* <img src={likeIcon} alt="点赞" className="w-3 h-3" /> */}
|
|
||||||
<i className="iconfont icon-Like-fill" />
|
|
||||||
<span className="text-xs font-medium text-white">
|
|
||||||
{formatNumberToKMB(character.likedNum ?? 0)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 角色信息 */}
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{/* 角色名称 */}
|
|
||||||
<div className="txt-title-m truncate">{character.nickname}</div>
|
|
||||||
|
|
||||||
{/* 标签 */}
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{/* 性格标签 */}
|
|
||||||
{character.characterName && <Tag size="small">{character.characterName}</Tag>}
|
|
||||||
|
|
||||||
{/* 标签 */}
|
|
||||||
{character.tagName && <Tag size="small">{character.tagName}</Tag>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CharacterCard
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import { useCurrentUser } from '@/hooks/auth'
|
|
||||||
import { useGetAICharacterList } from '@/hooks/create'
|
|
||||||
import useCreatorNavigation from '@/hooks/useCreatorNavigation'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
import { isVipDrawerOpenAtom } from '@/atoms/im'
|
|
||||||
import { VipType } from '@/services/wallet'
|
|
||||||
|
|
||||||
const CharacterCardAdd = () => {
|
|
||||||
const { routerToCreate } = useCreatorNavigation()
|
|
||||||
const { data: user } = useCurrentUser()
|
|
||||||
const { isMember, canCreateAiCount } = user || {}
|
|
||||||
const { data: characters = [] } = useGetAICharacterList()
|
|
||||||
const isFull = characters.length >= (canCreateAiCount || 0)
|
|
||||||
const setIsVipDrawerOpen = useSetAtom(isVipDrawerOpenAtom)
|
|
||||||
|
|
||||||
const handleVip = () => {
|
|
||||||
setIsVipDrawerOpen({ open: true, vipType: VipType.ADD_CREATE_AI })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMember && isFull) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMember && isFull) {
|
|
||||||
return (
|
|
||||||
<div className="cursor-pointer" onClick={handleVip}>
|
|
||||||
<div className="bg-surface-element-normal rounded-m pointer-events-none relative w-full pb-[133%]">
|
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
|
||||||
<Image src="/icons/vip.svg" alt="VIP" width={24} height={24} />
|
|
||||||
<span className="bg-gradient-to-r from-[#ff9696] via-[#aa90f9] to-[#8df3e2] bg-clip-text text-base font-medium text-transparent">
|
|
||||||
Add More Characters
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="cursor-pointer" onClick={routerToCreate}>
|
|
||||||
<div className="bg-surface-element-normal rounded-m relative w-full pb-[133%]">
|
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
|
||||||
<i className="iconfont icon-add text-txt-tertiary-normal !text-[24px]" />
|
|
||||||
<span className="text-txt-tertiary-normal txt-body-l">Create a Character</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CharacterCardAdd
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
const CharacterCardVipAdd = () => {
|
|
||||||
return (
|
|
||||||
<div className="cursor-pointer">
|
|
||||||
<div className="bg-surface-element-normal rounded-m border-gradient relative w-full border border-dashed pb-[133%]">
|
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
|
||||||
<i className="iconfont icon-add text-txt-tertiary-normal !text-[24px]" />
|
|
||||||
<span className="text-txt-tertiary-normal txt-body-l">Add More Character</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CharacterCardVipAdd
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import { useGetAICharacterList } from '@/hooks/create'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import CharacterCard from './CharacterCard'
|
|
||||||
import CharacterCardAdd from './CharacterCardAdd'
|
|
||||||
import { useCurrentUser } from '@/hooks/auth'
|
|
||||||
import { isVipDrawerOpenAtom } from '@/atoms/im'
|
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
import { VipType } from '@/services/wallet'
|
|
||||||
|
|
||||||
const AlbumSkeleton = () => (
|
|
||||||
<div className="bg-surface-nest-normal relative animate-pulse overflow-hidden rounded-2xl pb-[134%]">
|
|
||||||
<div className="absolute inset-0">
|
|
||||||
<div className="h-full w-full rounded-2xl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const CharacterList = () => {
|
|
||||||
const { data: characters = [], isPending } = useGetAICharacterList()
|
|
||||||
const { data: user } = useCurrentUser()
|
|
||||||
const { isMember, canCreateAiCount } = user || {}
|
|
||||||
const setIsVipDrawerOpen = useSetAtom(isVipDrawerOpenAtom)
|
|
||||||
|
|
||||||
const [hoveredCard, setHoveredCard] = useState<number | null>(null)
|
|
||||||
|
|
||||||
const handleUnlockMore = () => {
|
|
||||||
setIsVipDrawerOpen({ open: true, vipType: VipType.ADD_CREATE_AI })
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderList = () => {
|
|
||||||
if (isPending) {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3">
|
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
|
||||||
<AlbumSkeleton key={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3">
|
|
||||||
{characters.map((character) => (
|
|
||||||
<CharacterCard
|
|
||||||
key={character.aiId}
|
|
||||||
character={character}
|
|
||||||
isHovered={hoveredCard === character.aiId}
|
|
||||||
onHover={(hovered) => setHoveredCard(hovered ? (character.aiId ?? null) : null)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<CharacterCardAdd />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-6 w-full">
|
|
||||||
{/* 标题和按钮 */}
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h2 className="txt-title-m text-white">Characters</h2>
|
|
||||||
<span className="txt-title-m text-white">
|
|
||||||
{characters.length}/{canCreateAiCount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{!isMember && (
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="cursor-pointer rounded-full p-px"
|
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(to right, #ff9696, #aa90f9, #8df3e2)',
|
|
||||||
}}
|
|
||||||
onClick={handleUnlockMore}
|
|
||||||
>
|
|
||||||
<div className="bg-background-default flex h-8 w-full items-center gap-2 rounded-full px-4">
|
|
||||||
<img src="/icons/vip.svg" alt="VIP" className="h-4 w-4" />
|
|
||||||
<span className="txt-label-s bg-gradient-to-r from-[#ff9696] via-[#aa90f9] to-[#8df3e2] bg-clip-text text-transparent">
|
|
||||||
Unlock More
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{renderList()}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CharacterList
|
|
||||||
|
|
@ -14,13 +14,14 @@ import { useState } from 'react';
|
||||||
import { useLayoutStore } from '@/stores';
|
import { useLayoutStore } from '@/stores';
|
||||||
import { useStreamChatStore } from '../../chat/[id]/stream-chat';
|
import { useStreamChatStore } from '../../chat/[id]/stream-chat';
|
||||||
import { useAsyncFn } from '@/hooks/tools';
|
import { useAsyncFn } from '@/hooks/tools';
|
||||||
|
import IconFont from '@/components/ui/iconFont';
|
||||||
|
|
||||||
const ProfileDropdownItem = ({
|
const ProfileDropdownItem = ({
|
||||||
icon,
|
icon,
|
||||||
children,
|
children,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
icon: string;
|
icon: string | React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -30,7 +31,7 @@ const ProfileDropdownItem = ({
|
||||||
className="flex w-full items-center justify-between gap-3 cursor-pointer px-5 py-4 hover:bg-white/5 transition-colors"
|
className="flex w-full items-center justify-between gap-3 cursor-pointer px-5 py-4 hover:bg-white/5 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<i className={`iconfont ${icon} text-xl text-white`} />
|
{typeof icon === 'string' ? <i className={`iconfont ${icon} text-xl text-white`} /> : icon}
|
||||||
<div className="txt-body-l text-white">{children}</div>
|
<div className="txt-body-l text-white">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
<i className="iconfont icon-arrow-right-border text-white/60 text-lg" />
|
<i className="iconfont icon-arrow-right-border text-white/60 text-lg" />
|
||||||
|
|
@ -61,7 +62,7 @@ const ProfileDropdown = () => {
|
||||||
| {
|
| {
|
||||||
type: 'item';
|
type: 'item';
|
||||||
label: string;
|
label: string;
|
||||||
icon: string;
|
icon: string | React.ReactNode;
|
||||||
href?: string;
|
href?: string;
|
||||||
target?: string;
|
target?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
|
@ -73,6 +74,12 @@ const ProfileDropdown = () => {
|
||||||
icon: 'icon-icon_order_remark',
|
icon: 'icon-icon_order_remark',
|
||||||
href: '/profile/edit',
|
href: '/profile/edit',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Masked Identity Mode',
|
||||||
|
icon: <IconFont type="icon-shezhi" />,
|
||||||
|
href: '/profile/mask',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Account',
|
label: 'Account',
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { calculateAge } from '@/lib/utils';
|
import { calculateAge } from '@/lib/utils';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import ProfileLayout from '@/layout/ProfileLayout';
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
|
|
@ -196,15 +197,7 @@ const EditPage = () => {
|
||||||
}, [selectedYear, selectedMonth, selectedDay, form]);
|
}, [selectedYear, selectedMonth, selectedDay, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto px-4 max-w-[752px] pt-6 pb-6">
|
<ProfileLayout title="Edit Profile">
|
||||||
{/* 标题栏 */}
|
|
||||||
<div className="mb-6 flex items-center gap-2">
|
|
||||||
<IconButton variant="ghost" size="large" onClick={handleBack} className="p-2">
|
|
||||||
<i className="iconfont icon-arrow-left !text-[16px]" />
|
|
||||||
</IconButton>
|
|
||||||
<h1 className="txt-title-l text-txt-primary-normal">Edit Profile</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 表单容器 */}
|
{/* 表单容器 */}
|
||||||
<div className="bg-surface-base-normal rounded-2xl p-6">
|
<div className="bg-surface-base-normal rounded-2xl p-6">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
|
|
@ -336,7 +329,7 @@ const EditPage = () => {
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ProfileLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
'use client';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { Gender } from '@/types/user';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useCurrentUser } from '@/hooks/auth';
|
||||||
|
import { useAsyncFn } from '@/hooks/tools';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import GenderInput from '@/components/features/genderInput';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { InputNumber } from '@/components/ui/inputnumber';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
nickname: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, 'Nickname is required')
|
||||||
|
.min(2, 'Nickname must be between 2 and 20 characters'),
|
||||||
|
gender: z.enum(Gender, { message: 'Please select a gender' }),
|
||||||
|
age: z.number().min(1, 'Age is required'),
|
||||||
|
whoAmI: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type MaskFormProps = {
|
||||||
|
onSubmitSuccess?: (data: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MaskForm = ({ onSubmitSuccess }: MaskFormProps) => {
|
||||||
|
const { data: user } = useCurrentUser();
|
||||||
|
const form = useForm<z.infer<typeof schema>>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
nickname: user?.nickname || '',
|
||||||
|
gender: user?.sex,
|
||||||
|
age: 18,
|
||||||
|
whoAmI: '',
|
||||||
|
},
|
||||||
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { run: onSubmitFn, loading } = useAsyncFn(async (data: any) => {
|
||||||
|
onSubmitSuccess?.(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
formState: { isValid, isDirty },
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmitFn)} className="space-y-6">
|
||||||
|
{/* 昵称字段 */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="nickname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="txt-label-m text-txt-primary-normal">Nickname</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter nickname"
|
||||||
|
maxLength={100}
|
||||||
|
showCount
|
||||||
|
error={!!form.formState.errors.nickname}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* 性别字段 */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="gender"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="txt-label-m text-txt-primary-normal">Gender</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<GenderInput value={field.value as Gender} onChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
<p className="txt-body-s text-txt-secondary-normal">
|
||||||
|
Please note: gender cannot be changed after setting
|
||||||
|
</p>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* AGE字段 */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="age"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="txt-label-m text-txt-primary-normal">Age</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<InputNumber
|
||||||
|
min={18}
|
||||||
|
placeholder="Enter age"
|
||||||
|
error={!!form.formState.errors.age}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* WHO AM I */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="whoAmI"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="txt-label-m text-txt-primary-normal">
|
||||||
|
Who am I(optional)
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Describe the character characteristics and scene settings of your role"
|
||||||
|
error={!!form.formState.errors.whoAmI}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* 保存按钮 */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
size="large"
|
||||||
|
disabled={!isValid || !isDirty}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Gender } from '@/types/user';
|
||||||
|
import { useCurrentUser } from '@/hooks/auth';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
type MaskListProps = {
|
||||||
|
selectAble?: boolean;
|
||||||
|
value?: null;
|
||||||
|
onChange?: (mask: any) => void;
|
||||||
|
onAdd?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MaskList(props: MaskListProps) {
|
||||||
|
const { selectAble, value, onChange, onAdd } = props;
|
||||||
|
const { data: user } = useCurrentUser();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const masks = [
|
||||||
|
{
|
||||||
|
nickname: 'NickName',
|
||||||
|
gender: 0,
|
||||||
|
age: 18,
|
||||||
|
whoAmI: ' whoAmI whoAmI whoAmI whoAmI whoAmI whoAmI whoAmI whoAmI',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSelect = (mask: any) => {
|
||||||
|
if (selectAble) {
|
||||||
|
onChange?.(mask);
|
||||||
|
} else {
|
||||||
|
router.push(`/profile/mask/${mask.nickname}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconRender = (mask: any) => {
|
||||||
|
if (selectAble) {
|
||||||
|
return <Checkbox shape="round" checked />;
|
||||||
|
} else {
|
||||||
|
return <i className="iconfont icon-arrow-right-border" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{masks.map((mask) => (
|
||||||
|
<div key={mask.nickname} className="rounded-lg overflow-hidden">
|
||||||
|
<div
|
||||||
|
onClick={() => handleSelect(mask)}
|
||||||
|
className="flex bg-white/10 px-4 py-3 items-center justify-between"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{mask.nickname}、{mask.gender === Gender.MALE ? 'Male' : 'Female'}、{mask.age}
|
||||||
|
</span>
|
||||||
|
{iconRender(mask)}
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/5 px-4 py-3">{mask.whoAmI}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{onAdd && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<Button variant="primary" className="w-full" onClick={onAdd}>
|
||||||
|
+ Add new Mask
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
'use client';
|
||||||
|
import { useCurrentUser } from '@/hooks/auth';
|
||||||
|
import { MaskForm } from '../MaskForm';
|
||||||
|
import { IconButton } from '@/components/ui/button';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import ProfileLayout from '@/layout/ProfileLayout';
|
||||||
|
|
||||||
|
export default function MaskPage() {
|
||||||
|
const { data: user } = useCurrentUser();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProfileLayout title="Edit">
|
||||||
|
<div className="bg-surface-base-normal rounded-2xl p-6">
|
||||||
|
<MaskForm onSubmitSuccess={() => router.push(`/profile/mask`)} />
|
||||||
|
</div>
|
||||||
|
</ProfileLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
'use client';
|
||||||
|
import { Button, IconButton } from '@/components/ui/button';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import ProfileLayout from '@/layout/ProfileLayout';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import IconFont from '@/components/ui/iconFont';
|
||||||
|
import { useLayoutStore } from '@/stores';
|
||||||
|
import MaskList from './MaskList';
|
||||||
|
|
||||||
|
export default function MaskPage() {
|
||||||
|
const response = useLayoutStore((s) => s.response);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProfileLayout
|
||||||
|
bottomButton={
|
||||||
|
<Link href={'/profile/mask/new'} className="w-full">
|
||||||
|
<Button className="w-full" variant="primary">
|
||||||
|
+ Add new Mask
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
rightDom={
|
||||||
|
response?.isPC && (
|
||||||
|
<IconFont
|
||||||
|
onClick={() => router.push('/profile/mask/new')}
|
||||||
|
size={32}
|
||||||
|
className="text-white cursor-pointer"
|
||||||
|
type="icon-tianjia"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title="Masked Identity Mode"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-6 bg-surface-base-normal p-6 rounded-2xl">
|
||||||
|
<MaskList />
|
||||||
|
</div>
|
||||||
|
</ProfileLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -82,8 +82,6 @@ export default function ProfilePage() {
|
||||||
<ProfileFeatureList />
|
<ProfileFeatureList />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <CharacterList /> */}
|
|
||||||
|
|
||||||
<ProfileDropdown />
|
<ProfileDropdown />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ import localFont from 'next/font/local';
|
||||||
import '../css/iconfont.css';
|
import '../css/iconfont.css';
|
||||||
import '../css/iconfont-v2.css';
|
import '../css/iconfont-v2.css';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
import { Providers } from '@/lib/providers';
|
import { Providers } from '@/layout/Providers';
|
||||||
import { DeviceIdProvider } from '@/components/device-id-provider';
|
import ProgressBar from '@/layout/Providers/ProgressBar';
|
||||||
import ProgressBar from '@/context/progress';
|
import Script from 'next/script';
|
||||||
|
|
||||||
const poppins = Poppins({
|
const poppins = Poppins({
|
||||||
variable: '--font-poppins',
|
variable: '--font-poppins',
|
||||||
|
|
@ -50,11 +50,12 @@ export default async function RootLayout({
|
||||||
<body
|
<body
|
||||||
className={`${poppins.variable} ${oleoScriptSwashCaps.variable} ${NumDisplay.variable} antialiased`}
|
className={`${poppins.variable} ${oleoScriptSwashCaps.variable} ${NumDisplay.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<DeviceIdProvider>
|
<Script
|
||||||
<Providers>
|
src="//at.alicdn.com/t/c/font_5076160_m6catzpb7dc.js"
|
||||||
<ProgressBar>{children}</ProgressBar>
|
strategy="afterInteractive"
|
||||||
</Providers>
|
async
|
||||||
</DeviceIdProvider>
|
/>
|
||||||
|
<Providers>{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { atom } from 'jotai'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建AI达到上限
|
|
||||||
*/
|
|
||||||
export const isCreateAiLimitReachedDialogOpenAtom = atom<boolean>(false)
|
|
||||||
|
|
@ -14,25 +14,7 @@ interface AIStandardCardProps {
|
||||||
|
|
||||||
const AIStandardCard: React.FC<AIStandardCardProps> = React.memo(
|
const AIStandardCard: React.FC<AIStandardCardProps> = React.memo(
|
||||||
({ character, disableHover = false }) => {
|
({ character, disableHover = false }) => {
|
||||||
const {
|
const { id, name, description, coverImage, headPortrait, tags } = character;
|
||||||
id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
coverImage,
|
|
||||||
sourceId,
|
|
||||||
sourceType,
|
|
||||||
headPortrait,
|
|
||||||
basicInfo,
|
|
||||||
exampleDialogue,
|
|
||||||
note,
|
|
||||||
firstSentence,
|
|
||||||
characterStand,
|
|
||||||
tagId,
|
|
||||||
greeting,
|
|
||||||
depth,
|
|
||||||
tags,
|
|
||||||
chatTarget,
|
|
||||||
} = character;
|
|
||||||
|
|
||||||
const introContainerRef = useRef<HTMLDivElement>(null);
|
const introContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const introTextRef = useRef<HTMLParagraphElement>(null);
|
const introTextRef = useRef<HTMLParagraphElement>(null);
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from '@/components/ui/alert-dialog'
|
|
||||||
import { useAtom } from 'jotai'
|
|
||||||
import { isCreateAiLimitReachedDialogOpenAtom } from '@/atoms/global'
|
|
||||||
|
|
||||||
const CreateReachedLimitDialog = () => {
|
|
||||||
const [isCreateAiLimitReachedDialogOpen, setIsCreateAiLimitReachedDialogOpen] = useAtom(
|
|
||||||
isCreateAiLimitReachedDialogOpenAtom
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertDialog
|
|
||||||
open={isCreateAiLimitReachedDialogOpen}
|
|
||||||
onOpenChange={setIsCreateAiLimitReachedDialogOpen}
|
|
||||||
>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Warning</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
you have reached the maximum number of AI creations.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogAction>Got it</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreateReachedLimitDialog
|
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
import * as React from 'react'
|
'use client';
|
||||||
import { Chip } from '@/components/ui/chip'
|
import { Chip } from '@/components/ui/chip';
|
||||||
import { Gender } from '@/types/user'
|
import { Gender } from '@/types/user';
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const GenderInput = ({
|
const GenderInput = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: {
|
}: {
|
||||||
value: Gender | undefined
|
value: Gender | undefined;
|
||||||
onChange: (value: Gender) => void
|
onChange: (value: Gender) => void;
|
||||||
disabled?: boolean
|
disabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
value: Gender.MALE,
|
value: Gender.MALE,
|
||||||
label: 'Male',
|
label: 'Male',
|
||||||
className: 'bg-[rgba(78,72,255,1)] border-[rgba(78,72,255,1)] text-white',
|
className: 'border-[rgba(78,72,255,1)] text-white',
|
||||||
icon: <i className="iconfont icon-male !text-[24px]" aria-hidden="true"></i>,
|
icon: <i className="iconfont icon-male !text-[24px]" aria-hidden="true"></i>,
|
||||||
|
activeClassName: 'bg-[rgba(78,72,255,1)]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: Gender.FEMALE,
|
value: Gender.FEMALE,
|
||||||
|
|
@ -29,29 +30,37 @@ const GenderInput = ({
|
||||||
label: 'Other',
|
label: 'Other',
|
||||||
icon: 'Other',
|
icon: 'Other',
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-3 gap-2" role="radiogroup" aria-label="Gender">
|
<div className="grid grid-cols-3 gap-2" role="radiogroup" aria-label="Gender">
|
||||||
{items.map((item) => (
|
{items.map((item) => {
|
||||||
|
const isActive = value === item.value;
|
||||||
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
size="large"
|
size="large"
|
||||||
block
|
block
|
||||||
key={item.value}
|
key={item.value}
|
||||||
disabled={disabled && value !== item.value}
|
disabled={disabled && value !== item.value}
|
||||||
state={value === item.value ? 'active' : 'inactive'}
|
state={isActive ? 'active' : 'inactive'}
|
||||||
className={cn('cursor-pointer', item.className, disabled && 'pointer-events-none')}
|
className={cn(
|
||||||
|
'cursor-pointer',
|
||||||
|
item.className,
|
||||||
|
isActive && item.activeClassName,
|
||||||
|
disabled && 'pointer-events-none'
|
||||||
|
)}
|
||||||
onClick={() => !disabled && onChange(item.value)}
|
onClick={() => !disabled && onChange(item.value)}
|
||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={value === item.value}
|
aria-checked={isActive}
|
||||||
aria-label={item.label}
|
aria-label={item.label}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
<span className="sr-only">{item.label}</span>
|
<span className="sr-only">{item.label}</span>
|
||||||
</Chip>
|
</Chip>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default GenderInput
|
export default GenderInput;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type IconFontProps = {
|
||||||
|
/** 图标名称,对应 iconfont 中的图标 ID */
|
||||||
|
type: string;
|
||||||
|
/** 图标大小,如果不传则继承父元素的 font-size */
|
||||||
|
size?: number;
|
||||||
|
/** 图标颜色,默认 currentColor(继承父元素颜色) */
|
||||||
|
color?: string;
|
||||||
|
/** 额外的 className */
|
||||||
|
className?: string;
|
||||||
|
} & React.HTMLAttributes<SVGSVGElement>;
|
||||||
|
|
||||||
|
const IconFont = ({
|
||||||
|
type,
|
||||||
|
size,
|
||||||
|
color = 'currentColor',
|
||||||
|
className = '',
|
||||||
|
...props
|
||||||
|
}: IconFontProps) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={`iconfont-v2 ${className}`}
|
||||||
|
style={size ? { fontSize: size, color } : { color }}
|
||||||
|
aria-hidden="true"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<use xlinkHref={`#${type}`}></use>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default IconFont;
|
||||||
|
|
@ -1,8 +1,57 @@
|
||||||
import React, { ReactNode, useEffect, useMemo, useRef } from 'react';
|
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useInfiniteScroll } from '@/hooks/useInfiniteScroll';
|
import { useInfiniteScroll } from '@/hooks/useInfiniteScroll';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useSize } from 'ahooks';
|
import { useSize } from 'ahooks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 懒渲染项组件
|
||||||
|
* 使用 IntersectionObserver 监控元素可见性,只渲染可见的内容
|
||||||
|
*/
|
||||||
|
interface LazyItemProps {
|
||||||
|
children: ReactNode;
|
||||||
|
rootMargin?: string;
|
||||||
|
placeholder?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LazyItem({ children, rootMargin = '300px', placeholder }: LazyItemProps) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [hasBeenVisible, setHasBeenVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = ref.current;
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
// 如果已经渲染过,不需要再观察
|
||||||
|
if (hasBeenVisible) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setHasBeenVisible(true);
|
||||||
|
// ✅ 渲染后立即断开观察,释放资源
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rootMargin,
|
||||||
|
threshold: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(element);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [rootMargin, hasBeenVisible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
{hasBeenVisible ? children : placeholder || <div className="aspect-[3/4]" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface InfiniteScrollListProps<T> {
|
interface InfiniteScrollListProps<T> {
|
||||||
/**
|
/**
|
||||||
* 数据项数组
|
* 数据项数组
|
||||||
|
|
@ -81,6 +130,14 @@ interface InfiniteScrollListProps<T> {
|
||||||
* 是否启用无限滚动
|
* 是否启用无限滚动
|
||||||
*/
|
*/
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否启用懒渲染(只渲染可见区域的内容)
|
||||||
|
*/
|
||||||
|
enableLazyRender?: boolean;
|
||||||
|
/**
|
||||||
|
* 懒渲染的根边距(提前多少px开始渲染)
|
||||||
|
*/
|
||||||
|
lazyRenderMargin?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -105,6 +162,8 @@ export function InfiniteScrollList<T>({
|
||||||
onRetry,
|
onRetry,
|
||||||
threshold = 200,
|
threshold = 200,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
|
enableLazyRender = false,
|
||||||
|
lazyRenderMargin = '300px',
|
||||||
}: InfiniteScrollListProps<T>) {
|
}: InfiniteScrollListProps<T>) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const size = useSize(ref);
|
const size = useSize(ref);
|
||||||
|
|
@ -199,9 +258,26 @@ export function InfiniteScrollList<T>({
|
||||||
<>
|
<>
|
||||||
{/* 主要内容 */}
|
{/* 主要内容 */}
|
||||||
<div className={cn('grid', gridColsClass, gapClass, className)}>
|
<div className={cn('grid', gridColsClass, gapClass, className)}>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => {
|
||||||
<React.Fragment key={getItemKey(item, index)}>{renderItem(item, index)}</React.Fragment>
|
const key = getItemKey(item, index);
|
||||||
))}
|
const content = renderItem(item, index);
|
||||||
|
|
||||||
|
if (enableLazyRender) {
|
||||||
|
return (
|
||||||
|
<LazyItem
|
||||||
|
key={key}
|
||||||
|
rootMargin={lazyRenderMargin}
|
||||||
|
placeholder={
|
||||||
|
<div className="bg-surface-nest-normal aspect-[3/4] animate-pulse rounded-2xl" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</LazyItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <React.Fragment key={key}>{content}</React.Fragment>;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{/* 加载更多触发器 - 只在没有错误时显示 */}
|
{/* 加载更多触发器 - 只在没有错误时显示 */}
|
||||||
{hasNextPage && !hasError && (
|
{hasNextPage && !hasError && (
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react';
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
type InputSize = 'small' | 'large'
|
type InputSize = 'small' | 'large';
|
||||||
|
|
||||||
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||||
prefixIcon?: React.ReactNode
|
prefixIcon?: React.ReactNode;
|
||||||
showCount?: boolean
|
showCount?: boolean;
|
||||||
size?: InputSize
|
size?: InputSize;
|
||||||
error?: boolean
|
error?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Input({
|
function Input({
|
||||||
|
|
@ -22,16 +22,16 @@ function Input({
|
||||||
size = 'large',
|
size = 'large',
|
||||||
...props
|
...props
|
||||||
}: InputProps) {
|
}: InputProps) {
|
||||||
let inputValue = ''
|
let inputValue = '';
|
||||||
if (typeof value === 'string' || typeof value === 'number') {
|
if (typeof value === 'string' || typeof value === 'number') {
|
||||||
inputValue = String(value)
|
inputValue = String(value);
|
||||||
}
|
}
|
||||||
const count = inputValue.length
|
const count = inputValue.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex w-full items-center',
|
'relative flex w-full gap-2 items-center',
|
||||||
error && '[&>input]:border-primary-variant-press'
|
error && '[&>input]:border-primary-variant-press'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -66,7 +66,7 @@ function Input({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Input }
|
export { Input };
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useControllableValue } from 'ahooks';
|
||||||
|
|
||||||
|
type InputSize = 'small' | 'large';
|
||||||
|
|
||||||
|
interface InputNumberProps extends Omit<
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
'size' | 'value' | 'onChange' | 'type'
|
||||||
|
> {
|
||||||
|
prefixIcon?: React.ReactNode;
|
||||||
|
size?: InputSize;
|
||||||
|
error?: boolean;
|
||||||
|
value?: number;
|
||||||
|
onChange?: (value: number) => void;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputNumber({
|
||||||
|
className,
|
||||||
|
prefixIcon,
|
||||||
|
error = false,
|
||||||
|
size = 'large',
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
...props
|
||||||
|
}: InputNumberProps) {
|
||||||
|
const [value, onChange] = useControllableValue(props);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const inputValue = e.target.value;
|
||||||
|
// 验证是否为有效数字
|
||||||
|
const numValue = parseFloat(inputValue);
|
||||||
|
if (isNaN(numValue)) {
|
||||||
|
onChange(inputValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange(numValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex w-full gap-2 items-center',
|
||||||
|
error && '[&>input]:border-primary-variant-press'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{prefixIcon && (
|
||||||
|
<span className="pointer-events-none absolute left-3 flex items-center">{prefixIcon}</span>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
'placeholder:text-txt-tertiary-normal bg-surface-element-normal rounded-sm',
|
||||||
|
'selection:bg-surface-element-normal selection:text-txt-primary-normal',
|
||||||
|
'border border-solid border-transparent',
|
||||||
|
'hover:border-outline-hover focus-visible:border-primary-variant-press',
|
||||||
|
'transition-[color,box-shadow] outline-none',
|
||||||
|
'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
size === 'small' && 'txt-body-m flex h-8 w-full min-w-0 px-4 py-1',
|
||||||
|
size === 'large' && 'txt-body-l flex h-12 w-full min-w-0 px-4 py-1',
|
||||||
|
error && '!border-important-variant-normal',
|
||||||
|
prefixIcon && 'pl-10',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
aria-invalid={error}
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
{min !== undefined && (
|
||||||
|
<span className="text-txt-secondary-normal absolute right-3 text-xs select-none">
|
||||||
|
{'≥'}
|
||||||
|
{min}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { InputNumber };
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react';
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useControllableValue } from 'ahooks';
|
||||||
|
|
||||||
interface TextareaProps extends React.ComponentProps<'textarea'> {
|
interface TextareaProps extends React.ComponentProps<'textarea'> {
|
||||||
showCount?: boolean
|
showCount?: boolean;
|
||||||
error?: boolean
|
error?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Textarea({
|
function Textarea({
|
||||||
|
|
@ -12,14 +13,9 @@ function Textarea({
|
||||||
showCount = false,
|
showCount = false,
|
||||||
error = false,
|
error = false,
|
||||||
maxLength,
|
maxLength,
|
||||||
value,
|
|
||||||
...props
|
...props
|
||||||
}: TextareaProps) {
|
}: TextareaProps) {
|
||||||
let inputValue = ''
|
const [value, onChange] = useControllableValue(props);
|
||||||
if (typeof value === 'string' || typeof value === 'number') {
|
|
||||||
inputValue = String(value)
|
|
||||||
}
|
|
||||||
const count = inputValue.length
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative w-full', error && '[&>textarea]:border-primary-variant-press')}>
|
<div className={cn('relative w-full', error && '[&>textarea]:border-primary-variant-press')}>
|
||||||
|
|
@ -38,17 +34,18 @@ function Textarea({
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
value={value}
|
|
||||||
aria-invalid={error}
|
aria-invalid={error}
|
||||||
{...props}
|
{...props}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
{showCount && typeof maxLength === 'number' && (
|
{showCount && typeof maxLength === 'number' && (
|
||||||
<span className="text-txt-secondary-normal absolute right-4 bottom-3 text-xs select-none">
|
<span className="text-txt-secondary-normal absolute right-4 bottom-3 text-xs select-none">
|
||||||
{count}/{maxLength}
|
{value.length}/{maxLength}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea };
|
||||||
|
|
|
||||||
|
|
@ -1,130 +1,12 @@
|
||||||
@font-face {
|
/* iconfont 默认样式 */
|
||||||
font-family: 'iconfont-v2'; /* Project id 5076160 - spicyxx.ai */
|
|
||||||
src:
|
|
||||||
url('/font-v2/iconfont.woff2?t=1766052523135') format('woff2'),
|
|
||||||
url('/font-v2/iconfont.woff?t=1766052523135') format('woff'),
|
|
||||||
url('/font-v2/iconfont.ttf?t=1766052523135') format('truetype');
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconfont-v2 {
|
.iconfont-v2 {
|
||||||
font-family: 'iconfont-v2' !important;
|
/* 不设置 font-size,让它继承父元素的 font-size */
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
fill: currentColor;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: -0.15em;
|
||||||
|
overflow: hidden;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logo */
|
|
||||||
.iconv2-Logo:before {
|
|
||||||
content: '\e624';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 我的界面前往 */
|
|
||||||
.iconv2-wodejiemianqianwang:before {
|
|
||||||
content: '\e622';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 群聊 */
|
|
||||||
.iconv2-qunliao:before {
|
|
||||||
content: '\e621';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 挂断电话 */
|
|
||||||
.iconv2-guaduandianhua:before {
|
|
||||||
content: '\e620';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 电话 */
|
|
||||||
.iconv2-dianhua:before {
|
|
||||||
content: '\e61f';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 艾特 */
|
|
||||||
.iconv2-aite:before {
|
|
||||||
content: '\e61d';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 性别 */
|
|
||||||
.iconv2-xingbie:before {
|
|
||||||
content: '\e61e';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Frame 247 */
|
|
||||||
.iconv2-a-Frame247:before {
|
|
||||||
content: '\e61c';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 编辑 */
|
|
||||||
.iconv2-bianji:before {
|
|
||||||
content: '\e61b';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 展开 */
|
|
||||||
.iconv2-zhankai1:before {
|
|
||||||
content: '\e615';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Frame 195 */
|
|
||||||
.iconv2-a-Frame195:before {
|
|
||||||
content: '\e616';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 展开-1 */
|
|
||||||
.iconv2-zhankai-1:before {
|
|
||||||
content: '\e617';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 生成 */
|
|
||||||
.iconv2-shengcheng:before {
|
|
||||||
content: '\e618';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 复制 */
|
|
||||||
.iconv2-fuzhi:before {
|
|
||||||
content: '\e619';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Frame 194 */
|
|
||||||
.iconv2-a-Frame194:before {
|
|
||||||
content: '\e61a';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 女性性别图标 */
|
|
||||||
.iconv2-gender-female-line:before {
|
|
||||||
content: '\e614';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 男性性别图标 */
|
|
||||||
.iconv2-gender-male-line:before {
|
|
||||||
content: '\e613';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 刷新 */
|
|
||||||
.iconv2-shuaxin:before {
|
|
||||||
content: '\e612';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 箭头 */
|
|
||||||
.iconv2-jiantou:before {
|
|
||||||
content: '\e610';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 关闭 */
|
|
||||||
.iconv2-guanbi:before {
|
|
||||||
content: '\e611';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 搜索 */
|
|
||||||
.iconv2-sousuo:before {
|
|
||||||
content: '\e60d';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 折叠 */
|
|
||||||
.iconv2-zhedie:before {
|
|
||||||
content: '\e60e';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 展开 */
|
|
||||||
.iconv2-zhankai:before {
|
|
||||||
content: '\e60f';
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,7 @@
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const ResponsiveConfig = {
|
export const useMedia = (config: Record<string, number>) => {
|
||||||
xs: 375,
|
|
||||||
// 从这里开始变为移动端试图
|
|
||||||
sm: 768,
|
|
||||||
md: 1024,
|
|
||||||
lg: 1280,
|
|
||||||
xl: 1440,
|
|
||||||
};
|
|
||||||
export const useMedia = (config: Record<string, number> = ResponsiveConfig) => {
|
|
||||||
// 追踪客户端是否已挂载
|
// 追踪客户端是否已挂载
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [response, setResponse] = useState<Record<keyof typeof config, boolean>>();
|
const [response, setResponse] = useState<Record<keyof typeof config, boolean>>();
|
||||||
|
|
@ -18,11 +10,9 @@ export const useMedia = (config: Record<string, number> = ResponsiveConfig) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
let hasChanged = false;
|
|
||||||
const newResponse = Object.fromEntries(
|
const newResponse = Object.fromEntries(
|
||||||
Object.entries(config).map(([key, value]) => {
|
Object.entries(config).map(([key, value]) => {
|
||||||
if (window.innerWidth > value) {
|
if (window.innerWidth > value) {
|
||||||
hasChanged = true;
|
|
||||||
return [key, true];
|
return [key, true];
|
||||||
}
|
}
|
||||||
return [key, false];
|
return [key, false];
|
||||||
|
|
@ -35,7 +25,6 @@ export const useMedia = (config: Record<string, number> = ResponsiveConfig) => {
|
||||||
return () => window.removeEventListener('resize', onResize);
|
return () => window.removeEventListener('resize', onResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 在服务端渲染和客户端首次渲染时返回 undefined,避免 hydration 不匹配
|
|
||||||
if (!mounted) return undefined;
|
if (!mounted) return undefined;
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { usePathname, useRouter } from 'next/navigation'
|
|
||||||
import { useCurrentUser, useToken } from './auth'
|
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
import { isCreateAiLimitReachedDialogOpenAtom } from '@/atoms/global'
|
|
||||||
import { VipType } from '@/services/wallet'
|
|
||||||
import { isVipDrawerOpenAtom } from '@/atoms/im'
|
|
||||||
|
|
||||||
const CREATE_CHARACTER_ROUTE = '/create/type'
|
|
||||||
|
|
||||||
const useCreatorNavigation = () => {
|
|
||||||
const router = useRouter()
|
|
||||||
const pathname = usePathname()
|
|
||||||
const { data: currentUser } = useCurrentUser()
|
|
||||||
const { isLogin } = useToken()
|
|
||||||
const { isMember, canCreateAiCount, createdAiCount } = currentUser || {}
|
|
||||||
const setIsCreateAiLimitReachedDialogOpen = useSetAtom(isCreateAiLimitReachedDialogOpenAtom)
|
|
||||||
const setIsVipDrawerOpen = useSetAtom(isVipDrawerOpenAtom)
|
|
||||||
|
|
||||||
const loginRedirectHref = `/login?redirect=${encodeURIComponent(CREATE_CHARACTER_ROUTE)}`
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// 根据登录状态选择性 prefetch,避免缓存重定向响应
|
|
||||||
if (isLogin) {
|
|
||||||
router.prefetch(CREATE_CHARACTER_ROUTE)
|
|
||||||
} else {
|
|
||||||
router.prefetch(loginRedirectHref)
|
|
||||||
}
|
|
||||||
}, [router, loginRedirectHref, isLogin])
|
|
||||||
|
|
||||||
const routerToCreate = () => {
|
|
||||||
// 检查是否登录,如果未登录则跳转到登录页面
|
|
||||||
if (!isLogin) {
|
|
||||||
router.push(loginRedirectHref)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((createdAiCount ?? 0) >= (canCreateAiCount ?? 0)) {
|
|
||||||
if (isMember) {
|
|
||||||
setIsCreateAiLimitReachedDialogOpen(true)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
setIsVipDrawerOpen({ open: true, vipType: VipType.ADD_CREATE_AI })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('before_creator_navigation', pathname)
|
|
||||||
router.push(CREATE_CHARACTER_ROUTE)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
routerToCreate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useCreatorNavigation
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { MenuItem } from '@/types/global';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import ChatSidebar from './components/ChatSidebar';
|
import ChatSidebar from './components/ChatSidebar';
|
||||||
import { Badge } from '../components/ui/badge';
|
// import { Badge } from '../components/ui/badge';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useLayoutStore } from '@/stores';
|
import { useLayoutStore } from '@/stores';
|
||||||
|
|
@ -24,12 +24,6 @@ function Sidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isSidebarExpanded = useLayoutStore((s) => s.isSidebarExpanded);
|
const isSidebarExpanded = useLayoutStore((s) => s.isSidebarExpanded);
|
||||||
const setSidebarExpanded = useLayoutStore((s) => s.setSidebarExpanded);
|
const setSidebarExpanded = useLayoutStore((s) => s.setSidebarExpanded);
|
||||||
const setHydrated = useLayoutStore((s) => s.setHydrated);
|
|
||||||
|
|
||||||
// 在客户端挂载后,从 localStorage 恢复侧边栏状态
|
|
||||||
useEffect(() => {
|
|
||||||
setHydrated();
|
|
||||||
}, [setHydrated]);
|
|
||||||
|
|
||||||
const menuItems: IMenuItem[] = [
|
const menuItems: IMenuItem[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -95,11 +89,7 @@ function Sidebar() {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{/* 分割线 */}
|
<ChatSidebar showSeparator />
|
||||||
<div className="mx-6 my-4">
|
|
||||||
<div className="bg-outline-normal h-px" />
|
|
||||||
</div>
|
|
||||||
<ChatSidebar />
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|
@ -1,47 +1,36 @@
|
||||||
'use client';
|
'use client';
|
||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import Image from 'next/image';
|
import { Button, IconButton } from '@/components/ui/button';
|
||||||
import { Button, IconButton } from '../components/ui/button';
|
|
||||||
import { useCurrentUser } from '@/hooks/auth';
|
import { useCurrentUser } from '@/hooks/auth';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '../components/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname, useSearchParams, useRouter } from 'next/navigation';
|
import { usePathname, useSearchParams, useRouter } from 'next/navigation';
|
||||||
import { useMedia } from '@/hooks/tools';
|
|
||||||
import Notice from './components/Notice';
|
import Notice from './components/Notice';
|
||||||
|
import LocaleSwitch from './components/LocaleSwitch';
|
||||||
import { items } from './BottomBar';
|
import { items } from './BottomBar';
|
||||||
|
import { getTopbarConfig } from './config';
|
||||||
const mobileHidenMenus = ['/profile/edit', '/profile/account', '/character/:id'];
|
import IconFont from '@/components/ui/iconFont';
|
||||||
|
import { useLayoutStore } from '@/stores';
|
||||||
// 将路由模式转换为正则表达式进行匹配
|
|
||||||
function matchRoutePattern(pathname: string, patterns: string[]): boolean {
|
|
||||||
return patterns.some((pattern) => {
|
|
||||||
// 将路由模式中的 :param 转换为正则表达式
|
|
||||||
const regexPattern = pattern
|
|
||||||
.replace(/:[^/]+/g, '[^/]+') // 将 :id, :userId 等替换为 [^/]+
|
|
||||||
.replace(/\//g, '\\/'); // 转义斜杠
|
|
||||||
const regex = new RegExp(`^${regexPattern}$`);
|
|
||||||
return regex.test(pathname);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const blurPages = ['/chat/:id'];
|
|
||||||
|
|
||||||
function Topbar() {
|
function Topbar() {
|
||||||
const [isBlur, setIsBlur] = useState(false);
|
const [isScrollBlur, setIsScrollBlur] = useState(false);
|
||||||
const { data: user } = useCurrentUser();
|
const { data: user, isLoading } = useCurrentUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const response = useMedia();
|
const response = useLayoutStore((s) => s.response);
|
||||||
const searchParamsString = searchParams.toString();
|
const searchParamsString = searchParams.toString();
|
||||||
const redirectURL = `${pathname}${searchParamsString ? `?${searchParamsString}` : ''}`;
|
const redirectURL = `${pathname}${searchParamsString ? `?${searchParamsString}` : ''}`;
|
||||||
const loginHref = `/login?redirect=${encodeURIComponent(redirectURL)}`;
|
const loginHref = `/login?redirect=${encodeURIComponent(redirectURL)}`;
|
||||||
|
|
||||||
|
// 获取当前路由的 Topbar 配置
|
||||||
|
const routeConfig = getTopbarConfig(pathname);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleScroll(event: Event) {
|
function handleScroll(event: Event) {
|
||||||
const dom = event.target as HTMLElement;
|
const dom = event.target as HTMLElement;
|
||||||
setIsBlur(dom.scrollTop > 0);
|
setIsScrollBlur(dom.scrollTop > 0);
|
||||||
}
|
}
|
||||||
const dom = document.getElementById('main-content');
|
const dom = document.getElementById('main-content');
|
||||||
if (dom) {
|
if (dom) {
|
||||||
|
|
@ -59,9 +48,6 @@ function Topbar() {
|
||||||
router.prefetch('/login');
|
router.prefetch('/login');
|
||||||
} else {
|
} else {
|
||||||
router.prefetch('/profile');
|
router.prefetch('/profile');
|
||||||
// if (user.cpUserInfo) {
|
|
||||||
// router.push('/login/fields?redirect=' + encodeURIComponent(redirectURL));
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
|
@ -70,9 +56,10 @@ function Topbar() {
|
||||||
if (response.sm || items.some((item) => item.path === pathname)) {
|
if (response.sm || items.some((item) => item.path === pathname)) {
|
||||||
return (
|
return (
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<i
|
<IconFont
|
||||||
className="iconfont-v2 iconv2-Logo"
|
size={response.sm ? 150 : 100}
|
||||||
style={{ fontSize: response.sm ? '50px' : '32px' }}
|
className="text-white cursor-pointer"
|
||||||
|
type="icon-Logo"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
@ -88,14 +75,17 @@ function Topbar() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const rightDomRender = () => {
|
const rightDomRender = () => {
|
||||||
|
if (!response || isLoading) return null;
|
||||||
|
|
||||||
if (!user)
|
if (!user)
|
||||||
return (
|
return (
|
||||||
<Link href={loginHref} prefetch>
|
<Link href={loginHref} prefetch>
|
||||||
<Button size="small">{`Login in${response?.sm ? ' / Sign up' : ''}`}</Button>
|
<Button size="small">Login in</Button>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
return (
|
|
||||||
<div className="flex items-center">
|
const userDom = user ? (
|
||||||
|
<>
|
||||||
<Notice />
|
<Notice />
|
||||||
<Link href="/profile" prefetch>
|
<Link href="/profile" prefetch>
|
||||||
<Avatar className="size-8 cursor-pointer">
|
<Avatar className="size-8 cursor-pointer">
|
||||||
|
|
@ -109,21 +99,37 @@ function Topbar() {
|
||||||
<AvatarFallback>{user.nickname?.slice(0, 1)}</AvatarFallback>
|
<AvatarFallback>{user.nickname?.slice(0, 1)}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</Link>
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Link href={loginHref} prefetch>
|
||||||
|
<Button size="small">Login in</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<LocaleSwitch />
|
||||||
|
{userDom}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (response && !response.sm && matchRoutePattern(pathname, mobileHidenMenus)) return null;
|
// 移动端根据配置决定是否隐藏 Topbar
|
||||||
|
const isMobile = response && !response.sm;
|
||||||
|
if (isMobile && routeConfig.hideOnMobile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const finalIgBlur = isBlur || matchRoutePattern(pathname, blurPages);
|
// 计算最终是否需要模糊效果(滚动模糊 或 路由配置模糊)
|
||||||
|
const shouldBlur = isScrollBlur || routeConfig.enableBlur;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn('flex h-16 w-full items-center justify-between px-4 sm:px-8 transition-all', {
|
className={cn('flex h-16 w-full items-center justify-between px-4 sm:px-8 transition-all', {
|
||||||
'backdrop-blur-[10px]': finalIgBlur,
|
'backdrop-blur-[10px]': shouldBlur,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{finalIgBlur && <div className="bg-background-default absolute inset-0 opacity-85" />}
|
{shouldBlur && <div className="bg-background-default absolute inset-0 opacity-85" />}
|
||||||
<div className="relative inset-0 flex w-full items-center justify-between">
|
<div className="relative inset-0 flex w-full items-center justify-between">
|
||||||
{leftDomRender()}
|
{leftDomRender()}
|
||||||
{rightDomRender()}
|
{rightDomRender()}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
import ChatSidebarItem from './ChatSidebarItem';
|
||||||
|
import { IconButton } from '@/components/ui/button';
|
||||||
|
import ChatSidebarAction from './ChatSidebarAction';
|
||||||
|
import ChatSearchResults from './ChatSearchResults';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useStreamChatStore } from '@/app/(main)/chat/[id]/stream-chat';
|
||||||
|
import { useLayoutStore } from '@/stores';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
|
||||||
|
const ChatSidebar = ({
|
||||||
|
expand,
|
||||||
|
showSeparator = false,
|
||||||
|
}: {
|
||||||
|
expand?: boolean;
|
||||||
|
showSeparator?: boolean;
|
||||||
|
}) => {
|
||||||
|
const isSidebarExpanded = useLayoutStore((s) => s.isSidebarExpanded);
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const channels = useStreamChatStore((state) => state.channels);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [inSearching, setIsSearching] = useState(false);
|
||||||
|
|
||||||
|
// 当侧边栏收缩时,取消搜索功能
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSidebarExpanded) {
|
||||||
|
setIsSearching(false);
|
||||||
|
setSearch('');
|
||||||
|
}
|
||||||
|
}, [isSidebarExpanded]);
|
||||||
|
|
||||||
|
const handleCloseSearch = useCallback(() => {
|
||||||
|
setSearch('');
|
||||||
|
setIsSearching(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 如果有搜索关键词,显示搜索结果
|
||||||
|
const isShowingSearchResults = search.trim().length > 0;
|
||||||
|
|
||||||
|
if (!channels.length && !isShowingSearchResults) {
|
||||||
|
return <div className="flex-1"></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalExpand = expand || isSidebarExpanded;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 分割线 */}
|
||||||
|
{showSeparator && channels.length > 0 && (
|
||||||
|
<div className="mx-6 my-4">
|
||||||
|
<div className="bg-outline-normal h-px" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col px-4">
|
||||||
|
{/* 聊天标题 */}
|
||||||
|
<div className="mb-2 flex h-10 items-center justify-between px-2 py-1">
|
||||||
|
<span className="txt-label-s text-txt-secondary-normal">Chats</span>
|
||||||
|
{finalExpand && (
|
||||||
|
<ChatSidebarAction
|
||||||
|
onSearchClick={() => setIsSearching(true)}
|
||||||
|
onCancelSearch={handleCloseSearch}
|
||||||
|
isSearchActive={inSearching}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 搜索框 - 根据设计稿实现 */}
|
||||||
|
{inSearching && finalExpand && (
|
||||||
|
<div className="relative mb-2 flex items-center gap-1 px-2 py-1">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
placeholder="Search"
|
||||||
|
size="small"
|
||||||
|
autoFocus
|
||||||
|
maxLength={50}
|
||||||
|
prefixIcon={
|
||||||
|
<i className="iconfont icon-Search text-txt-secondary-normal text-sm" />
|
||||||
|
}
|
||||||
|
className="rounded-full"
|
||||||
|
/>
|
||||||
|
{isShowingSearchResults && (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setSearch('')}
|
||||||
|
size="mini"
|
||||||
|
variant="tertiary"
|
||||||
|
className="absolute top-1/2 right-3 shrink-0 -translate-y-1/2 transform"
|
||||||
|
>
|
||||||
|
<i className="iconfont icon-close" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleCloseSearch}
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
<i className="iconfont icon-close" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 根据搜索状态显示不同内容 */}
|
||||||
|
{inSearching ? (
|
||||||
|
isShowingSearchResults ? (
|
||||||
|
<ChatSearchResults searchKeyword={search} isExpanded={finalExpand} />
|
||||||
|
) : (
|
||||||
|
<div className="flex-1" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="relative min-h-0 flex-1">
|
||||||
|
<div className="h-full max-w-full space-y-2 overflow-x-hidden overflow-y-auto py-2">
|
||||||
|
{channels.map((chat) => (
|
||||||
|
<ChatSidebarItem
|
||||||
|
key={chat.id}
|
||||||
|
chanel={chat}
|
||||||
|
isExpanded={finalExpand}
|
||||||
|
isSelected={id === chat.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatSidebar;
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useLocale } from '@/layout/Providers/IntlProvider';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
|
||||||
|
export default function LocaleSwitch() {
|
||||||
|
const { locale, setLocale } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select value={locale} onValueChange={(value) => setLocale(value as 'zh' | 'en')}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择语言" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="zh">
|
||||||
|
<span>中文</span>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="en">
|
||||||
|
<span>English</span>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* Topbar 路由行为配置
|
||||||
|
* 用于管理不同路由下 Topbar 的显示和样式行为
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface TopbarRouteConfig {
|
||||||
|
/** 移动端是否隐藏 Topbar */
|
||||||
|
hideOnMobile?: boolean;
|
||||||
|
/** 是否启用背景模糊效果 */
|
||||||
|
enableBlur?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由配置映射表
|
||||||
|
* key: 路由路径模式(支持 :id 等动态参数)
|
||||||
|
* value: 对应的行为配置
|
||||||
|
*/
|
||||||
|
export const topbarRouteConfigs: Record<string, TopbarRouteConfig> = {
|
||||||
|
'/profile/edit': { hideOnMobile: true },
|
||||||
|
'/profile/mask': { hideOnMobile: true },
|
||||||
|
'/profile/mask/:id': { hideOnMobile: true },
|
||||||
|
'/profile/account': { hideOnMobile: true },
|
||||||
|
'/character/:id': { hideOnMobile: true },
|
||||||
|
'/chat/:id': { enableBlur: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将路由模式转换为正则表达式进行匹配
|
||||||
|
* @param pathname 当前路由路径
|
||||||
|
* @param pattern 路由模式(支持 :id 等动态参数)
|
||||||
|
* @returns 是否匹配
|
||||||
|
*/
|
||||||
|
export function matchRoutePattern(pathname: string, pattern: string): boolean {
|
||||||
|
// 将路由模式中的 :param 转换为正则表达式
|
||||||
|
const regexPattern = pattern
|
||||||
|
.replace(/:[^/]+/g, '[^/]+') // 将 :id, :userId 等替换为 [^/]+
|
||||||
|
.replace(/\//g, '\\/'); // 转义斜杠
|
||||||
|
const regex = new RegExp(`^${regexPattern}$`);
|
||||||
|
return regex.test(pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前路由的 Topbar 配置
|
||||||
|
* @param pathname 当前路由路径
|
||||||
|
* @returns 合并后的配置对象
|
||||||
|
*/
|
||||||
|
export function getTopbarConfig(pathname: string): TopbarRouteConfig {
|
||||||
|
for (const [pattern, config] of Object.entries(topbarRouteConfigs)) {
|
||||||
|
if (matchRoutePattern(pathname, pattern)) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 返回默认配置
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
@ -8,10 +8,10 @@ import Topbar from './Topbar';
|
||||||
// import SubscribeVipDrawer from '@/app/(main)/vip/components/SubscribeVipDrawer';
|
// import SubscribeVipDrawer from '@/app/(main)/vip/components/SubscribeVipDrawer';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
// import CreateReachedLimitDialog from '../components/features/create-reached-limit-dialog';
|
// import CreateReachedLimitDialog from '../components/features/create-reached-limit-dialog';
|
||||||
import { useMedia } from '@/hooks/tools';
|
|
||||||
import BottomBar from './BottomBar';
|
import BottomBar from './BottomBar';
|
||||||
import { useStreamChatStore } from '@/app/(main)/chat/[id]/stream-chat';
|
import { useStreamChatStore } from '@/app/(main)/chat/[id]/stream-chat';
|
||||||
import { useCurrentUser } from '@/hooks/auth';
|
import { useCurrentUser } from '@/hooks/auth';
|
||||||
|
import { useLayoutStore } from '@/stores';
|
||||||
|
|
||||||
interface ConditionalLayoutProps {
|
interface ConditionalLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -35,7 +35,7 @@ export default function ConditionalLayout({ children }: ConditionalLayoutProps)
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const mainContentRef = useRef<HTMLDivElement>(null);
|
const mainContentRef = useRef<HTMLDivElement>(null);
|
||||||
const prevPathnameRef = useRef<string>(pathname);
|
const prevPathnameRef = useRef<string>(pathname);
|
||||||
const response = useMedia();
|
const response = useLayoutStore((s) => s.response);
|
||||||
|
|
||||||
// 初始化聊天
|
// 初始化聊天
|
||||||
useInitChat();
|
useInitChat();
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
'use client';
|
||||||
|
import { IconButton } from '@/components/ui/button';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useLayoutStore } from '@/stores';
|
||||||
|
|
||||||
|
type ProfileLayoutProps = {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
title?: string;
|
||||||
|
rightDom?: React.ReactNode;
|
||||||
|
bottomButton?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProfileLayout(props: ProfileLayoutProps) {
|
||||||
|
const { children, title, rightDom, bottomButton } = props;
|
||||||
|
const router = useRouter();
|
||||||
|
const response = useLayoutStore((s) => s.response);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto px-4 h-full flex flex-col max-w-[752px] pt-6">
|
||||||
|
{/* 标题栏 */}
|
||||||
|
<div className="mb-6 flex shrink-0 justify-between items-center">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconButton variant="ghost" size="large" onClick={() => router.back()} className="p-2">
|
||||||
|
<i className="iconfont icon-arrow-left !text-[16px]" />
|
||||||
|
</IconButton>
|
||||||
|
<h1 className="txt-title-l text-txt-primary-normal">{title}</h1>
|
||||||
|
</div>
|
||||||
|
{rightDom}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">{children}</div>
|
||||||
|
{bottomButton && response && !response.sm && (
|
||||||
|
<div className="shrink-0 h-18 flex items-center">{bottomButton}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { useLayoutStore } from '@/stores';
|
||||||
|
|
||||||
|
type Locale = 'zh' | 'en';
|
||||||
|
interface LocaleContextType {
|
||||||
|
locale: Locale;
|
||||||
|
setLocale: (locale: Locale) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocaleContext = createContext<LocaleContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function useLocale() {
|
||||||
|
const context = useContext(LocaleContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useLocale must be used within IntlProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntlProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLocaleToCookie(locale: Locale) {
|
||||||
|
Cookies.set('locale', locale, { expires: 365, path: '/' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocaleFromCookie(): Locale {
|
||||||
|
const cookieLocale = Cookies.get('locale') as Locale | undefined;
|
||||||
|
if (cookieLocale && (cookieLocale === 'zh' || cookieLocale === 'en')) {
|
||||||
|
return cookieLocale;
|
||||||
|
}
|
||||||
|
setLocaleToCookie('en');
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IntlProvider({ children }: IntlProviderProps) {
|
||||||
|
const [locale, setLocaleState] = useState<Locale>('en');
|
||||||
|
const [messages, setMessages] = useState<Record<string, any>>();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const setHydrated = useLayoutStore((s) => s.setHydrated);
|
||||||
|
useEffect(() => {
|
||||||
|
setHydrated();
|
||||||
|
}, [setHydrated]);
|
||||||
|
|
||||||
|
const loadLocale = useMemoizedFn(async (locale: Locale) => {
|
||||||
|
// 动态加载, 提升首屏加载速度
|
||||||
|
const messages = await import(`@/locales/${locale}.ts`);
|
||||||
|
setMessages(messages.default);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cookieLocale = getLocaleFromCookie();
|
||||||
|
if (cookieLocale) {
|
||||||
|
loadLocale(cookieLocale);
|
||||||
|
setLocaleState(cookieLocale);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setLocale = useMemoizedFn((newLocale: Locale) => {
|
||||||
|
setLocaleState(newLocale);
|
||||||
|
setLocaleToCookie(newLocale);
|
||||||
|
loadLocale(newLocale);
|
||||||
|
router.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!messages) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LocaleContext.Provider value={{ locale, setLocale }}>
|
||||||
|
<NextIntlClientProvider
|
||||||
|
locale={locale as any}
|
||||||
|
messages={messages}
|
||||||
|
timeZone={Intl.DateTimeFormat().resolvedOptions().timeZone}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NextIntlClientProvider>
|
||||||
|
</LocaleContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,12 @@
|
||||||
'use client'
|
'use client';
|
||||||
import { AppProgressProvider as ProgressProvider } from '@bprogress/next'
|
import { AppProgressProvider as ProgressProvider } from '@bprogress/next';
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
const ProgressBar = ({ children }: { children: React.ReactNode }) => {
|
const ProgressBar = ({ children }: { children: React.ReactNode }) => {
|
||||||
useEffect(() => {
|
|
||||||
console.log('progress')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProgressProvider height="4px" color="#E63C8B" options={{ showSpinner: false }} shallowRouting>
|
<ProgressProvider height="4px" color="#E63C8B" options={{ showSpinner: false }} shallowRouting>
|
||||||
{children}
|
{children}
|
||||||
</ProgressProvider>
|
</ProgressProvider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ProgressBar
|
export default ProgressBar;
|
||||||
|
|
@ -1,23 +1,16 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { ApiError } from '@/types/api';
|
import { ApiError } from '@/types/api';
|
||||||
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||||
import React, { useState, useRef, type ReactNode } from 'react';
|
import { useState, useRef, type ReactNode } from 'react';
|
||||||
import { toast, Toaster } from 'sonner';
|
import { toast, Toaster } from 'sonner';
|
||||||
import { tokenManager } from './auth/token';
|
|
||||||
import { COIN_INSUFFICIENT_ERROR_CODE } from '@/hooks/useWallet';
|
import { COIN_INSUFFICIENT_ERROR_CODE } from '@/hooks/useWallet';
|
||||||
import { walletKeys } from './query-keys';
|
import { walletKeys } from '@/lib/query-keys';
|
||||||
interface ProvidersProps {
|
interface QueryProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// const ReactQueryDevtoolsProduction = React.lazy(() =>
|
|
||||||
// import('@tanstack/react-query-devtools/build/modern/production.js').then((d) => ({
|
|
||||||
// default: d.ReactQueryDevtools,
|
|
||||||
// }))
|
|
||||||
// );
|
|
||||||
|
|
||||||
const EXPIRED_ERROR_CODES = [
|
const EXPIRED_ERROR_CODES = [
|
||||||
'10050001',
|
'10050001',
|
||||||
'10050002',
|
'10050002',
|
||||||
|
|
@ -27,7 +20,7 @@ const EXPIRED_ERROR_CODES = [
|
||||||
'10050006',
|
'10050006',
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Providers({ children }: ProvidersProps) {
|
export function QueryProvider({ children }: QueryProviderProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// 用于错误去重的引用
|
// 用于错误去重的引用
|
||||||
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
@ -120,13 +113,6 @@ export function Providers({ children }: ProvidersProps) {
|
||||||
'!bg-surface-base-normal !border-none !px-4 !py-3 !rounded-m !txt-body-m !text-txt-primary-normal',
|
'!bg-surface-base-normal !border-none !px-4 !py-3 !rounded-m !txt-body-m !text-txt-primary-normal',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* <ReactQueryDevtools initialIsOpen={true} /> */}
|
|
||||||
|
|
||||||
{/* {showDevtools && (
|
|
||||||
<React.Suspense fallback={null}>
|
|
||||||
<ReactQueryDevtoolsProduction />
|
|
||||||
</React.Suspense>
|
|
||||||
)} */}
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { IntlProvider } from './IntlProvider';
|
||||||
|
import { QueryProvider } from './QueryProvider';
|
||||||
|
import { DeviceIdProvider } from './DeviceIdProvider';
|
||||||
|
import ProgressBar from './ProgressBar';
|
||||||
|
|
||||||
|
interface ProvidersProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Providers({ children }: ProvidersProps) {
|
||||||
|
return (
|
||||||
|
<DeviceIdProvider>
|
||||||
|
<IntlProvider>
|
||||||
|
<ProgressBar>
|
||||||
|
<QueryProvider>{children}</QueryProvider>
|
||||||
|
</ProgressBar>
|
||||||
|
</IntlProvider>
|
||||||
|
</DeviceIdProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
import ChatSidebarItem from './ChatSidebarItem';
|
|
||||||
import { IconButton } from '@/components/ui/button';
|
|
||||||
import ChatSidebarAction from './ChatSidebarAction';
|
|
||||||
import ChatSearchResults from './ChatSearchResults';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { useStreamChatStore } from '@/app/(main)/chat/[id]/stream-chat';
|
|
||||||
import { useLayoutStore } from '@/stores';
|
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
|
|
||||||
const ChatSidebar = ({ expand }: { expand?: boolean }) => {
|
|
||||||
const isSidebarExpanded = useLayoutStore((s) => s.isSidebarExpanded);
|
|
||||||
const { id } = useParams<{ id: string }>();
|
|
||||||
const channels = useStreamChatStore((state) => state.channels);
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const [inSearching, setIsSearching] = useState(false);
|
|
||||||
// console.log('channels', channels);
|
|
||||||
|
|
||||||
// 当侧边栏收缩时,取消搜索功能
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSidebarExpanded) {
|
|
||||||
setIsSearching(false);
|
|
||||||
setSearch('');
|
|
||||||
}
|
|
||||||
}, [isSidebarExpanded]);
|
|
||||||
|
|
||||||
const handleCloseSearch = useCallback(() => {
|
|
||||||
setSearch('');
|
|
||||||
setIsSearching(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 如果有搜索关键词,显示搜索结果
|
|
||||||
const isShowingSearchResults = search.trim().length > 0;
|
|
||||||
|
|
||||||
if (!channels.length && !isShowingSearchResults) {
|
|
||||||
return <div className="flex-1"></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalExpand = expand || isSidebarExpanded;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-0 flex-1 flex-col px-4">
|
|
||||||
{/* 聊天标题 */}
|
|
||||||
<div className="mb-2 flex h-10 items-center justify-between px-2 py-1">
|
|
||||||
<span className="txt-label-s text-txt-secondary-normal">Chats</span>
|
|
||||||
{finalExpand && (
|
|
||||||
<ChatSidebarAction
|
|
||||||
onSearchClick={() => setIsSearching(true)}
|
|
||||||
onCancelSearch={handleCloseSearch}
|
|
||||||
isSearchActive={inSearching}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 搜索框 - 根据设计稿实现 */}
|
|
||||||
{inSearching && finalExpand && (
|
|
||||||
<div className="relative mb-2 flex items-center gap-1 px-2 py-1">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
placeholder="Search"
|
|
||||||
size="small"
|
|
||||||
autoFocus
|
|
||||||
maxLength={50}
|
|
||||||
prefixIcon={<i className="iconfont icon-Search text-txt-secondary-normal text-sm" />}
|
|
||||||
className="rounded-full"
|
|
||||||
/>
|
|
||||||
{isShowingSearchResults && (
|
|
||||||
<IconButton
|
|
||||||
onClick={() => setSearch('')}
|
|
||||||
size="mini"
|
|
||||||
variant="tertiary"
|
|
||||||
className="absolute top-1/2 right-3 shrink-0 -translate-y-1/2 transform"
|
|
||||||
>
|
|
||||||
<i className="iconfont icon-close" />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<IconButton onClick={handleCloseSearch} variant="ghost" size="small" className="shrink-0">
|
|
||||||
<i className="iconfont icon-close" />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 根据搜索状态显示不同内容 */}
|
|
||||||
{inSearching ? (
|
|
||||||
isShowingSearchResults ? (
|
|
||||||
<ChatSearchResults searchKeyword={search} isExpanded={finalExpand} />
|
|
||||||
) : (
|
|
||||||
<div className="flex-1" />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="relative min-h-0 flex-1">
|
|
||||||
<div className="h-full max-w-full space-y-2 overflow-x-hidden overflow-y-auto py-2">
|
|
||||||
{channels.map((chat) => (
|
|
||||||
<ChatSidebarItem
|
|
||||||
key={chat.id}
|
|
||||||
chanel={chat}
|
|
||||||
isExpanded={finalExpand}
|
|
||||||
isSelected={id === chat.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChatSidebar;
|
|
||||||
|
|
@ -1,37 +1,36 @@
|
||||||
// Google Identity Services (GIS) 配置
|
// Google Identity Services (GIS) 配置
|
||||||
// const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!
|
const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!;
|
||||||
const GOOGLE_CLIENT_ID = '606396962663-9pagar3g9vuhovi37vq9jqob6q1gngns.apps.googleusercontent.com'
|
console.log('GOOGLE_CLIENT_ID', GOOGLE_CLIENT_ID);
|
||||||
|
|
||||||
// Google OAuth scopes
|
// Google OAuth scopes
|
||||||
const GOOGLE_SCOPES = [
|
const GOOGLE_SCOPES = [
|
||||||
'https://www.googleapis.com/auth/userinfo.email',
|
'https://www.googleapis.com/auth/userinfo.email',
|
||||||
'https://www.googleapis.com/auth/userinfo.profile',
|
'https://www.googleapis.com/auth/userinfo.profile',
|
||||||
].join(' ')
|
].join(' ');
|
||||||
|
|
||||||
export interface GoogleUser {
|
export interface GoogleUser {
|
||||||
id: string
|
id: string;
|
||||||
email: string
|
email: string;
|
||||||
verified_email: boolean
|
verified_email: boolean;
|
||||||
name: string
|
name: string;
|
||||||
given_name: string
|
given_name: string;
|
||||||
family_name: string
|
family_name: string;
|
||||||
picture?: string
|
picture?: string;
|
||||||
locale?: string
|
locale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google Identity Services 响应类型
|
// Google Identity Services 响应类型
|
||||||
export interface GoogleCredentialResponse {
|
export interface GoogleCredentialResponse {
|
||||||
credential: string // JWT ID token
|
credential: string; // JWT ID token
|
||||||
select_by?: string
|
select_by?: string;
|
||||||
clientId?: string
|
clientId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google OAuth Code Response (使用 Code Model)
|
// Google OAuth Code Response (使用 Code Model)
|
||||||
export interface GoogleCodeResponse {
|
export interface GoogleCodeResponse {
|
||||||
code: string // Authorization code
|
code: string; // Authorization code
|
||||||
scope: string
|
scope: string;
|
||||||
authuser?: string
|
authuser?: string;
|
||||||
prompt?: string
|
prompt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 声明 Google Identity Services 全局对象
|
// 声明 Google Identity Services 全局对象
|
||||||
|
|
@ -40,84 +39,84 @@ declare global {
|
||||||
google?: {
|
google?: {
|
||||||
accounts: {
|
accounts: {
|
||||||
id: {
|
id: {
|
||||||
initialize: (config: GoogleIdConfiguration) => void
|
initialize: (config: GoogleIdConfiguration) => void;
|
||||||
prompt: (momentListener?: (notification: PromptMomentNotification) => void) => void
|
prompt: (momentListener?: (notification: PromptMomentNotification) => void) => void;
|
||||||
renderButton: (parent: HTMLElement, options: GsiButtonConfiguration) => void
|
renderButton: (parent: HTMLElement, options: GsiButtonConfiguration) => void;
|
||||||
disableAutoSelect: () => void
|
disableAutoSelect: () => void;
|
||||||
cancel: () => void
|
cancel: () => void;
|
||||||
}
|
};
|
||||||
oauth2: {
|
oauth2: {
|
||||||
initCodeClient: (config: CodeClientConfig) => CodeClient
|
initCodeClient: (config: CodeClientConfig) => CodeClient;
|
||||||
initTokenClient: (config: TokenClientConfig) => TokenClient
|
initTokenClient: (config: TokenClientConfig) => TokenClient;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GoogleIdConfiguration {
|
interface GoogleIdConfiguration {
|
||||||
client_id: string
|
client_id: string;
|
||||||
callback?: (response: GoogleCredentialResponse) => void
|
callback?: (response: GoogleCredentialResponse) => void;
|
||||||
auto_select?: boolean
|
auto_select?: boolean;
|
||||||
cancel_on_tap_outside?: boolean
|
cancel_on_tap_outside?: boolean;
|
||||||
context?: 'signin' | 'signup' | 'use'
|
context?: 'signin' | 'signup' | 'use';
|
||||||
ux_mode?: 'popup' | 'redirect'
|
ux_mode?: 'popup' | 'redirect';
|
||||||
login_uri?: string
|
login_uri?: string;
|
||||||
native_callback?: (response: GoogleCredentialResponse) => void
|
native_callback?: (response: GoogleCredentialResponse) => void;
|
||||||
itp_support?: boolean
|
itp_support?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GsiButtonConfiguration {
|
interface GsiButtonConfiguration {
|
||||||
type?: 'standard' | 'icon'
|
type?: 'standard' | 'icon';
|
||||||
theme?: 'outline' | 'filled_blue' | 'filled_black'
|
theme?: 'outline' | 'filled_blue' | 'filled_black';
|
||||||
size?: 'large' | 'medium' | 'small'
|
size?: 'large' | 'medium' | 'small';
|
||||||
text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin'
|
text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin';
|
||||||
shape?: 'rectangular' | 'pill' | 'circle' | 'square'
|
shape?: 'rectangular' | 'pill' | 'circle' | 'square';
|
||||||
logo_alignment?: 'left' | 'center'
|
logo_alignment?: 'left' | 'center';
|
||||||
width?: string
|
width?: string;
|
||||||
locale?: string
|
locale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PromptMomentNotification {
|
interface PromptMomentNotification {
|
||||||
isDisplayMoment: () => boolean
|
isDisplayMoment: () => boolean;
|
||||||
isDisplayed: () => boolean
|
isDisplayed: () => boolean;
|
||||||
isNotDisplayed: () => boolean
|
isNotDisplayed: () => boolean;
|
||||||
getNotDisplayedReason: () => string
|
getNotDisplayedReason: () => string;
|
||||||
isSkippedMoment: () => boolean
|
isSkippedMoment: () => boolean;
|
||||||
getSkippedReason: () => string
|
getSkippedReason: () => string;
|
||||||
isDismissedMoment: () => boolean
|
isDismissedMoment: () => boolean;
|
||||||
getDismissedReason: () => string
|
getDismissedReason: () => string;
|
||||||
getMomentType: () => string
|
getMomentType: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodeClientConfig {
|
interface CodeClientConfig {
|
||||||
client_id: string
|
client_id: string;
|
||||||
scope: string
|
scope: string;
|
||||||
callback: (response: GoogleCodeResponse) => void
|
callback: (response: GoogleCodeResponse) => void;
|
||||||
error_callback?: (error: { type: string; message: string }) => void
|
error_callback?: (error: { type: string; message: string }) => void;
|
||||||
ux_mode?: 'popup' | 'redirect'
|
ux_mode?: 'popup' | 'redirect';
|
||||||
redirect_uri?: string
|
redirect_uri?: string;
|
||||||
state?: string
|
state?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodeClient {
|
interface CodeClient {
|
||||||
requestCode: () => void
|
requestCode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TokenClientConfig {
|
interface TokenClientConfig {
|
||||||
client_id: string
|
client_id: string;
|
||||||
scope: string
|
scope: string;
|
||||||
callback: (response: {
|
callback: (response: {
|
||||||
access_token: string
|
access_token: string;
|
||||||
expires_in: number
|
expires_in: number;
|
||||||
scope: string
|
scope: string;
|
||||||
token_type: string
|
token_type: string;
|
||||||
}) => void
|
}) => void;
|
||||||
error_callback?: (error: { type: string; message: string }) => void
|
error_callback?: (error: { type: string; message: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TokenClient {
|
interface TokenClient {
|
||||||
requestAccessToken: () => void
|
requestAccessToken: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const googleOAuth = {
|
export const googleOAuth = {
|
||||||
|
|
@ -129,20 +128,20 @@ export const googleOAuth = {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// 检查是否已加载
|
// 检查是否已加载
|
||||||
if (window.google?.accounts) {
|
if (window.google?.accounts) {
|
||||||
resolve()
|
resolve();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 script 标签
|
// 创建 script 标签
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script');
|
||||||
script.src = 'https://accounts.google.com/gsi/client'
|
script.src = 'https://accounts.google.com/gsi/client';
|
||||||
script.async = true
|
script.async = true;
|
||||||
script.defer = true
|
script.defer = true;
|
||||||
script.onload = () => resolve()
|
script.onload = () => resolve();
|
||||||
script.onerror = () => reject(new Error('Failed to load Google Identity Services SDK'))
|
script.onerror = () => reject(new Error('Failed to load Google Identity Services SDK'));
|
||||||
|
|
||||||
document.head.appendChild(script)
|
document.head.appendChild(script);
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 初始化 Code Client(推荐方式,获取授权码)
|
// 初始化 Code Client(推荐方式,获取授权码)
|
||||||
|
|
@ -151,7 +150,7 @@ export const googleOAuth = {
|
||||||
errorCallback?: (error: any) => void
|
errorCallback?: (error: any) => void
|
||||||
) => {
|
) => {
|
||||||
if (!window.google?.accounts?.oauth2) {
|
if (!window.google?.accounts?.oauth2) {
|
||||||
throw new Error('Google Identity Services SDK not loaded')
|
throw new Error('Google Identity Services SDK not loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.google.accounts.oauth2.initCodeClient({
|
return window.google.accounts.oauth2.initCodeClient({
|
||||||
|
|
@ -160,21 +159,21 @@ export const googleOAuth = {
|
||||||
ux_mode: 'popup', // 使用 popup 模式,不需要 redirect_uri
|
ux_mode: 'popup', // 使用 popup 模式,不需要 redirect_uri
|
||||||
callback,
|
callback,
|
||||||
error_callback: errorCallback,
|
error_callback: errorCallback,
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 初始化 Token Client(直接获取 access token)
|
// 初始化 Token Client(直接获取 access token)
|
||||||
initTokenClient: (
|
initTokenClient: (
|
||||||
callback: (response: {
|
callback: (response: {
|
||||||
access_token: string
|
access_token: string;
|
||||||
expires_in: number
|
expires_in: number;
|
||||||
scope: string
|
scope: string;
|
||||||
token_type: string
|
token_type: string;
|
||||||
}) => void,
|
}) => void,
|
||||||
errorCallback?: (error: any) => void
|
errorCallback?: (error: any) => void
|
||||||
) => {
|
) => {
|
||||||
if (!window.google?.accounts?.oauth2) {
|
if (!window.google?.accounts?.oauth2) {
|
||||||
throw new Error('Google Identity Services SDK not loaded')
|
throw new Error('Google Identity Services SDK not loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.google.accounts.oauth2.initTokenClient({
|
return window.google.accounts.oauth2.initTokenClient({
|
||||||
|
|
@ -182,13 +181,13 @@ export const googleOAuth = {
|
||||||
scope: GOOGLE_SCOPES,
|
scope: GOOGLE_SCOPES,
|
||||||
callback,
|
callback,
|
||||||
error_callback: errorCallback,
|
error_callback: errorCallback,
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 初始化 Google Identity (获取 ID Token - JWT)
|
// 初始化 Google Identity (获取 ID Token - JWT)
|
||||||
initGoogleId: (callback: (response: GoogleCredentialResponse) => void) => {
|
initGoogleId: (callback: (response: GoogleCredentialResponse) => void) => {
|
||||||
if (!window.google?.accounts?.id) {
|
if (!window.google?.accounts?.id) {
|
||||||
throw new Error('Google Identity Services SDK not loaded')
|
throw new Error('Google Identity Services SDK not loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.google.accounts.id.initialize({
|
window.google.accounts.id.initialize({
|
||||||
|
|
@ -197,12 +196,12 @@ export const googleOAuth = {
|
||||||
auto_select: false,
|
auto_select: false,
|
||||||
cancel_on_tap_outside: true,
|
cancel_on_tap_outside: true,
|
||||||
ux_mode: 'popup',
|
ux_mode: 'popup',
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 触发 One Tap 流程
|
// 触发 One Tap 流程
|
||||||
promptOneTap: (callback: (response: GoogleCredentialResponse) => void) => {
|
promptOneTap: (callback: (response: GoogleCredentialResponse) => void) => {
|
||||||
googleOAuth.initGoogleId(callback)
|
googleOAuth.initGoogleId(callback);
|
||||||
|
|
||||||
if (window.google?.accounts?.id) {
|
if (window.google?.accounts?.id) {
|
||||||
window.google.accounts.id.prompt((notification) => {
|
window.google.accounts.id.prompt((notification) => {
|
||||||
|
|
@ -210,9 +209,9 @@ export const googleOAuth = {
|
||||||
console.log(
|
console.log(
|
||||||
'One Tap not displayed:',
|
'One Tap not displayed:',
|
||||||
notification.getNotDisplayedReason() || notification.getSkippedReason()
|
notification.getNotDisplayedReason() || notification.getSkippedReason()
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -224,7 +223,7 @@ export const googleOAuth = {
|
||||||
options?: Partial<GsiButtonConfiguration>
|
options?: Partial<GsiButtonConfiguration>
|
||||||
) => {
|
) => {
|
||||||
if (!window.google?.accounts?.id) {
|
if (!window.google?.accounts?.id) {
|
||||||
throw new Error('Google Identity Services SDK not loaded')
|
throw new Error('Google Identity Services SDK not loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先初始化
|
// 先初始化
|
||||||
|
|
@ -233,7 +232,7 @@ export const googleOAuth = {
|
||||||
callback,
|
callback,
|
||||||
auto_select: false,
|
auto_select: false,
|
||||||
cancel_on_tap_outside: true,
|
cancel_on_tap_outside: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
// 渲染 Google 标准按钮
|
// 渲染 Google 标准按钮
|
||||||
window.google.accounts.id.renderButton(parent, {
|
window.google.accounts.id.renderButton(parent, {
|
||||||
|
|
@ -244,6 +243,6 @@ export const googleOAuth = {
|
||||||
shape: 'rectangular',
|
shape: 'rectangular',
|
||||||
logo_alignment: 'left',
|
logo_alignment: 'left',
|
||||||
...options,
|
...options,
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default {
|
||||||
|
hello: 'Hello',
|
||||||
|
home: {
|
||||||
|
character: 'Character',
|
||||||
|
story: 'Story',
|
||||||
|
},
|
||||||
|
chat: {
|
||||||
|
chats: 'Chats',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default {
|
||||||
|
hello: '你好',
|
||||||
|
home: {
|
||||||
|
character: '角色',
|
||||||
|
story: '故事',
|
||||||
|
},
|
||||||
|
chat: {
|
||||||
|
chats: '聊天',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,61 +1,64 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
const responsiveConfig = {
|
||||||
|
xs: 375,
|
||||||
|
sm: 768,
|
||||||
|
md: 1024,
|
||||||
|
lg: 1280,
|
||||||
|
xl: 1440,
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResponsiveResponse =
|
||||||
|
| (Record<keyof typeof responsiveConfig, boolean> & { isMobile: boolean; isPC: boolean })
|
||||||
|
| null;
|
||||||
|
|
||||||
type LayoutStore = {
|
type LayoutStore = {
|
||||||
isSidebarExpanded: boolean;
|
isSidebarExpanded: boolean;
|
||||||
setSidebarExpanded: (isSidebarExpanded: boolean) => void;
|
setSidebarExpanded: (isSidebarExpanded: boolean) => void;
|
||||||
hydrated: boolean;
|
hydrated: boolean;
|
||||||
setHydrated: () => void;
|
setHydrated: () => void;
|
||||||
|
response: ResponsiveResponse;
|
||||||
|
initResponse: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// localStorage key
|
// localStorage key
|
||||||
const SIDEBAR_EXPANDED_KEY = 'sidebarExpanded';
|
const SIDEBAR_EXPANDED_KEY = 'sidebarExpanded';
|
||||||
|
|
||||||
export const useLayoutStore = create<LayoutStore>((set) => ({
|
export const useLayoutStore = create<LayoutStore>((set, get) => ({
|
||||||
// 默认值在服务器端和客户端都是 false,确保 Hydration 一致
|
// 默认值在服务器端和客户端都是 false,确保 Hydration 一致
|
||||||
isSidebarExpanded: false,
|
isSidebarExpanded: false,
|
||||||
hydrated: false,
|
hydrated: false,
|
||||||
|
response: null,
|
||||||
|
|
||||||
setHydrated: () => {
|
initResponse: () => {
|
||||||
// Hydration 完成后,从 localStorage 读取真实的状态
|
const onResize = () => {
|
||||||
if (typeof window !== 'undefined') {
|
const newResponse = Object.fromEntries(
|
||||||
try {
|
Object.entries(responsiveConfig).map(([key, value]) => {
|
||||||
const saved = localStorage.getItem(SIDEBAR_EXPANDED_KEY);
|
if (window.innerWidth > value) {
|
||||||
if (saved !== null) {
|
return [key, true];
|
||||||
const parsed = JSON.parse(saved);
|
|
||||||
let actualState = false;
|
|
||||||
|
|
||||||
// 从 persist 中间件的格式中提取 state.isSidebarExpanded
|
|
||||||
if (typeof parsed === 'object' && parsed !== null && 'state' in parsed) {
|
|
||||||
actualState = parsed.state.isSidebarExpanded ?? false;
|
|
||||||
}
|
}
|
||||||
// 兼容新格式(直接保存布尔值)
|
return [key, false];
|
||||||
else if (typeof parsed === 'boolean') {
|
})
|
||||||
actualState = parsed;
|
) as Record<keyof typeof responsiveConfig, boolean>;
|
||||||
}
|
set({
|
||||||
|
response: { ...newResponse, isMobile: !newResponse.sm, isPC: newResponse.sm },
|
||||||
set({ hydrated: true, isSidebarExpanded: actualState });
|
});
|
||||||
return;
|
};
|
||||||
}
|
window.addEventListener('resize', onResize);
|
||||||
} catch (error) {
|
onResize();
|
||||||
console.error('Failed to load sidebar state from localStorage:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set({ hydrated: true });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setSidebarExpanded: (isSidebarExpanded: boolean) => {
|
setSidebarExpanded: (isSidebarExpanded: boolean) => {
|
||||||
// 更新状态
|
|
||||||
set({ isSidebarExpanded });
|
set({ isSidebarExpanded });
|
||||||
|
|
||||||
// 手动保存到 localStorage
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(SIDEBAR_EXPANDED_KEY, JSON.stringify(isSidebarExpanded));
|
localStorage.setItem(SIDEBAR_EXPANDED_KEY, JSON.stringify(isSidebarExpanded));
|
||||||
} catch (error) {
|
},
|
||||||
console.error('Failed to save sidebar state to localStorage:', error);
|
|
||||||
}
|
setHydrated: () => {
|
||||||
}
|
const { initResponse, setSidebarExpanded } = get();
|
||||||
|
const saved = localStorage.getItem(SIDEBAR_EXPANDED_KEY);
|
||||||
|
initResponse();
|
||||||
|
setSidebarExpanded(JSON.parse(saved || 'false'));
|
||||||
|
set({ hydrated: true });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue