diff --git a/src/app/(main)/character/[id]/(detail)/components/BasicInfo.tsx b/src/app/(main)/character/[id]/(detail)/components/BasicInfo.tsx index 1d7cba4..9c090fa 100644 --- a/src/app/(main)/character/[id]/(detail)/components/BasicInfo.tsx +++ b/src/app/(main)/character/[id]/(detail)/components/BasicInfo.tsx @@ -22,21 +22,21 @@ export default function CharacterBasicInfo({ } const from = characterDetail?.from || "The CEO's Contract Wife"; - const avatar = characterDetail?.avatar || '/test.png'; - const fromImage = characterDetail?.fromImage || '/images/character/from.png'; + const avatar = characterDetail?.characterStand || '/test.png'; + const fromImage = + characterDetail?.headPortrait || '/images/character/from.png'; const characterName = characterDetail?.name || '未知角色'; return (
{/* 角色头像 - 使用 figure 语义化标签 */}
- {`${characterName}
@@ -93,8 +93,8 @@ export default function CharacterBasicInfo({
({ - label: tag, - value: tag, + label: tag.name, + value: tag.tagId, }))} />
@@ -130,7 +130,7 @@ export default function CharacterBasicInfo({ {from && (
- {`$来源作品封面`}('/review'); const tabs = [ { path: '/review', Icon: CommentIcon, label: 'Review' }, @@ -23,10 +21,10 @@ export default function TabsNavigation() { <>
{tabs.map((tab, index) => { - const isActive = pathname.includes(tab.path); + const isActive = activeKey === tab.path; const dom = (
router.push(`/character/${id}${tab.path}`)} + onClick={() => setActiveKey(tab.path as any)} className={cn( 'flex gap-2.5 hover:cursor-pointer', !isActive && 'text-text-color/60' @@ -49,7 +47,7 @@ export default function TabsNavigation() { })}
- {pathname.includes('/review') ? : } + {activeKey === '/review' ? : }
); diff --git a/src/app/(main)/character/[id]/(detail)/page.tsx b/src/app/(main)/character/[id]/(detail)/page.tsx index d35cc06..538edde 100644 --- a/src/app/(main)/character/[id]/(detail)/page.tsx +++ b/src/app/(main)/character/[id]/(detail)/page.tsx @@ -4,6 +4,8 @@ import Image from 'next/image'; import TabsNavigation from './components/TabsNavigation'; import { fetchCharacterDetail } from '../../service-server'; +export const revalidate = 300; + type CharacterDetailLayoutProps = { params: Promise<{ id: string; diff --git a/src/app/(main)/character/page.tsx b/src/app/(main)/character/page.tsx index 32ac2ba..266fded 100644 --- a/src/app/(main)/character/page.tsx +++ b/src/app/(main)/character/page.tsx @@ -13,10 +13,8 @@ const RoleCard: React.FC = React.memo(({ item }) => { return (
router.push(`/character/${item.id}`)} - className="cover-bg relative flex h-full w-full cursor-pointer flex-col justify-between overflow-hidden rounded-[20]" - style={{ - backgroundImage: `url(${item.coverImage || '/test.png'})`, - }} + className="cover-bg relative flex h-full w-full cursor-pointer flex-col justify-between overflow-hidden rounded-[20px]" + style={{ backgroundImage: `url(${item.coverImage})` }} > {/* from */} = React.memo(({ item }) => { /> {/* info */} -
-
- {item.name} -
- + {/* 模糊背景层 - 从上到下渐变显示 */} +
-
- {item.description} -
-
- -
+ +
+
+ {item.name} +
+ +
+ {item.description} +
+
+ +
+
@@ -58,9 +80,9 @@ export default function Novel() { noMoreData, onLoadMore, onSearch, - } = useSmartInfiniteQuery(fetchCharacters, { + } = useSmartInfiniteQuery(fetchCharacters, { queryKey: 'characters', - defaultQuery: { tagId: undefined }, + defaultQuery: { tagIds: [] }, }); // 使用useQuery查询tags @@ -87,9 +109,10 @@ export default function Novel() { header={ `# ${item.label}`} onChange={(v) => { - onSearch({ tagId: v as any }); + onSearch({ tagIds: v as string[] }); }} className="mx-12.5 my-7.5 mb-7.5" /> diff --git a/src/app/(main)/character/service-server.ts b/src/app/(main)/character/service-server.ts index bfd8b3d..a1bdf9e 100644 --- a/src/app/(main)/character/service-server.ts +++ b/src/app/(main)/character/service-server.ts @@ -1,12 +1,12 @@ import { cache } from 'react'; -import { publicServerRequest } from '@/lib/server/request'; +import { serverRequest } from '@/lib/server/request'; export const fetchCharacterDetail = cache(async (id: string) => { - const res = await publicServerRequest(`/character/detail`, { - method: 'post', - params: { - roleId: id, - }, + const res = await serverRequest(`/character/detail`, { + method: 'POST', + data: { id }, + // Next.js 缓存:每小时重新验证一次 + revalidate: 3600, }); return res.data ?? {}; }); diff --git a/src/components/ui/inputs/select.tsx b/src/components/ui/inputs/select.tsx index 2bb28ce..7d3c884 100644 --- a/src/components/ui/inputs/select.tsx +++ b/src/components/ui/inputs/select.tsx @@ -92,7 +92,7 @@ function Select(props: SelectProps) { = { @@ -7,76 +6,90 @@ type ResponseType = { data: T; }; -/** - * 服务端请求函数,用于 SSR - * 从 cookies 中获取 token 并添加到 Authorization header - * 如果接口不需要 token(如公开数据),token 会自动省略 - */ -async function serverRequest( +export async function fetchServerRequest( url: string, - config?: AxiosRequestConfig & { - requireAuth?: boolean; // 是否必须需要 token,默认 false(可选) + options?: { + method?: 'GET' | 'POST'; + params?: Record; + data?: Record; + requireAuth?: boolean; + // Next.js 缓存选项 + revalidate?: number | false; // 秒数,或 false 表示永不过期 + tags?: string[]; // 缓存标签,用于手动刷新 } ): Promise> { - const { requireAuth = false, ...requestConfig } = config || {}; + const { + method = 'GET', + params, + data, + requireAuth = false, + revalidate, + tags, + } = options || {}; - // 从 cookies 中获取 token - const token = await getServerToken(); - - // 如果需要 token 但没有 token,可以选择抛出错误或继续请求 - // 这里选择继续请求,让后端决定是否允许访问 - if (requireAuth && !token) { - throw new Error('Authentication required'); + // 获取 token(如果需要) + let token: string | null = null; + if (requireAuth) { + token = await getServerToken(); + if (!token) { + throw new Error('Authentication required'); + } } - // 创建 axios 实例 - const instance = axios.create({ - withCredentials: false, - baseURL: 'http://54.223.196.180:8091', - validateStatus: (status) => { - return status >= 200 && status < 500; + // 构建请求配置 + const fetchOptions: RequestInit = { + method, + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), }, - }); + // Next.js 缓存配置 + next: { + ...(revalidate !== undefined && { revalidate }), + ...(tags && { tags }), + }, + }; - // 设置请求拦截器,添加 token 到 Authorization header - // 与客户端 request.ts 保持一致 - // 注意:token 是可选的,用于 SEO 的公开数据可以不带 token - instance.interceptors.request.use((config: InternalAxiosRequestConfig) => { - if (token) { - config.headers.setAuthorization(`Bearer ${token}`); - } - return config; - }); + // 构建完整 URL(服务端必须用完整 URL) + const baseURL = 'http://54.223.196.180:8091'; + const fullURL = new URL(url, baseURL); - // 处理参数 - let data: any; - if (requestConfig && requestConfig?.params) { - const { params } = requestConfig; - data = Object.fromEntries( - Object.entries(params).filter(([, value]) => value !== '') + // 处理查询参数 + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (value !== '') { + fullURL.searchParams.append(key, String(value)); + } + }); + } + + // 处理 body 数据 + if (data) { + fetchOptions.body = JSON.stringify( + Object.fromEntries( + Object.entries(data).filter(([, value]) => value !== '') + ) ); } - console.log('url', url); - // 发起请求 - const response = await instance>(url, { - ...requestConfig, - params: data, - }); + const response = await fetch(fullURL.toString(), fetchOptions); + if (!response.ok) { + return { code: 400, message: '请求失败', data: {} as T }; + } - return response.data; + return await response.json(); } -/** - * 公开数据请求函数(不需要 token) - * 用于 SEO 友好的公开数据接口 - */ -export async function publicServerRequest( +export async function serverRequest( url: string, - config?: AxiosRequestConfig + options?: { + method?: 'GET' | 'POST'; + params?: Record; + data?: Record; + revalidate?: number | false; + tags?: string[]; + } ): Promise> { - return serverRequest(url, { ...config, requireAuth: false }); + return fetchServerRequest(url, { ...options, requireAuth: false }); } - -export default serverRequest;