feat: 简体中文切换为繁体;优化角色卡;
This commit is contained in:
parent
8b933d26d6
commit
5cb01064ef
|
|
@ -77,7 +77,7 @@ export default function SettingDialog({ open, onOpenChange }: SettingProps) {
|
||||||
)}
|
)}
|
||||||
{activeTab !== 'profile' && (
|
{activeTab !== 'profile' && (
|
||||||
<IconButton variant="tertiary" size="small" onClick={() => setActiveTab('profile')}>
|
<IconButton variant="tertiary" size="small" onClick={() => setActiveTab('profile')}>
|
||||||
<IconFont size={24} className="text-white cursor-pointer" type="icon-jiantou" />
|
<IconFont className="text-white cursor-pointer text-2xl" type="icon-jiantou" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ export default function Input() {
|
||||||
<div className="flex w-full items-end gap-4">
|
<div className="flex w-full items-end gap-4">
|
||||||
{/* 打电话按钮 */}
|
{/* 打电话按钮 */}
|
||||||
<IconButton onClick={() => {}}>
|
<IconButton onClick={() => {}}>
|
||||||
<IconFont type="icon-dianhua" size={30} />
|
<IconFont type="icon-dianhua" className="text-3xl" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<div className="flex-1 flex items-end gap-2 min-h-12 py-2 px-2 bg-white/15 rounded-3xl">
|
<div className="flex-1 flex items-end gap-2 min-h-12 py-2 px-2 bg-white/15 rounded-3xl">
|
||||||
{/* 语音录制按钮 */}
|
{/* 语音录制按钮 */}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export default function ChatPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
<IconFont size={24} className="text-white cursor-pointer" type="icon-zhankai-1" />
|
<IconFont className="text-white text-2xl cursor-pointer" type="icon-zhankai-1" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<SettingDialog open={settingOpen} onOpenChange={setSettingOpen} />
|
<SettingDialog open={settingOpen} onOpenChange={setSettingOpen} />
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,11 @@ import { TagListSelect } from './TagListSelect';
|
||||||
import { MoreFilter } from './MoreFilter';
|
import { MoreFilter } from './MoreFilter';
|
||||||
|
|
||||||
const Filter = () => {
|
const Filter = () => {
|
||||||
const tab = useHomeStore((state) => state.tab);
|
|
||||||
const setTab = useHomeStore((state) => state.setTab);
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const characterParams = useHomeStore((state) => state.characterParams);
|
const { tab, setTab, characterParams, novelParams, setCharacterParams, setNovelParams } =
|
||||||
const setCharacterParams = useHomeStore((state) => state.setCharacterParams);
|
useHomeStore();
|
||||||
const t = useTranslations('home');
|
const t = useTranslations('home');
|
||||||
|
const currentTabTagIds = tab === 'character' ? characterParams.tagIds : novelParams.tagIds;
|
||||||
|
|
||||||
const { data: tags = [] } = useQuery({
|
const { data: tags = [] } = useQuery({
|
||||||
queryKey: ['tags', tab],
|
queryKey: ['tags', tab],
|
||||||
|
|
@ -71,12 +70,16 @@ const Filter = () => {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<MoreFilter />
|
{tab === 'character' && <MoreFilter />}
|
||||||
</div>
|
</div>
|
||||||
<TagListSelect
|
<TagListSelect
|
||||||
options={tags.map((tag: any) => ({ label: tag.name, value: tag.id }))}
|
options={tags.map((tag: any) => ({ label: tag.name, value: tag.id }))}
|
||||||
value={characterParams.tagIds || []}
|
value={currentTabTagIds || []}
|
||||||
onChange={(values) => setCharacterParams({ tagIds: values as string[] })}
|
onChange={(values) =>
|
||||||
|
tab === 'character'
|
||||||
|
? setCharacterParams({ tagIds: values as string[] })
|
||||||
|
: setNovelParams({ tagIds: values as string[] })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,41 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
import { InfiniteScrollList } from '@/components/ui/infinite-scroll-list';
|
||||||
|
import AIStandardCard from '@/components/features/ai-standard-card';
|
||||||
|
import useSmartInfiniteQuery from '../../useSmartInfiniteQuery';
|
||||||
|
import { fetchCharacters } from '@/services/editor';
|
||||||
|
import { useHomeStore } from '../../store';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import StoryContent from '@/components/features/StoryContent';
|
||||||
|
|
||||||
const Story = () => {
|
const Story = () => {
|
||||||
return <div>开发中ing</div>;
|
const characterParams = useHomeStore((state) => state.characterParams);
|
||||||
|
|
||||||
|
// const { dataSource, isFirstLoading, isLoadingMore, noMoreData, onLoadMore, onSearch } =
|
||||||
|
// useSmartInfiniteQuery<any, any>(fetchCharacters, {
|
||||||
|
// queryKey: 'characters',
|
||||||
|
// defaultQuery: characterParams,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// onSearch(characterParams);
|
||||||
|
// }, [characterParams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 sm:mt-8">
|
||||||
|
<StoryContent />
|
||||||
|
{/* <InfiniteScrollList<any>
|
||||||
|
items={dataSource}
|
||||||
|
enableLazyRender
|
||||||
|
lazyRenderMargin="500px"
|
||||||
|
columns={1}
|
||||||
|
renderItem={(character) => <AIStandardCard character={character} />}
|
||||||
|
getItemKey={(character, index) => character.id + index}
|
||||||
|
hasNextPage={!noMoreData}
|
||||||
|
isLoading={isFirstLoading || isLoadingMore}
|
||||||
|
fetchNextPage={onLoadMore}
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Story;
|
export default Story;
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ const HomePage = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-full text-txt-primary-normal relative px-4 sm:px-16">
|
<div className="h-full text-txt-primary-normal relative px-4 sm:px-16 min-w-0">
|
||||||
<div className="mx-auto max-w-[1136px] pb-32">
|
<div className="mx-auto max-w-[1136px] pb-32 w-full">
|
||||||
<Header />
|
<Header />
|
||||||
<Filter />
|
<Filter />
|
||||||
{tab === 'story' ? <Story /> : <Character />}
|
{tab === 'story' ? <Story /> : <Character />}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,17 @@ type CharacterParams = {
|
||||||
tagIds?: string[];
|
tagIds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type NovelParams = {
|
||||||
|
tagIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
interface HomeStore {
|
interface HomeStore {
|
||||||
tab: 'story' | 'character';
|
tab: 'story' | 'character';
|
||||||
characterParams: CharacterParams;
|
characterParams: CharacterParams;
|
||||||
|
novelParams: NovelParams;
|
||||||
setTab: (tab: 'story' | 'character') => void;
|
setTab: (tab: 'story' | 'character') => void;
|
||||||
setCharacterParams: (params: Partial<CharacterParams>) => void;
|
setCharacterParams: (params: Partial<CharacterParams>) => void;
|
||||||
|
setNovelParams: (params: Partial<NovelParams>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHomeStore = create<HomeStore>((set, get) => ({
|
export const useHomeStore = create<HomeStore>((set, get) => ({
|
||||||
|
|
@ -18,9 +24,16 @@ export const useHomeStore = create<HomeStore>((set, get) => ({
|
||||||
genders: [],
|
genders: [],
|
||||||
tagIds: [],
|
tagIds: [],
|
||||||
},
|
},
|
||||||
|
novelParams: {
|
||||||
|
tagIds: [],
|
||||||
|
},
|
||||||
setTab: (tab: 'story' | 'character') => set({ tab }),
|
setTab: (tab: 'story' | 'character') => set({ tab }),
|
||||||
setCharacterParams: (params) => {
|
setCharacterParams: (params) => {
|
||||||
const { characterParams } = get();
|
const { characterParams } = get();
|
||||||
set({ characterParams: { ...characterParams, ...params } });
|
set({ characterParams: { ...characterParams, ...params } });
|
||||||
},
|
},
|
||||||
|
setNovelParams: (params) => {
|
||||||
|
const { novelParams } = get();
|
||||||
|
set({ novelParams: { ...novelParams, ...params } });
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@ export default function MaskPage() {
|
||||||
response?.isPC && (
|
response?.isPC && (
|
||||||
<IconFont
|
<IconFont
|
||||||
onClick={() => router.push('/profile/mask/new')}
|
onClick={() => router.push('/profile/mask/new')}
|
||||||
size={32}
|
className="text-white cursor-pointer text-3xl"
|
||||||
className="text-white cursor-pointer"
|
|
||||||
type="icon-tianjia"
|
type="icon-tianjia"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import IconFont from '@/components/ui/iconFont';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
type ScrollBoxProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
contentClassName?: string; // 内容容器的样式类名
|
||||||
|
} & React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export default function ScrollBox(props: ScrollBoxProps) {
|
||||||
|
const { children, contentClassName, ...rest } = props;
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const leftRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const rightRef = useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
const left = leftRef.current;
|
||||||
|
const right = rightRef.current;
|
||||||
|
if (!left || !right) return;
|
||||||
|
if (container.scrollLeft <= 0) {
|
||||||
|
left.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
left.style.display = 'flex';
|
||||||
|
}
|
||||||
|
// 添加 1 像素的容差来处理浮点数精度问题
|
||||||
|
const isAtEnd = container.scrollLeft + container.clientWidth >= container.scrollWidth - 1;
|
||||||
|
if (isAtEnd) {
|
||||||
|
right.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
right.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleScroll();
|
||||||
|
}, [handleScroll]);
|
||||||
|
|
||||||
|
const scrollBy = (type: 'left' | 'right') => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
container.scrollBy({
|
||||||
|
left: type === 'left' ? -container.clientWidth * 0.8 : container.clientWidth * 0.8,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconClass =
|
||||||
|
'rounded-full absolute h-8 w-8 sm:h-12 sm:w-12 cursor-pointer items-center justify-center z-10';
|
||||||
|
const iconStyle: React.CSSProperties = {
|
||||||
|
background: 'var(--sys-color-surface-element-normal, rgba(251, 222, 255, 0.08))',
|
||||||
|
backdropFilter: 'blur(var(--sys-visualstyle-blur-s, 16px))',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('w-full max-w-full min-w-0 relative', rest.className)}>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
className={cn(
|
||||||
|
'w-full max-w-full min-w-0 overflow-x-auto overflow-y-hidden',
|
||||||
|
contentClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 左箭头 */}
|
||||||
|
<span
|
||||||
|
ref={leftRef}
|
||||||
|
className={cn(iconClass, '-left-4 sm:-left-6')}
|
||||||
|
style={iconStyle}
|
||||||
|
onClick={() => scrollBy('left')}
|
||||||
|
>
|
||||||
|
<IconFont type="icon-a-Frame194" className="sm:text-2xl" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* 右箭头 */}
|
||||||
|
<span
|
||||||
|
ref={rightRef}
|
||||||
|
className={cn(iconClass, '-right-4 sm:-right-6')}
|
||||||
|
style={iconStyle}
|
||||||
|
onClick={() => scrollBy('right')}
|
||||||
|
>
|
||||||
|
<IconFont type="icon-a-Frame195" className="sm:text-2xl" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import IconFont from '@/components/ui/iconFont';
|
||||||
|
import ScrollBox from './ScrollBox';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface StoryContentProps {
|
||||||
|
story?: any;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
export default function StoryContent(props: StoryContentProps) {
|
||||||
|
const { story } = props;
|
||||||
|
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const cList = [123, 123, 123, 123, 123, 123, 123, 123];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-full min-w-0">
|
||||||
|
<div className="flex w-full min-w-0 items-center justify-between gap-2 mb-3">
|
||||||
|
<span className="txt-title-m line-clamp-1 min-w-0 text-[rgba(208,232,255,1)]">
|
||||||
|
The Bossy CEO's Contract Lover
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="cursor-pointer flex items-center gap-1 shrink-0"
|
||||||
|
>
|
||||||
|
<span>More</span>
|
||||||
|
<IconFont type="icon-jiantou" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ScrollBox contentClassName="inline-flex w-max gap-3">
|
||||||
|
{cList.map((item, index) => (
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 w-40 h-55 bg-gray-800/40 rounded-lg flex items-center justify-center"
|
||||||
|
key={`Item-${index}`}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ScrollBox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useRef, useEffect, useState } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { formatNumberToKMB } from '@/lib/utils';
|
import { formatNumberToKMB } from '@/lib/utils';
|
||||||
import { Tag } from '@/components/ui/tag';
|
import { Tag } from '@/components/ui/tag';
|
||||||
import { Avatar, AvatarImage } from '@/components/ui/avatar';
|
import { Avatar, AvatarImage } from '@/components/ui/avatar';
|
||||||
|
|
@ -12,59 +12,55 @@ interface AIStandardCardProps {
|
||||||
disableHover?: boolean;
|
disableHover?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AIStandardCard: React.FC<AIStandardCardProps> = React.memo(
|
// 常量提取
|
||||||
({ character, disableHover = false }) => {
|
const MAX_DESCRIPTION_LINES = 7;
|
||||||
const { id, name, description, coverImage, headPortrait, tags } = character;
|
const MAX_VISIBLE_TAGS = 2;
|
||||||
|
|
||||||
const introContainerRef = useRef<HTMLDivElement>(null);
|
// 提取点赞数组件
|
||||||
const introTextRef = useRef<HTMLParagraphElement>(null);
|
const LikeCount: React.FC<{ count: number }> = ({ count }) => (
|
||||||
const [maxLines, setMaxLines] = useState<number>(6);
|
<div className="flex items-center gap-1 rounded-xs px-1 py-0.5">
|
||||||
|
<i className="iconfont icon-Like-fill" />
|
||||||
|
<span className="txt-label-s text-txt-primary-specialmap-normal">
|
||||||
|
{formatNumberToKMB(count)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
// 动态计算可用空间的行数
|
// 提取渐变遮罩层样式
|
||||||
useEffect(() => {
|
const gradientOverlayStyle = {
|
||||||
const calculateMaxLines = () => {
|
background:
|
||||||
if (introContainerRef.current) {
|
'var(--color-overlay-background, linear-gradient(180deg, rgba(33, 26, 43, 0.00) 0%, var(--sys-color-background-default, #211A2B) 100%))',
|
||||||
const containerHeight = introContainerRef.current.offsetHeight;
|
maskImage: 'linear-gradient(180deg, rgba(33, 26, 43, 0.00) 0%, #211A2B 100%)',
|
||||||
const lineHeight = 20; // 对应 leading-[20px]
|
|
||||||
const calculatedLines = Math.floor(containerHeight / lineHeight);
|
|
||||||
// 确保至少显示 1 行,最多不超过合理的行数
|
|
||||||
const finalLines = Math.max(1, Math.min(calculatedLines, 12));
|
|
||||||
setMaxLines(finalLines);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
calculateMaxLines();
|
const AIStandardCard: React.FC<AIStandardCardProps> = React.memo(
|
||||||
|
({ character, disableHover = false }) => {
|
||||||
// 监听窗口大小变化
|
const { id, name, description, coverImage, headPortrait, tags, likeCount = 0 } = character;
|
||||||
window.addEventListener('resize', calculateMaxLines);
|
|
||||||
return () => window.removeEventListener('resize', calculateMaxLines);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 获取显示的背景图片
|
// 获取显示的背景图片
|
||||||
const displayImage = coverImage;
|
const displayImage = coverImage;
|
||||||
|
|
||||||
|
// 可见标签
|
||||||
|
const visibleTags = useMemo(() => tags?.slice(0, MAX_VISIBLE_TAGS) || [], [tags]);
|
||||||
|
|
||||||
|
// 容器类名
|
||||||
|
const containerClassName = `relative flex shrink-0 grow basis-0 cursor-pointer flex-col content-stretch items-start justify-start gap-3 ${disableHover ? '' : 'group'}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/character/${id}`} className="h-full w-full" prefetch={false}>
|
<Link href={`/character/${id}`} className="h-full w-full" prefetch={false}>
|
||||||
<div
|
<div className={containerClassName}>
|
||||||
className={`relative flex shrink-0 grow basis-0 cursor-pointer flex-col content-stretch items-start justify-start gap-3 ${disableHover ? '' : 'group'}`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="relative aspect-[3/4] w-full shrink-0 overflow-hidden rounded-2xl bg-cover bg-top bg-no-repeat transition-transform"
|
className="relative aspect-[3/4] w-full shrink-0 overflow-hidden rounded-2xl bg-cover bg-top bg-no-repeat transition-transform"
|
||||||
style={{ backgroundImage: displayImage ? `url('${displayImage}')` : undefined }}
|
style={{ backgroundImage: displayImage ? `url('${displayImage}')` : undefined }}
|
||||||
>
|
>
|
||||||
<div className="relative box-border flex aspect-[3/4] size-full flex-col content-stretch items-end justify-end overflow-clip p-[12px]">
|
<div className="relative box-border flex aspect-[3/4] size-full flex-col content-stretch items-end justify-end overflow-clip p-3">
|
||||||
{/* 默认渐变遮罩层 */}
|
{/* 默认渐变遮罩层 */}
|
||||||
<div
|
<div
|
||||||
className={`absolute right-0 bottom-[-0.33px] left-0 flex flex-col items-start justify-start gap-2 px-3 pt-6 pb-3 transition-opacity duration-300 ${disableHover ? '' : 'group-hover:opacity-0'}`}
|
className={`absolute right-0 bottom-[-0.33px] left-0 flex flex-col items-start justify-start gap-2 px-3 pt-6 pb-3 transition-opacity duration-300 ${
|
||||||
|
disableHover ? '' : 'group-hover:opacity-0'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div className="absolute inset-0 backdrop-blur-3xl" style={gradientOverlayStyle} />
|
||||||
className="absolute inset-0 backdrop-blur-3xl"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
'var(--color-overlay-background, linear-gradient(180deg, rgba(33, 26, 43, 0.00) 0%, var(--sys-color-background-default, #211A2B) 100%))',
|
|
||||||
maskImage: 'linear-gradient(180deg, rgba(33, 26, 43, 0.00) 0%, #211A2B 100%)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* 角色信息区域 */}
|
{/* 角色信息区域 */}
|
||||||
<div className="relative flex w-full shrink-0 flex-col content-stretch items-start justify-start gap-2">
|
<div className="relative flex w-full shrink-0 flex-col content-stretch items-start justify-start gap-2">
|
||||||
|
|
@ -76,26 +72,22 @@ const AIStandardCard: React.FC<AIStandardCardProps> = React.memo(
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 标签 */}
|
||||||
|
{visibleTags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{!!tags?.length && (
|
|
||||||
<div className="relative flex shrink-0 flex-wrap content-start items-start justify-start gap-1">
|
<div className="relative flex shrink-0 flex-wrap content-start items-start justify-start gap-1">
|
||||||
{tags?.slice(0, 2).map((tag, index) => (
|
{visibleTags.map((tag, index) => (
|
||||||
<Tag key={index} size="small" className="backdrop-blur-[8px]">
|
<Tag key={tag.id || index} size="small" className="backdrop-blur-[8px]">
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 点赞数 */}
|
{/* 点赞数 */}
|
||||||
<div className="flex items-center gap-1 rounded-xs px-1 py-0.5">
|
<LikeCount count={likeCount} />
|
||||||
<i className="iconfont icon-Like-fill" />
|
|
||||||
<span className="txt-label-s text-txt-primary-specialmap-normal">
|
|
||||||
{formatNumberToKMB(1000)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -104,38 +96,28 @@ const AIStandardCard: React.FC<AIStandardCardProps> = React.memo(
|
||||||
<div className="absolute inset-0 flex flex-col items-start justify-end gap-2 bg-black/65 px-3 pt-6 pb-3 opacity-0 backdrop-blur-[8px] transition-opacity duration-300 group-hover:opacity-100">
|
<div className="absolute inset-0 flex flex-col items-start justify-end gap-2 bg-black/65 px-3 pt-6 pb-3 opacity-0 backdrop-blur-[8px] transition-opacity duration-300 group-hover:opacity-100">
|
||||||
{/* 头像和名称 */}
|
{/* 头像和名称 */}
|
||||||
<div className="flex w-full items-center gap-2">
|
<div className="flex w-full items-center gap-2">
|
||||||
<Avatar className="sh size-12">
|
<Avatar className="size-12">
|
||||||
<AvatarImage src={headPortrait} alt={name} />
|
<AvatarImage src={headPortrait} alt={name} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="txt-title-s min-w-0 flex-1 truncate">{name}</div>
|
<div className="txt-title-s min-w-0 flex-1 truncate">{name}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 简介文本 */}
|
{/* 简介文本 */}
|
||||||
<div
|
<div className="flex min-h-0 w-full flex-1 items-start justify-start gap-1">
|
||||||
ref={introContainerRef}
|
|
||||||
className="flex min-h-0 w-full flex-1 items-start justify-start gap-1"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="txt-body-m text-txt-secondary-normal w-full overflow-hidden"
|
className="txt-body-m text-txt-secondary-normal w-full overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
WebkitLineClamp: maxLines,
|
WebkitLineClamp: MAX_DESCRIPTION_LINES,
|
||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p ref={introTextRef} className="leading-[20px]">
|
<p className="leading-[20px]">{description || 'No description'}</p>
|
||||||
{description || 'No description'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 点赞数 */}
|
{/* 点赞数 */}
|
||||||
<div className="flex items-center gap-1 rounded-xs px-1 py-0.5">
|
<LikeCount count={likeCount} />
|
||||||
<i className="iconfont icon-Like-fill" />
|
|
||||||
<span className="txt-label-s text-txt-primary-specialmap-normal">
|
|
||||||
{formatNumberToKMB(100)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,30 +5,15 @@ import React from 'react';
|
||||||
type IconFontProps = {
|
type IconFontProps = {
|
||||||
/** 图标名称,对应 iconfont 中的图标 ID */
|
/** 图标名称,对应 iconfont 中的图标 ID */
|
||||||
type: string;
|
type: string;
|
||||||
/** 图标大小,如果不传则继承父元素的 font-size */
|
|
||||||
size?: number;
|
|
||||||
/** 图标颜色,默认 currentColor(继承父元素颜色) */
|
/** 图标颜色,默认 currentColor(继承父元素颜色) */
|
||||||
color?: string;
|
color?: string;
|
||||||
/** 额外的 className */
|
/** 额外的 className */
|
||||||
className?: string;
|
className?: string;
|
||||||
} & React.HTMLAttributes<SVGSVGElement>;
|
} & React.HTMLAttributes<SVGSVGElement>;
|
||||||
|
|
||||||
const IconFont = ({
|
const IconFont = ({ type, color = 'currentColor', className = '', ...props }: IconFontProps) => {
|
||||||
type,
|
|
||||||
size,
|
|
||||||
color = 'currentColor',
|
|
||||||
className = '',
|
|
||||||
...props
|
|
||||||
}: IconFontProps) => {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg className={`iconfont-v2 ${className}`} style={{ color }} aria-hidden="true" {...props}>
|
||||||
className={`iconfont-v2 ${className}`}
|
|
||||||
style={size ? { fontSize: size, color } : { color }}
|
|
||||||
aria-hidden="true"
|
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<use xlinkHref={`#${type}`}></use>
|
<use xlinkHref={`#${type}`}></use>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
export default {
|
|
||||||
common: {
|
|
||||||
search: '搜索',
|
|
||||||
cancel: '取消',
|
|
||||||
edit: '编辑',
|
|
||||||
select: '选择',
|
|
||||||
default: '默认',
|
|
||||||
gender: '性别',
|
|
||||||
gender_0: '男性',
|
|
||||||
gender_1: '女性',
|
|
||||||
gender_2: '其他',
|
|
||||||
filter: '筛选',
|
|
||||||
},
|
|
||||||
bottomBar: {
|
|
||||||
explore: '首页',
|
|
||||||
search: '搜索',
|
|
||||||
chat: '聊天',
|
|
||||||
me: '我的',
|
|
||||||
},
|
|
||||||
home: {
|
|
||||||
home: '首页',
|
|
||||||
character: '角色',
|
|
||||||
story: '故事',
|
|
||||||
mark_all: '已读全部',
|
|
||||||
clear: '清空聊天记录',
|
|
||||||
check_in: '签到',
|
|
||||||
check_in_desc: '每日签到,免费获取金币',
|
|
||||||
},
|
|
||||||
character: {
|
|
||||||
liked: '喜欢',
|
|
||||||
hot: '热度',
|
|
||||||
introduction: '人物简介',
|
|
||||||
},
|
|
||||||
chat: {
|
|
||||||
chats: '聊天',
|
|
||||||
drawer: {
|
|
||||||
maskedIdentityMode: '面具设置',
|
|
||||||
createMask: '创建面具',
|
|
||||||
history: '历史记录',
|
|
||||||
voiceActorTitle: '声优',
|
|
||||||
fontTitle: '字体',
|
|
||||||
maxToken: '最大回复',
|
|
||||||
background: '背景',
|
|
||||||
model: '聊天模型',
|
|
||||||
profile: {
|
|
||||||
preset: '预设',
|
|
||||||
gender: '性别',
|
|
||||||
age: '年龄',
|
|
||||||
whoAmI: '我是谁',
|
|
||||||
unfilled: '未填写',
|
|
||||||
chatSetting: '聊天设置',
|
|
||||||
voiceSetting: '语音设置',
|
|
||||||
chatModel: '聊天模型',
|
|
||||||
longText: '长文本',
|
|
||||||
maximumReplies: '最大回复数',
|
|
||||||
font: '字体',
|
|
||||||
chatBackground: '聊天背景',
|
|
||||||
voiceArtist: '声优',
|
|
||||||
delete: '删除',
|
|
||||||
newChat: '新建聊天',
|
|
||||||
},
|
|
||||||
chatModel: {
|
|
||||||
textMessagePrice: '文本消息价格',
|
|
||||||
textMessagePriceDesc:
|
|
||||||
'指通过文本消息与角色聊天的费用,包括发送文字、图片或礼物。按消息计费。',
|
|
||||||
voiceMessagePrice: '语音消息价格',
|
|
||||||
voiceMessagePriceDesc: '指向角色发送语音消息或播放角色语音的费用。按次计费。',
|
|
||||||
voiceCallPrice: '语音通话价格',
|
|
||||||
voiceCallPriceDesc: '指与角色进行语音通话的费用。按分钟计费。',
|
|
||||||
rolePlayDesc: '与 AI 进行角色扮演对话',
|
|
||||||
textMessage: '文本消息',
|
|
||||||
sendOrPlayVoice: '发送或播放语音',
|
|
||||||
voiceCallPerMin: '语音通话/分钟',
|
|
||||||
stayTuned: '敬请期待更多模型',
|
|
||||||
},
|
|
||||||
voiceActor: {
|
|
||||||
all: '全部',
|
|
||||||
male: '男性',
|
|
||||||
female: '女性',
|
|
||||||
description: '与 AI 进行角色扮演对话',
|
|
||||||
},
|
|
||||||
font: {
|
|
||||||
standard: '标准',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
profile: {
|
|
||||||
copiedToClipboard: '已复制到剪贴板',
|
|
||||||
vip: 'VIP',
|
|
||||||
notUnlocked: '未解锁',
|
|
||||||
wallet: '钱包',
|
|
||||||
editProfile: '编辑资料',
|
|
||||||
maskedIdentityMode: '面具身份模式',
|
|
||||||
account: '账户',
|
|
||||||
aboutUs: '关于我们',
|
|
||||||
termsOfServices: '服务条款',
|
|
||||||
privacyPolicy: '隐私政策',
|
|
||||||
logOut: '退出登录',
|
|
||||||
logOutConfirm: '确定要退出登录吗?',
|
|
||||||
avatarSetting: {
|
|
||||||
selectImageFile: '请选择图片文件',
|
|
||||||
gifNotSupported: '不支持 GIF 格式,请选择 JPG、JPEG 或 PNG 格式的图片',
|
|
||||||
imageSizeLimit: '图片文件不能超过 10MB',
|
|
||||||
uploadPrompt: '请上传 10MB 以内的 JPG、JPEG 或 PNG 图片。',
|
|
||||||
upload: '上传',
|
|
||||||
},
|
|
||||||
account_page: {
|
|
||||||
account: '账户',
|
|
||||||
disableAccount: '停用账户',
|
|
||||||
disableAccountConfirm: '确定要停用您的账户吗?',
|
|
||||||
cancel: '取消',
|
|
||||||
disable: '停用',
|
|
||||||
accountDisabledSuccess: '账户已成功停用',
|
|
||||||
},
|
|
||||||
edit: {
|
|
||||||
editProfile: '编辑资料',
|
|
||||||
nickname: '昵称',
|
|
||||||
enterNickname: '输入昵称',
|
|
||||||
nicknameRequired: '昵称不能为空',
|
|
||||||
nicknameLength: '昵称长度必须在 2 到 20 个字符之间',
|
|
||||||
gender: '性别',
|
|
||||||
selectGender: '请选择性别',
|
|
||||||
genderNote: '请注意:性别设置后不可更改',
|
|
||||||
age: '年龄',
|
|
||||||
year: '年',
|
|
||||||
month: '月',
|
|
||||||
day: '日',
|
|
||||||
selectBirthYear: '请选择出生年份',
|
|
||||||
selectBirthMonth: '请选择出生月份',
|
|
||||||
selectBirthDay: '请选择出生日期',
|
|
||||||
ageLimit: '角色年龄必须至少为 18 岁',
|
|
||||||
nicknameTaken: '此昵称已被使用',
|
|
||||||
save: '保存',
|
|
||||||
},
|
|
||||||
mask: {
|
|
||||||
maskedIdentityMode: '面具身份模式',
|
|
||||||
addNewMask: '+ 添加新面具',
|
|
||||||
nickname: '昵称',
|
|
||||||
enterNickname: '输入昵称',
|
|
||||||
nicknameRequired: '昵称不能为空',
|
|
||||||
nicknameLength: '昵称长度必须在 2 到 20 个字符之间',
|
|
||||||
gender: '性别',
|
|
||||||
selectGender: '请选择性别',
|
|
||||||
genderNote: '请注意:性别设置后不可更改',
|
|
||||||
age: '年龄',
|
|
||||||
enterAge: '输入年龄',
|
|
||||||
ageRequired: '年龄不能为空',
|
|
||||||
whoAmI: '我是谁(可选)',
|
|
||||||
whoAmIPlaceholder: '描述你的角色特征和场景设定',
|
|
||||||
save: '保存',
|
|
||||||
male: '男性',
|
|
||||||
female: '女性',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
crushcoin: {
|
|
||||||
title: '每日免费金币',
|
|
||||||
consecutiveDays: '你已经连续签到 {days} 天',
|
|
||||||
description1: '金币可用于支付聊天服务和解锁其他物品。',
|
|
||||||
description2: '如果错过签到,签到计数将重置并从第一天重新开始。',
|
|
||||||
day: '第 {day} 天',
|
|
||||||
checked: '已签到',
|
|
||||||
notStarted: '未开始',
|
|
||||||
loading: '签到中ing',
|
|
||||||
checked_success: '签到成功',
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
slogan: '用 Spicyxx.AI 成长你的爱情故事——从"你好"到"我愿意",每一次对话都点燃火花',
|
|
||||||
features: '功能',
|
|
||||||
recharge: '充值',
|
|
||||||
crushLevelVip: 'Spicyxx.AI VIP',
|
|
||||||
explore: '探索',
|
|
||||||
dailyFreeCrushCoins: '每日免费金币',
|
|
||||||
aboutUs: '关于我们',
|
|
||||||
contactUs: '联系我们',
|
|
||||||
legal: '法律',
|
|
||||||
userAgreement: '用户协议',
|
|
||||||
privacyPolicy: '隐私政策',
|
|
||||||
copyright: '版权所有 © {year} Spicyxx.AI. 保留所有权利',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
export default {
|
||||||
|
common: {
|
||||||
|
search: '搜尋',
|
||||||
|
cancel: '取消',
|
||||||
|
edit: '編輯',
|
||||||
|
select: '選擇',
|
||||||
|
default: '預設',
|
||||||
|
gender: '性別',
|
||||||
|
gender_0: '男性',
|
||||||
|
gender_1: '女性',
|
||||||
|
gender_2: '其他',
|
||||||
|
filter: '篩選',
|
||||||
|
},
|
||||||
|
bottomBar: {
|
||||||
|
explore: '首頁',
|
||||||
|
search: '搜尋',
|
||||||
|
chat: '聊天',
|
||||||
|
me: '我的',
|
||||||
|
},
|
||||||
|
home: {
|
||||||
|
home: '首頁',
|
||||||
|
character: '角色',
|
||||||
|
story: '故事',
|
||||||
|
mark_all: '已讀全部',
|
||||||
|
clear: '清空聊天記錄',
|
||||||
|
check_in: '簽到',
|
||||||
|
check_in_desc: '每日簽到,免費獲取金幣',
|
||||||
|
},
|
||||||
|
character: {
|
||||||
|
liked: '喜歡',
|
||||||
|
hot: '熱度',
|
||||||
|
introduction: '人物簡介',
|
||||||
|
},
|
||||||
|
chat: {
|
||||||
|
chats: '聊天',
|
||||||
|
drawer: {
|
||||||
|
maskedIdentityMode: '面具設定',
|
||||||
|
createMask: '建立面具',
|
||||||
|
history: '歷史記錄',
|
||||||
|
voiceActorTitle: '聲優',
|
||||||
|
fontTitle: '字型',
|
||||||
|
maxToken: '最大回覆',
|
||||||
|
background: '背景',
|
||||||
|
model: '聊天模型',
|
||||||
|
profile: {
|
||||||
|
preset: '預設',
|
||||||
|
gender: '性別',
|
||||||
|
age: '年齡',
|
||||||
|
whoAmI: '我是誰',
|
||||||
|
unfilled: '未填寫',
|
||||||
|
chatSetting: '聊天設定',
|
||||||
|
voiceSetting: '語音設定',
|
||||||
|
chatModel: '聊天模型',
|
||||||
|
longText: '長文字',
|
||||||
|
maximumReplies: '最大回覆數',
|
||||||
|
font: '字型',
|
||||||
|
chatBackground: '聊天背景',
|
||||||
|
voiceArtist: '聲優',
|
||||||
|
delete: '刪除',
|
||||||
|
newChat: '新增聊天',
|
||||||
|
},
|
||||||
|
chatModel: {
|
||||||
|
textMessagePrice: '文字訊息價格',
|
||||||
|
textMessagePriceDesc:
|
||||||
|
'指透過文字訊息與角色聊天的費用,包括傳送文字、圖片或禮物。按訊息計費。',
|
||||||
|
voiceMessagePrice: '語音訊息價格',
|
||||||
|
voiceMessagePriceDesc: '指向角色傳送語音訊息或播放角色語音的費用。按次計費。',
|
||||||
|
voiceCallPrice: '語音通話價格',
|
||||||
|
voiceCallPriceDesc: '指與角色進行語音通話的費用。按分鐘計費。',
|
||||||
|
rolePlayDesc: '與 AI 進行角色扮演對話',
|
||||||
|
textMessage: '文字訊息',
|
||||||
|
sendOrPlayVoice: '傳送或播放語音',
|
||||||
|
voiceCallPerMin: '語音通話/分鐘',
|
||||||
|
stayTuned: '敬請期待更多模型',
|
||||||
|
},
|
||||||
|
voiceActor: {
|
||||||
|
all: '全部',
|
||||||
|
male: '男性',
|
||||||
|
female: '女性',
|
||||||
|
description: '與 AI 進行角色扮演對話',
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
standard: '標準',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
copiedToClipboard: '已複製到剪貼簿',
|
||||||
|
vip: 'VIP',
|
||||||
|
notUnlocked: '未解鎖',
|
||||||
|
wallet: '錢包',
|
||||||
|
editProfile: '編輯資料',
|
||||||
|
maskedIdentityMode: '面具身份模式',
|
||||||
|
account: '帳戶',
|
||||||
|
aboutUs: '關於我們',
|
||||||
|
termsOfServices: '服務條款',
|
||||||
|
privacyPolicy: '隱私權政策',
|
||||||
|
logOut: '登出',
|
||||||
|
logOutConfirm: '確定要登出嗎?',
|
||||||
|
avatarSetting: {
|
||||||
|
selectImageFile: '請選擇圖片檔案',
|
||||||
|
gifNotSupported: '不支援 GIF 格式,請選擇 JPG、JPEG 或 PNG 格式的圖片',
|
||||||
|
imageSizeLimit: '圖片檔案不能超過 10MB',
|
||||||
|
uploadPrompt: '請上傳 10MB 以內的 JPG、JPEG 或 PNG 圖片。',
|
||||||
|
upload: '上傳',
|
||||||
|
},
|
||||||
|
account_page: {
|
||||||
|
account: '帳戶',
|
||||||
|
disableAccount: '停用帳戶',
|
||||||
|
disableAccountConfirm: '確定要停用您的帳戶嗎?',
|
||||||
|
cancel: '取消',
|
||||||
|
disable: '停用',
|
||||||
|
accountDisabledSuccess: '帳戶已成功停用',
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
editProfile: '編輯資料',
|
||||||
|
nickname: '暱稱',
|
||||||
|
enterNickname: '輸入暱稱',
|
||||||
|
nicknameRequired: '暱稱不能為空',
|
||||||
|
nicknameLength: '暱稱長度必須在 2 到 20 個字元之間',
|
||||||
|
gender: '性別',
|
||||||
|
selectGender: '請選擇性別',
|
||||||
|
genderNote: '請注意:性別設定後不可更改',
|
||||||
|
age: '年齡',
|
||||||
|
year: '年',
|
||||||
|
month: '月',
|
||||||
|
day: '日',
|
||||||
|
selectBirthYear: '請選擇出生年份',
|
||||||
|
selectBirthMonth: '請選擇出生月份',
|
||||||
|
selectBirthDay: '請選擇出生日期',
|
||||||
|
ageLimit: '角色年齡必須至少為 18 歲',
|
||||||
|
nicknameTaken: '此暱稱已被使用',
|
||||||
|
save: '儲存',
|
||||||
|
},
|
||||||
|
mask: {
|
||||||
|
maskedIdentityMode: '面具身份模式',
|
||||||
|
addNewMask: '+ 新增面具',
|
||||||
|
nickname: '暱稱',
|
||||||
|
enterNickname: '輸入暱稱',
|
||||||
|
nicknameRequired: '暱稱不能為空',
|
||||||
|
nicknameLength: '暱稱長度必須在 2 到 20 個字元之間',
|
||||||
|
gender: '性別',
|
||||||
|
selectGender: '請選擇性別',
|
||||||
|
genderNote: '請注意:性別設定後不可更改',
|
||||||
|
age: '年齡',
|
||||||
|
enterAge: '輸入年齡',
|
||||||
|
ageRequired: '年齡不能為空',
|
||||||
|
whoAmI: '我是誰(可選)',
|
||||||
|
whoAmIPlaceholder: '描述你的角色特徵和場景設定',
|
||||||
|
save: '儲存',
|
||||||
|
male: '男性',
|
||||||
|
female: '女性',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
crushcoin: {
|
||||||
|
title: '每日免費金幣',
|
||||||
|
consecutiveDays: '你已經連續簽到 {days} 天',
|
||||||
|
description1: '金幣可用於支付聊天服務和解鎖其他物品。',
|
||||||
|
description2: '如果錯過簽到,簽到計數將重置並從第一天重新開始。',
|
||||||
|
day: '第 {day} 天',
|
||||||
|
checked: '已簽到',
|
||||||
|
notStarted: '未開始',
|
||||||
|
loading: '簽到中ing',
|
||||||
|
checked_success: '簽到成功',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
slogan: '用 Spicyxx.AI 成長你的愛情故事——從「你好」到「我願意」,每一次對話都點燃火花',
|
||||||
|
features: '功能',
|
||||||
|
recharge: '儲值',
|
||||||
|
crushLevelVip: 'Spicyxx.AI VIP',
|
||||||
|
explore: '探索',
|
||||||
|
dailyFreeCrushCoins: '每日免費金幣',
|
||||||
|
aboutUs: '關於我們',
|
||||||
|
contactUs: '聯絡我們',
|
||||||
|
legal: '法律',
|
||||||
|
userAgreement: '使用者協議',
|
||||||
|
privacyPolicy: '隱私權政策',
|
||||||
|
copyright: '版權所有 © {year} Spicyxx.AI. 保留所有權利',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -13,12 +13,12 @@ export default function LocaleSwitch() {
|
||||||
const { locale, setLocale } = useLocale();
|
const { locale, setLocale } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select value={locale} onValueChange={(value) => setLocale(value as 'zh-CN' | 'en-US')}>
|
<Select value={locale} onValueChange={(value) => setLocale(value as 'zh-TW' | 'en-US')}>
|
||||||
<SelectTrigger className="rounded-full h-8 w-16 px-3">
|
<SelectTrigger className="rounded-full h-8 w-16 px-3">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="min-w-0 w-22">
|
<SelectContent className="min-w-0 w-22">
|
||||||
<SelectItem value="zh-CN">中</SelectItem>
|
<SelectItem value="zh-TW">繁</SelectItem>
|
||||||
<SelectItem value="en-US">En</SelectItem>
|
<SelectItem value="en-US">En</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,11 @@ export default function ConditionalLayout({ children }: ConditionalLayoutProps)
|
||||||
useAutoScrollOnRouteChange(mainContentRef);
|
useAutoScrollOnRouteChange(mainContentRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-[url('/common-bg.png')] bg-[length:100%_30%] bg-top bg-no-repeat">
|
<div className="flex h-screen w-screen bg-[url('/common-bg.png')] bg-[length:100%_30%] bg-top bg-no-repeat overflow-hidden">
|
||||||
{response?.isPC && <Sidebar />}
|
{response?.isPC && <Sidebar />}
|
||||||
<div ref={mainContentRef} className={cn('relative flex flex-1 flex-col')}>
|
<div ref={mainContentRef} className={cn('relative flex w-full flex-1 flex-col min-w-0')}>
|
||||||
<Topbar />
|
<Topbar />
|
||||||
<main id="main-content" className="overflow-auto flex-1">
|
<main id="main-content" className="overflow-auto w-full flex-1 min-w-0">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
{response?.isMobile && <BottomBar />}
|
{response?.isMobile && <BottomBar />}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import Cookies from 'js-cookie';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { useLayoutStore } from '@/stores';
|
import { useLayoutStore } from '@/stores';
|
||||||
|
|
||||||
type Locale = 'zh-CN' | 'en-US';
|
type Locale = 'zh-TW' | 'en-US';
|
||||||
interface LocaleContextType {
|
interface LocaleContextType {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
setLocale: (locale: Locale) => void;
|
setLocale: (locale: Locale) => void;
|
||||||
|
|
@ -33,7 +33,7 @@ function setLocaleToCookie(locale: Locale) {
|
||||||
|
|
||||||
function getLocaleFromCookie(): Locale {
|
function getLocaleFromCookie(): Locale {
|
||||||
const cookieLocale = Cookies.get('locale') as Locale | undefined;
|
const cookieLocale = Cookies.get('locale') as Locale | undefined;
|
||||||
if (cookieLocale && (cookieLocale === 'zh-CN' || cookieLocale === 'en-US')) {
|
if (cookieLocale && (cookieLocale === 'zh-TW' || cookieLocale === 'en-US')) {
|
||||||
return cookieLocale;
|
return cookieLocale;
|
||||||
}
|
}
|
||||||
setLocaleToCookie('en-US');
|
setLocaleToCookie('en-US');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue