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