- 新增 SettingsService 接口 + Context 注入,组件层不再直接 import users/messaging actions - 新增 resolveRoleSettingsConfig 配置驱动角色路由,删除 parent/student/teacher-settings-view 冗余文件 - 新增 SettingsSectionErrorBoundary,每个 TabsContent + profile 角色概览区块均包裹 - 新增 ProfileStudentOverview/ProfileTeacherOverview 异步 Server Component + 骨架屏,支持流式渲染 - 抽取 buildStudentOverviewData 等纯函数到 lib/student-overview-data.ts,便于单元测试 - 新增 settings.json 翻译文件(zh-CN + en),所有组件改用 useTranslations/getTranslations - 重构 profile/page.tsx:i18n 适配 + Suspense 分区加载 + 业务逻辑抽离 - 同步更新架构图 004/005
167 lines
7.2 KiB
TypeScript
167 lines
7.2 KiB
TypeScript
import Link from "next/link"
|
|
import { redirect } from "next/navigation"
|
|
import { Suspense, type ReactElement } from "react"
|
|
import { getTranslations } from "next-intl/server"
|
|
import { User, Mail, Phone, MapPin, Calendar, Clock, Shield } from "lucide-react"
|
|
|
|
import { requireAuth } from "@/shared/lib/auth-guard"
|
|
import { getUserProfile } from "@/modules/users/data-access"
|
|
import { ProfileStudentOverview, ProfileStudentOverviewSkeleton } from "@/modules/settings/components/profile-student-overview"
|
|
import { ProfileTeacherOverview, ProfileTeacherOverviewSkeleton } from "@/modules/settings/components/profile-teacher-overview"
|
|
import { SettingsSectionErrorBoundary } from "@/modules/settings/components/settings-section-error-boundary"
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/components/ui/avatar"
|
|
import { Badge } from "@/shared/components/ui/badge"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
|
import { PageHeader } from "@/shared/components/ui/page-header"
|
|
import { formatDate } from "@/shared/lib/utils"
|
|
|
|
export const dynamic = "force-dynamic"
|
|
|
|
export async function generateMetadata() {
|
|
const t = await getTranslations("settings.profilePage")
|
|
return { title: t("title") }
|
|
}
|
|
|
|
export default async function ProfilePage(): Promise<ReactElement> {
|
|
const ctx = await requireAuth()
|
|
|
|
const userId = ctx.userId
|
|
const userProfile = await getUserProfile(userId)
|
|
|
|
if (!userProfile) {
|
|
redirect("/login")
|
|
}
|
|
|
|
const roles = ctx.roles
|
|
const isStudent = roles.includes("student")
|
|
const isTeacher = roles.includes("teacher")
|
|
const t = await getTranslations("settings.profilePage")
|
|
|
|
return (
|
|
<div className="flex h-full flex-col gap-8 p-8">
|
|
<PageHeader
|
|
title={t("title")}
|
|
description={t("description")}
|
|
actions={
|
|
<Button asChild variant="outline">
|
|
<Link href="/settings">{t("editProfile")}</Link>
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<Avatar className="h-20 w-20">
|
|
{userProfile.image ? <AvatarImage src={userProfile.image} alt={userProfile.name ?? t("title")} /> : null}
|
|
<AvatarFallback className="text-xl font-semibold">
|
|
{(userProfile.name ?? userProfile.email).slice(0, 2).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="space-y-1">
|
|
<div className="text-xl font-semibold tracking-tight">{userProfile.name ?? "-"}</div>
|
|
<div className="text-sm text-muted-foreground">{userProfile.email}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<User className="h-5 w-5" />
|
|
{t("personalInfo.title")}
|
|
</CardTitle>
|
|
<CardDescription>{t("personalInfo.description")}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div className="space-y-1">
|
|
<div className="text-sm font-medium text-muted-foreground">{t("personalInfo.fullName")}</div>
|
|
<div className="text-sm font-medium">{userProfile.name ?? "-"}</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="text-sm font-medium text-muted-foreground">{t("personalInfo.gender")}</div>
|
|
<div className="text-sm capitalize">{userProfile.gender ?? "-"}</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="text-sm font-medium text-muted-foreground">{t("personalInfo.age")}</div>
|
|
<div className="text-sm">{userProfile.age ?? "-"}</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="text-sm font-medium text-muted-foreground">{t("personalInfo.phone")}</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
{userProfile.phone ? <Phone className="h-3 w-3 text-muted-foreground" /> : null}
|
|
{userProfile.phone ?? "-"}
|
|
</div>
|
|
</div>
|
|
<div className="col-span-1 sm:col-span-2 space-y-1">
|
|
<div className="text-sm font-medium text-muted-foreground">{t("personalInfo.address")}</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
{userProfile.address ? <MapPin className="h-3 w-3 text-muted-foreground" /> : null}
|
|
{userProfile.address ?? "-"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Shield className="h-5 w-5" />
|
|
{t("accountInfo.title")}
|
|
</CardTitle>
|
|
<CardDescription>{t("accountInfo.description")}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div className="col-span-1 sm:col-span-2 space-y-1">
|
|
<div className="text-sm font-medium text-muted-foreground">{t("accountInfo.email")}</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Mail className="h-3 w-3 text-muted-foreground" />
|
|
{userProfile.email}
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="text-sm font-medium text-muted-foreground">{t("accountInfo.role")}</div>
|
|
<Badge variant="secondary" className="capitalize">
|
|
{userProfile.role}
|
|
</Badge>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="text-sm font-medium text-muted-foreground">{t("accountInfo.memberSince")}</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Calendar className="h-3 w-3 text-muted-foreground" />
|
|
{formatDate(userProfile.createdAt)}
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="text-sm font-medium text-muted-foreground">{t("accountInfo.onboardedAt")}</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Clock className="h-3 w-3 text-muted-foreground" />
|
|
{userProfile.onboardedAt ? formatDate(userProfile.onboardedAt) : "-"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{isStudent ? (
|
|
<SettingsSectionErrorBoundary>
|
|
<Suspense fallback={<ProfileStudentOverviewSkeleton />}>
|
|
<ProfileStudentOverview userId={userId} />
|
|
</Suspense>
|
|
</SettingsSectionErrorBoundary>
|
|
) : null}
|
|
|
|
{isTeacher ? (
|
|
<SettingsSectionErrorBoundary>
|
|
<Suspense fallback={<ProfileTeacherOverviewSkeleton />}>
|
|
<ProfileTeacherOverview />
|
|
</Suspense>
|
|
</SettingsSectionErrorBoundary>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|