341 lines
12 KiB
Vue
341 lines
12 KiB
Vue
<script setup>
|
|
import { useToast } from 'primevue/usetoast';
|
|
import { reactive, ref } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
|
|
// PrimeVue components
|
|
import Badge from 'primevue/badge';
|
|
import Button from 'primevue/button';
|
|
import Column from 'primevue/column';
|
|
import DataTable from 'primevue/datatable';
|
|
import Dialog from 'primevue/dialog';
|
|
import InputNumber from 'primevue/inputnumber';
|
|
import InputText from 'primevue/inputtext';
|
|
import ProgressSpinner from 'primevue/progressspinner';
|
|
import Textarea from 'primevue/textarea';
|
|
import Toast from 'primevue/toast';
|
|
|
|
const router = useRouter();
|
|
const toast = useToast();
|
|
|
|
// Form state
|
|
const post = reactive({
|
|
title: '',
|
|
price: 0,
|
|
introduction: '',
|
|
selectedMedia: []
|
|
});
|
|
|
|
// Validation state
|
|
const errors = reactive({
|
|
title: '',
|
|
introduction: '',
|
|
selectedMedia: ''
|
|
});
|
|
|
|
// Media selection dialog state
|
|
const mediaDialogVisible = ref(false);
|
|
const selectedMediaItems = ref([]);
|
|
const mediaLoading = ref(false);
|
|
const mediaGlobalFilter = ref('');
|
|
|
|
// Sample media data - in a real app, this would come from an API
|
|
const mediaItems = ref([
|
|
{
|
|
id: 1,
|
|
fileName: 'sunset-beach.jpg',
|
|
fileType: 'Image',
|
|
thumbnailUrl: 'https://via.placeholder.com/300x225',
|
|
fileSize: '2.4 MB',
|
|
uploadTime: 'Today, 10:30 AM'
|
|
},
|
|
{
|
|
id: 2,
|
|
fileName: 'presentation.pdf',
|
|
fileType: 'PDF',
|
|
thumbnailUrl: null,
|
|
fileSize: '4.8 MB',
|
|
uploadTime: 'Yesterday, 3:45 PM'
|
|
},
|
|
{
|
|
id: 3,
|
|
fileName: 'promo_video.mp4',
|
|
fileType: 'Video',
|
|
thumbnailUrl: null,
|
|
fileSize: '24.8 MB',
|
|
uploadTime: 'Aug 28, 2023'
|
|
},
|
|
{
|
|
id: 4,
|
|
fileName: 'report_q3.docx',
|
|
fileType: 'Document',
|
|
thumbnailUrl: null,
|
|
fileSize: '1.2 MB',
|
|
uploadTime: 'Aug 25, 2023'
|
|
},
|
|
{
|
|
id: 5,
|
|
fileName: 'podcast_interview.mp3',
|
|
fileType: 'Audio',
|
|
thumbnailUrl: null,
|
|
fileSize: '18.5 MB',
|
|
uploadTime: 'Aug 20, 2023'
|
|
}
|
|
]);
|
|
|
|
// Open media selection dialog
|
|
const openMediaDialog = () => {
|
|
mediaDialogVisible.value = true;
|
|
loadMediaItems();
|
|
};
|
|
|
|
// Load media items
|
|
const loadMediaItems = async () => {
|
|
mediaLoading.value = true;
|
|
try {
|
|
// In a real app, this would be an API call
|
|
// const response = await mediaApi.getMediaFiles();
|
|
// mediaItems.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: '错误', detail: '加载媒体文件失败', life: 3000 });
|
|
} finally {
|
|
mediaLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// Confirm media selection
|
|
const confirmMediaSelection = () => {
|
|
if (selectedMediaItems.value.length) {
|
|
post.selectedMedia = [...selectedMediaItems.value];
|
|
errors.selectedMedia = '';
|
|
}
|
|
mediaDialogVisible.value = false;
|
|
};
|
|
|
|
// Cancel media selection
|
|
const cancelMediaSelection = () => {
|
|
mediaDialogVisible.value = false;
|
|
};
|
|
|
|
// Remove a selected media item
|
|
const removeMedia = (media) => {
|
|
const index = post.selectedMedia.findIndex(item => item.id === media.id);
|
|
if (index > -1) {
|
|
post.selectedMedia.splice(index, 1);
|
|
}
|
|
};
|
|
|
|
// Save the post
|
|
const savePost = async () => {
|
|
// Reset errors
|
|
Object.keys(errors).forEach(key => errors[key] = '');
|
|
|
|
// Validate form
|
|
let valid = true;
|
|
|
|
if (!post.title.trim()) {
|
|
errors.title = '请填写文章标题';
|
|
valid = false;
|
|
}
|
|
|
|
if (!post.introduction.trim()) {
|
|
errors.introduction = '请填写文章介绍';
|
|
valid = false;
|
|
}
|
|
|
|
if (post.selectedMedia.length === 0) {
|
|
errors.selectedMedia = '请选择至少一个媒体文件';
|
|
valid = false;
|
|
}
|
|
|
|
if (!valid) {
|
|
toast.add({ severity: 'error', summary: '表单错误', detail: '请检查表单中的错误并修正', life: 3000 });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// In a real app, you would call an API to save the post
|
|
// await postApi.createPost(post);
|
|
|
|
// Simulate API delay
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
toast.add({ severity: 'success', summary: '成功', detail: '文章已成功创建', life: 3000 });
|
|
|
|
// Navigate back to the post list
|
|
router.push('/posts');
|
|
} catch (error) {
|
|
toast.add({ severity: 'error', summary: '错误', detail: '创建文章失败', life: 3000 });
|
|
}
|
|
};
|
|
|
|
// Cancel and go back to post list
|
|
const cancelCreate = () => {
|
|
router.push('/posts');
|
|
};
|
|
|
|
// 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) => {
|
|
const map = {
|
|
'Image': 'pi-image',
|
|
'PDF': 'pi-file-pdf',
|
|
'Video': 'pi-video',
|
|
'Document': 'pi-file',
|
|
'Audio': 'pi-volume-up'
|
|
};
|
|
return `pi ${map[file.fileType] || 'pi-file'}`;
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Toast />
|
|
|
|
<div class="w-full max-w-6xl mx-auto">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h1 class="text-2xl font-bold text-gray-800">创建文章</h1>
|
|
<div class="flex gap-2">
|
|
<Button label="取消" icon="pi pi-times" severity="secondary" @click="cancelCreate" />
|
|
<Button label="保存" icon="pi pi-check" severity="primary" @click="savePost" />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<!-- Title -->
|
|
<div class="col-span-2">
|
|
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">标题</label>
|
|
<InputText id="title" v-model="post.title" class="w-full p-inputtext-lg" placeholder="输入文章标题" />
|
|
<small v-if="errors.title" class="p-error">{{ errors.title }}</small>
|
|
</div>
|
|
|
|
<!-- Price -->
|
|
<div class="col-span-1">
|
|
<label for="price" class="block text-sm font-medium text-gray-700 mb-1">价格 (¥)</label>
|
|
<InputNumber id="price" v-model="post.price" mode="currency" currency="CNY" :minFractionDigits="2"
|
|
class="w-full" />
|
|
</div>
|
|
|
|
<!-- Introduction -->
|
|
<div class="col-span-2">
|
|
<label for="introduction" class="block text-sm font-medium text-gray-700 mb-1">文章介绍</label>
|
|
<Textarea id="introduction" v-model="post.introduction" rows="5" class="w-full"
|
|
placeholder="输入文章介绍内容" />
|
|
<small v-if="errors.introduction" class="p-error">{{ errors.introduction }}</small>
|
|
</div>
|
|
|
|
<!-- Media Selection -->
|
|
<div class="col-span-2">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">媒体资源</label>
|
|
<div class="p-4 border border-gray-200 rounded-md">
|
|
<div v-if="post.selectedMedia.length === 0"
|
|
class="flex justify-center items-center flex-col space-y-3 py-6">
|
|
<i class="pi pi-image text-gray-400 text-3xl"></i>
|
|
<p class="text-gray-500">尚未选择任何媒体文件</p>
|
|
<Button label="选择媒体" icon="pi pi-plus" @click="openMediaDialog" outlined />
|
|
<small v-if="errors.selectedMedia" class="p-error">{{ errors.selectedMedia }}</small>
|
|
</div>
|
|
<div v-else>
|
|
<div class="mb-4">
|
|
<Button label="添加更多媒体" icon="pi pi-plus" @click="openMediaDialog" outlined />
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
<div v-for="media in post.selectedMedia" :key="media.id"
|
|
class="relative border border-gray-200 rounded-md p-2 flex items-center">
|
|
<div v-if="media.thumbnailUrl" class="flex-shrink-0 h-10 w-10 mr-3">
|
|
<img class="h-10 w-10 object-cover rounded" :src="media.thumbnailUrl"
|
|
:alt="media.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(media)" class="text-2xl"></i>
|
|
</div>
|
|
<div class="flex-1 overflow-hidden">
|
|
<div class="text-sm font-medium text-gray-900 truncate">{{ media.fileName }}
|
|
</div>
|
|
<Badge :value="media.fileType" :severity="getBadgeSeverity(media.fileType)"
|
|
class="text-xs" />
|
|
</div>
|
|
<Button icon="pi pi-times" class="p-button-rounded p-button-text p-button-sm"
|
|
@click="removeMedia(media)" aria-label="移除" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Media Selection Dialog -->
|
|
<Dialog v-model:visible="mediaDialogVisible" header="选择媒体" :modal="true" :dismissableMask="true" :closable="true"
|
|
:style="{ width: '80vw' }" :breakpoints="{ '960px': '90vw' }">
|
|
<div class="mb-4">
|
|
<InputText v-model="mediaGlobalFilter" placeholder="搜索媒体..." class="w-full" />
|
|
</div>
|
|
|
|
<DataTable v-model:selection="selectedMediaItems" :value="mediaItems" :loading="mediaLoading" dataKey="id"
|
|
selectionMode="multiple" :globalFilterFields="['fileName', 'fileType']"
|
|
:filters="{ global: { value: mediaGlobalFilter, matchMode: 'contains' } }" stripedRows
|
|
responsiveLayout="scroll">
|
|
|
|
<template #empty>
|
|
<div class="text-center p-4">没有可用的媒体文件</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">加载媒体数据...</span>
|
|
</div>
|
|
</template>
|
|
|
|
<Column selectionMode="multiple" style="width: 3rem"></Column>
|
|
|
|
<Column field="fileName" header="文件名">
|
|
<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="fileType" header="文件类型">
|
|
<template #body="{ data }">
|
|
<Badge :value="data.fileType" :severity="getBadgeSeverity(data.fileType)" />
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="fileSize" header="文件大小"></Column>
|
|
<Column field="uploadTime" header="上传时间"></Column>
|
|
</DataTable>
|
|
|
|
<template #footer>
|
|
<Button label="取消" icon="pi pi-times" @click="cancelMediaSelection" class="p-button-text" />
|
|
<Button label="确认选择" icon="pi pi-check" @click="confirmMediaSelection"
|
|
:disabled="selectedMediaItems.length === 0" />
|
|
</template>
|
|
</Dialog>
|
|
</template> |