优化: 项目文件树/依赖图组件 + 灵感对抗校验 + 小程序脚本 + 模块管理

This commit is contained in:
lxy
2026-08-10 00:00:31 +08:00
parent 6bafdcd5a5
commit cf00345c35
19 changed files with 521 additions and 185 deletions
+4 -1
View File
@@ -36,6 +36,9 @@ export interface FileTreeEntry {
size: number
/** Git 状态码(`git status --porcelain` 的 XY 两字符,如 " M"/"M "/"??");文件夹恒无 */
git_status?: string
/** 目录是否含可见子项(过滤隐藏文件/噪音目录后,与展开实际展示一致);文件恒为 false。
* 后端列目录时即时统计,前端未展开即可据此行内标注空目录。 */
has_children: boolean
}
/** getModuleFileTree 返回结构。 */
@@ -177,7 +180,7 @@ export const moduleApi = {
},
/** 分页查询工程 Git 提交历史。返回 { commits, has_more }。 */
/** 列出工程本地分支(只读)。返回 { current, branches: [{ name, is_current }] }。 */
/** 列出工程分支(本地 + 远程跟踪,只读)。返回 { current, branches: [{ name, is_current }] }。 */
listBranches(moduleId: string): Promise<{ current: string; branches: { name: string; is_current: boolean }[] }> {
return invoke('list_branches', { moduleId })
},
+75 -16
View File
@@ -80,9 +80,9 @@
*
* 节点用 Vue 组件(ModuleNode.vue)渲染,通过 @antv/x6-vue-shape 注册。
*/
import { onMounted, onBeforeUnmount, ref, watch, computed, markRaw } from 'vue'
import { onMounted, onBeforeUnmount, onActivated, ref, watch, computed, markRaw } from 'vue'
import { useI18n } from 'vue-i18n'
import { Graph, Selection, Snapline, History, Scroller, MiniMap } from '@antv/x6'
import { Graph, Selection, Snapline, History, Scroller, MiniMap, Export } from '@antv/x6'
import dagre from 'dagre'
import { register } from '@antv/x6-vue-shape'
import '@antv/x6-vue-shape'
@@ -261,13 +261,38 @@ function renderGraph(opts?: { center?: boolean }) {
if (shouldCenter) graph.centerContent()
}
/**
* F4:局部更新节点选中态(替代整图 fromJSON 重建,保留平移/缩放态)。
* 只刷新被点节点 + 上次选中节点两个 cell:更新其 data.selected,并手动触发该 node view 的
* 'vue' action 重挂载 Vue 组件。原因:vue-shape 下 node.setData 不触发组件重渲染
* (NodeView/VueShapeView 的 actions 映射无 data→render,'component' 变化才触发 'vue' action)。
*/
function updateNodeSelection(nextId: string) {
if (!graph) return
const prevId = selectedId.value
selectedId.value = nextId
const ids = new Set<string>()
if (nextId) ids.add(nextId)
if (prevId) ids.add(prevId)
for (const id of ids) {
const cell = graph.getCellById(id)
if (!cell) continue
const d = cell.getData() ?? {}
cell.setData({ ...d, selected: id === nextId })
// 'vue' 不在库导出 FlagManagerAction 联合类型内(vue-shape 扩展),故以 any 访问。
const view = graph.findViewByCell(cell) as any
if (view?.confirmUpdate) view.confirmUpdate(view.getFlag('vue'))
}
}
function buildGraph() {
if (!containerRef.value) return
graph = new Graph({
container: containerRef.value,
background: { color: '#1a1a2e' },
grid: { visible: true, size: 10, type: 'dot', args: { color: '#2a2a4e' } },
// U1:画布/grid 用主题 token 实际色值(X6 background/grid 需具体色值,非 CSS 变量),深色硬编码移除。
background: { color: themeColor('--df-bg', '#0c0e1a') },
grid: { visible: true, size: 10, type: 'dot', args: { color: themeColor('--df-border-strong', '#2a2a4e') } },
mousewheel: { enabled: true, modifiers: ['ctrl'], minScale: 0.3, maxScale: 3 },
interacting: { nodeMovable: true },
})
@@ -277,18 +302,31 @@ function buildGraph() {
graph.use(new History({ enabled: true }))
graph.use(new Scroller({ enabled: true, pannable: true }))
graph.use(new MiniMap({ width: 200, height: 120, padding: 10 }))
// F3:注册 Export 插件。X6 的 toPNG 依赖 graph.use(new Export()) 挂载 'export' 插件,
// 缺注册时 Graph.prototype.toPNG 静默 no-op(回调永不触发、无报错),导出按钮失效。
graph.use(new Export())
graph.on('node:click', ({ node }) => {
// 组件内选中高亮(G4):点击节点仅高亮,不 emit 不导航(依赖图是查看/编辑关系,
// 跳工程详情与 Tab 语义冲突,原 emit+router.push 是无意义死代码)。
selectedId.value = String(node.id)
renderGraph({ center: false })
// F4:局部刷新选中态(仅被点节点+上次选中节点),不再整图 fromJSON 重建,保留平移/缩放态。
updateNodeSelection(String(node.id))
})
renderGraph()
bindEdgeDeleteButtons()
}
/**
* U1:读取 CSS 主题 token 的实际色值(X6 画布 background/grid 需要具体颜色字符串,不支持 CSS 变量)。
* 取不到时回退传入的默认色值。buildGraph 在挂载后执行,此时 data-theme 已生效,读到当前主题色。
*/
function themeColor(varName: string, fallback: string): string {
if (typeof window === 'undefined') return fallback
const v = getComputedStyle(document.documentElement).getPropertyValue(varName).trim()
return v || fallback
}
function fitContent() {
graph?.zoomToFit({ padding: 20, maxScale: 1.5 })
}
@@ -320,19 +358,25 @@ async function checkCycles() {
}
}
/** 导出 PNG。 */
async function exportPNG() {
/**
* 导出 PNG。
* F3:toPNG 回调是异步触发,外层 try/catch 捕不到回调内错误;且 X6 的 toPNGAsync 在导出失败时
* 吞错永不 resolve(会挂死),故沿用回调风格,错误处理放入回调体(try/catch 包下载逻辑),
* 失败 console.error + Message.error 用户可见提示。
*/
function exportPNG() {
if (!graph) return
try {
graph.toPNG((dataUri: string) => {
graph.toPNG((dataUri: string) => {
try {
const a = document.createElement('a')
a.href = dataUri
a.download = `dependency-graph-${props.projectId}.png`
a.click()
})
} catch (e) {
console.error('[DependencyGraph] 导出 PNG 失败:', e)
}
} catch (e) {
console.error('[DependencyGraph] 导出 PNG 失败:', e)
Message.error(t('common.unknownError'))
}
})
}
/** 删除依赖(二次确认 + 重绘)。 */
@@ -384,12 +428,15 @@ function hideEdgeDeleteBtn() {
deleteDepTarget = null
}
// F2:edge hover 处理器提升为 setup 作用域稳定函数引用(非每次 bind 新建对象),
// 使 graph.off 能删掉旧 handler,避免 edge:mouseenter/leave 监听累积导致 showEdgeDeleteBtn 重复触发。
function onEdgeEnter(e: any) { showEdgeDeleteBtn(e.edge) }
function onEdgeLeave() { hideEdgeDeleteBtn() }
/** 给所有 edge 绑 hover 显示删除按钮的监听。 */
function bindEdgeDeleteButtons() {
if (!graph) return
try { graph.off('edge:mouseenter', onEdgeEnter); graph.off('edge:mouseleave', onEdgeLeave) } catch { /* ignore */ }
function onEdgeEnter(e: any) { showEdgeDeleteBtn(e.edge) }
function onEdgeLeave() { hideEdgeDeleteBtn() }
graph.on('edge:mouseenter', onEdgeEnter)
graph.on('edge:mouseleave', onEdgeLeave)
}
@@ -410,6 +457,18 @@ onMounted(async () => {
buildGraph()
})
// KeepAlive 保活:本组件被 KeepAlive 包裹不重挂载,缺 onActivated 时切回依赖图 tab 不刷新(F1)。
// 切回 tab 时重新拉模块+依赖并重绘(覆盖概览页增删模块后切回的场景)。
// 首次挂载 KeepAlive 激活也会触发,此时 onMounted 已 loadModules+renderGraph,守卫跳过避免重复拉。
let activatedOnce = false
onActivated(() => {
if (!activatedOnce) {
activatedOnce = true
return
}
void loadModules().then(() => renderGraph())
})
watch(() => props.projectId, async () => {
await loadModules()
renderGraph()
+11 -2
View File
@@ -129,6 +129,7 @@
:file-path="selectedFilePath"
:module-root-path="currentModule?.path ?? ''"
:git-status="selectedFileGitStatus"
:external-diff="selectedCommitDiff"
/>
<div v-else class="preview-placeholder-inline">{{ $t('fileExplorer.selectFileHint') }}</div>
</div>
@@ -252,6 +253,8 @@ const expandedPaths = reactive(new Set<string>())
const loadedChildren = reactive(new Map<string, FileTreeEntry[]>())
const selectedFilePath = ref<string | null>(null)
const selectedFileGitStatus = ref<string | undefined>(undefined)
/** GitChanges(变更/提交历史)点文件注入的 diff:传给右侧 FilePreview 在宽区渲染(历史提交的 diff 优先)。 */
const selectedCommitDiff = ref('')
const currentModule = computed(
() => modules.value.find((m) => m.id === currentModuleId.value) ?? null,
@@ -302,6 +305,7 @@ function resetTreeState() {
loadedChildren.clear()
selectedFilePath.value = null
selectedFileGitStatus.value = undefined
selectedCommitDiff.value = ''
}
/** 自定义下拉选择工程。 */
@@ -317,6 +321,8 @@ function onFileSelect(path: string) {
selectedFilePath.value = path
// 从 loadedChildren 反查 git_status(任意层目录的条目都可能含此文件)。
selectedFileGitStatus.value = findEntryGitStatus(path)
// 树视图选中 → 回到内容预览,不残留 GitChanges 注入的 diff
selectedCommitDiff.value = ''
}
/** 递归在 loadedChildren 缓存中查指定路径条目的 git_status。 */
@@ -351,6 +357,7 @@ async function refresh() {
loadedChildren.clear()
selectedFilePath.value = null
selectedFileGitStatus.value = undefined
selectedCommitDiff.value = ''
// 等一个 tick 让 FileTree watch 触发重拉;实际拉取在子组件,这里只做状态清空。
await new Promise((r) => setTimeout(r, 50))
refreshing.value = false
@@ -402,10 +409,11 @@ async function switchToChanges() {
}
}
/** Git 变更视图中选择文件 → 在右侧预览显示 diff。 */
function onChangeFileSelect(path: string) {
/** Git 变更视图中选择文件 → 在右侧预览显示 diff(变化/提交历史点文件都走此路,宽区渲染)。 */
function onChangeFileSelect(path: string, diff?: string) {
selectedFilePath.value = path
selectedFileGitStatus.value = gitStatusData.value?.changed_files.find(f => f.path === path)?.status
selectedCommitDiff.value = diff ?? ''
}
watch(() => props.projectId, loadModules, { immediate: true })
@@ -761,6 +769,7 @@ async function onRemoveModule() {
.explorer-tree {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 8px 4px 8px 0;
+28 -13
View File
@@ -39,28 +39,28 @@
<div class="preview-placeholder-text">{{ $t('fileExplorer.selectFileHint') }}</div>
</div>
<!-- 加载中 -->
<div v-else-if="loading" class="preview-loading">
<!-- 加载中(外部 diff 存在时让位于 diff 视图,历史提交的文件可能不在当前工作树) -->
<div v-else-if="loading && !isExternalDiff" class="preview-loading">
<span class="spinner"></span>
{{ $t('fileExplorer.loadingFile') }}
</div>
<!-- 错误 -->
<div v-else-if="error" class="preview-error"> {{ error }}</div>
<!-- 错误(外部 diff 存在时同样让位:历史文件当前树不存在不应遮住 diff) -->
<div v-else-if="error && !isExternalDiff" class="preview-error"> {{ error }}</div>
<!-- 二进制 -->
<div v-else-if="isBinary" class="preview-binary">
<!-- 二进制(外部 diff 存在时让位) -->
<div v-else-if="isBinary && !isExternalDiff" class="preview-binary">
<span class="binary-icon">📄</span>
<p>{{ $t('fileExplorer.binaryNotSupported') }}</p>
</div>
<!-- 图片 -->
<div v-else-if="isImage" class="preview-image">
<!-- 图片(外部 diff 存在时让位) -->
<div v-else-if="isImage && !isExternalDiff" class="preview-image">
<img :src="imageUrl ?? ''" :alt="filePath ?? ''" />
</div>
<!-- Diff 视图(切到 diff 模式时显;置于 Markdown 之前,使 .md 文件也支持 Diff) -->
<div v-else-if="showDiff && diffContent" class="preview-diff">
<!-- Diff 视图(外部注入的 diff 优先展;置于 Markdown 之前,使 .md 文件也支持 Diff) -->
<div v-else-if="showDiff && diffViewContent" class="preview-diff">
<div v-for="(ln, idx) in diffLines" :key="idx" class="diff-line" :class="'diff-' + ln.type">
<span class="diff-line-num">{{ ln.oldNum || '' }}</span>
<span class="diff-line-num">{{ ln.newNum || '' }}</span>
@@ -98,6 +98,9 @@ const props = defineProps<{
filePath: string | null
moduleRootPath: string
gitStatus?: string
/** 外部注入的 diff(如 GitChanges 变更/提交历史点文件得到的该文件 diff,含历史提交的 diff)。
* 非空时优先渲染此 diff(宽预览区),不发起重复的 git diff 请求。 */
externalDiff?: string
}>()
const { } = useMarkdown()
@@ -127,6 +130,17 @@ const showDiff = ref(false)
const diffContent = ref('')
const diffLoading = ref(false)
/** 是否存在外部注入的 diff(非空即视为外部 diff 模式:默认展示 diff,且 diff 优先级最高)。 */
const isExternalDiff = computed(() => !!props.externalDiff && props.externalDiff.trim().length > 0)
/** 实际展示的 diff 内容:外部注入优先,否则用本组件拉取的 git diff。 */
const diffViewContent = computed(() => (isExternalDiff.value ? props.externalDiff! : diffContent.value))
/** 外部 diff 注入时默认切到 diff 视图(点文件即看变化,无需再点 📝);清空则回到内容视图。 */
watch(() => props.externalDiff, (val) => {
showDiff.value = !!(val && val.trim().length > 0)
})
/** 请求序号守卫:loadFile/loadDiff 各自单调递增,await 返回后校验仍是"最新 seq"才写状态,
* 否则丢弃(快速切文件/切视图时旧响应晚到不覆盖新内容)。
* 注:loadFile 与 loadDiff 分用两个计数器 —— 若共用一个,loadDiff 自增会让在途的 loadFile
@@ -152,11 +166,11 @@ function parseHunkHeader(line: string): { oldStart: number; newStart: number } {
}
const diffLines = computed<DiffLine[]>(() => {
if (!diffContent.value) return []
if (!diffViewContent.value) return []
const lines: DiffLine[] = []
let oldNum = 0
let newNum = 0
for (const raw of diffContent.value.split('\n')) {
for (const raw of diffViewContent.value.split('\n')) {
if (raw.startsWith('@@')) {
const h = parseHunkHeader(raw)
oldNum = h.oldStart
@@ -180,7 +194,8 @@ const diffLines = computed<DiffLine[]>(() => {
function toggleDiff() {
showDiff.value = !showDiff.value
if (showDiff.value && !diffContent.value) {
// 外部 diff 已由调用方提供,无需再发 git diff;仅内部模式在首次切到 diff 时才拉取
if (showDiff.value && !diffContent.value && !isExternalDiff.value) {
loadDiff()
}
}
+31 -2
View File
@@ -13,8 +13,11 @@
<!-- 错误 -->
<div v-else-if="error" class="tree-error"> {{ error }}</div>
<!-- 空目录 -->
<div v-else-if="entries.length === 0" class="tree-empty">{{ $t('fileExplorer.emptyDir') }}</div>
<!-- 空目录(展开后子条目为空) -->
<div v-else-if="entries.length === 0" class="tree-empty">
<span class="tree-empty-icon">📭</span>
<span>{{ $t('fileExplorer.emptyDir') }}</span>
</div>
<!-- 条目列表 -->
<ul v-else class="tree-list">
@@ -39,6 +42,13 @@
<span class="expand-icon">{{ expandedPaths.has(entry.path) ? '▾' : '▸' }}</span>
<span class="file-icon">📁</span>
<span class="file-name">{{ entry.name }}</span>
<!-- 空目录标:has_children 由后端列目录时即时统计,未展开即可见,
避免用户误以为目录没加载出来(展开后才显空态) -->
<span
v-if="entry.has_children === false"
class="tree-empty-badge"
:title="$t('fileExplorer.emptyDir')"
>{{ $t('fileExplorer.emptyDirMark') }}</span>
<!-- B3[PD-P1-6]:展开失败 行内 (title 显原因),点击重试;不设整树 error,不炸其他已展开节点 -->
<span
v-if="toggleErrors.has(entry.path)"
@@ -356,6 +366,25 @@ watch(
cursor: pointer;
}
/* 空目录小标(目录行右侧,未展开即可见;flex-shrink 防挤压) */
.tree-empty-badge {
flex-shrink: 0;
/* 文件名后稍靠右一点(不贴名、也不推到最右):固定小间隔即可 */
margin-left: 8px;
font-size: 10px;
line-height: 1.4;
padding: 1px 5px;
border-radius: 8px;
background: rgba(150, 150, 150, 0.14);
color: var(--df-text-dim);
}
/* 展开后空态图标(📭,与文字同行) */
.tree-empty-icon {
font-size: 14px;
line-height: 1;
}
/* Git 状态徽章 */
.git-badge {
flex-shrink: 0;
+18 -103
View File
@@ -130,7 +130,7 @@
</div>
<div v-if="commitDetailLoading" class="gc-loading"><span class="spinner"></span></div>
<template v-else-if="commitDetail">
<!-- 变更文件列表 -->
<!-- 变更文件列表(点击文件 diff 渲染到右侧宽预览区, emit select-file) -->
<div class="gc-commit-files">
<div
v-for="f in commitDetail.files"
@@ -143,25 +143,6 @@
<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>
@@ -172,8 +153,9 @@
/**
* Git 变更查看器 — 变更文件列表(按目录树分组) + 提交历史(分页/搜索) + 文件 diff 预览。
*
* 历史 Tab 布局:列表始终可见,选中提交的详情从底部滑出(可收起),diff 在更宽容器渲染,
* 避免几百行 diff 把列表挤走、或在 30% 窄栏内挤压。
* 布局:变更 Tab 点文件 / 提交历史点文件 → emit select-file,把该文件 diff 注入右侧 FilePreview
* 宽预览区渲染;本组件窄栏只保留文件列表与提交详情(文件清单),不再内嵌 diff —— 几百行 diff
* 在 30% 窄栏内渲染会被挤压/截断,故 diff 展示统一走右侧宽区。
*/
import { ref, computed, watch } from 'vue'
import { moduleApi, type GitStatusResult } from '@/api/module'
@@ -188,7 +170,8 @@ const props = defineProps<{
}>()
const emit = defineEmits<{
(e: 'select-file', path: string): void
/** 选择文件 → 在右侧宽预览区展示其 diff(第二个参数为可选注入的 diff 内容)。 */
(e: 'select-file', path: string, diff?: string): void
}>()
const loading = ref(false)
@@ -231,8 +214,6 @@ 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 || '')
@@ -271,33 +252,6 @@ const groupedChanges = computed<FileGroup[]>(() => {
return groups.filter(g => g.files.length > 0)
})
interface DiffLine {
type: 'add' | 'del' | 'ctx' | 'hdr'
prefix: string
text: string
}
const commitDiffLines = computed<DiffLine[]>(() => parseDiff(commitDiff.value))
function parseDiff(text: string): DiffLine[] {
if (!text) return []
const lines: DiffLine[] = []
for (const raw of text.split('\n')) {
if (raw.startsWith('@@')) {
lines.push({ type: 'hdr', prefix: '', text: raw })
} else if (raw.startsWith('+')) {
lines.push({ type: 'add', prefix: '+', text: raw.slice(1) })
} else if (raw.startsWith('-')) {
lines.push({ type: 'del', prefix: '-', text: raw.slice(1) })
} else if (raw.startsWith('\\')) {
continue
} else {
lines.push({ type: 'ctx', prefix: ' ', text: raw })
}
}
return lines
}
function statusLabel(s: string): string {
const x = s.trim()
if (x === '??') return 'U'
@@ -478,6 +432,7 @@ async function switchToHistory() {
async function selectFile(path: string) {
selectedFile.value = path
// 先切预览到该文件(内容视图),diff 拉到后回填,点文件即展示变化
emit('select-file', path)
diffContent.value = ''
diffLoading.value = true
@@ -489,6 +444,10 @@ async function selectFile(path: string) {
} finally {
diffLoading.value = false
}
// 仅当仍是当前选中文件时才回填 diff,防快速切换时旧响应覆盖新文件
if (selectedFile.value === path) {
emit('select-file', path, diffContent.value)
}
}
/** 点击提交行 → 加载该提交的变更文件列表 */
@@ -501,8 +460,8 @@ async function selectCommit(c: CommitItem) {
selectedCommit.value = c
commitDetail.value = null
commitSelectedFile.value = ''
commitDiff.value = ''
commitDiffCollapsed.value = false
// 切换提交 → 清空右侧宽预览,避免残留上一个提交文件的 diff 与当前提交错位
emit('select-file', '', '')
commitDetailLoading.value = true
try {
const res = await moduleApi.getCommitDetail(props.moduleId, c.hash)
@@ -518,18 +477,17 @@ function closeCommitDetail() {
selectedCommit.value = null
commitDetail.value = null
commitSelectedFile.value = ''
commitDiff.value = ''
emit('select-file', '', '')
}
/** 在提交详情中点击文件 → 精确提取该文件的 diff 块(按 diff --git 头切块) */
/** 在提交详情中点击文件 → 精确提取该文件的 diff 块(按 diff --git 头切块),路由到右侧宽预览区渲染 */
function showCommitFileDiff(path: string) {
commitSelectedFile.value = path
commitDiffCollapsed.value = false
if (!commitDetail.value) {
commitDiff.value = ''
emit('select-file', path, '')
return
}
commitDiff.value = extractFileDiff(commitDetail.value.diff, path)
emit('select-file', path, extractFileDiff(commitDetail.value.diff, path))
}
watch(() => props.moduleId, loadStatus, { immediate: true })
@@ -599,23 +557,7 @@ watch(() => props.refreshKey, () => {
position: sticky; top: 0; z-index: 1;
}
/* Diff 预览 */
.gc-diff-content {
font-family: var(--df-font-mono, Consolas, monospace);
font-size: 12px; line-height: 1.5; overflow: auto; scrollbar-width: thin;
}
.diff-line { display: flex; padding: 0 14px; }
.diff-line-prefix { width: 16px; flex-shrink: 0; text-align: center; user-select: none; }
.diff-line-text { flex: 1; white-space: pre; overflow: hidden; }
.diff-hdr-text {
padding: 4px 14px; background: rgba(60,140,220,0.08);
color: var(--df-text-dim); font-weight: 500; font-size: 11px; display: block;
}
.diff-add { background: rgba(40,160,70,0.12); }
.diff-add .diff-line-prefix { color: #4caf50; }
.diff-del { background: rgba(220,60,60,0.12); }
.diff-del .diff-line-prefix { color: #e05050; }
.diff-ctx { color: var(--df-text); }
/* Diff 预览样式已随窄栏内 diff 面板移除(diff 统一在右侧 FilePreview 宽区渲染) */
/* ====== 提交历史 ====== */
.gc-history { display: flex; flex-direction: column; min-height: 0; }
@@ -714,31 +656,6 @@ watch(() => props.refreshKey, () => {
.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: 4px;
}
.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;
@@ -851,8 +768,6 @@ watch(() => props.refreshKey, () => {
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 {
width: 16px; height: 16px;
border: 2px solid var(--df-border); border-top-color: var(--df-accent);
+10 -10
View File
@@ -66,21 +66,21 @@ const stackList = computed(() => parseJsonArray(data.value?.stack).slice(0, 3))
width: 100%;
height: 100%;
box-sizing: border-box;
background: #16213e;
border: 1px solid #0f3460;
background: var(--df-bg-card);
border: 1px solid var(--df-border-strong);
border-radius: 6px;
cursor: pointer;
transition: border-color 0.15s;
}
.module-node.active {
border-color: #5378e8;
box-shadow: 0 0 0 2px rgba(83, 120, 232, 0.3);
border-color: var(--df-accent);
box-shadow: 0 0 0 2px var(--df-accent-soft);
}
/* (G2):vue-shape markup body , attrs.body ,
改由依赖图把 isCycle 注入节点 data,组件根元素按类描边 */
.module-node--cycle {
border-color: #e05050;
box-shadow: 0 0 0 2px rgba(224, 80, 80, 0.3);
border-color: var(--df-danger);
box-shadow: 0 0 0 2px var(--df-danger-bg);
}
.module-node__header {
display: flex;
@@ -93,14 +93,14 @@ const stackList = computed(() => parseJsonArray(data.value?.stack).slice(0, 3))
.module-node__name {
font-size: 13px;
font-weight: 600;
color: #e0e0e0;
color: var(--df-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.module-node__path {
font-size: 10px;
color: #888;
color: var(--df-text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -114,7 +114,7 @@ const stackList = computed(() => parseJsonArray(data.value?.stack).slice(0, 3))
font-size: 9px;
padding: 1px 6px;
border-radius: 4px;
background: rgba(83, 120, 232, 0.15);
color: #7c9aff;
background: var(--df-accent-bg);
color: var(--df-accent);
}
</style>
+1
View File
@@ -10,6 +10,7 @@ export default {
// File tree
loading: 'Loading…',
emptyDir: 'Empty directory',
emptyDirMark: 'empty',
// File preview
selectFileHint: '← Select a file on the left to preview',
loadingFile: 'Loading file…',
+1
View File
@@ -10,6 +10,7 @@ export default {
// 文件树
loading: '加载中…',
emptyDir: '空目录',
emptyDirMark: '空',
// 文件预览
selectFileHint: '← 选择左侧文件查看预览',
loadingFile: '加载文件中…',
+1 -1
View File
@@ -1256,7 +1256,7 @@ onUnmounted(() => {
</style>
<style scoped>
.project-detail { padding: 16px 20px 20px; display: flex; flex-direction: column; min-height: 0; flex: 1; max-height: 100%; overflow: hidden; }
.project-detail { padding: 16px 20px 20px; display: flex; flex-direction: column; height: 100%; min-height: 0; max-height: 100%; overflow: hidden; }
/* ===== Tab 导航(Batch 10 文件浏览器) ===== */
.detail-tabs {