feat: add medias page
This commit is contained in:
@@ -15,7 +15,7 @@ const navItems = ref([
|
|||||||
{
|
{
|
||||||
label: 'Media',
|
label: 'Media',
|
||||||
icon: 'pi pi-image',
|
icon: 'pi pi-image',
|
||||||
command: () => router.push('/media')
|
command: () => router.push('/medias')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Articles',
|
label: 'Articles',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Aura from '@primeuix/themes/aura';
|
import Aura from '@primeuix/themes/aura';
|
||||||
import PrimeVue from 'primevue/config';
|
import PrimeVue from 'primevue/config';
|
||||||
|
import ConfirmationService from 'primevue/confirmationservice';
|
||||||
|
import ToastService from 'primevue/toastservice';
|
||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { router } from './router.js';
|
import { router } from './router.js';
|
||||||
@@ -30,6 +32,12 @@ app.use(PrimeVue, {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Register the ConfirmationService to fix the error
|
||||||
|
app.use(ConfirmationService);
|
||||||
|
|
||||||
|
// Register the ToastService
|
||||||
|
app.use(ToastService);
|
||||||
|
|
||||||
// Remove global component registrations
|
// Remove global component registrations
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|||||||
289
frontend/admin/src/pages/MediaPage.vue
Normal file
289
frontend/admin/src/pages/MediaPage.vue
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
<script setup>
|
||||||
|
import { InputText } from 'primevue';
|
||||||
|
import Badge from 'primevue/badge';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import Column from 'primevue/column';
|
||||||
|
import ConfirmDialog from 'primevue/confirmdialog';
|
||||||
|
import DataTable from 'primevue/datatable';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import Dropdown from 'primevue/dropdown';
|
||||||
|
import FileUpload from 'primevue/fileupload';
|
||||||
|
import ProgressSpinner from 'primevue/progressspinner';
|
||||||
|
import Toast from 'primevue/toast';
|
||||||
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
// Import useConfirm dynamically to avoid the error when the service is not provided
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// Add ref for upload dialog visibility
|
||||||
|
const uploadDialogVisible = ref(false);
|
||||||
|
|
||||||
|
// Function to open upload dialog
|
||||||
|
const openUploadDialog = () => {
|
||||||
|
uploadDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to close upload dialog
|
||||||
|
const closeUploadDialog = () => {
|
||||||
|
uploadDialogVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Media types for filtering
|
||||||
|
const mediaTypes = ref([
|
||||||
|
{ name: 'All media', value: null },
|
||||||
|
{ name: 'Images', value: 'Image' },
|
||||||
|
{ name: 'Videos', value: 'Video' },
|
||||||
|
{ name: 'Documents', value: 'Document' },
|
||||||
|
{ name: 'Audio', value: 'Audio' },
|
||||||
|
{ name: 'PDF', value: 'PDF' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selectedMediaType = ref(mediaTypes.value[0]);
|
||||||
|
const globalFilterValue = ref('');
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// Sample data - in a real app, this would come from an API
|
||||||
|
const mediaFiles = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
fileName: 'sunset-beach.jpg',
|
||||||
|
uploadTime: 'Today, 10:30 AM',
|
||||||
|
fileSize: '2.4 MB',
|
||||||
|
fileType: 'Image',
|
||||||
|
thumbnailUrl: 'https://via.placeholder.com/300x225',
|
||||||
|
iconClass: 'pi-image'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
fileName: 'presentation.pdf',
|
||||||
|
uploadTime: 'Yesterday, 3:45 PM',
|
||||||
|
fileSize: '4.8 MB',
|
||||||
|
fileType: 'PDF',
|
||||||
|
thumbnailUrl: null,
|
||||||
|
iconClass: 'pi-file-pdf'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
fileName: 'promo_video.mp4',
|
||||||
|
uploadTime: 'Aug 28, 2023',
|
||||||
|
fileSize: '24.8 MB',
|
||||||
|
fileType: 'Video',
|
||||||
|
thumbnailUrl: null,
|
||||||
|
iconClass: 'pi-video'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
fileName: 'report_q3.docx',
|
||||||
|
uploadTime: 'Aug 25, 2023',
|
||||||
|
fileSize: '1.2 MB',
|
||||||
|
fileType: 'Document',
|
||||||
|
thumbnailUrl: null,
|
||||||
|
iconClass: 'pi-file'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
fileName: 'podcast_interview.mp3',
|
||||||
|
uploadTime: 'Aug 20, 2023',
|
||||||
|
fileSize: '18.5 MB',
|
||||||
|
fileType: 'Audio',
|
||||||
|
thumbnailUrl: null,
|
||||||
|
iconClass: 'pi-volume-up'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// File upload handling
|
||||||
|
const onUpload = (event) => {
|
||||||
|
toast.add({ severity: 'success', summary: 'Success', detail: 'Files uploaded successfully', life: 3000 });
|
||||||
|
// In a real app, you would process the files from event.files and update the mediaFiles list
|
||||||
|
// Here we're just showing a success message
|
||||||
|
|
||||||
|
// Close the dialog after successful upload
|
||||||
|
closeUploadDialog();
|
||||||
|
|
||||||
|
// Refresh the media files list
|
||||||
|
fetchMediaFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preview file
|
||||||
|
const previewFile = (file) => {
|
||||||
|
// In a real app, this would open a modal with a preview or navigate to a preview page
|
||||||
|
toast.add({ severity: 'info', summary: 'Preview', detail: `Previewing ${file.fileName}`, life: 3000 });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Download file
|
||||||
|
const downloadFile = (file) => {
|
||||||
|
// In a real app, this would trigger a download
|
||||||
|
toast.add({ severity: 'info', summary: 'Download', detail: `Downloading ${file.fileName}`, life: 3000 });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete file
|
||||||
|
const confirmDelete = (file) => {
|
||||||
|
confirm.require({
|
||||||
|
message: `Are you sure you want to delete ${file.fileName}?`,
|
||||||
|
header: 'Confirmation',
|
||||||
|
icon: 'pi pi-exclamation-triangle',
|
||||||
|
acceptClass: 'p-button-danger',
|
||||||
|
accept: () => {
|
||||||
|
// In a real app, you would call an API to delete the file
|
||||||
|
mediaFiles.value = mediaFiles.value.filter(f => f.id !== file.id);
|
||||||
|
toast.add({ severity: 'success', summary: 'Success', detail: 'File deleted', life: 3000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch media data
|
||||||
|
const fetchMediaFiles = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// In a real app, this would be an API call
|
||||||
|
// const response = await mediaApi.getMediaFiles();
|
||||||
|
// mediaFiles.value = response.data;
|
||||||
|
|
||||||
|
// Simulate API delay
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
// Using sample data already defined above
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to load media files', life: 3000 });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchMediaFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
// File type badge severity mapping
|
||||||
|
const getBadgeSeverity = (fileType) => {
|
||||||
|
const map = {
|
||||||
|
'Image': 'info',
|
||||||
|
'PDF': 'danger',
|
||||||
|
'Video': 'warning',
|
||||||
|
'Document': 'primary',
|
||||||
|
'Audio': 'success'
|
||||||
|
};
|
||||||
|
return map[fileType] || 'info';
|
||||||
|
};
|
||||||
|
|
||||||
|
// File type icon mapping
|
||||||
|
const getFileIcon = (file) => {
|
||||||
|
if (file.thumbnailUrl) return null;
|
||||||
|
return `pi ${file.iconClass}`;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Toast />
|
||||||
|
<ConfirmDialog />
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex justify-between items-center mb-6 gap-4">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-800 text-nowrap">Media Library</h1>
|
||||||
|
|
||||||
|
<Button class="text-nowrap !px-8" icon="pi pi-upload" label="上传" severity="primary"
|
||||||
|
@click="openUploadDialog" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Media Table -->
|
||||||
|
<div class="card mt-10">
|
||||||
|
<div class="pb-10 flex">
|
||||||
|
<InputText v-model="globalFilterValue" placeholder="Search media..." class="flex-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable v-model:filters="filters" :value="mediaFiles" :paginator="true" :rows="5"
|
||||||
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
|
:rowsPerPageOptions="[5, 10, 25]"
|
||||||
|
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries" :loading="loading"
|
||||||
|
dataKey="id" :globalFilterFields="['fileName', 'fileType']"
|
||||||
|
:filters="{ global: { value: globalFilterValue, matchMode: 'contains' } }" stripedRows removableSort
|
||||||
|
class="p-datatable-sm" responsiveLayout="scroll">
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center p-4">No media files found.</div>
|
||||||
|
</template>
|
||||||
|
<template #loading>
|
||||||
|
<div class="flex flex-col items-center justify-center p-4">
|
||||||
|
<ProgressSpinner style="width:50px;height:50px" />
|
||||||
|
<span class="mt-2">Loading media data...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Column field="fileName" header="File Name" sortable>
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div v-if="data.thumbnailUrl" class="flex-shrink-0 h-10 w-10 mr-3">
|
||||||
|
<img class="h-10 w-10 object-cover rounded" :src="data.thumbnailUrl"
|
||||||
|
:alt="data.fileName">
|
||||||
|
</div>
|
||||||
|
<div v-else
|
||||||
|
class="flex-shrink-0 h-10 w-10 mr-3 bg-gray-100 rounded flex items-center justify-center">
|
||||||
|
<i :class="getFileIcon(data)" class="text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">{{ data.fileName }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="uploadTime" header="Upload Time" sortable></Column>
|
||||||
|
<Column field="fileSize" header="File Size" sortable></Column>
|
||||||
|
|
||||||
|
<Column field="fileType" header="File Type" sortable>
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Badge :value="data.fileType" :severity="getBadgeSeverity(data.fileType)" />
|
||||||
|
</template>
|
||||||
|
<template #filter="{ filterModel, filterCallback }">
|
||||||
|
<Dropdown v-model="filterModel.value" @change="filterCallback()" :options="mediaTypes"
|
||||||
|
optionLabel="name" optionValue="value" placeholder="Any" class="p-column-filter"
|
||||||
|
showClear />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column header="Actions" :exportable="false" style="min-width:8rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex justify-center space-x-2">
|
||||||
|
<Button icon="pi pi-eye" rounded text severity="info" @click="previewFile(data)"
|
||||||
|
aria-label="Preview" />
|
||||||
|
<Button icon="pi pi-download" rounded text severity="secondary" @click="downloadFile(data)"
|
||||||
|
aria-label="Download" />
|
||||||
|
<Button icon="pi pi-trash" rounded text severity="danger" @click="confirmDelete(data)"
|
||||||
|
aria-label="Delete" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Dialog -->
|
||||||
|
<Dialog v-model:visible="uploadDialogVisible" header="Upload Media Files" :modal="true" :dismissableMask="true"
|
||||||
|
:closable="true" :style="{ width: '50vw' }" :breakpoints="{ '960px': '75vw', '640px': '90vw' }">
|
||||||
|
<FileUpload name="media[]" url="/api/upload" @upload="onUpload" :multiple="true"
|
||||||
|
accept="image/*,video/*,audio/*,.pdf,.doc,.docx" :maxFileSize="50000000" class="w-full">
|
||||||
|
<template #empty>
|
||||||
|
<div class="flex flex-col items-center justify-center p-8">
|
||||||
|
<i class="pi pi-cloud-upload text-5xl text-blue-500 mb-4"></i>
|
||||||
|
<p class="text-gray-600 text-center mb-2">Drag and drop files here or click to browse</p>
|
||||||
|
<p class="text-gray-400 text-sm text-center">Supports: JPG, PNG, GIF, MP4, PDF, DOC</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FileUpload>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button label="Cancel" icon="pi pi-times" @click="closeUploadDialog" text severity="secondary" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.p-fileupload {
|
||||||
|
border: 2px dashed #cbd5e0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-fileupload:hover {
|
||||||
|
border-color: #4299e1;
|
||||||
|
background-color: rgba(66, 153, 225, 0.05);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
|
|
||||||
// Define your routes here
|
// Define your routes here
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -7,10 +7,16 @@ const routes = [
|
|||||||
name: 'Home',
|
name: 'Home',
|
||||||
component: () => import('./pages/HomePage.vue')
|
component: () => import('./pages/HomePage.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/medias',
|
||||||
|
name: 'Medias',
|
||||||
|
component: () => import('./pages/MediaPage.vue')
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Create the router instance
|
// Create the router instance
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHistory(),
|
// history: createWebHistory(),
|
||||||
|
history: createWebHashHistory(),
|
||||||
routes
|
routes
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user