support context menu
This commit is contained in:
@@ -155,6 +155,18 @@ pub fn get_children(parent_path: String, state: &DiskState) -> Vec<FileTreeNode>
|
|||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn open_explorer(path: String) -> Result<(), String> {
|
||||||
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
// 使用 /select, 参数可以在打开目录的同时选中目标
|
||||||
|
Command::new("explorer.exe")
|
||||||
|
.arg("/select,")
|
||||||
|
.arg(&path)
|
||||||
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// --- 快速模式配置与逻辑 ---
|
// --- 快速模式配置与逻辑 ---
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ async fn get_tree_children(path: String, state: State<'_, cleaner::DiskState>) -
|
|||||||
Ok(cleaner::get_children(path, &state))
|
Ok(cleaner::get_children(path, &state))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn open_in_explorer(path: String) -> Result<(), String> {
|
||||||
|
cleaner::open_explorer(path).await
|
||||||
|
}
|
||||||
|
|
||||||
// --- 高级清理命令 ---
|
// --- 高级清理命令 ---
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -54,6 +59,7 @@ pub fn run() {
|
|||||||
start_fast_clean,
|
start_fast_clean,
|
||||||
start_full_disk_scan,
|
start_full_disk_scan,
|
||||||
get_tree_children,
|
get_tree_children,
|
||||||
|
open_in_explorer,
|
||||||
clean_system_components,
|
clean_system_components,
|
||||||
clean_thumbnails,
|
clean_thumbnails,
|
||||||
disable_hibernation
|
disable_hibernation
|
||||||
|
|||||||
110
src/App.vue
110
src/App.vue
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
import pkg from "../package.json";
|
import pkg from "../package.json";
|
||||||
|
|
||||||
// --- 导航状态 ---
|
// --- 导航状态 ---
|
||||||
@@ -41,6 +42,55 @@ function showAlert(title: string, message: string, type: 'info' | 'success' | 'e
|
|||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 右键菜单状态 ---
|
||||||
|
const contextMenu = ref({
|
||||||
|
show: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
node: null as FileNode | null
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleContextMenu(e: MouseEvent, node: FileNode) {
|
||||||
|
e.preventDefault();
|
||||||
|
contextMenu.value = {
|
||||||
|
show: true,
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
node: node
|
||||||
|
};
|
||||||
|
// 监听一次性点击以关闭菜单
|
||||||
|
const close = () => {
|
||||||
|
contextMenu.value.show = false;
|
||||||
|
window.removeEventListener('click', close);
|
||||||
|
};
|
||||||
|
window.addEventListener('click', close);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openNodeInExplorer() {
|
||||||
|
if (contextMenu.value.node) {
|
||||||
|
try {
|
||||||
|
await invoke("open_in_explorer", { path: contextMenu.value.node.path });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchNode(type: 'google' | 'perplexity') {
|
||||||
|
if (contextMenu.value.node) {
|
||||||
|
const name = contextMenu.value.node.name;
|
||||||
|
const query = encodeURIComponent(`Windows 文件或目录 ${name} 是做什么用的,我可以删除吗`);
|
||||||
|
const url = type === 'google'
|
||||||
|
? `https://www.google.com/search?q=${query}`
|
||||||
|
: `https://www.perplexity.ai/?q=${query}`;
|
||||||
|
try {
|
||||||
|
await openUrl(url);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 高级模式特有
|
// 高级模式特有
|
||||||
const expandedAdvanced = ref<string | null>(null);
|
const expandedAdvanced = ref<string | null>(null);
|
||||||
const advLoading = ref<Record<string, boolean>>({});
|
const advLoading = ref<Record<string, boolean>>({});
|
||||||
@@ -431,6 +481,7 @@ function splitSize(sizeStr: string | number) {
|
|||||||
class="tree-row"
|
class="tree-row"
|
||||||
:class="{ 'is-file': !node.is_dir }"
|
:class="{ 'is-file': !node.is_dir }"
|
||||||
:style="{ paddingLeft: (node.level * 20 + 16) + 'px' }"
|
:style="{ paddingLeft: (node.level * 20 + 16) + 'px' }"
|
||||||
|
@contextmenu="handleContextMenu($event, node)"
|
||||||
>
|
>
|
||||||
<div class="col-name" @click="toggleNode(index)">
|
<div class="col-name" @click="toggleNode(index)">
|
||||||
<span v-if="node.is_dir" class="node-toggle">
|
<span v-if="node.is_dir" class="node-toggle">
|
||||||
@@ -462,6 +513,28 @@ function splitSize(sizeStr: string | number) {
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- 右键菜单 -->
|
||||||
|
<div
|
||||||
|
v-if="contextMenu.show"
|
||||||
|
class="context-menu shadow-card"
|
||||||
|
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="menu-item" @click="openNodeInExplorer">
|
||||||
|
<span class="menu-icon">📂</span>
|
||||||
|
<span>在文件夹中打开</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
<div class="menu-item" @click="searchNode('google')">
|
||||||
|
<span class="menu-icon">🌐</span>
|
||||||
|
<span>用 Google 搜索</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="searchNode('perplexity')">
|
||||||
|
<span class="menu-icon">🤖</span>
|
||||||
|
<span>询问 Perplexity</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 自定义弹窗 -->
|
<!-- 自定义弹窗 -->
|
||||||
<div class="modal-overlay" v-if="showModal" @click.self="showModal = false">
|
<div class="modal-overlay" v-if="showModal" @click.self="showModal = false">
|
||||||
<div class="modal-card" :class="modalType">
|
<div class="modal-card" :class="modalType">
|
||||||
@@ -952,4 +1025,41 @@ body {
|
|||||||
|
|
||||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||||
@keyframes modalIn { from { opacity: 0; transform: scale(0.9) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
@keyframes modalIn { from { opacity: 0; transform: scale(0.9) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
||||||
|
|
||||||
|
/* --- 右键菜单样式 --- */
|
||||||
|
.context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
min-width: 180px;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
z-index: 2000;
|
||||||
|
border: 1px solid rgba(0,0,0,0.08);
|
||||||
|
animation: fadeIn 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-main);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: #F2F2F7;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon { font-size: 16px; }
|
||||||
|
|
||||||
|
.menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #F5F5F7;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user