From 8b933d26d629fe0da4d246e582411e8184dcf9a9 Mon Sep 17 00:00:00 2001 From: liuyonghe0111 <1763195287@qq.com> Date: Wed, 24 Dec 2025 19:29:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(home):=20=E8=A7=92=E8=89=B2=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=80=A7=E5=88=AB=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierrc | 4 +- package.json | 3 + pnpm-lock.yaml | 150 ++++++++++++++++++ public/logo.svg | 13 +- src/app/(auth)/share/[userId]/test.tsx | 52 +++--- .../crushcoin/components/CheckInGrid.tsx | 2 +- .../home/components/Character/index.tsx | 10 +- src/app/(main)/home/components/Filter.tsx | 116 -------------- .../home/components/Filter/MoreFilter.tsx | 68 ++++++++ .../home/components/Filter/TagListSelect.tsx | 41 +++++ .../(main)/home/components/Filter/index.tsx | 85 ++++++++++ src/app/(main)/home/components/Header.tsx | 4 + .../(main)/home/components/HomePageFooter.tsx | 15 +- src/app/(main)/home/store.ts | 21 ++- .../SubscribeVipDrawer/CarouselBanner.tsx | 126 +++++++-------- src/app/layout.tsx | 2 +- src/components/features/ai-standard-card.tsx | 4 +- src/components/ui/popver.tsx | 56 +++++++ src/hooks/services/signin.ts | 9 ++ src/i18n/en-US.ts | 8 +- src/i18n/zh-CN.ts | 2 + src/layout/BasicLayout/Topbar.tsx | 12 +- src/types/user.ts | 2 +- 23 files changed, 567 insertions(+), 238 deletions(-) delete mode 100644 src/app/(main)/home/components/Filter.tsx create mode 100644 src/app/(main)/home/components/Filter/MoreFilter.tsx create mode 100644 src/app/(main)/home/components/Filter/TagListSelect.tsx create mode 100644 src/app/(main)/home/components/Filter/index.tsx create mode 100644 src/components/ui/popver.tsx diff --git a/.prettierrc b/.prettierrc index 1f4c4bb..c918614 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,7 @@ "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", - "printWidth": 100 + "printWidth": 100, + "arrowParens": "always", + "endOfLine": "lf" } diff --git a/package.json b/package.json index 2fd3a92..234eed1 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", @@ -48,6 +49,7 @@ "embla-carousel-react": "^8.6.0", "js-cookie": "^3.0.5", "lamejs": "^1.2.1", + "lodash": "^4.17.21", "lucide-react": "^0.525.0", "next": "16.0.8", "next-intl": "^4.6.1", @@ -71,6 +73,7 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/js-cookie": "^3.0.6", + "@types/lodash": "^4.17.21", "@types/node": "^20", "@types/numeral": "^2.0.5", "@types/qs": "^6.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 203c96f..260581a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: '@radix-ui/react-label': specifier: ^2.1.7 version: 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-radio-group': specifier: ^1.3.7 version: 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -80,6 +83,9 @@ importers: '@types/crypto-js': specifier: ^4.2.2 version: 4.2.2 + '@types/lodash': + specifier: ^4.17.21 + version: 4.17.21 '@types/react-stickynode': specifier: ^4.0.3 version: 4.0.3 @@ -113,6 +119,9 @@ importers: lamejs: specifier: ^1.2.1 version: 1.2.1 + lodash: + specifier: ^4.17.21 + version: 4.17.21 lucide-react: specifier: ^0.525.0 version: 0.525.0(react@19.2.1) @@ -1209,6 +1218,9 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-alert-dialog@1.1.14': resolution: {integrity: sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==} peerDependencies: @@ -1340,6 +1352,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dropdown-menu@2.1.15': resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==} peerDependencies: @@ -1362,6 +1387,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-focus-scope@1.1.7': resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} peerDependencies: @@ -1410,6 +1444,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.7': resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} peerDependencies: @@ -1423,6 +1470,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.9': resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} peerDependencies: @@ -1449,6 +1509,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: @@ -2103,6 +2176,9 @@ packages: '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/lodash@4.17.21': + resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -5757,6 +5833,8 @@ snapshots: '@radix-ui/primitive@1.1.2': {} + '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-alert-dialog@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -5883,6 +5961,19 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -5904,6 +5995,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.7)(react@19.2.1)': + dependencies: + react: 19.2.1 + optionalDependencies: + '@types/react': 19.2.7 + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) @@ -5957,6 +6054,29 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1) + aria-hidden: 1.2.6 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + react-remove-scroll: 2.7.1(@types/react@19.2.7)(react@19.2.1) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-popper@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@floating-ui/react-dom': 2.1.4(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -5975,6 +6095,24 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/rect': 1.1.1 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -5995,6 +6133,16 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1) @@ -6725,6 +6873,8 @@ snapshots: '@types/ms': 2.1.0 '@types/node': 20.19.8 + '@types/lodash@4.17.21': {} + '@types/ms@2.1.0': {} '@types/node@20.19.8': diff --git a/public/logo.svg b/public/logo.svg index 7f0a69d..822c0f2 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1,5 +1,10 @@ - - - - + + + + + + + + + diff --git a/src/app/(auth)/share/[userId]/test.tsx b/src/app/(auth)/share/[userId]/test.tsx index 62279d4..9189fd9 100644 --- a/src/app/(auth)/share/[userId]/test.tsx +++ b/src/app/(auth)/share/[userId]/test.tsx @@ -1,20 +1,20 @@ -'use client' +'use client'; -import { useParams } from 'next/navigation' -import { useGetAIUserBaseInfo } from '@/hooks/aiUser' -import Image from 'next/image' +import { useParams } from 'next/navigation'; +import { useGetAIUserBaseInfo } from '@/hooks/aiUser'; +import Image from 'next/image'; // 按钮组件 interface MobileButtonProps { - showIcon?: boolean - icon?: React.ReactNode | null - btnTxt?: string - showTxt?: boolean - size?: 'Large' | 'Medium' | 'Small' - variant?: 'Contrast' | 'Basic' | 'Ghost' - type?: 'Primary' | 'Secondary' | 'Tertiary' | 'Destructive' | 'VIP' - state?: 'Default' | 'Disabled' | 'Pressed' - onClick?: () => void + showIcon?: boolean; + icon?: React.ReactNode | null; + btnTxt?: string; + showTxt?: boolean; + size?: 'Large' | 'Medium' | 'Small'; + variant?: 'Contrast' | 'Basic' | 'Ghost'; + type?: 'Primary' | 'Secondary' | 'Tertiary' | 'Destructive' | 'VIP'; + state?: 'Default' | 'Disabled' | 'Pressed'; + onClick?: () => void; } function MobileButton({ @@ -53,7 +53,7 @@ function MobileButton({ )} - ) + ); } return ( @@ -64,22 +64,22 @@ function MobileButton({ {showIcon && icon} {showTxt && {btnTxt}} - ) + ); } const SharePage = () => { - const { userId } = useParams() + const { userId } = useParams(); const { data: userInfo } = useGetAIUserBaseInfo({ aiId: userId ? Number(userId) : 0, - }) + }); - const { homeImageUrl } = userInfo || {} + const { homeImageUrl } = userInfo || {}; const handleChatClick = () => { // 跳转到应用或下载页面 - window.open('https://crushlevel.com/download', '_blank') - } + window.open('https://crushlevel.com/download', '_blank'); + }; return (
@@ -140,7 +140,7 @@ const SharePage = () => {
{/* AI信息卡片 */}
-
+
{/* 介绍文本 */}

@@ -197,7 +197,7 @@ const SharePage = () => { {/* 示例对话消息 */}

-
+
{/* 语音标签 */}
@@ -224,7 +224,7 @@ const SharePage = () => { {/* 底部品牌区域 */}
-
+
{/* App图标 */}
{
- ) -} + ); +}; -export default SharePage +export default SharePage; diff --git a/src/app/(main)/crushcoin/components/CheckInGrid.tsx b/src/app/(main)/crushcoin/components/CheckInGrid.tsx index 4d7667a..9cc436b 100644 --- a/src/app/(main)/crushcoin/components/CheckInGrid.tsx +++ b/src/app/(main)/crushcoin/components/CheckInGrid.tsx @@ -20,7 +20,7 @@ export function CheckInGrid() { {Array.from({ length: 6 }).map((_, index) => (
))}
diff --git a/src/app/(main)/home/components/Character/index.tsx b/src/app/(main)/home/components/Character/index.tsx index b167fdd..71f8bea 100644 --- a/src/app/(main)/home/components/Character/index.tsx +++ b/src/app/(main)/home/components/Character/index.tsx @@ -7,20 +7,20 @@ import { useHomeStore } from '../../store'; import { useEffect } from 'react'; const Character = () => { - const selectedTags = useHomeStore((state) => state.selectedTags); + const characterParams = useHomeStore((state) => state.characterParams); const { dataSource, isFirstLoading, isLoadingMore, noMoreData, onLoadMore, onSearch } = useSmartInfiniteQuery(fetchCharacters, { queryKey: 'characters', - defaultQuery: { tagIds: selectedTags }, + defaultQuery: characterParams, }); useEffect(() => { - onSearch({ tagIds: selectedTags }); - }, [selectedTags]); + onSearch(characterParams); + }, [characterParams]); return ( -
+
items={dataSource} enableLazyRender diff --git a/src/app/(main)/home/components/Filter.tsx b/src/app/(main)/home/components/Filter.tsx deleted file mode 100644 index ff8555d..0000000 --- a/src/app/(main)/home/components/Filter.tsx +++ /dev/null @@ -1,116 +0,0 @@ -'use client'; - -import { cn } from '@/lib/utils'; -import Image from 'next/image'; -import { Chip } from '@/components/ui/chip'; -import { useHomeStore } from '../store'; -import { useQuery } from '@tanstack/react-query'; -import { fetchCharacterTags } from '@/services/editor'; -import { useRef } from 'react'; -import { useTranslations } from 'next-intl'; - -const Filter = () => { - const tab = useHomeStore((state) => state.tab); - const setTab = useHomeStore((state) => state.setTab); - const ref = useRef(null); - const selectedTags = useHomeStore((state) => state.selectedTags); - const setSelectedTags = useHomeStore((state) => state.setSelectedTags); - const t = useTranslations('home'); - - // useEffect(() => { - // const mainContent = document.getElementById('main-content'); - // if (!mainContent) { - // return; - // } - // const handleScroll = () => { - // const scrollTop = mainContent.scrollTop; - // console.log('scrollTop', scrollTop, ref.current); - // const className = 'absolute bg-bg-primary-normal'; - // if (scrollTop > 248) { - // ref.current?.classList.add('absolute bg-bg-primary-normal'); - // } else { - // } - // }; - // mainContent?.addEventListener('scroll', handleScroll); - // return () => { - // mainContent?.removeEventListener('scroll', handleScroll); - // }; - // }, []); - - const { data: tags = [] } = useQuery({ - queryKey: ['tags', tab], - queryFn: async () => { - if (tab === 'character') { - const { data } = await fetchCharacterTags({ limit: 10 }); - return data.rows; - } - return []; - }, - }); - - const tabs = [ - { - label: t('story'), - value: 'story', - icon: 'icon-story', - activeIcon: 'icon-story-active', - }, - { - label: t('character'), - value: 'character', - icon: 'icon-character', - activeIcon: 'icon-character-active', - }, - ] as const; - - const handleSelect = (tagId: string) => { - if (selectedTags.includes(tagId)) { - setSelectedTags(selectedTags.filter((id) => id !== tagId)); - } else { - setSelectedTags([...selectedTags, tagId]); - } - }; - - return ( -
-
- {tabs.map((item) => { - const active = tab === item.value; - return ( -
setTab(item.value)} - className={cn( - 'flex items-center cursor-pointer gap-2', - active ? 'text-txt-primary-normal' : 'text-txt-secondary-normal' - )} - > - {item.label} - {item.label} -
- ); - })} -
-
- {tags?.map((tag: any) => ( - handleSelect(tag.id)} - > - # {tag.name} - - ))} -
-
- ); -}; - -export default Filter; diff --git a/src/app/(main)/home/components/Filter/MoreFilter.tsx b/src/app/(main)/home/components/Filter/MoreFilter.tsx new file mode 100644 index 0000000..0790628 --- /dev/null +++ b/src/app/(main)/home/components/Filter/MoreFilter.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { cn } from '@/lib/utils'; +import { useHomeStore } from '../../store'; +import React, { useMemo } from 'react'; +import { useTranslations } from 'next-intl'; +import IconFont from '@/components/ui/iconFont'; +import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popver'; +import omit from 'lodash/omit'; +import { TagListSelect } from './TagListSelect'; + +export const MoreFilter = React.memo(() => { + const characterParams = useHomeStore((state) => state.characterParams); + const setCharacterParams = useHomeStore((state) => state.setCharacterParams); + const t = useTranslations('common'); + + // 计算筛选条件数量 + const queryKeyNum = useMemo(() => { + const currentParams = omit(characterParams, ['tagIds']); + return Object.values(currentParams).reduce((acc, curr) => { + const count = Array.isArray(curr) ? curr.length : 1; + return acc + count; + }, 0); + }, [characterParams]); + + const genderOptions = [ + { + label: t('gender_0'), + value: 0, + }, + { + label: t('gender_1'), + value: 1, + }, + { + label: t('gender_2'), + value: 2, + }, + ]; + + return ( + + + + + +
{t('gender')}
+ setCharacterParams({ genders: values as number[] })} + /> +
+
+ ); +}); + +MoreFilter.displayName = 'MoreFilter'; diff --git a/src/app/(main)/home/components/Filter/TagListSelect.tsx b/src/app/(main)/home/components/Filter/TagListSelect.tsx new file mode 100644 index 0000000..6e7533f --- /dev/null +++ b/src/app/(main)/home/components/Filter/TagListSelect.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { cn } from '@/lib/utils'; +import { Chip } from '@/components/ui/chip'; +import React from 'react'; + +type TagListSelectProps = { + options: { label: string; value: string | number }[]; + value: (string | number)[]; + onChange: (values: (string | number)[]) => void; + chipProps?: React.ComponentProps; +} & Omit, 'onChange'>; + +export const TagListSelect = (props: TagListSelectProps) => { + const { options, value = [], onChange, chipProps, ...rest } = props; + + const handleSelect = (id: string | number) => { + if (value.includes(id)) { + onChange(value.filter((v: string | number) => v !== id)); + } else { + onChange([...value, id]); + } + }; + + return ( +
+ {options.map((option) => ( + handleSelect(option.value)} + > + {option.label} + + ))} +
+ ); +}; diff --git a/src/app/(main)/home/components/Filter/index.tsx b/src/app/(main)/home/components/Filter/index.tsx new file mode 100644 index 0000000..e4cc1a8 --- /dev/null +++ b/src/app/(main)/home/components/Filter/index.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { cn } from '@/lib/utils'; +import Image from 'next/image'; +import { useHomeStore } from '../../store'; +import { useQuery } from '@tanstack/react-query'; +import { fetchCharacterTags } from '@/services/editor'; +import { useRef } from 'react'; +import { useTranslations } from 'next-intl'; +import { TagListSelect } from './TagListSelect'; +import { MoreFilter } from './MoreFilter'; + +const Filter = () => { + const tab = useHomeStore((state) => state.tab); + const setTab = useHomeStore((state) => state.setTab); + const ref = useRef(null); + const characterParams = useHomeStore((state) => state.characterParams); + const setCharacterParams = useHomeStore((state) => state.setCharacterParams); + const t = useTranslations('home'); + + const { data: tags = [] } = useQuery({ + queryKey: ['tags', tab], + queryFn: async () => { + if (tab === 'character') { + const { data } = await fetchCharacterTags({ limit: 10 }); + return data.rows; + } + return []; + }, + }); + + const tabs = [ + { + label: t('story'), + value: 'story', + icon: 'icon-story', + activeIcon: 'icon-story-active', + }, + { + label: t('character'), + value: 'character', + icon: 'icon-character', + activeIcon: 'icon-character-active', + }, + ] as const; + + return ( +
+
+
+ {tabs.map((item) => { + const active = tab === item.value; + return ( +
setTab(item.value)} + className={cn( + 'flex items-center cursor-pointer gap-1 sm:gap-2', + active ? 'text-txt-primary-normal' : 'text-txt-secondary-normal' + )} + > + {item.label} + {item.label} +
+ ); + })} +
+ +
+ ({ label: tag.name, value: tag.id }))} + value={characterParams.tagIds || []} + onChange={(values) => setCharacterParams({ tagIds: values as string[] })} + /> +
+ ); +}; + +export default Filter; diff --git a/src/app/(main)/home/components/Header.tsx b/src/app/(main)/home/components/Header.tsx index ee16daa..c1e73f0 100644 --- a/src/app/(main)/home/components/Header.tsx +++ b/src/app/(main)/home/components/Header.tsx @@ -6,10 +6,14 @@ import Link from 'next/link'; import React from 'react'; import { useLayoutStore } from '@/stores'; import { useTranslations } from 'next-intl'; +import { useSignIn } from '@/hooks/services/signin'; const Header = React.memo(() => { const response = useLayoutStore((s) => s.response); const t = useTranslations('home'); + const { isTodaySigned, signInListData } = useSignIn(); + + if (!signInListData?.list?.length || isTodaySigned) return
; return ( diff --git a/src/app/(main)/home/components/HomePageFooter.tsx b/src/app/(main)/home/components/HomePageFooter.tsx index 1855747..557ac1f 100644 --- a/src/app/(main)/home/components/HomePageFooter.tsx +++ b/src/app/(main)/home/components/HomePageFooter.tsx @@ -1,13 +1,12 @@ 'use client'; -import { useState } from 'react'; +import React, { useState } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { cn } from '@/lib/utils'; import { useTranslations } from 'next-intl'; -import IconFont from '@/components/ui/iconFont'; -const HomePageFooter = () => { +const HomePageFooter = React.memo(() => { const t = useTranslations('footer'); const [isExpanded, setIsExpanded] = useState(false); @@ -33,7 +32,13 @@ const HomePageFooter = () => { {/* Logo 和 Slogan */}
- + logo

{t('slogan')}

@@ -123,6 +128,6 @@ const HomePageFooter = () => {
); -}; +}); export default HomePageFooter; diff --git a/src/app/(main)/home/store.ts b/src/app/(main)/home/store.ts index 43f0971..2a54b8c 100644 --- a/src/app/(main)/home/store.ts +++ b/src/app/(main)/home/store.ts @@ -1,15 +1,26 @@ import { create } from 'zustand'; +type CharacterParams = { + genders?: number[]; + tagIds?: string[]; +}; + interface HomeStore { tab: 'story' | 'character'; - selectedTags: string[]; + characterParams: CharacterParams; setTab: (tab: 'story' | 'character') => void; - setSelectedTags: (selectedTags: string[]) => void; + setCharacterParams: (params: Partial) => void; } -export const useHomeStore = create((set) => ({ +export const useHomeStore = create((set, get) => ({ tab: 'character', + characterParams: { + genders: [], + tagIds: [], + }, setTab: (tab: 'story' | 'character') => set({ tab }), - selectedTags: [], - setSelectedTags: (selectedTags: string[]) => set({ selectedTags }), + setCharacterParams: (params) => { + const { characterParams } = get(); + set({ characterParams: { ...characterParams, ...params } }); + }, })); diff --git a/src/app/(main)/vip/components/SubscribeVipDrawer/CarouselBanner.tsx b/src/app/(main)/vip/components/SubscribeVipDrawer/CarouselBanner.tsx index 2ba8a65..fd363ef 100644 --- a/src/app/(main)/vip/components/SubscribeVipDrawer/CarouselBanner.tsx +++ b/src/app/(main)/vip/components/SubscribeVipDrawer/CarouselBanner.tsx @@ -1,22 +1,22 @@ -'use client' +'use client'; -import { useState, useEffect, useRef, useCallback } from 'react' -import { IconButton } from '@/components/ui/button' -import { useGetMemberDetail } from '@/hooks/useWallet' -import { VipType } from '@/services/wallet' +import { useState, useEffect, useRef, useCallback } from 'react'; +import { IconButton } from '@/components/ui/button'; +import { useGetMemberDetail } from '@/hooks/useWallet'; +import { VipType } from '@/services/wallet'; -const AUTO_PLAY_INTERVAL = 4000 // 自动轮播间隔 4 秒 +const AUTO_PLAY_INTERVAL = 4000; // 自动轮播间隔 4 秒 -const FADE_DURATION = 400 // 渐变动画时长(毫秒) +const FADE_DURATION = 400; // 渐变动画时长(毫秒) const CarouselBanner = ({ vipType, visible }: { vipType?: VipType; visible: boolean }) => { - const [currentIndex, setCurrentIndex] = useState(0) - const [isPaused, setIsPaused] = useState(false) // 鼠标悬停时暂停轮播 - const [isFading, setIsFading] = useState(false) // 控制淡入淡出动画 - const { data: memberDetail, isLoading } = useGetMemberDetail() - const { memberPrivList } = memberDetail || {} - const autoPlayTimerRef = useRef(null) - const pendingIndexRef = useRef(null) + const [currentIndex, setCurrentIndex] = useState(0); + const [isPaused, setIsPaused] = useState(false); // 鼠标悬停时暂停轮播 + const [isFading, setIsFading] = useState(false); // 控制淡入淡出动画 + const { data: memberDetail, isLoading } = useGetMemberDetail(); + const { memberPrivList } = memberDetail || {}; + const autoPlayTimerRef = useRef(null); + const pendingIndexRef = useRef(null); // 将 memberPrivList 转换为轮播数据格式 const carouselData = @@ -25,109 +25,109 @@ const CarouselBanner = ({ vipType, visible }: { vipType?: VipType; visible: bool title: item.title || 'VIP Feature', description: item.desc || 'Enjoy exclusive VIP benefits', backgroundImage: item.img || '/images/vip/drawer-bg.png', - })) || [] + })) || []; // 清除定时器 const clearAutoPlayTimer = useCallback(() => { if (autoPlayTimerRef.current) { - clearInterval(autoPlayTimerRef.current) - autoPlayTimerRef.current = null + clearInterval(autoPlayTimerRef.current); + autoPlayTimerRef.current = null; } - }, []) + }, []); // 启动自动轮播定时器 const startAutoPlayTimer = useCallback(() => { - clearAutoPlayTimer() + clearAutoPlayTimer(); if (visible && carouselData.length > 1 && !isPaused) { autoPlayTimerRef.current = setInterval(() => { - setCurrentIndex((prev) => (prev + 1) % carouselData.length) - }, AUTO_PLAY_INTERVAL) + setCurrentIndex((prev) => (prev + 1) % carouselData.length); + }, AUTO_PLAY_INTERVAL); } - }, [visible, carouselData.length, isPaused, clearAutoPlayTimer]) + }, [visible, carouselData.length, isPaused, clearAutoPlayTimer]); // 鼠标悬停时暂停轮播 const handleMouseEnter = () => { - setIsPaused(true) - clearAutoPlayTimer() - } + setIsPaused(true); + clearAutoPlayTimer(); + }; // 鼠标离开时恢复轮播 const handleMouseLeave = () => { - setIsPaused(false) - } + setIsPaused(false); + }; // 带渐变动画的切换 const changeSlideWithFade = useCallback( (newIndex: number) => { - if (isFading) return // 防止动画过程中重复触发 - setIsFading(true) - pendingIndexRef.current = newIndex + if (isFading) return; // 防止动画过程中重复触发 + setIsFading(true); + pendingIndexRef.current = newIndex; setTimeout(() => { - setCurrentIndex(newIndex) - setIsFading(false) - }, FADE_DURATION) + setCurrentIndex(newIndex); + setIsFading(false); + }, FADE_DURATION); }, [isFading] - ) + ); const nextSlide = () => { if (carouselData.length > 0 && !isFading) { - const newIndex = (currentIndex + 1) % carouselData.length - changeSlideWithFade(newIndex) - startAutoPlayTimer() // 手动切换后重置定时器 + const newIndex = (currentIndex + 1) % carouselData.length; + changeSlideWithFade(newIndex); + startAutoPlayTimer(); // 手动切换后重置定时器 } - } + }; const prevSlide = () => { if (carouselData.length > 0 && !isFading) { - const newIndex = (currentIndex - 1 + carouselData.length) % carouselData.length - changeSlideWithFade(newIndex) - startAutoPlayTimer() // 手动切换后重置定时器 + const newIndex = (currentIndex - 1 + carouselData.length) % carouselData.length; + changeSlideWithFade(newIndex); + startAutoPlayTimer(); // 手动切换后重置定时器 } - } + }; // 自动轮播(带渐变动画) useEffect(() => { if (visible && carouselData.length > 1 && !isPaused) { autoPlayTimerRef.current = setInterval(() => { - setIsFading(true) + setIsFading(true); setTimeout(() => { - setCurrentIndex((prev) => (prev + 1) % carouselData.length) - setIsFading(false) - }, FADE_DURATION) - }, AUTO_PLAY_INTERVAL) + setCurrentIndex((prev) => (prev + 1) % carouselData.length); + setIsFading(false); + }, FADE_DURATION); + }, AUTO_PLAY_INTERVAL); } return () => { - clearAutoPlayTimer() - } - }, [visible, carouselData.length, isPaused, clearAutoPlayTimer]) + clearAutoPlayTimer(); + }; + }, [visible, carouselData.length, isPaused, clearAutoPlayTimer]); useEffect(() => { if (!memberPrivList) { - return + return; } if (visible) { if (vipType) { - const targetIndex = memberPrivList?.findIndex((item) => item.code === vipType) ?? -1 + const targetIndex = memberPrivList?.findIndex((item) => item.code === vipType) ?? -1; if (targetIndex >= 0) { - setCurrentIndex(targetIndex) + setCurrentIndex(targetIndex); } else { - setCurrentIndex(0) + setCurrentIndex(0); } } } - }, [visible, memberPrivList]) + }, [visible, memberPrivList]); - const currentSlide = carouselData[currentIndex] + const currentSlide = carouselData[currentIndex]; // 加载状态 if (isLoading) { return (
{/* 轮播图片容器 - 骨架屏 */} -
-
+
+
{/* 轮播内容信息 - 骨架屏 */} @@ -136,7 +136,7 @@ const CarouselBanner = ({ vipType, visible }: { vipType?: VipType; visible: bool
- ) + ); } return ( @@ -146,7 +146,7 @@ const CarouselBanner = ({ vipType, visible }: { vipType?: VipType; visible: bool onMouseLeave={handleMouseLeave} > {/* 轮播图片容器 */} -
+
{/* 背景图片 */}
@@ -194,7 +194,7 @@ const CarouselBanner = ({ vipType, visible }: { vipType?: VipType; visible: bool
- ) -} + ); +}; -export default CarouselBanner +export default CarouselBanner; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fb99715..6a25138 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -50,7 +50,7 @@ export default async function RootLayout({ className={`${poppins.variable} ${oleoScriptSwashCaps.variable} ${NumDisplay.variable} antialiased`} >