feat: 简体中文切换为繁体;优化角色卡;
This commit is contained in:
parent
8b933d26d6
commit
5cb01064ef
|
|
@ -77,7 +77,7 @@ export default function SettingDialog({ open, onOpenChange }: SettingProps) {
|
|||
)}
|
||||
{activeTab !== '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>
|
||||
)}
|
||||
</AlertDialogTitle>
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export default function Input() {
|
|||
<div className="flex w-full items-end gap-4">
|
||||
{/* 打电话按钮 */}
|
||||
<IconButton onClick={() => {}}>
|
||||
<IconFont type="icon-dianhua" size={30} />
|
||||
<IconFont type="icon-dianhua" className="text-3xl" />
|
||||
</IconButton>
|
||||
<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"
|
||||
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>
|
||||
</div>
|
||||
<SettingDialog open={settingOpen} onOpenChange={setSettingOpen} />
|
||||
|
|
|
|||
|
|
@ -11,12 +11,11 @@ import { TagListSelect } from './TagListSelect';
|
|||
import { MoreFilter } from './MoreFilter';
|
||||
|
||||
const Filter = () => {
|
||||
const tab = useHomeStore((state) => state.tab);
|
||||
const setTab = useHomeStore((state) => state.setTab);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const characterParams = useHomeStore((state) => state.characterParams);
|
||||
const setCharacterParams = useHomeStore((state) => state.setCharacterParams);
|
||||
const { tab, setTab, characterParams, novelParams, setCharacterParams, setNovelParams } =
|
||||
useHomeStore();
|
||||
const t = useTranslations('home');
|
||||
const currentTabTagIds = tab === 'character' ? characterParams.tagIds : novelParams.tagIds;
|
||||
|
||||
const { data: tags = [] } = useQuery({
|
||||
queryKey: ['tags', tab],
|
||||
|
|
@ -71,12 +70,16 @@ const Filter = () => {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
<MoreFilter />
|
||||
{tab === 'character' && <MoreFilter />}
|
||||
</div>
|
||||
<TagListSelect
|
||||
options={tags.map((tag: any) => ({ label: tag.name, value: tag.id }))}
|
||||
value={characterParams.tagIds || []}
|
||||
onChange={(values) => setCharacterParams({ tagIds: values as string[] })}
|
||||
value={currentTabTagIds || []}
|
||||
onChange={(values) =>
|
||||
tab === 'character'
|
||||
? setCharacterParams({ tagIds: values as string[] })
|
||||
: setNovelParams({ tagIds: values as string[] })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,41 @@
|
|||
'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 = () => {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ const HomePage = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full text-txt-primary-normal relative px-4 sm:px-16">
|
||||
<div className="mx-auto max-w-[1136px] pb-32">
|
||||
<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 w-full">
|
||||
<Header />
|
||||
<Filter />
|
||||
{tab === 'story' ? <Story /> : <Character />}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,17 @@ type CharacterParams = {
|
|||
tagIds?: string[];
|
||||
};
|
||||
|
||||
type NovelParams = {
|
||||
tagIds?: string[];
|
||||
};
|
||||
|
||||
interface HomeStore {
|
||||
tab: 'story' | 'character';
|
||||
characterParams: CharacterParams;
|
||||
novelParams: NovelParams;
|
||||
setTab: (tab: 'story' | 'character') => void;
|
||||
setCharacterParams: (params: Partial<CharacterParams>) => void;
|
||||
setNovelParams: (params: Partial<NovelParams>) => void;
|
||||
}
|
||||
|
||||
export const useHomeStore = create<HomeStore>((set, get) => ({
|
||||
|
|
@ -18,9 +24,16 @@ export const useHomeStore = create<HomeStore>((set, get) => ({
|
|||
genders: [],
|
||||
tagIds: [],
|
||||
},
|
||||
novelParams: {
|
||||
tagIds: [],
|
||||
},
|
||||
setTab: (tab: 'story' | 'character') => set({ tab }),
|
||||
setCharacterParams: (params) => {
|
||||
const { characterParams } = get();
|
||||
set({ characterParams: { ...characterParams, ...params } });
|
||||
},
|
||||
setNovelParams: (params) => {
|
||||
const { novelParams } = get();
|
||||
set({ novelParams: { ...novelParams, ...params } });
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ export default function MaskPage() {
|
|||
response?.isPC && (
|
||||
<IconFont
|
||||
onClick={() => router.push('/profile/mask/new')}
|
||||
size={32}
|
||||
className="text-white cursor-pointer"
|
||||
className="text-white cursor-pointer text-3xl"
|
||||
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';
|
||||
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { formatNumberToKMB } from '@/lib/utils';
|
||||
import { Tag } from '@/components/ui/tag';
|
||||
import { Avatar, AvatarImage } from '@/components/ui/avatar';
|
||||
|
|
@ -12,59 +12,55 @@ interface AIStandardCardProps {
|
|||
disableHover?: boolean;
|
||||
}
|
||||
|
||||
const AIStandardCard: React.FC<AIStandardCardProps> = React.memo(
|
||||
({ character, disableHover = false }) => {
|
||||
const { id, name, description, coverImage, headPortrait, tags } = character;
|
||||
// 常量提取
|
||||
const MAX_DESCRIPTION_LINES = 7;
|
||||
const MAX_VISIBLE_TAGS = 2;
|
||||
|
||||
const introContainerRef = useRef<HTMLDivElement>(null);
|
||||
const introTextRef = useRef<HTMLParagraphElement>(null);
|
||||
const [maxLines, setMaxLines] = useState<number>(6);
|
||||
// 提取点赞数组件
|
||||
const LikeCount: React.FC<{ count: number }> = ({ count }) => (
|
||||
<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 calculateMaxLines = () => {
|
||||
if (introContainerRef.current) {
|
||||
const containerHeight = introContainerRef.current.offsetHeight;
|
||||
const lineHeight = 20; // 对应 leading-[20px]
|
||||
const calculatedLines = Math.floor(containerHeight / lineHeight);
|
||||
// 确保至少显示 1 行,最多不超过合理的行数
|
||||
const finalLines = Math.max(1, Math.min(calculatedLines, 12));
|
||||
setMaxLines(finalLines);
|
||||
}
|
||||
// 提取渐变遮罩层样式
|
||||
const gradientOverlayStyle = {
|
||||
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%)',
|
||||
};
|
||||
|
||||
calculateMaxLines();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', calculateMaxLines);
|
||||
return () => window.removeEventListener('resize', calculateMaxLines);
|
||||
}, []);
|
||||
const AIStandardCard: React.FC<AIStandardCardProps> = React.memo(
|
||||
({ character, disableHover = false }) => {
|
||||
const { id, name, description, coverImage, headPortrait, tags, likeCount = 0 } = character;
|
||||
|
||||
// 获取显示的背景图片
|
||||
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 (
|
||||
<Link href={`/character/${id}`} className="h-full w-full" prefetch={false}>
|
||||
<div
|
||||
className={`relative flex shrink-0 grow basis-0 cursor-pointer flex-col content-stretch items-start justify-start gap-3 ${disableHover ? '' : 'group'}`}
|
||||
>
|
||||
<div className={containerClassName}>
|
||||
<div
|
||||
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 }}
|
||||
>
|
||||
<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
|
||||
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
|
||||
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="absolute inset-0 backdrop-blur-3xl" style={gradientOverlayStyle} />
|
||||
<div className="relative">
|
||||
{/* 角色信息区域 */}
|
||||
<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}
|
||||
</div>
|
||||
|
||||
{/* 标签 */}
|
||||
{visibleTags.length > 0 && (
|
||||
<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">
|
||||
{tags?.slice(0, 2).map((tag, index) => (
|
||||
<Tag key={index} size="small" className="backdrop-blur-[8px]">
|
||||
{visibleTags.map((tag, index) => (
|
||||
<Tag key={tag.id || index} size="small" className="backdrop-blur-[8px]">
|
||||
{tag.name}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 点赞数 */}
|
||||
<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(1000)}
|
||||
</span>
|
||||
</div>
|
||||
<LikeCount count={likeCount} />
|
||||
</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="flex w-full items-center gap-2">
|
||||
<Avatar className="sh size-12">
|
||||
<Avatar className="size-12">
|
||||
<AvatarImage src={headPortrait} alt={name} />
|
||||
</Avatar>
|
||||
<div className="txt-title-s min-w-0 flex-1 truncate">{name}</div>
|
||||
</div>
|
||||
|
||||
{/* 简介文本 */}
|
||||
<div
|
||||
ref={introContainerRef}
|
||||
className="flex min-h-0 w-full flex-1 items-start justify-start gap-1"
|
||||
>
|
||||
<div className="flex min-h-0 w-full flex-1 items-start justify-start gap-1">
|
||||
<div
|
||||
className="txt-body-m text-txt-secondary-normal w-full overflow-hidden"
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: maxLines,
|
||||
WebkitLineClamp: MAX_DESCRIPTION_LINES,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
<p ref={introTextRef} className="leading-[20px]">
|
||||
{description || 'No description'}
|
||||
</p>
|
||||
<p className="leading-[20px]">{description || 'No description'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 点赞数 */}
|
||||
<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(100)}
|
||||
</span>
|
||||
</div>
|
||||
<LikeCount count={likeCount} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,30 +5,15 @@ import React from 'react';
|
|||
type IconFontProps = {
|
||||
/** 图标名称,对应 iconfont 中的图标 ID */
|
||||
type: string;
|
||||
/** 图标大小,如果不传则继承父元素的 font-size */
|
||||
size?: number;
|
||||
/** 图标颜色,默认 currentColor(继承父元素颜色) */
|
||||
color?: string;
|
||||
/** 额外的 className */
|
||||
className?: string;
|
||||
} & React.HTMLAttributes<SVGSVGElement>;
|
||||
|
||||
const IconFont = ({
|
||||
type,
|
||||
size,
|
||||
color = 'currentColor',
|
||||
className = '',
|
||||
...props
|
||||
}: IconFontProps) => {
|
||||
const IconFont = ({ type, color = 'currentColor', className = '', ...props }: IconFontProps) => {
|
||||
return (
|
||||
<svg
|
||||
className={`iconfont-v2 ${className}`}
|
||||
style={size ? { fontSize: size, color } : { color }}
|
||||
aria-hidden="true"
|
||||
width={size}
|
||||
height={size}
|
||||
{...props}
|
||||
>
|
||||
<svg className={`iconfont-v2 ${className}`} style={{ color }} aria-hidden="true" {...props}>
|
||||
<use xlinkHref={`#${type}`}></use>
|
||||
</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();
|
||||
|
||||
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">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="min-w-0 w-22">
|
||||
<SelectItem value="zh-CN">中</SelectItem>
|
||||
<SelectItem value="zh-TW">繁</SelectItem>
|
||||
<SelectItem value="en-US">En</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
|
|
|||
|
|
@ -55,11 +55,11 @@ export default function ConditionalLayout({ children }: ConditionalLayoutProps)
|
|||
useAutoScrollOnRouteChange(mainContentRef);
|
||||
|
||||
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 />}
|
||||
<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 />
|
||||
<main id="main-content" className="overflow-auto flex-1">
|
||||
<main id="main-content" className="overflow-auto w-full flex-1 min-w-0">
|
||||
{children}
|
||||
</main>
|
||||
{response?.isMobile && <BottomBar />}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import Cookies from 'js-cookie';
|
|||
import { useMemoizedFn } from 'ahooks';
|
||||
import { useLayoutStore } from '@/stores';
|
||||
|
||||
type Locale = 'zh-CN' | 'en-US';
|
||||
type Locale = 'zh-TW' | 'en-US';
|
||||
interface LocaleContextType {
|
||||
locale: Locale;
|
||||
setLocale: (locale: Locale) => void;
|
||||
|
|
@ -33,7 +33,7 @@ function setLocaleToCookie(locale: Locale) {
|
|||
|
||||
function getLocaleFromCookie(): Locale {
|
||||
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;
|
||||
}
|
||||
setLocaleToCookie('en-US');
|
||||
|
|
|
|||
Loading…
Reference in New Issue