浏览代码

Merge remote-tracking branch 'origin/master'

hanchaolong 2 天之前
父节点
当前提交
ede113c55f

+ 22 - 0
jd-logistics-ui-v3/src/api/file.js

@@ -0,0 +1,22 @@
+import request from '@/utils/request' // 关键:导入 request
+
+/**
+ * 根据文件路径获取图片的 Base64 Data URL
+ * @param {string} filePath 文件路径
+ * @returns {Promise<string>} 完整的 Data URL,例如 "data:image/jpeg;base64,/9j/4AAQ..."
+ */
+export function getFileBase64(filePath) {
+    return request({
+        url: '/file/download',
+        method: 'get',
+        params: { fileName: filePath }, // 根据实际接口参数调整
+        responseType: 'json' // 重要:后端返回 JSON,直接解析
+    }).then(response => {
+        if (response.code === 200) {
+            // response.data 已经是完整的 data URL
+            return response.data
+        } else {
+            throw new Error(response.msg || '获取图片失败')
+        }
+    })
+}

+ 226 - 147
jd-logistics-ui-v3/src/components/ImageUpload/index.vue

@@ -1,25 +1,46 @@
 <template>
   <div class="component-upload-image">
     <el-upload
-      multiple
-      :disabled="disabled"
-      :action="uploadImgUrl"
-      list-type="picture-card"
-      :on-success="handleUploadSuccess"
-      :before-upload="handleBeforeUpload"
-      :data="data"
-      :limit="limit"
-      :on-error="handleUploadError"
-      :on-exceed="handleExceed"
-      ref="imageUpload"
-      :before-remove="handleDelete"
-      :show-file-list="true"
-      :headers="headers"
-      :file-list="fileList"
-      :on-preview="handlePictureCardPreview"
-      :class="{ hide: fileList.length >= limit }"
+        multiple
+        :disabled="disabled"
+        :action="uploadImgUrl"
+        list-type="picture-card"
+        :on-success="handleUploadSuccess"
+        :before-upload="handleBeforeUpload"
+        :data="data"
+        :limit="limit"
+        :on-error="handleUploadError"
+        :on-exceed="handleExceed"
+        ref="imageUpload"
+        :before-remove="handleDelete"
+        :show-file-list="true"
+        :headers="headers"
+        :file-list="fileList"
+        :on-preview="handlePictureCardPreview"
+        :class="{ hide: fileList.length >= limit }"
     >
-      <el-icon class="avatar-uploader-icon"><plus /></el-icon>
+      <!-- 自定义文件项模板,包含删除和预览按钮 -->
+      <template #file="{ file }">
+        <div class="el-upload-list__item" :class="`is-${file.status}`">
+          <img
+              class="el-upload-list__item-thumbnail"
+              :src="file.baseUrl"
+              alt=""
+              @click="handlePictureCardPreview(file)"
+          />
+          <!-- 操作按钮区域 -->
+          <span class="el-upload-list__item-actions">
+            <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
+              <el-icon><ZoomIn /></el-icon>
+            </span>
+            <span class="el-upload-list__item-delete" @click="handleDelete(file)">
+              <el-icon><Delete /></el-icon>
+            </span>
+          </span>
+        </div>
+      </template>
+      <!-- 上传按钮 -->
+      <el-icon class="avatar-uploader-icon"><Plus /></el-icon>
     </el-upload>
     <!-- 上传提示 -->
     <div class="el-upload__tip" v-if="showTip && !disabled">
@@ -28,110 +49,89 @@
         大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
       </template>
       <template v-if="fileType">
-        格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
+        格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
       </template>
       的文件
     </div>
 
-    <el-dialog
-      v-model="dialogVisible"
-      title="预览"
-      width="800px"
-      append-to-body
-    >
-      <img
-        :src="dialogImageUrl"
-        style="display: block; max-width: 100%; margin: 0 auto"
-      />
+    <el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
+      <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
     </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { getToken } from "@/utils/auth"
+import { getToken } from '@/utils/auth'
 import Sortable from 'sortablejs'
+import { getFileBase64 } from '@/api/file' // 确保路径正确
+import { Plus, ZoomIn, Delete } from '@element-plus/icons-vue'
 
 const props = defineProps({
   modelValue: [String, Object, Array],
-  // 上传接口地址
-  action: {
-    type: String,
-    default: "/file/upload"
-  },
-  // 上传携带的参数
-  data: {
-    type: Object
-  },
-  // 图片数量限制
-  limit: {
-    type: Number,
-    default: 5
-  },
-  // 大小限制(MB)
-  fileSize: {
-    type: Number,
-    default: 5
-  },
-  // 文件类型, 例如['png', 'jpg', 'jpeg']
-  fileType: {
-    type: Array,
-    default: () => ["png", "jpg", "jpeg"]
-  },
-  // 是否显示提示
-  isShowTip: {
-    type: Boolean,
-    default: true
-  },
-  // 禁用组件(仅查看图片)
-  disabled: {
-    type: Boolean,
-    default: false
-  },
-  // 拖动排序
-  drag: {
-    type: Boolean,
-    default: true
-  }
+  action: { type: String, default: '/file/upload' },
+  data: { type: Object },
+  limit: { type: Number, default: 5 },
+  fileSize: { type: Number, default: 5 },
+  fileType: { type: Array, default: () => ['png', 'jpg', 'jpeg'] },
+  isShowTip: { type: Boolean, default: true },
+  disabled: { type: Boolean, default: false },
+  drag: { type: Boolean, default: true }
 })
 
 const { proxy } = getCurrentInstance()
-const emit = defineEmits()
-const number = ref(0)
-const uploadList = ref([])
-const dialogImageUrl = ref("")
+const emit = defineEmits(['update:modelValue'])
+
+const uploadingCount = ref(0)
+const dialogImageUrl = ref('')
 const dialogVisible = ref(false)
 const baseUrl = import.meta.env.VITE_APP_BASE_API
-const uploadImgUrl = ref(baseUrl + props.action) // 上传的图片服务器地址
-const headers = ref({ Authorization: "Bearer " + getToken() })
+const uploadImgUrl = ref(baseUrl + props.action)
+const headers = ref({ Authorization: 'Bearer ' + getToken() })
 const fileList = ref([])
-const showTip = computed(
-  () => props.isShowTip && (props.fileType || props.fileSize)
-)
+const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize))
+
+// 缓存 base64
+const base64Cache = new Map()
 
-watch(() => props.modelValue, val => {
+// 监听外部 modelValue 变化(编辑回显)
+watch(() => props.modelValue, async (val) => {
   if (val) {
-    // 首先将值转为数组
-    const list = Array.isArray(val) ? val : props.modelValue.split(",")
-    // 然后将数组转为对象数组
-    fileList.value = list.map(item => {
-      if (typeof item === "string") {
-        item = { name: item, url: item }
+    const paths = Array.isArray(val) ? val : (val ? val.split(',').filter(p => p) : [])
+    const results = await Promise.all(paths.map(async (path) => {
+      if (!base64Cache.has(path)) {
+        try {
+          const base64 = await getFileBase64(path)
+          base64Cache.set(path, base64)
+        } catch (e) {
+          console.error(`获取图片 ${path} 的 base64 失败:`, e)
+          base64Cache.set(path, '')
+        }
       }
-      return item
-    })
+      const base64 = base64Cache.get(path)
+      return {
+        uid: Symbol('uid'),          // 生成唯一 uid,避免与上传文件冲突
+        name: path,
+        url:  path,         // 优先使用 base64,否则用原始路径(可能不显示)
+        rawUrl: path,
+        baseUrl:base64,
+        status: 'success'
+      }
+    }))
+    fileList.value = results
   } else {
     fileList.value = []
-    return []
+    base64Cache.clear()
   }
-},{ deep: true, immediate: true })
+}, { deep: true, immediate: true })
 
-// 上传前loading加载
+// 上传前校验
 function handleBeforeUpload(file) {
+  // 文件类型校验
   let isImg = false
   if (props.fileType.length) {
-    let fileExtension = ""
-    if (file.name.lastIndexOf(".") > -1) {
-      fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1)
+    let fileExtension = ''
+    if (file.name.lastIndexOf('.') > -1) {
+      fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
     }
     isImg = props.fileType.some(type => {
       if (file.type.indexOf(type) > -1) return true
@@ -139,10 +139,10 @@ function handleBeforeUpload(file) {
       return false
     })
   } else {
-    isImg = file.type.indexOf("image") > -1
+    isImg = file.type.indexOf('image') > -1
   }
   if (!isImg) {
-    proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`)
+    proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join('/')}图片格式文件!`)
     return false
   }
   if (file.name.includes(',')) {
@@ -152,12 +152,13 @@ function handleBeforeUpload(file) {
   if (props.fileSize) {
     const isLt = file.size / 1024 / 1024 < props.fileSize
     if (!isLt) {
-      proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`)
+      proxy.$modal.msgError(`上传图片大小不能超过 ${props.fileSize} MB!`)
       return false
     }
   }
-  proxy.$modal.loading("正在上传图片,请稍候...")
-  number.value++
+  proxy.$modal.loading('正在上传图片,请稍候...')
+  uploadingCount.value++
+  return true
 }
 
 // 文件个数超出
@@ -166,88 +167,166 @@ function handleExceed() {
 }
 
 // 上传成功回调
-function handleUploadSuccess(res, file) {
+async function handleUploadSuccess(res, file) {
   if (res.code === 200) {
-    uploadList.value.push({ name: res.data.url, url: res.data.url })
-    uploadedSuccessfully()
+    const rawUrl = res.data.url
+    // 立即添加文件到 fileList,使用原始路径占位(可能会裂图)
+    const newItem = {
+      uid: file.uid,               // 使用 el-upload 的 uid,保证删除时匹配
+      name: rawUrl,
+      url: rawUrl,                 // 先用原始路径,等 base64 获取后替换
+      rawUrl: rawUrl,
+      baseUrl:rawUrl,
+      status: 'uploading'          // 标记正在获取 base64
+    }
+    fileList.value.push(newItem)
+
+    // 获取 base64(现在返回完整的 data URL)
+    try {
+      const base64 = await getFileBase64(rawUrl)
+      console.log('获取到 base64 预览:', base64 ? base64.substring(0, 100) + '...' : '空')
+
+      // 更新对应项的 url 和状态(通过重新赋值数组触发响应式更新)
+      fileList.value = fileList.value.map(item => {
+        if (item.uid === file.uid) {
+          return {
+            ...item,
+            baseUrl: base64,
+            status: 'success'
+          }
+        }
+        return item
+      })
+      // 存入缓存
+      base64Cache.set(rawUrl, base64)
+    } catch (error) {
+      console.error('获取图片 base64 失败:', error)
+      // 更新状态为失败,可显示裂图或默认占位图
+      fileList.value = fileList.value.map(item => {
+        if (item.uid === file.uid) {
+          return {
+            ...item,
+            status: 'error'
+          }
+        }
+        return item
+      })
+    } finally {
+      uploadingCount.value--
+      if (uploadingCount.value === 0) {
+        proxy.$modal.closeLoading()
+      }
+    }
+
+    // 更新 v-model 值(原始路径拼接)
+    emit('update:modelValue', listToString(fileList.value))
   } else {
-    number.value--
-    proxy.$modal.closeLoading()
+    // 上传失败
     proxy.$modal.msgError(res.msg)
     proxy.$refs.imageUpload.handleRemove(file)
-    uploadedSuccessfully()
+    uploadingCount.value--
+    if (uploadingCount.value === 0) {
+      proxy.$modal.closeLoading()
+    }
   }
 }
 
 // 删除图片
 function handleDelete(file) {
-  const findex = fileList.value.map(f => f.name).indexOf(file.name)
-  if (findex > -1 && uploadList.value.length === number.value) {
-    fileList.value.splice(findex, 1)
-    emit("update:modelValue", listToString(fileList.value))
-    return false
+  // 优先使用 rawUrl 查找(确保唯一性)
+  const rawUrl = file.rawUrl || file.name || file.url
+  let index = fileList.value.findIndex(f => f.rawUrl === rawUrl)
+
+  // 如果找不到,尝试用 uid(针对上传的文件)
+  if (index === -1 && file.uid) {
+    index = fileList.value.findIndex(f => f.uid === file.uid)
   }
-}
 
-// 上传结束处理
-function uploadedSuccessfully() {
-  if (number.value > 0 && uploadList.value.length === number.value) {
-    fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
-    uploadList.value = []
-    number.value = 0
-    emit("update:modelValue", listToString(fileList.value))
-    proxy.$modal.closeLoading()
+  if (index > -1) {
+    const removed = fileList.value[index]
+    if (removed.rawUrl) {
+      base64Cache.delete(removed.rawUrl)
+    }
+    fileList.value.splice(index, 1)
+    emit('update:modelValue', listToString(fileList.value))
+  } else {
+    console.warn('未找到要删除的文件', file)
   }
+  // 阻止 el-upload 默认删除行为(已经手动处理)
+  return false
 }
 
 // 上传失败
-function handleUploadError() {
-  proxy.$modal.msgError("上传图片失败")
+function handleUploadError(err, file) {
+  console.error('上传错误', err)
+  proxy.$modal.msgError('上传图片失败')
   proxy.$modal.closeLoading()
+  uploadingCount.value--
+  if (uploadingCount.value === 0) {
+    proxy.$modal.closeLoading()
+  }
 }
 
-// 预览
+// 预览大图
 function handlePictureCardPreview(file) {
-  dialogImageUrl.value = file.url
+  // 如果有 base64 优先使用,否则用原始路径
+  dialogImageUrl.value = file.baseUrl || file.rawUrl
   dialogVisible.value = true
 }
 
-// 对象转成指定字符串分隔
-function listToString(list, separator) {
-  let strs = ""
-  separator = separator || ","
-  for (let i in list) {
-    if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
-      strs += list[i].url.replace(baseUrl, "") + separator
-    }
-  }
-  return strs != "" ? strs.substr(0, strs.length - 1) : ""
+// 文件列表转字符串(使用原始路径)
+function listToString(list, separator = ',') {
+  return list.map(item => item.rawUrl).filter(Boolean).join(separator)
 }
 
-// 初始化拖拽排序
+// 拖拽排序(可选)
 onMounted(() => {
   if (props.drag && !props.disabled) {
     nextTick(() => {
       const element = proxy.$refs.imageUpload?.$el?.querySelector('.el-upload-list')
-      Sortable.create(element, {
-        onEnd: (evt) => {
-          const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
-          fileList.value.splice(evt.newIndex, 0, movedItem)
-          emit('update:modelValue', listToString(fileList.value))
-        }
-      })
+      if (element) {
+        Sortable.create(element, {
+          onEnd: (evt) => {
+            const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
+            fileList.value.splice(evt.newIndex, 0, movedItem)
+            emit('update:modelValue', listToString(fileList.value))
+          }
+        })
+      }
     })
   }
 })
 </script>
 
 <style scoped lang="scss">
-// .el-upload--picture-card 控制加号部分
-:deep(.hide .el-upload--picture-card) {
-    display: none;
-}
+:deep(.hide .el-upload--picture-card) { display: none; }
+:deep(.el-upload.el-upload--picture-card.is-disabled) { display: none !important; }
 
-:deep(.el-upload.el-upload--picture-card.is-disabled) {
-  display: none !important;
-} 
+/* 自定义文件项样式,确保操作按钮在 hover 时显示 */
+:deep(.el-upload-list__item) {
+  position: relative;
+  &:hover .el-upload-list__item-actions {
+    opacity: 1;
+  }
+  .el-upload-list__item-actions {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    transition: opacity 0.3s;
+    .el-upload-list__item-preview,
+    .el-upload-list__item-delete {
+      color: #fff;
+      font-size: 20px;
+      margin: 0 8px;
+      cursor: pointer;
+    }
+  }
+}
 </style>

+ 48 - 17
jd-logistics-ui-v3/src/views/logistics/banner/index.vue

@@ -107,7 +107,7 @@
       <el-table-column label="轮播图名称" align="center" prop="bannerName" />
       <el-table-column label="轮播图" align="center" prop="imageUrl" width="100">
         <template #default="scope">
-          <image-preview :src="scope.row.imageUrl" :width="50" :height="50"/>
+          <img :src="scope.row.imageBase64 || ''" :width="50" :height="50"/>
         </template>
       </el-table-column>
       <el-table-column label="跳转链接" align="center" prop="linkUrl"  width="150" :show-overflow-tooltip="true"/>
@@ -153,10 +153,12 @@
           <el-input v-model="form.bannerName" placeholder="请输入轮播图名称" />
         </el-form-item>
         <el-form-item label="轮播图" prop="imageUrl">
-          <image-upload v-model="form.imageUrl" :limit="1"/>
-        </el-form-item>
-        <el-form-item label="跳转链接" prop="linkUrl">
-          <el-input v-model="form.linkUrl" placeholder="请输入跳转链接" />
+          <image-upload
+              v-model="form.imageUrl"
+              :limit="1"
+              :preview-base64="form.imagePreviewBase64"
+              @update:preview-base64="val => form.imagePreviewBase64 = val"
+          />
         </el-form-item>
 <!--        <el-form-item label="排序值" prop="sortOrder">
           <el-input v-model="form.sortOrder" placeholder="请输入排序值" />
@@ -179,6 +181,10 @@
             >{{dict.label}}</el-radio>
           </el-radio-group>
         </el-form-item>
+
+        <el-form-item label="跳转链接" >
+          <el-input v-model="form.linkUrl" placeholder="请输入跳转链接" />
+        </el-form-item>
 <!--        <el-form-item label="备注" prop="remark">
           <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
         </el-form-item>
@@ -204,6 +210,7 @@
 
 <script setup name="Banner">
 import { listBanner, getBanner, delBanner, addBanner, updateBanner } from "@/api/logistics/banner"
+import { getFileBase64 } from "@/api/file" // 新增:下载接口
 
 const { proxy } = getCurrentInstance()
 const { banner_status, banner_sys_type } = proxy.useDict('banner_status', 'banner_sys_type')
@@ -218,6 +225,7 @@ const multiple = ref(true)
 const total = ref(0)
 const title = ref("")
 
+
 const data = reactive({
   form: {},
   queryParams: {
@@ -253,13 +261,28 @@ const data = reactive({
 const { queryParams, form, rules } = toRefs(data)
 
 /** 查询轮播图列表 */
-function getList() {
+/** 查询轮播图列表 */
+async function getList() {
   loading.value = true
-  listBanner(queryParams.value).then(response => {
-    bannerList.value = response.rows
-    total.value = response.total
-    loading.value = false
+  const response = await listBanner(queryParams.value)
+  const rows = response.rows
+  // 并发获取每个图片的 base64
+  const promises = rows.map(async (item) => {
+    if (item.imageUrl) {
+      try {
+        const base64 = await getFileBase64(item.imageUrl)
+        item.imageBase64 = base64
+      } catch (error) {
+        console.error('获取图片Base64失败', error)
+        item.imageBase64 = '' // 可设置默认图片
+      }
+    }
+    return item
   })
+  await Promise.all(promises)
+  bannerList.value = rows
+  total.value = response.total
+  loading.value = false
 }
 
 // 取消按钮
@@ -274,6 +297,7 @@ function reset() {
     bannerId: null,
     bannerName: null,
     imageUrl: null,
+    imagePreviewBase64: null, // 重置
     linkUrl: null,
     sortOrder: null,
     isActive: null,
@@ -317,16 +341,23 @@ function handleAdd() {
 }
 
 /** 修改按钮操作 */
-function handleUpdate(row) {
+async function handleUpdate(row) {
   reset()
   const _bannerId = row.bannerId || ids.value
-  getBanner(_bannerId).then(response => {
-    form.value = response.data
-    open.value = true
-    title.value = "修改轮播图"
-  })
+  const response = await getBanner(_bannerId)
+  form.value = response.data
+  // 如果已有图片,获取其 base64 用于预览
+  if (form.value.imageUrl) {
+    try {
+      const base64 = await getFileBase64(form.value.imageUrl)
+      form.value.imagePreviewBase64 = base64
+    } catch (error) {
+      console.error('获取预览图片失败', error)
+    }
+  }
+  open.value = true
+  title.value = "修改轮播图"
 }
-
 /** 提交按钮 */
 function submitForm() {
   proxy.$refs["bannerRef"].validate(valid => {