Преглед на файлове

feat(gwnrgl): 实现新闻发布管理功能

- 添加新闻列表展示与分页功能
- 实现新闻添加、编辑、删除操作
- 集成富文本编辑器用于新闻详情编辑- 支持新闻封面图片上传与预览
- 添加图片变化追踪与处理逻辑- 实现新闻详情查看功能
- 支持批量删除新闻数据
- 添加特别新闻标识开关- 集成图片预览对话框组件
- 实现新闻发布日期选择功能
nahida преди 8 месеца
родител
ревизия
1ff5dd4c35
променени са 1 файла, в които са добавени 749 реда и са изтрити 0 реда
  1. 749 0
      src/views/gwnrgl/xwgl/index.vue

+ 749 - 0
src/views/gwnrgl/xwgl/index.vue

@@ -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>