zen-gitsync 2.0.5 → 2.0.7
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 +31 -2
- package/package.json +3 -1
- package/src/ui/client/components.d.ts +1 -0
- package/src/ui/client/src/App.vue +417 -123
- package/src/ui/client/src/components/CommitForm.vue +553 -285
- package/src/ui/client/src/components/GitStatus.vue +954 -186
- package/src/ui/client/src/components/LogList.vue +1462 -169
- package/src/ui/client/src/stores/gitLogStore.ts +340 -17
- package/src/ui/client/src/stores/gitStore.ts +42 -1
- package/src/ui/client/stats.html +1 -1
- package/src/ui/client/vite.config.ts +2 -0
- package/src/ui/public/assets/index-B1vQ6PC_.js +20 -0
- package/src/ui/public/assets/index-CC5qWyQ-.css +1 -0
- package/src/ui/public/assets/vendor-BmPvvgTD.js +45 -0
- package/src/ui/public/index.html +3 -3
- package/src/ui/server/index.js +552 -54
- package/src/ui/public/assets/index-CALk9kKc.js +0 -9
- package/src/ui/public/assets/index-D3zIiSNw.css +0 -1
- package/src/ui/public/assets/vendor-BfXVsoKv.js +0 -45
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, onMounted, computed, watch } from 'vue'
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { ref, onMounted, computed, watch, onBeforeUnmount, nextTick } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
ElTable,
|
|
5
|
+
ElTableColumn,
|
|
6
|
+
ElTag,
|
|
7
|
+
ElButton,
|
|
8
|
+
ElSlider,
|
|
9
|
+
ElDialog,
|
|
10
|
+
ElSelect,
|
|
11
|
+
ElOption,
|
|
12
|
+
ElDatePicker,
|
|
13
|
+
ElInput,
|
|
14
|
+
ElBadge,
|
|
15
|
+
ElMessage,
|
|
16
|
+
ElMessageBox
|
|
17
|
+
} from 'element-plus'
|
|
18
|
+
import { RefreshRight, ZoomIn, ZoomOut, Filter, Document, TrendCharts, List, More } from '@element-plus/icons-vue'
|
|
5
19
|
import 'element-plus/dist/index.css'
|
|
6
20
|
import { createGitgraph } from '@gitgraph/js'
|
|
7
21
|
import { useGitLogStore } from '../stores/gitLogStore'
|
|
@@ -30,9 +44,23 @@ const localLoading = ref(false)
|
|
|
30
44
|
const isLoading = computed(() => gitLogStore.isLoadingLog || localLoading.value)
|
|
31
45
|
const showAllCommits = ref(false)
|
|
32
46
|
const totalCommits = ref(0)
|
|
33
|
-
const showGraphView = ref(
|
|
47
|
+
const showGraphView = ref(false)
|
|
34
48
|
const graphContainer = ref<HTMLElement | null>(null)
|
|
35
49
|
|
|
50
|
+
// 添加分页相关变量
|
|
51
|
+
const currentPage = ref(1)
|
|
52
|
+
const hasMoreData = ref(true)
|
|
53
|
+
const isLoadingMore = ref(false)
|
|
54
|
+
const loadTimerInterval = ref<number | null>(null)
|
|
55
|
+
|
|
56
|
+
// 添加提交详情弹窗相关变量
|
|
57
|
+
const commitDetailVisible = ref(false)
|
|
58
|
+
const selectedCommit = ref<LogItem | null>(null)
|
|
59
|
+
const commitFiles = ref<string[]>([])
|
|
60
|
+
const commitDiff = ref('')
|
|
61
|
+
const isLoadingCommitDetail = ref(false)
|
|
62
|
+
const selectedCommitFile = ref('')
|
|
63
|
+
|
|
36
64
|
// 添加图表缩放控制
|
|
37
65
|
const graphScale = ref(1)
|
|
38
66
|
const minScale = 0.5
|
|
@@ -42,8 +70,27 @@ const scaleStep = 0.1
|
|
|
42
70
|
// 添加日志被刷新的提示状态
|
|
43
71
|
const logRefreshed = ref(false)
|
|
44
72
|
|
|
73
|
+
// 添加筛选相关变量
|
|
74
|
+
const filterVisible = ref(false)
|
|
75
|
+
const authorFilter = ref<string[]>([])
|
|
76
|
+
const messageFilter = ref('')
|
|
77
|
+
const dateRangeFilter = ref<any>(null)
|
|
78
|
+
const availableAuthors = ref<string[]>([])
|
|
79
|
+
|
|
80
|
+
// 添加右键菜单相关变量
|
|
81
|
+
const contextMenuVisible = ref(false)
|
|
82
|
+
const contextMenuTop = ref(0)
|
|
83
|
+
const contextMenuLeft = ref(0)
|
|
84
|
+
const selectedContextCommit = ref<LogItem | null>(null)
|
|
85
|
+
|
|
86
|
+
// 应用筛选后的日志
|
|
87
|
+
const filteredLogs = computed(() => {
|
|
88
|
+
// 不再在前端进行筛选,直接使用加载的日志
|
|
89
|
+
return logs.value;
|
|
90
|
+
})
|
|
91
|
+
|
|
45
92
|
// 加载提交历史
|
|
46
|
-
async function loadLog(all = false) {
|
|
93
|
+
async function loadLog(all = false, page = 1) {
|
|
47
94
|
// 从gitStore获取仓库状态
|
|
48
95
|
const gitStore = useGitStore()
|
|
49
96
|
|
|
@@ -54,57 +101,81 @@ async function loadLog(all = false) {
|
|
|
54
101
|
}
|
|
55
102
|
|
|
56
103
|
try {
|
|
57
|
-
|
|
104
|
+
// 设置加载状态
|
|
105
|
+
if (page > 1) {
|
|
106
|
+
isLoadingMore.value = true
|
|
107
|
+
} else {
|
|
108
|
+
localLoading.value = true
|
|
109
|
+
}
|
|
58
110
|
|
|
59
|
-
|
|
60
|
-
localLoading.value = true
|
|
111
|
+
console.log(`加载提交历史: page=${page}, all=${all}`)
|
|
61
112
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
113
|
+
// 构建查询参数
|
|
114
|
+
const queryParams = new URLSearchParams()
|
|
115
|
+
queryParams.append('page', page.toString())
|
|
116
|
+
queryParams.append('all', all.toString())
|
|
66
117
|
|
|
67
|
-
|
|
68
|
-
|
|
118
|
+
// 添加筛选参数
|
|
119
|
+
if (authorFilter.value.length > 0) {
|
|
120
|
+
queryParams.append('author', authorFilter.value.join(','))
|
|
121
|
+
}
|
|
69
122
|
|
|
70
|
-
|
|
71
|
-
|
|
123
|
+
if (messageFilter.value) {
|
|
124
|
+
queryParams.append('message', messageFilter.value)
|
|
125
|
+
}
|
|
72
126
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
totalCommits.value = data.log.length
|
|
90
|
-
} else {
|
|
91
|
-
console.error('未知的日志数据格式:', data)
|
|
92
|
-
errorMessage.value = '日志数据格式错误'
|
|
127
|
+
if (dateRangeFilter.value && Array.isArray(dateRangeFilter.value) && dateRangeFilter.value.length === 2) {
|
|
128
|
+
queryParams.append('dateFrom', dateRangeFilter.value[0])
|
|
129
|
+
queryParams.append('dateTo', dateRangeFilter.value[1])
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 添加时间戳防止缓存
|
|
133
|
+
queryParams.append('_t', Date.now().toString())
|
|
134
|
+
|
|
135
|
+
const response = await fetch(`/api/log?${queryParams.toString()}`)
|
|
136
|
+
const result = await response.json()
|
|
137
|
+
|
|
138
|
+
// 确保result有正确的数据结构
|
|
139
|
+
if (!result || !result.data || !Array.isArray(result.data)) {
|
|
140
|
+
console.error('API返回的数据格式不正确:', result)
|
|
141
|
+
errorMessage.value = '加载提交历史失败: 服务器返回数据格式不正确'
|
|
93
142
|
return
|
|
94
143
|
}
|
|
95
144
|
|
|
96
|
-
|
|
145
|
+
const isLoadMore = page > 1
|
|
146
|
+
|
|
147
|
+
// 处理结果
|
|
148
|
+
// 如果是加载更多,追加数据,否则替换数据
|
|
149
|
+
if (isLoadMore) {
|
|
150
|
+
result.data.forEach((item: LogItem) => logsData.push(item))
|
|
151
|
+
} else {
|
|
152
|
+
logsData.length = 0
|
|
153
|
+
result.data.forEach((item: LogItem) => logsData.push(item))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 强制重新渲染列表
|
|
97
157
|
logs.value = [...logsData]
|
|
98
158
|
|
|
99
|
-
|
|
159
|
+
// 更新当前页码
|
|
160
|
+
currentPage.value = page
|
|
100
161
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
162
|
+
// 更新总数和分页标记
|
|
163
|
+
totalCommits.value = result.total || logsData.length
|
|
164
|
+
hasMoreData.value = result.hasMore === true
|
|
165
|
+
|
|
166
|
+
if (!hasMoreData.value) {
|
|
167
|
+
console.log('已加载所有提交记录')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 设置刷新提示状态(仅在初次加载时)
|
|
171
|
+
if (!isLoadMore) {
|
|
172
|
+
logRefreshed.value = true
|
|
173
|
+
// 2秒后隐藏提示
|
|
174
|
+
setTimeout(() => { logRefreshed.value = false }, 2000)
|
|
175
|
+
}
|
|
105
176
|
|
|
106
|
-
//
|
|
107
|
-
if (showGraphView.value) {
|
|
177
|
+
// 加载完数据后渲染图表(仅在初次加载时)
|
|
178
|
+
if (!isLoadMore && showGraphView.value) {
|
|
108
179
|
setTimeout(renderGraph, 0)
|
|
109
180
|
}
|
|
110
181
|
|
|
@@ -112,9 +183,18 @@ async function loadLog(all = false) {
|
|
|
112
183
|
} catch (error) {
|
|
113
184
|
errorMessage.value = '加载提交历史失败: ' + (error instanceof Error ? error.message : String(error))
|
|
114
185
|
console.error('加载日志失败:', error)
|
|
186
|
+
|
|
187
|
+
// 如果加载更多失败,标记没有更多数据
|
|
188
|
+
if (page > 1) {
|
|
189
|
+
hasMoreData.value = false
|
|
190
|
+
}
|
|
115
191
|
} finally {
|
|
116
|
-
//
|
|
117
|
-
|
|
192
|
+
// 重置加载状态
|
|
193
|
+
if (page > 1) {
|
|
194
|
+
isLoadingMore.value = false
|
|
195
|
+
} else {
|
|
196
|
+
localLoading.value = false
|
|
197
|
+
}
|
|
118
198
|
}
|
|
119
199
|
}
|
|
120
200
|
|
|
@@ -224,6 +304,59 @@ function getBranchTagType(ref: string) {
|
|
|
224
304
|
return 'info'
|
|
225
305
|
}
|
|
226
306
|
|
|
307
|
+
// 添加对表格实例的引用
|
|
308
|
+
const tableRef = ref<InstanceType<typeof ElTable> | null>(null)
|
|
309
|
+
const tableBodyWrapper = ref<HTMLElement | null>(null)
|
|
310
|
+
|
|
311
|
+
// 监听表格滚动事件的处理函数
|
|
312
|
+
function handleTableScroll(event: Event) {
|
|
313
|
+
if (showGraphView.value || !hasMoreData.value || isLoadingMore.value || isLoading.value) {
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const target = event.target as HTMLElement
|
|
318
|
+
const { scrollTop, scrollHeight, clientHeight } = target
|
|
319
|
+
const scrollDistance = scrollHeight - scrollTop - clientHeight
|
|
320
|
+
|
|
321
|
+
// 调试信息
|
|
322
|
+
console.log('表格滚动:', {
|
|
323
|
+
scrollTop,
|
|
324
|
+
scrollHeight,
|
|
325
|
+
clientHeight,
|
|
326
|
+
scrollDistance
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
// 当滚动到距离底部20px时触发加载
|
|
330
|
+
if (scrollDistance <= 20) {
|
|
331
|
+
console.log('已滚动到底部,加载更多数据')
|
|
332
|
+
loadMoreLogs()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 设置表格滚动监听
|
|
337
|
+
function setupTableScrollListener() {
|
|
338
|
+
if (!tableRef.value) return
|
|
339
|
+
|
|
340
|
+
// 获取表格的body-wrapper
|
|
341
|
+
tableBodyWrapper.value = tableRef.value.$el.querySelector('.el-table__body-wrapper')
|
|
342
|
+
|
|
343
|
+
if (tableBodyWrapper.value) {
|
|
344
|
+
console.log('添加表格滚动监听')
|
|
345
|
+
tableBodyWrapper.value.addEventListener('scroll', handleTableScroll, true)
|
|
346
|
+
} else {
|
|
347
|
+
console.error('未找到表格的body-wrapper元素')
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 移除表格滚动监听
|
|
352
|
+
function removeTableScrollListener() {
|
|
353
|
+
if (tableBodyWrapper.value) {
|
|
354
|
+
console.log('移除表格滚动监听')
|
|
355
|
+
tableBodyWrapper.value.removeEventListener('scroll', handleTableScroll, true)
|
|
356
|
+
tableBodyWrapper.value = null
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
227
360
|
onMounted(() => {
|
|
228
361
|
// 检查gitLogStore中是否已有数据
|
|
229
362
|
if (gitStore.isGitRepo) {
|
|
@@ -250,9 +383,30 @@ onMounted(() => {
|
|
|
250
383
|
console.log('初始加载日志数据')
|
|
251
384
|
loadLog()
|
|
252
385
|
}
|
|
386
|
+
|
|
387
|
+
// 加载所有可能的作者列表
|
|
388
|
+
fetchAllAuthors()
|
|
253
389
|
} else {
|
|
254
390
|
errorMessage.value = '当前目录不是Git仓库'
|
|
255
391
|
}
|
|
392
|
+
|
|
393
|
+
// 在下一个tick中设置表格滚动监听
|
|
394
|
+
nextTick(() => {
|
|
395
|
+
setTimeout(() => {
|
|
396
|
+
setupTableScrollListener()
|
|
397
|
+
}, 500) // 给表格足够的时间来渲染
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
onBeforeUnmount(() => {
|
|
402
|
+
// 清除表格滚动监听
|
|
403
|
+
removeTableScrollListener()
|
|
404
|
+
|
|
405
|
+
// 清除定时器
|
|
406
|
+
if (loadTimerInterval.value !== null) {
|
|
407
|
+
window.clearInterval(loadTimerInterval.value)
|
|
408
|
+
loadTimerInterval.value = null
|
|
409
|
+
}
|
|
256
410
|
})
|
|
257
411
|
|
|
258
412
|
// 简化刷新函数,只需调用loadLog即可
|
|
@@ -261,36 +415,49 @@ const refreshLog = () => {
|
|
|
261
415
|
errorMessage.value = '当前目录不是Git仓库'
|
|
262
416
|
return
|
|
263
417
|
}
|
|
264
|
-
|
|
418
|
+
// 重置页码,重新加载第一页
|
|
419
|
+
currentPage.value = 1
|
|
420
|
+
hasMoreData.value = true
|
|
421
|
+
loadLog(showAllCommits.value, 1)
|
|
265
422
|
}
|
|
266
423
|
|
|
267
424
|
// 监听store中的日志变化
|
|
268
425
|
watch(() => gitLogStore.log, (newLogs) => {
|
|
269
426
|
console.log('监听到gitLogStore.log变化,更新图表数据')
|
|
270
427
|
|
|
271
|
-
// 清空logsData
|
|
272
|
-
logsData.length = 0
|
|
273
|
-
|
|
274
|
-
// 重新填充数据
|
|
275
|
-
newLogs.forEach((item: LogItem) => logsData.push(item))
|
|
276
|
-
|
|
277
|
-
// 更新计数器
|
|
278
|
-
totalCommits.value = newLogs.length
|
|
279
|
-
|
|
280
|
-
// 尝试解决logs.value赋值问题
|
|
281
428
|
try {
|
|
282
|
-
//
|
|
429
|
+
// 清空logsData
|
|
430
|
+
logsData.length = 0
|
|
431
|
+
|
|
432
|
+
// 重新填充数据,使用类型断言
|
|
433
|
+
if (Array.isArray(newLogs)) {
|
|
434
|
+
// @ts-ignore - 忽略类型校验
|
|
435
|
+
newLogs.forEach(item => item && logsData.push(item))
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 更新计数器
|
|
439
|
+
totalCommits.value = logsData.length
|
|
440
|
+
|
|
441
|
+
// 重置当前页为第1页
|
|
442
|
+
currentPage.value = 1
|
|
443
|
+
|
|
444
|
+
// 确保引用更新,触发UI重渲染
|
|
445
|
+
// @ts-ignore - 忽略类型校验
|
|
283
446
|
logs.value = [...logsData]
|
|
447
|
+
|
|
448
|
+
// 设置刷新提示
|
|
449
|
+
logRefreshed.value = true
|
|
450
|
+
setTimeout(() => { logRefreshed.value = false }, 2000)
|
|
451
|
+
|
|
452
|
+
console.log(`数据更新完成,共${logs.value.length}条记录,准备渲染图表`)
|
|
453
|
+
|
|
454
|
+
if (showGraphView.value && logsData.length > 0) {
|
|
455
|
+
setTimeout(renderGraph, 0)
|
|
456
|
+
}
|
|
284
457
|
} catch (error) {
|
|
285
|
-
console.
|
|
458
|
+
console.error('更新日志数据失败:', error)
|
|
286
459
|
}
|
|
287
|
-
|
|
288
|
-
console.log(`数据更新完成,准备渲染图表(${logsData.length}条记录)`)
|
|
289
|
-
|
|
290
|
-
if (showGraphView.value && logsData.length > 0) {
|
|
291
|
-
setTimeout(renderGraph, 0)
|
|
292
|
-
}
|
|
293
|
-
})
|
|
460
|
+
}, { immediate: true })
|
|
294
461
|
|
|
295
462
|
// 暴露方法给父组件
|
|
296
463
|
defineExpose({
|
|
@@ -345,18 +512,373 @@ function fitGraphToContainer() {
|
|
|
345
512
|
|
|
346
513
|
applyScale()
|
|
347
514
|
}
|
|
515
|
+
|
|
516
|
+
// 查看提交详情
|
|
517
|
+
async function viewCommitDetail(commit: LogItem | null) {
|
|
518
|
+
if (!commit) return
|
|
519
|
+
|
|
520
|
+
selectedCommit.value = commit
|
|
521
|
+
commitDetailVisible.value = true
|
|
522
|
+
isLoadingCommitDetail.value = true
|
|
523
|
+
commitFiles.value = []
|
|
524
|
+
commitDiff.value = ''
|
|
525
|
+
selectedCommitFile.value = ''
|
|
526
|
+
|
|
527
|
+
// 调试输出当前提交对象的所有属性
|
|
528
|
+
console.log('提交详情对象:', JSON.stringify(commit, null, 2))
|
|
529
|
+
console.log('哈希值类型和长度:', typeof commit.hash, commit.hash ? commit.hash.length : 0)
|
|
530
|
+
console.log('提交信息类型和长度:', typeof commit.message, commit.message ? commit.message.length : 0)
|
|
531
|
+
console.log('提交分支:', commit.branch)
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
console.log(`获取提交详情: ${commit.hash}`)
|
|
535
|
+
|
|
536
|
+
// 确保哈希值有效
|
|
537
|
+
if (!commit.hash || commit.hash.length < 7) {
|
|
538
|
+
console.error('无效的提交哈希值:', commit.hash)
|
|
539
|
+
commitDiff.value = '无效的提交哈希值'
|
|
540
|
+
isLoadingCommitDetail.value = false
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 获取提交的变更文件列表
|
|
545
|
+
const filesResponse = await fetch(`/api/commit-files?hash=${commit.hash}`)
|
|
546
|
+
console.log('API响应状态: ', filesResponse.status)
|
|
547
|
+
const filesData = await filesResponse.json()
|
|
548
|
+
console.log('文件列表数据: ', filesData)
|
|
549
|
+
|
|
550
|
+
if (filesData.success && Array.isArray(filesData.files)) {
|
|
551
|
+
commitFiles.value = filesData.files
|
|
552
|
+
|
|
553
|
+
// 如果有文件,自动加载第一个文件的差异
|
|
554
|
+
if (commitFiles.value.length > 0) {
|
|
555
|
+
await getCommitFileDiff(commit.hash, commitFiles.value[0])
|
|
556
|
+
} else {
|
|
557
|
+
console.log('没有找到变更文件')
|
|
558
|
+
commitDiff.value = '该提交没有变更文件'
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
console.error('获取提交文件列表失败:', filesData.error || '未知错误')
|
|
562
|
+
commitDiff.value = `获取文件列表失败: ${filesData.error || '未知错误'}`
|
|
563
|
+
}
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.error('获取提交详情失败:', error)
|
|
566
|
+
commitDiff.value = `获取提交详情失败: ${(error as Error).message}`
|
|
567
|
+
} finally {
|
|
568
|
+
isLoadingCommitDetail.value = false
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// 获取提交中特定文件的差异
|
|
573
|
+
async function getCommitFileDiff(hash: string, filePath: string) {
|
|
574
|
+
isLoadingCommitDetail.value = true
|
|
575
|
+
selectedCommitFile.value = filePath
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
console.log(`获取文件差异: hash=${hash}, file=${filePath}`)
|
|
579
|
+
const diffResponse = await fetch(`/api/commit-file-diff?hash=${hash}&file=${encodeURIComponent(filePath)}`)
|
|
580
|
+
console.log('差异API响应状态: ', diffResponse.status)
|
|
581
|
+
const diffData = await diffResponse.json()
|
|
582
|
+
console.log('差异数据: ', diffData.success, typeof diffData.diff)
|
|
583
|
+
|
|
584
|
+
if (diffData.success) {
|
|
585
|
+
commitDiff.value = diffData.diff || '没有变更内容'
|
|
586
|
+
} else {
|
|
587
|
+
console.error('获取差异失败: ', diffData.error)
|
|
588
|
+
commitDiff.value = `获取差异失败: ${diffData.error || '未知错误'}`
|
|
589
|
+
}
|
|
590
|
+
} catch (error) {
|
|
591
|
+
console.error('获取文件差异失败:', error)
|
|
592
|
+
commitDiff.value = `获取差异失败: ${(error as Error).message}`
|
|
593
|
+
} finally {
|
|
594
|
+
isLoadingCommitDetail.value = false
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// 格式化差异内容,添加颜色
|
|
599
|
+
function formatDiff(diffText: string) {
|
|
600
|
+
if (!diffText) return '';
|
|
601
|
+
|
|
602
|
+
// 将差异内容按行分割
|
|
603
|
+
const lines = diffText.split('\n');
|
|
604
|
+
|
|
605
|
+
// 转义 HTML 标签的函数
|
|
606
|
+
function escapeHtml(text: string) {
|
|
607
|
+
return text
|
|
608
|
+
.replace(/&/g, '&')
|
|
609
|
+
.replace(/</g, '<')
|
|
610
|
+
.replace(/>/g, '>')
|
|
611
|
+
.replace(/"/g, '"')
|
|
612
|
+
.replace(/'/g, ''');
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// 为每行添加适当的 CSS 类
|
|
616
|
+
return lines.map(line => {
|
|
617
|
+
// 先转义 HTML 标签,再添加样式
|
|
618
|
+
const escapedLine = escapeHtml(line);
|
|
619
|
+
|
|
620
|
+
if (line.startsWith('diff --git')) {
|
|
621
|
+
return `<div class="diff-header">${escapedLine}</div>`;
|
|
622
|
+
} else if (line.startsWith('---')) {
|
|
623
|
+
return `<div class="diff-old-file">${escapedLine}</div>`;
|
|
624
|
+
} else if (line.startsWith('+++')) {
|
|
625
|
+
return `<div class="diff-new-file">${escapedLine}</div>`;
|
|
626
|
+
} else if (line.startsWith('@@')) {
|
|
627
|
+
return `<div class="diff-hunk-header">${escapedLine}</div>`;
|
|
628
|
+
} else if (line.startsWith('+')) {
|
|
629
|
+
return `<div class="diff-added">${escapedLine}</div>`;
|
|
630
|
+
} else if (line.startsWith('-')) {
|
|
631
|
+
return `<div class="diff-removed">${escapedLine}</div>`;
|
|
632
|
+
} else {
|
|
633
|
+
return `<div class="diff-context">${escapedLine}</div>`;
|
|
634
|
+
}
|
|
635
|
+
}).join('');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// 格式化提交信息,支持多行显示
|
|
639
|
+
function formatCommitMessage(message: string) {
|
|
640
|
+
if (!message) return '(无提交信息)';
|
|
641
|
+
|
|
642
|
+
// 调试输出
|
|
643
|
+
console.log('格式化前的提交信息:', message)
|
|
644
|
+
console.log('提交信息中的换行符数量:', (message.match(/\n/g) || []).length)
|
|
645
|
+
|
|
646
|
+
// 返回格式化后的提交信息,保留换行符
|
|
647
|
+
return message.trim();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// 加载更多日志
|
|
651
|
+
function loadMoreLogs() {
|
|
652
|
+
if (!hasMoreData.value || isLoadingMore.value || isLoading.value) return
|
|
653
|
+
|
|
654
|
+
console.log(`加载更多日志,页码: ${currentPage.value + 1}`)
|
|
655
|
+
loadLog(showAllCommits.value, currentPage.value + 1)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// 重置筛选条件并重新加载数据
|
|
659
|
+
function resetFilters() {
|
|
660
|
+
authorFilter.value = []
|
|
661
|
+
messageFilter.value = ''
|
|
662
|
+
dateRangeFilter.value = null
|
|
663
|
+
|
|
664
|
+
// 重置筛选后重新加载数据
|
|
665
|
+
currentPage.value = 1
|
|
666
|
+
loadLog(showAllCommits.value, 1)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// 应用筛选条件
|
|
670
|
+
function applyFilters() {
|
|
671
|
+
// 应用筛选时重置到第一页
|
|
672
|
+
currentPage.value = 1
|
|
673
|
+
loadLog(showAllCommits.value, 1)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// 添加获取所有作者的函数
|
|
677
|
+
async function fetchAllAuthors() {
|
|
678
|
+
try {
|
|
679
|
+
console.log('获取所有可用作者...')
|
|
680
|
+
const response = await fetch('/api/authors')
|
|
681
|
+
const result = await response.json()
|
|
682
|
+
|
|
683
|
+
if (result.success && Array.isArray(result.authors)) {
|
|
684
|
+
// 更新可用作者列表
|
|
685
|
+
availableAuthors.value = result.authors.sort()
|
|
686
|
+
console.log(`获取到${availableAuthors.value.length}位作者`)
|
|
687
|
+
} else {
|
|
688
|
+
// 如果获取作者列表失败,但正常获取了日志
|
|
689
|
+
// 从当前加载的日志中提取作者列表作为备选
|
|
690
|
+
console.warn('从API获取作者列表失败,将从现有日志中提取作者列表')
|
|
691
|
+
extractAuthorsFromLogs()
|
|
692
|
+
}
|
|
693
|
+
} catch (error) {
|
|
694
|
+
console.error('获取作者列表失败:', error)
|
|
695
|
+
// 从当前加载的日志中提取作者列表作为备选
|
|
696
|
+
extractAuthorsFromLogs()
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// 从已加载的日志中提取作者列表
|
|
701
|
+
function extractAuthorsFromLogs() {
|
|
702
|
+
const authors = new Set<string>()
|
|
703
|
+
logs.value.forEach(log => {
|
|
704
|
+
if (log.author) {
|
|
705
|
+
authors.add(log.author)
|
|
706
|
+
}
|
|
707
|
+
})
|
|
708
|
+
availableAuthors.value = Array.from(authors).sort()
|
|
709
|
+
console.log(`从现有日志中提取了${availableAuthors.value.length}位作者`)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// 处理右键菜单事件
|
|
713
|
+
function handleContextMenu(row: LogItem, column: any, event: MouseEvent) {
|
|
714
|
+
console.log('handleContextMenu', row, column, event)
|
|
715
|
+
// 阻止默认右键菜单
|
|
716
|
+
event.preventDefault()
|
|
717
|
+
|
|
718
|
+
// 设置右键菜单位置
|
|
719
|
+
contextMenuTop.value = event.clientY
|
|
720
|
+
contextMenuLeft.value = event.clientX
|
|
721
|
+
|
|
722
|
+
// 设置选中的提交
|
|
723
|
+
selectedContextCommit.value = row
|
|
724
|
+
|
|
725
|
+
// 显示右键菜单
|
|
726
|
+
contextMenuVisible.value = true
|
|
727
|
+
|
|
728
|
+
// 点击其他地方时隐藏菜单
|
|
729
|
+
const hideContextMenu = () => {
|
|
730
|
+
contextMenuVisible.value = false
|
|
731
|
+
document.removeEventListener('click', hideContextMenu)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// 添加点击监听器
|
|
735
|
+
setTimeout(() => {
|
|
736
|
+
document.addEventListener('click', hideContextMenu)
|
|
737
|
+
}, 0)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// 撤销提交 (Revert)
|
|
741
|
+
async function revertCommit(commit: LogItem | null) {
|
|
742
|
+
if (!commit) return
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
// 询问确认
|
|
746
|
+
await ElMessageBox.confirm(
|
|
747
|
+
`确定要撤销提交 ${commit.hash.substring(0, 7)} 吗?这将创建一个新的提交来撤销这次提交的更改。`,
|
|
748
|
+
'撤销提交确认',
|
|
749
|
+
{
|
|
750
|
+
confirmButtonText: '确认',
|
|
751
|
+
cancelButtonText: '取消',
|
|
752
|
+
type: 'warning'
|
|
753
|
+
}
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
// 发送请求
|
|
757
|
+
const response = await fetch('/api/revert-commit', {
|
|
758
|
+
method: 'POST',
|
|
759
|
+
headers: {
|
|
760
|
+
'Content-Type': 'application/json'
|
|
761
|
+
},
|
|
762
|
+
body: JSON.stringify({ hash: commit.hash })
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
const result = await response.json()
|
|
766
|
+
|
|
767
|
+
if (result.success) {
|
|
768
|
+
ElMessage.success(result.message || '已成功撤销提交')
|
|
769
|
+
// 刷新日志
|
|
770
|
+
refreshLog()
|
|
771
|
+
// 刷新Git状态
|
|
772
|
+
gitLogStore.fetchStatus()
|
|
773
|
+
// 添加: 刷新分支状态
|
|
774
|
+
gitStore.getBranchStatus()
|
|
775
|
+
} else {
|
|
776
|
+
ElMessage.error(result.error || '撤销提交失败')
|
|
777
|
+
}
|
|
778
|
+
} catch (error: any) {
|
|
779
|
+
if (error !== 'cancel') {
|
|
780
|
+
console.error('撤销提交出错:', error)
|
|
781
|
+
ElMessage.error('撤销提交失败: ' + (error.message || error))
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Cherry-pick提交
|
|
787
|
+
async function cherryPickCommit(commit: LogItem | null) {
|
|
788
|
+
if (!commit) return
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
// 询问确认
|
|
792
|
+
await ElMessageBox.confirm(
|
|
793
|
+
`确定要将提交 ${commit.hash.substring(0, 7)} Cherry-Pick 到当前分支吗?`,
|
|
794
|
+
'Cherry-Pick确认',
|
|
795
|
+
{
|
|
796
|
+
confirmButtonText: '确认',
|
|
797
|
+
cancelButtonText: '取消',
|
|
798
|
+
type: 'warning'
|
|
799
|
+
}
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
// 发送请求
|
|
803
|
+
const response = await fetch('/api/cherry-pick-commit', {
|
|
804
|
+
method: 'POST',
|
|
805
|
+
headers: {
|
|
806
|
+
'Content-Type': 'application/json'
|
|
807
|
+
},
|
|
808
|
+
body: JSON.stringify({ hash: commit.hash })
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
const result = await response.json()
|
|
812
|
+
|
|
813
|
+
if (result.success) {
|
|
814
|
+
ElMessage.success(result.message || '已成功Cherry-Pick提交')
|
|
815
|
+
// 刷新日志
|
|
816
|
+
refreshLog()
|
|
817
|
+
// 刷新Git状态
|
|
818
|
+
gitLogStore.fetchStatus()
|
|
819
|
+
// 添加: 刷新分支状态
|
|
820
|
+
gitStore.getBranchStatus()
|
|
821
|
+
} else {
|
|
822
|
+
ElMessage.error(result.error || 'Cherry-Pick提交失败')
|
|
823
|
+
}
|
|
824
|
+
} catch (error: any) {
|
|
825
|
+
if (error !== 'cancel') {
|
|
826
|
+
console.error('Cherry-Pick提交出错:', error)
|
|
827
|
+
ElMessage.error('Cherry-Pick提交失败: ' + (error.message || error))
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
348
831
|
</script>
|
|
349
832
|
|
|
350
833
|
<template>
|
|
351
834
|
<div class="card">
|
|
835
|
+
<!-- 添加刷新提示,移到最外层 -->
|
|
836
|
+
<div v-if="logRefreshed" class="refresh-notification">
|
|
837
|
+
提交历史已刷新
|
|
838
|
+
</div>
|
|
839
|
+
|
|
840
|
+
<!-- 固定头部区域 -->
|
|
352
841
|
<div class="log-header">
|
|
353
|
-
<
|
|
842
|
+
<div class="header-left">
|
|
843
|
+
<h2>提交历史</h2>
|
|
844
|
+
<!-- <el-tag type="info" effect="plain" size="small" class="record-count" v-if="!showGraphView">
|
|
845
|
+
<template #icon>
|
|
846
|
+
<el-icon><Document /></el-icon>
|
|
847
|
+
</template>
|
|
848
|
+
{{ filteredLogs.length }}/{{ logs.length }}
|
|
849
|
+
<el-tag v-if="!showAllCommits" type="warning" size="small" effect="plain" style="margin-left: 5px">
|
|
850
|
+
分页加载 (每页100条)
|
|
851
|
+
</el-tag>
|
|
852
|
+
<el-tag v-else type="success" size="small" effect="plain" style="margin-left: 5px">
|
|
853
|
+
全部
|
|
854
|
+
</el-tag>
|
|
855
|
+
</el-tag> -->
|
|
856
|
+
</div>
|
|
857
|
+
|
|
354
858
|
<div class="log-actions">
|
|
859
|
+
<!-- 筛选按钮移到这里 -->
|
|
860
|
+
<el-button
|
|
861
|
+
v-if="!showGraphView"
|
|
862
|
+
:type="filterVisible ? 'primary' : 'default'"
|
|
863
|
+
size="small"
|
|
864
|
+
@click="filterVisible = !filterVisible"
|
|
865
|
+
>
|
|
866
|
+
<template #icon>
|
|
867
|
+
<el-icon><Filter /></el-icon>
|
|
868
|
+
</template>
|
|
869
|
+
筛选
|
|
870
|
+
<el-badge v-if="filteredLogs.length !== logs.length" :value="filteredLogs.length" class="filter-badge" />
|
|
871
|
+
</el-button>
|
|
872
|
+
|
|
873
|
+
<!-- 原有的按钮 -->
|
|
355
874
|
<el-button
|
|
356
875
|
type="primary"
|
|
357
876
|
size="small"
|
|
358
877
|
@click="toggleViewMode"
|
|
359
878
|
>
|
|
879
|
+
<template #icon>
|
|
880
|
+
<el-icon><component :is="showGraphView ? Document : TrendCharts" /></el-icon>
|
|
881
|
+
</template>
|
|
360
882
|
{{ showGraphView ? '表格视图' : '图表视图' }}
|
|
361
883
|
</el-button>
|
|
362
884
|
<el-button
|
|
@@ -365,119 +887,288 @@ function fitGraphToContainer() {
|
|
|
365
887
|
@click="toggleAllCommits"
|
|
366
888
|
:loading="isLoading"
|
|
367
889
|
>
|
|
368
|
-
|
|
890
|
+
<template #icon>
|
|
891
|
+
<el-icon><component :is="showAllCommits ? List : More" /></el-icon>
|
|
892
|
+
</template>
|
|
893
|
+
{{ showAllCommits ? '显示分页加载 (每页100条)' : '显示所有提交' }}
|
|
369
894
|
</el-button>
|
|
370
895
|
<el-button
|
|
371
|
-
:icon="RefreshRight"
|
|
372
896
|
circle
|
|
373
897
|
size="small"
|
|
374
898
|
@click="refreshLog()"
|
|
375
899
|
:loading="isLoading"
|
|
376
900
|
:class="{ 'refresh-button-animated': logRefreshed }"
|
|
377
|
-
|
|
901
|
+
>
|
|
902
|
+
<el-icon><RefreshRight /></el-icon>
|
|
903
|
+
</el-button>
|
|
378
904
|
</div>
|
|
379
905
|
</div>
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
<div
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
906
|
+
|
|
907
|
+
<!-- 筛选面板放在头部下方,但在内容区域之前 -->
|
|
908
|
+
<div v-if="filterVisible && !showGraphView" class="filter-panel-header">
|
|
909
|
+
<div class="filter-form">
|
|
910
|
+
<div class="filter-item">
|
|
911
|
+
<div class="filter-label">作者:</div>
|
|
912
|
+
<el-select
|
|
913
|
+
v-model="authorFilter"
|
|
914
|
+
placeholder="选择作者"
|
|
915
|
+
multiple
|
|
916
|
+
clearable
|
|
917
|
+
filterable
|
|
918
|
+
class="filter-input"
|
|
919
|
+
size="small"
|
|
920
|
+
>
|
|
921
|
+
<el-option
|
|
922
|
+
v-for="author in availableAuthors"
|
|
923
|
+
:key="author"
|
|
924
|
+
:label="author"
|
|
925
|
+
:value="author"
|
|
926
|
+
/>
|
|
927
|
+
</el-select>
|
|
391
928
|
</div>
|
|
392
929
|
|
|
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
|
-
|
|
930
|
+
<div class="filter-item">
|
|
931
|
+
<div class="filter-label">提交信息包含:</div>
|
|
932
|
+
<el-input
|
|
933
|
+
v-model="messageFilter"
|
|
934
|
+
placeholder="关键词"
|
|
935
|
+
clearable
|
|
936
|
+
class="filter-input"
|
|
937
|
+
size="small"
|
|
938
|
+
/>
|
|
939
|
+
</div>
|
|
940
|
+
|
|
941
|
+
<div class="filter-item">
|
|
942
|
+
<div class="filter-label">日期范围:</div>
|
|
943
|
+
<el-date-picker
|
|
944
|
+
v-model="dateRangeFilter"
|
|
945
|
+
type="daterange"
|
|
946
|
+
range-separator="至"
|
|
947
|
+
start-placeholder="开始日期"
|
|
948
|
+
end-placeholder="结束日期"
|
|
949
|
+
format="YYYY-MM-DD"
|
|
950
|
+
value-format="YYYY-MM-DD"
|
|
951
|
+
class="filter-input date-range"
|
|
952
|
+
size="small"
|
|
953
|
+
/>
|
|
954
|
+
</div>
|
|
955
|
+
|
|
956
|
+
<div class="filter-actions">
|
|
957
|
+
<el-button type="primary" size="small" @click="applyFilters">应用筛选</el-button>
|
|
958
|
+
<el-button type="info" size="small" @click="resetFilters">重置</el-button>
|
|
959
|
+
</div>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
|
|
963
|
+
<!-- 内容区域,添加上边距以避免被固定头部遮挡 -->
|
|
964
|
+
<div class="content-area" :class="{'with-filter': filterVisible && !showGraphView}">
|
|
965
|
+
<div v-if="errorMessage">{{ errorMessage }}</div>
|
|
966
|
+
<div v-else>
|
|
967
|
+
<!-- 图表视图 -->
|
|
968
|
+
<div v-if="showGraphView" class="graph-view">
|
|
969
|
+
<div class="commit-count" v-if="logsData.length > 0">
|
|
970
|
+
显示 {{ logsData.length }} 条提交记录 {{ showAllCommits ? '(全部)' : '(分页加载,每页100条)' }}
|
|
430
971
|
</div>
|
|
431
972
|
|
|
432
|
-
|
|
433
|
-
|
|
973
|
+
<!-- 添加缩放控制 -->
|
|
974
|
+
<div class="graph-controls">
|
|
975
|
+
<div class="zoom-controls">
|
|
976
|
+
<el-button
|
|
977
|
+
type="primary"
|
|
978
|
+
circle
|
|
979
|
+
size="small"
|
|
980
|
+
@click="zoomOut"
|
|
981
|
+
:disabled="graphScale <= minScale"
|
|
982
|
+
>
|
|
983
|
+
<el-icon><ZoomOut /></el-icon>
|
|
984
|
+
</el-button>
|
|
985
|
+
|
|
986
|
+
<el-slider
|
|
987
|
+
v-model="graphScale"
|
|
988
|
+
:min="minScale"
|
|
989
|
+
:max="maxScale"
|
|
990
|
+
:step="scaleStep"
|
|
991
|
+
@change="applyScale"
|
|
992
|
+
class="zoom-slider"
|
|
993
|
+
/>
|
|
994
|
+
|
|
995
|
+
<el-button
|
|
996
|
+
type="primary"
|
|
997
|
+
circle
|
|
998
|
+
size="small"
|
|
999
|
+
@click="zoomIn"
|
|
1000
|
+
:disabled="graphScale >= maxScale"
|
|
1001
|
+
>
|
|
1002
|
+
<el-icon><ZoomIn /></el-icon>
|
|
1003
|
+
</el-button>
|
|
1004
|
+
|
|
1005
|
+
<el-button
|
|
1006
|
+
type="primary"
|
|
1007
|
+
size="small"
|
|
1008
|
+
@click="fitGraphToContainer"
|
|
1009
|
+
>
|
|
1010
|
+
自适应大小
|
|
1011
|
+
</el-button>
|
|
1012
|
+
</div>
|
|
1013
|
+
|
|
1014
|
+
<div class="scale-info">
|
|
1015
|
+
缩放: {{ Math.round(graphScale * 100) }}%
|
|
1016
|
+
</div>
|
|
434
1017
|
</div>
|
|
1018
|
+
|
|
1019
|
+
<div ref="graphContainer" class="graph-container"></div>
|
|
435
1020
|
</div>
|
|
436
1021
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
1022
|
+
<!-- 表格视图 -->
|
|
1023
|
+
<div v-else class="table-view-container">
|
|
1024
|
+
<el-table ref="tableRef" :data="filteredLogs" stripe border v-loading="isLoading" class="log-table" :empty-text="isLoading ? '加载中...' : '没有匹配的提交记录'" height="500" @row-contextmenu="handleContextMenu" >
|
|
1025
|
+
<el-table-column label="提交哈希" width="100" resizable>
|
|
1026
|
+
<template #default="scope">
|
|
1027
|
+
<span class="commit-hash" @click="viewCommitDetail(scope.row)">{{ scope.row.hash.substring(0, 7) }}</span>
|
|
1028
|
+
</template>
|
|
1029
|
+
</el-table-column>
|
|
1030
|
+
<el-table-column prop="date" label="日期" width="120" resizable />
|
|
1031
|
+
<el-table-column label="作者" width="120" resizable>
|
|
1032
|
+
<template #default="scope">
|
|
1033
|
+
<el-tooltip :content="scope.row.email" placement="top" :hide-after="1000">
|
|
1034
|
+
<span class="author-name">{{ scope.row.author }}</span>
|
|
1035
|
+
</el-tooltip>
|
|
1036
|
+
</template>
|
|
1037
|
+
</el-table-column>
|
|
1038
|
+
<el-table-column label="分支" width="180" resizable>
|
|
1039
|
+
<template #default="scope">
|
|
1040
|
+
<div v-if="scope.row.branch" class="branch-container">
|
|
1041
|
+
<el-tag
|
|
1042
|
+
v-for="(ref, index) in scope.row.branch.split(',')"
|
|
1043
|
+
:key="index"
|
|
1044
|
+
size="small"
|
|
1045
|
+
:type="getBranchTagType(ref)"
|
|
1046
|
+
class="branch-tag"
|
|
1047
|
+
>
|
|
1048
|
+
{{ formatBranchName(ref) }}
|
|
1049
|
+
</el-tag>
|
|
1050
|
+
</div>
|
|
1051
|
+
</template>
|
|
1052
|
+
</el-table-column>
|
|
1053
|
+
<el-table-column prop="message" label="提交信息" min-width="250" />
|
|
1054
|
+
</el-table>
|
|
1055
|
+
|
|
1056
|
+
<!-- 添加底部加载状态和加载更多按钮 -->
|
|
1057
|
+
<div v-if="!showAllCommits" class="load-more-container">
|
|
1058
|
+
<!-- 显示加载状态和页码信息 -->
|
|
1059
|
+
<div class="pagination-info">
|
|
1060
|
+
<span>第 {{ currentPage }} 页 {{ totalCommits > 0 ? `/ 共 ${Math.ceil(totalCommits / 100) || 1} 页` : '' }} (总计 {{ totalCommits }} 条记录)</span>
|
|
1061
|
+
</div>
|
|
1062
|
+
|
|
1063
|
+
<div v-if="isLoadingMore" class="loading-more">
|
|
1064
|
+
<div class="loading-spinner"></div>
|
|
1065
|
+
<span>加载更多...</span>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div v-else-if="hasMoreData" class="load-more-button" @click="loadMoreLogs">
|
|
1068
|
+
<span>加载更多</span>
|
|
1069
|
+
</div>
|
|
1070
|
+
<div v-else class="no-more-data">
|
|
1071
|
+
<span>没有更多数据了</span>
|
|
1072
|
+
<span v-if="logs.length > 0" class="total-loaded">(已加载 {{ logs.length }} 条记录)</span>
|
|
1073
|
+
</div>
|
|
1074
|
+
</div>
|
|
444
1075
|
</div>
|
|
445
|
-
<el-table :data="logs" style="width: 100%" stripe border v-loading="isLoading">
|
|
446
|
-
<el-table-column prop="hash" label="提交哈希" width="100" resizable />
|
|
447
|
-
<el-table-column prop="date" label="日期" width="180" resizable />
|
|
448
|
-
<el-table-column label="作者" width="200" resizable>
|
|
449
|
-
<template #default="scope">
|
|
450
|
-
{{ scope.row.author }} <{{ scope.row.email }}>
|
|
451
|
-
</template>
|
|
452
|
-
</el-table-column>
|
|
453
|
-
<el-table-column label="分支" width="180" resizable>
|
|
454
|
-
<template #default="scope">
|
|
455
|
-
<div v-if="scope.row.branch" class="branch-container">
|
|
456
|
-
<el-tag
|
|
457
|
-
v-for="(ref, index) in scope.row.branch.split(',')"
|
|
458
|
-
:key="index"
|
|
459
|
-
size="small"
|
|
460
|
-
:type="getBranchTagType(ref)"
|
|
461
|
-
class="branch-tag"
|
|
462
|
-
>
|
|
463
|
-
{{ formatBranchName(ref) }}
|
|
464
|
-
</el-tag>
|
|
465
|
-
</div>
|
|
466
|
-
</template>
|
|
467
|
-
</el-table-column>
|
|
468
|
-
<el-table-column prop="message" label="提交信息" min-width="250" />
|
|
469
|
-
</el-table>
|
|
470
1076
|
</div>
|
|
471
1077
|
</div>
|
|
472
|
-
</div>
|
|
473
|
-
</template>
|
|
474
1078
|
|
|
475
|
-
|
|
1079
|
+
<!-- 提交详情弹窗 -->
|
|
1080
|
+
<el-dialog
|
|
1081
|
+
v-model="commitDetailVisible"
|
|
1082
|
+
:title="`提交详情: ${selectedCommit?.hash ? selectedCommit.hash.substring(0, 7) : '未知'}`"
|
|
1083
|
+
width="80%"
|
|
1084
|
+
destroy-on-close
|
|
1085
|
+
class="commit-detail-dialog"
|
|
1086
|
+
>
|
|
1087
|
+
<div v-loading="isLoadingCommitDetail" class="commit-detail-container">
|
|
1088
|
+
<!-- 提交基本信息 -->
|
|
1089
|
+
<div v-if="selectedCommit" class="commit-info">
|
|
1090
|
+
<div class="commit-info-header">
|
|
1091
|
+
<div class="info-item">
|
|
1092
|
+
<span class="item-label">哈希:</span>
|
|
1093
|
+
<span class="item-value">{{ selectedCommit.hash }}</span>
|
|
1094
|
+
</div>
|
|
1095
|
+
<div class="info-item">
|
|
1096
|
+
<span class="item-label">作者:</span>
|
|
1097
|
+
<span class="item-value">{{ selectedCommit.author }} <{{ selectedCommit.email }}></span>
|
|
1098
|
+
</div>
|
|
1099
|
+
<div class="info-item">
|
|
1100
|
+
<span class="item-label">日期:</span>
|
|
1101
|
+
<span class="item-value">{{ selectedCommit.date }}</span>
|
|
1102
|
+
</div>
|
|
1103
|
+
</div>
|
|
1104
|
+
<div class="commit-message-container">
|
|
1105
|
+
<div class="message-label">提交信息:</div>
|
|
1106
|
+
<div class="message-content" v-html="formatCommitMessage(selectedCommit.message).replace(/\n/g, '<br>')"></div>
|
|
1107
|
+
</div>
|
|
1108
|
+
</div>
|
|
1109
|
+
|
|
1110
|
+
<!-- 变更文件列表和差异 -->
|
|
1111
|
+
<div class="commit-files-diff">
|
|
1112
|
+
<div class="files-list">
|
|
1113
|
+
<h3>变更文件</h3>
|
|
1114
|
+
<el-empty v-if="commitFiles.length === 0" description="没有找到变更文件"></el-empty>
|
|
1115
|
+
<ul v-else>
|
|
1116
|
+
<li
|
|
1117
|
+
v-for="file in commitFiles"
|
|
1118
|
+
:key="file"
|
|
1119
|
+
:class="{ 'active-file': file === selectedCommitFile }"
|
|
1120
|
+
@click="getCommitFileDiff(selectedCommit!.hash, file)"
|
|
1121
|
+
>
|
|
1122
|
+
{{ file }}
|
|
1123
|
+
</li>
|
|
1124
|
+
</ul>
|
|
1125
|
+
</div>
|
|
1126
|
+
<div class="file-diff">
|
|
1127
|
+
<h3 v-if="selectedCommitFile">文件差异: {{ selectedCommitFile }}</h3>
|
|
1128
|
+
<h3 v-else>文件差异</h3>
|
|
1129
|
+
<el-empty v-if="!commitDiff && !isLoadingCommitDetail" description="选择文件查看差异"></el-empty>
|
|
1130
|
+
<div v-else-if="commitDiff" v-html="formatDiff(commitDiff)" class="diff-content"></div>
|
|
1131
|
+
</div>
|
|
1132
|
+
</div>
|
|
1133
|
+
</div>
|
|
1134
|
+
</el-dialog> </div> <!-- 添加右键菜单 --> <div v-show="contextMenuVisible" class="context-menu" :style="{ top: contextMenuTop + 'px', left: contextMenuLeft + 'px' }" > <div class="context-menu-item" @click="viewCommitDetail(selectedContextCommit)"> <i class="el-icon-view"></i> 查看详情 </div> <div class="context-menu-item" @click="revertCommit(selectedContextCommit)"> <i class="el-icon-delete"></i> 撤销提交 (Revert) </div> <div class="context-menu-item" @click="cherryPickCommit(selectedContextCommit)"> <i class="el-icon-edit"></i> Cherry-Pick 到当前分支 </div> </div></template><style scoped>
|
|
1135
|
+
.card {
|
|
1136
|
+
background-color: white;
|
|
1137
|
+
border-radius: 8px;
|
|
1138
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03);
|
|
1139
|
+
height: 100%;
|
|
1140
|
+
width: 100%;
|
|
1141
|
+
display: flex;
|
|
1142
|
+
flex-direction: column;
|
|
1143
|
+
border: 1px solid rgba(0, 0, 0, 0.03);
|
|
1144
|
+
overflow: hidden;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
476
1147
|
.log-header {
|
|
477
1148
|
display: flex;
|
|
478
1149
|
justify-content: space-between;
|
|
479
1150
|
align-items: center;
|
|
480
|
-
margin-bottom:
|
|
1151
|
+
margin-bottom: 0;
|
|
1152
|
+
padding: 8px 16px;
|
|
1153
|
+
background-color: white;
|
|
1154
|
+
border-bottom: 1px solid #f0f0f0;
|
|
1155
|
+
position: sticky;
|
|
1156
|
+
top: 0;
|
|
1157
|
+
z-index: 100;
|
|
1158
|
+
height: 36px;
|
|
1159
|
+
flex-shrink: 0; /* 防止头部被压缩 */
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.header-left {
|
|
1163
|
+
display: flex;
|
|
1164
|
+
align-items: center;
|
|
1165
|
+
gap: 8px;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
.log-header h2 {
|
|
1169
|
+
margin: 0;
|
|
1170
|
+
font-size: 16px;
|
|
1171
|
+
font-weight: 500;
|
|
481
1172
|
}
|
|
482
1173
|
|
|
483
1174
|
.log-actions {
|
|
@@ -485,6 +1176,40 @@ function fitGraphToContainer() {
|
|
|
485
1176
|
gap: 8px;
|
|
486
1177
|
}
|
|
487
1178
|
|
|
1179
|
+
.content-area {
|
|
1180
|
+
padding: 10px 0;
|
|
1181
|
+
flex: 1;
|
|
1182
|
+
min-height: 100px;
|
|
1183
|
+
height: calc(100% - 52px);
|
|
1184
|
+
display: flex;
|
|
1185
|
+
flex-direction: column;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
.content-area.with-filter {
|
|
1189
|
+
height: calc(100% - 52px - 60px); /* 减去header高度和filter高度 */
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/* 确保内容区域内的直接子元素占满高度 */
|
|
1193
|
+
.content-area > div {
|
|
1194
|
+
flex: 1;
|
|
1195
|
+
display: flex;
|
|
1196
|
+
flex-direction: column;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/* 优化表格区域 */
|
|
1200
|
+
.el-table {
|
|
1201
|
+
--el-table-border-color: #f0f0f0;
|
|
1202
|
+
--el-table-header-bg-color: #f8f9fa;
|
|
1203
|
+
border-radius: 4px;
|
|
1204
|
+
overflow: hidden;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/* 统一按钮间距 */
|
|
1208
|
+
.log-actions {
|
|
1209
|
+
display: flex;
|
|
1210
|
+
gap: 12px;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
488
1213
|
.branch-container {
|
|
489
1214
|
display: flex;
|
|
490
1215
|
flex-wrap: wrap;
|
|
@@ -502,10 +1227,18 @@ function fitGraphToContainer() {
|
|
|
502
1227
|
text-align: right;
|
|
503
1228
|
}
|
|
504
1229
|
|
|
1230
|
+
.graph-view {
|
|
1231
|
+
width: 100%;
|
|
1232
|
+
flex: 1;
|
|
1233
|
+
display: flex;
|
|
1234
|
+
flex-direction: column;
|
|
1235
|
+
overflow-y: auto;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
505
1238
|
.graph-container {
|
|
506
1239
|
width: 100%;
|
|
507
|
-
|
|
508
|
-
|
|
1240
|
+
flex: 1;
|
|
1241
|
+
min-height: 500px;
|
|
509
1242
|
border: 1px solid #ebeef5;
|
|
510
1243
|
border-radius: 4px;
|
|
511
1244
|
padding: 10px;
|
|
@@ -518,10 +1251,6 @@ function fitGraphToContainer() {
|
|
|
518
1251
|
transition: transform 0.2s ease;
|
|
519
1252
|
}
|
|
520
1253
|
|
|
521
|
-
.graph-view {
|
|
522
|
-
width: 100%;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
1254
|
.graph-controls {
|
|
526
1255
|
display: flex;
|
|
527
1256
|
justify-content: space-between;
|
|
@@ -550,13 +1279,18 @@ function fitGraphToContainer() {
|
|
|
550
1279
|
.refresh-notification {
|
|
551
1280
|
background-color: #f0f9eb;
|
|
552
1281
|
color: #67c23a;
|
|
553
|
-
padding:
|
|
554
|
-
border-radius:
|
|
555
|
-
margin-bottom: 10px;
|
|
556
|
-
text-align: center;
|
|
1282
|
+
padding: 10px 15px;
|
|
1283
|
+
border-radius: 8px;
|
|
557
1284
|
font-size: 14px;
|
|
558
1285
|
border-left: 4px solid #67c23a;
|
|
559
1286
|
animation: fadeOut 2s forwards;
|
|
1287
|
+
position: fixed;
|
|
1288
|
+
top: 20px;
|
|
1289
|
+
right: 20px;
|
|
1290
|
+
z-index: 9999;
|
|
1291
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
1292
|
+
max-width: 300px;
|
|
1293
|
+
text-align: center;
|
|
560
1294
|
}
|
|
561
1295
|
|
|
562
1296
|
@keyframes pulse {
|
|
@@ -566,13 +1300,572 @@ function fitGraphToContainer() {
|
|
|
566
1300
|
}
|
|
567
1301
|
|
|
568
1302
|
@keyframes fadeOut {
|
|
569
|
-
0% { opacity:
|
|
570
|
-
|
|
1303
|
+
0% { opacity: 0; transform: translateY(-20px); }
|
|
1304
|
+
20% { opacity: 1; transform: translateY(0); }
|
|
1305
|
+
80% { opacity: 1; }
|
|
571
1306
|
100% { opacity: 0; }
|
|
572
1307
|
}
|
|
1308
|
+
|
|
1309
|
+
.author-name {
|
|
1310
|
+
cursor: pointer;
|
|
1311
|
+
white-space: nowrap;
|
|
1312
|
+
overflow: hidden;
|
|
1313
|
+
text-overflow: ellipsis;
|
|
1314
|
+
display: inline-block;
|
|
1315
|
+
max-width: 100%;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
.commit-hash {
|
|
1319
|
+
cursor: pointer;
|
|
1320
|
+
color: #409EFF;
|
|
1321
|
+
font-family: monospace;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
.commit-hash:hover {
|
|
1325
|
+
text-decoration: underline;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
.commit-detail-container {
|
|
1329
|
+
display: flex;
|
|
1330
|
+
flex-direction: column;
|
|
1331
|
+
gap: 20px;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
.commit-info {
|
|
1335
|
+
padding: 12px;
|
|
1336
|
+
background-color: #f5f7fa;
|
|
1337
|
+
border-radius: 8px;
|
|
1338
|
+
font-size: 14px;
|
|
1339
|
+
display: flex;
|
|
1340
|
+
flex-direction: column;
|
|
1341
|
+
gap: 12px;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
.commit-info-header {
|
|
1345
|
+
display: flex;
|
|
1346
|
+
flex-wrap: wrap;
|
|
1347
|
+
gap: 15px;
|
|
1348
|
+
align-items: center;
|
|
1349
|
+
background-color: #fff;
|
|
1350
|
+
padding: 10px;
|
|
1351
|
+
border-radius: 4px;
|
|
1352
|
+
border: 1px solid #e4e7ed;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
.info-item {
|
|
1356
|
+
display: flex;
|
|
1357
|
+
align-items: center;
|
|
1358
|
+
gap: 5px;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.item-label {
|
|
1362
|
+
font-weight: bold;
|
|
1363
|
+
color: #606266;
|
|
1364
|
+
white-space: nowrap;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
.item-value {
|
|
1368
|
+
color: #333;
|
|
1369
|
+
word-break: break-all;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
.commit-message-container {
|
|
1373
|
+
display: flex;
|
|
1374
|
+
flex-direction: column;
|
|
1375
|
+
gap: 5px;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
.message-label {
|
|
1379
|
+
font-weight: bold;
|
|
1380
|
+
color: #606266;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.message-content {
|
|
1384
|
+
background-color: #fff;
|
|
1385
|
+
padding: 12px;
|
|
1386
|
+
border-radius: 4px;
|
|
1387
|
+
font-family: monospace;
|
|
1388
|
+
white-space: pre-wrap;
|
|
1389
|
+
border-left: 4px solid #409EFF;
|
|
1390
|
+
line-height: 1.5;
|
|
1391
|
+
border: 1px solid #e4e7ed;
|
|
1392
|
+
border-left: 4px solid #409EFF;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
.commit-files-diff {
|
|
1396
|
+
margin-top: 5px;
|
|
1397
|
+
display: flex;
|
|
1398
|
+
gap: 20px;
|
|
1399
|
+
height: 60vh;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
.files-list {
|
|
1403
|
+
width: 25%;
|
|
1404
|
+
overflow-y: auto;
|
|
1405
|
+
background-color: #f5f7fa;
|
|
1406
|
+
border-radius: 8px;
|
|
1407
|
+
padding: 10px;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
.files-list h3 {
|
|
1411
|
+
margin-top: 0;
|
|
1412
|
+
padding-bottom: 10px;
|
|
1413
|
+
border-bottom: 1px solid #dcdfe6;
|
|
1414
|
+
font-size: 16px;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
.files-list ul {
|
|
1418
|
+
list-style: none;
|
|
1419
|
+
padding: 0;
|
|
1420
|
+
margin: 0;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
.files-list li {
|
|
1424
|
+
padding: 8px 10px;
|
|
1425
|
+
cursor: pointer;
|
|
1426
|
+
border-radius: 4px;
|
|
1427
|
+
margin-bottom: 5px;
|
|
1428
|
+
font-family: monospace;
|
|
1429
|
+
white-space: nowrap;
|
|
1430
|
+
overflow: hidden;
|
|
1431
|
+
text-overflow: ellipsis;
|
|
1432
|
+
font-size: 13px;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.files-list li:hover {
|
|
1436
|
+
background-color: #ecf5ff;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.files-list li.active-file {
|
|
1440
|
+
background-color: #409eff;
|
|
1441
|
+
color: white;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
.file-diff {
|
|
1445
|
+
flex: 1;
|
|
1446
|
+
display: flex;
|
|
1447
|
+
flex-direction: column;
|
|
1448
|
+
background-color: #f5f7fa;
|
|
1449
|
+
border-radius: 8px;
|
|
1450
|
+
padding: 10px;
|
|
1451
|
+
overflow: hidden;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
.file-diff h3 {
|
|
1455
|
+
margin-top: 0;
|
|
1456
|
+
padding-bottom: 10px;
|
|
1457
|
+
border-bottom: 1px solid #dcdfe6;
|
|
1458
|
+
font-size: 16px;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
.diff-content {
|
|
1462
|
+
flex: 1;
|
|
1463
|
+
overflow-y: auto;
|
|
1464
|
+
background-color: #fff;
|
|
1465
|
+
padding: 10px;
|
|
1466
|
+
border-radius: 4px;
|
|
1467
|
+
font-family: monospace;
|
|
1468
|
+
font-size: 13px;
|
|
1469
|
+
line-height: 1.5;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
.diff-header {
|
|
1473
|
+
font-weight: bold;
|
|
1474
|
+
color: #409EFF;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
.diff-old-file {
|
|
1478
|
+
color: #E6A23C;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.diff-new-file {
|
|
1482
|
+
color: #67C23A;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
.diff-hunk-header {
|
|
1486
|
+
font-weight: bold;
|
|
1487
|
+
color: #409EFF;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
.diff-added {
|
|
1491
|
+
background-color: #f0f9eb;
|
|
1492
|
+
color: #67C23A;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
.diff-removed {
|
|
1496
|
+
background-color: #fef2f2;
|
|
1497
|
+
color: #F56C6C;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
.diff-context {
|
|
1501
|
+
background-color: #f5f7fa;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/* 减小对话框的顶部边距 */
|
|
1505
|
+
:deep(.commit-detail-dialog) {
|
|
1506
|
+
--el-dialog-margin-top: 5vh;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.history-controls {
|
|
1510
|
+
display: flex;
|
|
1511
|
+
justify-content: space-between;
|
|
1512
|
+
align-items: center;
|
|
1513
|
+
margin-bottom: 15px;
|
|
1514
|
+
padding: 0;
|
|
1515
|
+
position: sticky;
|
|
1516
|
+
top: 52px; /* 与 log-header 的高度匹配 */
|
|
1517
|
+
z-index: 90;
|
|
1518
|
+
background-color: white;
|
|
1519
|
+
padding: 5px 0;
|
|
1520
|
+
transition: box-shadow 0.3s ease, background-color 0.3s ease, padding 0.2s ease;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/* 当滚动时添加微妙的阴影效果 */
|
|
1524
|
+
.content-area:not(:hover) .history-controls:not(:hover) {
|
|
1525
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
.history-stats {
|
|
1529
|
+
display: flex;
|
|
1530
|
+
align-items: center;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
.record-count {
|
|
1534
|
+
display: flex;
|
|
1535
|
+
align-items: center;
|
|
1536
|
+
height: 36px;
|
|
1537
|
+
padding-left: 15px;
|
|
1538
|
+
padding-right: 15px;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
.record-count :deep(.el-icon) {
|
|
1542
|
+
margin-right: 6px;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
.filter-badge :deep(.el-badge__content) {
|
|
1546
|
+
background-color: #409EFF;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
.filter-panel {
|
|
1550
|
+
background-color: #f5f7fa;
|
|
1551
|
+
border-radius: 8px;
|
|
1552
|
+
padding: 15px;
|
|
1553
|
+
margin-bottom: 15px;
|
|
1554
|
+
border: 1px solid #e4e7ed;
|
|
1555
|
+
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
1556
|
+
position: sticky;
|
|
1557
|
+
top: 98px; /* history-controls的高度(36px) + padding(10px) + log-header的高度(52px) */
|
|
1558
|
+
z-index: 89; /* 比history-controls低一点 */
|
|
1559
|
+
transition: box-shadow 0.3s ease, background-color 0.3s ease, transform 0.2s ease;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/* 当滚动时增强视觉效果 */
|
|
1563
|
+
.content-area:not(:hover) .filter-panel:not(:hover) {
|
|
1564
|
+
box-shadow: 0 3px 16px rgba(0, 0, 0, 0.1);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
.filter-panel:hover {
|
|
1568
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
.filter-form {
|
|
1572
|
+
display: flex;
|
|
1573
|
+
flex-wrap: wrap;
|
|
1574
|
+
gap: 15px;
|
|
1575
|
+
align-items: flex-end;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
.filter-item {
|
|
1579
|
+
display: flex;
|
|
1580
|
+
flex-direction: column;
|
|
1581
|
+
gap: 5px;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
.filter-label {
|
|
1585
|
+
font-size: 13px;
|
|
1586
|
+
color: #606266;
|
|
1587
|
+
font-weight: bold;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
.filter-input {
|
|
1591
|
+
width: 200px;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
.filter-input.date-range {
|
|
1595
|
+
width: 350px;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
.filter-actions {
|
|
1599
|
+
display: flex;
|
|
1600
|
+
align-items: center;
|
|
1601
|
+
gap: 10px;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/* 表格样式 */
|
|
1605
|
+
.log-table {
|
|
1606
|
+
transition: margin-top 0.3s ease;
|
|
1607
|
+
min-height: 200px; /* 确保表格有最小高度 */
|
|
1608
|
+
height: 100%; /* 填充可用空间 */
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/* 为带筛选面板的表格添加顶部边距 */
|
|
1612
|
+
.log-table.has-filter {
|
|
1613
|
+
margin-top: 10px;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/* 当控件固定时增加表格上边距 */
|
|
1617
|
+
.log-table.has-sticky-controls {
|
|
1618
|
+
margin-top: 52px !important; /* controls 高度 + 一些额外空间 */
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/* 当筛选面板固定时再增加表格上边距 */
|
|
1622
|
+
.log-table.has-sticky-filter {
|
|
1623
|
+
margin-top: 140px !important; /* 筛选面板高度 + controls 高度 + 一些额外空间 */
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/* 表格为空时的样式 */
|
|
1627
|
+
.el-table__empty-block {
|
|
1628
|
+
min-height: 200px;
|
|
1629
|
+
justify-content: center;
|
|
1630
|
+
align-items: center;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/* 确保表格容器占满可用空间 */
|
|
1634
|
+
.content-area > div:not(.graph-view) {
|
|
1635
|
+
display: flex;
|
|
1636
|
+
flex-direction: column;
|
|
1637
|
+
height: 100%;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/* 表格视图容器 */
|
|
1641
|
+
.table-view-container {
|
|
1642
|
+
display: flex;
|
|
1643
|
+
flex-direction: column;
|
|
1644
|
+
flex: 1;
|
|
1645
|
+
height: 100%;
|
|
1646
|
+
overflow-y: auto;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
.log-table {
|
|
1650
|
+
width: 100%;
|
|
1651
|
+
flex: 1;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
/* 表格容器样式 */
|
|
1655
|
+
.table-view-container .el-table {
|
|
1656
|
+
flex: 1;
|
|
1657
|
+
width: 100%;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
.table-view-container .el-table__inner-wrapper {
|
|
1661
|
+
width: 100%;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
.table-view-container .el-table__body-wrapper {
|
|
1665
|
+
width: 100%;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
.filter-panel.filter-sticky {
|
|
1669
|
+
background-color: rgba(245, 247, 250, 0.97);
|
|
1670
|
+
backdrop-filter: blur(4px);
|
|
1671
|
+
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);
|
|
1672
|
+
border-top: none;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
.history-controls.controls-sticky {
|
|
1676
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
1677
|
+
background-color: rgba(255, 255, 255, 0.98);
|
|
1678
|
+
backdrop-filter: blur(4px);
|
|
1679
|
+
padding: 8px 0;
|
|
1680
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/* 筛选面板头部样式 */
|
|
1684
|
+
.filter-panel-header {
|
|
1685
|
+
background-color: #f5f7fa;
|
|
1686
|
+
padding: 10px 16px;
|
|
1687
|
+
border-bottom: 1px solid #e4e7ed;
|
|
1688
|
+
position: sticky;
|
|
1689
|
+
top: 36px; /* 紧贴log-header下方 */
|
|
1690
|
+
z-index: 99;
|
|
1691
|
+
transition: all 0.3s ease;
|
|
1692
|
+
flex-shrink: 0;
|
|
1693
|
+
margin-bottom: 0; /* 移除底部边距 */
|
|
1694
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
.filter-panel-header .filter-form {
|
|
1698
|
+
display: flex;
|
|
1699
|
+
flex-wrap: wrap;
|
|
1700
|
+
gap: 10px;
|
|
1701
|
+
align-items: flex-end;
|
|
1702
|
+
justify-content: flex-start;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
.filter-panel-header .filter-item {
|
|
1706
|
+
margin-right: 12px;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
.filter-panel-header .filter-label {
|
|
1710
|
+
font-size: 12px;
|
|
1711
|
+
color: #606266;
|
|
1712
|
+
margin-bottom: 4px;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
.filter-panel-header .filter-input {
|
|
1716
|
+
width: 180px;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
.filter-panel-header .filter-input.date-range {
|
|
1720
|
+
width: 320px;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
.content-area.with-filter {
|
|
1724
|
+
height: calc(100% - 52px - 60px); /* 减去header高度和filter高度 */
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/* 记录计数标签 */
|
|
1728
|
+
.record-count {
|
|
1729
|
+
display: flex;
|
|
1730
|
+
align-items: center;
|
|
1731
|
+
height: 24px;
|
|
1732
|
+
padding-left: 8px;
|
|
1733
|
+
padding-right: 8px;
|
|
1734
|
+
margin-left: 8px;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
.record-count :deep(.el-icon) {
|
|
1738
|
+
margin-right: 4px;
|
|
1739
|
+
font-size: 12px;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/* 表格视图容器简化 */
|
|
1743
|
+
.table-view-container {
|
|
1744
|
+
display: flex;
|
|
1745
|
+
flex-direction: column;
|
|
1746
|
+
flex: 1;
|
|
1747
|
+
min-height: 400px; /* 确保至少有一定高度 */
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
.log-table {
|
|
1751
|
+
width: 100%;
|
|
1752
|
+
height: 100%;
|
|
1753
|
+
min-height: 300px;
|
|
1754
|
+
flex: 1;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/* 重置或移除不再需要的样式 */
|
|
1758
|
+
.history-controls,
|
|
1759
|
+
.filter-panel {
|
|
1760
|
+
display: none; /* 隐藏原来的筛选组件 */
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
.log-table.has-filter,
|
|
1764
|
+
.log-table.has-sticky-controls,
|
|
1765
|
+
.log-table.has-sticky-filter {
|
|
1766
|
+
margin-top: 0 !important; /* 重置原来的边距 */
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/* 添加底部加载更多相关样式 */
|
|
1770
|
+
.load-more-container {
|
|
1771
|
+
display: flex;
|
|
1772
|
+
flex-direction: column;
|
|
1773
|
+
align-items: center;
|
|
1774
|
+
padding: 15px 0;
|
|
1775
|
+
border-top: 1px dashed #ebeef5;
|
|
1776
|
+
gap: 10px;
|
|
1777
|
+
display: none;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
.pagination-info {
|
|
1781
|
+
font-size: 13px;
|
|
1782
|
+
color: #909399;
|
|
1783
|
+
margin-bottom: 5px;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
.loading-more {
|
|
1787
|
+
display: flex;
|
|
1788
|
+
align-items: center;
|
|
1789
|
+
color: #909399;
|
|
1790
|
+
font-size: 14px;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
.loading-spinner {
|
|
1794
|
+
width: 20px;
|
|
1795
|
+
height: 20px;
|
|
1796
|
+
margin-right: 10px;
|
|
1797
|
+
border: 2px solid #dcdfe6;
|
|
1798
|
+
border-top-color: #409eff;
|
|
1799
|
+
border-radius: 50%;
|
|
1800
|
+
animation: spinner 1s linear infinite;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
@keyframes spinner {
|
|
1804
|
+
to {transform: rotate(360deg);}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
.load-more-button {
|
|
1808
|
+
cursor: pointer;
|
|
1809
|
+
color: #409eff;
|
|
1810
|
+
font-size: 14px;
|
|
1811
|
+
padding: 6px 16px;
|
|
1812
|
+
border: 1px solid #d9ecff;
|
|
1813
|
+
background-color: #ecf5ff;
|
|
1814
|
+
border-radius: 4px;
|
|
1815
|
+
transition: all 0.3s;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
.load-more-button:hover {
|
|
1819
|
+
background-color: #d9ecff;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
.no-more-data {
|
|
1823
|
+
color: #909399;
|
|
1824
|
+
font-size: 14px;
|
|
1825
|
+
font-style: italic;
|
|
1826
|
+
display: flex;
|
|
1827
|
+
flex-direction: column;
|
|
1828
|
+
align-items: center;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
.total-loaded {
|
|
1832
|
+
font-size: 12px;
|
|
1833
|
+
margin-top: 5px;
|
|
1834
|
+
color: #c0c4cc;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/* 右键菜单样式 */
|
|
1838
|
+
.context-menu {
|
|
1839
|
+
position: fixed;
|
|
1840
|
+
background: white;
|
|
1841
|
+
border: 1px solid #dcdfe6;
|
|
1842
|
+
border-radius: 4px;
|
|
1843
|
+
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
1844
|
+
padding: 8px 0;
|
|
1845
|
+
z-index: 3000;
|
|
1846
|
+
min-width: 180px;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
.context-menu-item {
|
|
1850
|
+
padding: 8px 16px;
|
|
1851
|
+
cursor: pointer;
|
|
1852
|
+
font-size: 14px;
|
|
1853
|
+
display: flex;
|
|
1854
|
+
align-items: center;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
.context-menu-item:hover {
|
|
1858
|
+
background-color: #f5f7fa;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
.context-menu-item i {
|
|
1862
|
+
margin-right: 8px;
|
|
1863
|
+
}
|
|
573
1864
|
</style>
|
|
574
1865
|
|
|
575
1866
|
/* 添加表格列调整样式 */
|
|
1867
|
+
<style>
|
|
576
1868
|
.el-table .el-table__cell .cell {
|
|
577
1869
|
word-break: break-all;
|
|
578
|
-
}
|
|
1870
|
+
}
|
|
1871
|
+
</style>
|