feat: i18n 翻译覆盖 + DB 动态数据 + 模型选择器 API
This commit is contained in:
@@ -10,12 +10,14 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { useT } from '@/i18n';
|
||||
import { API_BASE } from '@/lib/config';
|
||||
|
||||
function AuthForm() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
const t = useT();
|
||||
const [tab, setTab] = useState<'login' | 'register'>(() =>
|
||||
searchParams.get('tab') === 'register' ? 'register' : 'login'
|
||||
);
|
||||
@@ -28,7 +30,7 @@ function AuthForm() {
|
||||
async function handleLogin(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
if (!loginForm.account || !loginForm.password) { setError('请填写账号和密码'); return; }
|
||||
if (!loginForm.account || !loginForm.password) { setError(t.auth.fillAccountAndPassword); return; }
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||
@@ -36,9 +38,9 @@ function AuthForm() {
|
||||
body: JSON.stringify({ account: loginForm.account, password: loginForm.password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.message || '登录失败');
|
||||
if (!res.ok) throw new Error(data.message || t.auth.loginFailed);
|
||||
login(data.accessToken, data.refreshToken);
|
||||
toast.success('登录成功', { description: '欢迎回来!' });
|
||||
toast.success(t.auth.loginSuccess);
|
||||
router.push('/');
|
||||
} catch (err: any) { setError(err.message); }
|
||||
finally { setLoading(false); }
|
||||
@@ -48,10 +50,10 @@ function AuthForm() {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
const { phone, email, password, confirmPassword, nickname } = registerForm;
|
||||
if (!phone && !email) { setError('请填写手机号或邮箱'); return; }
|
||||
if (!password) { setError('请填写密码'); return; }
|
||||
if (password.length < 6) { setError('密码至少 6 位'); return; }
|
||||
if (password !== confirmPassword) { setError('两次密码不一致'); return; }
|
||||
if (!phone && !email) { setError(t.auth.fillPhoneOrEmail); return; }
|
||||
if (!password) { setError(t.auth.fillPassword); return; }
|
||||
if (password.length < 6) { setError(t.auth.passwordMinLength); return; }
|
||||
if (password !== confirmPassword) { setError(t.auth.passwordsNotMatch); return; }
|
||||
setLoading(true);
|
||||
try {
|
||||
const body: Record<string, string> = { password };
|
||||
@@ -62,9 +64,9 @@ function AuthForm() {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.message || '注册失败');
|
||||
if (!res.ok) throw new Error(data.message || t.auth.registerFailed);
|
||||
login(data.accessToken, data.refreshToken);
|
||||
toast.success('注册成功', { description: '欢迎加入宇之然!' });
|
||||
toast.success(t.auth.registerSuccess);
|
||||
router.push('/');
|
||||
} catch (err: any) { setError(err.message); }
|
||||
finally { setLoading(false); }
|
||||
@@ -77,16 +79,16 @@ function AuthForm() {
|
||||
<div className="mx-auto mb-3 w-12 h-12 bg-gradient-to-br from-brand-500 to-brand-700 rounded-2xl flex items-center justify-center">
|
||||
<span className="text-white font-bold text-lg">Y</span>
|
||||
</div>
|
||||
<CardTitle className="text-xl">{tab === 'login' ? '欢迎回来' : '加入宇之然'}</CardTitle>
|
||||
<CardTitle className="text-xl">{tab === 'login' ? t.auth.welcomeBack : t.auth.joinTitle}</CardTitle>
|
||||
<CardDescription>
|
||||
{tab === 'login' ? '登录继续你的 AI 探索之旅' : '免费注册,开始探索 AI'}
|
||||
{tab === 'login' ? t.auth.loginSubtitle : t.auth.registerSubtitle}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs value={tab} onValueChange={(v) => { setTab(v as 'login' | 'register'); setError(''); }}>
|
||||
<TabsList className="w-full mb-6">
|
||||
<TabsTrigger value="login" className="flex-1">登录</TabsTrigger>
|
||||
<TabsTrigger value="register" className="flex-1">注册</TabsTrigger>
|
||||
<TabsTrigger value="login" className="flex-1">{t.auth.loginTitle}</TabsTrigger>
|
||||
<TabsTrigger value="register" className="flex-1">{t.auth.registerTitle}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{error && (
|
||||
@@ -97,38 +99,38 @@ function AuthForm() {
|
||||
|
||||
<TabsContent value="login">
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<Input type="text" placeholder="手机号 / 邮箱" value={loginForm.account}
|
||||
<Input type="text" placeholder={t.auth.accountPlaceholder} value={loginForm.account}
|
||||
onChange={(e) => setLoginForm({ ...loginForm, account: e.target.value })} />
|
||||
<Input type="password" placeholder="密码" value={loginForm.password}
|
||||
<Input type="password" placeholder={t.auth.password} value={loginForm.password}
|
||||
onChange={(e) => setLoginForm({ ...loginForm, password: e.target.value })} />
|
||||
<Button type="submit" disabled={loading} className="w-full">
|
||||
{loading ? '登录中...' : '登录'}
|
||||
{loading ? t.auth.loggingIn : t.auth.loginTitle}
|
||||
</Button>
|
||||
</form>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="register">
|
||||
<form onSubmit={handleRegister} className="space-y-4">
|
||||
<Input type="text" placeholder="手机号(选填)" value={registerForm.phone}
|
||||
<Input type="text" placeholder={t.auth.phone} value={registerForm.phone}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, phone: e.target.value })} />
|
||||
<Input type="email" placeholder="邮箱(选填,与手机号至少填一项)" value={registerForm.email}
|
||||
<Input type="email" placeholder="Email" value={registerForm.email}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, email: e.target.value })} />
|
||||
<Input type="text" placeholder="昵称(选填)" value={registerForm.nickname}
|
||||
<Input type="text" placeholder={t.auth.nicknameOptional} value={registerForm.nickname}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, nickname: e.target.value })} />
|
||||
<Input type="password" placeholder="密码(至少 6 位)" value={registerForm.password}
|
||||
<Input type="password" placeholder={t.auth.passwordHint} value={registerForm.password}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, password: e.target.value })} />
|
||||
<Input type="password" placeholder="确认密码" value={registerForm.confirmPassword}
|
||||
<Input type="password" placeholder={t.auth.confirmPassword} value={registerForm.confirmPassword}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, confirmPassword: e.target.value })} />
|
||||
<Button type="submit" disabled={loading} className="w-full">
|
||||
{loading ? '注册中...' : '注册'}
|
||||
{loading ? t.auth.registering : t.auth.registerTitle}
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground text-center leading-relaxed">
|
||||
注册即表示同意{' '}
|
||||
<Link href="/terms" className="text-brand-600 hover:underline dark:text-brand-400">服务协议</Link>
|
||||
{' '}和{' '}
|
||||
<Link href="/privacy" className="text-brand-600 hover:underline dark:text-brand-400">隐私政策</Link>
|
||||
{' '}和{' '}
|
||||
<Link href="/ai-agreement" className="text-brand-600 hover:underline dark:text-brand-400">AI 服务协议</Link>
|
||||
{t.auth.agreePrefix}{' '}
|
||||
<Link href="/terms" className="text-brand-600 hover:underline dark:text-brand-400">{t.auth.termsOfService}</Link>
|
||||
{' '}{' '}
|
||||
<Link href="/privacy" className="text-brand-600 hover:underline dark:text-brand-400">{t.auth.privacyPolicy}</Link>
|
||||
{' '}{' '}
|
||||
<Link href="/ai-agreement" className="text-brand-600 hover:underline dark:text-brand-400">{t.auth.aiAgreement}</Link>
|
||||
</p>
|
||||
</form>
|
||||
</TabsContent>
|
||||
|
||||
@@ -7,28 +7,13 @@ import { apiFetch } from "../../lib/auth";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
interface Post {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
tags?: string | null;
|
||||
viewCount: number;
|
||||
likeCount: number;
|
||||
commentCount: number;
|
||||
createdAt: string;
|
||||
user: { id: number; nickname: string; avatar: string | null };
|
||||
comments?: Comment[];
|
||||
}
|
||||
|
||||
interface Comment {
|
||||
id: number;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
user: { id: number; nickname: string; avatar: string | null };
|
||||
}
|
||||
interface Post { id: number; title: string; content: string; tags?: string | null; viewCount: number; likeCount: number; commentCount: number; createdAt: string; user: { id: number; nickname: string; avatar: string | null }; comments?: Comment[] }
|
||||
interface Comment { id: number; content: string; createdAt: string; user: { id: number; nickname: string; avatar: string | null } }
|
||||
|
||||
export default function CommunityPage() {
|
||||
const t = useT();
|
||||
const router = useRouter();
|
||||
const [posts, setPosts] = useState<Post[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -45,9 +30,7 @@ export default function CommunityPage() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const url = activeTab === 'feed' && token
|
||||
? '/community/feed'
|
||||
: '/community/posts';
|
||||
const url = activeTab === 'feed' && token ? '/community/feed' : '/community/posts';
|
||||
const res = await apiFetch(url);
|
||||
const data = await res.json();
|
||||
setPosts(data.items || []);
|
||||
@@ -66,10 +49,7 @@ export default function CommunityPage() {
|
||||
if (post.user?.id) {
|
||||
try {
|
||||
const res = await apiFetch(`/community/users/${post.user.id}/follow`);
|
||||
if (res.ok) {
|
||||
const d = await res.json();
|
||||
if (d.followed) followed.add(post.user.id);
|
||||
}
|
||||
if (res.ok) { const d = await res.json(); if (d.followed) followed.add(post.user.id); }
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
}
|
||||
@@ -81,37 +61,23 @@ export default function CommunityPage() {
|
||||
if (!title.trim() || !content.trim()) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await apiFetch("/community/posts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ title, content, tags: tags || undefined }),
|
||||
});
|
||||
setTitle(""); setContent(""); setTags("");
|
||||
setShowForm(false);
|
||||
await apiFetch("/community/posts", { method: "POST", body: JSON.stringify({ title, content, tags: tags || undefined }) });
|
||||
setTitle(""); setContent(""); setTags(""); setShowForm(false);
|
||||
loadPosts();
|
||||
} catch (e) { console.error(e) }
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function handleLike(postId: number) {
|
||||
try {
|
||||
await apiFetch(`/community/posts/${postId}/like`, { method: "POST" });
|
||||
loadPosts();
|
||||
} catch (e) { console.error(e) }
|
||||
try { await apiFetch(`/community/posts/${postId}/like`, { method: "POST" }); loadPosts(); } catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
async function handleFollow(userId: number) {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const token = localStorage.getItem('token'); if (!token) return;
|
||||
try {
|
||||
const isFollowed = followedUsers.has(userId);
|
||||
await apiFetch(`/community/users/${userId}/follow`, {
|
||||
method: isFollowed ? 'DELETE' : 'POST',
|
||||
});
|
||||
setFollowedUsers(prev => {
|
||||
const next = new Set(prev);
|
||||
isFollowed ? next.delete(userId) : next.add(userId);
|
||||
return next;
|
||||
});
|
||||
await apiFetch(`/community/users/${userId}/follow`, { method: isFollowed ? 'DELETE' : 'POST' });
|
||||
setFollowedUsers(prev => { const next = new Set(prev); isFollowed ? next.delete(userId) : next.add(userId); return next; });
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
@@ -125,12 +91,8 @@ export default function CommunityPage() {
|
||||
if (!comment.trim()) return;
|
||||
setSubmittingComment(true);
|
||||
try {
|
||||
await apiFetch(`/community/posts/${post.id}/comments`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: comment }),
|
||||
});
|
||||
setComment("");
|
||||
loadPosts();
|
||||
await apiFetch(`/community/posts/${post.id}/comments`, { method: "POST", body: JSON.stringify({ content: comment }) });
|
||||
setComment(""); loadPosts();
|
||||
} catch (e) { console.error(e) }
|
||||
setSubmittingComment(false);
|
||||
}
|
||||
@@ -149,12 +111,8 @@ export default function CommunityPage() {
|
||||
</Link>
|
||||
{isLoggedIn && (
|
||||
<button onClick={() => handleFollow(post.user.id)}
|
||||
className={`ml-auto text-xs px-2 py-1 rounded transition-colors ${
|
||||
followedUsers.has(post.user.id)
|
||||
? 'bg-muted text-muted-foreground hover:bg-accent'
|
||||
: 'bg-brand-50 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 hover:bg-brand-100 dark:hover:bg-brand-900/50'
|
||||
}`}>
|
||||
{followedUsers.has(post.user.id) ? '已关注' : '+ 关注'}
|
||||
className={`ml-auto text-xs px-2 py-1 rounded transition-colors ${followedUsers.has(post.user.id) ? 'bg-muted text-muted-foreground hover:bg-accent' : 'bg-brand-50 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 hover:bg-brand-100 dark:hover:bg-brand-900/50'}`}>
|
||||
{followedUsers.has(post.user.id) ? t.community.followed : t.community.follow}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -170,21 +128,15 @@ export default function CommunityPage() {
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-6 text-sm text-muted-foreground">
|
||||
<button onClick={() => handleLike(post.id)} className="flex items-center gap-1 hover:text-brand-600 transition-colors">
|
||||
❤️ {post.likeCount}
|
||||
</button>
|
||||
<button onClick={() => setExpanded(!expanded)} className="hover:text-brand-600 transition-colors">
|
||||
💬 {post.commentCount}
|
||||
</button>
|
||||
<button onClick={() => handleLike(post.id)} className="flex items-center gap-1 hover:text-brand-600 transition-colors">❤️ {post.likeCount}</button>
|
||||
<button onClick={() => setExpanded(!expanded)} className="hover:text-brand-600 transition-colors">💬 {post.commentCount}</button>
|
||||
<span>👁 {post.viewCount}</span>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="mt-4 pt-4 border-t border-border">
|
||||
{post.comments?.map((c: Comment) => (
|
||||
<div key={c.id} className="flex gap-3 mb-3">
|
||||
<Avatar className="w-6 h-6">
|
||||
<AvatarFallback className="text-[10px]">{c.user.nickname?.[0] || "U"}</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="w-6 h-6"><AvatarFallback className="text-[10px]">{c.user.nickname?.[0] || "U"}</AvatarFallback></Avatar>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs text-muted-foreground mb-1">{c.user.nickname} · {new Date(c.createdAt).toLocaleDateString()}</div>
|
||||
<p className="text-sm text-muted-foreground">{c.content}</p>
|
||||
@@ -192,12 +144,11 @@ export default function CommunityPage() {
|
||||
</div>
|
||||
))}
|
||||
<form onSubmit={handleComment} className="mt-3 flex gap-2">
|
||||
<input value={comment} onChange={e => setComment(e.target.value)}
|
||||
placeholder="写下你的评论..."
|
||||
<input value={comment} onChange={e => setComment(e.target.value)} placeholder={t.community.commentPlaceholder}
|
||||
className="flex-1 px-3 py-2 bg-background border border-input rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
<button type="submit" disabled={submittingComment}
|
||||
className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700 disabled:opacity-50">
|
||||
{submittingComment ? "发送中..." : "评论"}
|
||||
{submittingComment ? t.community.sending : t.community.comment}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -206,57 +157,46 @@ export default function CommunityPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-6" />
|
||||
<Skeleton className="h-4 w-32 mb-8" />
|
||||
<Skeleton className="h-64 w-full mb-4" />
|
||||
<Skeleton className="h-4 w-full mb-2" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (loading) return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-6" /><Skeleton className="h-4 w-32 mb-8" />
|
||||
<Skeleton className="h-64 w-full mb-4" /><Skeleton className="h-4 w-full mb-2" /><Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">社区</h1>
|
||||
<p className="mt-2 text-muted-foreground">与 AI 学习者交流心得,分享实战经验</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">{t.community.title}</h1>
|
||||
<p className="mt-2 text-muted-foreground">{t.community.desc}</p>
|
||||
</div>
|
||||
<button onClick={() => setShowForm(!showForm)}
|
||||
className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 transition-colors">
|
||||
{showForm ? "取消" : "+ 发帖"}
|
||||
{showForm ? t.common.cancel : t.community.createPost}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 mb-6 bg-muted rounded-lg p-1">
|
||||
<button onClick={() => setActiveTab('latest')}
|
||||
className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
activeTab === 'latest' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
||||
}`}>最新</button>
|
||||
className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${activeTab === 'latest' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}>{t.community.latest}</button>
|
||||
<button onClick={() => setActiveTab('feed')}
|
||||
className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
activeTab === 'feed' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
||||
}`}>关注</button>
|
||||
className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${activeTab === 'feed' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}>{t.community.following}</button>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<form onSubmit={handleCreate} className="bg-card rounded-xl border border-border p-6 mb-6">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4">发布新帖</h3>
|
||||
<input value={title} onChange={e => setTitle(e.target.value)} placeholder="标题"
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4">{t.community.newPost}</h3>
|
||||
<input value={title} onChange={e => setTitle(e.target.value)} placeholder={t.community.postTitle}
|
||||
className="w-full px-3 py-2 bg-background border border-input rounded-lg text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
<textarea value={content} onChange={e => setContent(e.target.value)}
|
||||
placeholder="分享你的 AI 学习心得、实战经验..." rows={4}
|
||||
<textarea value={content} onChange={e => setContent(e.target.value)} placeholder={t.community.contentPlaceholder} rows={4}
|
||||
className="w-full px-3 py-2 bg-background border border-input rounded-lg text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-ring resize-none" />
|
||||
<input value={tags} onChange={e => setTags(e.target.value)}
|
||||
placeholder="标签(逗号分隔,如:AI,提示词)"
|
||||
<input value={tags} onChange={e => setTags(e.target.value)} placeholder={t.community.tagsPlaceholder}
|
||||
className="w-full px-3 py-2 bg-background border border-input rounded-lg text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
<div className="flex justify-end">
|
||||
<button type="submit" disabled={submitting}
|
||||
className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 disabled:opacity-50">
|
||||
{submitting ? "发布中..." : "发布"}
|
||||
{submitting ? t.community.publishing : t.community.publish}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -264,7 +204,7 @@ export default function CommunityPage() {
|
||||
|
||||
{posts.length === 0 ? (
|
||||
<div className="text-center py-20 text-muted-foreground">
|
||||
<p>{activeTab === 'feed' ? '关注更多用户,发现精彩内容' : '还没有帖子,来发第一帖吧!'}</p>
|
||||
<p>{activeTab === 'feed' ? t.community.feedEmpty : t.community.postsEmpty}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>{posts.map(post => <PostCard key={post.id} post={post} />)}</div>
|
||||
|
||||
@@ -5,13 +5,11 @@ import Link from 'next/link';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { BookOpen, Users } from 'lucide-react';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
import { API_BASE } from '@/lib/config';
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
interface Course {
|
||||
id: number; title: string; description: string; cover: string | null;
|
||||
isFree: boolean; chapters?: { lessons: any[] }[];
|
||||
}
|
||||
interface Course { id: number; title: string; description: string; cover: string | null; isFree: boolean; chapters?: { lessons: any[] }[] }
|
||||
|
||||
function CourseSkeleton() {
|
||||
return (
|
||||
@@ -25,6 +23,7 @@ function CourseSkeleton() {
|
||||
}
|
||||
|
||||
export default function CoursesPage() {
|
||||
const t = useT();
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -37,18 +36,16 @@ export default function CoursesPage() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-10">
|
||||
<h1 className="text-3xl font-bold text-foreground">专题</h1>
|
||||
<p className="mt-2 text-muted-foreground">系统化探索 AI,从入门到精通</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">{t.courses.title}</h1>
|
||||
<p className="mt-2 text-muted-foreground">{t.courses.desc}</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[1,2,3,4,5,6].map(i => <CourseSkeleton key={i} />)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">{[1,2,3,4,5,6].map(i => <CourseSkeleton key={i} />)}</div>
|
||||
) : courses.length === 0 ? (
|
||||
<div className="text-center py-20 text-muted-foreground">
|
||||
<BookOpen className="w-12 h-12 mx-auto mb-4 opacity-30" />
|
||||
<p>暂无专题内容</p>
|
||||
<p>{t.courses.empty}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@@ -63,15 +60,15 @@ export default function CoursesPage() {
|
||||
</div>
|
||||
)}
|
||||
<div className="p-5">
|
||||
<Badge variant={course.isFree ? 'success' : 'destructive'} className="mb-3">
|
||||
{course.isFree ? '免费' : '付费'}
|
||||
<Badge variant={course.isFree ? 'secondary' : 'destructive'} className="mb-3">
|
||||
{course.isFree ? t.courses.free : t.courses.paid}
|
||||
</Badge>
|
||||
<h3 className="font-semibold group-hover:text-brand-600 transition-colors mb-2">{course.title}</h3>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">{course.description}</p>
|
||||
{course.chapters && (
|
||||
<div className="flex items-center gap-2 mt-3 text-xs text-muted-foreground">
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
<span>{course.chapters.reduce((s, ch) => s + (ch.lessons?.length || 0), 0)} 模块</span>
|
||||
<BookOpen className="w-3.5 h-3.5" />
|
||||
<span>{t.courses.moduleCount.replace('{n}', String(course.chapters.reduce((s, ch) => s + (ch.lessons?.length || 0), 0)))}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,56 +6,18 @@ import { Progress } from '@/components/ui/progress';
|
||||
import Link from 'next/link';
|
||||
import { apiFetch, isLoggedIn, clearTokens } from '../../lib/auth';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
interface Stats {
|
||||
inProgressCourses: number;
|
||||
completedLessons: number;
|
||||
favoritePrompts: number;
|
||||
studyDays: number;
|
||||
todayLearned: number;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
nickname: string;
|
||||
avatar: string | null;
|
||||
memberPlan: string;
|
||||
memberExpire: string | null;
|
||||
sandboxDaily: number;
|
||||
joinedAt: string;
|
||||
}
|
||||
|
||||
interface CourseProgress {
|
||||
course: { id: number; title: string; cover: string | null };
|
||||
progress: number;
|
||||
completedCount: number;
|
||||
totalCount: number;
|
||||
recentLessons: { id: number; title: string; completed: boolean; progress: number; updatedAt: string }[];
|
||||
}
|
||||
|
||||
interface RecentRecord {
|
||||
lessonId: number;
|
||||
lessonTitle: string;
|
||||
courseId: number;
|
||||
courseTitle: string;
|
||||
completed: boolean;
|
||||
progress: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface PromptFavorite {
|
||||
id: number;
|
||||
promptId: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
model: string | null;
|
||||
viewCount: number;
|
||||
likeCount: number;
|
||||
favoritedAt: string;
|
||||
}
|
||||
interface Stats { inProgressCourses: number; completedLessons: number; favoritePrompts: number; studyDays: number; todayLearned: number }
|
||||
interface UserInfo { nickname: string; avatar: string | null; memberPlan: string; memberExpire: string | null; sandboxDaily: number; joinedAt: string }
|
||||
interface CourseProgress { course: { id: number; title: string; cover: string | null }; progress: number; completedCount: number; totalCount: number; recentLessons: { id: number; title: string; completed: boolean; progress: number; updatedAt: string }[] }
|
||||
interface RecentRecord { lessonId: number; lessonTitle: string; courseId: number; courseTitle: string; completed: boolean; progress: number; updatedAt: string }
|
||||
interface PromptFavorite { id: number; promptId: number; title: string; description: string | null; model: string | null; viewCount: number; likeCount: number; favoritedAt: string }
|
||||
|
||||
type Tab = 'progress' | 'favorites' | 'profile';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const t = useT();
|
||||
const router = useRouter();
|
||||
const [activeTab, setActiveTab] = useState<Tab>('progress');
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -70,149 +32,97 @@ export default function DashboardPage() {
|
||||
const [saveMsg, setSaveMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn()) {
|
||||
router.push('/auth');
|
||||
return;
|
||||
}
|
||||
if (!isLoggedIn()) { router.push('/auth'); return; }
|
||||
loadAll();
|
||||
}, []);
|
||||
|
||||
async function loadAll() {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const [statsRes, progressRes, favRes, profileRes] = await Promise.all([
|
||||
apiFetch('/dashboard/stats'),
|
||||
apiFetch('/dashboard/progress'),
|
||||
apiFetch('/dashboard/favorites'),
|
||||
apiFetch('/dashboard/profile'),
|
||||
apiFetch('/dashboard/stats'), apiFetch('/dashboard/progress'), apiFetch('/dashboard/favorites'), apiFetch('/dashboard/profile'),
|
||||
]);
|
||||
|
||||
if (!statsRes.ok || !progressRes.ok || !favRes.ok || !profileRes.ok) {
|
||||
throw new Error('加载数据失败');
|
||||
}
|
||||
|
||||
if (!statsRes.ok || !progressRes.ok || !favRes.ok || !profileRes.ok) throw new Error(t.dashboard.loadFailed);
|
||||
const statsData = await statsRes.json();
|
||||
setStats(statsData.stats); setUserInfo(statsData.user);
|
||||
const progressData = await progressRes.json();
|
||||
const favData = await favRes.json();
|
||||
const profileData = await profileRes.json();
|
||||
|
||||
setStats(statsData.stats);
|
||||
setUserInfo(statsData.user);
|
||||
setCourses(progressData.courses || []);
|
||||
setRecentRecords(progressData.recentRecords || []);
|
||||
setFavorites(favData || []);
|
||||
setNickname(profileData.nickname || '');
|
||||
setFavorites((await favRes.json()) || []);
|
||||
setNickname((await profileRes.json()).nickname || '');
|
||||
} catch (e: any) {
|
||||
if (e.message?.includes('401') || e.message?.includes('Unauthorized')) {
|
||||
clearTokens();
|
||||
router.push('/auth');
|
||||
}
|
||||
setError(e.message || '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
if (e.message?.includes('401') || e.message?.includes('Unauthorized')) { clearTokens(); router.push('/auth'); }
|
||||
setError(e.message || t.dashboard.loadFailed);
|
||||
} finally { setLoading(false); }
|
||||
}
|
||||
|
||||
async function handleSaveProfile() {
|
||||
setSaving(true);
|
||||
setSaveMsg('');
|
||||
setSaving(true); setSaveMsg('');
|
||||
try {
|
||||
const res = await apiFetch('/dashboard/profile', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ nickname }),
|
||||
});
|
||||
if (!res.ok) throw new Error('保存失败');
|
||||
setSaveMsg('保存成功');
|
||||
const res = await apiFetch('/dashboard/profile', { method: 'PUT', body: JSON.stringify({ nickname }) });
|
||||
if (!res.ok) throw new Error(t.dashboard.saveFailed);
|
||||
setSaveMsg(t.dashboard.saveSuccess);
|
||||
setUserInfo(prev => prev ? { ...prev, nickname } : prev);
|
||||
} catch {
|
||||
setSaveMsg('保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
setTimeout(() => setSaveMsg(''), 2000);
|
||||
}
|
||||
} catch { setSaveMsg(t.dashboard.saveFailed); }
|
||||
finally { setSaving(false); setTimeout(() => setSaveMsg(''), 2000); }
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
clearTokens();
|
||||
router.push('/');
|
||||
}
|
||||
function handleLogout() { clearTokens(); router.push('/'); }
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-2" />
|
||||
<Skeleton className="h-5 w-72 mb-8" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
{[1,2,3,4].map(i => <Skeleton key={i} className="h-24 rounded-xl" />)}
|
||||
</div>
|
||||
<Skeleton className="h-64 rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (loading) return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-2" /><Skeleton className="h-5 w-72 mb-8" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">{[1,2,3,4].map(i => <Skeleton key={i} className="h-24 rounded-xl" />)}</div>
|
||||
<Skeleton className="h-64 rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 py-20 text-center">
|
||||
<p className="text-red-500 mb-4">{error}</p>
|
||||
<button onClick={loadAll} className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700">
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error) return (
|
||||
<div className="max-w-7xl mx-auto px-4 py-20 text-center">
|
||||
<p className="text-red-500 mb-4">{error}</p>
|
||||
<button onClick={loadAll} className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700">{t.common.retry}</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const tabs: { key: Tab; label: string }[] = [
|
||||
{ key: 'progress', label: '学习进度' },
|
||||
{ key: 'favorites', label: '收藏夹' },
|
||||
{ key: 'profile', label: '个人设置' },
|
||||
{ key: 'progress', label: t.dashboard.tabProgress },
|
||||
{ key: 'favorites', label: t.dashboard.tabFavorites },
|
||||
{ key: 'profile', label: t.dashboard.tabProfile },
|
||||
];
|
||||
|
||||
const statItems = [
|
||||
{ label: t.dashboard.inProgressCourses, value: stats?.inProgressCourses ?? 0 },
|
||||
{ label: t.dashboard.completedLessons, value: stats?.completedLessons ?? 0 },
|
||||
{ label: t.dashboard.favoritePrompts, value: stats?.favoritePrompts ?? 0 },
|
||||
{ label: t.dashboard.studyDays, value: stats?.studyDays ?? 0 },
|
||||
{ label: t.dashboard.todayLearned, value: stats?.todayLearned ?? 0 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">我的学习</h1>
|
||||
<p className="mt-2 text-muted-foreground">掌握你的学习进度和统计</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">{t.dashboard.title}</h1>
|
||||
<p className="mt-2 text-muted-foreground">{t.dashboard.desc}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-sm text-muted-foreground hover:text-red-500 transition-colors mt-1"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
<button onClick={handleLogout} className="text-sm text-muted-foreground hover:text-red-500 transition-colors mt-1">{t.common.logout}</button>
|
||||
</div>
|
||||
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
|
||||
{[
|
||||
{ label: '学习中课程', value: stats.inProgressCourses },
|
||||
{ label: '已完成课时', value: stats.completedLessons },
|
||||
{ label: '收藏提示词', value: stats.favoritePrompts },
|
||||
{ label: '学习天数', value: stats.studyDays },
|
||||
{ label: '今日学习', value: stats.todayLearned },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="bg-card rounded-xl border border-border p-4 text-center">
|
||||
<div className="text-2xl font-bold text-brand-600">{item.value}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
|
||||
{statItems.map((item) => (
|
||||
<div key={item.label} className="bg-card rounded-xl border border-border p-4 text-center">
|
||||
<div className="text-2xl font-bold text-brand-600">{item.value}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 flex-col lg:flex-row">
|
||||
<div className="lg:w-48 flex-shrink-0">
|
||||
<nav className="flex lg:flex-col gap-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`px-4 py-2.5 text-sm font-medium rounded-lg text-left transition-colors ${
|
||||
activeTab === tab.key
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
<button key={tab.key} onClick={() => setActiveTab(tab.key)}
|
||||
className={`px-4 py-2.5 text-sm font-medium rounded-lg text-left transition-colors ${activeTab === tab.key ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent'}`}>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
@@ -224,51 +134,32 @@ export default function DashboardPage() {
|
||||
<div>
|
||||
{courses.length === 0 ? (
|
||||
<div className="bg-card rounded-xl border border-border p-12 text-center">
|
||||
<p className="text-muted-foreground mb-4">还没有学习记录</p>
|
||||
<Link href="/courses" className="inline-flex px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700">
|
||||
浏览课程
|
||||
</Link>
|
||||
<p className="text-muted-foreground mb-4">{t.dashboard.noLearningRecords}</p>
|
||||
<Link href="/courses" className="inline-flex px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700">{t.dashboard.browseCourses}</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{courses.map((entry) => (
|
||||
<div key={entry.course.id} className="bg-card rounded-xl border border-border p-6">
|
||||
<Link href={`/courses/${entry.course.id}`} className="text-lg font-semibold text-foreground hover:text-brand-600">
|
||||
{entry.course.title}
|
||||
</Link>
|
||||
<Link href={`/courses/${entry.course.id}`} className="text-lg font-semibold text-foreground hover:text-brand-600">{entry.course.title}</Link>
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground mb-1.5">
|
||||
<span>学习进度</span>
|
||||
<span>{entry.completedCount}/{entry.totalCount} 课时 ({entry.progress}%)</span>
|
||||
<span>{t.dashboard.learningProgress}</span>
|
||||
<span>{t.dashboard.lessonCount.replace('{completed}', String(entry.completedCount)).replace('{total}', String(entry.totalCount)).replace('{progress}', String(entry.progress))}</span>
|
||||
</div>
|
||||
<Progress value={entry.progress} className="h-2" />
|
||||
</div>
|
||||
{entry.recentLessons.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-border">
|
||||
<div className="text-xs text-muted-foreground mb-2">最近学习</div>
|
||||
<div className="space-y-1.5">
|
||||
{entry.recentLessons.map((lesson) => (
|
||||
<div key={lesson.id} className="flex items-center gap-2 text-sm">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${lesson.completed ? 'bg-green-500' : 'bg-brand-300'}`} />
|
||||
<span className="text-muted-foreground">{lesson.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{recentRecords.length > 0 && (
|
||||
<div className="bg-card rounded-xl border border-border p-6">
|
||||
<h3 className="text-base font-semibold text-foreground mb-4">最近学习记录</h3>
|
||||
<h3 className="text-base font-semibold text-foreground mb-4">{t.dashboard.recentLearning}</h3>
|
||||
<div className="space-y-3">
|
||||
{recentRecords.map((r, i) => (
|
||||
<div key={i} className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${r.completed ? 'bg-green-500' : 'bg-brand-300'}`} />
|
||||
<span className="text-muted-foreground">{r.lessonTitle}</span>
|
||||
<span className="text-muted-foreground">- {r.courseTitle}</span>
|
||||
<span className="text-muted-foreground">{r.lessonTitle} - {r.courseTitle}</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{new Date(r.updatedAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
@@ -285,25 +176,19 @@ export default function DashboardPage() {
|
||||
<div>
|
||||
{favorites.length === 0 ? (
|
||||
<div className="bg-card rounded-xl border border-border p-12 text-center">
|
||||
<p className="text-muted-foreground mb-4">还没有收藏的提示词</p>
|
||||
<Link href="/prompts" className="inline-flex px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700">
|
||||
浏览提示词
|
||||
</Link>
|
||||
<p className="text-muted-foreground mb-4">{t.dashboard.noFavorites}</p>
|
||||
<Link href="/prompts" className="inline-flex px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700">{t.dashboard.browsePrompts}</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{favorites.map((fav) => (
|
||||
<Link
|
||||
key={fav.id}
|
||||
href={`/prompts`}
|
||||
className="block bg-card rounded-xl border border-border p-5 hover:border-brand-200 transition-colors"
|
||||
>
|
||||
<Link key={fav.id} href="/prompts" className="block bg-card rounded-xl border border-border p-5 hover:border-brand-200 transition-colors">
|
||||
<h3 className="font-semibold text-foreground">{fav.title}</h3>
|
||||
{fav.description && <p className="text-sm text-muted-foreground mt-1 line-clamp-2">{fav.description}</p>}
|
||||
<div className="flex items-center gap-4 mt-3 text-xs text-muted-foreground">
|
||||
{fav.model && <span>模型: {fav.model}</span>}
|
||||
<span>{fav.viewCount} 次浏览</span>
|
||||
<span>{fav.likeCount} 赞</span>
|
||||
{fav.model && <span>{t.dashboard.modelLabel.replace('{model}', fav.model)}</span>}
|
||||
<span>{t.dashboard.viewCount.replace('{n}', String(fav.viewCount))}</span>
|
||||
<span>{t.dashboard.likeCount.replace('{n}', String(fav.likeCount))}</span>
|
||||
<span className="ml-auto">{new Date(fav.favoritedAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -315,45 +200,34 @@ export default function DashboardPage() {
|
||||
|
||||
{activeTab === 'profile' && (
|
||||
<div className="bg-card rounded-xl border border-border p-6">
|
||||
<h3 className="text-base font-semibold text-foreground mb-6">个人资料</h3>
|
||||
<h3 className="text-base font-semibold text-foreground mb-6">{t.dashboard.profile}</h3>
|
||||
<div className="space-y-5 max-w-md">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">昵称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nickname}
|
||||
onChange={e => setNickname(e.target.value)}
|
||||
<label className="block text-sm font-medium text-foreground mb-1">{t.dashboard.nicknameLabel}</label>
|
||||
<input type="text" value={nickname} onChange={e => setNickname(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent"
|
||||
placeholder="输入昵称"
|
||||
/>
|
||||
placeholder={t.dashboard.nicknamePlaceholder} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">会员计划</label>
|
||||
<p className="text-sm text-muted-foreground">{userInfo?.memberPlan === 'FREE' ? '免费用户' : userInfo?.memberPlan}</p>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">{t.dashboard.memberPlan}</label>
|
||||
<p className="text-sm text-muted-foreground">{userInfo?.memberPlan === 'FREE' ? t.dashboard.freeUser : userInfo?.memberPlan}</p>
|
||||
</div>
|
||||
{userInfo?.memberExpire && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">会员到期</label>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">{t.dashboard.memberExpire}</label>
|
||||
<p className="text-sm text-muted-foreground">{new Date(userInfo.memberExpire).toLocaleDateString()}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">注册时间</label>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">{t.dashboard.joinDate}</label>
|
||||
<p className="text-sm text-muted-foreground">{userInfo?.joinedAt ? new Date(userInfo.joinedAt).toLocaleDateString() : '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={handleSaveProfile}
|
||||
disabled={saving}
|
||||
className="px-6 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 disabled:opacity-50"
|
||||
>
|
||||
{saving ? '保存中...' : '保存'}
|
||||
<button onClick={handleSaveProfile} disabled={saving}
|
||||
className="px-6 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 disabled:opacity-50">
|
||||
{saving ? t.common.loading : t.settings.saveChanges}
|
||||
</button>
|
||||
{saveMsg && (
|
||||
<span className={`ml-3 text-sm ${saveMsg === '保存成功' ? 'text-green-600' : 'text-red-500'}`}>
|
||||
{saveMsg}
|
||||
</span>
|
||||
)}
|
||||
{saveMsg && <span className={`ml-3 text-sm ${saveMsg === t.dashboard.saveSuccess ? 'text-green-600' : 'text-red-500'}`}>{saveMsg}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,29 +5,17 @@ import Link from 'next/link';
|
||||
import { apiFetch } from '@/lib/auth';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
interface Notification {
|
||||
id: number;
|
||||
type: 'like' | 'comment' | 'follow' | 'system';
|
||||
title: string;
|
||||
content?: string;
|
||||
link?: string;
|
||||
relatedId?: number;
|
||||
isRead: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
interface Notification { id: number; type: 'like' | 'comment' | 'follow' | 'system'; title: string; content?: string; link?: string; relatedId?: number; isRead: boolean; createdAt: string }
|
||||
|
||||
function NotificationIcon({ type }: { type: string }) {
|
||||
const icons: Record<string, string> = {
|
||||
like: '❤️',
|
||||
comment: '💬',
|
||||
follow: '👤',
|
||||
system: '🔔',
|
||||
};
|
||||
const icons: Record<string, string> = { like: '❤️', comment: '💬', follow: '👤', system: '🔔' };
|
||||
return <span className="text-lg">{icons[type] || '🔔'}</span>;
|
||||
}
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const t = useT();
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -35,11 +23,7 @@ export default function NotificationsPage() {
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiFetch('/notifications');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setNotifications(data.items || []);
|
||||
setUnreadCount(data.unread || 0);
|
||||
}
|
||||
if (res.ok) { const data = await res.json(); setNotifications(data.items || []); setUnreadCount(data.unread || 0); }
|
||||
} catch (e) { console.error(e) }
|
||||
setLoading(false);
|
||||
}, []);
|
||||
@@ -64,11 +48,8 @@ export default function NotificationsPage() {
|
||||
|
||||
if (loading) return (
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-6" />
|
||||
<Skeleton className="h-4 w-32 mb-8" />
|
||||
<Skeleton className="h-64 w-full mb-4" />
|
||||
<Skeleton className="h-4 w-full mb-2" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-8 w-48 mb-6" /><Skeleton className="h-4 w-32 mb-8" />
|
||||
<Skeleton className="h-64 w-full mb-4" /><Skeleton className="h-4 w-full mb-2" /><Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -76,54 +57,39 @@ export default function NotificationsPage() {
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">通知</h1>
|
||||
<h1 className="text-3xl font-bold text-foreground">{t.notifications.title}</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{unreadCount > 0 ? `你有 ${unreadCount} 条未读通知` : '暂无未读通知'}
|
||||
{unreadCount > 0 ? t.notifications.unreadCount.replace('{n}', String(unreadCount)) : t.notifications.noUnread}
|
||||
</p>
|
||||
</div>
|
||||
{unreadCount > 0 && (
|
||||
<Button variant="outline" size="sm" onClick={markAllRead}>
|
||||
全部已读
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={markAllRead}>{t.notifications.markAllRead}</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{notifications.map(n => (
|
||||
<div key={n.id}
|
||||
className={`flex items-start gap-4 p-4 rounded-xl border transition-colors ${
|
||||
n.isRead
|
||||
? 'bg-card border-border'
|
||||
: 'bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800'
|
||||
}`}>
|
||||
<div key={n.id} className={`flex items-start gap-4 p-4 rounded-xl border transition-colors ${n.isRead ? 'bg-card border-border' : 'bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800'}`}>
|
||||
<div className="mt-1"><NotificationIcon type={n.type} /></div>
|
||||
<div className="flex-1 min-w-0">
|
||||
{n.link ? (
|
||||
<Link href={n.link} onClick={() => { if (!n.isRead) markRead(n.id); }}
|
||||
className="text-sm font-medium text-foreground hover:text-brand-600">
|
||||
{n.title}
|
||||
</Link>
|
||||
<Link href={n.link} onClick={() => { if (!n.isRead) markRead(n.id); }} className="text-sm font-medium text-foreground hover:text-brand-600">{n.title}</Link>
|
||||
) : (
|
||||
<p className="text-sm font-medium text-foreground">{n.title}</p>
|
||||
)}
|
||||
{n.content && <p className="text-xs text-muted-foreground mt-1 line-clamp-2">{n.content}</p>}
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{new Date(n.createdAt).toLocaleString('zh-CN')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{new Date(n.createdAt).toLocaleString('zh-CN')}</p>
|
||||
</div>
|
||||
{!n.isRead && (
|
||||
<button onClick={() => markRead(n.id)}
|
||||
className="text-xs text-muted-foreground hover:text-foreground shrink-0 px-2 py-1 rounded hover:bg-accent">
|
||||
已读
|
||||
</button>
|
||||
<button onClick={() => markRead(n.id)} className="text-xs text-muted-foreground hover:text-foreground shrink-0 px-2 py-1 rounded hover:bg-accent">{t.notifications.markRead}</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{notifications.length === 0 && (
|
||||
<div className="text-center py-20 text-muted-foreground">
|
||||
<p className="text-4xl mb-4">🔔</p>
|
||||
<p>暂无通知</p>
|
||||
<p className="text-sm mt-1">点赞、评论或关注你的人会出现在这里</p>
|
||||
<p>{t.notifications.emptyTitle}</p>
|
||||
<p className="text-sm mt-1">{t.notifications.emptyDesc}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+37
-109
@@ -1,22 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { HomePageClient } from './home-client';
|
||||
import { ArrowRight, Sparkles, BookOpen, Bot, Compass, Zap } from 'lucide-react';
|
||||
|
||||
const stats = [
|
||||
{ value: '50+', label: 'AI 专题' },
|
||||
{ value: '200+', label: '精选提示词' },
|
||||
{ value: '30+', label: 'AI 工具评测' },
|
||||
{ value: '10,000+', label: '探索者' },
|
||||
];
|
||||
|
||||
const features = [
|
||||
{ icon: Compass, title: '分领域指南', desc: '按职业和场景分类内容,学即所用,精准提升 AI 应用能力' },
|
||||
{ icon: Bot, title: 'AI 沙盒实战', desc: '内置 AI 对话沙盒,边学边练,在实践中掌握提示词技巧' },
|
||||
{ icon: BookOpen, title: '提示词库', desc: '精选 200+ 提示词模板,覆盖办公、编程、创作等场景' },
|
||||
{ icon: Zap, title: '持续更新', desc: '紧跟大模型迭代,内容实时更新,始终走在 AI 前沿' },
|
||||
];
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
export default function HomePage() {
|
||||
const t = useT();
|
||||
const stats = [
|
||||
{ value: '50+', label: t.home.statTopics },
|
||||
{ value: '200+', label: t.home.statPrompts },
|
||||
{ value: '30+', label: t.home.statTools },
|
||||
{ value: '10,000+', label: t.home.statExplorers },
|
||||
];
|
||||
const features = [
|
||||
{ icon: Compass, title: t.home.featureGuide, desc: t.home.featureGuideDesc },
|
||||
{ icon: Bot, title: t.home.featureSandbox, desc: t.home.featureSandboxDesc },
|
||||
{ icon: BookOpen, title: t.home.featurePrompts, desc: t.home.featurePromptsDesc },
|
||||
{ icon: Zap, title: t.home.featureUpdate, desc: t.home.featureUpdateDesc },
|
||||
];
|
||||
return (
|
||||
<HomePageClient>
|
||||
{/* Hero */}
|
||||
@@ -30,33 +32,25 @@ export default function HomePage() {
|
||||
<div className="text-center max-w-3xl mx-auto animate-fade-in-up">
|
||||
<span className="inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium text-brand-700 dark:text-brand-300 bg-brand-100 dark:bg-brand-900/50 rounded-full mb-8 border border-brand-200 dark:border-brand-800">
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
免费 AI 知识社区
|
||||
{t.home.badge}
|
||||
</span>
|
||||
<h1 className="text-4xl md:text-6xl font-bold tracking-tight leading-tight">
|
||||
<span className="bg-gradient-to-r from-brand-600 via-brand-500 to-blue-500 bg-clip-text text-transparent">
|
||||
让每个人
|
||||
{t.home.heroHighlight}
|
||||
</span>
|
||||
<br />都能用好 AI
|
||||
<br />{t.home.heroRest}
|
||||
</h1>
|
||||
<p className="mt-6 text-lg md:text-xl text-muted-foreground leading-relaxed max-w-2xl mx-auto">
|
||||
宇之然 AI 是面向大众的 AI 工具与知识社区
|
||||
<br className="hidden sm:block" />
|
||||
涵盖 AI 通识、提示词工程、沙盒实战、模型百科
|
||||
{t.home.desc}
|
||||
</p>
|
||||
<div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link
|
||||
href="/courses"
|
||||
className="inline-flex items-center justify-center gap-2 px-8 py-3 text-base font-medium text-white bg-brand-600 rounded-xl hover:bg-brand-700 transition-all shadow-lg shadow-brand-200/50 dark:shadow-brand-900/30 hover:shadow-xl hover:-translate-y-0.5 active:scale-[0.98]"
|
||||
>
|
||||
<Link href="/courses" className="inline-flex items-center justify-center gap-2 px-8 py-3 text-base font-medium text-white bg-brand-600 rounded-xl hover:bg-brand-700 transition-all shadow-lg shadow-brand-200/50 dark:shadow-brand-900/30 hover:shadow-xl hover:-translate-y-0.5 active:scale-[0.98]">
|
||||
<BookOpen className="w-5 h-5" />
|
||||
开始探索
|
||||
{t.home.startExplore}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/auth?tab=register"
|
||||
className="inline-flex items-center justify-center gap-2 px-8 py-3 text-base font-medium text-foreground bg-card border border-border rounded-xl hover:bg-accent transition-all hover:-translate-y-0.5 active:scale-[0.98] shadow-sm"
|
||||
>
|
||||
免费注册
|
||||
<Link href="/auth?tab=register" className="inline-flex items-center justify-center gap-2 px-8 py-3 text-base font-medium text-foreground bg-card border border-border rounded-xl hover:bg-accent transition-all hover:-translate-y-0.5 active:scale-[0.98] shadow-sm">
|
||||
{t.home.freeRegister}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,8 +77,8 @@ export default function HomePage() {
|
||||
<section className="py-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold">为什么选择宇之然?</h2>
|
||||
<p className="mt-4 text-lg text-muted-foreground">四大核心优势,助你快速掌握 AI</p>
|
||||
<h2 className="text-3xl font-bold">{t.home.whyTitle}</h2>
|
||||
<p className="mt-4 text-lg text-muted-foreground">{t.home.whyDesc}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{features.map((feat) => (
|
||||
@@ -105,18 +99,18 @@ export default function HomePage() {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between mb-10">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold">热门专题</h2>
|
||||
<p className="mt-2 text-muted-foreground">从入门到精通,系统探索 AI</p>
|
||||
<h2 className="text-3xl font-bold">{t.home.popularTopics}</h2>
|
||||
<p className="mt-2 text-muted-foreground">{t.home.popularDesc}</p>
|
||||
</div>
|
||||
<Link href="/courses" className="hidden sm:inline-flex items-center gap-1 text-brand-600 hover:text-brand-700 font-medium text-sm group">
|
||||
查看全部 <ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
|
||||
{t.common.viewAll} <ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{[
|
||||
{ title: 'AI 通识:零基础入门', lessons: '12 模块', students: '1,280', tag: '免费', gradient: 'from-brand-500 to-blue-500', desc: '面向零基础用户,带你了解 AI 的基本概念、发展历程和实际应用。' },
|
||||
{ title: '提示词工程从入门到精通', lessons: '20 模块', students: '860', tag: '热门', gradient: 'from-violet-500 to-purple-500', desc: '系统学习提示词编写技巧,掌握与 AI 高效沟通的方法。' },
|
||||
{ title: '用 AI 提升 10 倍办公效率', lessons: '15 模块', students: '2,150', tag: '推荐', gradient: 'from-amber-500 to-orange-500', desc: '学习使用 AI 工具处理文档、数据分析、演示制作等日常工作。' },
|
||||
{ title: 'AI 通识:零基础入门', students: '1,280', tag: t.courses.free, gradient: 'from-brand-500 to-blue-500', desc: '面向零基础用户,带你了解 AI 的基本概念、发展历程和实际应用。' },
|
||||
{ title: '提示词工程从入门到精通', students: '860', tag: '热门', gradient: 'from-violet-500 to-purple-500', desc: '系统学习提示词编写技巧,掌握与 AI 高效沟通的方法。' },
|
||||
{ title: '用 AI 提升 10 倍办公效率', students: '2,150', tag: '推荐', gradient: 'from-amber-500 to-orange-500', desc: '学习使用 AI 工具处理文档、数据分析、演示制作等日常工作。' },
|
||||
].map((course) => (
|
||||
<div key={course.title} className="group bg-card rounded-xl border border-border overflow-hidden hover:shadow-xl transition-all hover:-translate-y-1">
|
||||
<div className={`h-2 bg-gradient-to-r ${course.gradient}`} />
|
||||
@@ -127,14 +121,7 @@ export default function HomePage() {
|
||||
<h3 className="text-lg font-semibold mb-2 group-hover:text-brand-600 transition-colors">{course.title}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4 line-clamp-2">{course.desc}</p>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<BookOpen className="w-4 h-4" />
|
||||
{course.lessons}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-lg leading-none">·</span>
|
||||
{course.students} 人关注
|
||||
</span>
|
||||
<span className="flex items-center gap-1"><BookOpen className="w-4 h-4" />{t.home.moduleCount.replace('{n}', course.students)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,73 +130,14 @@ export default function HomePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* AI Sandbox Preview */}
|
||||
<section className="py-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between mb-10">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold">AI 沙盒</h2>
|
||||
<p className="mt-2 text-muted-foreground">在线体验 AI 对话,边学边练</p>
|
||||
</div>
|
||||
<Link href="/sandbox" className="hidden sm:inline-flex items-center gap-1 text-brand-600 hover:text-brand-700 font-medium text-sm group">
|
||||
打开沙盒 <ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="bg-card border border-border rounded-2xl overflow-hidden shadow-xl">
|
||||
<div className="flex items-center gap-1.5 px-4 pt-3 pb-2 border-b border-border">
|
||||
<div className="flex gap-1.5">
|
||||
<span className="w-3 h-3 rounded-full bg-red-400" />
|
||||
<span className="w-3 h-3 rounded-full bg-yellow-400" />
|
||||
<span className="w-3 h-3 rounded-full bg-green-400" />
|
||||
</div>
|
||||
<span className="ml-2 text-xs text-muted-foreground">AI 沙盒 - 在线体验</span>
|
||||
</div>
|
||||
<div className="p-4 space-y-4 bg-muted/30 dark:bg-muted/10">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="w-7 h-7 bg-brand-600 rounded-lg flex items-center justify-center text-white text-xs font-bold shrink-0">Y</span>
|
||||
<div className="bg-card dark:bg-card rounded-xl rounded-tl-none px-3 py-2.5 text-sm shadow-sm max-w-[80%]">
|
||||
你好!我是宇之然 AI 助手。你可以问我任何问题,我会尽力帮你解答。试试问我关于 AI、编程、写作等方面的问题吧!
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 justify-end">
|
||||
<div className="bg-brand-50 dark:bg-brand-900/30 rounded-xl rounded-tr-none px-3 py-2.5 text-sm max-w-[80%]">
|
||||
帮我用 Python 写一个 Fibonacci 函数
|
||||
</div>
|
||||
<span className="w-7 h-7 bg-muted-foreground/20 rounded-lg flex items-center justify-center text-xs font-bold shrink-0">我</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground pl-9">
|
||||
<span className="w-2 h-2 bg-brand-500 rounded-full animate-pulse" />
|
||||
正在输入...
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-border p-3 bg-card">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="输入你的问题..."
|
||||
readOnly
|
||||
className="flex-1 bg-muted border-0 rounded-lg px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
<button className="px-4 py-2 bg-brand-600 text-white text-sm font-medium rounded-lg hover:bg-brand-700 transition-colors cursor-default">
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-20 bg-gradient-to-r from-brand-600 to-brand-800 dark:from-brand-900 dark:to-brand-950 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjAgMjB2MTBoLTEwVjIwaDEwek0yMCAwaDEwdjEwSDIwVjB6IiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9IjAuMDMiLz48L3N2Zz4=')] opacity-50" />
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative">
|
||||
<h2 className="text-3xl font-bold text-white mb-4">准备好开启 AI 之旅了吗?</h2>
|
||||
<p className="text-brand-100/80 dark:text-brand-200/80 mb-8 text-lg">立即注册,免费探索所有内容</p>
|
||||
<Link
|
||||
href="/auth?tab=register"
|
||||
className="inline-flex items-center gap-2 px-8 py-3 text-base font-medium text-brand-600 bg-white rounded-xl hover:bg-brand-50 transition-all hover:-translate-y-0.5 active:scale-[0.98] shadow-xl"
|
||||
>
|
||||
免费注册
|
||||
<h2 className="text-3xl font-bold text-white mb-4">{t.home.ctaTitle}</h2>
|
||||
<p className="text-brand-100/80 dark:text-brand-200/80 mb-8 text-lg">{t.home.ctaDesc}</p>
|
||||
<Link href="/auth?tab=register" className="inline-flex items-center gap-2 px-8 py-3 text-base font-medium text-brand-600 bg-white rounded-xl hover:bg-brand-50 transition-all hover:-translate-y-0.5 active:scale-[0.98] shadow-xl">
|
||||
{t.home.freeRegister}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -14,25 +14,63 @@ interface Message {
|
||||
|
||||
const AVAILABLE_TOOLS_DESC = `可用工具列表(需要执行操作时,返回 JSON:{"tool":"工具名","params":{...},"description":"简述"}):
|
||||
|
||||
- **get-dashboard**: 获取仪表盘概览数据(用户数、课程数、内容数、提示词数、订单数)
|
||||
- **list-users**: 列出用户,可按关键词搜索(参数: search, page, pageSize)
|
||||
- **get-user**: 查看用户详情(参数: id)
|
||||
- **update-user-status**: 修改用户状态(参数: id, status: ACTIVE|INACTIVE|BANNED)
|
||||
- **list-orders**: 查看最近订单列表
|
||||
- **get-analytics-overview**: 获取数据分析概览(用户增长、收入、趋势)
|
||||
- **list-comments**: 查看评论(参数: status: PENDING_REVIEW|PUBLISHED|REJECTED)
|
||||
- **approve-comment**: 通过评论(参数: id)
|
||||
- **reject-comment**: 拒绝评论(参数: id, reason?)
|
||||
- **list-banners**: 查看所有Banner
|
||||
- **list-notifications**: 查看系统通知
|
||||
- **list-config**: 查看系统配置
|
||||
- **list-roles**: 查看管理角色
|
||||
- **list-admins**: 查看管理员
|
||||
- **get-enterprise-orgs**: 查看企业版组织
|
||||
- **toggle-course-status**: 切换课程上下架(参数: id)
|
||||
- **toggle-content-status**: 切换内容上下架(参数: id)
|
||||
- **toggle-prompt-status**: 切换提示词上下架(参数: id)
|
||||
- **navigate**: 跳转到某个管理页面(参数: path — 如 /admin/users, /admin/orders, /admin/analytics, /admin/enterprise, /admin/operations/banners, /admin/operations/notifications, /admin/settings/roles, /admin/settings/config, /admin/comments, /admin/courses, /admin/prompts, /admin/contents, /admin/tools)
|
||||
=== 查看 ===
|
||||
- **get-dashboard**: 仪表盘概览
|
||||
- **get-analytics-overview**: 数据分析概览
|
||||
- **list-users**: 用户列表(search?, page?, pageSize?)
|
||||
- **get-user**: 用户详情(id)
|
||||
- **list-orders**: 订单列表
|
||||
- **list-courses**: 课程列表 / **get-course**: 课程详情(id)
|
||||
- **list-contents**: 内容列表
|
||||
- **list-prompts**: 提示词列表
|
||||
- **list-comments**: 评论列表(status?)
|
||||
- **list-banners**: Banner列表
|
||||
- **list-notifications**: 通知列表
|
||||
- **list-config**: 配置列表
|
||||
- **list-roles**: 角色列表
|
||||
- **list-admins**: 管理员列表
|
||||
- **get-enterprise-orgs**: 企业组织列表
|
||||
|
||||
=== 创建 ===
|
||||
- **create-course**: 创建课程(title必填, description?, price?, isFree?, status?)
|
||||
- **create-content**: 创建内容(title必填, summary?, content?, tags?, status?)
|
||||
- **create-prompt**: 创建提示词(title必填, content必填, description?, tags?, status?)
|
||||
- **create-banner**: 创建Banner(title必填, image必填, link?, position?, sortOrder?)
|
||||
- **create-notification**: 创建通知(title必填, content?, link?, userId?)
|
||||
- **create-role**: 创建角色(name必填, description?, permissions?)
|
||||
- **create-admin**: 创建管理员(username必填, password必填, nickname?, roleId?)
|
||||
- **create-organization**: 创建企业组织(name必填, description?, contactName?, contactPhone?)
|
||||
|
||||
=== 修改 ===
|
||||
- **update-user**: 修改用户(id, nickname?, phone?, email?, status?, memberPlan?, sandboxDaily?)
|
||||
- **update-user-status**: 修改用户状态(id, status: ACTIVE|INACTIVE|BANNED)
|
||||
- **update-course**: 修改课程(id, title?, description?, price?, isFree?, status?)
|
||||
- **update-content**: 修改内容(id, title?, summary?, content?, tags?, status?)
|
||||
- **update-prompt**: 修改提示词(id, title?, content?, description?, tags?, status?)
|
||||
- **update-banner**: 修改Banner(id, title?, image?, link?, sortOrder?)
|
||||
- **update-config**: 修改配置(key必填, value必填, category?, description?)
|
||||
- **update-role**: 修改角色(id, name?, description?, permissions?)
|
||||
- **update-admin**: 修改管理员(id, nickname?, roleId?, password?)
|
||||
- **update-organization**: 修改企业组织(id, name?, description?, contactName?, contactPhone?)
|
||||
- **toggle-course-status**: 切换课程上下架(id)
|
||||
- **toggle-content-status**: 切换内容上下架(id)
|
||||
- **toggle-prompt-status**: 切换提示词上下架(id)
|
||||
|
||||
=== 删除 ===
|
||||
- **delete-user**: 删除用户(id)
|
||||
- **delete-course**: 删除课程(id)
|
||||
- **delete-content**: 删除内容(id)
|
||||
- **delete-prompt**: 删除提示词(id)
|
||||
- **delete-banner**: 删除Banner(id)
|
||||
- **delete-notification**: 删除通知(id)
|
||||
- **delete-role**: 禁用角色(id)
|
||||
- **delete-admin**: 禁用管理员(id)
|
||||
- **delete-organization**: 删除组织(id)
|
||||
- **add-org-member**: 添加组织成员(organizationId, userId, role?)
|
||||
- **remove-org-member**: 移除组织成员(organizationId, userId)
|
||||
|
||||
=== 导航 ===
|
||||
- **navigate**: 跳转到页面(path: /admin/users等)
|
||||
|
||||
当用户请求执行操作时,先调用对应工具。工具执行完毕后会用自然语言总结结果。`;
|
||||
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
import Link from 'next/link';
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
export function Footer() {
|
||||
const t = useT();
|
||||
return (
|
||||
<footer className="border-t border-border bg-muted/30">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<h3 className="text-lg font-bold bg-gradient-to-r from-brand-600 to-brand-400 bg-clip-text text-transparent mb-4">宇之然 AI</h3>
|
||||
<p className="text-sm text-muted-foreground">让每个人都能用好 AI</p>
|
||||
<p className="text-sm text-muted-foreground">{t.footer.tagline}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-3">探索</h4>
|
||||
<h4 className="text-sm font-semibold mb-3">{t.footer.explore}</h4>
|
||||
<ul className="space-y-2.5">
|
||||
<li><Link href="/courses" className="text-sm text-muted-foreground hover:text-foreground transition-colors">专题</Link></li>
|
||||
<li><Link href="/sandbox" className="text-sm text-muted-foreground hover:text-foreground transition-colors">AI 沙盒</Link></li>
|
||||
<li><Link href="/prompts" className="text-sm text-muted-foreground hover:text-foreground transition-colors">提示词库</Link></li>
|
||||
<li><Link href="/models" className="text-sm text-muted-foreground hover:text-foreground transition-colors">模型百科</Link></li>
|
||||
<li><Link href="/tools" className="text-sm text-muted-foreground hover:text-foreground transition-colors">AI 工具</Link></li>
|
||||
<li><Link href="/courses" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.nav.courses}</Link></li>
|
||||
<li><Link href="/sandbox" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.nav.sandbox}</Link></li>
|
||||
<li><Link href="/prompts" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.nav.prompts}</Link></li>
|
||||
<li><Link href="/models" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.models}</Link></li>
|
||||
<li><Link href="/tools" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.aiTools}</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-3">关于</h4>
|
||||
<h4 className="text-sm font-semibold mb-3">{t.footer.about}</h4>
|
||||
<ul className="space-y-2.5">
|
||||
<li><Link href="/about" className="text-sm text-muted-foreground hover:text-foreground transition-colors">关于我们</Link></li>
|
||||
<li><Link href="/privacy" className="text-sm text-muted-foreground hover:text-foreground transition-colors">隐私政策</Link></li>
|
||||
<li><Link href="/terms" className="text-sm text-muted-foreground hover:text-foreground transition-colors">服务协议</Link></li>
|
||||
<li><Link href="/ai-agreement" className="text-sm text-muted-foreground hover:text-foreground transition-colors">AI 服务协议</Link></li>
|
||||
<li><Link href="/about" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.aboutUs}</Link></li>
|
||||
<li><Link href="/privacy" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.privacy}</Link></li>
|
||||
<li><Link href="/terms" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.terms}</Link></li>
|
||||
<li><Link href="/ai-agreement" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.aiAgreement}</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-3">联系方式</h4>
|
||||
<h4 className="text-sm font-semibold mb-3">{t.footer.contact}</h4>
|
||||
<ul className="space-y-2.5">
|
||||
<li className="text-sm text-muted-foreground">邮箱:contact@yuzhiran.com</li>
|
||||
<li className="text-sm text-muted-foreground">北京宇之然科技中心</li>
|
||||
@@ -38,7 +40,7 @@ export function Footer() {
|
||||
</div>
|
||||
<div className="mt-10 pt-8 border-t border-border">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||
<p>© {new Date().getFullYear()} 北京宇之然科技中心 版权所有</p>
|
||||
<p>{t.footer.copyright.replace('{year}', String(new Date().getFullYear()))}</p>
|
||||
<p>
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">
|
||||
ICP 备案号:京ICP备XXXXXXXX号
|
||||
|
||||
@@ -9,19 +9,7 @@ import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||
import { Search, Menu, X, Bell, Languages } from 'lucide-react';
|
||||
import { apiFetch } from '@/lib/auth';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { useLang } from '@/i18n';
|
||||
|
||||
const navItems = [
|
||||
{ href: '/', label: '首页' },
|
||||
{ href: '/courses', label: '专题' },
|
||||
{ href: '/sandbox', label: '沙盒' },
|
||||
{ href: '/skills', label: '技能' },
|
||||
{ href: '/models', label: '模型' },
|
||||
{ href: '/prompts', label: '提示词' },
|
||||
{ href: '/contents', label: '文章' },
|
||||
{ href: '/tools', label: 'AI 工具' },
|
||||
{ href: '/community', label: '社区' },
|
||||
];
|
||||
import { useLang, useT } from '@/i18n';
|
||||
|
||||
function NotificationBellComponent() {
|
||||
const [count, setCount] = useState(0);
|
||||
@@ -63,6 +51,19 @@ export function Header() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const { isLoggedIn, logout } = useAuth();
|
||||
const { lang, setLang } = useLang();
|
||||
const t = useT();
|
||||
|
||||
const navItems = [
|
||||
{ href: '/', label: t.nav.home },
|
||||
{ href: '/courses', label: t.nav.courses },
|
||||
{ href: '/sandbox', label: t.nav.sandbox },
|
||||
{ href: '/skills', label: t.discover?.skills || '技能' },
|
||||
{ href: '/models', label: t.discover?.models || '模型' },
|
||||
{ href: '/prompts', label: t.nav.prompts },
|
||||
{ href: '/contents', label: t.discover?.articles || '文章' },
|
||||
{ href: '/tools', label: t.nav.tools },
|
||||
{ href: '/community', label: t.nav.community },
|
||||
];
|
||||
|
||||
function isActive(href: string) {
|
||||
if (href === '/') return pathname === '/';
|
||||
@@ -112,7 +113,7 @@ export function Header() {
|
||||
<Input
|
||||
name="q"
|
||||
type="text"
|
||||
placeholder="搜索..."
|
||||
placeholder={t.common.search}
|
||||
className="w-36 lg:w-48 pl-8 h-9 text-sm bg-muted/50 border-0 focus-visible:ring-1"
|
||||
/>
|
||||
</form>
|
||||
@@ -128,23 +129,23 @@ export function Header() {
|
||||
<>
|
||||
<NotificationBellComponent />
|
||||
<Button variant="default" size="sm" asChild>
|
||||
<Link href="/dashboard">控制台</Link>
|
||||
<Link href="/dashboard">{t.nav?.my || '控制台'}</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => { logout(); window.location.href = '/'; }}
|
||||
>
|
||||
退出
|
||||
{t.common.logout}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/auth">登录</Link>
|
||||
<Link href="/auth">{t.common.login}</Link>
|
||||
</Button>
|
||||
<Button variant="default" size="sm" asChild>
|
||||
<Link href="/auth?tab=register">注册</Link>
|
||||
<Link href="/auth?tab=register">{t.common.register}</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -187,19 +188,19 @@ export function Header() {
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
<Button className="w-full" size="sm" asChild>
|
||||
<Link href="/dashboard" onClick={() => setMobileOpen(false)}>控制台</Link>
|
||||
<Link href="/dashboard" onClick={() => setMobileOpen(false)}>{t.nav?.my || '控制台'}</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="w-full" onClick={() => { logout(); window.location.href = '/'; }}>
|
||||
退出
|
||||
{t.common.logout}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="outline" className="w-full" size="sm" asChild>
|
||||
<Link href="/auth" onClick={() => setMobileOpen(false)}>登录</Link>
|
||||
<Link href="/auth" onClick={() => setMobileOpen(false)}>{t.common.login}</Link>
|
||||
</Button>
|
||||
<Button className="w-full" size="sm" asChild>
|
||||
<Link href="/auth?tab=register" onClick={() => setMobileOpen(false)}>注册</Link>
|
||||
<Link href="/auth?tab=register" onClick={() => setMobileOpen(false)}>{t.common.register}</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { AVAILABLE_MODELS, type ModelOption } from '@/lib/models';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { AVAILABLE_MODELS as FALLBACK_MODELS, type ModelOption } from '@/lib/models';
|
||||
import { API_BASE } from '@/lib/config';
|
||||
|
||||
interface ModelSelectorProps {
|
||||
value: string;
|
||||
@@ -11,7 +12,25 @@ interface ModelSelectorProps {
|
||||
|
||||
export function ModelSelector({ value, onChange, className = '' }: ModelSelectorProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const selected = AVAILABLE_MODELS.find(m => m.id === value) || AVAILABLE_MODELS[0];
|
||||
const [models, setModels] = useState<ModelOption[]>(FALLBACK_MODELS);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/public/models`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.items?.length) {
|
||||
setModels(data.items.map((m: any) => ({
|
||||
id: m.id,
|
||||
label: m.name,
|
||||
provider: m.provider,
|
||||
desc: m.description || '',
|
||||
})));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const selected = models.find(m => m.id === value) || models[0];
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
@@ -27,7 +46,7 @@ export function ModelSelector({ value, onChange, className = '' }: ModelSelector
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
|
||||
<div className="absolute right-0 top-full mt-1 z-20 w-64 bg-card border border-border rounded-xl shadow-lg overflow-hidden">
|
||||
{AVAILABLE_MODELS.map(m => (
|
||||
{models.map(m => (
|
||||
<button key={m.id} onClick={() => { onChange(m.id); setOpen(false); }}
|
||||
className={`w-full text-left px-4 py-3 hover:bg-accent transition-colors flex items-center gap-3 ${m.id === value ? 'bg-accent' : ''}`}>
|
||||
<span className={`w-2 h-2 rounded-full shrink-0 ${m.id === value ? 'bg-brand-600' : 'bg-muted-foreground/30'}`} />
|
||||
|
||||
+29
-199
@@ -1,205 +1,35 @@
|
||||
import type { Translations } from './zh'
|
||||
|
||||
const en: Translations = {
|
||||
common: {
|
||||
loading: 'Loading...',
|
||||
save: 'Save',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete',
|
||||
confirm: 'Confirm',
|
||||
search: 'Search',
|
||||
back: 'Back',
|
||||
login: 'Login',
|
||||
register: 'Register',
|
||||
logout: 'Logout',
|
||||
retry: 'Retry',
|
||||
noData: 'No data',
|
||||
viewAll: 'View all',
|
||||
},
|
||||
nav: {
|
||||
home: 'Home',
|
||||
courses: 'Courses',
|
||||
prompts: 'Prompts',
|
||||
sandbox: 'AI Sandbox',
|
||||
discover: 'Discover',
|
||||
my: 'My',
|
||||
tools: 'Tools',
|
||||
community: 'Community',
|
||||
},
|
||||
sandbox: {
|
||||
title: 'AI Sandbox',
|
||||
subtitle: 'Experience AI conversations online, learn by doing',
|
||||
placeholder: 'Type your question...',
|
||||
send: 'Send',
|
||||
sending: 'Sending...',
|
||||
newChat: 'New Chat',
|
||||
history: 'History',
|
||||
searchHistory: 'Search history...',
|
||||
noHistory: 'No history yet',
|
||||
sceneGeneral: 'General',
|
||||
sceneCoding: 'Coding',
|
||||
sceneWriting: 'Writing',
|
||||
sceneStudy: 'Study',
|
||||
sceneEnglish: 'English',
|
||||
modelGeneral: 'General',
|
||||
modelDeepSeek: 'DeepSeek V4 Flash',
|
||||
advancedParams: 'Advanced',
|
||||
temperature: 'Temperature',
|
||||
topP: 'Top P',
|
||||
maxTokens: 'Max Tokens',
|
||||
helpful: 'Helpful',
|
||||
notHelpful: 'Not helpful',
|
||||
runInCodeSandbox: 'Run in Code Sandbox',
|
||||
shareToCommunity: 'Share to Community',
|
||||
copyShareLink: 'Copy Share Link',
|
||||
linkCopied: 'Link copied',
|
||||
loginForMore: 'Login for more',
|
||||
dailyQuota: '{used} used today, {remaining} remaining',
|
||||
aiReplyDisclaimer: 'AI replies are for reference only.',
|
||||
loginForMoreQuota: 'Login to get more usage and models.',
|
||||
justNow: 'just now',
|
||||
minutesAgo: '{n} min ago',
|
||||
hoursAgo: '{n} hour ago',
|
||||
},
|
||||
auth: {
|
||||
loginTitle: 'Login',
|
||||
registerTitle: 'Register',
|
||||
phone: 'Phone',
|
||||
password: 'Password',
|
||||
nickname: 'Nickname',
|
||||
},
|
||||
learning: {
|
||||
analytics: 'Learning Analytics',
|
||||
analyticsDesc: 'Analyze your learning progress from AI sandbox conversations',
|
||||
path: 'Learning Path',
|
||||
pathDesc: 'Master AI skills systematically, step by step',
|
||||
totalSessions: 'AI Conversations',
|
||||
domainsCovered: 'Domains Covered',
|
||||
avgMastery: 'Avg Mastery',
|
||||
knowledgeDomains: 'Knowledge Domains',
|
||||
weakAreas: 'Weak Areas',
|
||||
weakDesc: 'You have less engagement in these areas. Consider strengthening:',
|
||||
recommendations: 'Recommendations',
|
||||
recDesc: 'Based on your weak areas, we recommend:',
|
||||
toStrengthen: 'Needs work',
|
||||
conversations: '{count} conversations',
|
||||
clickToGo: 'Click to visit',
|
||||
},
|
||||
member: {
|
||||
title: 'Membership',
|
||||
desc: 'Manage your subscription',
|
||||
currentPlan: 'Current Plan',
|
||||
freeUser: 'You are on the free plan',
|
||||
monthly: 'Monthly',
|
||||
yearly: 'Yearly',
|
||||
monthlyPrice: 'Subscribe ¥29.9/month',
|
||||
yearlyPrice: 'Subscribe ¥199/year',
|
||||
expires: 'Expires: {date}',
|
||||
benefits: 'All courses + unlimited sandbox + exclusive prompts + ad-free',
|
||||
orderHistory: 'Order History',
|
||||
noOrders: 'No orders yet',
|
||||
processing: 'Processing...',
|
||||
planFree: 'Free',
|
||||
planMonthly: 'Monthly',
|
||||
planYearly: 'Yearly',
|
||||
priceMonthly: '¥29.9',
|
||||
priceYearly: '¥199',
|
||||
perMonth: '/mo',
|
||||
perYear: '/yr',
|
||||
popular: 'Most Popular',
|
||||
featureSandbox: 'AI Sandbox',
|
||||
featureSandboxFree: '10/day',
|
||||
featureSandboxPro: '100/day',
|
||||
featureSandboxUnlimited: 'Unlimited',
|
||||
featureModels: 'Models',
|
||||
featureModelsFree: '1 model',
|
||||
featureModelsPro: '2 models',
|
||||
featureModelsPremium: 'All models',
|
||||
featurePrompts: 'Prompts',
|
||||
featurePromptsFree: 'Basic',
|
||||
featurePromptsPro: 'All',
|
||||
featurePromptsPremium: 'All + Exclusive',
|
||||
featureCourses: 'Courses',
|
||||
featureCoursesFree: 'Some free',
|
||||
featureCoursesPro: 'All',
|
||||
featureCoursesPremium: 'All',
|
||||
featureAds: 'Ads',
|
||||
featureAdsFree: 'Yes',
|
||||
featureAdsPro: 'Ad-free',
|
||||
featureAdsPremium: 'Ad-free',
|
||||
dailyQuota: 'Daily Quota Usage',
|
||||
used: '{n} used',
|
||||
subscribe: 'Subscribe',
|
||||
currentPlan_badge: 'Current Plan',
|
||||
},
|
||||
compare: {
|
||||
title: 'Compare Lab',
|
||||
desc: 'Compare model responses side by side',
|
||||
placeholder: 'Enter your prompt to compare...',
|
||||
startCompare: 'Compare',
|
||||
comparing: 'Comparing...',
|
||||
},
|
||||
codeSandbox: {
|
||||
title: 'Code Sandbox',
|
||||
run: 'Run',
|
||||
runShortcut: 'Run (⌘⏎)',
|
||||
template: 'Template...',
|
||||
blank: 'Blank',
|
||||
react: 'React (CDN)',
|
||||
chart: 'Chart.js',
|
||||
three: 'Three.js',
|
||||
console: 'Console Output',
|
||||
},
|
||||
skills: {
|
||||
title: 'Skill Library',
|
||||
desc: 'Composable AI learning skill modules',
|
||||
search: 'Search skills...',
|
||||
allCategories: 'All Categories',
|
||||
allDifficulties: 'All Levels',
|
||||
beginner: 'Beginner',
|
||||
intermediate: 'Intermediate',
|
||||
advanced: 'Advanced',
|
||||
tasks: 'Practice Tasks',
|
||||
starters: 'Try these questions',
|
||||
prerequisites: 'Prerequisites',
|
||||
apply: 'Use this skill',
|
||||
categories: {
|
||||
basic: 'Basic',
|
||||
technical: 'Technical',
|
||||
creative: 'Creative',
|
||||
education: 'Education',
|
||||
advanced: 'Advanced',
|
||||
career: 'Career',
|
||||
},
|
||||
},
|
||||
promptWorkshop: {
|
||||
title: 'Prompt Workshop',
|
||||
desc: 'Write, test, and optimize your prompts',
|
||||
editor: 'Prompt Editor',
|
||||
test: 'Test Prompt',
|
||||
testing: 'Testing...',
|
||||
clear: 'Clear',
|
||||
saveToLibrary: 'Save to Library',
|
||||
saveSuccess: 'Saved!',
|
||||
variables: 'Variables',
|
||||
role: 'Role',
|
||||
task: 'Task',
|
||||
outputFormat: 'Output Format',
|
||||
constraints: 'Constraints',
|
||||
insert: 'Insert',
|
||||
testResult: 'Test Result',
|
||||
saveDialogTitle: 'Save Prompt',
|
||||
saveTitle: 'Title *',
|
||||
saveDesc: 'Description',
|
||||
saveTags: 'Tags',
|
||||
saveTagsPlaceholder: 'Separate by commas, e.g. coding,Python,debug',
|
||||
saving: 'Saving...',
|
||||
},
|
||||
assistant: {
|
||||
title: 'AI Assistant',
|
||||
greeting: 'Hi! I\'m the Yuzhiran AI assistant. I can help you learn about and use this site. Try asking:',
|
||||
placeholder: 'Type your question...',
|
||||
},
|
||||
common: { loading: 'Loading...', save: 'Save', cancel: 'Cancel', delete: 'Delete', confirm: 'Confirm', search: 'Search', back: 'Back', login: 'Login', register: 'Register', logout: 'Logout', retry: 'Retry', noData: 'No data', viewAll: 'View all' },
|
||||
nav: { home: 'Home', courses: 'Courses', prompts: 'Prompts', sandbox: 'AI Sandbox', discover: 'Discover', my: 'My', tools: 'Tools', community: 'Community' },
|
||||
home: { badge: 'Free AI Learning Community', heroHighlight: 'Empower Everyone', heroRest: 'to Master AI', desc: 'AI knowledge, prompt engineering, sandbox practice & model encyclopedia', startExplore: 'Get Started', freeRegister: 'Register Free', statTopics: 'AI Topics', statPrompts: 'Curated Prompts', statTools: 'AI Tool Reviews', statExplorers: 'Explorers', whyTitle: 'Why Yuzhiran?', whyDesc: 'Four core advantages to master AI fast', featureGuide: 'Guided Learning', featureGuideDesc: 'Content organized by role and scenario', featureSandbox: 'AI Sandbox', featureSandboxDesc: 'Built-in AI sandbox to learn by doing', featurePrompts: 'Prompt Library', featurePromptsDesc: '200+ curated prompt templates', featureUpdate: 'Always Up-to-date', featureUpdateDesc: 'Content updated as AI evolves', popularTopics: 'Popular Topics', popularDesc: 'From beginner to expert, explore AI systematically', moduleCount: '{n} modules', studentCount: '{n} learners', openSandbox: 'Open Sandbox', ctaTitle: 'Ready to Start Your AI Journey?', ctaDesc: 'Register now and explore everything for free' },
|
||||
auth: { loginTitle: 'Login', registerTitle: 'Register', phone: 'Phone', password: 'Password', nickname: 'Nickname', welcomeBack: 'Welcome back', loginSubtitle: 'Log in to continue your AI journey', joinTitle: 'Join Yuzhiran', registerSubtitle: 'Register for free and explore AI', accountPlaceholder: 'Phone / Email', loggingIn: 'Logging in...', nicknameOptional: 'Nickname (optional)', passwordHint: 'Password (min 6 characters)', confirmPassword: 'Confirm password', registering: 'Registering...', agreePrefix: 'By registering, you agree to our', termsOfService: 'Terms of Service', privacyPolicy: 'Privacy Policy', aiAgreement: 'AI Service Agreement', fillAccountAndPassword: 'Please enter account and password', fillPhoneOrEmail: 'Please enter phone or email', fillPassword: 'Please enter password', passwordMinLength: 'Password must be at least 6 characters', passwordsNotMatch: 'Passwords do not match', loginFailed: 'Login failed', registerFailed: 'Registration failed', loginSuccess: 'Login successful', registerSuccess: 'Registration successful' },
|
||||
dashboard: { title: 'My Learning', desc: 'Track your learning progress and stats', inProgressCourses: 'Courses in Progress', completedLessons: 'Lessons Completed', favoritePrompts: 'Favorite Prompts', studyDays: 'Study Days', todayLearned: "Today's Learning", tabProgress: 'Progress', tabFavorites: 'Favorites', tabProfile: 'Profile', noLearningRecords: 'No learning records yet', browseCourses: 'Browse Courses', learningProgress: 'Learning Progress', lessonCount: '{completed}/{total} lessons ({progress}%)', noFavorites: 'No favorite prompts yet', browsePrompts: 'Browse Prompts', profile: 'Profile', nicknameLabel: 'Nickname', nicknamePlaceholder: 'Enter nickname', memberPlan: 'Membership', freeUser: 'Free User', memberExpire: 'Membership Expires', joinDate: 'Joined', saveSuccess: 'Saved successfully', saveFailed: 'Save failed', loadFailed: 'Failed to load data' },
|
||||
community: { title: 'Community', desc: 'Share AI learning experiences with others', createPost: '+ New Post', latest: 'Latest', following: 'Following', newPost: 'New Post', postTitle: 'Title', contentPlaceholder: 'Share your AI learning experience...', tagsPlaceholder: 'Tags (comma-separated)', publishing: 'Publishing...', publish: 'Publish', feedEmpty: 'Follow more users to discover great content', postsEmpty: 'No posts yet — be the first!', commentPlaceholder: 'Write a comment...', sending: 'Sending...', comment: 'Comment', followed: 'Following', follow: '+ Follow' },
|
||||
notifications: { title: 'Notifications', unreadCount: 'You have {n} unread notifications', noUnread: 'No unread notifications', markAllRead: 'Mark all read', markRead: 'Read', emptyTitle: 'No notifications', emptyDesc: 'Likes, comments and follows will appear here' },
|
||||
my: { desc: 'Manage your profile and favorites', learningProgress: 'Learning Progress', learningProgressDesc: 'View your course progress', favorites: 'My Favorites', favoritesDesc: 'Saved prompts and courses', memberCenter: 'Membership', memberDesc: 'Manage subscription and benefits', settings: 'Settings', settingsDesc: 'Account settings and preferences', analyticsDesc: 'Knowledge mastery analysis based on conversations', pathDesc: 'Master AI skills systematically' },
|
||||
settings: { title: 'Settings', desc: 'Manage your account preferences', personalInfo: 'Personal Info', nicknameLabel: 'Nickname', emailLabel: 'Email', saveChanges: 'Save Changes', saveSuccess: 'Saved successfully', accountSecurity: 'Account Security' },
|
||||
myLearning: { back: 'Back', title: 'My Learning', desc: 'Track your course learning progress', empty: 'No courses taken yet', browseCourses: 'Browse Courses', lessonCount: '{completed}/{total} lessons completed' },
|
||||
favorites: { back: 'Back', title: 'My Favorites', desc: 'Saved prompts and courses', empty: 'No favorites yet', browsePrompts: 'Browse Prompts' },
|
||||
courses: { title: 'Courses', desc: 'Explore AI systematically from beginner to expert', empty: 'No courses available', free: 'Free', paid: 'Paid', moduleCount: '{n} modules' },
|
||||
search: { title: 'Search Results', placeholder: 'Search courses, prompts, tools, articles...', emptyHint: 'Enter keywords to search', noResults: 'No results found for "{q}"', resultsCount: '{n} results found', groupCourse: 'Courses', groupPrompt: 'Prompts', groupTool: 'AI Tools', groupContent: 'Articles' },
|
||||
tools: { title: 'AI Tools', desc: 'Curated AI tools to boost your productivity' },
|
||||
discover: { desc: 'Explore trending content and curated picks', hotCourses: 'Hot Courses', hotPrompts: 'Trending Prompts', hotPosts: 'Popular Discussions' },
|
||||
footer: { tagline: 'Empowering Everyone to Master AI', explore: 'Explore', about: 'About', aboutUs: 'About Us', privacy: 'Privacy Policy', terms: 'Terms of Service', aiAgreement: 'AI Service Agreement', contact: 'Contact', copyright: '© {year} Yuzhiran Technology Center. All rights reserved.', models: 'Models', aiTools: 'AI Tools', articles: 'Articles', skills: 'Skills' },
|
||||
models: { title: 'AI Model Encyclopedia', desc: 'Compare mainstream LLMs to find the best fit', tableName: 'Model', tableProvider: 'Provider', tableCapabilities: 'Capabilities', tableContext: 'Context', tableMaxOutput: 'Max Output', tablePricing: 'Pricing', free: 'Free', recommended: 'Recommended', pricingFree: 'Free', pricingMixed: 'Free/Paid', pricingPaid: 'Paid' },
|
||||
error: { title: 'Something went wrong', desc: 'Page failed to load. Please try again.', reload: 'Reload' },
|
||||
notFound: { title: '404', desc: 'Page not found', backToHome: 'Back to Home' },
|
||||
share: { missingToken: 'Missing share token', invalidLink: 'Invalid share link', notAvailable: 'Shared content not available', expired: 'This share link may have expired', goToSandbox: 'Go to AI Sandbox', backToSandbox: 'AI Sandbox', modelInfo: 'Model: {model} · {date}' },
|
||||
path: { back: 'Back', totalProgress: 'Total Progress', taskCount: '{completed}/{total} tasks' },
|
||||
sandbox: { title: 'AI Sandbox', subtitle: 'Experience AI conversations online', placeholder: 'Ask me anything...', send: 'Send', sending: 'Sending', newChat: 'New Chat', history: 'History', searchHistory: 'Search history...', noHistory: 'No history', sceneGeneral: 'General', sceneCoding: 'Coding', sceneWriting: 'Writing', sceneStudy: 'Study', sceneEnglish: 'English', modelGeneral: 'General', modelDeepSeek: 'DeepSeek V4 Flash', advancedParams: 'Advanced', temperature: 'Temperature', topP: 'Top P', maxTokens: 'Max Tokens', helpful: 'Helpful', notHelpful: 'Not Helpful', runInCodeSandbox: 'Run in Code Sandbox', shareToCommunity: 'Share to Community', copyShareLink: 'Copy Link', linkCopied: 'Link copied', loginForMore: 'Login for more', dailyQuota: 'Used {used} today, {remaining} remaining', aiReplyDisclaimer: 'AI replies are for reference only.', loginForMoreQuota: 'Login for more daily quota and models.', justNow: 'just now', minutesAgo: '{n}m ago', hoursAgo: '{n}h ago' },
|
||||
learning: { analytics: 'Learning Analytics', analyticsDesc: 'Analyze your learning based on AI conversations', path: 'Learning Path', pathDesc: 'Master AI skills systematically', totalSessions: 'AI Sessions', domainsCovered: 'Domains Covered', avgMastery: 'Avg Mastery', knowledgeDomains: 'Knowledge Domains', weakAreas: 'Weak Areas', weakDesc: 'Consider strengthening these areas:', recommendations: 'Recommendations', recDesc: 'Based on your weak areas', toStrengthen: 'To Strengthen', conversations: '{count} conversations', clickToGo: 'Go' },
|
||||
member: { title: 'Membership', desc: 'Manage your subscription', currentPlan: 'Current Plan', freeUser: 'You are on the Free plan', monthly: 'Monthly', yearly: 'Yearly', monthlyPrice: '¥29.9/month', yearlyPrice: '¥199/year', expires: 'Expires: {date}', benefits: 'All courses + unlimited sandbox + premium prompts + ad-free', orderHistory: 'Order History', noOrders: 'No orders yet', processing: 'Processing...', planFree: 'Free', planMonthly: 'Monthly', planYearly: 'Yearly', priceMonthly: '¥29.9', priceYearly: '¥199', perMonth: '/mo', perYear: '/yr', popular: 'Popular', featureSandbox: 'AI Sandbox', featureSandboxFree: '10/day', featureSandboxPro: '100/day', featureSandboxUnlimited: 'Unlimited', featureModels: 'Models', featureModelsFree: '1 model', featureModelsPro: '2 models', featureModelsPremium: 'All models', featurePrompts: 'Prompts', featurePromptsFree: 'Basic', featurePromptsPro: 'All', featurePromptsPremium: 'All + Exclusive', featureCourses: 'Courses', featureCoursesFree: 'Partial', featureCoursesPro: 'All', featureCoursesPremium: 'All', featureAds: 'Ads', featureAdsFree: 'Ads', featureAdsPro: 'Ad-free', featureAdsPremium: 'Ad-free', dailyQuota: 'Daily Quota', used: '{n} used', subscribe: 'Subscribe', currentPlan_badge: 'Current' },
|
||||
compare: { title: 'Compare Lab', desc: 'Compare how different models respond', placeholder: 'Enter a question or prompt to compare...', startCompare: 'Start Compare', comparing: 'Comparing...', backToSandbox: 'Back to Sandbox', noResponse: 'No response' },
|
||||
codeSandbox: { title: 'Code Sandbox', run: 'Run', runShortcut: 'Run (⌘⏎)', template: 'Template...', blank: 'Blank', react: 'React (CDN)', chart: 'Chart (Chart.js)', three: '3D (Three.js)', console: 'Console' },
|
||||
skills: { title: 'Skills', desc: 'Composable AI learning skill modules', search: 'Search skills...', allCategories: 'All Categories', allDifficulties: 'All Levels', beginner: 'Beginner', intermediate: 'Intermediate', advanced: 'Advanced', tasks: 'Practice Tasks', starters: 'Try These', prerequisites: 'Prerequisites', apply: 'Use This Skill', categories: { basic: 'Basic', technical: 'Technical', creative: 'Creative', education: 'Education', advanced: 'Advanced', career: 'Career' } },
|
||||
promptWorkshop: { title: 'Prompt Workshop', desc: 'Write, test, and optimize your prompts', editor: 'Prompt Editor', test: 'Test Prompt', testing: 'Testing...', clear: 'Clear', saveToLibrary: 'Save to Library', saveSuccess: 'Saved successfully!', variables: 'Variables', role: 'Role', task: 'Task', outputFormat: 'Output Format', constraints: 'Constraints', insert: 'Insert', testResult: 'Test Result', saveDialogTitle: 'Save Prompt', saveTitle: 'Title *', saveDesc: 'Description', saveTags: 'Tags', saveTagsPlaceholder: 'e.g. programming,Python,debug', saving: 'Saving...' },
|
||||
assistant: { title: 'AI Assistant', greeting: 'Hi! I can help you explore and use this site. Try asking:', placeholder: 'Ask me anything...', error: 'Error: {message}', loginPrompt: '📝 Log in to unlock the full AI experience.\n\nClick "Login" or "Register" in the top right corner.' },
|
||||
}
|
||||
|
||||
export default en
|
||||
|
||||
+29
-199
@@ -1,203 +1,33 @@
|
||||
const zh = {
|
||||
common: {
|
||||
loading: '加载中...',
|
||||
save: '保存',
|
||||
cancel: '取消',
|
||||
delete: '删除',
|
||||
confirm: '确认',
|
||||
search: '搜索',
|
||||
back: '返回',
|
||||
login: '登录',
|
||||
register: '注册',
|
||||
logout: '退出登录',
|
||||
retry: '重试',
|
||||
noData: '暂无数据',
|
||||
viewAll: '查看全部',
|
||||
},
|
||||
nav: {
|
||||
home: '首页',
|
||||
courses: '课程',
|
||||
prompts: '提示词库',
|
||||
sandbox: 'AI 沙盒',
|
||||
discover: '发现',
|
||||
my: '我的',
|
||||
tools: '工具',
|
||||
community: '社区',
|
||||
},
|
||||
sandbox: {
|
||||
title: 'AI 沙盒',
|
||||
subtitle: '在线体验 AI 对话,边学边练',
|
||||
placeholder: '输入你的问题...',
|
||||
send: '发送',
|
||||
sending: '发送中',
|
||||
newChat: '新对话',
|
||||
history: '历史记录',
|
||||
searchHistory: '搜索历史...',
|
||||
noHistory: '暂无历史记录',
|
||||
sceneGeneral: '通用对话',
|
||||
sceneCoding: '编程助手',
|
||||
sceneWriting: '写作助手',
|
||||
sceneStudy: '学习辅导',
|
||||
sceneEnglish: '英语学习',
|
||||
modelGeneral: '通用模式',
|
||||
modelDeepSeek: 'DeepSeek V4 Flash',
|
||||
advancedParams: '高级参数',
|
||||
temperature: 'Temperature',
|
||||
topP: 'Top P',
|
||||
maxTokens: 'Max Tokens',
|
||||
helpful: '有用',
|
||||
notHelpful: '没用',
|
||||
runInCodeSandbox: '在代码沙盒中运行',
|
||||
shareToCommunity: '分享到社区',
|
||||
copyShareLink: '复制分享链接',
|
||||
linkCopied: '链接已复制',
|
||||
loginForMore: '登录使用更多',
|
||||
dailyQuota: '今日已用 {used} 次,剩余 {remaining} 次',
|
||||
aiReplyDisclaimer: 'AI 回复由人工智能生成,仅供参考。',
|
||||
loginForMoreQuota: '登录后可获得更多使用次数和更多模型选择。',
|
||||
justNow: '刚刚',
|
||||
minutesAgo: '{n} 分钟前',
|
||||
hoursAgo: '{n} 小时前',
|
||||
},
|
||||
auth: {
|
||||
loginTitle: '登录',
|
||||
registerTitle: '注册',
|
||||
phone: '手机号',
|
||||
password: '密码',
|
||||
nickname: '昵称',
|
||||
},
|
||||
learning: {
|
||||
analytics: '学情分析',
|
||||
analyticsDesc: '基于 AI 沙盒对话分析你的学习情况',
|
||||
path: '学习路径',
|
||||
pathDesc: '从入门到精通,系统掌握 AI 技能',
|
||||
totalSessions: 'AI 对话次数',
|
||||
domainsCovered: '涉及知识领域',
|
||||
avgMastery: '平均掌握度',
|
||||
knowledgeDomains: '知识领域覆盖',
|
||||
weakAreas: '薄弱环节',
|
||||
weakDesc: '以下领域你较少涉及,建议加强学习:',
|
||||
recommendations: '推荐学习',
|
||||
recDesc: '根据你的薄弱环节推荐以下内容',
|
||||
toStrengthen: '待加强',
|
||||
conversations: '{count} 次对话',
|
||||
clickToGo: '点击前往',
|
||||
},
|
||||
member: {
|
||||
title: '会员中心',
|
||||
desc: '管理你的会员订阅',
|
||||
currentPlan: '当前会员',
|
||||
freeUser: '你当前是免费用户',
|
||||
monthly: '月卡会员',
|
||||
yearly: '年卡会员',
|
||||
monthlyPrice: '开通月卡 ¥29.9/月',
|
||||
yearlyPrice: '开通年卡 ¥199/年',
|
||||
expires: '到期时间:{date}',
|
||||
benefits: '会员权益:全部课程 + 不限次沙箱 + 专属提示词库 + 去广告',
|
||||
orderHistory: '订单记录',
|
||||
noOrders: '暂无订单记录',
|
||||
processing: '处理中...',
|
||||
planFree: '免费',
|
||||
planMonthly: '月卡',
|
||||
planYearly: '年卡',
|
||||
priceMonthly: '¥29.9',
|
||||
priceYearly: '¥199',
|
||||
perMonth: '/月',
|
||||
perYear: '/年',
|
||||
popular: '最受欢迎',
|
||||
featureSandbox: 'AI 沙盒',
|
||||
featureSandboxFree: '10 次/日',
|
||||
featureSandboxPro: '100 次/日',
|
||||
featureSandboxUnlimited: '不限次',
|
||||
featureModels: '模型选择',
|
||||
featureModelsFree: '1 个模型',
|
||||
featureModelsPro: '2 个模型',
|
||||
featureModelsPremium: '全部模型',
|
||||
featurePrompts: '提示词库',
|
||||
featurePromptsFree: '基础',
|
||||
featurePromptsPro: '全部',
|
||||
featurePromptsPremium: '全部 + 专属',
|
||||
featureCourses: '课程学习',
|
||||
featureCoursesFree: '部分免费',
|
||||
featureCoursesPro: '全部',
|
||||
featureCoursesPremium: '全部',
|
||||
featureAds: '广告',
|
||||
featureAdsFree: '有广告',
|
||||
featureAdsPro: '去广告',
|
||||
featureAdsPremium: '去广告',
|
||||
dailyQuota: '日配额用量',
|
||||
used: '已用 {n} 次',
|
||||
subscribe: '开通',
|
||||
currentPlan_badge: '当前方案',
|
||||
},
|
||||
compare: {
|
||||
title: '对比实验室',
|
||||
desc: '同题对比不同模型的表现',
|
||||
placeholder: '输入你想对比的问题或提示词...',
|
||||
startCompare: '开始对比',
|
||||
comparing: '对比中...',
|
||||
},
|
||||
codeSandbox: {
|
||||
title: '代码沙盒',
|
||||
run: '运行',
|
||||
runShortcut: '运行 (⌘⏎)',
|
||||
template: '模板...',
|
||||
blank: '空白',
|
||||
react: 'React (CDN)',
|
||||
chart: '图表 (Chart.js)',
|
||||
three: '3D (Three.js)',
|
||||
console: '控制台输出',
|
||||
},
|
||||
skills: {
|
||||
title: '技能库',
|
||||
desc: '可组合的 AI 学习技能模块',
|
||||
search: '搜索技能...',
|
||||
allCategories: '全部分类',
|
||||
allDifficulties: '全部难度',
|
||||
beginner: '入门',
|
||||
intermediate: '中级',
|
||||
advanced: '高级',
|
||||
tasks: '练习任务',
|
||||
starters: '试试这些问题',
|
||||
prerequisites: '前置技能',
|
||||
apply: '使用此技能',
|
||||
categories: {
|
||||
basic: '基础',
|
||||
technical: '技术',
|
||||
creative: '创意',
|
||||
education: '教育',
|
||||
advanced: '进阶',
|
||||
career: '职业',
|
||||
},
|
||||
},
|
||||
promptWorkshop: {
|
||||
title: '提示词工坊',
|
||||
desc: '编写、测试、优化你的提示词',
|
||||
editor: '提示词编辑',
|
||||
test: '测试提示词',
|
||||
testing: '测试中...',
|
||||
clear: '清空',
|
||||
saveToLibrary: '保存到提示词库',
|
||||
saveSuccess: '保存成功!',
|
||||
variables: '变量设置',
|
||||
role: '角色',
|
||||
task: '任务',
|
||||
outputFormat: '输出格式',
|
||||
constraints: '约束条件',
|
||||
insert: '插入',
|
||||
testResult: '测试结果',
|
||||
saveDialogTitle: '保存提示词',
|
||||
saveTitle: '标题 *',
|
||||
saveDesc: '描述',
|
||||
saveTags: '标签',
|
||||
saveTagsPlaceholder: '用逗号分隔,如:编程,Python,调试',
|
||||
saving: '保存中...',
|
||||
},
|
||||
assistant: {
|
||||
title: 'AI 助手',
|
||||
greeting: '你好!我是宇之然 AI 助手,可以帮你了解和使用本站功能。试试下面的问题:',
|
||||
placeholder: '输入你的问题...',
|
||||
},
|
||||
common: { loading: '加载中...', save: '保存', cancel: '取消', delete: '删除', confirm: '确认', search: '搜索', back: '返回', login: '登录', register: '注册', logout: '退出登录', retry: '重试', noData: '暂无数据', viewAll: '查看全部' },
|
||||
nav: { home: '首页', courses: '课程', prompts: '提示词库', sandbox: 'AI 沙盒', discover: '发现', my: '我的', tools: '工具', community: '社区' },
|
||||
home: { badge: '免费 AI 知识社区', heroHighlight: '让每个人', heroRest: '都能用好 AI', desc: '涵盖 AI 通识、提示词工程、沙盒实战、模型百科', startExplore: '开始探索', freeRegister: '免费注册', statTopics: 'AI 专题', statPrompts: '精选提示词', statTools: 'AI 工具评测', statExplorers: '探索者', whyTitle: '为什么选择宇之然?', whyDesc: '四大核心优势,助你快速掌握 AI', featureGuide: '分领域指南', featureGuideDesc: '按职业和场景分类内容,学即所用', featureSandbox: 'AI 沙盒实战', featureSandboxDesc: '内置 AI 对话沙盒,边学边练', featurePrompts: '提示词库', featurePromptsDesc: '精选 200+ 提示词模板', featureUpdate: '持续更新', featureUpdateDesc: '紧跟大模型迭代,内容实时更新', popularTopics: '热门专题', popularDesc: '从入门到精通,系统探索 AI', moduleCount: '{n} 模块', studentCount: '{n} 人关注', openSandbox: '打开沙盒', ctaTitle: '准备好开启 AI 之旅了吗?', ctaDesc: '立即注册,免费探索所有内容' },
|
||||
auth: { loginTitle: '登录', registerTitle: '注册', phone: '手机号', password: '密码', nickname: '昵称', welcomeBack: '欢迎回来', loginSubtitle: '登录继续你的 AI 探索之旅', joinTitle: '加入宇之然', registerSubtitle: '免费注册,开始探索 AI', accountPlaceholder: '手机号 / 邮箱', loggingIn: '登录中...', nicknameOptional: '昵称(选填)', passwordHint: '密码(至少 6 位)', confirmPassword: '确认密码', registering: '注册中...', agreePrefix: '注册即表示同意', termsOfService: '服务协议', privacyPolicy: '隐私政策', aiAgreement: 'AI 服务协议', fillAccountAndPassword: '请填写账号和密码', fillPhoneOrEmail: '请填写手机号或邮箱', fillPassword: '请填写密码', passwordMinLength: '密码至少 6 位', passwordsNotMatch: '两次密码不一致', loginFailed: '登录失败', registerFailed: '注册失败', loginSuccess: '登录成功', registerSuccess: '注册成功' },
|
||||
dashboard: { title: '我的学习', desc: '掌握你的学习进度和统计', inProgressCourses: '学习中课程', completedLessons: '已完成课时', favoritePrompts: '收藏提示词', studyDays: '学习天数', todayLearned: '今日学习', tabProgress: '学习进度', tabFavorites: '收藏夹', tabProfile: '个人设置', noLearningRecords: '还没有学习记录', browseCourses: '浏览课程', learningProgress: '学习进度', lessonCount: '{completed}/{total} 课时 ({progress}%)', noFavorites: '还没有收藏的提示词', browsePrompts: '浏览提示词', profile: '个人资料', nicknameLabel: '昵称', nicknamePlaceholder: '输入昵称', memberPlan: '会员计划', freeUser: '免费用户', memberExpire: '会员到期', joinDate: '注册时间', saveSuccess: '保存成功', saveFailed: '保存失败', loadFailed: '加载数据失败' },
|
||||
community: { title: '社区', desc: '与 AI 学习者交流心得', createPost: '+ 发帖', latest: '最新', following: '关注', newPost: '发布新帖', postTitle: '标题', contentPlaceholder: '分享你的 AI 学习心得、实战经验...', tagsPlaceholder: '标签(逗号分隔)', publishing: '发布中...', publish: '发布', feedEmpty: '关注更多用户,发现精彩内容', postsEmpty: '还没有帖子,来发第一帖吧!', commentPlaceholder: '写下你的评论...', sending: '发送中...', comment: '评论', followed: '已关注', follow: '+ 关注' },
|
||||
notifications: { title: '通知', unreadCount: '你有 {n} 条未读通知', noUnread: '暂无未读通知', markAllRead: '全部已读', markRead: '已读', emptyTitle: '暂无通知', emptyDesc: '点赞、评论或关注你的人会出现在这里' },
|
||||
my: { desc: '管理你的个人信息和收藏', learningProgress: '学习进度', learningProgressDesc: '查看你的课程学习进度', favorites: '我的收藏', favoritesDesc: '提示词、课程等收藏内容', memberCenter: '会员中心', memberDesc: '管理会员订阅和权益', settings: '设置', settingsDesc: '账号设置和安全偏好', analyticsDesc: '基于对话的知识掌握度分析', pathDesc: '分阶段系统掌握 AI 技能' },
|
||||
settings: { title: '设置', desc: '管理你的账号偏好', personalInfo: '个人信息', nicknameLabel: '昵称', emailLabel: '邮箱', saveChanges: '保存修改', saveSuccess: '保存成功', accountSecurity: '账号安全' },
|
||||
myLearning: { back: '返回我的', title: '学习进度', desc: '跟踪你的课程学习进度', empty: '还没有学习任何课程', browseCourses: '去选课', lessonCount: '已完成 {completed}/{total} 课时' },
|
||||
favorites: { back: '返回我的', title: '我的收藏', desc: '收藏的提示词和课程内容', empty: '还没有收藏任何内容', browsePrompts: '浏览提示词' },
|
||||
courses: { title: '专题', desc: '系统化探索 AI,从入门到精通', empty: '暂无专题内容', free: '免费', paid: '付费', moduleCount: '{n} 模块' },
|
||||
search: { title: '搜索结果', placeholder: '搜索专题、提示词、工具、文章...', emptyHint: '输入关键词搜索', noResults: '未找到与 "{q}" 相关的结果', resultsCount: '找到 {n} 个结果', groupCourse: '专题', groupPrompt: '提示词', groupTool: 'AI 工具', groupContent: '文章' },
|
||||
tools: { title: 'AI 工具库', desc: '收录优质 AI 工具,助力工作效率提升' },
|
||||
discover: { desc: '探索热门内容和精选推荐', hotCourses: '热门课程', hotPrompts: '热门提示词', hotPosts: '热门讨论' },
|
||||
footer: { tagline: '让每个人都能用好 AI', explore: '探索', about: '关于', aboutUs: '关于我们', privacy: '隐私政策', terms: '服务协议', aiAgreement: 'AI 服务协议', contact: '联系方式', copyright: '© {year} 北京宇之然科技中心 版权所有', models: '模型百科', aiTools: 'AI 工具', articles: '文章', skills: '技能' },
|
||||
models: { title: 'AI 模型百科', desc: '收录主流大语言模型,全面对比各项参数', tableName: '模型名称', tableProvider: '提供商', tableCapabilities: '能力', tableContext: '上下文', tableMaxOutput: '最大输出', tablePricing: '价格', free: '免费', recommended: '推荐', pricingFree: '免费', pricingMixed: '免费/付费', pricingPaid: '付费' },
|
||||
error: { title: '出错了', desc: '页面加载失败,请稍后重试', reload: '重新加载' },
|
||||
notFound: { title: '404', desc: '页面未找到', backToHome: '返回首页' },
|
||||
share: { missingToken: '缺少分享参数', invalidLink: '分享链接无效', notAvailable: '分享内容不可用', expired: '该分享链接可能已过期或不存在', goToSandbox: '前往 AI 沙盒', backToSandbox: 'AI 沙盒', modelInfo: '模型: {model} · {date}' },
|
||||
path: { back: '返回我的', totalProgress: '总进度', taskCount: '{completed}/{total} 任务' },
|
||||
sandbox: { title: 'AI 沙盒', subtitle: '在线体验 AI 对话,边学边练', placeholder: '输入你的问题...', send: '发送', sending: '发送中', newChat: '新对话', history: '历史记录', searchHistory: '搜索历史...', noHistory: '暂无历史记录', sceneGeneral: '通用对话', sceneCoding: '编程助手', sceneWriting: '写作助手', sceneStudy: '学习辅导', sceneEnglish: '英语学习', modelGeneral: '通用模式', modelDeepSeek: 'DeepSeek V4 Flash', advancedParams: '高级参数', temperature: 'Temperature', topP: 'Top P', maxTokens: 'Max Tokens', helpful: '有用', notHelpful: '没用', runInCodeSandbox: '在代码沙盒中运行', shareToCommunity: '分享到社区', copyShareLink: '复制分享链接', linkCopied: '链接已复制', loginForMore: '登录使用更多', dailyQuota: '今日已用 {used} 次,剩余 {remaining} 次', aiReplyDisclaimer: 'AI 回复由人工智能生成,仅供参考。', loginForMoreQuota: '登录后可获得更多使用次数和更多模型选择。', justNow: '刚刚', minutesAgo: '{n} 分钟前', hoursAgo: '{n} 小时前' },
|
||||
learning: { analytics: '学情分析', analyticsDesc: '基于 AI 沙盒对话分析你的学习情况', path: '学习路径', pathDesc: '从入门到精通,系统掌握 AI 技能', totalSessions: 'AI 对话次数', domainsCovered: '涉及知识领域', avgMastery: '平均掌握度', knowledgeDomains: '知识领域覆盖', weakAreas: '薄弱环节', weakDesc: '以下领域你较少涉及,建议加强学习:', recommendations: '推荐学习', recDesc: '根据你的薄弱环节推荐以下内容', toStrengthen: '待加强', conversations: '{count} 次对话', clickToGo: '点击前往' },
|
||||
member: { title: '会员中心', desc: '管理你的会员订阅', currentPlan: '当前会员', freeUser: '你当前是免费用户', monthly: '月卡会员', yearly: '年卡会员', monthlyPrice: '开通月卡 ¥29.9/月', yearlyPrice: '开通年卡 ¥199/年', expires: '到期时间:{date}', benefits: '会员权益:全部课程 + 不限次沙箱 + 专属提示词库 + 去广告', orderHistory: '订单记录', noOrders: '暂无订单记录', processing: '处理中...', planFree: '免费', planMonthly: '月卡', planYearly: '年卡', priceMonthly: '¥29.9', priceYearly: '¥199', perMonth: '/月', perYear: '/年', popular: '最受欢迎', featureSandbox: 'AI 沙盒', featureSandboxFree: '10 次/日', featureSandboxPro: '100 次/日', featureSandboxUnlimited: '不限次', featureModels: '模型选择', featureModelsFree: '1 个模型', featureModelsPro: '2 个模型', featureModelsPremium: '全部模型', featurePrompts: '提示词库', featurePromptsFree: '基础', featurePromptsPro: '全部', featurePromptsPremium: '全部 + 专属', featureCourses: '课程学习', featureCoursesFree: '部分免费', featureCoursesPro: '全部', featureCoursesPremium: '全部', featureAds: '广告', featureAdsFree: '有广告', featureAdsPro: '去广告', featureAdsPremium: '去广告', dailyQuota: '日配额用量', used: '已用 {n} 次', subscribe: '开通', currentPlan_badge: '当前方案' },
|
||||
compare: { title: '对比实验室', desc: '同题对比不同模型的表现', placeholder: '输入你想对比的问题或提示词...', startCompare: '开始对比', comparing: '对比中...', backToSandbox: '返回沙箱', noResponse: '无响应' },
|
||||
codeSandbox: { title: '代码沙盒', run: '运行', runShortcut: '运行 (⌘⏎)', template: '模板...', blank: '空白', react: 'React (CDN)', chart: '图表 (Chart.js)', three: '3D (Three.js)', console: '控制台输出' },
|
||||
skills: { title: '技能库', desc: '可组合的 AI 学习技能模块', search: '搜索技能...', allCategories: '全部分类', allDifficulties: '全部难度', beginner: '入门', intermediate: '中级', advanced: '高级', tasks: '练习任务', starters: '试试这些问题', prerequisites: '前置技能', apply: '使用此技能', categories: { basic: '基础', technical: '技术', creative: '创意', education: '教育', advanced: '进阶', career: '职业' } },
|
||||
promptWorkshop: { title: '提示词工坊', desc: '编写、测试、优化你的提示词', editor: '提示词编辑', test: '测试提示词', testing: '测试中...', clear: '清空', saveToLibrary: '保存到提示词库', saveSuccess: '保存成功!', variables: '变量设置', role: '角色', task: '任务', outputFormat: '输出格式', constraints: '约束条件', insert: '插入', testResult: '测试结果', saveDialogTitle: '保存提示词', saveTitle: '标题 *', saveDesc: '描述', saveTags: '标签', saveTagsPlaceholder: '用逗号分隔,如:编程,Python,调试', saving: '保存中...' },
|
||||
assistant: { title: 'AI 助手', greeting: '你好!我是宇之然 AI 助手,可以帮你了解和使用本站功能。试试下面的问题:', placeholder: '输入你的问题...', error: '出错啦:{message}', loginPrompt: '📝 登录后可体验完整 AI 对话功能。\n\n点击右上角「登录」或「注册」即可开始使用,解锁 AI 助手的全部能力。' },
|
||||
}
|
||||
|
||||
export type Translations = typeof zh
|
||||
|
||||
Reference in New Issue
Block a user