clean c advanced

This commit is contained in:
Julian Freeman
2026-03-01 19:38:31 -04:00
parent ebef785f9a
commit a170e2a4bd
3 changed files with 280 additions and 199 deletions

View File

@@ -4,11 +4,13 @@ use std::time::{SystemTime, Duration};
use serde::Serialize; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Mutex; use std::sync::Mutex;
use std::process::Command;
use std::os::windows::process::CommandExt;
// 存储全盘扫描后的结果,以便前端按需查询 // 存储全盘扫描后的结果
pub struct DiskState { pub struct DiskState {
pub dir_sizes: Mutex<HashMap<String, u64>>, pub dir_sizes: Mutex<HashMap<String, u64>>,
pub file_info: Mutex<HashMap<String, (u64, u32)>>, // 路径 -> (文件总大小, 文件数量) pub file_info: Mutex<HashMap<String, (u64, u32)>>,
} }
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
@@ -18,8 +20,8 @@ pub struct FileTreeNode {
pub is_dir: bool, pub is_dir: bool,
pub size: u64, pub size: u64,
pub size_str: String, pub size_str: String,
pub percent: f32, // 占父目录的百分比 pub percent: f32,
pub file_count: u32, // 仅对 "X个文件" 节点有效 pub file_count: u32,
pub has_children: bool, pub has_children: bool,
} }
@@ -33,6 +35,69 @@ pub fn format_size(size: u64) -> String {
else { format!("{} B", size) } else { format!("{} B", size) }
} }
// --- 高级清理功能实现 ---
// 1. 系统组件清理 (DISM)
pub async fn run_dism_cleanup() -> Result<String, String> {
const CREATE_NO_WINDOW: u32 = 0x08000000;
let output = Command::new("dism.exe")
.args(&["/online", "/Cleanup-Image", "/StartComponentCleanup"])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| e.to_string())?;
if output.status.success() {
Ok("系统组件清理完成。".into())
} else {
Err(String::from_utf8_lossy(&output.stderr).to_string())
}
}
// 2. 清理缩略图缓存
pub async fn clean_thumbnails() -> Result<String, String> {
let local_app_data = std::env::var("LOCALAPPDATA").map_err(|_| "无法获取 LocalAppData 路径")?;
let thumb_path = Path::new(&local_app_data).join("Microsoft\\Windows\\Explorer");
let mut count = 0;
if thumb_path.exists() {
if let Ok(entries) = fs::read_dir(thumb_path) {
for entry in entries.filter_map(|e| e.ok()) {
let name = entry.file_name().to_string_lossy().to_lowercase();
if name.starts_with("thumbcache_") && name.ends_with(".db") {
// 缩略图文件通常被 Explorer 占用,这里尝试删除,失败也继续
if fs::remove_file(entry.path()).is_ok() {
count += 1;
}
}
}
}
}
if count > 0 {
Ok(format!("成功清理 {} 个缩略图缓存文件。", count))
} else {
Ok("未发现可清理的缩略图缓存,或文件正被系统占用。".into())
}
}
// 3. 关闭休眠文件
pub async fn disable_hibernation() -> Result<String, String> {
const CREATE_NO_WINDOW: u32 = 0x08000000;
let output = Command::new("powercfg.exe")
.args(&["-h", "off"])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| e.to_string())?;
if output.status.success() {
Ok("休眠模式已关闭hiberfil.sys 已移除。".into())
} else {
Err("执行失败,请确保以管理员身份运行。".into())
}
}
// --- 原有逻辑保持 (磁盘树等) ---
pub async fn run_full_scan(root_path: String, state: &DiskState) { pub async fn run_full_scan(root_path: String, state: &DiskState) {
use jwalk::WalkDir; use jwalk::WalkDir;
let mut dir_sizes = HashMap::new(); let mut dir_sizes = HashMap::new();
@@ -42,41 +107,30 @@ pub async fn run_full_scan(root_path: String, state: &DiskState) {
for entry in WalkDir::new(root).skip_hidden(false).into_iter().filter_map(|e| e.ok()) { for entry in WalkDir::new(root).skip_hidden(false).into_iter().filter_map(|e| e.ok()) {
if entry.file_type.is_file() { if entry.file_type.is_file() {
let size = entry.metadata().map(|m| m.len()).unwrap_or(0); let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
// 统计文件信息(归属于直接父目录)
if let Some(parent) = entry.parent_path().to_str() { if let Some(parent) = entry.parent_path().to_str() {
let info = file_info.entry(parent.to_string()).or_insert((0, 0)); let info = file_info.entry(parent.to_string()).or_insert((0, 0));
info.0 += size; info.0 += size; info.1 += 1;
info.1 += 1;
} }
// 递归向上累加目录大小
let mut current_path = entry.parent_path().to_path_buf(); let mut current_path = entry.parent_path().to_path_buf();
while current_path.starts_with(root) { while current_path.starts_with(root) {
let path_str = current_path.to_string_lossy().to_string(); let path_str = current_path.to_string_lossy().to_string();
*dir_sizes.entry(path_str).or_insert(0) += size; *dir_sizes.entry(path_str).or_insert(0) += size;
if current_path == root { break; } if current_path == root { break; }
if let Some(parent) = current_path.parent() { if let Some(parent) = current_path.parent() { current_path = parent.to_path_buf(); } else { break; }
current_path = parent.to_path_buf();
} else { break; }
} }
} }
} }
let mut state_dirs = state.dir_sizes.lock().unwrap(); let mut state_dirs = state.dir_sizes.lock().unwrap();
let mut state_files = state.file_info.lock().unwrap(); let mut state_files = state.file_info.lock().unwrap();
*state_dirs = dir_sizes; *state_dirs = dir_sizes; *state_files = file_info;
*state_files = file_info;
} }
pub fn get_children(parent_path: String, state: &DiskState) -> Vec<FileTreeNode> { pub fn get_children(parent_path: String, state: &DiskState) -> Vec<FileTreeNode> {
let dir_sizes = state.dir_sizes.lock().unwrap(); let dir_sizes = state.dir_sizes.lock().unwrap();
let file_info = state.file_info.lock().unwrap(); let file_info = state.file_info.lock().unwrap();
let mut results = Vec::new(); let mut results = Vec::new();
let parent_size = *dir_sizes.get(&parent_path).unwrap_or(&1); let parent_size = *dir_sizes.get(&parent_path).unwrap_or(&1);
// 1. 获取子文件夹
if let Ok(entries) = fs::read_dir(Path::new(&parent_path)) { if let Ok(entries) = fs::read_dir(Path::new(&parent_path)) {
for entry in entries.filter_map(|e| e.ok()) { for entry in entries.filter_map(|e| e.ok()) {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
@@ -84,40 +138,29 @@ pub fn get_children(parent_path: String, state: &DiskState) -> Vec<FileTreeNode>
if let Some(&size) = dir_sizes.get(&path_str) { if let Some(&size) = dir_sizes.get(&path_str) {
results.push(FileTreeNode { results.push(FileTreeNode {
name: entry.file_name().to_string_lossy().to_string(), name: entry.file_name().to_string_lossy().to_string(),
path: path_str, path: path_str, is_dir: true, size, size_str: format_size(size),
is_dir: true,
size,
size_str: format_size(size),
percent: (size as f64 / parent_size as f64 * 100.0) as f32, percent: (size as f64 / parent_size as f64 * 100.0) as f32,
file_count: 0, file_count: 0, has_children: true,
has_children: true, // 简化处理,假设都有子项
}); });
} }
} }
} }
} }
// 2. 获取该目录下的合并文件项
if let Some(&(size, count)) = file_info.get(&parent_path) { if let Some(&(size, count)) = file_info.get(&parent_path) {
if count > 0 { if count > 0 {
results.push(FileTreeNode { results.push(FileTreeNode {
name: format!("[{} 个文件]", count), name: format!("[{} 个文件]", count),
path: format!("{}\\__files__", parent_path), path: format!("{}\\__files__", parent_path),
is_dir: false, is_dir: false, size, size_str: format_size(size),
size,
size_str: format_size(size),
percent: (size as f64 / parent_size as f64 * 100.0) as f32, percent: (size as f64 / parent_size as f64 * 100.0) as f32,
file_count: count, file_count: count, has_children: false,
has_children: false,
}); });
} }
} }
results.sort_by(|a, b| b.size.cmp(&a.size)); results.sort_by(|a, b| b.size.cmp(&a.size));
results results
} }
// 快速扫描逻辑 (保持不变)
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
pub struct ScanItem { pub name: String, pub path: String, pub size: u64, pub count: u32 } pub struct ScanItem { pub name: String, pub path: String, pub size: u64, pub count: u32 }
#[derive(Serialize)] #[derive(Serialize)]
@@ -127,19 +170,15 @@ pub async fn run_fast_scan() -> FastScanResult {
let mut items = Vec::new(); let mut items = Vec::new();
let mut total_bytes = 0; let mut total_bytes = 0;
let mut total_count = 0; let mut total_count = 0;
let mut add_item = |name: &str, path: &str, filter: Option<u64>| { let mut add_item = |name: &str, path: &str, filter: Option<u64>| {
let (size, count) = get_dir_stats(Path::new(path), filter); let (size, count) = get_dir_stats(Path::new(path), filter);
items.push(ScanItem { name: name.into(), path: path.into(), size, count }); items.push(ScanItem { name: name.into(), path: path.into(), size, count });
total_bytes += size; total_bytes += size; total_count += count;
total_count += count;
}; };
if let Ok(t) = std::env::var("TEMP") { add_item("用户临时文件", &t, None); } if let Ok(t) = std::env::var("TEMP") { add_item("用户临时文件", &t, None); }
add_item("系统临时文件", "C:\\Windows\\Temp", None); add_item("系统临时文件", "C:\\Windows\\Temp", None);
add_item("Windows 更新残留", "C:\\Windows\\SoftwareDistribution\\Download", Some(10)); add_item("Windows 更新残留", "C:\\Windows\\SoftwareDistribution\\Download", Some(10));
add_item("传递优化缓存", "C:\\Windows\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Windows\\DeliveryOptimization", None); add_item("传递优化缓存", "C:\\Windows\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Windows\\DeliveryOptimization", None);
FastScanResult { items, total_size: format_size(total_bytes), total_count } FastScanResult { items, total_size: format_size(total_bytes), total_count }
} }
@@ -162,7 +201,6 @@ fn get_dir_stats(path: &Path, filter_days: Option<u64>) -> (u64, u32) {
(size, count) (size, count)
} }
pub async fn run_fast_clean(is_simulation: bool) -> Result<String, String> { pub async fn run_fast_clean(_is_simulation: bool) -> Result<String, String> {
// 简化版,复用之前的逻辑 Ok("快速清理任务已成功模拟执行。".into())
Ok("清理完成".into())
} }

View File

@@ -24,6 +24,23 @@ 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 clean_system_components() -> Result<String, String> {
cleaner::run_dism_cleanup().await
}
#[tauri::command]
async fn clean_thumbnails() -> Result<String, String> {
cleaner::clean_thumbnails().await
}
#[tauri::command]
async fn disable_hibernation() -> Result<String, String> {
cleaner::disable_hibernation().await
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
@@ -36,7 +53,10 @@ pub fn run() {
start_fast_scan, start_fast_scan,
start_fast_clean, start_fast_clean,
start_full_disk_scan, start_full_disk_scan,
get_tree_children get_tree_children,
clean_system_components,
clean_thumbnails,
disable_hibernation
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@@ -3,7 +3,7 @@ import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
// --- 导航状态 --- // --- 导航状态 ---
type Tab = 'clean-c-fast' | 'clean-c-advanced' | 'clean-browser' | 'clean-memory'; type Tab = 'clean-c-fast' | 'clean-c-advanced' | 'clean-c-deep' | 'clean-browser' | 'clean-memory';
const activeTab = ref<Tab>('clean-c-fast'); const activeTab = ref<Tab>('clean-c-fast');
const isCMenuOpen = ref(true); const isCMenuOpen = ref(true);
@@ -27,6 +27,10 @@ const fastScanResult = ref<FastScanResult | null>(null);
const treeData = ref<FileNode[]>([]); const treeData = ref<FileNode[]>([]);
const cleanMessage = ref(""); const cleanMessage = ref("");
// 高级模式展开状态
const expandedAdvanced = ref<string | null>(null);
const advLoading = ref<Record<string, boolean>>({});
// --- 快速模式逻辑 --- // --- 快速模式逻辑 ---
async function startFastScan() { async function startFastScan() {
isScanning.value = true; isScanning.value = true;
@@ -57,7 +61,25 @@ async function startFastClean() {
} finally { isCleaning.value = false; } } finally { isCleaning.value = false; }
} }
// --- 高级模式 (TreeSize) 逻辑 --- // --- 高级模式逻辑 ---
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() { async function startFullDiskScan() {
isFullScanning.value = true; isFullScanning.value = true;
treeData.value = []; treeData.value = [];
@@ -112,7 +134,6 @@ function formatItemSize(bytes: number): string {
<template> <template>
<div class="app-container"> <div class="app-container">
<!-- 侧边栏 -->
<aside class="sidebar"> <aside class="sidebar">
<div class="sidebar-header"><h2 class="brand">WinCleaner</h2></div> <div class="sidebar-header"><h2 class="brand">WinCleaner</h2></div>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
@@ -124,6 +145,7 @@ function formatItemSize(bytes: number): string {
<div class="nav-sub-items" v-show="isCMenuOpen"> <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-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-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> </div>
<div class="nav-item" :class="{ active: activeTab === 'clean-browser' }" @click="activeTab = 'clean-browser'"> <div class="nav-item" :class="{ active: activeTab === 'clean-browser' }" @click="activeTab = 'clean-browser'">
@@ -136,27 +158,16 @@ function formatItemSize(bytes: number): string {
<div class="sidebar-footer"><span class="version">v1.0.0</span></div> <div class="sidebar-footer"><span class="version">v1.0.0</span></div>
</aside> </aside>
<!-- 内容区 -->
<main class="content"> <main class="content">
<!-- 1. 快速清理页面 --> <!-- 1. 快速清理 -->
<section v-if="activeTab === 'clean-c-fast'" class="page-container"> <section v-if="activeTab === 'clean-c-fast'" class="page-container">
<div class="page-header"> <div class="page-header"><h1>快速清理系统盘</h1><p>一键释放 C 盘空间不影响系统安全</p></div>
<h1>快速清理系统盘</h1>
<p>一键释放 C 盘空间不影响系统安全</p>
</div>
<div class="main-action"> <div class="main-action">
<!-- 扫描中状态 -->
<div class="scan-circle-container" v-if="!fastScanResult && !isCleanDone"> <div class="scan-circle-container" v-if="!fastScanResult && !isCleanDone">
<div class="scan-circle" :class="{ scanning: isScanning }"> <div class="scan-circle" :class="{ scanning: isScanning }">
<div class="scan-inner" @click="!isScanning && startFastScan()"> <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>
<span v-if="!isScanning" class="scan-btn-text">开始扫描</span>
<span v-else class="scan-percent">{{ scanProgress }}%</span>
</div>
</div> </div>
</div> </div>
<!-- 扫描完成待清理 -->
<div class="result-card" v-else-if="fastScanResult && !isCleanDone"> <div class="result-card" v-else-if="fastScanResult && !isCleanDone">
<div class="result-header"><span class="result-icon">📋</span><h2>扫描完成</h2></div> <div class="result-header"><span class="result-icon">📋</span><h2>扫描完成</h2></div>
<div class="result-stats"> <div class="result-stats">
@@ -168,195 +179,207 @@ function formatItemSize(bytes: number): string {
<label class="switch"><input type="checkbox" v-model="isSimulation"><span class="slider round"></span></label> <label class="switch"><input type="checkbox" v-model="isSimulation"><span class="slider round"></span></label>
<span class="toggle-label">模拟清理 (不实际删除文件)</span> <span class="toggle-label">模拟清理 (不实际删除文件)</span>
</div> </div>
<button class="btn-primary main-btn" @click="startFastClean" :disabled="isCleaning"> <button class="btn-primary main-btn" @click="startFastClean" :disabled="isCleaning">{{ isCleaning ? '正在清理...' : (isSimulation ? '开始模拟清理' : '立即清理') }}</button>
{{ isCleaning ? '正在清理...' : (isSimulation ? '开始模拟清理' : '立即清理') }}
</button>
</div> </div>
<!-- 清理报告 -->
<div class="result-card done-card" v-else-if="isCleanDone"> <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> <div class="result-header"><span class="result-icon success"></span><h2>清理完成</h2><p class="clean-summary">{{ cleanMessage }}</p></div>
<div class="done-info"><p>您的 C 盘现在更加清爽了</p></div>
<button class="btn-secondary" @click="resetAll">返回首页</button> <button class="btn-secondary" @click="resetAll">返回首页</button>
</div> </div>
</div> </div>
<div class="detail-list" v-if="(isScanning || fastScanResult) && !isCleanDone"> <div class="detail-list" v-if="(isScanning || fastScanResult) && !isCleanDone">
<h3>清理项详情</h3> <h3>清理项详情</h3>
<div class="detail-item" v-for="item in fastScanResult?.items || []" :key="item.path"> <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>
<span>{{ item.name }}</span><span class="item-size">{{ formatItemSize(item.size) }}</span>
</div>
<div v-if="isScanning" class="scanning-placeholder">正在深度扫描系统目录...</div>
</div> </div>
</section> </section>
<!-- 2. 高级磁盘树页面 --> <!-- 2. 高级模式 (新增) -->
<section v-else-if="activeTab === 'clean-c-advanced'" class="page-container advanced-page"> <section v-else-if="activeTab === 'clean-c-advanced'" class="page-container">
<div class="page-header"> <div class="page-header"><h1>高级清理工具</h1><p>执行深层系统优化释放更多被占用的磁盘空间</p></div>
<h1>磁盘树深度分析</h1>
<p> TreeSize 一样层级化查看 C 盘占用锁定空间大户</p> <div class="adv-card-list">
</div> <!-- 1: DISM -->
<div class="adv-card" :class="{ expanded: expandedAdvanced === 'dism' }">
<div class="advanced-actions"> <div class="adv-card-main" @click="expandedAdvanced = expandedAdvanced === 'dism' ? null : 'dism'">
<button class="btn-primary" @click="startFullDiskScan" :disabled="isFullScanning"> <div class="adv-card-info">
{{ isFullScanning ? '正在建立全盘索引...' : '开始深度分析' }} <span class="adv-card-icon"></span>
</button> <div class="adv-card-text">
</div> <h3>系统组件清理</h3>
<p>通过 DISM 命令移除不再需要的系统冗余组件</p>
<div class="tree-table-container shadow-card" v-if="treeData.length > 0 || isFullScanning"> </div>
<div v-if="isFullScanning" class="scanning-overlay"> </div>
<div class="spinner"></div> <button class="btn-action" @click.stop="runAdvancedTask('dism')" :disabled="advLoading['dism']">
<p>正在分析数百万个文件请稍候...</p> {{ 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>
<div v-else> <!-- 2: 缩略图 -->
<div class="tree-header"> <div class="adv-card" :class="{ expanded: expandedAdvanced === 'thumb' }">
<span class="col-name">文件/文件夹名称</span> <div class="adv-card-main" @click="expandedAdvanced = expandedAdvanced === 'thumb' ? null : 'thumb'">
<span class="col-size">大小</span> <div class="adv-card-info">
<span class="col-graph">相对于父目录占比</span> <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>
<div class="tree-body"> <div class="adv-card-detail" v-show="expandedAdvanced === 'thumb'">
<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="detail-content">
<div class="col-name" @click="toggleNode(index)"> <h4>详细信息</h4>
<span v-if="node.is_dir" class="node-toggle">{{ node.isLoading ? '' : (node.isOpen ? '' : '') }}</span> <p>系统会自动生成图片和视频的缩略图缓存thumbcache_*.db当缓存过大或出现显示错误时建议清理</p>
<span v-else class="node-icon">📄</span> <h4 class="warning-title">注意事项</h4>
<span class="node-text" :title="node.path">{{ node.name }}</span> <ul>
</div> <li>清理后再次打开图片文件夹时加载预览会稍慢</li>
<div class="col-size">{{ node.size_str }}</div> <li>部分文件正被资源管理器使用时可能无法彻底删除</li>
<div class="col-graph"> </ul>
<div class="mini-bar-bg"><div class="mini-bar-fill" :style="{ width: node.percent + '%' }"></div></div> </div>
<span class="percent-text">{{ Math.round(node.percent) }}%</span> </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>
</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> </div>
</div> </div>
</section> </section>
<!-- 3. 占位页面 --> <!-- 3. 深度分析 (原高级模式) -->
<section v-else class="placeholder-page"> <section v-else-if="activeTab === 'clean-c-deep'" class="page-container advanced-page">
<div class="empty-state"> <div class="page-header"><h1>磁盘树深度分析</h1><p>层级化查看 C 盘占用锁定空间大户</p></div>
<span class="empty-icon">🛠</span> <div class="advanced-actions"><button class="btn-primary" @click="startFullDiskScan" :disabled="isFullScanning">{{ isFullScanning ? '正在建立索引...' : '开始深度分析' }}</button></div>
<h1>功能开发中</h1> <div class="tree-table-container shadow-card" v-if="treeData.length > 0 || isFullScanning">
<p>此模块正在逐步完善敬请期待</p> <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> </div>
</section> </section>
<!-- 4. 其他占位 -->
<section v-else class="placeholder-page"><div class="empty-state"><h1>功能开发中</h1><p>敬请期待</p></div></section>
</main> </main>
</div> </div>
</template> </template>
<style> <style>
/* --- 全局基础样式 --- */ /* --- 全局样式复用 --- */
:root { :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); }
--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; } * { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: var(--text-main); background-color: var(--bg-light); height: 100vh; overflow: hidden; } body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; color: var(--text-main); background-color: var(--bg-light); height: 100vh; overflow: hidden; }
#app { height: 100%; }
.app-container { display: flex; height: 100%; } .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 { width: 260px; background-color: var(--sidebar-bg); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; padding: 24px 0; z-index: 10; }
.sidebar-header { padding: 0 24px 32px; }
.brand { font-size: 22px; font-weight: 800; color: var(--primary-color); letter-spacing: -0.5px; }
.sidebar-nav { flex: 1; } .sidebar-nav { flex: 1; }
.nav-item, .nav-item-header { padding: 12px 24px; display: flex; align-items: center; cursor: pointer; transition: all 0.2s; color: #424245; font-size: 15px; font-weight: 500; } .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:hover, .nav-item-header:hover { background-color: #F5F5F7; color: var(--primary-color); } .nav-item.active { background: #F0F7FF; color: var(--primary-color); font-weight: 600; border-right: 3px solid var(--primary-color); }
.nav-item.active { background-color: #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; }
.icon { margin-right: 12px; font-size: 18px; } .nav-sub-item.active { color: var(--primary-color); font-weight: 600; background: #F0F7FF; }
.arrow { margin-left: auto; transition: transform 0.3s; font-size: 12px; color: #C1C1C1; } .arrow { margin-left: auto; transition: transform 0.3s; font-size: 12px; } .arrow.open { transform: rotate(180deg); }
.arrow.open { transform: rotate(180deg); }
.nav-sub-items { background-color: #FAFAFA; }
.nav-sub-item { padding: 10px 24px 10px 54px; cursor: pointer; font-size: 14px; color: #6E6E73; transition: all 0.2s; }
.nav-sub-item:hover { color: var(--primary-color); }
.nav-sub-item.active { color: var(--primary-color); font-weight: 600; background-color: #F0F7FF; }
.sidebar-footer { padding: 16px 24px; border-top: 1px solid var(--border-color); }
.version { font-size: 12px; color: var(--text-sec); }
/* --- 内容区 --- */
.content { flex: 1; padding: 40px; overflow-y: auto; } .content { flex: 1; padding: 40px; overflow-y: auto; }
.page-container { max-width: 900px; margin: 0 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; }
.page-header { margin-bottom: 32px; }
.page-header h1 { font-size: 32px; font-weight: 800; margin-bottom: 8px; letter-spacing: -0.5px; }
.page-header p { color: var(--text-sec); font-size: 16px; }
/* --- 按钮样式 --- */ /* --- 按钮样式 --- */
.btn-primary { background-color: var(--primary-color); color: white; border: none; padding: 14px 32px; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .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-primary:hover { background-color: var(--primary-hover); transform: translateY(-1px); } .btn-secondary { background: #fff; border: 1px solid var(--border-color); padding: 12px 32px; border-radius: 12px; font-size: 15px; cursor: pointer; }
.btn-primary:active { transform: translateY(0); } .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-primary:disabled { background-color: #A5C7FF; cursor: not-allowed; transform: none; } .btn-action:hover { background: var(--primary-color); color: #fff; }
.btn-secondary { background-color: white; color: var(--text-main); border: 1px solid var(--border-color); padding: 12px 32px; border-radius: 12px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .btn-action:disabled { opacity: 0.5; cursor: wait; }
.btn-secondary:hover { background-color: #F5F5F7; border-color: #D1D1D6; }
/* --- 快速清理特有 UI --- */ /* --- 高级模式卡片 --- */
.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; } .main-action { margin: 60px 0; display: flex; justify-content: center; }
.scan-circle-container { width: 220px; height: 220px; } .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; transition: all 0.5s; } .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); box-shadow: 0 0 40px rgba(0, 122, 255, 0.15); animation: pulse 2s infinite; } .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); transition: transform 0.2s; } .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-inner:hover { transform: scale(1.05); }
.scan-btn-text { font-size: 22px; font-weight: 700; color: var(--primary-color); } .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); } .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-card { background: white; border-radius: 24px; padding: 40px; width: 100%; box-shadow: var(--card-shadow); text-align: center; border: 1px solid rgba(0,0,0,0.03); }
.result-header { margin-bottom: 32px; }
.result-icon { font-size: 54px; display: block; margin-bottom: 16px; }
.result-stats { display: flex; justify-content: center; align-items: center; margin-bottom: 32px; } .result-stats { display: flex; justify-content: center; align-items: center; margin-bottom: 32px; }
.stat-item { flex: 1; } .stat-value { font-size: 36px; font-weight: 800; color: var(--primary-color); display: block; }
.stat-value { display: block; font-size: 36px; font-weight: 800; color: var(--primary-color); } .stat-divider { width: 1px; height: 50px; background: var(--border-color); margin: 0 40px; }
.stat-label { font-size: 14px; color: var(--text-sec); font-weight: 500; }
.stat-divider { width: 1px; height: 50px; background-color: var(--border-color); margin: 0 40px; }
.simulation-toggle { display: flex; align-items: center; justify-content: center; margin-bottom: 24px; gap: 12px; } .simulation-toggle { display: flex; align-items: center; justify-content: center; margin-bottom: 24px; gap: 12px; }
.toggle-label { font-size: 14px; color: var(--text-sec); } .switch { position: relative; width: 44px; height: 24px; }
.switch { position: relative; display: inline-block; width: 44px; height: 24px; }
.switch input { opacity: 0; width: 0; height: 0; } .switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #E5E5E7; transition: .4s; border-radius: 24px; } .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-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: var(--primary-color); } input:checked + .slider { background: var(--primary-color); }
input:checked + .slider:before { transform: translateX(20px); } input:checked + .slider:before { transform: translateX(20px); }
.done-card { border: 2px solid #E8F5E9; } /* --- 磁盘树样式 --- */
.result-icon.success { color: #34C759; } .tree-table-container { background: #fff; border-radius: 16px; overflow: hidden; margin-top: 24px; min-height: 400px; box-shadow: var(--card-shadow); }
.clean-summary { font-size: 20px; color: var(--text-main); font-weight: 700; margin-top: 12px; }
.done-info { margin-bottom: 32px; color: var(--text-sec); font-size: 15px; }
/* --- 磁盘树特有 UI --- */
.tree-table-container { background: #fff; border-radius: 16px; overflow: hidden; margin-top: 24px; position: relative; min-height: 400px; }
.shadow-card { box-shadow: var(--card-shadow); border: 1px solid rgba(0,0,0,0.03); }
.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-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-body { max-height: 600px; overflow-y: auto; } .tree-row { display: flex; align-items: center; padding: 12px 20px; border-bottom: 1px solid #F2F2F7; font-size: 14px; }
.tree-row { display: flex; align-items: center; padding: 12px 20px; border-bottom: 1px solid #F2F2F7; font-size: 14px; transition: background 0.1s; }
.tree-row:hover { background: #F9FAFB; } .tree-row:hover { background: #F9FAFB; }
.tree-row.is-file { color: var(--text-sec); } .col-name { flex: 2; display: flex; align-items: center; overflow: hidden; }
.col-name { flex: 2; display: flex; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .col-size { width: 110px; text-align: right; font-family: monospace; font-weight: 600; }
.col-size { width: 110px; text-align: right; font-family: 'SF Mono', 'Roboto Mono', monospace; font-weight: 600; color: #1D1D1F; }
.col-graph { width: 200px; display: flex; align-items: center; gap: 12px; padding-left: 30px; } .col-graph { width: 200px; display: flex; align-items: center; gap: 12px; padding-left: 30px; }
.node-toggle { width: 24px; cursor: pointer; color: #C1C1C1; font-size: 11px; display: inline-block; text-align: center; }
.node-icon { width: 24px; font-size: 14px; }
.mini-bar-bg { flex: 1; height: 6px; background: #F2F2F7; border-radius: 3px; overflow: hidden; } .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); border-radius: 3px; } .mini-bar-fill { height: 100%; background: linear-gradient(90deg, #007AFF, #5856D6); }
.percent-text { font-size: 11px; color: var(--text-sec); width: 35px; font-weight: 600; } .percent-text { font-size: 11px; width: 35px; }
.node-toggle { width: 24px; cursor: pointer; color: #C1C1C1; }
.scanning-overlay { padding: 100px 40px; text-align: center; color: var(--text-sec); } .detail-list { background: white; border-radius: 16px; padding: 24px; box-shadow: var(--card-shadow); margin-top: 32px; }
.spinner { width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid var(--primary-color); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 20px; } .detail-item { display: flex; justify-content: space-between; padding: 14px 0; border-bottom: 1px solid #F2F2F7; font-size: 14px; }
/* --- 通用列表 --- */
.detail-list { background: white; border-radius: 16px; padding: 24px; box-shadow: var(--card-shadow); margin-top: 32px; border: 1px solid rgba(0,0,0,0.02); }
.detail-list h3 { font-size: 17px; margin-bottom: 16px; font-weight: 700; }
.detail-item { display: flex; justify-content: space-between; padding: 14px 0; border-bottom: 1px solid #F2F2F7; font-size: 14px; color: #424245; }
.item-size { font-weight: 600; color: var(--primary-color); }
.placeholder-page { padding-top: 100px; text-align: center; color: var(--text-sec); }
.empty-icon { font-size: 64px; display: block; margin-bottom: 24px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @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); } } @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); } }