fix: create component StorageFileBrowser
This commit is contained in:
2
bo/components.d.ts
vendored
2
bo/components.d.ts
vendored
@@ -40,6 +40,7 @@ declare module 'vue' {
|
|||||||
Profile: typeof import('./src/components/customer-management/Profile.vue')['default']
|
Profile: typeof import('./src/components/customer-management/Profile.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
StorageFileBrowser: typeof import('./src/components/customer/StorageFileBrowser.vue')['default']
|
||||||
ThemeSwitch: typeof import('./src/components/inner/ThemeSwitch.vue')['default']
|
ThemeSwitch: typeof import('./src/components/inner/ThemeSwitch.vue')['default']
|
||||||
TopBar: typeof import('./src/components/TopBar.vue')['default']
|
TopBar: typeof import('./src/components/TopBar.vue')['default']
|
||||||
TopBarLogin: typeof import('./src/components/TopBarLogin.vue')['default']
|
TopBarLogin: typeof import('./src/components/TopBarLogin.vue')['default']
|
||||||
@@ -68,5 +69,6 @@ declare module 'vue' {
|
|||||||
UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default']
|
UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default']
|
||||||
UTabs: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
UTabs: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
||||||
UTextarea: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
|
UTextarea: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
|
||||||
|
UTree: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tree.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
159
bo/src/components/customer/StorageFileBrowser.vue
Normal file
159
bo/src/components/customer/StorageFileBrowser.vue
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="Default || 'div'">
|
||||||
|
<div class="p-4">
|
||||||
|
<div v-if="loading" class="flex justify-center py-8">
|
||||||
|
<ULoader />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error" class="text-red-500">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<UTree v-else :items="treeItems" :expanded="expandedFolders">
|
||||||
|
<template #item-wrapper="{ item }">
|
||||||
|
<div class="flex items-start cursor-pointer" @click="onItemClick(item)">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<UIcon :name="item.icon" :size="30" />
|
||||||
|
<div class="flex gap-1 items-center">
|
||||||
|
<span class="text-[15px] font-medium">{{ item.label }}</span>
|
||||||
|
<UButton v-if="!item.isFolder && item.fileName" size="xxs" color="neutral"
|
||||||
|
variant="outline" icon="i-lucide-download"
|
||||||
|
@click.stop="downloadFile(item.path, item.fileName)" :ui="{ base: 'ring-0!' }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTree>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useFetchJson } from '@/composable/useFetchJson'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
|
|
||||||
|
interface FileItemRaw {
|
||||||
|
Name: string
|
||||||
|
IsFolder: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileItem {
|
||||||
|
name: string
|
||||||
|
type: 'file' | 'folder'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TreeItem {
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
children?: TreeItem[]
|
||||||
|
isFolder: boolean
|
||||||
|
path: string
|
||||||
|
value: string
|
||||||
|
fileName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{ initialPath?: string }>()
|
||||||
|
|
||||||
|
const currentPath = ref(props.initialPath || '')
|
||||||
|
const allData = ref<Map<string, FileItem[]>>(new Map())
|
||||||
|
const expandedFolders = ref<string[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
|
||||||
|
async function fetchFolderContents(path: string): Promise<FileItem[]> {
|
||||||
|
const url = `/api/v1/restricted/storage/list-content/${path}`
|
||||||
|
const data = await useFetchJson<FileItemRaw[]>(url)
|
||||||
|
return (data.items || []).map(i => ({
|
||||||
|
name: i.Name,
|
||||||
|
type: i.IsFolder ? 'folder' : 'file'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function loadFolder(path: string) {
|
||||||
|
if (allData.value.has(path)) return
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const items = await fetchFolderContents(path)
|
||||||
|
allData.value.set(path, items)
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof Error ? e.message : 'Failed to load folder contents'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function toggleFolder(item: TreeItem) {
|
||||||
|
if (!item.isFolder) return
|
||||||
|
|
||||||
|
if (!expandedFolders.value.includes(item.value)) {
|
||||||
|
expandedFolders.value.push(item.value)
|
||||||
|
await loadFolder(item.value)
|
||||||
|
} else {
|
||||||
|
expandedFolders.value = expandedFolders.value.filter(v => v !== item.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onItemClick(item: TreeItem) {
|
||||||
|
if (item.isFolder) {
|
||||||
|
toggleFolder(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function buildTreeItems(items: FileItem[], path: string): TreeItem[] {
|
||||||
|
return items.map(item => {
|
||||||
|
const itemPath = path ? `${path}/${item.name}` : item.name
|
||||||
|
const isFolder = item.type === 'folder'
|
||||||
|
|
||||||
|
const children = isFolder
|
||||||
|
? buildTreeItems(allData.value.get(itemPath) || [], itemPath)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: item.name,
|
||||||
|
|
||||||
|
icon: isFolder ? 'fxemoji:folder' : 'flat-color-icons:file',
|
||||||
|
isFolder,
|
||||||
|
path: isFolder ? itemPath : path,
|
||||||
|
value: itemPath,
|
||||||
|
fileName: isFolder ? undefined : item.name,
|
||||||
|
children
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFile(path: string, fileName: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/v1/restricted/storage/download-file/${path}/${fileName}`)
|
||||||
|
if (!response.ok) throw new Error('Failed to download file')
|
||||||
|
|
||||||
|
const blob = await response.blob()
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = fileName
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Download error:', e)
|
||||||
|
alert('Failed to download file')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeItems = computed<TreeItem[]>(() => {
|
||||||
|
const items = allData.value.get(currentPath.value) || []
|
||||||
|
return buildTreeItems(items, currentPath.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
loadFolder(currentPath.value).then(() => {
|
||||||
|
const rootItems = treeItems.value
|
||||||
|
if (rootItems.length > 0 && rootItems[0].isFolder) {
|
||||||
|
toggleFolder(rootItems[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<UDropdownMenu :items="userItems" :content="{ align: 'center', collisionPadding: 12 }"
|
<UDropdownMenu :items="userItems" :content="{ align: 'center', collisionPadding: 12 }"
|
||||||
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
|
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
|
||||||
<UButton v-bind="userStore.user" :label="userStore.user?.email" trailing-icon="i-lucide-chevrons-up-down" color="neutral"
|
<UButton v-bind="userStore.user" :label="userStore.user?.email" trailing-icon="i-lucide-chevrons-up-down"
|
||||||
variant="ghost" square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
|
color="neutral" variant="ghost" square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
|
||||||
trailingIcon: 'text-dimmed ms-auto'
|
trailingIcon: 'text-dimmed ms-auto'
|
||||||
}" />
|
}" />
|
||||||
</UDropdownMenu>
|
</UDropdownMenu>
|
||||||
@@ -34,8 +34,12 @@
|
|||||||
|
|
||||||
<div class="flex-1 flex flex-col">
|
<div class="flex-1 flex flex-col">
|
||||||
<div class="flex h-(--ui-header-height) shrink-0 items-center justify-between px-4 border-b border-default">
|
<div class="flex h-(--ui-header-height) shrink-0 items-center justify-between px-4 border-b border-default">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
|
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
|
||||||
@click="open = !open" />
|
@click="open = !open" />
|
||||||
|
<span class="text-[20px] font-medium">{{ pageTitle }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="hidden md:flex items-center gap-12">
|
<div class="hidden md:flex items-center gap-12">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<CountryCurrencySwitch />
|
<CountryCurrencySwitch />
|
||||||
@@ -44,8 +48,9 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
|
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
|
||||||
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark) whitespace-nowrap">
|
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark) whitespace-nowrap">
|
||||||
{{ $t('general.logout') }}
|
{{ $t('general.logout') }}
|
||||||
|
<UIcon name="ic:baseline-logout" class="text-red-700 dark:text-red-500"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,6 +73,8 @@ import { useAuthStore } from '../stores/customer/auth'
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const pageTitle = computed(() => route.meta.name ?? 'Default Page')
|
||||||
await userStore.getUser()
|
await userStore.getUser()
|
||||||
|
|
||||||
const open = ref(true)
|
const open = ref(true)
|
||||||
@@ -157,7 +164,7 @@ function getItems(state: 'collapsed' | 'expanded') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { currentLang } from '@/router/langs'
|
import { currentLang } from '@/router/langs'
|
||||||
import { useFetchJson } from '@/composable/useFetchJson'
|
import { useFetchJson } from '@/composable/useFetchJson'
|
||||||
import CountryCurrencySwitch from '@/components/inner/CountryCurrencySwitch.vue'
|
import CountryCurrencySwitch from '@/components/inner/CountryCurrencySwitch.vue'
|
||||||
|
|||||||
@@ -38,7 +38,8 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
|
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
|
||||||
@click="open = !open" />
|
@click="open = !open" />
|
||||||
<p class="font-bold text-xl">Customer-Management: <span class="text-[20px] font-medium">{{ pageTitle }}</span></p>
|
<p class="font-bold text-xl">Customer-Management: <span class="text-[20px] font-medium">{{ pageTitle
|
||||||
|
}}</span></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:flex items-center gap-12">
|
<div class="hidden md:flex items-center gap-12">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -48,8 +49,9 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
|
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
|
||||||
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark) whitespace-nowrap">
|
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark) whitespace-nowrap">
|
||||||
{{ $t('general.logout') }}
|
{{ $t('general.logout') }}
|
||||||
|
<UIcon name="ic:baseline-logout" class="text-red-700 dark:text-red-500" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
12
bo/src/views/StorageView.vue
Normal file
12
bo/src/views/StorageView.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<Default>
|
||||||
|
<div class="h-full">
|
||||||
|
<StorageFileBrowser initial-path="dest/src" />
|
||||||
|
</div>
|
||||||
|
</Default>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
|
import StorageFileBrowser from '@/components/customer/StorageFileBrowser.vue'
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user