Compare commits

37 Commits

Author SHA1 Message Date
Julian Freeman
94fb93eed1 remove some dir 2026-04-17 12:52:21 -04:00
Julian Freeman
dab7209a69 upgrade 2026-04-17 12:26:35 -04:00
Julian Freeman
4e40fa9f80 support clean orogress 2026-04-17 12:25:40 -04:00
Julian Freeman
54b8701644 alert before clean recycle.bin 2026-04-17 12:10:24 -04:00
Julian Freeman
8764af1a56 alert before clean browsers 2026-04-17 12:05:33 -04:00
Julian Freeman
ab1da1ff0e add recycle.bin to fast_clean 2026-04-17 11:13:06 -04:00
Julian Freeman
fd2566ca26 refactor backend 2026-04-17 11:09:49 -04:00
Julian Freeman
40215bcbcb refactor frontend style 2026-04-17 11:00:42 -04:00
Julian Freeman
11a8955aca refactor frontend 2026-04-17 10:39:25 -04:00
Julian Freeman
9e06791019 desc optimize 2026-03-17 18:42:12 -04:00
Julian Freeman
40d7b5af91 little optimize 2026-03-13 19:29:10 -04:00
Julian Freeman
26ca5273d2 add visual hints on advanced 2026-03-13 17:07:19 -04:00
Julian Freeman
14b9acafdf change some desc 2026-03-13 16:49:36 -04:00
Julian Freeman
278a574b4f adjust ui 2026-03-03 23:05:17 -04:00
Julian Freeman
351b0b7fa0 change icons 2026-03-03 22:50:53 -04:00
Julian Freeman
e7ac620302 fix ui 2026-03-03 22:20:02 -04:00
Julian Freeman
92adfd7a2e add clean memory 2026-03-03 22:03:36 -04:00
Julian Freeman
0c77149c9e add failed count 2026-03-03 20:51:53 -04:00
Julian Freeman
91936aab19 add selection buttons 2026-03-03 20:01:50 -04:00
Julian Freeman
5cd6210dd2 add more browser paths 2026-03-03 17:46:01 -04:00
Julian Freeman
6a61e36c21 sort clean browsers 2026-03-03 17:29:02 -04:00
Julian Freeman
33a1dfa27b add clean browsers 2026-03-03 17:02:40 -04:00
Julian Freeman
39e2f41aab show scan progress 2026-03-03 16:27:43 -04:00
Julian Freeman
8efbb3ebc4 adjust sidebar ui 2026-03-03 16:12:56 -04:00
Julian Freeman
e7fe50946c adjust ui fast clean 2026-03-03 16:06:25 -04:00
Julian Freeman
f9eb191bb6 adjust ui 2026-03-03 15:58:36 -04:00
Julian Freeman
616483b2a8 dynamic calc 2026-03-03 13:43:10 -04:00
Julian Freeman
4cbf6379ba support fast clean options 2026-03-03 13:35:44 -04:00
Julian Freeman
dbec37cd3d support context menu 2026-03-03 13:19:18 -04:00
Julian Freeman
e7030f0be1 modify file tree 2026-03-03 11:50:54 -04:00
Julian Freeman
add9227fa1 fix ui 2026-03-03 11:36:15 -04:00
Julian Freeman
ba7aa8cc22 change ui 2026-03-03 11:23:26 -04:00
Julian Freeman
39e36fb4e1 change ui color 2026-03-03 11:04:26 -04:00
Julian Freeman
588f7740a1 support uac 2026-03-03 10:51:50 -04:00
Julian Freeman
6ffb80f265 change icon 2026-03-03 10:36:43 -04:00
Julian Freeman
9fe3f606f4 upgrade fast clean 2026-03-03 10:16:00 -04:00
Julian Freeman
b63135a0e0 fix size result 2026-03-03 09:59:28 -04:00
97 changed files with 3839 additions and 1161 deletions

View File

@@ -1,3 +1,3 @@
# Windows 清理工具
用 Gemini CLI 生成。
用 Gemini CLI & Codex 生成。

BIN
app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/app-icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Windows 清理工具</title>
</head>

View File

@@ -1,7 +1,7 @@
{
"name": "win-cleaner",
"private": true,
"version": "0.1.0",
"version": "0.1.1",
"type": "module",
"scripts": {
"dev": "vite",

42
public/app-icon.svg Normal file
View File

@@ -0,0 +1,42 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg_gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00A4EF"/>
<stop offset="100%" stop-color="#0078D4"/>
</linearGradient>
<filter id="soft_glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="5" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<!-- 背景圆角矩形 -->
<rect width="512" height="512" rx="112" fill="url(#bg_gradient)"/>
<!-- 扫帚主体 (倾斜 30度) -->
<g transform="translate(256, 256) rotate(-30) translate(-256, -256)">
<!-- 扫帚手柄 -->
<rect x="236" y="80" width="40" height="200" rx="20" fill="white"/>
<!-- 扫帚头/刷柄连接处 -->
<path d="M160 280H352L362 320H150L160 280Z" fill="#F2F2F2"/>
<!-- 刷毛细节 -->
<path d="M150 320L130 420H382L362 320H150Z" fill="white"/>
<!-- 刷毛间的缝隙感 -->
<line x1="180" y1="330" x2="170" y2="410" stroke="#0078D4" stroke-width="4" stroke-linecap="round" opacity="0.2"/>
<line x1="220" y1="330" x2="215" y2="410" stroke="#0078D4" stroke-width="4" stroke-linecap="round" opacity="0.2"/>
<line x1="260" y1="330" x2="260" y2="410" stroke="#0078D4" stroke-width="4" stroke-linecap="round" opacity="0.2"/>
<line x1="300" y1="330" x2="305" y2="410" stroke="#0078D4" stroke-width="4" stroke-linecap="round" opacity="0.2"/>
<line x1="340" y1="330" x2="350" y2="410" stroke="#0078D4" stroke-width="4" stroke-linecap="round" opacity="0.2"/>
</g>
<!-- 清理后的闪烁火花 -->
<g filter="url(#soft_glow)">
<path d="M400 120L410 150L440 160L410 170L400 200L390 170L360 160L390 150L400 120Z" fill="white"/>
<path d="M440 220L445 235L460 240L445 245L440 260L435 245L420 240L435 235L440 220Z" fill="white" opacity="0.8"/>
</g>
<!-- 扫过的动态弧线 -->
<path d="M100 400C150 450 350 450 420 380" stroke="white" stroke-width="12" stroke-linecap="round" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,6 +0,0 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,6 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/target*/
# Generated by Tauri
# will have schema files for capabilities auto-completion

123
src-tauri/Cargo.lock generated
View File

@@ -2112,6 +2112,15 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "ntapi"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
dependencies = [
"winapi",
]
[[package]]
name = "num-conv"
version = "0.2.0"
@@ -3491,6 +3500,20 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "sysinfo"
version = "0.33.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01"
dependencies = [
"core-foundation-sys",
"libc",
"memchr",
"ntapi",
"rayon",
"windows 0.57.0",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@@ -3538,7 +3561,7 @@ dependencies = [
"tao-macros",
"unicode-segmentation",
"url",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",
@@ -3609,7 +3632,7 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
"windows",
"windows 0.61.3",
]
[[package]]
@@ -3710,7 +3733,7 @@ dependencies = [
"tauri-plugin",
"thiserror 2.0.18",
"url",
"windows",
"windows 0.61.3",
"zbus",
]
@@ -3736,7 +3759,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
]
[[package]]
@@ -3762,7 +3785,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
"wry",
]
@@ -3929,11 +3952,25 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -4527,10 +4564,10 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-implement",
"windows-interface",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
]
[[package]]
@@ -4551,22 +4588,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
dependencies = [
"thiserror 2.0.18",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
]
[[package]]
name = "win-cleaner"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"chrono",
"jwalk",
"serde",
"serde_json",
"sysinfo",
"tauri",
"tauri-build",
"tauri-plugin-opener",
"tokio",
"walkdir",
"windows-sys 0.59.0",
]
[[package]]
@@ -4615,6 +4655,16 @@ dependencies = [
"windows-version",
]
[[package]]
name = "windows"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.57.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.3"
@@ -4637,14 +4687,26 @@ dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement 0.57.0",
"windows-interface 0.57.0",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
@@ -4656,8 +4718,8 @@ version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
@@ -4674,6 +4736,17 @@ dependencies = [
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
@@ -4685,6 +4758,17 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
@@ -4718,6 +4802,15 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -5155,7 +5248,7 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",

View File

@@ -1,6 +1,6 @@
[package]
name = "win-cleaner"
version = "0.1.0"
version = "0.1.1"
description = "A Windows Cleaner"
authors = ["Julian"]
edition = "2021"
@@ -25,4 +25,14 @@ serde_json = "1"
chrono = "0.4.44"
walkdir = "2.5.0"
jwalk = "0.8.1"
sysinfo = "0.33.1"
tokio = { version = "1", features = ["full"] }
windows-sys = { version = "0.59", features = [
"Win32_System_Memory",
"Win32_Foundation",
"Win32_System_ProcessStatus",
"Win32_System_Threading",
"Win32_Security",
"Win32_System_SystemInformation"
] }

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="WinCleaner"
type="win32"
/>
<description>Windows Cleaner</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 & 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
</application>
</compatibility>
<!-- 启用长路径支持 -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>

View File

@@ -1,3 +1,8 @@
fn main() {
tauri_build::build()
let mut windows = tauri_build::WindowsAttributes::new();
windows = windows.app_manifest(include_str!("admin.exe.manifest"));
tauri_build::try_build(
tauri_build::Attributes::new().windows_attributes(windows)
).expect("failed to run build script");
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,61 @@
use std::fs;
use std::os::windows::process::CommandExt;
use std::path::Path;
use std::process::Command;
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())
}
}
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") {
if fs::remove_file(entry.path()).is_ok() {
count += 1;
}
}
}
}
}
if count > 0 {
Ok(format!("成功清理 {} 个缩略图缓存文件。", count))
} else {
Ok("未发现可清理的缩略图缓存,或文件正被系统占用。".into())
}
}
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())
}
}

View File

@@ -0,0 +1,128 @@
use std::fs;
use std::path::{Path, PathBuf};
use crate::backend::fast_clean::clean_directory_contents;
use crate::backend::models::{
BrowserProfile, BrowserScanResult, BrowserType, CleanResult, ProjectCleanProgress,
};
use crate::backend::utils::{format_size, get_dir_size_simple};
use tauri::Emitter;
const BROWSER_CACHE_DIRS: &[&str] = &[
"Cache",
"Code Cache",
"GPUCache",
"Media Cache",
"Service Worker/CacheStorage",
// "Service Worker/ScriptCache",
// "GrShaderCache",
// "DawnCache",
// "File System",
// "blob_storage",
];
impl BrowserType {
fn get_user_data_path(&self) -> Result<PathBuf, String> {
let local_app_data = std::env::var("LOCALAPPDATA").map_err(|_| "无法获取 LocalAppData 路径")?;
let base = Path::new(&local_app_data);
match self {
BrowserType::Chrome => Ok(base.join("Google\\Chrome\\User Data")),
BrowserType::Edge => Ok(base.join("Microsoft\\Edge\\User Data")),
}
}
}
pub async fn run_browser_scan(browser: BrowserType) -> Result<BrowserScanResult, String> {
let user_data_path = browser.get_user_data_path()?;
let local_state_path = user_data_path.join("Local State");
let mut profiles = Vec::new();
let mut total_bytes = 0;
if local_state_path.exists() {
let content = fs::read_to_string(local_state_path).map_err(|e| e.to_string())?;
let value: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
if let Some(info_cache) = value
.get("profile")
.and_then(|profile| profile.get("info_cache"))
.and_then(|info| info.as_object())
{
for (dir_name, info) in info_cache {
let profile_display_name = info.get("name").and_then(|name| name.as_str()).unwrap_or(dir_name);
let profile_path = user_data_path.join(dir_name);
if profile_path.exists() {
let mut size = 0;
for sub_dir in BROWSER_CACHE_DIRS {
let target = profile_path.join(sub_dir);
if target.exists() {
size += get_dir_size_simple(&target);
}
}
total_bytes += size;
profiles.push(BrowserProfile {
name: profile_display_name.to_string(),
path_name: dir_name.clone(),
cache_size: size,
cache_size_str: format_size(size),
});
}
}
}
}
Ok(BrowserScanResult {
profiles,
total_size: format_size(total_bytes),
})
}
pub async fn run_browser_clean(
browser: BrowserType,
profile_paths: Vec<String>,
app_handle: tauri::AppHandle,
) -> Result<CleanResult, String> {
let user_data_path = browser.get_user_data_path()?;
let mut total_freed = 0;
let mut success_count = 0;
let mut fail_count = 0;
let mut approx_completed_bytes = 0;
let total_items = profile_paths.len() as u32;
for (index, profile_dir) in profile_paths.into_iter().enumerate() {
let profile_path = user_data_path.join(&profile_dir);
let mut profile_estimated_size = 0;
if profile_path.exists() {
for sub_dir in BROWSER_CACHE_DIRS {
let target = profile_path.join(sub_dir);
if target.exists() {
profile_estimated_size += get_dir_size_simple(&target);
let (freed, success, fail) = clean_directory_contents(&target, None);
total_freed += freed;
success_count += success;
fail_count += fail;
}
}
}
approx_completed_bytes += profile_estimated_size;
let _ = app_handle.emit(
"browser-clean-progress",
ProjectCleanProgress {
completed_items: (index + 1) as u32,
total_items,
current_item: profile_dir,
approx_completed_bytes,
},
);
}
Ok(CleanResult {
total_freed: format_size(total_freed),
success_count,
fail_count,
})
}

View File

@@ -0,0 +1,103 @@
use std::collections::HashMap;
use std::fs;
use std::os::windows::process::CommandExt;
use std::path::Path;
use std::process::Command;
use tauri::Emitter;
use crate::backend::models::{FileTreeNode, ScanProgress};
use crate::backend::state::DiskState;
use crate::backend::utils::format_size;
pub async fn run_full_scan(root_path: String, state: &DiskState, app_handle: tauri::AppHandle) {
use jwalk::WalkDir;
let mut dir_sizes = HashMap::new();
let root = Path::new(&root_path);
let mut file_count = 0;
for entry in WalkDir::new(root).skip_hidden(false).into_iter().filter_map(|e| e.ok()) {
if entry.file_type.is_file() {
file_count += 1;
if file_count % 2000 == 0 {
let _ = app_handle.emit(
"scan-progress",
ScanProgress {
file_count,
current_path: entry.parent_path().to_string_lossy().to_string(),
},
);
}
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
let mut current_path = entry.parent_path().to_path_buf();
while current_path.starts_with(root) {
let path_str = current_path.to_string_lossy().to_string();
*dir_sizes.entry(path_str).or_insert(0) += size;
if current_path == root {
break;
}
if let Some(parent) = current_path.parent() {
current_path = parent.to_path_buf();
} else {
break;
}
}
}
}
let mut state_dirs = state.dir_sizes.lock().unwrap();
*state_dirs = dir_sizes;
}
pub fn get_children(parent_path: String, state: &DiskState) -> Vec<FileTreeNode> {
let dir_sizes = state.dir_sizes.lock().unwrap();
let mut results = Vec::new();
let parent_size = *dir_sizes.get(&parent_path).unwrap_or(&1);
if let Ok(entries) = fs::read_dir(Path::new(&parent_path)) {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
let path_str = path.to_string_lossy().to_string();
let is_dir = path.is_dir();
let name = entry.file_name().to_string_lossy().to_string();
let size = if is_dir {
*dir_sizes.get(&path_str).unwrap_or(&0)
} else {
entry.metadata().map(|m| m.len()).unwrap_or(0)
};
if size > 0 || !is_dir {
results.push(FileTreeNode {
name,
path: path_str,
is_dir,
size,
size_str: format_size(size),
percent: (size as f64 / parent_size as f64 * 100.0) as f32,
file_count: 0,
has_children: is_dir,
});
}
}
}
results.sort_by(|a, b| b.size.cmp(&a.size));
results
}
pub async fn open_explorer(path: String) -> Result<(), String> {
const CREATE_NO_WINDOW: u32 = 0x08000000;
Command::new("explorer.exe")
.arg("/select,")
.arg(&path)
.creation_flags(CREATE_NO_WINDOW)
.spawn()
.map_err(|e| e.to_string())?;
Ok(())
}

View File

@@ -0,0 +1,184 @@
use std::fs;
use std::path::Path;
use std::time::{Duration, SystemTime};
use tauri::Emitter;
use crate::backend::models::{CleanResult, CleaningConfig, FastScanResult, ProjectCleanProgress, ScanItem};
use crate::backend::utils::format_size;
fn get_fast_cleaning_configs() -> Vec<CleaningConfig> {
let mut configs = Vec::new();
if let Ok(temp) = std::env::var("TEMP") {
configs.push(CleaningConfig::new("用户临时文件", &temp, None, true));
}
configs.push(CleaningConfig::new("系统临时文件", "C:\\Windows\\Temp", None, true));
configs.push(CleaningConfig::new(
"Windows 更新残留",
"C:\\Windows\\SoftwareDistribution\\Download",
Some(10),
true,
));
configs.push(CleaningConfig::new("回收站", "C:\\$Recycle.Bin", None, true));
configs.push(CleaningConfig::new(
"内核转储文件",
"C:\\Windows\\LiveKernelReports",
None,
false,
));
configs
}
pub async fn run_fast_scan() -> FastScanResult {
let configs = get_fast_cleaning_configs();
let mut items = Vec::new();
let mut total_bytes = 0;
let mut total_count = 0;
for config in configs {
let (size, count) = get_dir_stats(Path::new(&config.path), config.filter_days);
items.push(ScanItem {
name: config.name,
path: config.path,
size,
count,
enabled: config.default_enabled,
});
total_bytes += size;
total_count += count;
}
FastScanResult {
items,
total_size: format_size(total_bytes),
total_count,
}
}
fn get_dir_stats(path: &Path, filter_days: Option<u64>) -> (u64, u32) {
if !path.exists() {
return (0, 0);
}
let mut size = 0;
let mut count = 0;
let now = SystemTime::now();
let dur = filter_days.map(|days| Duration::from_secs(days * 24 * 3600));
for entry in walkdir::WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_file() {
let mut allowed = true;
if let (Some(filter_duration), Ok(metadata)) = (dur, entry.metadata()) {
if let Ok(modified_time) = metadata.modified() {
if let Ok(elapsed) = now.duration_since(modified_time) {
if elapsed < filter_duration {
allowed = false;
}
}
}
}
if allowed {
size += entry.metadata().map(|m| m.len()).unwrap_or(0);
count += 1;
}
}
}
(size, count)
}
pub async fn run_fast_clean(
selected_paths: Vec<String>,
app_handle: tauri::AppHandle,
) -> Result<CleanResult, String> {
let selected_configs: Vec<CleaningConfig> = get_fast_cleaning_configs()
.into_iter()
.filter(|config| selected_paths.contains(&config.path))
.collect();
let mut success_count = 0;
let mut fail_count = 0;
let mut total_freed = 0;
let mut approx_completed_bytes = 0;
let total_items = selected_configs.len() as u32;
for (index, config) in selected_configs.into_iter().enumerate() {
let path = Path::new(&config.path);
let item_size = get_dir_stats(path, config.filter_days).0;
if path.exists() {
let (freed, success, fail) = clean_directory_contents(path, config.filter_days);
total_freed += freed;
success_count += success;
fail_count += fail;
}
approx_completed_bytes += item_size;
let _ = app_handle.emit(
"fast-clean-progress",
ProjectCleanProgress {
completed_items: (index + 1) as u32,
total_items,
current_item: config.name,
approx_completed_bytes,
},
);
}
Ok(CleanResult {
total_freed: format_size(total_freed),
success_count,
fail_count,
})
}
pub fn clean_directory_contents(path: &Path, filter_days: Option<u64>) -> (u64, u32, u32) {
let mut freed = 0;
let mut success = 0;
let mut fail = 0;
let now = SystemTime::now();
let dur = filter_days.map(|days| Duration::from_secs(days * 24 * 3600));
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.filter_map(|e| e.ok()) {
let entry_path = entry.path();
let metadata = entry.metadata();
let size = metadata.as_ref().map(|m| m.len()).unwrap_or(0);
if let (Some(filter_duration), Ok(metadata)) = (dur, &metadata) {
if let Ok(modified_time) = metadata.modified() {
if let Ok(elapsed) = now.duration_since(modified_time) {
if elapsed < filter_duration {
continue;
}
}
}
}
if entry_path.is_file() {
if fs::remove_file(&entry_path).is_ok() {
freed += size;
success += 1;
} else {
fail += 1;
}
} else if entry_path.is_dir() {
let (dir_freed, dir_success, dir_fail) =
clean_directory_contents(&entry_path, filter_days);
freed += dir_freed;
success += dir_success;
fail += dir_fail;
if fs::remove_dir(&entry_path).is_ok() {
success += 1;
}
}
}
}
(freed, success, fail)
}

View File

@@ -0,0 +1,62 @@
use sysinfo::{ProcessesToUpdate, System};
use crate::backend::models::MemoryStats;
pub fn get_memory_stats() -> MemoryStats {
let mut sys = System::new_all();
sys.refresh_memory();
let total = sys.total_memory();
let used = sys.used_memory();
let free = total.saturating_sub(used);
let percent = (used as f32 / total as f32) * 100.0;
MemoryStats {
total,
used,
free,
percent,
}
}
pub async fn run_memory_clean() -> Result<u64, String> {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::ProcessStatus::EmptyWorkingSet;
use windows_sys::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_SET_QUOTA,
};
let before = get_memory_stats().used;
let mut sys = System::new_all();
sys.refresh_processes(ProcessesToUpdate::All, true);
for (pid, _) in sys.processes() {
let pid_u32 = pid.as_u32();
unsafe {
let handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_SET_QUOTA, 0, pid_u32);
if handle != std::ptr::null_mut() {
EmptyWorkingSet(handle);
CloseHandle(handle);
}
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let after = get_memory_stats().used;
Ok(before.saturating_sub(after))
}
pub async fn run_deep_memory_clean() -> Result<u64, String> {
use windows_sys::Win32::System::Memory::SetSystemFileCacheSize;
let before = get_memory_stats().used;
unsafe {
SetSystemFileCacheSize(usize::MAX, usize::MAX, 0);
}
let after = get_memory_stats().used;
Ok(before.saturating_sub(after))
}

View File

@@ -0,0 +1,8 @@
pub mod advanced_clean;
pub mod browser_clean;
pub mod disk_analysis;
pub mod fast_clean;
pub mod memory_clean;
pub mod models;
pub mod state;
pub mod utils;

View File

@@ -0,0 +1,96 @@
use serde::Serialize;
#[derive(Serialize, Clone)]
pub struct FileTreeNode {
pub name: String,
pub path: String,
pub is_dir: bool,
pub size: u64,
pub size_str: String,
pub percent: f32,
pub file_count: u32,
pub has_children: bool,
}
#[derive(Serialize, Clone)]
pub struct ScanProgress {
pub file_count: u64,
pub current_path: String,
}
#[derive(Serialize, Clone)]
pub struct ProjectCleanProgress {
pub completed_items: u32,
pub total_items: u32,
pub current_item: String,
pub approx_completed_bytes: u64,
}
#[derive(Clone)]
pub struct CleaningConfig {
pub name: String,
pub path: String,
pub filter_days: Option<u64>,
pub default_enabled: bool,
}
impl CleaningConfig {
pub fn new(name: &str, path: &str, filter_days: Option<u64>, default_enabled: bool) -> Self {
Self {
name: name.into(),
path: path.into(),
filter_days,
default_enabled,
}
}
}
#[derive(Serialize, Clone)]
pub struct ScanItem {
pub name: String,
pub path: String,
pub size: u64,
pub count: u32,
pub enabled: bool,
}
#[derive(Serialize)]
pub struct FastScanResult {
pub items: Vec<ScanItem>,
pub total_size: String,
pub total_count: u32,
}
#[derive(Serialize)]
pub struct CleanResult {
pub total_freed: String,
pub success_count: u32,
pub fail_count: u32,
}
#[derive(Serialize, Clone)]
pub struct BrowserProfile {
pub name: String,
pub path_name: String,
pub cache_size: u64,
pub cache_size_str: String,
}
#[derive(Serialize)]
pub struct BrowserScanResult {
pub profiles: Vec<BrowserProfile>,
pub total_size: String,
}
pub enum BrowserType {
Chrome,
Edge,
}
#[derive(Serialize, Clone)]
pub struct MemoryStats {
pub total: u64,
pub used: u64,
pub free: u64,
pub percent: f32,
}

View File

@@ -0,0 +1,6 @@
use std::collections::HashMap;
use std::sync::Mutex;
pub struct DiskState {
pub dir_sizes: Mutex<HashMap<String, u64>>,
}

View File

@@ -0,0 +1,26 @@
use std::path::Path;
pub fn format_size(size: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if size >= GB {
format!("{:.2} GB", size as f64 / GB as f64)
} else if size >= MB {
format!("{:.2} MB", size as f64 / MB as f64)
} else if size >= KB {
format!("{:.2} KB", size as f64 / KB as f64)
} else {
format!("{} B", size)
}
}
pub fn get_dir_size_simple(path: &Path) -> u64 {
walkdir::WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.map(|e| e.metadata().map(|m| m.len()).unwrap_or(0))
.sum()
}

View File

@@ -1,281 +0,0 @@
use std::fs;
use std::path::Path;
use std::time::{SystemTime, Duration};
use serde::Serialize;
use std::collections::HashMap;
use std::sync::Mutex;
use std::process::Command;
use std::os::windows::process::CommandExt;
// 存储全盘扫描后的结果
pub struct DiskState {
pub dir_sizes: Mutex<HashMap<String, u64>>,
pub file_info: Mutex<HashMap<String, (u64, u32)>>,
}
#[derive(Serialize, Clone)]
pub struct FileTreeNode {
pub name: String,
pub path: String,
pub is_dir: bool,
pub size: u64,
pub size_str: String,
pub percent: f32,
pub file_count: u32,
pub has_children: bool,
}
pub fn format_size(size: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if size >= GB { format!("{:.2} GB", size as f64 / GB as f64) }
else if size >= MB { format!("{:.2} MB", size as f64 / MB as f64) }
else if size >= KB { format!("{:.2} KB", size as f64 / KB as f64) }
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) {
use jwalk::WalkDir;
let mut dir_sizes = HashMap::new();
let mut file_info = HashMap::new();
let root = Path::new(&root_path);
for entry in WalkDir::new(root).skip_hidden(false).into_iter().filter_map(|e| e.ok()) {
if entry.file_type.is_file() {
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
if let Some(parent) = entry.parent_path().to_str() {
let info = file_info.entry(parent.to_string()).or_insert((0, 0));
info.0 += size; info.1 += 1;
}
let mut current_path = entry.parent_path().to_path_buf();
while current_path.starts_with(root) {
let path_str = current_path.to_string_lossy().to_string();
*dir_sizes.entry(path_str).or_insert(0) += size;
if current_path == root { break; }
if let Some(parent) = current_path.parent() { current_path = parent.to_path_buf(); } else { break; }
}
}
}
let mut state_dirs = state.dir_sizes.lock().unwrap();
let mut state_files = state.file_info.lock().unwrap();
*state_dirs = dir_sizes; *state_files = file_info;
}
pub fn get_children(parent_path: String, state: &DiskState) -> Vec<FileTreeNode> {
let dir_sizes = state.dir_sizes.lock().unwrap();
let file_info = state.file_info.lock().unwrap();
let mut results = Vec::new();
let parent_size = *dir_sizes.get(&parent_path).unwrap_or(&1);
if let Ok(entries) = fs::read_dir(Path::new(&parent_path)) {
for entry in entries.filter_map(|e| e.ok()) {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
let path_str = entry.path().to_string_lossy().to_string();
if let Some(&size) = dir_sizes.get(&path_str) {
results.push(FileTreeNode {
name: entry.file_name().to_string_lossy().to_string(),
path: path_str, is_dir: true, size, size_str: format_size(size),
percent: (size as f64 / parent_size as f64 * 100.0) as f32,
file_count: 0, has_children: true,
});
}
}
}
}
if let Some(&(size, count)) = file_info.get(&parent_path) {
if count > 0 {
results.push(FileTreeNode {
name: format!("[{} 个文件]", count),
path: format!("{}\\__files__", parent_path),
is_dir: false, size, size_str: format_size(size),
percent: (size as f64 / parent_size as f64 * 100.0) as f32,
file_count: count, has_children: false,
});
}
}
results.sort_by(|a, b| b.size.cmp(&a.size));
results
}
#[derive(Serialize, Clone)]
pub struct ScanItem { pub name: String, pub path: String, pub size: u64, pub count: u32 }
#[derive(Serialize)]
pub struct FastScanResult { pub items: Vec<ScanItem>, pub total_size: String, pub total_count: u32 }
pub async fn run_fast_scan() -> FastScanResult {
let mut items = Vec::new();
let mut total_bytes = 0;
let mut total_count = 0;
let mut add_item = |name: &str, path: &str, filter: Option<u64>| {
let (size, count) = get_dir_stats(Path::new(path), filter);
items.push(ScanItem { name: name.into(), path: path.into(), size, count });
total_bytes += size; total_count += count;
};
if let Ok(t) = std::env::var("TEMP") { add_item("用户临时文件", &t, None); }
add_item("系统临时文件", "C:\\Windows\\Temp", None);
add_item("Windows 更新残留", "C:\\Windows\\SoftwareDistribution\\Download", Some(10));
add_item("传递优化缓存", "C:\\Windows\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Windows\\DeliveryOptimization", None);
FastScanResult { items, total_size: format_size(total_bytes), total_count }
}
fn get_dir_stats(path: &Path, filter_days: Option<u64>) -> (u64, u32) {
if !path.exists() { return (0, 0); }
let mut size = 0; let mut count = 0;
let now = SystemTime::now();
let dur = filter_days.map(|d| Duration::from_secs(d * 24 * 3600));
for entry in walkdir::WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_file() {
let mut ok = true;
if let (Some(d), Ok(m)) = (dur, entry.metadata()) {
if let Ok(mod_t) = m.modified() {
if let Ok(el) = now.duration_since(mod_t) { if el < d { ok = false; } }
}
}
if ok { size += entry.metadata().map(|m| m.len()).unwrap_or(0); count += 1; }
}
}
(size, count)
}
#[derive(Serialize)]
pub struct CleanResult {
pub total_freed: String,
pub success_count: u32,
pub fail_count: u32,
}
pub async fn run_fast_clean(is_simulation: bool) -> Result<CleanResult, String> {
if is_simulation {
return Ok(CleanResult {
total_freed: "0 B".into(),
success_count: 0,
fail_count: 0,
});
}
let mut success_count = 0;
let mut fail_count = 0;
let mut total_freed: u64 = 0;
let mut target_paths = Vec::new();
if let Ok(t) = std::env::var("TEMP") { target_paths.push(t); }
target_paths.push("C:\\Windows\\Temp".into());
target_paths.push("C:\\Windows\\SoftwareDistribution\\Download".into());
target_paths.push("C:\\Windows\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Windows\\DeliveryOptimization".into());
for path_str in target_paths {
let path = Path::new(&path_str);
if path.exists() {
let (freed, s, f) = clean_directory_contents(path);
total_freed += freed;
success_count += s;
fail_count += f;
}
}
Ok(CleanResult {
total_freed: format_size(total_freed),
success_count,
fail_count,
})
}
fn clean_directory_contents(path: &Path) -> (u64, u32, u32) {
let mut freed = 0;
let mut success = 0;
let mut fail = 0;
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.filter_map(|e| e.ok()) {
let entry_path = entry.path();
let metadata = entry.metadata();
let size = metadata.as_ref().map(|m| m.len()).unwrap_or(0);
if entry_path.is_file() {
if fs::remove_file(&entry_path).is_ok() {
freed += size;
success += 1;
} else {
fail += 1;
}
} else if entry_path.is_dir() {
// 递归清理子目录
let (f, s, fl) = clean_directory_contents(&entry_path);
freed += f;
success += s;
fail += fl;
// 尝试删除已清空的目录
if fs::remove_dir(&entry_path).is_ok() {
success += 1;
} else {
fail += 1;
}
}
}
}
(freed, success, fail)
}

View File

@@ -1,62 +1,121 @@
mod cleaner;
use tauri::State;
use std::collections::HashMap;
use std::sync::Mutex;
use tauri::State;
mod backend;
#[tauri::command]
async fn start_fast_scan() -> cleaner::FastScanResult {
cleaner::run_fast_scan().await
async fn start_fast_scan() -> backend::models::FastScanResult {
backend::fast_clean::run_fast_scan().await
}
#[tauri::command]
async fn start_fast_clean(is_simulation: bool) -> Result<cleaner::CleanResult, String> {
cleaner::run_fast_clean(is_simulation).await
async fn start_fast_clean(
selected_paths: Vec<String>,
app_handle: tauri::AppHandle,
) -> Result<backend::models::CleanResult, String> {
backend::fast_clean::run_fast_clean(selected_paths, app_handle).await
}
#[tauri::command]
async fn start_full_disk_scan(state: State<'_, cleaner::DiskState>) -> Result<(), String> {
cleaner::run_full_scan("C:\\".to_string(), &state).await;
async fn start_full_disk_scan(
state: State<'_, backend::state::DiskState>,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
backend::disk_analysis::run_full_scan("C:\\".to_string(), &state, app_handle).await;
Ok(())
}
#[tauri::command]
async fn get_tree_children(path: String, state: State<'_, cleaner::DiskState>) -> Result<Vec<cleaner::FileTreeNode>, String> {
Ok(cleaner::get_children(path, &state))
async fn get_tree_children(
path: String,
state: State<'_, backend::state::DiskState>,
) -> Result<Vec<backend::models::FileTreeNode>, String> {
Ok(backend::disk_analysis::get_children(path, &state))
}
// --- 高级清理命令 ---
#[tauri::command]
async fn open_in_explorer(path: String) -> Result<(), String> {
backend::disk_analysis::open_explorer(path).await
}
#[tauri::command]
async fn clean_system_components() -> Result<String, String> {
cleaner::run_dism_cleanup().await
backend::advanced_clean::run_dism_cleanup().await
}
#[tauri::command]
async fn clean_thumbnails() -> Result<String, String> {
cleaner::clean_thumbnails().await
backend::advanced_clean::clean_thumbnails().await
}
#[tauri::command]
async fn disable_hibernation() -> Result<String, String> {
cleaner::disable_hibernation().await
backend::advanced_clean::disable_hibernation().await
}
#[tauri::command]
async fn start_browser_scan(browser: String) -> Result<backend::models::BrowserScanResult, String> {
let browser_type = if browser == "chrome" {
backend::models::BrowserType::Chrome
} else {
backend::models::BrowserType::Edge
};
backend::browser_clean::run_browser_scan(browser_type).await
}
#[tauri::command]
async fn start_browser_clean(
browser: String,
profiles: Vec<String>,
app_handle: tauri::AppHandle,
) -> Result<backend::models::CleanResult, String> {
let browser_type = if browser == "chrome" {
backend::models::BrowserType::Chrome
} else {
backend::models::BrowserType::Edge
};
backend::browser_clean::run_browser_clean(browser_type, profiles, app_handle).await
}
#[tauri::command]
async fn get_memory_stats() -> backend::models::MemoryStats {
backend::memory_clean::get_memory_stats()
}
#[tauri::command]
async fn run_memory_clean() -> Result<u64, String> {
backend::memory_clean::run_memory_clean().await
}
#[tauri::command]
async fn run_deep_memory_clean() -> Result<u64, String> {
backend::memory_clean::run_deep_memory_clean().await
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.manage(cleaner::DiskState {
.manage(backend::state::DiskState {
dir_sizes: Mutex::new(HashMap::new()),
file_info: Mutex::new(HashMap::new()),
})
.invoke_handler(tauri::generate_handler![
start_fast_scan,
start_fast_clean,
start_full_disk_scan,
get_tree_children,
open_in_explorer,
clean_system_components,
clean_thumbnails,
disable_hibernation
disable_hibernation,
start_browser_scan,
start_browser_clean,
get_memory_stats,
run_memory_clean,
run_deep_memory_clean
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "win-cleaner",
"version": "0.1.0",
"version": "0.1.1",
"identifier": "top.volan.win-cleaner",
"build": {
"beforeDevCommand": "pnpm dev",
@@ -13,8 +13,8 @@
"windows": [
{
"title": "Windows 清理工具",
"width": 1400,
"height": 900
"width": 1150,
"height": 750
}
],
"security": {

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import type { ModalMode, ModalType } from "../../types/cleaner";
defineProps<{
open: boolean;
title: string;
message: string;
type: ModalType;
mode?: ModalMode;
confirmText?: string;
cancelText?: string;
}>();
const emit = defineEmits<{
close: [];
confirm: [];
}>();
</script>
<template>
<div v-if="open" class="modal-overlay" @click.self="emit('close')">
<div class="modal-card" :class="type">
<div class="modal-header">
<span class="modal-icon">
<template v-if="type === 'success'"></template>
<template v-else-if="type === 'error'"></template>
<template v-else></template>
</span>
<h3>{{ title }}</h3>
</div>
<div class="modal-body">
<p>{{ message }}</p>
</div>
<div class="modal-footer" :class="{ 'is-confirm': mode === 'confirm' }">
<button v-if="mode === 'confirm'" class="btn-secondary modal-btn" @click="emit('close')">
{{ cancelText || "取消" }}
</button>
<button class="btn-primary modal-btn" @click="mode === 'confirm' ? emit('confirm') : emit('close')">
{{ mode === "confirm" ? confirmText || "确定清理" : confirmText || "确定" }}
</button>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import pkg from "../../../package.json";
import type { Tab } from "../../types/cleaner";
defineProps<{
activeTab: Tab;
isCMenuOpen: boolean;
isBrowserMenuOpen: boolean;
}>();
const emit = defineEmits<{
"update:activeTab": [tab: Tab];
"toggle-c-menu": [];
"toggle-browser-menu": [];
}>();
</script>
<template>
<aside class="sidebar">
<div class="sidebar-header">
<h2 class="brand">Windows 清理工具</h2>
</div>
<nav class="sidebar-nav">
<div class="nav-group">
<div class="nav-item-header" @click="emit('toggle-c-menu')">
<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="emit('update:activeTab', 'clean-c-fast')">
快速模式
</div>
<div class="nav-sub-item" :class="{ active: activeTab === 'clean-c-advanced' }" @click="emit('update:activeTab', 'clean-c-advanced')">
高级模式
</div>
<div class="nav-sub-item" :class="{ active: activeTab === 'clean-c-deep' }" @click="emit('update:activeTab', 'clean-c-deep')">
查找大目录
</div>
</div>
</div>
<div class="nav-group">
<div class="nav-item-header" @click="emit('toggle-browser-menu')">
<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="emit('update:activeTab', 'clean-browser-chrome')">
谷歌浏览器
</div>
<div class="nav-sub-item" :class="{ active: activeTab === 'clean-browser-edge' }" @click="emit('update:activeTab', 'clean-browser-edge')">
微软浏览器
</div>
</div>
</div>
<div class="nav-item" :class="{ active: activeTab === 'clean-memory' }" @click="emit('update: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>
</template>

View File

@@ -0,0 +1,48 @@
import { ref } from "vue";
import {
cleanSystemComponents,
cleanThumbnails,
disableHibernation,
} from "../services/tauri/cleaner";
import type { AlertOptions } from "../types/cleaner";
export function useAdvancedClean(showAlert: (options: AlertOptions) => void) {
const expandedAdvanced = ref<string | null>(null);
const loading = ref<Record<string, boolean>>({});
async function runTask(task: string) {
loading.value[task] = true;
try {
let title = "";
let result = "";
if (task === "dism") {
title = "系统组件清理";
result = await cleanSystemComponents();
} else if (task === "thumb") {
title = "缩略图清理";
result = await cleanThumbnails();
} else if (task === "hiber") {
title = "休眠文件优化";
result = await disableHibernation();
}
showAlert({ title, message: result, type: "success" });
} catch (err) {
showAlert({
title: "任务失败",
message: String(err),
type: "error",
});
} finally {
loading.value[task] = false;
}
}
return {
expandedAdvanced,
loading,
runTask,
};
}

View File

@@ -0,0 +1,197 @@
import { computed, ref } from "vue";
import {
startBrowserClean as runBrowserCleanCommand,
startBrowserScan as runBrowserScanCommand,
subscribeBrowserCleanProgress,
} from "../services/tauri/cleaner";
import type {
AlertOptions,
BrowserScanResult,
CleanResult,
ConfirmOptions,
ProjectCleanProgressPayload,
} from "../types/cleaner";
import { formatItemSize } from "../utils/format";
interface BrowserState {
isScanning: boolean;
isCleaning: boolean;
isDone: boolean;
scanResult: BrowserScanResult | null;
cleanResult: CleanResult | null;
}
interface BrowserCleanProgressState {
completedItems: number;
totalItems: number;
currentItem: string;
approxCompletedBytes: number;
}
export function useBrowserClean(
browser: "chrome" | "edge",
showAlert: (options: AlertOptions) => void,
requestConfirm: (options: ConfirmOptions) => Promise<boolean>,
) {
const state = ref<BrowserState>({
isScanning: false,
isCleaning: false,
isDone: false,
scanResult: null,
cleanResult: null,
});
const cleanProgress = ref<BrowserCleanProgressState>({
completedItems: 0,
totalItems: 0,
currentItem: "",
approxCompletedBytes: 0,
});
const selectedStats = computed(() => {
const scanResult = state.value.scanResult;
if (!scanResult) return { totalBytes: 0, sizeStr: "0 B", count: 0, hasSelection: false };
const enabledProfiles = scanResult.profiles.filter((profile) => profile.enabled);
const totalBytes = enabledProfiles.reduce((acc, profile) => acc + profile.cache_size, 0);
return {
totalBytes,
sizeStr: formatItemSize(totalBytes),
count: enabledProfiles.length,
hasSelection: enabledProfiles.length > 0,
};
});
const cleanProgressSizeStr = computed(() => formatSizeValue(cleanProgress.value.approxCompletedBytes));
function formatSizeValue(bytes: number) {
return formatItemSize(bytes);
}
function resetCleanProgress() {
cleanProgress.value = {
completedItems: 0,
totalItems: 0,
currentItem: "",
approxCompletedBytes: 0,
};
}
function handleCleanProgress(payload: ProjectCleanProgressPayload) {
cleanProgress.value = {
completedItems: payload.completed_items,
totalItems: payload.total_items,
currentItem: payload.current_item,
approxCompletedBytes: payload.approx_completed_bytes,
};
}
async function startScan() {
const current = state.value;
current.isScanning = true;
current.isDone = false;
current.scanResult = null;
current.cleanResult = null;
try {
const result = await runBrowserScanCommand(browser);
current.scanResult = {
...result,
profiles: result.profiles
.map((profile) => ({ ...profile, enabled: true }))
.sort((a, b) => b.cache_size - a.cache_size),
};
} catch (err) {
showAlert({
title: "扫描失败",
message: String(err),
type: "error",
});
} finally {
current.isScanning = false;
}
}
async function startClean() {
const current = state.value;
if (!current.scanResult || current.isCleaning) return;
const selectedProfiles = current.scanResult.profiles
.filter((profile) => profile.enabled)
.map((profile) => profile.path_name);
if (selectedProfiles.length === 0) {
showAlert({
title: "未选择",
message: "请选择至少一个用户资料进行清理。",
type: "info",
});
return;
}
const browserName = browser === "chrome" ? "谷歌浏览器" : "微软浏览器";
const confirmed = await requestConfirm({
title: "确认清理浏览器缓存",
message: `即将清理 ${browserName} 的缓存和临时文件。\n\n建议先关闭浏览器以避免部分文件被占用导致清理不完整。\n\n是否继续`,
type: "info",
confirmText: "继续清理",
cancelText: "暂不清理",
});
if (!confirmed) {
return;
}
resetCleanProgress();
current.isCleaning = true;
const unlisten = await subscribeBrowserCleanProgress(handleCleanProgress);
try {
current.cleanResult = await runBrowserCleanCommand(browser, selectedProfiles);
current.isDone = true;
current.scanResult = null;
} catch (err) {
showAlert({
title: "清理失败",
message: String(err),
type: "error",
});
} finally {
unlisten();
current.isCleaning = false;
}
}
function toggleAllProfiles(enabled: boolean) {
state.value.scanResult?.profiles.forEach((profile) => {
profile.enabled = enabled;
});
}
function invertProfiles() {
state.value.scanResult?.profiles.forEach((profile) => {
profile.enabled = !profile.enabled;
});
}
function reset() {
state.value = {
isScanning: false,
isCleaning: false,
isDone: false,
scanResult: null,
cleanResult: null,
};
resetCleanProgress();
}
return {
state,
selectedStats,
cleanProgress,
cleanProgressSizeStr,
startScan,
startClean,
toggleAllProfiles,
invertProfiles,
reset,
};
}

View File

@@ -0,0 +1,89 @@
import { ref } from "vue";
import {
getTreeChildren,
startFullDiskScan as runFullDiskScanCommand,
subscribeScanProgress,
} from "../services/tauri/cleaner";
import type { AlertOptions, FileNode } from "../types/cleaner";
export function useDiskAnalysis(showAlert: (options: AlertOptions) => void) {
const isFullScanning = ref(false);
const fullScanProgress = ref({ fileCount: 0, currentPath: "" });
const treeData = ref<FileNode[]>([]);
async function startFullDiskScan() {
isFullScanning.value = true;
treeData.value = [];
fullScanProgress.value = { fileCount: 0, currentPath: "" };
const unlisten = await subscribeScanProgress((payload) => {
fullScanProgress.value.fileCount = payload.file_count;
fullScanProgress.value.currentPath = payload.current_path;
});
try {
await runFullDiskScanCommand();
const rootChildren = await getTreeChildren("C:\\");
treeData.value = rootChildren.map((node) => ({
...node,
level: 0,
isOpen: false,
isLoading: false,
}));
} catch {
showAlert({
title: "扫描失败",
message: "请确保以管理员身份运行程序。",
type: "error",
});
} 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 += 1) {
if (treeData.value[i].level > node.level) removeCount += 1;
else break;
}
treeData.value.splice(index + 1, removeCount);
node.isOpen = false;
return;
}
node.isLoading = true;
try {
const children = await getTreeChildren(node.path);
const mappedChildren = children.map((child) => ({
...child,
level: node.level + 1,
isOpen: false,
isLoading: false,
}));
treeData.value.splice(index + 1, 0, ...mappedChildren);
node.isOpen = true;
} catch (err) {
showAlert({
title: "展开失败",
message: String(err),
type: "error",
});
} finally {
node.isLoading = false;
}
}
return {
isFullScanning,
fullScanProgress,
treeData,
startFullDiskScan,
toggleNode,
};
}

View File

@@ -0,0 +1,185 @@
import { computed, ref } from "vue";
import {
startFastClean as runFastCleanCommand,
startFastScan as runFastScanCommand,
subscribeFastCleanProgress,
} from "../services/tauri/cleaner";
import type {
AlertOptions,
CleanResult,
ConfirmOptions,
FastScanResult,
ProjectCleanProgressPayload,
} from "../types/cleaner";
import { formatItemSize } from "../utils/format";
interface FastState {
isScanning: boolean;
isCleaning: boolean;
isDone: boolean;
progress: number;
scanResult: FastScanResult | null;
cleanResult: CleanResult | null;
}
interface FastCleanProgressState {
completedItems: number;
totalItems: number;
currentItem: string;
approxCompletedBytes: number;
}
export function useFastClean(
showAlert: (options: AlertOptions) => void,
requestConfirm: (options: ConfirmOptions) => Promise<boolean>,
) {
const state = ref<FastState>({
isScanning: false,
isCleaning: false,
isDone: false,
progress: 0,
scanResult: null,
cleanResult: null,
});
const cleanProgress = ref<FastCleanProgressState>({
completedItems: 0,
totalItems: 0,
currentItem: "",
approxCompletedBytes: 0,
});
const selectedStats = computed(() => {
const scanResult = state.value.scanResult;
if (!scanResult) return { totalBytes: 0, sizeStr: "0 B", count: 0, hasSelection: false };
const enabledItems = scanResult.items.filter((item) => item.enabled);
const totalBytes = enabledItems.reduce((acc, item) => acc + item.size, 0);
const totalCount = enabledItems.reduce((acc, item) => acc + item.count, 0);
return {
totalBytes,
sizeStr: formatItemSize(totalBytes),
count: totalCount,
hasSelection: enabledItems.length > 0,
};
});
const cleanProgressSizeStr = computed(() => formatItemSize(cleanProgress.value.approxCompletedBytes));
function resetCleanProgress() {
cleanProgress.value = {
completedItems: 0,
totalItems: 0,
currentItem: "",
approxCompletedBytes: 0,
};
}
function handleCleanProgress(payload: ProjectCleanProgressPayload) {
cleanProgress.value = {
completedItems: payload.completed_items,
totalItems: payload.total_items,
currentItem: payload.current_item,
approxCompletedBytes: payload.approx_completed_bytes,
};
}
async function startScan() {
const current = state.value;
current.isScanning = true;
current.isDone = false;
current.progress = 0;
current.scanResult = null;
const interval = window.setInterval(() => {
if (current.progress < 95) {
current.progress += Math.floor(Math.random() * 5);
}
}, 100);
try {
current.scanResult = await runFastScanCommand();
current.progress = 100;
} catch {
showAlert({
title: "扫描失败",
message: "请尝试以管理员身份运行程序。",
type: "error",
});
} finally {
window.clearInterval(interval);
current.isScanning = false;
}
}
async function startClean() {
const current = state.value;
if (current.isCleaning || !current.scanResult) return;
const selectedPaths = current.scanResult.items
.filter((item) => item.enabled)
.map((item) => item.path);
if (selectedPaths.length === 0) {
showAlert({
title: "未选择任何项",
message: "请至少勾选一个需要清理的项目。",
type: "info",
});
return;
}
if (selectedPaths.includes("C:\\$Recycle.Bin")) {
const confirmed = await requestConfirm({
title: "确认清空回收站",
message: "当前勾选项包含回收站。\n\n清空后回收站中的文件将被永久删除通常无法直接恢复。\n\n是否继续清理",
type: "info",
confirmText: "继续清理",
cancelText: "返回检查",
});
if (!confirmed) {
return;
}
}
resetCleanProgress();
current.isCleaning = true;
const unlisten = await subscribeFastCleanProgress(handleCleanProgress);
try {
current.cleanResult = await runFastCleanCommand(selectedPaths);
current.isDone = true;
current.scanResult = null;
} catch (err) {
showAlert({
title: "清理失败",
message: String(err),
type: "error",
});
} finally {
unlisten();
current.isCleaning = false;
}
}
function reset() {
state.value = {
isScanning: false,
isCleaning: false,
isDone: false,
progress: 0,
scanResult: null,
cleanResult: null,
};
resetCleanProgress();
}
return {
state,
selectedStats,
cleanProgress,
cleanProgressSizeStr,
startScan,
startClean,
reset,
};
}

View File

@@ -0,0 +1,82 @@
import { onMounted, onUnmounted, ref } from "vue";
import {
getMemoryStats as fetchMemoryStats,
runDeepMemoryClean,
runMemoryClean,
} from "../services/tauri/cleaner";
import type { AlertOptions, MemoryStats } from "../types/cleaner";
import { formatItemSize } from "../utils/format";
interface MemoryState {
stats: MemoryStats | null;
isCleaning: boolean;
cleaningType: "fast" | "deep" | null;
lastFreed: string;
isDone: boolean;
}
export function useMemoryClean(showAlert: (options: AlertOptions) => void) {
const state = ref<MemoryState>({
stats: null,
isCleaning: false,
cleaningType: null,
lastFreed: "",
isDone: false,
});
let memoryInterval: number | null = null;
async function getStats() {
try {
state.value.stats = await fetchMemoryStats();
} catch (err) {
console.error("Failed to fetch memory stats", err);
}
}
async function startClean(deep = false) {
if (state.value.isCleaning) return;
state.value.isCleaning = true;
state.value.cleaningType = deep ? "deep" : "fast";
try {
const freedBytes = deep ? await runDeepMemoryClean() : await runMemoryClean();
state.value.lastFreed = formatItemSize(freedBytes);
showAlert({
title: "优化完成",
message: `已为您释放 ${state.value.lastFreed} 内存空间`,
type: "success",
});
await getStats();
} catch (err) {
showAlert({
title: "清理失败",
message: String(err),
type: "error",
});
} finally {
state.value.isCleaning = false;
state.value.cleaningType = null;
}
}
onMounted(() => {
void getStats();
memoryInterval = window.setInterval(() => {
void getStats();
}, 3000);
});
onUnmounted(() => {
if (memoryInterval) {
window.clearInterval(memoryInterval);
}
});
return {
state,
getStats,
startClean,
};
}

View File

@@ -1,4 +1,7 @@
import { createApp } from "vue";
import App from "./App.vue";
import "./styles/base.css";
import "./styles/layout.css";
import "./styles/common.css";
createApp(App).mount("#app");

View File

@@ -0,0 +1,273 @@
<script setup lang="ts">
import { useAdvancedClean } from "../composables/useAdvancedClean";
import type { AlertOptions } from "../types/cleaner";
const props = defineProps<{
showAlert: (options: AlertOptions) => void;
}>();
const { expandedAdvanced, loading, runTask } = useAdvancedClean(props.showAlert);
</script>
<template>
<section 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" :disabled="loading.dism" @click.stop="runTask('dism')">
{{ loading.dism ? "执行中..." : "执行" }}
</button>
</div>
</div>
<div v-show="expandedAdvanced === 'dism'" class="adv-card-detail">
<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" :disabled="loading.thumb" @click.stop="runTask('thumb')">执行</button>
</div>
</div>
<div v-show="expandedAdvanced === 'thumb'" class="adv-card-detail">
<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" :disabled="loading.hiber" @click.stop="runTask('hiber')">执行</button>
</div>
</div>
<div v-show="expandedAdvanced === 'hiber'" class="adv-card-detail">
<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>
</template>
<style scoped>
.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;
}
</style>

View File

@@ -0,0 +1,120 @@
<script setup lang="ts">
import { useBrowserClean } from "../composables/useBrowserClean";
import type { AlertOptions, ConfirmOptions } from "../types/cleaner";
import { splitSize } from "../utils/format";
const props = defineProps<{
browser: "chrome" | "edge";
showAlert: (options: AlertOptions) => void;
requestConfirm: (options: ConfirmOptions) => Promise<boolean>;
}>();
const { state, selectedStats, cleanProgress, cleanProgressSizeStr, startScan, startClean, toggleAllProfiles, invertProfiles, reset } =
useBrowserClean(props.browser, props.showAlert, props.requestConfirm);
const browserName = props.browser === "chrome" ? "谷歌浏览器" : "微软浏览器";
</script>
<template>
<section class="page-container">
<div class="page-header">
<div class="header-info">
<h1>清理{{ browserName }}</h1>
<p>安全清理浏览器缓存临时文件等不会删除账号和插件数据注意清理前需要关闭浏览器</p>
</div>
</div>
<div class="main-action">
<div v-if="!state.scanResult && !state.isDone" class="scan-circle-container">
<div class="scan-circle" :class="{ scanning: state.isScanning }">
<div class="scan-inner" @click="!state.isScanning && startScan()">
<span v-if="!state.isScanning" class="scan-btn-text">开始扫描</span>
<span v-else class="spinner"></span>
</div>
</div>
</div>
<div v-else-if="state.scanResult && !state.isDone" class="result-card">
<div class="result-header">
<span class="result-icon">{{ state.isCleaning ? "🧹" : "🌍" }}</span>
<h2>{{ state.isCleaning ? "正在清理" : "扫描完成" }}</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(state.isCleaning ? cleanProgressSizeStr : selectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(state.isCleaning ? cleanProgressSizeStr : selectedStats.sizeStr).unit }}</span>
</span>
<span class="stat-label">{{ state.isCleaning ? "已处理约" : "预计释放" }}</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ state.isCleaning ? `${cleanProgress.completedItems}/${cleanProgress.totalItems}` : selectedStats.count }}</span>
<span class="stat-label">{{ state.isCleaning ? "已完成资料" : "用户资料数量" }}</span>
</div>
</div>
<p v-if="state.isCleaning" class="cleaning-note">
正在清理{{ cleanProgress.currentItem || "准备开始..." }}建议保持浏览器关闭以减少文件占用
</p>
<button class="btn-primary main-btn" :disabled="state.isCleaning || !selectedStats.hasSelection" @click="startClean">
{{ state.isCleaning ? "正在清理..." : "立即清理" }}
</button>
</div>
<div v-else-if="state.isDone && state.cleanResult" class="result-card done-card">
<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(state.cleanResult.total_freed).value }}
<span class="unit">{{ splitSize(state.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">{{ state.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">{{ state.cleanResult.fail_count }}</span>
<span class="stat-label">跳过/失败</span>
</div>
</div>
<button class="btn-secondary" @click="reset">返回</button>
</div>
</div>
<div v-if="(state.isScanning || state.scanResult) && !state.isDone && !state.isCleaning" class="detail-list">
<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
v-for="profile in state.scanResult?.profiles || []"
:key="profile.path_name"
class="detail-item"
:class="{ disabled: !profile.enabled }"
@click="profile.enabled = !profile.enabled"
>
<div class="item-info">
<label class="checkbox-container" @click.stop>
<input v-model="profile.enabled" type="checkbox">
<span class="checkmark"></span>
</label>
<span>{{ profile.name }}</span>
</div>
<span class="item-size">{{ profile.cache_size_str }}</span>
</div>
<div v-if="state.isScanning" class="scanning-placeholder">正在定位并分析浏览器用户资料...</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,213 @@
<script setup lang="ts">
import { useDiskAnalysis } from "../composables/useDiskAnalysis";
import type { AlertOptions, FileNode } from "../types/cleaner";
const props = defineProps<{
showAlert: (options: AlertOptions) => void;
}>();
const emit = defineEmits<{
"open-context-menu": [event: MouseEvent, node: FileNode];
}>();
const { isFullScanning, fullScanProgress, treeData, startFullDiskScan, toggleNode } =
useDiskAnalysis(props.showAlert);
</script>
<template>
<section 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" :disabled="isFullScanning" @click="startFullDiskScan">
{{ isFullScanning ? "正在扫描..." : "开始扫描" }}
</button>
</div>
</div>
<div v-if="treeData.length > 0 || isFullScanning" class="tree-table-container shadow-card">
<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 v-if="fullScanProgress.currentPath" class="scanning-current-path">
当前{{ 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="emit('open-context-menu', $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>
</template>
<style scoped>
.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;
}
</style>

115
src/pages/FastCleanPage.vue Normal file
View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { useFastClean } from "../composables/useFastClean";
import type { AlertOptions, ConfirmOptions } from "../types/cleaner";
import { splitSize, formatItemSize } from "../utils/format";
const props = defineProps<{
showAlert: (options: AlertOptions) => void;
requestConfirm: (options: ConfirmOptions) => Promise<boolean>;
}>();
const { state, selectedStats, cleanProgress, cleanProgressSizeStr, startScan, startClean, reset } = useFastClean(
props.showAlert,
props.requestConfirm,
);
</script>
<template>
<section class="page-container">
<div class="page-header">
<div class="header-info">
<h1>清理系统盘</h1>
<p>快速清理 C 盘缓存不影响系统运行</p>
</div>
</div>
<div class="main-action">
<div v-if="!state.scanResult && !state.isDone" class="scan-circle-container">
<div class="scan-circle" :class="{ scanning: state.isScanning }">
<div class="scan-inner" @click="!state.isScanning && startScan()">
<span v-if="!state.isScanning" class="scan-btn-text">开始扫描</span>
<span v-else class="scan-percent">{{ state.progress }}%</span>
</div>
</div>
</div>
<div v-else-if="state.scanResult && !state.isDone" class="result-card">
<div class="result-header">
<span class="result-icon">{{ state.isCleaning ? "🧹" : "📋" }}</span>
<h2>{{ state.isCleaning ? "正在清理" : "扫描完成" }}</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(state.isCleaning ? cleanProgressSizeStr : selectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(state.isCleaning ? cleanProgressSizeStr : selectedStats.sizeStr).unit }}</span>
</span>
<span class="stat-label">{{ state.isCleaning ? "已处理约" : "预计释放" }}</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ state.isCleaning ? `${cleanProgress.completedItems}/${cleanProgress.totalItems}` : selectedStats.count }}</span>
<span class="stat-label">{{ state.isCleaning ? "已完成项目" : "文件数量" }}</span>
</div>
</div>
<p v-if="state.isCleaning" class="cleaning-note">
正在清理{{ cleanProgress.currentItem || "准备开始..." }}请稍候不要关闭程序
</p>
<button class="btn-primary main-btn" :disabled="state.isCleaning || !selectedStats.hasSelection" @click="startClean">
{{ state.isCleaning ? "正在清理..." : "立即清理" }}
</button>
</div>
<div v-else-if="state.isDone && state.cleanResult" class="result-card done-card">
<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(state.cleanResult.total_freed).value }}
<span class="unit">{{ splitSize(state.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">{{ state.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">{{ state.cleanResult.fail_count }}</span>
<span class="stat-label">跳过/失败</span>
</div>
</div>
<button class="btn-secondary" @click="reset">返回</button>
</div>
</div>
<div v-if="(state.isScanning || state.scanResult) && !state.isDone && !state.isCleaning" class="detail-list">
<h3>清理项详情</h3>
<div
v-for="item in state.scanResult?.items || []"
:key="item.path"
class="detail-item"
:class="{ disabled: !item.enabled }"
@click="item.enabled = !item.enabled"
>
<div class="item-info">
<label class="checkbox-container" @click.stop>
<input v-model="item.enabled" type="checkbox">
<span class="checkmark"></span>
</label>
<span>{{ item.name }}</span>
</div>
<span class="item-size">{{ formatItemSize(item.size) }}</span>
</div>
<div v-if="state.isScanning" class="scanning-placeholder">正在深度扫描文件系统...</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,247 @@
<script setup lang="ts">
import { useMemoryClean } from "../composables/useMemoryClean";
import type { AlertOptions } from "../types/cleaner";
import { formatItemSize } from "../utils/format";
const props = defineProps<{
showAlert: (options: AlertOptions) => void;
}>();
const { state, startClean } = useMemoryClean(props.showAlert);
</script>
<template>
<section 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': state.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 * (state.stats?.percent || 0)) / 100 }"></circle>
</svg>
<div class="gauge-content">
<span class="gauge-value">{{ Math.round(state.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(state.stats?.used || 0) }}</span>
</div>
<div class="stat-divider-h"></div>
<div class="stat-box-v2">
<span class="label">可用内存</span>
<span class="value">{{ formatItemSize(state.stats?.free || 0) }}</span>
</div>
<div class="stat-divider-h"></div>
<div class="stat-box-v2">
<span class="label">内存总量</span>
<span class="value">{{ formatItemSize(state.stats?.total || 0) }}</span>
</div>
</div>
</div>
<div class="memory-actions-v2">
<div class="action-card shadow-card" :class="{ cleaning: state.isCleaning }">
<div class="action-info">
<h3>普通加速</h3>
<p>建议在需要开启更多软件但内存占用居高不下时使用</p>
</div>
<button class="btn-primary" :disabled="state.isCleaning" @click="startClean(false)">
{{ state.cleaningType === "fast" ? "清理中..." : "立即加速" }}
</button>
</div>
<div class="action-card shadow-card secondary" :class="{ cleaning: state.isCleaning }">
<div class="action-info">
<h3>深度加速</h3>
<p>可以在长时间使用电脑后感觉电脑有点卡顿时执行</p>
</div>
<button class="btn-secondary" :disabled="state.isCleaning" @click="startClean(true)">
{{ state.cleaningType === "deep" ? "清理中..." : "深度加速" }}
</button>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.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 :deep(.btn-primary),
.action-card :deep(.btn-secondary) {
width: 100%;
padding: 14px;
}
.action-card.cleaning {
filter: grayscale(1);
opacity: 0.7;
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,98 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { openUrl } from "@tauri-apps/plugin-opener";
import type {
BrowserScanResult,
CleanResult,
FastScanResult,
FileNode,
MemoryStats,
ProjectCleanProgressPayload,
ScanProgressPayload,
} from "../../types/cleaner";
export function startFastScan() {
return invoke<FastScanResult>("start_fast_scan");
}
export function startFastClean(selectedPaths: string[]) {
return invoke<CleanResult>("start_fast_clean", { selectedPaths });
}
export function cleanSystemComponents() {
return invoke<string>("clean_system_components");
}
export function cleanThumbnails() {
return invoke<string>("clean_thumbnails");
}
export function disableHibernation() {
return invoke<string>("disable_hibernation");
}
export function startBrowserScan(browser: "chrome" | "edge") {
return invoke<BrowserScanResult>("start_browser_scan", { browser });
}
export function startBrowserClean(browser: "chrome" | "edge", profiles: string[]) {
return invoke<CleanResult>("start_browser_clean", { browser, profiles });
}
export function startFullDiskScan() {
return invoke("start_full_disk_scan");
}
export function getTreeChildren(path: string) {
return invoke<FileNode[]>("get_tree_children", { path });
}
export function subscribeScanProgress(
handler: (payload: ScanProgressPayload) => void,
) {
return listen<ScanProgressPayload>("scan-progress", (event) => {
handler(event.payload);
});
}
export function subscribeFastCleanProgress(
handler: (payload: ProjectCleanProgressPayload) => void,
) {
return listen<ProjectCleanProgressPayload>("fast-clean-progress", (event) => {
handler(event.payload);
});
}
export function subscribeBrowserCleanProgress(
handler: (payload: ProjectCleanProgressPayload) => void,
) {
return listen<ProjectCleanProgressPayload>("browser-clean-progress", (event) => {
handler(event.payload);
});
}
export function openInExplorer(path: string) {
return invoke("open_in_explorer", { path });
}
export function openSearch(query: string, provider: "google" | "perplexity") {
const encoded = encodeURIComponent(query);
const url =
provider === "google"
? `https://www.google.com/search?q=${encoded}`
: `https://www.perplexity.ai/?q=${encoded}`;
return openUrl(url);
}
export function getMemoryStats() {
return invoke<MemoryStats>("get_memory_stats");
}
export function runMemoryClean() {
return invoke<number>("run_memory_clean");
}
export function runDeepMemoryClean() {
return invoke<number>("run_deep_memory_clean");
}

87
src/styles/base.css Normal file
View File

@@ -0,0 +1,87 @@
: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%;
}
.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;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@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); }
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}

525
src/styles/common.css Normal file
View File

@@ -0,0 +1,525 @@
.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;
}
.cleaning-note {
margin: -8px 0 24px;
font-size: 13px;
line-height: 1.6;
color: var(--text-sec);
}
.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;
}
.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);
}
.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;
white-space: pre-line;
}
.modal-footer {
display: flex;
justify-content: center;
gap: 12px;
}
.modal-footer.is-confirm {
justify-content: center;
}
.modal-btn {
min-width: 120px;
}
.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;
}

183
src/styles/layout.css Normal file
View File

@@ -0,0 +1,183 @@
.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: 0 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: 0;
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;
}
.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;
}

86
src/types/cleaner.ts Normal file
View File

@@ -0,0 +1,86 @@
export type Tab =
| "clean-c-fast"
| "clean-c-advanced"
| "clean-c-deep"
| "clean-browser-chrome"
| "clean-browser-edge"
| "clean-memory";
export interface ScanItem {
name: string;
path: string;
size: number;
count: number;
enabled: boolean;
}
export interface FastScanResult {
items: ScanItem[];
total_size: string;
total_count: number;
}
export interface CleanResult {
total_freed: string;
success_count: number;
fail_count: number;
}
export interface BrowserProfile {
name: string;
path_name: string;
cache_size: number;
cache_size_str: string;
enabled: boolean;
}
export interface BrowserScanResult {
profiles: BrowserProfile[];
total_size: string;
}
export 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;
}
export interface MemoryStats {
total: number;
used: number;
free: number;
percent: number;
}
export interface ScanProgressPayload {
file_count: number;
current_path: string;
}
export interface ProjectCleanProgressPayload {
completed_items: number;
total_items: number;
current_item: string;
approx_completed_bytes: number;
}
export type ModalType = "info" | "success" | "error";
export type ModalMode = "alert" | "confirm";
export interface AlertOptions {
title: string;
message: string;
type?: ModalType;
}
export interface ConfirmOptions extends AlertOptions {
confirmText?: string;
cancelText?: string;
}

20
src/utils/format.ts Normal file
View File

@@ -0,0 +1,20 @@
export 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]}`;
}
export 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: "" };
}