Files
win-cleaner/src/App.vue
Julian Freeman add9227fa1 fix ui
2026-03-03 11:36:15 -04:00

955 lines
32 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 pkg from "../package.json";
// --- 导航状态 ---
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 CleanResult { total_freed: string; success_count: number; fail_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 isCleanDone = ref(false);
const isFullScanning = ref(false);
const scanProgress = ref(0);
const fastScanResult = ref<FastScanResult | null>(null);
const cleanResult = ref<CleanResult | null>(null);
const treeData = ref<FileNode[]>([]);
// --- 弹窗状态 ---
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 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) {
showAlert("扫描失败", "请尝试以管理员身份运行程序。", 'error');
} finally {
clearInterval(interval);
isScanning.value = false;
}
}
async function startFastClean() {
if (isCleaning.value) return;
isCleaning.value = true;
try {
const res = await invoke<CleanResult>("start_fast_clean", { isSimulation: false });
cleanResult.value = res;
isCleanDone.value = true;
fastScanResult.value = null;
} catch (err) {
showAlert("清理失败", String(err), 'error');
} finally { isCleaning.value = 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;
}
}
// --- 深度分析 (TreeSize) 逻辑 ---
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;
} catch (err) {
console.error(err);
} finally { node.isLoading = false; }
}
}
function resetAll() {
isCleanDone.value = false;
fastScanResult.value = null;
cleanResult.value = null;
treeData.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];
}
function splitSize(sizeStr: string | number) {
const str = String(sizeStr);
const parts = str.split(' ');
if (parts.length === 2) {
return { value: parts[0], unit: parts[1] };
}
return { value: str, unit: '' };
}
</script>
<template>
<div class="app-container">
<!-- 侧边栏 -->
<aside class="sidebar">
<div class="sidebar-header">
<h2 class="brand">Windows 清理工具</h2>
</div>
<nav class="sidebar-nav">
<!-- 清理 C 盘组 -->
<div class="nav-group">
<div class="nav-item-header" @click="isCMenuOpen = !isCMenuOpen">
<span class="icon">💾</span>
<span class="label">清理 C </span>
<span class="arrow" :class="{ open: isCMenuOpen }"></span>
</div>
<div class="nav-sub-items" v-show="isCMenuOpen">
<div
class="nav-sub-item"
:class="{ active: activeTab === 'clean-c-fast' }"
@click="activeTab = 'clean-c-fast'"
>
快速模式
</div>
<div
class="nav-sub-item"
:class="{ active: activeTab === 'clean-c-advanced' }"
@click="activeTab = 'clean-c-advanced'"
>
高级模式
</div>
<div
class="nav-sub-item"
:class="{ active: activeTab === 'clean-c-deep' }"
@click="activeTab = 'clean-c-deep'"
>
深度分析
</div>
</div>
</div>
<!-- 其它项 -->
<div
class="nav-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">v{{ pkg.version }}</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">
{{ splitSize(fastScanResult.total_size).value }}
<span class="unit">{{ splitSize(fastScanResult.total_size).unit }}</span>
</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>
<button class="btn-primary main-btn" @click="startFastClean" :disabled="isCleaning">
{{ isCleaning ? '正在清理...' : '立即清理' }}
</button>
</div>
<!-- 清理完成报告 -->
<div class="result-card done-card" v-else-if="isCleanDone && 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(cleanResult.total_freed).value }}
<span class="unit">{{ splitSize(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">{{ 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">{{ cleanResult.fail_count }}</span>
<span class="stat-label">跳过/失败</span>
</div>
</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 v-if="isScanning" class="scanning-placeholder">正在深度扫描文件系统...</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">
<!-- 系统组件 -->
<div class="adv-card" :class="{ expanded: expandedAdvanced === 'dism' }">
<div class="adv-card-main" @click="expandedAdvanced = expandedAdvanced === 'dism' ? null : 'dism'">
<div class="adv-card-info">
<span class="adv-card-icon"></span>
<div class="adv-card-text">
<h3>系统组件清理</h3>
<p>通过 DISM 命令移除不再需要的系统冗余组件</p>
</div>
</div>
<button class="btn-action" @click.stop="runAdvancedTask('dism')" :disabled="advLoading['dism']">
{{ advLoading['dism'] ? '执行中...' : '执行' }}
</button>
</div>
<div class="adv-card-detail" v-show="expandedAdvanced === 'dism'">
<div class="detail-content">
<h4>详细信息</h4>
<p>Windows 在更新后会保留旧版本的组件此操作会调用系统底层的 DISM 工具StartComponentCleanup进行物理移除</p>
<h4 class="warning-title">注意事项</h4>
<ul>
<li>执行后将无法卸载已安装的 Windows 更新</li>
<li>过程可能较慢 1-5 分钟请勿中途关闭程序</li>
<li>需要管理员权限执行</li>
</ul>
</div>
</div>
</div>
<!-- 缩略图 -->
<div class="adv-card" :class="{ expanded: expandedAdvanced === 'thumb' }">
<div class="adv-card-main" @click="expandedAdvanced = expandedAdvanced === 'thumb' ? null : 'thumb'">
<div class="adv-card-info">
<span class="adv-card-icon">🖼</span>
<div class="adv-card-text">
<h3>清理缩略图缓存</h3>
<p>重置文件夹预览缩略图数据库以释放空间</p>
</div>
</div>
<button class="btn-action" @click.stop="runAdvancedTask('thumb')" :disabled="advLoading['thumb']">执行</button>
</div>
<div class="adv-card-detail" v-show="expandedAdvanced === 'thumb'">
<div class="detail-content">
<h4>详细信息</h4>
<p>系统会自动生成图片和视频的缩略图缓存thumbcache_*.db当缓存过大或出现显示错误时建议清理</p>
<h4 class="warning-title">注意事项</h4>
<ul>
<li>清理后再次打开图片文件夹时加载预览会稍慢</li>
<li>部分文件正被资源管理器使用时可能无法彻底删除</li>
</ul>
</div>
</div>
</div>
<!-- 休眠 -->
<div class="adv-card" :class="{ expanded: expandedAdvanced === 'hiber' }">
<div class="adv-card-main" @click="expandedAdvanced = expandedAdvanced === 'hiber' ? null : 'hiber'">
<div class="adv-card-info">
<span class="adv-card-icon">🌙</span>
<div class="adv-card-text">
<h3>关闭休眠文件</h3>
<p>永久删除 hiberfil.sys 文件大小等同于内存</p>
</div>
</div>
<button class="btn-action" @click.stop="runAdvancedTask('hiber')" :disabled="advLoading['hiber']">执行</button>
</div>
<div class="adv-card-detail" v-show="expandedAdvanced === 'hiber'">
<div class="detail-content">
<h4>详细信息</h4>
<p>休眠文件hiberfil.sys占用大量 C 盘空间对于使用 SSD 且不常用休眠功能的用户关闭它可以释放巨额空间</p>
<h4 class="warning-title">注意事项</h4>
<ul>
<li>关闭后将无法使用休眠功能及快速启动技术</li>
<li>需要管理员权限</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<!-- 3. 深度分析页面 -->
<section v-else-if="activeTab === 'clean-c-deep'" class="page-container full-width">
<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 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' }"
>
<div class="col-name" @click="toggleNode(index)">
<span v-if="node.is_dir" class="node-toggle">
{{ node.isLoading ? '' : (node.isOpen ? '' : '') }}
</span>
<span v-else class="node-icon">📄</span>
<span class="node-text">{{ node.name }}</span>
</div>
<div class="col-size">{{ node.size_str }}</div>
<div class="col-graph">
<div class="mini-bar-bg">
<div class="mini-bar-fill" :style="{ width: node.percent + '%' }"></div>
</div>
<span class="percent-text">{{ Math.round(node.percent) }}%</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 4. 其他占位 -->
<section v-else class="placeholder-page">
<div class="empty-state">
<span class="empty-icon">🛠</span>
<h1>功能开发中</h1>
<p>此模块正在逐步完善敬请期待</p>
</div>
</section>
</main>
<!-- 自定义弹窗 -->
<div 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 32px;
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: 20px 24px;
border-top: 1px solid #E9EFF6;
}
.version {
font-size: 11px;
color: #A0AEC0;
font-weight: 600;
letter-spacing: 0.5px;
}
/* --- 内容区 --- */
.content {
flex: 1;
padding: 48px 60px;
overflow-y: auto;
height: 100%;
}
/* 当处于全屏模式(深度分析)时,内容区本身不滚动,让内部树形滚动 */
.content:has(.page-container.full-width) {
overflow-y: hidden;
}
.page-container { max-width: 800px; margin: 0 auto; padding-bottom: 10px; transition: max-width 0.4s ease; }
.page-container.full-width {
max-width: 1400px;
height: calc(100vh - 96px);
display: flex;
flex-direction: column;
}
.page-header { margin-bottom: 40px; text-align: center; flex-shrink: 0; }
.page-header h1 { font-size: 28px; font-weight: 700; margin-bottom: 8px; color: var(--text-main); }
.page-header p { color: var(--text-sec); font-size: 15px; }
/* --- 按钮样式重构 --- */
.btn-primary {
background-color: var(--primary-color);
color: white;
border: none;
padding: 14px 44px;
border-radius: 14px;
font-size: 16px;
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-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: 12px 32px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover { background-color: #F5F5F7; }
/* --- 扫描结果卡片 (重点优化) --- */
.main-action { margin: 40px 0; display: flex; justify-content: center; }
.result-card {
background: white;
border-radius: 28px;
padding: 48px;
width: 100%;
box-shadow: var(--card-shadow);
text-align: center;
border: 1px solid rgba(0,0,0,0.02);
}
.result-header { margin-bottom: 32px; }
.result-icon { font-size: 32px; display: block; margin-bottom: 12px; }
.result-header h2 { font-size: 24px; font-weight: 700; color: var(--text-main); }
.result-stats {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 40px;
}
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
display: flex;
align-items: baseline;
justify-content: center;
font-size: 44px;
font-weight: 800;
color: var(--primary-color);
letter-spacing: -1.5px;
line-height: 1;
margin-bottom: 8px;
white-space: nowrap;
}
.stat-value .unit {
font-size: 16px;
font-weight: 700;
margin-left: 4px;
letter-spacing: 0;
opacity: 0.9;
}
.stat-label { font-size: 14px; color: var(--text-sec); font-weight: 500; }
.stat-divider {
width: 1px;
height: 48px;
background-color: #F2F2F7;
margin: 0 20px;
flex-shrink: 0;
}
.main-btn { width: 220px; }
/* --- 扫描中 UI --- */
.scan-circle-container { width: 240px; height: 240px; 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: 4px solid var(--primary-color);
border-top-color: transparent;
animation: spin 1s linear infinite;
}
.scan-inner {
width: 200px; height: 200px; 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: 20px; font-weight: 700; color: var(--primary-color); }
.scan-percent { font-size: 40px; 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: 54px; margin-bottom: 16px; display: block; }
.clean-summary { font-size: 20px; color: var(--text-main); font-weight: 700; margin-top: 12px; }
/* --- 高级模式卡片 --- */
.adv-card-list { display: flex; flex-direction: column; gap: 20px; }
.adv-card {
background: #fff;
border-radius: 20px;
box-shadow: var(--card-shadow);
border: 1px solid rgba(0,0,0,0.02);
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.adv-card:hover { transform: translateY(-2px); box-shadow: 0 15px 35px rgba(0,0,0,0.06); }
.adv-card-main {
padding: 24px 32px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.adv-card-main:hover { background: #FAFAFA; }
.adv-card-info { display: flex; align-items: center; gap: 24px; }
.adv-card-icon { font-size: 32px; }
.adv-card-text h3 { font-size: 18px; margin-bottom: 4px; font-weight: 700; color: var(--text-main); }
.adv-card-text p { color: var(--text-sec); font-size: 14px; }
.btn-action {
background-color: #F2F2F7;
color: var(--primary-color);
border: none;
padding: 10px 24px;
border-radius: 10px;
font-weight: 700;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
min-width: 90px;
}
.btn-action:hover { background-color: var(--primary-color); color: #fff; transform: scale(1.05); }
.btn-action:disabled { background-color: #E5E5E7; color: #A1A1A1; cursor: not-allowed; transform: none; }
.adv-card-detail {
padding: 0 32px 32px 88px;
border-top: 1px solid #F5F5F7;
background: #FCFCFD;
}
.detail-content { padding-top: 24px; }
.detail-content h4 { font-size: 14px; margin-bottom: 10px; color: var(--text-main); font-weight: 700; }
.detail-content p { font-size: 14px; color: var(--text-sec); line-height: 1.6; margin-bottom: 16px; }
.warning-title { color: #FF9500 !important; margin-top: 20px; }
.detail-content ul { padding-left: 18px; color: var(--text-sec); font-size: 13px; }
.detail-content li { margin-bottom: 8px; line-height: 1.4; }
/* --- 磁盘树样式 --- */
.advanced-actions { display: flex; justify-content: center; margin-bottom: 32px; flex-shrink: 0; }
.tree-table-container {
background: #fff;
border-radius: 24px;
overflow: hidden;
margin-top: 32px;
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); }
.spinner {
width: 44px; height: 44px;
border: 3px solid #F2F2F7;
border-top: 3px solid var(--primary-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 24px;
}
.detail-list {
background: white;
border-radius: 20px;
padding: 32px;
box-shadow: var(--card-shadow);
margin-top: 40px;
border: 1px solid rgba(0,0,0,0.02);
}
.detail-list h3 { font-size: 18px; margin-bottom: 20px; font-weight: 700; color: var(--text-main); }
.detail-item {
display: flex;
justify-content: space-between;
padding: 16px 0;
border-bottom: 1px solid #F5F5F7;
font-size: 14px;
color: #424245;
transition: transform 0.2s ease;
}
.detail-item:hover { transform: translateX(4px); }
.detail-item:last-child { border-bottom: none; }
.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); } }
</style>