|
<script setup lang="ts"> |
|
import { type TableProps } from 'tdesign-vue-next'; |
|
import { computed, ref, watch, onMounted } from 'vue'; |
|
import { useRoute, useRouter } from 'vue-router'; |
|
import { MessagePlugin } from 'tdesign-vue-next'; |
|
import { useAccountStore } from '../stores/accountStorage'; |
|
import { repoApi, type RepoContent, type Account } from '../services/repoApi'; |
|
import { FolderIcon, FileIcon, CodeIcon, ImageIcon } from 'tdesign-icons-vue-next'; |
|
import RepoHeader from '../components/RepoHeader.vue'; |
|
|
|
const router = useRouter(); |
|
const route = useRoute(); |
|
|
|
|
|
const loading = ref(false); |
|
const allFiles = ref<RepoContent[]>([]); |
|
const selectedAccount = ref<number>(0); |
|
const currentPath = ref(''); |
|
const store = useAccountStore(); |
|
|
|
|
|
const sortedFiles = computed(() => { |
|
|
|
const dirs = allFiles.value.filter(item => item.type === 'dir'); |
|
const files = allFiles.value.filter(item => item.type === 'file'); |
|
|
|
|
|
dirs.sort((a, b) => a.name.localeCompare(b.name)); |
|
files.sort((a, b) => a.name.localeCompare(b.name)); |
|
|
|
|
|
return [...dirs, ...files]; |
|
}); |
|
|
|
|
|
const getFileIcon = (filename: string) => { |
|
const extension = filename.split('.').pop()?.toLowerCase(); |
|
|
|
if (!extension) return FileIcon; |
|
|
|
const codeExtensions = ['js', 'ts', 'py', 'java', 'c', 'cpp', 'cs', 'go', 'php', 'html', 'css', 'vue', 'jsx', 'tsx']; |
|
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp']; |
|
const textExtensions = ['txt', 'md', 'json', 'xml', 'csv', 'yml', 'yaml']; |
|
|
|
if (codeExtensions.includes(extension)) return CodeIcon; |
|
if (imageExtensions.includes(extension)) return ImageIcon; |
|
|
|
|
|
|
|
return FileIcon; |
|
}; |
|
|
|
const columns = ref<TableProps['columns']>([ |
|
{ |
|
colKey: 'name', |
|
title: '文件名', |
|
width: 300, |
|
cell: (h, { row }) => { |
|
const IconComponent = row.type === 'dir' ? FolderIcon : getFileIcon(row.name); |
|
return h('div', { class: 'flex items-center gap-2' }, [ |
|
h(IconComponent, { |
|
class: row.type === 'dir' ? 'text-blue-500' : 'text-gray-600', |
|
style: { fontSize: '1.25rem' } |
|
}), |
|
h('span', row.name) |
|
]); |
|
} |
|
}, |
|
{ colKey: 'type', title: '类型', width: 100 }, |
|
{ |
|
colKey: 'size', |
|
title: '大小', |
|
width: 100, |
|
cell: (h, { row }) => { |
|
if (row.type === 'dir') return '-'; |
|
|
|
const size = Number(row.size); |
|
if (size < 1024) return `${size} B`; |
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; |
|
return `${(size / (1024 * 1024)).toFixed(1)} MB`; |
|
} |
|
}, |
|
{ colKey: 'sha', title: 'SHA', width: 200 } |
|
]); |
|
|
|
const handleRowClick = (e: any) => { |
|
const item: RepoContent = e.row; |
|
if (item.type === 'dir') { |
|
currentPath.value = item.path; |
|
fetchRepo(); |
|
} else if (item.type === 'file') { |
|
router.push({ |
|
path: '/content', |
|
query: { |
|
id: selectedAccount.value, |
|
path: item.path |
|
} |
|
}); |
|
} |
|
}; |
|
|
|
const fetchRepo = async () => { |
|
if (!selectedAccount.value) return; |
|
|
|
loading.value = true; |
|
try { |
|
const account = store.accounts.find(acc => acc.id === selectedAccount.value); |
|
if (!account) { |
|
MessagePlugin.error(`未找到账户信息${selectedAccount.value}`); |
|
return; |
|
} |
|
const result = await repoApi.getContents(account, currentPath.value); |
|
allFiles.value = Array.isArray(result) ? result : [result]; |
|
} catch (error) { |
|
MessagePlugin.error('获取仓库内容失败'); |
|
} finally { |
|
loading.value = false; |
|
} |
|
}; |
|
|
|
const handleBreadcrumbClick = (path: string) => { |
|
currentPath.value = path; |
|
fetchRepo(); |
|
}; |
|
|
|
const handleRootClick = () => { |
|
currentPath.value = ''; |
|
fetchRepo(); |
|
}; |
|
|
|
const handleAccountChange = (val: number) => { |
|
selectedAccount.value = val; |
|
currentPath.value = ''; |
|
router.push({ |
|
path: '/repo', |
|
query: { |
|
id: selectedAccount.value, |
|
} |
|
}); |
|
} |
|
|
|
|
|
watch(() => route.query, async (query) => { |
|
|
|
if (route.path !== '/repo') return; |
|
await store.fetchAccounts(); |
|
const { id, path } = query; |
|
|
|
if (id) { |
|
|
|
selectedAccount.value = Number(id); |
|
currentPath.value = path ? path as string : ''; |
|
} |
|
else { |
|
|
|
if (store.accounts.length > 0) { |
|
selectedAccount.value = store.accounts[0].id; |
|
} |
|
currentPath.value = ''; |
|
} |
|
|
|
await fetchRepo(); |
|
}, { immediate: true }); |
|
|
|
|
|
onMounted(async () => { |
|
|
|
}); |
|
|
|
|
|
|
|
const handleNewFile = () => { |
|
if (!selectedAccount.value) { |
|
MessagePlugin.error('请先选择仓库'); |
|
return; |
|
} |
|
let path = currentPath.value; |
|
router.push({ |
|
path: '/content', |
|
query: { |
|
id: selectedAccount.value, |
|
path: path, |
|
newFile: 1, |
|
} |
|
}); |
|
}; |
|
</script> |
|
|
|
<template> |
|
<div class="repository-browser w-full flex flex-col p-3 md:p-5 gap-3 md:gap-5 bg-gray-50"> |
|
<RepoHeader :selected-account="selectedAccount" :current-path="currentPath" :accounts="store.accounts" |
|
@path-click="handleBreadcrumbClick" @root-click="handleRootClick" |
|
@update:selected-account="handleAccountChange"> |
|
<div class="flex gap-2 items-center"> |
|
<t-button theme="primary" @click="handleNewFile" class="flex items-center gap-1"> |
|
<template #icon> |
|
<FileIcon /> |
|
</template> |
|
新建文件 |
|
</t-button> |
|
</div> |
|
</RepoHeader> |
|
|
|
<div class="content-section flex-1 bg-white rounded-lg shadow-sm"> |
|
<t-table :data="sortedFiles" :loading="loading" :columns="columns" row-key="sha" hover stripe size="medium" |
|
class="min-w-full" @row-click="handleRowClick" row-class-name="hover:bg-blue-50 cursor-pointer" /> |
|
</div> |
|
</div> |
|
</template> |
|
|
|
<style scoped> |
|
.repository-browser { |
|
min-height: 600px; |
|
} |
|
|
|
|
|
.t-table__row:hover { |
|
transition: all 0.2s ease; |
|
} |
|
|
|
|
|
@media (max-width: 640px) { |
|
.t-table { |
|
font-size: 0.875rem; |
|
} |
|
} |
|
</style> |
|
|