文件比对标红显示

既然你已经有了工程文件的结构数据(比如是从 project.config.json 读取出来的树形结构),现在的核心任务是:验证这些路径在磁盘上是否真的存在,并根据结果更新 UI。

这是一个典型的 “元数据” vs “物理数据” 的校验场景。

为了保证性能(避免成百上千次 IPC 通信),我们应该采用 “批量校验” 的策略。

1. 后端:批量路径校验 (src/main/ipc/fs-ipc.ts)

不要一个一个文件去调 fs.existsSync,那样 UI 会卡顿。我们要提供一个接口,前端传一个路径数组,后端返回一个状态 Map。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/main/ipc/fs-ipc.ts
import { ipcMain } from 'electron'
import fs from 'fs-extra'

export function setupFsIPC() {
// ... 之前的代码 ...

// ✅ 新增:批量检查路径是否存在
// 参数:paths: string[]
// 返回:Record<string, boolean> { "/path/to/a": true, "/path/to/b": false }
ipcMain.handle('fs-check-paths-batch', async (_, paths: string[]) => {
const result: Record<string, boolean> = {}

// 使用 Promise.all 并行检查,性能最高
await Promise.all(paths.map(async (p) => {
try {
// access 比 exists 更推荐,因为它不会阻塞事件循环
await fs.access(p)
result[p] = true
} catch {
result[p] = false
}
}))

return result
})
}

2. Preload 暴露 API (src/preload/index.ts)

1
2
3
4
5
6
// src/preload/index.ts
const fsAPI = {
// ... 之前的 API ...
checkPathsBatch: (paths: string[]) => ipcRenderer.invoke('fs-check-paths-batch', paths)
}
// ...

3. 前端:递归组件实现 (FileTreeItem.vue)

我们需要修改组件,接收一个全局的 fileStatusMap(记录谁存在,谁不存在)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<!-- src/components/explorer/FileTreeItem.vue -->
<template>
<div class="tree-node">
<div
class="item-label"
:class="{
'selected': isSelected,
'missing': isMissing // 🔴 关键样式:不存在标红
}"
:style="{ paddingLeft: `${depth * 16 + 8}px` }"
@click.stop="handleClick"
@contextmenu.stop="handleRightClick"
>
<!-- 图标:如果丢失显示警告,否则显示正常的文件夹/文件图标 -->
<span class="icon">
<template v-if="isMissing">⚠️</template>
<template v-else>
{{ node.type === 'directory' ? (isOpen ? '📂' : '📁') : '📄' }}
</template>
</span>

<span class="name">{{ node.name }}</span>
<span v-if="isMissing" class="missing-tag">(已删除)</span>
</div>

<!-- 递归渲染 -->
<div v-if="node.type === 'directory' && isOpen">
<FileTreeItem
v-for="child in node.children"
:key="child.path"
:node="child"
:depth="depth + 1"
:selectedPath="selectedPath"
:statusMap="statusMap"
@select="$emit('select', $event)"
/>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'

const props = defineProps<{
node: any
depth: number
selectedPath: string
// 接收父组件传下来的状态表 { "path": true/false }
statusMap: Record<string, boolean>
}>()

const emit = defineEmits(['select'])

const isOpen = ref(false)

const isSelected = computed(() => props.selectedPath === props.node.path)

// ⚠️ 核心判断:如果 map 里显式记为 false,则视为丢失
// (如果 map 里没有这个 key,通常视作还没加载完,暂不标红,或者默认为 true)
const isMissing = computed(() => {
return props.statusMap[props.node.path] === false
})

const handleClick = () => {
// 🚫 核心逻辑:不存在则禁止点击(或者只能触发特定逻辑)
if (isMissing.value) {
console.warn('文件不存在,无法打开')
return
}

if (props.node.type === 'directory') {
isOpen.value = !isOpen.value
}
emit('select', props.node)
}

const handleRightClick = () => {
// 即使文件不存在,通常也允许右键,以便用户选择 "从列表中删除"
}
</script>

<style scoped>
.item-label {
display: flex;
align-items: center;
cursor: pointer;
padding: 4px 0;
color: #ccc;
user-select: none;
}

/* 正常状态 hover */
.item-label:hover:not(.missing) { background-color: #2a2d2e; }
.item-label.selected { background-color: #37373d; color: white; }

/* 🔴 丢失文件样式 */
.item-label.missing {
color: #e51400; /* 鲜艳的红 */
opacity: 0.8;
cursor: not-allowed; /* 禁止手势 */
}
.item-label.missing:hover {
background-color: rgba(229, 20, 0, 0.1); /* 红色背景微光 */
}

.icon { margin-right: 6px; width: 16px; text-align: center; }
.missing-tag { font-size: 10px; margin-left: 8px; opacity: 0.7; }
</style>

4. 前端:父组件逻辑 (ProjectExplorer.vue)

在这里我们将树形数据拍平,批量发送给后端校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<!-- src/components/explorer/ProjectExplorer.vue -->
<template>
<div class="explorer">
<div class="toolbar">
<button @click="validateFiles">🔄 刷新状态</button>
</div>

<div class="tree-container">
<FileTreeItem
v-for="node in projectData"
:key="node.path"
:node="node"
:depth="0"
:selectedPath="selectedPath"
:statusMap="fileStatusMap"
@select="handleSelect"
/>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import FileTreeItem from './FileTreeItem.vue'

// 假设这是你从 project.config.json 读出来的原始数据
const projectData = ref<any[]>([
// 示例数据
{
name: 'src', path: 'D:/project/src', type: 'directory',
children: [
{ name: 'main.ts', path: 'D:/project/src/main.ts', type: 'file' },
{ name: 'deleted.ts', path: 'D:/project/src/deleted.ts', type: 'file' } // 假设这个文件被删了
]
}
])

const selectedPath = ref('')
const fileStatusMap = ref<Record<string, boolean>>({})

// === 核心工具:拍平树结构获取所有路径 ===
const getAllPaths = (nodes: any[]): string[] => {
let paths: string[] = []
for (const node of nodes) {
paths.push(node.path)
if (node.children) {
paths = paths.concat(getAllPaths(node.children))
}
}
return paths
}

// === 核心逻辑:批量校验 ===
const validateFiles = async () => {
if (projectData.value.length === 0) return

// 1. 提取所有路径
const allPaths = getAllPaths(projectData.value)

// 2. 发送给主进程批量检查
console.log('正在校验文件数量:', allPaths.length)
const result = await window.fsAPI.checkPathsBatch(allPaths)

// 3. 更新状态 Map
fileStatusMap.value = result
}

// 初始化时校验一次
onMounted(() => {
validateFiles()
})

// 监听数据变化(如果工程文件变了,重新校验)
watch(projectData, () => {
validateFiles()
}, { deep: true })

const handleSelect = (node: any) => {
selectedPath.value = node.path
// 打开文件逻辑...
}
</script>

方案总结

  1. 性能优化:通过 getAllPaths 将树拍平,只需 一次 IPC 调用 即可验证整个工程成百上千个文件的状态,不会卡顿。
  2. 状态驱动 UI:使用 fileStatusMap 传递给所有子组件。Vue 的响应式系统会自动把状态为 false 的节点渲染成红色。
  3. 交互控制:在子组件内部拦截 handleClick,如果 isMissing 为真,直接 return,实现了“不能点击”。

这完全符合你的需求。你可以直接把这段样式和逻辑贴进你现有的左侧菜单组件中。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器