فهرست منبع

初步实现签名组件和转换PDF预览

yangguanjin 1 ماه پیش
والد
کامیت
6791651c14

+ 1 - 0
package.json

@@ -102,6 +102,7 @@
     "echarts": "^5.6.0",
     "lodash-es": "^4.17.21",
     "md5": "^2.3.0",
+    "pdfjs-dist": "^5.7.284",
     "pinia": "2.0.36",
     "pinia-plugin-persistedstate": "3.2.1",
     "qs": "6.5.3",

+ 134 - 0
pnpm-lock.yaml

@@ -134,6 +134,9 @@ importers:
       md5:
         specifier: ^2.3.0
         version: 2.3.0
+      pdfjs-dist:
+        specifier: ^5.7.284
+        version: 5.7.284
       pinia:
         specifier: 2.0.36
         version: 2.0.36(typescript@5.7.2)(vue@3.4.21(typescript@5.7.2))
@@ -1960,6 +1963,81 @@ packages:
   '@jridgewell/trace-mapping@0.3.25':
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
 
+  '@napi-rs/canvas-android-arm64@0.1.100':
+    resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [android]
+
+  '@napi-rs/canvas-darwin-arm64@0.1.100':
+    resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@napi-rs/canvas-darwin-x64@0.1.100':
+    resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
+    resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==}
+    engines: {node: '>= 10'}
+    cpu: [arm]
+    os: [linux]
+
+  '@napi-rs/canvas-linux-arm64-gnu@0.1.100':
+    resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  '@napi-rs/canvas-linux-arm64-musl@0.1.100':
+    resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  '@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
+    resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==}
+    engines: {node: '>= 10'}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [glibc]
+
+  '@napi-rs/canvas-linux-x64-gnu@0.1.100':
+    resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  '@napi-rs/canvas-linux-x64-musl@0.1.100':
+    resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  '@napi-rs/canvas-win32-arm64-msvc@0.1.100':
+    resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@napi-rs/canvas-win32-x64-msvc@0.1.100':
+    resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [win32]
+
+  '@napi-rs/canvas@0.1.100':
+    resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==}
+    engines: {node: '>= 10'}
+
   '@nodelib/fs.scandir@2.1.5':
     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
     engines: {node: '>= 8'}
@@ -5549,6 +5627,10 @@ packages:
   pathe@1.1.2:
     resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
 
+  pdfjs-dist@5.7.284:
+    resolution: {integrity: sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==}
+    engines: {node: '>=22.13.0 || >=24'}
+
   perfect-debounce@1.0.0:
     resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
 
@@ -9496,6 +9578,54 @@ snapshots:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.0
 
+  '@napi-rs/canvas-android-arm64@0.1.100':
+    optional: true
+
+  '@napi-rs/canvas-darwin-arm64@0.1.100':
+    optional: true
+
+  '@napi-rs/canvas-darwin-x64@0.1.100':
+    optional: true
+
+  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
+    optional: true
+
+  '@napi-rs/canvas-linux-arm64-gnu@0.1.100':
+    optional: true
+
+  '@napi-rs/canvas-linux-arm64-musl@0.1.100':
+    optional: true
+
+  '@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
+    optional: true
+
+  '@napi-rs/canvas-linux-x64-gnu@0.1.100':
+    optional: true
+
+  '@napi-rs/canvas-linux-x64-musl@0.1.100':
+    optional: true
+
+  '@napi-rs/canvas-win32-arm64-msvc@0.1.100':
+    optional: true
+
+  '@napi-rs/canvas-win32-x64-msvc@0.1.100':
+    optional: true
+
+  '@napi-rs/canvas@0.1.100':
+    optionalDependencies:
+      '@napi-rs/canvas-android-arm64': 0.1.100
+      '@napi-rs/canvas-darwin-arm64': 0.1.100
+      '@napi-rs/canvas-darwin-x64': 0.1.100
+      '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100
+      '@napi-rs/canvas-linux-arm64-gnu': 0.1.100
+      '@napi-rs/canvas-linux-arm64-musl': 0.1.100
+      '@napi-rs/canvas-linux-riscv64-gnu': 0.1.100
+      '@napi-rs/canvas-linux-x64-gnu': 0.1.100
+      '@napi-rs/canvas-linux-x64-musl': 0.1.100
+      '@napi-rs/canvas-win32-arm64-msvc': 0.1.100
+      '@napi-rs/canvas-win32-x64-msvc': 0.1.100
+    optional: true
+
   '@nodelib/fs.scandir@2.1.5':
     dependencies:
       '@nodelib/fs.stat': 2.0.5
@@ -13765,6 +13895,10 @@ snapshots:
 
   pathe@1.1.2: {}
 
+  pdfjs-dist@5.7.284:
+    optionalDependencies:
+      '@napi-rs/canvas': 0.1.100
+
   perfect-debounce@1.0.0: {}
 
   phin@2.9.3: {}

+ 19 - 0
src/api/ApiRouter/taskOrder.ts

@@ -22,6 +22,7 @@ import {
   confirmBoilerTaskOrder,
   confirmBoilerEquipmentClaim,
   cancelBoilerEquipmentClaim,
+  getBoilerTaskItemListByOrderId,
 } from '@/api/boiler/boilerTaskOrder'
 
 type Adapter = {
@@ -35,6 +36,7 @@ export enum TaskOrderFuncName {
   TaskEquipList,
   CheckEquipTaskList,
   TaskConfirm,
+  TaskOrderzTaskItemList,
   EquipmentConfirmClaim,
   EquipmentCancelClaim,
 }
@@ -143,6 +145,23 @@ const map = {
       outputAdapter: null,
     },
   },
+  [TaskOrderFuncName.TaskOrderzTaskItemList]: {
+    [EquipmentType.BOILER]: {
+      inputAdapter: null,
+      reqFunction: getBoilerTaskItemListByOrderId,
+      outputAdapter: null,
+    },
+    [EquipmentType.PIPE]: {
+      inputAdapter: null,
+      reqFunction: getBoilerTaskItemListByOrderId,
+      outputAdapter: null,
+    },
+    [EquipmentType.CONTAINER]: {
+      inputAdapter: null,
+      reqFunction: getBoilerTaskItemListByOrderId,
+      outputAdapter: null,
+    },
+  },
 }
 
 export const requestFunc = (funcName: TaskOrderFuncName, equipType: EquipmentType, params: any) => {

+ 5 - 0
src/api/boiler/boilerTaskOrder.ts

@@ -20,6 +20,11 @@ export const confirmBoilerTaskOrder = (data: any) => {
   return httpPost('/pressure2/boiler-task-order/confirm', data)
 }
 
+// 任务单认领
+export const getBoilerTaskItemListByOrderId = (params: any) => {
+  return httpGet('/pressure2/boiler-task-order/get', params)
+}
+
 // 设备认领
 export const confirmBoilerEquipmentClaim = (data: { id: string }) => {
   return httpPost('/pressure2/boiler-task-order/order-item/claim', data)

+ 339 - 0
src/components/PDFViewer/index.vue

@@ -0,0 +1,339 @@
+<template>
+  <view class="pdf-viewer">
+    <view v-if="loading" class="pdf-loading">
+      <text class="loading-text">PDF加载中...</text>
+    </view>
+    <view v-else-if="errorMsg" class="pdf-error">
+      <text class="error-text">{{ errorMsg }}</text>
+    </view>
+    <template v-else>
+      <!-- #ifdef H5 -->
+      <view class="pdf-viewer-wrapper">
+        <view class="pdf-container" :id="containerId"></view>
+        <view v-if="pageCount > 1" class="page-control">
+          <button class="page-btn" :disabled="currentPage <= 1" @click="prevPage">上一页</button>
+          <text class="page-info">{{ currentPage }} / {{ pageCount }}</text>
+          <button class="page-btn" :disabled="currentPage >= pageCount" @click="nextPage">下一页</button>
+        </view>
+      </view>
+      <!-- #endif -->
+      <!-- #ifndef H5 -->
+      <view v-if="imageSrc" class="pdf-image-container">
+        <image :src="imageSrc" mode="aspectFit" class="pdf-image" />
+      </view>
+      <view v-else class="pdf-placeholder">
+        <text class="placeholder-text">暂无PDF预览</text>
+      </view>
+      <!-- #endif -->
+    </template>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
+
+interface Props {
+  source: ArrayBuffer | Blob | null
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  source: null,
+})
+
+const emit = defineEmits<{
+  loaded: [pageCount: number]
+  error: [message: string]
+}>()
+
+const containerId = ref(`pdf-viewer-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`)
+const loading = ref(false)
+const errorMsg = ref('')
+
+const pageCount = ref(0)
+const currentPage = ref(1)
+let pdfDoc: any = null
+
+const imageSrc = ref('')
+const imageWidth = ref(0)
+const imageHeight = ref(0)
+
+let isMounted = false
+
+// #ifdef H5
+import * as pdfjsLib from 'pdfjs-dist'
+import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
+// #endif
+
+const renderPage = async (pageNum: number) => {
+  if (!pdfDoc) return
+
+  try {
+    const page = await pdfDoc.getPage(pageNum)
+    const containerWidth = uni.getSystemInfoSync().windowWidth - 32
+    const unscaledViewport = page.getViewport({ scale: 1 })
+    const scale = containerWidth / unscaledViewport.width
+    const viewport = page.getViewport({ scale })
+
+    const canvas = document.createElement('canvas')
+    canvas.width = viewport.width
+    canvas.height = viewport.height
+    canvas.style.display = 'block'
+    canvas.style.width = viewport.width + 'px'
+    canvas.style.height = viewport.height + 'px'
+
+    const context = canvas.getContext('2d')
+    await page.render({ canvasContext: context!, viewport }).promise
+
+    const container = document.getElementById(containerId.value)
+    if (container) {
+      container.innerHTML = ''
+      container.appendChild(canvas)
+    }
+  } catch (error) {
+    console.error('PDF页面渲染失败:', error)
+    errorMsg.value = 'PDF页面渲染失败'
+    emit('error', 'PDF页面渲染失败')
+  }
+}
+
+const loadPDFFromArrayBuffer = async (data: ArrayBuffer) => {
+  loading.value = true
+  errorMsg.value = ''
+
+  try {
+    pdfDoc = null
+    pageCount.value = 0
+    currentPage.value = 1
+
+    // #ifdef H5
+    pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
+
+    const loadingTask = pdfjsLib.getDocument({ data })
+    const pdf = await loadingTask.promise
+    pdfDoc = pdf
+    pageCount.value = pdf.numPages
+    currentPage.value = 1
+
+    loading.value = false
+    emit('loaded', pdf.numPages)
+
+    await nextTick()
+    if (isMounted) {
+      await renderPage(1)
+    }
+    // #endif
+
+    // #ifndef H5
+    const uint8Array = new Uint8Array(data)
+    const binaryString = uint8Array.reduce((acc, byte) => {
+      return acc + String.fromCharCode(byte)
+    }, '')
+    const base64 = btoa(binaryString)
+    imageSrc.value = `data:application/pdf;base64,${base64}`
+
+    uni.getImageInfo({
+      src: `data:application/pdf;base64,${base64}`,
+      success: (imageInfo) => {
+        const screenWidth = uni.getSystemInfoSync().windowWidth - 32
+        imageWidth.value = screenWidth
+        imageHeight.value = (screenWidth * imageInfo.height) / imageInfo.width
+        pageCount.value = 1
+        loading.value = false
+        emit('loaded', 1)
+      },
+      fail: () => {
+        loading.value = false
+        pageCount.value = 1
+        emit('loaded', 1)
+      },
+    })
+    // #endif
+  } catch (error: any) {
+    loading.value = false
+    console.error('PDF加载失败:', error)
+    errorMsg.value = error?.message || 'PDF加载失败'
+    emit('error', errorMsg.value)
+  }
+}
+
+const loadPDFFromBlob = async (blob: Blob) => {
+  const arrayBuffer = await blob.arrayBuffer()
+  await loadPDFFromArrayBuffer(arrayBuffer)
+}
+
+const prevPage = async () => {
+  if (currentPage.value <= 1) return
+  currentPage.value--
+  await renderPage(currentPage.value)
+}
+
+const nextPage = async () => {
+  if (currentPage.value >= pageCount.value) return
+  currentPage.value++
+  await renderPage(currentPage.value)
+}
+
+const resetViewer = () => {
+  pdfDoc = null
+  pageCount.value = 0
+  currentPage.value = 1
+  imageSrc.value = ''
+  errorMsg.value = ''
+}
+
+watch(
+  () => props.source,
+  async (newSource) => {
+    if (!newSource) {
+      resetViewer()
+      return
+    }
+
+    try {
+      if (newSource instanceof Blob) {
+        await loadPDFFromBlob(newSource)
+      } else if (newSource instanceof ArrayBuffer) {
+        await loadPDFFromArrayBuffer(newSource)
+      } else {
+        errorMsg.value = '无效的PDF数据源'
+        emit('error', '无效的PDF数据源')
+      }
+    } catch (error: any) {
+      console.error('PDF加载失败:', error)
+      errorMsg.value = error?.message || 'PDF加载失败'
+      emit('error', errorMsg.value)
+    }
+  },
+)
+
+onMounted(async () => {
+  isMounted = true
+
+  if (props.source) {
+    if (props.source instanceof Blob) {
+      await loadPDFFromBlob(props.source)
+    } else if (props.source instanceof ArrayBuffer) {
+      await loadPDFFromArrayBuffer(props.source)
+    }
+  }
+})
+
+onUnmounted(() => {
+  isMounted = false
+  resetViewer()
+})
+
+defineExpose({
+  prevPage,
+  nextPage,
+  goToPage: (page: number) => {
+    if (page >= 1 && page <= pageCount.value) {
+      currentPage.value = page
+      renderPage(page)
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.pdf-viewer {
+  width: 100%;
+}
+
+.pdf-loading {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 300px;
+  background-color: #f5f5f5;
+  border-radius: 8px;
+
+  .loading-text {
+    font-size: 14px;
+    color: #999;
+  }
+}
+
+.pdf-error {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 200px;
+  background-color: #fff5f5;
+  border-radius: 8px;
+
+  .error-text {
+    font-size: 14px;
+    color: #f56c6c;
+  }
+}
+
+.pdf-viewer-wrapper {
+  width: 100%;
+  margin-bottom: 16px;
+}
+
+.pdf-container {
+  width: 100%;
+  overflow: auto;
+  background-color: #f5f5f5;
+  border-radius: 8px;
+  min-height: 200px;
+}
+
+.page-control {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  gap: 16px;
+  padding: 12px 0;
+
+  .page-btn {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 32px;
+    padding: 0 16px;
+    font-size: 14px;
+    color: #333;
+    background-color: #f0f0f0;
+    border: 1px solid #ddd;
+    border-radius: 6px;
+  }
+
+  .page-info {
+    font-size: 14px;
+    color: #666;
+  }
+}
+
+.pdf-image-container {
+  width: 100%;
+  overflow: hidden;
+  background-color: #f5f5f5;
+  border-radius: 8px;
+
+  .pdf-image {
+    display: block;
+    width: 100%;
+  }
+}
+
+.pdf-placeholder {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 200px;
+  background-color: #f5f5f5;
+  border-radius: 8px;
+
+  .placeholder-text {
+    font-size: 14px;
+    color: #999;
+  }
+}
+</style>

+ 16 - 21
src/components/Signature/SignatureCanvas.vue

@@ -42,8 +42,8 @@ const emit = defineEmits<{
 const points = ref<{ X: number; Y: number }[]>([])
 const isDraw = ref(false)
 let ctx: UniNamespace.CanvasContext | null = null
-let canvasWidth = ref(300)
-let canvasHeight = ref(200)
+const canvasWidth = ref(300)
+const canvasHeight = ref(200)
 
 const initCanvas = () => {
   nextTick(() => {
@@ -73,7 +73,6 @@ const clearCanvas = () => {
 }
 
 const handleTouchStart = (e: any) => {
-  console.log('touchstart.....', e)
   if (!ctx) return
 
   const touch = e.touches[0]
@@ -90,7 +89,6 @@ const handleTouchStart = (e: any) => {
 }
 
 const handleTouchMove = (e: any) => {
-  console.log('touchmove.....', e)
   if (!ctx) return
 
   const touch = e.touches[0]
@@ -119,7 +117,6 @@ const drawLine = () => {
 }
 
 const handleTouchEnd = (e: any) => {
-  console.log('touchend.....', e)
   points.value = []
   emit('signed', isDraw.value)
 }
@@ -131,21 +128,19 @@ const getImage = (quality = 1, callback?: (path: string) => void): Promise<strin
       return
     }
 
-    ctx.draw(false, () => {
-      uni.canvasToTempFilePath({
-        canvasId: props.canvasId,
-        quality: quality,
-        success: (res) => {
-          const path = res.tempFilePath
-          if (callback) {
-            callback(path)
-          }
-          resolve(path)
-        },
-        fail: (err) => {
-          reject(err)
-        },
-      })
+    uni.canvasToTempFilePath({
+      canvasId: props.canvasId,
+      quality,
+      success: (res) => {
+        const path = res.tempFilePath
+        if (callback) {
+          callback(path)
+        }
+        resolve(path)
+      },
+      fail: (err) => {
+        reject(err)
+      },
     })
   })
 }
@@ -205,4 +200,4 @@ onUnmounted(() => {
   border: 1px solid #d9d9d9;
   border-radius: 4px;
 }
-</style>
+</style>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1519 - 0
src/components/SpreadDesigner/SpreadPDFViewer.vue


+ 7 - 1
src/pages/equipment/detail/equipTestRecordEditor.vue

@@ -210,7 +210,13 @@ const handleSave = async (data: any) => {
   try {
     if (useOnline === '1') {
       const instId = data.instId
-      saveDynamicTbVal({ params: data.prepareJson, instId: data.instId })
+      const result = await saveDynamicTbVal({ params: data.prepareJson, instId })
+      if (result?.code === 0 && result?.data) {
+        uni.showToast({ title: '保存成功', icon: 'success' })
+      } else {
+        const msg = result?.msg || '保存失败'
+        uni.showToast({ title: msg, icon: 'error' })
+      }
     } else {
       uni.showToast({ title: '文件存储成功', icon: 'success' })
       uni.$emit('webViewSaved', {

+ 138 - 54
src/pages/sign-detail/index.vue

@@ -80,10 +80,12 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, watch } from 'vue'
 import { onLoad, onShow } from '@dcloudio/uni-app'
-import { getGcConfig, getTaskOrderSignImg, signConfirm, pushTaskOrder } from '@/api/sign'
+import { getGcConfig, getTaskOrderSignImg, pushTaskOrder } from '@/api/sign'
 import { useUserStore } from '@/store/user'
+import { createBusinessConfig } from '@/pages/webview/common/config/businessEditorConfig'
+import { setSpreadsheetEditParams } from '@/common/global'
 
 const title = ref('')
 const routeType = ref('')
@@ -108,11 +110,14 @@ const inputEmail = ref('')
 const userStore = useUserStore()
 const userInfo = ref(userStore.userInfo)
 
+const gcConfig = ref<any>({})
+const initData = ref<any>({})
+
 const titleTextMap: Record<string, string> = {
-  FWD: '服务单/受理单详情',
-  JYRS: '检验结果告知详情',
-  AQJC: '安全检查记录详情',
-  ZXXX: '重大问题线索告知详情'
+  FWD: '服务单/受理单',
+  JYRS: '检验结果告知',
+  AQJC: '安全检查记录',
+  ZXXX: '重大问题线索告知'
 }
 
 const businessTypeMap: Record<string, number> = {
@@ -123,23 +128,29 @@ const businessTypeMap: Record<string, number> = {
 }
 
 onLoad((options) => {
-  const type = options.type || ''
+  const type = options?.type || ''
   routeType.value = type
   title.value = titleTextMap[type] || '签字详情'
-  orderId.value = options.orderId || ''
-  orderItemId.value = options.orderItemId || ''
-  securityCheckId.value = options.securityCheckId || ''
-  reportId.value = options.reportId || ''
-  unitContact.value = options.unitContact || ''
-  unitPhone.value = options.unitPhone || ''
-  receiverEmail.value = options.receiverEmail || ''
-  templateId.value = options.templateId || ''
-  
+  orderId.value = options?.orderId || ''
+  orderItemId.value = options?.orderItemId || ''
+  securityCheckId.value = options?.securityCheckId || ''
+  reportId.value = options?.reportId || ''
+  unitContact.value = options?.unitContact || ''
+  unitPhone.value = options?.unitPhone || ''
+  receiverEmail.value = options?.receiverEmail || ''
+  templateId.value = options?.templateId || ''
+
   inputName.value = unitContact.value
   inputPhone.value = unitPhone.value
   inputEmail.value = receiverEmail.value
 })
 
+watch(orderId, (val) => {
+  if (val) {
+    getTaskOrderGcConfig()
+  }
+})
+
 // 获取任务单配置
 const getTaskOrderGcConfig = async () => {
   loading.value = true
@@ -148,11 +159,13 @@ const getTaskOrderGcConfig = async () => {
       orderId: orderId.value,
       businessType: routeType.value ? businessTypeMap[routeType.value] : '',
       orderItemId: orderItemId.value || undefined,
-      templateId: templateId.value || undefined,
     }, userInfo.value)
-    
+
+    console.log('获取任务单葡萄城配置信息:', result)
     const res = result?.data
-    
+    gcConfig.value = res || {}
+    initData.value = result?.initData || {}
+
     if (res) {
       await getTaskOrderSignImg(res)
     }
@@ -166,43 +179,54 @@ const getTaskOrderGcConfig = async () => {
 
 // 获取已签名的图片
 const getTaskOrderSignImg = async (res: any) => {
-  const { dataStr, templateUrl, templateId, fileVersionId } = res
-  
+  const { dataStr, templateUrl, templateId } = res
+
   if (!orderId.value) return
-  
-  if (orderId.value && dataStr && templateUrl) {
+
+  if (orderId.value) {
     const params: any = {
-      dataStr,
-      templateUrl,
+      dataStr: dataStr || 'xxx',
+      templateUrl: templateUrl || 'xxxx',
       orderId: orderId.value,
       businessType: routeType.value ? businessTypeMap[routeType.value] : '',
       fileType: 100,
       orderItemId: orderItemId.value || undefined,
-      templateId,
+      templateId: templateId || '',
     }
-    
-    if (routeType.value === 'ZXXX' && fileVersionId) {
-      params.fileVersionId = fileVersionId
+
+    if (routeType.value === 'ZXXX' && res.fileVersionId) {
+      params.fileVersionId = res.fileVersionId
     }
-    
+
     try {
       const result: any = await getTaskOrderSignImg(params)
-      if (result) {
-        pdfImg.value = result
-        
-        // 计算图片显示尺寸
+      if (typeof result === 'string') {
+        if (!/^data:image\/\w+;base64,/.test(result)) return
+        const base64Data = result.replace(/^data:image\/\w+;base64,/, '')
+        const fileName = `upload_${Date.now()}.png`
+        const fs = uni.getFileSystemManager()
+        const filePath = `${uni.env.USER_DATA_PATH}/${fileName}`
+        fs.writeFileSync(filePath, base64Data, 'base64')
+        pdfImg.value = filePath
+
         uni.getImageInfo({
-          src: result,
+          src: filePath,
           success: (imageInfo) => {
             const screenWidth = uni.getSystemInfoSync().windowWidth - 32
             pdfWidth.value = screenWidth
             pdfHeight.value = screenWidth * imageInfo.height / imageInfo.width
           }
         })
+      } else {
+        uni.showToast({ title: '获取任务单图片失败', icon: 'none' })
       }
     } catch (error) {
       console.error('获取签名图片失败:', error)
+      uni.showToast({ title: '获取任务单图片失败', icon: 'none' })
     }
+  } else {
+    console.log('缺少必要的配置信息:', { dataStr: !!dataStr, templateUrl: !!templateUrl })
+    loading.value = false
   }
 }
 
@@ -220,7 +244,7 @@ const handleReSign = () => {
 // 更多操作
 const handlePushOrder = () => {
   const itemList = routeType.value === 'ZXXX' ? ['出具重大问题线索告知单', '更新'] : ['推送', '更新']
-  
+
   uni.showActionSheet({
     itemList: itemList,
     success: (res) => {
@@ -228,18 +252,76 @@ const handlePushOrder = () => {
         // 推送
         showPushPopup.value = true
       } else if (res.tapIndex === 1) {
-        // 更新
-        handleUpdate()
+        // 更新 - 检查签名状态
+        handleUpdateCheck()
       }
     }
   })
 }
 
-// 更新
-const handleUpdate = () => {
-  // 跳转到电子表格编辑器
-  const url = `/pages/webview/generic-webview?businessType=${routeType.value}&orderId=${orderId.value}&templateId=${templateId.value}&useOnline=1`
-  uni.navigateTo({ url })
+// 更新前检查签名状态
+const handleUpdateCheck = () => {
+  if (pdfImg.value) {
+    const confirmMessageMap: Record<string, string> = {
+      FWD: '更新会清除当前签约代表签名,是否确认?',
+      JYRS: '更新会清除当前客户代表签名,是否确认?',
+      AQJC: '更新会清除当前受检单位签名,是否确认?',
+      ZXXX: '更新会清除当前受检单位签名,是否确认?'
+    }
+    const confirmMessage = confirmMessageMap[routeType.value]
+    if (confirmMessage) {
+      uni.showModal({
+        title: '温馨提示',
+        content: confirmMessage,
+        success: (modalRes) => {
+          if (modalRes.confirm) {
+            handleSpreadsheetEdit()
+          }
+        }
+      })
+    } else {
+      handleSpreadsheetEdit()
+    }
+  } else {
+    handleSpreadsheetEdit()
+  }
+}
+
+// 跳转到电子表格编辑器
+const handleSpreadsheetEdit = () => {
+  if (!routeType.value || !orderId.value || !initData.value) {
+    uni.showToast({ title: '配置信息不完整', icon: 'none' })
+    return
+  }
+
+  try {
+    const businessConfig = createBusinessConfig(routeType.value as any)
+
+    const editInitData = { ...initData.value }
+    if (orderItemId.value) {
+      editInitData.orderItemId = orderItemId.value
+    }
+    editInitData.securityCheckId = securityCheckId.value || ''
+    editInitData.reportId = reportId.value || ''
+
+    const editParams = {
+      routeType: routeType.value,
+      orderId: orderId.value,
+      gcConfig: editInitData,
+      businessConfig,
+      orderItemId: orderItemId.value || undefined,
+    }
+
+    setSpreadsheetEditParams(editParams)
+    console.log('设置全局编辑参数:', JSON.stringify(editParams))
+
+    uni.navigateTo({
+      url: `/pages/webview/generic-webview?businessType=${routeType.value}&orderId=${orderId.value}&templateId=${editInitData.templateId || ''}&useOnline=1`,
+    })
+  } catch (error) {
+    console.error('创建业务配置失败:', error)
+    uni.showToast({ title: '启动编辑器失败,请重试', icon: 'none' })
+  }
 }
 
 // 关闭推送弹窗
@@ -250,20 +332,21 @@ const closePushPopup = () => {
 
 // 推送任务单提交
 const handlePushOrderSubmit = () => {
+  console.log('输入的内容:', inputName.value, inputPhone.value, inputEmail.value)
   if (!inputName.value || !inputPhone.value) {
     return uni.showToast({ title: '请输入接收人姓名和手机号', icon: 'none' })
   }
-  
+
   // 校验手机号
   if (!/^1[3456789]\d{9}$/.test(inputPhone.value)) {
     return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
   }
-  
+
   // 校验邮箱(如果填写了)
   if (inputEmail.value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(inputEmail.value)) {
     return uni.showToast({ title: '请输入正确的电子邮箱', icon: 'none' })
   }
-  
+
   const params = {
     id: orderId.value,
     receiver: inputName.value,
@@ -272,8 +355,9 @@ const handlePushOrderSubmit = () => {
     businessType: routeType.value ? businessTypeMap[routeType.value] : '',
     orderItemId: orderItemId.value || undefined,
   }
-  
+
   pushTaskOrder(params).then((res: any) => {
+    console.log(res, 'pushTaskOrder res')
     if (res?.code === 0) {
       uni.showToast({ title: '推送成功', icon: 'success' })
       showPushPopup.value = false
@@ -288,10 +372,11 @@ const handlePushOrderSubmit = () => {
   })
 }
 
-// 页面显示时重新获取数据
+// 页面显示时重新获取数据(从编辑器返回时)
 onShow(() => {
   if (orderId.value) {
-    getTaskOrderGcConfig()
+    console.log('详情页面显示,重新获取签名图片数据')
+    gcConfig.value && getTaskOrderSignImg(gcConfig.value)
   }
 })
 
@@ -444,7 +529,7 @@ onMounted(() => {
 }
 
 .form-label {
-  width: 100px;
+  width: 120px;
   font-size: 14px;
   color: #666;
   flex-shrink: 0;
@@ -480,13 +565,12 @@ onMounted(() => {
 }
 
 .cancel-btn {
-  background-color: #fff;
-  color: #666;
-  border: 1px solid #ddd;
+  background-color: #94bddfff;
+  color: #fff;
 }
 
 .confirm-btn {
-  background-color: rgb(47, 142, 255);
+  background-color: #071F50;
   color: #fff;
 }
 </style>

+ 872 - 0
src/pages/sign/index-back.vue

@@ -0,0 +1,872 @@
+<route lang="json5" type="page">
+{
+  layout: 'default',
+  style: {
+    navigationBarTitleText: '签字',
+    navigationStyle: 'custom',
+  },
+}
+</route>
+
+<template>
+  <view class="sign-container">
+    <!-- 导航栏 -->
+    <view class="navigate-view">
+      <view class="navigate-left" @click="goBack">
+        <image class="back-icon" src="/static/images/back.png" />
+      </view>
+      <text class="navigate-title">{{ title }}</text>
+      <view class="navigate-right"></view>
+    </view>
+
+    <!-- 内容区域 -->
+    <scroll-view class="scroll-content" scroll-y>
+      <!-- PDF 图片预览 -->
+      <view
+        v-if="pdfImg"
+        class="pdf-preview"
+        :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }"
+      >
+        <image :src="pdfImg" mode="aspectFit" class="pdf-image" />
+      </view>
+      <view v-else class="pdf-placeholder">
+        <text class="placeholder-text">加载中...</text>
+      </view>
+
+      <!-- 签名区域分割线 -->
+      <view class="sign-divider"></view>
+
+      <!-- 签名区域(三种状态:空/签名中/已签名) -->
+      <view class="sign-section">
+        <!-- 状态1:空状态 - 点击签名 -->
+        <template v-if="signStatus === 'empty'">
+          <view class="sign-header">
+            <text class="sign-title-text">手写签名</text>
+          </view>
+          <view class="sign-view" @click="handleToSign">
+            <text class="sign-view-text">点击签名</text>
+          </view>
+        </template>
+
+        <!-- 状态2:签名中 - 显示画布 -->
+        <template v-if="signStatus === 'signing'">
+          <view class="sign-header">
+            <text class="sign-title-text">手写签名</text>
+            <view class="sign-header-actions">
+              <button class="header-action-btn header-reset-btn" @click="clearCanvas">清除</button>
+              <button class="header-action-btn header-cancel-btn" @click="handleCancelSign">
+                取消
+              </button>
+              <button class="header-action-btn header-confirm-btn" @click="confirmSign">
+                确认
+              </button>
+            </view>
+          </view>
+          <view class="sign-canvas-wrapper">
+            <SignatureCanvas
+              ref="signatureRef"
+              canvas-id="signCanvas"
+              :width="canvasWidth"
+              :height="178"
+              @signed="onCanvasSigned"
+            />
+          </view>
+        </template>
+
+        <!-- 状态3:已签名 - 显示签名图片 -->
+        <template v-if="signStatus === 'signed'">
+          <view class="sign-header">
+            <text class="sign-title-text">签名时间:</text>
+            <text class="sign-title-text">{{ signTime }}</text>
+          </view>
+          <view class="sign-img">
+            <view class="sign-img-del" @click="handleDelSign">X</view>
+            <image :src="showSignImg" mode="aspectFit" class="sign-image" :style="signImgStyle" />
+          </view>
+        </template>
+      </view>
+
+      <!-- 联系信息表单(服务单/受理单类型) -->
+      <view v-if="routeType === 'FWD'" class="form-section">
+        <view class="form-item">
+          <text class="form-label">接收人手机号:</text>
+          <input
+            v-model="fwdInputPhone"
+            class="form-input"
+            type="number"
+            maxlength="11"
+            placeholder="请输入接收人手机号"
+          />
+        </view>
+      </view>
+    </scroll-view>
+
+    <!-- 底部按钮 -->
+    <view class="footer-bar">
+      <button class="footer-btn more-btn" @click="handlePushOrder">更多操作</button>
+      <button class="footer-btn confirm-btn" @click="signSubmit">{{ signButtonText }}</button>
+    </view>
+
+    <!-- 服务单接收人确认弹窗 -->
+    <view v-if="showFwdPopup" class="popup-overlay" @click="closeFwdPopup">
+      <view class="popup-content" @click.stop>
+        <text class="popup-title">确认提交</text>
+        <text class="popup-message">是否确认签约代表签名?</text>
+        <view class="popup-actions">
+          <button class="action-btn cancel-btn" @click="closeFwdPopup">取消</button>
+          <button class="action-btn confirm-btn" @click="confirmFwdSubmit">确定</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 推送任务单弹窗 -->
+    <view v-if="showInputPopup" class="popup-overlay" @click="closeInputPopup">
+      <view class="popup-content" @click.stop>
+        <text class="popup-title">推送任务单</text>
+        <view class="form-item">
+          <text class="form-label">接收人名称:</text>
+          <input class="form-input" v-model="inputName" placeholder="请输入接收人名称" />
+        </view>
+        <view class="form-item">
+          <text class="form-label">接收人手机号:</text>
+          <input
+            class="form-input"
+            v-model="inputPhone"
+            type="number"
+            maxlength="11"
+            placeholder="请输入接收人手机号"
+          />
+        </view>
+        <view v-if="routeType !== 'ZXXX'" class="form-item">
+          <text class="form-label">电子邮箱:</text>
+          <input
+            class="form-input"
+            v-model="inputEmail"
+            type="text"
+            placeholder="请输入电子邮箱(选填)"
+          />
+        </view>
+        <view class="popup-actions">
+          <button class="action-btn cancel-btn" @click="closeInputPopup">取消</button>
+          <button class="action-btn confirm-btn" @click="handlePushOrderSubmit">确定</button>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, onMounted } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import {
+  getGcConfig,
+  getTaskOrderImg,
+  signConfirm,
+  pushTaskOrder,
+  createSafetyCheckRecord,
+} from '@/api/sign'
+import { useUserStore } from '@/store/user'
+import SignatureCanvas from '@/components/Signature/SignatureCanvas.vue'
+
+const title = ref('')
+const routeType = ref<'FWD' | 'JYRS' | 'AQJC' | 'ZXXX'>()
+const orderId = ref('')
+const orderItemId = ref('')
+const securityCheckId = ref('')
+const reportId = ref('')
+const unitContact = ref('')
+const unitPhone = ref('')
+const receiverEmail = ref('')
+const templateId = ref('')
+
+const pdfImg = ref('')
+const pdfWidth = ref(0)
+const pdfHeight = ref(0)
+const showSignImg = ref('')
+const signImg = ref('')
+const signImgStyle = ref<any>({})
+const signTime = ref('')
+const fwdInputPhone = ref('')
+
+const showFwdPopup = ref(false)
+const showInputPopup = ref(false)
+const canvasWidth = ref(300)
+const inputName = ref('')
+const inputPhone = ref('')
+const inputEmail = ref('')
+const gcConfig = ref<any>(null)
+
+// 签名区域状态:empty | signing | signed
+const signStatus = ref<'empty' | 'signing' | 'signed'>('empty')
+
+const signatureRef = ref<any>(null)
+
+const userStore = useUserStore()
+const userInfo = computed(() => userStore.userInfo)
+
+const titleTextMap: Record<string, string> = {
+  FWD: '服务单/受理单',
+  JYRS: '检验结果告知',
+  AQJC: '安全检查记录',
+  ZXXX: '重大问题线索告知',
+}
+
+const businessTypeMap: Record<string, number> = {
+  FWD: 100,
+  JYRS: 200,
+  AQJC: 300,
+  ZXXX: 400,
+}
+
+const signButtonTextMap: Record<string, string> = {
+  FWD: '签约代表签名',
+  JYRS: '客户代表签名',
+  AQJC: '受检单位签名',
+  ZXXX: '受检单位签名',
+}
+
+const confirmTextMap: Record<string, string> = {
+  FWD: '是否确认签约代表签名?',
+  JYRS: '是否确认客户代表签名?',
+  AQJC: '是否确认受检单位签名?',
+  ZXXX: '是否提交审核?',
+}
+
+const signButtonText = computed(() => {
+  return routeType.value ? signButtonTextMap[routeType.value] || '签名' : '签名'
+})
+
+onLoad((options) => {
+  const type = options?.type as string
+  routeType.value = type as any
+  title.value = titleTextMap[type] || '签字'
+  orderId.value = options?.orderId || ''
+  orderItemId.value = options?.orderItemId || ''
+  securityCheckId.value = options?.securityCheckId || ''
+  reportId.value = options?.reportId || ''
+  unitContact.value = options?.unitContact || ''
+  unitPhone.value = options?.unitPhone || ''
+  receiverEmail.value = options?.receiverEmail || ''
+  templateId.value = options?.templateId || ''
+
+  fwdInputPhone.value = unitPhone.value
+  inputName.value = unitContact.value
+  inputPhone.value = unitPhone.value
+  inputEmail.value = receiverEmail.value
+
+  // 初始化画布宽度
+  const sysInfo = uni.getSystemInfoSync()
+  canvasWidth.value = sysInfo.windowWidth - 32
+})
+
+// 获取任务单配置
+const getTaskOrderGcConfig = async () => {
+  try {
+    const result: any = await getGcConfig(
+      {
+        orderId: orderId.value,
+        businessType: routeType.value ? businessTypeMap[routeType.value] : '',
+        orderItemId: orderItemId.value || undefined,
+        templateId: templateId.value || undefined,
+      },
+      userInfo.value,
+    )
+
+    const res = result?.data
+    gcConfig.value = res
+
+    if (res) {
+      fetchTaskOrderImg(res)
+    } else {
+      uni.showToast({ title: '获取配置信息失败', icon: 'error' })
+    }
+  } catch (error) {
+    console.error('获取配置失败:', error)
+    uni.showToast({ title: '获取配置失败', icon: 'error' })
+  }
+}
+
+// 获取任务单图片
+const fetchTaskOrderImg = async (res: any) => {
+  const { dataStr, templateUrl, templateId, fileVersionId } = res
+
+  if (!orderId.value) return
+
+  if (orderId.value && dataStr && templateUrl) {
+    const params: any = {
+      dataStr,
+      templateUrl,
+      orderId: orderId.value,
+      businessType: routeType.value ? businessTypeMap[routeType.value] : '',
+      fileType: 100,
+      orderItemId: orderItemId.value || undefined,
+      templateId,
+    }
+
+    if (routeType.value === 'ZXXX' && fileVersionId) {
+      params.fileVersionId = fileVersionId
+    }
+
+    try {
+      const result: any = await getTaskOrderImg(params)
+      if (result) {
+        pdfImg.value = result
+        uni.getImageInfo({
+          src: result,
+          success: (imageInfo) => {
+            const screenWidth = uni.getSystemInfoSync().windowWidth - 32
+            pdfWidth.value = screenWidth
+            pdfHeight.value = (screenWidth * imageInfo.height) / imageInfo.width
+          },
+        })
+      }
+    } catch (error) {
+      console.error('获取图片失败:', error)
+    }
+  }
+}
+
+// 点击签名 - 切换到签名画布状态
+const handleToSign = () => {
+  signStatus.value = 'signing'
+  // 重新计算画布宽度
+  const sysInfo = uni.getSystemInfoSync()
+  canvasWidth.value = sysInfo.windowWidth - 32
+}
+
+// 画布签名完成回调
+const onCanvasSigned = (hasContent: boolean) => {
+  console.log('画布签名状态:', hasContent)
+}
+
+// 清除画布
+const clearCanvas = () => {
+  if (signatureRef.value) {
+    signatureRef.value.clear()
+  }
+}
+
+// 取消签名 - 返回空状态
+const handleCancelSign = () => {
+  signStatus.value = 'empty'
+}
+
+// 确认签名
+const confirmSign = async () => {
+  if (!signatureRef.value) return
+  if (signatureRef.value.isEmpty()) {
+    uni.showToast({ title: '请先签字再保存', icon: 'none' })
+    return
+  }
+
+  try {
+    const path = await signatureRef.value.getImage()
+    signImg.value = path
+    showSignImg.value = path
+    console.log('imagePath....', path)
+
+    uni.getImageInfo({
+      src: path,
+      success: (imageInfo) => {
+        const screenWidth = uni.getSystemInfoSync().windowWidth - 32
+        signImgStyle.value = {
+          width: `${screenWidth}px`,
+          height: `${(screenWidth * imageInfo.height) / imageInfo.width}px`,
+        }
+      },
+    })
+
+    const now = new Date()
+    signTime.value = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`
+
+    signStatus.value = 'signed'
+  } catch (err) {
+    console.error('转换失败:', err)
+    uni.showToast({ title: '签名失败', icon: 'error' })
+  }
+}
+
+// 删除签名
+const handleDelSign = () => {
+  signImg.value = ''
+  showSignImg.value = ''
+  signTime.value = ''
+  signStatus.value = 'empty'
+}
+
+// 提交签名
+const signSubmit = () => {
+  if (!signImg.value) {
+    uni.showToast({ title: '请先签名', icon: 'none' })
+    return
+  }
+
+  if (routeType.value === 'FWD') {
+    if (!fwdInputPhone.value) {
+      uni.showToast({ title: '请输入接收人手机号', icon: 'none' })
+      return
+    }
+    if (!/^1[3456789]\d{9}$/.test(fwdInputPhone.value)) {
+      uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
+      return
+    }
+    showFwdPopup.value = true
+  } else {
+    submitConfirm()
+  }
+}
+
+// 确认提交
+const submitConfirm = async () => {
+  try {
+    const params: any = {
+      id: orderId.value,
+      signUrl: signImg.value,
+      businessType: routeType.value ? businessTypeMap[routeType.value] : '',
+      orderItemId: orderItemId.value || undefined,
+    }
+
+    if (routeType.value === 'FWD') {
+      params.receiverPhone = fwdInputPhone.value
+    }
+
+    const result: any = await signConfirm(params)
+
+    if (result?.code === 0) {
+      uni.showToast({
+        title: routeType.value === 'ZXXX' ? '已自动提交审核' : '签名成功',
+        icon: 'success',
+      })
+      setTimeout(() => {
+        uni.redirectTo({
+          url: `/pages/orderDetail/detail?orderId=${orderId.value}&type=${routeType.value}`,
+        })
+      }, 1500)
+    } else {
+      uni.showToast({ title: result?.msg || '签名失败', icon: 'error' })
+    }
+  } catch (error: any) {
+    console.error('签名失败:', error)
+    uni.showToast({ title: error?.msg || '签名失败', icon: 'error' })
+  } finally {
+    showFwdPopup.value = false
+  }
+}
+
+// 服务单提交确认
+const confirmFwdSubmit = () => {
+  submitConfirm()
+}
+
+// 关闭服务单弹窗
+const closeFwdPopup = () => {
+  showFwdPopup.value = false
+}
+
+// 更多操作
+const handlePushOrder = () => {
+  if (routeType.value === 'ZXXX') {
+    uni.showActionSheet({
+      itemList: ['小程序推送签名', '更新'],
+      success: (res) => {
+        if (res.tapIndex === 0) {
+          showInputPopup.value = true
+        } else if (res.tapIndex === 1) {
+          handleSpreadsheetEdit()
+        }
+      },
+    })
+  } else {
+    uni.showActionSheet({
+      itemList: ['推送', '更新'],
+      success: (res) => {
+        if (res.tapIndex === 0) {
+          showInputPopup.value = true
+        } else if (res.tapIndex === 1) {
+          handleSpreadsheetEdit()
+        }
+      },
+    })
+  }
+}
+
+// 跳转到电子表格编辑器
+const handleSpreadsheetEdit = () => {
+  if (!routeType.value || !orderId.value || !gcConfig.value) {
+    return uni.showToast({ title: '配置信息不完整', icon: 'none' })
+  }
+
+  const url = `/pages/webview/generic-webview?businessType=${routeType.value}&orderId=${orderId.value}&templateId=${gcConfig.value.templateId || ''}&useOnline=1`
+  uni.navigateTo({ url })
+}
+
+// 关闭推送弹窗
+const closeInputPopup = () => {
+  showInputPopup.value = false
+  inputEmail.value = ''
+}
+
+// 推送任务单提交
+const handlePushOrderSubmit = () => {
+  if (!inputName.value || !inputPhone.value) {
+    return uni.showToast({ title: '请输入接收人姓名和手机号', icon: 'none' })
+  }
+
+  if (!/^1[3456789]\d{9}$/.test(inputPhone.value)) {
+    return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
+  }
+
+  const params: any = {
+    id: orderId.value,
+    receiver: inputName.value,
+    receiverPhone: inputPhone.value,
+    receiverEmail: routeType.value !== 'ZXXX' ? inputEmail.value || '' : '',
+    businessType: routeType.value ? businessTypeMap[routeType.value] : '',
+    orderItemId: orderItemId.value || undefined,
+    securityCheckId: securityCheckId.value,
+  }
+
+  pushTaskOrder(params)
+    .then((res: any) => {
+      if (res?.code === 0) {
+        uni.showToast({ title: '推送成功', icon: 'success' })
+        showInputPopup.value = false
+        getTaskOrderGcConfig()
+      } else {
+        uni.showToast({ title: res?.msg || '推送失败', icon: 'none' })
+      }
+    })
+    .catch((error) => {
+      console.error('推送失败:', error)
+      uni.showToast({ title: '推送失败', icon: 'none' })
+    })
+}
+
+// 返回
+const goBack = () => {
+  uni.navigateBack()
+}
+
+onMounted(() => {
+  if (orderId.value) {
+    getTaskOrderGcConfig()
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.sign-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #fff;
+}
+
+.navigate-view {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  height: 44px;
+  padding: 0 15px;
+  background-color: #fff;
+  border-bottom: 1px solid #eee;
+
+  .navigate-left {
+    display: flex;
+    align-items: center;
+  }
+
+  .back-icon {
+    width: 20px;
+    height: 20px;
+  }
+
+  .navigate-title {
+    font-size: 17px;
+    font-weight: 500;
+    color: #333;
+  }
+
+  .navigate-right {
+    width: 20px;
+  }
+}
+
+.scroll-content {
+  flex: 1;
+  padding: 16px;
+}
+
+.pdf-preview {
+  margin: 0 auto 16px;
+  overflow: hidden;
+  background-color: #f5f5f5;
+  border-radius: 8px;
+
+  .pdf-image {
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.pdf-placeholder {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 200px;
+  margin-bottom: 16px;
+  background-color: #f5f5f5;
+  border-radius: 8px;
+
+  .placeholder-text {
+    font-size: 14px;
+    color: #999;
+  }
+}
+
+// 分割线
+.sign-divider {
+  width: 100%;
+  height: 1px;
+  margin: 0 0 12px;
+  background-color: #e0e0e0;
+}
+
+// 签名区域
+.sign-section {
+  width: 100%;
+  padding-bottom: 12px;
+  background-color: #fff;
+
+  .sign-header {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 8px;
+
+    .sign-title-text {
+      font-size: 20px;
+      line-height: 28px;
+      color: #333;
+    }
+
+    .sign-header-actions {
+      display: flex;
+      flex-direction: row;
+      gap: 8px;
+
+      .header-action-btn {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 30px;
+        padding: 0 12px;
+        font-size: 14px;
+        border: none;
+        border-radius: 6px;
+      }
+
+      .header-reset-btn {
+        color: rgb(16, 16, 16);
+        background-color: rgb(230, 238, 245);
+        border: 1px solid rgb(187, 187, 187);
+      }
+
+      .header-cancel-btn {
+        color: rgb(16, 16, 16);
+        background-color: #f5f5f5;
+        border: 1px solid rgb(187, 187, 187);
+      }
+
+      .header-confirm-btn {
+        color: #fff;
+        background-color: rgb(7, 31, 80);
+      }
+    }
+  }
+
+  // 空状态 - 点击签名
+  .sign-view {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 178px;
+    background-color: #d8d8d8;
+
+    .sign-view-text {
+      font-size: 42px;
+      font-weight: bold;
+      color: #999;
+    }
+  }
+
+  // 签名画布区域
+  .sign-canvas-wrapper {
+    width: 100%;
+    overflow: hidden;
+    background-color: #ffffff;
+    border: 2px dashed rgb(187, 187, 187);
+    border-radius: 8px;
+  }
+
+  // 已签名 - 图片展示
+  .sign-img {
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 10px;
+    border: 1px solid #ccc;
+    border-radius: 10px;
+
+    .sign-img-del {
+      position: absolute;
+      top: 0;
+      right: 0;
+      z-index: 2;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 20px;
+      height: 20px;
+      font-size: 12px;
+      color: #333;
+      background-color: #e0e0e0;
+      border-radius: 10px;
+    }
+
+    .sign-image {
+      display: block;
+      width: 100%;
+    }
+  }
+}
+
+// 表单区域
+.form-section {
+  padding: 15px 0;
+  margin-bottom: 10px;
+  background-color: #fff;
+  border-top: 1px solid #eee;
+
+  .form-item {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+
+    .form-label {
+      width: 120px;
+      font-size: 14px;
+      color: #666;
+    }
+
+    .form-input {
+      flex: 1;
+      height: 40px;
+      padding: 0 10px;
+      font-size: 14px;
+      border: 1px solid #ddd;
+      border-radius: 5px;
+    }
+  }
+}
+
+// 底部按钮
+.footer-bar {
+  display: flex;
+  flex-direction: row;
+  gap: 12px;
+  padding: 12px 16px 16px;
+  background-color: #ffffff;
+  border-top: 1px solid #e0e0e0;
+
+  .footer-btn {
+    display: flex;
+    flex: 1;
+    align-items: center;
+    justify-content: center;
+    height: 44px;
+    font-size: 16px;
+    color: #fff;
+    border: none;
+    border-radius: 6px;
+  }
+
+  .more-btn {
+    background-color: #e6a23c;
+  }
+
+  .confirm-btn {
+    background-color: #00a811;
+  }
+}
+
+// 弹窗
+.popup-overlay {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: rgba(0, 0, 0, 0.5);
+
+  .popup-content {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    width: 66.67%;
+    padding: 20px;
+    background-color: #fff;
+    border-radius: 10px;
+
+    .popup-title {
+      margin-bottom: 15px;
+      font-size: 18px;
+      font-weight: 500;
+      color: #333;
+    }
+
+    .popup-message {
+      margin-bottom: 20px;
+      font-size: 15px;
+      color: #666;
+    }
+
+    .popup-actions {
+      display: flex;
+      flex-direction: row;
+      gap: 10px;
+      width: 100%;
+
+      .action-btn {
+        display: flex;
+        flex: 1;
+        align-items: center;
+        justify-content: center;
+        height: 40px;
+        font-size: 15px;
+        border: none;
+        border-radius: 5px;
+      }
+
+      .cancel-btn {
+        color: #fff;
+        background-color: #94bddf;
+      }
+
+      .confirm-btn {
+        color: #fff;
+        background-color: #071f50;
+      }
+    }
+  }
+}
+</style>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 496 - 337
src/pages/sign/index.vue


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1225 - 0
src/pages/taskOnline/TaskOnlineEquipmentList.vue


+ 2 - 2
src/pages/taskOnlinePage/taskOnline.vue

@@ -203,9 +203,9 @@ const handleClaimTask = async (id: string, isClaim: boolean) => {
   try {
     let result = null
     if (isClaim) {
-      result = requestFunc(TaskOrderFuncName.EquipmentConfirmClaim, equipType, { id })
+      result = await requestFunc(TaskOrderFuncName.EquipmentConfirmClaim, equipType, { id })
     } else {
-      result = requestFunc(TaskOrderFuncName.EquipmentCancelClaim, equipType, { id })
+      result = await requestFunc(TaskOrderFuncName.EquipmentCancelClaim, equipType, { id })
     }
 
     if (result?.code === 0 && result?.data) {

+ 8 - 4
src/pages/unClaim/components/TaskItem.vue

@@ -14,9 +14,13 @@
           <text class="task-top-num-box">{{ item.equipNum }}台</text>
         </view>
         <text
-          :style="{ fontSize: '15px', color: isClaim ? '#2F8EFF' : '#FF9925', marginLeft: '10px' }"
+          :style="{
+            fontSize: '15px',
+            color: isClaimed() ? '#2F8EFF' : '#FF9925',
+            marginLeft: '10px',
+          }"
         >
-          {{ isClaim ? '已认领' : '待认领' }}
+          {{ isClaimed() ? '已认领' : '待认领' }}
         </text>
       </view>
     </view>
@@ -146,7 +150,7 @@ const taskItemInfo = computed(() => ({
 
 // 跳转到设备列表
 const pushEquipmentList = () => {
-  if (!isClaim.value) {
+  if (!isClaimed()) {
     // 显示确认弹窗
     uni.showModal({
       title: '确认',
@@ -190,7 +194,7 @@ const handleUpdateContact = () => {
 
 // 认领/取消认领
 const handleClaim = () => {
-  emit('claimTask', props.item.id, !isClaim.value)
+  emit('claimTask', props.item.id, !isClaimed())
 }
 
 // PDF 详情

+ 3 - 3
src/pages/unClaim/unClaimList.vue

@@ -240,14 +240,14 @@ const handleClaimTask = async (id: string, isClaim: boolean) => {
   try {
     const result = await requestFunc(TaskOrderFuncName.TaskConfirm, equipType, {
       id,
-      confirm: !isClaim,
+      confirm: isClaim,
     })
     if (result?.code === 0 && result?.data) {
-      uni.showToast({ title: `${isClaim ? '取消认领' : '认领'}成功`, icon: 'success' })
+      uni.showToast({ title: `${isClaim ? '认领' : '取消认领'}成功`, icon: 'success' })
       itemRefs[id]?.setIsClaim(isClaim)
       refreshList()
     } else {
-      const msg = result?.msg || `${isClaim ? '取消认领' : '认领'}失败`
+      const msg = result?.msg || `${isClaim ? '认领' : '取消认领'}失败`
       uni.showToast({ title: msg, icon: 'error' })
     }
   } catch (error) {

+ 2 - 0
src/types/uni-pages.d.ts

@@ -13,10 +13,12 @@ interface NavigateToOptions {
        "/pages/securityCheck/securityCheckEditor" |
        "/pages/securityCheck/securityCheckList" |
        "/pages/serviceOrderDetail/index" |
+       "/pages/sign/index-back" |
        "/pages/sign/index" |
        "/pages/sign-detail/index" |
        "/pages/spread/index" |
        "/pages/systemFile/systemFile" |
+       "/pages/taskOnline/TaskOnlineEquipmentList" |
        "/pages/taskOnlinePage/taskOnline" |
        "/pages/unClaim/unClaimList" |
        "/pages/unitQuery/unitQuery" |