feat: 优化代码

This commit is contained in:
liuyonghe0111 2025-11-07 17:10:40 +08:00
parent 346cfa7ab3
commit e2d9348ee0
6 changed files with 160 additions and 85 deletions

View File

@ -1,75 +0,0 @@
'use client';
import {
GenerateInputIcon,
PhoneCallIcon,
PortraitModeIcon,
} from '@/assets/chatacter';
import { useChatStore } from '../../store';
import IconFont from '@/components/ui/iconFont';
import { cn } from '@/lib';
import Image from 'next/image';
export default function Actions() {
const isPortraitMode = useChatStore((state) => state.isPortraitMode);
const setIsPortraitMode = useChatStore((state) => state.setIsPortraitMode);
const setIsPhoneCallMode = useChatStore((state) => state.setIsPhoneCallMode);
const className = 'text-[#0066FF] cursor-pointer hover:text-[#4269D6]';
const suggestMessages = [
'The threads of fate intertwine once more...The threads of fate intert',
'The threads of fate intertwine once more.',
'The threads of fate intertwine once more.',
];
return (
<div className="mb-4">
<div className="relative mb-5 flex flex-col gap-2">
{suggestMessages.map((message, index) => (
<div
style={{
backgroundColor: 'rgba(36, 44, 80, 0.9)',
width: '70%',
}}
className="flex h-10 cursor-pointer items-center rounded-full px-4"
key={`message-${index}`}
>
<span className="line-clamp-2 text-xs">{message}</span>
</div>
))}
<span
style={{ left: '71%' }}
className="flex-center absolute bottom-0 h-7 w-7 cursor-pointer rounded-full bg-black/40"
>
<IconFont type="icon-zhongxie" size={18} />
</span>
</div>
{/* action */}
<div className="flex justify-between">
<div onClick={() => null} className="flex items-center gap-5">
<div className={className}>
<GenerateInputIcon />
</div>
<div
onClick={() => setIsPhoneCallMode(true)}
className={cn(className, 'relative')}
>
<PhoneCallIcon />
<Image
className="absolute right-0 bottom-0"
src="/component/vip.svg"
alt="phone call"
width={20}
height={20}
/>
</div>
</div>
<div
onClick={() => setIsPortraitMode(!isPortraitMode)}
className="hover:cursor-pointer"
>
<PortraitModeIcon />
</div>
</div>
</div>
);
}

View File

@ -61,7 +61,7 @@ export default function PhoneCallMode() {
'bg-white/10 hover:bg-white/20'
)}
>
<IconFont type="icon-tonghua" size={20} />
<IconFont type="icon-zimu" size={22} />
</div>
</>
);

View File

@ -1,9 +1,8 @@
'use client';
import { TagSelect, VirtualGrid, Rate } from '@/components';
import React from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { useInfiniteScroll } from '@/hooks/useInfiniteScroll';
import useSmartInfiniteQuery from '@/hooks/useSmartInfiniteQuery';
import { fetchCharacters } from './service-client';
import { fetchTags } from '@/services/tag';
import { useQuery } from '@tanstack/react-query';
@ -16,11 +15,16 @@ const RoleCard: React.FC<any> = React.memo(({ item }) => {
onClick={() => 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.from || '/test.png'})`,
backgroundImage: `url(${item.coverImage || '/test.png'})`,
}}
>
{/* from */}
<Image src={item.from || '/test.png'} alt="from" width={55} height={78} />
<img
src={item.coverImage || '/test.png'}
alt="from"
width={55}
height={78}
/>
{/* info */}
<div className="px-2.5 pb-3">
@ -54,8 +58,9 @@ export default function Novel() {
noMoreData,
onLoadMore,
onSearch,
} = useInfiniteScroll(fetchCharacters, {
} = useSmartInfiniteQuery(fetchCharacters, {
queryKey: 'characters',
defaultQuery: { tagId: undefined },
});
// 使用useQuery查询tags
@ -84,7 +89,7 @@ export default function Novel() {
options={tags}
render={(item) => `# ${item.label}`}
onChange={(v) => {
onSearch({ tagId: v });
onSearch({ tagId: v as any });
}}
className="mx-12.5 my-7.5 mb-7.5"
/>

View File

@ -30,7 +30,7 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Script
src="//at.alicdn.com/t/c/font_5054282_z80k01jnmui.js"
src="//at.alicdn.com/t/c/font_5054282_g1w4osco3ua.js"
strategy="afterInteractive"
async
/>

View File

@ -0,0 +1,147 @@
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import { useDebounceFn, useMemoizedFn, useThrottleFn } from 'ahooks';
import { useState, useRef } from 'react';
type ParamsType<Q = any> = {
index: number;
limit: number;
query: Q;
};
type PropsType<T = any, Q = any> = {
queryKey: string;
defaultQuery?: Q;
defaultIndex?: number;
limit?: number;
isRowSame?: (d: T, n: T) => boolean;
};
type RequestType<T = any, Q = any> = (
params: ParamsType<Q>
) => Promise<{ rows: T[]; total: number } | undefined>;
type UseInfiniteScrollValue<T = any, Q = any> = {
query: Q;
onLoadMore: () => void;
onSearch: (query: Q) => void;
dataSource: T[];
total: number;
// 是否正在加载第一页,包括 初始化加载 和 参数改变时加载
isFirstLoading: boolean;
isLoadingMore: boolean;
noMoreData: boolean;
};
type DataType<T = any> = {
rows: T[];
total: number;
index: number;
};
const useSmartInfiniteQuery = <T = any, Q = any>(
request: RequestType<T, Q>,
props: PropsType<T, Q>
): UseInfiniteScrollValue<T, Q> => {
const {
queryKey,
defaultQuery,
defaultIndex = 1,
limit = 20,
isRowSame = (d, n) => {
return (d as any)?.id === (n as any)?.id;
},
} = props;
const [query, setQuery] = useState<Q>(defaultQuery as Q);
const index = useRef<number>(defaultIndex);
// 判断第一页数据和缓存中的第一页数据是否相等
const isSameData = useMemoizedFn(
(prevData: DataType<T>, result: Omit<DataType<T>, 'index'>) => {
// 没有缓存,必不相等
if (prevData.total <= 0 || !prevData.rows?.length) {
return false;
}
// 如果第一个元素相等,则认为数据相等(实际上并不一定)
if (!isRowSame(prevData.rows[0], result?.rows[0])) {
return false;
}
return true;
}
);
const { data, refetch, isFetching } = useQuery({
queryKey: [queryKey, query],
placeholderData: keepPreviousData,
queryFn: async ({ client }) => {
const params = {
index: index.current,
limit,
query,
};
const result = await request(params);
const prevData = (client.getQueryData([
queryKey,
query,
]) as DataType<T>) ?? {
rows: [],
total: 0,
index: 1,
};
// 如果是第一页
if (params.index === defaultIndex) {
// 第一页数据和缓存中相等
if (isSameData(prevData, result!)) {
// 更新索引
index.current = prevData.index;
// 直接返回缓存里的
return prevData;
} else {
// 第一页数据和缓存中不等, 返回新的数据
return {
total: result?.total || 0,
rows: result?.rows || [],
index: params.index,
};
}
}
// 不是第一页, 返回合并后的数据
return {
total: result?.total || 0,
rows: [...(prevData?.rows || []), ...(result?.rows || [])],
index: params.index,
};
},
});
const { run: onLoadMore } = useDebounceFn(
() => {
if (isFetching) return;
index.current = index.current + 1;
refetch();
},
{
wait: 300,
maxWait: 500,
}
);
const onSearch = useMemoizedFn((query: Q) => {
index.current = defaultIndex;
setQuery(query);
});
return {
query,
onLoadMore,
onSearch,
dataSource: data?.rows || [],
total: data?.total || 0,
isFirstLoading: isFetching && index.current === defaultIndex,
isLoadingMore: isFetching && index.current > defaultIndex,
noMoreData: data?.total === data?.rows?.length && !isFetching,
};
};
export default useSmartInfiniteQuery;

View File

@ -33,12 +33,10 @@ interface IntlProviderProps {
}
function setLocaleToCookie(locale: Locale) {
if (typeof window === 'undefined') return;
Cookies.set('locale', locale, { expires: 365, path: '/' });
}
function getLocaleFromCookie(): Locale {
if (typeof window === 'undefined') return 'en';
const cookieLocale = Cookies.get('locale') as Locale | undefined;
if (cookieLocale && (cookieLocale === 'zh' || cookieLocale === 'en')) {
return cookieLocale;