refactor frontend
This commit is contained in:
1
src-tauri/.gitignore
vendored
1
src-tauri/.gitignore
vendored
@@ -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
|
||||
|
||||
1073
src/App.vue
1073
src/App.vue
File diff suppressed because it is too large
Load Diff
35
src/components/common/AppModal.vue
Normal file
35
src/components/common/AppModal.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModalType } from "../../types/cleaner";
|
||||
|
||||
defineProps<{
|
||||
open: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
type: ModalType;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
}>();
|
||||
</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">
|
||||
<button class="btn-primary" @click="emit('close')">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
76
src/components/layout/AppSidebar.vue
Normal file
76
src/components/layout/AppSidebar.vue
Normal 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>
|
||||
48
src/composables/useAdvancedClean.ts
Normal file
48
src/composables/useAdvancedClean.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
133
src/composables/useBrowserClean.ts
Normal file
133
src/composables/useBrowserClean.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { computed, ref } from "vue";
|
||||
import {
|
||||
startBrowserClean as runBrowserCleanCommand,
|
||||
startBrowserScan as runBrowserScanCommand,
|
||||
} from "../services/tauri/cleaner";
|
||||
import type { AlertOptions, BrowserScanResult, CleanResult } from "../types/cleaner";
|
||||
import { formatItemSize } from "../utils/format";
|
||||
|
||||
interface BrowserState {
|
||||
isScanning: boolean;
|
||||
isCleaning: boolean;
|
||||
isDone: boolean;
|
||||
scanResult: BrowserScanResult | null;
|
||||
cleanResult: CleanResult | null;
|
||||
}
|
||||
|
||||
export function useBrowserClean(
|
||||
browser: "chrome" | "edge",
|
||||
showAlert: (options: AlertOptions) => void,
|
||||
) {
|
||||
const state = ref<BrowserState>({
|
||||
isScanning: false,
|
||||
isCleaning: false,
|
||||
isDone: false,
|
||||
scanResult: null,
|
||||
cleanResult: null,
|
||||
});
|
||||
|
||||
const selectedStats = computed(() => {
|
||||
const scanResult = state.value.scanResult;
|
||||
if (!scanResult) return { 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 {
|
||||
sizeStr: formatItemSize(totalBytes),
|
||||
count: enabledProfiles.length,
|
||||
hasSelection: enabledProfiles.length > 0,
|
||||
};
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
current.isCleaning = true;
|
||||
try {
|
||||
current.cleanResult = await runBrowserCleanCommand(browser, selectedProfiles);
|
||||
current.isDone = true;
|
||||
current.scanResult = null;
|
||||
} catch (err) {
|
||||
showAlert({
|
||||
title: "清理失败",
|
||||
message: String(err),
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
selectedStats,
|
||||
startScan,
|
||||
startClean,
|
||||
toggleAllProfiles,
|
||||
invertProfiles,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
89
src/composables/useDiskAnalysis.ts
Normal file
89
src/composables/useDiskAnalysis.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
119
src/composables/useFastClean.ts
Normal file
119
src/composables/useFastClean.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { startFastClean as runFastCleanCommand, startFastScan as runFastScanCommand } from "../services/tauri/cleaner";
|
||||
import type { AlertOptions, CleanResult, FastScanResult } from "../types/cleaner";
|
||||
import { formatItemSize } from "../utils/format";
|
||||
|
||||
interface FastState {
|
||||
isScanning: boolean;
|
||||
isCleaning: boolean;
|
||||
isDone: boolean;
|
||||
progress: number;
|
||||
scanResult: FastScanResult | null;
|
||||
cleanResult: CleanResult | null;
|
||||
}
|
||||
|
||||
export function useFastClean(showAlert: (options: AlertOptions) => void) {
|
||||
const state = ref<FastState>({
|
||||
isScanning: false,
|
||||
isCleaning: false,
|
||||
isDone: false,
|
||||
progress: 0,
|
||||
scanResult: null,
|
||||
cleanResult: null,
|
||||
});
|
||||
|
||||
const selectedStats = computed(() => {
|
||||
const scanResult = state.value.scanResult;
|
||||
if (!scanResult) return { 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 {
|
||||
sizeStr: formatItemSize(totalBytes),
|
||||
count: totalCount,
|
||||
hasSelection: enabledItems.length > 0,
|
||||
};
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
current.isCleaning = true;
|
||||
try {
|
||||
current.cleanResult = await runFastCleanCommand(selectedPaths);
|
||||
current.isDone = true;
|
||||
current.scanResult = null;
|
||||
} catch (err) {
|
||||
showAlert({
|
||||
title: "清理失败",
|
||||
message: String(err),
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
current.isCleaning = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
state.value = {
|
||||
isScanning: false,
|
||||
isCleaning: false,
|
||||
isDone: false,
|
||||
progress: 0,
|
||||
scanResult: null,
|
||||
cleanResult: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
selectedStats,
|
||||
startScan,
|
||||
startClean,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
82
src/composables/useMemoryClean.ts
Normal file
82
src/composables/useMemoryClean.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
112
src/pages/AdvancedCleanPage.vue
Normal file
112
src/pages/AdvancedCleanPage.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<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>
|
||||
116
src/pages/BrowserCleanPage.vue
Normal file
116
src/pages/BrowserCleanPage.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { useBrowserClean } from "../composables/useBrowserClean";
|
||||
import type { AlertOptions } from "../types/cleaner";
|
||||
import { splitSize } from "../utils/format";
|
||||
|
||||
const props = defineProps<{
|
||||
browser: "chrome" | "edge";
|
||||
showAlert: (options: AlertOptions) => void;
|
||||
}>();
|
||||
|
||||
const { state, selectedStats, startScan, startClean, toggleAllProfiles, invertProfiles, reset } =
|
||||
useBrowserClean(props.browser, props.showAlert);
|
||||
|
||||
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">🌍</span>
|
||||
<h2>扫描完成</h2>
|
||||
</div>
|
||||
<div class="result-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">
|
||||
{{ splitSize(selectedStats.sizeStr).value }}
|
||||
<span class="unit">{{ splitSize(selectedStats.sizeStr).unit }}</span>
|
||||
</span>
|
||||
<span class="stat-label">预计释放</span>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ selectedStats.count }}</span>
|
||||
<span class="stat-label">用户资料数量</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-primary main-btn" :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" 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>
|
||||
81
src/pages/DiskAnalysisPage.vue
Normal file
81
src/pages/DiskAnalysisPage.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<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>
|
||||
107
src/pages/FastCleanPage.vue
Normal file
107
src/pages/FastCleanPage.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { useFastClean } from "../composables/useFastClean";
|
||||
import type { AlertOptions } from "../types/cleaner";
|
||||
import { splitSize, formatItemSize } from "../utils/format";
|
||||
|
||||
const props = defineProps<{
|
||||
showAlert: (options: AlertOptions) => void;
|
||||
}>();
|
||||
|
||||
const { state, selectedStats, startScan, startClean, reset } = useFastClean(props.showAlert);
|
||||
</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">📋</span>
|
||||
<h2>扫描完成</h2>
|
||||
</div>
|
||||
<div class="result-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">
|
||||
{{ splitSize(selectedStats.sizeStr).value }}
|
||||
<span class="unit">{{ splitSize(selectedStats.sizeStr).unit }}</span>
|
||||
</span>
|
||||
<span class="stat-label">预计释放</span>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ selectedStats.count }}</span>
|
||||
<span class="stat-label">文件数量</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-primary main-btn" :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" 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>
|
||||
78
src/pages/MemoryCleanPage.vue
Normal file
78
src/pages/MemoryCleanPage.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<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>
|
||||
81
src/services/tauri/cleaner.ts
Normal file
81
src/services/tauri/cleaner.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
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,
|
||||
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 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");
|
||||
}
|
||||
73
src/types/cleaner.ts
Normal file
73
src/types/cleaner.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
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 type ModalType = "info" | "success" | "error";
|
||||
|
||||
export interface AlertOptions {
|
||||
title: string;
|
||||
message: string;
|
||||
type?: ModalType;
|
||||
}
|
||||
20
src/utils/format.ts
Normal file
20
src/utils/format.ts
Normal 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: "" };
|
||||
}
|
||||
Reference in New Issue
Block a user