12 Commits b7f3d93502 ... dfa23d7ba5

Autor SHA1 Nachricht Datum
  zengjun dfa23d7ba5 Merge remote-tracking branch 'origin/master' vor 6 Monaten
  zengjun 21846a3751 添加图片上传前压缩到1MB内 vor 6 Monaten
  zengjun 1e10913030 修改图片上传问题 vor 6 Monaten
  zengjun 9fb938a513 添加货主名称显示 vor 6 Monaten
  zengjun 4c077171a4 导入异常处理 vor 6 Monaten
  zengjun d7f279485d 添加默认仓库筛选 vor 6 Monaten
  zengjun a1b8efa591 代码review vor 6 Monaten
  zengjun 545658e24d 添加图片上传失败处理 vor 6 Monaten
  zengjun cb7b7f0ddd 登陆相关 vor 6 Monaten
  zengjun 4e78c10392 备注显示样式修改 vor 6 Monaten
  zengjun 4d64d50333 添加分页 vor 6 Monaten
  zengjun ad38d47889 添加图片预览 vor 6 Monaten

+ 50 - 1
src/api/processing/index.ts

@@ -1,5 +1,6 @@
 // @ts-ignore
 import request from '@/utils/request'
+
 /**
  * 加工-获取加工类型
  * @param params
@@ -10,11 +11,12 @@ export function getProcessingTypeList() {
     method: 'get',
   })
 }
+
 /**
  * 加工-创建加工单
  * @param params
  */
-export function createProcessing(data:any) {
+export function createProcessing(data: any) {
   return request({
     url: 'api/device/check/processing/reference',
     method: 'post',
@@ -22,3 +24,50 @@ export function createProcessing(data:any) {
   })
 }
 
+/**
+ * 加工单拍摄任务
+ * @param params
+ */
+export function getProcessingPhotoTask(params) {
+  return request({
+    url: 'api/device/check/processing/photo-task',
+    method: 'get',
+    params,
+  })
+}
+
+/**
+ * 对应的拍摄图片
+ * @param id {number} processing photo task id
+ */
+export function processingPhotoTaskItems(id) {
+  return request({
+    url: `api/device/check/processing/photo-task/${id}/photos`,
+    method: 'get',
+  })
+}
+
+/**
+ * 上传图片
+ * @param  id  processing photo task id
+ * @param params  params
+ */
+export function uploadPhoto(id, params) {
+  return request({
+    url: `api/device/check/processing/photo-task/upload-photo/${id}`,
+    method: 'put',
+    headers: { 'Content-Type': 'multipart/form-data' },
+    data: params,
+  })
+}
+
+/**
+ * 对应的状态
+ */
+export function status(){
+  return request({
+    url: 'api/device/check/processing/photo-task/status',
+    method: 'get',
+  })
+}
+

+ 5 - 1
src/hooks/basic/menu.js

@@ -124,7 +124,11 @@ export default function() {
               icon: 'newspaper-o',
               path: 'container-operation',
             },
-
+            {
+              title: '加工拍摄任务',
+              icon: 'newspaper-o',
+              path: 'processing-photo-task',
+            },
           ],
         },
       ],

+ 6 - 0
src/router/index.ts

@@ -145,6 +145,12 @@ const routes: RouteRecordRaw[] = [
     meta:{title:'移库'},
     component: () => import('@/views/inventory/transfer/index.vue')
   },
+  {
+    path: '/processing-photo-task',
+    name: 'ProcessingPhotoTask',
+    meta:{title:'加工拍摄任务'},
+    component: () => import('@/views/processing/photoTask/index.vue')
+  },
 ];
 
 // 创建路由实例

+ 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)
+  })
+}

+ 693 - 0
src/views/processing/photoTask/index.vue

@@ -0,0 +1,693 @@
+  <template>
+  <div class="container">
+    <div class="processing-photo-task">
+      <div class="top">
+        <div class="nav-bar">
+          <van-nav-bar
+            title="加工拍照任务"
+            left-arrow
+            @click-left="goBack"
+            @click-right="onRefresh"
+          >
+            <template #left>
+              <van-icon name="arrow-left" size="25" />
+              <div
+                style="
+                  color: #fff;
+                  height: 46px;
+                  padding-right: 20px;
+                  line-height: 46px;
+                "
+              >
+                返回
+              </div>
+            </template>
+            <template #right>
+              <div class="nav-right" style="color: #fff">重置</div>
+            </template>
+          </van-nav-bar>
+        </div>
+
+        <div class="context">
+          <!-- 筛选区域 -->
+          <div class="filter-section">
+            <van-row gutter="10">
+              <van-col span="8">
+                <van-field
+                  v-model="filterForm.owners"
+                  placeholder="请选择货主"
+                  readonly
+                  clickable
+                  clearable
+                  clear-trigger="always"
+                  @click="showOwnerDialog"
+                  @clear="onOwnerClear"
+                />
+              </van-col>
+              <van-col span="8">
+                <van-field
+                  v-model="filterForm.status"
+                  placeholder="请选择状态"
+                  readonly
+                  clickable
+                  @click="showStatusPicker = true"
+                />
+              </van-col>
+              <van-col span="8">
+                <van-button type="primary" size="small" block @click="onFilterSearch">
+                  搜索
+                </van-button>
+              </van-col>
+            </van-row>
+          </div>
+
+          <van-pull-refresh
+            v-model="loading"
+            @refresh="onRefresh"
+            :style="{
+              'max-height': computedMaxHeight,
+              'min-height': computedMaxHeight,
+              overflow: 'auto',
+            }"
+          >
+            <table class="photo-task-table">
+              <thead>
+                <tr>
+                  <th style="width: 25%">货主</th>
+                  <th style="width: 35%">加工单号</th>
+                  <th style="width: 15%">状态</th>
+                  <th style="width: 12.5%">查看</th>
+                  <th style="width: 12.5%">操作</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="(row, rowIndex) in photoTasks" :key="rowIndex">
+                  <td>
+                    <span class="text-compact">{{ row.ownerName }}</span>
+                  </td>
+                  <td>
+                    <span class="text-sm code-link" @click="showNotes(row)">{{ row.code }}</span>
+                  </td>
+                  <td>
+                    <span class="text-compact">{{ row.status }}</span>
+                  </td>
+                  <td>
+                    <van-button type="primary" plain size="small" @click="showPhotos(row)" class="compact-button"
+                      >查看
+                    </van-button>
+                  </td>
+                  <td>
+                    <van-button v-if="row.status !== '完成'" type="primary" size="small" @click="toPhoto(row)" class="compact-button"
+                      >拍摄
+                    </van-button>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+            
+            <!-- 分页控件 -->
+            <van-pagination
+              v-model="paginate.page"
+              :page-size="paginate.size"
+              :total-items="paginate.total"
+              :page-count="paginate.pages"
+              :show-page-size="5"
+              model="simple"
+              forced="true"
+              @change="onPageChange"
+              class="pagination"
+            />
+          </van-pull-refresh>
+        </div>
+      </div>
+
+      <!-- 货主选择器弹窗 -->
+      <Owner 
+        ref="ownerRef" 
+        @onOwner="onOwnerSelect"
+      />
+
+      <!-- 状态选择器弹窗 -->
+      <van-popup v-model:show="showStatusPicker" position="bottom">
+        <van-picker
+          :columns="statusOptions"
+          @confirm="onStatusConfirm"
+          @cancel="showStatusPicker = false"
+        />
+      </van-popup>
+
+      <!-- 图片上传弹窗 -->
+      <van-popup v-model:show="showUpload" position="bottom" :style="{ height: '80%' }">
+        <div class="upload-popup">
+          <van-nav-bar
+            title="图片上传"
+            left-arrow
+            @click-left="showUpload = false"
+          >
+            <template #left>
+              <van-icon name="arrow-left" size="25" />
+              <div style="color: #fff; height: 46px; padding-right: 20px; line-height: 46px;">
+                返回
+              </div>
+            </template>
+          </van-nav-bar>
+
+          <div class="upload-content">
+            <van-uploader
+              v-model="uploadImages"
+              :max-count="9"
+              :max-size="10 * 1024 * 1024"
+              :before-read="beforeReadImage"
+              :after-read="afterReadImage"
+              multiple
+              preview-full-image
+            />
+
+            <div class="upload-footer">
+              <van-button
+                type="primary"
+                block
+                :loading="uploading"
+                @click="submitUpload"
+              >
+                {{ uploading ? '上传中...' : '提交上传' }}
+              </van-button>
+            </div>
+          </div>
+        </div>
+      </van-popup>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useRoute } from 'vue-router'
+import { showNotify, showImagePreview, showFailToast, showLoadingToast, closeToast, showDialog } from 'vant'
+import {
+  getProcessingPhotoTask,
+  processingPhotoTaskItems,
+  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 {
+  getHeader()
+} catch (error) {
+  console.log(error)
+}
+const store = useStore()
+const warehouse = store.warehouse
+
+const route = useRoute()
+const loading = ref(false)
+const photoTasks = ref([])
+const showUpload = ref(false)
+const uploadImages = ref([])
+const uploading = ref(false)
+const currentTaskId = ref(null)
+const ownerRef = ref(null)
+
+const paginate = ref({
+  page: 1,
+  size: 20,
+  total: 0,
+  pages: 0,
+})
+const reviewImages = ref([])
+
+// 筛选相关数据
+const filterForm = ref({
+  owners: '',
+  ownerCode: '',
+  status: '',
+  statusCode: ''
+})
+
+const showStatusPicker = ref(false)
+const statusOptions = ref([])
+
+// 计算最大高度适配手持设备
+const computedMaxHeight = computed(() => {
+  // 减去导航栏高度(46px)和筛选区域高度(约80px)
+  return window.innerHeight - 46 - 80 + 'px'
+})
+
+// 获取从其他页面传递的参数
+onMounted(() => {
+  // 可以从route.query或route.params中获取参数
+  if (route.query.id) {
+    // 如果有传入id参数,可以做特殊处理
+  }
+
+  status().then(res=>{
+    if (res.data) {
+      statusOptions.value = res.data.map(item=>{
+        return {text:item.name,value:item.code}
+      })
+    }
+  })
+})
+
+// 显示notes信息 - 改为弹框方式
+function showNotes(row) {
+  const notes = row.notes || '暂无备注信息'
+  showDialog({
+    title: '任务备注',
+    message: notes,
+    confirmButtonText: '确定',
+    messageAlign: 'left'
+  })
+}
+
+function onRefresh() {
+  // 刷新时重置到第一页
+  paginate.value.page = 1
+  filterForm.value.owners = null
+  filterForm.value.ownerCode = null
+  filterForm.value.status = null
+  filterForm.value.statusCode = null
+  listProcessingPhoto()
+}
+
+function listProcessingPhoto() {
+  loading.value = true
+  // 合并筛选条件和分页参数,使用statusCode而不是status
+  const params = {
+    ...paginate.value,
+    warehouses: warehouse,
+    owners: filterForm.value.ownerCode,
+    status: filterForm.value.statusCode
+  }
+  getProcessingPhotoTask(params).then((res) => {
+    photoTasks.value = res?.data?.records || []
+    // 更新分页信息
+    if (res?.data) {
+      paginate.value.size = res.data.size || 0
+      paginate.value.total = res.data.total || 0
+      paginate.value.page = res.data.current || 0
+      paginate.value.pages = res.data.pages || 0
+    }
+  }).finally(() => {
+    loading.value = false
+  })
+}
+
+// 分页变更处理
+function onPageChange(page) {
+  paginate.value.page = page
+  listProcessingPhoto()
+}
+
+function showPhotos(row) {
+  processingPhotoTaskItems(row.id).then((res) => {
+    const items = res.data
+    if (!items || items.length === 0) {
+      showNotify({ type: 'warning', message: '没有对应的拍摄' })
+      return
+    }
+    const baseUrl = import.meta.env.VITE_APP_BASE_API + 'statics/storage/'
+    const paths = baseUrl.split('.')
+    paths[1] = 'swms'
+    const base = paths.join('.')
+    // 解构图片url
+    reviewImages.value = items.map(item => {
+      const {path,module} = item
+      return `${base}${module}/${path}`
+    }).filter(url => url)
+
+    // 预览图片
+    if (reviewImages.value.length > 0) {
+      showImagePreview({
+        images: reviewImages.value,
+        closeable: true,
+      })
+    } else {
+      showNotify({ type: 'warning', message: '没有可预览的图片' })
+    }
+  }).catch(err => {
+    showNotify({ type: 'danger', message: '获取图片失败' })
+  })
+}
+
+function toPhoto(row) {
+  currentTaskId.value = row.id
+  uploadImages.value = []
+  showUpload.value = true
+}
+
+// 检查图片格式和大小
+const beforeReadImage = (file) => {
+  const files = Array.isArray(file) ? file : [file]
+  for (const f of files) {
+    // 检查文件格式
+    if (f.name.toLowerCase().endsWith('.heic') || f.name.toLowerCase().endsWith('.heif')) {
+      showFailToast('不支持HEIC/HEIF格式的图片')
+      return false
+    }
+
+    // 检查文件类型
+    const isImage = /^image\//.test(f.type)
+    if (!isImage) {
+      showFailToast('请上传图片文件')
+      return false
+    }
+
+    // 检查文件大小 (10MB)
+    if (f.size > 10 * 1024 * 1024) {
+      showFailToast('图片大小不能超过10MB')
+      return false
+    }
+  }
+  return true
+}
+
+// 图片读取完成后处理
+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 
+    })
+  }
+}
+
+// 提交上传
+const submitUpload = async () => {
+  if (!uploadImages.value.length) {
+    showFailToast('请先选择图片')
+    return
+  }
+
+  if (!currentTaskId.value) {
+    showFailToast('任务ID无效')
+    return
+  }
+
+  uploading.value = true
+  const toast = showLoadingToast('上传中...')
+
+  try {
+    const totalImages = uploadImages.value.length
+    let successCount = 0
+    let failedImages = []
+
+    // 逐个上传图片
+    const failedMessages = []
+    for (let i = 0; i < uploadImages.value.length; i++) {
+      const image = uploadImages.value[i]
+      try {
+        const formData = new FormData()
+        formData.append('file', image.file)
+        // 调用上传API
+        await uploadPhoto(currentTaskId.value, formData)
+        successCount++
+      } catch (err) {
+        failedMessages.push(`${i+1}/${uploadImages.value.length}张图片上传失败:${err.message}`)
+        console.error(`第${i + 1}张图片上传失败:`, err.message)
+        failedImages.push({
+          index: i,
+          image: image,
+          error: err.message || '上传失败'
+        })
+      }
+    }
+
+    closeToast()
+
+    if (failedMessages.length !== 0) {
+      showNotify({
+        type: 'danger',
+        message: failedMessages.join("\\t\\n")
+      })
+    }
+
+    if (successCount === totalImages) {
+      // 全部上传成功
+      showNotify({ type: 'success', message: '全部图片上传成功' })
+      // 关闭弹窗并刷新列表
+      showUpload.value = false
+      uploadImages.value = []
+      onRefresh()
+    } else if (successCount > 0) {
+      // 部分上传成功
+      showNotify({ 
+        type: 'warning', 
+        message: `${successCount}/${totalImages}张图片上传成功,${failedImages.length}张失败` 
+      })
+      // 移除上传成功的图片,保留失败的图片
+      uploadImages.value = failedImages.map(item => item.image)
+    }
+  } catch (err) {
+    closeToast()
+    showNotify({ type: 'danger', message: '上传过程发生异常: ' + (err.message || '未知错误') })
+    // 发生异常时保留所有图片,让用户可以重试
+  } finally {
+    uploading.value = false
+  }
+}
+
+function onFilterSearch() {
+  // 搜索时重置到第一页
+  paginate.value.page = 1
+  listProcessingPhoto()
+}
+
+// 显示货主选择弹窗
+function showOwnerDialog() {
+  if (ownerRef.value) {
+    ownerRef.value.show('filter')
+  }
+}
+
+// 处理货主选择
+function onOwnerSelect(item, type) {
+  filterForm.value.owners = item.name
+  filterForm.value.ownerCode = item.code
+  // 选择货主后自动搜索item
+  onFilterSearch()
+}
+
+// 处理货主清空
+function onOwnerClear() {
+  filterForm.value.owners = ''
+  filterForm.value.ownerCode = ''
+  // 清空货主后自动搜索
+  onFilterSearch()
+}
+
+function onStatusConfirm({ selectedOptions }) {
+  filterForm.value.status = selectedOptions[0].text
+  filterForm.value.statusCode = selectedOptions[0].value
+  showStatusPicker.value = false
+  // 选择状态后自动搜索
+  onFilterSearch()
+}
+
+// 初始化加载数据
+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;
+  align-items: center;
+  justify-content: center;
+  color: #c8c9cc;
+  font-size: 16px;
+  cursor: pointer;
+
+  &:active {
+    color: #969799;
+  }
+}
+
+// 确保van-field右侧区域有足够空间显示清空按钮
+:deep(.van-field__right-icon) {
+  display: flex !important;
+  align-items: center;
+}
+
+.photo-task-table {
+  width: 100%;
+  border-collapse: collapse;
+  text-align: center;
+  table-layout: fixed;
+  font-size: 12px;
+
+  thead {
+    tr {
+      background-color: #f5f5f5;
+      
+      th {
+        padding: 6px 2px;
+        font-weight: 500;
+      }
+    }
+  }
+
+  tbody {
+    tr {
+      height: 36px;
+      border-bottom: 1px solid #e0e0e0;
+      
+      &:last-child {
+        border-bottom: none;
+      }
+      
+      td {
+        padding: 3px 2px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        
+        .text-compact {
+          font-size: 12px;
+          line-height: 1.2;
+          display: block;
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+        
+        .text-sm {
+          font-size: 11px;
+          line-height: 1.2;
+          display: block;
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+        
+        .compact-button {
+          height: 24px;
+          line-height: 22px;
+          padding: 0 6px;
+          font-size: 11px;
+          min-width: 36px;
+        }
+      }
+    }
+  }
+}
+
+.pagination {
+  padding: 10px 0;
+  display: flex;
+  justify-content: center;
+}
+
+.upload-popup {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+
+  .upload-content {
+    flex: 1;
+    padding: 10px;
+    overflow-y: auto;
+
+    .upload-footer {
+      padding: 10px 0;
+    }
+  }
+}
+
+.filter-section {
+  padding: 10px;
+  background: #fff;
+  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;
+  text-decoration: underline;
+  
+  &:hover {
+    color: #0570db;
+  }
+}
+</style>