Browse Source

检测录入-新增检测项目(锅炉)

yangguanjin 2 months ago
parent
commit
a9d5523f05

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

@@ -29,3 +29,12 @@ export const confirmBoilerEquipmentClaim = (data: { id: string }) => {
 export const cancelBoilerEquipmentClaim = (data: { id: string }) => {
   return httpPost('/pressure2/boiler-task-order/order-item/cancelClaim', data)
 }
+
+// 获取检验项目
+export const getInspectProjectItemPage = (data: any) => {
+  return httpPost('/pressure2/boiler-task-order/cost/itemInfoList', data)
+}
+
+export const addInspectProject = (data: any) => {
+  return httpPost('/pressure2/boiler-task-order/order-item/add-report-v3', data)
+}

+ 208 - 0
src/components/Signature/SignatureCanvas.vue

@@ -0,0 +1,208 @@
+<template>
+  <view class="signature-container">
+    <canvas
+      class="signature-canvas"
+      :canvas-id="canvasId"
+      :disable-scroll="true"
+      :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
+      @touchstart="handleTouchStart"
+      @touchmove="handleTouchMove"
+      @touchend="handleTouchEnd"
+      @touchcancel="handleTouchEnd"
+    />
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
+
+interface Props {
+  canvasId?: string
+  lineWidth?: number
+  lineColor?: string
+  backgroundColor?: string
+  width?: number
+  height?: number
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  canvasId: 'signatureCanvas',
+  lineWidth: 4,
+  lineColor: '#000000',
+  backgroundColor: '#ffffff',
+  width: 0,
+  height: 0,
+})
+
+const emit = defineEmits<{
+  signed: [hasContent: boolean]
+  ready: []
+}>()
+
+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 initCanvas = () => {
+  nextTick(() => {
+    ctx = uni.createCanvasContext(props.canvasId)
+
+    if (props.width > 0 && props.height > 0) {
+      canvasWidth.value = props.width
+      canvasHeight.value = props.height
+    } else {
+      uni.getSystemInfo({
+        success: (res) => {
+          canvasWidth.value = res.windowWidth
+          canvasHeight.value = res.windowHeight * 0.5
+          clearCanvas()
+        },
+      })
+    }
+  })
+}
+
+const clearCanvas = () => {
+  if (!ctx) return
+  ctx.setFillStyle(props.backgroundColor)
+  ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value)
+  ctx.draw()
+  isDraw.value = false
+}
+
+const handleTouchStart = (e: any) => {
+  console.log('touchstart.....', e)
+  if (!ctx) return
+
+  const touch = e.touches[0]
+  const startX = touch.x
+  const startY = touch.y
+  const startPoint = { X: startX, Y: startY }
+
+  points.value.push(startPoint)
+
+  ctx.lineWidth = props.lineWidth
+  ctx.lineCap = 'round'
+  ctx.lineJoin = 'round'
+  ctx.strokeStyle = props.lineColor
+}
+
+const handleTouchMove = (e: any) => {
+  console.log('touchmove.....', e)
+  if (!ctx) return
+
+  const touch = e.touches[0]
+  const moveX = touch.x
+  const moveY = touch.y
+  const movePoint = { X: moveX, Y: moveY }
+
+  points.value.push(movePoint)
+  drawLine()
+}
+
+const drawLine = () => {
+  if (!ctx || points.value.length < 2) return
+
+  isDraw.value = true
+
+  const start = points.value[0]
+  const end = points.value[1]
+  points.value.shift()
+
+  ctx.beginPath()
+  ctx.moveTo(start.X, start.Y)
+  ctx.lineTo(end.X, end.Y)
+  ctx.stroke()
+  ctx.draw(true)
+}
+
+const handleTouchEnd = (e: any) => {
+  console.log('touchend.....', e)
+  points.value = []
+  emit('signed', isDraw.value)
+}
+
+const getImage = (quality = 1, callback?: (path: string) => void): Promise<string> => {
+  return new Promise((resolve, reject) => {
+    if (!ctx) {
+      reject(new Error('Canvas not initialized'))
+      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)
+        },
+      })
+    })
+  })
+}
+
+const getImageBase64 = async (quality = 1): Promise<string> => {
+  const path = await getImage(quality)
+
+  return new Promise((resolve, reject) => {
+    if (uni.canIUse('getFileSystemManager')) {
+      const fs = uni.getFileSystemManager()
+      fs.readFile({
+        filePath: path,
+        encoding: 'base64',
+        success: (res) => {
+          resolve('data:image/png;base64,' + res.data)
+        },
+        fail: (err) => {
+          reject(err)
+        },
+      })
+    } else {
+      resolve(path)
+    }
+  })
+}
+
+const isEmpty = (): boolean => {
+  return !isDraw.value
+}
+
+defineExpose({
+  clear: clearCanvas,
+  getImage,
+  getImageBase64,
+  isEmpty,
+})
+
+onMounted(() => {
+  initCanvas()
+})
+
+onUnmounted(() => {
+  ctx = null
+})
+</script>
+
+<style lang="scss" scoped>
+.signature-container {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+}
+
+.signature-canvas {
+  background-color: #ffffff;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+}
+</style>

File diff suppressed because it is too large
+ 1974 - 0
src/pages/equipment/detail/components/BoilerInspectProject.vue


File diff suppressed because it is too large
+ 1654 - 0
src/pages/equipment/detail/components/ContainerInspectProject.vue


+ 1 - 0
src/pages/equipment/detail/components/InspectProject.vue

@@ -357,6 +357,7 @@ interface Props {
   reportList: CheckItem[]
   taskOrder: StringAnyObj
   equipment: StringAnyObj
+  dataSource: any
   orderId: string
   orderItemId: string
   useOnline?: string

+ 486 - 0
src/pages/equipment/detail/components/checkProjectPopup/BoilerCheckProject.vue

@@ -0,0 +1,486 @@
+<template>
+  <view class="check-project-container">
+    <view class="search-bar">
+      <view class="dropdown-wrapper">
+        <view class="dropdown-selector" @click="showDropdown = !showDropdown">
+          <text class="dropdown-text">{{ boilerCheckTypeLabelName }} bo</text>
+          <!-- <text class="dropdown-arrow">▼</text> -->
+        </view>
+        <!-- 下拉先注释掉,检验性质貌似不需要再修改了 -->
+        <!-- <view v-if="showDropdown" class="dropdown-menu">
+          <view
+            v-for="option in inspectionNatureOptions"
+            :key="option.value"
+            class="dropdown-item"
+            :class="{ active: inspectionNature === option.value }"
+            @click="selectInspectionNature(option)"
+          >
+            <text>{{ option.label }}</text>
+          </view>
+        </view> -->
+      </view>
+
+      <input
+        v-model="searchKeyword"
+        class="search-input"
+        placeholder="搜索项目名称"
+        @confirm="handleSearch"
+      />
+    </view>
+
+    <scroll-view class="project-list" :scroll-y="true">
+      <view v-if="dataSource.length === 0" class="empty-state">
+        <text class="empty-text">暂无数据</text>
+      </view>
+      <wd-collapse v-else v-model="expandedNames">
+        <wd-collapse-item
+          v-for="group in groupedData"
+          :key="group.type"
+          :title="group.title"
+          :name="group.type"
+        >
+          <view class="project-grid">
+            <view
+              v-for="(item, index) in group.items"
+              :key="item.templateId"
+              class="project-item"
+              :class="{
+                first: index === 0 || index === 1,
+                last: index === group.items.length - 1 || index === group.items.length - 2,
+                selected: isSelected(item),
+              }"
+              @click="handleSelect(item)"
+            >
+              <text class="project-name">{{ item.name }}</text>
+            </view>
+          </view>
+        </wd-collapse-item>
+      </wd-collapse>
+    </scroll-view>
+
+    <view class="bottom-actions">
+      <button class="action-btn cancel-btn" @click="handleCancel">取消</button>
+      <button class="action-btn confirm-btn" @click="handleConfirm">确认</button>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, onMounted, watch } from 'vue'
+import { getInspectProjectItemPage, addInspectProject } from '@/api/boiler/boilerTaskOrder'
+
+interface ReportTemplate {
+  orderId: string
+  name: string
+  isAutoAmount: string
+  templateId: string
+  connectId: string
+  formulaTemplateUrl: string
+  fee: number
+  use: boolean
+  reportType: number
+  recordTemplateUrl: string
+  reportTemplateUrl: string
+  isMainProject: string
+  feeCalcType: string
+  taskOrderItemId: string
+}
+
+interface Props {
+  propjectList: ReportTemplate[][]
+  selectTemplates: Record<string, any[]>
+  useOnline?: string
+  equipData: any
+}
+
+const props = defineProps<Props>()
+
+const emit = defineEmits<{
+  setIds: [item: any, type: string]
+  cleanIds: [type: string]
+  change: [selectedItems: any[]]
+  confirm: [selectedItems: any[]]
+  cancel: []
+}>()
+
+const searchKeyword = ref('')
+const dataSource = ref<ReportTemplate[]>([])
+const showDropdown = ref(false)
+const hadTemplateId = ref('')
+const expandedNames = ref<string[]>(['100'])
+const localSelectedTemplates = ref<ReportTemplate[]>([])
+
+const boilerCheckTypeMap = {
+  100: '内部检验',
+  200: '外部检验',
+  300: '耐压检验',
+}
+
+interface GroupItem {
+  type: string
+  title: string
+  items: ReportTemplate[]
+}
+
+const groupedData = computed(() => {
+  const result: GroupItem[] = []
+
+  result.push({
+    type: '100',
+    title: boilerCheckTypeLabelName.value + ' 法定收费项目',
+    items: [...dataSource.value],
+  })
+
+  result.push({
+    type: '200',
+    title: boilerCheckTypeLabelName.value + ' 服务收费项目',
+    items: [...dataSource.value],
+  })
+
+  console.log(result, dataSource.value)
+
+  return result
+})
+
+const boilerCheckTypeLabelName = computed(() => {
+  const checkTypeNum = props.equipData?.taskOrder?.checkType
+  return boilerCheckTypeMap[checkTypeNum]
+})
+
+const isSelected = (item: ReportTemplate): boolean => {
+  return localSelectedTemplates.value.some((template) => template.templateId === item.templateId)
+}
+
+const selectedItems = computed(() => {
+  return localSelectedTemplates.value
+})
+
+const handleSearch = () => {
+  loadTemplates()
+}
+
+const loadTemplates = async () => {
+  if (props.useOnline === '1') {
+    const resp = await getInspectProjectItemPage({
+      orderId: props.equipData?.taskOrder?.id,
+      equipmentCategory: '200',
+      inspectionNature: [props.equipData?.taskOrder?.checkType],
+      equipType: props.equipData?.taskOrderItem?.boilerType,
+      itemIds: [props.equipData?.taskOrderItem?.id],
+    })
+
+    const templateList = resp?.data?.filter((item: any) =>
+      ['100', '200', '300'].includes(String(item.reportType)),
+    )
+
+    checkExistingTemplates(templateList || [])
+    dataSource.value = templateList || []
+  } else {
+    uni.showToast({ title: '离线模式暂不支持添加项目', icon: 'none' })
+    dataSource.value = []
+  }
+}
+
+const checkExistingTemplates = (templates: ReportTemplate[]) => {
+  for (const list of props.propjectList) {
+    for (const item of list) {
+      if (item.reportType == 100) {
+        for (const template of templates) {
+          if (template.templateId == item.templateId) {
+            hadTemplateId.value = item.templateId
+            break
+          }
+        }
+      }
+    }
+  }
+}
+
+const handleSelect = (item: ReportTemplate) => {
+  if (hadTemplateId.value === item.templateId && item.reportType === 100) {
+    uni.showToast({ title: '该主报告已存在', icon: 'none' })
+    return
+  }
+
+  const index = localSelectedTemplates.value.findIndex(
+    (template) => template.templateId === item.templateId,
+  )
+
+  debugger
+  if (index > -1) {
+    localSelectedTemplates.value.splice(index, 1)
+  } else {
+    localSelectedTemplates.value.push(item)
+  }
+
+  emit('setIds', item, 'CheckProject')
+  emit('change', selectedItems.value)
+}
+
+onMounted(() => {
+  const initSelected = props.selectTemplates.CheckProject || []
+  localSelectedTemplates.value = [...initSelected]
+  loadTemplates()
+})
+
+watch(
+  () => props.selectTemplates.CheckProject,
+  (newVal) => {
+    if (newVal) {
+      localSelectedTemplates.value = [...newVal]
+    }
+  },
+  { deep: true },
+)
+
+const handleConfirm = async () => {
+  const selected = localSelectedTemplates.value
+  if (selected.length === 0) {
+    uni.showToast({ title: '请至少选择一个检验项目', icon: 'none' })
+    return
+  }
+  const itemList = selected.map((item) => ({
+    connectId: item.connectId,
+    fee: item.fee,
+    orderItemId: props.equipData?.taskOrderItem?.id,
+    templateId: item.templateId,
+    type: props.equipData?.taskOrderItem?.boilerType,
+  }))
+
+  await addInspectProject({
+    itemList,
+    type: 200,
+  })
+
+  console.log('确认选中的检验项目:', itemList)
+  emit('confirm', itemList)
+}
+
+const handleCancel = () => {
+  localSelectedTemplates.value = []
+  emit('cancel')
+}
+</script>
+
+<style lang="scss" scoped>
+.check-project-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden;
+}
+
+.search-bar {
+  display: flex;
+  flex-shrink: 0;
+  gap: 10px;
+  align-items: center;
+  padding: 10px 0;
+}
+
+.dropdown-wrapper {
+  position: relative;
+}
+
+.dropdown-selector {
+  display: flex;
+  align-items: center;
+  min-width: 100px;
+  padding: 8px 12px;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+}
+
+.dropdown-text {
+  margin-right: 8px;
+  font-size: 14px;
+  color: #333;
+}
+
+.dropdown-arrow {
+  font-size: 10px;
+  color: #666;
+}
+
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 100;
+  min-width: 100px;
+  background-color: #fff;
+  border: 1px solid #e0e0e0;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.dropdown-item {
+  padding: 10px 12px;
+  font-size: 14px;
+  color: #333;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.dropdown-item:last-child {
+  border-bottom: none;
+}
+
+.dropdown-item.active {
+  color: #2f8eff;
+  background-color: #e6f7ff;
+}
+
+.search-input {
+  flex: 1;
+  padding: 0 12px;
+  font-size: 14px;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+}
+
+.project-list {
+  flex: 1;
+  height: 0;
+}
+
+.project-group {
+  margin-bottom: 10px;
+}
+
+.project-group-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px;
+  background-color: #f5f5f5;
+}
+
+.project-group-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #333;
+}
+
+.project-group-count {
+  font-size: 12px;
+  color: #999;
+}
+
+.project-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.project-item {
+  display: flex;
+  align-items: center;
+  min-width: calc(50% - 5px);
+  max-width: calc(50% - 5px);
+  padding: 10px 15px;
+  background-color: #fff;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+}
+
+.project-item.first {
+  border-top-left-radius: 4px;
+}
+
+.project-item.first:last-child,
+.project-item.first:nth-last-child(2) {
+  border-top-right-radius: 4px;
+}
+
+.project-item.last {
+  border-bottom-left-radius: 4px;
+}
+
+.project-item.last:nth-child(odd) {
+  border-bottom-right-radius: 4px;
+}
+
+.project-item.selected {
+  background-color: #e6f7ff;
+  border-color: #2f8eff;
+}
+
+.project-name {
+  flex: 1;
+  overflow: hidden;
+  font-size: 14px;
+  color: #333;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.empty-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 0;
+}
+
+.empty-text {
+  font-size: 14px;
+  color: #999;
+}
+
+:deep(.wd-collapse-item) {
+  margin-bottom: 10px;
+  overflow: hidden;
+  background-color: #fff;
+  border-radius: 8px;
+}
+
+:deep(.wd-collapse-item__title) {
+  padding: 12px 15px;
+  font-size: 15px;
+  font-weight: 500;
+  color: #333;
+}
+
+:deep(.wd-collapse-item__title-wrapper) {
+  background-color: #fff;
+}
+
+:deep(.wd-collapse-item__content) {
+  padding: 10px;
+}
+
+:deep(.wd-collapse-item__arrow) {
+  color: #999;
+}
+
+.bottom-actions {
+  display: flex;
+  flex-direction: row;
+  flex-shrink: 0;
+  gap: 10px;
+  padding: 10px 15px;
+  background-color: #fff;
+  border-top: 1px solid #eee;
+
+  .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: #666;
+    background-color: #f5f5f5;
+    border: 1px solid #ddd;
+  }
+
+  .confirm-btn {
+    color: #fff;
+    background-color: rgb(47, 142, 255);
+  }
+}
+</style>

+ 41 - 41
src/pages/equipment/detail/components/checkProjectPopup/checkProject.vue

@@ -7,8 +7,8 @@
           <text class="dropdown-arrow">▼</text>
         </view>
         <view v-if="showDropdown" class="dropdown-menu">
-          <view 
-            v-for="option in inspectionNatureOptions" 
+          <view
+            v-for="option in inspectionNatureOptions"
             :key="option.value"
             class="dropdown-item"
             :class="{ active: inspectionNature === option.value }"
@@ -18,25 +18,25 @@
           </view>
         </view>
       </view>
-      
-      <input 
+
+      <input
         v-model="searchKeyword"
         class="search-input"
         placeholder="搜索项目名称"
         @confirm="handleSearch"
       />
     </view>
-    
+
     <view class="project-list">
       <view class="project-grid">
-        <view 
-          v-for="(item, index) in dataSource" 
+        <view
+          v-for="(item, index) in dataSource"
           :key="item.id"
           class="project-item"
           :class="{
             first: index === 0 || index === 1,
             last: index === dataSource.length - 1 || index === dataSource.length - 2,
-            selected: isSelected(item)
+            selected: isSelected(item),
           }"
           @click="handleSelect(item)"
         >
@@ -48,7 +48,7 @@
           <text class="project-name">{{ item.name }}</text>
         </view>
       </view>
-      
+
       <view v-if="dataSource.length === 0" class="empty-state">
         <text class="empty-text">暂无数据</text>
       </view>
@@ -91,14 +91,14 @@ const inspectionNatureOptions = [
 ]
 
 const inspectionNatureLabel = computed(() => {
-  const option = inspectionNatureOptions.find(opt => opt.value === inspectionNature.value)
+  const option = inspectionNatureOptions.find((opt) => opt.value === inspectionNature.value)
   return option?.label || '定期检验'
 })
 
 const isSelected = (item: ReportTemplate): boolean => {
   const type = 'CheckProject'
   const array = props.selectTemplates[type] || []
-  return array.some(template => template.templateId === item.id)
+  return array.some((template) => template.templateId === item.id)
 }
 
 const selectInspectionNature = (option: any) => {
@@ -114,18 +114,18 @@ const handleSearch = () => {
 const loadTemplates = async () => {
   if (props.useOnline === '1') {
     const { getReportTemplateList } = await import('@/api')
-    const resp = await getReportTemplateList({ 
-      inspectionNature: inspectionNature.value, 
-      name: searchKeyword.value, 
-      status: 200, 
-      pageNo: 1, 
-      pageSize: 1000 
+    const resp = await getReportTemplateList({
+      inspectionNature: inspectionNature.value,
+      name: searchKeyword.value,
+      status: 200,
+      pageNo: 1,
+      pageSize: 1000,
     })
-    
-    const templateList = resp?.data?.list?.filter((item: any) => 
-      ['100', '200', '300'].includes(item.reportType)
+
+    const templateList = resp?.data?.list?.filter((item: any) =>
+      ['100', '200', '300'].includes(item.reportType),
     )
-    
+
     checkExistingTemplates(templateList || [])
     dataSource.value = templateList || []
   } else {
@@ -154,7 +154,7 @@ const handleSelect = (item: ReportTemplate) => {
     uni.showToast({ title: '该主报告已存在', icon: 'none' })
     return
   }
-  
+
   emit('setIds', item, 'CheckProject')
 }
 
@@ -165,15 +165,15 @@ onMounted(() => {
 
 <style lang="scss" scoped>
 .check-project-container {
-  height: 100%;
   display: flex;
   flex-direction: column;
+  height: 100%;
 }
 
 .search-bar {
   display: flex;
-  align-items: center;
   gap: 10px;
+  align-items: center;
   padding: 10px 0;
 }
 
@@ -184,16 +184,16 @@ onMounted(() => {
 .dropdown-selector {
   display: flex;
   align-items: center;
+  min-width: 100px;
   padding: 8px 12px;
   background-color: #f5f5f5;
   border-radius: 4px;
-  min-width: 100px;
 }
 
 .dropdown-text {
+  margin-right: 8px;
   font-size: 14px;
   color: #333;
-  margin-right: 8px;
 }
 
 .dropdown-arrow {
@@ -205,12 +205,12 @@ onMounted(() => {
   position: absolute;
   top: 100%;
   left: 0;
+  z-index: 100;
+  min-width: 100px;
   background-color: #fff;
   border: 1px solid #e0e0e0;
   border-radius: 4px;
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
-  z-index: 100;
-  min-width: 100px;
 }
 
 .dropdown-item {
@@ -225,16 +225,16 @@ onMounted(() => {
 }
 
 .dropdown-item.active {
-  background-color: #e6f7ff;
   color: #2F8EFF;
+  background-color: #e6f7ff;
 }
 
 .search-input {
   flex: 1;
   padding: 8px 12px;
+  font-size: 14px;
   background-color: #f5f5f5;
   border-radius: 4px;
-  font-size: 14px;
 }
 
 .project-list {
@@ -251,12 +251,12 @@ onMounted(() => {
 .project-item {
   display: flex;
   align-items: center;
+  min-width: calc(50% - 5px);
+  max-width: calc(50% - 5px);
   padding: 10px 15px;
   background-color: #fff;
   border: 1px solid #d9d9d9;
   border-radius: 4px;
-  min-width: calc(50% - 5px);
-  max-width: calc(50% - 5px);
 }
 
 .project-item.first {
@@ -278,7 +278,7 @@ onMounted(() => {
 
 .project-item.selected {
   background-color: #e6f7ff;
-  border-color: #2F8EFF;
+  border-color: #2f8eff;
 }
 
 .checkbox-wrapper {
@@ -286,39 +286,39 @@ onMounted(() => {
 }
 
 .checkbox {
+  display: flex;
+  align-items: center;
+  justify-content: center;
   width: 16px;
   height: 16px;
   border: 1px solid #d9d9d9;
   border-radius: 2px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
 }
 
 .checkbox.checked {
-  background-color: #2F8EFF;
-  border-color: #2F8EFF;
+  background-color: #2f8eff;
+  border-color: #2f8eff;
 }
 
 .check-icon {
-  color: #fff;
   font-size: 12px;
   font-weight: bold;
+  color: #fff;
 }
 
 .project-name {
   flex: 1;
+  overflow: hidden;
   font-size: 14px;
   color: #333;
-  overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
 .empty-state {
   display: flex;
-  justify-content: center;
   align-items: center;
+  justify-content: center;
   padding: 40px 0;
 }
 

+ 329 - 0
src/pages/equipment/detail/components/checkProjectPopup/PipeCheckProject.vue

@@ -0,0 +1,329 @@
+<template>
+  <view class="check-project-container">
+    <view class="search-bar">
+      <view class="dropdown-wrapper">
+        <view class="dropdown-selector" @click="showDropdown = !showDropdown">
+          <text class="dropdown-text">{{ inspectionNatureLabel }}</text>
+          <text class="dropdown-arrow">▼</text>
+        </view>
+        <view v-if="showDropdown" class="dropdown-menu">
+          <view
+            v-for="option in inspectionNatureOptions"
+            :key="option.value"
+            class="dropdown-item"
+            :class="{ active: inspectionNature === option.value }"
+            @click="selectInspectionNature(option)"
+          >
+            <text>{{ option.label }}</text>
+          </view>
+        </view>
+      </view>
+
+      <input
+        v-model="searchKeyword"
+        class="search-input"
+        placeholder="搜索项目名称"
+        @confirm="handleSearch"
+      />
+    </view>
+
+    <view class="project-list">
+      <view class="project-grid">
+        <view
+          v-for="(item, index) in dataSource"
+          :key="item.id"
+          class="project-item"
+          :class="{
+            first: index === 0 || index === 1,
+            last: index === dataSource.length - 1 || index === dataSource.length - 2,
+            selected: isSelected(item),
+          }"
+          @click="handleSelect(item)"
+        >
+          <view class="checkbox-wrapper">
+            <view class="checkbox" :class="{ checked: isSelected(item) }">
+              <text v-if="isSelected(item)" class="check-icon">✓</text>
+            </view>
+          </view>
+          <text class="project-name">{{ item.name }}</text>
+        </view>
+      </view>
+
+      <view v-if="dataSource.length === 0" class="empty-state">
+        <text class="empty-text">暂无数据</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, onMounted } from 'vue'
+
+interface ReportTemplate {
+  id: string
+  name: string
+  reportType: number
+  isWallThick?: string
+}
+
+interface Props {
+  propjectList: ReportTemplate[][]
+  selectTemplates: Record<string, any[]>
+  useOnline?: string
+}
+
+const props = defineProps<Props>()
+
+const emit = defineEmits<{
+  setIds: [item: any, type: string]
+  cleanIds: [type: string]
+}>()
+
+const inspectionNature = ref(100)
+const searchKeyword = ref('')
+const dataSource = ref<ReportTemplate[]>([])
+const showDropdown = ref(false)
+const hadTemplateId = ref('')
+
+const inspectionNatureOptions = [
+  { label: '定期检验', value: 100 },
+  { label: '监督检验', value: 200 },
+]
+
+const inspectionNatureLabel = computed(() => {
+  const option = inspectionNatureOptions.find((opt) => opt.value === inspectionNature.value)
+  return option?.label || '定期检验'
+})
+
+const isSelected = (item: ReportTemplate): boolean => {
+  const type = 'CheckProject'
+  const array = props.selectTemplates[type] || []
+  return array.some((template) => template.templateId === item.id)
+}
+
+const selectInspectionNature = (option: any) => {
+  inspectionNature.value = option.value
+  showDropdown.value = false
+  loadTemplates()
+}
+
+const handleSearch = () => {
+  loadTemplates()
+}
+
+const loadTemplates = async () => {
+  if (props.useOnline === '1') {
+    const { getReportTemplateList } = await import('@/api')
+    const resp = await getReportTemplateList({
+      inspectionNature: inspectionNature.value,
+      name: searchKeyword.value,
+      status: 200,
+      pageNo: 1,
+      pageSize: 1000,
+    })
+
+    const templateList = resp?.data?.list?.filter((item: any) =>
+      ['100', '200', '300'].includes(item.reportType),
+    )
+
+    checkExistingTemplates(templateList || [])
+    dataSource.value = templateList || []
+  } else {
+    uni.showToast({ title: '离线模式暂不支持添加项目', icon: 'none' })
+    dataSource.value = []
+  }
+}
+
+const checkExistingTemplates = (templates: ReportTemplate[]) => {
+  for (const list of props.propjectList) {
+    for (const item of list) {
+      if (item.reportType == 100) {
+        for (const template of templates) {
+          if (template.id == item.templateId) {
+            hadTemplateId.value = item.templateId
+            break
+          }
+        }
+      }
+    }
+  }
+}
+
+const handleSelect = (item: ReportTemplate) => {
+  if (hadTemplateId.value === item.id && item.reportType === 100) {
+    uni.showToast({ title: '该主报告已存在', icon: 'none' })
+    return
+  }
+
+  emit('setIds', item, 'CheckProject')
+}
+
+onMounted(() => {
+  loadTemplates()
+})
+</script>
+
+<style lang="scss" scoped>
+.check-project-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.search-bar {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+  padding: 10px 0;
+}
+
+.dropdown-wrapper {
+  position: relative;
+}
+
+.dropdown-selector {
+  display: flex;
+  align-items: center;
+  min-width: 100px;
+  padding: 8px 12px;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+}
+
+.dropdown-text {
+  margin-right: 8px;
+  font-size: 14px;
+  color: #333;
+}
+
+.dropdown-arrow {
+  font-size: 10px;
+  color: #666;
+}
+
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 100;
+  min-width: 100px;
+  background-color: #fff;
+  border: 1px solid #e0e0e0;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.dropdown-item {
+  padding: 10px 12px;
+  font-size: 14px;
+  color: #333;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.dropdown-item:last-child {
+  border-bottom: none;
+}
+
+.dropdown-item.active {
+  color: #2F8EFF;
+  background-color: #e6f7ff;
+}
+
+.search-input {
+  flex: 1;
+  padding: 8px 12px;
+  font-size: 14px;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+}
+
+.project-list {
+  flex: 1;
+  overflow: auto;
+}
+
+.project-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.project-item {
+  display: flex;
+  align-items: center;
+  min-width: calc(50% - 5px);
+  max-width: calc(50% - 5px);
+  padding: 10px 15px;
+  background-color: #fff;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+}
+
+.project-item.first {
+  border-top-left-radius: 4px;
+}
+
+.project-item.first:last-child,
+.project-item.first:nth-last-child(2) {
+  border-top-right-radius: 4px;
+}
+
+.project-item.last {
+  border-bottom-left-radius: 4px;
+}
+
+.project-item.last:nth-child(odd) {
+  border-bottom-right-radius: 4px;
+}
+
+.project-item.selected {
+  background-color: #e6f7ff;
+  border-color: #2f8eff;
+}
+
+.checkbox-wrapper {
+  margin-right: 8px;
+}
+
+.checkbox {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 16px;
+  height: 16px;
+  border: 1px solid #d9d9d9;
+  border-radius: 2px;
+}
+
+.checkbox.checked {
+  background-color: #2f8eff;
+  border-color: #2f8eff;
+}
+
+.check-icon {
+  font-size: 12px;
+  font-weight: bold;
+  color: #fff;
+}
+
+.project-name {
+  flex: 1;
+  overflow: hidden;
+  font-size: 14px;
+  color: #333;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.empty-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 0;
+}
+
+.empty-text {
+  font-size: 14px;
+  color: #999;
+}
+</style>

+ 71 - 9
src/pages/equipment/detail/equipmentDetail.vue

@@ -35,7 +35,7 @@
     </view>
 
     <!-- 内容区域 -->
-    <scroll-view class="scroll-content" scroll-y>
+    <view class="scroll-content">
       <!-- 基本信息 Tab -->
       <view v-if="currentTab === 0" class="tab-content">
         <component
@@ -66,10 +66,12 @@
 
       <!-- 检验项目 Tab -->
       <view v-if="currentTab === 2 && isInspectMode" class="tab-content">
-        <InspectProject
+        <component
+          :is="InspectProjectComponent"
           :report-list="dataSource?.reportList"
           :task-order="dataSource?.taskOrder"
           :equipment="dataSource?.equipment"
+          :dataSource="dataSource"
           :order-id="orderId"
           :order-item-id="orderItemId"
           :use-online="useOnline"
@@ -100,7 +102,7 @@
           @viewDetail="handleViewDetailOtherReport"
         />
       </view>
-    </scroll-view>
+    </view>
 
     <!-- 选择校核人弹窗 -->
     <view v-if="showSelectUserPopup" class="popup-overlay" @click="closeSelectUserPopup">
@@ -118,17 +120,27 @@
       </view>
     </view>
 
-    <view v-if="showCheckProject" class="popup-overlay" @click="closeCheckProjectPopup">
-      <view class="popup-content check-project-popup" @click.stop>
+    <view
+      v-if="showCheckProject"
+      class="popup-overlay"
+      @click="closeCheckProjectPopup"
+      @touchmove.stop.prevent
+    >
+      <view class="popup-content check-project-popup" @click.stop @touchmove.stop>
         <view class="popup-header">
           <text class="popup-title">添加检验项目</text>
           <text class="popup-close" @click="closeCheckProjectPopup">✕</text>
         </view>
-        <CheckProjectPopup
+        <component
+          :is="CheckProjectPopupComponent"
           v-if="showCheckProject"
           :propject-list="checkProjectList"
           :select-templates="selectTemplates"
           :use-online="useOnline"
+          :equip-data="dataSource"
+          @change="handleCheckProjectChange"
+          @confirm="handleCheckProjectConfirm"
+          @cancel="closeCheckProjectPopup"
         />
       </view>
     </view>
@@ -158,10 +170,14 @@ import BoilerBaseInfo from './components/BoilerBaseInfo.vue'
 import ContainerEquipmentInfo from './components/ContainerEquipmentInfo.vue'
 import BoilerEquipmentInfo from './components/BoilerEquipmentInfo.vue'
 import PipeEquipmentInfo from './components/PipeEquipmentInfo.vue'
-import InspectProject from './components/InspectProject.vue'
+import ContainerInspectProject from './components/ContainerInspectProject.vue'
+import BoilerInspectProject from './components/BoilerInspectProject.vue'
+import PipeInspectProject from './components/PipeInspectProject.vue'
 import HistoryReport from './components/HistoryReport.vue'
 import OtherReport from './components/OtherReport.vue'
-import CheckProjectPopup from './components/checkProjectPopup/checkProject.vue'
+import ContainerCheckProject from './components/checkProjectPopup/ContainerCheckProject.vue'
+import BoilerCheckProject from './components/checkProjectPopup/BoilerCheckProject.vue'
+import PipeCheckProject from './components/checkProjectPopup/PipeCheckProject.vue'
 import { useConfigStore } from '@/store/config'
 import { TaskOrderFuncName, requestFunc } from '@/api/ApiRouter/taskOrder'
 import { EquipFuncName, requestFunc as equipRequestFunc } from '@/api/ApiRouter/equipment'
@@ -178,6 +194,7 @@ const currentCheckItem = ref<any>(null)
 
 const checkProjectList = ref<any[][]>([])
 const selectTemplates = ref<Record<string, any[]>>({})
+const currentSelectedItems = ref<any[]>([])
 
 let orderId = ''
 let orderItemId = ''
@@ -217,6 +234,32 @@ const EquipmentInfoComponent = computed(() => {
   }
 })
 
+const InspectProjectComponent = computed(() => {
+  switch (equipType) {
+    case EquipmentType.BOILER:
+      return BoilerInspectProject
+    case EquipmentType.PIPE:
+      return PipeInspectProject
+    case EquipmentType.CONTAINER:
+      return ContainerInspectProject
+    default:
+      return ContainerInspectProject
+  }
+})
+
+const CheckProjectPopupComponent = computed(() => {
+  switch (equipType) {
+    case EquipmentType.BOILER:
+      return BoilerCheckProject
+    case EquipmentType.PIPE:
+      return PipeCheckProject
+    case EquipmentType.CONTAINER:
+      return ContainerCheckProject
+    default:
+      return ContainerCheckProject
+  }
+})
+
 onLoad((options: any) => {
   orderId = options.orderId || ''
   orderItemId = options.orderItemId || ''
@@ -466,6 +509,18 @@ const showCheckProjectPopup = () => {
 
 const closeCheckProjectPopup = () => {
   showCheckProject.value = false
+  currentSelectedItems.value = []
+}
+
+const handleCheckProjectChange = (selectedItems: any[]) => {
+  currentSelectedItems.value = selectedItems
+  console.log('选中的检验项目变化:', selectedItems)
+  console.log('选中的项目数量:', selectedItems.length)
+}
+
+const handleCheckProjectConfirm = (itemList: any[]) => {
+  fetchGetOnlineEquipmentDetail()
+  closeCheckProjectPopup()
 }
 
 // 操作指导书关联
@@ -638,6 +693,13 @@ watch(
   gap: 10px;
 }
 
+.check-project-actions {
+  display: flex;
+  flex-direction: row;
+  gap: 10px;
+  margin-top: 10px;
+}
+
 .action-btn {
   display: flex;
   flex: 1;
@@ -662,7 +724,7 @@ watch(
 
 .check-project-popup {
   width: 85%;
-  max-height: 80vh;
+  height: 80vh;
   padding: 0;
   overflow: hidden;
 }

+ 43 - 136
src/pages/sign/index.vue

@@ -76,13 +76,12 @@
           </view>
         </view>
         <view class="canvas-content">
-          <canvas
-            type="2d"
-            id="signCanvas"
-            class="sign-canvas"
-            @touchstart="onTouchStart"
-            @touchmove="onTouchMove"
-            @touchend="onTouchEnd"
+          <SignatureCanvas
+            ref="signatureRef"
+            canvas-id="signCanvas"
+            :width="canvasWidth"
+            :height="300"
+            @signed="onCanvasSigned"
           />
         </view>
       </view>
@@ -141,10 +140,11 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, computed, onMounted, getCurrentInstance } from 'vue'
+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'>()
@@ -169,13 +169,14 @@ const fwdInputPhone = ref('')
 const showCanvas = ref(false)
 const showFwdPopup = ref(false)
 const showInputPopup = ref(false)
-const canvasCtx = ref<any>(null)
-const lastPoint = ref<{ x: number, y: number } | null>(null)
+const canvasWidth = ref(300)
 const inputName = ref('')
 const inputPhone = ref('')
 const inputEmail = ref('')
 const gcConfig = ref<any>(null)
 
+const signatureRef = ref<any>(null)
+
 const userStore = useUserStore()
 const userInfo = computed(() => userStore.userInfo)
 
@@ -293,93 +294,19 @@ const fetchTaskOrderImg = async (res: any) => {
 const openSignCanvas = () => {
   showCanvas.value = true
   setTimeout(() => {
-    initCanvas()
+    canvasWidth.value = uni.getSystemInfoSync().windowWidth - 40
   }, 100)
 }
 
-// 初始化画布
-const initCanvas = () => {
-  const query = uni.createSelectorQuery()
-  query.select('#signCanvas')
-    .fields({ node: true, size: true })
-    .exec((res) => {
-      if (res[0]) {
-        const canvas = res[0].node
-        const ctx = canvas.getContext('2d')
-        canvasCtx.value = ctx
-        
-        // 设置 canvas 大小
-        const dpr = uni.getSystemInfoSync().pixelRatio
-        canvas.width = res[0].width * dpr
-        canvas.height = res[0].height * dpr
-        ctx.scale(dpr, dpr)
-        
-        // 设置画笔样式
-        ctx.strokeStyle = '#000000'
-        ctx.lineWidth = 2
-        ctx.lineCap = 'round'
-        ctx.lineJoin = 'round'
-      }
-    })
-}
-
-// 获取 canvas 位置信息
-const getCanvasRect = () => {
-  return new Promise<{ left: number; top: number; width: number; height: number }>((resolve) => {
-    const query = uni.createSelectorQuery().in(getCurrentInstance())
-    query.select('#signCanvas').boundingClientRect((rect) => {
-      if (rect) {
-        resolve({
-          left: rect.left,
-          top: rect.top,
-          width: rect.width,
-          height: rect.height
-        })
-      } else {
-        resolve({ left: 0, top: 0, width: 300, height: 300 })
-      }
-    }).exec()
-  })
-}
-
-// 触摸开始
-const onTouchStart = async (e: any) => {
-  const rect = await getCanvasRect()
-  const touch = e.touches[0]
-  lastPoint.value = {
-    x: (touch.clientX - rect.left),
-    y: (touch.clientY - rect.top)
-  }
-}
-
-// 触摸移动
-const onTouchMove = async (e: any) => {
-  if (!lastPoint.value || !canvasCtx.value) return
-  
-  const rect = await getCanvasRect()
-  const touch = e.touches[0]
-  const currentPoint = {
-    x: (touch.clientX - rect.left),
-    y: (touch.clientY - rect.top)
-  }
-  
-  canvasCtx.value.beginPath()
-  canvasCtx.value.moveTo(lastPoint.value.x, lastPoint.value.y)
-  canvasCtx.value.lineTo(currentPoint.x, currentPoint.y)
-  canvasCtx.value.stroke()
-  
-  lastPoint.value = currentPoint
-}
-
-// 触摸结束
-const onTouchEnd = () => {
-  lastPoint.value = null
+// 画布签名完成回调
+const onCanvasSigned = (hasContent: boolean) => {
+  console.log('画布签名状态:', hasContent)
 }
 
 // 清除画布
 const clearCanvas = () => {
-  if (canvasCtx.value) {
-    canvasCtx.value.clearRect(0, 0, 1000, 1000)
+  if (signatureRef.value) {
+    signatureRef.value.clear()
   }
 }
 
@@ -389,47 +316,33 @@ const closeCanvas = () => {
 }
 
 // 确认签名
-const confirmSign = () => {
-  if (!canvasCtx.value) return
-  
-  // 将 canvas 转为图片
-  const query = uni.createSelectorQuery()
-  query.select('#signCanvas')
-    .fields({ node: true })
-    .exec((res) => {
-      if (res[0]) {
-        const canvas = res[0].node
-        uni.canvasToTempFilePath({
-          canvas: canvas,
-          success: (res) => {
-            signImg.value = res.tempFilePath
-            showSignImg.value = res.tempFilePath
-            
-            // 获取图片尺寸
-            uni.getImageInfo({
-              src: res.tempFilePath,
-              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()}日`
-            
-            closeCanvas()
-          },
-          fail: (err) => {
-            console.error('转换失败:', err)
-            uni.showToast({ title: '签名失败', icon: 'error' })
-          }
-        })
+const confirmSign = async () => {
+  if (!signatureRef.value) return
+
+  try {
+    const path = await signatureRef.value.getImage()
+    signImg.value = path
+    showSignImg.value = 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()}日`
+
+    closeCanvas()
+  } catch (err) {
+    console.error('转换失败:', err)
+    uni.showToast({ title: '签名失败', icon: 'error' })
+  }
 }
 
 // 删除签名
@@ -842,14 +755,8 @@ onMounted(() => {
   padding: 20px;
   display: flex;
   justify-content: center;
-}
-
-.sign-canvas {
-  width: 100%;
-  height: 300px;
-  border: 1px solid #ddd;
-  border-radius: 5px;
   background-color: #fff;
+  border-radius: 5px;
 }
 
 .popup-overlay {