Просмотр исходного кода

安全检查记录列表、已签名安全检查记录详情渲染

yangguanjin 1 месяц назад
Родитель
Сommit
68b3781e9b

+ 14 - 1
src/pages.json

@@ -109,7 +109,11 @@
       "layout": "default",
       "style": {
         "navigationBarTitleText": "安全检查记录",
-        "navigationStyle": "custom"
+        "navigationStyle": "custom",
+        "disableScroll": true,
+        "app-plus": {
+          "bounce": "none"
+        }
       }
     },
     {
@@ -365,6 +369,15 @@
       "path": "pages/pendingVerification/preViewReport/HeadView",
       "type": "page"
     },
+    {
+      "path": "pages/securityCheck/detail/index",
+      "type": "page",
+      "layout": "default",
+      "style": {
+        "navigationBarTitleText": "安全检查记录详情",
+        "navigationStyle": "custom"
+      }
+    },
     {
       "path": "pages/workInstructionAudit/list/WorkInstructionAuditList",
       "type": "page",

+ 80 - 0
src/pages/securityCheck/components/HeadView.vue

@@ -0,0 +1,80 @@
+<template>
+  <view class="head-view" :style="{ width: headWidth + 'px' }">
+    <view class="head-left">
+      <text class="head-title">{{ title }}</text>
+    </view>
+    <view class="head-right">
+      <view
+        v-for="(btn, index) in btnArray"
+        :key="index"
+        class="operation-btn"
+        :style="{ backgroundColor: btn.color }"
+        @click="btn.click"
+      >
+        <text class="btn-text">{{ btn.value }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted } from 'vue'
+
+interface Props {
+  title?: string
+  btnArray?: { value: string; color: string; click: () => void }[]
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  title: '',
+  btnArray: () => [],
+})
+
+const headWidth = computed(() => {
+  return (typeof uni !== 'undefined' ? uni.getSystemInfoSync().windowWidth : 375) - 44
+})
+</script>
+
+<style lang="scss" scoped>
+.head-view {
+  margin-left: 44px;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.head-left {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.head-title {
+  color: rgb(51, 51, 51);
+  font-size: 18px;
+}
+
+.head-right {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-right: 15px;
+}
+
+.operation-btn {
+  margin-left: 3px;
+  height: 35px;
+  min-width: 50px;
+  justify-content: center;
+  align-items: center;
+  padding: 0 5px;
+  border-radius: 3px;
+}
+
+.btn-text {
+  font-size: 11px;
+  color: rgb(251, 253, 255);
+}
+</style>

+ 179 - 0
src/pages/securityCheck/components/Item.vue

@@ -0,0 +1,179 @@
+<template>
+  <view class="item-box">
+    <view class="item-top">
+      <view class="item-top-left">
+        <view class="row">
+          <text class="item-top-text">名称:{{ item.name }}</text>
+        </view>
+      </view>
+      <view class="view-detail" @click="handleViewDetail">
+        <text class="view-detail-text">查看详情</text>
+      </view>
+    </view>
+
+    <view class="item-center">
+      <view class="info-row">
+        <text class="title">日期:</text>
+        <text class="text">{{ date }}</text>
+      </view>
+      <view class="info-row">
+        <text class="title">有效期至:</text>
+        <text class="text">{{ validityDate }}</text>
+      </view>
+      <view class="info-row">
+        <text class="title">是否签名:</text>
+        <text class="text">{{ isSign }}</text>
+      </view>
+      <view class="info-row">
+        <text class="title">检查结论:</text>
+        <text class="text">{{ item.conclusion || '-' }}</text>
+      </view>
+    </view>
+
+    <view class="task-btn">
+      <view class="row">
+        <button class="task-btn-box delete-btn" @click="handleDelete">
+          <text class="task-btn-text">删除</text>
+        </button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import dayjs from 'dayjs'
+
+interface Props {
+  item: any
+  operateText?: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  operateText: '审核',
+})
+
+const emit = defineEmits<{
+  viewDetail: []
+  delete: []
+}>()
+
+const date = computed(() => {
+  return props.item.date ? dayjs(props.item.date).format('YYYY-MM-DD') : '-'
+})
+
+const validityDate = computed(() => {
+  return props.item.validityDate ? dayjs(props.item.validityDate).format('YYYY-MM-DD') : '-'
+})
+
+const isSign = computed(() => {
+  return props.item.signFilePdf ? '是' : '否'
+})
+
+const handleViewDetail = () => {
+  emit('viewDetail')
+}
+
+const handleDelete = () => {
+  emit('delete')
+}
+</script>
+
+<style lang="scss" scoped>
+.item-box {
+  margin: 0 13px;
+  padding: 13px;
+  background-color: #fff;
+  border-radius: 5px;
+}
+
+.row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.item-top {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  border-bottom: 1px solid rgb(244, 244, 244);
+  padding-bottom: 10px;
+  margin-bottom: 10px;
+}
+
+.item-top-left {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.item-top-text {
+  font-size: 17px;
+  color: rgb(51, 51, 51);
+  font-weight: bold;
+}
+
+.view-detail {
+  margin-left: 10px;
+}
+
+.view-detail-text {
+  font-size: 15px;
+  color: #ff9925;
+}
+
+.item-center {
+  padding: 8px 12px;
+  background-color: rgb(244, 244, 244);
+  border-radius: 5px;
+}
+
+.info-row {
+  display: flex;
+  flex-direction: row;
+  line-height: 25px;
+}
+
+.title {
+  color: rgb(108, 108, 108);
+  font-size: 12px;
+  margin-right: 5px;
+}
+
+.text {
+  color: rgb(51, 51, 51);
+  font-size: 12px;
+}
+
+.task-btn {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-end;
+  margin-top: 10px;
+}
+
+.task-btn-box {
+  min-width: 60px;
+  height: 29px;
+  border-radius: 3px;
+  padding: 0 5px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-left: 5px;
+  border: none;
+}
+
+.delete-btn {
+  background-color: rgb(255, 79, 79);
+}
+
+.task-btn-text {
+  color: #fff;
+  font-size: 12px;
+}
+</style>

+ 84 - 0
src/pages/securityCheck/components/OperationBtn.vue

@@ -0,0 +1,84 @@
+<template>
+  <button
+    class="operation-btn"
+    :class="{ disabled: isDisabled || disabled }"
+    :style="buttonStyle"
+    @click="handleClick"
+    :disabled="disabled || isDisabled"
+  >
+    <text v-if="isDisabled" class="btn-text loading">...</text>
+    <text v-else class="btn-text">{{ title }}</text>
+  </button>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+
+interface Props {
+  title: string
+  style?: Record<string, any>
+  click: () => void
+  disabled?: boolean
+  debounceTime?: number
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  style: () => ({}),
+  disabled: false,
+  debounceTime: 500,
+})
+
+const isDisabled = ref(false)
+
+const buttonStyle = computed(() => {
+  const baseStyle: any = {
+    marginLeft: '3px',
+    height: '35px',
+    minWidth: '50px',
+    justifyContent: 'center',
+    alignItems: 'center',
+    padding: '0 5px',
+    borderRadius: '3px',
+  }
+  return {
+    ...baseStyle,
+    ...props.style,
+    opacity: props.disabled || isDisabled.value ? 0.6 : 1,
+  }
+})
+
+const handleClick = () => {
+  if (props.disabled || isDisabled.value) {
+    return
+  }
+
+  isDisabled.value = true
+  props.click()
+
+  setTimeout(() => {
+    isDisabled.value = false
+  }, props.debounceTime)
+}
+</script>
+
+<style lang="scss" scoped>
+.operation-btn {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border: none;
+}
+
+.btn-text {
+  font-size: 11px;
+  color: rgb(251, 253, 255);
+}
+
+.loading {
+  color: #fff;
+}
+
+.disabled {
+  opacity: 0.6;
+}
+</style>

+ 360 - 0
src/pages/securityCheck/detail/index.vue

@@ -0,0 +1,360 @@
+<route lang="json5" type="page">
+{
+  layout: 'default',
+  style: {
+    navigationBarTitleText: '安全检查记录详情',
+    navigationStyle: 'custom',
+  },
+}
+</route>
+
+<template>
+  <view class="detail-container">
+    <!-- 导航栏 -->
+    <view class="navigate-view">
+      <view class="navigate-left" @click="goBack">
+        <image class="back-icon" src="/static/images/back.png" />
+      </view>
+      <view class="navigate-center">
+        <HeadView v-if="renderCenter" title="操作指导书批准详情" :btn-array="btnArray" />
+        <text v-else class="navigate-title">安全检查记录详情</text>
+      </view>
+      <view class="navigate-right"></view>
+    </view>
+
+    <!-- PDF 预览区域 -->
+    <view class="pdf-container">
+      <view v-if="loading" class="loading-container">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <!-- #ifdef H5 -->
+      <view v-else-if="pdfUrl" id="pdf-viewer-container" class="pdf-viewer-container"></view>
+      <!-- #endif -->
+      <!-- #ifndef H5 -->
+      <web-view v-else-if="pdfUrl" :src="pdfUrl" />
+      <!-- #endif -->
+      <view v-else class="empty-container">
+        <text class="empty-text">暂无PDF</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, onMounted, watch, nextTick } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { getSafetyCheckRecordTemplate, getServiceOrderPDF } from '@/api/task'
+import { useUserStore } from '@/store/user'
+import { isH5 } from '@/utils/platform'
+import dayjs from 'dayjs'
+import HeadView from '../components/HeadView.vue'
+
+// 路由参数
+const detailItem = ref<any>({})
+const orderId = ref('')
+const unitContact = ref('')
+const unitPhone = ref('')
+const receiverEmail = ref('')
+const templateId = ref('')
+
+onLoad((options) => {
+  orderId.value = options.orderId || ''
+  unitContact.value = options.unitContact || ''
+  unitPhone.value = options.unitPhone || ''
+  receiverEmail.value = options.receiverEmail || ''
+  templateId.value = options.templateId || ''
+
+  if (options.detailItem) {
+    try {
+      detailItem.value = JSON.parse(decodeURIComponent(options.detailItem))
+    } catch (e) {
+      console.error('解析 detailItem 失败:', e)
+    }
+  }
+})
+
+// 用户信息
+const userStore = useUserStore()
+
+// 状态
+const loading = ref(false)
+const pdfUrl = ref('')
+const templateParams = ref<any>({})
+
+// 是否显示操作按钮 (对应 PJ 中的 useMemo 计算)
+const renderCenter = computed(() => {
+  const { id } = detailItem.value || {}
+  if (!id) return false
+  return true
+})
+
+// 操作按钮数组 (与 PJ 版本一致,空数组)
+const btnArray = computed(() => {
+  const { signFilePdf, id } = detailItem.value || {}
+  if (!id) return []
+
+  return [
+    // { value: signFilePdf ? '重新签名' : '签名', color: 'rgb(47,142,255)', click: handleToSign },
+  ]
+})
+
+// Blob 转 Base64
+const blobToBase64 = (blob: Blob): Promise<string> => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader()
+    reader.onloadend = () => {
+      const base64data = reader.result as string
+      resolve(base64data.split(',')[1])
+    }
+    reader.onerror = reject
+    reader.readAsDataURL(blob)
+  })
+}
+
+// 获取模板
+const handleGetTemplate = async () => {
+  const { nickname } = userStore.userInfo || {}
+
+  const response = await getSafetyCheckRecordTemplate({ orderId: orderId.value })
+  const result = response?.data || {}
+  const dataStr = result.dataSource ? JSON.parse(result.dataSource) : {}
+
+  const temPar = {
+    ...result,
+    templateUrl: result.recordTemplateUrl,
+    dataSource: JSON.stringify({
+      ...dataStr,
+      signName: nickname,
+      signDate: dayjs().format('YYYY年MM月DD日'),
+    }),
+  }
+
+  templateParams.value = temPar
+  return temPar
+}
+
+// 获取安全检查记录 PDF
+const getAppServiceOrderPDF = async (tplParams: any) => {
+  const { signFilePdf, id } = detailItem.value || {}
+  const { dataSource, templateUrl } = tplParams || {}
+
+  if (signFilePdf) {
+    pdfUrl.value = buildFileUrl(signFilePdf)
+    return
+  }
+
+  const params = {
+    dataStr: dataSource,
+    templateUrl,
+    orderId: orderId.value,
+    securityCheckId: id,
+  }
+
+  const response: any = await getServiceOrderPDF(params)
+  const base64Data = await blobToBase64(response)
+  const filePath = `data:application/pdf;base64,${base64Data}`
+  if (filePath) {
+    pdfUrl.value = filePath
+  }
+}
+
+// 构建文件 URL
+const buildFileUrl = (fileId: string) => {
+  // const baseUrl = uni.getStorageSync('BASE_URL') || 'http://192.168.0.113:40080'
+  const baseUrl = 'https://youdao.hofo.co/dexdev/'
+  return `${baseUrl}${fileId}`
+}
+
+// 签名 (保留但不使用,与 PJ 版本一致)
+const handleToSign = async () => {
+  const { signFilePdf, id } = detailItem.value || {}
+  if (!id) return
+
+  const url = signFilePdf ? '/pages/sign-detail/index' : '/pages/sign/index'
+  uni.redirectTo({
+    url: `${url}?orderId=${orderId.value}&type=AQJC&securityCheckId=${id}&unitContact=${unitContact.value}&unitPhone=${unitPhone.value}&receiverEmail=${receiverEmail.value}&templateId=${templateId.value}`,
+  })
+}
+
+// 初始化
+const init = async () => {
+  loading.value = true
+  try {
+    const tplParams = await handleGetTemplate()
+    await getAppServiceOrderPDF(tplParams)
+  } catch (error) {
+    console.error('[Page] 初始化失败:', error)
+    uni.showToast({ title: 'PDF 加载失败', icon: 'none' })
+  } finally {
+    loading.value = false
+  }
+}
+
+// 返回
+const goBack = () => {
+  uni.navigateBack()
+}
+
+// H5 端 PDF 渲染
+const initH5PdfViewer = async () => {
+  if (!pdfUrl.value) return
+
+  const container = document.getElementById('pdf-viewer-container')
+  if (!container) return
+
+  container.innerHTML = ''
+
+  const isBase64 = pdfUrl.value.startsWith('data:application/pdf;base64,')
+
+  if (!(window as any).pdfjsLib) {
+    const script = document.createElement('script')
+    script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js'
+    script.onerror = () => uni.showToast({ title: 'PDF.js 加载失败', icon: 'none' })
+    document.head.appendChild(script)
+    await new Promise<void>((resolve, reject) => {
+      script.onload = () => resolve()
+      script.onerror = reject
+    })
+  }
+
+  try {
+    const pdfjsLib = (window as any).pdfjsLib
+    pdfjsLib.GlobalWorkerOptions.workerSrc =
+      'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'
+
+    let loadingTask
+    if (isBase64) {
+      const base64Data = pdfUrl.value.replace(/^data:application\/pdf;base64,/, '')
+      const binaryString = atob(base64Data)
+      const bytes = new Uint8Array(binaryString.length)
+      for (let i = 0; i < binaryString.length; i++) {
+        bytes[i] = binaryString.charCodeAt(i)
+      }
+      loadingTask = pdfjsLib.getDocument({ data: bytes })
+    } else {
+      loadingTask = pdfjsLib.getDocument(pdfUrl.value)
+    }
+
+    const pdf = await loadingTask.promise
+    container.style.overflow = 'auto'
+
+    for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
+      const page = await pdf.getPage(pageNum)
+      const scale = 1.5
+      const viewport = page.getViewport({ scale })
+
+      const canvas = document.createElement('canvas')
+      canvas.id = `pdf-page-${pageNum}`
+      canvas.style.display = 'block'
+      canvas.style.marginBottom = '10px'
+
+      const context = canvas.getContext('2d')
+      canvas.height = viewport.height
+      canvas.width = viewport.width
+
+      container.appendChild(canvas)
+
+      await page.render({ canvasContext: context, viewport }).promise
+    }
+  } catch (error) {
+    console.error('[PDF] Error rendering PDF:', error)
+    uni.showToast({ title: 'PDF 渲染失败', icon: 'none' })
+  }
+}
+
+onMounted(async () => {
+  await init()
+
+  if (isH5) {
+    await nextTick()
+    if (pdfUrl.value) {
+      setTimeout(() => initH5PdfViewer(), 100)
+    } else {
+      watch(pdfUrl, (val) => {
+        if (val) {
+          setTimeout(() => initH5PdfViewer(), 100)
+        }
+      })
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.detail-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #f5f5f5;
+}
+
+.navigate-view {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  height: 44px;
+  background-color: #fff;
+  border-bottom: 1px solid #eee;
+  padding: 0 15px;
+}
+
+.navigate-left {
+  display: flex;
+  align-items: center;
+  width: 44px;
+}
+
+.back-icon {
+  width: 20px;
+  height: 20px;
+}
+
+.navigate-center {
+  flex: 1;
+  display: flex;
+  justify-content: center;
+}
+
+.navigate-title {
+  font-size: 17px;
+  font-weight: 500;
+  color: #333;
+}
+
+.navigate-right {
+  width: 44px;
+  display: flex;
+  align-items: center;
+}
+
+.pdf-container {
+  flex: 1;
+  background-color: #f5f5f5;
+}
+
+/* #ifdef H5 */
+.pdf-viewer-container {
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  text-align: center;
+  padding: 10px;
+  box-sizing: border-box;
+}
+/* #endif */
+
+.loading-container,
+.empty-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100%;
+}
+
+.loading-text,
+.empty-text {
+  color: #999;
+  font-size: 14px;
+}
+</style>

+ 174 - 154
src/pages/securityCheck/securityCheckList.vue

@@ -4,181 +4,198 @@
   style: {
     navigationBarTitleText: '安全检查记录',
     navigationStyle: 'custom',
+    disableScroll: true,
+    'app-plus': {
+      bounce: 'none',
+    },
   },
 }
 </route>
 
 <template>
-  <view class="preview-container">
+  <view class="security-check-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">安全检查记录</text>
-      <view class="navigate-right"></view>
     </view>
 
-    <!-- PDF 预览区域 -->
-    <view class="pdf-container">
-      <view v-if="loading" class="loading-container">
-        <text class="loading-text">加载中...</text>
+    <!-- 列表 -->
+    <scroll-view class="list-scroll" scroll-y @scrolltolower="loadMore">
+      <view v-for="item in listData" :key="item.id" class="item-box">
+        <Item
+          :item="item"
+          operate-text="审核"
+          @view-detail="handleModifySavetyRecord(item)"
+          @delete="handleDeleteSavetyRecord(item)"
+        />
+      </view>
+
+      <view v-if="loading" class="loading-text">加载中...</view>
+      <view v-if="!hasMore && listData.length > 0" class="no-more-text">没有更多了</view>
+      <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>
-      <web-view v-else-if="pdfUrl" :src="pdfUrl" @error="onPdfError" />
     </view>
   </view>
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted } from 'vue'
-import { onLoad } from '@dcloudio/uni-app'
-import { getSafetyCheckRecordTemplate, getServiceOrderPDF } from '@/api/task'
-import dayjs from 'dayjs'
-
-const loading = ref(false)
-const pdfUrl = ref('')
-
-interface DetailItem {
-  id: string
-  signFilePdf?: string
-  [key: string]: any
-}
+import { ref, reactive, onMounted, 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'
+
+defineOptions({
+  name: 'securityCheckList',
+})
 
-let detailItem: DetailItem = {} as DetailItem
-let orderId = ''
-let unitContact = ''
-let unitPhone = ''
-let receiverEmail = ''
-let templateId = ''
+// 路由参数
+const orderId = ref('')
+const unitContact = ref('')
+const unitPhone = ref('')
+const receiverEmail = ref('')
 
 onLoad((options) => {
-  orderId = options.orderId || ''
-  unitContact = options.unitContact || ''
-  unitPhone = options.unitPhone || ''
-  receiverEmail = options.receiverEmail || ''
-  templateId = options.templateId || ''
-  
-  if (options.detailItem) {
-    try {
-      detailItem = JSON.parse(decodeURIComponent(options.detailItem))
-    } catch (e) {
-      console.error('解析 detailItem 失败:', e)
-    }
-  }
+  orderId.value = options.orderId || ''
+  unitContact.value = options.unitContact || ''
+  unitPhone.value = options.unitPhone || ''
+  receiverEmail.value = options.receiverEmail || ''
 })
 
-// Blob 转 Base64
-const blobToBase64 = async (blob: Blob): Promise<string> => {
-  return new Promise((resolve, reject) => {
-    const reader = new FileReader()
-    reader.onloadend = () => {
-      const base64data = (reader.result as string).split(',')[1]
-      resolve(base64data)
-    }
-    reader.onerror = reject
-    reader.readAsDataURL(blob)
-  })
-}
+// 列表数据
+const listData = ref<any[]>([])
+const loading = ref(false)
+const hasMore = ref(true)
+const params = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  orderId: '',
+})
 
-// 获取安全检查记录模板
-const handleGetTemplate = async () => {
+// 删除弹窗
+const showDeletePopup = ref(false)
+const deleteItem = ref<any>(null)
+
+// 获取列表数据
+const fetchList = async (refresh = false) => {
+  if (loading.value) return
+
+  params.orderId = orderId.value
+  params.pageNo = refresh ? 1 : params.pageNo + 1
+
+  loading.value = true
+  uni.showLoading({ title: '加载中...', mask: true })
+  
   try {
-    const userInfo = uni.getStorageSync('USER_INFO')
-    const nickname = userInfo?.nickname || ''
-    
-    const response: any = await getSafetyCheckRecordTemplate({ orderId })
-    const result = response?.data || {}
-    const dataStr = result.dataSource ? JSON.parse(result.dataSource) : {}
+    const result = await getSafetyCheckRecordPage(params)
+    uni.hideLoading()
     
-    const temPar = {
-      ...result,
-      templateUrl: result.recordTemplateUrl,
-      dataSource: JSON.stringify({
-        ...dataStr,
-        signName: nickname,
-        signDate: dayjs().format('YYYY 年 MM 月 DD 日')
-      })
+    if (result?.code === 0 && result?.data?.list?.length) {
+      const newList = result.data.list
+      if (refresh) {
+        listData.value = newList
+      } else {
+        listData.value = [...listData.value, ...newList]
+      }
+      hasMore.value = newList.length >= params.pageSize
+    } else {
+      if (refresh) {
+        listData.value = []
+      }
+      hasMore.value = false
     }
-    
-    return temPar
-  } catch (error) {
-    console.error('获取模板失败:', error)
-    return null
+  } catch (error: any) {
+    uni.hideLoading()
+    console.error('获取安全检查记录失败:', error)
+    uni.showToast({ title: error?.msg || '获取安全检查记录失败', icon: 'none', duration: 3000 })
+  } finally {
+    loading.value = false
   }
 }
 
-// 获取安全检查记录 PDF
-const getAppServiceOrderPDF = async (templateParams: any) => {
-  const { signFilePdf } = detailItem || {}
-  
-  if (signFilePdf) {
-    // 如果有已签署的 PDF 文件
-    pdfUrl.value = buildFileUrl(signFilePdf)
-    return
-  }
-  
-  const { dataSource, templateUrl } = templateParams || {}
-  const params = {
-    dataStr: dataSource,
-    templateUrl,
-    orderId: orderId,
-    securityCheckId: detailItem?.id
-  }
-  
-  try {
-    // TODO: 调用 PDF 生成 API
-    // const response = await getServiceOrderPDF(params)
-    // const base64Data = await blobToBase64(response)
-    // pdfUrl.value = `data:application/pdf;base64,${base64Data}`
-    
-    // 临时使用空 URL
-    pdfUrl.value = ''
-  } catch (error) {
-    console.error('获取 PDF 失败:', error)
-    uni.showToast({ title: 'PDF 加载失败', icon: 'error' })
+// 加载更多
+const loadMore = () => {
+  if (!loading.value && hasMore.value && listData.value.length > 0) {
+    fetchList(false)
   }
 }
 
-// 构建文件 URL
-const buildFileUrl = (fileId: string) => {
-  const baseUrl = uni.getStorageSync('BASE_URL') || 'http://192.168.0.113:40080'
-  return `${baseUrl}/dexdev${fileId}`
+// 刷新列表
+const refreshList = () => {
+  params.pageNo = 1
+  fetchList(true)
 }
 
-// 初始化
-const init = async () => {
-  loading.value = true
+// 查看/编辑详情
+const handleModifySavetyRecord = (item: any) => {
+  const detail = encodeURIComponent(JSON.stringify(item))
+  uni.navigateTo({
+    url: `/pages/securityCheck/detail/index?orderId=${orderId.value}&detailItem=${detail}&unitContact=${unitContact.value}&unitPhone=${unitPhone.value}&receiverEmail=${receiverEmail.value}`,
+  })
+}
+
+// 删除记录
+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 {
-    const templateParams = await handleGetTemplate()
-    if (templateParams) {
-      await getAppServiceOrderPDF(templateParams)
+    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) {
-    console.error('初始化失败:', error)
-    uni.showToast({ title: '加载失败', icon: 'error' })
+  } catch (error: any) {
+    uni.hideLoading()
+    console.error('删除安全检查记录失败:', error)
+    uni.showToast({ title: error?.msg || '删除失败', icon: 'none', duration: 3000 })
   } finally {
-    loading.value = false
+    closeDeletePopup()
   }
 }
 
-// 返回
-const goBack = () => {
-  uni.navigateBack()
-}
-
-// PDF 加载错误
-const onPdfError = (event: any) => {
-  console.error('PDF 加载失败:', event)
-  uni.showToast({ title: 'PDF 加载失败', icon: 'error' })
-}
+// 页面显示时刷新
+onShow(() => {
+  refreshList()
+})
 
-onMounted(() => {
-  init()
+// 监听页面卸载
+onUnmounted(() => {
+  uni.$emit('onRefresh')
 })
+
+defineExpose({})
 </script>
 
 <style lang="scss" scoped>
-.preview-container {
+.security-check-container {
   display: flex;
   flex-direction: column;
   height: 100vh;
@@ -187,23 +204,11 @@ onMounted(() => {
 
 .navigate-view {
   display: flex;
-  flex-direction: row;
-  justify-content: space-between;
+  justify-content: center;
   align-items: center;
   height: 44px;
   background-color: #fff;
   border-bottom: 1px solid #eee;
-  padding: 0 15px;
-}
-
-.navigate-left {
-  display: flex;
-  align-items: center;
-}
-
-.back-icon {
-  width: 20px;
-  height: 20px;
 }
 
 .navigate-title {
@@ -212,24 +217,39 @@ onMounted(() => {
   color: #333;
 }
 
-.navigate-right {
-  width: 20px;
+.list-scroll {
+  flex: 1;
+  overflow: hidden;
+}
+
+.item-box {
+  margin-top: 13px;
 }
 
-.pdf-container {
-  flex: 1;
-  background-color: #f5f5f5;
+.loading-text,
+.no-more-text,
+.empty-text {
+  text-align: center;
+  padding: 15px;
+  color: #999;
+  font-size: 14px;
 }
 
-.loading-container {
+.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;
-  height: 100%;
+  z-index: 999;
 }
 
-.loading-text {
-  color: #999;
-  font-size: 14px;
+.popup-content {
+  width: 80%;
+  max-width: 320px;
 }
 </style>

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

@@ -37,6 +37,7 @@ interface NavigateToOptions {
        "/pages/pendingVerification/list/Item" |
        "/pages/pendingVerification/list/PendingVerificationList" |
        "/pages/pendingVerification/preViewReport/HeadView" |
+       "/pages/securityCheck/detail/index" |
        "/pages/workInstructionAudit/list/WorkInstructionAuditList" |
        "/pages/workInstructionAudit/preViewReport/index" |
        "/pages-home/home/home" |

+ 43 - 37
src/utils/http.ts

@@ -1,20 +1,24 @@
 import { CustomRequestOptions } from '@/interceptors/request'
 import signMd5Utils from '@/utils/signMd5Utils'
 import { isH5 } from '@/utils/platform'
-import {getEnvBaseUrl} from "@/utils/index";
-import {buildURL,buildFullPath} from "@/utils/helpers/buildURL";
+import { getEnvBaseUrl } from '@/utils/index'
+import { buildURL, buildFullPath } from '@/utils/helpers/buildURL'
 
 export const http = <T>(options: CustomRequestOptions) => {
   // 1. 返回 Promise 对象
   return new Promise<IResData<T>>((resolve, reject) => {
-    //update-begin-author:liusq date:20240422 for: post 请求接口加签参数设置
+    // update-begin-author:liusq date:20240422 for: post 请求接口加签参数设置
     let params = options.query
     if (options.data && Object.keys(options.data).length > 0) {
       params = Object.assign({}, options?.query, options?.data)
     }
-    let sign = signMd5Utils.getSign(options.url, params)
-    let vSign = signMd5Utils.getVSign(options.data, sign)
-    if ( JSON.parse(import.meta.env.VITE_APP_PROXY) && isH5 && import.meta.env.MODE === 'development') {
+    const sign = signMd5Utils.getSign(options.url, params)
+    const vSign = signMd5Utils.getVSign(options.data, sign)
+    if (
+      JSON.parse(import.meta.env.VITE_APP_PROXY) &&
+      isH5 &&
+      import.meta.env.MODE === 'development'
+    ) {
       // 开启了代理须加前缀。仅支持 H5 端
       options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url
     }
@@ -23,9 +27,11 @@ export const http = <T>(options: CustomRequestOptions) => {
     const token = uni.getStorageSync('ACCESS_TOKEN')
 
     uni.request({
+      // #ifdef MP-WEIXIN
       dataType: 'json',
+      // #endif
       // #ifndef MP-WEIXIN
-      responseType: 'json',
+      dataType: options.responseType === 'blob' ? 'text' : 'json',
       // #endif
       header: {
         'X-Access-Token': token || '',
@@ -118,14 +124,12 @@ export const httpPost = <T>(
  * @param query 请求 query 参数,post 请求也支持 query,很多微信接口都需要
  * @returns
  */
-export const httpPUT = <T>(
-  url: string,
-  data?: Record<string, any>,
-) => {
+export const httpPUT = <T>(url: string, data?: Record<string, any>) => {
   return http<T>({
     url,
     data,
     method: 'PUT',
+    responseType: 'blob',
   })
 }
 /**
@@ -155,25 +159,28 @@ const requestComFail = (response) => {
  * @param query 请求 query 参数
  * @returns
  */
-export const httpUpload = <T>(url: string, {
-  // #ifdef APP-PLUS || H5
-  files,
-  // #endif
-  filePath,
-  name,
-  // #ifdef H5
-  file,
-  // #endif
-  header = {},
-  formData = {},
-  custom = {},
-  params = {},
-  getTask
-}: any) => {
+export const httpUpload = <T>(
+  url: string,
+  {
+    // #ifdef APP-PLUS || H5
+    files,
+    // #endif
+    filePath,
+    name,
+    // #ifdef H5
+    file,
+    // #endif
+    header = {},
+    formData = {},
+    custom = {},
+    params = {},
+    getTask,
+  }: any,
+) => {
   return new Promise((resolve, reject) => {
-    let next = true;
+    const next = true
     // 从本地存储获取 token
-    const token = uni.getStorageSync('ACCESS_TOKEN');
+    const token = uni.getStorageSync('ACCESS_TOKEN')
     const globalHeader = {
       'X-Access-Token': token || '',
       'X-Tenant-Id': 1,
@@ -185,11 +192,11 @@ export const httpUpload = <T>(url: string, {
       filePath,
       method: 'UPLOAD',
       name,
-      header: {...globalHeader, ...header},
+      header: { ...globalHeader, ...header },
       formData,
       params,
-      custom: { ...custom},
-      getTask
+      custom: { ...custom },
+      getTask,
     } as any
     // #ifdef APP-PLUS || H5
     if (files) {
@@ -204,11 +211,11 @@ export const httpUpload = <T>(url: string, {
     const requestBeforeFun = (config) => {
       return config
     }
-    //校验状态码
-    const validateStatus= (statusCode) => {
+    // 校验状态码
+    const validateStatus = (statusCode) => {
       return statusCode === 200
     }
-    const handleRe = {...pubConfig}
+    const handleRe = { ...pubConfig }
     const _config = {
       url: buildURL(buildFullPath(handleRe.baseUrl, handleRe.url), handleRe.params),
       filePath: handleRe.filePath,
@@ -222,8 +229,7 @@ export const httpUpload = <T>(url: string, {
           if (typeof response.data === 'string') {
             response.data = JSON.parse(response.data)
           }
-        } catch (e) {
-        }
+        } catch (e) {}
         if (validateStatus(response.statusCode)) {
           // 成功
           response = requestComFun(response)
@@ -232,7 +238,7 @@ export const httpUpload = <T>(url: string, {
           response = requestComFail(response)
           reject(response)
         }
-      }
+      },
     } as any
 
     // #ifdef APP-PLUS || H5