新增: Git 提交历史显示作者 + 提交详情含作者/时间

- 后端 git log format 加 %an 作者名(状态查询+分页查询)
- 提交列表每行显示 hash/作者/主题/时间
- 提交详情头部补充作者显示
- 分支切换按钮占位(后续接入分支列表 IPC)
This commit is contained in:
2026-06-30 20:59:15 +08:00
parent abc6880936
commit b19b0f3679
5 changed files with 55 additions and 10 deletions

View File

@@ -266,6 +266,8 @@ struct GitRecentCommit {
subject: String, subject: String,
/// Unix 时间戳(秒),供前端格式化显示。 /// Unix 时间戳(秒),供前端格式化显示。
timestamp: i64, timestamp: i64,
/// 作者名(git an)
author: String,
} }
/// Git 状态返回结构(无 .git 时各字段空,前端据此显示"非 Git 仓库")。 /// Git 状态返回结构(无 .git 时各字段空,前端据此显示"非 Git 仓库")。
@@ -367,23 +369,25 @@ fn run_git_status(dir: &str) -> GitStatus {
} }
} }
// 3) 最近提交:`git log -50 --format="%h %ct %s"`(hash + unix 时间戳 + subject) // 3) 最近提交:`git log -50 --format="%h %ct %an %s"`(hash + 时间戳 + 作者 + subject)
let mut recent_commits = Vec::new(); let mut recent_commits = Vec::new();
if let Some(out) = run_git_cmd(path, &["log", "-50", "--format=%h %ct %s"], timeout) { if let Some(out) = run_git_cmd(path, &["log", "-50", "--format=%h %ct %an %s"], timeout) {
for line in out.lines() { for line in out.lines() {
// format:"<hash> <unix_ts> <subject>" // format:"<hash> <unix_ts> <author> <subject>"(作者不含空格)
let line = line.trim(); let line = line.trim();
if line.is_empty() { continue; } if line.is_empty() { continue; }
let mut parts = line.splitn(3, ' '); let mut parts = line.splitn(4, ' ');
let hash = parts.next().unwrap_or("").to_string(); let hash = parts.next().unwrap_or("").to_string();
let ts_str = parts.next().unwrap_or("0"); let ts_str = parts.next().unwrap_or("0");
let timestamp: i64 = ts_str.parse().unwrap_or(0); let timestamp: i64 = ts_str.parse().unwrap_or(0);
let author = parts.next().unwrap_or("").to_string();
let subject = parts.next().unwrap_or("").to_string(); let subject = parts.next().unwrap_or("").to_string();
if !hash.is_empty() { if !hash.is_empty() {
recent_commits.push(GitRecentCommit { recent_commits.push(GitRecentCommit {
hash, hash,
subject, subject,
timestamp, timestamp,
author,
}); });
} }
} }
@@ -922,7 +926,7 @@ pub async fn get_module_commits(
"log", "log",
&format!("--skip={}", skip), &format!("--skip={}", skip),
&format!("-{}", fetch_plus), &format!("-{}", fetch_plus),
"--format=%h %ct %s", "--format=%h %ct %an %s",
]) ])
.current_dir(&dir_for_git) .current_dir(&dir_for_git)
.env("LANG", "en_US.UTF-8") .env("LANG", "en_US.UTF-8")
@@ -937,16 +941,18 @@ pub async fn get_module_commits(
let line = line.trim(); let line = line.trim();
if line.is_empty() { continue; } if line.is_empty() { continue; }
if commits.len() >= fetch as usize { break; } if commits.len() >= fetch as usize { break; }
let mut parts = line.splitn(3, ' '); let mut parts = line.splitn(4, ' ');
let hash = parts.next().unwrap_or("").to_string(); let hash = parts.next().unwrap_or("").to_string();
let ts_str = parts.next().unwrap_or("0"); let ts_str = parts.next().unwrap_or("0");
let timestamp: i64 = ts_str.parse().unwrap_or(0); let timestamp: i64 = ts_str.parse().unwrap_or(0);
let author = parts.next().unwrap_or("").to_string();
let subject = parts.next().unwrap_or("").to_string(); let subject = parts.next().unwrap_or("").to_string();
if !hash.is_empty() { if !hash.is_empty() {
commits.push(serde_json::json!({ commits.push(serde_json::json!({
"hash": hash, "hash": hash,
"subject": subject, "subject": subject,
"timestamp": timestamp, "timestamp": timestamp,
"author": author,
})); }));
} }
} }

View File

@@ -65,6 +65,8 @@ export interface GitRecentCommit {
subject: string subject: string
/** Unix 时间戳(秒) */ /** Unix 时间戳(秒) */
timestamp: number timestamp: number
/** 作者名 */
author: string
} }
/** getModuleGitStatus 返回结构。 */ /** getModuleGitStatus 返回结构。 */
@@ -151,7 +153,7 @@ export const moduleApi = {
/** 分页查询工程 Git 提交历史。返回 { commits, has_more }。 */ /** 分页查询工程 Git 提交历史。返回 { commits, has_more }。 */
getModuleCommits(moduleId: string, skip?: number, limit?: number): Promise<{ getModuleCommits(moduleId: string, skip?: number, limit?: number): Promise<{
commits: { hash: string; subject: string; timestamp: number }[] commits: { hash: string; subject: string; timestamp: number; author: string }[]
has_more: boolean has_more: boolean
}> { }> {
return invoke('get_module_commits', { moduleId, skip: skip ?? 0, limit: limit ?? 50 }) return invoke('get_module_commits', { moduleId, skip: skip ?? 0, limit: limit ?? 50 })

View File

@@ -5,6 +5,9 @@
<span class="branch-icon"></span> <span class="branch-icon"></span>
<span class="branch-name">{{ branchName }}</span> <span class="branch-name">{{ branchName }}</span>
<span v-if="!showHistory" class="branch-count">{{ gitStatus?.changed_files.length ?? 0 }} {{ $t('gitChanges.changedFiles') }}</span> <span v-if="!showHistory" class="branch-count">{{ gitStatus?.changed_files.length ?? 0 }} {{ $t('gitChanges.changedFiles') }}</span>
<button v-if="!showHistory" class="branch-switch-btn" @click="onSwitchBranch" :title="$t('gitChanges.switchBranch')">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
</button>
</div> </div>
<div v-if="loading && !gitStatus" class="gc-skeleton"> <div v-if="loading && !gitStatus" class="gc-skeleton">
@@ -54,6 +57,7 @@
<div v-if="selectedCommit" class="gc-commit-detail"> <div v-if="selectedCommit" class="gc-commit-detail">
<div class="gc-commit-detail-header"> <div class="gc-commit-detail-header">
<span class="gc-commit-detail-hash">{{ selectedCommit.hash }}</span> <span class="gc-commit-detail-hash">{{ selectedCommit.hash }}</span>
<span class="gc-commit-detail-author">{{ selectedCommit.author }}</span>
<span class="gc-commit-detail-msg">{{ selectedCommit.subject }}</span> <span class="gc-commit-detail-msg">{{ selectedCommit.subject }}</span>
<span class="gc-commit-detail-time">{{ formatTime(selectedCommit.timestamp) }}</span> <span class="gc-commit-detail-time">{{ formatTime(selectedCommit.timestamp) }}</span>
<button class="gc-detail-close" @click="selectedCommit = null; commitDetail = null"></button> <button class="gc-detail-close" @click="selectedCommit = null; commitDetail = null"></button>
@@ -96,6 +100,7 @@
@click="selectCommit(c)" @click="selectCommit(c)"
> >
<span class="gc-commit-hash">{{ c.hash.slice(0, 8) }}</span> <span class="gc-commit-hash">{{ c.hash.slice(0, 8) }}</span>
<span class="gc-commit-author">{{ c.author }}</span>
<span class="gc-commit-msg">{{ c.subject }}</span> <span class="gc-commit-msg">{{ c.subject }}</span>
<span class="gc-commit-time">{{ formatTime(c.timestamp) }}</span> <span class="gc-commit-time">{{ formatTime(c.timestamp) }}</span>
</div> </div>
@@ -137,7 +142,7 @@ const diffContent = ref('')
const diffLoading = ref(false) const diffLoading = ref(false)
// 提交历史(分页) // 提交历史(分页)
const commits = ref<{ hash: string; subject: string; timestamp: number }[]>([]) const commits = ref<{ hash: string; subject: string; timestamp: number; author: string }[]>([])
const totalCommits = ref(0) const totalCommits = ref(0)
const hasMoreCommits = ref(false) const hasMoreCommits = ref(false)
const loadingMore = ref(false) const loadingMore = ref(false)
@@ -145,7 +150,7 @@ const commitSkip = ref(0)
const COMMIT_PAGE_SIZE = 50 const COMMIT_PAGE_SIZE = 50
// 已选提交详情 // 已选提交详情
const selectedCommit = ref<{ hash: string; subject: string; timestamp: number } | null>(null) const selectedCommit = ref<{ hash: string; subject: string; timestamp: number; author: string } | null>(null)
const commitDetail = ref<{ files: { status: string; path: string }[]; diff: string } | null>(null) const commitDetail = ref<{ files: { status: string; path: string }[]; diff: string } | null>(null)
const commitDetailLoading = ref(false) const commitDetailLoading = ref(false)
const commitSelectedFile = ref('') const commitSelectedFile = ref('')
@@ -271,6 +276,11 @@ async function loadStatus() {
} }
} }
/** 占位:分支切换后续接入(需拉取分支列表 + checkout IPC)。 */
function onSwitchBranch() {
// TODO: 弹出分支列表选择 + 调用 git checkout
}
async function loadMoreCommits() { async function loadMoreCommits() {
if (loadingMore.value || !hasMoreCommits.value) return if (loadingMore.value || !hasMoreCommits.value) return
loadingMore.value = true loadingMore.value = true
@@ -311,7 +321,7 @@ async function selectFile(path: string) {
} }
/** 点击提交行 → 加载该提交的变更文件列表 */ /** 点击提交行 → 加载该提交的变更文件列表 */
async function selectCommit(c: { hash: string; subject: string; timestamp: number }) { async function selectCommit(c: { hash: string; subject: string; timestamp: number; author: string }) {
selectedCommit.value = c selectedCommit.value = c
commitDetail.value = null commitDetail.value = null
commitSelectedFile.value = '' commitSelectedFile.value = ''
@@ -370,6 +380,21 @@ watch(() => props.moduleId, loadStatus, { immediate: true })
.branch-icon { font-size: 16px; } .branch-icon { font-size: 16px; }
.branch-name { font-weight: 600; color: var(--df-accent); } .branch-name { font-weight: 600; color: var(--df-accent); }
.branch-count { margin-left: auto; font-size: 12px; color: var(--df-text-dim); } .branch-count { margin-left: auto; font-size: 12px; color: var(--df-text-dim); }
.branch-switch-btn {
background: none;
border: none;
color: var(--df-text-dim);
cursor: pointer;
padding: 2px 6px;
border-radius: 3px;
display: flex;
align-items: center;
transition: all 0.15s;
}
.branch-switch-btn:hover {
color: var(--df-text);
background: rgba(255,255,255,0.05);
}
/* Tabs */ /* Tabs */
.gc-tabs { display: flex; border-bottom: 0.5px solid var(--df-border); flex-shrink: 0; } .gc-tabs { display: flex; border-bottom: 0.5px solid var(--df-border); flex-shrink: 0; }
@@ -424,6 +449,12 @@ watch(() => props.moduleId, loadStatus, { immediate: true })
font-size: 11px; color: var(--df-accent); flex-shrink: 0; font-size: 11px; color: var(--df-accent); flex-shrink: 0;
} }
.gc-commit-msg { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; } .gc-commit-msg { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.gc-commit-author {
font-size: 11px; color: var(--df-text-dim);
flex-shrink: 0;
max-width: 100px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gc-commit-time { font-size: 11px; color: var(--df-text-dim); flex-shrink: 0; } .gc-commit-time { font-size: 11px; color: var(--df-text-dim); flex-shrink: 0; }
/* 提交详情 */ /* 提交详情 */
@@ -438,6 +469,10 @@ watch(() => props.moduleId, loadStatus, { immediate: true })
font-size: 11px; color: var(--df-accent); font-size: 11px; color: var(--df-accent);
} }
.gc-commit-detail-msg { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; } .gc-commit-detail-msg { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.gc-commit-detail-author {
font-size: 11px; color: var(--df-text-dim);
flex-shrink: 0;
}
.gc-commit-detail-time { font-size: 11px; color: var(--df-text-dim); flex-shrink: 0; } .gc-commit-detail-time { font-size: 11px; color: var(--df-text-dim); flex-shrink: 0; }
.gc-detail-close { .gc-detail-close {
background: none; border: none; color: var(--df-text-dim); background: none; border: none; color: var(--df-text-dim);

View File

@@ -13,5 +13,6 @@ export default {
notGitRepo: 'Not a git repository', notGitRepo: 'Not a git repository',
detached: 'HEAD detached', detached: 'HEAD detached',
loadMore: 'Load more', loadMore: 'Load more',
switchBranch: 'Switch branch',
}, },
} }

View File

@@ -13,5 +13,6 @@ export default {
notGitRepo: '非 Git 仓库', notGitRepo: '非 Git 仓库',
detached: 'HEAD 分离', detached: 'HEAD 分离',
loadMore: '加载更多', loadMore: '加载更多',
switchBranch: '切换分支',
}, },
} }