zen-gitsync 2.0.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-gitsync",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "一个 git 自动查看差异并提交的工具",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, onMounted, defineExpose } from 'vue'
3
- import { ElMessage } from 'element-plus'
3
+ import { ElMessage, ElMessageBox } from 'element-plus'
4
4
  // import { io } from 'socket.io-client'
5
- import { Refresh, ArrowLeft, ArrowRight, Folder } from '@element-plus/icons-vue'
5
+ import { Refresh, ArrowLeft, ArrowRight, Folder, Document, ArrowUp, RefreshRight } from '@element-plus/icons-vue'
6
6
 
7
7
  const status = ref('加载中...')
8
8
  // const socket = io()
@@ -14,6 +14,16 @@ const diffDialogVisible = ref(false)
14
14
  const isLoadingDiff = ref(false)
15
15
  // 添加当前文件索引
16
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('')
17
27
 
18
28
  // 解析 git status 输出,提取文件及类型
19
29
  function parseStatus(statusText: string) {
@@ -158,16 +168,205 @@ async function goToNextFile() {
158
168
  await getFileDiff(nextFile.path)
159
169
  }
160
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
+
161
309
  // 处理文件点击
162
310
  function handleFileClick(file: {path: string, type: string}) {
163
311
  getFileDiff(file.path)
164
312
  }
165
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
+
166
325
  // 刷新Git状态的方法
167
326
  async function refreshStatus() {
168
327
  await loadStatus()
169
328
  }
170
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
+
171
370
  onMounted(() => {
172
371
  loadStatus()
173
372
 
@@ -193,6 +392,9 @@ defineExpose({
193
392
  <div class="current-directory">
194
393
  <el-icon><Folder /></el-icon>
195
394
  <span>{{ currentDirectory }}</span>
395
+ <el-button type="primary" size="small" @click="openDirectoryDialog" plain>
396
+ 切换目录
397
+ </el-button>
196
398
  </div>
197
399
  <div class="status-header">
198
400
  <h2>Git 状态</h2>
@@ -212,13 +414,99 @@ defineExpose({
212
414
  v-for="file in fileList"
213
415
  :key="file.path"
214
416
  :class="['file-item', file.type]"
215
- @click="handleFileClick(file)"
216
417
  >
217
- <span class="file-type">{{ fileTypeLabel(file.type) }}</span>
218
- <span class="file-path">{{ file.path }}</span>
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>
219
433
  </div>
220
434
  </div>
221
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
+
222
510
  <!-- 文件差异对话框 -->
223
511
  <el-dialog
224
512
  v-model="diffDialogVisible"
@@ -251,18 +539,15 @@ defineExpose({
251
539
  </div>
252
540
  </template>
253
541
 
254
- <script lang="ts">
255
- // 辅助函数:类型标签
256
- function fileTypeLabel(type: string) {
257
- if (type === 'added') return '新增'
258
- if (type === 'modified') return '修改'
259
- if (type === 'deleted') return '删除'
260
- if (type === 'untracked') return '未跟踪'
261
- return '其它'
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;
262
549
  }
263
- </script>
264
550
 
265
- <style scoped>
266
551
  .status-header {
267
552
  display: flex;
268
553
  justify-content: space-between;
@@ -274,130 +559,241 @@ function fileTypeLabel(type: string) {
274
559
  margin: 0;
275
560
  }
276
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
+
277
573
  .file-list {
278
- margin-top: 10px;
574
+ max-height: 300px;
575
+ overflow-y: auto;
279
576
  }
280
577
 
281
578
  .file-item {
579
+ padding: 8px 12px;
580
+ margin-bottom: 5px;
581
+ border-radius: 4px;
582
+ cursor: pointer;
282
583
  display: flex;
283
584
  align-items: center;
284
- margin-bottom: 4px;
285
- padding: 2px 6px;
286
- border-radius: 3px;
287
- font-size: 14px;
288
- cursor: pointer;
289
- transition: opacity 0.2s;
585
+ justify-content: space-between;
290
586
  }
587
+
291
588
  .file-item:hover {
292
- opacity: 0.8;
293
- }
294
- .file-item.added {
295
- background: #e6ffed;
296
- color: #22863a;
589
+ background-color: #f5f7fa;
297
590
  }
298
- .file-item.modified {
299
- background: #fff5b1;
300
- color: #b08800;
591
+
592
+ .file-info {
593
+ display: flex;
594
+ align-items: center;
595
+ flex-grow: 1;
301
596
  }
302
- .file-item.deleted {
303
- background: #ffeef0;
304
- color: #cb2431;
597
+
598
+ .file-actions {
599
+ margin-left: 10px;
600
+ opacity: 0.5;
601
+ transition: opacity 0.2s;
305
602
  }
306
- .file-item.untracked {
307
- background: #f1f8ff;
308
- color: #0366d6;
603
+
604
+ .file-item:hover .file-actions {
605
+ opacity: 1;
309
606
  }
607
+
310
608
  .file-type {
311
- font-weight: bold;
312
- margin-right: 8px;
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;
313
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
+
314
636
  .file-path {
315
637
  font-family: monospace;
638
+ word-break: break-all;
316
639
  }
317
640
 
318
641
  .diff-content {
319
- max-height: 70vh;
320
- overflow-y: auto;
321
- background-color: #f6f8fa;
322
- border: 1px solid #e1e4e8;
323
- border-radius: 3px;
324
- padding: 15px;
325
642
  font-family: monospace;
326
643
  white-space: pre-wrap;
644
+ max-height: 60vh;
645
+ overflow-y: auto;
646
+ padding: 10px;
647
+ background-color: #f5f7fa;
648
+ border-radius: 4px;
327
649
  }
328
650
 
329
651
  .diff-formatted {
330
- margin: 0;
652
+ font-size: 14px;
653
+ line-height: 1.5;
331
654
  }
332
655
 
333
- /* 差异内容的颜色样式 - 使用深度选择器 */
334
- :deep(.diff-header) {
335
- color: #24292e;
336
- font-weight: bold;
656
+ .file-navigation {
657
+ display: flex;
658
+ justify-content: center;
659
+ align-items: center;
660
+ margin-top: 15px;
337
661
  }
338
662
 
339
- :deep(.diff-old-file) {
340
- color: #cb2431;
341
- background-color: #ffeef0;
663
+ .file-counter {
664
+ margin: 0 15px;
665
+ font-size: 14px;
666
+ color: #606266;
342
667
  }
343
668
 
344
- :deep(.diff-new-file) {
345
- color: #22863a;
346
- background-color: #e6ffed;
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;
347
677
  }
348
678
 
349
- :deep(.diff-hunk-header) {
350
- color: #6f42c1;
351
- background-color: #f1f8ff;
679
+ .current-directory .el-icon {
680
+ margin-right: 8px;
681
+ color: #409eff;
352
682
  }
353
683
 
354
- :deep(.diff-added) {
355
- color: #22863a;
356
- background-color: #e6ffed;
684
+ .current-directory span {
685
+ flex-grow: 1;
686
+ word-break: break-all;
687
+ margin-right: 10px;
357
688
  }
358
689
 
359
- :deep(.diff-removed) {
360
- color: #cb2431;
361
- background-color: #ffeef0;
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;
362
699
  }
363
700
 
364
- :deep(.diff-context) {
365
- color: #24292e;
701
+ .browser-error {
702
+ margin-bottom: 10px;
703
+ color: #f56c6c;
704
+ padding: 8px 12px;
705
+ background-color: #fef0f0;
706
+ border-radius: 4px;
366
707
  }
367
708
 
368
- .no-diff {
369
- text-align: center;
370
- padding: 20px;
371
- color: #666;
709
+ .directory-browser {
710
+ padding: 10px;
711
+ max-height: 400px;
712
+ overflow-y: auto;
372
713
  }
373
714
 
374
- /* 添加文件导航样式 */
375
- .file-navigation {
715
+ .browser-nav {
716
+ margin-bottom: 10px;
376
717
  display: flex;
377
- justify-content: center;
378
- align-items: center;
379
- margin-top: 15px;
380
- gap: 10px;
718
+ justify-content: space-between;
381
719
  }
382
720
 
383
- .file-counter {
384
- font-size: 14px;
385
- color: #606266;
721
+ .directory-items {
722
+ list-style: none;
723
+ padding: 0;
724
+ margin: 0;
386
725
  }
387
- .current-directory {
388
- padding: 10px 15px;
389
- background-color: #f0f0f0;
390
- border-bottom: 1px solid #e1e4e8;
726
+
727
+ .directory-item {
728
+ padding: 8px 12px;
729
+ margin-bottom: 5px;
730
+ border-radius: 4px;
731
+ cursor: pointer;
391
732
  display: flex;
392
733
  align-items: center;
393
- gap: 8px;
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 {
394
753
  font-family: monospace;
754
+ word-break: break-all;
395
755
  }
396
- .not-git-repo {
397
- margin: 10px 0;
398
- padding: 10px;
399
- background-color: #fffbf6;
400
- border: 1px solid #f0c78a;
401
- border-radius: 4px;
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;
402
798
  }
403
799
  </style>