上传完整的WebUI前端仓库

This commit is contained in:
墨梓柒
2026-01-13 06:24:35 +08:00
parent a9187dc312
commit 812296590e
184 changed files with 47854 additions and 1 deletions

View File

@@ -0,0 +1,949 @@
import { Users, Search, Edit, Trash2, Eye, User, MessageSquare, Hash, Clock, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'
import { useState, useEffect, useMemo } from 'react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area'
import { useToast } from '@/hooks/use-toast'
import { Checkbox } from '@/components/ui/checkbox'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import type { PersonInfo, PersonUpdateRequest } from '@/types/person'
import { getPersonList, getPersonDetail, updatePerson, deletePerson, getPersonStats, batchDeletePersons } from '@/lib/person-api'
export function PersonManagementPage() {
const [persons, setPersons] = useState<PersonInfo[]>([])
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [search, setSearch] = useState('')
const [filterKnown, setFilterKnown] = useState<boolean | undefined>(undefined)
const [filterPlatform, setFilterPlatform] = useState<string | undefined>(undefined)
const [selectedPerson, setSelectedPerson] = useState<PersonInfo | null>(null)
const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [deleteConfirmPerson, setDeleteConfirmPerson] = useState<PersonInfo | null>(null)
const [stats, setStats] = useState({ total: 0, known: 0, unknown: 0, platforms: {} as Record<string, number> })
const [selectedPersons, setSelectedPersons] = useState<Set<string>>(new Set())
const [batchDeleteDialogOpen, setBatchDeleteDialogOpen] = useState(false)
const [jumpToPage, setJumpToPage] = useState('')
const { toast } = useToast()
// 加载人物列表
const loadPersons = async () => {
try {
setLoading(true)
const response = await getPersonList({
page,
page_size: pageSize,
search: search || undefined,
is_known: filterKnown,
platform: filterPlatform,
})
setPersons(response.data)
setTotal(response.total)
} catch (error) {
toast({
title: '加载失败',
description: error instanceof Error ? error.message : '无法加载人物信息',
variant: 'destructive',
})
} finally {
setLoading(false)
}
}
// 加载统计数据
const loadStats = async () => {
try {
const response = await getPersonStats()
if (response?.data) {
setStats(response.data)
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
// 初始加载
useEffect(() => {
loadPersons()
loadStats()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, pageSize, search, filterKnown, filterPlatform])
// 查看详情
const handleViewDetail = async (person: PersonInfo) => {
try {
const response = await getPersonDetail(person.person_id)
setSelectedPerson(response.data)
setIsDetailDialogOpen(true)
} catch (error) {
toast({
title: '加载详情失败',
description: error instanceof Error ? error.message : '无法加载人物详情',
variant: 'destructive',
})
}
}
// 编辑人物
const handleEdit = (person: PersonInfo) => {
setSelectedPerson(person)
setIsEditDialogOpen(true)
}
// 删除人物
const handleDelete = async (person: PersonInfo) => {
try {
await deletePerson(person.person_id)
toast({
title: '删除成功',
description: `已删除人物信息: ${person.person_name || person.nickname || person.user_id}`,
})
setDeleteConfirmPerson(null)
loadPersons()
loadStats()
} catch (error) {
toast({
title: '删除失败',
description: error instanceof Error ? error.message : '无法删除人物信息',
variant: 'destructive',
})
}
}
// 获取平台列表
const platforms = useMemo(() => {
return Object.keys(stats.platforms)
}, [stats.platforms])
// 切换单个人物选择
const togglePersonSelection = (personId: string) => {
const newSelected = new Set(selectedPersons)
if (newSelected.has(personId)) {
newSelected.delete(personId)
} else {
newSelected.add(personId)
}
setSelectedPersons(newSelected)
}
// 全选/取消全选
const toggleSelectAll = () => {
if (selectedPersons.size === persons.length && persons.length > 0) {
setSelectedPersons(new Set())
} else {
setSelectedPersons(new Set(persons.map(p => p.person_id)))
}
}
// 打开批量删除对话框
const openBatchDeleteDialog = () => {
if (selectedPersons.size === 0) {
toast({
title: '未选择任何人物',
description: '请先选择要删除的人物',
variant: 'destructive',
})
return
}
setBatchDeleteDialogOpen(true)
}
// 批量删除确认
const handleBatchDelete = async () => {
try {
const result = await batchDeletePersons(Array.from(selectedPersons))
toast({
title: '批量删除完成',
description: result.message,
})
setSelectedPersons(new Set())
setBatchDeleteDialogOpen(false)
loadPersons()
loadStats()
} catch (error) {
toast({
title: '批量删除失败',
description: error instanceof Error ? error.message : '批量删除失败',
variant: 'destructive',
})
}
}
// 页面跳转
const handleJumpToPage = () => {
const targetPage = parseInt(jumpToPage)
const totalPages = Math.ceil(total / pageSize)
if (targetPage >= 1 && targetPage <= totalPages) {
setPage(targetPage)
setJumpToPage('')
} else {
toast({
title: '无效的页码',
description: `请输入1-${totalPages}之间的页码`,
variant: 'destructive',
})
}
}
// 格式化时间
const formatTime = (timestamp: number | null) => {
if (!timestamp) return '-'
return new Date(timestamp * 1000).toLocaleString('zh-CN')
}
return (
<div className="h-[calc(100vh-4rem)] flex flex-col p-4 sm:p-6">
{/* 页面标题 */}
<div className="mb-4 sm:mb-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold flex items-center gap-2">
<Users className="h-8 w-8" strokeWidth={2} />
</h1>
<p className="text-muted-foreground mt-1 text-sm sm:text-base">
</p>
</div>
</div>
</div>
<ScrollArea className="flex-1">
<div className="space-y-4 sm:space-y-6 pr-4">
{/* 统计卡片 */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="rounded-lg border bg-card p-4">
<div className="text-sm text-muted-foreground"></div>
<div className="text-2xl font-bold mt-1">{stats.total}</div>
</div>
<div className="rounded-lg border bg-card p-4">
<div className="text-sm text-muted-foreground"></div>
<div className="text-2xl font-bold mt-1 text-green-600">{stats.known}</div>
</div>
<div className="rounded-lg border bg-card p-4">
<div className="text-sm text-muted-foreground"></div>
<div className="text-2xl font-bold mt-1 text-muted-foreground">{stats.unknown}</div>
</div>
</div>
{/* 搜索和过滤 */}
<div className="rounded-lg border bg-card p-4">
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4">
<div className="sm:col-span-2">
<Label htmlFor="search"></Label>
<div className="relative mt-1.5">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search"
placeholder="搜索名称、昵称或用户ID..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9"
/>
</div>
</div>
<div>
<Label htmlFor="filter-known"></Label>
<Select
value={filterKnown === undefined ? 'all' : filterKnown.toString()}
onValueChange={(value) => {
setFilterKnown(value === 'all' ? undefined : value === 'true')
setPage(1)
}}
>
<SelectTrigger id="filter-known" className="mt-1.5">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="true"></SelectItem>
<SelectItem value="false"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="filter-platform"></Label>
<Select
value={filterPlatform || 'all'}
onValueChange={(value) => {
setFilterPlatform(value === 'all' ? undefined : value)
setPage(1)
}}
>
<SelectTrigger id="filter-platform" className="mt-1.5">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{platforms.map((platform) => (
<SelectItem key={platform} value={platform}>
{platform} ({stats.platforms[platform]})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 批量操作工具栏 */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mt-4 pt-4 border-t">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{selectedPersons.size > 0 && (
<span> {selectedPersons.size} </span>
)}
</div>
<div className="flex items-center gap-2">
<Label htmlFor="page-size" className="text-sm whitespace-nowrap"></Label>
<Select
value={pageSize.toString()}
onValueChange={(value) => {
setPageSize(parseInt(value))
setPage(1)
setSelectedPersons(new Set())
}}
>
<SelectTrigger id="page-size" className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
{selectedPersons.size > 0 && (
<>
<Button
variant="outline"
size="sm"
onClick={() => setSelectedPersons(new Set())}
>
</Button>
<Button
variant="destructive"
size="sm"
onClick={openBatchDeleteDialog}
>
<Trash2 className="h-4 w-4 mr-1" />
</Button>
</>
)}
</div>
</div>
</div>
{/* 人物列表 */}
<div className="rounded-lg border bg-card">
{/* 桌面端表格视图 */}
<div className="hidden md:block">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={persons.length > 0 && selectedPersons.size === persons.length}
onCheckedChange={toggleSelectAll}
aria-label="全选"
/>
</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
...
</TableCell>
</TableRow>
) : persons.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
</TableCell>
</TableRow>
) : (
persons.map((person) => (
<TableRow key={person.id}>
<TableCell>
<Checkbox
checked={selectedPersons.has(person.person_id)}
onCheckedChange={() => togglePersonSelection(person.person_id)}
aria-label={`选择 ${person.person_name || person.nickname || person.user_id}`}
/>
</TableCell>
<TableCell>
<div className={cn(
'inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium',
person.is_known
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400'
)}>
{person.is_known ? '已认识' : '未认识'}
</div>
</TableCell>
<TableCell className="font-medium">
{person.person_name || <span className="text-muted-foreground">-</span>}
</TableCell>
<TableCell>{person.nickname || '-'}</TableCell>
<TableCell>{person.platform}</TableCell>
<TableCell className="font-mono text-sm">{person.user_id}</TableCell>
<TableCell className="text-sm text-muted-foreground">
{formatTime(person.last_know)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="default"
size="sm"
onClick={() => handleViewDetail(person)}
>
<Eye className="h-4 w-4 mr-1" />
</Button>
<Button
variant="default"
size="sm"
onClick={() => handleEdit(person)}
>
<Edit className="h-4 w-4 mr-1" />
</Button>
<Button
size="sm"
onClick={() => setDeleteConfirmPerson(person)}
className="bg-red-600 hover:bg-red-700 text-white"
>
<Trash2 className="h-4 w-4 mr-1" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* 移动端卡片视图 */}
<div className="md:hidden space-y-3 p-4">
{loading ? (
<div className="text-center py-8 text-muted-foreground">
...
</div>
) : persons.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
</div>
) : (
persons.map((person) => (
<div key={person.id} className="rounded-lg border bg-card p-4 space-y-3 overflow-hidden">
{/* 复选框和状态 */}
<div className="flex items-start gap-3">
<Checkbox
checked={selectedPersons.has(person.person_id)}
onCheckedChange={() => togglePersonSelection(person.person_id)}
className="mt-1"
/>
<div className="flex-1 min-w-0">
<div className={cn(
'inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium mb-2',
person.is_known
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400'
)}>
{person.is_known ? '已认识' : '未认识'}
</div>
<h3 className="font-semibold text-sm line-clamp-1 w-full break-all">
{person.person_name || <span className="text-muted-foreground"></span>}
</h3>
{person.nickname && (
<p className="text-xs text-muted-foreground mt-1 line-clamp-1 w-full break-all">
: {person.nickname}
</p>
)}
</div>
</div>
{/* 平台和用户信息 */}
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<div className="text-xs text-muted-foreground mb-1"></div>
<p className="font-medium text-xs">{person.platform}</p>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">ID</div>
<p className="font-mono text-xs truncate" title={person.user_id}>{person.user_id}</p>
</div>
<div className="col-span-2">
<div className="text-xs text-muted-foreground mb-1"></div>
<p className="text-xs">{formatTime(person.last_know)}</p>
</div>
</div>
{/* 操作按钮 */}
<div className="flex flex-wrap gap-1 pt-2 border-t overflow-hidden">
<Button
variant="outline"
size="sm"
onClick={() => handleViewDetail(person)}
className="text-xs px-2 py-1 h-auto flex-shrink-0"
>
<Eye className="h-3 w-3 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleEdit(person)}
className="text-xs px-2 py-1 h-auto flex-shrink-0"
>
<Edit className="h-3 w-3 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setDeleteConfirmPerson(person)}
className="text-xs px-2 py-1 h-auto flex-shrink-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3 mr-1" />
</Button>
</div>
</div>
))
)}
</div>
{/* 分页 - 增强版 */}
{total > 0 && (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 px-4 py-3 border-t">
<div className="text-sm text-muted-foreground">
{total} {page} / {Math.ceil(total / pageSize)}
</div>
<div className="flex items-center gap-2">
{/* 首页 */}
<Button
variant="outline"
size="sm"
onClick={() => setPage(1)}
disabled={page === 1}
className="hidden sm:flex"
>
<ChevronsLeft className="h-4 w-4" />
</Button>
{/* 上一页 */}
<Button
variant="outline"
size="sm"
onClick={() => setPage(page - 1)}
disabled={page === 1}
>
<ChevronLeft className="h-4 w-4 sm:mr-1" />
<span className="hidden sm:inline"></span>
</Button>
{/* 页码跳转 */}
<div className="flex items-center gap-2">
<Input
type="number"
value={jumpToPage}
onChange={(e) => setJumpToPage(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleJumpToPage()}
placeholder={page.toString()}
className="w-16 h-8 text-center"
min={1}
max={Math.ceil(total / pageSize)}
/>
<Button
variant="outline"
size="sm"
onClick={handleJumpToPage}
disabled={!jumpToPage}
className="h-8"
>
</Button>
</div>
{/* 下一页 */}
<Button
variant="outline"
size="sm"
onClick={() => setPage(page + 1)}
disabled={page >= Math.ceil(total / pageSize)}
>
<span className="hidden sm:inline"></span>
<ChevronRight className="h-4 w-4 sm:ml-1" />
</Button>
{/* 末页 */}
<Button
variant="outline"
size="sm"
onClick={() => setPage(Math.ceil(total / pageSize))}
disabled={page >= Math.ceil(total / pageSize)}
className="hidden sm:flex"
>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</div>
</div>
</ScrollArea>
{/* 详情对话框 */}
<PersonDetailDialog
person={selectedPerson}
open={isDetailDialogOpen}
onOpenChange={setIsDetailDialogOpen}
/>
{/* 编辑对话框 */}
<PersonEditDialog
person={selectedPerson}
open={isEditDialogOpen}
onOpenChange={setIsEditDialogOpen}
onSuccess={() => {
loadPersons()
loadStats()
setIsEditDialogOpen(false)
}}
/>
{/* 删除确认对话框 */}
<AlertDialog
open={!!deleteConfirmPerson}
onOpenChange={() => setDeleteConfirmPerson(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"{deleteConfirmPerson?.person_name || deleteConfirmPerson?.nickname || deleteConfirmPerson?.user_id}"
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteConfirmPerson && handleDelete(deleteConfirmPerson)}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 批量删除确认对话框 */}
<AlertDialog open={batchDeleteDialogOpen} onOpenChange={setBatchDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{selectedPersons.size}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={handleBatchDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}
// 人物详情对话框
function PersonDetailDialog({
person,
open,
onOpenChange,
}: {
person: PersonInfo | null
open: boolean
onOpenChange: (open: boolean) => void
}) {
if (!person) return null
const formatTime = (timestamp: number | null) => {
if (!timestamp) return '-'
return new Date(timestamp * 1000).toLocaleString('zh-CN')
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
{person.person_name || person.nickname || person.user_id}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 基本信息 */}
<div className="grid grid-cols-2 gap-4">
<InfoItem icon={User} label="人物名称" value={person.person_name} />
<InfoItem icon={MessageSquare} label="昵称" value={person.nickname} />
<InfoItem icon={Hash} label="用户ID" value={person.user_id} mono />
<InfoItem icon={Hash} label="人物ID" value={person.person_id} mono />
<InfoItem label="平台" value={person.platform} />
<InfoItem label="状态" value={person.is_known ? '已认识' : '未认识'} />
</div>
{/* 名称原因 */}
{person.name_reason && (
<div className="rounded-lg border bg-muted/50 p-3">
<Label className="text-xs text-muted-foreground"></Label>
<p className="mt-1 text-sm">{person.name_reason}</p>
</div>
)}
{/* 记忆点 */}
{person.memory_points && (
<div className="rounded-lg border bg-muted/50 p-3">
<Label className="text-xs text-muted-foreground"></Label>
<p className="mt-1 text-sm whitespace-pre-wrap">{person.memory_points}</p>
</div>
)}
{/* 群昵称列表 */}
{person.group_nick_name && person.group_nick_name.length > 0 && (
<div className="rounded-lg border bg-muted/50 p-3">
<Label className="text-xs text-muted-foreground"></Label>
<div className="mt-2 space-y-1">
{person.group_nick_name.map((item, index) => (
<div key={index} className="text-sm flex items-center gap-2">
<span className="font-mono text-xs text-muted-foreground">{item.group_id}</span>
<span></span>
<span>{item.group_nick_name}</span>
</div>
))}
</div>
</div>
)}
{/* 时间信息 */}
<div className="grid grid-cols-3 gap-4">
<InfoItem icon={Clock} label="认识时间" value={formatTime(person.know_times)} />
<InfoItem icon={Clock} label="首次记录" value={formatTime(person.know_since)} />
<InfoItem icon={Clock} label="最后更新" value={formatTime(person.last_know)} />
</div>
</div>
<DialogFooter>
<Button onClick={() => onOpenChange(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
// 信息项组件
function InfoItem({
icon: Icon,
label,
value,
mono = false,
}: {
icon?: typeof User
label: string
value: string | null | undefined
mono?: boolean
}) {
return (
<div className="space-y-1">
<Label className="text-xs text-muted-foreground flex items-center gap-1">
{Icon && <Icon className="h-3 w-3" />}
{label}
</Label>
<div className={cn('text-sm', mono && 'font-mono', !value && 'text-muted-foreground')}>
{value || '-'}
</div>
</div>
)
}
// 人物编辑对话框
function PersonEditDialog({
person,
open,
onOpenChange,
onSuccess,
}: {
person: PersonInfo | null
open: boolean
onOpenChange: (open: boolean) => void
onSuccess: () => void
}) {
const [formData, setFormData] = useState<PersonUpdateRequest>({})
const [saving, setSaving] = useState(false)
const { toast } = useToast()
useEffect(() => {
if (person) {
setFormData({
person_name: person.person_name || '',
name_reason: person.name_reason || '',
nickname: person.nickname || '',
is_known: person.is_known,
})
}
}, [person])
const handleSave = async () => {
if (!person) return
try {
setSaving(true)
await updatePerson(person.person_id, formData)
toast({
title: '保存成功',
description: '人物信息已更新',
})
onSuccess()
} catch (error) {
toast({
title: '保存失败',
description: error instanceof Error ? error.message : '无法更新人物信息',
variant: 'destructive',
})
} finally {
setSaving(false)
}
}
if (!person) return null
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
{person.person_name || person.nickname || person.user_id}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="person_name"></Label>
<Input
id="person_name"
value={formData.person_name || ''}
onChange={(e) => setFormData({ ...formData, person_name: e.target.value })}
placeholder="为这个人设置一个名称"
/>
</div>
<div className="space-y-2">
<Label htmlFor="nickname"></Label>
<Input
id="nickname"
value={formData.nickname || ''}
onChange={(e) => setFormData({ ...formData, nickname: e.target.value })}
placeholder="昵称"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="name_reason"></Label>
<Textarea
id="name_reason"
value={formData.name_reason || ''}
onChange={(e) => setFormData({ ...formData, name_reason: e.target.value })}
placeholder="为什么这样称呼这个人?"
rows={2}
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-3">
<div>
<Label htmlFor="is_known" className="text-base font-medium">
</Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
id="is_known"
checked={formData.is_known}
onCheckedChange={(checked) => setFormData({ ...formData, is_known: checked })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSave} disabled={saving}>
{saving ? '保存中...' : '保存'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}