Files
win-cleaner/src/App.vue
2026-03-03 17:02:40 -04:00

1454 lines
50 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { openUrl } from "@tauri-apps/plugin-opener";
import pkg from "../package.json";
// --- 导航状态 ---
type Tab = 'clean-c-fast' | 'clean-c-advanced' | 'clean-c-deep' | 'clean-browser-chrome' | 'clean-browser-edge' | 'clean-memory';
const activeTab = ref<Tab>('clean-c-fast');
const isCMenuOpen = ref(true);
const isBrowserMenuOpen = ref(false);
// --- 数据结构 ---
interface ScanItem { name: string; path: string; size: number; count: number; enabled: boolean; }
interface FastScanResult { items: ScanItem[]; total_size: string; total_count: number; }
interface CleanResult { total_freed: string; success_count: number; fail_count: number; }
interface BrowserProfile { name: string; path_name: string; cache_size: number; cache_size_str: string; enabled: boolean; }
interface BrowserScanResult { profiles: BrowserProfile[]; total_size: string; }
interface FileNode {
name: string; path: string; is_dir: boolean; size: number; size_str: string;
percent: number; has_children: boolean;
level: number; isOpen: boolean; isLoading: boolean;
}
// --- 状态管理 ---
const fastState = ref({
isScanning: false,
isCleaning: false,
isDone: false,
progress: 0,
scanResult: null as FastScanResult | null,
cleanResult: null as CleanResult | null,
});
const chromeState = ref({
isScanning: false,
isCleaning: false,
isDone: false,
scanResult: null as BrowserScanResult | null,
cleanResult: null as CleanResult | null,
});
const edgeState = ref({
isScanning: false,
isCleaning: false,
isDone: false,
scanResult: null as BrowserScanResult | null,
cleanResult: null as CleanResult | null,
});
const isFullScanning = ref(false);
const fullScanProgress = ref({ fileCount: 0, currentPath: "" });
const treeData = ref<FileNode[]>([]);
// --- 动态汇总计算 ---
import { computed } from "vue";
const selectedStats = computed(() => {
const s = fastState.value;
if (!s.scanResult) return { sizeStr: "0 B", count: 0, hasSelection: false };
const enabledItems = s.scanResult.items.filter(i => i.enabled);
const totalBytes = enabledItems.reduce((acc, i) => acc + i.size, 0);
const totalCount = enabledItems.reduce((acc, i) => acc + i.count, 0);
return {
sizeStr: formatItemSize(totalBytes),
count: totalCount,
hasSelection: enabledItems.length > 0
};
});
function getBrowserSelectedStats(state: any) {
if (!state.scanResult) return { sizeStr: "0 B", count: 0, hasSelection: false };
const enabledProfiles = state.scanResult.profiles.filter((p: any) => p.enabled);
const totalBytes = enabledProfiles.reduce((acc: number, p: any) => acc + p.cache_size, 0);
return {
sizeStr: formatItemSize(totalBytes),
count: enabledProfiles.length,
hasSelection: enabledProfiles.length > 0
};
}
const chromeSelectedStats = computed(() => getBrowserSelectedStats(chromeState.value));
const edgeSelectedStats = computed(() => getBrowserSelectedStats(edgeState.value));
// --- 弹窗状态 ---
const showModal = ref(false);
const modalTitle = ref("");
const modalMessage = ref("");
const modalType = ref<'info' | 'success' | 'error'>('info');
function showAlert(title: string, message: string, type: 'info' | 'success' | 'error' = 'info') {
modalTitle.value = title;
modalMessage.value = message;
modalType.value = type;
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 advLoading = ref<Record<string, boolean>>({});
// --- 快速模式逻辑 ---
async function startFastScan() {
const s = fastState.value;
s.isScanning = true;
s.isDone = false;
s.progress = 0;
s.scanResult = null;
const interval = setInterval(() => { if (s.progress < 95) s.progress += Math.floor(Math.random() * 5); }, 100);
try {
const result = await invoke<FastScanResult>("start_fast_scan");
s.progress = 100;
s.scanResult = result;
} catch (err) {
showAlert("扫描失败", "请尝试以管理员身份运行程序。", 'error');
} finally {
clearInterval(interval);
s.isScanning = false;
}
}
async function startFastClean() {
const s = fastState.value;
if (s.isCleaning || !s.scanResult) return;
const selectedPaths = s.scanResult.items
.filter(item => item.enabled)
.map(item => item.path);
if (selectedPaths.length === 0) {
showAlert("未选择任何项", "请至少勾选一个需要清理的项目。", 'info');
return;
}
s.isCleaning = true;
try {
const res = await invoke<CleanResult>("start_fast_clean", { selectedPaths });
s.cleanResult = res;
s.isDone = true;
s.scanResult = null;
} catch (err) {
showAlert("清理失败", String(err), 'error');
} finally { s.isCleaning = false; }
}
// --- 高级模式逻辑 ---
async function runAdvancedTask(task: string) {
advLoading.value[task] = true;
try {
let cmd = "";
let title = "";
if (task === 'dism') { cmd = "clean_system_components"; title = "系统组件清理"; }
else if (task === 'thumb') { cmd = "clean_thumbnails"; title = "缩略图清理"; }
else if (task === 'hiber') { cmd = "disable_hibernation"; title = "休眠文件优化"; }
const res = await invoke<string>(cmd);
showAlert(title, res, 'success');
} catch (err) {
showAlert("任务失败", String(err), 'error');
} finally {
advLoading.value[task] = false;
}
}
// --- 浏览器清理逻辑 ---
async function startBrowserScan(browser: 'chrome' | 'edge') {
const s = browser === 'chrome' ? chromeState.value : edgeState.value;
s.isScanning = true;
s.isDone = false;
s.scanResult = null;
s.cleanResult = null;
try {
const res = await invoke<BrowserScanResult>("start_browser_scan", { browser });
s.scanResult = {
...res,
profiles: res.profiles.map(p => ({ ...p, enabled: true }))
};
} catch (err) {
showAlert("扫描失败", String(err), 'error');
} finally {
s.isScanning = false;
}
}
async function startBrowserClean(browser: 'chrome' | 'edge') {
const s = browser === 'chrome' ? chromeState.value : edgeState.value;
if (!s.scanResult || s.isCleaning) return;
const selectedProfiles = s.scanResult.profiles
.filter(p => p.enabled)
.map(p => p.path_name);
if (selectedProfiles.length === 0) {
showAlert("未选择", "请选择至少一个用户资料进行清理。", 'info');
return;
}
s.isCleaning = true;
try {
const res = await invoke<CleanResult>("start_browser_clean", { browser, profiles: selectedProfiles });
s.cleanResult = res;
s.isDone = true;
s.scanResult = null;
} catch (err) {
showAlert("清理失败", String(err), 'error');
} finally {
s.isCleaning = false;
}
}
// --- 深度分析 (TreeSize) 逻辑 ---
async function startFullDiskScan() {
isFullScanning.value = true;
treeData.value = [];
fullScanProgress.value = { fileCount: 0, currentPath: "" };
// 监听进度
const unlisten = await listen<{file_count: number, current_path: string}>("scan-progress", (event) => {
fullScanProgress.value.fileCount = event.payload.file_count;
fullScanProgress.value.currentPath = event.payload.current_path;
});
try {
await invoke("start_full_disk_scan");
const rootChildren = await invoke<FileNode[]>("get_tree_children", { path: "C:\\" });
treeData.value = rootChildren.map(node => ({ ...node, level: 0, isOpen: false, isLoading: false }));
} catch (err) {
alert("扫描失败,请确保以管理员身份运行。");
} finally {
isFullScanning.value = false;
unlisten();
}
}
async function toggleNode(index: number) {
const node = treeData.value[index];
if (!node.is_dir || node.isLoading) return;
if (node.isOpen) {
let removeCount = 0;
for (let i = index + 1; i < treeData.value.length; i++) {
if (treeData.value[i].level > node.level) removeCount++;
else break;
}
treeData.value.splice(index + 1, removeCount);
node.isOpen = false;
} else {
node.isLoading = true;
try {
const children = await invoke<FileNode[]>("get_tree_children", { path: node.path });
const mapped = children.map(c => ({ ...c, level: node.level + 1, isOpen: false, isLoading: false }));
treeData.value.splice(index + 1, 0, ...mapped);
node.isOpen = true;
} catch (err) {
console.error(err);
} finally { node.isLoading = false; }
}
}
function resetPageState() {
if (activeTab.value === 'clean-c-fast') {
fastState.value = { isScanning: false, isCleaning: false, isDone: false, progress: 0, scanResult: null, cleanResult: null };
} else if (activeTab.value === 'clean-browser-chrome') {
chromeState.value = { isScanning: false, isCleaning: false, isDone: false, scanResult: null, cleanResult: null };
} else if (activeTab.value === 'clean-browser-edge') {
edgeState.value = { isScanning: false, isCleaning: false, isDone: false, scanResult: null, cleanResult: null };
}
}
function formatItemSize(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function splitSize(sizeStr: string | number) {
const str = String(sizeStr);
const parts = str.split(' ');
if (parts.length === 2) {
return { value: parts[0], unit: parts[1] };
}
return { value: str, unit: '' };
}
</script>
<template>
<div class="app-container">
<!-- 侧边栏 -->
<aside class="sidebar">
<div class="sidebar-header">
<h2 class="brand">Windows 清理工具</h2>
</div>
<nav class="sidebar-nav">
<!-- 清理 C 盘组 -->
<div class="nav-group">
<div class="nav-item-header" @click="isCMenuOpen = !isCMenuOpen">
<span class="icon">💾</span>
<span class="label">清理 C </span>
<span class="arrow" :class="{ open: isCMenuOpen }"></span>
</div>
<div class="nav-sub-items" v-show="isCMenuOpen">
<div
class="nav-sub-item"
:class="{ active: activeTab === 'clean-c-fast' }"
@click="activeTab = 'clean-c-fast'"
>
快速模式
</div>
<div
class="nav-sub-item"
:class="{ active: activeTab === 'clean-c-advanced' }"
@click="activeTab = 'clean-c-advanced'"
>
高级模式
</div>
<div
class="nav-sub-item"
:class="{ active: activeTab === 'clean-c-deep' }"
@click="activeTab = 'clean-c-deep'"
>
深度分析
</div>
</div>
</div>
<!-- 清理浏览器组 -->
<div class="nav-group">
<div class="nav-item-header" @click="isBrowserMenuOpen = !isBrowserMenuOpen">
<span class="icon">🌐</span>
<span class="label">清理浏览器</span>
<span class="arrow" :class="{ open: isBrowserMenuOpen }"></span>
</div>
<div class="nav-sub-items" v-show="isBrowserMenuOpen">
<div
class="nav-sub-item"
:class="{ active: activeTab === 'clean-browser-chrome' }"
@click="activeTab = 'clean-browser-chrome'"
>
谷歌浏览器
</div>
<div
class="nav-sub-item"
:class="{ active: activeTab === 'clean-browser-edge' }"
@click="activeTab = 'clean-browser-edge'"
>
微软浏览器
</div>
</div>
</div>
<!-- 其它项 -->
<div
class="nav-item"
:class="{ active: activeTab === 'clean-memory' }"
@click="activeTab = 'clean-memory'"
>
<span class="icon">🚀</span>
<span class="label">清理内存</span>
</div>
</nav>
<div class="sidebar-footer">
<span class="version">v{{ pkg.version }}</span>
</div>
</aside>
<!-- 内容区 -->
<main class="content">
<!-- 1. 快速清理页面 -->
<section v-if="activeTab === 'clean-c-fast'" class="page-container">
<div class="page-header">
<div class="header-info">
<h1>快速清理系统盘</h1>
<p>一键释放 C 盘空间不影响系统运行</p>
</div>
</div>
<div class="main-action">
<!-- 扫描前/ -->
<div class="scan-circle-container" v-if="!fastState.scanResult && !fastState.isDone">
<div class="scan-circle" :class="{ scanning: fastState.isScanning }">
<div class="scan-inner" @click="!fastState.isScanning && startFastScan()">
<span v-if="!fastState.isScanning" class="scan-btn-text">开始扫描</span>
<span v-else class="scan-percent">{{ fastState.progress }}%</span>
</div>
</div>
</div>
<!-- 扫描完成 -->
<div class="result-card" v-else-if="fastState.scanResult && !fastState.isDone">
<div class="result-header">
<span class="result-icon">📋</span>
<h2>扫描完成</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(selectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(selectedStats.sizeStr).unit }}</span>
</span>
<span class="stat-label">预计释放</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ selectedStats.count }}</span>
<span class="stat-label">文件数量</span>
</div>
</div>
<button
class="btn-primary main-btn"
@click="startFastClean"
:disabled="fastState.isCleaning || !selectedStats.hasSelection"
>
{{ fastState.isCleaning ? '正在清理...' : '立即清理' }}
</button>
</div>
<!-- 清理完成报告 -->
<div class="result-card done-card" v-else-if="fastState.isDone && fastState.cleanResult">
<div class="result-header">
<span class="result-icon success">🎉</span>
<h2>清理完成</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(fastState.cleanResult.total_freed).value }}
<span class="unit">{{ splitSize(fastState.cleanResult.total_freed).unit }}</span>
</span>
<span class="stat-label">释放空间</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ fastState.cleanResult.success_count }}</span>
<span class="stat-label">成功清理</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value highlight-gray">{{ fastState.cleanResult.fail_count }}</span>
<span class="stat-label">跳过/失败</span>
</div>
</div>
<button class="btn-secondary" @click="resetPageState">返回</button>
</div>
</div>
<div class="detail-list" v-if="(fastState.isScanning || fastState.scanResult) && !fastState.isDone">
<h3>清理项详情</h3>
<div
class="detail-item"
v-for="item in fastState.scanResult?.items || []"
:key="item.path"
@click="item.enabled = !item.enabled"
:class="{ disabled: !item.enabled }"
>
<div class="item-info">
<label class="checkbox-container" @click.stop>
<input type="checkbox" v-model="item.enabled">
<span class="checkmark"></span>
</label>
<span>{{ item.name }}</span>
</div>
<span class="item-size">{{ formatItemSize(item.size) }}</span>
</div>
<div v-if="fastState.isScanning" class="scanning-placeholder">正在深度扫描文件系统...</div>
</div>
</section>
<!-- 2. 高级模式页面 -->
<section v-else-if="activeTab === 'clean-c-advanced'" class="page-container">
<div class="page-header">
<div class="header-info">
<h1>高级清理工具</h1>
<p>执行深层系统优化释放更多磁盘空间</p>
</div>
</div>
<div class="adv-card-list">
<!-- 系统组件 -->
<div class="adv-card" :class="{ expanded: expandedAdvanced === 'dism' }">
<div class="adv-card-main" @click="expandedAdvanced = expandedAdvanced === 'dism' ? null : 'dism'">
<div class="adv-card-info">
<span class="adv-card-icon"></span>
<div class="adv-card-text">
<h3>系统组件清理</h3>
<p>通过 DISM 命令移除不再需要的系统冗余组件</p>
</div>
</div>
<button class="btn-action" @click.stop="runAdvancedTask('dism')" :disabled="advLoading['dism']">
{{ advLoading['dism'] ? '执行中...' : '执行' }}
</button>
</div>
<div class="adv-card-detail" v-show="expandedAdvanced === 'dism'">
<div class="detail-content">
<h4>详细信息</h4>
<p>Windows 在更新后会保留旧版本的组件此操作会调用系统底层的 DISM 工具StartComponentCleanup进行物理移除</p>
<h4 class="warning-title">注意事项</h4>
<ul>
<li>执行后将无法卸载已安装的 Windows 更新</li>
<li>过程可能较慢 1-5 分钟请勿中途关闭程序</li>
<li>需要管理员权限执行</li>
</ul>
</div>
</div>
</div>
<!-- 缩略图 -->
<div class="adv-card" :class="{ expanded: expandedAdvanced === 'thumb' }">
<div class="adv-card-main" @click="expandedAdvanced = expandedAdvanced === 'thumb' ? null : 'thumb'">
<div class="adv-card-info">
<span class="adv-card-icon">🖼</span>
<div class="adv-card-text">
<h3>清理缩略图缓存</h3>
<p>重置文件夹预览缩略图数据库以释放空间</p>
</div>
</div>
<button class="btn-action" @click.stop="runAdvancedTask('thumb')" :disabled="advLoading['thumb']">执行</button>
</div>
<div class="adv-card-detail" v-show="expandedAdvanced === 'thumb'">
<div class="detail-content">
<h4>详细信息</h4>
<p>系统会自动生成图片和视频的缩略图缓存thumbcache_*.db当缓存过大或出现显示错误时建议清理</p>
<h4 class="warning-title">注意事项</h4>
<ul>
<li>清理后再次打开图片文件夹时加载预览会稍慢</li>
<li>部分文件正被资源管理器使用时可能无法彻底删除</li>
</ul>
</div>
</div>
</div>
<!-- 休眠 -->
<div class="adv-card" :class="{ expanded: expandedAdvanced === 'hiber' }">
<div class="adv-card-main" @click="expandedAdvanced = expandedAdvanced === 'hiber' ? null : 'hiber'">
<div class="adv-card-info">
<span class="adv-card-icon">🌙</span>
<div class="adv-card-text">
<h3>关闭休眠文件</h3>
<p>永久删除 hiberfil.sys 文件大小等同于内存</p>
</div>
</div>
<button class="btn-action" @click.stop="runAdvancedTask('hiber')" :disabled="advLoading['hiber']">执行</button>
</div>
<div class="adv-card-detail" v-show="expandedAdvanced === 'hiber'">
<div class="detail-content">
<h4>详细信息</h4>
<p>休眠文件hiberfil.sys占用大量 C 盘空间对于使用 SSD且不常用休眠功能的用户关闭它可以释放巨额空间</p>
<h4 class="warning-title">注意事项</h4>
<ul>
<li>关闭后将无法使用休眠功能及快速启动技术</li>
<li>需要管理员权限</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<!-- 2.5 浏览器清理页面 -->
<section v-else-if="activeTab === 'clean-browser-chrome' || activeTab === 'clean-browser-edge'" class="page-container">
<div class="page-header">
<div class="header-info">
<h1>清理{{ activeTab === 'clean-browser-chrome' ? '谷歌浏览器' : '微软浏览器' }}</h1>
<p>安全清理浏览器缓存临时文件释放磁盘空间</p>
</div>
</div>
<div class="main-action">
<!-- 谷歌浏览器视图 -->
<template v-if="activeTab === 'clean-browser-chrome'">
<!-- 扫描前 -->
<div class="scan-circle-container" v-if="!chromeState.scanResult && !chromeState.isDone">
<div class="scan-circle" :class="{ scanning: chromeState.isScanning }">
<div class="scan-inner" @click="!chromeState.isScanning && startBrowserScan('chrome')">
<span v-if="!chromeState.isScanning" class="scan-btn-text">开始扫描</span>
<span v-else class="spinner"></span>
</div>
</div>
</div>
<!-- 扫描完成 -->
<div class="result-card" v-else-if="chromeState.scanResult && !chromeState.isDone">
<div class="result-header">
<span class="result-icon">🌐</span>
<h2>扫描完成</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(chromeSelectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(chromeSelectedStats.sizeStr).unit }}</span>
</span>
<span class="stat-label">预计释放</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ chromeSelectedStats.count }}</span>
<span class="stat-label">用户资料数量</span>
</div>
</div>
<button class="btn-primary main-btn" @click="startBrowserClean('chrome')" :disabled="chromeState.isCleaning || !chromeSelectedStats.hasSelection">
{{ chromeState.isCleaning ? '正在清理...' : '立即清理' }}
</button>
</div>
<!-- 清理完成 -->
<div class="result-card done-card" v-else-if="chromeState.isDone && chromeState.cleanResult">
<div class="result-header">
<span class="result-icon success">🎉</span>
<h2>清理完成</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(chromeState.cleanResult.total_freed).value }}
<span class="unit">{{ splitSize(chromeState.cleanResult.total_freed).unit }}</span>
</span>
<span class="stat-label">释放空间</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ chromeState.cleanResult.success_count }}</span>
<span class="stat-label">成功清理</span>
</div>
</div>
<button class="btn-secondary" @click="resetPageState">返回</button>
</div>
</template>
<!-- 微软浏览器视图 -->
<template v-else-if="activeTab === 'clean-browser-edge'">
<div class="scan-circle-container" v-if="!edgeState.scanResult && !edgeState.isDone">
<div class="scan-circle" :class="{ scanning: edgeState.isScanning }">
<div class="scan-inner" @click="!edgeState.isScanning && startBrowserScan('edge')">
<span v-if="!edgeState.isScanning" class="scan-btn-text">开始扫描</span>
<span v-else class="spinner"></span>
</div>
</div>
</div>
<div class="result-card" v-else-if="edgeState.scanResult && !edgeState.isDone">
<div class="result-header">
<span class="result-icon">🌐</span>
<h2>扫描完成</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(edgeSelectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(edgeSelectedStats.sizeStr).unit }}</span>
</span>
<span class="stat-label">预计释放</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ edgeSelectedStats.count }}</span>
<span class="stat-label">用户资料数量</span>
</div>
</div>
<button class="btn-primary main-btn" @click="startBrowserClean('edge')" :disabled="edgeState.isCleaning || !edgeSelectedStats.hasSelection">
{{ edgeState.isCleaning ? '正在清理...' : '立即清理' }}
</button>
</div>
<div class="result-card done-card" v-else-if="edgeState.isDone && edgeState.cleanResult">
<div class="result-header">
<span class="result-icon success">🎉</span>
<h2>清理完成</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(edgeState.cleanResult.total_freed).value }}
<span class="unit">{{ splitSize(edgeState.cleanResult.total_freed).unit }}</span>
</span>
<span class="stat-label">释放空间</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ edgeState.cleanResult.success_count }}</span>
<span class="stat-label">成功清理</span>
</div>
</div>
<button class="btn-secondary" @click="resetPageState">返回</button>
</div>
</template>
</div>
<!-- 详情列表 -->
<div class="detail-list" v-if="activeTab === 'clean-browser-chrome' ? (chromeState.isScanning || chromeState.scanResult) && !chromeState.isDone : (edgeState.isScanning || edgeState.scanResult) && !edgeState.isDone">
<h3>用户资料列表</h3>
<div
class="detail-item"
v-for="profile in (activeTab === 'clean-browser-chrome' ? chromeState.scanResult?.profiles : edgeState.scanResult?.profiles) || []"
:key="profile.path_name"
@click="profile.enabled = !profile.enabled"
:class="{ disabled: !profile.enabled }"
>
<div class="item-info">
<label class="checkbox-container" @click.stop>
<input type="checkbox" v-model="profile.enabled">
<span class="checkmark"></span>
</label>
<span>{{ profile.name }}</span>
</div>
<span class="item-size">{{ profile.cache_size_str }}</span>
</div>
<div v-if="activeTab === 'clean-browser-chrome' ? chromeState.isScanning : edgeState.isScanning" class="scanning-placeholder">正在定位并分析浏览器用户资料...</div>
</div>
</section>
<!-- 3. 深度分析页面 -->
<section v-else-if="activeTab === 'clean-c-deep'" class="page-container full-width">
<div class="page-header">
<div class="header-info">
<h1>查找大目录</h1>
<p>层级化查看 C 盘占用锁定空间大户</p>
</div>
<div class="header-actions">
<button class="btn-primary btn-sm" @click="startFullDiskScan" :disabled="isFullScanning">
{{ isFullScanning ? '正在扫描...' : '开始扫描' }}
</button>
</div>
</div>
<div class="tree-table-container shadow-card" v-if="treeData.length > 0 || isFullScanning">
<div v-if="isFullScanning" class="scanning-overlay">
<div class="spinner"></div>
<div class="scanning-status">
<p class="scanning-main-text">正在扫描全盘文件...</p>
<div class="scanning-stats-row">
<span class="stat-badge">已扫描{{ fullScanProgress.fileCount.toLocaleString() }} 个文件</span>
</div>
<p class="scanning-current-path" v-if="fullScanProgress.currentPath">
当前{{ fullScanProgress.currentPath }}
</p>
</div>
</div>
<div v-else class="tree-content-wrapper">
<div class="tree-header">
<span class="col-name">文件/文件夹名称</span>
<span class="col-size">大小</span>
<span class="col-graph">相对于父目录占比</span>
</div>
<div class="tree-body">
<div
v-for="(node, index) in treeData"
:key="node.path"
class="tree-row"
:class="{ 'is-file': !node.is_dir }"
:style="{ paddingLeft: (node.level * 20 + 16) + 'px' }"
@contextmenu="handleContextMenu($event, node)"
>
<div class="col-name" @click="toggleNode(index)">
<span v-if="node.is_dir" class="node-toggle">
{{ node.isLoading ? '' : (node.isOpen ? '' : '') }}
</span>
<span v-else class="node-icon">📄</span>
<span class="node-text">{{ node.name }}</span>
</div>
<div class="col-size">{{ node.size_str }}</div>
<div class="col-graph">
<div class="mini-bar-bg">
<div class="mini-bar-fill" :style="{ width: node.percent + '%' }"></div>
</div>
<span class="percent-text">{{ Math.round(node.percent) }}%</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 4. 其他占位 -->
<section v-else class="placeholder-page">
<div class="empty-state">
<span class="empty-icon">🛠</span>
<h1>功能开发中</h1>
<p>此模块正在逐步完善敬请期待</p>
</div>
</section>
</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-card" :class="modalType">
<div class="modal-header">
<span class="modal-icon">
<template v-if="modalType === 'success'"></template>
<template v-else-if="modalType === 'error'"></template>
<template v-else></template>
</span>
<h3>{{ modalTitle }}</h3>
</div>
<div class="modal-body">
<p>{{ modalMessage }}</p>
</div>
<div class="modal-footer">
<button class="btn-primary" @click="showModal = false">确定</button>
</div>
</div>
</div>
</div>
</template>
<style>
/* --- 全局基础样式修复 --- */
:root {
--primary-color: #007AFF;
--primary-hover: #0063CC;
--bg-light: #FBFBFD;
--sidebar-bg: #FFFFFF;
--text-main: #1D1D1F;
--text-sec: #86868B;
--border-color: #E5E5E7;
--card-shadow: 0 12px 30px rgba(0, 0, 0, 0.04);
--btn-shadow: 0 4px 12px rgba(0, 122, 255, 0.25);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
color: var(--text-main);
background-color: var(--bg-light);
height: 100vh;
overflow: hidden;
-webkit-font-smoothing: antialiased;
}
#app { height: 100%; }
.app-container {
display: flex;
height: 100%;
}
/* --- 侧边栏优化 (清爽浅蓝重构) --- */
.sidebar {
width: 240px;
background-color: #F8FAFD;
border-right: 1px solid #E9EFF6;
display: flex;
flex-direction: column;
padding: 40px 0 20px;
z-index: 10;
}
.sidebar-header { padding: 0 28px 36px; }
.brand { font-size: 20px; font-weight: 700; color: var(--text-main); letter-spacing: -0.3px; }
.sidebar-nav { flex: 1; }
.nav-item, .nav-item-header {
padding: 12px 20px;
margin: 4px 12px;
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
color: #4A5568;
font-size: 14px;
font-weight: 500;
border-radius: 12px;
}
.nav-item:hover, .nav-item-header:hover {
background-color: #EDF2F7;
color: #2D3748;
}
.nav-item.active {
background-color: #EBF4FF;
color: #04448a;
font-weight: 600;
}
.icon { margin-right: 12px; font-size: 18px; width: 24px; text-align: center; }
.arrow {
margin-left: auto;
transition: transform 0.3s;
font-size: 10px;
color: #A0AEC0;
}
.arrow.open { transform: rotate(180deg); color: #4A5568; }
.nav-sub-items { margin-bottom: 8px; }
.nav-sub-item {
padding: 10px 20px 10px 52px;
margin: 2px 12px;
cursor: pointer;
font-size: 13px;
color: #718096;
transition: all 0.2s;
border-radius: 10px;
}
.nav-sub-item:hover {
background-color: #EDF2F7;
color: #2D3748;
}
.nav-sub-item.active {
color: #007AFF;
font-weight: 600;
background-color: #EBF4FF;
}
.sidebar-footer {
padding: 0px 20px;
text-align: center;
}
.version {
font-size: 12px;
color: #CBD5E0;
font-weight: 500;
letter-spacing: 0.2px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
/* --- 内容区 --- */
.content {
flex: 1;
padding: 40px 60px;
overflow-y: auto;
height: 100%;
}
/* 当处于全屏模式(深度分析)时,内容区本身不滚动,让内部树形滚动 */
.content:has(.page-container.full-width) {
overflow-y: hidden;
padding-bottom: 24px; /* 调小全屏模式下的底部留白 */
}
.page-container { max-width: 800px; margin: 0 auto; padding-bottom: 0px; transition: max-width 0.4s ease; }
.page-container.full-width {
max-width: 1400px;
height: calc(100vh - 64px); /* 相应调整高度计算 */
display: flex;
flex-direction: column;
}
.page-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.header-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.page-header h1 { font-size: 22px; font-weight: 700; margin-bottom: 0; color: var(--text-main); line-height: 1.2; }
.page-header p { color: var(--text-sec); font-size: 13px; margin-bottom: 0; line-height: 1.2; }
.header-actions { display: flex; align-items: center; }
/* --- 按钮样式重构 --- */
.btn-primary {
background-color: var(--primary-color);
color: white;
border: none;
padding: 12px 40px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--btn-shadow);
flex-shrink: 0;
}
.btn-sm { padding: 8px 24px; font-size: 14px; border-radius: 10px; }
.btn-primary:hover { background-color: var(--primary-hover); transform: translateY(-1.5px); box-shadow: 0 6px 16px rgba(0, 122, 255, 0.35); }
.btn-primary:active { transform: translateY(0); }
.btn-primary:disabled { background-color: #D1D1D6; box-shadow: none; cursor: not-allowed; transform: none; }
.btn-secondary {
background-color: white;
color: var(--text-main);
border: 1px solid var(--border-color);
padding: 10px 28px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover { background-color: #F5F5F7; }
/* --- 扫描结果卡片 (重点优化) --- */
.main-action { margin: 24px 0; display: flex; justify-content: center; }
.result-card {
background: white;
border-radius: 24px;
padding: 32px 40px;
width: 100%;
box-shadow: var(--card-shadow);
text-align: center;
border: 1px solid rgba(0,0,0,0.02);
}
.result-header { margin-bottom: 24px; }
.result-icon { font-size: 28px; display: block; margin-bottom: 8px; }
.result-header h2 { font-size: 20px; font-weight: 700; color: var(--text-main); }
.result-stats {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 32px;
}
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
display: flex;
align-items: baseline;
justify-content: center;
font-size: 38px;
font-weight: 800;
color: var(--primary-color);
letter-spacing: -1.2px;
line-height: 1;
margin-bottom: 6px;
white-space: nowrap;
}
.stat-value .unit {
font-size: 14px;
font-weight: 700;
margin-left: 3px;
letter-spacing: 0;
opacity: 0.9;
}
.stat-label { font-size: 13px; color: var(--text-sec); font-weight: 500; }
.stat-divider {
width: 1px;
height: 40px;
background-color: #F2F2F7;
margin: 0 16px;
flex-shrink: 0;
}
.main-btn { width: 180px; }
/* --- 扫描中 UI --- */
.scan-circle-container { width: 200px; height: 200px; margin: 0 auto; }
.scan-circle {
width: 100%; height: 100%; border-radius: 50%; border: 2px solid #F2F2F7;
display: flex; align-items: center; justify-content: center; position: relative;
}
.scan-circle.scanning { border-color: transparent; }
.scan-circle.scanning::before {
content: "";
position: absolute;
top: -2px; left: -2px; right: -2px; bottom: -2px;
border-radius: 50%;
border: 3px solid var(--primary-color);
border-top-color: transparent;
animation: spin 1s linear infinite;
}
.scan-inner {
width: 168px; height: 168px; border-radius: 50%; background: white;
display: flex; align-items: center; justify-content: center;
cursor: pointer; box-shadow: 0 10px 25px rgba(0,0,0,0.05); transition: transform 0.2s ease;
}
.scan-inner:hover { transform: scale(1.03); }
.scan-btn-text { font-size: 18px; font-weight: 700; color: var(--primary-color); }
.scan-percent { font-size: 36px; font-weight: 800; color: var(--primary-color); letter-spacing: -1px; }
.stat-value.highlight-gray { color: #8E8E93; }
.done-card { border: 2px solid #E8F5E9; }
.result-icon.success { color: #34C759; font-size: 48px; margin-bottom: 12px; display: block; }
.clean-summary { font-size: 18px; color: var(--text-main); font-weight: 700; margin-top: 10px; }
/* --- 高级模式卡片 --- */
.adv-card-list { display: flex; flex-direction: column; gap: 20px; }
.adv-card {
background: #fff;
border-radius: 20px;
box-shadow: var(--card-shadow);
border: 1px solid rgba(0,0,0,0.02);
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.adv-card:hover { transform: translateY(-2px); box-shadow: 0 15px 35px rgba(0,0,0,0.06); }
.adv-card-main {
padding: 24px 32px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.adv-card-main:hover { background: #FAFAFA; }
.adv-card-info { display: flex; align-items: center; gap: 24px; }
.adv-card-icon { font-size: 32px; }
.adv-card-text h3 { font-size: 18px; margin-bottom: 4px; font-weight: 700; color: var(--text-main); }
.adv-card-text p { color: var(--text-sec); font-size: 14px; }
.btn-action {
background-color: #F2F2F7;
color: var(--primary-color);
border: none;
padding: 10px 24px;
border-radius: 10px;
font-weight: 700;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
min-width: 90px;
}
.btn-action:hover { background-color: var(--primary-color); color: #fff; transform: scale(1.05); }
.btn-action:disabled { background-color: #E5E5E7; color: #A1A1A1; cursor: not-allowed; transform: none; }
.adv-card-detail {
padding: 0 32px 32px 88px;
border-top: 1px solid #F5F5F7;
background: #FCFCFD;
}
.detail-content { padding-top: 24px; }
.detail-content h4 { font-size: 14px; margin-bottom: 10px; color: var(--text-main); font-weight: 700; }
.detail-content p { font-size: 14px; color: var(--text-sec); line-height: 1.6; margin-bottom: 16px; }
.warning-title { color: #FF9500 !important; margin-top: 20px; }
.detail-content ul { padding-left: 18px; color: var(--text-sec); font-size: 13px; }
.detail-content li { margin-bottom: 8px; line-height: 1.4; }
/* --- 磁盘树样式 --- */
/* .advanced-actions { display: flex; justify-content: center; margin-bottom: 32px; flex-shrink: 0; } */
.tree-table-container {
background: #fff;
border-radius: 24px;
overflow: hidden;
margin-top: 8px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
box-shadow: var(--card-shadow);
border: 1px solid rgba(0,0,0,0.02);
}
.tree-content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.tree-header {
display: flex;
background: #F9FAFB;
padding: 16px 24px;
font-size: 12px;
font-weight: 700;
color: var(--text-sec);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.tree-body {
flex: 1;
overflow-y: auto;
}
.tree-row {
display: flex;
align-items: center;
padding: 14px 24px;
border-bottom: 1px solid #F5F5F7;
font-size: 14px;
transition: background 0.15s ease;
}
.tree-row:hover { background: #F9F9FB; }
.tree-row.is-file { color: #424245; }
.col-name {
flex: 2;
display: flex;
align-items: center;
font-weight: 500;
min-width: 0;
}
.node-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.col-size { width: 100px; text-align: right; font-weight: 600; color: var(--text-main); flex-shrink: 0; }
.col-graph { width: 180px; display: flex; align-items: center; gap: 12px; padding-left: 32px; flex-shrink: 0; }
.mini-bar-bg { flex: 1; height: 6px; background: #F0F0F2; border-radius: 3px; overflow: hidden; }
.mini-bar-fill {
height: 100%;
background: linear-gradient(90deg, #007AFF, #5856D6);
border-radius: 3px;
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.percent-text { font-size: 11px; color: var(--text-sec); width: 32px; font-weight: 600; text-align: right; }
.node-toggle { width: 24px; cursor: pointer; color: #C1C1C1; display: inline-block; text-align: center; font-size: 10px; transition: color 0.2s; }
.node-toggle:hover { color: var(--primary-color); }
.node-icon { width: 24px; font-size: 14px; opacity: 0.7; }
/* --- 通用状态 --- */
.scanning-loader, .scanning-overlay { padding: 100px 40px; text-align: center; color: var(--text-sec); }
.scanning-status { margin-top: 16px; }
.scanning-main-text { font-size: 16px; font-weight: 600; color: var(--text-main); margin-bottom: 12px; }
.scanning-stats-row { margin-bottom: 16px; }
.stat-badge { background: #EBF4FF; color: var(--primary-color); padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 700; }
.scanning-current-path { font-size: 12px; color: var(--text-sec); max-width: 500px; margin: 0 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; opacity: 0.8; }
.spinner {
width: 44px; height: 44px;
border: 3px solid #F2F2F7;
border-top: 3px solid var(--primary-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 24px;
}
.detail-list {
background: white;
border-radius: 20px;
padding: 32px;
box-shadow: var(--card-shadow);
margin-top: 40px;
border: 1px solid rgba(0,0,0,0.02);
}
.detail-list h3 { font-size: 18px; margin-bottom: 20px; font-weight: 700; color: var(--text-main); }
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #F5F5F7;
font-size: 14px;
color: #424245;
transition: all 0.2s ease;
cursor: pointer;
}
.detail-item:hover { background-color: #FAFAFB; padding-left: 8px; padding-right: 8px; border-radius: 8px; }
.detail-item.disabled { opacity: 0.5; }
.detail-item:last-child { border-bottom: none; }
.item-info { display: flex; align-items: center; gap: 12px; }
/* --- 自定义复选框 --- */
.checkbox-container {
display: block;
position: relative;
width: 20px;
height: 20px;
cursor: pointer;
user-select: none;
}
.checkbox-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0; width: 0;
}
.checkmark {
position: absolute;
top: 0; left: 0;
height: 20px; width: 20px;
background-color: #F2F2F7;
border-radius: 6px;
transition: all 0.2s;
border: 1px solid #E5E5E7;
}
.checkbox-container:hover input ~ .checkmark { background-color: #E5E5E7; }
.checkbox-container input:checked ~ .checkmark { background-color: var(--primary-color); border-color: var(--primary-color); }
.checkmark:after {
content: "";
position: absolute;
display: none;
left: 6px; top: 1px;
width: 5px; height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-container input:checked ~ .checkmark:after { display: block; }
.item-size { font-weight: 600; color: var(--primary-color); }
.placeholder-page { padding-top: 120px; text-align: center; color: var(--text-sec); }
.empty-icon { font-size: 64px; display: block; margin-bottom: 24px; opacity: 0.5; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(0, 122, 255, 0.3); } 70% { box-shadow: 0 0 0 25px rgba(0, 122, 255, 0); } 100% { box-shadow: 0 0 0 0 rgba(0, 122, 255, 0); } }
/* --- 自定义弹窗样式 --- */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.2s ease;
}
.modal-card {
background: white;
width: 400px;
border-radius: 24px;
padding: 32px;
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
text-align: center;
animation: modalIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.modal-header { margin-bottom: 20px; }
.modal-icon { font-size: 40px; display: block; margin-bottom: 12px; }
.modal-header h3 { font-size: 20px; font-weight: 700; color: var(--text-main); }
.modal-card.success .modal-header h3 { color: #34C759; }
.modal-card.error .modal-header h3 { color: #FF3B30; }
.modal-body { margin-bottom: 32px; }
.modal-body p { color: var(--text-sec); font-size: 15px; line-height: 1.6; }
.modal-footer { display: flex; justify-content: center; }
@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); } }
/* --- 右键菜单样式 --- */
.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>