新增: 文件浏览器收尾(滚动条/阶段进度条/空状态/骨架屏/工程下拉)
- 提交之前未提交的滚动条与布局修复 - 移除阶段进度条死代码(模板+CSS+常量) - 文件预览空状态加图标引导 - 变更视图加载骨架屏 - 工程选择器改为自定义下拉(显示工程路径)
This commit is contained in:
@@ -9,14 +9,24 @@
|
||||
<div class="explorer-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<!-- 多工程下拉(单工程隐藏) -->
|
||||
<select
|
||||
v-if="modules.length > 1"
|
||||
class="module-select"
|
||||
:value="currentModuleId"
|
||||
@change="onModuleChange"
|
||||
>
|
||||
<option v-for="m in modules" :key="m.id" :value="m.id">{{ m.name }}</option>
|
||||
</select>
|
||||
<div v-if="modules.length > 1" class="module-dropdown">
|
||||
<button class="module-select" @click="moduleDropdownOpen = !moduleDropdownOpen">
|
||||
<span class="module-select-name">{{ currentModule?.name ?? '...' }}</span>
|
||||
<span class="module-select-arrow" :class="{ open: moduleDropdownOpen }">▾</span>
|
||||
</button>
|
||||
<div v-if="moduleDropdownOpen" class="module-dropdown-menu" @click.stop>
|
||||
<div
|
||||
v-for="m in modules"
|
||||
:key="m.id"
|
||||
class="module-dropdown-item"
|
||||
:class="{ active: m.id === currentModuleId }"
|
||||
@click="onModuleSelect(m.id)"
|
||||
>
|
||||
<span class="module-dropdown-name">{{ m.name }}</span>
|
||||
<span v-if="m.path" class="module-dropdown-path" :title="m.path">{{ m.path }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 路径面包屑 -->
|
||||
<nav class="breadcrumb">
|
||||
@@ -160,6 +170,7 @@ const { t } = useI18n()
|
||||
const modules = ref<ProjectModuleRecord[]>([])
|
||||
const currentModuleId = ref<string>('')
|
||||
const refreshing = ref(false)
|
||||
const moduleDropdownOpen = ref(false)
|
||||
|
||||
// 新增工程弹窗
|
||||
const showAddModule = ref(false)
|
||||
@@ -216,10 +227,10 @@ function resetTreeState() {
|
||||
selectedFileGitStatus.value = undefined
|
||||
}
|
||||
|
||||
/** 切换工程(下拉变更)。 */
|
||||
function onModuleChange(e: Event) {
|
||||
const target = e.target as HTMLSelectElement
|
||||
currentModuleId.value = target.value
|
||||
/** 自定义下拉选择工程。 */
|
||||
function onModuleSelect(id: string) {
|
||||
currentModuleId.value = id
|
||||
moduleDropdownOpen.value = false
|
||||
resetTreeState()
|
||||
}
|
||||
|
||||
@@ -310,6 +321,9 @@ function onChangeFileSelect(path: string) {
|
||||
|
||||
watch(() => props.projectId, loadModules, { immediate: true })
|
||||
|
||||
// 点击外部关闭工程下拉
|
||||
document.addEventListener('click', () => { moduleDropdownOpen.value = false })
|
||||
|
||||
/** 提交新增工程。 */
|
||||
async function onAddModule() {
|
||||
if (addingModule.value) return
|
||||
@@ -340,6 +354,7 @@ async function onAddModule() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg, 8px);
|
||||
@@ -367,6 +382,9 @@ async function onAddModule() {
|
||||
}
|
||||
|
||||
.module-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
@@ -377,9 +395,54 @@ async function onAddModule() {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.module-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--df-accent);
|
||||
.module-select:hover { border-color: var(--df-accent); }
|
||||
.module-select-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.module-select-arrow {
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.module-select-arrow.open { transform: rotate(180deg); }
|
||||
|
||||
.module-dropdown { position: relative; flex-shrink: 0; }
|
||||
|
||||
.module-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
min-width: 220px;
|
||||
max-width: 320px;
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm, 4px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
|
||||
z-index: 20;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.module-dropdown-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 0.5px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
.module-dropdown-item:last-child { border-bottom: none; }
|
||||
.module-dropdown-item:hover { background: rgba(255,255,255,0.04); }
|
||||
.module-dropdown-item.active {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-left: 2px solid var(--df-accent);
|
||||
}
|
||||
.module-dropdown-name { font-size: 12px; color: var(--df-text); }
|
||||
.module-dropdown-path {
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 面包屑 */
|
||||
@@ -458,6 +521,7 @@ async function onAddModule() {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 左侧边栏(视图切换 + 内容) */
|
||||
@@ -564,8 +628,8 @@ async function onAddModule() {
|
||||
.explorer-preview {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.preview-placeholder-inline {
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
<div class="preview-body">
|
||||
<!-- 未选文件占位 -->
|
||||
<div v-if="!filePath" class="preview-placeholder">
|
||||
{{ $t('fileExplorer.selectFileHint') }}
|
||||
<div class="preview-placeholder-icon">📄</div>
|
||||
<div class="preview-placeholder-text">{{ $t('fileExplorer.selectFileHint') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
@@ -75,9 +76,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 文本/代码(highlight.js 语法高亮 + 行号;非 diff 模式时显示) -->
|
||||
<div v-else class="preview-code-wrap">
|
||||
<div class="preview-line-numbers" ref="lineNumRef">
|
||||
<div v-for="n in lineCount" :key="n" class="preview-line-num" :class="{ 'is-active': n === activeLine }">{{ n }}</div>
|
||||
<div v-else class="preview-code-scroll">
|
||||
<div class="preview-line-numbers" aria-hidden="true">
|
||||
<div v-for="n in lineCount" :key="n" class="preview-line-num">{{ n }}</div>
|
||||
</div>
|
||||
<pre class="preview-code"><code :class="hljsClass" v-html="htmlContent"></code></pre>
|
||||
</div>
|
||||
@@ -116,10 +117,6 @@ const lineCount = computed(() => {
|
||||
return lines.length
|
||||
})
|
||||
|
||||
/** 当前激活行(预留:后续可点击行号跳转/选中高亮)。 */
|
||||
const activeLine = ref<number | null>(null)
|
||||
const lineNumRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** Diff 显示控制 */
|
||||
const showDiff = ref(false)
|
||||
const diffContent = ref('')
|
||||
@@ -398,6 +395,15 @@ onUnmounted(() => {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.preview-placeholder-icon {
|
||||
font-size: 48px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.preview-placeholder-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.preview-error { color: #e05050; }
|
||||
.binary-icon { font-size: 32px; opacity: 0.5; }
|
||||
|
||||
@@ -423,24 +429,22 @@ onUnmounted(() => {
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
overflow: visible;
|
||||
tab-size: 2;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.preview-code code { font-family: inherit; }
|
||||
|
||||
/* 行号 + 代码并排容器 */
|
||||
.preview-code-wrap {
|
||||
/* 行号 + 代码共享同一个滚动容器 */
|
||||
.preview-code-scroll {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex: 1 1 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 行号列 */
|
||||
/* 行号列 — sticky 固定在左侧,跟随滚动容器一起滚 */
|
||||
.preview-line-numbers {
|
||||
flex-shrink: 0;
|
||||
padding: 14px 0;
|
||||
@@ -449,13 +453,16 @@ onUnmounted(() => {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-right: 0.5px solid var(--df-border);
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.preview-line-num {
|
||||
padding: 0 10px 0 8px;
|
||||
font-family: var(--df-font-mono, 'Cascadia Code', Consolas, monospace);
|
||||
font-size: 12px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
color: var(--df-text-dim);
|
||||
opacity: 0.5;
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
<span v-if="!showHistory" class="branch-count">{{ gitStatus?.changed_files.length ?? 0 }} {{ $t('gitChanges.changedFiles') }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !gitStatus" class="gc-loading"><span class="spinner"></span></div>
|
||||
<div v-if="loading && !gitStatus" class="gc-skeleton">
|
||||
<div class="gc-skeleton-row" v-for="i in 6" :key="i"></div>
|
||||
</div>
|
||||
|
||||
<!-- 非 Git 仓库 -->
|
||||
<div v-else-if="gitStatus && !gitStatus.is_git_repo" class="gc-empty">
|
||||
@@ -474,6 +476,25 @@ watch(() => props.moduleId, loadStatus, { immediate: true })
|
||||
}
|
||||
.gc-load-more:hover { background: rgba(255,255,255,0.04); }
|
||||
|
||||
/* 骨架屏 */
|
||||
.gc-skeleton {
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.gc-skeleton-row {
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(90deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.04) 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: gc-shimmer 1.5s infinite linear;
|
||||
}
|
||||
@keyframes gc-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* 通用 */
|
||||
.gc-loading, .gc-empty {
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
|
||||
@@ -38,27 +38,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阶段进度条(暂时隐藏:阶段流转逻辑未接通,后续接入真实状态机后启用) -->
|
||||
<!--
|
||||
<div class="stage-pipeline">
|
||||
<div
|
||||
v-for="(stage, idx) in stages"
|
||||
:key="stage.key"
|
||||
class="stage-step"
|
||||
:class="{
|
||||
'stage-active': idx === currentStageIndex,
|
||||
'stage-done': idx < currentStageIndex,
|
||||
}"
|
||||
>
|
||||
<div class="stage-dot">
|
||||
<span v-if="idx < currentStageIndex">✓</span>
|
||||
<span v-else>{{ idx + 1 }}</span>
|
||||
</div>
|
||||
<div class="stage-label">{{ $t('projectDetail.' + stage.labelKey) }}</div>
|
||||
<div v-if="idx < stages.length - 1" class="stage-connector" :class="{ 'connector-done': idx < currentStageIndex }"></div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<!-- 阶段进度条已移除(阶段流转逻辑未接通,后续接入真实状态机后重启) -->
|
||||
|
||||
<!-- Tab 导航(概览 / 文件浏览器) -->
|
||||
<nav class="detail-tabs">
|
||||
@@ -689,7 +669,7 @@ onUnmounted(() => {
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.project-detail { padding: 16px 20px 20px; display: flex; flex-direction: column; min-height: 0; flex: 1; }
|
||||
.project-detail { padding: 16px 20px 20px; display: flex; flex-direction: column; min-height: 0; flex: 1; max-height: 100%; overflow: hidden; }
|
||||
|
||||
/* ===== Tab 导航(Batch 10 文件浏览器) ===== */
|
||||
.detail-tabs {
|
||||
@@ -766,72 +746,15 @@ onUnmounted(() => {
|
||||
.btn-danger { background: transparent; color: var(--df-danger); border: 0.5px solid rgba(240,101,101,0.4); }
|
||||
.btn-danger:hover { background: rgba(240,101,101,0.12); }
|
||||
|
||||
/* ===== 阶段进度条 ===== */
|
||||
.stage-pipeline {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
padding: var(--df-pad-panel);
|
||||
margin-bottom: var(--df-gap-page);
|
||||
}
|
||||
.stage-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
.stage-dot {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
background: var(--df-border);
|
||||
color: var(--df-text-dim);
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.stage-active .stage-dot {
|
||||
background: var(--df-accent);
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
.stage-done .stage-dot {
|
||||
background: var(--df-success);
|
||||
color: var(--df-bg);
|
||||
}
|
||||
.stage-label {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.stage-active .stage-label { color: var(--df-accent); font-weight: 500; }
|
||||
.stage-done .stage-label { color: var(--df-success); }
|
||||
|
||||
.stage-connector {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: calc(50% + 16px);
|
||||
width: calc(100% - 32px);
|
||||
height: 2px;
|
||||
background: var(--df-border);
|
||||
}
|
||||
.connector-done { background: var(--df-success); }
|
||||
|
||||
/* ===== 两栏布局 ===== */
|
||||
/* ===== 两栏布局 — 自己滚动,不撑大父级 ===== */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 380px;
|
||||
gap: var(--df-gap-grid);
|
||||
gap: var(--df-gap-page);
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
align-content: start;
|
||||
}
|
||||
.right-column { display: flex; flex-direction: column; gap: var(--df-gap-grid); }
|
||||
|
||||
@@ -949,6 +872,5 @@ onUnmounted(() => {
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.stage-pipeline { overflow-x: auto; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user