0.0.1.0
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useSession } from "next-auth/react"
|
||||
import Link from "next/link"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Users, Calendar, Plus, ArrowRight } from "lucide-react"
|
||||
import { format } from "date-fns"
|
||||
import { zhCN } from "date-fns/locale"
|
||||
|
||||
interface FamilyTree {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
_count: {
|
||||
members: number
|
||||
collaborators: number
|
||||
}
|
||||
}
|
||||
|
||||
export function FamilyTreesList() {
|
||||
const { data: session } = useSession()
|
||||
const [trees, setTrees] = useState<FamilyTree[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user?.id) {
|
||||
fetch('/api/trees')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.trees) {
|
||||
setTrees(data.trees)
|
||||
}
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('获取家族树失败:', err)
|
||||
setLoading(false)
|
||||
})
|
||||
} else {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [session?.user?.id])
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>我的家族树</CardTitle>
|
||||
<CardDescription>请先登录查看您的家族树</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Link href="/auth/signin">
|
||||
<Button>登录</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>我的家族树</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground">加载中...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>我的家族树</CardTitle>
|
||||
<CardDescription>
|
||||
{trees.length > 0 ? `共 ${trees.length} 个家族树` : '还没有家族树'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Link href="/trees/new">
|
||||
<Button size="sm">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
创建家族树
|
||||
</Button>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{trees.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-muted-foreground mb-4">您还没有创建任何家族树</p>
|
||||
<Link href="/trees/new">
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
创建第一个家族树
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{trees.map((tree) => (
|
||||
<div
|
||||
key={tree.id}
|
||||
className="flex items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-lg">{tree.name}</h3>
|
||||
{tree.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{tree.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="h-3 w-3" />
|
||||
{tree._count.members} 个成员
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
创建于 {format(new Date(tree.createdAt), 'yyyy年MM月dd日', { locale: zhCN })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Link href={`/tree?treeId=${tree.id}`}>
|
||||
<Button variant="ghost" size="sm">
|
||||
查看
|
||||
<ArrowRight className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react"
|
||||
|
||||
export function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||
return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Lock, CheckCircle2 } from "lucide-react"
|
||||
|
||||
export function ChangePassword() {
|
||||
const [currentPassword, setCurrentPassword] = useState("")
|
||||
const [newPassword, setNewPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setSuccess(false)
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/user/change-password", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
currentPassword,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
setError(data.error || "修改密码失败")
|
||||
return
|
||||
}
|
||||
|
||||
setSuccess(true)
|
||||
setCurrentPassword("")
|
||||
setNewPassword("")
|
||||
setConfirmPassword("")
|
||||
|
||||
// 3秒后清除成功消息
|
||||
setTimeout(() => setSuccess(false), 3000)
|
||||
} catch (err) {
|
||||
setError("修改密码失败,请稍后重试")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Lock className="h-5 w-5" /> 修改密码
|
||||
</CardTitle>
|
||||
<CardDescription>更改您的登录密码</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert className="bg-green-50 border-green-200">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
<AlertDescription className="text-green-800">
|
||||
密码修改成功!
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="currentPassword">当前密码</Label>
|
||||
<Input
|
||||
id="currentPassword"
|
||||
type="password"
|
||||
placeholder="输入当前密码"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="newPassword">新密码</Label>
|
||||
<Input
|
||||
id="newPassword"
|
||||
type="password"
|
||||
placeholder="至少6个字符"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
minLength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">确认新密码</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
placeholder="再次输入新密码"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={loading} className="w-full">
|
||||
{loading ? "修改中..." : "修改密码"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
+168
-11
@@ -1,19 +1,95 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { BookOpen, Search, Bell, Settings } from "lucide-react"
|
||||
import { BookOpen, Search, Bell, Settings, LogOut, User, ChevronDown, Plus } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
import { useState, useRef, useEffect } from "react"
|
||||
import { useState, useRef, useEffect, useCallback } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useSession, signOut } from "next-auth/react"
|
||||
|
||||
interface FamilyTree {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
}
|
||||
|
||||
export function SiteHeader() {
|
||||
const { searchMembers, searchResults } = useFamily()
|
||||
const { data: session, status } = useSession()
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [showResults, setShowResults] = useState(false)
|
||||
const [familyTrees, setFamilyTrees] = useState<FamilyTree[]>([])
|
||||
const [currentTree, setCurrentTree] = useState<FamilyTree | null>(null)
|
||||
const searchRef = useRef<HTMLDivElement>(null)
|
||||
const router = useRouter()
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await signOut({ callbackUrl: "/auth/signin" })
|
||||
}
|
||||
|
||||
// 获取用户名首字母作为头像
|
||||
const getUserInitial = () => {
|
||||
if (session?.user?.name) {
|
||||
return session.user.name.charAt(0).toUpperCase()
|
||||
}
|
||||
if (session?.user?.email) {
|
||||
return session.user.email.charAt(0).toUpperCase()
|
||||
}
|
||||
return "U"
|
||||
}
|
||||
|
||||
// 获取家族树列表
|
||||
const loadFamilyTrees = useCallback(() => {
|
||||
if (session?.user?.id) {
|
||||
fetch('/api/trees')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.trees) {
|
||||
setFamilyTrees(data.trees)
|
||||
// 如果 URL 中有 treeId,使用该树,否则使用第一个
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const treeId = urlParams.get('treeId')
|
||||
if (treeId) {
|
||||
const tree = data.trees.find((t: FamilyTree) => t.id === treeId)
|
||||
if (tree) {
|
||||
setCurrentTree(tree)
|
||||
} else if (data.trees.length > 0) {
|
||||
setCurrentTree(data.trees[0])
|
||||
}
|
||||
} else if (data.trees.length > 0) {
|
||||
setCurrentTree(data.trees[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('获取家族树失败:', err))
|
||||
}
|
||||
}, [session?.user?.id])
|
||||
|
||||
useEffect(() => {
|
||||
loadFamilyTrees()
|
||||
}, [loadFamilyTrees])
|
||||
|
||||
// 监听路由变化,刷新家族树列表
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
loadFamilyTrees()
|
||||
}
|
||||
|
||||
window.addEventListener('focus', handleRouteChange)
|
||||
return () => window.removeEventListener('focus', handleRouteChange)
|
||||
}, [loadFamilyTrees])
|
||||
|
||||
// 点击外部关闭搜索结果
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
@@ -40,11 +116,52 @@ export function SiteHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto flex h-16 items-center px-4">
|
||||
<div className="mr-8 flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded bg-primary text-primary-foreground">
|
||||
<BookOpen className="h-5 w-5" />
|
||||
</div>
|
||||
<span className="text-xl font-serif font-bold tracking-tight">华夏谱 (HuaXiaPu)</span>
|
||||
<div className="mr-8 flex items-center gap-4">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded bg-primary text-primary-foreground">
|
||||
<BookOpen className="h-5 w-5" />
|
||||
</div>
|
||||
<span className="text-xl font-serif font-bold tracking-tight">华夏谱</span>
|
||||
</Link>
|
||||
|
||||
{/* 家族树选择器 */}
|
||||
{session && familyTrees.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
<span className="max-w-[150px] truncate">{currentTree?.name || '选择家族树'}</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuLabel>我的家族树</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{familyTrees.map((tree) => (
|
||||
<DropdownMenuItem
|
||||
key={tree.id}
|
||||
onClick={() => {
|
||||
setCurrentTree(tree)
|
||||
router.push(`/?treeId=${tree.id}`)
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<span className="flex-1 truncate">{tree.name}</span>
|
||||
{currentTree?.id === tree.id && (
|
||||
<span className="text-primary">✓</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => router.push('/trees/new')}
|
||||
className="cursor-pointer text-primary"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
创建新家族树
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-6 text-sm font-medium text-muted-foreground">
|
||||
@@ -125,10 +242,50 @@ export function SiteHeader() {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Avatar className="h-8 w-8 border border-border">
|
||||
<AvatarImage src="/placeholder-user.jpg" alt="User" />
|
||||
<AvatarFallback>李</AvatarFallback>
|
||||
</Avatar>
|
||||
{status === "loading" ? (
|
||||
<Avatar className="h-8 w-8 border border-border">
|
||||
<AvatarFallback>...</AvatarFallback>
|
||||
</Avatar>
|
||||
) : session ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<Avatar className="h-8 w-8 border border-border">
|
||||
<AvatarImage src={undefined} alt={session.user?.name || "User"} />
|
||||
<AvatarFallback>{getUserInitial()}</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{session.user?.name || "用户"}</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
{session.user?.email}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings" className="cursor-pointer">
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
<span>个人设置</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleSignOut} className="cursor-pointer text-red-600">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span>退出登录</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Link href="/auth/signin">
|
||||
<Button variant="default" size="sm">
|
||||
登录
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user