zen-gitsync 1.5.6 → 2.0.1
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 +403 -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-BcTk2R6G.js +9 -0
- package/src/ui/public/assets/index-ChUZ1vPG.css +1 -0
- package/src/ui/public/assets/vendor-BAXrrwNU.js +41 -0
- package/src/ui/public/assets/vendor-Dp0FkvMe.css +1 -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 +447 -0
- package/src/utils/index.js +4 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, defineExpose } from 'vue'
|
|
3
|
+
import { ElMessage } from 'element-plus'
|
|
4
|
+
// import { io } from 'socket.io-client'
|
|
5
|
+
import { Refresh, ArrowLeft, ArrowRight, Folder } 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
|
+
// 解析 git status 输出,提取文件及类型
|
|
19
|
+
function parseStatus(statusText: string) {
|
|
20
|
+
if (statusText === undefined) return
|
|
21
|
+
const lines = statusText.split('\n')
|
|
22
|
+
const files: {path: string, type: string}[] = []
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
// 匹配常见的 git status --porcelain 格式
|
|
25
|
+
// M: 修改, A: 新增, D: 删除, ??: 未跟踪
|
|
26
|
+
const match = line.match(/^([ MADRCU\?]{2})\s+(.+)$/)
|
|
27
|
+
if (match) {
|
|
28
|
+
let type = ''
|
|
29
|
+
const code = match[1].trim()
|
|
30
|
+
if (code === 'M' || code === 'MM' || code === 'AM' || code === 'RM') type = 'modified'
|
|
31
|
+
else if (code === 'A' || code === 'AA') type = 'added'
|
|
32
|
+
else if (code === 'D' || code === 'AD' || code === 'DA') type = 'deleted'
|
|
33
|
+
else if (code === '??') type = 'untracked'
|
|
34
|
+
else type = 'other'
|
|
35
|
+
files.push({ path: match[2], type })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
fileList.value = files
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const currentDirectory = ref('')
|
|
42
|
+
async function loadStatus() {
|
|
43
|
+
try {
|
|
44
|
+
isRefreshing.value = true
|
|
45
|
+
// 获取当前工作目录
|
|
46
|
+
const responseDir = await fetch('/api/current_directory')
|
|
47
|
+
const dirData = await responseDir.json()
|
|
48
|
+
currentDirectory.value = dirData.directory || '未知目录'
|
|
49
|
+
|
|
50
|
+
// 如果不是Git仓库,显示提示并返回
|
|
51
|
+
if (dirData.isGitRepo === false) {
|
|
52
|
+
status.value = '当前目录不是一个Git仓库'
|
|
53
|
+
fileList.value = []
|
|
54
|
+
ElMessage.warning('当前目录不是一个Git仓库')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const response = await fetch('/api/status')
|
|
59
|
+
const data = await response.json()
|
|
60
|
+
status.value = data.status
|
|
61
|
+
|
|
62
|
+
const response_porcelain = await fetch('/api/status_porcelain')
|
|
63
|
+
const data_porcelain = await response_porcelain.json()
|
|
64
|
+
parseStatus(data_porcelain.status)
|
|
65
|
+
ElMessage({
|
|
66
|
+
message: 'Git 状态已刷新',
|
|
67
|
+
type: 'success',
|
|
68
|
+
})
|
|
69
|
+
} catch (error) {
|
|
70
|
+
status.value = '加载状态失败: ' + (error as Error).message
|
|
71
|
+
fileList.value = []
|
|
72
|
+
ElMessage({
|
|
73
|
+
message: '刷新失败: ' + (error as Error).message,
|
|
74
|
+
type: 'error',
|
|
75
|
+
})
|
|
76
|
+
} finally {
|
|
77
|
+
isRefreshing.value = false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 格式化差异内容,添加颜色
|
|
82
|
+
function formatDiff(diffText: string) {
|
|
83
|
+
if (!diffText) return '';
|
|
84
|
+
|
|
85
|
+
// 将差异内容按行分割
|
|
86
|
+
const lines = diffText.split('\n');
|
|
87
|
+
|
|
88
|
+
// 转义 HTML 标签的函数
|
|
89
|
+
function escapeHtml(text: string) {
|
|
90
|
+
return text
|
|
91
|
+
.replace(/&/g, '&')
|
|
92
|
+
.replace(/</g, '<')
|
|
93
|
+
.replace(/>/g, '>')
|
|
94
|
+
.replace(/"/g, '"')
|
|
95
|
+
.replace(/'/g, ''');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 为每行添加适当的 CSS 类
|
|
99
|
+
return lines.map(line => {
|
|
100
|
+
// 先转义 HTML 标签,再添加样式
|
|
101
|
+
const escapedLine = escapeHtml(line);
|
|
102
|
+
|
|
103
|
+
if (line.startsWith('diff --git')) {
|
|
104
|
+
return `<div class="diff-header">${escapedLine}</div>`;
|
|
105
|
+
} else if (line.startsWith('---')) {
|
|
106
|
+
return `<div class="diff-old-file">${escapedLine}</div>`;
|
|
107
|
+
} else if (line.startsWith('+++')) {
|
|
108
|
+
return `<div class="diff-new-file">${escapedLine}</div>`;
|
|
109
|
+
} else if (line.startsWith('@@')) {
|
|
110
|
+
return `<div class="diff-hunk-header">${escapedLine}</div>`;
|
|
111
|
+
} else if (line.startsWith('+')) {
|
|
112
|
+
return `<div class="diff-added">${escapedLine}</div>`;
|
|
113
|
+
} else if (line.startsWith('-')) {
|
|
114
|
+
return `<div class="diff-removed">${escapedLine}</div>`;
|
|
115
|
+
} else {
|
|
116
|
+
return `<div class="diff-context">${escapedLine}</div>`;
|
|
117
|
+
}
|
|
118
|
+
}).join('');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 获取文件差异
|
|
122
|
+
async function getFileDiff(filePath: string) {
|
|
123
|
+
try {
|
|
124
|
+
isLoadingDiff.value = true
|
|
125
|
+
selectedFile.value = filePath
|
|
126
|
+
// 设置当前文件索引
|
|
127
|
+
currentFileIndex.value = fileList.value.findIndex(file => file.path === filePath)
|
|
128
|
+
const response = await fetch(`/api/diff?file=${encodeURIComponent(filePath)}`)
|
|
129
|
+
const data = await response.json()
|
|
130
|
+
diffContent.value = data.diff || '没有变更'
|
|
131
|
+
diffDialogVisible.value = true
|
|
132
|
+
} catch (error) {
|
|
133
|
+
ElMessage({
|
|
134
|
+
message: '获取文件差异失败: ' + (error as Error).message,
|
|
135
|
+
type: 'error',
|
|
136
|
+
})
|
|
137
|
+
diffContent.value = '获取差异失败: ' + (error as Error).message
|
|
138
|
+
} finally {
|
|
139
|
+
isLoadingDiff.value = false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 添加切换到上一个文件的方法
|
|
144
|
+
async function goToPreviousFile() {
|
|
145
|
+
if (fileList.value.length === 0 || currentFileIndex.value <= 0) return
|
|
146
|
+
|
|
147
|
+
const newIndex = currentFileIndex.value - 1
|
|
148
|
+
const prevFile = fileList.value[newIndex]
|
|
149
|
+
await getFileDiff(prevFile.path)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 添加切换到下一个文件的方法
|
|
153
|
+
async function goToNextFile() {
|
|
154
|
+
if (fileList.value.length === 0 || currentFileIndex.value >= fileList.value.length - 1) return
|
|
155
|
+
|
|
156
|
+
const newIndex = currentFileIndex.value + 1
|
|
157
|
+
const nextFile = fileList.value[newIndex]
|
|
158
|
+
await getFileDiff(nextFile.path)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 处理文件点击
|
|
162
|
+
function handleFileClick(file: {path: string, type: string}) {
|
|
163
|
+
getFileDiff(file.path)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 刷新Git状态的方法
|
|
167
|
+
async function refreshStatus() {
|
|
168
|
+
await loadStatus()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
onMounted(() => {
|
|
172
|
+
loadStatus()
|
|
173
|
+
|
|
174
|
+
// Socket.io 事件
|
|
175
|
+
// socket.on('status_update', (data: { status: string }) => {
|
|
176
|
+
// status.value = data.status
|
|
177
|
+
// parseStatus(data.status)
|
|
178
|
+
// })
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// onUnmounted(() => {
|
|
182
|
+
// socket.disconnect()
|
|
183
|
+
// })
|
|
184
|
+
|
|
185
|
+
// 暴露刷新方法给父组件
|
|
186
|
+
defineExpose({
|
|
187
|
+
refreshStatus
|
|
188
|
+
})
|
|
189
|
+
</script>
|
|
190
|
+
|
|
191
|
+
<template>
|
|
192
|
+
<div class="card">
|
|
193
|
+
<div class="current-directory">
|
|
194
|
+
<el-icon><Folder /></el-icon>
|
|
195
|
+
<span>{{ currentDirectory }}</span>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="status-header">
|
|
198
|
+
<h2>Git 状态</h2>
|
|
199
|
+
<el-button
|
|
200
|
+
type="primary"
|
|
201
|
+
:icon="Refresh"
|
|
202
|
+
circle
|
|
203
|
+
size="small"
|
|
204
|
+
@click="refreshStatus"
|
|
205
|
+
:loading="isRefreshing"
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="status-box">{{ status }}</div>
|
|
209
|
+
<!-- 颜色区分不同类型文件 -->
|
|
210
|
+
<div v-if="fileList.length" class="file-list">
|
|
211
|
+
<div
|
|
212
|
+
v-for="file in fileList"
|
|
213
|
+
:key="file.path"
|
|
214
|
+
:class="['file-item', file.type]"
|
|
215
|
+
@click="handleFileClick(file)"
|
|
216
|
+
>
|
|
217
|
+
<span class="file-type">{{ fileTypeLabel(file.type) }}</span>
|
|
218
|
+
<span class="file-path">{{ file.path }}</span>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<!-- 文件差异对话框 -->
|
|
223
|
+
<el-dialog
|
|
224
|
+
v-model="diffDialogVisible"
|
|
225
|
+
:title="`文件差异: ${selectedFile}`"
|
|
226
|
+
width="80%"
|
|
227
|
+
destroy-on-close
|
|
228
|
+
>
|
|
229
|
+
<div v-loading="isLoadingDiff" class="diff-content">
|
|
230
|
+
<div v-if="diffContent" v-html="formatDiff(diffContent)" class="diff-formatted"></div>
|
|
231
|
+
<div v-else class="no-diff">该文件没有差异或是新文件</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<!-- 添加文件导航按钮 -->
|
|
235
|
+
<div class="file-navigation">
|
|
236
|
+
<el-button
|
|
237
|
+
:icon="ArrowLeft"
|
|
238
|
+
@click="goToPreviousFile"
|
|
239
|
+
:disabled="currentFileIndex <= 0 || fileList.length === 0"
|
|
240
|
+
circle
|
|
241
|
+
/>
|
|
242
|
+
<span class="file-counter">{{ currentFileIndex + 1 }} / {{ fileList.length }}</span>
|
|
243
|
+
<el-button
|
|
244
|
+
:icon="ArrowRight"
|
|
245
|
+
@click="goToNextFile"
|
|
246
|
+
:disabled="currentFileIndex >= fileList.length - 1 || fileList.length === 0"
|
|
247
|
+
circle
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
</el-dialog>
|
|
251
|
+
</div>
|
|
252
|
+
</template>
|
|
253
|
+
|
|
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 '其它'
|
|
262
|
+
}
|
|
263
|
+
</script>
|
|
264
|
+
|
|
265
|
+
<style scoped>
|
|
266
|
+
.status-header {
|
|
267
|
+
display: flex;
|
|
268
|
+
justify-content: space-between;
|
|
269
|
+
align-items: center;
|
|
270
|
+
margin-bottom: 10px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.status-header h2 {
|
|
274
|
+
margin: 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.file-list {
|
|
278
|
+
margin-top: 10px;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.file-item {
|
|
282
|
+
display: flex;
|
|
283
|
+
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;
|
|
290
|
+
}
|
|
291
|
+
.file-item:hover {
|
|
292
|
+
opacity: 0.8;
|
|
293
|
+
}
|
|
294
|
+
.file-item.added {
|
|
295
|
+
background: #e6ffed;
|
|
296
|
+
color: #22863a;
|
|
297
|
+
}
|
|
298
|
+
.file-item.modified {
|
|
299
|
+
background: #fff5b1;
|
|
300
|
+
color: #b08800;
|
|
301
|
+
}
|
|
302
|
+
.file-item.deleted {
|
|
303
|
+
background: #ffeef0;
|
|
304
|
+
color: #cb2431;
|
|
305
|
+
}
|
|
306
|
+
.file-item.untracked {
|
|
307
|
+
background: #f1f8ff;
|
|
308
|
+
color: #0366d6;
|
|
309
|
+
}
|
|
310
|
+
.file-type {
|
|
311
|
+
font-weight: bold;
|
|
312
|
+
margin-right: 8px;
|
|
313
|
+
}
|
|
314
|
+
.file-path {
|
|
315
|
+
font-family: monospace;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.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
|
+
font-family: monospace;
|
|
326
|
+
white-space: pre-wrap;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.diff-formatted {
|
|
330
|
+
margin: 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* 差异内容的颜色样式 - 使用深度选择器 */
|
|
334
|
+
:deep(.diff-header) {
|
|
335
|
+
color: #24292e;
|
|
336
|
+
font-weight: bold;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
:deep(.diff-old-file) {
|
|
340
|
+
color: #cb2431;
|
|
341
|
+
background-color: #ffeef0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
:deep(.diff-new-file) {
|
|
345
|
+
color: #22863a;
|
|
346
|
+
background-color: #e6ffed;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
:deep(.diff-hunk-header) {
|
|
350
|
+
color: #6f42c1;
|
|
351
|
+
background-color: #f1f8ff;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
:deep(.diff-added) {
|
|
355
|
+
color: #22863a;
|
|
356
|
+
background-color: #e6ffed;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
:deep(.diff-removed) {
|
|
360
|
+
color: #cb2431;
|
|
361
|
+
background-color: #ffeef0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
:deep(.diff-context) {
|
|
365
|
+
color: #24292e;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.no-diff {
|
|
369
|
+
text-align: center;
|
|
370
|
+
padding: 20px;
|
|
371
|
+
color: #666;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* 添加文件导航样式 */
|
|
375
|
+
.file-navigation {
|
|
376
|
+
display: flex;
|
|
377
|
+
justify-content: center;
|
|
378
|
+
align-items: center;
|
|
379
|
+
margin-top: 15px;
|
|
380
|
+
gap: 10px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.file-counter {
|
|
384
|
+
font-size: 14px;
|
|
385
|
+
color: #606266;
|
|
386
|
+
}
|
|
387
|
+
.current-directory {
|
|
388
|
+
padding: 10px 15px;
|
|
389
|
+
background-color: #f0f0f0;
|
|
390
|
+
border-bottom: 1px solid #e1e4e8;
|
|
391
|
+
display: flex;
|
|
392
|
+
align-items: center;
|
|
393
|
+
gap: 8px;
|
|
394
|
+
font-family: monospace;
|
|
395
|
+
}
|
|
396
|
+
.not-git-repo {
|
|
397
|
+
margin: 10px 0;
|
|
398
|
+
padding: 10px;
|
|
399
|
+
background-color: #fffbf6;
|
|
400
|
+
border: 1px solid #f0c78a;
|
|
401
|
+
border-radius: 4px;
|
|
402
|
+
}
|
|
403
|
+
</style>
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, defineExpose } from 'vue'
|
|
3
|
+
import { ElTable, ElTableColumn, ElTag, ElButton } from 'element-plus'
|
|
4
|
+
import { RefreshRight } from '@element-plus/icons-vue'
|
|
5
|
+
import 'element-plus/dist/index.css'
|
|
6
|
+
import { createGitgraph } from '@gitgraph/js'
|
|
7
|
+
|
|
8
|
+
interface LogItem {
|
|
9
|
+
hash: string
|
|
10
|
+
date: string
|
|
11
|
+
author: string
|
|
12
|
+
email: string // 添加邮箱字段
|
|
13
|
+
message: string
|
|
14
|
+
branch?: string // 添加分支信息字段
|
|
15
|
+
parents?: string[] // 添加父提交信息
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const logs = ref<LogItem[]>([])
|
|
19
|
+
const errorMessage = ref('')
|
|
20
|
+
const isLoading = ref(false)
|
|
21
|
+
const showAllCommits = ref(false)
|
|
22
|
+
const totalCommits = ref(0)
|
|
23
|
+
const showGraphView = ref(true) // 控制是否显示图表视图
|
|
24
|
+
const graphContainer = ref<HTMLElement | null>(null)
|
|
25
|
+
|
|
26
|
+
// 加载提交历史
|
|
27
|
+
async function loadLog(all = false) {
|
|
28
|
+
try {
|
|
29
|
+
isLoading.value = true
|
|
30
|
+
showAllCommits.value = all
|
|
31
|
+
// 修改API调用,获取更详细的提交信息,包括父提交
|
|
32
|
+
const url = all ? '/api/log?all=true&graph=true' : '/api/log?graph=true'
|
|
33
|
+
const response = await fetch(url)
|
|
34
|
+
logs.value = await response.json()
|
|
35
|
+
totalCommits.value = logs.value.length
|
|
36
|
+
errorMessage.value = ''
|
|
37
|
+
|
|
38
|
+
// 加载完数据后渲染图表
|
|
39
|
+
if (showGraphView.value) {
|
|
40
|
+
setTimeout(renderGraph, 0)
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
errorMessage.value = '加载提交历史失败: ' + (error as Error).message
|
|
44
|
+
} finally {
|
|
45
|
+
isLoading.value = false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 渲染Git图表
|
|
50
|
+
async function renderGraph() {
|
|
51
|
+
if (!graphContainer.value || logs.value.length === 0) return
|
|
52
|
+
|
|
53
|
+
// 清空容器
|
|
54
|
+
graphContainer.value.innerHTML = ''
|
|
55
|
+
|
|
56
|
+
// 获取当前分支
|
|
57
|
+
const branchResponse = await fetch('/api/branch')
|
|
58
|
+
const { branch: currentBranch } = await branchResponse.json()
|
|
59
|
+
|
|
60
|
+
// 创建gitgraph实例
|
|
61
|
+
const gitgraph = createGitgraph(graphContainer.value, {
|
|
62
|
+
// 自定义选项
|
|
63
|
+
// @ts-ignore: true
|
|
64
|
+
orientation: 'vertical-reverse', // 从上到下的方向
|
|
65
|
+
// @ts-ignore: true
|
|
66
|
+
template: 'metro', // 使用metro模板
|
|
67
|
+
author: '提交者 <committer@example.com>'
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// 处理分支和提交数据
|
|
71
|
+
// 注意:这里的实现是简化的,实际需要根据API返回的数据结构调整
|
|
72
|
+
const branches: Record<string, any> = {}
|
|
73
|
+
const mainBranch = gitgraph.branch(currentBranch || 'main') // 使用API获取的分支或默认main
|
|
74
|
+
branches[currentBranch || 'main'] = mainBranch
|
|
75
|
+
|
|
76
|
+
// 简化示例 - 实际实现需要根据API返回的数据结构调整
|
|
77
|
+
logs.value.forEach(commit => {
|
|
78
|
+
// 这里需要根据实际数据结构构建分支图
|
|
79
|
+
let currentBranch = mainBranch
|
|
80
|
+
|
|
81
|
+
// 如果有分支信息,使用对应的分支
|
|
82
|
+
if (commit.branch) {
|
|
83
|
+
const branchName = formatBranchName(commit.branch.split(',')[0])
|
|
84
|
+
if (!branches[branchName]) {
|
|
85
|
+
branches[branchName] = gitgraph.branch(branchName)
|
|
86
|
+
}
|
|
87
|
+
currentBranch = branches[branchName]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 创建提交,添加邮箱信息
|
|
91
|
+
currentBranch.commit({
|
|
92
|
+
hash: commit.hash,
|
|
93
|
+
subject: commit.message,
|
|
94
|
+
author: `${commit.author} <${commit.email}>`
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 切换视图模式
|
|
100
|
+
function toggleViewMode() {
|
|
101
|
+
showGraphView.value = !showGraphView.value
|
|
102
|
+
if (showGraphView.value && logs.value.length > 0) {
|
|
103
|
+
// 延迟执行以确保DOM已更新
|
|
104
|
+
setTimeout(renderGraph, 0)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 切换显示所有提交
|
|
109
|
+
function toggleAllCommits() {
|
|
110
|
+
loadLog(!showAllCommits.value)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onMounted(() => {
|
|
114
|
+
loadLog()
|
|
115
|
+
})
|
|
116
|
+
const refreshLog = () => loadLog(showAllCommits.value)
|
|
117
|
+
// 暴露方法给父组件
|
|
118
|
+
defineExpose({
|
|
119
|
+
refreshLog
|
|
120
|
+
})
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<template>
|
|
124
|
+
<div class="card">
|
|
125
|
+
<div class="log-header">
|
|
126
|
+
<h2>提交历史</h2>
|
|
127
|
+
<div class="log-actions">
|
|
128
|
+
<el-button
|
|
129
|
+
type="primary"
|
|
130
|
+
size="small"
|
|
131
|
+
@click="toggleViewMode"
|
|
132
|
+
>
|
|
133
|
+
{{ showGraphView ? '表格视图' : '图表视图' }}
|
|
134
|
+
</el-button>
|
|
135
|
+
<el-button
|
|
136
|
+
type="primary"
|
|
137
|
+
size="small"
|
|
138
|
+
@click="toggleAllCommits"
|
|
139
|
+
:loading="isLoading"
|
|
140
|
+
>
|
|
141
|
+
{{ showAllCommits ? '显示最近100条' : '显示所有提交' }}
|
|
142
|
+
</el-button>
|
|
143
|
+
<el-button
|
|
144
|
+
:icon="RefreshRight"
|
|
145
|
+
circle
|
|
146
|
+
size="small"
|
|
147
|
+
@click="refreshLog()"
|
|
148
|
+
:loading="isLoading"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div v-if="errorMessage">{{ errorMessage }}</div>
|
|
153
|
+
<div v-else>
|
|
154
|
+
<!-- 图表视图 -->
|
|
155
|
+
<div v-if="showGraphView" class="graph-view">
|
|
156
|
+
<div class="commit-count" v-if="logs.length > 0">
|
|
157
|
+
显示 {{ logs.length }} 条提交记录 {{ showAllCommits ? '(全部)' : '(最近100条)' }}
|
|
158
|
+
</div>
|
|
159
|
+
<div ref="graphContainer" class="graph-container"></div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- 表格视图 -->
|
|
163
|
+
<div v-else>
|
|
164
|
+
<div class="commit-count" v-if="logs.length > 0">
|
|
165
|
+
显示 {{ logs.length }} 条提交记录 {{ showAllCommits ? '(全部)' : '(最近100条)' }}
|
|
166
|
+
</div>
|
|
167
|
+
<el-table :data="logs" style="width: 100%" stripe border v-loading="isLoading">
|
|
168
|
+
<el-table-column prop="hash" label="提交哈希" width="100" resizable />
|
|
169
|
+
<el-table-column prop="date" label="日期" width="180" resizable />
|
|
170
|
+
<el-table-column label="作者" width="200" resizable>
|
|
171
|
+
<template #default="scope">
|
|
172
|
+
{{ scope.row.author }} <{{ scope.row.email }}>
|
|
173
|
+
</template>
|
|
174
|
+
</el-table-column>
|
|
175
|
+
<el-table-column label="分支" width="180" resizable>
|
|
176
|
+
<template #default="scope">
|
|
177
|
+
<div v-if="scope.row.branch" class="branch-container">
|
|
178
|
+
<el-tag
|
|
179
|
+
v-for="(ref, index) in scope.row.branch.split(',')"
|
|
180
|
+
:key="index"
|
|
181
|
+
size="small"
|
|
182
|
+
:type="getBranchTagType(ref)"
|
|
183
|
+
class="branch-tag"
|
|
184
|
+
>
|
|
185
|
+
{{ formatBranchName(ref) }}
|
|
186
|
+
</el-tag>
|
|
187
|
+
</div>
|
|
188
|
+
</template>
|
|
189
|
+
</el-table-column>
|
|
190
|
+
<el-table-column prop="message" label="提交信息" min-width="250" />
|
|
191
|
+
</el-table>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
196
|
+
|
|
197
|
+
<style scoped>
|
|
198
|
+
.log-header {
|
|
199
|
+
display: flex;
|
|
200
|
+
justify-content: space-between;
|
|
201
|
+
align-items: center;
|
|
202
|
+
margin-bottom: 10px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.log-actions {
|
|
206
|
+
display: flex;
|
|
207
|
+
gap: 8px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.branch-container {
|
|
211
|
+
display: flex;
|
|
212
|
+
flex-wrap: wrap;
|
|
213
|
+
gap: 4px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.branch-tag {
|
|
217
|
+
margin-right: 4px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.commit-count {
|
|
221
|
+
margin-bottom: 10px;
|
|
222
|
+
font-size: 14px;
|
|
223
|
+
color: #606266;
|
|
224
|
+
text-align: right;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.graph-container {
|
|
228
|
+
width: 100%;
|
|
229
|
+
height: 600px;
|
|
230
|
+
overflow: auto;
|
|
231
|
+
border: 1px solid #ebeef5;
|
|
232
|
+
border-radius: 4px;
|
|
233
|
+
padding: 10px;
|
|
234
|
+
background-color: #fff;
|
|
235
|
+
/* transform: scale(0.8); */
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.graph-view {
|
|
239
|
+
width: 100%;
|
|
240
|
+
}
|
|
241
|
+
</style>
|
|
242
|
+
|
|
243
|
+
<script lang="ts">
|
|
244
|
+
// 辅助函数:格式化分支名称
|
|
245
|
+
function formatBranchName(ref: string) {
|
|
246
|
+
// 移除 "HEAD -> " 前缀
|
|
247
|
+
ref = ref.trim().replace(/^HEAD\s*->\s*/, '')
|
|
248
|
+
|
|
249
|
+
// 移除 "origin/" 前缀
|
|
250
|
+
ref = ref.replace(/^origin\//, '')
|
|
251
|
+
|
|
252
|
+
// 移除 "tag: " 前缀,但保留标签名
|
|
253
|
+
ref = ref.replace(/^tag:\s*/, '')
|
|
254
|
+
|
|
255
|
+
return ref.trim()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 辅助函数:根据分支类型返回不同的标签类型
|
|
259
|
+
function getBranchTagType(ref: string) {
|
|
260
|
+
if (ref.includes('HEAD')) return 'success'
|
|
261
|
+
if (ref.includes('tag:')) return 'warning'
|
|
262
|
+
if (ref.includes('origin/')) return 'info'
|
|
263
|
+
return 'primary' // 修改空字符串为有效的type值
|
|
264
|
+
}
|
|
265
|
+
</script>
|
|
266
|
+
|
|
267
|
+
/* 添加表格列调整样式 */
|
|
268
|
+
.el-table .el-table__cell .cell {
|
|
269
|
+
word-break: break-all;
|
|
270
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|