优化: 前端批(审批体系一致 + GitChanges UI + token/控件统一 + 死代码)

审批体系一致:ApprovalPopup 浮窗加挂起时长提示(H1)+session 按钮+path deny 二次确认(M4/M5);ToolCard/useToolApproval 加 session+onDeny 确认(对齐 DirAuthDialog 四按钮)+ToolCardList decision 类型加 session;ApprovalDialog 失败显 toast(治 store 吞错 M31);ApprovalOverlay dismiss 改响应式(L26)

GitChanges UI(问题12 前端 12b-12h):showCommitFileDiff 精确匹配(12b)+去重 git log(12c)+详情滑出不挤压(12d)+双行提交行(12f)+搜索过滤(12g)+历史 Tab 显分支(12h)+diff 侧边栏内更宽可折叠面板(12e:FilePreview 不支持任意提交 diff,折中)

token+控件:global.css 定义 --df-primary(治幽灵变量 M26)+settings.css 加 .setting-number/.setting-input/.setting-textarea+4 Section 套用(问题8)

死代码:删 PlanProgress.vue(零引用)+PlanLayerInfo 类型+i18n 孤儿 key(L1)
This commit is contained in:
lxy
2026-08-02 12:42:52 +08:00
parent ffb202e2e9
commit caaabf0c15
19 changed files with 654 additions and 335 deletions
+36
View File
@@ -49,12 +49,17 @@
<button class="btn btn-ghost" @click="emit('later')">{{ $t('projectDetail.approvalLater') }}</button>
</div>
</div>
<!-- M31: 审批失败反馈(store approveHumanApproval 内部 catch 吞错,失败时 pendingApproval 不清空)
对话框保持开启,用户可看到错误并重试 -->
<div v-if="toast.visible" class="ad-toast" :class="'ad-toast-' + toast.type">{{ toast.msg }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useProjectStore } from '@/stores/project'
import { useToast } from '@/composables/useToast'
// 抽离自 ProjectDetail.vue(L175-225 模板 + L416-471 审批逻辑/watch)
// store 共享:父级 useProjectStore() 实例传入(同源 store,响应式共享 pendingApproval)
@@ -76,11 +81,20 @@ const pendingApproval = computed(() => props.store.pendingApproval)
const isMultipleSelect = computed(() => pendingApproval.value?.select_type === 'multiple')
const multiDecisions = ref<string[]>([])
const submitting = ref(false)
const { t } = useI18n()
const { toast, showToast } = useToast()
// M31: store.approveHumanApproval 内部 try/catch 吞错(只 console.error + 写 state.error,不 rethrow),
// 失败时 state.pendingApproval 不清空 → 以此为失败信号。失败显 toast、对话框保持开启,用户可重试或改取消。
async function handleApproval(decision: string) {
submitting.value = true
try {
await props.store.approveHumanApproval([decision])
if (props.store.pendingApproval) {
// 仍 pending = IPC 失败(成功路径 store 已置 null)
showToast(props.store.error || t('projects.err.approveFailed'), 'error', 5000)
return
}
emit('cancel')
} finally {
submitting.value = false
@@ -94,6 +108,10 @@ async function handleApprovalMulti() {
try {
// 统一传 decisions 数组,store 按 select_type 分派单/多选
await props.store.approveHumanApproval([...multiDecisions.value])
if (props.store.pendingApproval) {
showToast(props.store.error || t('projects.err.approveFailed'), 'error', 5000)
return
}
multiDecisions.value = []
emit('cancel')
} finally {
@@ -108,3 +126,21 @@ watch(() => pendingApproval.value, (newApproval) => {
}
}, { immediate: true })
</script>
<style scoped>
/* M31: 审批失败 toast(scoped 自治,不引入全局样式;沿用 Projects.vue .toast 视觉规范) */
.ad-toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 10001;
padding: 10px 18px;
border-radius: var(--df-radius-md);
font-size: 13px;
font-weight: 500;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.3);
pointer-events: none;
}
.ad-toast-error { background: var(--df-danger); color: #fff; }
</style>
+300 -135
View File
@@ -1,10 +1,11 @@
<template>
<div class="git-changes">
<!-- 分支信息 -->
<!-- 分支信息(变更 + 历史 Tab 均显示当前分支) -->
<div v-if="branchName" class="git-branch-bar">
<span class="branch-icon"></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-else class="branch-count">{{ totalCommits }} {{ $t('gitChanges.commitsUnit') }}</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>
@@ -72,79 +73,40 @@
<span class="gc-file-name">{{ filePathLeaf(f.path) }}</span>
</div>
</div>
<!-- 变更文件 diff 预览(选中文件时在列表下方展开) -->
<div v-if="selectedFile" class="gc-file-diff">
<div class="gc-file-diff-header">
<span class="gc-file-diff-path">{{ selectedFile }}</span>
<button class="gc-file-diff-close" @click="selectedFile = ''; diffContent = ''"></button>
</div>
<div v-if="diffLoading" class="gc-loading"><span class="spinner"></span></div>
<div v-else-if="!diffContent" class="gc-diff-empty">{{ $t('gitChanges.noDiff') }}</div>
<div v-else class="gc-diff-content">
<div v-for="(ln, idx) in fileDiffLines" :key="idx" class="diff-line" :class="'diff-' + ln.type">
<span class="diff-hdr-text" v-if="ln.type === 'hdr'">{{ ln.text }}</span>
<template v-else>
<span class="diff-line-prefix">{{ ln.prefix }}</span>
<span class="diff-line-text">{{ ln.text }}</span>
</template>
</div>
</div>
</div>
</div>
<!-- ====== 提交历史 ====== -->
<div v-else class="gc-history-wrapper">
<!-- 已选提交的详情 -->
<div v-if="selectedCommit" class="gc-commit-detail">
<div class="gc-commit-detail-header">
<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-time">{{ formatTime(selectedCommit.timestamp) }}</span>
<button class="gc-detail-close" @click="selectedCommit = null; commitDetail = null"></button>
</div>
<div v-if="commitDetailLoading" class="gc-loading"><span class="spinner"></span></div>
<template v-else-if="commitDetail">
<!-- 变更文件列表 -->
<div class="gc-commit-files">
<div
v-for="f in commitDetail.files"
:key="f.path"
class="gc-commit-file-row"
:class="{ active: commitSelectedFile === f.path }"
@click="showCommitFileDiff(f.path)"
>
<span class="gc-file-status" :class="'gc-st-' + statusKey(f.status)">{{ statusLabel(f.status) }}</span>
<span class="gc-file-path">{{ f.path }}</span>
</div>
</div>
<!-- Diff 预览 -->
<div v-if="commitSelectedFile && commitDiff" class="gc-diff-content">
<div v-for="(ln, idx) in commitDiffLines" :key="idx" class="diff-line" :class="'diff-' + ln.type">
<span class="diff-hdr-text" v-if="ln.type === 'hdr'">{{ ln.text }}</span>
<template v-else>
<span class="diff-line-prefix">{{ ln.prefix }}</span>
<span class="diff-line-text">{{ ln.text }}</span>
</template>
</div>
</div>
</template>
<!-- ====== 提交历史(列表始终可见,详情侧边/底部) ====== -->
<div v-else class="gc-history">
<!-- 提交搜索框(前端过滤已加载提交:subject/author) -->
<div class="gc-search">
<svg class="gc-search-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input
v-model="commitSearch"
class="gc-search-input"
:placeholder="$t('gitChanges.searchPlaceholder')"
type="text"
/>
<button v-if="commitSearch" class="gc-search-clear" @click="commitSearch = ''"></button>
</div>
<!-- 提交列表 -->
<!-- 提交列表(始终可见,双行布局) -->
<div class="gc-history-list">
<div v-if="filteredCommits.length === 0" class="gc-empty gc-empty-inline">
{{ commitSearch ? $t('gitChanges.noSearchResult') : $t('gitChanges.noCommits') }}
</div>
<div
v-for="c in commits"
v-for="c in filteredCommits"
:key="c.hash"
class="gc-commit-row"
:class="{ selected: selectedCommit?.hash === c.hash }"
@click="selectCommit(c)"
>
<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-time">{{ formatTime(c.timestamp) }}</span>
<div class="gc-commit-line1">
<span class="gc-commit-hash">{{ c.hash.slice(0, 8) }}</span>
<span class="gc-commit-author">{{ c.author }}</span>
<span class="gc-commit-time">{{ formatTime(c.timestamp) }}</span>
</div>
<div class="gc-commit-msg">{{ c.subject }}</div>
</div>
</div>
@@ -155,12 +117,63 @@
<div v-if="loadingMore" class="gc-loading"><span class="spinner"></span></div>
</div>
</div>
<!-- ====== 提交详情(从底部/侧边滑出,列表始终可见) ====== -->
<transition name="gc-detail-slide">
<div v-if="selectedCommit" class="gc-commit-detail">
<div class="gc-commit-detail-header">
<span class="gc-commit-detail-hash">{{ selectedCommit.hash }}</span>
<span class="gc-commit-detail-author">{{ selectedCommit.author }}</span>
<span class="gc-commit-detail-time">{{ formatTime(selectedCommit.timestamp) }}</span>
<span class="gc-commit-detail-msg">{{ selectedCommit.subject }}</span>
<button class="gc-detail-close" @click="closeCommitDetail"></button>
</div>
<div v-if="commitDetailLoading" class="gc-loading"><span class="spinner"></span></div>
<template v-else-if="commitDetail">
<!-- 变更文件列表 -->
<div class="gc-commit-files">
<div
v-for="f in commitDetail.files"
:key="f.path"
class="gc-commit-file-row"
:class="{ active: commitSelectedFile === f.path }"
@click="showCommitFileDiff(f.path)"
>
<span class="gc-file-status" :class="'gc-st-' + statusKey(f.status)">{{ statusLabel(f.status) }}</span>
<span class="gc-file-path">{{ f.path }}</span>
</div>
</div>
<!-- Diff 预览(可收起,占更宽空间) -->
<div v-if="commitSelectedFile" class="gc-commit-diff">
<div class="gc-commit-diff-header">
<span class="gc-commit-diff-path">{{ commitSelectedFile }}</span>
<button class="gc-commit-diff-toggle" @click="commitDiffCollapsed = !commitDiffCollapsed">
{{ commitDiffCollapsed ? $t('gitChanges.expand') : $t('gitChanges.collapse') }}
</button>
</div>
<div v-show="!commitDiffCollapsed" class="gc-diff-content">
<div v-if="!commitDiff" class="gc-diff-empty">{{ $t('gitChanges.noDiff') }}</div>
<div v-for="(ln, idx) in commitDiffLines" :key="idx" class="diff-line" :class="'diff-' + ln.type">
<span class="diff-hdr-text" v-if="ln.type === 'hdr'">{{ ln.text }}</span>
<template v-else>
<span class="diff-line-prefix">{{ ln.prefix }}</span>
<span class="diff-line-text">{{ ln.text }}</span>
</template>
</div>
</div>
</div>
</template>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
/**
* Git 变更查看器 — 变更文件列表(按目录树分组) + 提交历史(分页) + 文件 diff 预览。
* Git 变更查看器 — 变更文件列表(按目录树分组) + 提交历史(分页/搜索) + 文件 diff 预览。
*
* 历史 Tab 布局:列表始终可见,选中提交的详情从底部滑出(可收起),diff 在更宽容器渲染,
* 避免几百行 diff 把列表挤走、或在 30% 窄栏内挤压。
*/
import { ref, computed, watch } from 'vue'
import { moduleApi, type GitStatusResult } from '@/api/module'
@@ -183,20 +196,41 @@ const selectedFile = ref('')
const diffContent = ref('')
const diffLoading = ref(false)
/** 单条提交记录形状(对齐 getModuleCommits / GitRecentCommit)。 */
interface CommitItem {
hash: string
subject: string
timestamp: number
author: string
}
// 提交历史(分页)
const commits = ref<{ hash: string; subject: string; timestamp: number; author: string }[]>([])
const commits = ref<CommitItem[]>([])
const totalCommits = ref(0)
const hasMoreCommits = ref(false)
const loadingMore = ref(false)
const commitSkip = ref(0)
const COMMIT_PAGE_SIZE = 50
// 提交搜索(前端过滤已加载提交:按 subject 关键词或作者)
const commitSearch = ref('')
/** 已加载提交按搜索词过滤(subject 包含或 author 包含)。 */
const filteredCommits = computed<CommitItem[]>(() => {
const q = commitSearch.value.trim().toLowerCase()
if (!q) return commits.value
return commits.value.filter(
(c) => c.subject.toLowerCase().includes(q) || c.author.toLowerCase().includes(q),
)
})
// 已选提交详情
const selectedCommit = ref<{ hash: string; subject: string; timestamp: number; author: string } | null>(null)
const selectedCommit = ref<CommitItem | null>(null)
const commitDetail = ref<{ files: { status: string; path: string }[]; diff: string } | null>(null)
const commitDetailLoading = ref(false)
const commitSelectedFile = ref('')
const commitDiff = ref('')
const commitDiffCollapsed = ref(false)
const branchName = computed(() => gitStatus.value?.branch || '')
@@ -242,7 +276,6 @@ interface DiffLine {
}
const commitDiffLines = computed<DiffLine[]>(() => parseDiff(commitDiff.value))
const fileDiffLines = computed<DiffLine[]>(() => parseDiff(diffContent.value))
function parseDiff(text: string): DiffLine[] {
if (!text) return []
@@ -300,17 +333,80 @@ function filePathLeaf(path: string): string {
return segs[segs.length - 1] || path
}
/**
* 从一次提交的全量 diff 文本中,精确提取指定 path 对应的那一块。
*
* 按 `diff --git a/<path> b/<path>` 头切块,避免子串误配
* (例:`line.includes("src/app.rs")` 会误匹配 `src/app.rs.bak`)。
* 解析 a/ 与 b/ 路径,任一等于目标 path 即命中;切块持续到下一个 `diff --git` 或文本末尾。
*/
function extractFileDiff(allDiff: string, targetPath: string): string {
if (!allDiff) return ''
const lines = allDiff.split('\n')
const chunks: string[][] = []
const chunkPaths: string[] = []
// 第一行若不是 diff 头(罕见:全局元信息),归入一个无 path 的前置块,不参与匹配。
let current: string[] | null = null
for (const line of lines) {
if (line.startsWith('diff --git ')) {
// 起新块
if (current) chunks.push(current)
current = [line]
chunkPaths.push(parseDiffGitPath(line))
} else {
if (!current) current = [] // 兜底:头之前的行
current.push(line)
}
}
if (current) chunks.push(current)
// 定位目标块:精确匹配 a/ 或 b/ 路径(支持重命名,两边任一命中即取该块)
for (let i = 0; i < chunks.length; i++) {
const p = chunkPaths[i]
if (p && (p === targetPath || pathEqualsAnySide(p, targetPath))) {
return chunks[i].join('\n')
}
}
return ''
}
/** 解析 `diff --git a/<path> b/<path>` 头,返回 "a/path|b/path" 复合串(用于后续精确比对)。 */
function parseDiffGitPath(header: string): string {
// 形如 "diff --git a/foo/bar.rs b/foo/bar.rs",空格分隔;path 内不含空格时成立。
// 取第 3、4 段(a/.. b/..),用 "|" 拼接便于 pathEqualsAnySide 判定。
const parts = header.split(/\s+/)
const a = parts[2] || ''
const b = parts[3] || ''
return `${a}|${b}`
}
/** 给定 parseDiffGitPath 产物与目标 path,判断目标是否等于 a/ 或 b/ 路径。 */
function pathEqualsAnySide(parsed: string, targetPath: string): boolean {
const [a, b] = parsed.split('|')
const aPath = a.startsWith('a/') ? a.slice(2) : a
const bPath = b.startsWith('b/') ? b.slice(2) : b
return aPath === targetPath || bPath === targetPath
}
async function loadStatus() {
if (!props.moduleId) return
loading.value = true
try {
gitStatus.value = await moduleApi.getModuleGitStatus(props.moduleId)
// 同时初始化前 50 条提交
const res = await moduleApi.getModuleCommits(props.moduleId, 0, COMMIT_PAGE_SIZE)
commits.value = res.commits
hasMoreCommits.value = res.has_more
totalCommits.value = res.commits.length
commitSkip.value = res.commits.length
// 复用 getModuleGitStatus 内部已返回的 recent_commits(避免再发一次 getModuleCommits
// 造成首次 git log -50 重复请求 + recent_commits 被丢弃)。
const recent = gitStatus.value?.recent_commits ?? []
commits.value = recent.map((c) => ({
hash: c.hash,
subject: c.subject,
timestamp: c.timestamp,
author: c.author,
}))
totalCommits.value = commits.value.length
commitSkip.value = commits.value.length
// recent_commits 通常为 50 条;若返回等于页大小则假定还有更多(精确 total 需后端,见 12a)。
hasMoreCommits.value = commits.value.length >= COMMIT_PAGE_SIZE
} catch {
gitStatus.value = null
commits.value = []
@@ -352,9 +448,13 @@ async function loadMoreCommits() {
loadingMore.value = true
try {
const res = await moduleApi.getModuleCommits(props.moduleId, commitSkip.value, COMMIT_PAGE_SIZE)
commits.value.push(...res.commits)
// 按哈希去重(防止分页边界与 recent_commits 重叠)
const exist = new Set(commits.value.map((c) => c.hash))
for (const c of res.commits) {
if (!exist.has(c.hash)) commits.value.push(c)
}
hasMoreCommits.value = res.has_more
commitSkip.value = commits.value.length
commitSkip.value += res.commits.length
totalCommits.value = commits.value.length
} catch {
// 静默失败
@@ -365,10 +465,7 @@ async function loadMoreCommits() {
async function switchToHistory() {
showHistory.value = true
// 如果只加载了初始数量,尝试补页
if (hasMoreCommits.value && commits.value.length < 50) {
await loadMoreCommits()
}
// recent_commits 已填首页,无需在此补页(原 < 50 补页逻辑已无意义,留 loadMore 手动触发)
}
async function selectFile(path: string) {
@@ -387,11 +484,17 @@ async function selectFile(path: string) {
}
/** 点击提交行 → 加载该提交的变更文件列表 */
async function selectCommit(c: { hash: string; subject: string; timestamp: number; author: string }) {
async function selectCommit(c: CommitItem) {
// 点击已选提交 → 收起详情(二次点击切换)
if (selectedCommit.value?.hash === c.hash) {
closeCommitDetail()
return
}
selectedCommit.value = c
commitDetail.value = null
commitSelectedFile.value = ''
commitDiff.value = ''
commitDiffCollapsed.value = false
commitDetailLoading.value = true
try {
const res = await moduleApi.getCommitDetail(props.moduleId, c.hash)
@@ -403,24 +506,22 @@ async function selectCommit(c: { hash: string; subject: string; timestamp: numbe
}
}
/** 在提交详情中点击文件 → 过滤出该文件的 diff */
function closeCommitDetail() {
selectedCommit.value = null
commitDetail.value = null
commitSelectedFile.value = ''
commitDiff.value = ''
}
/** 在提交详情中点击文件 → 精确提取该文件的 diff 块(按 diff --git 头切块) */
function showCommitFileDiff(path: string) {
commitSelectedFile.value = path
if (!commitDetail.value) return
// 从全量 diff 中提取该文件的块
const diff = commitDetail.value.diff
const lines = diff.split('\n')
let inTarget = false
const targetLines: string[] = []
for (const line of lines) {
if (line.startsWith('diff --git')) {
inTarget = line.includes(path)
}
if (inTarget) {
targetLines.push(line)
}
commitDiffCollapsed.value = false
if (!commitDetail.value) {
commitDiff.value = ''
return
}
commitDiff.value = targetLines.join('\n')
commitDiff.value = extractFileDiff(commitDetail.value.diff, path)
}
watch(() => props.moduleId, loadStatus, { immediate: true })
@@ -483,27 +584,6 @@ watch(() => props.moduleId, loadStatus, { immediate: true })
position: sticky; top: 0; z-index: 1;
}
/* 变更文件 diff 预览(列表下方展开) */
.gc-file-diff {
margin-top: 8px;
border-top: 0.5px solid var(--df-border);
}
.gc-file-diff-header {
display: flex;
align-items: center;
padding: 6px 14px;
background: rgba(255,255,255,0.03);
font-family: var(--df-font-mono, Consolas, monospace);
font-size: 11px;
border-bottom: 0.5px solid var(--df-border);
}
.gc-file-diff-path { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--df-text); }
.gc-file-diff-close {
background: none; border: none; color: var(--df-text-dim);
cursor: pointer; font-size: 14px; padding: 0 4px;
}
.gc-file-diff-close:hover { color: var(--df-text); }
/* Diff 预览 */
.gc-diff-content {
font-family: var(--df-font-mono, Consolas, monospace);
@@ -522,52 +602,95 @@ watch(() => props.moduleId, loadStatus, { immediate: true })
.diff-del .diff-line-prefix { color: #e05050; }
.diff-ctx { color: var(--df-text); }
/* 提交历史 */
.gc-history-wrapper { display: flex; flex-direction: column; }
/* ====== 提交历史 ====== */
.gc-history { display: flex; flex-direction: column; min-height: 0; }
/* 搜索框 */
.gc-search {
display: flex; align-items: center; gap: 6px;
padding: 6px 10px; border-bottom: 0.5px solid var(--df-border);
background: var(--df-bg-card); flex-shrink: 0;
}
.gc-search-icon { color: var(--df-text-dim); flex-shrink: 0; }
.gc-search-input {
flex: 1; min-width: 0;
background: rgba(255,255,255,0.04); border: 0.5px solid var(--df-border);
border-radius: 4px; padding: 4px 8px;
color: var(--df-text); font-size: 12px; outline: none;
}
.gc-search-input:focus { border-color: var(--df-accent); }
.gc-search-input::placeholder { color: var(--df-text-dim); }
.gc-search-clear {
background: none; border: none; color: var(--df-text-dim);
cursor: pointer; font-size: 12px; padding: 0 4px;
}
.gc-search-clear:hover { color: var(--df-text); }
/* 提交列表 */
.gc-history-list { flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: thin; }
.gc-commit-row {
display: flex; align-items: center; gap: 10px;
padding: 8px 14px; border-bottom: 0.5px solid rgba(255,255,255,0.03);
display: flex; flex-direction: column; gap: 2px;
padding: 6px 14px; border-bottom: 0.5px solid rgba(255,255,255,0.03);
cursor: pointer; transition: background 0.1s;
}
.gc-commit-row:hover { background: rgba(255,255,255,0.04); }
.gc-commit-row.selected { background: rgba(255,255,255,0.06); }
.gc-commit-line1 {
display: flex; align-items: center; gap: 8px;
font-size: 11px; color: var(--df-text-dim);
}
.gc-commit-hash {
font-family: var(--df-font-mono, Consolas, monospace);
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-author {
font-size: 11px; color: var(--df-text-dim);
flex-shrink: 0;
max-width: 100px;
flex-shrink: 0; max-width: 140px;
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 { margin-left: auto; font-size: 11px; color: var(--df-text-dim); flex-shrink: 0; }
.gc-commit-msg {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px;
color: var(--df-text);
}
/* 提交详情 */
.gc-commit-detail { border-bottom: 0.5px solid var(--df-border); }
/* ====== 提交详情(从底部滑出,列表始终可见) ====== */
.gc-commit-detail {
flex-shrink: 0;
border-top: 0.5px solid var(--df-border);
background: var(--df-bg-card);
max-height: 55%;
display: flex; flex-direction: column;
box-shadow: 0 -4px 12px rgba(0,0,0,0.18);
}
.gc-commit-detail-header {
display: flex; align-items: center; gap: 10px;
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
padding: 8px 14px; background: rgba(255,255,255,0.03);
position: sticky; top: 0; z-index: 1;
border-bottom: 0.5px solid var(--df-border);
}
.gc-commit-detail-hash {
font-family: var(--df-font-mono, Consolas, monospace);
font-size: 11px; color: var(--df-accent);
font-size: 11px; color: var(--df-accent); flex-shrink: 0;
}
.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;
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-msg {
flex: 1; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px;
}
.gc-detail-close {
background: none; border: none; color: var(--df-text-dim);
cursor: pointer; font-size: 14px; padding: 0 4px;
cursor: pointer; font-size: 14px; padding: 0 4px; flex-shrink: 0;
}
.gc-detail-close:hover { color: var(--df-text); }
/* 提交的文件列表 */
.gc-commit-files { border-bottom: 0.5px solid var(--df-border); }
.gc-commit-files {
border-bottom: 0.5px solid var(--df-border);
overflow-y: auto; max-height: 140px; scrollbar-width: thin; flex-shrink: 0;
}
.gc-commit-file-row {
display: flex; align-items: center; gap: 8px;
padding: 5px 14px 5px 28px; cursor: pointer; transition: background 0.1s;
@@ -576,25 +699,66 @@ watch(() => props.moduleId, loadStatus, { immediate: true })
.gc-commit-file-row:hover { background: rgba(255,255,255,0.04); }
.gc-commit-file-row.active { background: rgba(255,255,255,0.06); }
/* 提交 diff(可收起,占更宽空间) */
.gc-commit-diff {
flex: 1; min-height: 0;
display: flex; flex-direction: column;
overflow: hidden;
}
.gc-commit-diff-header {
display: flex; align-items: center; gap: 8px;
padding: 5px 14px; background: rgba(255,255,255,0.03);
border-bottom: 0.5px solid var(--df-border); flex-shrink: 0;
}
.gc-commit-diff-path {
flex: 1; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-family: var(--df-font-mono, Consolas, monospace); font-size: 11px;
color: var(--df-text);
}
.gc-commit-diff-toggle {
background: none; border: 0.5px solid var(--df-border);
color: var(--df-text-dim); cursor: pointer; font-size: 11px;
padding: 2px 8px; border-radius: 3px;
}
.gc-commit-diff-toggle:hover { color: var(--df-text); border-color: var(--df-accent); }
.gc-commit-diff .gc-diff-content { flex: 1; min-height: 0; }
/* 滑出动画 */
.gc-detail-slide-enter-active, .gc-detail-slide-leave-active {
transition: transform 0.18s ease, opacity 0.18s ease;
}
.gc-detail-slide-enter-from, .gc-detail-slide-leave-to {
transform: translateY(12px); opacity: 0;
}
.gc-file-status { width: 20px; text-align: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
.gc-st-modified { color: #f0a020; }
.gc-st-added { color: #4caf50; }
.gc-st-deleted { color: #e05050; }
.gc-st-untracked { color: #999; }
.gc-file-path {
flex: 1; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-family: var(--df-font-mono, Consolas, monospace); font-size: 12px;
}
.gc-file-name {
flex: 1; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-family: var(--df-font-mono, Consolas, monospace); font-size: 12px;
}
.gc-file-row {
display: flex; align-items: center; gap: 8px;
cursor: pointer; transition: background 0.1s;
}
.gc-file-row:hover { background: rgba(255,255,255,0.04); }
.gc-file-row.selected { background: rgba(255,255,255,0.06); }
/* 加载更多 */
.gc-load-more {
text-align: center; padding: 12px; cursor: pointer;
color: var(--df-accent); font-size: 12px;
transition: background 0.1s;
transition: background 0.1s; flex-shrink: 0;
}
.gc-load-more:hover { background: rgba(255,255,255,0.04); }
@@ -671,6 +835,7 @@ watch(() => props.moduleId, loadStatus, { immediate: true })
display: flex; justify-content: center; align-items: center;
padding: 40px 0; color: var(--df-text-dim); font-size: 13px;
}
.gc-empty-inline { padding: 24px 0; }
.gc-diff-loading { display: flex; justify-content: center; padding: 20px; }
.gc-diff-empty { padding: 20px 14px; color: var(--df-text-dim); font-size: 12px; text-align: center; }
.spinner {