截图模块初始化
This commit is contained in:
@@ -3,12 +3,12 @@ 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 { useClipboardStore, type ClipboardItem, type ClipboardKind, type ClipboardItemDetail } from '@/stores/clipboardStore'
|
||||
import { useModuleTabs } from '@/lib/useModuleTabs'
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -22,6 +22,9 @@ import {
|
||||
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription,
|
||||
AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import {
|
||||
Pagination, PaginationContent, PaginationItem, PaginationEllipsis,
|
||||
} from '@/components/ui/pagination'
|
||||
|
||||
const store = useClipboardStore()
|
||||
|
||||
@@ -66,7 +69,7 @@ const gotoPage = async (p: number) => {
|
||||
// 详情弹窗
|
||||
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 detail = ref<ClipboardItemDetail | null>(null)
|
||||
const openDetail = async (item: ClipboardItem) => {
|
||||
detailOpen.value = true
|
||||
detailLoading.value = true
|
||||
@@ -76,6 +79,15 @@ const openDetail = async (item: ClipboardItem) => {
|
||||
detailLoading.value = false
|
||||
}
|
||||
|
||||
/** 根据 base64 前缀判断图片 MIME 类型(dib_to_png 可能直接透传 PNG/JPEG) */
|
||||
const imageSrc = computed(() => {
|
||||
const b64 = detail.value?.imageBase64
|
||||
if (!b64) return ''
|
||||
// PNG base64 以 iVBORw0KGgo 开头,JPEG 以 /9j/ 开头
|
||||
const mime = b64.startsWith('/9j/') ? 'image/jpeg' : 'image/png'
|
||||
return `data:${mime};base64,${b64}`
|
||||
})
|
||||
|
||||
// 清空确认
|
||||
const clearOpen = ref(false)
|
||||
|
||||
@@ -226,7 +238,6 @@ onUnmounted(() => {
|
||||
</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">
|
||||
@@ -234,10 +245,40 @@ onUnmounted(() => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto space-y-1.5 pr-1">
|
||||
<!-- 分页(置于列表上方,靠左显示) -->
|
||||
<div v-if="totalPages > 1" class="flex items-center justify-start pb-2 shrink-0">
|
||||
<Pagination
|
||||
v-slot="{ page }"
|
||||
:page="currentPage"
|
||||
:total="store.historyTotal"
|
||||
:items-per-page="PAGE_SIZE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
@update:page="gotoPage"
|
||||
class="justify-start"
|
||||
>
|
||||
<PaginationContent v-slot="{ items }" class="gap-1">
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<PaginationItem
|
||||
v-if="item.type === 'page'"
|
||||
:value="item.value"
|
||||
:is-active="item.value === page"
|
||||
size="icon"
|
||||
class="size-7 text-xs"
|
||||
>
|
||||
{{ item.value }}
|
||||
</PaginationItem>
|
||||
<PaginationEllipsis v-else class="size-7" />
|
||||
</template>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
|
||||
<ScrollArea class="flex-1 min-h-0">
|
||||
<div class="space-y-1.5 pr-2">
|
||||
<div
|
||||
v-if="!historyList.length"
|
||||
class="flex flex-col items-center justify-center h-full text-muted-foreground"
|
||||
class="flex flex-col items-center justify-center text-muted-foreground py-12"
|
||||
>
|
||||
<ClipboardList class="h-12 w-12 mb-3 opacity-40" />
|
||||
<p class="text-sm">暂无历史记录,复制内容后将自动收录</p>
|
||||
@@ -270,18 +311,8 @@ onUnmounted(() => {
|
||||
</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>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 固定 -->
|
||||
@@ -428,11 +459,14 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<template v-else-if="detail">
|
||||
<img
|
||||
v-if="detail.kind === 'image' && detail.imageBase64"
|
||||
:src="`data:image/png;base64,${detail.imageBase64}`"
|
||||
v-if="detail.kind === 'image' && imageSrc"
|
||||
:src="imageSrc"
|
||||
class="max-w-full max-h-[60vh] mx-auto rounded"
|
||||
alt="剪贴板图片"
|
||||
/>
|
||||
<p v-else-if="detail.kind === 'image'" class="text-sm text-muted-foreground text-center py-8">
|
||||
图片预览不可用
|
||||
</p>
|
||||
<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
|
||||
|
||||
@@ -6,9 +6,13 @@ import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { Effect, EffectState } from '@tauri-apps/api/window'
|
||||
import {
|
||||
ClipboardList, Pin, PinOff, Trash2, Search, Image as ImageIcon,
|
||||
FileText, Files, Loader2, ChevronLeft, ChevronRight,
|
||||
FileText, Files, Loader2,
|
||||
} from '@lucide/vue'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Pagination, PaginationContent, PaginationItem, PaginationEllipsis,
|
||||
} from '@/components/ui/pagination'
|
||||
|
||||
// ===== 与 Rust 端对应的数据结构(camelCase) =====
|
||||
type ClipboardKind = 'text' | 'image' | 'files'
|
||||
@@ -123,10 +127,14 @@ function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
selectedIndex.value = Math.min(selectedIndex.value + 1, items.value.length - 1)
|
||||
cancelHoverTimer()
|
||||
previewVisible.value = false
|
||||
scrollSelectedIntoView()
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
selectedIndex.value = Math.max(selectedIndex.value - 1, 0)
|
||||
cancelHoverTimer()
|
||||
previewVisible.value = false
|
||||
scrollSelectedIntoView()
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
@@ -169,6 +177,62 @@ const formatTime = (ms: number) => {
|
||||
|
||||
const hasItems = computed(() => items.value.length > 0)
|
||||
|
||||
// ===== 图片悬停预览(悬停 100ms 后显示缩略图) =====
|
||||
const previewSrc = ref('')
|
||||
const previewVisible = ref(false)
|
||||
let hoverTimer: ReturnType<typeof setTimeout> | null = null
|
||||
// 缓存已加载的图片 id → dataUrl,避免重复请求
|
||||
const imageCache = new Map<number, string>()
|
||||
|
||||
/** 根据 base64 前缀判断 MIME 类型 */
|
||||
function buildImageDataUrl(b64: string): string {
|
||||
const mime = b64.startsWith('/9j/') ? 'image/jpeg' : 'image/png'
|
||||
return `data:${mime};base64,${b64}`
|
||||
}
|
||||
|
||||
async function onItemHover(idx: number, item: ClipboardItem) {
|
||||
selectedIndex.value = idx
|
||||
// 仅图片类型触发预览
|
||||
if (item.kind !== 'image') {
|
||||
cancelHoverTimer()
|
||||
previewVisible.value = false
|
||||
return
|
||||
}
|
||||
// 先取消之前的定时器和预览
|
||||
cancelHoverTimer()
|
||||
// 100ms 后加载并显示(快速响应悬停意图)
|
||||
hoverTimer = setTimeout(async () => {
|
||||
try {
|
||||
let src = imageCache.get(item.id)
|
||||
if (!src) {
|
||||
const detail = await invoke<{ imageBase64: string | null } | null>('clipboard_get_item', { id: item.id })
|
||||
if (detail?.imageBase64) {
|
||||
src = buildImageDataUrl(detail.imageBase64)
|
||||
imageCache.set(item.id, src)
|
||||
}
|
||||
}
|
||||
if (src) {
|
||||
previewSrc.value = src
|
||||
previewVisible.value = true
|
||||
}
|
||||
} catch {
|
||||
/* 忽略加载失败 */
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function cancelHoverTimer() {
|
||||
if (hoverTimer) {
|
||||
clearTimeout(hoverTimer)
|
||||
hoverTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function onItemLeave() {
|
||||
cancelHoverTimer()
|
||||
previewVisible.value = false
|
||||
}
|
||||
|
||||
// ===== 主题应用(与主应用同步) =====
|
||||
/** 从 localStorage 读取主应用的主题设置 */
|
||||
function readMainTheme(): { theme: string; effect: string } {
|
||||
@@ -271,6 +335,10 @@ onMounted(async () => {
|
||||
await applyTheme()
|
||||
searchQuery.value = ''
|
||||
currentPage.value = 1
|
||||
// 重置预览状态,清空缓存避免历史图片占用内存
|
||||
cancelHoverTimer()
|
||||
previewVisible.value = false
|
||||
imageCache.clear()
|
||||
await loadData()
|
||||
await nextTick()
|
||||
searchInputRef.value?.focus()
|
||||
@@ -290,74 +358,102 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelHoverTimer()
|
||||
unlistenFns.forEach((fn) => fn())
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="popup-root flex flex-col h-screen w-screen" @keydown="onKeydown">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="popup-header shrink-0">
|
||||
<div class="search-wrap">
|
||||
<Search class="h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
<!-- 搜索栏(与剪切板主页统一样式) -->
|
||||
<div class="flex items-center gap-2 px-3 py-2 border-b border-border 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
|
||||
ref="searchInputRef"
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索剪贴板历史..."
|
||||
class="search-input"
|
||||
class="pl-8 text-sm"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
<span class="popup-count">{{ total }} 条</span>
|
||||
<span class="text-xs text-muted-foreground whitespace-nowrap">{{ total }} 条</span>
|
||||
</div>
|
||||
|
||||
<!-- 图片悬停预览浮层 -->
|
||||
<Transition name="popup-preview">
|
||||
<div v-if="previewVisible && previewSrc" class="popup-preview">
|
||||
<img :src="previewSrc" alt="预览" />
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ScrollArea class="popup-list flex-1 min-h-0">
|
||||
<div v-if="loading && !hasItems" class="popup-empty">
|
||||
<Loader2 class="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<div v-else-if="!hasItems" class="popup-empty">
|
||||
<ClipboardList class="h-10 w-10 mb-2 opacity-40" />
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ searchQuery ? '无匹配结果' : '暂无历史记录' }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, idx) in items"
|
||||
:key="item.id"
|
||||
class="popup-item"
|
||||
:class="{ 'popup-item-selected': idx === selectedIndex }"
|
||||
@click="selectAndPaste(item)"
|
||||
@mouseenter="selectedIndex = idx"
|
||||
>
|
||||
<component :is="kindIcon(item.kind)" class="popup-item-icon" />
|
||||
<div class="popup-item-body">
|
||||
<p class="popup-item-preview">{{ item.preview }}</p>
|
||||
<div class="popup-item-meta">
|
||||
<span class="popup-item-kind" :class="kindBadgeClass(item.kind)">{{ kindLabel(item.kind) }}</span>
|
||||
<span>{{ formatTime(item.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="space-y-1.5 p-2">
|
||||
<div v-if="loading && !hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
<div class="popup-item-actions">
|
||||
<button class="popup-action-btn" title="固定" @click="togglePin(item, $event)">
|
||||
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button class="popup-action-btn hover:text-destructive" title="删除" @click="deleteItem(item, $event)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<div v-else-if="!hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<ClipboardList class="h-10 w-10 mb-2 opacity-40" />
|
||||
<p class="text-sm">
|
||||
{{ searchQuery ? '无匹配结果' : '暂无历史记录' }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, idx) in items"
|
||||
:key="item.id"
|
||||
class="popup-item group"
|
||||
:class="{ 'popup-item-selected': idx === selectedIndex }"
|
||||
@click="selectAndPaste(item)"
|
||||
@mouseenter="onItemHover(idx, item)"
|
||||
@mouseleave="onItemLeave"
|
||||
>
|
||||
<component :is="kindIcon(item.kind)" class="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<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">
|
||||
<span class="popup-item-kind px-1.5 py-0 text-[10px] border rounded-sm" :class="kindBadgeClass(item.kind)">{{ kindLabel(item.kind) }}</span>
|
||||
<span>{{ formatTime(item.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="popup-item-actions shrink-0">
|
||||
<button class="popup-action-btn h-7 w-7" title="固定" @click="togglePin(item, $event)">
|
||||
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button class="popup-action-btn h-7 w-7 hover:text-destructive" title="删除" @click="deleteItem(item, $event)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="totalPages > 1" class="popup-pagination">
|
||||
<button class="popup-page-btn" :disabled="currentPage <= 1" @click="gotoPage(currentPage - 1)">
|
||||
<ChevronLeft class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<span class="popup-page-info">{{ currentPage }} / {{ totalPages }}</span>
|
||||
<button class="popup-page-btn" :disabled="currentPage >= totalPages" @click="gotoPage(currentPage + 1)">
|
||||
<ChevronRight class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<!-- 分页(与剪切板历史统一样式) -->
|
||||
<div v-if="totalPages > 1" class="flex items-center justify-center gap-1 px-2 py-1 border-t border-border">
|
||||
<Pagination
|
||||
v-slot="{ page }"
|
||||
:page="currentPage"
|
||||
:total="total"
|
||||
:items-per-page="PAGE_SIZE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
@update:page="gotoPage"
|
||||
>
|
||||
<PaginationContent v-slot="{ items: pageItems }" class="gap-1">
|
||||
<template v-for="(item, index) in pageItems" :key="index">
|
||||
<PaginationItem
|
||||
v-if="item.type === 'page'"
|
||||
:value="item.value"
|
||||
:is-active="item.value === page"
|
||||
size="icon"
|
||||
class="size-7 text-xs"
|
||||
>
|
||||
{{ item.value }}
|
||||
</PaginationItem>
|
||||
<PaginationEllipsis v-else class="size-7" />
|
||||
</template>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
@@ -377,49 +473,12 @@ onUnmounted(() => {
|
||||
color: var(--foreground);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--input);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--foreground);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.popup-count {
|
||||
font-size: 11px;
|
||||
color: var(--muted-foreground);
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.popup-list {
|
||||
padding: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* reka-ui ScrollAreaViewport 内部会出现一个 div,需保证高度撑满 */
|
||||
@@ -427,66 +486,26 @@ onUnmounted(() => {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* 条目卡片样式(与主界面 Card 统一) */
|
||||
.popup-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
/* 预留边框空间,避免 hover/选中时布局抖动 */
|
||||
border: 1px solid transparent;
|
||||
/* 统一 hover 和 selected 为同一高亮效果,避免错乱 */
|
||||
transition: background-color 0.1s, border-color 0.1s;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card);
|
||||
transition: box-shadow 0.2s, background-color 0.1s;
|
||||
}
|
||||
|
||||
/* hover 使用 shadow(同主界面 hover:shadow-md),键盘选中保留轻微高亮 */
|
||||
.popup-item:hover {
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* 鼠标悬停和键盘选中统一使用 accent 高亮 + 反色边框增强提醒 */
|
||||
.popup-item:hover,
|
||||
.popup-item-selected {
|
||||
background: var(--accent);
|
||||
border-color: var(--foreground);
|
||||
}
|
||||
|
||||
.popup-item-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: 1px;
|
||||
color: var(--muted-foreground);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.popup-item-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.popup-item-preview {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.popup-item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 3px;
|
||||
font-size: 11px;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.popup-item-kind {
|
||||
background: var(--secondary);
|
||||
color: var(--secondary-foreground);
|
||||
padding: 0 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
/* 类型 badge 配色(与主界面一致) */
|
||||
@@ -522,7 +541,6 @@ onUnmounted(() => {
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 选中项和悬停项都显示操作按钮 */
|
||||
@@ -535,27 +553,20 @@ onUnmounted(() => {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
color: var(--muted-foreground);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--muted-foreground);
|
||||
transition: background-color 0.1s, color 0.1s;
|
||||
}
|
||||
|
||||
.popup-action-btn:hover {
|
||||
background: var(--muted);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.popup-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
/* 空状态(使用 Tailwind 类,无需额外 CSS) */
|
||||
|
||||
/* 底部 */
|
||||
.popup-footer {
|
||||
@@ -568,42 +579,37 @@ onUnmounted(() => {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.popup-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 4px 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.popup-page-btn {
|
||||
background: transparent;
|
||||
/* 图片悬停预览浮层:固定在弹窗右上角,不遮挡列表操作 */
|
||||
.popup-preview {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 10px;
|
||||
z-index: 100;
|
||||
max-width: 180px;
|
||||
max-height: 180px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
padding: 3px;
|
||||
border-radius: 4px;
|
||||
color: var(--foreground);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
|
||||
background: var(--popover, var(--background));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.popup-page-btn:hover:not(:disabled) {
|
||||
background: var(--accent);
|
||||
.popup-preview img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 180px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.popup-page-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
/* 预览浮层淡入淡出 */
|
||||
.popup-preview-enter-active,
|
||||
.popup-preview-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.popup-page-info {
|
||||
font-size: 11px;
|
||||
color: var(--muted-foreground);
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
.popup-preview-enter-from,
|
||||
.popup-preview-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.popup-footer kbd {
|
||||
|
||||
Reference in New Issue
Block a user