feat: Add project file tree viewer to Git page
Add Files tab with browsable project structure and file content viewer. New components: ProjectTree for navigation, FileViewer for content display. Backend endpoints: /api/git/tree and /api/git/file.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useGitApi } from '@/composables/git'
|
||||
import { DiffViewer, FileTree, CommitList, BranchSelector } from '@/components/git'
|
||||
import { DiffViewer, FileTree, CommitList, BranchSelector, ProjectTree, FileViewer } from '@/components/git'
|
||||
|
||||
type TabName = 'status' | 'history' | 'compare'
|
||||
type TabName = 'status' | 'history' | 'compare' | 'files'
|
||||
|
||||
const {
|
||||
status,
|
||||
@@ -13,6 +13,8 @@ const {
|
||||
diff,
|
||||
compareResult,
|
||||
selectedCommit,
|
||||
fileTree,
|
||||
fileContent,
|
||||
loading,
|
||||
error,
|
||||
fetchStatus,
|
||||
@@ -21,7 +23,11 @@ const {
|
||||
fetchCommit,
|
||||
fetchBranches,
|
||||
compare,
|
||||
clearCompare
|
||||
clearDiff,
|
||||
clearCompare,
|
||||
fetchFileTree,
|
||||
fetchFileContent,
|
||||
clearFileContent
|
||||
} = useGitApi()
|
||||
|
||||
const activeTab = ref<TabName>('status')
|
||||
@@ -33,6 +39,9 @@ const expandedFiles = ref<Set<string>>(new Set())
|
||||
const compareBase = ref('')
|
||||
const compareHead = ref('')
|
||||
|
||||
// Files tab state
|
||||
const selectedFilePath = ref<string | null>(null)
|
||||
|
||||
// Load initial data
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
@@ -51,6 +60,8 @@ watch(activeTab, async (tab) => {
|
||||
await fetchDiff()
|
||||
} else if (tab === 'history') {
|
||||
await fetchLog(30)
|
||||
} else if (tab === 'files') {
|
||||
await fetchFileTree()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -107,6 +118,18 @@ async function refresh() {
|
||||
await fetchDiff()
|
||||
}
|
||||
|
||||
// Handle file selection in files tab
|
||||
async function handleFileTreeSelect(path: string) {
|
||||
selectedFilePath.value = path
|
||||
await fetchFileContent(path)
|
||||
}
|
||||
|
||||
// Close file viewer
|
||||
function closeFileViewer() {
|
||||
selectedFilePath.value = null
|
||||
clearFileContent()
|
||||
}
|
||||
|
||||
// Total changes count
|
||||
const totalChanges = computed(() => {
|
||||
if (!status.value) return 0
|
||||
@@ -190,6 +213,15 @@ const totalChanges = computed(() => {
|
||||
</svg>
|
||||
Compare
|
||||
</button>
|
||||
<button
|
||||
:class="['tab', { active: activeTab === 'files' }]"
|
||||
@click="activeTab = 'files'"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
Files
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab content -->
|
||||
@@ -396,6 +428,37 @@ const totalChanges = computed(() => {
|
||||
<p>Selecciona dos ramas o commits para comparar</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Files Tab -->
|
||||
<div v-if="activeTab === 'files'" class="files-tab">
|
||||
<div class="files-layout">
|
||||
<div class="file-tree-panel">
|
||||
<div class="panel-header">
|
||||
<span>Project Files</span>
|
||||
<button class="refresh-tree-btn" @click="fetchFileTree" :disabled="loading" title="Refresh">
|
||||
<svg :class="{ spinning: loading }" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tree-container">
|
||||
<ProjectTree
|
||||
:nodes="fileTree"
|
||||
:selected-path="selectedFilePath"
|
||||
:loading="loading && !fileTree.length"
|
||||
@select="handleFileTreeSelect"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-content-panel">
|
||||
<FileViewer
|
||||
:file="fileContent"
|
||||
:loading="loading && selectedFilePath !== null"
|
||||
@close="closeFileViewer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
@@ -873,6 +936,78 @@ const totalChanges = computed(() => {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Files tab */
|
||||
.files-tab {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.files-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-tree-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.refresh-tree-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.refresh-tree-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.refresh-tree-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.refresh-tree-btn svg.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.file-content-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Error message */
|
||||
.error-message {
|
||||
padding: 0.75rem 1rem;
|
||||
@@ -893,6 +1028,10 @@ const totalChanges = computed(() => {
|
||||
.history-layout {
|
||||
grid-template-columns: 280px 1fr;
|
||||
}
|
||||
|
||||
.files-layout {
|
||||
grid-template-columns: 240px 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -1071,5 +1210,31 @@ const totalChanges = computed(() => {
|
||||
padding: 0.5rem;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.files-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-tree-panel {
|
||||
max-height: 250px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.file-content-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user