Pārlūkot izejas kodu

完成签名上传、完善任务认领安全检测记录的列表和删除、调整设备列表样式

yangguanjin 2 nedēļas atpakaļ
vecāks
revīzija
b0400c07e3

+ 79 - 0
src/api/ApiRouter/taskOrderSecurityCheck.ts

@@ -0,0 +1,79 @@
+import { EquipmentType } from '@/utils/dictMap'
+import { getSafetyCheckRecordPage, deleteSafetyCheckRecord } from '@/api/task'
+import {
+  getBoilerSecurityCheckPage,
+  delBoilerSecurityCheck,
+} from '@/api/boiler/boilerTaskOrderSecurityCheck'
+import {
+  getPipeSecurityCheckPage,
+  delPipeSecurityCheck,
+} from '@/api/pipe/pipeTaskOrderSecurityCheck'
+
+type Adapter = {
+  inputAdapter: (data: any) => any
+  reqFunction: (params: any) => any
+  outputAdapter: (data: any) => any
+}
+
+export enum SecurityCheckFuncName {
+  getPage,
+  delSecurityCheckItem,
+}
+
+const map = {
+  [SecurityCheckFuncName.getPage]: {
+    [EquipmentType.BOILER]: {
+      inputAdapter: null,
+      reqFunction: getBoilerSecurityCheckPage,
+      outputAdapter: null,
+    },
+    [EquipmentType.PIPE]: {
+      inputAdapter: null,
+      reqFunction: getPipeSecurityCheckPage,
+      outputAdapter: null,
+    },
+    [EquipmentType.CONTAINER]: {
+      inputAdapter: null,
+      reqFunction: getSafetyCheckRecordPage,
+      outputAdapter: null,
+    },
+  },
+  [SecurityCheckFuncName.delSecurityCheckItem]: {
+    [EquipmentType.BOILER]: {
+      inputAdapter: null,
+      reqFunction: delBoilerSecurityCheck,
+      outputAdapter: null,
+    },
+    [EquipmentType.PIPE]: {
+      inputAdapter: null,
+      reqFunction: delPipeSecurityCheck,
+      outputAdapter: null,
+    },
+    [EquipmentType.CONTAINER]: {
+      inputAdapter: null,
+      reqFunction: deleteSafetyCheckRecord,
+      outputAdapter: null,
+    },
+  },
+}
+
+export const requestFunc = (funcName: IndexFuncName, equipType: EquipmentType, params: any) => {
+  const funMap = map[funcName]
+  const adapter = funMap[equipType]
+  // 1. input adapter
+  let reqParams = params
+  if (adapter.inputAdapter != null) {
+    reqParams = adapter.inputAdapter(params)
+  }
+  if (!adapter.reqFunction) {
+    throw new Error('api for send is not exists')
+  }
+  // 2. send req
+  const respData = adapter.reqFunction(params)
+  // 3. output adapter
+  let adaptedRespData = respData
+  if (adapter.outputAdapter) {
+    adaptedRespData = adaptedRespData = adapter.outputAdapter(respData)
+  }
+  return adaptedRespData
+}

+ 11 - 1
src/api/boiler/boilerTaskOrderSecurityCheck.ts

@@ -3,4 +3,14 @@ import { httpGet, httpPost, httpPUT, httpDelete } from '@/utils/http'
 // 任务确认分页列表
 export const getSecurityCheckTemplate = (params: any) => {
   return httpGet('/pressure2/boiler-task-order-security-check/default-template', params)
-}
+}
+
+// 获取安全检查记录分页
+export const getBoilerSecurityCheckPage = (params: any) => {
+  return httpGet('/pressure2/boiler-task-order-security-check/page', params)
+}
+
+// 删除安全检查记录
+export const delBoilerSecurityCheck = (params: any) => {
+  return httpDelete('/pressure2/boiler-task-order-security-check/delete', params)
+}

+ 16 - 0
src/api/pipe/pipeTaskOrderSecurityCheck.ts

@@ -0,0 +1,16 @@
+import { httpGet, httpPost, httpPUT, httpDelete } from '@/utils/http'
+
+// 任务确认分页列表
+export const getSecurityCheckTemplate = (params: any) => {
+  return httpGet('/pressure2/pipe-task-order-security-check/default-template', params)
+}
+
+// 获取服务单下的安全检查记录
+export const getPipeSecurityCheckPage = (params: any) => {
+  return httpGet('/pressure2/pipe-task-order-security-check/page', params)
+}
+
+// 删除安全检查记录
+export const delPipeSecurityCheck = (params: any) => {
+  return httpDelete('/pressure2/pipe-task-order-security-check/delete', params)
+}

+ 21 - 0
src/api/sign.ts

@@ -1,4 +1,5 @@
 import { httpGet, httpPost, httpPUT } from '@/utils/http'
+import { getEnvBaseUrl } from '@/utils/index'
 
 /**
  * 获取任务单葡萄城配置信息
@@ -98,3 +99,23 @@ export const addFiniteSpaceReport = (data: any) => {
 export const saveReportPrepare = (data: any) => {
   return httpPUT('/pressure/task-order/order-item/report/prepare/save', data)
 }
+
+export const uploadSignImage = (data: any) => {
+  const url = '/infra/file/upload'
+  let requestUrl = url
+  if (JSON.parse(import.meta.env.VITE_APP_PROXY) && import.meta.env.MODE === 'development') {
+    requestUrl = import.meta.env.VITE_APP_PROXY_PREFIX + url
+  } else {
+    requestUrl = getEnvBaseUrl() + url
+  }
+  const token = uni.getStorageSync('ACCESS_TOKEN')
+  const headers: Record<string, string> = {}
+  if (token) {
+    headers.Authorization = `Bearer ${token}`
+  }
+  return fetch(requestUrl, {
+    method: 'POST',
+    headers,
+    body: data,
+  }).then(res => res.json())
+}

+ 11 - 17
src/components/Popup/components/TipsPopup.vue

@@ -26,6 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
 
 const emit = defineEmits<{
   hide: []
+  confirm: []
 }>()
 
 // 取消
@@ -34,15 +35,8 @@ const handleCancel = () => {
 }
 
 // 确认
-const handleConfirm = async () => {
-  if (props.confirm) {
-    const result = await props.confirm()
-    if (result !== false) {
-      emit('hide')
-    }
-  } else {
-    emit('hide')
-  }
+const handleConfirm = () => {
+  emit('confirm')
 }
 </script>
 
@@ -62,9 +56,9 @@ const handleConfirm = async () => {
 
 .tips-text {
   font-size: 16px;
+  line-height: 1.5;
   color: #333;
   text-align: center;
-  line-height: 1.5;
 }
 
 .tips-buttons {
@@ -74,24 +68,24 @@ const handleConfirm = async () => {
 }
 
 .tips-btn {
-  flex: 1;
-  height: 44px;
-  border-radius: 8px;
   display: flex;
-  justify-content: center;
+  flex: 1;
   align-items: center;
-  border: none;
+  justify-content: center;
+  height: 44px;
   font-size: 15px;
+  border: none;
+  border-radius: 8px;
 }
 
 .tips-btn-cancel {
-  background-color: #f5f5f5;
   color: #666;
+  background-color: #f5f5f5;
 }
 
 .tips-btn-confirm {
-  background-color: #2f8eff;
   color: #fff;
+  background-color: #2f8eff;
 }
 
 .tips-btn-text {

+ 39 - 71
src/pages/securityCheck/securityCheckList.vue

@@ -35,21 +35,16 @@
       <view v-if="!loading && listData.length === 0" class="empty-text">暂无数据</view>
     </scroll-view>
 
-    <!-- 删除确认弹窗 -->
-    <view v-if="showDeletePopup" class="popup-mask" @click="closeDeletePopup">
-      <view class="popup-content" @click.stop>
-        <TipsPopup text="确认删除吗?" @hide="closeDeletePopup" @confirm="confirmDelete" />
-      </view>
-    </view>
+
   </view>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive, onMounted, onUnmounted } from 'vue'
+import { ref, reactive, onUnmounted } from 'vue'
 import { onLoad, onShow } from '@dcloudio/uni-app'
-import { getSafetyCheckRecordPage, deleteSafetyCheckRecord } from '@/api/task'
 import Item from './components/Item.vue'
-import TipsPopup from '@/components/Popup/components/TipsPopup.vue'
+import { useConfigStore } from '@/store/config'
+import { SecurityCheckFuncName, requestFunc } from '@/api/ApiRouter/taskOrderSecurityCheck'
 
 defineOptions({
   name: 'securityCheckList',
@@ -60,6 +55,8 @@ const orderId = ref('')
 const unitContact = ref('')
 const unitPhone = ref('')
 const receiverEmail = ref('')
+const configStore = useConfigStore()
+const equipType = configStore.equipType
 
 onLoad((options: any) => {
   orderId.value = options.orderId || ''
@@ -78,9 +75,35 @@ const params = reactive({
   orderId: '',
 })
 
-// 删除弹窗
-const showDeletePopup = ref(false)
-const deleteItem = ref<any>(null)
+// 删除记录
+const handleDeleteSavetyRecord = (item: any) => {
+  uni.showModal({
+    title: '提示',
+    content: '确认删除吗?',
+    success: async (res) => {
+      if (!res.confirm) return
+
+      try {
+        uni.showLoading({ title: '删除中...', mask: true })
+        const result = await requestFunc(SecurityCheckFuncName.delSecurityCheckItem, equipType, {
+          id: item.id,
+        })
+        uni.hideLoading()
+
+        if (result?.code === 0) {
+          uni.showToast({ title: '删除成功', icon: 'success', duration: 3000 })
+          refreshList()
+        } else {
+          uni.showToast({ title: result?.msg || '删除失败', icon: 'none', duration: 3000 })
+        }
+      } catch (error: any) {
+        uni.hideLoading()
+        console.error('删除安全检查记录失败:', error)
+        uni.showToast({ title: error?.msg || '删除失败', icon: 'none', duration: 3000 })
+      }
+    },
+  })
+}
 
 // 获取列表数据
 const fetchList = async (refresh = false) => {
@@ -93,7 +116,7 @@ const fetchList = async (refresh = false) => {
   uni.showLoading({ title: '加载中...', mask: true })
 
   try {
-    const result = await getSafetyCheckRecordPage(params)
+    const result = await requestFunc(SecurityCheckFuncName.getPage, equipType, params)
     uni.hideLoading()
 
     if (result?.code === 0 && result?.data?.list?.length) {
@@ -140,43 +163,6 @@ const handleModifySavetyRecord = (item: any) => {
   })
 }
 
-// 删除记录
-const handleDeleteSavetyRecord = (item: any) => {
-  deleteItem.value = item
-  showDeletePopup.value = true
-}
-
-// 关闭删除弹窗
-const closeDeletePopup = () => {
-  showDeletePopup.value = false
-  deleteItem.value = null
-}
-
-// 确认删除
-const confirmDelete = async () => {
-  if (!deleteItem.value) return
-
-  try {
-    uni.showLoading({ title: '删除中...', mask: true })
-    const res = await deleteSafetyCheckRecord({ id: deleteItem.value.id })
-    uni.hideLoading()
-
-    console.log('删除结果:', res)
-    if (res?.code === 0) {
-      uni.showToast({ title: '删除成功', icon: 'success', duration: 3000 })
-      refreshList()
-    } else {
-      uni.showToast({ title: res?.msg || '删除失败', icon: 'none', duration: 3000 })
-    }
-  } catch (error: any) {
-    uni.hideLoading()
-    console.error('删除安全检查记录失败:', error)
-    uni.showToast({ title: error?.msg || '删除失败', icon: 'none', duration: 3000 })
-  } finally {
-    closeDeletePopup()
-  }
-}
-
 // 页面显示时刷新
 onShow(() => {
   refreshList()
@@ -200,8 +186,8 @@ defineExpose({})
 
 .navigate-view {
   display: flex;
-  justify-content: center;
   align-items: center;
+  justify-content: center;
   height: 44px;
   background-color: #fff;
   border-bottom: 1px solid #eee;
@@ -225,27 +211,9 @@ defineExpose({})
 .loading-text,
 .no-more-text,
 .empty-text {
-  text-align: center;
   padding: 15px;
-  color: #999;
   font-size: 14px;
-}
-
-.popup-mask {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background-color: rgba(0, 0, 0, 0.5);
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  z-index: 999;
-}
-
-.popup-content {
-  width: 80%;
-  max-width: 320px;
+  color: #999;
+  text-align: center;
 }
 </style>

+ 106 - 1
src/pages/sign/index.vue

@@ -165,6 +165,7 @@ import {
   signConfirm,
   pushTaskOrder,
   createSafetyCheckRecord,
+  uploadSignImage,
 } from '@/api/sign'
 import { getDynamicTbVal } from '@/api/task'
 import { getStandardTemplate } from '@/api/index'
@@ -189,6 +190,7 @@ const pdfSource = ref<ArrayBuffer | Blob | null>(null)
 const pdfViewerRef = ref<any>(null)
 const showSignImg = ref('')
 const signImg = ref('')
+const uploadedSignUrl = ref('')
 const signImgStyle = ref<any>({})
 const signTime = ref('')
 const fwdInputPhone = ref('')
@@ -491,6 +493,94 @@ const handleCancelSign = () => {
   signStatus.value = 'empty'
 }
 
+const base64ToFile = (base64Data: string, fileName: string = 'signature.png'): File | null => {
+  // #ifdef H5
+  try {
+    const arr = base64Data.split(',')
+    const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png'
+    const bstr = atob(arr[1])
+    let n = bstr.length
+    const u8arr = new Uint8Array(n)
+    while (n--) {
+      u8arr[n] = bstr.charCodeAt(n)
+    }
+    return new File([u8arr], fileName, { type: mime })
+  } catch (e) {
+    console.error('base64 转 File 失败:', e)
+    return null
+  }
+  // #endif
+
+  // #ifndef H5
+  return null
+  // #endif
+}
+
+const base64ToTempFilePath = (base64Data: string): Promise<string> => {
+  return new Promise((resolve, reject) => {
+    // #ifdef H5
+    const file = base64ToFile(base64Data)
+    if (file) {
+      resolve('') // H5 使用 File 对象
+    } else {
+      reject(new Error('转换失败'))
+    }
+    // #endif
+
+    // #ifndef H5
+    const fs = uni.getFileSystemManager()
+    const filePath = `${uni.env.USER_DATA_PATH}/signature_${Date.now()}.png`
+    
+    // 移除 data:image/png;base64, 前缀
+    const base64 = base64Data.replace(/^data:image\/\w+;base64,/, '')
+    
+    fs.writeFile({
+      filePath,
+      data: base64,
+      encoding: 'base64',
+      success: () => {
+        resolve(filePath)
+      },
+      fail: (err) => {
+        reject(err)
+      }
+    })
+    // #endif
+  })
+}
+
+const uploadSignature = async (base64Data: string): Promise<string> => {
+  try {
+    // #ifdef H5
+    const file = base64ToFile(base64Data)
+    if (!file) {
+      throw new Error('签名图片转换失败')
+    }
+    const fd = new FormData()
+    fd.append('file', file)
+    const resp: any = await uploadSignImage(fd)
+    if (resp?.code === 0 && resp?.data) {
+      return resp.data
+    }
+    throw new Error(resp?.msg || '上传失败')
+    // #endif
+
+    // #ifndef H5
+    const filePath = await base64ToTempFilePath(base64Data)
+    const resp: any = await uploadSignImage(filePath)
+    const data = JSON.parse(resp.data)
+    console.log('resp......', data)
+    if (data?.code === 0 && data?.data) {
+      return data.data
+    }
+    throw new Error(resp?.msg || '上传失败')
+    // #endif
+  } catch (error) {
+    console.error('签名图片上传失败:', error)
+    throw error
+  }
+}
+
 // 确认签名
 const confirmSign = async () => {
   if (!signatureRef.value) return
@@ -500,6 +590,8 @@ const confirmSign = async () => {
   }
 
   try {
+    uni.showLoading({ title: '正在保存...' })
+    
     const path = await signatureRef.value.getImage()
     signImg.value = path
     showSignImg.value = path
@@ -516,11 +608,23 @@ const confirmSign = async () => {
       },
     })
 
+    // 上传签名图片
+    try {
+      const uploadedUrl = await uploadSignature(path)
+      uploadedSignUrl.value = uploadedUrl
+      console.log('签名图片上传成功:', uploadedUrl)
+    } catch (uploadError) {
+      console.error('签名图片上传失败,继续使用原图片:', uploadError)
+      uni.showToast({ title: '签名保存成功,上传失败', icon: 'none' })
+    }
+
     const now = new Date()
     signTime.value = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`
 
     signStatus.value = 'signed'
+    uni.hideLoading()
   } catch (err) {
+    uni.hideLoading()
     console.error('转换失败:', err)
     uni.showToast({ title: '签名失败', icon: 'error' })
   }
@@ -530,6 +634,7 @@ const confirmSign = async () => {
 const handleDelSign = () => {
   signImg.value = ''
   showSignImg.value = ''
+  uploadedSignUrl.value = ''
   signTime.value = ''
   signStatus.value = 'empty'
 }
@@ -561,7 +666,7 @@ const submitConfirm = async () => {
   try {
     const params: any = {
       id: orderId.value,
-      signUrl: signImg.value,
+      signUrl: uploadedSignUrl.value || signImg.value,
       businessType: routeType.value ? businessTypeMap[routeType.value] : '',
       orderItemId: orderItemId.value || undefined,
     }

+ 98 - 64
src/pages/taskOnline/TaskOnlineEquipmentList.vue

@@ -138,46 +138,69 @@
       </view>
     </view>
 
-    <view v-if="showMoreOperate" class="popup-mask" @click="showMoreOperate = false">
-      <view class="popup-content more-popup" @click.stop>
-        <view class="more-grid">
-          <view
-            class="more-item"
-            :class="{ disabled: !canInform }"
-            @click="canInform && showAddSpacePopup()"
-          >
-            <text class="more-item-text">添加有限\n空间记录</text>
+    <view v-if="showMoreOperate" class="more-operate-overlay" @click="showMoreOperate = false">
+      <view class="more-operate-panel" :class="{ 'more-panel-show': showMoreOperate }">
+        <view
+          class="more-btn-item"
+          :class="{ disabled: !canInform }"
+          @click="canInform && showAddSpacePopup()"
+        >
+          <view class="more-btn-inner">
+            <text class="more-btn-text" :style="{ color: canInform ? 'rgb(51,51,51)' : '#ccc' }">
+              添加有限空间记录
+            </text>
           </view>
-          <view
-            class="more-item"
-            :class="{ disabled: !canInform }"
-            @click="canInform && createInform()"
-          >
-            <text class="more-item-text">重大问题\n线索告知</text>
+        </view>
+        <view
+          class="more-btn-item"
+          :class="{ disabled: !canInform }"
+          @click="canInform && createInform()"
+        >
+          <view class="more-btn-inner more-btn-border">
+            <text class="more-btn-text" :style="{ color: canInform ? 'rgb(51,51,51)' : '#ccc' }">
+              重大问题线索告知
+            </text>
           </view>
-          <view
-            class="more-item"
-            :class="{ disabled: !canSuspend }"
-            @click="canSuspend && showSuspendPopupFunc()"
-          >
-            <text class="more-item-text">客户拒检</text>
+        </view>
+        <view
+          class="more-btn-item"
+          :class="{ disabled: !canSuspend }"
+          @click="canSuspend && showSuspendPopupFunc()"
+        >
+          <view class="more-btn-inner more-btn-border">
+            <text class="more-btn-text" :style="{ color: canSuspend ? 'rgb(51,51,51)' : '#ccc' }">
+              客户拒检
+            </text>
           </view>
-          <view
-            class="more-item"
-            :class="{ disabled: !canAddInspectionplan }"
-            @click="canAddInspectionplan && showAddInspectionplanPopup()"
-          >
-            <text class="more-item-text">检验方案</text>
+        </view>
+        <view
+          class="more-btn-item"
+          :class="{ disabled: !canAddInspectionplan }"
+          @click="canAddInspectionplan && showAddInspectionplanPopup()"
+        >
+          <view class="more-btn-inner more-btn-border">
+            <text
+              class="more-btn-text"
+              :style="{ color: canAddInspectionplan ? 'rgb(51,51,51)' : '#ccc' }"
+            >
+              检验方案
+            </text>
           </view>
-          <view
-            class="more-item"
-            :class="{ disabled: !canAddInspectionplan }"
-            @click="canAddInspectionplan && handleUpdateContact()"
-          >
-            <text class="more-item-text">修改安全\n管理员</text>
+        </view>
+        <view
+          class="more-btn-item"
+          :class="{ disabled: !canAddInspectionplan }"
+          @click="canAddInspectionplan && handleUpdateContact()"
+        >
+          <view class="more-btn-inner more-btn-border">
+            <text
+              class="more-btn-text"
+              :style="{ color: canAddInspectionplan ? 'rgb(51,51,51)' : '#ccc' }"
+            >
+              修改安全管理员
+            </text>
           </view>
         </view>
-        <button class="close-more-btn" @click="showMoreOperate = false">关闭</button>
       </view>
     </view>
 
@@ -1111,51 +1134,62 @@ const goBack = () => {
   background-color: #2f8eff;
 }
 
-.more-popup {
-  width: 90%;
-  max-width: 360px;
+.more-operate-overlay {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 998;
+}
+
+.more-operate-panel {
+  position: fixed;
+  right: 0;
+  bottom: 68px;
+  left: 0;
+  z-index: 999;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background-color: #fff;
+  border-radius: 5px;
+  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
+  transition: transform 0.3s ease;
+  transform: translateY(100%);
+}
+
+.more-panel-show {
+  transform: translateY(0);
 }
 
-.more-grid {
+.more-btn-item {
   display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  justify-content: space-between;
+  align-items: center;
+  justify-content: center;
+  padding: 0 15px 10px;
+  background-color: #fff;
 }
 
-.more-item {
+.more-btn-inner {
   display: flex;
   align-items: center;
   justify-content: center;
-  width: 30%;
-  height: 60px;
-  margin-bottom: 10px;
-  background-color: #f5f5f5;
-  border-radius: 6px;
+  width: 100%;
+  padding-top: 10px;
 }
 
-.more-item.disabled {
-  opacity: 0.4;
+.more-btn-border {
+  border-top: 1px solid #f4f5f6;
 }
 
-.more-item-text {
-  font-size: 13px;
-  color: #333;
-  text-align: center;
+.more-btn-item.disabled {
+  opacity: 1;
 }
 
-.close-more-btn {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  height: 36px;
-  margin-top: 10px;
+.more-btn-text {
   font-size: 14px;
-  color: #666;
-  background-color: #f5f5f5;
-  border: none;
-  border-radius: 4px;
+  color: #333;
 }
 
 .suspend-popup .popup-title,

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

@@ -59,11 +59,9 @@
 <script lang="ts" setup>
 import { ref, reactive, computed, onMounted } from 'vue'
 import { useUserStore } from '@/store/user'
-import { confirmTaskClaim, cancelClaim } from '@/api/task'
 import QueryView from './components/query/QueryView.vue'
 import TaskItem from './components/TaskItem.vue'
 import { useConfigStore } from '@/store/config'
-import { EquipmentType } from '@/utils/dictMap'
 import { TaskOrderFuncName, requestFunc } from '@/api/ApiRouter/taskOrder'
 
 defineOptions({

+ 2 - 1
src/utils/http.ts

@@ -138,9 +138,10 @@ export const httpPUT = <T>(url: string, data?: Record<string, any>) => {
  * @param query 请求 query 参数
  * @returns
  */
-export const httpDelete = <T>(url: string, data?: Record<string, any>, header?: any) => {
+export const httpDelete = <T>(url: string, query?: Record<string, any>, data?: Record<string, any>, header?: any) => {
   return http<T>({
     url,
+    query,
     data,
     method: 'DELETE',
   })