<template>
|
<el-dialog
|
v-model="visible"
|
:title="isEdit ? '编辑新闻' : '新增新闻'"
|
width="800px"
|
:before-close="handleClose"
|
>
|
<el-form
|
ref="formRef"
|
:model="form"
|
:rules="rules"
|
label-width="100px"
|
label-position="left"
|
>
|
<!-- 基本信息 -->
|
<el-form-item label="新闻标题" prop="title">
|
<el-input
|
v-model="form.title"
|
placeholder="请输入新闻标题"
|
maxlength="100"
|
show-word-limit
|
/>
|
</el-form-item>
|
|
<el-form-item label="排序字段" prop="sortOrder">
|
<el-input-number
|
v-model="form.sortOrder"
|
:min="0"
|
:max="9999"
|
placeholder="请输入排序数字,数字越小越靠前"
|
style="width: 200px"
|
/>
|
<span class="form-tip">数字越小越靠前显示,0表示最优先</span>
|
</el-form-item>
|
|
<el-form-item label="新闻内容" prop="content">
|
<el-input
|
v-model="form.content"
|
type="textarea"
|
:rows="6"
|
placeholder="请输入新闻内容"
|
maxlength="2000"
|
show-word-limit
|
/>
|
</el-form-item>
|
|
<!-- 媒体文件上传 -->
|
<el-form-item label="媒体文件">
|
<div class="media-upload-container">
|
<!-- 现有媒体文件显示 -->
|
<div v-if="form.mediaFiles.length > 0" class="media-list">
|
<div
|
v-for="(media, index) in form.mediaFiles"
|
:key="media.id || index"
|
class="media-item"
|
>
|
<!-- 图片预览 -->
|
<div v-if="isImage(media)" class="media-preview">
|
<img :src="media.url" class="media-image" />
|
<div class="media-info">
|
<div class="media-name">{{ media.name }}</div>
|
<div class="media-actions">
|
<el-button
|
type="danger"
|
size="small"
|
@click="handleDeleteMedia(media, index)"
|
:loading="media.deleting"
|
>
|
删除
|
</el-button>
|
</div>
|
</div>
|
</div>
|
|
<!-- 视频预览 -->
|
<div v-else-if="isVideo(media)" class="media-preview">
|
<video :src="media.url" class="media-video" controls></video>
|
<div class="media-info">
|
<div class="media-name">{{ media.name }}</div>
|
<div class="media-actions">
|
<el-button
|
type="danger"
|
size="small"
|
@click="handleDeleteMedia(media, index)"
|
:loading="media.deleting"
|
>
|
删除
|
</el-button>
|
</div>
|
</div>
|
</div>
|
|
<!-- 其他文件 -->
|
<div v-else class="media-preview">
|
<div class="file-icon">📄</div>
|
<div class="media-info">
|
<div class="media-name">{{ media.name }}</div>
|
<div class="media-actions">
|
<el-button
|
type="danger"
|
size="small"
|
@click="handleDeleteMedia(media, index)"
|
:loading="media.deleting"
|
>
|
删除
|
</el-button>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 上传按钮 -->
|
<el-upload
|
class="media-uploader"
|
:show-file-list="false"
|
:before-upload="beforeMediaUpload"
|
:http-request="handleMediaUpload"
|
accept="image/*,video/*"
|
multiple
|
>
|
<div class="upload-area">
|
<el-icon class="upload-icon"><Plus /></el-icon>
|
<div class="upload-text">选择媒体文件</div>
|
<div class="upload-hint">支持图片、视频格式,图片≤10MB,视频≤500MB</div>
|
</div>
|
</el-upload>
|
</div>
|
</el-form-item>
|
</el-form>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="handleClose">取消</el-button>
|
<el-button type="primary" :loading="loading" @click="handleSubmit">
|
{{ isEdit ? '更新' : '创建' }}
|
</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</template>
|
|
<script setup>
|
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { Plus } from '@element-plus/icons-vue'
|
import { CarouselApi } from '@/api/carousel'
|
import { getMediasByTarget, deleteMedia, uploadFile } from '@/api/media'
|
|
const props = defineProps({
|
modelValue: Boolean,
|
carouselData: Object
|
})
|
|
const emit = defineEmits(['update:modelValue', 'success'])
|
|
const formRef = ref()
|
const loading = ref(false)
|
|
// 控制弹窗显示
|
const visible = computed({
|
get: () => props.modelValue,
|
set: (value) => emit('update:modelValue', value)
|
})
|
|
// 是否为编辑模式
|
const isEdit = computed(() => !!props.carouselData?.id)
|
|
// 表单数据
|
const form = reactive({
|
id: undefined,
|
title: '',
|
content: '',
|
sortOrder: null,
|
mediaFiles: []
|
})
|
|
// 表单验证规则
|
const rules = {
|
title: [
|
{ required: true, message: '请输入新闻标题', trigger: 'blur' },
|
{ min: 2, max: 100, message: '标题长度在 2 到 100 个字符', trigger: 'blur' }
|
]
|
}
|
|
// 监听轮播图数据变化,填充表单
|
watch(() => props.carouselData, (data) => {
|
nextTick(async () => {
|
if (data && data.id) {
|
form.id = data.id
|
form.title = data.title || ''
|
form.content = data.content || ''
|
form.sortOrder = data.sortOrder
|
|
// 加载轮播图媒体文件(targetType=4 表示轮播图)
|
try {
|
console.log('=== 加载轮播图媒体 ===');
|
console.log('轮播图ID:', data.id);
|
const medias = await getMediasByTarget(4, parseInt(data.id))
|
console.log('获取到的媒体列表:', medias);
|
|
// 转换媒体数据格式,标记为已存在的文件
|
form.mediaFiles = (medias || []).map(media => ({
|
...media,
|
file: null,
|
uploaded: true,
|
isExisting: true,
|
url: media.fullUrl
|
}));
|
|
console.log('转换后的媒体项:', form.mediaFiles);
|
} catch (e) {
|
console.warn('加载轮播图媒体失败:', e)
|
}
|
} else {
|
resetForm()
|
}
|
})
|
}, { immediate: true })
|
|
// 重置表单
|
const resetForm = () => {
|
form.id = undefined
|
form.title = ''
|
form.content = ''
|
form.sortOrder = null
|
form.mediaFiles = []
|
|
nextTick(() => {
|
formRef.value?.clearValidate()
|
})
|
}
|
|
// 判断是否为图片
|
const isImage = (media) => {
|
if (media.mediaType === 1) return true
|
if (media.fileExt) {
|
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(media.fileExt.toLowerCase())
|
}
|
if (media.name) {
|
const ext = media.name.split('.').pop()?.toLowerCase()
|
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(ext)
|
}
|
return false
|
}
|
|
// 判断是否为视频
|
const isVideo = (media) => {
|
if (media.mediaType === 2) return true
|
if (media.fileExt) {
|
return ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(media.fileExt.toLowerCase())
|
}
|
if (media.name) {
|
const ext = media.name.split('.').pop()?.toLowerCase()
|
return ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(ext)
|
}
|
return false
|
}
|
|
// 处理媒体文件上传
|
const handleMediaFileUpload = async (carouselId) => {
|
console.log('=== 开始上传媒体文件 ===');
|
console.log('轮播图ID:', carouselId);
|
console.log('待上传的文件:', form.mediaFiles.filter(m => !m.uploaded));
|
|
// 只处理未上传的文件
|
const filesToUpload = form.mediaFiles.filter(media => !media.uploaded && media.file);
|
|
for (const media of filesToUpload) {
|
try {
|
console.log('上传文件:', media.name);
|
|
// 上传文件
|
const uploadResult = await uploadFile(media.file);
|
console.log('上传结果:', uploadResult);
|
|
if (uploadResult.success) {
|
// 保存媒体信息到数据库
|
await CarouselApi.saveMedia({
|
name: uploadResult.fileName,
|
path: uploadResult.path,
|
fileSize: uploadResult.fileSize,
|
fileExt: uploadResult.fileName.split('.').pop() || 'jpg',
|
mediaType: isVideo(media) ? 2 : 1, // 1表示图片,2表示视频
|
targetType: 4, // 4表示轮播图
|
targetId: parseInt(carouselId)
|
});
|
|
// 标记为已上传
|
media.uploaded = true;
|
media.url = uploadResult.fullUrl;
|
console.log('文件上传并保存成功:', media.name);
|
}
|
} catch (error) {
|
console.error('文件上传失败:', media.name, error);
|
ElMessage.error(`文件 ${media.name} 上传失败: ${error.message}`);
|
}
|
}
|
}
|
|
// 删除媒体文件
|
const handleDeleteMedia = async (media, index) => {
|
try {
|
await ElMessageBox.confirm('确定要删除这个媒体文件吗?', '确认删除', {
|
type: 'warning'
|
});
|
|
media.deleting = true;
|
|
if (media.isExisting && media.id) {
|
// 删除已存在的媒体文件
|
console.log('删除已存在的媒体文件:', media.id);
|
const result = await deleteMedia(media.id);
|
console.log('删除结果:', result);
|
|
if (result) {
|
form.mediaFiles.splice(index, 1);
|
ElMessage.success('媒体文件删除成功');
|
} else {
|
throw new Error('删除失败');
|
}
|
} else {
|
// 删除新选择但未上传的文件
|
form.mediaFiles.splice(index, 1);
|
ElMessage.success('已取消选择的媒体文件');
|
}
|
} catch (error) {
|
console.error('删除媒体失败:', error);
|
if (error !== 'cancel') {
|
ElMessage.error('媒体删除失败: ' + error.message);
|
}
|
} finally {
|
media.deleting = false;
|
}
|
}
|
|
// 媒体文件上传前验证
|
const beforeMediaUpload = (file) => {
|
const isImage = file.type.startsWith('image/')
|
const isVideo = file.type.startsWith('video/')
|
|
if (!isImage && !isVideo) {
|
ElMessage.error('只能上传图片或视频文件!')
|
return false
|
}
|
|
if (isImage && file.size / 1024 / 1024 > 10) {
|
ElMessage.error('图片大小不能超过 10MB!')
|
return false
|
}
|
|
if (isVideo && file.size / 1024 / 1024 > 500) {
|
ElMessage.error('视频大小不能超过 500MB!')
|
return false
|
}
|
|
return true
|
}
|
|
// 处理媒体文件上传
|
const handleMediaUpload = async (options) => {
|
const { file } = options
|
try {
|
// 不立即上传,只添加到待上传列表
|
const mediaItem = {
|
file: file,
|
name: file.name,
|
type: file.type.startsWith('video/') ? 'video' : 'image',
|
uploaded: false,
|
isExisting: false,
|
url: URL.createObjectURL(file) // 本地预览
|
};
|
|
form.mediaFiles.push(mediaItem);
|
ElMessage.success('媒体文件已选择,点击更新按钮时将上传');
|
} catch (error) {
|
console.error('媒体文件处理失败:', error)
|
ElMessage.error('媒体文件处理失败: ' + error.message)
|
}
|
}
|
|
// 提交表单
|
const handleSubmit = async () => {
|
if (!formRef.value) return
|
|
try {
|
await formRef.value.validate()
|
loading.value = true
|
|
// 1. 先保存轮播图基本信息
|
const submitData = {
|
id: form.id,
|
title: form.title,
|
content: form.content || undefined,
|
sortOrder: form.sortOrder
|
}
|
|
const savedCarousel = await CarouselApi.saveCarousel(submitData)
|
console.log('轮播图保存成功:', savedCarousel);
|
|
// 2. 上传新选择的媒体文件
|
if (savedCarousel.id) {
|
await handleMediaFileUpload(savedCarousel.id);
|
}
|
|
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
emit('success')
|
handleClose()
|
} catch (error) {
|
console.error('保存失败:', error)
|
ElMessage.error('保存失败: ' + error.message)
|
} finally {
|
loading.value = false
|
}
|
}
|
|
// 关闭弹窗
|
const handleClose = () => {
|
visible.value = false
|
resetForm()
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
.dialog-footer {
|
text-align: right;
|
}
|
|
.form-tip {
|
margin-left: 10px;
|
color: #909399;
|
font-size: 12px;
|
}
|
|
.media-upload-container {
|
display: flex;
|
flex-direction: column;
|
gap: 16px;
|
}
|
|
.media-list {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 16px;
|
}
|
|
.media-item {
|
position: relative;
|
display: inline-block;
|
}
|
|
.media-preview {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
border: 1px solid var(--el-border-color);
|
border-radius: 6px;
|
padding: 8px;
|
background: #fafafa;
|
}
|
|
.media-image {
|
width: 150px;
|
height: 100px;
|
object-fit: cover;
|
border-radius: 4px;
|
}
|
|
.media-video {
|
width: 200px;
|
height: 120px;
|
border-radius: 4px;
|
}
|
|
.file-icon {
|
font-size: 48px;
|
margin-bottom: 8px;
|
}
|
|
.media-info {
|
margin-top: 8px;
|
text-align: center;
|
}
|
|
.media-name {
|
font-size: 12px;
|
color: #606266;
|
margin-bottom: 8px;
|
max-width: 150px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.upload-area {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
width: 200px;
|
height: 120px;
|
border: 1px dashed var(--el-border-color);
|
border-radius: 6px;
|
cursor: pointer;
|
transition: var(--el-transition-duration-fast);
|
text-align: center;
|
|
&:hover {
|
border-color: var(--el-color-primary);
|
}
|
}
|
|
.upload-icon {
|
font-size: 28px;
|
color: #8c939d;
|
margin-bottom: 8px;
|
}
|
|
.upload-text {
|
font-size: 14px;
|
color: #8c939d;
|
margin-bottom: 4px;
|
}
|
|
.upload-hint {
|
font-size: 12px;
|
color: #999;
|
line-height: 1.2;
|
}
|
</style>
|