Forráskód Böngészése

feat(gwnrgl): 添加公司荣誉资质证书管理页面

- 实现证书列表展示功能,支持分页和筛选
- 添加证书类型、文件预览、创建和更新时间显示- 实现添加和编辑证书对话框,支持文件上传
- 支持批量删除和单个删除证书操作
- 添加文件预览功能,包括缩略图和大图查看- 实现表单验证和文件大小限制检查
- 集成Element Plus组件库和Lucide Vue图标库
- 添加响应式布局和样式优化
nahida 8 hónapja
szülő
commit
0d2d4fa514
1 módosított fájl, 566 hozzáadás és 0 törlés
  1. 566 0
      src/views/gwnrgl/ryzzgl/index.vue

+ 566 - 0
src/views/gwnrgl/ryzzgl/index.vue

@@ -0,0 +1,566 @@
+<template>
+  <div class="p-6 bg-gray-50 min-h-screen">
+    <!-- 页面标题 -->
+    <div class="mb-6">
+      <h1 class="text-3xl font-bold text-gray-800 mb-2">公司荣誉资质证书</h1>
+      <p class="text-gray-600">管理公司的各类荣誉证书和资质文件</p>
+    </div>
+
+    <!-- 操作栏 -->
+    <div class="mb-6 flex justify-between items-center">
+      <div class="flex gap-3">
+        <el-button type="primary" @click="showAddDialog" :icon="Plus">
+          添加证书
+        </el-button>
+        <el-button
+            type="danger"
+            :disabled="selectedIds.length === 0"
+            @click="handleBatchDelete"
+            :icon="Trash2"
+        >
+          批量删除 ({{ selectedIds.length }})
+        </el-button>
+      </div>
+      <div class="flex gap-3 items-center">
+        <el-button @click="refreshList" :icon="RefreshCw">刷新</el-button>
+      </div>
+    </div>
+
+    <!-- 证书列表 -->
+    <div class="bg-white rounded-lg shadow-sm">
+      <el-table
+          :data="certificates"
+          v-loading="loading"
+          @selection-change="handleSelectionChange"
+          stripe
+          class="w-full"
+      >
+        <el-table-column type="selection" width="55" />
+        <el-table-column prop="certificateType" label="证书类型" min-width="200">
+          <template #default="{ row }">
+            <div class="flex items-center gap-2">
+              <Award class="w-4 h-4 text-yellow-500" />
+              <span class="font-medium">{{ row.certificateType }}</span>
+            </div>
+          </template>
+        </el-table-column>
+
+        <!-- 改成预览按钮 -->
+        <el-table-column label="证书文件" min-width="150">
+          <template #default="{ row }">
+            <el-button
+                type="primary"
+                link
+                size="small"
+                @click="openFilePreview(row.fileList)"
+            >
+              预览文件 ({{ row.fileList?.length || 0 }})
+            </el-button>
+          </template>
+        </el-table-column>
+
+        <el-table-column prop="createTime" label="创建时间" width="180">
+          <template #default="{ row }">
+            <div class="flex items-center gap-2">
+              <Calendar class="w-4 h-4 text-gray-400" />
+              <span>{{ formatDate(row.createTime) }}</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="updateTime" label="更新时间" width="180">
+          <template #default="{ row }">
+            <div class="flex items-center gap-2">
+              <Clock class="w-4 h-4 text-gray-400" />
+              <span>{{ formatDate(row.updateTime) }}</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="150" fixed="right">
+          <template #default="{ row }">
+            <div class="flex gap-2">
+              <el-button
+                  type="primary"
+                  size="small"
+                  @click="showEditDialog(row)"
+                  :icon="Edit"
+                  circle
+              />
+              <el-button
+                  type="danger"
+                  size="small"
+                  @click="handleDelete(row)"
+                  :icon="Trash2"
+                  circle
+              />
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="p-4 flex justify-center">
+        <el-pagination
+            v-model:current-page="pagination.pageNum"
+            v-model:page-size="pagination.pageSize"
+            :page-sizes="[10, 20, 50, 100]"
+            :total="pagination.total"
+            layout="total, sizes, prev, pager, next, jumper"
+            @size-change="handleSizeChange"
+            @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+
+    <!-- 添加/编辑对话框 -->
+    <el-dialog
+        :title="dialogMode === 'add' ? '添加证书' : '编辑证书'"
+        v-model="dialogVisible"
+        width="600px"
+        :before-close="handleDialogClose"
+    >
+      <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          label-width="100px"
+          class="p-4"
+      >
+        <el-form-item label="证书类型" prop="certificateType">
+          <el-input
+              v-model="formData.certificateType"
+              placeholder="请输入证书类型"
+              :prefix-icon="Award"
+          />
+        </el-form-item>
+        <el-form-item label="证书文件" prop="fileList">
+          <div class="w-full">
+            <el-upload
+                ref="uploadRef"
+                :file-list="uploadFileList"
+                :on-change="handleFileChange"
+                :on-remove="handleFileRemove"
+                :before-upload="beforeUpload"
+                :auto-upload="false"
+                multiple
+                drag
+                class="w-full"
+            >
+              <div class="flex flex-col items-center justify-center py-8">
+                <Upload class="w-12 h-12 text-gray-400 mb-4" />
+                <div class="text-lg text-gray-600 mb-2">点击或拖拽文件到此处上传</div>
+                <div class="text-sm text-gray-400">支持多个文件同时上传</div>
+              </div>
+            </el-upload>
+            <!-- 已有文件列表 -->
+            <div v-if="formData.fileList && formData.fileList.length > 0" class="mt-4">
+              <div class="text-sm text-gray-600 mb-2">已有文件:</div>
+              <div class="flex flex-wrap gap-2">
+                <div
+                    v-for="(file, index) in formData.fileList"
+                    :key="file.id"
+                    class="relative group cursor-pointer"
+                    @click="previewSingleFile(file.url)"
+                >
+                  <img
+                      :src="BASE_URL + file.url"
+                      alt="证书缩略图"
+                      class="w-20 h-20 object-cover rounded border border-gray-200"
+                      @error="handleImageError"
+                  />
+                  <div
+                      class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center text-xs cursor-pointer hover:bg-red-600 opacity-0 group-hover:opacity-100 transition-opacity"
+                      @click.stop="removeExistingFile(index)"
+                  >
+                    ×
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="flex justify-end gap-3">
+          <el-button @click="handleDialogClose">取消</el-button>
+          <el-button type="primary" @click="handleSubmit" :loading="submitting">
+            {{ dialogMode === 'add' ? '添加' : '更新' }}
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 文件列表预览对话框 -->
+    <el-dialog
+        title="证书文件预览"
+        v-model="filePreviewVisible"
+        width="70%"
+    >
+      <div class="flex flex-wrap gap-4">
+        <div
+            v-for="(file, index) in previewFileList"
+            :key="index"
+            class="cursor-pointer"
+            @click="previewSingleFile(file.url)"
+        >
+          <img
+              :src="BASE_URL + file.url"
+              alt="证书缩略图"
+              class="w-32 h-32 object-cover rounded border border-gray-200 shadow-sm"
+              @error="handleImageError"
+          />
+        </div>
+        <span
+            v-if="!previewFileList || previewFileList.length === 0"
+            class="text-gray-400"
+        >
+          暂无文件
+        </span>
+      </div>
+    </el-dialog>
+
+    <!-- 单个文件大图预览对话框 -->
+    <el-dialog
+        title="文件预览"
+        v-model="previewVisible"
+        width="80%"
+        class="preview-dialog"
+    >
+      <div class="flex justify-center">
+        <img
+            v-if="previewUrl"
+            :src="BASE_URL + previewUrl"
+            alt="证书预览"
+            class="max-w-full max-h-96 object-contain"
+        />
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import {onMounted, reactive, ref} from 'vue'
+import {ElMessage, ElMessageBox} from 'element-plus'
+import {Award, Calendar, Clock, Edit, Plus, RefreshCw, Trash2, Upload} from 'lucide-vue-next'
+import request from '@/utils/request.js'
+
+// 基础URL
+const BASE_URL = import.meta.env.VITE_APP_BASE_URL
+
+// 响应式数据
+const loading = ref(false)
+const submitting = ref(false)
+const dialogVisible = ref(false)
+const previewVisible = ref(false)
+const previewUrl = ref('')
+const dialogMode = ref('add') // 'add' | 'edit'
+const selectedIds = ref([])
+const certificates = ref([])
+const uploadFileList = ref([])
+
+// 文件列表预览
+const filePreviewVisible = ref(false)
+const previewFileList = ref([])
+
+// 分页数据
+const pagination = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+})
+
+// 表单数据
+const formData = reactive({
+  id: '',
+  certificateType: '',
+  fileList: [],
+  multipartFile: []
+})
+
+// 表单引用
+const formRef = ref()
+const uploadRef = ref()
+
+// 表单验证规则
+const formRules = {
+  certificateType: [
+    { required: true, message: '请输入证书类型', trigger: 'blur' }
+  ]
+}
+
+// 获取证书列表
+const getList = async () => {
+  loading.value = true
+  try {
+    const res = await request.get('/qualificationCertificate/findByPage', {
+      params: {
+        pageNum: pagination.pageNum,
+        pageSize: pagination.pageSize
+      }
+    })
+    if (res.code !== 200) {
+      ElMessage.error(res.msg)
+      return
+    }
+    certificates.value = res.data.records || []
+    pagination.total = res.data.total || 0
+  } catch (error) {
+    ElMessage.error('获取数据失败')
+    console.error(error)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 格式化日期
+const formatDate = (dateString) => {
+  if (!dateString) return '-'
+  return new Date(dateString).toLocaleString('zh-CN')
+}
+
+// 图片加载错误处理
+const handleImageError = (event) => {
+  event.target.src =
+      'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik0yMCAyMEg0NFY0NEgyMFYyMFoiIHN0cm9rZT0iIzlDQTNBRiIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIi8+CjxwYXRoIGQ9Ik0yOCAzMkwzMiAyOEwzNiAzMkw0MCAyOEw0NCAzMlY0MEgyOFYzMloiIGZpbGw9IiM5Q0EzQUYiLz4KPC9zdmc+'
+}
+
+// 打开整行文件预览
+const openFilePreview = (files) => {
+  previewFileList.value = files || []
+  filePreviewVisible.value = true
+}
+
+// 点击单个文件时再进入大图预览
+const previewSingleFile = (url) => {
+  previewUrl.value = url
+  previewVisible.value = true
+}
+
+// 显示添加对话框
+const showAddDialog = () => {
+  dialogMode.value = 'add'
+  resetForm()
+  dialogVisible.value = true
+}
+
+// 显示编辑对话框
+const showEditDialog = (row) => {
+  dialogMode.value = 'edit'
+  formData.id = row.id
+  formData.certificateType = row.certificateType
+  formData.fileList = [...(row.fileList || [])]
+  formData.multipartFile = []
+  uploadFileList.value = []
+  dialogVisible.value = true
+}
+
+// 重置表单
+const resetForm = () => {
+  formData.id = ''
+  formData.certificateType = ''
+  formData.fileList = []
+  formData.multipartFile = []
+  uploadFileList.value = []
+  formRef.value?.resetFields()
+}
+
+// 关闭对话框
+const handleDialogClose = () => {
+  resetForm()
+  dialogVisible.value = false
+}
+
+// 文件上传前检查
+const beforeUpload = (file) => {
+  const isValidSize = file.size / 1024 / 1024 < 10 // 10MB
+  if (!isValidSize) {
+    ElMessage.error('文件大小不能超过 10MB!')
+    return false
+  }
+  return false // 阻止自动上传
+}
+
+// 文件变化处理
+const handleFileChange = (file, fileList) => {
+  uploadFileList.value = fileList
+  formData.multipartFile = fileList.map(item => item.raw).filter(Boolean)
+}
+
+// 移除文件
+const handleFileRemove = (file, fileList) => {
+  uploadFileList.value = fileList
+  formData.multipartFile = fileList.map(item => item.raw).filter(Boolean)
+}
+
+// 移除已有文件
+const removeExistingFile = (index) => {
+  formData.fileList.splice(index, 1)
+}
+
+// 提交表单
+const handleSubmit = async () => {
+  try {
+    await formRef.value.validate()
+    submitting.value = true
+
+    const formDataToSend = new FormData()
+    formDataToSend.append('certificateType', formData.certificateType)
+
+    // 添加新上传的文件
+    formData.multipartFile.forEach(file => {
+      formDataToSend.append('multipartFile', file)
+    })
+
+    if (dialogMode.value === 'add') {
+      await save(formDataToSend)
+    } else {
+      formDataToSend.append('id', formData.id)
+      formDataToSend.append('fileListJson', JSON.stringify(formData.fileList))
+      await update(formDataToSend)
+    }
+
+    dialogVisible.value = false
+    await getList()
+  } catch (error) {
+    console.error('表单验证失败:', error)
+  } finally {
+    submitting.value = false
+  }
+}
+
+// 保存证书
+const save = async (formDataToSend) => {
+  const res = await request.post('/qualificationCertificate/save', formDataToSend, {
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
+  if (res.code !== 200) {
+    ElMessage.error(res.msg)
+    return
+  }
+  getList()
+  ElMessage.success(res.msg)
+}
+
+// 更新证书
+const update = async (formDataToSend) => {
+  const res = await request.post('/qualificationCertificate/update', formDataToSend, {
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
+  if (res.code !== 200) {
+    ElMessage.error(res.msg)
+    return
+  }
+  getList()
+  ElMessage.success(res.msg)
+}
+
+// 删除单个证书
+const handleDelete = async (row) => {
+  try {
+    await ElMessageBox.confirm(
+        `确定要删除证书"${row.certificateType}"吗?`,
+        '确认删除',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+    )
+    await deleteBatch([row.id])
+    await getList()
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除失败:', error)
+    }
+  }
+}
+
+// 批量删除
+const handleBatchDelete = async () => {
+  if (selectedIds.value.length === 0) {
+    ElMessage.warning('请选择要删除的证书')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+        `确定要删除选中的 ${selectedIds.value.length} 个证书吗?`,
+        '确认批量删除',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+    )
+    await deleteBatch(selectedIds.value)
+    selectedIds.value = []
+    await getList()
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('批量删除失败:', error)
+    }
+  }
+}
+
+// 删除证书API
+const deleteBatch = async (ids) => {
+  const res = await request.post('/qualificationCertificate/deleteBatch', ids)
+  if (res.code !== 200) {
+    ElMessage.error(res.msg)
+    return
+  }
+  ElMessage.success(res.msg)
+}
+
+// 选择变化处理
+const handleSelectionChange = (selection) => {
+  selectedIds.value = selection.map(item => item.id)
+}
+
+// 分页大小变化
+const handleSizeChange = (size) => {
+  pagination.pageSize = size
+  pagination.pageNum = 1
+  getList()
+}
+
+// 当前页变化
+const handleCurrentChange = (page) => {
+  pagination.pageNum = page
+  getList()
+}
+
+// 刷新列表
+const refreshList = () => {
+  pagination.pageNum = 1
+  getList()
+}
+
+// 预览文件
+const previewFile = (url) => {
+  previewUrl.value = url
+  previewVisible.value = true
+}
+
+// 组件挂载时获取数据
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped>
+.preview-dialog :deep(.el-dialog__body) {
+  padding: 20px;
+}
+
+:deep(.el-upload-dragger) {
+  width: 100%;
+}
+
+:deep(.el-table .el-table__cell) {
+  padding: 12px 0;
+}
+</style>