File management

This commit is contained in:
2025-07-27 00:29:59 +08:00
parent 02af78ca99
commit 46612b28aa
6 changed files with 250 additions and 13 deletions

View File

@@ -16,7 +16,7 @@
</template>
<script setup lang="ts">
import { DataUsageRound } from '@vicons/material'
import { DataUsageRound, AllInboxFilled } from '@vicons/material'
import { NIcon, NLayout, NLayoutSider, NMenu, type MenuOption } from 'naive-ui'
import { h, type Component } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
@@ -34,6 +34,11 @@ const menuOptions: MenuOption[] = [
key: 'dashboardUsage',
icon: renderIcon(DataUsageRound),
},
{
label: 'Files',
key: 'dashboardFiles',
icon: renderIcon(AllInboxFilled),
}
]
function updateMenuSelect(key: string) {

View File

@@ -2,7 +2,15 @@
import LayoutDefault from './layouts/default.vue'
import { RouterView } from 'vue-router'
import { NGlobalStyle, NConfigProvider, NMessageProvider, lightTheme, darkTheme } from 'naive-ui'
import {
NGlobalStyle,
NConfigProvider,
NMessageProvider,
NDialogProvider,
NLoadingBarProvider,
lightTheme,
darkTheme,
} from 'naive-ui'
import { usePreferredDark } from '@vueuse/core'
import { useUserStore } from './stores/user'
import { onMounted } from 'vue'
@@ -34,10 +42,14 @@ onMounted(() => {
<template>
<n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme">
<n-global-style />
<n-message-provider placement="bottom">
<layout-default>
<router-view />
</layout-default>
</n-message-provider>
<n-loading-bar-provider>
<n-dialog-provider>
<n-message-provider placement="bottom">
<layout-default>
<router-view />
</layout-default>
</n-message-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</n-config-provider>
</template>

View File

@@ -27,6 +27,12 @@ const router = createRouter({
component: () => import('../views/dashboard/usage.vue'),
meta: { requiresAuth: true },
},
{
path: 'files',
name: 'dashboardFiles',
component: () => import('../views/dashboard/files.vue'),
meta: { requiresAuth: true },
}
],
},
{
@@ -42,7 +48,7 @@ router.beforeEach(async (to, from, next) => {
const servicesStore = useServicesStore()
// Initialize user state if not already initialized
if (!userStore.user && localStorage.getItem('authToken')) {
if (!userStore.user) {
await userStore.fetchUser()
}

View File

@@ -0,0 +1,184 @@
<template>
<section class="h-full px-5 py-4">
<n-data-table :columns="tableColumns" :data="files" :pagination="tablePagination" />
</section>
</template>
<script lang="ts" setup>
import {
NDataTable,
NIcon,
NImage,
NButton,
NSpace,
type DataTableColumns,
type PaginationProps,
useDialog,
useMessage,
useLoadingBar,
} from 'naive-ui'
import {
AudioFileRound,
InsertDriveFileRound,
VideoFileRound,
FileDownloadOutlined,
DeleteRound,
} from '@vicons/material'
import { h, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { formatBytes } from '../format'
const router = useRouter()
const files = ref<any[]>([])
const tableColumns: DataTableColumns<any> = [
{
title: 'Preview',
key: 'preview',
render(row: any) {
switch (row.mime_type.split('/')[0]) {
case 'image':
return h(NImage, {
src: '/api/files/' + row.id,
width: 32,
height: 32,
objectFit: 'contain',
style: { aspectRatio: 1 },
})
case 'video':
return h(NIcon, { size: 32 }, { default: () => h(VideoFileRound) })
case 'audio':
return h(NIcon, { size: 32 }, { default: () => h(AudioFileRound) })
default:
return h(NIcon, { size: 32 }, { default: () => h(InsertDriveFileRound) })
}
},
},
{
title: 'Name',
key: 'name',
render(row: any) {
return h(
NButton,
{
text: true,
onClick: () => {
router.push(`/files/${row.id}`)
},
},
{
default: () => row.name,
},
)
},
},
{
title: 'Size',
key: 'size',
render(row: any) {
return formatBytes(row.size)
},
},
{
title: 'Uploaded At',
key: 'created_at',
render(row: any) {
return new Date(row.created_at).toLocaleString()
},
},
{
title: 'Action',
key: 'action',
render(row: any) {
return h(NSpace, {}, [
h(
NButton,
{
quaternary: true,
circle: true,
text: true,
onClick: () => {
window.open(`/api/files/${row.id}`, '_blank')
},
},
{
icon: () => h(NIcon, {}, { default: () => h(FileDownloadOutlined) }),
},
),
h(
NButton,
{
quaternary: true,
circle: true,
text: true,
type: 'error',
onClick: () => {
askDeleteFile(row)
},
},
{
icon: () => h(NIcon, {}, { default: () => h(DeleteRound) }),
},
),
])
},
},
]
const tablePagination = ref<PaginationProps>({
page: 1,
itemCount: 0,
})
async function fetchFiles() {
try {
const response = await fetch('/api/files/me')
if (!response.ok) {
throw new Error('Network response was not ok')
}
const data = await response.json()
files.value = data
tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0')
} catch (error) {
console.error('Failed to fetch files:', error)
}
}
onMounted(() => fetchFiles())
const dialog = useDialog()
const messageDialog = useMessage()
const loadingBar = useLoadingBar()
async function askDeleteFile(file: any) {
dialog.warning({
title: 'Confirm',
content: `Are you sure you want delete ${file.name}?`,
positiveText: 'Sure',
negativeText: 'Not Sure',
draggable: true,
onPositiveClick: () => {
deleteFile(file)
},
})
}
async function deleteFile(file: any) {
try {
loadingBar.start()
const response = await fetch(`/api/files/${file.id}`, {
method: 'DELETE',
})
if (!response.ok) {
throw new Error('Network response was not ok')
}
tablePagination.value.page = 1
await fetchFiles()
loadingBar.finish()
messageDialog.success('File deleted successfully')
} catch (error) {
loadingBar.error()
messageDialog.error('Failed to delete file: ' + (error as Error).message)
}
}
</script>