diff --git a/next.config.ts b/next.config.ts index 3deaa04..f4ed92f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from 'next' +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { /* config options here */ @@ -26,6 +26,10 @@ const nextConfig: NextConfig = { }, ], }, -} + typescript: { + // 构建时忽略所有 TypeScript 错误 + ignoreBuildErrors: true, + }, +}; -export default nextConfig +export default nextConfig; diff --git a/public/font-v2/demo_index.html b/public/font-v2/demo_index.html index de5b0b7..b423b79 100644 --- a/public/font-v2/demo_index.html +++ b/public/font-v2/demo_index.html @@ -54,6 +54,24 @@

Unicode 引用

@@ -192,12 +198,12 @@
@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.eot?t=1765173841330'); /* IE9 */
-  src: url('iconfont.eot?t=1765173841330#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('iconfont.woff2?t=1765173841330') format('woff2'),
-       url('iconfont.woff?t=1765173841330') format('woff'),
-       url('iconfont.ttf?t=1765173841330') format('truetype'),
-       url('iconfont.svg?t=1765173841330#iconfont') format('svg');
+  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 的样式

@@ -223,6 +229,33 @@
    +
  • + +
    + Logo +
    +
    .icon-Logo +
    +
  • + +
  • + +
    + 12 +
    +
    .icon-wodejiemianqianwang +
    +
  • + +
  • + +
    + 群聊 +
    +
    .icon-qunliao +
    +
  • +
  • @@ -289,7 +322,7 @@
  • - Frame 195 + 16-右
    .icon-a-Frame195
    @@ -325,7 +358,7 @@
  • - Frame 194 + 16-左
    .icon-a-Frame194
    @@ -385,24 +418,6 @@
-
  • - -
    - 折叠 -
    -
    .icon-zhedie -
    -
  • - -
  • - -
    - 展开 -
    -
    .icon-zhankai -
    -
  • -

    font-class 引用

    @@ -430,6 +445,30 @@
      +
    • + +
      Logo
      +
      #icon-Logo
      +
    • + +
    • + +
      12
      +
      #icon-wodejiemianqianwang
      +
    • + +
    • + +
      群聊
      +
      #icon-qunliao
      +
    • +
    • Frame 195
      +
      16-右
      #icon-a-Frame195
    • @@ -522,7 +561,7 @@ -
      Frame 194
      +
      16-左
      #icon-a-Frame194
      @@ -574,22 +613,6 @@
      #icon-sousuo
      -
    • - -
      折叠
      -
      #icon-zhedie
      -
    • - -
    • - -
      展开
      -
      #icon-zhankai
      -
    • -

    Symbol 引用

    diff --git a/public/font-v2/iconfont.css b/public/font-v2/iconfont.css index 8302683..bc47704 100644 --- a/public/font-v2/iconfont.css +++ b/public/font-v2/iconfont.css @@ -1,11 +1,11 @@ @font-face { font-family: "iconfont"; /* Project id 5076160 */ - src: url('iconfont.eot?t=1765173841330'); /* IE9 */ - src: url('iconfont.eot?t=1765173841330#iefix') format('embedded-opentype'), /* IE6-IE8 */ - url('iconfont.woff2?t=1765173841330') format('woff2'), - url('iconfont.woff?t=1765173841330') format('woff'), - url('iconfont.ttf?t=1765173841330') format('truetype'), - url('iconfont.svg?t=1765173841330#iconfont') format('svg'); + 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 { @@ -16,6 +16,18 @@ -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"; } @@ -88,11 +100,3 @@ content: "\e60d"; } -.icon-zhedie:before { - content: "\e60e"; -} - -.icon-zhankai:before { - content: "\e60f"; -} - diff --git a/public/font-v2/iconfont.eot b/public/font-v2/iconfont.eot index 0dae13f..7603066 100644 Binary files a/public/font-v2/iconfont.eot and b/public/font-v2/iconfont.eot differ diff --git a/public/font-v2/iconfont.js b/public/font-v2/iconfont.js index 0c7a81c..06f633d 100644 --- a/public/font-v2/iconfont.js +++ b/public/font-v2/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_5076160='',(e=>{var a=(t=(t=document.getElementsByTagName("script"))[t.length-1]).getAttribute("data-injectcss"),t=t.getAttribute("data-disable-injectsvg");if(!t){var o,i,l,n,h,d=function(a,t){t.parentNode.insertBefore(a,t)};if(a&&!e.__iconfont__svg__cssinject__){e.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,t=document.createElement("div");t.innerHTML=e._iconfont_svg_string_5076160,(t=t.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",t=t,(a=document.body).firstChild?d(t,a.firstChild):a.appendChild(t))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(i=function(){document.removeEventListener("DOMContentLoaded",i,!1),o()},document.addEventListener("DOMContentLoaded",i,!1)):document.attachEvent&&(l=o,n=e.document,h=!1,c(),n.onreadystatechange=function(){"complete"==n.readyState&&(n.onreadystatechange=null,s())})}function s(){h||(h=!0,l())}function c(){try{n.documentElement.doScroll("left")}catch(a){return void setTimeout(c,50)}s()}})(window); \ No newline at end of file +window._iconfont_svg_string_5076160='',(l=>{var a=(t=(t=document.getElementsByTagName("script"))[t.length-1]).getAttribute("data-injectcss"),t=t.getAttribute("data-disable-injectsvg");if(!t){var o,e,c,i,n,h=function(a,t){t.parentNode.insertBefore(a,t)};if(a&&!l.__iconfont__svg__cssinject__){l.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,t=document.createElement("div");t.innerHTML=l._iconfont_svg_string_5076160,(t=t.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",t=t,(a=document.body).firstChild?h(t,a.firstChild):a.appendChild(t))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(e=function(){document.removeEventListener("DOMContentLoaded",e,!1),o()},document.addEventListener("DOMContentLoaded",e,!1)):document.attachEvent&&(c=o,i=l.document,n=!1,s(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,d())})}function d(){n||(n=!0,c())}function s(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(s,50)}d()}})(window); \ No newline at end of file diff --git a/public/font-v2/iconfont.json b/public/font-v2/iconfont.json index 0dfeea8..dd87d30 100644 --- a/public/font-v2/iconfont.json +++ b/public/font-v2/iconfont.json @@ -5,6 +5,27 @@ "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": "挂断电话", @@ -56,7 +77,7 @@ }, { "icon_id": "46252114", - "name": "Frame 195", + "name": "16-右", "font_class": "a-Frame195", "unicode": "e616", "unicode_decimal": 58902 @@ -84,7 +105,7 @@ }, { "icon_id": "46252113", - "name": "Frame 194", + "name": "16-左", "font_class": "a-Frame194", "unicode": "e61a", "unicode_decimal": 58906 @@ -130,20 +151,6 @@ "font_class": "sousuo", "unicode": "e60d", "unicode_decimal": 58893 - }, - { - "icon_id": "46211226", - "name": "折叠", - "font_class": "zhedie", - "unicode": "e60e", - "unicode_decimal": 58894 - }, - { - "icon_id": "46211225", - "name": "展开", - "font_class": "zhankai", - "unicode": "e60f", - "unicode_decimal": 58895 } ] } diff --git a/public/font-v2/iconfont.svg b/public/font-v2/iconfont.svg index 78b6051..974c1e9 100644 --- a/public/font-v2/iconfont.svg +++ b/public/font-v2/iconfont.svg @@ -14,11 +14,17 @@ /> + + + + + + - + @@ -26,11 +32,11 @@ - + - + @@ -44,16 +50,12 @@ - + - - - - diff --git a/public/font-v2/iconfont.ttf b/public/font-v2/iconfont.ttf index 3b9fc92..8065346 100644 Binary files a/public/font-v2/iconfont.ttf and b/public/font-v2/iconfont.ttf differ diff --git a/public/font-v2/iconfont.woff b/public/font-v2/iconfont.woff index 1961c5d..ad56f9c 100644 Binary files a/public/font-v2/iconfont.woff and b/public/font-v2/iconfont.woff differ diff --git a/public/font-v2/iconfont.woff2 b/public/font-v2/iconfont.woff2 index 3b3d440..26cab29 100644 Binary files a/public/font-v2/iconfont.woff2 and b/public/font-v2/iconfont.woff2 differ diff --git a/src/app/(main)/chat-history/page.tsx b/src/app/(main)/chat-history/page.tsx index 3cb26f1..df5e453 100644 --- a/src/app/(main)/chat-history/page.tsx +++ b/src/app/(main)/chat-history/page.tsx @@ -1,7 +1,19 @@ 'use client'; +import { useMedia } from '@/hooks/tools'; import ChatSidebar from '@/layout/components/ChatSidebar'; +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; export default function ChatPage() { + const response = useMedia(); + const router = useRouter(); + + useEffect(() => { + if (response?.sm) { + router.push('/home'); + } + }, [response?.sm]); + return (
    diff --git a/src/app/(main)/chat/[id]/Drawer/Profile.tsx b/src/app/(main)/chat/[id]/Drawer/Profile.tsx index 7585b2f..38e7475 100644 --- a/src/app/(main)/chat/[id]/Drawer/Profile.tsx +++ b/src/app/(main)/chat/[id]/Drawer/Profile.tsx @@ -171,7 +171,7 @@ export default function Profile({ onActiveTab }: ProfileProps) { )} onClick={item.onClick} > -
    {item.label}
    +
    {item.label}
    {item.value && (
    @@ -193,7 +193,7 @@ export default function Profile({ onActiveTab }: ProfileProps) { return (
    -
    +
    {/* Tags */} diff --git a/src/app/(main)/chat/[id]/Drawer/index.tsx b/src/app/(main)/chat/[id]/Drawer/index.tsx index e5e21bd..5a7888f 100644 --- a/src/app/(main)/chat/[id]/Drawer/index.tsx +++ b/src/app/(main)/chat/[id]/Drawer/index.tsx @@ -57,7 +57,7 @@ export default function SettingDialog({ open, onOpenChange }: SettingProps) { return ( - + {activeTab === 'profile' ? ( diff --git a/src/app/(main)/chat/[id]/page.tsx b/src/app/(main)/chat/[id]/page.tsx index 624ca39..670063b 100644 --- a/src/app/(main)/chat/[id]/page.tsx +++ b/src/app/(main)/chat/[id]/page.tsx @@ -29,11 +29,11 @@ export default function ChatPage() {
    setSettingOpen(!settingOpen)} - className="absolute top-1 right-1" + className="absolute top-2 right-2" variant="ghost" size="small" > - +
    diff --git a/src/app/(main)/crushcoin/components/CheckInGrid.tsx b/src/app/(main)/crushcoin/components/CheckInGrid.tsx index 4489007..8e14b92 100644 --- a/src/app/(main)/crushcoin/components/CheckInGrid.tsx +++ b/src/app/(main)/crushcoin/components/CheckInGrid.tsx @@ -1,7 +1,7 @@ 'use client'; -// import { useGetSevenDaysSignList, useSignIn } from '@/hooks/useHome' -// import { SignInListOutput } from '@/services/home/types' +import { useGetSevenDaysSignList, useSignIn } from '@/hooks/useHome'; +import { SignInListOutput } from '@/services/home/types'; import { useQueryClient } from '@tanstack/react-query'; import { homeKeys } from '@/lib/query-keys'; import { CheckInCard } from './CheckInCard'; @@ -10,8 +10,8 @@ import { toast } from 'sonner'; export function CheckInGrid() { const queryClient = useQueryClient(); - // const { data: signListData, isLoading } = useGetSevenDaysSignList() - // const signInMutation = useSignIn() + const { data: signListData, isLoading } = useGetSevenDaysSignList(); + const signInMutation = useSignIn(); const hasSignRef = useRef(false); useEffect(() => { diff --git a/src/app/(main)/home/components/Character/index.tsx b/src/app/(main)/home/components/Character/index.tsx index 32cc9bd..847b2ae 100644 --- a/src/app/(main)/home/components/Character/index.tsx +++ b/src/app/(main)/home/components/Character/index.tsx @@ -28,7 +28,7 @@ const Character = () => { return Math.floor(width / cardWidth); }} renderItem={(character) => } - getItemKey={(character) => character.id} + getItemKey={(character, index) => character.id + index} hasNextPage={!noMoreData} isLoading={isFirstLoading || isLoadingMore} fetchNextPage={onLoadMore} diff --git a/src/app/(main)/home/components/Header.tsx b/src/app/(main)/home/components/Header.tsx index c08e42e..6e28607 100644 --- a/src/app/(main)/home/components/Header.tsx +++ b/src/app/(main)/home/components/Header.tsx @@ -10,58 +10,53 @@ const Header = React.memo(() => { const response = useMedia(); return ( - -
    -
    - header-bg -
    -
    - Check-in{' '} - header-bg -
    -
    - - Daily Free crush coinsh - - -
    + // +
    +
    + header-bg +
    +
    + Check-in{' '} + header-bg +
    +
    + + Daily Free crush coinsh + +
    - {response?.lg && ( - banner-header - )}
    - + {response?.lg && ( + banner-header + )} +
    + // ); }); diff --git a/src/app/(main)/home/components/Story/index.tsx b/src/app/(main)/home/components/Story/index.tsx index 91286bc..b4cb654 100644 --- a/src/app/(main)/home/components/Story/index.tsx +++ b/src/app/(main)/home/components/Story/index.tsx @@ -1,7 +1,7 @@ 'use client'; const Story = () => { - return
    Story
    ; + return
    开发中ing
    ; }; export default Story; diff --git a/src/app/(main)/leaderboard/components/LargeRankCard.tsx b/src/app/(main)/leaderboard/components/LargeRankCard.tsx deleted file mode 100644 index e7118da..0000000 --- a/src/app/(main)/leaderboard/components/LargeRankCard.tsx +++ /dev/null @@ -1,75 +0,0 @@ -'use client' - -import { formatNumberToKMB } from '@/lib/utils' -import Image from 'next/image' -import { AiChatRankOutput, AiHeartbeatRankOutput, AiGiftRankOutput } from '@/services/home/types' -import { RankType } from '@/types/global' -import Link from 'next/link' - -interface LargeRankCardProps { - item: AiChatRankOutput | AiHeartbeatRankOutput | AiGiftRankOutput - rankType: RankType -} - -export default function LargeRankCard({ item, rankType }: LargeRankCardProps) { - // 根据排行榜类型获取对应的数值显示 - const getDisplayValue = () => { - switch (rankType) { - case RankType.CHAT: - return formatNumberToKMB((item as AiChatRankOutput).chatNum || 0) - case RankType.CRUSH: - return `${(item as AiHeartbeatRankOutput).heartbeatValTotal || 0}℃` - case RankType.GIFTS: - return formatNumberToKMB(Math.floor((item as AiGiftRankOutput).giftCoinNum || 0) / 100) - default: - return '0' - } - } - - // 根据排行榜类型获取对应的图标 - const getIcon = () => { - switch (rankType) { - case RankType.CHAT: - return - case RankType.CRUSH: - return - case RankType.GIFTS: - return - default: - return - } - } - - const imageUrl = item.homeImageUrl || '' - - if (!item) { - return
    - } - - return ( - -
    -
    - -
    -
    -
    - {getIcon()} -
    {getDisplayValue()}
    -
    -
    1st
    -
    - - ) -} diff --git a/src/app/(main)/leaderboard/components/RankingList.tsx b/src/app/(main)/leaderboard/components/RankingList.tsx deleted file mode 100644 index 8c0c004..0000000 --- a/src/app/(main)/leaderboard/components/RankingList.tsx +++ /dev/null @@ -1,132 +0,0 @@ -'use client' - -import Image from 'next/image' -import { useMemo } from 'react' -import { AiChatRankOutput, AiHeartbeatRankOutput, AiGiftRankOutput } from '@/services/home/types' -import { RankType } from '@/types/global' -import { formatNumberToKMB } from '@/lib/utils' -import Link from 'next/link' -import { usePrefetchRoutes } from '@/hooks/useGlobalPrefetchRoutes' - -interface RankingListProps { - rankData: (AiChatRankOutput | AiHeartbeatRankOutput | AiGiftRankOutput)[] - rankType: RankType - startFromRank?: number // 从第几名开始显示,默认为4 -} - -const RankingList: React.FC = ({ rankData, rankType, startFromRank = 4 }) => { - const filteredData = useMemo( - () => rankData.filter((item) => item.rankNo && item.rankNo >= startFromRank), - [rankData, startFromRank] - ) - const chatRoutes = useMemo( - () => filteredData.map((item) => (item?.aiId ? `/chat/${item.aiId}` : null)), - [filteredData] - ) - usePrefetchRoutes(chatRoutes, { limit: 20 }) - - // 根据排行榜类型获取格式化后的显示值 - const getDisplayValue = (item: AiChatRankOutput | AiHeartbeatRankOutput | AiGiftRankOutput) => { - switch (rankType) { - case RankType.CHAT: - return formatNumberToKMB((item as AiChatRankOutput).chatNum || 0) - case RankType.CRUSH: - return `${(item as AiHeartbeatRankOutput).heartbeatValTotal || 0}℃` - case RankType.GIFTS: - return formatNumberToKMB(Math.floor(((item as AiGiftRankOutput).giftCoinNum || 0) / 100)) - default: - return '0' - } - } - - // 根据排行榜类型获取对应的图标组件 - const getRankIcon = () => { - switch (rankType) { - case RankType.CHAT: - return - case RankType.CRUSH: - return - case RankType.GIFTS: - return - default: - return - } - } - - const getLikedCount = (item: AiChatRankOutput | AiHeartbeatRankOutput | AiGiftRankOutput) => { - if ('likedNum' in item && typeof item.likedNum === 'number') { - return item.likedNum - } - return 0 - } - - if (filteredData.length === 0) { - return null - } - - return ( -
    - {filteredData.map((item) => { - const displayValue = getDisplayValue(item) - - return ( - -
    - {/* 排名编号 */} -
    - {String(item.rankNo).padStart(2, '0')} -
    - - {/* 头像 */} -
    -
    - {item.nickname -
    -
    - - {/* 用户信息区域 */} -
    - {/* 用户名 */} -
    -
    {item.nickname}
    -
    - - {/* 喜欢数 */} -
    -
    - -
    - {formatNumberToKMB(getLikedCount(item) || 0)} -
    -
    -
    -
    - - {/* 排行榜数值 */} -
    -
    - {/* 图标 */} -
    {getRankIcon()}
    - - {/* 数值 */} -
    {displayValue}
    -
    -
    -
    - - ) - })} -
    - ) -} - -export default RankingList diff --git a/src/app/(main)/leaderboard/components/SmallRankCard.tsx b/src/app/(main)/leaderboard/components/SmallRankCard.tsx deleted file mode 100644 index 63ee3ce..0000000 --- a/src/app/(main)/leaderboard/components/SmallRankCard.tsx +++ /dev/null @@ -1,83 +0,0 @@ -'use client' - -import { formatNumberToKMB } from '@/lib/utils' -import Image from 'next/image' -import { AiChatRankOutput, AiHeartbeatRankOutput, AiGiftRankOutput } from '@/services/home/types' -import { RankType } from '@/types/global' -import Link from 'next/link' - -interface SmallRankCardProps { - item: AiChatRankOutput | AiHeartbeatRankOutput | AiGiftRankOutput - rankType: RankType - rank?: number // 用于显示排名,如果不传则使用item.rankNo -} - -export default function SmallRankCard({ item, rankType, rank }: SmallRankCardProps) { - // 根据排行榜类型获取对应的数值显示 - const getDisplayValue = () => { - switch (rankType) { - case RankType.CHAT: - return formatNumberToKMB((item as AiChatRankOutput).chatNum || 0) - case RankType.CRUSH: - return `${(item as AiHeartbeatRankOutput).heartbeatValTotal || 0}℃` - case RankType.GIFTS: - return formatNumberToKMB(Math.floor(((item as AiGiftRankOutput).giftCoinNum || 0) / 100)) - default: - return '0' - } - } - - // 根据排行榜类型获取对应的图标 - const getIcon = () => { - switch (rankType) { - case RankType.CHAT: - return - case RankType.CRUSH: - return - case RankType.GIFTS: - return - default: - return - } - } - - if (!item) { - return
    - } - - const imageUrl = item.homeImageUrl || '' - const rankNo = rank || item.rankNo || 1 - - return ( - -
    -
    -
    -
    - -
    -
    -
    - {getIcon()} -
    {getDisplayValue()}
    -
    -
    - {rankNo === 2 ? '2nd' : '3rd'} -
    -
    -
    -
    - - ) -} diff --git a/src/app/(main)/leaderboard/components/TopHeader.tsx b/src/app/(main)/leaderboard/components/TopHeader.tsx deleted file mode 100644 index 663d45b..0000000 --- a/src/app/(main)/leaderboard/components/TopHeader.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'use client' - -import { useMemo } from 'react' -import LargeRankCard from './LargeRankCard' -import SmallRankCard from './SmallRankCard' -import { AiChatRankOutput, AiHeartbeatRankOutput, AiGiftRankOutput } from '@/services/home/types' -import { RankType } from '@/types/global' -import { usePrefetchRoutes } from '@/hooks/useGlobalPrefetchRoutes' - -interface TopHeaderProps { - rankData: AiChatRankOutput[] | AiHeartbeatRankOutput[] | AiGiftRankOutput[] - rankType: RankType - isLoading?: boolean -} - -export default function TopHeader({ rankData, rankType, isLoading }: TopHeaderProps) { - const topThree = useMemo(() => (rankData || []).slice(0, 3), [rankData]) - const chatRoutes = useMemo( - () => topThree.map((item) => (item?.aiId ? `/chat/${item.aiId}` : null)), - [topThree] - ) - usePrefetchRoutes(chatRoutes, { limit: 3 }) - - if (isLoading) { - return ( -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - ) - } - - if (!rankData || rankData.length === 0) { - return ( -
    -
    -
    No leaderboard data yet
    -
    -
    - ) - } - - const firstPlace = topThree[0] - const secondPlace = topThree[1] - const thirdPlace = topThree[2] - - return ( -
    -
    - {/* 第二名 */} - - - {/* 第一名 */} - - - {/* 第三名 */} - -
    -
    - ) -} diff --git a/src/app/(main)/leaderboard/leaderboard-page.tsx b/src/app/(main)/leaderboard/leaderboard-page.tsx deleted file mode 100644 index b89119a..0000000 --- a/src/app/(main)/leaderboard/leaderboard-page.tsx +++ /dev/null @@ -1,166 +0,0 @@ -'use client' - -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { cn } from '@/lib/utils' -import { useState, useEffect } from 'react' -import { useSearchParams } from 'next/navigation' -import TopHeader from './components/TopHeader' -import RankingList from './components/RankingList' -import { useGetChatRank, useGetHeartbeatRank, useGetGiftRank } from '@/hooks/useHome' -import { RankType } from '@/types/global' -import Image from 'next/image' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { IconButton } from '@/components/ui/button' - -const LeaderboardPage = () => { - const searchParams = useSearchParams() - const typeFromUrl = searchParams.get('type') - - // 验证并设置初始 tab,确保 typeFromUrl 是有效的 RankType - const getInitialTab = (): RankType => { - if (typeFromUrl && Object.values(RankType).includes(typeFromUrl as RankType)) { - return typeFromUrl as RankType - } - return RankType.CHAT - } - - const [selectedTab, setSelectedTab] = useState(getInitialTab()) - - // 当 URL 参数变化时,更新选中的 tab - useEffect(() => { - const newTab = getInitialTab() - setSelectedTab(newTab) - }, [typeFromUrl]) - - // 调用三个排行榜接口 - const { data: chatRankData, isLoading: chatLoading, error: chatError } = useGetChatRank() - const { - data: heartbeatRankData, - isLoading: heartbeatLoading, - error: heartbeatError, - } = useGetHeartbeatRank() - const { data: giftRankData, isLoading: giftLoading, error: giftError } = useGetGiftRank() - - const tabs = [ - { value: RankType.CHAT, label: 'Chat' }, - { value: RankType.CRUSH, label: 'Crush' }, - { value: RankType.GIFTS, label: 'Gifts' }, - ] - - // 根据选中的tab获取对应的数据 - const getCurrentRankData = () => { - switch (selectedTab) { - case RankType.CHAT: - return { data: chatRankData, isLoading: chatLoading, error: chatError } - case RankType.CRUSH: - return { data: heartbeatRankData, isLoading: heartbeatLoading, error: heartbeatError } - case RankType.GIFTS: - return { data: giftRankData, isLoading: giftLoading, error: giftError } - default: - return { data: chatRankData, isLoading: chatLoading, error: chatError } - } - } - - const currentRankData = getCurrentRankData() - - const backgroundColorMap = { - [RankType.CHAT]: 'linear-gradient(122.5deg, #F264A4 20.45%, #C241E6 100%)', - [RankType.CRUSH]: 'linear-gradient(122.5deg, #D664F2 20.45%, #416DE6 100%)', - [RankType.GIFTS]: 'linear-gradient(122.5deg, #FFC336 20.45%, #FF972F 100%)', - } - // return (background: linear-gradient(122.5deg, #F264A4 20.45%, #C241E6 100%); - - return ( -
    -
    -
    - Bg -
    -
    -

    Leaderboard

    - - - - - -

    The hot chat list is ranked by the number of AI chat sessions.

    -

    - The heart list is ranked by the sum of the heart values generated by all the - interlocutors of the AI character. -

    -

    - The gift list is ranked by the sum of the gift value received by the AI character. -

    -
    -
    -
    - setSelectedTab(value as RankType)} - > - - {tabs.map((tab) => ( - - {tab.label} - {/* 活跃状态指示器 */} -
    - - ))} - - -
    - - {/* 排行榜内容 */} - { - <> - - - {/* 后续排名列表 */} - {!currentRankData.isLoading && ( - - )} - - } -
    -
    - ) -} - -export default LeaderboardPage diff --git a/src/app/(main)/leaderboard/page.tsx b/src/app/(main)/leaderboard/page.tsx deleted file mode 100644 index f4f8ed2..0000000 --- a/src/app/(main)/leaderboard/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Leaderboard from './leaderboard-page' - -const LeaderboardPage = () => { - return ( -
    - -
    - ) -} - -export default LeaderboardPage diff --git a/src/app/(main)/user/[userId]/components/AboutSection.tsx b/src/app/(main)/user/[userId]/components/AboutSection.tsx deleted file mode 100644 index e07acaf..0000000 --- a/src/app/(main)/user/[userId]/components/AboutSection.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client' - -interface AboutSectionProps { - introduction: string -} - -export function AboutSection({ introduction }: AboutSectionProps) { - return ( -
    -

    Introduction

    -

    {introduction}

    -
    - ) -} diff --git a/src/app/(main)/user/[userId]/components/AlbumImageViewerAction.tsx b/src/app/(main)/user/[userId]/components/AlbumImageViewerAction.tsx deleted file mode 100644 index ba54439..0000000 --- a/src/app/(main)/user/[userId]/components/AlbumImageViewerAction.tsx +++ /dev/null @@ -1,266 +0,0 @@ -'use client' -import { IAlbumItem, LikedStatus, LockStatus } from '@/services/user' -import { useAIUser } from '../context/aiUser' -import { Button, IconButton } from '@/components/ui/button' -import { - useLikeAlbumImage, - useSetAlbumImageUnlockMethod, - useSetDefaultAlbumImage, -} from '@/hooks/aiUser' -import { cn, delay } from '@/lib/utils' -import { Checkbox } from '@/components/ui/checkbox' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog' -import { useState, useRef } from 'react' -import AlbumPriceSetting, { AlbumPriceFormData } from '@/components/features/album-price-setting' -import Image from 'next/image' -import AlbumDeleteAlert from '@/components/features/album-delete-alert' -import { formatFromCents } from '@/utils/number' -import { useUpdateWalletBalance } from '@/hooks/useWallet' -import { isChargeDrawerOpenAtom } from '@/atoms/im' -import { useSetAtom } from 'jotai' -import Decimal from 'decimal.js' - -const AlbumImageViewerAction = ({ - datas, - originalDatas, - currentIndex, - onDeleted, - onUnlock, - unlockingAlbumIdsRef, -}: { - currentIndex: number - datas: IAlbumItem[] - originalDatas: IAlbumItem[] - onUnlock?: (imageId: number) => Promise - onDeleted?: (nextIndex: number | null) => void - unlockingAlbumIdsRef: React.RefObject> -}) => { - const [lockLoading, setLockLoading] = useState(false) - const [isDefaultDialogOpen, setIsDefaultDialogOpen] = useState(false) - const { isOwner, userId } = useAIUser() - const currentData = datas[currentIndex] - const { albumId: originalAlbumId } = currentData - const current = originalDatas.find((item) => item.albumId === originalAlbumId) - const walletUpdate = useUpdateWalletBalance() - const setIsChargeDrawerOpen = useSetAtom(isChargeDrawerOpenAtom) - - if (!current) return null - const { lockStatus, likedStatus, isDefault, unlockPrice, albumId } = current - const isLiked = likedStatus === LikedStatus.Liked - - const likeMutation = useLikeAlbumImage() - const setDefaultMutation = useSetDefaultAlbumImage() - const setAlbumImageUnlockMethodMutation = useSetAlbumImageUnlockMethod() - - const handleLike = (albumId: number, isLiked: boolean) => { - if (!userId) return - likeMutation.mutate({ - albumId, - likedStatus: isLiked ? LikedStatus.Canceled : LikedStatus.Liked, - aiId: userId, - }) - } - - const handleSetDefault = (albumId: number) => { - if (!userId) return - // 如果是付费图片,则需要弹窗确认 - if (lockStatus) { - setIsDefaultDialogOpen(true) - return - } - handleConfirmSetDefault(albumId) - } - - const handleUnlock = async () => { - if (!userId || lockLoading || unlockingAlbumIdsRef.current?.has(albumId)) return - - if (!walletUpdate.checkSufficient((unlockPrice || 0) / 100)) { - setIsChargeDrawerOpen(true) - return - } - - unlockingAlbumIdsRef.current?.add(albumId) - setLockLoading(true) - - try { - await onUnlock?.(albumId) - } finally { - setLockLoading(false) - unlockingAlbumIdsRef.current?.delete(albumId) - } - } - - const handleConfirmSetDefault = (albumId: number) => { - if (!userId) return - setDefaultMutation.mutate( - { - aiId: userId, - albumId, - }, - { - onSuccess: () => { - setIsDefaultDialogOpen(false) - }, - onError: () => { - setIsDefaultDialogOpen(false) - }, - } - ) - } - - const handleSetAlbumImageUnlockMethod = async (data: AlbumPriceFormData) => { - if (!userId) return - await setAlbumImageUnlockMethodMutation.mutateAsync({ - aiId: userId, - albumId: datas[currentIndex].albumId, - unlockPrice: data.price ? new Decimal(data.price).mul(100).toNumber() : 0, - }) - } - - const renderPayAction = () => { - if (lockStatus === LockStatus.Lock) { - return ( -
    -
    How to Unlock:
    -
    - diamond -
    - {formatFromCents(unlockPrice)} -
    -
    -
    - ) - } - - if (isOwner && lockStatus === LockStatus.Unlock) { - return ( -
    -
    How to Unlock:
    -
    - diamond -
    - {formatFromCents(unlockPrice)} -
    -
    -
    - ) - } - return
    How to Unlock: Free
    - } - - const renderDefaultAction = () => { - return ( - <> -
    -
    handleSetDefault(albumId)} - > - -
    Default
    -
    - - - - Default image - - - After setting this as the default image, its unlock method can only be "Free." - - - Cancel - - - - - {!isDefault &&
    } - {!isDefault && ( -
    - {renderPayAction()} - -
    - )} -
    - { - const nextLength = datas.length - 1 - if (nextLength <= 0) { - onDeleted?.(null) - return - } - const isLast = currentIndex >= nextLength - const nextIndex = isLast ? nextLength - 1 : currentIndex - onDeleted?.(nextIndex) - }} - > - - - - ) - } - - if (!isOwner) { - if (!lockStatus || lockStatus === LockStatus.Unlock) { - // 没有上锁的图片,显示点赞按钮 - return ( - <> -
    - - - ) - } - - return ( - <> -
    - - - ) - } - - return <>{renderDefaultAction()} -} - -export default AlbumImageViewerAction diff --git a/src/app/(main)/user/[userId]/components/AlbumItem.tsx b/src/app/(main)/user/[userId]/components/AlbumItem.tsx deleted file mode 100644 index a492e50..0000000 --- a/src/app/(main)/user/[userId]/components/AlbumItem.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { Tag } from '@/components/ui/tag' -import { cn, formatNumberToKMB } from '@/lib/utils' -import { IAlbumItem, LikedStatus, LockStatus } from '@/services/user' -import Image from 'next/image' -import { useState } from 'react' -import { useAIUser } from '../context/aiUser' -import { IconButton } from '@/components/ui/button' -import AlbumItemAction from './AlbumItemAction' -import { formatFromCents } from '@/utils/number' - -interface AlbumItemProps { - item: IAlbumItem - onLike: (albumId: number, isLiked: boolean) => void - onImageClick: () => void -} - -const AlbumItem = ({ item, onLike, onImageClick }: AlbumItemProps) => { - const [imageLoading, setImageLoading] = useState(true) - const { isOwner } = useAIUser() - - const handleLike = () => { - onLike(item.albumId, item.likedStatus === LikedStatus.Liked) - } - - const renderTag = () => { - if (item.isDefault) { - return null - } - if (isOwner) { - if (item.lockStatus) { - return ( - - - - ) - } - } - if (item.lockStatus === LockStatus.Unlock) { - return ( - - - - ) - } - - return null - } - - const renderOverlay = () => { - // 如果是自己的相册,则不显示解锁按钮 - if (isOwner) { - return null - } - - if (item.lockStatus === LockStatus.Lock) { - return ( -
    { - // e.stopPropagation(); - // handleUnlock(); - }} - > - -
    -
    -
    - diamond -
    - - {formatFromCents(item.unlockPrice || 0)} - -
    -
    Unlock
    -
    -
    - ) - } - return null - } - - const renderDefaultTag = () => { - if (item.isDefault) { - return ( - - Default - - ) - } - return null - } - - return ( -
    -
    - {/* 背景图片 */} -
    - Album image setImageLoading(false)} - sizes="(max-width: 768px) 50vw, 176px" - /> - {imageLoading && ( -
    - )} -
    - {renderDefaultTag()} - - {/* 标签 */} - {renderTag()} - - {/* 付费内容遮罩 */} - {renderOverlay()} - - {/* 底部操作区 */} -
    e.stopPropagation()} - > - {/* 点赞按钮 */} - {(item.lockStatus !== LockStatus.Lock || isOwner) && ( -
    - - {item.likedStatus === LikedStatus.Liked ? ( - - ) : ( - - )} - - - {formatNumberToKMB(item.likedCount ?? 0)} - -
    - )} - - -
    -
    -
    - ) -} - -export default AlbumItem diff --git a/src/app/(main)/user/[userId]/components/AlbumItemAction.tsx b/src/app/(main)/user/[userId]/components/AlbumItemAction.tsx deleted file mode 100644 index 08e1a4f..0000000 --- a/src/app/(main)/user/[userId]/components/AlbumItemAction.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import { IconButton } from '@/components/ui/button' -import { useAIUser } from '../context/aiUser' -import { IAlbumItem } from '@/services/user/types' -import AlbumDeleteAlert from '@/components/features/album-delete-alert' - -const AlbumItemAction = ({ data }: { data: IAlbumItem }) => { - const { isOwner, userId } = useAIUser() - - if (!isOwner) { - return null - } - - return ( - - - - - - - - - e.preventDefault()}> - - Delete - - - - - ) -} - -export default AlbumItemAction diff --git a/src/app/(main)/user/[userId]/components/AlbumList.tsx b/src/app/(main)/user/[userId]/components/AlbumList.tsx deleted file mode 100644 index 1f166f1..0000000 --- a/src/app/(main)/user/[userId]/components/AlbumList.tsx +++ /dev/null @@ -1,233 +0,0 @@ -'use client' - -import { useParams, useRouter, usePathname } from 'next/navigation' -import { - useGetAIUserAlbumInfinite, - useLikeAlbumImage, - useUnlockAlbumImage, - useUnlockImage, -} from '@/hooks/aiUser' -import { IAlbumItem, LikedStatus, LockStatus } from '@/services/user/types' -import Empty from '@/components/ui/empty' -import { toast } from 'sonner' -import AlbumItem from './AlbumItem' -import { InfiniteScrollList } from '@/components/ui/infinite-scroll-list' -import { ImageViewer, ImageViewerPaginationContent } from '@/components/ui/image-viewer' -import { useImageViewer } from '@/hooks/useImageViewer' -import { useMemo, useRef, useState } from 'react' -import { useAIUser } from '../context/aiUser' -import AlbumImageViewerAction from './AlbumImageViewerAction' -import Image from 'next/image' -import { formatFromCents } from '@/utils/number' -import { useToken } from '@/hooks/auth' - -// 专门的相册骨架屏组件 -const AlbumSkeleton = () => ( -
    -
    -
    -
    -
    -) - -const AlbumList = () => { - const { userId } = useParams() - const router = useRouter() - const pathname = usePathname() - const { isLogin } = useToken() - const pageSize = 20 - const unlockingAlbumIdsRef = useRef>(new Set()) - - const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, isError, refetch } = - useGetAIUserAlbumInfinite(Number(userId), pageSize) - const likeMutation = useLikeAlbumImage() - const unlockMutation = useUnlockImage() - const { isOwner } = useAIUser() - const [tempList, setTempList] = useState([]) - - // 图片查看器 - const { - isOpen: isViewerOpen, - currentIndex: viewerIndex, - openViewer, - closeViewer, - handleIndexChange, - } = useImageViewer() - - // 展平所有页面的数据 - const albumItems = useMemo(() => { - if (!data?.pages) return [] - return data.pages.flatMap((page) => page.datas || []) - }, [data?.pages]) - - const handleLike = (albumId: number, isLiked: boolean) => { - likeMutation.mutate({ - albumId, - likedStatus: isLiked ? LikedStatus.Canceled : LikedStatus.Liked, - aiId: Number(userId), - }) - } - - const handleUnlock = async (imageId: number) => { - await unlockMutation.mutateAsync( - { aiId: Number(userId), albumId: imageId }, - { - onSuccess: () => { - toast.success('Unlocked successfully!') - }, - onError: (error) => {}, - } - ) - } - - const handleImageClick = (item: IAlbumItem, index: number) => { - // 检查是否登录,如果未登录则跳转到登录页面 - if (!isLogin) { - const loginUrl = `/login?redirect=${encodeURIComponent(pathname)}` - router.push(loginUrl) - return - } - - // 获取所有图片URL - const imageUrls = albumItems.map((albumItem) => albumItem.imgUrl || albumItem.img3) - setTempList(albumItems) - // 打开图片查看器 - openViewer(imageUrls, index) - } - - const viewerImages: IAlbumItem[] = useMemo(() => { - return tempList.map((item) => { - const { albumId } = item - const data = albumItems.find((item) => item.albumId === albumId) || {} - return data as IAlbumItem - }) - }, [tempList, albumItems]) - - return ( - <> - - items={albumItems} - renderItem={(item, index) => ( - handleImageClick(item, index)} - /> - )} - getItemKey={(item) => item.albumId} - hasNextPage={!!hasNextPage} - isLoading={isLoading || isFetchingNextPage} - fetchNextPage={fetchNextPage} - columns={{ - default: 2, - sm: 3, - md: 4, - lg: 4, - xl: 5, - }} - gap={4} - LoadingSkeleton={AlbumSkeleton} - EmptyComponent={() => ( -
    - -
    - )} - hasError={isError} - onRetry={refetch} - threshold={300} - enabled={true} - /> - - {/* 图片查看器 */} - albumItem.imgUrl || albumItem.img3)} - currentIndex={viewerIndex} - open={isViewerOpen} - onClose={closeViewer} - onIndexChange={handleIndexChange} - showChooseButton={false} - ActionComponent={() => { - return ( - { - if (nextIndex === null) { - // 删除后没有图片了 - closeViewer() - return - } - // 调整到新的索引,避免越界 - handleIndexChange(nextIndex) - }} - /> - ) - }} - OverlayComponent={() => { - const findItem = albumItems.find((item, index) => index === viewerIndex) - const { unlockPrice, lockStatus } = findItem || {} - if (isOwner || !lockStatus || lockStatus === LockStatus.Unlock) { - return null - } - return ( -
    - -
    - diamond -
    {`${formatFromCents(unlockPrice || 0)} to unlock`}
    -
    -
    - ) - }} - PaginationComponent={() => { - const currentIndex = viewerIndex + 1 - const findItem = tempList.find((item, index) => index === viewerIndex) - const { albumId } = findItem || {} - const { lockStatus } = albumItems.find((item) => item.albumId === albumId) || {} - - if (isOwner) { - if (lockStatus) { - return ( - - - {`${currentIndex}/${albumItems.length}`} - - ) - } - return ( - - {`${currentIndex}/${albumItems.length}`} - - ) - } - if (lockStatus === LockStatus.Lock) { - return ( - - - {`${currentIndex}/${albumItems.length}`} - - ) - } - if (lockStatus === LockStatus.Unlock) { - return ( - - - {`${currentIndex}/${albumItems.length}`} - - ) - } - return ( - - {`${currentIndex}/${albumItems.length}`} - - ) - }} - /> - - ) -} - -export default AlbumList diff --git a/src/app/(main)/user/[userId]/components/GiftGrid.tsx b/src/app/(main)/user/[userId]/components/GiftGrid.tsx deleted file mode 100644 index f832f43..0000000 --- a/src/app/(main)/user/[userId]/components/GiftGrid.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client' -import Empty from '@/components/ui/empty' -import { useGetAIUserGifts } from '@/hooks/aiUser' -import { formatNumberToKMB } from '@/lib/utils' -import Image from 'next/image' - -export function GiftGrid({ userId }: { userId: string }) { - const { data } = useGetAIUserGifts({ aiId: Number(userId), page: { pn: 1, ps: 100 } }) - const { datas } = data || {} - - if (datas && !datas.length) { - return ( -
    -

    - Gifts -

    - -
    - -
    -
    - ) - } - - return ( -
    -

    - Gifts -

    - -
    - {datas?.map((gift) => ( -
    -
    -
    - {gift.name -
    -
    -
    - {gift.name} X{formatNumberToKMB(gift.getNum ?? 0)} -
    -
    - ))} -
    -
    - ) -} diff --git a/src/app/(main)/user/[userId]/components/TabNavigation.tsx b/src/app/(main)/user/[userId]/components/TabNavigation.tsx deleted file mode 100644 index 1bcec2a..0000000 --- a/src/app/(main)/user/[userId]/components/TabNavigation.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client' - -import { useState } from 'react' -import { cn } from '@/lib/utils' -import { UserTab } from '../types' - -interface TabNavigationProps { - activeTab?: UserTab - onTabChange?: (tab: UserTab) => void -} - -export function TabNavigation({ activeTab = UserTab.About, onTabChange }: TabNavigationProps) { - const [currentTab, setCurrentTab] = useState(activeTab) - - const handleTabClick = (tab: UserTab) => { - setCurrentTab(tab) - onTabChange?.(tab) - } - - return ( -
    - {/* About 选项卡 */} -
    - -
    - - {/* Album 选项卡 */} -
    - -
    -
    - ) -} diff --git a/src/app/(main)/user/[userId]/components/UserActionDropdown.tsx b/src/app/(main)/user/[userId]/components/UserActionDropdown.tsx deleted file mode 100644 index ef29714..0000000 --- a/src/app/(main)/user/[userId]/components/UserActionDropdown.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog' -import { IconButton } from '@/components/ui/button' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import { useDeleteCharacter } from '@/hooks/aiUser' -import { useState } from 'react' -import { useAIUser } from '../context/aiUser' -import { useParams, useRouter } from 'next/navigation' -import { useNimChat, useNimConversation, useNimMsgContext } from '@/context/NimChat/useNimChat' - -const UserActionDropdown = () => { - const router = useRouter() - const [isDeleteCharacterDialogOpen, setIsDeleteCharacterDialogOpen] = useState(false) - const { userId, isOwner } = useAIUser() - const { removeConversationById } = useNimConversation() - const { clearHistoryMessage } = useNimMsgContext() - const { nim } = useNimChat() - - const { mutate: deleteCharacter, isPending: isDeleteCharacterLoading } = useDeleteCharacter({ - aiId: Number(userId), - }) - - const handleDeleteCharacter = () => { - setIsDeleteCharacterDialogOpen(true) - } - - const handleDeleteCharacterConfirm = async () => { - const conversationId = await nim.V2NIMConversationIdUtil.p2pConversationId( - `${userId}@r@t` as string - ) - await removeConversationById(conversationId) - await clearHistoryMessage(conversationId) - await deleteCharacter({ aiId: Number(userId) }) - router.replace('/profile') - } - - if (!isOwner) return null - - return ( - <> -
    -
    -
    -
    - - - - - - - - Delete Character - - - -
    -
    -
    -
    - - - - - Delete Character - - - Once deleted, the character cannot be restored. To ensure a good user experience, users - who have previously chatted with or made purchases for this character will still be able - to interact with them. - - - Cancel - - Delete - - - - - - ) -} - -export default UserActionDropdown diff --git a/src/app/(main)/user/[userId]/components/UserBackground.tsx b/src/app/(main)/user/[userId]/components/UserBackground.tsx deleted file mode 100644 index c9feea2..0000000 --- a/src/app/(main)/user/[userId]/components/UserBackground.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useGetAIUserBaseInfo } from '@/hooks/aiUser' -import { cn, loadImageAsync } from '@/lib/utils' -import { useParams } from 'next/navigation' -import { useEffect, useRef, useState } from 'react' - -const getDominantColor = (data: Uint8ClampedArray) => { - let red = 0, - green = 0, - blue = 0 - const length = data.length - for (let i = 0; i < length; i += 4) { - red += data[i] - green += data[i + 1] - blue += data[i + 2] - } - // 计算像素点数 - const pixelCount = length / 4 - red = Math.round(red / pixelCount) - green = Math.round(green / pixelCount) - blue = Math.round(blue / pixelCount) - return `${red}, ${green}, ${blue}` -} - -const getImageRightDominantColor = (ctx: any, image: any) => { - const imageData = ctx.getImageData(image.width - 1, 0, 1, image.height) - return getDominantColor(imageData.data) -} -const getImageLeftDominantColor = (ctx: any, image: any) => { - const imageData = ctx.getImageData(0, 0, 1, image.height) - return getDominantColor(imageData.data) -} - -const UserBackground = () => { - const { userId } = useParams() - const { data } = useGetAIUserBaseInfo({ aiId: Number(userId) }) - const [colors, setColors] = useState({ - leftColor: '#37363b', - rightColor: '#313133', - }) - const [isLoading, setIsLoading] = useState(true) - const canvasRef = useRef(null) - - // 获取图片两边的颜色 - const getImageColors = async (url: string) => { - const image: any = await loadImageAsync(url) - const canvas: any = canvasRef.current - canvas.width = image.width - canvas.height = image.height - const ctx = canvas.getContext('2d') - ctx.drawImage(image, 0, 0) - const rightColor = getImageRightDominantColor(ctx, image) - const leftColor = getImageLeftDominantColor(ctx, image) - return { - leftColor, - rightColor, - } - } - - useEffect(() => { - const init = async () => { - if (data?.homeImageUrl) { - setIsLoading(true) - try { - const { leftColor, rightColor } = await getImageColors(data.homeImageUrl) - setColors({ - leftColor, - rightColor, - }) - } catch (error) { - console.error('获取图片颜色失败:', error) - } finally { - setIsLoading(false) - } - } else { - setIsLoading(false) - } - } - init() - }, [data?.homeImageUrl]) - - if (!data) { - return null - } - - return ( - <> - {/* 背景头部区域 */} -
    - {/* 加载状态 - 显示默认背景 */} - {isLoading && ( - <> -
    -
    - - )} - - {/* 加载完成后的背景 */} - {!isLoading && ( - <> -
    -
    - - {/* 背景图片 */} -
    - - {/* 渐变遮罩 */} -
    - {/*
    -
    */} -
    - - )} - - {/* 底部渐变遮罩 */} -
    - -
    - - ) -} - -export default UserBackground diff --git a/src/app/(main)/user/[userId]/components/UserCard.tsx b/src/app/(main)/user/[userId]/components/UserCard.tsx deleted file mode 100644 index da8b973..0000000 --- a/src/app/(main)/user/[userId]/components/UserCard.tsx +++ /dev/null @@ -1,223 +0,0 @@ -'use client' - -import { Button, IconButton } from '@/components/ui/button' -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' -import { useGetAIUserBaseInfo, useGetAIUserStat } from '@/hooks/aiUser' -import { useParams, useRouter } from 'next/navigation' -import { formatNumberToKMB, getAge } from '@/lib/utils' -import { Tag } from '@/components/ui/tag' -import Image from 'next/image' -import { useAIUser } from '../context/aiUser' -import { useEditFormStorage, useEditAI, useEditAIAvatar } from '@/hooks/create' -import { useNimChat, useNimConversation } from '@/context/NimChat/useNimChat' -import { useCurrentUser, useToken } from '@/hooks/auth' -import { AvatarCropModal } from '@/components/ui/avatar-crop-modal' -import { useState, useRef } from 'react' -import { BizTypeEnum } from '@/services/common' -import { AIPermission, CreateOrEditAiRequest } from '@/services/create' -import UserShare from './UserShare' -import UserLikeButton from './UserLikeButton' -import Decimal from 'decimal.js' - -const genderMap = { - 0: '/icons/male.svg', - 1: '/icons/female.svg', - 2: '/icons/gender-neutral.svg', -} - -export function UserCard() { - const { userId } = useParams() - const { data } = useGetAIUserBaseInfo({ aiId: Number(userId) }) - const { isOwner } = useAIUser() - const router = useRouter() - const { insertConversationActive } = useNimConversation() - - const { birthday, characterName, headImg, nickname, sex, tagName, homeImageUrl } = data || {} - const { data: statData } = useGetAIUserStat({ aiId: Number(userId) }) - const { chatNum, coinNum, conversationNum, likedNum } = statData || {} - const { clearFormData } = useEditFormStorage() - const { isNimLoggedIn } = useNimChat() - const { getLoginStatus } = useToken() - - // AI用户编辑hook - const { mutateAsync: editAIAvatar, isPending: isEditingAIAvatar } = useEditAIAvatar() - - // 头像裁剪相关状态 - const [showCropModal, setShowCropModal] = useState(false) - const fileInputRef = useRef(null) - - const statList = [ - { - label: 'Likes', - value: formatNumberToKMB(likedNum || 0), - }, - { - label: 'Chats', - value: formatNumberToKMB(chatNum || 0), - }, - { - label: 'Users', - value: formatNumberToKMB(conversationNum || 0), - }, - isOwner && { - label: 'CrushCoin', - value: formatNumberToKMB(new Decimal(coinNum || 0).div(100).toNumber()), - }, - ].filter(Boolean) as { label: string; value: string | number }[] - - const handleEdit = () => { - clearFormData() - router.push(`/edit/${userId}/type`) - } - - const handleChat = () => { - if (!getLoginStatus()) { - router.push(`/chat/${userId}`) - return - } - - if (!isNimLoggedIn) { - return - } - // insertConversationActive({ - // receiverId: `${userId}@r@t` as string, - // }) - router.push(`/chat/${userId}`) - } - - // 处理头像点击 - const handleAvatarClick = () => { - if (!isOwner) return - setShowCropModal(true) - } - - // 处理裁剪完成 - const handleCropComplete = async (croppedImageUrl: string) => { - try { - // 更新AI用户头像 - 只更新头像,其他信息保持原样 - await editAIAvatar({ - aiId: Number(userId), - userHead: croppedImageUrl, - }) - } catch (error) { - // 可以添加错误提示 - } finally { - setShowCropModal(false) - // 清空文件输入 - if (fileInputRef.current) { - fileInputRef.current.value = '' - } - } - } - - // 处理裁剪取消 - const handleCropCancel = () => { - setShowCropModal(false) - // 清空文件输入 - if (fileInputRef.current) { - fileInputRef.current.value = '' - } - } - - const renderChatButton = () => { - if (isOwner) { - return ( - - ) - } - - return ( - - ) - } - - if (!data) { - return null - } - - return ( -
    - {/* 背景卡片 */} -
    - - {/* 内容 */} -
    - {/* 头像 */} -
    - - - - {nickname?.slice(0, 1)} - - -
    - - {/* 用户信息 */} -
    - {/* 用户名 */} -

    {nickname}

    - - {/* 标签 */} -
    - {/* 年龄和性别标签 */} - - Gender -
    {getAge(birthday ?? 0)}
    -
    - {characterName} - {tagName} -
    -
    - - {/* 统计数据 */} -
    - {statList.map((item, index) => ( -
    -
    {item.value}
    -
    {item.label}
    -
    - ))} -
    - - {/* 操作按钮 */} -
    - {/* 分享按钮 */} - - - {/* 聊天 */} - {isOwner && ( - - - - )} - - - - {renderChatButton()} -
    -
    - - {/* 头像裁剪弹窗 */} - {homeImageUrl && ( - setShowCropModal(false)} - image={homeImageUrl} - onConfirm={handleCropComplete} - onCancel={handleCropCancel} - bizType={BizTypeEnum.Album} - /> - )} -
    - ) -} diff --git a/src/app/(main)/user/[userId]/components/UserLikeButton.tsx b/src/app/(main)/user/[userId]/components/UserLikeButton.tsx deleted file mode 100644 index a7a4b14..0000000 --- a/src/app/(main)/user/[userId]/components/UserLikeButton.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client' -import { IconButton } from '@/components/ui/button' -import { useGetAIUserBaseInfo } from '@/hooks/aiUser' -import { useDoAiUserLiked } from '@/hooks/useCommon' -import { aiUserKeys, imKeys } from '@/lib/query-keys' -import { useQueryClient } from '@tanstack/react-query' -import { useParams, useRouter } from 'next/navigation' -import { useToken } from '@/hooks/auth' - -const UserLikeButton = () => { - const { userId } = useParams() - const router = useRouter() - const { isLogin } = useToken() - const { data } = useGetAIUserBaseInfo({ aiId: Number(userId) }) - const { mutateAsync: doAiUserLiked } = useDoAiUserLiked() - const { liked } = data || {} - const queryClient = useQueryClient() - - const handleLike = () => { - // 检查是否登录,如果未登录则跳转到登录页面 - if (!isLogin) { - const currentPath = `/@${userId}` - router.push(`/login?redirect=${encodeURIComponent(currentPath)}`) - return - } - - doAiUserLiked({ aiId: Number(userId), likedStatus: liked ? 'CANCELED' : 'LIKED' }) - queryClient.setQueryData(aiUserKeys.baseInfo({ aiId: Number(userId) }), (oldData: any) => { - return { - ...oldData, - liked: !liked, - } - }) - queryClient.setQueryData(imKeys.imUserInfo(Number(userId)), (oldData: any) => { - return { - ...oldData, - liked: !liked, - } - }) - queryClient.setQueryData(aiUserKeys.stat({ aiId: Number(userId) }), (oldData: any) => { - return { - ...oldData, - likedNum: !liked ? oldData.likedNum + 1 : oldData.likedNum - 1, - } - }) - } - - return ( - - {!liked ? ( - - ) : ( - - )} - - ) -} - -export default UserLikeButton diff --git a/src/app/(main)/user/[userId]/components/UserProfileTabs.tsx b/src/app/(main)/user/[userId]/components/UserProfileTabs.tsx deleted file mode 100644 index 25c2780..0000000 --- a/src/app/(main)/user/[userId]/components/UserProfileTabs.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { TabsList, TabsTrigger } from '@/components/ui/tabs' -import { UserTab } from '../types' -import { useAIUser } from '../context/aiUser' -import { Button } from '@/components/ui/button' -import { useRouter } from 'next/navigation' - -const UserProfileTabs = ({ currentTab }: { currentTab: UserTab }) => { - const { isOwner, userId } = useAIUser() - const router = useRouter() - - const handleCreatePhoto = () => { - router.push(`/generate/image-2-image?id=${userId}`) - } - - return ( -
    - {/* Tab 列表 */} - - - - About - -
    - - - - - Album - -
    - - - - {isOwner && currentTab === UserTab.Album && ( - - )} -
    - ) -} - -export default UserProfileTabs diff --git a/src/app/(main)/user/[userId]/components/UserShare.tsx b/src/app/(main)/user/[userId]/components/UserShare.tsx deleted file mode 100644 index 44fa025..0000000 --- a/src/app/(main)/user/[userId]/components/UserShare.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client' - -import { IconButton } from '@/components/ui/button' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import useShare from '@/hooks/useShare' -import { useParams } from 'next/navigation' - -const UserShare = () => { - const { userId } = useParams() - - const { shareFacebook, shareTwitter } = useShare() - - const handleShareFacebook = () => { - shareFacebook({ - text: 'Come to Crushlevel for chat, Crush, and AI - chat.', - shareUrl: `${process.env.NEXT_PUBLIC_APP_URL}/@${userId}`, - }) - } - const handleShareTwitter = () => { - shareTwitter({ - text: 'Come to Crushlevel for chat, Crush, and AI - chat.', - shareUrl: `${process.env.NEXT_PUBLIC_APP_URL}/@${userId}`, - }) - } - - return ( - - - - - - - - - - Share to Facebook - - - - Share to X - - - - ) -} - -export default UserShare diff --git a/src/app/(main)/user/[userId]/context/aiUser/index.tsx b/src/app/(main)/user/[userId]/context/aiUser/index.tsx deleted file mode 100644 index da3387c..0000000 --- a/src/app/(main)/user/[userId]/context/aiUser/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import { AiUserBaseOutput } from '@/services/user' -import { createContext } from 'react' -import { useGetAIUserBaseInfo } from '@/hooks/aiUser' -import { useParams } from 'next/navigation' -import { useCurrentUser } from '@/hooks/auth' -import Empty from '@/components/ui/empty' - -export * from './useAIUser' - -const AIUserContext = createContext<{ - user: AiUserBaseOutput | undefined - isOwner: boolean - userId: number | undefined -}>({ - user: undefined, - isOwner: false, - userId: undefined, -}) - -export const AIUserProvider = ({ children }: { children: React.ReactNode }) => { - const { userId } = useParams() - - const { data, error } = useGetAIUserBaseInfo({ aiId: Number(userId) }) - const { data: currentUser } = useCurrentUser() - const isOwner = currentUser?.userId === data?.userId - - if (error) { - return ( -
    - -
    - ) - } - - return ( - - {children} - - ) -} - -export default AIUserContext diff --git a/src/app/(main)/user/[userId]/context/aiUser/useAIUser.ts b/src/app/(main)/user/[userId]/context/aiUser/useAIUser.ts deleted file mode 100644 index ad982c5..0000000 --- a/src/app/(main)/user/[userId]/context/aiUser/useAIUser.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useContext } from 'react' -import AIUserContext from '.' - -export const useAIUser = () => { - const context = useContext(AIUserContext) - if (!context) { - throw new Error('useAIUser must be used within a AIUserProvider') - } - return context -} diff --git a/src/app/(main)/user/[userId]/not-found.tsx b/src/app/(main)/user/[userId]/not-found.tsx deleted file mode 100644 index 2ea4904..0000000 --- a/src/app/(main)/user/[userId]/not-found.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import Empty from '@/components/ui/empty' - -export default async function NotFound() { - return ( -
    - -
    - ) -} diff --git a/src/app/(main)/user/[userId]/page.tsx b/src/app/(main)/user/[userId]/page.tsx deleted file mode 100644 index 33a3e79..0000000 --- a/src/app/(main)/user/[userId]/page.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import UserPage from './user-page' -import { HydrationBoundary } from '@tanstack/react-query' -import { dehydrate } from '@tanstack/react-query' -import { QueryClient } from '@tanstack/react-query' -import { aiUserKeys } from '@/lib/query-keys' -import { userService } from '@/services/user' -import { ApiError } from '@/types/api' -import { notFound, redirect } from 'next/navigation' -import { headers } from 'next/headers' -import { isMobileDevice } from '@/utils/device' - -export const generateMetadata = async ({ params }: { params: Promise<{ userId: string }> }) => { - const { userId } = await params - try { - const resp = await userService.getSEOUserBaseInfo({ aiId: Number(userId) }) - const { nickname, homeImageUrl } = resp || {} - - return { - title: `${nickname} - Crushlevel AI`, - description: `${nickname}`, - openGraph: { - title: `Crushlevel AI`, - description: `Grow your love story with CrushLevel AI—From ‘Hi’ to ‘I Do', sparked by every chat`, - images: { - url: homeImageUrl, - width: 720, - height: 1280, - alt: `${nickname} - Crushlevel AI`, - }, - siteName: 'Crushlevel AI', - type: 'website', - url: `https://www.crushlevel.com/@${userId}`, - }, - twitter: { - title: `Crushlevel AI`, - description: `Grow your love story with CrushLevel AI—From ‘Hi’ to ‘I Do', sparked by every chat`, - images: { - url: homeImageUrl, - width: 720, - height: 1280, - alt: `${nickname} - Crushlevel AI`, - }, - siteName: 'Crushlevel AI', - type: 'website', - url: `https://www.crushlevel.com/@${userId}`, - }, - } - } catch (error) { - return { - title: `Crushlevel AI`, - description: `Grow your love story with CrushLevel AI—From ‘Hi’ to ‘I Do', sparked by every chat`, - openGraph: { - title: `Crushlevel AI`, - description: `Grow your love story with CrushLevel AI—From ‘Hi’ to ‘I Do', sparked by every chat`, - }, - } - } -} - -const Page = async ({ params }: { params: Promise<{ userId: string }> }) => { - const { userId } = await params - - // 检测移动端设备并重定向到分享页面 - const headersList = await headers() - const userAgent = headersList.get('user-agent') || '' - - if (isMobileDevice(userAgent)) { - redirect(`/share/${userId}`) - } - - const queryClient = new QueryClient() - - // try { - // // 使用 fetchQuery 替代 prefetchQuery,因为 fetchQuery 会抛出错误 - // await queryClient.fetchQuery({ - // queryKey: aiUserKeys.tempBaseInfo({ aiId: Number(userId) }), - // queryFn: () => userService.getAIUserBaseInfo({ aiId: Number(userId) }), - // }); - // } catch (error) { - // if (error instanceof ApiError && error.errorCode === "10010012") { - // notFound(); - // } - // } - - return ( - - - - ) -} - -export default Page diff --git a/src/app/(main)/user/[userId]/types.ts b/src/app/(main)/user/[userId]/types.ts deleted file mode 100644 index 7e1f1e7..0000000 --- a/src/app/(main)/user/[userId]/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum UserTab { - About = 'about', - Album = 'album', -} diff --git a/src/app/(main)/user/[userId]/user-page.tsx b/src/app/(main)/user/[userId]/user-page.tsx deleted file mode 100644 index 262a6fb..0000000 --- a/src/app/(main)/user/[userId]/user-page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -'use client' - -import { useParams, useSearchParams, useRouter } from 'next/navigation' -import { UserCard } from './components/UserCard' -import { AboutSection } from './components/AboutSection' -import { GiftGrid } from './components/GiftGrid' -import UserBackground from './components/UserBackground' -import AlbumList from './components/AlbumList' -import { UserTab } from './types' -import { useGetAIUserBaseInfo } from '@/hooks/aiUser' -import { Tabs, TabsContent } from '@/components/ui/tabs' -import { AIUserProvider } from './context/aiUser' -import UserActionDropdown from './components/UserActionDropdown' -import UserProfileTabs from './components/UserProfileTabs' -import { useCallback } from 'react' - -const UserPage = () => { - const { userId } = useParams() - const searchParams = useSearchParams() - const router = useRouter() - const { data } = useGetAIUserBaseInfo({ aiId: Number(userId) }) - const { introduction } = data || {} - - // 从 URL 参数中获取当前 tab,如果没有或无效则默认为 About - const tabParam = searchParams.get('tab') - const currentTab = ( - Object.values(UserTab).includes(tabParam as UserTab) ? tabParam : UserTab.About - ) as UserTab - - // 处理 tab 切换 - const handleTabChange = useCallback( - (newTab: string) => { - const params = new URLSearchParams(searchParams.toString()) - params.set('tab', newTab) - router.push(`/@${userId}?${params.toString()}`, { scroll: false }) - }, - [searchParams, router, userId] - ) - - return ( - - {data && ( -
    - - - {/* 主要内容 */} -
    - - - {/* 左侧用户卡片 */} -
    - -
    - - {/* 右侧内容区域 */} -
    - - - - {/* Tab 内容 */} - -
    - - -
    -
    - - - - -
    -
    -
    -
    - )} -
    - ) -} - -export default UserPage diff --git a/src/app/(main)/vip/components/SubscribeVipDrawer/index.tsx b/src/app/(main)/vip/components/SubscribeVipDrawer/index.tsx index dbf3c8b..8e625fd 100644 --- a/src/app/(main)/vip/components/SubscribeVipDrawer/index.tsx +++ b/src/app/(main)/vip/components/SubscribeVipDrawer/index.tsx @@ -1,17 +1,16 @@ -'use client' +'use client'; -import { Button, IconButton } from '@/components/ui/button' -import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer' -import { useState, useMemo } from 'react' -import SubscribeProducts from './SubscribeProducs' -import SubscribeProductsSkeleton from './SubscribeProductsSkeleton' -import CarouselBanner from './CarouselBanner' -import { useGetSubProductCheckoutLink, useGetSubProductList } from '@/hooks/useWallet' -import { SubProductListOutput, Period, VipType } from '@/services/wallet/types' -import { isVipDrawerOpenAtom } from '@/atoms/im' -import { useAtom } from 'jotai' -import QueryString from 'qs' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { Button, IconButton } from '@/components/ui/button'; +import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; +import { useState, useMemo } from 'react'; +import SubscribeProducts from './SubscribeProducs'; +import SubscribeProductsSkeleton from './SubscribeProductsSkeleton'; +import CarouselBanner from './CarouselBanner'; +import { useGetSubProductCheckoutLink, useGetSubProductList } from '@/hooks/useWallet'; +import { SubProductListOutput, Period, VipType } from '@/services/wallet/types'; +import { useAtom } from 'jotai'; +import QueryString from 'qs'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; function SubscribeBackground() { return ( @@ -24,43 +23,43 @@ function SubscribeBackground() { backgroundRepeat: 'no-repeat', }} >
    - ) + ); } // 数据转换函数 const convertProductListToPricingPlans = (productList: SubProductListOutput[]) => { - if (!productList) return [] + if (!productList) return []; return productList.map((product, index) => { // 根据订阅时长获取标题 const getTitleByPeriod = (period?: Period) => { switch (period) { case Period.SubMonth: - return '1 Month' + return '1 Month'; case Period.SubSeason: - return '3 Months' + return '3 Months'; case Period.SubYear: - return '12 Months' + return '12 Months'; default: - return 'Unknown' + return 'Unknown'; } - } + }; // 计算月单价(如果不是月订阅) const getSubtitle = (period?: Period, payAmount?: number) => { - if (!payAmount) return '' + if (!payAmount) return ''; - const monthlyPrice = payAmount / 100 // 转换为元 + const monthlyPrice = payAmount / 100; // 转换为元 switch (period) { case Period.SubSeason: - return `$${(monthlyPrice / 3).toFixed(2)}/Month` + return `$${(monthlyPrice / 3).toFixed(2)}/Month`; case Period.SubYear: - return `$${(monthlyPrice / 12).toFixed(2)}/Month` + return `$${(monthlyPrice / 12).toFixed(2)}/Month`; default: - return '' + return ''; } - } + }; return { id: index + 1, // 使用索引+1作为ID @@ -70,69 +69,73 @@ const convertProductListToPricingPlans = (productList: SubProductListOutput[]) = discount: product.discount || '', isSelected: false, // 初始都不选中 productId: product.productId, // 保留原始产品ID用于后续操作 - } - }) -} + }; + }); +}; const SubscribeVipDrawer = () => { - const [selectedPlan, setSelectedPlan] = useState(0) // 默认不选择任何计划 - const [isVipDrawerOpen, setIsVipDrawerOpen] = useAtom(isVipDrawerOpenAtom) + const [selectedPlan, setSelectedPlan] = useState(0); // 默认不选择任何计划 + // const [isVipDrawerOpen, setIsVipDrawerOpen] = useAtom(isVipDrawerOpenAtom) + const [isVipDrawerOpen, setIsVipDrawerOpen] = useState({ + open: false, + vipType: undefined, + }); - const { data: productList, isLoading } = useGetSubProductList() - const { mutateAsync, isPending } = useGetSubProductCheckoutLink() - const pathname = usePathname() - const searchParams = useSearchParams() + const { data: productList, isLoading } = useGetSubProductList(); + const { mutateAsync, isPending } = useGetSubProductCheckoutLink(); + const pathname = usePathname(); + const searchParams = useSearchParams(); // 转换产品数据为组件需要的格式 const pricingPlans = useMemo(() => { - const plans = convertProductListToPricingPlans(productList || []) + const plans = convertProductListToPricingPlans(productList || []); // 设置选中状态 return plans.map((plan) => ({ ...plan, isSelected: plan.id === selectedPlan, - })) - }, [productList, selectedPlan]) + })); + }, [productList, selectedPlan]); // 设置默认选中第二个计划(3个月) useMemo(() => { if (pricingPlans.length > 0 && selectedPlan === 0) { - const seasonPlan = pricingPlans.find((plan) => plan.title === '3 Month') + const seasonPlan = pricingPlans.find((plan) => plan.title === '3 Month'); if (seasonPlan) { - setSelectedPlan(seasonPlan.id) + setSelectedPlan(seasonPlan.id); } else { - setSelectedPlan(pricingPlans[0].id) // 如果没有3个月计划,选择第一个 + setSelectedPlan(pricingPlans[0].id); // 如果没有3个月计划,选择第一个 } } - }, [pricingPlans, selectedPlan]) + }, [pricingPlans, selectedPlan]); const handleClose = () => { - setIsVipDrawerOpen({ open: false, vipType: undefined }) - } + setIsVipDrawerOpen({ open: false, vipType: undefined }); + }; const handleSubscribe = async () => { // 获取选中的计划详情 - const selectedPlanData = pricingPlans.find((plan) => plan.id === selectedPlan) + const selectedPlanData = pricingPlans.find((plan) => plan.id === selectedPlan); if (selectedPlanData && selectedPlanData.productId) { // 这里可以调用订阅API,使用selectedPlanData.productId const query = { ...QueryString.parse(searchParams.toString()), back: 1, - } + }; - const baseURL = `${process.env.NEXT_PUBLIC_APP_URL}${pathname}?${QueryString.stringify(query)}` + const baseURL = `${process.env.NEXT_PUBLIC_APP_URL}${pathname}?${QueryString.stringify(query)}`; const response = await mutateAsync({ subProductId: selectedPlanData.productId, returnUrl: baseURL, cancelUrl: baseURL, - }) - const { payUrl } = response || {} + }); + const { payUrl } = response || {}; if (payUrl) { - window.location.href = payUrl + window.location.href = payUrl; } } - } + }; return ( {
    - ) -} + ); +}; -export default SubscribeVipDrawer +export default SubscribeVipDrawer; diff --git a/src/app/(main)/vip/vip-page.tsx b/src/app/(main)/vip/vip-page.tsx index 1242431..f151d3e 100644 --- a/src/app/(main)/vip/vip-page.tsx +++ b/src/app/(main)/vip/vip-page.tsx @@ -1,43 +1,41 @@ -'use client' -import { Button, IconButton } from '@/components/ui/button' -import { useRouter, useSearchParams } from 'next/navigation' -import { useGetMemberDetail } from '@/hooks/useWallet' -import SubscribeText from './components/SubscribeText' -import { useSetAtom } from 'jotai' -import { isVipDrawerOpenAtom } from '@/atoms/im' -import { VipType } from '@/services/wallet' -import { useEffect, useState } from 'react' +'use client'; +import { Button, IconButton } from '@/components/ui/button'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useGetMemberDetail } from '@/hooks/useWallet'; +import SubscribeText from './components/SubscribeText'; +import { VipType } from '@/services/wallet'; +import { useEffect, useState } from 'react'; const VipPage = () => { - const router = useRouter() - const searchParams = useSearchParams() - const back = searchParams.get('back') - const setIsVipDrawerOpen = useSetAtom(isVipDrawerOpenAtom) - const [enableQuery, setEnableQuery] = useState(!back) + const router = useRouter(); + const searchParams = useSearchParams(); + const back = searchParams.get('back'); + const setIsVipDrawerOpen = (P: any) => null; + const [enableQuery, setEnableQuery] = useState(!back); useEffect(() => { if (back) { // 如果有 back 参数,等待 2 秒后再启用查询 const timer = setTimeout(() => { - setEnableQuery(true) - }, 2000) - return () => clearTimeout(timer) + setEnableQuery(true); + }, 2000); + return () => clearTimeout(timer); } - }, [back]) + }, [back]); - const { data: memberDetail, isLoading } = useGetMemberDetail({ enabled: enableQuery }) - const { memberPrivList, userMemberInfo } = memberDetail || {} + const { data: memberDetail, isLoading } = useGetMemberDetail({ enabled: enableQuery }); + const { memberPrivList, userMemberInfo } = memberDetail || {}; const hasSubscribe = - userMemberInfo && userMemberInfo.expTime && new Date(userMemberInfo.expTime) > new Date() + userMemberInfo && userMemberInfo.expTime && new Date(userMemberInfo.expTime) > new Date(); const handleBack = () => { if (back) { - window.history.go(-3) - return + window.history.go(-3); + return; } - router.back() - } + router.back(); + }; return (
    @@ -126,7 +124,7 @@ const VipPage = () => { )}
    - ) -} + ); +}; -export default VipPage +export default VipPage; diff --git a/src/app/api/auth/discord/callback/route.ts b/src/app/api/auth/discord/callback/route.ts index fae3af6..a8e25b0 100644 --- a/src/app/api/auth/discord/callback/route.ts +++ b/src/app/api/auth/discord/callback/route.ts @@ -1,8 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; export async function GET(request: NextRequest) { - console.log('request', request); - const url = 'http://localhost:3000'; + // console.log('request', request); + const url = request.nextUrl.origin; try { const { searchParams } = new URL(request.url); const code = searchParams.get('code'); diff --git a/src/css/iconfont-v2.css b/src/css/iconfont-v2.css index 8f0d8a4..f28c517 100644 --- a/src/css/iconfont-v2.css +++ b/src/css/iconfont-v2.css @@ -1,9 +1,9 @@ @font-face { font-family: 'iconfont-v2'; /* Project id 5076160 - spicyxx.ai */ src: - url('/font-v2/iconfont.woff2?t=1765173841330') format('woff2'), - url('/font-v2/iconfont.woff?t=1765173841330') format('woff'), - url('/font-v2/iconfont.ttf?t=1765173841330') format('truetype'); + 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 { @@ -14,6 +14,21 @@ -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'; diff --git a/src/hooks/useGlobalPrefetchRoutes.ts b/src/hooks/useGlobalPrefetchRoutes.ts new file mode 100644 index 0000000..e2defdd --- /dev/null +++ b/src/hooks/useGlobalPrefetchRoutes.ts @@ -0,0 +1,109 @@ +'use client' + +import { useEffect, useMemo } from 'react' +import { useRouter } from 'next/navigation' +import { useCurrentUser } from '@/hooks/auth' +import { useToken } from '@/hooks/auth' + +const PUBLIC_PREFETCH_TARGETS = ['/'] + +const AUTH_PREFETCH_TARGETS = [ + '/contact', + '/profile', + '/profile/edit', + '/profile/account', + '/vip', + '/wallet', + '/wallet/charge', + '/wallet/charge/result', + '/wallet/transactions', + '/leaderboard', + '/crushcoin', + '/generate/image', + '/generate/image-2-image', + '/generate/image-edit', + '/generate/image-2-background', + '/explore', + '/creator', + '/create/type', + '/create/dialogue', + '/create/character', + '/create/image', +] + +// 受保护路由前缀列表(需要登录才能访问的路由) +const PROTECTED_ROUTE_PREFIXES = [ + '/profile', + '/create', + '/settings', + '/login/fields', + '/chat', + '/contact', + '/vip', + '/wallet', + '/crushcoin', + '/leaderboard', + '/generate', + '/explore', + '/creator', +] + +// 检查路由是否是受保护路由 +const isProtectedRoute = (href: string): boolean => { + return PROTECTED_ROUTE_PREFIXES.some((prefix) => href.startsWith(prefix)) +} + +const DEFAULT_PREFETCH_TARGETS = [...PUBLIC_PREFETCH_TARGETS, ...AUTH_PREFETCH_TARGETS] + +type NullableRoute = string | null | undefined + +const prefetchedRouteCache = new Set() + +export function usePrefetchRoutes( + routes?: NullableRoute[], + options?: { + limit?: number + } +) { + const router = useRouter() + const { isLogin } = useToken() + const normalizedRoutes = useMemo(() => { + if (!routes || routes.length === 0) return [] + return routes.filter(Boolean) as string[] + }, [routes]) + const limit = options?.limit ?? Infinity + + useEffect(() => { + if (!normalizedRoutes.length) return + let count = 0 + for (const href of normalizedRoutes) { + if (!href || prefetchedRouteCache.has(href)) continue + // 如果未登录且是受保护路由,跳过 prefetch,避免缓存重定向响应 + if (!isLogin && isProtectedRoute(href)) continue + prefetchedRouteCache.add(href) + router.prefetch(href) + count += 1 + if (count >= limit) break + } + }, [limit, normalizedRoutes, router, isLogin]) +} + +export function useGlobalPrefetchRoutes(extraRoutes?: NullableRoute[]) { + const { data: user } = useCurrentUser() + const isAuthenticated = !!user + + const protectedTargets = useMemo(() => { + const routes = new Set(AUTH_PREFETCH_TARGETS) + extraRoutes?.forEach((href) => { + if (href && href !== '/') { + routes.add(href) + } + }) + return Array.from(routes) + }, [extraRoutes]) + + usePrefetchRoutes(PUBLIC_PREFETCH_TARGETS) + usePrefetchRoutes(isAuthenticated ? protectedTargets : []) +} + +export const GLOBAL_PREFETCH_ROUTES = DEFAULT_PREFETCH_TARGETS diff --git a/src/hooks/useHome.ts b/src/hooks/useHome.ts new file mode 100644 index 0000000..e8e44e9 --- /dev/null +++ b/src/hooks/useHome.ts @@ -0,0 +1,105 @@ +import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'; +import { homeKeys } from '@/lib/query-keys'; +import { GetMeetListRequest, homeService } from '@/services/home'; + +type PageParam = number | { page: number; exList: number[] }; + +export function useGetMeetList(params: Omit, enabled: boolean = true) { + return useInfiniteQuery({ + queryKey: homeKeys.getMeetList(params), + queryFn: ({ pageParam }: { pageParam: PageParam }) => { + // pageParam 可能是数字(第一页)或对象(后续页面包含 exList) + const page = typeof pageParam === 'number' ? pageParam : pageParam.page; + const exList = + typeof pageParam === 'object' && pageParam !== null ? pageParam.exList : undefined; + + return homeService.getMeetList({ + ...params, + pn: page, + ...(exList && { exList }), + }); + }, + initialPageParam: 1 as PageParam, + getNextPageParam: (lastPage, allPages) => { + // 如果最后一页的数据数量少于每页大小,说明没有更多数据了 + if (lastPage.length < params.ps) { + return undefined; + } + + // 收集所有已获取的 aiId 作为下一页的排除列表 + const exList: number[] = []; + + // 首先添加初始传入的 exList(如果有的话) + if (params.exList && Array.isArray(params.exList)) { + exList.push(...params.exList); + } + + // 然后添加所有已获取页面的 aiId + for (const page of allPages) { + for (const item of page) { + if (item.aiId) { + exList.push(item.aiId); + } + } + } + + return { page: allPages.length + 1, exList } as PageParam; + }, + enabled, // 控制是否启用查询 + }); +} + +export function useGetChatRank() { + return useQuery({ + queryKey: homeKeys.getChatRank(), + queryFn: homeService.getChatRank, + }); +} + +export function useGetHeartbeatRank() { + return useQuery({ + queryKey: homeKeys.getHeartbeatRank(), + queryFn: homeService.getHeartbeatRank, + }); +} + +export function useGetGiftRank() { + return useQuery({ + queryKey: homeKeys.getGiftRank(), + queryFn: homeService.getGiftRank, + }); +} + +export function useGetSevenDaysSignList() { + return useQuery({ + queryKey: homeKeys.getSevenDaysSignList(), + queryFn: homeService.getSevenDaysSignList, + }); +} + +export function useSignIn() { + return useMutation({ + mutationFn: homeService.signIn, + }); +} + +export function useGetExplore() { + return useQuery({ + queryKey: homeKeys.getExplore(), + queryFn: homeService.getExplore, + }); +} + +export function useGetHomeAiCarouselList() { + return useQuery({ + queryKey: homeKeys.getHomeAiCarouselList(), + queryFn: homeService.getHomeAiCarouselList, + }); +} + +export function useGetHomeAggregateRecommend() { + return useQuery({ + queryKey: homeKeys.getHomeAggregateRecommend(), + queryFn: homeService.getHomeAggregateRecommend, + }); +} diff --git a/src/layout/BasicLayout.tsx b/src/layout/BasicLayout.tsx index 42ef571..949e444 100644 --- a/src/layout/BasicLayout.tsx +++ b/src/layout/BasicLayout.tsx @@ -4,10 +4,10 @@ import { usePathname } from 'next/navigation'; import { useEffect, useRef } from 'react'; import Sidebar from './Sidebar'; import Topbar from './Topbar'; -import ChargeDrawer from '../components/features/charge-drawer'; -import SubscribeVipDrawer from '@/app/(main)/vip/components/SubscribeVipDrawer'; +// import ChargeDrawer from '../components/features/charge-drawer'; +// import SubscribeVipDrawer from '@/app/(main)/vip/components/SubscribeVipDrawer'; 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 { useStreamChatStore } from '@/app/(main)/chat/[id]/stream-chat'; @@ -60,9 +60,9 @@ export default function ConditionalLayout({ children }: ConditionalLayoutProps) {response && !response.sm && }
    - - - + {/* */} + {/* */} + {/* */}
    ); } diff --git a/src/layout/Topbar.tsx b/src/layout/Topbar.tsx index 9906beb..e5c45c8 100644 --- a/src/layout/Topbar.tsx +++ b/src/layout/Topbar.tsx @@ -22,7 +22,7 @@ function Topbar() { const response = useMedia(); const searchParamsString = searchParams.toString(); const redirectURL = `${pathname}${searchParamsString ? `?${searchParamsString}` : ''}`; - const loginHref = `/login?redirect=${encodeURIComponent(redirectURL)}`; + // const loginHref = `/login?redirect=${encodeURIComponent(redirectURL)}`; useEffect(() => { function handleScroll(event: Event) { @@ -42,7 +42,7 @@ function Topbar() { useEffect(() => { if (!user) { - router.prefetch(loginHref); + router.prefetch('/login'); } else { router.prefetch('/profile'); if (user.cpUserInfo) { @@ -55,11 +55,9 @@ function Topbar() { if (!response) return null; if (response.sm || items.some((item) => item.path === pathname)) { return ( -
    - - logo - -
    + + + ); } return ( @@ -75,7 +73,7 @@ function Topbar() { const rightDomRender = () => { if (!user) return ( - + ); diff --git a/src/layout/components/Notice.tsx b/src/layout/components/Notice.tsx index 319f609..a9ed8f9 100644 --- a/src/layout/components/Notice.tsx +++ b/src/layout/components/Notice.tsx @@ -17,6 +17,7 @@ const Notice = () => { // 监听路径变化,刷新通知统计 useEffect(() => { + return; if (user) { // 当路径变化时,无效化并重新获取通知统计数据 queryClient.invalidateQueries({ @@ -26,6 +27,7 @@ const Notice = () => { }, [pathname]); useEffect(() => { + return; if (isDrawerOpen && user) { queryClient.invalidateQueries({ queryKey: userKeys.noticeStat(), diff --git a/src/proxy.ts b/src/proxy.ts index 41cc5d6..62ab494 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -3,10 +3,9 @@ import type { NextRequest } from 'next/server'; // 需要认证的路由 const protectedRoutes = [ - // '/profile', - // '/profile/account', - // '/profile/edit', - '/create', + '/profile', + '/profile/account', + '/profile/edit', '/settings', '/login/fields', '/chat', diff --git a/src/services/home/home.service.ts b/src/services/home/home.service.ts new file mode 100644 index 0000000..0e83727 --- /dev/null +++ b/src/services/home/home.service.ts @@ -0,0 +1,59 @@ +import { frogHttp } from '@/lib/http/instances' +import { + AiCarouselListOutput, + AiChatRankOutput, + AiGiftRankOutput, + AiHeartbeatRankOutput, + ExploreInfoOutput, + GetMeetListRequest, + GetMeetListResponse, + HomeRecommendV2Output, + SignInRoundOutput, +} from './types' + +export const homeService = { + // 发现 + getExplore: (): Promise => { + return frogHttp.post('/web/explore/info') + }, + + // 首页分类列表 + getMeetList: (data: GetMeetListRequest): Promise => { + return frogHttp.post('/web/home/classification-list', data) + }, + + // 热聊榜 + getChatRank: (): Promise => { + return frogHttp.post('/web/rank/chat') + }, + + // 心动榜 + getHeartbeatRank: (): Promise => { + return frogHttp.post('/web/rank/heartbeat') + }, + + // 送礼榜 + getGiftRank: (): Promise => { + return frogHttp.post('/web/rank/gift') + }, + + // 七天签到列表 + getSevenDaysSignList: (): Promise => { + return frogHttp.post('/web/si/list') + }, + + // 签到 + signIn: (): Promise => { + return frogHttp.post('/web/si/asi') + }, + + // 首页AI轮播列表 + getHomeAiCarouselList: (): Promise => { + return frogHttp.post('/web/home/ai-carousel-list') + }, + + // 首页聚合推荐 + getHomeAggregateRecommend: (): Promise => { + return frogHttp.post('/web/home/agg-recommend') + }, +} diff --git a/src/services/home/index.ts b/src/services/home/index.ts new file mode 100644 index 0000000..5d0027b --- /dev/null +++ b/src/services/home/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './home.service' diff --git a/src/services/home/types.ts b/src/services/home/types.ts new file mode 100644 index 0000000..71be8ee --- /dev/null +++ b/src/services/home/types.ts @@ -0,0 +1,819 @@ +import { Gender } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/UserServiceInterface' + +/** + * ClassificationListInput + */ +export interface GetMeetListRequest { + /** + * 情感性格code + */ + characterCodeList: string[] + /** + * 需要排除的aiId列表 + */ + exList?: number[] + /** + * 页码 + */ + pn?: number + /** + * 每页大小 + */ + ps: number + /** + * 角色code列表 + */ + roleCodeList?: string[] + sex?: Gender + age?: Age + /** 多选:性别列表 */ + sexList?: Gender[] + /** 多选:年龄列表 */ + ageList?: Age[] + /** + * 标签code列表 + */ + tagCodeList?: string[] +} + +/** + * GetMeetListResponse + */ +export interface GetMeetListResponse { + /** + * AI的id + */ + aiId?: number + /** + * 出生日期 + */ + birthday?: string + /** + * 性格名称 + */ + characterName?: string + /** + * 头像 + */ + headImg?: string + /** + * 主页头图 + */ + homeImageUrl?: string + /** + * 简介 + */ + introduction?: string + /** + * 被喜欢数 + */ + likedNum?: number + /** + * 昵称 + */ + nickname?: string + /** + * 角色名称 + */ + roleName?: string + /** + * 0,男;1,女;2,自定义 + */ + sex?: number + /** + * 标签名称 + */ + tagName?: string + /** + * ai所属用户id + */ + userId?: number +} + +/** + * AiChatRankOutput + */ +export interface AiChatRankOutput { + /** + * AI的id + */ + aiId?: number + /** + * 出生日期 + */ + birthday?: string + /** + * 性格名称 + */ + characterName?: string + /** + * 聊天次数 + */ + chatNum?: number + /** + * 头像 + */ + headImg?: string + /** + * 主页头图 + */ + homeImageUrl?: string + /** + * 简介 + */ + introduction?: string + /** + * 昵称 + */ + nickname?: string + /** + * 排名编号 + */ + rankNo?: number + /** + * 角色名称 + */ + roleName?: string + /** + * 0,男;1,女;2,自定义 + */ + sex?: number + /** + * 标签名称 + */ + tagName?: string +} + +/** + * AiHeartbeatRankOutput + */ +export interface AiHeartbeatRankOutput { + /** + * AI的id + */ + aiId?: number + /** + * 出生日期 + */ + birthday?: string + /** + * 性格名称 + */ + characterName?: string + /** + * 头像 + */ + headImg?: string + /** + * 心动总分值 + */ + heartbeatValTotal?: number + /** + * 主页头图 + */ + homeImageUrl?: string + /** + * 简介 + */ + introduction?: string + /** + * 昵称 + */ + nickname?: string + /** + * 排名编号 + */ + rankNo?: number + /** + * 角色名称 + */ + roleName?: string + /** + * 0,男;1,女;2,自定义 + */ + sex?: number + /** + * 标签名称 + */ + tagName?: string +} + +/** + * AiGiftRankOutput + */ +export interface AiGiftRankOutput { + /** + * AI的id + */ + aiId?: number + /** + * 出生日期 + */ + birthday?: string + /** + * 性格名称 + */ + characterName?: string + /** + * 礼物 + */ + giftCoinNum?: number + /** + * 头像 + */ + headImg?: string + /** + * 主页头图 + */ + homeImageUrl?: string + /** + * 简介 + */ + introduction?: string + /** + * 昵称 + */ + nickname?: string + /** + * 排名编号 + */ + rankNo?: number + /** + * 角色名称 + */ + roleName?: string + /** + * 0,男;1,女;2,自定义 + */ + sex?: number + /** + * 标签名称 + */ + tagName?: string +} + +/** + * SignInRoundOutput + */ +export interface SignInRoundOutput { + /** + * 最大连续签到天数 + */ + continuousDays?: number + /** + * 签到周期基础数据 + */ + list?: SignInListOutput[] +} + +/** + * SignInListOutput + */ +export interface SignInListOutput { + /** + * 得到coin的数量 + */ + coinNum?: number + /** + * PST 天(yyyy-MM-dd) + */ + dayStr?: string + /** + * 是否已签到 + */ + signIn?: boolean +} + +/** + * ExploreInfoOutput + */ +export interface ExploreInfoOutput { + /** + * AI总心动值榜单top3 + */ + aiChatRankTop3List?: AiChatRankOutput[] + /** + * AI总心动值榜单top3 + */ + aiGiftRankTop3List?: AiGiftRankOutput[] + /** + * AI总心动值榜单top3 + */ + aiHeartbeatRankTop3List?: AiHeartbeatRankOutput[] + /** + * 广告列表 + */ + outputList?: AdvertiseOutput[] +} + +/** + * com.sonic.frog.domain.output.AiChatRankOutput + * + * AiChatRankOutput + */ +export interface AiChatRankOutput { + /** + * AI的id + */ + aiId?: number + /** + * 出生日期 + */ + birthday?: string + /** + * 性格名称 + */ + characterName?: string + /** + * 聊天次数 + */ + chatNum?: number + /** + * 头像 + */ + headImg?: string + /** + * 主页头图 + */ + homeImageUrl?: string + /** + * 简介 + */ + introduction?: string + /** + * 昵称 + */ + nickname?: string + /** + * 排名编号 + */ + rankNo?: number + /** + * 角色名称 + */ + roleName?: string + /** + * 0,男;1,女;2,自定义 + */ + sex?: number + /** + * 标签名称 + */ + tagName?: string +} + +/** + * com.sonic.frog.domain.output.AiGiftRankOutput + * + * AiGiftRankOutput + */ +export interface AiGiftRankOutput { + /** + * AI的id + */ + aiId?: number + /** + * 出生日期 + */ + birthday?: string + /** + * 性格名称 + */ + characterName?: string + /** + * 礼物 + */ + giftCoinNum?: number + /** + * 头像 + */ + headImg?: string + /** + * 主页头图 + */ + homeImageUrl?: string + /** + * 简介 + */ + introduction?: string + /** + * 昵称 + */ + nickname?: string + /** + * 排名编号 + */ + rankNo?: number + /** + * 角色名称 + */ + roleName?: string + /** + * 0,男;1,女;2,自定义 + */ + sex?: number + /** + * 标签名称 + */ + tagName?: string +} + +/** + * com.sonic.frog.domain.output.AiHeartbeatRankOutput + * + * AiHeartbeatRankOutput + */ +export interface AiHeartbeatRankOutput { + /** + * AI的id + */ + aiId?: number + /** + * 出生日期 + */ + birthday?: string + /** + * 性格名称 + */ + characterName?: string + /** + * 头像 + */ + headImg?: string + /** + * 心动总分值 + */ + heartbeatValTotal?: number + /** + * 主页头图 + */ + homeImageUrl?: string + /** + * 简介 + */ + introduction?: string + /** + * 昵称 + */ + nickname?: string + /** + * 排名编号 + */ + rankNo?: number + /** + * 角色名称 + */ + roleName?: string + /** + * 0,男;1,女;2,自定义 + */ + sex?: number + /** + * 标签名称 + */ + tagName?: string +} + +/** + * AdvertiseOutput + */ +export interface AdvertiseOutput { + /** + * 使用端点(WEB/ANDROID/IOS) + * endpoint + */ + endpoint?: string + /** + * 扩展字段 + * ext + */ + ext?: string + /** + * 广告配图 + * icon + */ + icon?: string + /** + * 是否弹窗(1.是,0.否) + * is_global + */ + isGlobal?: number + /** + * 跳转连接 + * jump_link + */ + jumpLink?: string + /** + * 广告名称 + * name + */ + name?: string + /** + * 展示结束时间 + * show_end_time + */ + showEndTime?: string + /** + * 展示开始时间 + * show_start_time + */ + showStartTime?: string + /** + * 排序 + * sort + */ + sort?: number +} + +/** + * 年龄:单选 AGE_1(18-24)、AGE_2(25-34)、AGE_3(35-44)、AGE_4(45-54)、AGE_5(>54) + */ +export enum Age { + Age1 = 'AGE_1', + Age2 = 'AGE_2', + Age3 = 'AGE_3', + Age4 = 'AGE_4', + Age5 = 'AGE_5', +} + +/** + * ai轮播列表输出 + * + * AiCarouselListOutput + */ +export interface AiCarouselListOutput { + /** + * AI的id + */ + aiId?: number + /** + * 出生日期 + */ + birthday?: string + /** + * 性格名称 + */ + characterName?: string + /** + * 头像 + */ + headImg?: string + /** + * 主页头图 + */ + homeImageUrl?: string + /** + * 简介 + */ + introduction?: string + /** + * 是否点赞过 + */ + liked?: boolean + /** + * 点赞数 + */ + likedCount?: number + /** + * 昵称 + */ + nickname?: string + /** + * 角色名称 + */ + roleName?: string + /** + * 0,男;1,女;2,自定义 + */ + sex?: number + /** + * 标签名称 + */ + tagName?: string + /** + * ai所属用户id + */ + userId?: number +} + +/** + * HomeRecommendV2Output + */ +export interface HomeRecommendV2Output { + mostChat?: AiChatRankOutput[] + mustCrush?: AiHeartbeatRankOutput[] + mustGifted?: AiGiftRankOutput[] + starAChat?: StartChatOutput[] +} + +/** + * com.sonic.frog.domain.output.AiChatRankOutput + * + * AiChatRankOutput + */ +export interface AiChatRankOutput { + /** + * AI的id + */ + aiId?: number + /** + * 出生日期 + */ + birthday?: string + /** + * 性格名称 + */ + characterName?: string + /** + * 聊天次数 + */ + chatNum?: number + /** + * 头像 + */ + headImg?: string + /** + * 主页头图 + */ + homeImageUrl?: string + /** + * 简介 + */ + introduction?: string + /** + * 被喜欢数 + */ + likedNum?: number + /** + * 昵称 + */ + nickname?: string + /** + * 排名编号 + */ + rankNo?: number + /** + * 角色名称 + */ + roleName?: string + /** + * 0,男;1,女;2,自定义 + */ + sex?: number + /** + * 标签名称 + */ + tagName?: string +} + +/** + * com.sonic.frog.domain.output.AiHeartbeatRankOutput + * + * AiHeartbeatRankOutput + */ +export interface AiHeartbeatRankOutput { + /** + * AI的id + */ + aiId?: number + /** + * 出生日期 + */ + birthday?: string + /** + * 性格名称 + */ + characterName?: string + /** + * 头像 + */ + headImg?: string + /** + * 心动总分值 + */ + heartbeatValTotal?: number + /** + * 主页头图 + */ + homeImageUrl?: string + /** + * 简介 + */ + introduction?: string + /** + * 被喜欢数 + */ + likedNum?: number + /** + * 昵称 + */ + nickname?: string + /** + * 排名编号 + */ + rankNo?: number + /** + * 角色名称 + */ + roleName?: string + /** + * 0,男;1,女;2,自定义 + */ + sex?: number + /** + * 标签名称 + */ + tagName?: string +} + +/** + * com.sonic.frog.domain.output.AiGiftRankOutput + * + * AiGiftRankOutput + */ +export interface AiGiftRankOutput { + /** + * AI的id + */ + aiId?: number + /** + * 出生日期 + */ + birthday?: string + /** + * 性格名称 + */ + characterName?: string + /** + * 礼物 + */ + giftCoinNum?: number + /** + * 头像 + */ + headImg?: string + /** + * 主页头图 + */ + homeImageUrl?: string + /** + * 简介 + */ + introduction?: string + /** + * 被喜欢数 + */ + likedNum?: number + /** + * 昵称 + */ + nickname?: string + /** + * 排名编号 + */ + rankNo?: number + /** + * 角色名称 + */ + roleName?: string + /** + * 0,男;1,女;2,自定义 + */ + sex?: number + /** + * 标签名称 + */ + tagName?: string +} + +/** + * com.sonic.frog.domain.output.StartChatOutput + * + * StartChatOutput + */ +export interface StartChatOutput { + /** + * AI的id + */ + aiId?: number + /** + * 开场白语音地址 + */ + dialoguePrologueSound?: string + /** + * 头像 + */ + headImg?: string + /** + * 被喜欢数 + */ + likedNum?: number + /** + * 昵称 + */ + nickname?: string + /** + * 主动聊天内容 + */ + supportingContentList?: string[] +}