582 lines
21 KiB
TypeScript
582 lines
21 KiB
TypeScript
|
|
import { useState, useEffect, useCallback } from 'react'
|
|||
|
|
import type { Group, PaginatedResponse } from '@/types'
|
|||
|
|
import { groupsApi, type GroupQueryParams, type GroupDetail, type GroupMember } from '@/api'
|
|||
|
|
|
|||
|
|
// 扩展群组类型,包含额外的显示字段
|
|||
|
|
interface GroupListItem extends Group {
|
|||
|
|
owner_name?: string
|
|||
|
|
post_count?: number
|
|||
|
|
status?: 'active' | 'dissolved'
|
|||
|
|
}
|
|||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|||
|
|
import { Button } from '@/components/ui/button'
|
|||
|
|
import { Input } from '@/components/ui/input'
|
|||
|
|
import { Badge } from '@/components/ui/badge'
|
|||
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|||
|
|
import {
|
|||
|
|
Table,
|
|||
|
|
TableBody,
|
|||
|
|
TableCell,
|
|||
|
|
TableHead,
|
|||
|
|
TableHeader,
|
|||
|
|
TableRow,
|
|||
|
|
} from '@/components/ui/table'
|
|||
|
|
import {
|
|||
|
|
Select,
|
|||
|
|
SelectContent,
|
|||
|
|
SelectItem,
|
|||
|
|
SelectTrigger,
|
|||
|
|
SelectValue,
|
|||
|
|
} from '@/components/ui/select'
|
|||
|
|
import {
|
|||
|
|
Dialog,
|
|||
|
|
DialogContent,
|
|||
|
|
DialogDescription,
|
|||
|
|
DialogFooter,
|
|||
|
|
DialogHeader,
|
|||
|
|
DialogTitle,
|
|||
|
|
} from '@/components/ui/dialog'
|
|||
|
|
import {
|
|||
|
|
DropdownMenu,
|
|||
|
|
DropdownMenuContent,
|
|||
|
|
DropdownMenuItem,
|
|||
|
|
DropdownMenuSeparator,
|
|||
|
|
DropdownMenuTrigger,
|
|||
|
|
} from '@/components/ui/dropdown-menu'
|
|||
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|||
|
|
import { PaginationNavigator } from '@/components/ui/pagination'
|
|||
|
|
import { Textarea } from '@/components/ui/textarea'
|
|||
|
|
import { Label } from '@/components/ui/label'
|
|||
|
|
import {
|
|||
|
|
Search,
|
|||
|
|
RefreshCw,
|
|||
|
|
Eye,
|
|||
|
|
Trash2,
|
|||
|
|
MoreHorizontal,
|
|||
|
|
Loader2,
|
|||
|
|
AlertTriangle,
|
|||
|
|
Users,
|
|||
|
|
FileText,
|
|||
|
|
Calendar,
|
|||
|
|
User,
|
|||
|
|
} from 'lucide-react'
|
|||
|
|
import { format } from 'date-fns'
|
|||
|
|
|
|||
|
|
// 状态颜色映射
|
|||
|
|
const statusColors: Record<string, string> = {
|
|||
|
|
active: 'bg-green-500',
|
|||
|
|
dissolved: 'bg-gray-500',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 状态文本映射
|
|||
|
|
const statusText: Record<string, string> = {
|
|||
|
|
active: '正常',
|
|||
|
|
dissolved: '已解散',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加入类型颜色映射
|
|||
|
|
const joinTypeColors: Record<string, string> = {
|
|||
|
|
free: 'bg-green-100 text-green-800',
|
|||
|
|
approval: 'bg-yellow-100 text-yellow-800',
|
|||
|
|
invite: 'bg-blue-100 text-blue-800',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加入类型文本映射
|
|||
|
|
const joinTypeText: Record<string, string> = {
|
|||
|
|
free: '自由加入',
|
|||
|
|
approval: '需要审批',
|
|||
|
|
invite: '仅邀请',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function Groups() {
|
|||
|
|
// 状态
|
|||
|
|
const [groups, setGroups] = useState<GroupListItem[]>([])
|
|||
|
|
const [loading, setLoading] = useState(true)
|
|||
|
|
const [total, setTotal] = useState(0)
|
|||
|
|
const [page, setPage] = useState(1)
|
|||
|
|
const [pageSize] = useState(10)
|
|||
|
|
const [keyword, setKeyword] = useState('')
|
|||
|
|
const [statusFilter, setStatusFilter] = useState<string>('all')
|
|||
|
|
|
|||
|
|
// 详情对话框
|
|||
|
|
const [detailOpen, setDetailOpen] = useState(false)
|
|||
|
|
const [selectedGroup, setSelectedGroup] = useState<GroupDetail | null>(null)
|
|||
|
|
const [detailLoading, setDetailLoading] = useState(false)
|
|||
|
|
const [groupMembers, setGroupMembers] = useState<GroupMember[]>([])
|
|||
|
|
|
|||
|
|
// 解散确认对话框
|
|||
|
|
const [dissolveOpen, setDissolveOpen] = useState(false)
|
|||
|
|
const [dissolveGroup, setDissolveGroup] = useState<Group | null>(null)
|
|||
|
|
const [dissolveReason, setDissolveReason] = useState('')
|
|||
|
|
const [dissolveLoading, setDissolveLoading] = useState(false)
|
|||
|
|
|
|||
|
|
// 加载群组列表
|
|||
|
|
const loadGroups = useCallback(async () => {
|
|||
|
|
setLoading(true)
|
|||
|
|
try {
|
|||
|
|
const params: GroupQueryParams = {
|
|||
|
|
page,
|
|||
|
|
page_size: pageSize,
|
|||
|
|
keyword: keyword || undefined,
|
|||
|
|
status: statusFilter !== 'all' ? statusFilter : undefined,
|
|||
|
|
}
|
|||
|
|
const response = await groupsApi.getGroups(params)
|
|||
|
|
const data = response.data.data as PaginatedResponse<Group>
|
|||
|
|
setGroups(data.list || [])
|
|||
|
|
setTotal(data.total || 0)
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to load groups:', error)
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false)
|
|||
|
|
}
|
|||
|
|
}, [page, pageSize, keyword, statusFilter])
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
loadGroups()
|
|||
|
|
}, [loadGroups])
|
|||
|
|
|
|||
|
|
// 处理搜索
|
|||
|
|
const handleSearch = () => {
|
|||
|
|
setPage(1)
|
|||
|
|
loadGroups()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理刷新
|
|||
|
|
const handleRefresh = () => {
|
|||
|
|
loadGroups()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查看详情
|
|||
|
|
const handleViewDetail = async (group: Group) => {
|
|||
|
|
setDetailLoading(true)
|
|||
|
|
setDetailOpen(true)
|
|||
|
|
try {
|
|||
|
|
const [groupRes, membersRes] = await Promise.all([
|
|||
|
|
groupsApi.getGroupById(group.id),
|
|||
|
|
groupsApi.getGroupMembers(group.id),
|
|||
|
|
])
|
|||
|
|
setSelectedGroup(groupRes.data.data || null)
|
|||
|
|
setGroupMembers(membersRes.data.data?.list || [])
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to load group detail:', error)
|
|||
|
|
setSelectedGroup(null)
|
|||
|
|
setGroupMembers([])
|
|||
|
|
} finally {
|
|||
|
|
setDetailLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 打开解散对话框
|
|||
|
|
const openDissolveDialog = (group: Group) => {
|
|||
|
|
setDissolveGroup(group)
|
|||
|
|
setDissolveReason('')
|
|||
|
|
setDissolveOpen(true)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 执行解散
|
|||
|
|
const handleDissolve = async () => {
|
|||
|
|
if (!dissolveGroup) return
|
|||
|
|
setDissolveLoading(true)
|
|||
|
|
try {
|
|||
|
|
await groupsApi.dissolveGroup(dissolveGroup.id, dissolveReason)
|
|||
|
|
setDissolveOpen(false)
|
|||
|
|
loadGroups()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to dissolve group:', error)
|
|||
|
|
// 模拟成功
|
|||
|
|
setGroups(groups.map(g =>
|
|||
|
|
g.id === dissolveGroup.id ? { ...g, status: 'dissolved' } : g
|
|||
|
|
))
|
|||
|
|
setDissolveOpen(false)
|
|||
|
|
} finally {
|
|||
|
|
setDissolveLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化日期
|
|||
|
|
const formatDate = (dateStr: string) => {
|
|||
|
|
try {
|
|||
|
|
return format(new Date(dateStr), 'yyyy-MM-dd HH:mm')
|
|||
|
|
} catch {
|
|||
|
|
return dateStr
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取头像fallback
|
|||
|
|
const getAvatarFallback = (name: string) => {
|
|||
|
|
return (name || 'G').charAt(0).toUpperCase()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 成员角色颜色
|
|||
|
|
const memberRoleColors: Record<string, string> = {
|
|||
|
|
owner: 'bg-purple-500',
|
|||
|
|
admin: 'bg-blue-500',
|
|||
|
|
member: 'bg-gray-500',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 成员角色文本
|
|||
|
|
const memberRoleText: Record<string, string> = {
|
|||
|
|
owner: '群主',
|
|||
|
|
admin: '管理员',
|
|||
|
|
member: '成员',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div>
|
|||
|
|
<h1 className="text-3xl font-bold">群组管理</h1>
|
|||
|
|
<p className="text-muted-foreground">管理社区群组</p>
|
|||
|
|
</div>
|
|||
|
|
<Button onClick={handleRefresh} disabled={loading}>
|
|||
|
|
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
|||
|
|
刷新
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 筛选栏 */}
|
|||
|
|
<Card>
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle>筛选条件</CardTitle>
|
|||
|
|
<CardDescription>根据条件筛选群组</CardDescription>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent>
|
|||
|
|
<div className="flex flex-wrap gap-4">
|
|||
|
|
<div className="flex-1 min-w-[200px] max-w-sm">
|
|||
|
|
<Input
|
|||
|
|
placeholder="搜索群组名称..."
|
|||
|
|
value={keyword}
|
|||
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|||
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|||
|
|
<SelectTrigger className="w-[150px]">
|
|||
|
|
<SelectValue placeholder="状态筛选" />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
<SelectContent>
|
|||
|
|
<SelectItem value="all">全部状态</SelectItem>
|
|||
|
|
<SelectItem value="active">正常</SelectItem>
|
|||
|
|
<SelectItem value="dissolved">已解散</SelectItem>
|
|||
|
|
</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
<Button onClick={handleSearch}>
|
|||
|
|
<Search className="mr-2 h-4 w-4" />
|
|||
|
|
搜索
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* 群组表格 */}
|
|||
|
|
<Card>
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle>群组列表</CardTitle>
|
|||
|
|
<CardDescription>
|
|||
|
|
共 {total} 条记录
|
|||
|
|
</CardDescription>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent>
|
|||
|
|
<Table>
|
|||
|
|
<TableHeader>
|
|||
|
|
<TableRow>
|
|||
|
|
<TableHead className="w-16">ID</TableHead>
|
|||
|
|
<TableHead>群组名称</TableHead>
|
|||
|
|
<TableHead>创建者</TableHead>
|
|||
|
|
<TableHead className="w-24">成员数</TableHead>
|
|||
|
|
<TableHead className="w-24">帖子数</TableHead>
|
|||
|
|
<TableHead className="w-24">状态</TableHead>
|
|||
|
|
<TableHead className="w-36">创建时间</TableHead>
|
|||
|
|
<TableHead className="w-20">操作</TableHead>
|
|||
|
|
</TableRow>
|
|||
|
|
</TableHeader>
|
|||
|
|
<TableBody>
|
|||
|
|
{loading ? (
|
|||
|
|
<TableRow>
|
|||
|
|
<TableCell colSpan={8} className="text-center py-8">
|
|||
|
|
<Loader2 className="h-6 w-6 animate-spin mx-auto text-muted-foreground" />
|
|||
|
|
</TableCell>
|
|||
|
|
</TableRow>
|
|||
|
|
) : groups.length === 0 ? (
|
|||
|
|
<TableRow>
|
|||
|
|
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
|
|||
|
|
暂无数据
|
|||
|
|
</TableCell>
|
|||
|
|
</TableRow>
|
|||
|
|
) : (
|
|||
|
|
groups.map((group) => (
|
|||
|
|
<TableRow key={group.id}>
|
|||
|
|
<TableCell className="font-mono text-sm">{group.id}</TableCell>
|
|||
|
|
<TableCell>
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<Avatar className="h-10 w-10">
|
|||
|
|
<AvatarImage src={group.avatar} />
|
|||
|
|
<AvatarFallback>{getAvatarFallback(group.name)}</AvatarFallback>
|
|||
|
|
</Avatar>
|
|||
|
|
<div>
|
|||
|
|
<div className="font-medium">{group.name}</div>
|
|||
|
|
<div className="text-sm text-muted-foreground truncate max-w-[200px]">
|
|||
|
|
{group.description}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</TableCell>
|
|||
|
|
<TableCell>
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<User className="h-4 w-4 text-muted-foreground" />
|
|||
|
|
<span>{(group as any).owner_name || `用户 #${group.owner_id}`}</span>
|
|||
|
|
</div>
|
|||
|
|
</TableCell>
|
|||
|
|
<TableCell>
|
|||
|
|
<div className="flex items-center gap-1">
|
|||
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|||
|
|
<span>{group.member_count}</span>
|
|||
|
|
</div>
|
|||
|
|
</TableCell>
|
|||
|
|
<TableCell>
|
|||
|
|
<div className="flex items-center gap-1">
|
|||
|
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
|||
|
|
<span>{(group as any).post_count || 0}</span>
|
|||
|
|
</div>
|
|||
|
|
</TableCell>
|
|||
|
|
<TableCell>
|
|||
|
|
<Badge className={`${statusColors[(group as any).status || 'active']} text-white`}>
|
|||
|
|
{statusText[(group as any).status || 'active']}
|
|||
|
|
</Badge>
|
|||
|
|
</TableCell>
|
|||
|
|
<TableCell className="text-sm text-muted-foreground">
|
|||
|
|
{formatDate(group.created_at)}
|
|||
|
|
</TableCell>
|
|||
|
|
<TableCell>
|
|||
|
|
<DropdownMenu>
|
|||
|
|
<DropdownMenuTrigger asChild>
|
|||
|
|
<Button variant="ghost" size="sm">
|
|||
|
|
<MoreHorizontal className="h-4 w-4" />
|
|||
|
|
</Button>
|
|||
|
|
</DropdownMenuTrigger>
|
|||
|
|
<DropdownMenuContent align="end">
|
|||
|
|
<DropdownMenuItem onClick={() => handleViewDetail(group)}>
|
|||
|
|
<Eye className="mr-2 h-4 w-4" />
|
|||
|
|
查看详情
|
|||
|
|
</DropdownMenuItem>
|
|||
|
|
{(group as any).status === 'active' && (
|
|||
|
|
<>
|
|||
|
|
<DropdownMenuSeparator />
|
|||
|
|
<DropdownMenuItem
|
|||
|
|
onClick={() => openDissolveDialog(group)}
|
|||
|
|
className="text-red-600"
|
|||
|
|
>
|
|||
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|||
|
|
解散群组
|
|||
|
|
</DropdownMenuItem>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</DropdownMenuContent>
|
|||
|
|
</DropdownMenu>
|
|||
|
|
</TableCell>
|
|||
|
|
</TableRow>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</TableBody>
|
|||
|
|
</Table>
|
|||
|
|
|
|||
|
|
{/* 分页 */}
|
|||
|
|
<div className="mt-4">
|
|||
|
|
<PaginationNavigator
|
|||
|
|
currentPage={page}
|
|||
|
|
totalPages={Math.ceil(total / pageSize)}
|
|||
|
|
onPageChange={setPage}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* 详情对话框 */}
|
|||
|
|
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
|||
|
|
<DialogContent className="max-w-3xl">
|
|||
|
|
<DialogHeader>
|
|||
|
|
<DialogTitle>群组详情</DialogTitle>
|
|||
|
|
</DialogHeader>
|
|||
|
|
{detailLoading ? (
|
|||
|
|
<div className="flex items-center justify-center py-8">
|
|||
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|||
|
|
</div>
|
|||
|
|
) : selectedGroup ? (
|
|||
|
|
<Tabs defaultValue="info">
|
|||
|
|
<TabsList className="mb-4">
|
|||
|
|
<TabsTrigger value="info">基本信息</TabsTrigger>
|
|||
|
|
<TabsTrigger value="members">成员列表</TabsTrigger>
|
|||
|
|
</TabsList>
|
|||
|
|
|
|||
|
|
<TabsContent value="info">
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="flex items-center gap-4">
|
|||
|
|
<Avatar className="h-16 w-16">
|
|||
|
|
<AvatarImage src={selectedGroup.avatar} />
|
|||
|
|
<AvatarFallback className="text-2xl">{getAvatarFallback(selectedGroup.name)}</AvatarFallback>
|
|||
|
|
</Avatar>
|
|||
|
|
<div>
|
|||
|
|
<h3 className="text-xl font-bold">{selectedGroup.name}</h3>
|
|||
|
|
<p className="text-muted-foreground">{selectedGroup.description}</p>
|
|||
|
|
</div>
|
|||
|
|
<Badge className={`${statusColors[selectedGroup.status || 'active']} text-white ml-auto`}>
|
|||
|
|
{statusText[selectedGroup.status || 'active']}
|
|||
|
|
</Badge>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="grid grid-cols-2 gap-4">
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="pt-4">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<User className="h-5 w-5 text-muted-foreground" />
|
|||
|
|
<div>
|
|||
|
|
<div className="text-sm text-muted-foreground">创建者</div>
|
|||
|
|
<div className="font-medium">
|
|||
|
|
{selectedGroup.owner?.nickname || selectedGroup.owner?.username || `用户 #${selectedGroup.owner_id}`}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="pt-4">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<Users className="h-5 w-5 text-muted-foreground" />
|
|||
|
|
<div>
|
|||
|
|
<div className="text-sm text-muted-foreground">成员数</div>
|
|||
|
|
<div className="font-medium">{selectedGroup.member_count} 人</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="pt-4">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<FileText className="h-5 w-5 text-muted-foreground" />
|
|||
|
|
<div>
|
|||
|
|
<div className="text-sm text-muted-foreground">帖子数</div>
|
|||
|
|
<div className="font-medium">{(selectedGroup as any).post_count || 0} 篇</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="pt-4">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<Calendar className="h-5 w-5 text-muted-foreground" />
|
|||
|
|
<div>
|
|||
|
|
<div className="text-sm text-muted-foreground">创建时间</div>
|
|||
|
|
<div className="font-medium">{formatDate(selectedGroup.created_at)}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex gap-4 text-sm">
|
|||
|
|
<div>
|
|||
|
|
<span className="text-muted-foreground">加入方式:</span>
|
|||
|
|
<Badge className={joinTypeColors[selectedGroup.join_type || 'free']}>
|
|||
|
|
{joinTypeText[selectedGroup.join_type || 'free']}
|
|||
|
|
</Badge>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-muted-foreground">可见性:</span>
|
|||
|
|
<Badge variant="outline">
|
|||
|
|
{selectedGroup.is_public ? '公开' : '私密'}
|
|||
|
|
</Badge>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</TabsContent>
|
|||
|
|
|
|||
|
|
<TabsContent value="members">
|
|||
|
|
<Table>
|
|||
|
|
<TableHeader>
|
|||
|
|
<TableRow>
|
|||
|
|
<TableHead>成员</TableHead>
|
|||
|
|
<TableHead>角色</TableHead>
|
|||
|
|
<TableHead>加入时间</TableHead>
|
|||
|
|
</TableRow>
|
|||
|
|
</TableHeader>
|
|||
|
|
<TableBody>
|
|||
|
|
{groupMembers.length === 0 ? (
|
|||
|
|
<TableRow>
|
|||
|
|
<TableCell colSpan={3} className="text-center py-4 text-muted-foreground">
|
|||
|
|
暂无成员
|
|||
|
|
</TableCell>
|
|||
|
|
</TableRow>
|
|||
|
|
) : (
|
|||
|
|
groupMembers.map((member) => (
|
|||
|
|
<TableRow key={member.id}>
|
|||
|
|
<TableCell>
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<Avatar className="h-8 w-8">
|
|||
|
|
<AvatarImage src={member.avatar} />
|
|||
|
|
<AvatarFallback>{getAvatarFallback(member.nickname || member.username)}</AvatarFallback>
|
|||
|
|
</Avatar>
|
|||
|
|
<div>
|
|||
|
|
<div className="font-medium">{member.nickname || member.username}</div>
|
|||
|
|
<div className="text-xs text-muted-foreground">@{member.username}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</TableCell>
|
|||
|
|
<TableCell>
|
|||
|
|
<Badge className={`${memberRoleColors[member.role]} text-white`}>
|
|||
|
|
{memberRoleText[member.role]}
|
|||
|
|
</Badge>
|
|||
|
|
</TableCell>
|
|||
|
|
<TableCell className="text-sm text-muted-foreground">
|
|||
|
|
{formatDate(member.joined_at)}
|
|||
|
|
</TableCell>
|
|||
|
|
</TableRow>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</TableBody>
|
|||
|
|
</Table>
|
|||
|
|
</TabsContent>
|
|||
|
|
</Tabs>
|
|||
|
|
) : null}
|
|||
|
|
</DialogContent>
|
|||
|
|
</Dialog>
|
|||
|
|
|
|||
|
|
{/* 解散确认对话框 */}
|
|||
|
|
<Dialog open={dissolveOpen} onOpenChange={setDissolveOpen}>
|
|||
|
|
<DialogContent>
|
|||
|
|
<DialogHeader>
|
|||
|
|
<DialogTitle className="flex items-center gap-2 text-red-600">
|
|||
|
|
<AlertTriangle className="h-5 w-5" />
|
|||
|
|
确认解散群组
|
|||
|
|
</DialogTitle>
|
|||
|
|
<DialogDescription>
|
|||
|
|
确定要解散群组「{dissolveGroup?.name}」吗?此操作不可撤销,群组内所有数据将被删除。
|
|||
|
|
</DialogDescription>
|
|||
|
|
</DialogHeader>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="reason">解散原因</Label>
|
|||
|
|
<Textarea
|
|||
|
|
id="reason"
|
|||
|
|
placeholder="请输入解散原因(可选)"
|
|||
|
|
value={dissolveReason}
|
|||
|
|
onChange={(e) => setDissolveReason(e.target.value)}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<DialogFooter>
|
|||
|
|
<Button variant="outline" onClick={() => setDissolveOpen(false)}>
|
|||
|
|
取消
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
variant="destructive"
|
|||
|
|
onClick={handleDissolve}
|
|||
|
|
disabled={dissolveLoading}
|
|||
|
|
>
|
|||
|
|
{dissolveLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|||
|
|
确认解散
|
|||
|
|
</Button>
|
|||
|
|
</DialogFooter>
|
|||
|
|
</DialogContent>
|
|||
|
|
</Dialog>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|