perf:优化webui配置展示,优化log显示,修复表达审核
This commit is contained in:
@@ -80,6 +80,7 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
|
||||
// 快速审核模式状态
|
||||
const [quickFilterType, setQuickFilterType] = useState<'unchecked' | 'passed' | 'rejected' | 'all'>('unchecked')
|
||||
const [quickExpressions, setQuickExpressions] = useState<Expression[]>([])
|
||||
const quickExpressionsRef = useRef<Expression[]>([])
|
||||
const [quickCurrentIndex, setQuickCurrentIndex] = useState(0)
|
||||
const [quickLoading, setQuickLoading] = useState(false)
|
||||
const [quickTotal, setQuickTotal] = useState(0)
|
||||
@@ -92,6 +93,10 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
|
||||
const cardRef = useRef<HTMLDivElement>(null)
|
||||
const dragStartRef = useRef<{ x: number; y: number } | null>(null)
|
||||
const isDraggingRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
quickExpressionsRef.current = quickExpressions
|
||||
}, [quickExpressions])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [statsLoading, setStatsLoading] = useState(false)
|
||||
const [total, setTotal] = useState(0)
|
||||
@@ -180,9 +185,13 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
|
||||
setQuickLoading(true)
|
||||
const pageToLoad = append ? quickPage + 1 : quickPage
|
||||
const result = await getReviewList({
|
||||
page: pageToLoad,
|
||||
page: quickFilterType === 'unchecked' ? 1 : pageToLoad,
|
||||
page_size: 20,
|
||||
filter_type: quickFilterType,
|
||||
order: quickFilterType === 'unchecked' ? 'random' : 'latest',
|
||||
exclude_ids: quickFilterType === 'unchecked' && append
|
||||
? quickExpressionsRef.current.map((expr) => expr.id)
|
||||
: undefined,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
|
||||
@@ -442,16 +442,20 @@ export async function getReviewList(params: {
|
||||
page?: number
|
||||
page_size?: number
|
||||
filter_type?: 'unchecked' | 'passed' | 'rejected' | 'all'
|
||||
order?: 'latest' | 'random'
|
||||
search?: string
|
||||
chat_id?: string
|
||||
exclude_ids?: number[]
|
||||
}): Promise<ApiResponse<ReviewListResponse>> {
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.page) queryParams.append('page', params.page.toString())
|
||||
if (params.page_size) queryParams.append('page_size', params.page_size.toString())
|
||||
if (params.filter_type) queryParams.append('filter_type', params.filter_type)
|
||||
if (params.order) queryParams.append('order', params.order)
|
||||
if (params.search) queryParams.append('search', params.search)
|
||||
if (params.chat_id) queryParams.append('chat_id', params.chat_id)
|
||||
params.exclude_ids?.forEach((id) => queryParams.append('exclude_ids', id.toString()))
|
||||
|
||||
const response = await fetchWithAuth(`${API_BASE}/review/list?${queryParams}`)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { parse as parseToml } from 'smol-toml'
|
||||
|
||||
import { AlertDescription, Alert } from '@/components/ui/alert'
|
||||
@@ -23,11 +23,13 @@ import { useToast } from '@/hooks/use-toast'
|
||||
import { getBotConfig, getBotConfigRaw, getBotConfigSchema, updateBotConfig, updateBotConfigRaw } from '@/lib/config-api'
|
||||
import { fieldHooks } from '@/lib/field-hooks'
|
||||
import { RestartProvider, useRestart } from '@/lib/restart-context'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { ChevronDown, ChevronUp, Code2, Info, Layout, Power, RefreshCw, Save } from 'lucide-react'
|
||||
|
||||
import type { ConfigSchema } from '@/types/config-schema'
|
||||
import {
|
||||
BotPlatformsHook,
|
||||
ChatPromptsHook,
|
||||
ChatTalkValueRulesHook,
|
||||
ExpressionGroupsHook,
|
||||
@@ -413,6 +415,7 @@ function BotConfigPageContent() {
|
||||
|
||||
useEffect(() => {
|
||||
const hookEntries = [
|
||||
['bot.platforms', BotPlatformsHook],
|
||||
['chat.chat_prompts', ChatPromptsHook],
|
||||
['chat.talk_value_rules', ChatTalkValueRulesHook],
|
||||
['expression.expression_groups', ExpressionGroupsHook],
|
||||
@@ -773,7 +776,23 @@ function BotConfigPageContent() {
|
||||
<p className="text-muted-foreground mt-1 text-xs sm:text-sm">管理麦麦的核心功能和行为设置</p>
|
||||
</div>
|
||||
{/* 按钮组 - 桌面端靠右 */}
|
||||
<div className="flex gap-2 flex-shrink-0">
|
||||
<div className="flex flex-wrap gap-2 flex-shrink-0 sm:justify-end">
|
||||
<Tabs
|
||||
value={editMode}
|
||||
onValueChange={(v) => handleModeChange(v as 'visual' | 'source')}
|
||||
className="w-full min-w-[13rem] sm:w-[14rem]"
|
||||
>
|
||||
<TabsList className="grid h-8 w-full grid-cols-2 sm:h-9">
|
||||
<TabsTrigger value="visual" className="px-2 text-xs">
|
||||
<Layout className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
可视化
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="source" className="px-2 text-xs">
|
||||
<Code2 className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
源代码
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<Button
|
||||
onClick={handleReloadFromFile}
|
||||
disabled={saving || autoSaving || isRestarting}
|
||||
@@ -833,22 +852,6 @@ function BotConfigPageContent() {
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模式切换 - 单独一行 */}
|
||||
<div className="flex">
|
||||
<Tabs value={editMode} onValueChange={(v) => handleModeChange(v as 'visual' | 'source')} className="w-full">
|
||||
<TabsList className="h-8 sm:h-9 w-full grid grid-cols-2">
|
||||
<TabsTrigger value="visual" className="text-xs sm:text-sm">
|
||||
<Layout className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
|
||||
可视化编辑
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="source" className="text-xs sm:text-sm">
|
||||
<Code2 className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
|
||||
源代码编辑
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 重启提示 */}
|
||||
@@ -975,6 +978,9 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) {
|
||||
? tabGroups
|
||||
: tabGroups.filter((tab) => DEFAULT_VISIBLE_TAB_IDS.has(tab.id))
|
||||
const hasCollapsibleTabs = tabGroups.some((tab) => !DEFAULT_VISIBLE_TAB_IDS.has(tab.id))
|
||||
const firstExpandedTabId = visibleTabGroups.find(
|
||||
(tab) => !DEFAULT_VISIBLE_TAB_IDS.has(tab.id)
|
||||
)?.id
|
||||
|
||||
const toggleExpanded = () => {
|
||||
setExpanded((current) => {
|
||||
@@ -1033,15 +1039,26 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) {
|
||||
return (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="flex flex-wrap h-auto gap-1 p-1">
|
||||
{visibleTabGroups.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
{visibleTabGroups.map((tab) => {
|
||||
const isExpandedOnlyTab = !DEFAULT_VISIBLE_TAB_IDS.has(tab.id)
|
||||
return (
|
||||
<Fragment key={tab.id}>
|
||||
{tab.id === firstExpandedTabId && (
|
||||
<span className="mx-1 hidden h-6 w-px bg-border/80 sm:block" />
|
||||
)}
|
||||
<TabsTrigger
|
||||
value={tab.id}
|
||||
className={cn(
|
||||
"text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm",
|
||||
isExpandedOnlyTab &&
|
||||
"border border-dashed border-border/70 bg-background/45 text-muted-foreground/80 hover:bg-background/70 data-[state=active]:border-primary/45 data-[state=active]:bg-primary/10 data-[state=active]:text-primary data-[state=active]:shadow-none"
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
{hasCollapsibleTabs && (
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -28,6 +28,11 @@ interface ExpressionGroupValue {
|
||||
expression_groups: ExpressionGroupTarget[]
|
||||
}
|
||||
|
||||
interface PlatformAccountRow {
|
||||
platform: string
|
||||
account: string
|
||||
}
|
||||
|
||||
const ruleTypeLabel = (rule: unknown) => {
|
||||
if (rule === 'private') return '私聊'
|
||||
if (rule === 'group') return '群聊'
|
||||
@@ -102,6 +107,30 @@ const formatExpressionTarget = (target: ExpressionGroupTarget): string => {
|
||||
return `${platform}:${itemId} · ${rule}`
|
||||
}
|
||||
|
||||
const normalizePlatformAccounts = (value: unknown): string[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.map((item) => String(item ?? ''))
|
||||
}
|
||||
|
||||
const parsePlatformAccount = (value: string): PlatformAccountRow => {
|
||||
const separatorIndex = value.indexOf(':')
|
||||
if (separatorIndex < 0) {
|
||||
return { platform: '', account: value }
|
||||
}
|
||||
return {
|
||||
platform: value.slice(0, separatorIndex),
|
||||
account: value.slice(separatorIndex + 1),
|
||||
}
|
||||
}
|
||||
|
||||
const formatPlatformAccount = (row: PlatformAccountRow): string => {
|
||||
const platform = row.platform.trim()
|
||||
const account = row.account.trim()
|
||||
if (!platform) return account
|
||||
if (!account) return `${platform}:`
|
||||
return `${platform}:${account}`
|
||||
}
|
||||
|
||||
export const ChatTalkValueRulesHook = createListItemEditorHook({
|
||||
addLabel: '添加发言频率规则',
|
||||
addButtonPlacement: 'top',
|
||||
@@ -175,6 +204,96 @@ export const ExpressionLearningListHook = createListItemEditorHook({
|
||||
},
|
||||
})
|
||||
|
||||
export const BotPlatformsHook: FieldHookComponent = ({ onChange, value }) => {
|
||||
const platforms = normalizePlatformAccounts(value)
|
||||
const rows = platforms.map(parsePlatformAccount)
|
||||
|
||||
const updateRows = (nextRows: PlatformAccountRow[]) => {
|
||||
onChange?.(nextRows.map(formatPlatformAccount))
|
||||
}
|
||||
|
||||
const addRow = () => {
|
||||
updateRows([...rows, { platform: '', account: '' }])
|
||||
}
|
||||
|
||||
const removeRow = (rowIndex: number) => {
|
||||
updateRows(rows.filter((_, index) => index !== rowIndex))
|
||||
}
|
||||
|
||||
const updateRow = (rowIndex: number, patch: Partial<PlatformAccountRow>) => {
|
||||
updateRows(
|
||||
rows.map((row, index) =>
|
||||
index === rowIndex ? { ...row, ...patch } : row
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm font-medium">其他平台</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
每行保存为 platform:account,例如 wx:114514。
|
||||
</p>
|
||||
</div>
|
||||
<Button type="button" size="sm" variant="outline" onClick={addRow}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
添加平台
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed bg-muted/30 px-4 py-5 text-center text-sm text-muted-foreground">
|
||||
暂无其他平台账号。
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div
|
||||
key={rowIndex}
|
||||
className="grid gap-2 rounded-md border bg-muted/20 p-3 sm:grid-cols-[minmax(7rem,0.6fr)_minmax(10rem,1fr)_auto]"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">平台</Label>
|
||||
<Input
|
||||
value={row.platform}
|
||||
placeholder="wx"
|
||||
onChange={(event) =>
|
||||
updateRow(rowIndex, { platform: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">账号</Label>
|
||||
<Input
|
||||
className="font-mono"
|
||||
value={row.account}
|
||||
placeholder="114514"
|
||||
onChange={(event) =>
|
||||
updateRow(rowIndex, { account: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label={`删除其他平台 ${rowIndex + 1}`}
|
||||
onClick={() => removeRow(rowIndex)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const KeywordRulesHook = createListItemEditorHook({
|
||||
addLabel: '添加关键词规则',
|
||||
helperText: '匹配命中后会用 reaction 内容作为额外上下文。keywords 至少填一条,或使用正则模式。',
|
||||
@@ -279,8 +398,8 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 rounded-lg border bg-card p-4 sm:p-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-3 rounded-lg border bg-card p-4 sm:p-5">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold">表达互通组</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -299,13 +418,13 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
|
||||
暂无互通组,点击“添加互通组”开始配置。
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
{groups.map((group, groupIndex) => (
|
||||
<div
|
||||
key={groupIndex}
|
||||
className="space-y-3 rounded-md border bg-muted/20 p-3 sm:p-4"
|
||||
className="space-y-2 rounded-md border bg-muted/20 p-2.5 sm:p-3"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
互通组 {groupIndex + 1}
|
||||
@@ -341,15 +460,16 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
|
||||
这个互通组还没有成员。
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{group.expression_groups.map((member, memberIndex) => (
|
||||
<div
|
||||
key={`${groupIndex}-${memberIndex}`}
|
||||
className="grid gap-3 rounded-md bg-background/80 p-3 md:grid-cols-[minmax(7rem,0.75fr)_minmax(10rem,1fr)_minmax(8rem,0.8fr)_auto]"
|
||||
className="grid items-end gap-2 rounded-md bg-background/80 px-2.5 py-2 md:grid-cols-[minmax(6rem,0.65fr)_minmax(9rem,1fr)_minmax(7rem,0.75fr)_2.25rem]"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">平台</Label>
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-[11px] leading-none text-muted-foreground">平台</Label>
|
||||
<Input
|
||||
className="h-8"
|
||||
value={member.platform}
|
||||
placeholder="qq"
|
||||
onChange={(event) =>
|
||||
@@ -359,10 +479,10 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">账号 / 群号</Label>
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-[11px] leading-none text-muted-foreground">账号 / 群号</Label>
|
||||
<Input
|
||||
className="font-mono"
|
||||
className="h-8 font-mono"
|
||||
value={member.item_id}
|
||||
placeholder="123456"
|
||||
onChange={(event) =>
|
||||
@@ -372,8 +492,8 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">类型</Label>
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-[11px] leading-none text-muted-foreground">类型</Label>
|
||||
<Select
|
||||
value={member.rule_type}
|
||||
onValueChange={(nextRuleType) =>
|
||||
@@ -382,7 +502,7 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -399,6 +519,7 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8"
|
||||
aria-label={`删除互通组 ${groupIndex + 1} 的成员 ${memberIndex + 1}`}
|
||||
onClick={() => removeMember(groupIndex, memberIndex)}
|
||||
>
|
||||
@@ -409,16 +530,6 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{group.expression_groups.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.expression_groups.map((member, memberIndex) => (
|
||||
<Badge key={memberIndex} variant="outline">
|
||||
{formatExpressionTarget(member)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ export type {
|
||||
UseAutoSaveReturnGeneric,
|
||||
} from './useAutoSave'
|
||||
export {
|
||||
BotPlatformsHook,
|
||||
ChatPromptsHook,
|
||||
ChatTalkValueRulesHook,
|
||||
ExpressionGroupsHook,
|
||||
|
||||
@@ -449,8 +449,8 @@ export function EmojiManagementPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 pt-4 border-t">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col gap-3 border-t pt-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{selectedIds.size > 0 && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
已选择 {selectedIds.size} 个表情包
|
||||
@@ -477,8 +477,41 @@ export function EmojiManagementPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={loadEmojiList}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
刷新
|
||||
</Button>
|
||||
|
||||
{selectedIds.size > 0 && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedIds(new Set())}
|
||||
>
|
||||
取消选择
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => setBatchDeleteDialogOpen(true)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
批量删除
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<div className="flex items-center gap-2 sm:ml-auto">
|
||||
<Label
|
||||
htmlFor="emoji-page-size"
|
||||
className="text-sm whitespace-nowrap"
|
||||
@@ -503,41 +536,8 @@ export function EmojiManagementPage() {
|
||||
<SelectItem value="100">100</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedIds.size > 0 && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedIds(new Set())}
|
||||
>
|
||||
取消选择
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => setBatchDeleteDialogOpen(true)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
批量删除
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={loadEmojiList}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user