快速面板模块

This commit is contained in:
zhongluofeng
2026-08-04 16:00:57 +08:00
parent 126f8896b6
commit c7578a2e6b
7 changed files with 954 additions and 117 deletions
+135 -5
View File
@@ -3,7 +3,7 @@ import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import { getCurrentWindow, Effect, EffectState } from '@tauri-apps/api/window'
import { Search, Command, Loader2, CornerDownLeft, Calculator, Globe, Lock, ChevronRight, Terminal, History } from '@lucide/vue'
import { Search, Command, Loader2, CornerDownLeft, Calculator, Globe, Lock, ChevronRight, Terminal, History, FolderOpen, Ruler, Trash2 } from '@lucide/vue'
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion'
import { aggregateSearch, loadAppIconsForResults, recordHistoryItem, getMoreHistoryItems, getMoreHistoryCount, setFileIndexReady, type QPItem, type QPSubAction } from './providers'
@@ -16,6 +16,8 @@ const loading = ref(false)
// 子动作展开:展开的 item 索引,null 表示未展开
const subActionExpanded = ref<number | null>(null)
const subActionIndex = ref(0)
// 待删除确认项(文件/文件夹删除前弹窗确认)
const pendingDelete = ref<{ path: string; isDir: boolean; name: string } | null>(null)
let unlistenFns: UnlistenFn[] = []
let searchTimer: ReturnType<typeof setTimeout> | null = null
@@ -119,7 +121,12 @@ async function executeItem(item: QPItem) {
await hideWindow()
}
async function executeSubAction(sub: QPSubAction) {
async function executeSubAction(sub: QPSubAction, item?: QPItem) {
// 删除类子动作:先弹窗确认,确认后由 confirmDelete 执行并隐藏窗口
if (sub.id === 'delete' && item?.deleteInfo) {
pendingDelete.value = { ...item.deleteInfo, name: item.title }
return
}
try {
await sub.action()
} catch (e) {
@@ -128,6 +135,23 @@ async function executeSubAction(sub: QPSubAction) {
await hideWindow()
}
// ===== 删除确认弹窗 =====
function cancelDelete() {
pendingDelete.value = null
}
async function confirmDelete() {
const pd = pendingDelete.value
if (!pd) return
pendingDelete.value = null
try {
await invoke('quickpanel_delete_file', { path: pd.path })
} catch (e) {
console.error('[quickpanel] 删除失败:', e)
}
await hideWindow()
}
// 子动作展开/收起
function toggleSubActions(idx: number) {
const item = results.value[idx]
@@ -152,8 +176,21 @@ function currentSubActions(): QPSubAction[] {
// ===== 键盘导航 =====
function onKeydown(e: KeyboardEvent) {
// 删除确认弹窗打开时:Esc 取消,Enter 确认
if (pendingDelete.value) {
if (e.key === 'Escape') {
e.preventDefault()
cancelDelete()
} else if (e.key === 'Enter') {
e.preventDefault()
confirmDelete()
}
return
}
const expanded = subActionExpanded.value !== null
const subs = currentSubActions()
const expandedItem = expanded ? results.value[subActionExpanded.value!] : undefined
if (expanded) {
// 子动作导航模式
@@ -168,7 +205,7 @@ function onKeydown(e: KeyboardEvent) {
} else if (e.key === 'Enter') {
e.preventDefault()
const sub = subs[subActionIndex.value]
if (sub) executeSubAction(sub)
if (sub) executeSubAction(sub, expandedItem)
} else if (e.key === 'Escape') {
e.preventDefault()
collapseSubActions()
@@ -180,7 +217,7 @@ function onKeydown(e: KeyboardEvent) {
e.preventDefault()
const idx = parseInt(e.key, 10) - 1
const sub = subs[idx]
if (sub) executeSubAction(sub)
if (sub) executeSubAction(sub, expandedItem)
}
return
}
@@ -247,6 +284,8 @@ const groupIcon = (group: string) => {
if (group === '系统') return Lock
if (group === '自定义') return Terminal
if (group === '历史') return History
if (group === '快捷') return FolderOpen
if (group === '换算') return Ruler
return Search
}
@@ -531,7 +570,7 @@ onUnmounted(() => {
:key="sub.id"
class="qp-sub-item"
:class="{ 'qp-sub-selected': sIdx === subActionIndex }"
@click="executeSubAction(sub)"
@click="executeSubAction(sub, item)"
@mouseenter="onSubActionHover(sIdx)"
>
<span class="qp-sub-num">{{ sIdx + 1 }}</span>
@@ -550,6 +589,22 @@ onUnmounted(() => {
<span><kbd>Enter</kbd> 执行</span>
<span><kbd>Esc</kbd> {{ subActionExpanded !== null ? '收起' : '关闭' }}</span>
</div>
<!-- 删除确认弹窗 -->
<div v-if="pendingDelete" class="qp-confirm-mask" @click.self="cancelDelete">
<div class="qp-confirm-box" role="alertdialog" aria-modal="true">
<div class="qp-confirm-head">
<Trash2 class="h-4 w-4 shrink-0 text-destructive" />
<p class="qp-confirm-title">确定删除</p>
</div>
<p class="qp-confirm-name truncate" :title="pendingDelete.name">{{ pendingDelete.name }}</p>
<p class="qp-confirm-tip">将移动到回收站{{ pendingDelete.isDir ? '(含所有子内容)' : '' }}删除后可在回收站中还原</p>
<div class="flex items-center justify-end gap-2 mt-4">
<button class="qp-confirm-btn" @click="cancelDelete">取消</button>
<button class="qp-confirm-btn qp-confirm-danger" @click="confirmDelete">删除</button>
</div>
</div>
</div>
</div>
</template>
@@ -757,4 +812,79 @@ onUnmounted(() => {
.qp-results::-webkit-scrollbar-track {
background: transparent;
}
/* ===== 删除确认弹窗 ===== */
.qp-confirm-mask {
position: absolute;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.45);
border-radius: 10px;
backdrop-filter: blur(2px);
}
.qp-confirm-box {
width: 300px;
max-width: 86%;
padding: 16px;
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--card);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
}
.qp-confirm-head {
display: flex;
align-items: center;
gap: 8px;
}
.qp-confirm-title {
font-size: 14px;
font-weight: 600;
color: var(--foreground);
}
.qp-confirm-name {
margin-top: 10px;
font-size: 13px;
color: var(--foreground);
}
.qp-confirm-tip {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
color: var(--muted-foreground);
}
.qp-confirm-btn {
padding: 5px 14px;
font-size: 12px;
font-weight: 500;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--muted);
color: var(--foreground);
cursor: pointer;
transition: background 0.15s ease;
}
.qp-confirm-btn:hover {
background: var(--accent);
}
.qp-confirm-danger {
background: var(--destructive);
border-color: transparent;
color: white;
}
.qp-confirm-danger:hover {
background: var(--destructive);
opacity: 0.9;
}
</style>