zen-gitsync 1.5.6 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/package.json +10 -3
- package/src/config.js +2 -1
- package/src/gitCommit.js +7 -0
- package/src/ui/client/README.md +5 -0
- package/src/ui/client/auto-imports.d.ts +10 -0
- package/src/ui/client/components.d.ts +31 -0
- package/src/ui/client/index.html +13 -0
- package/src/ui/client/package.json +27 -0
- package/src/ui/client/public/favicon.svg +27 -0
- package/src/ui/client/public/logo.svg +27 -0
- package/src/ui/client/public/vite.svg +1 -0
- package/src/ui/client/src/App.vue +539 -0
- package/src/ui/client/src/assets/logo.svg +27 -0
- package/src/ui/client/src/components/CommitForm.vue +904 -0
- package/src/ui/client/src/components/GitStatus.vue +799 -0
- package/src/ui/client/src/components/LogList.vue +270 -0
- package/src/ui/client/src/main.ts +4 -0
- package/src/ui/client/src/vite-env.d.ts +1 -0
- package/src/ui/client/stats.html +4949 -0
- package/src/ui/client/tsconfig.app.json +14 -0
- package/src/ui/client/tsconfig.json +7 -0
- package/src/ui/client/tsconfig.node.json +24 -0
- package/src/ui/client/vite.config.ts +48 -0
- package/src/ui/public/assets/index-BHmYZROy.css +1 -0
- package/src/ui/public/assets/index-kfMX1bxz.js +9 -0
- package/src/ui/public/assets/vendor-Dp0FkvMe.css +1 -0
- package/src/ui/public/assets/vendor-DxvF30ca.js +41 -0
- package/src/ui/public/favicon.svg +27 -0
- package/src/ui/public/index.html +16 -0
- package/src/ui/public/logo.svg +27 -0
- package/src/ui/public/vite.svg +1 -0
- package/src/ui/server/index.js +598 -0
- package/src/utils/index.js +4 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, defineExpose } from 'vue'
|
|
3
|
+
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
4
|
+
// import { io } from 'socket.io-client'
|
|
5
|
+
import { Refresh, ArrowLeft, ArrowRight, Folder, Document, ArrowUp, RefreshRight } from '@element-plus/icons-vue'
|
|
6
|
+
|
|
7
|
+
const status = ref('加载中...')
|
|
8
|
+
// const socket = io()
|
|
9
|
+
const isRefreshing = ref(false)
|
|
10
|
+
const fileList = ref<{path: string, type: string}[]>([])
|
|
11
|
+
const selectedFile = ref('')
|
|
12
|
+
const diffContent = ref('')
|
|
13
|
+
const diffDialogVisible = ref(false)
|
|
14
|
+
const isLoadingDiff = ref(false)
|
|
15
|
+
// 添加当前文件索引
|
|
16
|
+
const currentFileIndex = ref(-1)
|
|
17
|
+
// 添加切换目录相关的状态
|
|
18
|
+
const isDirectoryDialogVisible = ref(false)
|
|
19
|
+
const newDirectoryPath = ref('')
|
|
20
|
+
const isChangingDirectory = ref(false)
|
|
21
|
+
// 添加目录浏览相关的状态
|
|
22
|
+
const isDirectoryBrowserVisible = ref(false)
|
|
23
|
+
const currentBrowsePath = ref('')
|
|
24
|
+
const directoryItems = ref<{name: string, path: string, type: string}[]>([])
|
|
25
|
+
const isBrowsing = ref(false)
|
|
26
|
+
const browseErrorMessage = ref('')
|
|
27
|
+
|
|
28
|
+
// 解析 git status 输出,提取文件及类型
|
|
29
|
+
function parseStatus(statusText: string) {
|
|
30
|
+
if (statusText === undefined) return
|
|
31
|
+
const lines = statusText.split('\n')
|
|
32
|
+
const files: {path: string, type: string}[] = []
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
// 匹配常见的 git status --porcelain 格式
|
|
35
|
+
// M: 修改, A: 新增, D: 删除, ??: 未跟踪
|
|
36
|
+
const match = line.match(/^([ MADRCU\?]{2})\s+(.+)$/)
|
|
37
|
+
if (match) {
|
|
38
|
+
let type = ''
|
|
39
|
+
const code = match[1].trim()
|
|
40
|
+
if (code === 'M' || code === 'MM' || code === 'AM' || code === 'RM') type = 'modified'
|
|
41
|
+
else if (code === 'A' || code === 'AA') type = 'added'
|
|
42
|
+
else if (code === 'D' || code === 'AD' || code === 'DA') type = 'deleted'
|
|
43
|
+
else if (code === '??') type = 'untracked'
|
|
44
|
+
else type = 'other'
|
|
45
|
+
files.push({ path: match[2], type })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
fileList.value = files
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const currentDirectory = ref('')
|
|
52
|
+
async function loadStatus() {
|
|
53
|
+
try {
|
|
54
|
+
isRefreshing.value = true
|
|
55
|
+
// 获取当前工作目录
|
|
56
|
+
const responseDir = await fetch('/api/current_directory')
|
|
57
|
+
const dirData = await responseDir.json()
|
|
58
|
+
currentDirectory.value = dirData.directory || '未知目录'
|
|
59
|
+
|
|
60
|
+
// 如果不是Git仓库,显示提示并返回
|
|
61
|
+
if (dirData.isGitRepo === false) {
|
|
62
|
+
status.value = '当前目录不是一个Git仓库'
|
|
63
|
+
fileList.value = []
|
|
64
|
+
ElMessage.warning('当前目录不是一个Git仓库')
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const response = await fetch('/api/status')
|
|
69
|
+
const data = await response.json()
|
|
70
|
+
status.value = data.status
|
|
71
|
+
|
|
72
|
+
const response_porcelain = await fetch('/api/status_porcelain')
|
|
73
|
+
const data_porcelain = await response_porcelain.json()
|
|
74
|
+
parseStatus(data_porcelain.status)
|
|
75
|
+
ElMessage({
|
|
76
|
+
message: 'Git 状态已刷新',
|
|
77
|
+
type: 'success',
|
|
78
|
+
})
|
|
79
|
+
} catch (error) {
|
|
80
|
+
status.value = '加载状态失败: ' + (error as Error).message
|
|
81
|
+
fileList.value = []
|
|
82
|
+
ElMessage({
|
|
83
|
+
message: '刷新失败: ' + (error as Error).message,
|
|
84
|
+
type: 'error',
|
|
85
|
+
})
|
|
86
|
+
} finally {
|
|
87
|
+
isRefreshing.value = false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 格式化差异内容,添加颜色
|
|
92
|
+
function formatDiff(diffText: string) {
|
|
93
|
+
if (!diffText) return '';
|
|
94
|
+
|
|
95
|
+
// 将差异内容按行分割
|
|
96
|
+
const lines = diffText.split('\n');
|
|
97
|
+
|
|
98
|
+
// 转义 HTML 标签的函数
|
|
99
|
+
function escapeHtml(text: string) {
|
|
100
|
+
return text
|
|
101
|
+
.replace(/&/g, '&')
|
|
102
|
+
.replace(/</g, '<')
|
|
103
|
+
.replace(/>/g, '>')
|
|
104
|
+
.replace(/"/g, '"')
|
|
105
|
+
.replace(/'/g, ''');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 为每行添加适当的 CSS 类
|
|
109
|
+
return lines.map(line => {
|
|
110
|
+
// 先转义 HTML 标签,再添加样式
|
|
111
|
+
const escapedLine = escapeHtml(line);
|
|
112
|
+
|
|
113
|
+
if (line.startsWith('diff --git')) {
|
|
114
|
+
return `<div class="diff-header">${escapedLine}</div>`;
|
|
115
|
+
} else if (line.startsWith('---')) {
|
|
116
|
+
return `<div class="diff-old-file">${escapedLine}</div>`;
|
|
117
|
+
} else if (line.startsWith('+++')) {
|
|
118
|
+
return `<div class="diff-new-file">${escapedLine}</div>`;
|
|
119
|
+
} else if (line.startsWith('@@')) {
|
|
120
|
+
return `<div class="diff-hunk-header">${escapedLine}</div>`;
|
|
121
|
+
} else if (line.startsWith('+')) {
|
|
122
|
+
return `<div class="diff-added">${escapedLine}</div>`;
|
|
123
|
+
} else if (line.startsWith('-')) {
|
|
124
|
+
return `<div class="diff-removed">${escapedLine}</div>`;
|
|
125
|
+
} else {
|
|
126
|
+
return `<div class="diff-context">${escapedLine}</div>`;
|
|
127
|
+
}
|
|
128
|
+
}).join('');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 获取文件差异
|
|
132
|
+
async function getFileDiff(filePath: string) {
|
|
133
|
+
try {
|
|
134
|
+
isLoadingDiff.value = true
|
|
135
|
+
selectedFile.value = filePath
|
|
136
|
+
// 设置当前文件索引
|
|
137
|
+
currentFileIndex.value = fileList.value.findIndex(file => file.path === filePath)
|
|
138
|
+
const response = await fetch(`/api/diff?file=${encodeURIComponent(filePath)}`)
|
|
139
|
+
const data = await response.json()
|
|
140
|
+
diffContent.value = data.diff || '没有变更'
|
|
141
|
+
diffDialogVisible.value = true
|
|
142
|
+
} catch (error) {
|
|
143
|
+
ElMessage({
|
|
144
|
+
message: '获取文件差异失败: ' + (error as Error).message,
|
|
145
|
+
type: 'error',
|
|
146
|
+
})
|
|
147
|
+
diffContent.value = '获取差异失败: ' + (error as Error).message
|
|
148
|
+
} finally {
|
|
149
|
+
isLoadingDiff.value = false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 添加切换到上一个文件的方法
|
|
154
|
+
async function goToPreviousFile() {
|
|
155
|
+
if (fileList.value.length === 0 || currentFileIndex.value <= 0) return
|
|
156
|
+
|
|
157
|
+
const newIndex = currentFileIndex.value - 1
|
|
158
|
+
const prevFile = fileList.value[newIndex]
|
|
159
|
+
await getFileDiff(prevFile.path)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 添加切换到下一个文件的方法
|
|
163
|
+
async function goToNextFile() {
|
|
164
|
+
if (fileList.value.length === 0 || currentFileIndex.value >= fileList.value.length - 1) return
|
|
165
|
+
|
|
166
|
+
const newIndex = currentFileIndex.value + 1
|
|
167
|
+
const nextFile = fileList.value[newIndex]
|
|
168
|
+
await getFileDiff(nextFile.path)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 打开切换目录对话框
|
|
172
|
+
function openDirectoryDialog() {
|
|
173
|
+
newDirectoryPath.value = currentDirectory.value
|
|
174
|
+
isDirectoryDialogVisible.value = true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 打开目录浏览器
|
|
178
|
+
function openDirectoryBrowser() {
|
|
179
|
+
browseErrorMessage.value = ''
|
|
180
|
+
currentBrowsePath.value = newDirectoryPath.value || currentDirectory.value
|
|
181
|
+
isDirectoryBrowserVisible.value = true
|
|
182
|
+
browseDirectory(currentBrowsePath.value)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 浏览目录
|
|
186
|
+
async function browseDirectory(directoryPath: string) {
|
|
187
|
+
try {
|
|
188
|
+
isBrowsing.value = true
|
|
189
|
+
browseErrorMessage.value = ''
|
|
190
|
+
|
|
191
|
+
// 确保Windows盘符路径格式正确
|
|
192
|
+
let normalizedPath = directoryPath
|
|
193
|
+
if (/^[A-Za-z]:$/.test(normalizedPath)) {
|
|
194
|
+
normalizedPath += '/'
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const response = await fetch(`/api/browse_directory?path=${encodeURIComponent(normalizedPath)}`)
|
|
198
|
+
|
|
199
|
+
if (response.status === 403) {
|
|
200
|
+
const data = await response.json()
|
|
201
|
+
browseErrorMessage.value = data.error || '目录浏览功能未启用'
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const data = await response.json()
|
|
207
|
+
browseErrorMessage.value = data.error || '获取目录内容失败'
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const data = await response.json()
|
|
212
|
+
|
|
213
|
+
if (data.success) {
|
|
214
|
+
directoryItems.value = data.items
|
|
215
|
+
currentBrowsePath.value = data.currentPath
|
|
216
|
+
} else {
|
|
217
|
+
browseErrorMessage.value = data.error || '获取目录内容失败'
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
browseErrorMessage.value = `获取目录内容失败: ${(error as Error).message}`
|
|
221
|
+
} finally {
|
|
222
|
+
isBrowsing.value = false
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 导航到父目录
|
|
227
|
+
function navigateToParent() {
|
|
228
|
+
// 检查是否已经是根目录
|
|
229
|
+
// Windows盘符根目录情况 (如 "E:")
|
|
230
|
+
if (/^[A-Za-z]:$/.test(currentBrowsePath.value) ||
|
|
231
|
+
/^[A-Za-z]:[\\/]$/.test(currentBrowsePath.value) ||
|
|
232
|
+
currentBrowsePath.value === '/') {
|
|
233
|
+
// 已经是根目录,不做任何操作
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 获取当前路径的父目录
|
|
238
|
+
let pathParts = currentBrowsePath.value.split(/[/\\]/)
|
|
239
|
+
|
|
240
|
+
// 移除最后一个目录部分
|
|
241
|
+
pathParts.pop()
|
|
242
|
+
|
|
243
|
+
// 处理Windows盘符特殊情况
|
|
244
|
+
let parentPath = pathParts.join('/')
|
|
245
|
+
if (pathParts.length === 1 && /^[A-Za-z]:$/.test(pathParts[0])) {
|
|
246
|
+
// 如果只剩下盘符,确保添加斜杠 (例如 "E:/")
|
|
247
|
+
parentPath = pathParts[0] + '/'
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (parentPath) {
|
|
251
|
+
browseDirectory(parentPath)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 选择目录项
|
|
256
|
+
function selectDirectoryItem(item: {name: string, path: string, type: string}) {
|
|
257
|
+
if (item.type === 'directory') {
|
|
258
|
+
browseDirectory(item.path)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 选择当前目录
|
|
263
|
+
function selectCurrentDirectory() {
|
|
264
|
+
newDirectoryPath.value = currentBrowsePath.value
|
|
265
|
+
isDirectoryBrowserVisible.value = false
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 切换工作目录
|
|
269
|
+
async function changeDirectory() {
|
|
270
|
+
if (!newDirectoryPath.value) {
|
|
271
|
+
ElMessage.warning('目录路径不能为空')
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
isChangingDirectory.value = true
|
|
277
|
+
const response = await fetch('/api/change_directory', {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
headers: {
|
|
280
|
+
'Content-Type': 'application/json'
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify({ path: newDirectoryPath.value })
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const result = await response.json()
|
|
286
|
+
|
|
287
|
+
if (result.success) {
|
|
288
|
+
ElMessage.success('已切换工作目录')
|
|
289
|
+
currentDirectory.value = result.directory
|
|
290
|
+
isDirectoryDialogVisible.value = false
|
|
291
|
+
|
|
292
|
+
// 如果新目录不是Git仓库,显示警告
|
|
293
|
+
if (!result.isGitRepo) {
|
|
294
|
+
ElMessage.warning('当前目录不是一个Git仓库')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 刷新状态
|
|
298
|
+
loadStatus()
|
|
299
|
+
} else {
|
|
300
|
+
ElMessage.error(result.error || '切换目录失败')
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
ElMessage.error(`切换目录失败: ${(error as Error).message}`)
|
|
304
|
+
} finally {
|
|
305
|
+
isChangingDirectory.value = false
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 处理文件点击
|
|
310
|
+
function handleFileClick(file: {path: string, type: string}) {
|
|
311
|
+
getFileDiff(file.path)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 文件类型标签显示
|
|
315
|
+
function fileTypeLabel(type: string) {
|
|
316
|
+
switch (type) {
|
|
317
|
+
case 'added': return '新增';
|
|
318
|
+
case 'modified': return '修改';
|
|
319
|
+
case 'deleted': return '删除';
|
|
320
|
+
case 'untracked': return '未跟踪';
|
|
321
|
+
default: return '其他';
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 刷新Git状态的方法
|
|
326
|
+
async function refreshStatus() {
|
|
327
|
+
await loadStatus()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 添加撤回文件修改的方法
|
|
331
|
+
async function revertFileChanges(filePath: string) {
|
|
332
|
+
try {
|
|
333
|
+
// 请求用户确认
|
|
334
|
+
await ElMessageBox.confirm(
|
|
335
|
+
`确定要撤回文件 "${filePath}" 的所有修改吗?此操作无法撤销。`,
|
|
336
|
+
'撤回修改',
|
|
337
|
+
{
|
|
338
|
+
confirmButtonText: '确定',
|
|
339
|
+
cancelButtonText: '取消',
|
|
340
|
+
type: 'warning'
|
|
341
|
+
}
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
// 发送请求到后端API
|
|
345
|
+
const response = await fetch('/api/revert_file', {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
headers: {
|
|
348
|
+
'Content-Type': 'application/json'
|
|
349
|
+
},
|
|
350
|
+
body: JSON.stringify({ filePath })
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const result = await response.json()
|
|
354
|
+
|
|
355
|
+
if (result.success) {
|
|
356
|
+
ElMessage.success('已撤回文件修改')
|
|
357
|
+
// 刷新Git状态
|
|
358
|
+
await loadStatus()
|
|
359
|
+
} else {
|
|
360
|
+
ElMessage.error(result.error || '撤回文件修改失败')
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
// 用户取消或发生错误
|
|
364
|
+
if ((error as Error).message !== 'cancel') {
|
|
365
|
+
ElMessage.error(`撤回文件修改失败: ${(error as Error).message}`)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
onMounted(() => {
|
|
371
|
+
loadStatus()
|
|
372
|
+
|
|
373
|
+
// Socket.io 事件
|
|
374
|
+
// socket.on('status_update', (data: { status: string }) => {
|
|
375
|
+
// status.value = data.status
|
|
376
|
+
// parseStatus(data.status)
|
|
377
|
+
// })
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// onUnmounted(() => {
|
|
381
|
+
// socket.disconnect()
|
|
382
|
+
// })
|
|
383
|
+
|
|
384
|
+
// 暴露刷新方法给父组件
|
|
385
|
+
defineExpose({
|
|
386
|
+
refreshStatus
|
|
387
|
+
})
|
|
388
|
+
</script>
|
|
389
|
+
|
|
390
|
+
<template>
|
|
391
|
+
<div class="card">
|
|
392
|
+
<div class="current-directory">
|
|
393
|
+
<el-icon><Folder /></el-icon>
|
|
394
|
+
<span>{{ currentDirectory }}</span>
|
|
395
|
+
<el-button type="primary" size="small" @click="openDirectoryDialog" plain>
|
|
396
|
+
切换目录
|
|
397
|
+
</el-button>
|
|
398
|
+
</div>
|
|
399
|
+
<div class="status-header">
|
|
400
|
+
<h2>Git 状态</h2>
|
|
401
|
+
<el-button
|
|
402
|
+
type="primary"
|
|
403
|
+
:icon="Refresh"
|
|
404
|
+
circle
|
|
405
|
+
size="small"
|
|
406
|
+
@click="refreshStatus"
|
|
407
|
+
:loading="isRefreshing"
|
|
408
|
+
/>
|
|
409
|
+
</div>
|
|
410
|
+
<div class="status-box">{{ status }}</div>
|
|
411
|
+
<!-- 颜色区分不同类型文件 -->
|
|
412
|
+
<div v-if="fileList.length" class="file-list">
|
|
413
|
+
<div
|
|
414
|
+
v-for="file in fileList"
|
|
415
|
+
:key="file.path"
|
|
416
|
+
:class="['file-item', file.type]"
|
|
417
|
+
>
|
|
418
|
+
<div class="file-info" @click="handleFileClick(file)">
|
|
419
|
+
<span class="file-type">{{ fileTypeLabel(file.type) }}</span>
|
|
420
|
+
<span class="file-path">{{ file.path }}</span>
|
|
421
|
+
</div>
|
|
422
|
+
<div class="file-actions">
|
|
423
|
+
<el-tooltip content="撤回修改" placement="top" :hide-after="1000">
|
|
424
|
+
<el-button
|
|
425
|
+
type="danger"
|
|
426
|
+
size="small"
|
|
427
|
+
:icon="RefreshRight"
|
|
428
|
+
circle
|
|
429
|
+
@click.stop="revertFileChanges(file.path)"
|
|
430
|
+
/>
|
|
431
|
+
</el-tooltip>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<!-- 切换目录对话框 -->
|
|
437
|
+
<el-dialog
|
|
438
|
+
v-model="isDirectoryDialogVisible"
|
|
439
|
+
title="切换工作目录"
|
|
440
|
+
width="500px"
|
|
441
|
+
>
|
|
442
|
+
<el-form>
|
|
443
|
+
<el-form-item label="目录路径">
|
|
444
|
+
<el-input v-model="newDirectoryPath" placeholder="请输入目录路径" clearable />
|
|
445
|
+
<div class="directory-buttons">
|
|
446
|
+
<el-button @click="openDirectoryBrowser" type="primary" plain class="no-padding-left">
|
|
447
|
+
<el-icon><Folder /></el-icon>
|
|
448
|
+
浏览
|
|
449
|
+
</el-button>
|
|
450
|
+
<el-button @click="changeDirectory" :loading="isChangingDirectory" type="primary">
|
|
451
|
+
切换
|
|
452
|
+
</el-button>
|
|
453
|
+
</div>
|
|
454
|
+
</el-form-item>
|
|
455
|
+
</el-form>
|
|
456
|
+
</el-dialog>
|
|
457
|
+
|
|
458
|
+
<!-- 目录浏览对话框 -->
|
|
459
|
+
<el-dialog
|
|
460
|
+
v-model="isDirectoryBrowserVisible"
|
|
461
|
+
title="浏览目录"
|
|
462
|
+
width="600px"
|
|
463
|
+
>
|
|
464
|
+
<div class="browser-current-path">
|
|
465
|
+
<span>当前路径: {{ currentBrowsePath }}</span>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<div v-if="browseErrorMessage" class="browser-error">
|
|
469
|
+
{{ browseErrorMessage }}
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<div v-loading="isBrowsing" class="directory-browser">
|
|
473
|
+
<!-- 导航栏 -->
|
|
474
|
+
<div class="browser-nav">
|
|
475
|
+
<el-button
|
|
476
|
+
@click="navigateToParent"
|
|
477
|
+
:disabled="!currentBrowsePath || isBrowsing"
|
|
478
|
+
size="small"
|
|
479
|
+
class="no-padding-left"
|
|
480
|
+
>
|
|
481
|
+
<el-icon><ArrowUp /></el-icon>
|
|
482
|
+
上级目录
|
|
483
|
+
</el-button>
|
|
484
|
+
<el-button
|
|
485
|
+
@click="selectCurrentDirectory"
|
|
486
|
+
type="primary"
|
|
487
|
+
size="small"
|
|
488
|
+
class="no-padding-left"
|
|
489
|
+
>
|
|
490
|
+
选择当前目录
|
|
491
|
+
</el-button>
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
<!-- 目录内容列表 -->
|
|
495
|
+
<ul class="directory-items">
|
|
496
|
+
<li
|
|
497
|
+
v-for="item in directoryItems"
|
|
498
|
+
:key="item.path"
|
|
499
|
+
:class="['directory-item', item.type]"
|
|
500
|
+
@click="selectDirectoryItem(item)"
|
|
501
|
+
>
|
|
502
|
+
<el-icon v-if="item.type === 'directory'"><Folder /></el-icon>
|
|
503
|
+
<el-icon v-else><Document /></el-icon>
|
|
504
|
+
<span>{{ item.name }}</span>
|
|
505
|
+
</li>
|
|
506
|
+
</ul>
|
|
507
|
+
</div>
|
|
508
|
+
</el-dialog>
|
|
509
|
+
|
|
510
|
+
<!-- 文件差异对话框 -->
|
|
511
|
+
<el-dialog
|
|
512
|
+
v-model="diffDialogVisible"
|
|
513
|
+
:title="`文件差异: ${selectedFile}`"
|
|
514
|
+
width="80%"
|
|
515
|
+
destroy-on-close
|
|
516
|
+
>
|
|
517
|
+
<div v-loading="isLoadingDiff" class="diff-content">
|
|
518
|
+
<div v-if="diffContent" v-html="formatDiff(diffContent)" class="diff-formatted"></div>
|
|
519
|
+
<div v-else class="no-diff">该文件没有差异或是新文件</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<!-- 添加文件导航按钮 -->
|
|
523
|
+
<div class="file-navigation">
|
|
524
|
+
<el-button
|
|
525
|
+
:icon="ArrowLeft"
|
|
526
|
+
@click="goToPreviousFile"
|
|
527
|
+
:disabled="currentFileIndex <= 0 || fileList.length === 0"
|
|
528
|
+
circle
|
|
529
|
+
/>
|
|
530
|
+
<span class="file-counter">{{ currentFileIndex + 1 }} / {{ fileList.length }}</span>
|
|
531
|
+
<el-button
|
|
532
|
+
:icon="ArrowRight"
|
|
533
|
+
@click="goToNextFile"
|
|
534
|
+
:disabled="currentFileIndex >= fileList.length - 1 || fileList.length === 0"
|
|
535
|
+
circle
|
|
536
|
+
/>
|
|
537
|
+
</div>
|
|
538
|
+
</el-dialog>
|
|
539
|
+
</div>
|
|
540
|
+
</template>
|
|
541
|
+
|
|
542
|
+
<style scoped>
|
|
543
|
+
.card {
|
|
544
|
+
background-color: #fff;
|
|
545
|
+
border-radius: 8px;
|
|
546
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
547
|
+
padding: 20px;
|
|
548
|
+
margin-bottom: 20px;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.status-header {
|
|
552
|
+
display: flex;
|
|
553
|
+
justify-content: space-between;
|
|
554
|
+
align-items: center;
|
|
555
|
+
margin-bottom: 10px;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.status-header h2 {
|
|
559
|
+
margin: 0;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.status-box {
|
|
563
|
+
white-space: pre-wrap;
|
|
564
|
+
font-family: monospace;
|
|
565
|
+
background-color: #f5f7fa;
|
|
566
|
+
padding: 15px;
|
|
567
|
+
border-radius: 4px;
|
|
568
|
+
margin-bottom: 15px;
|
|
569
|
+
max-height: 300px;
|
|
570
|
+
overflow-y: auto;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.file-list {
|
|
574
|
+
max-height: 300px;
|
|
575
|
+
overflow-y: auto;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.file-item {
|
|
579
|
+
padding: 8px 12px;
|
|
580
|
+
margin-bottom: 5px;
|
|
581
|
+
border-radius: 4px;
|
|
582
|
+
cursor: pointer;
|
|
583
|
+
display: flex;
|
|
584
|
+
align-items: center;
|
|
585
|
+
justify-content: space-between;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.file-item:hover {
|
|
589
|
+
background-color: #f5f7fa;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.file-info {
|
|
593
|
+
display: flex;
|
|
594
|
+
align-items: center;
|
|
595
|
+
flex-grow: 1;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.file-actions {
|
|
599
|
+
margin-left: 10px;
|
|
600
|
+
opacity: 0.5;
|
|
601
|
+
transition: opacity 0.2s;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.file-item:hover .file-actions {
|
|
605
|
+
opacity: 1;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.file-type {
|
|
609
|
+
font-size: 12px;
|
|
610
|
+
padding: 2px 6px;
|
|
611
|
+
border-radius: 10px;
|
|
612
|
+
margin-right: 10px;
|
|
613
|
+
flex-shrink: 0;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.added .file-type {
|
|
617
|
+
background-color: #e1f3d8;
|
|
618
|
+
color: #67c23a;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.modified .file-type {
|
|
622
|
+
background-color: #e6f1fc;
|
|
623
|
+
color: #409eff;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.deleted .file-type {
|
|
627
|
+
background-color: #fef0f0;
|
|
628
|
+
color: #f56c6c;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.untracked .file-type {
|
|
632
|
+
background-color: #fdf6ec;
|
|
633
|
+
color: #e6a23c;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.file-path {
|
|
637
|
+
font-family: monospace;
|
|
638
|
+
word-break: break-all;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.diff-content {
|
|
642
|
+
font-family: monospace;
|
|
643
|
+
white-space: pre-wrap;
|
|
644
|
+
max-height: 60vh;
|
|
645
|
+
overflow-y: auto;
|
|
646
|
+
padding: 10px;
|
|
647
|
+
background-color: #f5f7fa;
|
|
648
|
+
border-radius: 4px;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.diff-formatted {
|
|
652
|
+
font-size: 14px;
|
|
653
|
+
line-height: 1.5;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.file-navigation {
|
|
657
|
+
display: flex;
|
|
658
|
+
justify-content: center;
|
|
659
|
+
align-items: center;
|
|
660
|
+
margin-top: 15px;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.file-counter {
|
|
664
|
+
margin: 0 15px;
|
|
665
|
+
font-size: 14px;
|
|
666
|
+
color: #606266;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.current-directory {
|
|
670
|
+
display: flex;
|
|
671
|
+
align-items: center;
|
|
672
|
+
margin-bottom: 15px;
|
|
673
|
+
padding: 8px 12px;
|
|
674
|
+
background-color: #f5f7fa;
|
|
675
|
+
border-radius: 4px;
|
|
676
|
+
font-family: monospace;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.current-directory .el-icon {
|
|
680
|
+
margin-right: 8px;
|
|
681
|
+
color: #409eff;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.current-directory span {
|
|
685
|
+
flex-grow: 1;
|
|
686
|
+
word-break: break-all;
|
|
687
|
+
margin-right: 10px;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.browser-current-path {
|
|
691
|
+
margin-bottom: 10px;
|
|
692
|
+
font-size: 14px;
|
|
693
|
+
color: #606266;
|
|
694
|
+
background-color: #f5f7fa;
|
|
695
|
+
padding: 8px 12px;
|
|
696
|
+
border-radius: 4px;
|
|
697
|
+
font-family: monospace;
|
|
698
|
+
word-break: break-all;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.browser-error {
|
|
702
|
+
margin-bottom: 10px;
|
|
703
|
+
color: #f56c6c;
|
|
704
|
+
padding: 8px 12px;
|
|
705
|
+
background-color: #fef0f0;
|
|
706
|
+
border-radius: 4px;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.directory-browser {
|
|
710
|
+
padding: 10px;
|
|
711
|
+
max-height: 400px;
|
|
712
|
+
overflow-y: auto;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.browser-nav {
|
|
716
|
+
margin-bottom: 10px;
|
|
717
|
+
display: flex;
|
|
718
|
+
justify-content: space-between;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.directory-items {
|
|
722
|
+
list-style: none;
|
|
723
|
+
padding: 0;
|
|
724
|
+
margin: 0;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.directory-item {
|
|
728
|
+
padding: 8px 12px;
|
|
729
|
+
margin-bottom: 5px;
|
|
730
|
+
border-radius: 4px;
|
|
731
|
+
cursor: pointer;
|
|
732
|
+
display: flex;
|
|
733
|
+
align-items: center;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.directory-item:hover {
|
|
737
|
+
background-color: #f5f7fa;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.directory-item.directory {
|
|
741
|
+
color: #409eff;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.directory-item.file {
|
|
745
|
+
color: #606266;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.directory-item .el-icon {
|
|
749
|
+
margin-right: 10px;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.directory-item span {
|
|
753
|
+
font-family: monospace;
|
|
754
|
+
word-break: break-all;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.directory-buttons {
|
|
758
|
+
display: flex;
|
|
759
|
+
gap: 10px;
|
|
760
|
+
margin-top: 10px;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/* 移除按钮左侧的内边距 */
|
|
764
|
+
.no-padding-left {
|
|
765
|
+
padding-left: 8px !important;
|
|
766
|
+
}
|
|
767
|
+
</style>
|
|
768
|
+
|
|
769
|
+
<!-- 添加非scoped样式,使diff格式化样式对动态内容生效 -->
|
|
770
|
+
<style>
|
|
771
|
+
.diff-header {
|
|
772
|
+
font-weight: bold;
|
|
773
|
+
background-color: #e6f1fc;
|
|
774
|
+
padding: 3px;
|
|
775
|
+
margin: 5px 0;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.diff-old-file, .diff-new-file {
|
|
779
|
+
color: #888;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.diff-hunk-header {
|
|
783
|
+
color: #6f42c1;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.diff-added {
|
|
787
|
+
background-color: #e6ffed;
|
|
788
|
+
color: #28a745;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.diff-removed {
|
|
792
|
+
background-color: #ffeef0;
|
|
793
|
+
color: #d73a49;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.diff-context {
|
|
797
|
+
color: #444;
|
|
798
|
+
}
|
|
799
|
+
</style>
|