快速面板模块
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user