import { useState, useEffect, useCallback, useMemo } from 'react' import { Filter, RefreshCw, Trash2, Edit, Info, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, CheckCircle2, Ban, Upload, ArrowLeft, Check, X, ImageIcon, } from 'lucide-react' import Uppy from '@uppy/core' import Dashboard from '@uppy/react/dashboard' import '@uppy/core/css/style.min.css' import '@uppy/dashboard/css/style.min.css' import '@/styles/uppy-custom.css' import { fetchWithAuth } from '@/lib/fetch-with-auth' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { EmojiThumbnail } from '@/components/emoji-thumbnail' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Badge } from '@/components/ui/badge' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Checkbox } from '@/components/ui/checkbox' import { ScrollArea } from '@/components/ui/scroll-area' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Markdown } from '@/components/ui/markdown' import { useToast } from '@/hooks/use-toast' import type { Emoji, EmojiStats } from '@/types/emoji' import { getEmojiList, getEmojiDetail, getEmojiStats, updateEmoji, deleteEmoji, registerEmoji, banEmoji, getEmojiThumbnailUrl, getEmojiOriginalUrl, batchDeleteEmojis, getEmojiUploadUrl, } from '@/lib/emoji-api' export function EmojiManagementPage() { const [emojiList, setEmojiList] = useState([]) const [stats, setStats] = useState(null) const [loading, setLoading] = useState(false) const [page, setPage] = useState(1) const [total, setTotal] = useState(0) const [pageSize, setPageSize] = useState(20) const [registeredFilter, setRegisteredFilter] = useState('all') const [bannedFilter, setBannedFilter] = useState('all') const [formatFilter, setFormatFilter] = useState('all') const [sortBy, setSortBy] = useState('usage_count') const [sortOrder, setSortOrder] = useState<'desc' | 'asc'>('desc') const [selectedEmoji, setSelectedEmoji] = useState(null) const [detailDialogOpen, setDetailDialogOpen] = useState(false) const [editDialogOpen, setEditDialogOpen] = useState(false) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [selectedIds, setSelectedIds] = useState>(new Set()) const [batchDeleteDialogOpen, setBatchDeleteDialogOpen] = useState(false) const [jumpToPage, setJumpToPage] = useState('') const [cardSize, setCardSize] = useState<'small' | 'medium' | 'large'>('medium') const [uploadDialogOpen, setUploadDialogOpen] = useState(false) const { toast } = useToast() // 加载表情包列表 const loadEmojiList = useCallback(async () => { try { setLoading(true) const response = await getEmojiList({ page, page_size: pageSize, is_registered: registeredFilter === 'all' ? undefined : registeredFilter === 'registered', is_banned: bannedFilter === 'all' ? undefined : bannedFilter === 'banned', format: formatFilter === 'all' ? undefined : formatFilter, sort_by: sortBy, sort_order: sortOrder, }) setEmojiList(response.data) setTotal(response.total) } catch (error) { const message = error instanceof Error ? error.message : '加载表情包列表失败' toast({ title: '错误', description: message, variant: 'destructive', }) } finally { setLoading(false) } }, [page, pageSize, registeredFilter, bannedFilter, formatFilter, sortBy, sortOrder, toast]) // 加载统计数据 const loadStats = async () => { try { const response = await getEmojiStats() setStats(response.data) } catch (error) { console.error('加载统计数据失败:', error) } } useEffect(() => { loadEmojiList() }, [loadEmojiList]) useEffect(() => { loadStats() }, []) // 查看详情 const handleViewDetail = async (emoji: Emoji) => { try { const response = await getEmojiDetail(emoji.id) setSelectedEmoji(response.data) setDetailDialogOpen(true) } catch (error) { const message = error instanceof Error ? error.message : '加载详情失败' toast({ title: '错误', description: message, variant: 'destructive', }) } } // 编辑表情包 const handleEdit = (emoji: Emoji) => { setSelectedEmoji(emoji) setEditDialogOpen(true) } // 删除表情包 const handleDelete = (emoji: Emoji) => { setSelectedEmoji(emoji) setDeleteDialogOpen(true) } // 确认删除 const confirmDelete = async () => { if (!selectedEmoji) return try { await deleteEmoji(selectedEmoji.id) toast({ title: '成功', description: '表情包已删除', }) setDeleteDialogOpen(false) setSelectedEmoji(null) loadEmojiList() loadStats() } catch (error) { const message = error instanceof Error ? error.message : '删除失败' toast({ title: '错误', description: message, variant: 'destructive', }) } } // 快速注册 const handleRegister = async (emoji: Emoji) => { try { await registerEmoji(emoji.id) toast({ title: '成功', description: '表情包已注册', }) loadEmojiList() loadStats() } catch (error) { const message = error instanceof Error ? error.message : '注册失败' toast({ title: '错误', description: message, variant: 'destructive', }) } } // 快速封禁 const handleBan = async (emoji: Emoji) => { try { await banEmoji(emoji.id) toast({ title: '成功', description: '表情包已封禁', }) loadEmojiList() loadStats() } catch (error) { const message = error instanceof Error ? error.message : '封禁失败' toast({ title: '错误', description: message, variant: 'destructive', }) } } // 切换选择 const toggleSelect = (id: number) => { const newSelected = new Set(selectedIds) if (newSelected.has(id)) { newSelected.delete(id) } else { newSelected.add(id) } setSelectedIds(newSelected) } // 批量删除 const handleBatchDelete = async () => { try { const result = await batchDeleteEmojis(Array.from(selectedIds)) toast({ title: '批量删除完成', description: result.message, }) setSelectedIds(new Set()) setBatchDeleteDialogOpen(false) loadEmojiList() 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 formatOptions = stats?.formats ? Object.keys(stats.formats) : [] return (
{/* 页面标题 */}

表情包管理

管理麦麦的表情包资源

{/* 统计卡片 */} {stats && (
总数 {stats.total} 已注册 {stats.registered} 已封禁 {stats.banned} 未注册 {stats.unregistered}
)} {/* 筛选和排序 */} 筛选和排序
{selectedIds.size > 0 && ( 已选择 {selectedIds.size} 个表情包 )} {/* 卡片尺寸切换 */}
{selectedIds.size > 0 && ( <> )}
{/* 表情包卡片列表 */} 表情包列表 共 {total} 个表情包,当前第 {page} 页 {/* 卡片网格视图 */} {emojiList.length === 0 ? (
暂无数据
) : (
{emojiList.map((emoji) => (
toggleSelect(emoji.id)} > {/* 选中指示器 */}
{selectedIds.has(emoji.id) && }
{/* 状态标签 */}
{emoji.is_registered && ( 已注册 )} {emoji.is_banned && ( 已封禁 )}
{/* 图片 */}
{/* 底部信息和操作 */}
{/* 使用次数和格式 */}
{emoji.format.toUpperCase()} {emoji.usage_count}次
{/* 操作按钮 - 悬停时显示 */}
{!emoji.is_registered && ( )} {!emoji.is_banned && ( )}
))}
)} {/* 分页 */} {/* 分页 - 增强版 */} {total > 0 && (
显示 {(page - 1) * pageSize + 1} 到{' '} {Math.min(page * pageSize, total)} 条,共 {total} 条
{/* 首页 */} {/* 上一页 */} {/* 页码跳转 */}
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)} />
{/* 下一页 */} {/* 末页 */}
)}
{/* 详情对话框 */} {/* 编辑对话框 */} { loadEmojiList() loadStats() }} /> {/* 上传对话框 */} { loadEmojiList() loadStats() }} />
{/* 批量删除确认对话框 */} 确认批量删除 你确定要删除选中的 {selectedIds.size} 个表情包吗?此操作不可撤销。 取消 确认删除 {/* 删除确认对话框 */} 确认删除 确定要删除这个表情包吗?此操作无法撤销。
) } // 详情对话框组件 function EmojiDetailDialog({ emoji, open, onOpenChange, }: { emoji: Emoji | null open: boolean onOpenChange: (open: boolean) => void }) { if (!emoji) return null const formatTime = (timestamp: number | null) => { if (!timestamp) return '-' return new Date(timestamp * 1000).toLocaleString('zh-CN') } return ( 表情包详情
{/* 表情包预览图 - 使用原图 */}
{emoji.description { const target = e.target as HTMLImageElement target.style.display = 'none' const parent = target.parentElement if (parent) { parent.innerHTML = '' } }} />
{emoji.id}
{emoji.format.toUpperCase()}
{emoji.full_path}
{emoji.emoji_hash}
{emoji.description ? (
{emoji.description}
) : (
-
)}
{emoji.emotion ? ( {emoji.emotion} ) : ( - )}
{emoji.is_registered && ( 已注册 )} {emoji.is_banned && ( 已封禁 )} {!emoji.is_registered && !emoji.is_banned && ( 未注册 )}
{emoji.usage_count}
{formatTime(emoji.record_time)}
{formatTime(emoji.register_time)}
{formatTime(emoji.last_used_time)}
) } // 编辑对话框组件 function EmojiEditDialog({ emoji, open, onOpenChange, onSuccess, }: { emoji: Emoji | null open: boolean onOpenChange: (open: boolean) => void onSuccess: () => void }) { const [emotionInput, setEmotionInput] = useState('') const [isRegistered, setIsRegistered] = useState(false) const [isBanned, setIsBanned] = useState(false) const [saving, setSaving] = useState(false) const { toast } = useToast() useEffect(() => { if (emoji) { setEmotionInput(emoji.emotion || '') setIsRegistered(emoji.is_registered) setIsBanned(emoji.is_banned) } }, [emoji]) const handleSave = async () => { if (!emoji) return try { setSaving(true) // 将输入的标签字符串标准化为逗号分隔格式 const emotionString = emotionInput .split(/[,,]/) .map((s) => s.trim()) .filter(Boolean) .join(',') await updateEmoji(emoji.id, { emotion: emotionString || undefined, is_registered: isRegistered, is_banned: isBanned, }) toast({ title: '成功', description: '表情包信息已更新', }) onOpenChange(false) onSuccess() } catch (error) { const message = error instanceof Error ? error.message : '保存失败' toast({ title: '错误', description: message, variant: 'destructive', }) } finally { setSaving(false) } } if (!emoji) return null return ( 编辑表情包 修改表情包的情绪和状态信息