-
+
+
+ {/* 用户头像和信息 */}
+
+ {/* 头像 */}
+
+
+
+
+ {user?.nickname?.slice(0, 1)}
+
+
+
+
+
+
+
+
+ {/* 用户名和ID */}
+
+
+
{user?.nickname}
+
+
+
+ ID: {user?.idCard}
+ {
+ navigator.clipboard.writeText(user?.idCard?.toString() || '');
+ toast.success('Copied to clipboard');
+ }}
+ >
+
+
+
+
+
+ {/* 功能卡片 */}
+
+
+
+ {/*
*/}
+
+
+
- )
+ );
}
diff --git a/src/app/(main)/profile/profile-page.tsx b/src/app/(main)/profile/profile-page.tsx
deleted file mode 100644
index 0bce99a..0000000
--- a/src/app/(main)/profile/profile-page.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-'use client'
-
-import React, { useState } from 'react'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { useCurrentUser } from '@/hooks/auth'
-import { IconButton } from '@/components/ui/button'
-import { toast } from 'sonner'
-import ProfileFeatureList from './components/ProfileFeatureList'
-import ProfileDropdown from './components/ProfileDropdown'
-import AvatarSetting from './components/AvatarSetting'
-// import CharacterList from './components/CharacterList'
-import Image from 'next/image'
-
-const genderMap = {
- 0: '/icons/male.svg',
- 1: '/icons/female.svg',
- 2: '/icons/gender-neutral.svg',
-}
-
-export default function ProfilePage() {
- const { data: user } = useCurrentUser()
- const [isAvatarSettingOpen, setIsAvatarSettingOpen] = useState(false)
-
- const openAvatarSetting = () => {
- setIsAvatarSettingOpen(true)
- }
-
- const closeAvatarSetting = () => {
- setIsAvatarSettingOpen(false)
- }
-
- return (
-
-
- {/* 用户头像和信息 */}
-
- {/* 头像 */}
-
-
-
-
- {user?.nickname?.slice(0, 1)}
-
-
-
-
-
-
-
-
- {/* 用户名和ID */}
-
-
-
{user?.nickname}
-
-
-
- ID: {user?.idCard}
- {
- navigator.clipboard.writeText(user?.idCard?.toString() || '')
- toast.success('Copied to clipboard')
- }}
- >
-
-
-
-
-
- {/* 功能卡片 */}
-
-
-
- {/*
*/}
-
-
-
-
- )
-}
diff --git a/src/app/login/components/DiscordButton.tsx b/src/app/login/components/DiscordButton.tsx
index a63aece..1829ed7 100644
--- a/src/app/login/components/DiscordButton.tsx
+++ b/src/app/login/components/DiscordButton.tsx
@@ -1,111 +1,111 @@
-'use client'
+'use client';
-import { discordOAuth } from '@/lib/oauth/discord'
-import { SocialButton } from './SocialButton'
-import { toast } from 'sonner'
-import { useEffect, useRef } from 'react'
-import { useLogin } from '@/hooks/auth'
-import { useRouter, useSearchParams } from 'next/navigation'
-import { tokenManager } from '@/lib/auth/token'
-import { AppClient, ThirdType } from '@/services/auth'
+import { discordOAuth } from '@/lib/oauth/discord';
+import { SocialButton } from './SocialButton';
+import { toast } from 'sonner';
+import { useEffect, useRef } from 'react';
+import { useLogin } from '@/hooks/auth';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { tokenManager } from '@/lib/auth/token';
+import { AppClient, ThirdType } from '@/services/auth';
const DiscordButton = () => {
- const login = useLogin()
- const router = useRouter()
- const searchParams = useSearchParams()
- const redirect = searchParams.get('redirect')
+ const login = useLogin();
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const redirect = searchParams.get('redirect');
// 处理Discord OAuth回调
useEffect(() => {
- const discordCode = searchParams.get('discord_code')
- const discordState = searchParams.get('discord_state')
- const error = searchParams.get('error')
+ const discordCode = searchParams.get('discord_code');
+ const discordState = searchParams.get('discord_state');
+ const error = searchParams.get('error');
// 处理错误情况
if (error) {
- toast.error('Discord login failed')
+ toast.error('Discord login failed');
// 清理URL参数
- const newUrl = new URL(window.location.href)
- newUrl.searchParams.delete('error')
- router.replace(newUrl.pathname)
- return
+ const newUrl = new URL(window.location.href);
+ newUrl.searchParams.delete('error');
+ router.replace(newUrl.pathname);
+ return;
}
// 处理Discord授权码
if (discordCode) {
// 验证state参数(可选的安全检查)
- const savedState = sessionStorage.getItem('discord_oauth_state')
+ const savedState = sessionStorage.getItem('discord_oauth_state');
if (savedState && discordState && savedState !== discordState) {
- toast.error('Discord login failed')
- return
+ toast.error('Discord login failed');
+ return;
}
// 使用code调用后端登录接口
- const deviceId = tokenManager.getDeviceId()
+ const deviceId = tokenManager.getDeviceId();
const loginData = {
appClient: AppClient.Web,
deviceCode: deviceId,
thirdToken: discordCode, // 直接传递Discord授权码
thirdType: ThirdType.Discord,
- }
+ };
login.mutate(loginData, {
onSuccess: () => {
- toast.success('Login successful')
+ toast.success('Login successful');
// 清除 Next.js 路由缓存,避免使用登录前 prefetch 的重定向响应
- router.refresh()
+ router.refresh();
// 清理URL参数和sessionStorage
- sessionStorage.removeItem('discord_oauth_state')
- const newUrl = new URL(window.location.href)
- newUrl.searchParams.delete('discord_code')
- newUrl.searchParams.delete('discord_state')
- router.replace(newUrl.pathname)
+ sessionStorage.removeItem('discord_oauth_state');
+ const newUrl = new URL(window.location.href);
+ newUrl.searchParams.delete('discord_code');
+ newUrl.searchParams.delete('discord_state');
+ router.replace(newUrl.pathname);
- const loginRedirectUrl = sessionStorage.getItem('login_redirect_url')
+ const loginRedirectUrl = sessionStorage.getItem('login_redirect_url');
// 重定向到首页或指定页面
if (loginRedirectUrl) {
- router.push(loginRedirectUrl)
+ router.push(loginRedirectUrl);
} else {
- router.push('/')
+ router.push('/');
}
},
onError: (error) => {
// 清理URL参数
- const newUrl = new URL(window.location.href)
- newUrl.searchParams.delete('discord_code')
- newUrl.searchParams.delete('discord_state')
- newUrl.searchParams.delete('redirect')
- router.replace(newUrl.pathname)
+ const newUrl = new URL(window.location.href);
+ newUrl.searchParams.delete('discord_code');
+ newUrl.searchParams.delete('discord_state');
+ newUrl.searchParams.delete('redirect');
+ router.replace(newUrl.pathname);
},
- })
+ });
}
- }, [])
+ }, []);
const handleDiscordLogin = () => {
try {
// 生成随机state用于安全验证
- const state = Math.random().toString(36).substring(2, 15)
+ const state = Math.random().toString(36).substring(2, 15);
// 获取Discord授权URL
- const authUrl = discordOAuth.getAuthUrl(state)
+ const authUrl = discordOAuth.getAuthUrl(state);
// 将state保存到sessionStorage用于后续验证
if (typeof window !== 'undefined') {
- sessionStorage.setItem('discord_oauth_state', state)
- sessionStorage.setItem('login_redirect_url', redirect || '')
+ sessionStorage.setItem('discord_oauth_state', state);
+ sessionStorage.setItem('login_redirect_url', redirect || '');
}
// 跳转到Discord授权页面
- window.location.href = authUrl
+ window.location.href = authUrl;
} catch (error) {
- console.error('Discord login error:', error)
- toast.error('Discord login failed')
+ console.error('Discord login error:', error);
+ toast.error('Discord login failed');
}
- }
+ };
return (
{
>
{login.isPending ? 'Signing in...' : 'Continue with Discord'}
- )
-}
+ );
+};
-export default DiscordButton
+export default DiscordButton;
diff --git a/src/components/ui/infinite-scroll-list.tsx b/src/components/ui/infinite-scroll-list.tsx
index a519000..3be938f 100644
--- a/src/components/ui/infinite-scroll-list.tsx
+++ b/src/components/ui/infinite-scroll-list.tsx
@@ -1,6 +1,7 @@
-import React, { ReactNode, useMemo } from 'react';
+import React, { ReactNode, useEffect, useMemo, useRef } from 'react';
import { useInfiniteScroll } from '@/hooks/useInfiniteScroll';
import { cn } from '@/lib/utils';
+import { useSize } from 'ahooks';
interface InfiniteScrollListProps
{
/**
@@ -42,7 +43,8 @@ interface InfiniteScrollListProps {
lg?: number;
xl?: number;
}
- | number;
+ | number
+ | ((width: number) => number);
/**
* 网格间距
*/
@@ -104,6 +106,8 @@ export function InfiniteScrollList({
threshold = 200,
enabled = true,
}: InfiniteScrollListProps) {
+ const ref = useRef(null);
+ const size = useSize(ref);
const { loadMoreRef, isFetching } = useInfiniteScroll({
hasNextPage,
isLoading,
@@ -115,17 +119,21 @@ export function InfiniteScrollList({
// 生成网格列数的CSS类名映射
const gridColsClass = useMemo(() => {
+ const gridClassMap: Record = {
+ 1: 'grid-cols-1',
+ 2: 'grid-cols-2',
+ 3: 'grid-cols-3',
+ 4: 'grid-cols-4',
+ 5: 'grid-cols-5',
+ 6: 'grid-cols-6',
+ };
if (typeof columns === 'number') {
- const gridClassMap: Record = {
- 1: 'grid-cols-1',
- 2: 'grid-cols-2',
- 3: 'grid-cols-3',
- 4: 'grid-cols-4',
- 5: 'grid-cols-5',
- 6: 'grid-cols-6',
- };
return gridClassMap[columns] || 'grid-cols-4';
}
+ if (typeof columns === 'function') {
+ const col = columns(size?.width || 0);
+ return gridClassMap[col] || 'grid-cols-4';
+ }
// 使用完整的类名字符串,让 Tailwind 能够正确识别
const classes: string[] = [];
@@ -171,7 +179,7 @@ export function InfiniteScrollList({
if (columns.xl === 6) classes.push('xl:grid-cols-6');
return classes.join(' ');
- }, [columns]);
+ }, [columns, size?.width]);
// 生成间距类名
const gapClass = useMemo(() => {
@@ -187,49 +195,14 @@ export function InfiniteScrollList({
return gapClassMap[gap] || 'gap-4';
}, [gap]);
- // 错误状态
- if (hasError && ErrorComponent && onRetry) {
- return ;
- }
-
- // 首次加载状态
- if (isLoading && items.length === 0) {
- if (LoadingSkeleton) {
- return (
-
- {Array.from({ length: 8 }).map((_, index) => (
-
- ))}
-
- );
- }
-
- return (
-
- {Array.from({ length: 8 }).map((_, index) => (
-
- ))}
-
- );
- }
-
- // 空状态
- if (items.length === 0 && EmptyComponent) {
- return ;
- }
-
- return (
-
+ let finalDom = (
+ <>
{/* 主要内容 */}
{items.map((item, index) => (
{renderItem(item, index)}
))}
-
{/* 加载更多触发器 - 只在没有错误时显示 */}
{hasNextPage && !hasError && (
@@ -247,6 +220,46 @@ export function InfiniteScrollList({
)}
)}
+ >
+ );
+
+ // 错误状态
+ if (hasError && ErrorComponent && onRetry) {
+ finalDom =
;
+ }
+
+ // 首次加载状态
+ if (isLoading && items.length === 0) {
+ if (LoadingSkeleton) {
+ finalDom = (
+
+ {Array.from({ length: 8 }).map((_, index) => (
+
+ ))}
+
+ );
+ }
+
+ finalDom = (
+
+ {Array.from({ length: 8 }).map((_, index) => (
+
+ ))}
+
+ );
+ }
+
+ // 空状态
+ if (items.length === 0 && EmptyComponent) {
+ finalDom =
;
+ }
+
+ return (
+
+ {finalDom}
);
}
diff --git a/src/hooks/auth.ts b/src/hooks/auth.ts
index cf9bf2b..7f0a06f 100644
--- a/src/hooks/auth.ts
+++ b/src/hooks/auth.ts
@@ -1,4 +1,4 @@
-import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
authService,
CheckNicknameRequest,
@@ -6,28 +6,29 @@ import {
UpdateUserInfoRequest,
type LoginRequest,
type LoginResponse,
-} from '@/services/auth'
-import { authKeys, userKeys } from '@/lib/query-keys'
-import { tokenManager } from '@/lib/auth/token'
-import { toast } from 'sonner'
-import type { ApiError } from '@/types/api'
-import { useRouter } from 'next/navigation'
-import { userService } from '@/services/user'
+} from '@/services/auth';
+import { authKeys, userKeys } from '@/lib/query-keys';
+import { tokenManager } from '@/lib/auth/token';
+import { toast } from 'sonner';
+import type { ApiError } from '@/types/api';
+import { useRouter } from 'next/navigation';
+import { userService } from '@/services/user';
export function useLogin() {
- const queryClient = useQueryClient()
+ const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: LoginRequest): Promise
=> authService.login(data),
onSuccess: (response: LoginResponse) => {
+ console.log('useLogin onSuccess save token', response.token);
// 保存token到cookie
- tokenManager.setToken(response.token)
+ tokenManager.setToken(response.token);
// 刷新当前用户信息
queryClient.invalidateQueries({
queryKey: authKeys.currentUser(),
- })
+ });
},
- })
+ });
}
export function useToken() {
@@ -35,33 +36,33 @@ export function useToken() {
isLogin: tokenManager.isAuthenticated(),
token: tokenManager.getToken(),
getLoginStatus: () => {
- return tokenManager.isAuthenticated()
+ return tokenManager.isAuthenticated();
},
- }
+ };
}
export function useLogout() {
- const queryClient = useQueryClient()
- const router = useRouter()
+ const queryClient = useQueryClient();
+ const router = useRouter();
return useMutation({
mutationFn: () => authService.logout(),
onSuccess: () => {
// 清除token
- tokenManager.removeToken()
+ tokenManager.removeToken();
// 显示成功提示
- toast.success('Log out successful!')
+ toast.success('Log out successful!');
// 清除所有查询缓存
- queryClient.clear()
- router.push('/')
+ queryClient.clear();
+ router.push('/');
},
onError: (error: ApiError) => {
// 即使登出接口失败,也要清除本地token
- tokenManager.removeToken()
- queryClient.clear()
+ tokenManager.removeToken();
+ queryClient.clear();
// 跳转到登录页
- router.push('/')
+ router.push('/');
},
- })
+ });
}
export function useCurrentUser() {
@@ -78,104 +79,104 @@ export function useCurrentUser() {
error.errorCode === 'AUTH_TOKEN_INVALID' ||
error.errorCode === 'AUTH_UNAUTHORIZED'
) {
- tokenManager.removeToken()
- return false
+ tokenManager.removeToken();
+ return false;
}
- return failureCount < 1
+ return failureCount < 1;
},
staleTime: 60 * 1000, // 60秒后缓存过期
- })
+ });
}
export function useCompleteUser() {
- const queryClient = useQueryClient()
+ const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CompleteUserInfoRequest) => authService.completeUserInfo(data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: authKeys.currentUser(),
- })
+ });
},
- })
+ });
}
export function useUpdateUser() {
- const queryClient = useQueryClient()
+ const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UpdateUserInfoRequest) => authService.updateUserInfo(data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: authKeys.currentUser(),
- })
+ });
},
- })
+ });
}
export function useDeleteUser() {
- const queryClient = useQueryClient()
- const router = useRouter()
+ const queryClient = useQueryClient();
+ const router = useRouter();
return useMutation({
mutationFn: () => authService.deleteUser(),
onSuccess: () => {
- tokenManager.removeToken()
- queryClient.clear()
- router.push('/login')
+ tokenManager.removeToken();
+ queryClient.clear();
+ router.push('/login');
},
- })
+ });
}
// 注册功能暂未实现,保留接口定义
export function useRegister() {
- const queryClient = useQueryClient()
+ const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => {
// TODO: 实现注册接口
- throw new Error('注册功能暂未实现')
+ throw new Error('注册功能暂未实现');
},
onSuccess: (response: LoginResponse) => {
// 注册成功后自动登录
- tokenManager.setToken(response.token)
- toast.success('Successful registration!')
+ tokenManager.setToken(response.token);
+ toast.success('Successful registration!');
queryClient.invalidateQueries({
queryKey: authKeys.currentUser(),
- })
+ });
},
onError: (error: ApiError) => {
console.error('注册失败:', {
errorCode: error.errorCode,
errorMsg: error.errorMsg,
traceId: error.traceId,
- })
+ });
toast.error('Registration failed.', {
description: error.errorMsg || '请稍后重试',
- })
+ });
},
- })
+ });
}
// 检查是否已登录的hook
export function useIsAuthenticated() {
- return tokenManager.isAuthenticated()
+ return tokenManager.isAuthenticated();
}
export function useCheckNickname({ onError }: { onError?: (error: ApiError) => void }) {
return useMutation({
mutationFn: (data: CheckNicknameRequest) => authService.checkNickname(data),
onError: onError,
- })
+ });
}
export function useCheckText() {
return useMutation({
mutationFn: async (data: { content: string }) => {
- const result = await authService.checkText(data)
+ const result = await authService.checkText(data);
if (result) {
- return `We found some words that might not be allowed: ${result}`
+ return `We found some words that might not be allowed: ${result}`;
}
- return ''
+ return '';
},
- })
+ });
}
export function useUserNoticeStat() {
@@ -183,7 +184,7 @@ export function useUserNoticeStat() {
queryKey: userKeys.noticeStat(),
queryFn: () => userService.getUserNoticeStat(),
enabled: tokenManager.isAuthenticated(),
- })
+ });
}
export function useUserNoticeListInfinite(pageSize: number = 20, enabled: boolean = true) {
@@ -192,11 +193,11 @@ export function useUserNoticeListInfinite(pageSize: number = 20, enabled: boolea
queryFn: ({ pageParam = 1 }) =>
userService.getUserNoticeList({ page: { pn: pageParam, ps: pageSize } }),
getNextPageParam: (lastPage, allPages) => {
- const totalPages = Math.ceil((lastPage.tc || 0) / pageSize)
- const nextPage = allPages.length + 1
- return nextPage <= totalPages ? nextPage : undefined
+ const totalPages = Math.ceil((lastPage.tc || 0) / pageSize);
+ const nextPage = allPages.length + 1;
+ return nextPage <= totalPages ? nextPage : undefined;
},
initialPageParam: 1,
enabled, // 只有在启用时才执行查询
- })
+ });
}
diff --git a/src/layout/BasicLayout.tsx b/src/layout/BasicLayout.tsx
index d04485d..e806ae8 100644
--- a/src/layout/BasicLayout.tsx
+++ b/src/layout/BasicLayout.tsx
@@ -10,17 +10,39 @@ import { cn } from '@/lib/utils';
import CreateReachedLimitDialog from '../components/features/create-reached-limit-dialog';
import { useMedia } from '@/hooks/tools';
import BottomBar from './BottomBar';
+import { useStreamChatStore } from '@/stores/stream-chat';
+import { useLogin } from '@/hooks/auth';
interface ConditionalLayoutProps {
children: React.ReactNode;
}
+const useInitChat = () => {
+ const { data } = useLogin();
+ const connect = useStreamChatStore((state) => state.connect);
+ const queryChannels = useStreamChatStore((state) => state.queryChannels);
+
+ const initChat = async () => {
+ if (data) {
+ await connect(data);
+ await queryChannels({});
+ }
+ };
+
+ useEffect(() => {
+ initChat();
+ }, [data]);
+};
+
export default function ConditionalLayout({ children }: ConditionalLayoutProps) {
const pathname = usePathname();
const mainContentRef = useRef(null);
const prevPathnameRef = useRef(pathname);
const response = useMedia();
+ // 初始化聊天
+ useInitChat();
+
// 路由切换时重置滚动位置
useEffect(() => {
if (prevPathnameRef.current !== pathname) {
diff --git a/src/layout/BottomBar.tsx b/src/layout/BottomBar.tsx
index 810ebf7..f91e529 100644
--- a/src/layout/BottomBar.tsx
+++ b/src/layout/BottomBar.tsx
@@ -6,39 +6,43 @@ import Image from 'next/image';
import { cn } from '@/lib/utils';
import { useMedia } from '@/hooks/tools';
+export const items = [
+ {
+ label: 'Explore',
+ path: '/home',
+ icon: '/images/layout/explore.svg',
+ selectedIcon: '/images/layout/explore_active.svg',
+ },
+ {
+ label: 'Search',
+ path: '/search',
+ icon: '/images/layout/search.svg',
+ selectedIcon: '/images/layout/search_active.svg',
+ },
+ {
+ label: 'Chat',
+ path: '/chat-history',
+ icon: '/images/layout/chat.svg',
+ selectedIcon: '/images/layout/chat_active.svg',
+ },
+ {
+ label: 'Me',
+ path: '/profile',
+ icon: '/images/layout/me.svg',
+ selectedIcon: '/images/layout/me_active.svg',
+ },
+];
+
export default function BottomBar() {
const pathname = usePathname();
const response = useMedia({ hide: 500 });
- const items = [
- {
- label: 'Explore',
- path: '/home',
- icon: '/images/layout/explore.svg',
- selectedIcon: '/images/layout/explore_active.svg',
- },
- {
- label: 'Search',
- path: '/search',
- icon: '/images/layout/search.svg',
- selectedIcon: '/images/layout/search_active.svg',
- },
- {
- label: 'Chat',
- path: '/chat-history',
- icon: '/images/layout/chat.svg',
- selectedIcon: '/images/layout/chat_active.svg',
- },
- {
- label: 'Me',
- path: '/profile',
- icon: '/images/layout/me.svg',
- selectedIcon: '/images/layout/me_active.svg',
- },
- ];
+ if (!items.some((i) => i.path === pathname)) {
+ return null;
+ }
return (
-
+
{items.map((item) => {
const isSelected = pathname === item.path;
return (
diff --git a/src/layout/Sidebar.tsx b/src/layout/Sidebar.tsx
index 458c527..8c5d998 100644
--- a/src/layout/Sidebar.tsx
+++ b/src/layout/Sidebar.tsx
@@ -48,7 +48,7 @@ function Sidebar() {
return (