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:
@@ -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" />
|
||||
|
||||
@@ -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 {
|
||||
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">
|
||||
<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 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>
|
||||
<CardTitle>基本信息</CardTitle>
|
||||
<CardDescription>您的账户基本信息</CardDescription>
|
||||
<p className="text-sm font-medium">{user.full_name || user.username}</p>
|
||||
<p className="text-xs text-muted-foreground">{user.email}</p>
|
||||
</div>
|
||||
{!isEditing && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
<Edit2 className="w-4 h-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
{/* 表单 */}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* 用户名(只读) */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-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>
|
||||
<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>
|
||||
<div>
|
||||
<Label className="text-sm text-muted-foreground">注册时间</Label>
|
||||
<Input value={formatDate(user.created_at)} disabled className="mt-1 bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 邮箱(可编辑) */}
|
||||
<div>
|
||||
<Label htmlFor="email">邮箱</Label>
|
||||
<Label htmlFor="email" className="text-sm text-muted-foreground">邮箱</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
{...register("email")}
|
||||
disabled={!isEditing}
|
||||
className={errors.email ? "border-red-500" : ""}
|
||||
className={`mt-1 ${errors.email ? "border-red-500" : ""}`}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
{errors.email && <p className="text-xs text-red-500 mt-1">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* 真实姓名(可编辑) */}
|
||||
<div>
|
||||
<Label htmlFor="full_name">真实姓名</Label>
|
||||
<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>
|
||||
|
||||
{/* 账户状态(只读) */}
|
||||
<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
|
||||
<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"
|
||||
variant="outline"
|
||||
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 mr-2" />
|
||||
取消
|
||||
</Button>
|
||||
<X className="w-4 h-4" />取消
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 右侧:学习统计 */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>学习统计</CardTitle>
|
||||
<CardDescription>您的学习数据概览</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* 学习统计 */}
|
||||
<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="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 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>
|
||||
<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 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 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>
|
||||
<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 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-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 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>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 移动端导航 */}
|
||||
<MobileNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
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,40 +107,32 @@ 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>
|
||||
)}
|
||||
|
||||
{/* 成功提示 */}
|
||||
{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="space-y-6">
|
||||
{/* 账户安全 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Shield className="w-5 h-5 text-blue-600" />
|
||||
<CardTitle>账户安全</CardTitle>
|
||||
<div className="mb-6 p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">
|
||||
{error}
|
||||
</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">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-w-2xl space-y-8">
|
||||
{/* 账户安全 */}
|
||||
<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>
|
||||
<CardDescription>修改您的登录密码</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* 当前密码 */}
|
||||
<div>
|
||||
<Label htmlFor="old_password">当前密码</Label>
|
||||
<div className="relative">
|
||||
<Label htmlFor="old_password" className="text-sm text-muted-foreground">当前密码</Label>
|
||||
<div className="relative mt-1">
|
||||
<Input
|
||||
id="old_password"
|
||||
type={showOldPassword ? "text" : "password"}
|
||||
@@ -170,31 +140,20 @@ export default function SettingsPage() {
|
||||
className={errors.old_password ? "border-red-500" : ""}
|
||||
placeholder="请输入当前密码"
|
||||
/>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
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>
|
||||
{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>
|
||||
)}
|
||||
{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">
|
||||
<Label htmlFor="new_password" className="text-sm text-muted-foreground">新密码</Label>
|
||||
<div className="relative mt-1">
|
||||
<Input
|
||||
id="new_password"
|
||||
type={showNewPassword ? "text" : "password"}
|
||||
@@ -202,52 +161,37 @@ export default function SettingsPage() {
|
||||
className={errors.new_password ? "border-red-500" : ""}
|
||||
placeholder="请输入新密码(至少6个字符)"
|
||||
/>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
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>
|
||||
{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">
|
||||
<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>
|
||||
<span className={`text-xs font-medium ${passwordStrength.color}`}>{passwordStrength.label}</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div className="w-full bg-muted rounded-full h-1.5">
|
||||
<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"
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
{errors.new_password && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors.new_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">确认新密码</Label>
|
||||
<div className="relative">
|
||||
<Label htmlFor="confirm_password" className="text-sm text-muted-foreground">确认新密码</Label>
|
||||
<div className="relative mt-1">
|
||||
<Input
|
||||
id="confirm_password"
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
@@ -255,97 +199,50 @@ export default function SettingsPage() {
|
||||
className={errors.confirm_password ? "border-red-500" : ""}
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
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>
|
||||
{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.confirm_password && <p className="text-xs text-red-500 mt-1">{errors.confirm_password.message}</p>}
|
||||
</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}>
|
||||
<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 mr-2 animate-spin" />
|
||||
修改中...
|
||||
</>
|
||||
<><Loader2 className="w-4 h-4 animate-spin" />修改中...</>
|
||||
) : (
|
||||
<>
|
||||
<Key className="w-4 h-4 mr-2" />
|
||||
修改密码
|
||||
</>
|
||||
<><Key className="w-4 h-4" />修改密码</>
|
||||
)}
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
<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 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>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* 未来可添加其他偏好设置 */}
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
更多偏好设置功能即将推出
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 移动端导航 */}
|
||||
<MobileNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 && (
|
||||
@@ -238,5 +224,6 @@ export default function ForumPostPage() {
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user