refactor: improve forum, profile and settings page UI

Streamline profile and settings pages, refine forum listing and post pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 16:45:40 +08:00
parent 38865b0f7d
commit 5d42a0573a
5 changed files with 293 additions and 553 deletions
+17 -37
View File
@@ -33,40 +33,22 @@ function getCategoryIcon(name: string) {
return CATEGORY_ICONS.default; return CATEGORY_ICONS.default;
} }
function getCategoryGradient(name: string) {
if (name.includes("公告") || name.includes("通知"))
return "from-rose-500/10 to-rose-50";
if (name.includes("学习") || name.includes("讨论") || name.includes("课程"))
return "from-blue-500/10 to-blue-50";
if (name.includes("反馈") || name.includes("使用"))
return "from-amber-500/10 to-amber-50";
return "from-slate-500/10 to-slate-50";
}
function getCategoryAccent(name: string) { function getCategoryAccent(name: string) {
if (name.includes("公告") || name.includes("通知")) return "text-rose-600"; if (name.includes("公告") || name.includes("通知")) return "text-rose-600 dark:text-rose-400";
if (name.includes("学习") || name.includes("讨论") || name.includes("课程")) return "text-blue-600"; if (name.includes("学习") || name.includes("讨论") || name.includes("课程")) return "text-blue-600 dark:text-blue-400";
if (name.includes("反馈") || name.includes("使用")) return "text-amber-600"; if (name.includes("反馈") || name.includes("使用")) return "text-amber-600 dark:text-amber-400";
return "text-slate-600"; return "text-slate-600 dark:text-slate-400";
} }
function formatRelativeTime(dateStr: string) { function getCategoryBorder(name: string) {
const date = new Date( if (name.includes("公告") || name.includes("通知")) return "border-l-rose-500";
new Date(dateStr).getTime() + 8 * 60 * 60 * 1000 if (name.includes("学习") || name.includes("讨论") || name.includes("课程")) return "border-l-blue-500";
); if (name.includes("反馈") || name.includes("使用")) return "border-l-amber-500";
const now = new Date(); return "border-l-slate-500";
const diffMs = now.getTime() - date.getTime();
const diffMinutes = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMinutes < 1) return "刚刚";
if (diffMinutes < 60) return `${diffMinutes} 分钟前`;
if (diffHours < 24) return `${diffHours} 小时前`;
if (diffDays < 7) return `${diffDays} 天前`;
return date.toLocaleDateString("zh-CN", { month: "short", day: "numeric" });
} }
import { format } from "date-fns";
export default function ForumHomePage() { export default function ForumHomePage() {
const [categories, setCategories] = useState<ForumCategory[]>([]); const [categories, setCategories] = useState<ForumCategory[]>([]);
const [categoryPosts, setCategoryPosts] = useState< const [categoryPosts, setCategoryPosts] = useState<
@@ -134,12 +116,10 @@ export default function ForumHomePage() {
return ( return (
<div <div
key={category.id} key={category.id}
className={`bg-gradient-to-r ${getCategoryGradient( className={`rounded-xl border border-border overflow-hidden border-l-4 ${getCategoryBorder(category.name)}`}
category.name
)} rounded-xl border border-border/40 overflow-hidden`}
> >
{/* Category header */} {/* Category header */}
<div className="px-5 py-3.5 flex items-center justify-between border-b border-border/20"> <div className="px-5 py-3.5 flex items-center justify-between border-b border-border">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className={getCategoryAccent(category.name)}> <span className={getCategoryAccent(category.name)}>
{getCategoryIcon(category.name)} {getCategoryIcon(category.name)}
@@ -170,15 +150,15 @@ export default function ForumHomePage() {
</div> </div>
{/* Posts list */} {/* Posts list */}
<div className="divide-y divide-border/20"> <div className="divide-y divide-border">
{posts.length > 0 ? ( {posts.length > 0 ? (
posts.map((post) => ( posts.map((post) => (
<Link <Link
key={post.id} key={post.id}
href={`/forum/post/${post.id}`} href={`/forum/post/${post.id}`}
className="flex items-start gap-3 px-5 py-3.5 hover:bg-white/40 transition-colors group" className="flex items-start gap-3 px-5 py-3.5 hover:bg-accent transition-colors group"
> >
<div className="mt-0.5 w-8 h-8 rounded-full bg-muted/60 flex items-center justify-center flex-shrink-0"> <div className="mt-0.5 w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
<User className="w-4 h-4 text-muted-foreground" /> <User className="w-4 h-4 text-muted-foreground" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -189,7 +169,7 @@ export default function ForumHomePage() {
<span>{post.author_name}</span> <span>{post.author_name}</span>
<span className="inline-flex items-center gap-0.5"> <span className="inline-flex items-center gap-0.5">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
{formatRelativeTime(post.created_at)} {format(new Date(post.created_at), "yyyy/M/d HH:mm")}
</span> </span>
<span className="inline-flex items-center gap-0.5"> <span className="inline-flex items-center gap-0.5">
<MessageCircle className="w-3 h-3" /> <MessageCircle className="w-3 h-3" />
+84 -190
View File
@@ -7,16 +7,10 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { useAuthStore } from "@/store/auth"; import { useAuthStore } from "@/store/auth";
import MobileNav from "@/components/layout/mobile-nav"; import MobileNav from "@/components/layout/mobile-nav";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { import {
Loader2, Loader2,
User as UserIcon,
Mail,
Calendar,
CheckCircle, CheckCircle,
Edit2, Edit2,
Save, Save,
@@ -69,10 +63,7 @@ export default function ProfilePage() {
router.push("/login"); router.push("/login");
return; return;
} }
if (isAuthenticated) loadUserData();
if (isAuthenticated) {
loadUserData();
}
}, [isAuthenticated, authLoading, router]); }, [isAuthenticated, authLoading, router]);
useEffect(() => { useEffect(() => {
@@ -86,12 +77,8 @@ export default function ProfilePage() {
try { try {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
// 加载用户信息
const userData = await authAPI.getCurrentUser(); const userData = await authAPI.getCurrentUser();
setUser(userData); setUser(userData);
// 加载统计数据
try { try {
const stats = await analyticsAPI.getStatistics(); const stats = await analyticsAPI.getStatistics();
setStatistics({ setStatistics({
@@ -102,7 +89,6 @@ export default function ProfilePage() {
}); });
} catch (err) { } catch (err) {
console.warn("加载统计数据失败:", err); console.warn("加载统计数据失败:", err);
// 统计数据加载失败不影响页面显示
} }
} catch (err) { } catch (err) {
console.error("加载用户信息失败:", err); console.error("加载用户信息失败:", err);
@@ -117,17 +103,13 @@ export default function ProfilePage() {
setIsSaving(true); setIsSaving(true);
setError(null); setError(null);
setSuccess(null); setSuccess(null);
const updatedUser = await authAPI.updateUserInfo({ const updatedUser = await authAPI.updateUserInfo({
email: data.email, email: data.email,
full_name: data.full_name || undefined, full_name: data.full_name || undefined,
}); });
setUser(updatedUser); setUser(updatedUser);
setIsEditing(false); setIsEditing(false);
setSuccess("个人信息更新成功"); setSuccess("个人信息更新成功");
// 3秒后清除成功消息
setTimeout(() => setSuccess(null), 3000); setTimeout(() => setSuccess(null), 3000);
} catch (err: any) { } catch (err: any) {
console.error("更新用户信息失败:", err); console.error("更新用户信息失败:", err);
@@ -155,235 +137,147 @@ export default function ProfilePage() {
); );
} }
if (!isAuthenticated || !user) { if (!isAuthenticated || !user) return null;
return null;
}
const initials = (user.full_name || user.username || "U").charAt(0).toUpperCase(); const initials = (user.full_name || user.username || "U").charAt(0).toUpperCase();
return ( return (
<div className="min-h-screen bg-app pb-16 lg:pb-0"> <div className="min-h-screen bg-app pb-16 lg:pb-0">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* 错误提示 */}
{/* 状态提示 */}
{error && ( {error && (
<Alert variant="destructive" className="mb-6"> <div className="mb-6 p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">{error}</div>
<AlertDescription>{error}</AlertDescription>
</Alert>
)} )}
{/* 成功提示 */}
{success && ( {success && (
<Alert className="mb-6 border-green-500 bg-green-50 dark:bg-green-950"> <div className="mb-6 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-600" /> <CheckCircle className="w-4 h-4" />{success}
<AlertDescription className="text-green-800 dark:text-green-200"> </div>
{success}
</AlertDescription>
</Alert>
)} )}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="max-w-2xl space-y-8">
{/* 左侧:用户头像基本信息 */} {/* 头像基本信息 */}
<div className="lg:col-span-2 space-y-6"> <section>
{/* 用户头像卡片 */} <div className="flex items-center justify-between mb-4 pb-3 border-b">
<Card> <h2 className="text-base font-semibold"></h2>
<CardHeader> {!isEditing && (
<CardTitle></CardTitle> <button
<CardDescription></CardDescription> onClick={() => setIsEditing(true)}
</CardHeader> className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
<CardContent> >
<div className="flex items-center space-x-6"> <Edit2 className="w-3.5 h-3.5" />
<div className="w-24 h-24 bg-gradient-to-r from-blue-600 to-purple-600 rounded-full flex items-center justify-center text-white text-3xl font-bold shadow-lg"> </button>
)}
</div>
{/* 头像 */}
<div className="flex items-center gap-5 mb-6">
<div className="w-16 h-16 bg-primary rounded-full flex items-center justify-center text-primary-foreground text-xl font-bold">
{initials} {initials}
</div> </div>
<div className="flex-1">
<p className="text-sm text-muted-foreground mb-2">
使
</p>
<p className="text-xs text-muted-foreground">
</p>
</div>
</div>
</CardContent>
</Card>
{/* 基本信息卡片 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div> <div>
<CardTitle></CardTitle> <p className="text-sm font-medium">{user.full_name || user.username}</p>
<CardDescription></CardDescription> <p className="text-xs text-muted-foreground">{user.email}</p>
</div> </div>
{!isEditing && (
<Button
variant="outline"
size="sm"
onClick={() => setIsEditing(true)}
>
<Edit2 className="w-4 h-4 mr-2" />
</Button>
)}
</div> </div>
</CardHeader>
<CardContent> {/* 表单 */}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* 用户名(只读) */} <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<Label htmlFor="username"></Label> <Label className="text-sm text-muted-foreground"></Label>
<Input <Input value={user.username} disabled className="mt-1 bg-muted" />
id="username" <p className="text-[11px] text-muted-foreground mt-1"></p>
value={user.username} </div>
disabled <div>
className="bg-muted" <Label className="text-sm text-muted-foreground"></Label>
/> <Input value={formatDate(user.created_at)} disabled className="mt-1 bg-muted" />
<p className="text-xs text-muted-foreground mt-1"> </div>
</p>
</div> </div>
{/* 邮箱(可编辑) */}
<div> <div>
<Label htmlFor="email"></Label> <Label htmlFor="email" className="text-sm text-muted-foreground"></Label>
<Input <Input
id="email" id="email"
type="email" type="email"
{...register("email")} {...register("email")}
disabled={!isEditing} disabled={!isEditing}
className={errors.email ? "border-red-500" : ""} className={`mt-1 ${errors.email ? "border-red-500" : ""}`}
/> />
{errors.email && ( {errors.email && <p className="text-xs text-red-500 mt-1">{errors.email.message}</p>}
<p className="text-xs text-red-500 mt-1">
{errors.email.message}
</p>
)}
</div> </div>
{/* 真实姓名(可编辑) */}
<div> <div>
<Label htmlFor="full_name"></Label> <Label htmlFor="full_name" className="text-sm text-muted-foreground"></Label>
<Input <Input
id="full_name" id="full_name"
{...register("full_name")} {...register("full_name")}
disabled={!isEditing} disabled={!isEditing}
placeholder="请输入您的真实姓名(可选)" placeholder="请输入您的真实姓名(可选)"
className="mt-1"
/> />
</div> </div>
{/* 注册时间(只读) */}
<div>
<Label htmlFor="created_at"></Label>
<Input
id="created_at"
value={formatDate(user.created_at)}
disabled
className="bg-muted"
/>
</div>
{/* 账户状态(只读) */}
<div>
<Label htmlFor="is_active"></Label>
<div className="flex items-center space-x-2 mt-2">
<Input
id="is_active"
value={user.is_active ? "已激活" : "未激活"}
disabled
className="bg-muted"
/>
{user.is_active && (
<CheckCircle className="w-5 h-5 text-green-600" />
)}
</div>
</div>
{/* 编辑模式下的按钮 */}
{isEditing && ( {isEditing && (
<div className="flex items-center space-x-3 pt-4"> <div className="flex items-center gap-3 pt-2">
<Button type="submit" disabled={isSaving}> <button
{isSaving ? ( type="submit"
<> disabled={isSaving}
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-50"
... >
</> {isSaving ? <><Loader2 className="w-4 h-4 animate-spin" />...</> : <><Save className="w-4 h-4" /></>}
) : ( </button>
<> <button
<Save className="w-4 h-4 mr-2" />
</>
)}
</Button>
<Button
type="button" type="button"
variant="outline"
onClick={handleCancel} onClick={handleCancel}
disabled={isSaving} disabled={isSaving}
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border text-sm hover:bg-muted disabled:opacity-50"
> >
<X className="w-4 h-4 mr-2" /> <X className="w-4 h-4" />
</button>
</Button>
</div> </div>
)} )}
</form> </form>
</CardContent> </section>
</Card>
</div>
{/* 右侧:学习统计 */} {/* 学习统计 */}
<div className="lg:col-span-1"> <section>
<Card> <div className="flex items-center gap-2 mb-4 pb-3 border-b">
<CardHeader> <h2 className="text-base font-semibold"></h2>
<CardTitle></CardTitle> </div>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
{statistics ? ( {statistics ? (
<div className="space-y-4"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"> <div className="p-3 rounded-lg border text-center">
<div className="flex items-center space-x-3"> <MessageSquare className="w-4 h-4 mx-auto mb-1.5 text-primary" />
<MessageSquare className="w-5 h-5 text-blue-600" /> <p className="text-lg font-semibold">{statistics.total_sessions}</p>
<span className="text-sm font-medium"></span> <p className="text-[11px] text-muted-foreground"></p>
</div> </div>
<span className="text-lg font-bold">{statistics.total_sessions}</span> <div className="p-3 rounded-lg border text-center">
<MessageSquare className="w-4 h-4 mx-auto mb-1.5 text-green-600" />
<p className="text-lg font-semibold">{statistics.total_messages}</p>
<p className="text-[11px] text-muted-foreground"></p>
</div> </div>
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"> <div className="p-3 rounded-lg border text-center">
<div className="flex items-center space-x-3"> <Database className="w-4 h-4 mx-auto mb-1.5 text-purple-600" />
<MessageSquare className="w-5 h-5 text-green-600" /> <p className="text-lg font-semibold">{statistics.total_documents}</p>
<span className="text-sm font-medium"></span> <p className="text-[11px] text-muted-foreground"></p>
</div> </div>
<span className="text-lg font-bold">{statistics.total_messages}</span> <div className="p-3 rounded-lg border text-center">
</div> <FileText className="w-4 h-4 mx-auto mb-1.5 text-orange-600" />
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"> <p className="text-lg font-semibold">{statistics.active_days}</p>
<div className="flex items-center space-x-3"> <p className="text-[11px] text-muted-foreground"></p>
<Database className="w-5 h-5 text-purple-600" />
<span className="text-sm font-medium"></span>
</div>
<span className="text-lg font-bold">-</span>
</div>
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
<div className="flex items-center space-x-3">
<FileText className="w-5 h-5 text-orange-600" />
<span className="text-sm font-medium"></span>
</div>
<span className="text-lg font-bold">{statistics.total_documents}</span>
</div> </div>
</div> </div>
) : ( ) : (
<div className="text-center py-8"> <div className="text-center py-6">
<Loader2 className="w-6 h-6 animate-spin mx-auto mb-2 text-muted-foreground" /> <Loader2 className="w-5 h-5 animate-spin mx-auto mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground">...</p> <p className="text-sm text-muted-foreground">...</p>
</div> </div>
)} )}
</CardContent> </section>
</Card>
</div> </div>
</div> </div>
</div>
{/* 移动端导航 */}
<MobileNav /> <MobileNav />
</div> </div>
); );
} }
+71 -174
View File
@@ -2,7 +2,6 @@
import { useState } from "react"; import { useState } from "react";
// 禁用静态生成
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -10,15 +9,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { useAuthStore } from "@/store/auth"; import { useAuthStore } from "@/store/auth";
import MobileNav from "@/components/layout/mobile-nav"; import MobileNav from "@/components/layout/mobile-nav";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { ThemeToggle } from "@/components/ui/theme-toggle"; import { ThemeToggle } from "@/components/ui/theme-toggle";
import { import {
Loader2, Loader2,
Lock,
Eye, Eye,
EyeOff, EyeOff,
CheckCircle, CheckCircle,
@@ -39,17 +34,8 @@ const passwordSchema = z.object({
type PasswordForm = z.infer<typeof passwordSchema>; type PasswordForm = z.infer<typeof passwordSchema>;
// 密码强度检查函数 function getPasswordStrength(password: string) {
function getPasswordStrength(password: string): { if (!password) return { label: "", color: "", score: 0 };
strength: "weak" | "medium" | "strong";
label: string;
color: string;
score: number;
} {
if (!password) {
return { strength: "weak", label: "", color: "", score: 0 };
}
let score = 0; let score = 0;
if (password.length >= 6) score++; if (password.length >= 6) score++;
if (password.length >= 8) score++; if (password.length >= 8) score++;
@@ -58,13 +44,9 @@ function getPasswordStrength(password: string): {
if (/[0-9]/.test(password)) score++; if (/[0-9]/.test(password)) score++;
if (/[^a-zA-Z0-9]/.test(password)) score++; if (/[^a-zA-Z0-9]/.test(password)) score++;
if (score <= 2) { if (score <= 2) return { label: "弱", color: "text-red-600", score };
return { strength: "weak", label: "", color: "text-red-600", score }; if (score <= 4) return { label: "", color: "text-yellow-600", score };
} else if (score <= 4) { return { label: "强", color: "text-green-600", score };
return { strength: "medium", label: "中", color: "text-yellow-600", score };
} else {
return { strength: "strong", label: "强", color: "text-green-600", score };
}
} }
export default function SettingsPage() { export default function SettingsPage() {
@@ -95,16 +77,12 @@ export default function SettingsPage() {
setIsSubmitting(true); setIsSubmitting(true);
setError(null); setError(null);
setSuccess(null); setSuccess(null);
await authAPI.changePassword({ await authAPI.changePassword({
old_password: data.old_password, old_password: data.old_password,
new_password: data.new_password, new_password: data.new_password,
}); });
setSuccess("密码修改成功"); setSuccess("密码修改成功");
reset(); reset();
// 3秒后清除成功消息
setTimeout(() => setSuccess(null), 3000); setTimeout(() => setSuccess(null), 3000);
} catch (err: any) { } catch (err: any) {
console.error("修改密码失败:", err); console.error("修改密码失败:", err);
@@ -129,40 +107,32 @@ export default function SettingsPage() {
return ( return (
<div className="min-h-screen bg-app pb-16 lg:pb-0"> <div className="min-h-screen bg-app pb-16 lg:pb-0">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* 错误提示 */}
{/* 状态提示 */}
{error && ( {error && (
<Alert variant="destructive" className="mb-6"> <div className="mb-6 p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">
<AlertDescription>{error}</AlertDescription> {error}
</Alert> </div>
)} )}
{success && (
{/* 成功提示 */} <div className="mb-6 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm flex items-center gap-2">
{success && ( <CheckCircle className="w-4 h-4" />
<Alert className="mb-6 border-green-500 bg-green-50 dark:bg-green-950"> {success}
<CheckCircle className="w-4 h-4 text-green-600" /> </div>
<AlertDescription className="text-green-800 dark:text-green-200"> )}
{success}
</AlertDescription> <div className="max-w-2xl space-y-8">
</Alert> {/* 账户安全 */}
)} <section>
<div className="flex items-center gap-2 mb-4 pb-3 border-b">
<div className="space-y-6"> <Shield className="w-4 h-4 text-primary" />
{/* 账户安全 */} <h2 className="text-base font-semibold"></h2>
<Card>
<CardHeader>
<div className="flex items-center space-x-2">
<Shield className="w-5 h-5 text-blue-600" />
<CardTitle></CardTitle>
</div> </div>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* 当前密码 */}
<div> <div>
<Label htmlFor="old_password"></Label> <Label htmlFor="old_password" className="text-sm text-muted-foreground"></Label>
<div className="relative"> <div className="relative mt-1">
<Input <Input
id="old_password" id="old_password"
type={showOldPassword ? "text" : "password"} type={showOldPassword ? "text" : "password"}
@@ -170,31 +140,20 @@ export default function SettingsPage() {
className={errors.old_password ? "border-red-500" : ""} className={errors.old_password ? "border-red-500" : ""}
placeholder="请输入当前密码" placeholder="请输入当前密码"
/> />
<Button <button
type="button" type="button"
variant="ghost" className="absolute right-3 top-1/2 -translate-y-1/2"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowOldPassword(!showOldPassword)} onClick={() => setShowOldPassword(!showOldPassword)}
> >
{showOldPassword ? ( {showOldPassword ? <EyeOff className="w-4 h-4 text-muted-foreground" /> : <Eye className="w-4 h-4 text-muted-foreground" />}
<EyeOff className="w-4 h-4 text-muted-foreground" /> </button>
) : (
<Eye className="w-4 h-4 text-muted-foreground" />
)}
</Button>
</div> </div>
{errors.old_password && ( {errors.old_password && <p className="text-xs text-red-500 mt-1">{errors.old_password.message}</p>}
<p className="text-xs text-red-500 mt-1">
{errors.old_password.message}
</p>
)}
</div> </div>
{/* 新密码 */}
<div> <div>
<Label htmlFor="new_password"></Label> <Label htmlFor="new_password" className="text-sm text-muted-foreground"></Label>
<div className="relative"> <div className="relative mt-1">
<Input <Input
id="new_password" id="new_password"
type={showNewPassword ? "text" : "password"} type={showNewPassword ? "text" : "password"}
@@ -202,52 +161,37 @@ export default function SettingsPage() {
className={errors.new_password ? "border-red-500" : ""} className={errors.new_password ? "border-red-500" : ""}
placeholder="请输入新密码(至少6个字符)" placeholder="请输入新密码(至少6个字符)"
/> />
<Button <button
type="button" type="button"
variant="ghost" className="absolute right-3 top-1/2 -translate-y-1/2"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowNewPassword(!showNewPassword)} onClick={() => setShowNewPassword(!showNewPassword)}
> >
{showNewPassword ? ( {showNewPassword ? <EyeOff className="w-4 h-4 text-muted-foreground" /> : <Eye className="w-4 h-4 text-muted-foreground" />}
<EyeOff className="w-4 h-4 text-muted-foreground" /> </button>
) : (
<Eye className="w-4 h-4 text-muted-foreground" />
)}
</Button>
</div> </div>
{newPassword && ( {newPassword && (
<div className="mt-2"> <div className="mt-2">
<div className="flex items-center space-x-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="text-xs text-muted-foreground"></span> <span className="text-xs text-muted-foreground"></span>
<span className={`text-xs font-medium ${passwordStrength.color}`}> <span className={`text-xs font-medium ${passwordStrength.color}`}>{passwordStrength.label}</span>
{passwordStrength.label}
</span>
</div> </div>
<div className="w-full bg-muted rounded-full h-2"> <div className="w-full bg-muted rounded-full h-1.5">
<div <div
className={`h-2 rounded-full transition-all ${ className={`h-1.5 rounded-full transition-all ${
passwordStrength.strength === "weak" passwordStrength.score <= 2 ? "bg-red-500 w-1/3"
? "bg-red-500 w-1/3" : passwordStrength.score <= 4 ? "bg-yellow-500 w-2/3"
: passwordStrength.strength === "medium"
? "bg-yellow-500 w-2/3"
: "bg-green-500 w-full" : "bg-green-500 w-full"
}`} }`}
/> />
</div> </div>
</div> </div>
)} )}
{errors.new_password && ( {errors.new_password && <p className="text-xs text-red-500 mt-1">{errors.new_password.message}</p>}
<p className="text-xs text-red-500 mt-1">
{errors.new_password.message}
</p>
)}
</div> </div>
{/* 确认新密码 */}
<div> <div>
<Label htmlFor="confirm_password"></Label> <Label htmlFor="confirm_password" className="text-sm text-muted-foreground"></Label>
<div className="relative"> <div className="relative mt-1">
<Input <Input
id="confirm_password" id="confirm_password"
type={showConfirmPassword ? "text" : "password"} type={showConfirmPassword ? "text" : "password"}
@@ -255,97 +199,50 @@ export default function SettingsPage() {
className={errors.confirm_password ? "border-red-500" : ""} className={errors.confirm_password ? "border-red-500" : ""}
placeholder="请再次输入新密码" placeholder="请再次输入新密码"
/> />
<Button <button
type="button" type="button"
variant="ghost" className="absolute right-3 top-1/2 -translate-y-1/2"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowConfirmPassword(!showConfirmPassword)} onClick={() => setShowConfirmPassword(!showConfirmPassword)}
> >
{showConfirmPassword ? ( {showConfirmPassword ? <EyeOff className="w-4 h-4 text-muted-foreground" /> : <Eye className="w-4 h-4 text-muted-foreground" />}
<EyeOff className="w-4 h-4 text-muted-foreground" /> </button>
) : (
<Eye className="w-4 h-4 text-muted-foreground" />
)}
</Button>
</div> </div>
{errors.confirm_password && ( {errors.confirm_password && <p className="text-xs text-red-500 mt-1">{errors.confirm_password.message}</p>}
<p className="text-xs text-red-500 mt-1">
{errors.confirm_password.message}
</p>
)}
</div> </div>
{/* 密码要求提示 */} <div className="pt-2">
<div className="p-4 bg-muted/50 rounded-lg"> <button
<p className="text-sm font-medium mb-2"></p> type="submit"
<ul className="text-xs text-muted-foreground space-y-1"> disabled={isSubmitting}
<li> 6</li> className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-50"
<li> </li> >
<li> 使</li>
</ul>
</div>
{/* 提交按钮 */}
<div className="pt-4">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? ( {isSubmitting ? (
<> <><Loader2 className="w-4 h-4 animate-spin" />...</>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : ( ) : (
<> <><Key className="w-4 h-4" /></>
<Key className="w-4 h-4 mr-2" />
</>
)} )}
</Button> </button>
</div> </div>
</form> </form>
</CardContent> </section>
</Card>
{/* 偏好设置 */} {/* 偏好设置 */}
<Card> <section>
<CardHeader> <div className="flex items-center gap-2 mb-4 pb-3 border-b">
<div className="flex items-center space-x-2"> <Palette className="w-4 h-4 text-primary" />
<Palette className="w-5 h-5 text-purple-600" /> <h2 className="text-base font-semibold"></h2>
<CardTitle></CardTitle>
</div> </div>
<CardDescription>使</CardDescription> <div className="flex items-center justify-between py-3 border-b last:border-0">
</CardHeader> <div>
<CardContent> <p className="text-sm font-medium"></p>
<div className="space-y-4"> <p className="text-xs text-muted-foreground mt-0.5"></p>
{/* 主题设置 */}
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<Palette className="w-4 h-4 text-muted-foreground" />
<Label className="text-base font-medium"></Label>
</div>
<p className="text-sm text-muted-foreground">
/
</p>
</div> </div>
<ThemeToggle /> <ThemeToggle />
</div> </div>
</section>
{/* 未来可添加其他偏好设置 */}
<div className="p-4 bg-muted/50 rounded-lg">
<p className="text-sm text-muted-foreground">
</p>
</div> </div>
</div> </div>
</CardContent>
</Card>
</div>
</div>
{/* 移动端导航 */}
<MobileNav /> <MobileNav />
</div> </div>
); );
} }
+14 -32
View File
@@ -5,7 +5,6 @@ import Link from "next/link";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { forumAPI } from "@/lib/api"; import { forumAPI } from "@/lib/api";
import type { ForumCategory, ForumPostSummary } from "@/types"; import type { ForumCategory, ForumPostSummary } from "@/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { import {
@@ -19,22 +18,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { useAuthStore } from "@/store/auth"; import { useAuthStore } from "@/store/auth";
function formatRelativeTime(dateStr: string) { import { format } from "date-fns";
const date = new Date(
new Date(dateStr).getTime() + 8 * 60 * 60 * 1000
);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMinutes = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMinutes < 1) return "刚刚";
if (diffMinutes < 60) return `${diffMinutes} 分钟前`;
if (diffHours < 24) return `${diffHours} 小时前`;
if (diffDays < 7) return `${diffDays} 天前`;
return date.toLocaleDateString("zh-CN", { month: "short", day: "numeric" });
}
export default function ForumCategoryPage() { export default function ForumCategoryPage() {
const params = useParams(); const params = useParams();
@@ -118,7 +102,7 @@ export default function ForumCategoryPage() {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Back nav */} {/* Back nav */}
<button <button
onClick={() => router.push("/forum")} onClick={() => router.push("/forum")}
@@ -144,20 +128,19 @@ export default function ForumCategoryPage() {
)} )}
</div> </div>
{isAuthenticated && ( {isAuthenticated && (
<Button <button
onClick={() => setShowForm(!showForm)} onClick={() => setShowForm(!showForm)}
size="sm" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-[#2563eb] text-white hover:bg-[#2563eb]/90 transition-colors"
className="gap-1.5"
> >
<PenLine className="w-3.5 h-3.5" /> <PenLine className="w-3.5 h-3.5" />
{showForm ? "收起" : "发帖"} {showForm ? "收起" : "发帖"}
</Button> </button>
)} )}
</div> </div>
{/* New post form (collapsible) */} {/* New post form (collapsible) */}
{showForm && ( {showForm && (
<div className="bg-muted/30 rounded-xl border border-border/40 p-5 mb-6 space-y-4"> <div className="bg-muted rounded-xl border border-border p-5 mb-6 space-y-4">
<form onSubmit={handleSubmit} className="space-y-3"> <form onSubmit={handleSubmit} className="space-y-3">
{submitError && ( {submitError && (
<p className="text-sm text-destructive">{submitError}</p> <p className="text-sm text-destructive">{submitError}</p>
@@ -179,11 +162,10 @@ export default function ForumCategoryPage() {
disabled={isSubmitting} disabled={isSubmitting}
/> />
<div className="flex justify-end"> <div className="flex justify-end">
<Button <button
type="submit" type="submit"
size="sm"
disabled={isSubmitting} disabled={isSubmitting}
className="gap-1.5" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-[#2563eb] text-white hover:bg-[#2563eb]/90 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
> >
{isSubmitting ? ( {isSubmitting ? (
<> <>
@@ -196,7 +178,7 @@ export default function ForumCategoryPage() {
</> </>
)} )}
</Button> </button>
</div> </div>
</form> </form>
</div> </div>
@@ -204,7 +186,7 @@ export default function ForumCategoryPage() {
{/* Login prompt */} {/* Login prompt */}
{!isAuthenticated && ( {!isAuthenticated && (
<div className="text-center py-4 mb-6 bg-muted/20 rounded-lg text-sm text-muted-foreground"> <div className="text-center py-4 mb-6 bg-muted rounded-lg text-sm text-muted-foreground">
<Link href="/login" className="text-primary hover:underline mx-1"> <Link href="/login" className="text-primary hover:underline mx-1">
@@ -235,14 +217,14 @@ export default function ForumCategoryPage() {
<p className="text-muted-foreground"></p> <p className="text-muted-foreground"></p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-border/40 rounded-xl border border-border/40 overflow-hidden"> <div className="divide-y divide-border/40 rounded-xl border border-border overflow-hidden">
{posts.map((post) => ( {posts.map((post) => (
<Link <Link
key={post.id} key={post.id}
href={`/forum/post/${post.id}`} href={`/forum/post/${post.id}`}
className="flex items-start gap-3 px-5 py-4 hover:bg-muted/30 transition-colors group" className="flex items-start gap-3 px-5 py-4 hover:bg-muted transition-colors group"
> >
<div className="mt-0.5 w-9 h-9 rounded-full bg-muted/60 flex items-center justify-center flex-shrink-0"> <div className="mt-0.5 w-9 h-9 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
<User className="w-4 h-4 text-muted-foreground" /> <User className="w-4 h-4 text-muted-foreground" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -253,7 +235,7 @@ export default function ForumCategoryPage() {
<span>{post.author_name}</span> <span>{post.author_name}</span>
<span className="inline-flex items-center gap-0.5"> <span className="inline-flex items-center gap-0.5">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
{formatRelativeTime(post.created_at)} {format(new Date(post.created_at), "yyyy/M/d HH:mm")}
</span> </span>
<span className="inline-flex items-center gap-0.5"> <span className="inline-flex items-center gap-0.5">
<MessageCircle className="w-3 h-3" /> <MessageCircle className="w-3 h-3" />
+13 -26
View File
@@ -9,22 +9,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Loader2, MessageCircle, Clock, User, Send, ArrowLeft } from "lucide-react"; import { Loader2, MessageCircle, Clock, User, Send, ArrowLeft } from "lucide-react";
import { useAuthStore } from "@/store/auth"; import { useAuthStore } from "@/store/auth";
function formatRelativeTime(dateStr: string) { import { format } from "date-fns";
const date = new Date(
new Date(dateStr).getTime() + 8 * 60 * 60 * 1000
);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMinutes = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMinutes < 1) return "刚刚";
if (diffMinutes < 60) return `${diffMinutes} 分钟前`;
if (diffHours < 24) return `${diffHours} 小时前`;
if (diffDays < 7) return `${diffDays} 天前`;
return date.toLocaleDateString("zh-CN", { month: "short", day: "numeric" });
}
export default function ForumPostPage() { export default function ForumPostPage() {
const params = useParams(); const params = useParams();
@@ -90,7 +75,8 @@ export default function ForumPostPage() {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="max-w-3xl mx-auto">
{/* Back nav */} {/* Back nav */}
<button <button
onClick={() => router.back()} onClick={() => router.back()}
@@ -112,8 +98,8 @@ export default function ForumPostPage() {
) : post ? ( ) : post ? (
<div className="space-y-6"> <div className="space-y-6">
{/* Post content */} {/* Post content */}
<article className="rounded-xl border border-border/40 overflow-hidden"> <article className="rounded-xl border border-border overflow-hidden">
<div className="px-6 py-5 border-b border-border/20"> <div className="px-6 py-5 border-b border-border">
<h1 <h1
className="text-xl font-bold tracking-tight leading-snug" className="text-xl font-bold tracking-tight leading-snug"
style={{ fontFamily: "var(--font-serif), Georgia, serif" }} style={{ fontFamily: "var(--font-serif), Georgia, serif" }}
@@ -122,14 +108,14 @@ export default function ForumPostPage() {
</h1> </h1>
<div className="flex items-center gap-3 mt-3 text-sm text-muted-foreground"> <div className="flex items-center gap-3 mt-3 text-sm text-muted-foreground">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className="w-6 h-6 rounded-full bg-muted/60 flex items-center justify-center"> <div className="w-6 h-6 rounded-full bg-muted flex items-center justify-center">
<User className="w-3 h-3 text-muted-foreground" /> <User className="w-3 h-3 text-muted-foreground" />
</div> </div>
<span>{post.author_name}</span> <span>{post.author_name}</span>
</div> </div>
<span className="inline-flex items-center gap-0.5"> <span className="inline-flex items-center gap-0.5">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
{formatRelativeTime(post.created_at)} {format(new Date(post.created_at), "yyyy/M/d HH:mm")}
</span> </span>
</div> </div>
</div> </div>
@@ -148,7 +134,7 @@ export default function ForumPostPage() {
</div> </div>
{post.replies.length === 0 ? ( {post.replies.length === 0 ? (
<div className="text-center py-8 text-sm text-muted-foreground bg-muted/20 rounded-xl"> <div className="text-center py-8 text-sm text-muted-foreground bg-muted rounded-xl">
</div> </div>
) : ( ) : (
@@ -156,9 +142,9 @@ export default function ForumPostPage() {
{post.replies.map((reply) => ( {post.replies.map((reply) => (
<div <div
key={reply.id} key={reply.id}
className="flex gap-3 px-5 py-4 rounded-xl border border-border/30 hover:border-border/60 transition-colors" className="flex gap-3 px-5 py-4 rounded-xl border border-border hover:bg-accent transition-colors"
> >
<div className="w-8 h-8 rounded-full bg-muted/60 flex items-center justify-center flex-shrink-0 mt-0.5"> <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0 mt-0.5">
<User className="w-3.5 h-3.5 text-muted-foreground" /> <User className="w-3.5 h-3.5 text-muted-foreground" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -167,7 +153,7 @@ export default function ForumPostPage() {
{reply.author_name} {reply.author_name}
</span> </span>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{formatRelativeTime(reply.created_at)} {format(new Date(reply.created_at), "yyyy/M/d HH:mm")}
</span> </span>
</div> </div>
<p className="text-sm whitespace-pre-wrap leading-relaxed text-foreground/90"> <p className="text-sm whitespace-pre-wrap leading-relaxed text-foreground/90">
@@ -181,7 +167,7 @@ export default function ForumPostPage() {
</section> </section>
{/* Reply form */} {/* Reply form */}
<div className="rounded-xl border border-border/40 p-5"> <div className="rounded-xl border border-border p-5">
{isAuthenticated ? ( {isAuthenticated ? (
<form onSubmit={handleReply} className="space-y-3"> <form onSubmit={handleReply} className="space-y-3">
{submitError && ( {submitError && (
@@ -238,5 +224,6 @@ export default function ForumPostPage() {
) : null} ) : null}
</div> </div>
</div> </div>
</div>
); );
} }