/** * InvitationsTab 组件 * * 邀请管理Tab组件 * 从主页面拆分出来,负责邀请管理功能 * 包括邀请列表、创建、删除、验证等 * 使用三层架构样式系统,支持深浅色主题 * * @created 2025-01-XX(拆分自 page.tsx) * @component */ 'use client' import { useState, useEffect, useCallback } from 'react' import { useThemeStyles, useTheme, cn } from '@/lib/ui-styles' import { useDialog } from '@/components/ui/DialogProvider' import Modal from '@/components/ui/Modal' import { formatDateTimeLocale } from '../_utils/formatters' import type { Invitation, ToastFn } from './types' interface InvitationsTabProps { showToast: ToastFn } export default function InvitationsTab({ showToast }: InvitationsTabProps) { const styles = useThemeStyles() const theme = useTheme() const { confirm } = useDialog() const [invitations, setInvitations] = useState([]) const [showCreateInvitationModal, setShowCreateInvitationModal] = useState(false) const [newInvitationMaxUses, setNewInvitationMaxUses] = useState(1) const [newInvitationExpiresInDays, setNewInvitationExpiresInDays] = useState(null) // 根据主题选择颜色类名 const borderColor = theme === 'dark' ? 'border-slate-800' : 'border-slate-200' const textTitle = theme === 'dark' ? 'text-white' : 'text-slate-900' const textDesc = theme === 'dark' ? 'text-slate-400' : 'text-slate-500' const textLabel = theme === 'dark' ? 'text-slate-300' : 'text-slate-700' const textMono = theme === 'dark' ? 'text-white' : 'text-slate-900' const textLink = theme === 'dark' ? 'text-slate-400' : 'text-slate-600' const textEmpty = theme === 'dark' ? 'text-slate-500' : 'text-slate-400' const hoverBg = theme === 'dark' ? 'hover:bg-slate-800/50' : 'hover:bg-slate-50' const divideColor = theme === 'dark' ? 'divide-slate-800' : 'divide-slate-200' const badgeSuccess = theme === 'dark' ? 'bg-green-900/30 text-green-300' : 'bg-green-100 text-green-800' const badgeDanger = theme === 'dark' ? 'bg-red-900/30 text-red-300' : 'bg-red-100 text-red-800' const fetchInvitations = useCallback(async () => { try { const response = await fetch('/api/invitations') if (response.ok) { const data = await response.json() setInvitations(data.invitations || []) } else { const errorData = await response.json().catch(() => ({})) console.error('Error fetching invitations:', errorData) showToast(errorData.error || '获取邀请链接失败', 'error') } } catch (error) { console.error('Error fetching invitations:', error) showToast('获取邀请链接失败,请稍后重试', 'error') } }, [showToast]) useEffect(() => { fetchInvitations() }, [fetchInvitations]) const handleCreateInvitation = async () => { try { const response = await fetch('/api/invitations', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ maxUses: newInvitationMaxUses, expiresInDays: newInvitationExpiresInDays, }), }) if (response.ok) { await fetchInvitations() setShowCreateInvitationModal(false) setNewInvitationMaxUses(1) setNewInvitationExpiresInDays(null) showToast('邀请链接创建成功') } else { const error = await response.json() showToast(error.error || '创建失败', 'error') } } catch (error) { console.error('Create invitation error:', error) showToast('创建失败,请稍后重试', 'error') } } const handleToggleInvitationActive = async (id: string, isActive: boolean) => { try { const response = await fetch(`/api/invitations/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ isActive: !isActive }), }) if (response.ok) { await fetchInvitations() showToast(isActive ? '邀请链接已禁用' : '邀请链接已激活') } else { const error = await response.json() showToast(error.error || '操作失败', 'error') } } catch (error) { console.error('Toggle invitation active error:', error) showToast('操作失败,请稍后重试', 'error') } } const handleDeleteInvitation = async (id: string) => { const confirmed = await confirm({ title: '确认删除', message: '确定要删除这个邀请链接吗?', variant: 'danger', }) if (!confirmed) return try { const response = await fetch(`/api/invitations/${id}`, { method: 'DELETE', }) if (response.ok) { await fetchInvitations() showToast('邀请链接已删除') } else { const error = await response.json() showToast(error.error || '删除失败', 'error') } } catch (error) { console.error('Delete invitation error:', error) showToast('删除失败,请稍后重试', 'error') } } const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text).then(() => { showToast('已复制到剪贴板') }).catch(() => { showToast('复制失败', 'error') }) } return ( <>

邀请管理

创建和管理邀请链接,成员通过邀请链接加入团队。

{invitations.length === 0 ? (
暂无邀请链接,点击上方按钮创建
) : (
{invitations.map((invitation) => { const isExpired = invitation.expiresAt && new Date(invitation.expiresAt) < new Date() const isUsedUp = (invitation.currentUses || 0) >= (invitation.maxUses || 1) const isValid = (invitation.isActive !== false) && !isExpired && !isUsedUp return ( ) })}
邀请码 邀请链接 使用次数 过期时间 状态 创建时间 操作
{invitation.code} {invitation.inviteUrl ? (
{invitation.inviteUrl}
) : ( - )}
{invitation.currentUses} / {invitation.maxUses} {invitation.expiresAt ? formatDateTimeLocale(invitation.expiresAt) : '永不过期'} {!invitation.isActive ? '已禁用' : isExpired ? '已过期' : isUsedUp ? '已用完' : '有效'} {formatDateTimeLocale(invitation.createdAt)}
)}
{/* Create Invitation Modal */} { setShowCreateInvitationModal(false) setNewInvitationMaxUses(1) setNewInvitationExpiresInDays(null) }} title="创建邀请链接" size="small" footer={
} >
setNewInvitationMaxUses(parseInt(e.target.value) || 1)} className={cn(styles.inputStyles.base)} placeholder="请输入最大使用次数" />

邀请链接最多可以被使用多少次

setNewInvitationExpiresInDays(e.target.value ? parseInt(e.target.value) : null)} className={cn(styles.inputStyles.base)} placeholder="留空表示永不过期" />

邀请链接在多少天后过期,留空表示永不过期

) }