This repository has been archived on 2026-04-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
win-cleaner/src/App.vue
Julian Freeman 9e06791019 desc optimize
2026-03-17 18:42:12 -04:00

1906 lines
64 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(true);
// --- 数据结构 ---
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;
}
interface MemoryStats {
total: number;
used: number;
free: number;
percent: number;
}
// --- 状态管理 ---
const fastState = ref({
isScanning: false,
isCleaning: false,
isDone: false,
progress: 0,
scanResult: null as FastScanResult | null,
cleanResult: null as CleanResult | null,
});
const memoryState = ref({
stats: null as MemoryStats | null,
isCleaning: false,
cleaningType: null as 'fast' | 'deep' | null,
lastFreed: "",
isDone: false,
});
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;
}
}
// --- 浏览器清理逻辑 ---
function toggleAllProfiles(enabled: boolean) {
const s = activeTab.value === 'clean-browser-chrome' ? chromeState.value : edgeState.value;
if (s.scanResult) {
s.scanResult.profiles.forEach(p => p.enabled = enabled);
}
}
function invertProfiles() {
const s = activeTab.value === 'clean-browser-chrome' ? chromeState.value : edgeState.value;
if (s.scanResult) {
s.scanResult.profiles.forEach(p => p.enabled = !p.enabled);
}
}
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 });
// 对 profiles 进行排序:按 cache_size 从大到小
const sortedProfiles = res.profiles
.map(p => ({ ...p, enabled: true }))
.sort((a, b) => b.cache_size - a.cache_size);
s.scanResult = {
...res,
profiles: sortedProfiles
};
} 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: '' };
}
// --- 内存清理逻辑 ---
async function getMemoryStats() {
try {
const stats = await invoke<MemoryStats>("get_memory_stats");
memoryState.value.stats = stats;
} catch (err) {
console.error("Failed to fetch memory stats", err);
}
}
async function startMemoryClean(deep = false) {
if (memoryState.value.isCleaning) return;
memoryState.value.isCleaning = true;
memoryState.value.cleaningType = deep ? 'deep' : 'fast';
try {
const cmd = deep ? "run_deep_memory_clean" : "run_memory_clean";
const freedBytes = await invoke<number>(cmd);
memoryState.value.lastFreed = formatItemSize(freedBytes);
// 使用弹窗显示结果
showAlert("优化完成", `已为您释放 ${memoryState.value.lastFreed} 内存空间`, 'success');
await getMemoryStats();
} catch (err) {
showAlert("清理失败", String(err), 'error');
} finally {
memoryState.value.isCleaning = false;
memoryState.value.cleaningType = null;
}
}
// 自动刷新内存
import { onMounted, onUnmounted, watch } from "vue";
let memoryInterval: any = null;
onMounted(() => {
getMemoryStats();
memoryInterval = setInterval(() => {
if (activeTab.value === 'clean-memory') {
getMemoryStats();
}
}, 3000);
});
onUnmounted(() => {
if (memoryInterval) clearInterval(memoryInterval);
});
watch(activeTab, (newTab) => {
if (newTab === 'clean-memory') getMemoryStats();
});
</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 svg-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
</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 svg-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M7 7h.01"/><path d="M11 7h.01"/></svg>
</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 svg-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</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>系统组件清理 <small class="detail-hint">(点击查看详情)</small></h3>
<p>通过 DISM 命令移除不再需要的系统冗余组件</p>
</div>
</div>
<div class="adv-card-right">
<span class="expand-icon" :class="{ rotated: expandedAdvanced === 'dism' }">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
</span>
<button class="btn-action" @click.stop="runAdvancedTask('dism')" :disabled="advLoading['dism']">
{{ advLoading['dism'] ? '执行中...' : '执行' }}
</button>
</div>
</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>
</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>清理缩略图缓存 <small class="detail-hint">(点击查看详情)</small></h3>
<p>重置文件夹预览缩略图数据库以释放空间</p>
</div>
</div>
<div class="adv-card-right">
<span class="expand-icon" :class="{ rotated: expandedAdvanced === 'thumb' }">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
</span>
<button class="btn-action" @click.stop="runAdvancedTask('thumb')" :disabled="advLoading['thumb']">执行</button>
</div>
</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>关闭休眠文件 <small class="detail-hint">(点击查看详情)</small></h3>
<p>永久删除 hiberfil.sys 文件大小等同于内存</p>
</div>
</div>
<div class="adv-card-right">
<span class="expand-icon" :class="{ rotated: expandedAdvanced === 'hiber' }">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
</span>
<button class="btn-action" @click.stop="runAdvancedTask('hiber')" :disabled="advLoading['hiber']">执行</button>
</div>
</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 class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value highlight-gray">{{ chromeState.cleanResult.fail_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 class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value highlight-gray">{{ edgeState.cleanResult.fail_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">
<div class="list-header">
<h3>用户资料列表</h3>
<div class="list-actions">
<button class="btn-text" @click="toggleAllProfiles(true)">全选</button>
<button class="btn-text" @click="toggleAllProfiles(false)">取消</button>
<button class="btn-text" @click="invertProfiles()">反选</button>
</div>
</div>
<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">正在扫描 C 盘文件...</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 svg-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
</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-if="activeTab === 'clean-memory'" class="page-container memory-page-spread">
<div class="page-header">
<div class="header-info">
<h1>清理内存</h1>
<p>释放内存占用不影响程序运行但释放内存后重新打开之前的软件会感到略微卡顿</p>
</div>
</div>
<div class="memory-layout-v2">
<!-- 顶部主展示区 -->
<div class="memory-main-card shadow-card">
<div class="gauge-section">
<div class="memory-gauge" :style="{ '--percent': memoryState.stats?.percent || 0 }">
<svg viewBox="0 0 100 100">
<circle class="gauge-bg" cx="50" cy="50" r="45"></circle>
<circle class="gauge-fill" cx="50" cy="50" r="45" :style="{ strokeDashoffset: 283 - (283 * (memoryState.stats?.percent || 0)) / 100 }"></circle>
</svg>
<div class="gauge-content">
<span class="gauge-value">{{ Math.round(memoryState.stats?.percent || 0) }}<small>%</small></span>
<span class="gauge-label">内存占用</span>
</div>
</div>
</div>
<div class="stats-section">
<div class="stat-box-v2">
<span class="label">已用内存</span>
<span class="value">{{ formatItemSize(memoryState.stats?.used || 0) }}</span>
</div>
<div class="stat-divider-h"></div>
<div class="stat-box-v2">
<span class="label">可用内存</span>
<span class="value">{{ formatItemSize(memoryState.stats?.free || 0) }}</span>
</div>
<div class="stat-divider-h"></div>
<div class="stat-box-v2">
<span class="label">内存总量</span>
<span class="value">{{ formatItemSize(memoryState.stats?.total || 0) }}</span>
</div>
</div>
</div>
<!-- 底部操作区 -->
<div class="memory-actions-v2">
<div class="action-card shadow-card" :class="{ cleaning: memoryState.isCleaning }">
<div class="action-info">
<h3>普通加速</h3>
<p>建议在需要开启更多软件但内存占用居高不下时使用</p>
</div>
<button class="btn-primary" @click="startMemoryClean(false)" :disabled="memoryState.isCleaning">
{{ memoryState.cleaningType === 'fast' ? '清理中...' : '立即加速' }}
</button>
</div>
<div class="action-card shadow-card secondary" :class="{ cleaning: memoryState.isCleaning }">
<div class="action-info">
<h3>深度加速</h3>
<p>可以在长时间使用电脑后感觉电脑有点卡顿时执行</p>
</div>
<button class="btn-secondary" @click="startMemoryClean(true)" :disabled="memoryState.isCleaning">
{{ memoryState.cleaningType === 'deep' ? '清理中...' : '深度加速' }}
</button>
</div>
</div>
</div>
</section>
<!-- 5. 其他占位 -->
<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 svg-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
</span>
<span>在文件夹中打开</span>
</div>
<div class="menu-divider"></div>
<div class="menu-item" @click="searchNode('google')">
<span class="menu-icon svg-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
</span>
<span> Google 搜索</span>
</div>
<div class="menu-item" @click="searchNode('perplexity')">
<span class="menu-icon svg-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>
</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%;
scrollbar-gutter: stable;
}
/* 当处于全屏模式(深度分析)时,内容区本身不滚动,让内部树形滚动 */
.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: 15px;
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); display: flex; align-items: center; gap: 8px; }
.adv-card-text p { color: var(--text-sec); font-size: 14px; }
.detail-hint { font-size: 12px; color: var(--text-sec); font-weight: 400; opacity: 0.7; }
.adv-card-right { display: flex; align-items: center; gap: 16px; }
.expand-icon {
width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
color: #C1C1C1;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 50%;
background: #F8F9FA;
}
.expand-icon svg { width: 18px; height: 18px; }
.expand-icon.rotated { transform: rotate(180deg); background: #EBF4FF; color: var(--primary-color); }
.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);
}
.list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.list-header h3 { font-size: 18px; margin-bottom: 0; font-weight: 700; color: var(--text-main); }
.list-actions { display: flex; gap: 8px; }
.btn-text {
background: none;
border: none;
color: var(--primary-color);
font-size: 12px;
font-weight: 600;
cursor: pointer;
padding: 6px 10px;
border-radius: 8px;
transition: all 0.2s;
}
.btn-text:hover { background-color: #F0F7FF; }
.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;
}
/* --- 内存清理特有样式 --- */
.memory-layout-v2 {
display: flex;
flex-direction: column;
gap: 32px;
margin-top: 24px;
}
.memory-main-card {
background: white;
border-radius: 32px;
padding: 48px;
display: flex;
align-items: center;
gap: 60px;
box-shadow: var(--card-shadow);
}
.gauge-section {
flex: 0 0 auto;
}
.memory-gauge {
position: relative;
width: 240px;
height: 240px;
}
.memory-gauge svg {
transform: rotate(-90deg);
width: 100%;
height: 100%;
}
.gauge-bg {
fill: none;
stroke: #F2F2F7;
stroke-width: 8;
}
.gauge-fill {
fill: none;
stroke: var(--primary-color);
stroke-width: 8;
stroke-linecap: round;
stroke-dasharray: 283;
transition: stroke-dashoffset 1s cubic-bezier(0.4, 0, 0.2, 1), stroke 0.3s;
}
.gauge-content {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
text-align: center;
display: flex;
flex-direction: column;
}
.gauge-value {
font-size: 64px;
font-weight: 800;
color: var(--text-main);
line-height: 1;
letter-spacing: -2px;
}
.gauge-value small {
font-size: 24px;
margin-left: 2px;
letter-spacing: 0;
}
.gauge-label {
font-size: 14px;
color: var(--text-sec);
font-weight: 600;
margin-top: 4px;
}
.stats-section {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.stat-box-v2 {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.stat-box-v2 .label {
font-size: 15px;
color: var(--text-sec);
font-weight: 500;
}
.stat-box-v2 .value {
font-size: 20px;
font-weight: 700;
color: var(--text-main);
}
.stat-divider-h {
height: 1px;
background: #F2F2F7;
width: 100%;
}
.memory-actions-v2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.action-card {
background: white;
padding: 32px;
border-radius: 28px;
display: flex;
flex-direction: column;
gap: 24px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.action-card:hover {
transform: translateY(-4px);
box-shadow: 0 16px 40px rgba(0,0,0,0.06);
}
.action-card.secondary {
background-color: #FBFBFD;
border: 1px dashed var(--border-color);
box-shadow: none;
}
.action-info {
flex: 1;
}
.action-info h3 {
font-size: 18px;
font-weight: 700;
margin-bottom: 8px;
color: var(--text-main);
}
.action-info p {
font-size: 13px;
color: var(--text-sec);
line-height: 1.5;
}
.action-card .btn-primary,
.action-card .btn-secondary {
width: 100%;
padding: 14px;
}
.clean-feedback {
margin-top: 40px;
text-align: center;
padding: 20px;
background-color: #E8F5E9;
color: #2E7D32;
border-radius: 20px;
font-weight: 600;
font-size: 15px;
animation: slideUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.freed-amount {
font-size: 20px;
font-weight: 800;
text-decoration: underline;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.action-card.cleaning {
filter: grayscale(1);
opacity: 0.7;
pointer-events: none;
}
/* --- SVG 图标通用样式 --- */
.svg-icon {
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
}
.svg-icon svg {
width: 1.25em;
height: 1.25em;
stroke: currentColor;
stroke-width: 2;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.svg-icon.big svg {
width: 2.5em;
height: 2.5em;
}
.icon.svg-icon {
margin-right: 12px;
color: inherit;
}
.icon.svg-icon svg {
width: 18px;
height: 18px;
}
</style>