|
|
@@ -0,0 +1,749 @@
|
|
|
+<script setup>
|
|
|
+import {nextTick, onMounted, reactive, ref} from 'vue'
|
|
|
+import {ElMessage, ElMessageBox} from 'element-plus'
|
|
|
+import {Delete, Edit, Eye, Plus, Upload} from 'lucide-vue-next'
|
|
|
+import {Quill, QuillEditor} from '@vueup/vue-quill'
|
|
|
+import BlotFormatter from "quill-blot-formatter";
|
|
|
+
|
|
|
+import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
|
|
+import request from "@/utils/request.js"
|
|
|
+import {findArrayDifferences} from "@/utils/index.js";
|
|
|
+
|
|
|
+const BASE_URL = import.meta.env.VITE_APP_BASE_URL
|
|
|
+Quill.register("modules/blotFormatter", BlotFormatter);
|
|
|
+
|
|
|
+// 响应式数据
|
|
|
+const tableData = ref([])
|
|
|
+const total = ref(0)
|
|
|
+const loading = ref(false)
|
|
|
+const dialogVisible = ref(false)
|
|
|
+const dialogTitle = ref('添加新闻')
|
|
|
+const isEdit = ref(false)
|
|
|
+
|
|
|
+const detailDialogVisible = ref(false)
|
|
|
+const detailDialogContent = ref('')
|
|
|
+
|
|
|
+const uploadLoading = ref(false)
|
|
|
+const coverFile = ref(null)
|
|
|
+const fileInputRef = ref(null)
|
|
|
+
|
|
|
+const imagePreviewVisible = ref(false)
|
|
|
+const previewImageUrl = ref('')
|
|
|
+
|
|
|
+const quillRef = ref(null)
|
|
|
+
|
|
|
+// 新增:记录图片变化的列表
|
|
|
+const insertedImages = ref([]) // 插入的图片列表
|
|
|
+const deletedImages = ref([]) // 删除的图片列表
|
|
|
+const originalImages = ref([]) // 原始图片列表(编辑时使用)
|
|
|
+
|
|
|
+// 分页参数
|
|
|
+const pagination = reactive({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10
|
|
|
+})
|
|
|
+
|
|
|
+// 表单数据
|
|
|
+const formData = reactive({
|
|
|
+ id: '',
|
|
|
+ newsName: '',
|
|
|
+ newsDetails: '',
|
|
|
+ newsUrl: '',
|
|
|
+ releaseTime: '',
|
|
|
+ isSpecial: 0
|
|
|
+})
|
|
|
+
|
|
|
+const quillOptions = {
|
|
|
+ theme: 'snow',
|
|
|
+ modules: {
|
|
|
+ blotFormatter: {},
|
|
|
+ toolbar: [
|
|
|
+ ['bold', 'italic', 'underline', 'strike'],
|
|
|
+ ['blockquote', 'code-block'],
|
|
|
+ [{'header': 1}, {'header': 2}],
|
|
|
+ [{'list': 'ordered'}, {'list': 'bullet'}],
|
|
|
+ [{'script': 'sub'}, {'script': 'super'}],
|
|
|
+ [{'indent': '-1'}, {'indent': '+1'}],
|
|
|
+ [{'direction': 'rtl'}],
|
|
|
+ [{'size': ['small', false, 'large', 'huge']}],
|
|
|
+ [{'header': [1, 2, 3, 4, 5, 6, false]}],
|
|
|
+ [{'color': []}, {'background': []}],
|
|
|
+ [{'font': []}],
|
|
|
+ [{'align': []}],
|
|
|
+ ['clean'],
|
|
|
+ ['link', 'image', 'video']
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ placeholder: '请输入新闻详情...',
|
|
|
+}
|
|
|
+
|
|
|
+// 新增:获取富文本中的所有图片
|
|
|
+const getImagesFromContent = (content) => {
|
|
|
+ if (!content) return []
|
|
|
+
|
|
|
+ const delta = typeof content === 'string' ?
|
|
|
+ quillRef.value?.clipboard.convert(content) :
|
|
|
+ content
|
|
|
+
|
|
|
+ const images = []
|
|
|
+ if (delta && delta.ops) {
|
|
|
+ delta.ops.forEach(op => {
|
|
|
+ if (op.insert && op.insert.image) {
|
|
|
+ images.push(op.insert.image)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return images
|
|
|
+}
|
|
|
+
|
|
|
+const onEditorReady = (quill) => {
|
|
|
+ quillRef.value = quill
|
|
|
+
|
|
|
+ // 图片上传逻辑(保留原来的)
|
|
|
+ const toolbar = quill.getModule('toolbar')
|
|
|
+ toolbar.addHandler('image', () => {
|
|
|
+ const input = document.createElement('input')
|
|
|
+ input.setAttribute('type', 'file')
|
|
|
+ input.setAttribute('accept', 'image/*')
|
|
|
+ input.click()
|
|
|
+
|
|
|
+ input.onchange = async () => {
|
|
|
+ const file = input.files[0]
|
|
|
+ if (!file) return
|
|
|
+
|
|
|
+ if (!file.type.startsWith('image/')) {
|
|
|
+ ElMessage.error('请选择图片文件')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (file.size > 5 * 1024 * 1024) {
|
|
|
+ ElMessage.error('图片大小不能超过5MB')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const formData = new FormData()
|
|
|
+ formData.append('file', file)
|
|
|
+ formData.append('moduleName', 'news')
|
|
|
+
|
|
|
+ const res = await request.post('/uploadFile', formData, {
|
|
|
+ headers: {'Content-Type': 'multipart/form-data'}
|
|
|
+ })
|
|
|
+
|
|
|
+ if (res.code === 200) {
|
|
|
+ const imageUrl = BASE_URL + res.data.fileUrl
|
|
|
+ const range = quill.getSelection()
|
|
|
+ quill.insertEmbed(range.index, 'image', imageUrl)
|
|
|
+ ElMessage.success('图片上传成功')
|
|
|
+ } else {
|
|
|
+ ElMessage.error('图片上传失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error('图片上传失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 修改:优化富文本变化监听逻辑
|
|
|
+ quill.on('text-change', (delta, oldDelta, source) => {
|
|
|
+ if (source !== 'user') return // 只处理用户操作
|
|
|
+
|
|
|
+ const newDelta = quill.getContents()
|
|
|
+ const diff = oldDelta.diff(newDelta)
|
|
|
+
|
|
|
+ diff.ops.forEach(op => {
|
|
|
+ if (op.delete) {
|
|
|
+ // 检测删除的图片
|
|
|
+ const {onlyInSecond} = findArrayDifferences(newDelta.ops, oldDelta.ops)
|
|
|
+ onlyInSecond.forEach(item => {
|
|
|
+ if (item.insert?.image) {
|
|
|
+ const imageUrl = item.insert.image
|
|
|
+ console.log('删除图片:', imageUrl)
|
|
|
+
|
|
|
+ // 添加到删除列表,避免重复
|
|
|
+ if (!deletedImages.value.includes(imageUrl)) {
|
|
|
+ deletedImages.value.push(imageUrl)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从插入列表中移除(如果存在)
|
|
|
+ const insertIndex = insertedImages.value.indexOf(imageUrl)
|
|
|
+ if (insertIndex > -1) {
|
|
|
+ insertedImages.value.splice(insertIndex, 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (op.insert && op.insert.image) {
|
|
|
+ // 检测插入的图片
|
|
|
+ const imageUrl = op.insert.image
|
|
|
+ console.log('插入图片:', imageUrl)
|
|
|
+
|
|
|
+ // 添加到插入列表,避免重复
|
|
|
+ if (!insertedImages.value.includes(imageUrl)) {
|
|
|
+ insertedImages.value.push(imageUrl)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从删除列表中移除(如果存在)
|
|
|
+ const deleteIndex = deletedImages.value.indexOf(imageUrl)
|
|
|
+ if (deleteIndex > -1) {
|
|
|
+ deletedImages.value.splice(deleteIndex, 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // console.log('当前插入图片列表:', insertedImages.value)
|
|
|
+ // console.log('当前删除图片列表:', deletedImages.value)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleImagePreview = (imageUrl) => {
|
|
|
+ previewImageUrl.value = imageUrl.startsWith('http') ? imageUrl : BASE_URL + imageUrl
|
|
|
+ imagePreviewVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const handleCoverUpload = (event) => {
|
|
|
+ const file = event.target.files[0]
|
|
|
+ if (!file) return
|
|
|
+
|
|
|
+ // 验证文件类型
|
|
|
+ if (!file.type.startsWith('image/')) {
|
|
|
+ ElMessage.error('请选择图片文件')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证文件大小 (5MB)
|
|
|
+ if (file.size > 5 * 1024 * 1024) {
|
|
|
+ ElMessage.error('图片大小不能超过5MB')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ coverFile.value = file
|
|
|
+
|
|
|
+ // 预览图片
|
|
|
+ const reader = new FileReader()
|
|
|
+ reader.onload = (e) => {
|
|
|
+ formData.newsUrl = e.target.result
|
|
|
+ }
|
|
|
+ reader.readAsDataURL(file)
|
|
|
+}
|
|
|
+
|
|
|
+const clearCover = () => {
|
|
|
+ coverFile.value = null
|
|
|
+ formData.newsUrl = ''
|
|
|
+ if (fileInputRef.value) {
|
|
|
+ fileInputRef.value.value = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取列表数据
|
|
|
+const getList = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const res = await request.get("/newsUpdates/findByPage", {
|
|
|
+ params: {
|
|
|
+ pageNum: pagination.pageNum,
|
|
|
+ pageSize: pagination.pageSize
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (res.code !== 200) {
|
|
|
+ ElMessage.error(res.msg)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ tableData.value = res.data.records
|
|
|
+ total.value = res.data.total
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error('获取数据失败')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 新增:重置图片变化记录
|
|
|
+const resetImageTracking = () => {
|
|
|
+ insertedImages.value = []
|
|
|
+ deletedImages.value = []
|
|
|
+ originalImages.value = []
|
|
|
+}
|
|
|
+
|
|
|
+// 添加新闻
|
|
|
+const handleAdd = () => {
|
|
|
+ dialogTitle.value = '添加新闻'
|
|
|
+ isEdit.value = false
|
|
|
+ resetForm()
|
|
|
+ resetImageTracking() // 重置图片跟踪
|
|
|
+ dialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 编辑新闻
|
|
|
+const handleEdit = async (row) => {
|
|
|
+ dialogTitle.value = '编辑新闻'
|
|
|
+ isEdit.value = true
|
|
|
+
|
|
|
+ const res = await request.get("/newsUpdates/getById/" + row.id)
|
|
|
+ if (res.code !== 200) {
|
|
|
+ ElMessage.error(res.msg)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if(res.data.isSpecial){
|
|
|
+ res.data.isSpecial = 1
|
|
|
+ }else{
|
|
|
+ res.data.isSpecial = 0
|
|
|
+ }
|
|
|
+ Object.assign(formData, res.data)
|
|
|
+ coverFile.value = null
|
|
|
+
|
|
|
+ // 新增:记录原始图片列表
|
|
|
+ resetImageTracking()
|
|
|
+ await nextTick() // 等待DOM更新
|
|
|
+
|
|
|
+ // 等待编辑器内容加载完成后获取原始图片
|
|
|
+ setTimeout(() => {
|
|
|
+ if (quillRef.value) {
|
|
|
+ originalImages.value = getImagesFromContent(quillRef.value.getContents())
|
|
|
+ // console.log('原始图片列表:', originalImages.value)
|
|
|
+ }
|
|
|
+ }, 100)
|
|
|
+
|
|
|
+ dialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 查看详情
|
|
|
+const handleView = async (row) => {
|
|
|
+ const res = await request.get("/newsUpdates/getById/" + row.id)
|
|
|
+ if (res.code !== 200) {
|
|
|
+ ElMessage.error(res.msg)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ detailDialogContent.value = res.data.newsDetails
|
|
|
+ detailDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 删除新闻
|
|
|
+const handleDelete = (row) => {
|
|
|
+ ElMessageBox.confirm('确定要删除这条新闻吗?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(async () => {
|
|
|
+ const res = await request.post("/newsUpdates/deleteBatch", [row.id])
|
|
|
+ if (res.code !== 200) {
|
|
|
+ ElMessage.error(res.msg)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ElMessage.success(res.msg)
|
|
|
+ getList()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 批量删除
|
|
|
+const selectedRows = ref([])
|
|
|
+const handleSelectionChange = (selection) => {
|
|
|
+ selectedRows.value = selection
|
|
|
+}
|
|
|
+
|
|
|
+const handleBatchDelete = () => {
|
|
|
+ if (selectedRows.value.length === 0) {
|
|
|
+ ElMessage.warning('请选择要删除的数据')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 条新闻吗?`, '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(async () => {
|
|
|
+ const ids = selectedRows.value.map(row => row.id)
|
|
|
+ const res = await request.post("/newsUpdates/deleteBatch", ids)
|
|
|
+ if (res.code !== 200) {
|
|
|
+ ElMessage.error(res.msg)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ElMessage.success(res.msg)
|
|
|
+ getList()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 修改:保存逻辑,添加图片变化处理
|
|
|
+const handleSave = async () => {
|
|
|
+ if (!formData.newsName.trim()) {
|
|
|
+ ElMessage.warning('请输入新闻名称')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!formData.newsDetails.trim()) {
|
|
|
+ ElMessage.warning('请输入新闻详情')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ uploadLoading.value = true
|
|
|
+
|
|
|
+ // 创建FormData
|
|
|
+ const submitData = new FormData()
|
|
|
+ submitData.append('newsName', formData.newsName)
|
|
|
+ submitData.append('newsDetails', formData.newsDetails)
|
|
|
+ submitData.append('isSpecial', formData.isSpecial)
|
|
|
+ submitData.append('releaseTime', formData.releaseTime)
|
|
|
+
|
|
|
+ // 如果有新的封面文件,添加到FormData
|
|
|
+ if (coverFile.value) {
|
|
|
+ submitData.append('file', coverFile.value)
|
|
|
+ } else if (formData.newsUrl && !formData.newsUrl.startsWith('data:')) {
|
|
|
+ // 如果是编辑且没有新文件,保持原有URL
|
|
|
+ submitData.append('newsUrl', formData.newsUrl)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 新增:添加图片变化信息
|
|
|
+ if (isEdit.value) {
|
|
|
+ // 编辑模式:计算实际的图片变化
|
|
|
+ const currentImages = getImagesFromContent(quillRef.value.getContents())
|
|
|
+
|
|
|
+ // 计算真正删除的图片(原始有,现在没有的)
|
|
|
+ const actualDeletedImages = originalImages.value.filter(img => !currentImages.includes(img))
|
|
|
+
|
|
|
+ // 计算真正新增的图片(现在有,原始没有的)
|
|
|
+ const actualInsertedImages = currentImages.filter(img => !originalImages.value.includes(img))
|
|
|
+
|
|
|
+ console.log('实际删除的图片:', actualDeletedImages)
|
|
|
+ console.log('实际新增的图片:', actualInsertedImages)
|
|
|
+
|
|
|
+ // 发送图片变化信息给后端
|
|
|
+ if (actualDeletedImages.length > 0) {
|
|
|
+ submitData.append('deletedImages', JSON.stringify(actualDeletedImages))
|
|
|
+ }
|
|
|
+ if (actualInsertedImages.length > 0) {
|
|
|
+ submitData.append('insertedImages', JSON.stringify(actualInsertedImages))
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 新增模式:所有图片都是新增的
|
|
|
+ const currentImages = getImagesFromContent(quillRef.value.getContents())
|
|
|
+ if (currentImages.length > 0) {
|
|
|
+ submitData.append('insertedImages', JSON.stringify(currentImages))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let res
|
|
|
+ if (isEdit.value) {
|
|
|
+ submitData.append('id', formData.id)
|
|
|
+ res = await request.post("/newsUpdates/update", submitData, {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'multipart/form-data'
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ res = await request.post("/newsUpdates/save", submitData, {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'multipart/form-data'
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (res.code !== 200) {
|
|
|
+ ElMessage.error(res.msg)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success(res.msg)
|
|
|
+ dialogVisible.value = false
|
|
|
+ getList()
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存失败:', error)
|
|
|
+ ElMessage.error('保存失败')
|
|
|
+ } finally {
|
|
|
+ uploadLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const resetForm = () => {
|
|
|
+ Object.assign(formData, {
|
|
|
+ id: '',
|
|
|
+ newsName: '',
|
|
|
+ newsDetails: '',
|
|
|
+ newsUrl: '',
|
|
|
+ releaseTime: '',
|
|
|
+ isSpecial: 0
|
|
|
+ })
|
|
|
+ coverFile.value = null
|
|
|
+ if (fileInputRef.value) {
|
|
|
+ fileInputRef.value.value = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 分页改变
|
|
|
+const handlePageChange = (page) => {
|
|
|
+ pagination.pageNum = page
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+const handleSizeChange = (size) => {
|
|
|
+ pagination.pageSize = size
|
|
|
+ pagination.pageNum = 1
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+const previewOptions = {
|
|
|
+ theme: 'snow',
|
|
|
+ modules: {
|
|
|
+ toolbar: false, // 预览模式不需要工具栏
|
|
|
+ },
|
|
|
+ readOnly: true, // 只读模式
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getList()
|
|
|
+})
|
|
|
+
|
|
|
+// 修改:对话框关闭时重置图片跟踪
|
|
|
+const handleDialogClose = () => {
|
|
|
+ resetForm()
|
|
|
+ resetImageTracking() // 重置图片跟踪
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="news-management p-6">
|
|
|
+ <!-- 页面标题 -->
|
|
|
+ <div class="mb-6">
|
|
|
+ <h1 class="text-2xl font-bold text-gray-800">新闻发布管理</h1>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 操作栏 -->
|
|
|
+ <div class="mb-4 flex justify-between items-center">
|
|
|
+ <div class="flex gap-2">
|
|
|
+ <el-button type="primary" @click="handleAdd">
|
|
|
+ <Plus class="w-4 h-4 mr-1"/>
|
|
|
+ 添加新闻
|
|
|
+ </el-button>
|
|
|
+ <el-button type="danger" @click="handleBatchDelete">
|
|
|
+ <Delete class="w-4 h-4 mr-1"/>
|
|
|
+ 批量删除
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 数据表格 -->
|
|
|
+ <el-table
|
|
|
+ :data="tableData"
|
|
|
+ v-loading="loading"
|
|
|
+ @selection-change="handleSelectionChange"
|
|
|
+ class="w-full"
|
|
|
+ >
|
|
|
+ <el-table-column type="selection" width="55"/>
|
|
|
+ <el-table-column prop="newsName" label="新闻名称" min-width="200"/>
|
|
|
+ <el-table-column prop="releaseTime" label="发布时间" min-width="200"/>
|
|
|
+ <el-table-column prop="newsUrl" label="新闻图片" width="120">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <!-- 修改图片预览方式,使用自定义点击事件 -->
|
|
|
+ <div v-if="row.newsUrl" class="cursor-pointer" @click="handleImagePreview(row.newsUrl)">
|
|
|
+ <img
|
|
|
+ :src="row.newsUrl.startsWith('http') ? row.newsUrl : BASE_URL + row.newsUrl"
|
|
|
+ class="w-16 h-12 object-cover rounded border hover:opacity-80 transition-opacity"
|
|
|
+ alt="新闻图片"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <span v-else class="text-gray-400">无封面</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="isSpecial" label="特别新闻" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.isSpecial ? 'danger' : 'info'">
|
|
|
+ {{ row.isSpecial ? '是' : '否' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="createBy" label="创建人" width="120"/>
|
|
|
+ <el-table-column prop="createTime" label="创建时间" width="180">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.createTime ? new Date(row.createTime).toLocaleString() : '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="200" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button size="small" @click="handleView(row)">
|
|
|
+ <Eye class="w-4 h-4 mr-1"/>
|
|
|
+ 查看
|
|
|
+ </el-button>
|
|
|
+ <el-button size="small" type="primary" @click="handleEdit(row)">
|
|
|
+ <Edit class="w-4 h-4 mr-1"/>
|
|
|
+ 编辑
|
|
|
+ </el-button>
|
|
|
+ <el-button size="small" type="danger" @click="handleDelete(row)">
|
|
|
+ <Delete class="w-4 h-4 mr-1"/>
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div class="mt-4 flex justify-end">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pagination.pageNum"
|
|
|
+ v-model:page-size="pagination.pageSize"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ :total="total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handlePageChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 添加/编辑对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="dialogVisible"
|
|
|
+ :title="dialogTitle"
|
|
|
+ destroy-on-close
|
|
|
+ width="900px"
|
|
|
+ @close="handleDialogClose"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ >
|
|
|
+ <el-form :model="formData" label-width="100px">
|
|
|
+ <el-form-item label="新闻名称" required>
|
|
|
+ <el-input v-model="formData.newsName" placeholder="请输入新闻名称"/>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="发布时间" required>
|
|
|
+ <el-date-picker
|
|
|
+ v-model="formData.releaseTime"
|
|
|
+ type="date"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ placeholder="请选择发布时间"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 修改新闻封面上传组件 -->
|
|
|
+ <el-form-item label="新闻封面">
|
|
|
+ <div class="w-full">
|
|
|
+ <div class="flex items-center gap-4">
|
|
|
+ <input
|
|
|
+ ref="fileInputRef"
|
|
|
+ type="file"
|
|
|
+ accept="image/*"
|
|
|
+ @change="handleCoverUpload"
|
|
|
+ style="display: none"
|
|
|
+ />
|
|
|
+ <el-button @click="fileInputRef?.click()">
|
|
|
+ <Upload class="w-4 h-4 mr-1"/>
|
|
|
+ 选择图片
|
|
|
+ </el-button>
|
|
|
+ <el-button v-if="formData.newsUrl" type="danger" @click="clearCover">
|
|
|
+ 清除图片
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div v-if="formData.newsUrl" class="mt-2">
|
|
|
+ <el-image
|
|
|
+ :src=" formData.newsUrl.startsWith('http')||formData.newsUrl.startsWith('data:image') ? formData.newsUrl : BASE_URL + formData.newsUrl"
|
|
|
+ class="w-32 h-24 object-cover rounded border"
|
|
|
+ fit="cover"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="特别新闻">
|
|
|
+ <el-switch
|
|
|
+ v-model="formData.isSpecial"
|
|
|
+ :active-value="1"
|
|
|
+ :inactive-value="0"
|
|
|
+ active-text="是"
|
|
|
+ inactive-text="否"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="新闻详情" required>
|
|
|
+ <div class="w-full">
|
|
|
+ <!-- 添加编辑器就绪事件 -->
|
|
|
+ <QuillEditor
|
|
|
+ :ref="quillRef"
|
|
|
+ v-model:content="formData.newsDetails"
|
|
|
+ :options="quillOptions"
|
|
|
+ content-type="html"
|
|
|
+ style="height: 400px;"
|
|
|
+ @ready="onEditorReady"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <div class="flex justify-end gap-2 mt-12">
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleSave" :loading="uploadLoading">
|
|
|
+ {{ uploadLoading ? '保存中...' : '保存' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 添加图片预览对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="imagePreviewVisible"
|
|
|
+ title="图片预览"
|
|
|
+ width="60%"
|
|
|
+ :close-on-click-modal="true"
|
|
|
+ >
|
|
|
+ <div class="flex justify-center">
|
|
|
+ <img
|
|
|
+ :src="previewImageUrl"
|
|
|
+ class="max-w-full max-h-96 object-contain"
|
|
|
+ alt="预览图片"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <el-dialog
|
|
|
+ v-model="detailDialogVisible"
|
|
|
+ title="新闻详情"
|
|
|
+ width="70%"
|
|
|
+ destroy-on-close
|
|
|
+ :close-on-click-modal="true"
|
|
|
+ >
|
|
|
+ <QuillEditor
|
|
|
+ v-model:content="detailDialogContent"
|
|
|
+ :options="previewOptions"
|
|
|
+ content-type="html"
|
|
|
+ readonly
|
|
|
+ />
|
|
|
+ <template #footer>
|
|
|
+ <div class="flex justify-end">
|
|
|
+ <el-button type="primary" @click="detailDialogVisible = false">关闭</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.news-management {
|
|
|
+ background: #f5f5f5;
|
|
|
+ min-height: 100vh;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table) {
|
|
|
+ background: white;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-dialog) {
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.ql-editor) {
|
|
|
+ min-height: 300px;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.ql-toolbar) {
|
|
|
+ border-top: 1px solid #ccc;
|
|
|
+ border-left: 1px solid #ccc;
|
|
|
+ border-right: 1px solid #ccc;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.ql-container) {
|
|
|
+ border-bottom: 1px solid #ccc;
|
|
|
+ border-left: 1px solid #ccc;
|
|
|
+ border-right: 1px solid #ccc;
|
|
|
+}
|
|
|
+</style>
|