clean c
This commit is contained in:
363
src/App.vue
Normal file
363
src/App.vue
Normal file
@@ -0,0 +1,363 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
// --- 导航状态 ---
|
||||
type Tab = 'clean-c-fast' | 'clean-c-advanced' | 'clean-browser' | 'clean-memory';
|
||||
const activeTab = ref<Tab>('clean-c-fast');
|
||||
const isCMenuOpen = ref(true);
|
||||
|
||||
// --- 数据结构 ---
|
||||
interface ScanItem { name: string; path: string; size: number; count: number; }
|
||||
interface FastScanResult { items: ScanItem[]; total_size: string; total_count: number; }
|
||||
interface FileNode {
|
||||
name: string; path: string; is_dir: boolean; size: number; size_str: string;
|
||||
percent: number; has_children: boolean;
|
||||
level: number; isOpen: boolean; isLoading: boolean;
|
||||
}
|
||||
|
||||
// --- 状态管理 ---
|
||||
const isScanning = ref(false);
|
||||
const isCleaning = ref(false);
|
||||
const isSimulation = ref(false);
|
||||
const isCleanDone = ref(false);
|
||||
const isFullScanning = ref(false);
|
||||
const scanProgress = ref(0);
|
||||
const fastScanResult = ref<FastScanResult | null>(null);
|
||||
const treeData = ref<FileNode[]>([]);
|
||||
const cleanMessage = ref("");
|
||||
|
||||
// --- 快速模式逻辑 ---
|
||||
async function startFastScan() {
|
||||
isScanning.value = true;
|
||||
isCleanDone.value = false;
|
||||
scanProgress.value = 0;
|
||||
fastScanResult.value = null;
|
||||
const interval = setInterval(() => { if (scanProgress.value < 95) scanProgress.value += Math.floor(Math.random() * 5); }, 100);
|
||||
try {
|
||||
const result = await invoke<FastScanResult>("start_fast_scan");
|
||||
scanProgress.value = 100;
|
||||
fastScanResult.value = result;
|
||||
} catch (err) {
|
||||
alert("扫描失败,请尝试以管理员身份运行。");
|
||||
} finally {
|
||||
clearInterval(interval);
|
||||
isScanning.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function startFastClean() {
|
||||
if (isCleaning.value) return;
|
||||
isCleaning.value = true;
|
||||
try {
|
||||
const msg = await invoke<string>("start_fast_clean", { isSimulation: isSimulation.value });
|
||||
cleanMessage.value = msg;
|
||||
isCleanDone.value = true;
|
||||
fastScanResult.value = null;
|
||||
} finally { isCleaning.value = false; }
|
||||
}
|
||||
|
||||
// --- 高级模式 (TreeSize) 逻辑 ---
|
||||
async function startFullDiskScan() {
|
||||
isFullScanning.value = true;
|
||||
treeData.value = [];
|
||||
try {
|
||||
await invoke("start_full_disk_scan");
|
||||
const rootChildren = await invoke<FileNode[]>("get_tree_children", { path: "C:\\" });
|
||||
treeData.value = rootChildren.map(node => ({ ...node, level: 0, isOpen: false, isLoading: false }));
|
||||
} catch (err) {
|
||||
alert("扫描失败,请确保以管理员身份运行。");
|
||||
} finally {
|
||||
isFullScanning.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleNode(index: number) {
|
||||
const node = treeData.value[index];
|
||||
if (!node.is_dir || node.isLoading) return;
|
||||
if (node.isOpen) {
|
||||
let removeCount = 0;
|
||||
for (let i = index + 1; i < treeData.value.length; i++) {
|
||||
if (treeData.value[i].level > node.level) removeCount++;
|
||||
else break;
|
||||
}
|
||||
treeData.value.splice(index + 1, removeCount);
|
||||
node.isOpen = false;
|
||||
} else {
|
||||
node.isLoading = true;
|
||||
try {
|
||||
const children = await invoke<FileNode[]>("get_tree_children", { path: node.path });
|
||||
const mapped = children.map(c => ({ ...c, level: node.level + 1, isOpen: false, isLoading: false }));
|
||||
treeData.value.splice(index + 1, 0, ...mapped);
|
||||
node.isOpen = true;
|
||||
} finally { node.isLoading = false; }
|
||||
}
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
isCleanDone.value = false;
|
||||
fastScanResult.value = null;
|
||||
treeData.value = [];
|
||||
cleanMessage.value = "";
|
||||
}
|
||||
|
||||
function formatItemSize(bytes: number): string {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header"><h2 class="brand">WinCleaner</h2></div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-group">
|
||||
<div class="nav-item-header" @click="isCMenuOpen = !isCMenuOpen">
|
||||
<span class="icon">💾</span><span class="label">清理 C 盘</span>
|
||||
<span class="arrow" :class="{ open: isCMenuOpen }">▾</span>
|
||||
</div>
|
||||
<div class="nav-sub-items" v-show="isCMenuOpen">
|
||||
<div class="nav-sub-item" :class="{ active: activeTab === 'clean-c-fast' }" @click="activeTab = 'clean-c-fast'">快速模式</div>
|
||||
<div class="nav-sub-item" :class="{ active: activeTab === 'clean-c-advanced' }" @click="activeTab = 'clean-c-advanced'">高级模式</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" :class="{ active: activeTab === 'clean-browser' }" @click="activeTab = 'clean-browser'">
|
||||
<span class="icon">🌐</span><span class="label">清理浏览器</span>
|
||||
</div>
|
||||
<div class="nav-item" :class="{ active: activeTab === 'clean-memory' }" @click="activeTab = 'clean-memory'">
|
||||
<span class="icon">🚀</span><span class="label">清理内存</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-footer"><span class="version">v1.0.0</span></div>
|
||||
</aside>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<main class="content">
|
||||
<!-- 1. 快速清理页面 -->
|
||||
<section v-if="activeTab === 'clean-c-fast'" class="page-container">
|
||||
<div class="page-header">
|
||||
<h1>快速清理系统盘</h1>
|
||||
<p>一键释放 C 盘空间,不影响系统安全。</p>
|
||||
</div>
|
||||
|
||||
<div class="main-action">
|
||||
<!-- 扫描中状态 -->
|
||||
<div class="scan-circle-container" v-if="!fastScanResult && !isCleanDone">
|
||||
<div class="scan-circle" :class="{ scanning: isScanning }">
|
||||
<div class="scan-inner" @click="!isScanning && startFastScan()">
|
||||
<span v-if="!isScanning" class="scan-btn-text">开始扫描</span>
|
||||
<span v-else class="scan-percent">{{ scanProgress }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 扫描完成,待清理 -->
|
||||
<div class="result-card" v-else-if="fastScanResult && !isCleanDone">
|
||||
<div class="result-header"><span class="result-icon">📋</span><h2>扫描完成</h2></div>
|
||||
<div class="result-stats">
|
||||
<div class="stat-item"><span class="stat-value">{{ fastScanResult.total_size }}</span><span class="stat-label">预计释放</span></div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item"><span class="stat-value">{{ fastScanResult.total_count }}</span><span class="stat-label">文件数量</span></div>
|
||||
</div>
|
||||
<div class="simulation-toggle">
|
||||
<label class="switch"><input type="checkbox" v-model="isSimulation"><span class="slider round"></span></label>
|
||||
<span class="toggle-label">模拟清理 (不实际删除文件)</span>
|
||||
</div>
|
||||
<button class="btn-primary main-btn" @click="startFastClean" :disabled="isCleaning">
|
||||
{{ isCleaning ? '正在清理...' : (isSimulation ? '开始模拟清理' : '立即清理') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 清理报告 -->
|
||||
<div class="result-card done-card" v-else-if="isCleanDone">
|
||||
<div class="result-header"><span class="result-icon success">✅</span><h2>清理完成</h2><p class="clean-summary">{{ cleanMessage }}</p></div>
|
||||
<div class="done-info"><p>您的 C 盘现在更加清爽了!</p></div>
|
||||
<button class="btn-secondary" @click="resetAll">返回首页</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-list" v-if="(isScanning || fastScanResult) && !isCleanDone">
|
||||
<h3>清理项详情</h3>
|
||||
<div class="detail-item" v-for="item in fastScanResult?.items || []" :key="item.path">
|
||||
<span>{{ item.name }}</span><span class="item-size">{{ formatItemSize(item.size) }}</span>
|
||||
</div>
|
||||
<div v-if="isScanning" class="scanning-placeholder">正在深度扫描系统目录...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. 高级磁盘树页面 -->
|
||||
<section v-else-if="activeTab === 'clean-c-advanced'" class="page-container advanced-page">
|
||||
<div class="page-header">
|
||||
<h1>磁盘树深度分析</h1>
|
||||
<p>像 TreeSize 一样层级化查看 C 盘占用,锁定空间大户。</p>
|
||||
</div>
|
||||
|
||||
<div class="advanced-actions">
|
||||
<button class="btn-primary" @click="startFullDiskScan" :disabled="isFullScanning">
|
||||
{{ isFullScanning ? '正在建立全盘索引...' : '开始深度分析' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tree-table-container shadow-card" v-if="treeData.length > 0 || isFullScanning">
|
||||
<div v-if="isFullScanning" class="scanning-overlay">
|
||||
<div class="spinner"></div>
|
||||
<p>正在分析数百万个文件,请稍候...</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="tree-header">
|
||||
<span class="col-name">文件/文件夹名称</span>
|
||||
<span class="col-size">大小</span>
|
||||
<span class="col-graph">相对于父目录占比</span>
|
||||
</div>
|
||||
<div class="tree-body">
|
||||
<div v-for="(node, index) in treeData" :key="node.path" class="tree-row" :class="{ 'is-file': !node.is_dir }" :style="{ paddingLeft: (node.level * 20 + 16) + 'px' }">
|
||||
<div class="col-name" @click="toggleNode(index)">
|
||||
<span v-if="node.is_dir" class="node-toggle">{{ node.isLoading ? '⌛' : (node.isOpen ? '▼' : '▶') }}</span>
|
||||
<span v-else class="node-icon">📄</span>
|
||||
<span class="node-text" :title="node.path">{{ 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>
|
||||
|
||||
<!-- 3. 占位页面 -->
|
||||
<section v-else class="placeholder-page">
|
||||
<div class="empty-state">
|
||||
<span class="empty-icon">🛠️</span>
|
||||
<h1>功能开发中</h1>
|
||||
<p>此模块正在逐步完善,敬请期待。</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* --- 全局基础样式 --- */
|
||||
:root {
|
||||
--primary-color: #007AFF;
|
||||
--primary-hover: #0063CC;
|
||||
--bg-light: #F9FAFB;
|
||||
--sidebar-bg: #FFFFFF;
|
||||
--text-main: #1D1D1F;
|
||||
--text-sec: #86868B;
|
||||
--border-color: #E5E5E7;
|
||||
--card-shadow: 0 8px 24px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: var(--text-main); background-color: var(--bg-light); height: 100vh; overflow: hidden; }
|
||||
#app { height: 100%; }
|
||||
.app-container { display: flex; height: 100%; }
|
||||
|
||||
/* --- 侧边栏 --- */
|
||||
.sidebar { width: 260px; background-color: var(--sidebar-bg); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; padding: 24px 0; z-index: 10; }
|
||||
.sidebar-header { padding: 0 24px 32px; }
|
||||
.brand { font-size: 22px; font-weight: 800; color: var(--primary-color); letter-spacing: -0.5px; }
|
||||
.sidebar-nav { flex: 1; }
|
||||
.nav-item, .nav-item-header { padding: 12px 24px; display: flex; align-items: center; cursor: pointer; transition: all 0.2s; color: #424245; font-size: 15px; font-weight: 500; }
|
||||
.nav-item:hover, .nav-item-header:hover { background-color: #F5F5F7; color: var(--primary-color); }
|
||||
.nav-item.active { background-color: #F0F7FF; color: var(--primary-color); font-weight: 600; border-right: 3px solid var(--primary-color); }
|
||||
.icon { margin-right: 12px; font-size: 18px; }
|
||||
.arrow { margin-left: auto; transition: transform 0.3s; font-size: 12px; color: #C1C1C1; }
|
||||
.arrow.open { transform: rotate(180deg); }
|
||||
.nav-sub-items { background-color: #FAFAFA; }
|
||||
.nav-sub-item { padding: 10px 24px 10px 54px; cursor: pointer; font-size: 14px; color: #6E6E73; transition: all 0.2s; }
|
||||
.nav-sub-item:hover { color: var(--primary-color); }
|
||||
.nav-sub-item.active { color: var(--primary-color); font-weight: 600; background-color: #F0F7FF; }
|
||||
.sidebar-footer { padding: 16px 24px; border-top: 1px solid var(--border-color); }
|
||||
.version { font-size: 12px; color: var(--text-sec); }
|
||||
|
||||
/* --- 内容区 --- */
|
||||
.content { flex: 1; padding: 40px; overflow-y: auto; }
|
||||
.page-container { max-width: 900px; margin: 0 auto; }
|
||||
.page-header { margin-bottom: 32px; }
|
||||
.page-header h1 { font-size: 32px; font-weight: 800; margin-bottom: 8px; letter-spacing: -0.5px; }
|
||||
.page-header p { color: var(--text-sec); font-size: 16px; }
|
||||
|
||||
/* --- 按钮样式 --- */
|
||||
.btn-primary { background-color: var(--primary-color); color: white; border: none; padding: 14px 32px; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
|
||||
.btn-primary:hover { background-color: var(--primary-hover); transform: translateY(-1px); }
|
||||
.btn-primary:active { transform: translateY(0); }
|
||||
.btn-primary:disabled { background-color: #A5C7FF; cursor: not-allowed; transform: none; }
|
||||
.btn-secondary { background-color: white; color: var(--text-main); border: 1px solid var(--border-color); padding: 12px 32px; border-radius: 12px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
|
||||
.btn-secondary:hover { background-color: #F5F5F7; border-color: #D1D1D6; }
|
||||
|
||||
/* --- 快速清理特有 UI --- */
|
||||
.main-action { margin: 60px 0; display: flex; justify-content: center; }
|
||||
.scan-circle-container { width: 220px; height: 220px; }
|
||||
.scan-circle { width: 100%; height: 100%; border-radius: 50%; border: 4px solid var(--border-color); display: flex; align-items: center; justify-content: center; position: relative; transition: all 0.5s; }
|
||||
.scan-circle.scanning { border-color: var(--primary-color); box-shadow: 0 0 40px rgba(0, 122, 255, 0.15); animation: pulse 2s infinite; }
|
||||
.scan-inner { width: 190px; height: 190px; border-radius: 50%; background: white; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: var(--card-shadow); transition: transform 0.2s; }
|
||||
.scan-inner:hover { transform: scale(1.05); }
|
||||
.scan-btn-text { font-size: 22px; font-weight: 700; color: var(--primary-color); }
|
||||
.scan-percent { font-size: 36px; font-weight: 800; color: var(--primary-color); }
|
||||
|
||||
.result-card { background: white; border-radius: 24px; padding: 40px; width: 100%; box-shadow: var(--card-shadow); text-align: center; border: 1px solid rgba(0,0,0,0.03); }
|
||||
.result-header { margin-bottom: 32px; }
|
||||
.result-icon { font-size: 54px; display: block; margin-bottom: 16px; }
|
||||
.result-stats { display: flex; justify-content: center; align-items: center; margin-bottom: 32px; }
|
||||
.stat-item { flex: 1; }
|
||||
.stat-value { display: block; font-size: 36px; font-weight: 800; color: var(--primary-color); }
|
||||
.stat-label { font-size: 14px; color: var(--text-sec); font-weight: 500; }
|
||||
.stat-divider { width: 1px; height: 50px; background-color: var(--border-color); margin: 0 40px; }
|
||||
|
||||
.simulation-toggle { display: flex; align-items: center; justify-content: center; margin-bottom: 24px; gap: 12px; }
|
||||
.toggle-label { font-size: 14px; color: var(--text-sec); }
|
||||
.switch { position: relative; display: inline-block; width: 44px; height: 24px; }
|
||||
.switch input { opacity: 0; width: 0; height: 0; }
|
||||
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #E5E5E7; transition: .4s; border-radius: 24px; }
|
||||
.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
input:checked + .slider { background-color: var(--primary-color); }
|
||||
input:checked + .slider:before { transform: translateX(20px); }
|
||||
|
||||
.done-card { border: 2px solid #E8F5E9; }
|
||||
.result-icon.success { color: #34C759; }
|
||||
.clean-summary { font-size: 20px; color: var(--text-main); font-weight: 700; margin-top: 12px; }
|
||||
.done-info { margin-bottom: 32px; color: var(--text-sec); font-size: 15px; }
|
||||
|
||||
/* --- 磁盘树特有 UI --- */
|
||||
.tree-table-container { background: #fff; border-radius: 16px; overflow: hidden; margin-top: 24px; position: relative; min-height: 400px; }
|
||||
.shadow-card { box-shadow: var(--card-shadow); border: 1px solid rgba(0,0,0,0.03); }
|
||||
.tree-header { display: flex; background: #F5F5F7; padding: 14px 20px; font-size: 13px; font-weight: 700; color: var(--text-sec); border-bottom: 1px solid var(--border-color); }
|
||||
.tree-body { max-height: 600px; overflow-y: auto; }
|
||||
.tree-row { display: flex; align-items: center; padding: 12px 20px; border-bottom: 1px solid #F2F2F7; font-size: 14px; transition: background 0.1s; }
|
||||
.tree-row:hover { background: #F9FAFB; }
|
||||
.tree-row.is-file { color: var(--text-sec); }
|
||||
.col-name { flex: 2; display: flex; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.col-size { width: 110px; text-align: right; font-family: 'SF Mono', 'Roboto Mono', monospace; font-weight: 600; color: #1D1D1F; }
|
||||
.col-graph { width: 200px; display: flex; align-items: center; gap: 12px; padding-left: 30px; }
|
||||
.node-toggle { width: 24px; cursor: pointer; color: #C1C1C1; font-size: 11px; display: inline-block; text-align: center; }
|
||||
.node-icon { width: 24px; font-size: 14px; }
|
||||
.mini-bar-bg { flex: 1; height: 6px; background: #F2F2F7; border-radius: 3px; overflow: hidden; }
|
||||
.mini-bar-fill { height: 100%; background: linear-gradient(90deg, #007AFF, #5856D6); border-radius: 3px; }
|
||||
.percent-text { font-size: 11px; color: var(--text-sec); width: 35px; font-weight: 600; }
|
||||
|
||||
.scanning-overlay { padding: 100px 40px; text-align: center; color: var(--text-sec); }
|
||||
.spinner { width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid var(--primary-color); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 20px; }
|
||||
|
||||
/* --- 通用列表 --- */
|
||||
.detail-list { background: white; border-radius: 16px; padding: 24px; box-shadow: var(--card-shadow); margin-top: 32px; border: 1px solid rgba(0,0,0,0.02); }
|
||||
.detail-list h3 { font-size: 17px; margin-bottom: 16px; font-weight: 700; }
|
||||
.detail-item { display: flex; justify-content: space-between; padding: 14px 0; border-bottom: 1px solid #F2F2F7; font-size: 14px; color: #424245; }
|
||||
.item-size { font-weight: 600; color: var(--primary-color); }
|
||||
|
||||
.placeholder-page { padding-top: 100px; text-align: center; color: var(--text-sec); }
|
||||
.empty-icon { font-size: 64px; display: block; margin-bottom: 24px; }
|
||||
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(0, 122, 255, 0.3); } 70% { box-shadow: 0 0 0 25px rgba(0, 122, 255, 0); } 100% { box-shadow: 0 0 0 0 rgba(0, 122, 255, 0); } }
|
||||
</style>
|
||||
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 496 B |
4
src/main.ts
Normal file
4
src/main.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
7
src/vite-env.d.ts
vendored
Normal file
7
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
Reference in New Issue
Block a user