兼容一键切换是否锁定文件夹

之前介绍了两种锁定文件的形式,

1、句柄占用
2、icacls 权限管理
最后我选择了icacls权限管理,因为它更加灵活,可以针对文件夹和文件分别进行权限控制。

问题,icacls管理 是可行的,但是我担心后面又不想要权限管理了(这个取决于需求)
研究后发现改动点其实主要在文件和文件夹的修改 保存 删除 重命名 的方式不一样。
icacls 都是先解锁 然后操作 然后上锁
正常模式就是没有解锁上锁的过程
目前就是要兼容这两种 一键切换。

解决方案

创建了一个文件管理 src\main\ipc\file-service.ts

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// src/main/file-service.ts
import fs from 'fs-extra'
import path from 'path'
import { securityManager } from './security-manager' // 复用上一条回答中的 SecurityManager

export class FileService {
// 当前是否处于锁定模式
private isLockedMode = false
// 当前打开的工程根目录
private currentRootPath = ''

/**
* 打开工程
*/
async openProject(dirPath: string) {
this.currentRootPath = dirPath

// 默认打开时为普通模式,或者你可以设计为默认锁定
// 这里演示:打开时不锁,由用户手动切换,或者保持上次状态
if (this.isLockedMode) {
await securityManager.lockProject(dirPath)
}
return true
}

/**
* 关闭工程
*/
async closeProject() {
if (this.isLockedMode) {
await securityManager.unlockProject()
}
this.currentRootPath = ''
this.isLockedMode = false
}

/**
* ✨ 核心功能:切换模式
*/
async toggleLockMode(locked: boolean) {
if (!this.currentRootPath) throw new Error('未打开工程')

if (locked) {
// 切换到锁定模式
await securityManager.lockProject(this.currentRootPath)
this.isLockedMode = true
console.log('🔄 模式切换: [独占锁定]')
} else {
// 切换到普通模式
await securityManager.unlockProject()
this.isLockedMode = false
console.log('🔄 模式切换: [普通模式]')
}
return this.isLockedMode
}

/**
* 获取当前模式
*/
getMode() {
return this.isLockedMode
}

// ==========================================
// 统一 CRUD 操作 (自动判断模式)
// ==========================================

async writeFile(filePath: string, content: string) {
if (this.isLockedMode) {
// 🔒 锁定模式:走特权通道 (Unlock -> Write -> Lock)
return await securityManager.privilegedWrite(filePath, content)
} else {
// 🔓 普通模式:走原生 Node.js API
return await fs.writeFile(filePath, content, 'utf-8')
}
}

// async createFile(targetPath: string, type: 'file' | 'directory') {
// if (this.isLockedMode) {
// return await securityManager.privilegedCreate(targetPath, type)
// } else {
// if (type === 'directory') {
// return await fs.ensureDir(targetPath)
// } else {
// return await fs.ensureFile(targetPath)
// }
// }
// }


async deleteEntry(targetPath: string) {
if (this.isLockedMode) {
return await securityManager.privilegedDelete(targetPath)
} else {
// 普通模式直接删 (建议移入回收站,这里演示直接删)
return await fs.remove(targetPath)
}
}

async renameEntry(oldPath: string, newName: string) {
const dir = path.dirname(oldPath)
const newPath = path.join(dir, newName)

if (this.isLockedMode) {
// SecurityManager 需要补充一个 privilegedRename 方法
// 逻辑同 delete/create:解锁父目录 -> rename -> 加锁
// 这里暂时先用 unlock -> rename -> lock 模拟
await securityManager.privilegedRename(oldPath, newPath)
} else {
await fs.rename(oldPath, newPath)
}
return newPath
}
/**
* 📖 读取文件内容
* (读取通常不受写锁影响,直接读即可)
*/
async readFile(filePath: string) {
// 统一读取,如果文件不存在会抛错
return await fs.readFile(filePath, 'utf-8')
}
/**
* ✨ 创建文件或文件夹 (自动判断模式)
*/
async createEntry(targetPath: string, type: 'file' | 'directory') {
if (this.isLockedMode) {
// 🔒 锁定模式:走特权创建流程
console.log(`[FileService] 特权创建: ${targetPath}`)
await securityManager.privilegedCreate(targetPath, type)
} else {
// 🔓 普通模式:直接创建
console.log(`[FileService] 普通创建: ${targetPath}`)
if (type === 'directory') {
await fs.ensureDir(targetPath)
} else {
await fs.ensureFile(targetPath)
}
}
return true
}
}

export const fileService = new FileService()

然后创建一个 src\main\ipc\project-ipc.ts

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
// src/main/ipc/project-ipc.ts
// src/main/ipc/project-ipc.ts
import { ipcMain, dialog } from 'electron'
import { fileService } from './file-service'
import fs from 'fs-extra' // 建议安装 fs-extra: npm install fs-extra @types/fs-extra
import path from 'path'
// 定义文件节点类型
export interface FileNode {
name: string
path: string
type: 'file' | 'directory'
children?: FileNode[]
}

// 忽略列表
const IGNORE_LIST = ['.git', 'node_modules', '.DS_Store']

// 递归构建文件树
const buildFileTree = (dirPath: string): FileNode[] => {
const stats = fs.statSync(dirPath)
if (!stats.isDirectory()) return []

const children = fs.readdirSync(dirPath)
.filter(name => !IGNORE_LIST.includes(name))
.map(name => {
const fullPath = path.join(dirPath, name)
const fileStats = fs.statSync(fullPath)

const node: FileNode = {
name,
path: fullPath,
type: fileStats.isDirectory() ? 'directory' : 'file'
}

if (node.type === 'directory') {
node.children = buildFileTree(fullPath)
}
return node
})

// 排序:文件夹在前,文件在后
return children.sort((a, b) => {
if (a.type === b.type) return a.name.localeCompare(b.name)
return a.type === 'directory' ? -1 : 1
})
}
export function setupProjectIPC() {
// 2. 读取整个工程结构
ipcMain.handle('fs-read-project-tree', async (_, rootPath: string) => {
try {
if (!fs.existsSync(rootPath)) throw new Error('Path not found')

// 读取或创建工程配置文件
// const configPath = path.join(rootPath, 'project.config.json')
let projectInfo = { name: path.basename(rootPath), version: '1.0.0' }

// if (fs.existsSync(configPath)) {
// projectInfo = await fs.readJson(configPath)
// } else {
// // await fs.writeJson(configPath, projectInfo, { spaces: 2 })
// await securityManager.privilegedWrite(configPath,JSON.stringify(projectInfo))
// }
// console.log('Project Info:', projectInfo,rootPath)

const tree = buildFileTree(rootPath)
return { tree, projectInfo }
} catch (error) {
console.error(error)
throw error
}
})
// 1. 打开并选择工程
ipcMain.handle('prj-open', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory']
})
if (canceled || !filePaths.length) return null

const path = filePaths[0]
await fileService.openProject(path)
return path
})

// 2. 关闭
ipcMain.handle('prj-close', () => fileService.closeProject())

// 3. 切换锁定状态
ipcMain.handle('prj-toggle-lock', (_, locked: boolean) => fileService.toggleLockMode(locked))

// 4. 统一 CRUD
ipcMain.handle('prj-write', (_, { path, content }) => fileService.writeFile(path, content))
// ipcMain.handle('prj-create', (_, { path, type }) => fileService.createFile(path, type))
ipcMain.handle('prj-delete', (_, path) => fileService.deleteEntry(path))
// ✅ 新增:读取文件
ipcMain.handle('prj-read', (_, path) => fileService.readFile(path))

// ✅ 新增:创建 (注意参数解构)
ipcMain.handle('prj-create', (_, { path, type }) => fileService.createEntry(path, type))
// ✅ 新增:重命名
// 参数接收一个对象 { oldPath, newName }
ipcMain.handle('prj-rename', (_, { oldPath, newName }) =>
fileService.renameEntry(oldPath, newName)
)
}

创建加密的管理方法 src\main\ipc\security-manager.ts

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// src/main/ipc/security-manager.ts
// src/main/security-manager.ts
import { exec } from 'child_process'
import { promisify } from 'util'
import path from 'path'
import fs from 'fs-extra'
import chokidar from 'chokidar'

const execAsync = promisify(exec)
// 🚫 黑名单列表
const IGNORE_DIRS = ['.git', 'node_modules', 'dist', 'build', '.vscode', '.idea']

export class SecurityManager {
private rootPath = ''
private watcher: chokidar.FSWatcher | null = null
private isWindows = process.platform === 'win32'

/**
* 🔒 开启“完全独占模式”
*/
async lockProject(dirPath: string) {
this.rootPath = dirPath
console.log(`🛡️ 正在开启独占模式 (智能排除): ${dirPath}`)

// 1. 系统级权限锁定 (递归)
// await this.applySystemLock(dirPath, true)
// 1. 智能遍历并锁定
await this.applySmartLock(dirPath, true)

// 2. 启动哨兵 (防止漏网之鱼)
this.startSentry(dirPath)
}

/**
* 🔓 关闭独占模式 (恢复权限)
*/
async unlockProject() {
if (!this.rootPath) return

console.log(`🔓 正在解除独占模式...`)

// 1. 关闭哨兵
if (this.watcher) {
await this.watcher.close()
this.watcher = null
}

// 2. 恢复系统权限
await this.applySystemLock(this.rootPath, false, true) // true 表示强制递归
this.rootPath = ''
}

/**
* ✏️ 特权写入文件
*/
async privilegedWrite(filePath: string, content: string) {
// 1. 临时解锁单个文件
await this.unlockSingleFile(filePath)

try {
// 2. 写入
await fs.writeFile(filePath, content, 'utf-8')
} finally {
// 3. 立即回锁
await this.lockSingleFile(filePath)
}
}

/**
* 📄 特权创建文件/文件夹
*/
async privilegedCreate(targetPath: string, type: 'file' | 'directory') {
const parentDir = path.dirname(targetPath)

// 1. 解锁父目录 (才有权限创建子项)
await this.unlockSingleFile(parentDir)

try {
if (type === 'directory') {
await fs.ensureDir(targetPath)
} else {
await fs.ensureFile(targetPath)
}
} finally {
// 3. 回锁父目录 & 锁定新文件
await this.lockSingleFile(parentDir)
await this.lockSingleFile(targetPath)
}
}

/**
* 🗑️ 特权删除
*/
async privilegedDelete(targetPath: string) {
const parentDir = path.dirname(targetPath)

// 1. 解锁父目录 (才有权限删除子项) 和 目标本身
await this.unlockSingleFile(parentDir)
await this.unlockSingleFile(targetPath) // 防止自身只读无法删除

try {
await fs.remove(targetPath)
} finally {
// 3. 回锁父目录
await this.lockSingleFile(parentDir)
}
}

/**
* ✨ 特权重命名 (或移动)
* 逻辑:解锁旧父目录 & 文件 -> 重命名 -> 锁旧父目录 & 锁新文件
*/
async privilegedRename(oldPath: string, newPath: string) {
const oldParent = path.dirname(oldPath)
const newParent = path.dirname(newPath)

// 判断是否是移动到另一个文件夹 (通常重命名是在同一个文件夹,但也支持移动)
const isMove = oldParent !== newParent

// 1. 解锁阶段
// 解锁旧父目录 (允许从中移除条目)
await this.unlockSingleFile(oldParent)

// 解锁文件本身 (允许修改其元数据/路径,Windows 上 Deny Delete 也会阻止重命名)
await this.unlockSingleFile(oldPath)

// 如果是移动到不同目录,还需要解锁目标父目录
if (isMove) {
await this.unlockSingleFile(newParent)
}

try {
// 2. 执行重命名
await fs.rename(oldPath, newPath)
} finally {
// 3. 回锁阶段 (无论成功失败,都要尝试恢复锁)

// 恢复旧父目录
await this.lockSingleFile(oldParent)

// 如果是移动,恢复新父目录
if (isMove) {
await this.lockSingleFile(newParent)
}

// ⚠️ 关键:新路径下的文件必须被锁住 (因为重命名后,原有的路径失效,新路径可能失去了锁)
// (虽然在同一个盘符下移动通常保留 ACL,但为了保险起见,显式锁一次)
if (await fs.pathExists(newPath)) {
await this.lockSingleFile(newPath)
}
}
}

// =========================================
// 底层系统命令实现
// =========================================

// =========================================
// 核心优化:智能锁定逻辑
// =========================================

private async applySmartLock(basePath: string, isLock: boolean) {
// 读取根目录下的第一级内容
const items = await fs.readdir(basePath, { withFileTypes: true })

for (const item of items) {
const fullPath = path.join(basePath, item.name)

// 1. 检查是否在黑名单中
if (IGNORE_DIRS.includes(item.name)) {
console.log(`⏩ 跳过忽略目录: ${item.name}`)
continue
}

// 2. 执行锁定
if (item.isDirectory()) {
// 如果是普通文件夹 (如 src),执行递归锁定 (/T)
// 这样 src 下面有 1000 个文件也会被一次性锁住,效率很高
await this.applySystemLock(fullPath, isLock, true)
} else {
// 如果是文件,直接锁
await this.lockSingleFile(fullPath)
}
}

// 最后,别忘了锁根目录本身(防止根目录被重命名),但不递归
// 注意:这里我们只锁根目录的“属性”,不加 /T
await this.lockSingleFile(basePath)
}
// 修改底层的命令执行方法,增加 recursive 参数
private async applySystemLock(targetPath: string, isLock: boolean, recursive: boolean) {
if (this.isWindows) {
// Windows: 使用 icacls 拒绝写入和删除
// /deny Everyone:(OI)(CI)(DE,DC,WD,AD,WEA,WA)
// OI: Object Inherit (对象继承)
// CI: Container Inherit (容器继承)
// DE: Delete, DC: Delete Child
// WD: Write Data, AD: Append Data
const perm = isLock
? '/deny Everyone:(OI)(CI)(DE,DC,WD,AD,WEA,WA)'
: '/remove:d Everyone'
// 关键:只有 recursive 为 true 时才加 /T
const recursiveFlag = recursive ? '/T' : ''
const cmd = `icacls "${targetPath}" ${perm} ${recursiveFlag} /C /Q`
try { await execAsync(cmd) } catch (e) {}
// const cmd = isLock
// ? `icacls "${targetPath}" /deny Everyone:(OI)(CI)(DE,DC,WD,AD,WEA,WA) /T /C /Q`
// : `icacls "${targetPath}" /remove:d Everyone /T /C /Q`

// try { await execAsync(cmd) } catch (e) { /* 忽略部分权限错误 */ }

} else {
// Mac/Linux: 移除写权限 (chmod -R a-w)
// 注意:这在 Linux 上不能完全防止拥有者删除,但能防止修改
// Mac/Linux
const perm = isLock ? 'a-w' : 'u+w'
const recursiveFlag = recursive ? '-R' : ''
const cmd = `chmod ${recursiveFlag} ${perm} "${targetPath}"`
try { await execAsync(cmd) } catch (e) {}
}
}

private async lockSingleFile(targetPath: string) {
if (this.isWindows) {
// 锁定单个文件 (不带 /T 递归)
await execAsync(`icacls "${targetPath}" /deny Everyone:(DE,DC,WD,AD,WEA,WA) /C /Q`)
} else {
await execAsync(`chmod a-w "${targetPath}"`)
}
}

private async unlockSingleFile(targetPath: string) {
if (this.isWindows) {
await execAsync(`icacls "${targetPath}" /remove:d Everyone /C /Q`)
} else {
await execAsync(`chmod u+w "${targetPath}"`)
}
}

// =========================================
// 哨兵模式:防止外部强行创建文件
// =========================================
private startSentry(dirPath: string) {
this.watcher = chokidar.watch(dirPath, {
ignored: /(^|[\/\\])\../,
ignoreInitial: true, // 不处理已存在的文件
persistent: true
})

// 如果检测到有新文件/文件夹被外部创建(此时我们没有调用 privilegedCreate)
// 说明用户可能通过更高权限强行塞了文件进来
// 策略:立即锁定它,或者根据需求直接删除它
this.watcher.on('add', async (filePath) => {
console.warn(`🚨 检测到外部新增文件: ${filePath},正在强制锁定...`)
await this.lockSingleFile(filePath)
})

this.watcher.on('addDir', async (p) => {
await this.lockSingleFile(p)
})
}
}

export const securityManager = new SecurityManager()

在 src\main\index.ts 中引入 SecurityManager 并初始化 projectIPC

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
// src/main/index.ts
import { securityManager } from './ipc/security-manager'
import {setupProjectIPC } from './ipc/project-ipc'

app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')

// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})



// setupWindowIPC()
// setupLockIPC()
// setupSecurityIPC()
setupProjectIPC()
createWindow()

app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

app.on('will-quit', async () => {
// 确保退出时解锁,否则你的文件夹会被锁死,需要管理员权限修复
await securityManager.unlockProject()
})

然后是 preload.ts 文件,用于在渲染进程中注入一些全局变量

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
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'

// Custom APIs for renderer
// 自定义 API
const api = {
// 调用主进程检测坐标
checkCursorOutside: () => ipcRenderer.invoke('check-cursor-outside-window'),
activateCurrentWindow: () => ipcRenderer.invoke('activate-current-window'),


}


const projectAPI = {
open: () => ipcRenderer.invoke('prj-open'),
close: () => ipcRenderer.invoke('prj-close'),

// 切换模式:传 true 开启独占,传 false 关闭
toggleLock: (locked: boolean) => ipcRenderer.invoke('prj-toggle-lock', locked),

// 统一的操作接口
writeFile: (path: string, content: string) => ipcRenderer.invoke('prj-write', { path, content }),
create: (path: string, type: 'file' | 'directory') => ipcRenderer.invoke('prj-create', { path, type }),
delete: (path: string) => ipcRenderer.invoke('prj-delete', path),

readDir: (path: string) => ipcRenderer.invoke('fs-read-project-tree', path),
// ✅ 新增
rename: (oldPath: string, newName: string) =>
ipcRenderer.invoke('prj-rename', { oldPath, newName }),
readFile: (path: string) => ipcRenderer.invoke('prj-read', path),
}

// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
contextBridge.exposeInMainWorld('lockAPI', lockAPI)
contextBridge.exposeInMainWorld('fsAPI', fsAPI)
contextBridge.exposeInMainWorld('securityAPI', securityAPI)
contextBridge.exposeInMainWorld('projectAPI', projectAPI)

} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
// @ts-ignore
window.projectAPI = projectAPI

}


对应的ts文件

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/preload/index.d.ts
import { ElectronAPI } from '@electron-toolkit/preload'

interface ProjectAPI {
open: () => Promise<string | null>;
close: () => Promise<void>;
toggleLock: (locked: boolean) => Promise<boolean>;
writeFile: (path: string, content: string) => Promise<void>;
create: (path: string, type: 'file' | 'directory') => Promise<void>;
delete: (path: string) => Promise<void>;
readDir: (path: string) => Promise<{ tree: FileNode[], projectInfo: string }>;
rename: (oldPath: string, newName: string) => Promise<string>;
readFile: (path: string) => Promise<string>;
}
declare global {
interface Window {
electron: ElectronAPI
api: {
checkCursorOutside: () => Promise<boolean>
activateCurrentWindow: () => Promise<boolean>
},

projectAPI:ProjectAPI

}
}

新建LockEditor.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
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
// src/renderer/components/LockEditor.vue
<template>
<div class="security-ide">
<!-- 1. 顶部状态栏 -->
<header class="header">
<div class="controls">
<button
class="btn primary"
@click="handleOpenProject"
:disabled="isLocked"
>
🛡️ 打开并锁定工程
</button>
<button
class="btn danger"
@click="handleUnlockProject"
:disabled="!isLocked"
>
🔓 解锁并关闭
</button>
</div>
<div class="status">
状态:
<span :class="['badge', isLocked ? 'locked' : 'unlocked']">
{{ isLocked ? 'ACL 权限独占中' : '普通模式' }}
</span>
<span class="path" v-if="projectPath">{{ projectPath }}</span>
</div>
</header>

<div>
<!-- ✨ 一键切换开关 -->
<label class="switch-container">
<input
type="checkbox"
v-model="isLocked_back"
@change="handleToggleLock"
/>
<span class="slider"></span>
<span class="label-text">{{ isLocked_back ? '🛡️ 独占模式' : '🔓 普通模式' }}</span>
</label>
</div>

<!-- 2. 主体内容 -->
<main class="workspace">
<!-- 左侧文件列表 -->
<aside class="sidebar" @contextmenu.prevent="onSidebarRightClick">
<div class="sidebar-title">资源管理器</div>

<div v-if="!files.length" class="empty-list">
{{ isLocked ? '目录为空' : '未打开工程' }}
</div>

<ul class="file-list">
<li
v-for="file in files"
:key="file.path"
class="file-item"
:class="{ active: currentFile?.path === file.path }"
@click="selectFile(file)"
@contextmenu.stop="onFileRightClick($event, file)"
>
<span class="icon">{{ file.type=== 'directory' ? '📁' : '📄' }}</span>
<span class="name">{{ file.name }}</span>
</li>
</ul>
</aside>

<!-- 右侧编辑器 -->
<section class="editor">
<div v-if="!currentFile" class="welcome">
<h3>验证说明</h3>
<ol>
<li>点击左上角打开一个测试文件夹。</li>
<li><strong>验证锁定</strong>:打开系统资源管理器,尝试删除该文件夹下的文件,系统应提示“需要权限”。</li>
<li><strong>验证特权</strong>:在下方编辑器修改内容,按 Ctrl+S 保存,应成功。</li>
<li><strong>验证管理</strong>:在左侧列表右键,尝试新建或删除,应成功。</li>
</ol>
</div>
<div v-else class="code-wrapper">
<div class="tab-header">
{{ currentFile.name }}
<span v-if="isDirty" class="dot"></span>
</div>
<textarea
class="code-area"
v-model="fileContent"
@input="isDirty = true"
@keydown.ctrl.s.prevent="saveFile"
@keydown.meta.s.prevent="saveFile"
></textarea>
</div>
</section>
</main>

<!-- 3. 底部日志 -->
<footer class="logger">
<div class="log-line" v-for="(log, idx) in logs" :key="idx">
<span class="time">[{{ log.time }}]</span> {{ log.msg }}
</div>
</footer>

<!-- 4. 自定义右键菜单 -->
<div
v-if="contextMenu.visible"
class="ctx-menu"
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
>
<div class="menu-item disabled" v-if="contextMenu.target">
操作: {{ contextMenu.target.name }}
</div>
<div class="divider"></div>
<div class="menu-item" @click="handleCreate('file')">✨ 特权新建文件</div>
<div class="menu-item" @click="handleCreate('directory')">📁 特权新建文件夹</div>
<div class="menu-item danger" @click="handleDelete" v-if="contextMenu.target">
🗑️ 特权删除
</div>
</div>

<!-- ✅ 新增:自定义输入弹窗 -->
<div v-if="inputBox.visible" class="modal-overlay">
<div class="modal-content">
<div class="modal-title">{{ inputBox.title }}</div>
<input
id="custom-input"
v-model="inputBox.value"
@keydown.enter="confirmInput"
@keydown.esc="cancelInput"
class="modal-input"
type="text"
autocomplete="off"
/>
<div class="modal-actions">
<button class="btn" @click="cancelInput">取消</button>
<button class="btn primary" @click="confirmInput">确定</button>
</div>
</div>
</div>
</div>
</template>

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

// --- 类型定义 ---
interface FileNode {
name: string
path: string
type: string
children?: FileNode[]
}

interface Log {
time: string
msg: string
}

// --- 状态数据 ---
const isLocked = ref(false)
const isLocked_back = ref(false)
const projectPath = ref('')
const files = ref<FileNode[]>([])
const currentFile = ref<FileNode | null>(null)
const fileContent = ref('')
const isDirty = ref(false)
const logs = ref<Log[]>([])

// 右键菜单状态
const contextMenu = reactive({
visible: false,
x: 0,
y: 0,
target: null as FileNode | null // null 代表在空白处点击
})


// --- 新增:Input 弹窗的状态 ---
const inputBox = reactive({
visible: false,
title: '',
value: '',
resolve: null as ((val: string | null) => void) | null
})

// --- 新增:封装一个 Promise 化的输入框函数 ---
const showInputBox = (title: string, defaultValue = '') => {
inputBox.title = title
inputBox.value = defaultValue
inputBox.visible = true

// 自动聚焦输入框 (使用 nextTick)
setTimeout(() => {
document.getElementById('custom-input')?.focus()
}, 100)

return new Promise<string | null>((resolve) => {
inputBox.resolve = resolve
})
}

// --- 新增:弹窗确认与取消 ---
const confirmInput = () => {
if (inputBox.resolve) inputBox.resolve(inputBox.value)
inputBox.visible = false
}

const cancelInput = () => {
if (inputBox.resolve) inputBox.resolve(null)
inputBox.visible = false
}

// --- 日志工具 ---
const addLog = (msg: string) => {
const time = new Date().toLocaleTimeString()
logs.value.unshift({ time, msg })
}


// --- 1. 工程操作 ---

const handleOpenProject = async () => {
try {
addLog('正在请求系统权限锁定...')
// const path = await window.securityAPI.openProject()
const path = await window.projectAPI.open()
if (path) {
projectPath.value = path
isLocked.value = true
addLog(`✅ 工程已锁定: ${path}`)
addLog('🔒 此时外部应用无法写入或删除此目录内容')
await refreshFiles()
} else {
addLog('用户取消了操作')
}
} catch (e: any) {
addLog(`❌ 锁定失败: ${e.message}`)
}
}

// 2. ✨ 核心:切换模式
const handleToggleLock = async () => {
try {
console.log('正在切换模式...', isLocked_back.value)
// 直接调用统一接口,后端负责上锁或解锁
await window.projectAPI.toggleLock(isLocked_back.value)
alert(isLocked_back.value ? '已开启独占锁定!外部无法修改文件。' : '已切换回普通模式。')
} catch (e) {
console.error(e)
// 如果切换失败(比如权限不够),回滚开关状态
isLocked_back.value = !isLocked_back.value
alert('切换失败,请检查日志')
}
}

const handleUnlockProject = async () => {
try {
// await window.securityAPI.closeProject()
await window.projectAPI.close()
isLocked.value = false
projectPath.value = ''
files.value = []
currentFile.value = null
addLog('🔓 工程已解锁,恢复系统默认权限')
} catch (e: any) {
addLog(`❌ 解锁失败: ${e.message}`)
}
}

const refreshFiles = async () => {
if (!projectPath.value) return
try {
// 假设后端 ipc 实现了 sec-read-dir
// 如果没有实现,请在后端补充 fs.readdir 逻辑
const {tree} = await window.projectAPI.readDir(projectPath.value)
files.value = tree
console.log('Files:', files.value)
} catch (e) {
addLog('读取文件列表失败 (请确认后端实现了 readDir)')
}
}

// --- 2. 文件操作 ---

const selectFile = async (file: FileNode) => {
if (file.type == 'directory') return
try {
// 这里为了演示简单,假设你有一个 readFile 的 API
// 或者我们直接显示一段 Mock 内容,重点验证 Save 功能
const content = await window.projectAPI.readFile(file.path)
currentFile.value = file
// fileContent.value = content
fileContent.value = `// 这是文件 ${file.name}\n// 它是只读的 (对于外部程序)\n// 试着修改并按 Ctrl+S 保存。\n// ${content}`
isDirty.value = false
addLog(`已打开: ${file.name}`)
} catch (e) {
addLog('读取文件内容失败')
}
}

const saveFile = async () => {
if (!currentFile.value) return
addLog(`正在通过特权通道保存 ${currentFile.value.name}...`)

try {
// ✨ 核心验证点:特权写入
// await window.securityAPI.writeFile(currentFile.value.path, fileContent.value)
await window.projectAPI.writeFile(currentFile.value.path, fileContent.value)
isDirty.value = false
addLog(`✅ 保存成功!(ACL 权限临时提权完成)`)
} catch (e: any) {
addLog(`❌ 保存失败: ${e.message}`)
}
}

// --- 3. 右键菜单与新建/删除 ---

const onSidebarRightClick = (e: MouseEvent) => {
showMenu(e.clientX, e.clientY, null)
}

const onFileRightClick = (e: MouseEvent, file: FileNode) => {
showMenu(e.clientX, e.clientY, file)
}

const showMenu = (x: number, y: number, target: FileNode | null) => {
contextMenu.visible = true
contextMenu.x = x
contextMenu.y = y
contextMenu.target = target
}

const closeMenu = () => { contextMenu.visible = false }

const handleCreate = async (type: 'file' | 'directory') => {
closeMenu()
if (!projectPath.value) return

// const name = prompt(`请输入${type === 'file' ? '文件' : '文件夹'}名称:`)
// ✅ 替换为自定义弹窗:
const name = await showInputBox(`请输入${type === 'file' ? '文件' : '文件夹'}名称:`)
if (!name) return

const targetPath = `${projectPath.value}/${name}` // 简单拼接,实际应用请用 path.join

try {
addLog(`正在创建: ${name}...`)
// ✨ 核心验证点:特权创建
await window.projectAPI.create(targetPath, type)
addLog(`✅ 创建成功`)
await refreshFiles()
} catch (e: any) {
addLog(`❌ 创建失败: ${e.message}`)
}
}

const handleDelete = async () => {
closeMenu()
const target = contextMenu.target
if (!target) return

if (!confirm(`确定要强制删除 ${target.name} 吗?`)) return

try {
addLog(`正在删除: ${target.name}...`)
// ✨ 核心验证点:特权删除
// await window.securityAPI.delete(target.path)
// 💥 同样不需要判断模式
await window.projectAPI.delete(target.path)
addLog(`✅ 删除成功`)
if (currentFile.value?.path === target.path) {
currentFile.value = null
fileContent.value = ''
}
await refreshFiles()
} catch (e: any) {
addLog(`❌ 删除失败: ${e.message}`)
}
}

const handleRename = async (file: any) => {
const newName = await showInputBox('请输入新名称', file.name)
if (!newName || newName === file.name) return

try {
// ✨ 无论是否锁定,直接调用统一接口
await window.projectAPI.rename(file.path, newName)

console.log('重命名成功')
// 刷新文件列表
await refreshFiles()
} catch (e) {
console.error('重命名失败', e)
alert('重命名失败:权限不足或文件被占用')
}
}

// 全局点击关闭菜单
onMounted(() => {
window.addEventListener('click', closeMenu)
})

onBeforeUnmount(() => {
window.removeEventListener('click', closeMenu)
// 防止开发时刷新页面导致锁死
if (isLocked.value) {
window.projectAPI.close()
}
})
</script>

<style scoped>
.security-ide {
display: flex;
flex-direction: column;
height: 100vh;
background: #1e1e1e;
color: #ccc;
font-family: 'Segoe UI', sans-serif;
user-select: none;
}

/* Header */
.header {
height: 50px;
background: #252526;
border-bottom: 1px solid #111;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
font-weight: bold;
color: white;
opacity: 0.9;
}
.btn:hover:not(:disabled) { opacity: 1; transform: translateY(-1px); }
.btn:disabled { opacity: 0.3; cursor: not-allowed; }
.primary { background: #0e639c; }
.danger { background: #ab1212; }

.status { font-size: 13px; display: flex; align-items: center; gap: 8px;}
.badge { padding: 2px 6px; border-radius: 3px; font-size: 12px; color: #111; font-weight: bold;}
.badge.locked { background: #4caf50; }
.badge.unlocked { background: #888; }
.path { color: #888; font-family: monospace; }

/* Workspace */
.workspace { flex: 1; display: flex; overflow: hidden; }

.sidebar {
width: 250px;
background: #252526;
border-right: 1px solid #111;
display: flex;
flex-direction: column;
}
.sidebar-title {
padding: 10px 16px; font-size: 11px; font-weight: bold; color: #888; text-transform: uppercase;
}
.empty-list { padding: 20px; text-align: center; color: #555; font-size: 13px; }
.file-list { list-style: none; padding: 0; margin: 0; overflow-y: auto; }
.file-item {
padding: 4px 16px; cursor: pointer; display: flex; align-items: center; font-size: 13px;
}
.file-item:hover { background: #2a2d2e; }
.file-item.active { background: #37373d; color: white; }
.icon { margin-right: 6px; }

.editor { flex: 1; display: flex; flex-direction: column; background: #1e1e1e; }
.welcome {
flex: 1; display: flex; flex-direction: column;
justify-content: center; align-items: center; color: #666;
padding: 40px; line-height: 1.8;
}
.welcome ol { text-align: left; }
.code-wrapper { display: flex; flex-direction: column; height: 100%; }
.tab-header {
background: #2d2d2d; height: 35px; display: flex; align-items: center;
padding: 0 16px; font-size: 13px; border-top: 1px solid #007fd4; color: white;
}
.dot { margin-left: 8px; font-size: 20px; line-height: 10px; }
.code-area {
flex: 1; background: #1e1e1e; color: #d4d4d4; border: none; resize: none;
padding: 16px; font-family: 'Consolas', monospace; outline: none; font-size: 14px;
}

/* Logger */
.logger {
height: 120px; background: #111; border-top: 1px solid #333;
padding: 8px; overflow-y: auto; font-family: monospace; font-size: 12px;
}
.log-line { margin-bottom: 4px; color: #ccc; }
.time { color: #569cd6; margin-right: 8px; }

/* Context Menu */
.ctx-menu {
position: fixed; background: #252526; border: 1px solid #454545;
box-shadow: 0 4px 10px rgba(0,0,0,0.5); z-index: 9999;
min-width: 160px; border-radius: 4px; padding: 4px 0;
}
.menu-item {
padding: 6px 12px; cursor: pointer; font-size: 13px; color: #ccc;
}
.menu-item:hover:not(.disabled) { background: #094771; color: white; }
.menu-item.disabled { color: #666; cursor: default; border-bottom: 1px solid #333; padding-bottom: 8px; margin-bottom: 4px;}
.menu-item.danger:hover { background: #881111; }
.divider { height: 1px; background: #454545; margin: 4px 0; }


/* ✅ 新增:弹窗样式 */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
}

.modal-content {
background: #252526;
border: 1px solid #454545;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
width: 300px;
padding: 16px;
border-radius: 4px;
}

.modal-title {
margin-bottom: 12px;
font-size: 14px;
color: #ccc;
font-weight: bold;
}

.modal-input {
width: 100%;
background: #3c3c3c;
border: 1px solid #3c3c3c;
color: white;
padding: 6px;
outline: 1px solid transparent;
margin-bottom: 16px;
font-family: inherit;
}

.modal-input:focus {
outline-color: #0e639c;
}

.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>

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