Files
win-cleaner/src/App.vue
Julian Freeman a170e2a4bd clean c advanced
2026-03-01 19:38:31 -04:00

386 lines
21 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";
// --- 导航状态 ---
type Tab = 'clean-c-fast' | 'clean-c-advanced' | 'clean-c-deep' | 'clean-browser' | 'clean-memory';
const activeTab = ref<Tab>('clean-c-fast');
const isCMenuOpen = ref(true);
// --- 数据结构 ---
interface ScanItem { name: string; path: string; size: number; count: number; }
interface FastScanResult { items: ScanItem[]; total_size: string; total_count: number; }
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 isScanning = ref(false);
const isCleaning = ref(false);
const isSimulation = ref(false);
const isCleanDone = ref(false);
const isFullScanning = ref(false);
const scanProgress = ref(0);
const fastScanResult = ref<FastScanResult | null>(null);
const treeData = ref<FileNode[]>([]);
const cleanMessage = ref("");
// 高级模式展开状态
const expandedAdvanced = ref<string | null>(null);
const advLoading = ref<Record<string, boolean>>({});
// --- 快速模式逻辑 ---
async function startFastScan() {
isScanning.value = true;
isCleanDone.value = false;
scanProgress.value = 0;
fastScanResult.value = null;
const interval = setInterval(() => { if (scanProgress.value < 95) scanProgress.value += Math.floor(Math.random() * 5); }, 100);
try {
const result = await invoke<FastScanResult>("start_fast_scan");
scanProgress.value = 100;
fastScanResult.value = result;
} catch (err) {
alert("扫描失败,请尝试以管理员身份运行。");
} finally {
clearInterval(interval);
isScanning.value = false;
}
}
async function startFastClean() {
if (isCleaning.value) return;
isCleaning.value = true;
try {
const msg = await invoke<string>("start_fast_clean", { isSimulation: isSimulation.value });
cleanMessage.value = msg;
isCleanDone.value = true;
fastScanResult.value = null;
} finally { isCleaning.value = false; }
}
// --- 高级模式逻辑 ---
async function runAdvancedTask(task: string) {
advLoading.value[task] = true;
try {
let cmd = "";
if (task === 'dism') cmd = "clean_system_components";
else if (task === 'thumb') cmd = "clean_thumbnails";
else if (task === 'hiber') cmd = "disable_hibernation";
const res = await invoke<string>(cmd);
alert(res);
} catch (err) {
alert("执行失败: " + err);
} finally {
advLoading.value[task] = false;
}
}
// --- 深度分析 (原高级模式) 逻辑 ---
async function startFullDiskScan() {
isFullScanning.value = true;
treeData.value = [];
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;
}
}
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;
} finally { node.isLoading = false; }
}
}
function resetAll() {
isCleanDone.value = false;
fastScanResult.value = null;
treeData.value = [];
cleanMessage.value = "";
}
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];
}
</script>
<template>
<div class="app-container">
<aside class="sidebar">
<div class="sidebar-header"><h2 class="brand">WinCleaner</h2></div>
<nav class="sidebar-nav">
<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-item" :class="{ active: activeTab === 'clean-browser' }" @click="activeTab = 'clean-browser'">
<span class="icon">🌐</span><span class="label">清理浏览器</span>
</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">v1.0.0</span></div>
</aside>
<main class="content">
<!-- 1. 快速清理 -->
<section v-if="activeTab === 'clean-c-fast'" class="page-container">
<div class="page-header"><h1>快速清理系统盘</h1><p>一键释放 C 盘空间不影响系统安全</p></div>
<div class="main-action">
<div class="scan-circle-container" v-if="!fastScanResult && !isCleanDone">
<div class="scan-circle" :class="{ scanning: isScanning }">
<div class="scan-inner" @click="!isScanning && startFastScan()"><span v-if="!isScanning" class="scan-btn-text">开始扫描</span><span v-else class="scan-percent">{{ scanProgress }}%</span></div>
</div>
</div>
<div class="result-card" v-else-if="fastScanResult && !isCleanDone">
<div class="result-header"><span class="result-icon">📋</span><h2>扫描完成</h2></div>
<div class="result-stats">
<div class="stat-item"><span class="stat-value">{{ fastScanResult.total_size }}</span><span class="stat-label">预计释放</span></div>
<div class="stat-divider"></div>
<div class="stat-item"><span class="stat-value">{{ fastScanResult.total_count }}</span><span class="stat-label">文件数量</span></div>
</div>
<div class="simulation-toggle">
<label class="switch"><input type="checkbox" v-model="isSimulation"><span class="slider round"></span></label>
<span class="toggle-label">模拟清理 (不实际删除文件)</span>
</div>
<button class="btn-primary main-btn" @click="startFastClean" :disabled="isCleaning">{{ isCleaning ? '正在清理...' : (isSimulation ? '开始模拟清理' : '立即清理') }}</button>
</div>
<div class="result-card done-card" v-else-if="isCleanDone">
<div class="result-header"><span class="result-icon success"></span><h2>清理完成</h2><p class="clean-summary">{{ cleanMessage }}</p></div>
<button class="btn-secondary" @click="resetAll">返回首页</button>
</div>
</div>
<div class="detail-list" v-if="(isScanning || fastScanResult) && !isCleanDone">
<h3>清理项详情</h3>
<div class="detail-item" v-for="item in fastScanResult?.items || []" :key="item.path"><span>{{ item.name }}</span><span class="item-size">{{ formatItemSize(item.size) }}</span></div>
</div>
</section>
<!-- 2. 高级模式 (新增) -->
<section v-else-if="activeTab === 'clean-c-advanced'" class="page-container">
<div class="page-header"><h1>高级清理工具</h1><p>执行深层系统优化释放更多被占用的磁盘空间</p></div>
<div class="adv-card-list">
<!-- 1: DISM -->
<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>
<!-- 2: 缩略图 -->
<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>
<!-- 3: 休眠文件 -->
<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>
<!-- 3. 深度分析 (原高级模式) -->
<section v-else-if="activeTab === 'clean-c-deep'" class="page-container advanced-page">
<div class="page-header"><h1>磁盘树深度分析</h1><p>层级化查看 C 盘占用锁定空间大户</p></div>
<div class="advanced-actions"><button class="btn-primary" @click="startFullDiskScan" :disabled="isFullScanning">{{ isFullScanning ? '正在建立索引...' : '开始深度分析' }}</button></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><p>正在分析数百万个文件...</p></div>
<div v-else>
<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' }">
<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"><h1>功能开发中</h1><p>敬请期待</p></div></section>
</main>
</div>
</template>
<style>
/* --- 全局样式复用 --- */
:root { --primary-color: #007AFF; --primary-hover: #0063CC; --bg-light: #F9FAFB; --sidebar-bg: #FFFFFF; --text-main: #1D1D1F; --text-sec: #86868B; --border-color: #E5E5E7; --card-shadow: 0 8px 24px rgba(0, 0, 0, 0.05); }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; color: var(--text-main); background-color: var(--bg-light); height: 100vh; overflow: hidden; }
.app-container { display: flex; height: 100%; }
.sidebar { width: 260px; background: var(--sidebar-bg); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; padding: 24px 0; }
.sidebar-header { padding: 0 24px 32px; } .brand { font-size: 22px; font-weight: 800; color: var(--primary-color); }
.sidebar-nav { flex: 1; }
.nav-item, .nav-item-header { padding: 12px 24px; display: flex; align-items: center; cursor: pointer; color: #424245; font-size: 15px; font-weight: 500; }
.nav-item.active { background: #F0F7FF; color: var(--primary-color); font-weight: 600; border-right: 3px solid var(--primary-color); }
.nav-sub-item { padding: 10px 24px 10px 54px; cursor: pointer; font-size: 14px; color: #6E6E73; }
.nav-sub-item.active { color: var(--primary-color); font-weight: 600; background: #F0F7FF; }
.arrow { margin-left: auto; transition: transform 0.3s; font-size: 12px; } .arrow.open { transform: rotate(180deg); }
.content { flex: 1; padding: 40px; overflow-y: auto; }
.page-container { max-width: 900px; margin: 0 auto; } .page-header { margin-bottom: 32px; } .page-header h1 { font-size: 32px; font-weight: 800; margin-bottom: 8px; }
/* --- 按钮样式 --- */
.btn-primary { background: var(--primary-color); color: #fff; border: none; padding: 14px 32px; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; }
.btn-secondary { background: #fff; border: 1px solid var(--border-color); padding: 12px 32px; border-radius: 12px; font-size: 15px; cursor: pointer; }
.btn-action { background: #F2F2F7; color: var(--primary-color); border: none; padding: 8px 20px; border-radius: 8px; font-weight: 700; cursor: pointer; transition: all 0.2s; min-width: 80px; }
.btn-action:hover { background: var(--primary-color); color: #fff; }
.btn-action:disabled { opacity: 0.5; cursor: wait; }
/* --- 高级模式卡片 --- */
.adv-card-list { display: flex; flex-direction: column; gap: 20px; }
.adv-card { background: #fff; border-radius: 16px; box-shadow: var(--card-shadow); border: 1px solid rgba(0,0,0,0.03); overflow: hidden; transition: all 0.3s; }
.adv-card-main { padding: 24px; 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: 20px; }
.adv-card-icon { font-size: 32px; }
.adv-card-text h3 { font-size: 18px; margin-bottom: 4px; }
.adv-card-text p { color: var(--text-sec); font-size: 14px; }
.adv-card-detail { padding: 0 24px 24px 76px; border-top: 1px solid #F2F2F7; background: #FAFAFA; }
.detail-content { padding-top: 20px; }
.detail-content h4 { font-size: 14px; margin-bottom: 8px; color: var(--text-main); }
.detail-content p { font-size: 14px; color: var(--text-sec); line-height: 1.6; margin-bottom: 16px; }
.warning-title { color: #FF9500 !important; margin-top: 16px; }
.detail-content ul { padding-left: 20px; color: var(--text-sec); font-size: 13px; }
.detail-content li { margin-bottom: 6px; }
/* --- 原有 UI 样式恢复 --- */
.main-action { margin: 60px 0; display: flex; justify-content: center; }
.scan-circle-container { width: 220px; height: 220px; }
.scan-circle { width: 100%; height: 100%; border-radius: 50%; border: 4px solid var(--border-color); display: flex; align-items: center; justify-content: center; position: relative; }
.scan-circle.scanning { border-color: var(--primary-color); animation: pulse 2s infinite; }
.scan-inner { width: 190px; height: 190px; border-radius: 50%; background: white; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: var(--card-shadow); }
.scan-btn-text { font-size: 22px; font-weight: 700; color: var(--primary-color); }
.scan-percent { font-size: 36px; font-weight: 800; color: var(--primary-color); }
.result-card { background: white; border-radius: 24px; padding: 40px; width: 100%; box-shadow: var(--card-shadow); text-align: center; }
.result-stats { display: flex; justify-content: center; align-items: center; margin-bottom: 32px; }
.stat-value { font-size: 36px; font-weight: 800; color: var(--primary-color); display: block; }
.stat-divider { width: 1px; height: 50px; background: var(--border-color); margin: 0 40px; }
.simulation-toggle { display: flex; align-items: center; justify-content: center; margin-bottom: 24px; gap: 12px; }
.switch { position: relative; width: 44px; height: 24px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #E5E5E7; transition: .4s; border-radius: 24px; }
.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background: var(--primary-color); }
input:checked + .slider:before { transform: translateX(20px); }
/* --- 磁盘树样式 --- */
.tree-table-container { background: #fff; border-radius: 16px; overflow: hidden; margin-top: 24px; min-height: 400px; box-shadow: var(--card-shadow); }
.tree-header { display: flex; background: #F5F5F7; padding: 14px 20px; font-size: 13px; font-weight: 700; color: var(--text-sec); border-bottom: 1px solid var(--border-color); }
.tree-row { display: flex; align-items: center; padding: 12px 20px; border-bottom: 1px solid #F2F2F7; font-size: 14px; }
.tree-row:hover { background: #F9FAFB; }
.col-name { flex: 2; display: flex; align-items: center; overflow: hidden; }
.col-size { width: 110px; text-align: right; font-family: monospace; font-weight: 600; }
.col-graph { width: 200px; display: flex; align-items: center; gap: 12px; padding-left: 30px; }
.mini-bar-bg { flex: 1; height: 6px; background: #F2F2F7; border-radius: 3px; overflow: hidden; }
.mini-bar-fill { height: 100%; background: linear-gradient(90deg, #007AFF, #5856D6); }
.percent-text { font-size: 11px; width: 35px; }
.node-toggle { width: 24px; cursor: pointer; color: #C1C1C1; }
.detail-list { background: white; border-radius: 16px; padding: 24px; box-shadow: var(--card-shadow); margin-top: 32px; }
.detail-item { display: flex; justify-content: space-between; padding: 14px 0; border-bottom: 1px solid #F2F2F7; font-size: 14px; }
@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); } }
</style>