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 (
-
-
-
-
-
-
- Check-in{' '}
-
-
-
-
- Daily Free crush coinsh
-
-
-
+ //
+
+
+
+
+
+ Check-in{' '}
+
+
+
+
+ Daily Free crush coinsh
+
+
- {response?.lg && (
-
- )}
-
+ {response?.lg && (
+
+ )}
+
+ //
);
});
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')}
-
-
- {/* 头像 */}
-
-
- {/* 用户信息区域 */}
-
- {/* 用户名 */}
-
-
- {/* 喜欢数 */}
-
-
-
-
- {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 (
-
-
-
-
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:
-
-
-
- {formatFromCents(unlockPrice)}
-
-
-
- )
- }
-
- if (isOwner && lockStatus === LockStatus.Unlock) {
- return (
-
-
How to Unlock:
-
-
-
- {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
- handleConfirmSetDefault(albumId)}
- >
- Confirm
-
-
-
-
- {!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 (
- <>
-
- handleLike(datas[currentIndex].albumId, isLiked)}
- >
-
- Like
-
- >
- )
- }
-
- return (
- <>
-
-
- Unlock
-
- >
- )
- }
-
- 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();
- }}
- >
-
-
-
-
-
-
-
- {formatFromCents(item.unlockPrice || 0)}
-
-
-
Unlock
-
-
- )
- }
- return null
- }
-
- const renderDefaultTag = () => {
- if (item.isDefault) {
- return (
-
- Default
-
- )
- }
- return null
- }
-
- return (
-
-
- {/* 背景图片 */}
-
-
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 (
-
-
-
-
-
{`${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 (
-
- )
- }
-
- return (
-
-
- Gifts
-
-
-
- {datas?.map((gift) => (
-
-
-
- {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 选项卡 */}
-
-
handleTabClick(UserTab.About)}
- className={cn(
- 'flex h-8 w-full flex-col items-center justify-start gap-1',
- 'transition-colors duration-200'
- )}
- >
-
- About
-
- {currentTab === UserTab.About &&
}
-
-
-
- {/* Album 选项卡 */}
-
-
handleTabClick(UserTab.Album)}
- className={cn(
- 'flex h-8 w-full flex-col items-center justify-start gap-1',
- 'transition-colors duration-200'
- )}
- >
-
- Album
-
- {currentTab === UserTab.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 (
-
- Edit
-
- )
- }
-
- return (
-
- Chat
-
- )
- }
-
- if (!data) {
- return null
- }
-
- return (
-
- {/* 背景卡片 */}
-
-
- {/* 内容 */}
-
- {/* 头像 */}
-
-
-
-
- {nickname?.slice(0, 1)}
-
-
-
-
- {/* 用户信息 */}
-
- {/* 用户名 */}
-
{nickname}
-
- {/* 标签 */}
-
- {/* 年龄和性别标签 */}
-
-
- {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 && (
-
- Generate
-
- )}
-
- )
-}
-
-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 (
-
-
-
-
-
+
+
+
);
}
return (
@@ -75,7 +73,7 @@ function Topbar() {
const rightDomRender = () => {
if (!user)
return (
-
+
Login in / Sign up
);
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[]
+}