refactor(frontend): extract shared modules, thin views, delete dead code
Shared utilities: - Add src/utils/formatters.js — formatDate, formatSize, providerColor, providerBg, providerLabel; all components import from here, no inline duplicates - Add src/components/ui/TreeItem.vue — generic expand/collapse tree node; FolderTreeItem, CloudFolderTreeItem, CloudProviderTreeItem now wrap it - Add src/components/storage/StorageBrowser.vue — unified file browser grid used by both FileManagerView and CloudFolderView View refactor (thin data-providers): - FileManagerView.vue: stripped to props + event wiring; all layout moved to StorageBrowser - CloudFolderView.vue: same treatment — feeds props into StorageBrowser - All tree sidebar components delegate expand/collapse to TreeItem.vue Dead code removed: - Delete HomeView.vue — no active route, replaced by FileManagerView - Delete FolderView.vue — no active route, logic merged into FileManagerView Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
a548266461
commit
cce70b2ef6
@@ -1,113 +1,26 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="sticky top-0 z-10 bg-white border-b border-gray-100">
|
||||
<div class="px-6 py-3 flex items-center gap-3">
|
||||
<button
|
||||
@click="goUp"
|
||||
class="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-800 transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back
|
||||
</button>
|
||||
<span class="text-gray-300">|</span>
|
||||
<span class="text-sm text-gray-400 capitalize">{{ providerLabel }}</span>
|
||||
<svg class="w-3 h-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-700 truncate">{{ folderName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto px-6 py-5">
|
||||
|
||||
<!-- Upload zone -->
|
||||
<div class="mb-5">
|
||||
<DropZone @files-selected="onFilesSelected" />
|
||||
<UploadProgress :items="uploadQueue" />
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="text-sm text-gray-400 py-8 text-center">Loading…</div>
|
||||
|
||||
<div v-else-if="error" class="text-sm text-red-500 py-8 text-center">
|
||||
<p>{{ error }}</p>
|
||||
<div class="flex items-center justify-center gap-3 mt-2">
|
||||
<router-link
|
||||
to="/settings"
|
||||
class="text-indigo-600 hover:underline text-sm"
|
||||
>
|
||||
Go to Settings
|
||||
</router-link>
|
||||
<button @click="load" class="text-indigo-600 hover:underline text-sm">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
|
||||
<!-- Column headers -->
|
||||
<div class="px-4 py-2 grid grid-cols-[2rem_1fr_6rem] gap-3 items-center rounded-lg bg-gray-50 text-xs font-semibold text-gray-400 uppercase tracking-wider select-none mb-1">
|
||||
<span></span>
|
||||
<span>Name</span>
|
||||
<span class="text-right hidden md:block">Size</span>
|
||||
</div>
|
||||
|
||||
<div v-if="items.length === 0" class="text-sm text-gray-400 py-10 text-center">
|
||||
This folder is empty.
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col divide-y divide-gray-100 border border-gray-100 rounded-xl overflow-hidden">
|
||||
|
||||
<!-- Folder rows -->
|
||||
<div
|
||||
v-for="item in folders"
|
||||
:key="item.id"
|
||||
class="px-4 py-2.5 grid grid-cols-[2rem_1fr_6rem] gap-3 items-center hover:bg-gray-50 group cursor-pointer transition-colors"
|
||||
@click="navigateTo(item)"
|
||||
>
|
||||
<div class="w-7 h-7 bg-amber-50 rounded-lg flex items-center justify-center shrink-0">
|
||||
<svg class="w-4 h-4 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900 truncate">{{ item.name }}</span>
|
||||
<span class="text-right text-xs text-gray-400 hidden md:block">—</span>
|
||||
</div>
|
||||
|
||||
<!-- File rows -->
|
||||
<div
|
||||
v-for="item in files"
|
||||
:key="item.id"
|
||||
class="px-4 py-2.5 grid grid-cols-[2rem_1fr_6rem] gap-3 items-center hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div class="w-7 h-7 bg-indigo-50 rounded-lg flex items-center justify-center shrink-0">
|
||||
<svg class="w-4 h-4 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700 truncate">{{ item.name }}</span>
|
||||
<span class="text-right text-xs text-gray-400 hidden md:block">{{ formatSize(item.size) }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<StorageBrowser
|
||||
mode="cloud"
|
||||
:folders="folders"
|
||||
:files="files"
|
||||
:breadcrumb="breadcrumb"
|
||||
:upload-queue="uploadQueue"
|
||||
:loading="loading"
|
||||
:empty-message="error || 'This folder is empty'"
|
||||
:empty-hint="error ? '' : 'Files stored here are managed by ' + providerLabel(provider)"
|
||||
@breadcrumb-navigate="handleBreadcrumbNavigate"
|
||||
@upload="onFilesSelected"
|
||||
@folder-navigate="item => navigateTo(item)"
|
||||
@file-open="file => {}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, reactive } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import * as api from '../api/client.js'
|
||||
import DropZone from '../components/upload/DropZone.vue'
|
||||
import UploadProgress from '../components/upload/UploadProgress.vue'
|
||||
import StorageBrowser from '../components/storage/StorageBrowser.vue'
|
||||
import { providerLabel } from '../utils/formatters.js'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -122,15 +35,14 @@ const folderId = computed(() => route.params.folderId)
|
||||
const folders = computed(() => items.value.filter(i => i.is_dir))
|
||||
const files = computed(() => items.value.filter(i => !i.is_dir))
|
||||
|
||||
const providerLabel = computed(() => {
|
||||
const map = { google_drive: 'Google Drive', onedrive: 'OneDrive', nextcloud: 'Nextcloud', webdav: 'WebDAV' }
|
||||
return map[provider.value] ?? provider.value
|
||||
})
|
||||
|
||||
const folderName = computed(() => {
|
||||
const id = folderId.value ?? ''
|
||||
const parts = id.replace(/\/$/, '').split('/')
|
||||
return parts[parts.length - 1] || providerLabel.value
|
||||
/** Breadcrumb built from the folder path segments. */
|
||||
const breadcrumb = computed(() => {
|
||||
if (!folderId.value || folderId.value === 'root') return []
|
||||
const parts = folderId.value.replace(/\/$/, '').split('/')
|
||||
return parts.map((seg, idx) => ({
|
||||
id: parts.slice(0, idx + 1).join('/'),
|
||||
name: seg,
|
||||
}))
|
||||
})
|
||||
|
||||
async function load() {
|
||||
@@ -155,22 +67,17 @@ function navigateTo(item) {
|
||||
router.push(`/cloud/${provider.value}/${item.id}`)
|
||||
}
|
||||
|
||||
function goUp() {
|
||||
const id = (folderId.value ?? '').replace(/\/$/, '')
|
||||
const lastSlash = id.lastIndexOf('/')
|
||||
if (lastSlash > 0) {
|
||||
router.push(`/cloud/${provider.value}/${id.slice(0, lastSlash + 1)}`)
|
||||
} else {
|
||||
router.back()
|
||||
}
|
||||
function handleBreadcrumbNavigate(id) {
|
||||
if (id == null) router.push(`/cloud/${provider.value}/root`)
|
||||
else router.push(`/cloud/${provider.value}/${id}`)
|
||||
}
|
||||
|
||||
// ── Upload ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const uploadQueue = ref([])
|
||||
|
||||
async function onFilesSelected({ files }) {
|
||||
const promises = files.map(file => {
|
||||
async function onFilesSelected({ files: selectedFiles }) {
|
||||
const promises = selectedFiles.map(file => {
|
||||
const item = reactive({ name: file.name, done: false, error: null, status: 'Uploading…' })
|
||||
uploadQueue.value.unshift(item)
|
||||
return api.uploadToCloud(file, provider.value, folderId.value || null)
|
||||
@@ -181,13 +88,6 @@ async function onFilesSelected({ files }) {
|
||||
await load()
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (!bytes) return '—'
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
watch([provider, folderId], load)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user