3 Revize 700b0e5fa7 ... 4d8f229aee

Autor SHA1 Zpráva Datum
  zengjun 4d8f229aee Merge remote-tracking branch 'origin/testing' into testing před 6 měsíci
  zengjun d241b662e7 Merge branch 'zengjun/计件/20250925加工拍照任务' into testing před 6 měsíci
  zengjun 21846a3751 添加图片上传前压缩到1MB内 před 6 měsíci

+ 317 - 0
src/utils/imageCompression.ts

@@ -0,0 +1,317 @@
+/**
+ * 图片压缩工具
+ * 用于压缩大于1MB的图片到0.8MB-1MB之间,保持原文件格式
+ */
+
+/**
+ * 压缩图片到目标大小范围
+ * @param file - 原始图片文件
+ * @param targetMinSize - 目标最小大小(字节)
+ * @param targetMaxSize - 目标最大大小(字节)
+ * @param quality - 压缩质量(0-1),默认0.9
+ * @returns Promise<File> - 压缩后的文件
+ */
+async function compressImageToTargetSize(
+  file: File,
+  targetMinSize: number,
+  targetMaxSize: number,
+  quality: number = 0.9
+): Promise<File> {
+  return new Promise((resolve, reject) => {
+    const canvas = document.createElement('canvas')
+    const ctx = canvas.getContext('2d')
+    const img = new Image()
+
+    img.onload = () => {
+      try {
+        const originalWidth = img.width
+        const originalHeight = img.height
+        
+        // 先尝试保持原尺寸,只调整质量
+        canvas.width = originalWidth
+        canvas.height = originalHeight
+
+        if (ctx) {
+          ctx.imageSmoothingEnabled = true
+          ctx.imageSmoothingQuality = 'high'
+        }
+
+        // 绘制原始尺寸的图片
+        ctx?.drawImage(img, 0, 0, originalWidth, originalHeight)
+
+        // 转换为blob
+        canvas.toBlob(
+          (blob) => {
+            if (!blob) {
+              reject(new Error('图片压缩失败'))
+              return
+            }
+
+            // 检查压缩结果是否在目标范围内
+            if (blob.size >= targetMinSize && blob.size <= targetMaxSize) {
+              const compressedFile = new File([blob], file.name, {
+                type: file.type,
+                lastModified: Date.now()
+              })
+              resolve(compressedFile)
+            } else if (blob.size > targetMaxSize) {
+              // 如果还是太大,需要调整尺寸
+              compressWithSizeAdjustment(file, img, targetMinSize, targetMaxSize, quality, resolve, reject)
+            } else {
+              // 如果太小了,提高质量重新压缩
+              compressWithHigherQuality(file, img, targetMinSize, targetMaxSize, quality, resolve, reject)
+            }
+          },
+          file.type,
+          quality
+        )
+      } catch (error) {
+        reject(new Error('图片压缩过程中发生错误: ' + error.message))
+      }
+    }
+
+    img.onerror = () => {
+      reject(new Error('图片加载失败'))
+    }
+
+    // 创建图片URL
+    const reader = new FileReader()
+    reader.onload = (e) => {
+      img.src = e.target?.result as string
+    }
+    reader.onerror = () => {
+      reject(new Error('文件读取失败'))
+    }
+    reader.readAsDataURL(file)
+  })
+}
+
+/**
+ * 通过调整尺寸进行压缩
+ */
+function compressWithSizeAdjustment(
+  file: File,
+  img: HTMLImageElement,
+  targetMinSize: number,
+  targetMaxSize: number,
+  quality: number,
+  resolve: (file: File) => void,
+  reject: (error: Error) => void
+) {
+  const canvas = document.createElement('canvas')
+  const ctx = canvas.getContext('2d')
+  
+  const originalWidth = img.width
+  const originalHeight = img.height
+  
+  // 计算目标尺寸,使用更保守的缩放策略
+  const targetSize = (targetMinSize + targetMaxSize) / 2
+  const sizeRatio = targetSize / file.size
+  
+  // 使用更温和的缩放比例,避免过度压缩
+  let scale = Math.sqrt(sizeRatio) * 1.2 // 比理论值稍大一些,避免过度压缩
+  
+  // 确保缩放比例在合理范围内
+  scale = Math.max(0.5, Math.min(1.0, scale))
+  
+  const targetWidth = Math.floor(originalWidth * scale)
+  const targetHeight = Math.floor(originalHeight * scale)
+  
+  canvas.width = targetWidth
+  canvas.height = targetHeight
+  
+  if (ctx) {
+    ctx.imageSmoothingEnabled = true
+    ctx.imageSmoothingQuality = 'high'
+  }
+  
+  // 绘制缩放后的图片
+  ctx?.drawImage(img, 0, 0, targetWidth, targetHeight)
+  
+  canvas.toBlob(
+    (blob) => {
+      if (!blob) {
+        reject(new Error('图片压缩失败'))
+        return
+      }
+      
+      const compressedFile = new File([blob], file.name, {
+        type: file.type,
+        lastModified: Date.now()
+      })
+      
+      // 检查结果,如果还是太大,继续调整
+      if (blob.size > targetMaxSize && scale > 0.5) {
+        // 递归调用,进一步缩小
+        const newScale = scale * 0.9
+        const newWidth = Math.floor(originalWidth * newScale)
+        const newHeight = Math.floor(originalHeight * newScale)
+        
+        canvas.width = newWidth
+        canvas.height = newHeight
+        ctx?.drawImage(img, 0, 0, newWidth, newHeight)
+        
+        canvas.toBlob(
+          (newBlob) => {
+            if (newBlob) {
+              const newFile = new File([newBlob], file.name, {
+                type: file.type,
+                lastModified: Date.now()
+              })
+              resolve(newFile)
+            } else {
+              resolve(compressedFile) // 如果二次压缩失败,返回第一次的结果
+            }
+          },
+          file.type,
+          quality
+        )
+      } else {
+        resolve(compressedFile)
+      }
+    },
+    file.type,
+    quality
+  )
+}
+
+/**
+ * 通过提高质量重新压缩(当压缩结果太小时)
+ */
+function compressWithHigherQuality(
+  file: File,
+  img: HTMLImageElement,
+  targetMinSize: number,
+  targetMaxSize: number,
+  currentQuality: number,
+  resolve: (file: File) => void,
+  reject: (error: Error) => void
+) {
+  const canvas = document.createElement('canvas')
+  const ctx = canvas.getContext('2d')
+  
+  canvas.width = img.width
+  canvas.height = img.height
+  
+  if (ctx) {
+    ctx.imageSmoothingEnabled = true
+    ctx.imageSmoothingQuality = 'high'
+  }
+  
+  // 提高质量,但不超过0.95
+  const newQuality = Math.min(0.95, currentQuality + 0.1)
+  
+  ctx?.drawImage(img, 0, 0, img.width, img.height)
+  
+  canvas.toBlob(
+    (blob) => {
+      if (!blob) {
+        reject(new Error('图片压缩失败'))
+        return
+      }
+      
+      const compressedFile = new File([blob], file.name, {
+        type: file.type,
+        lastModified: Date.now()
+      })
+      
+      // 如果提高质量后仍然太小,或者已经达到最高质量,直接返回
+      if (blob.size < targetMinSize && newQuality < 0.95) {
+        compressWithHigherQuality(file, img, targetMinSize, targetMaxSize, newQuality, resolve, reject)
+      } else {
+        resolve(compressedFile)
+      }
+    },
+    file.type,
+    newQuality
+  )
+}
+
+/**
+ * 压缩图片
+ * @param file - 原始图片文件
+ * @param maxSize - 最大文件大小(字节),默认1MB
+ * @param quality - 压缩质量(0-1),默认0.9
+ * @returns Promise<File> - 压缩后的文件
+ */
+export async function compressImage(
+  file: File,
+  maxSize: number = 1 * 1024 * 1024,
+  quality: number = 0.9
+): Promise<File> {
+  // 如果文件小于等于最大限制,直接返回原文件
+  if (file.size <= maxSize) {
+    return file
+  }
+
+  // 对于大于1MB的文件,设置目标大小为0.8MB-1MB之间
+  const targetMinSize = 800 * 1024 // 800KB
+  const targetMaxSize = maxSize // 1MB
+  return compressImageToTargetSize(file, targetMinSize, targetMaxSize, quality)
+}
+
+/**
+ * 批量压缩图片
+ * @param files - 图片文件数组
+ * @param maxSize - 最大文件大小(字节),默认1MB
+ * @param quality - 压缩质量(0-1),默认0.8
+ * @returns Promise<File[]> - 压缩后的文件数组
+ */
+export async function compressImages(
+  files: File[],
+  maxSize: number = 1 * 1024 * 1024,
+  quality: number = 0.8
+): Promise<File[]> {
+  const results: File[] = []
+  
+  for (const file of files) {
+    try {
+      const compressedFile = await compressImage(file, maxSize, quality)
+      results.push(compressedFile)
+    } catch (error) {
+      console.error(`压缩文件 ${file.name} 失败:`, error)
+      // 压缩失败时保留原文件
+      results.push(file)
+    }
+  }
+  
+  return results
+}
+
+/**
+ * 获取图片信息
+ * @param file - 图片文件
+ * @returns Promise<{width: number, height: number, size: number, type: string}>
+ */
+export function getImageInfo(file: File): Promise<{
+  width: number
+  height: number
+  size: number
+  type: string
+}> {
+  return new Promise((resolve, reject) => {
+    const img = new Image()
+    
+    img.onload = () => {
+      resolve({
+        width: img.width,
+        height: img.height,
+        size: file.size,
+        type: file.type
+      })
+    }
+    
+    img.onerror = () => {
+      reject(new Error('无法获取图片信息'))
+    }
+    
+    const reader = new FileReader()
+    reader.onload = (e) => {
+      img.src = e.target?.result as string
+    }
+    reader.onerror = () => {
+      reject(new Error('文件读取失败'))
+    }
+    reader.readAsDataURL(file)
+  })
+}

+ 93 - 7
src/views/processing/photoTask/index.vue

@@ -156,7 +156,7 @@
             <van-uploader
               v-model="uploadImages"
               :max-count="9"
-              :max-size="15 * 1024 * 1024"
+              :max-size="10 * 1024 * 1024"
               :before-read="beforeReadImage"
               :after-read="afterReadImage"
               multiple
@@ -190,6 +190,7 @@ import {
   uploadPhoto,status
 } from '@/api/processing/index'
 import { getHeader, goBack } from '@/utils/android'
+import { compressImage } from '@/utils/imageCompression'
 import Owner from '@/components/Owner.vue'
 import { useStore } from '@/store/modules/user'
 try {
@@ -230,7 +231,8 @@ const statusOptions = ref([])
 
 // 计算最大高度适配手持设备
 const computedMaxHeight = computed(() => {
-  return window.innerHeight - 46 - 20 + 'px'
+  // 减去导航栏高度(46px)和筛选区域高度(约80px)
+  return window.innerHeight - 46 - 80 + 'px'
 })
 
 // 获取从其他页面传递的参数
@@ -353,9 +355,9 @@ const beforeReadImage = (file) => {
       return false
     }
 
-    // 检查文件大小 (15MB)
-    if (f.size > 15 * 1024 * 1024) {
-      showFailToast('图片大小不能超过15MB')
+    // 检查文件大小 (10MB)
+    if (f.size > 10 * 1024 * 1024) {
+      showFailToast('图片大小不能超过10MB')
       return false
     }
   }
@@ -363,8 +365,57 @@ const beforeReadImage = (file) => {
 }
 
 // 图片读取完成后处理
-const afterReadImage = (file) => {
-  // file 是上传的文件信息,可以在这里进行额外处理
+const afterReadImage = async (file) => {
+  try {
+    // 处理单个文件或多个文件
+    const files = Array.isArray(file) ? file : [file]
+    
+    for (let i = 0; i < files.length; i++) {
+      const fileItem = files[i]
+      const originalFile = fileItem.file
+      
+      if (originalFile) {
+        // 检查文件大小,如果大于1MB则进行压缩
+        if (originalFile.size > 1 * 1024 * 1024) {
+          showNotify({ 
+            type: 'primary', 
+            message: `正在压缩图片: ${originalFile.name} (${(originalFile.size / 1024 / 1024).toFixed(2)}MB)` 
+          })
+          
+          try {
+            // 压缩图片到1MB以内
+            const compressedFile = await compressImage(originalFile, 1 * 1024 * 1024)
+            
+            // 更新文件对象
+            fileItem.file = compressedFile
+            fileItem.content = URL.createObjectURL(compressedFile)
+            
+            // 显示压缩结果
+            const compressionRatio = ((originalFile.size - compressedFile.size) / originalFile.size * 100).toFixed(1)
+            showNotify({ 
+              type: 'success', 
+              message: `图片压缩完成: ${(compressedFile.size / 1024 / 1024).toFixed(2)}MB (压缩${compressionRatio}%)` 
+            })
+          } catch (error) {
+            console.error('图片压缩失败:', error)
+            showNotify({ 
+              type: 'warning', 
+              message: `图片压缩失败,将使用原图上传: ${error.message}` 
+            })
+            // 压缩失败时保持原文件
+          }
+        } else {
+          console.log(`图片大小符合要求,无需压缩: ${(originalFile.size / 1024).toFixed(2)}KB`)
+        }
+      }
+    }
+  } catch (error) {
+    console.error('处理图片时发生错误:', error)
+    showNotify({ 
+      type: 'danger', 
+      message: '处理图片时发生错误: ' + error.message 
+    })
+  }
 }
 
 // 提交上传
@@ -484,6 +535,32 @@ onRefresh()
 </script>
 
 <style scoped lang="scss">
+// 固定顶部区域样式
+.top {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 1000;
+  background: #fff;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); // 添加阴影效果
+}
+
+.nav-bar {
+  position: relative;
+  z-index: 1001;
+}
+
+.context {
+  position: relative;
+  z-index: 999;
+}
+
+// 为整个容器添加上边距,避免内容被固定区域遮挡
+.container {
+  padding-top: 126px; // 导航栏46px + 筛选区域约80px
+}
+
 // 确保van-field的清空按钮样式正确显示
 :deep(.van-field__clear) {
   display: flex !important;
@@ -595,6 +672,15 @@ onRefresh()
   border-bottom: 1px solid #e0e0e0;
 }
 
+// 移除之前的padding-top,因为已经在.container中设置了
+
+:deep(.van-pull-refresh) {
+  // 调整滚动区域的高度计算,减去固定的导航栏和筛选区域高度
+  max-height: calc(100vh - 126px) !important; // 总共减去126px
+  min-height: calc(100vh - 126px) !important;
+  overflow: auto !important;
+}
+
 .code-link {
   color: #1989fa;
   cursor: pointer;