托盘
This commit is contained in:
@@ -1,24 +1,479 @@
|
||||
<script setup lang="ts">
|
||||
import { ClipboardList } from '@lucide/vue'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import {
|
||||
ClipboardList, Copy, Pin, PinOff, Trash2, Search, Image as ImageIcon,
|
||||
FileText, Files, Settings as SettingsIcon, Loader2, Eraser, Check, Keyboard,
|
||||
ChevronLeft, ChevronRight,
|
||||
} from '@lucide/vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { useClipboardStore, type ClipboardItem, type ClipboardKind } from '@/stores/clipboardStore'
|
||||
import { useModuleTabs } from '@/lib/useModuleTabs'
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription,
|
||||
AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
|
||||
const store = useClipboardStore()
|
||||
|
||||
const activeTab = ref('history')
|
||||
const tabsListRef = useModuleTabs(activeTab, [
|
||||
{ value: 'history', label: '历史' },
|
||||
{ value: 'pinned', label: '固定' },
|
||||
{ value: 'settings', label: '设置' },
|
||||
])
|
||||
|
||||
// 历史搜索/过滤
|
||||
const searchQuery = ref('')
|
||||
const kindFilter = ref<'all' | ClipboardKind>('all')
|
||||
const PAGE_SIZE = 50
|
||||
const currentPage = ref(1)
|
||||
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const totalPages = computed(() =>
|
||||
Math.max(1, Math.ceil(store.historyTotal / PAGE_SIZE))
|
||||
)
|
||||
|
||||
const loadPage = async () => {
|
||||
if (searchQuery.value.trim()) {
|
||||
await store.searchPage(searchQuery.value.trim(), currentPage.value, PAGE_SIZE)
|
||||
} else {
|
||||
await store.fetchHistoryPage({ kind: kindFilter.value, page: currentPage.value, pageSize: PAGE_SIZE })
|
||||
}
|
||||
}
|
||||
|
||||
watch([searchQuery, kindFilter], () => {
|
||||
// 过滤/搜索变化时回到第一页
|
||||
currentPage.value = 1
|
||||
if (searchTimer) clearTimeout(searchTimer)
|
||||
searchTimer = setTimeout(loadPage, 300)
|
||||
})
|
||||
|
||||
const gotoPage = async (p: number) => {
|
||||
currentPage.value = Math.min(Math.max(1, p), totalPages.value)
|
||||
await loadPage()
|
||||
}
|
||||
|
||||
// 详情弹窗
|
||||
const detailOpen = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detail = ref<ReturnType<typeof Object> & { content?: string | null; imageBase64?: string | null; kind?: string } | null>(null)
|
||||
const openDetail = async (item: ClipboardItem) => {
|
||||
detailOpen.value = true
|
||||
detailLoading.value = true
|
||||
detail.value = null
|
||||
const d = await store.getItem(item.id)
|
||||
detail.value = d
|
||||
detailLoading.value = false
|
||||
}
|
||||
|
||||
// 清空确认
|
||||
const clearOpen = ref(false)
|
||||
|
||||
// 设置表单(本地副本,保存时提交)
|
||||
const form = ref({ ...store.settings })
|
||||
watch(() => store.settings, (s) => { form.value = { ...s } }, { deep: true })
|
||||
|
||||
const handleEnabledToggle = async (val: boolean) => {
|
||||
form.value.enabled = val
|
||||
try {
|
||||
await store.saveSettings({ ...form.value })
|
||||
toast.success(val ? '已开启剪贴板监听' : '已停止剪贴板监听')
|
||||
} catch {
|
||||
toast.error('切换监听失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveSettings = async () => {
|
||||
try {
|
||||
await store.saveSettings({ ...form.value })
|
||||
toast.success('设置已保存')
|
||||
} catch {
|
||||
toast.error('保存设置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleShortcutChange = async () => {
|
||||
// 快捷键变化立即保存并注册(不等点击"保存设置")
|
||||
try {
|
||||
await store.saveSettings({ ...form.value })
|
||||
toast.success(`快捷键已更新为 ${form.value.shortcut || '(已禁用)'}`)
|
||||
} catch {
|
||||
toast.error('快捷键注册失败,可能被其他程序占用')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTestPopup = async () => {
|
||||
try {
|
||||
await store.showPopup()
|
||||
} catch {
|
||||
toast.error('打开弹窗失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 操作
|
||||
const handleCopy = async (item: ClipboardItem) => {
|
||||
try {
|
||||
await store.copyBack(item.id)
|
||||
toast.success('已复制到剪贴板')
|
||||
} catch {
|
||||
toast.error('复制失败')
|
||||
}
|
||||
}
|
||||
const handlePin = async (item: ClipboardItem) => {
|
||||
await store.setPinned(item.id, !item.pinned)
|
||||
toast.success(item.pinned ? '已取消固定' : '已固定')
|
||||
// 固定/取消后当前页数据变化,重新加载
|
||||
await loadPage()
|
||||
}
|
||||
const handleDelete = async (item: ClipboardItem) => {
|
||||
await store.remove(item.id)
|
||||
toast.success('已删除')
|
||||
// 删除后总数可能变化,若当前页已空则回退一页
|
||||
if (!store.history.length && currentPage.value > 1) {
|
||||
currentPage.value -= 1
|
||||
}
|
||||
await loadPage()
|
||||
}
|
||||
const handleClear = async () => {
|
||||
clearOpen.value = false
|
||||
await store.clear()
|
||||
currentPage.value = 1
|
||||
await loadPage()
|
||||
toast.success('已清空历史')
|
||||
}
|
||||
|
||||
// 显示辅助
|
||||
const kindIcon = (k: ClipboardKind) => {
|
||||
if (k === 'text') return FileText
|
||||
if (k === 'image') return ImageIcon
|
||||
return Files
|
||||
}
|
||||
const kindLabel = (k: ClipboardKind) => (k === 'text' ? '文本' : k === 'image' ? '图片' : '文件')
|
||||
const kindBadgeClass = (k: ClipboardKind) =>
|
||||
k === 'text'
|
||||
? 'border-blue-500/40 bg-blue-500/10 text-blue-600 dark:text-blue-400'
|
||||
: k === 'image'
|
||||
? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-amber-500/40 bg-amber-500/10 text-amber-600 dark:text-amber-400'
|
||||
const formatSize = (bytes: number) => {
|
||||
if (!bytes) return ''
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
const formatTime = (ms: number) => {
|
||||
const diff = Date.now() - ms
|
||||
if (diff < 60_000) return '刚刚'
|
||||
if (diff < 3600_000) return `${Math.floor(diff / 60_000)} 分钟前`
|
||||
if (diff < 86_400_000) return `${Math.floor(diff / 3600_000)} 小时前`
|
||||
if (diff < 7 * 86_400_000) return `${Math.floor(diff / 86_400_000)} 天前`
|
||||
const d = new Date(ms)
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const historyList = computed(() => store.history)
|
||||
const pinnedList = computed(() => store.pinned)
|
||||
|
||||
onMounted(async () => {
|
||||
await store.init()
|
||||
await Promise.all([loadPage(), store.refreshPinned()])
|
||||
form.value = { ...store.settings }
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
store.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full p-6 overflow-y-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<ClipboardList class="h-5 w-5 text-primary" />
|
||||
剪贴板增强模块
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col items-center justify-center h-64 text-muted-foreground">
|
||||
<ClipboardList class="h-16 w-16 mb-4 opacity-50" />
|
||||
<p>剪贴板增强功能开发中...</p>
|
||||
<p class="text-sm mt-2">支持剪贴板历史记录、搜索、固定常用条目等功能</p>
|
||||
<div class="h-full p-6 overflow-hidden flex flex-col">
|
||||
<Tabs v-model="activeTab" class="flex-1 min-h-0 flex flex-col">
|
||||
<div ref="tabsListRef" class="shrink-0">
|
||||
<TabsList class="grid w-full max-w-md grid-cols-3 !bg-transparent !p-0 !shadow-none">
|
||||
<TabsTrigger value="history" class="gap-1.5"><ClipboardList class="size-3.5" />历史</TabsTrigger>
|
||||
<TabsTrigger value="pinned" class="gap-1.5"><Pin class="size-3.5" />固定</TabsTrigger>
|
||||
<TabsTrigger value="settings" class="gap-1.5"><SettingsIcon class="size-3.5" />设置</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<!-- 历史 -->
|
||||
<TabsContent value="history" class="flex-1 min-h-0 flex flex-col mt-4 tab-animate">
|
||||
<div class="flex items-center gap-2 mb-3 shrink-0">
|
||||
<div class="relative flex-1 max-w-sm">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="searchQuery" placeholder="搜索历史..." class="pl-8" />
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-for="k in (['all','text','image','files'] as const)"
|
||||
:key="k"
|
||||
:variant="kindFilter === k ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
@click="kindFilter = k"
|
||||
>
|
||||
{{ k === 'all' ? '全部' : k === 'text' ? '文本' : k === 'image' ? '图片' : '文件' }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
<Badge variant="secondary">
|
||||
<Loader2 v-if="store.status.running" class="h-3 w-3 mr-1 animate-spin" />
|
||||
{{ store.historyTotal }} 条
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm" @click="clearOpen = true" :disabled="!historyList.length">
|
||||
<Eraser class="h-4 w-4 mr-1" />清空
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto space-y-1.5 pr-1">
|
||||
<div
|
||||
v-if="!historyList.length"
|
||||
class="flex flex-col items-center justify-center h-full text-muted-foreground"
|
||||
>
|
||||
<ClipboardList class="h-12 w-12 mb-3 opacity-40" />
|
||||
<p class="text-sm">暂无历史记录,复制内容后将自动收录</p>
|
||||
</div>
|
||||
<Card
|
||||
v-for="item in historyList"
|
||||
:key="item.id"
|
||||
class="group hover:shadow-md transition-shadow py-0"
|
||||
>
|
||||
<CardContent class="flex items-center gap-3 px-3 py-2">
|
||||
<component :is="kindIcon(item.kind)" class="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)">
|
||||
<p class="text-sm break-all line-clamp-1" :title="item.preview">{{ item.preview }}</p>
|
||||
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
|
||||
<Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge>
|
||||
<span>{{ formatTime(item.createdAt) }}</span>
|
||||
<span v-if="item.size">· {{ formatSize(item.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" title="复制" @click.stop="handleCopy(item)">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" :title="item.pinned ? '取消固定' : '固定'" @click.stop="handlePin(item)">
|
||||
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 hover:text-destructive" title="删除" @click.stop="handleDelete(item)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="totalPages > 1" class="flex items-center justify-center gap-2 pt-3 shrink-0">
|
||||
<Button variant="outline" size="icon" class="h-7 w-7" :disabled="currentPage <= 1" @click="gotoPage(currentPage - 1)">
|
||||
<ChevronLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
<span class="text-xs text-muted-foreground tabular-nums">{{ currentPage }} / {{ totalPages }}</span>
|
||||
<Button variant="outline" size="icon" class="h-7 w-7" :disabled="currentPage >= totalPages" @click="gotoPage(currentPage + 1)">
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 固定 -->
|
||||
<TabsContent value="pinned" class="flex-1 min-h-0 flex flex-col mt-4 tab-animate">
|
||||
<div class="flex-1 min-h-0 overflow-y-auto space-y-1.5 pr-1">
|
||||
<div
|
||||
v-if="!pinnedList.length"
|
||||
class="flex flex-col items-center justify-center h-full text-muted-foreground"
|
||||
>
|
||||
<Pin class="h-12 w-12 mb-3 opacity-40" />
|
||||
<p class="text-sm">暂无固定条目</p>
|
||||
<p class="text-xs mt-1">鼠标悬停历史条目,点击图钉按钮即可固定</p>
|
||||
</div>
|
||||
<Card v-for="item in pinnedList" :key="item.id" class="group hover:shadow-md transition-shadow py-0">
|
||||
<CardContent class="flex items-center gap-3 px-3 py-2">
|
||||
<component :is="kindIcon(item.kind)" class="h-4 w-4 text-primary shrink-0" />
|
||||
<div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)">
|
||||
<p class="text-sm break-all line-clamp-1" :title="item.preview">{{ item.preview }}</p>
|
||||
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
|
||||
<Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge>
|
||||
<span>{{ formatTime(item.createdAt) }}</span>
|
||||
<span v-if="item.size">· {{ formatSize(item.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" title="复制" @click.stop="handleCopy(item)">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" title="取消固定" @click.stop="handlePin(item)">
|
||||
<PinOff class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 hover:text-destructive" title="删除" @click.stop="handleDelete(item)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 设置 -->
|
||||
<TabsContent value="settings" class="flex-1 min-h-0 overflow-y-auto mt-4 tab-animate">
|
||||
<div class="max-w-xl space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-medium flex items-center gap-2">
|
||||
<SettingsIcon class="h-4 w-4" />基本设置
|
||||
</h3>
|
||||
<Button @click="handleSaveSettings"><Check class="h-4 w-4 mr-1" />保存设置</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent class="space-y-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>启用剪贴板监听</Label>
|
||||
<p class="text-xs text-muted-foreground mt-1">关闭后将停止记录剪贴板内容</p>
|
||||
</div>
|
||||
<Switch
|
||||
:model-value="form.enabled"
|
||||
@update:model-value="handleEnabledToggle"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>记录文本</Label>
|
||||
<p class="text-xs text-muted-foreground mt-1">收录复制/剪切的文本</p>
|
||||
</div>
|
||||
<Switch :model-value="form.recordText" @update:model-value="form.recordText = $event" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>记录图片</Label>
|
||||
<p class="text-xs text-muted-foreground mt-1">收录复制的图片(截图等)</p>
|
||||
</div>
|
||||
<Switch :model-value="form.recordImage" @update:model-value="form.recordImage = $event" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>记录文件</Label>
|
||||
<p class="text-xs text-muted-foreground mt-1">收录复制的文件列表</p>
|
||||
</div>
|
||||
<Switch :model-value="form.recordFiles" @update:model-value="form.recordFiles = $event" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>内容去重</Label>
|
||||
<p class="text-xs text-muted-foreground mt-1">相同内容只保留一条,重复复制将置顶</p>
|
||||
</div>
|
||||
<Switch :model-value="form.dedup" @update:model-value="form.dedup = $event" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent class="space-y-4 p-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label>最大历史条数</Label>
|
||||
<Input v-model.number="form.maxItems" type="number" min="50" max="10000" />
|
||||
<p class="text-xs text-muted-foreground">超出后自动清理最旧的非固定项</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>图片大小上限 (KB)</Label>
|
||||
<Input v-model.number="form.maxImageKb" type="number" min="0" />
|
||||
<p class="text-xs text-muted-foreground">0 表示不限制</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent class="space-y-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Label class="flex items-center gap-2">
|
||||
<Keyboard class="h-4 w-4" />快捷弹窗快捷键
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground mt-1">全局快捷键触发鼠标位置历史弹窗,留空禁用</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" @click="handleTestPopup">测试弹窗</Button>
|
||||
</div>
|
||||
<Input
|
||||
v-model="form.shortcut"
|
||||
placeholder="如 Alt+V / Ctrl+Shift+V"
|
||||
@change="handleShortcutChange"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">格式:修饰键+主键,如 Ctrl+C、Alt+V、Shift+F1。修改后自动生效。</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<Dialog v-model:open="detailOpen">
|
||||
<DialogContent class="max-w-2xl max-h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>内容详情</DialogTitle>
|
||||
<DialogDescription>{{ detail ? kindLabel(detail.kind as ClipboardKind) : '' }}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="flex-1 min-h-0 overflow-auto">
|
||||
<div v-if="detailLoading" class="flex items-center justify-center py-12">
|
||||
<Loader2 class="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<template v-else-if="detail">
|
||||
<img
|
||||
v-if="detail.kind === 'image' && detail.imageBase64"
|
||||
:src="`data:image/png;base64,${detail.imageBase64}`"
|
||||
class="max-w-full max-h-[60vh] mx-auto rounded"
|
||||
alt="剪贴板图片"
|
||||
/>
|
||||
<pre v-else-if="detail.kind === 'text'" class="text-sm whitespace-pre-wrap break-all font-mono bg-muted/50 p-3 rounded">{{ detail.content }}</pre>
|
||||
<ul v-else-if="detail.kind === 'files'" class="space-y-1 text-sm">
|
||||
<li
|
||||
v-for="(p, i) in (parseFiles(detail.content))"
|
||||
:key="i"
|
||||
class="font-mono break-all p-2 rounded bg-muted/50"
|
||||
>{{ p }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- 清空确认 -->
|
||||
<AlertDialog v-model:open="clearOpen">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>清空历史记录?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
将删除所有非固定历史条目,此操作不可撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction class="bg-destructive text-destructive-foreground hover:bg-destructive/90" @click="handleClear">
|
||||
清空
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
function parseFiles(content: string | null): string[] {
|
||||
if (!content) return []
|
||||
try {
|
||||
return JSON.parse(content) as string[]
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user