Quellcode durchsuchen

完善设备详情信息展示UI

yangguanjin vor 1 Monat
Ursprung
Commit
49131a8ac0

+ 15 - 0
src/api/index.ts

@@ -14,6 +14,13 @@ export const getEquipContainerById = (params: any) => {
   return httpGet('/pressure/equip-container/get', params)
 }
 
+/**
+ * 部门列表
+ */
+export const getDeptListApi = (params: any) => {
+  return httpGet('/system/dept/list', params)
+}
+
 /**
  * 单位查询:列表
  */
@@ -45,3 +52,11 @@ export const getStandardDocFileByUploadId = (params: any) => {
 export const getStandardTemplate = (params: any) => {
   return httpGet('/pressure2/standard-template/v2/get', params)
 }
+
+export const getReportTemplateDetail = (params: any) => {
+  return httpGet('/pressure/report-template/get', params)
+}
+
+export const getReportTemplateList = (params: any) => {
+  return httpGet('/pressure/report-template/page', params)
+}

+ 53 - 52
src/pages/deviceExam/deviceExamDetail.vue

@@ -36,64 +36,30 @@
     <scroll-view class="scroll-content" scroll-y>
       <!-- 基本信息 -->
       <view v-if="currentTab === 0" class="tab-content">
-        <view class="section">
-          <view class="section-header">
-            <text class="section-title">基本信息</text>
-          </view>
-          <view class="info-list">
-            <view class="info-item">
-              <text class="info-label">设备名称:</text>
-              <text class="info-value">{{ equipmentData.equipName || '--' }}</text>
-            </view>
-            <view class="info-item">
-              <text class="info-label">设备注册代码:</text>
-              <text class="info-value">{{ equipmentData.equipCode || '--' }}</text>
-            </view>
-            <view class="info-item">
-              <text class="info-label">出厂编号:</text>
-              <text class="info-value">{{ equipmentData.productNo || '--' }}</text>
-            </view>
-            <view class="info-item">
-              <text class="info-label">使用单位:</text>
-              <text class="info-value">{{ equipmentData.unitName || '--' }}</text>
-            </view>
-          </view>
-        </view>
+        <BaseInfo
+          v-if="dataSource"
+          :data-source="dataSource"
+          use-online="1"
+          can-edit="false"
+        />
       </view>
 
       <!-- 设备信息 -->
       <view v-if="currentTab === 1" class="tab-content">
-        <view class="section">
-          <view class="section-header">
-            <text class="section-title">设备信息</text>
-          </view>
-          <view class="info-list">
-            <view class="info-item">
-              <text class="info-label">设备类别:</text>
-              <text class="info-value">{{ equipmentData.equipCategory || '--' }}</text>
-            </view>
-            <view class="info-item">
-              <text class="info-label">设备类型:</text>
-              <text class="info-value">{{ equipmentData.equipType || '--' }}</text>
-            </view>
-            <view class="info-item">
-              <text class="info-label">设备等级:</text>
-              <text class="info-value">{{ equipmentData.equipLevel || '--' }}</text>
-            </view>
-          </view>
-        </view>
+        <EquipmentInfo
+          v-if="equipmentData"
+          :equipment-data="equipmentData"
+          use-online="1"
+          can-edit="false"
+        />
       </view>
 
       <!-- 历史报告 -->
       <view v-if="currentTab === 2" class="tab-content">
-        <view class="section">
-          <view class="section-header">
-            <text class="section-title">历史报告</text>
-          </view>
-          <view class="empty-state">
-            <text class="empty-text">暂无历史报告</text>
-          </view>
-        </view>
+        <HistoryReport
+          :equip-id="id"
+          use-online="1"
+        />
       </view>
     </scroll-view>
   </view>
@@ -102,7 +68,10 @@
 <script lang="ts" setup>
 import { ref, onMounted } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
-import { getEquipContainerById } from '@/api/index'
+import { getEquipContainerById, getDeptListApi } from '@/api/index'
+import BaseInfo from '@/pages/equipment/detail/components/BaseInfo.vue'
+import EquipmentInfo from '@/pages/equipment/detail/components/EquipmentInfo.vue'
+import HistoryReport from '@/pages/equipment/detail/components/HistoryReport.vue'
 
 interface TabItem {
   value: string
@@ -112,6 +81,7 @@ interface TabItem {
 const currentTab = ref(0)
 const equipmentData = ref<any>({})
 const taskOrder = ref<any>({})
+const dataSource = ref<any>(null)
 
 const tabList: TabItem[] = [
   { value: '基本信息', id: 'baseInfo' },
@@ -140,14 +110,45 @@ const switchTab = (index: number) => {
 // 获取设备详情
 const getEquipmentDetail = async () => {
   try {
+    uni.showLoading({ title: '加载中...' })
     const result: any = await getEquipContainerById({ id })
+    
     if (result?.data) {
       taskOrder.value = result.data
-      equipmentData.value = result.data
+      
+      let deptName = result.data.deptName
+      
+      if (result.data.deptId && !result.data.deptName) {
+        try {
+          const deptResult: any = await getDeptListApi({})
+          if (deptResult?.code === 0 && deptResult?.data && Array.isArray(deptResult.data)) {
+            const deptData = deptResult.data.find((dept: any) => 
+              dept.id === result.data.deptId
+            )
+            if (deptData?.name) {
+              deptName = deptData.name
+            }
+          }
+        } catch (error) {
+          console.error('获取部门名称失败:', error)
+        }
+      }
+      
+      equipmentData.value = {
+        ...result.data,
+        deptName: deptName || result.data.deptName
+      }
+      
+      dataSource.value = {
+        taskOrder: result.data,
+        equipment: equipmentData.value
+      }
     }
   } catch (error) {
     console.error('获取设备详情失败:', error)
     uni.showToast({ title: '加载失败', icon: 'error' })
+  } finally {
+    uni.hideLoading()
   }
 }
 

+ 11 - 3
src/pages/equipment/detail/components/EquipmentInfo.vue

@@ -652,9 +652,9 @@
           <view class="table-cell title">
             <text class="cell-title">主体材质</text>
           </view>
-          <view class="table-cell content">
+                              <view class="table-cell content">
             <input
-              v-if="isEdit"
+              v-if="isEdit && tempData"
               v-model="tempData.mainMaterial"
               class="edit-input-text"
               placeholder="请输入主体材质"
@@ -686,7 +686,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive, onMounted, watch } from 'vue'
+import { ref, reactive, onMounted, watch, computed } from 'vue'
 import { updateEquipApi } from '@/api/task'
 import { useUserStore } from '@/store/user'
 
@@ -719,6 +719,8 @@ const equipment = ref<any>({})
 const tempData = ref<any>({})
 const cacheData = ref<any>({})
 
+const safeTempData = computed(() => tempData.value || {})
+
 const STORAGE_KEY = `equipment-info-cache-${userInfo.value?.id}-${props.orderId}-${props.equipId}`
 
 const showPicker = ref(false)
@@ -934,6 +936,12 @@ watch(
   { immediate: true, deep: true },
 )
 
+watch(isEdit, (newIsEdit) => {
+  if (newIsEdit && !tempData.value) {
+    tempData.value = {}
+  }
+})
+
 onMounted(() => {
   if (props.equipmentData) {
     equipment.value = props.equipmentData

+ 214 - 16
src/pages/equipment/detail/components/HistoryReport.vue

@@ -1,23 +1,160 @@
 <template>
   <view class="history-report-section">
-    <view class="section">
-      <view class="section-header">
-        <text class="section-title">历年报告</text>
+    <scroll-view
+      class="scroll-view"
+      scroll-y
+      @scrolltolower="loadMore"
+    >
+      <view v-if="listData.length > 0">
+        <view
+          v-for="item in listData"
+          :key="item.id"
+          class="item-box"
+          @click="lookReport(item)"
+        >
+          <view class="item-top">
+            <text class="title">
+              报告编号:{{ item.reportNo }}
+              <text>(<text :style="{ color: getCheckTypeColor(item.checkType) }">{{ getCheckTypeText(item.checkType) }}</text>)</text>
+            </text>
+            <text class="title">
+              检验日期:{{ formatCheckDate(item.checkDate) }}
+            </text>
+          </view>
+
+          <view class="item-center">
+            <view class="item-center-content">
+              <text class="item-center-text">检验结论:<text class="value-text">{{ item.result || '无' }}</text></text>
+              <text class="item-center-text">费用:<text class="value-text">{{ item.fee || '无' }}</text></text>
+              <text class="item-center-text">检测部门:<text class="value-text">{{ item?.dept?.name }}</text></text>
+            </view>
+            <view class="look-report" @click.stop="lookReport(item)">
+              <text class="look-report-text">查看报告</text>
+            </view>
+          </view>
+        </view>
       </view>
-      <view class="empty-state">
+
+      <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-state">
         <text class="empty-text">暂无历年报告</text>
       </view>
-    </view>
+    </scroll-view>
   </view>
 </template>
 
 <script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import { getHistoryReportApi } from '@/api/task'
+
 interface Props {
   equipId: string
   useOnline?: string
 }
 
-defineProps<Props>()
+const props = defineProps<Props>()
+
+const listData = ref<any[]>([])
+const loading = ref(false)
+const hasMore = ref(true)
+const params = ref({
+  pageNo: 1,
+  pageSize: 10,
+  equipId: '',
+})
+
+const fetchList = async (refresh = false) => {
+  if (loading.value) return
+
+  params.value.pageNo = refresh ? 1 : params.value.pageNo + 1
+
+  loading.value = true
+  try {
+    const result: any = await getHistoryReportApi(params.value)
+    
+    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.value.pageSize
+    } else {
+      if (refresh) {
+        listData.value = []
+      }
+      hasMore.value = false
+    }
+  } catch (error) {
+    console.error('获取历年报告失败:', error)
+    if (refresh) {
+      listData.value = []
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadMore = () => {
+  if (!loading.value && hasMore.value) {
+    fetchList(false)
+  }
+}
+
+const formatCheckDate = (checkDate: any): string => {
+  if (!checkDate) return ''
+  if (Array.isArray(checkDate)) {
+    return checkDate.join('-')
+  }
+  return checkDate
+}
+
+const getCheckTypeColor = (checkType: number): string => {
+  switch (checkType) {
+    case 200:
+      return 'rgb(251, 127, 55)'
+    case 300:
+      return '#ff4d4f'
+    default:
+      return '#2F8EFF'
+  }
+}
+
+const getCheckTypeText = (checkType: number): string => {
+  switch (checkType) {
+    case 100:
+      return '待检验'
+    case 200:
+      return '定期检验'
+    case 300:
+      return '全面检验'
+    default:
+      return '未知'
+  }
+}
+
+const lookReport = (item: any) => {
+  if (!item.issueUrl) {
+    uni.showToast({ title: '没有报告', icon: 'error' })
+    return
+  }
+
+  const fileUrl = item.issueUrl.startsWith('http') ? item.issueUrl : 'https://api3.boot.jeecg.com' + item.issueUrl
+
+  uni.navigateTo({
+    url: `/pages/preViewPdf/PreViewPdf?url=${encodeURIComponent(fileUrl)}`,
+  })
+}
+
+onMounted(() => {
+  if (props.equipId) {
+    params.value.equipId = props.equipId
+    fetchList(true)
+  }
+})
 </script>
 
 <style lang="scss" scoped>
@@ -25,22 +162,83 @@ defineProps<Props>()
   padding: 10px;
 }
 
-.section {
+.scroll-view {
+  height: calc(100vh - 200px);
+}
+
+.item-box {
+  margin-bottom: 12px;
   background-color: #fff;
+  padding: 13px;
   border-radius: 5px;
-  overflow: hidden;
 }
 
-.section-header {
-  padding: 15px;
-  background-color: #f9f9f9;
-  border-bottom: 1px solid #f0f0f0;
+.item-top {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding-bottom: 13px;
+  border-bottom: 1px solid rgb(244, 244, 244);
 }
 
-.section-title {
-  font-size: 15px;
-  font-weight: 600;
-  color: #333;
+.title {
+  font-size: 13px;
+  color: rgb(51, 51, 51);
+}
+
+.item-center {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-top: 13px;
+}
+
+.item-center-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  background-color: rgb(244, 244, 244);
+  border-radius: 5px;
+  padding: 12px;
+}
+
+.item-center-text {
+  font-size: 13px;
+  color: rgb(102, 102, 102);
+  margin-bottom: 5px;
+}
+
+.item-center-text:last-child {
+  margin-bottom: 0;
+}
+
+.value-text {
+  color: rgb(51, 51, 51);
+}
+
+.look-report {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 25px;
+  padding: 0 10px;
+  background-color: rgb(47, 142, 255);
+  border-radius: 3px;
+  margin-left: 10px;
+}
+
+.look-report-text {
+  font-size: 12px;
+  color: #fff;
+}
+
+.loading-text,
+.no-more-text {
+  text-align: center;
+  padding: 15px;
+  color: #999;
+  font-size: 14px;
 }
 
 .empty-state {

+ 289 - 13
src/pages/equipment/detail/components/InspectProject.vue

@@ -293,6 +293,21 @@
         </view>
       </view>
     </view>
+
+    <view v-if="showSelectTemplatePopup" class="popup-overlay" @click="closeSelectTemplatePopup">
+      <view class="popup-content" @click.stop>
+        <text class="popup-title">选择模板</text>
+        <picker :range="templateList" range-key="label" @change="onTemplateChange">
+          <view class="picker-value">
+            <text>{{ selectedTemplate?.label || '请选择' }}</text>
+          </view>
+        </picker>
+        <view class="popup-actions">
+          <button class="action-btn cancel-btn" @click="closeSelectTemplatePopup">取消</button>
+          <button class="action-btn confirm-btn" @click="handleConfirmTemplate">确定</button>
+        </view>
+      </view>
+    </view>
   </view>
 </template>
 
@@ -304,12 +319,14 @@ import {
   PressureCheckerMyTaskStatusMap,
 } from '@/utils/dictMap'
 import { isCheckItemEditable, isAssignedToOthers } from '@/utils/equipmentPermissions'
-import { getApprovalDetail, getUserGroupUserList } from '@/api/task'
+import { getApprovalDetail, getUserGroupUserList, notVerifyPageApi, addMajorIssuesApi } from '@/api/task'
+import { getReportTemplateDetail } from '@/api'
 import TipsPopup from './inspectProjectComponent/TipsPopup.vue'
 import CalcCheckItemPopup from './inspectProject/component/calcCheckItemPopup.vue'
 import ExchangeChecker from './inspectProject/component/ExchangeChecker.vue'
 import UpdateConclusionPopup from './inspectProject/component/UpdateConclusionPopup.vue'
 import eventBus from '@/utils/eventBus'
+import dayjs from 'dayjs'
 
 interface CheckConclusionItem {
   value: string
@@ -344,6 +361,7 @@ interface Props {
 }
 
 const props = withDefaults(defineProps<Props>(), {
+  reportList: () => [],
   useOnline: '1',
   isMainChecker: false,
   canModifyAssignments: false,
@@ -378,6 +396,10 @@ const currentReportItem = ref<any>(null)
 const recheckUserGroupList = ref<any[]>([])
 const currentReckUser = ref<any>(null)
 
+const showSelectTemplatePopup = ref(false)
+const templateList = ref<any[]>([])
+const selectedTemplate = ref<any>(null)
+
 const userInfo = computed(() => uni.getStorageSync('userInfo'))
 
 const canModifyChecker = computed(() => {
@@ -550,9 +572,10 @@ const getStatusColor = (item: CheckItem, useOnline?: string): string => {
 
 const shouldShowFeeInput = (item: CheckItem): boolean => {
   if (props.useOnline === '1') {
-    return item.taskStatus !== undefined
+    return item.taskStatus !== undefined && 
+      [PressureReportType.MAIN, PressureReportType.SUB, PressureReportType.SINGLE].includes(item.reportType)
   }
-  return true
+  return [PressureReportType.MAIN, PressureReportType.SUB, PressureReportType.SINGLE].includes(item.reportType)
 }
 
 const shouldShowSign = (item: CheckItem): boolean => {
@@ -877,25 +900,199 @@ const handleAssociationOperation = (item: CheckItem) => {
 }
 
 const showAddWorkInstructionPopup = async () => {
-  uni.showToast({ title: '添加指导书功能开发中', icon: 'none' })
+  try {
+    const result = await notVerifyPageApi({ type: '5', status: 200, pageNo: 1, pageSize: 9999 })
+    const list = (result?.data?.list || []).map((item: any) => ({
+      ...item,
+      label: item.name || '',
+      value: item.id || ''
+    }))
+    
+    if (!list.length) {
+      uni.showToast({ title: '暂无指导书模板', icon: 'error' })
+      return
+    }
+    
+    templateList.value = list
+    selectedTemplate.value = null
+    showSelectTemplatePopup.value = true
+  } catch (error) {
+    console.error('获取操作指导书模板失败:', error)
+    uni.showToast({ title: '获取操作指导书模板失败', icon: 'error' })
+  }
+}
+
+const closeSelectTemplatePopup = () => {
+  showSelectTemplatePopup.value = false
+  selectedTemplate.value = null
+}
+
+const onTemplateChange = (e: any) => {
+  const index = e.detail.value
+  selectedTemplate.value = templateList.value[index]
+}
+
+const handleConfirmTemplate = async () => {
+  if (!selectedTemplate.value) {
+    uni.showToast({ title: '请选择模板', icon: 'error' })
+    return
+  }
+
+  try {
+    uni.showLoading({ title: '添加中...' })
+    
+    const templateResult = await getReportTemplateDetail({ id: selectedTemplate.value.value })
+    const initTemplateJson = templateResult?.data?.reportDefaultJson 
+      ? JSON.parse(templateResult?.data?.reportDefaultJson || '{}') 
+      : {}
+    
+    const defaultDataSource: Record<string, any> = {
+      ...initTemplateJson,
+      ...props.equipment,
+      prepareName: userInfo.value?.nickname || '',
+      prepareDate: dayjs().format('YYYY年MM月DD日')
+    }
+    
+    const params = {
+      orderId: props.orderId,
+      orderItemId: props.orderItemId,
+      userGroupCategory: selectedTemplate.value?.userGroupCategory || '',
+      templateId: selectedTemplate.value.value,
+      prepareId: userInfo.value?.id || '',
+      prepareName: userInfo.value?.nickname || '',
+      prepareJson: JSON.stringify(defaultDataSource)
+    }
+    
+    const result = await addMajorIssuesApi(params)
+    
+    if (result.code !== 0) {
+      uni.hideLoading()
+      uni.showToast({ title: result?.msg || '添加失败', icon: 'error' })
+      return
+    }
+    
+    const newReportId = result?.data || ''
+    if (!newReportId) {
+      uni.hideLoading()
+      uni.showToast({ title: '添加失败', icon: 'error' })
+      return
+    }
+    
+    uni.hideLoading()
+    closeSelectTemplatePopup()
+    props.refreshDetail?.()
+    
+    uni.navigateTo({
+      url: `/pages/webview/webview?userId=${userInfo.value?.id}&orderItemId=${props.orderItemId}&checkItemId=${newReportId}&templateId=${selectedTemplate.value.id}&equipCode=${props.equipment?.equipCode || ''}&useOnline=1&reportUrl=`
+    })
+  } catch (error) {
+    uni.hideLoading()
+    console.error('添加指导书失败:', error)
+    uni.showToast({ title: '添加指导书失败', icon: 'error' })
+  }
 }
 
 const showCheckProjectPopup = () => {
   props.showCheckProjectPopupFn?.()
 }
 
-const handleCheckReport = () => {
+const handleCheckReport = async () => {
   if (selectedProjects.value.length === 0) {
     uni.showToast({ title: '请选择要上传的项目', icon: 'error' })
     return
   }
 
-  tipsPopupRef.value?.show({
-    text: '是否上传已选择的检验项目?',
-    confirm: async () => {
-      uni.showToast({ title: '上传功能开发中', icon: 'none' })
-    },
-  })
+  try {
+    const itemArray = selectedProjects.value.map(id => props.reportList.find((i) => i.id === id)).filter(Boolean)
+    
+    for (const checkItem of itemArray) {
+      const userId =
+        checkItem.reportType === PressureReportType.MAIN
+          ? props.equipment?.mainCheckerUser?.id
+          : checkItem?.checkUsers?.[0]?.id
+      
+      if (userId && userId !== userInfo.value?.id) {
+        uni.showToast({ title: '已分配项目不支持上传', icon: 'error' })
+        return
+      }
+    }
+
+    const { checkedCheckItemhasSubmited } = await import('@/api/task')
+    const promiseArr = itemArray.map(item => checkedCheckItemhasSubmited({ id: item.id }))
+    const result = await Promise.all(promiseArr)
+
+    const checkNameArr = []
+    for (const res of result) {
+      if (res?.code === 0 && res?.data?.isSubmit) {
+        checkNameArr.push(res?.data.checkName)
+      }
+    }
+
+    if (checkNameArr?.length) {
+      tipsPopupRef.value?.show({
+        text: `检验员:${checkNameArr.join('、')}已经提交过,确定要覆盖吗?`,
+        confirm: handleBatchUpload,
+      })
+    } else {
+      handleBatchUpload()
+    }
+  } catch (error) {
+    console.error('批量上传失败:', error)
+    uni.showToast({ title: '项目检验失败', icon: 'error' })
+  }
+}
+
+const handleBatchUpload = async () => {
+  try {
+    uni.showLoading({ title: '上传中...' })
+    
+    const itemArray = selectedProjects.value.map(id => props.reportList.find((i) => i.id === id)).filter(Boolean)
+    
+    const params: any = { addList: [], updateList: [] }
+    
+    itemArray.forEach((item) => {
+      const dic = {
+        orderId: props.orderId,
+        orderItemId: props.orderItemId,
+        reportUrl: item.reportUrl || '',
+        dataJson: '',
+        prepareUrl: '',
+        prepareJson: item.prepareJson || '',
+        modifiedReason: '',
+        templateId: item.templateId,
+        reportType: item.reportType,
+        equipAddress: props.equipment?.equipAddress || '',
+        equipLatitude: props.equipment?.equipLatitude || '',
+        equipLongitude: props.equipment?.equipLongitude || '',
+        fee: item?.fee || 0,
+      }
+
+      if (item.isLocal) {
+        dic['localId'] = item.id
+        params['addList'].push(dic)
+      } else {
+        dic['id'] = item.id
+        params['updateList'].push(dic)
+      }
+    })
+
+    const { postCheckItemRecordEnter } = await import('@/api/task')
+    const postResult = await postCheckItemRecordEnter(params)
+
+    uni.hideLoading()
+
+    if (postResult.data) {
+      uni.showToast({ title: '提交成功', icon: 'success' })
+      props.refreshDetail?.()
+      initSelected()
+    } else {
+      uni.showToast({ title: postResult?.msg || '提交失败', icon: 'error' })
+    }
+  } catch (error) {
+    uni.hideLoading()
+    console.error('批量上传失败:', error)
+    uni.showToast({ title: '批量上传失败', icon: 'error' })
+  }
 }
 
 const handleDelReport = () => {
@@ -904,6 +1101,16 @@ const handleDelReport = () => {
     return
   }
 
+  const hasMain = selectedProjects.value.some((id) => {
+    const item = props.reportList.find((i) => i.id === id)
+    return item?.reportType === PressureReportType.MAIN
+  })
+  
+  if (hasMain) {
+    uni.showToast({ title: '主报告不能作废', icon: 'error' })
+    return
+  }
+
   const hasReportEnd =
     props.useOnline === '1' &&
     selectedProjects.value.some((id) => {
@@ -919,7 +1126,36 @@ const handleDelReport = () => {
   tipsPopupRef.value?.show({
     text: '是否作废已选择的检验项目?',
     confirm: async () => {
-      uni.showToast({ title: '作废功能开发中', icon: 'none' })
+      try {
+        uni.showLoading({ title: '作废中...' })
+        
+        const cancelIds = selectedProjects.value
+        let result = null
+        
+        if (props.useOnline === '1') {
+          const { delReportApi } = await import('@/api/task')
+          const fetchResult = await delReportApi({ ids: cancelIds })
+          result = fetchResult.data
+        } else {
+          uni.showToast({ title: '离线模式作废功能开发中', icon: 'none' })
+          uni.hideLoading()
+          return
+        }
+        
+        uni.hideLoading()
+        
+        if (result) {
+          props.refreshDetail?.()
+          initSelected()
+          uni.showToast({ title: '作废成功', icon: 'success' })
+        } else {
+          uni.showToast({ title: '作废失败', icon: 'error' })
+        }
+      } catch (error) {
+        uni.hideLoading()
+        console.error('作废失败:', error)
+        uni.showToast({ title: '作废失败', icon: 'error' })
+      }
     },
   })
 }
@@ -936,7 +1172,47 @@ const uploadFileAndSubmitCheckItemCallback = {
       return
     }
 
-    uni.showToast({ title: '上传功能开发中', icon: 'none' })
+    try {
+      uni.showLoading({ title: '上传中...' })
+      
+      const params = {
+        orderId: props.orderId,
+        orderItemId: props.orderItemId,
+        reportUrl: item.reportUrl || '',
+        dataJson: '',
+        prepareUrl: '',
+        prepareJson: item.prepareJson || '',
+        modifiedReason: '',
+        templateId: item.templateId,
+        reportType: item.reportType,
+        fee: item?.fee || 0,
+      }
+
+      const newParams: any = {}
+      if (item.isLocal) {
+        params['localId'] = item.id
+        newParams['addList'] = [params]
+      } else {
+        params['id'] = item.id
+        newParams['updateList'] = [params]
+      }
+
+      const { postCheckItemRecordEnter } = await import('@/api/task')
+      const postResult = await postCheckItemRecordEnter(newParams)
+
+      uni.hideLoading()
+
+      if (postResult.data) {
+        uni.showToast({ title: '提交成功', icon: 'success' })
+        props.refreshDetail?.()
+      } else {
+        uni.showToast({ title: postResult?.msg || '提交失败', icon: 'error' })
+      }
+    } catch (error) {
+      uni.hideLoading()
+      console.error('上传失败:', error)
+      uni.showToast({ title: '上传失败', icon: 'error' })
+    }
   },
 }
 </script>

+ 341 - 96
src/pages/equipment/detail/components/OtherReport.vue

@@ -1,50 +1,82 @@
 <template>
   <view class="other-report-section">
-    <view class="section">
-      <view class="section-header">
-        <text class="section-title">记录文件</text>
-      </view>
-      <view class="report-list">
-        <view v-for="item in reportList" :key="item.id" class="report-item">
-          <view class="report-content">
-            <view class="report-info">
-              <text
-                v-if="showStatusTag(item)"
-                class="status-tag"
-              >
+    <scroll-view class="scroll-view" scroll-y>
+      <view v-for="group in dataSource" :key="group.reportType" class="group-box">
+        <view class="group-header">
+          <text class="group-title">{{ group.reportName }}</text>
+        </view>
+
+        <view 
+          v-for="(item, index) in group.data" 
+          :key="item.id" 
+          class="report-item"
+          :class="{ 'first-item': index === 0 }"
+          @click="handleSelectItem(item)"
+        >
+          <view class="item-content">
+            <view class="item-left">
+              <checkbox 
+                :checked="isSelected(item)" 
+                color="#4B8CD9"
+                @click.stop="handleSelectItem(item)"
+              />
+              <text v-if="showStatusTag(item)" class="status-tag">
                 {{ getStatusText(item) }}
               </text>
               <text class="report-name">{{ item.reportName }}</text>
             </view>
-            <view class="report-actions">
-              <view
-                v-if="canEdit(item)"
-                class="action-btn edit-btn"
-                @click="handleEdit(item)"
+            <view class="item-right">
+              <view 
+                v-if="canEdit(item)" 
+                class="action-btn edit-btn" 
+                @click.stop="handleEdit(item)"
               >
                 <text>修改</text>
               </view>
-              <view
-                class="action-btn view-btn"
-                @click="handleViewDetail(item)"
+              <view 
+                class="action-btn view-btn" 
+                @click.stop="handleViewDetail(item)"
               >
                 <text>查看详情</text>
               </view>
             </view>
           </view>
         </view>
+      </view>
 
-        <view v-if="!reportList || reportList.length === 0" class="empty-state">
-          <text class="empty-text">暂无记录文件</text>
-          <text class="empty-sub-text">包括重大问题线索告知表、检验方案、操作指导书等</text>
+      <view v-if="!dataSource || dataSource.length === 0" class="empty-state">
+        <text class="empty-text">暂无项目文件</text>
+        <text class="empty-sub-text">包括重大问题线索告知表、检验方案、操作指导书等</text>
+      </view>
+    </scroll-view>
+
+    <view class="bottom-bar">
+      <view class="select-all-box" @click="handleSelectAll">
+        <checkbox 
+          class="square-checkbox" 
+          :checked="selectAll" 
+          color="#4B8CD9"
+          @click.stop="handleSelectAll"
+        />
+        <text class="select-all-text">{{ selectAll ? '取消全选' : '全选' }}</text>
+      </view>
+      
+      <view class="action-buttons">
+        <view class="operate-btn delete-btn" @click="showDelReportPopup">
+          <text class="operate-btn-text">作废项目</text>
         </view>
       </view>
     </view>
+
+    <TipsPopup ref="tipsPopupRef" />
   </view>
 </template>
 
 <script lang="ts" setup>
-import { PressureReportType } from '@/utils/dictMap'
+import { ref, watch, withDefaults } from 'vue'
+import { PressureReportType, PressureReportTypeMap } from '@/utils/dictMap'
+import TipsPopup from './inspectProjectComponent/TipsPopup.vue'
+import eventBus from '@/utils/eventBus'
 
 interface ReportItem {
   id: string
@@ -56,20 +88,157 @@ interface ReportItem {
   [key: string]: any
 }
 
+interface GroupItem {
+  reportType: number
+  reportName: string
+  data: ReportItem[]
+}
+
 interface Props {
-  reportList: ReportItem[]
-  orderItemId: string
+  otherReportList?: ReportItem[]
   useOnline?: string
+  orderItemId?: string
 }
 
-const props = defineProps<Props>()
+const props = withDefaults(defineProps<Props>(), {
+  otherReportList: () => [],
+})
 const emit = defineEmits<{
   edit: [item: ReportItem]
   viewDetail: [item: ReportItem]
+  handleEditWorkInstruction: [item: ReportItem]
 }>()
 
+const tipsPopupRef = ref<any>(null)
+const selectedProjects = ref<ReportItem[]>([])
+const selectAll = ref(false)
+const itemRefs = ref<Record<string, any>>({})
+
+const dataSource = ref<GroupItem[]>([])
+
+watch(
+  () => props.otherReportList,
+  (newList) => {
+    if (!newList || !Array.isArray(newList)) {
+      dataSource.value = []
+      return
+    }
+
+    const array: GroupItem[] = []
+
+    for (const item of newList) {
+      const index = array.findIndex(ele => ele.reportType === item.reportType)
+
+      if (index !== -1) {
+        if (array[index]['data']) {
+          array[index]['data'].push(item)
+        } else {
+          array[index]['data'] = [item]
+        }
+      } else {
+        array.push({
+          reportType: item.reportType,
+          reportName: PressureReportTypeMap[item.reportType] || '其他',
+          data: [item],
+        })
+      }
+    }
+
+    dataSource.value = array
+  },
+  { immediate: true }
+)
+
+const isSelected = (item: ReportItem): boolean => {
+  return selectedProjects.value.some(p => p.id === item.id)
+}
+
+const handleSelectItem = (item: ReportItem) => {
+  const index = selectedProjects.value.findIndex(p => p.id === item.id)
+  
+  if (index === -1) {
+    selectedProjects.value.push(item)
+  } else {
+    selectedProjects.value.splice(index, 1)
+  }
+
+  selectAll.value = selectedProjects.value.length === dataSource.value.length
+}
+
+const handleSelectAll = () => {
+  selectAll.value = !selectAll.value
+
+  selectedProjects.value = []
+
+  if (selectAll.value) {
+    selectedProjects.value = [...(props.otherReportList || [])]
+  }
+}
+
+const showDelReportPopup = () => {
+  if (!selectedProjects.value.length) {
+    uni.showToast({ title: '请选择作废的项目', icon: 'error' })
+    return
+  }
+
+  const hasMain = selectedProjects.value.some(item => item.reportType === 100)
+  if (hasMain) {
+    uni.showToast({ title: '主报告不能作废', icon: 'error' })
+    return
+  }
+
+  tipsPopupRef.value?.show({
+    text: '是否作废已选择的检验项目?',
+    confirm: handleDelReport,
+  })
+}
+
+const handleDelReport = async () => {
+  try {
+    uni.showLoading({ title: '作废中...' })
+
+    const cancelIds = selectedProjects.value.map(item => item.id)
+    let result = null
+
+    if (props.useOnline === '1') {
+      const { delReportApi } = await import('@/api/task')
+      const fetchResult = await delReportApi({ ids: cancelIds })
+      result = fetchResult.data
+    } else {
+      uni.showToast({ title: '离线模式暂不支持作废', icon: 'none' })
+      uni.hideLoading()
+      return
+    }
+
+    uni.hideLoading()
+
+    if (result) {
+      eventBus.emit('RefreshOnlineData')
+      uni.showToast({ title: '作废成功', icon: 'success' })
+      initSelected()
+    } else {
+      uni.showToast({ title: '作废失败', icon: 'error' })
+    }
+  } catch (error) {
+    uni.hideLoading()
+    console.error('作废失败:', error)
+    uni.showToast({ title: '作废失败', icon: 'error' })
+  }
+}
+
+const initSelected = () => {
+  selectAll.value = false
+  selectedProjects.value = []
+
+  for (const key in itemRefs.value) {
+    if (itemRefs.value[key]?.setSelect) {
+      itemRefs.value[key].setSelect(false)
+    }
+  }
+}
+
 const showStatusTag = (item: ReportItem): boolean => {
-  return [PressureReportType.WORKINSTRUCTION].includes(item.reportType)
+  return [PressureReportType.WORKINSTRUCTION].includes(item.reportType) && item.status !== undefined
 }
 
 const getStatusText = (item: ReportItem): string => {
@@ -98,110 +267,129 @@ const canEdit = (item: ReportItem): boolean => {
   return false
 }
 
-const getReportTypeText = (reportType?: number): string => {
-  const typeMap: Record<number, string> = {
-    400: '附',
-    500: '附',
-    600: '附',
-    700: '指',
-  }
-  return typeMap[reportType || 0] || '附'
-}
-
-const getReportTypeColor = (reportType?: number): string => {
-  const colorMap: Record<number, string> = {
-    700: '#FF3B3B',
-  }
-  return colorMap[reportType || 0] || '#fff'
-}
-
 const handleEdit = (item: ReportItem) => {
   emit('edit', item)
+  emit('handleEditWorkInstruction', item)
 }
 
-const handleViewDetail = (item: ReportItem) => {
-  emit('viewDetail', item)
+const handleViewDetail = async (item: ReportItem) => {
+  try {
+    uni.showLoading({ title: '加载中...' })
+
+    const { reportPreviewApi } = await import('@/api')
+    
+    const params = {
+      reportId: item?.id,
+      equipCode: item?.equipCode,
+      type: 300,
+      fileType: 200,
+      isBase64: true,
+    }
+
+    const result = await reportPreviewApi(params)
+
+    uni.hideLoading()
+
+    if (result) {
+      const pdfUrl = `data:application/pdf;base64,${result}`
+      uni.navigateTo({
+        url: `/pages/preViewPdf/PreViewPdf?title=${encodeURIComponent(item.reportName)}&url=${encodeURIComponent(pdfUrl)}`,
+      })
+    }
+  } catch (error) {
+    uni.hideLoading()
+    console.error('获取报告预览失败:', error)
+    uni.showToast({ title: '获取报告预览失败', icon: 'error' })
+  }
 }
 </script>
 
 <style lang="scss" scoped>
 .other-report-section {
-  padding: 10px;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background-color: #f5f5f5;
 }
 
-.section {
+.scroll-view {
+  flex: 1;
+  padding: 12px;
+  padding-bottom: 80px;
+}
+
+.group-box {
+  margin-bottom: 12px;
+  padding: 15px;
   background-color: #fff;
   border-radius: 5px;
-  overflow: hidden;
 }
 
-.section-header {
-  padding: 15px;
-  background-color: #f9f9f9;
-  border-bottom: 1px solid #f0f0f0;
+.group-header {
+  padding-bottom: 10px;
+  margin-bottom: 10px;
+  border-bottom: 1px solid rgb(244, 244, 244);
 }
 
-.section-title {
+.group-title {
   font-size: 15px;
   font-weight: 600;
-  color: #333;
-}
-
-.report-list {
-  padding: 0 15px;
+  color: rgb(51, 51, 51);
 }
 
 .report-item {
   padding: 10px 0;
-  border-bottom: 1px solid #f4f4f4;
+  border-bottom: 1px solid rgb(244, 244, 244);
 }
 
-.report-item:last-child {
+.report-item.last {
   border-bottom: none;
 }
 
-.report-content {
+.first-item {
+  padding-top: 5px;
+}
+
+.item-content {
   display: flex;
   flex-direction: row;
   align-items: center;
   justify-content: space-between;
 }
 
-.report-info {
+.item-left {
   display: flex;
+  flex: 1;
   flex-direction: row;
   align-items: center;
-  flex: 1;
   min-width: 0;
 }
 
 .status-tag {
-  flex-shrink: 0;
-  height: 25px;
-  padding: 0 4px;
   display: flex;
-  flex-direction: row;
-  justify-content: center;
   align-items: center;
-  border-radius: 3px;
-  border: 1px solid #ff3b3b;
+  justify-content: center;
+  height: 25px;
+  padding: 0 4px;
   margin-right: 5px;
-  line-height: 25px;
-  color: #ff3b3b;
   font-size: 12px;
+  color: rgb(255, 59, 59);
+  border: 1px solid rgb(255, 59, 59);
+  border-radius: 3px;
+  flex-shrink: 0;
 }
 
 .report-name {
-  font-size: 14px;
-  color: #333;
   flex: 1;
   min-width: 0;
+  font-size: 14px;
+  color: rgb(51, 51, 51);
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
-.report-actions {
+.item-right {
   display: flex;
   flex-direction: row;
   align-items: center;
@@ -211,51 +399,108 @@ const handleViewDetail = (item: ReportItem) => {
 
 .action-btn {
   display: flex;
-  flex-direction: row;
-  justify-content: center;
   align-items: center;
+  justify-content: center;
   height: 25px;
+  padding: 0 10px;
   border-radius: 3px;
+  margin-left: 5px;
+}
+
+.action-btn text {
   font-size: 12px;
+  color: #fff;
 }
 
 .edit-btn {
-  background-color: #e6a23c;
-  padding: 0 10px;
-  margin-right: 5px;
-
-  text {
-    color: #fff;
-  }
+  background-color: rgb(230, 162, 60);
 }
 
 .view-btn {
-  background-color: #2f8aff;
-  padding: 0 10px;
-
-  text {
-    color: #fff;
-  }
+  background-color: rgb(47, 142, 255);
 }
 
 .empty-state {
   display: flex;
   flex-direction: column;
-  justify-content: center;
   align-items: center;
+  justify-content: center;
   padding: 60px 20px;
 }
 
 .empty-text {
-  color: #999;
+  margin-bottom: 8px;
   font-size: 16px;
+  color: #999;
   text-align: center;
-  margin-bottom: 8px;
 }
 
 .empty-sub-text {
-  color: #ccc;
   font-size: 14px;
+  color: #ccc;
   text-align: center;
 }
+
+.bottom-bar {
+  position: fixed;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 100;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  height: 68px;
+  padding: 0 12px;
+  background-color: #fff;
+  border-top: 1px solid rgb(244, 244, 244);
+}
+
+.select-all-box {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  width: 68px;
+}
+
+.square-checkbox {
+  border-radius: 0 !important;
+  transform: scale(0.9);
+}
+
+.select-all-text {
+  width: 50px;
+  margin-left: 10px;
+  font-size: 15px;
+  line-height: 1.4;
+  color: rgb(51, 51, 51);
+  white-space: pre-line;
+}
+
+.action-buttons {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.operate-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 66px;
+  height: 40px;
+  padding: 0 5px;
+  margin-left: 5px;
+  border-radius: 4px;
+}
+
+.operate-btn-text {
+  font-size: 14px;
+  color: rgb(251, 253, 255);
+}
+
+.delete-btn {
+  background-color: #ff4445;
+}
 </style>

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

@@ -113,7 +113,7 @@ const handleSearch = () => {
 
 const loadTemplates = async () => {
   if (props.useOnline === '1') {
-    const { getReportTemplateList } = await import('@/api/task')
+    const { getReportTemplateList } = await import('@/api')
     const resp = await getReportTemplateList({ 
       inspectionNature: inspectionNature.value, 
       name: searchKeyword.value, 
@@ -129,19 +129,8 @@ const loadTemplates = async () => {
     checkExistingTemplates(templateList || [])
     dataSource.value = templateList || []
   } else {
-    const { services } = await import('@/models')
-    const result = await services.reportTemplate.getTemplatesByReportType(
-      ['100', '200', '300'], 
-      inspectionNature.value, 
-      searchKeyword.value
-    )
-    
-    const array = result?.filter(item => 
-      item.reportType != 800 && item.reportType != 600 && item.reportType != 700 && item.reportType != 500
-    )
-    
-    checkExistingTemplates(array || [])
-    dataSource.value = array || []
+    uni.showToast({ title: '离线模式暂不支持添加项目', icon: 'none' })
+    dataSource.value = []
   }
 }
 

+ 46 - 1
src/pages/equipment/detail/equipmentDetail.vue

@@ -115,6 +115,21 @@
         </view>
       </view>
     </view>
+
+    <view v-if="showCheckProject" class="popup-overlay" @click="closeCheckProjectPopup">
+      <view class="popup-content check-project-popup" @click.stop>
+        <view class="popup-header">
+          <text class="popup-title">添加检验项目</text>
+          <text class="popup-close" @click="closeCheckProjectPopup">✕</text>
+        </view>
+        <CheckProjectPopup
+          v-if="showCheckProject"
+          :propject-list="checkProjectList"
+          :select-templates="selectTemplates"
+          :use-online="useOnline"
+        />
+      </view>
+    </view>
   </view>
 </template>
 
@@ -140,6 +155,7 @@ import EquipmentInfo from './components/EquipmentInfo.vue'
 import InspectProject from './components/InspectProject.vue'
 import HistoryReport from './components/HistoryReport.vue'
 import OtherReport from './components/OtherReport.vue'
+import CheckProjectPopup from './components/checkProjectPopup/checkProject.vue'
 import { useConfigStore } from '@/store/config'
 import { TaskOrderFuncName, requestFunc } from '@/api/ApiRouter/taskOrder'
 
@@ -148,10 +164,14 @@ const dataSource = ref<any>({})
 const tabList = ref<any[]>([])
 const isInspectMode = ref(false)
 const showSelectUserPopup = ref(false)
+const showCheckProject = ref(false)
 const recheckUserGroupList = ref<any[]>([])
 const currentReckUser = ref<any>(null)
 const currentCheckItem = ref<any>(null)
 
+const checkProjectList = ref<any[][]>([])
+const selectTemplates = ref<Record<string, any[]>>({})
+
 let orderId = ''
 let orderItemId = ''
 let equipId = ''
@@ -408,7 +428,11 @@ const goBack = () => {
 
 // 显示添加项目弹窗
 const showCheckProjectPopup = () => {
-  uni.showToast({ title: '添加项目功能开发中', icon: 'none' })
+  showCheckProject.value = true
+}
+
+const closeCheckProjectPopup = () => {
+  showCheckProject.value = false
 }
 
 // 操作指导书关联
@@ -602,4 +626,25 @@ watch(
   color: #fff;
   background-color: rgb(47, 142, 255);
 }
+
+.check-project-popup {
+  width: 85%;
+  max-height: 80vh;
+  padding: 0;
+  overflow: hidden;
+}
+
+.popup-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 15px;
+  border-bottom: 1px solid #eee;
+}
+
+.popup-close {
+  font-size: 20px;
+  color: #999;
+}
 </style>

+ 494 - 0
src/pages/uploadFile/UploadFile.vue

@@ -0,0 +1,494 @@
+<route lang="json5" type="page">
+{
+  layout: 'default',
+  style: {
+    navigationBarTitleText: '附件上传',
+    navigationStyle: 'custom',
+  },
+}
+</route>
+
+<template>
+  <view class="upload-file-container">
+    <view class="navigate-view">
+      <view class="navigate-left" @click="handleBack">
+        <image class="back-icon" src="/static/images/back.png" />
+      </view>
+      <text class="navigate-title">{{ isEdit ? '附件上传' : '查看附件' }}</text>
+      <view class="navigate-right"></view>
+    </view>
+
+    <view v-if="readOnly" class="readonly-tip">
+      <text class="readonly-text">⚠️ 只读模式:只能查看文件,无法上传</text>
+    </view>
+
+    <scroll-view class="scroll-content" scroll-y>
+      <view class="file-section">
+        <view class="section-title">图片</view>
+        <view class="file-list">
+          <view 
+            v-for="(img, index) in imageList" 
+            :key="'img-' + index"
+            class="file-item"
+            @click="previewImage(index)"
+          >
+            <image class="file-preview" :src="img" mode="aspectFill" />
+            <view v-if="isEdit && !readOnly" class="delete-btn" @click.stop="deleteImage(index)">×</view>
+          </view>
+          <view v-if="isEdit && !readOnly" class="add-btn" @click="chooseImage">
+            <text class="add-icon">+</text>
+            <text class="add-text">添加图片</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="file-section">
+        <view class="section-title">视频</view>
+        <view class="file-list">
+          <view 
+            v-for="(video, index) in videoList" 
+            :key="'video-' + index"
+            class="file-item video-item"
+            @click="previewVideo(index)"
+          >
+            <video class="file-preview" :src="video" />
+            <view v-if="isEdit && !readOnly" class="delete-btn" @click.stop="deleteVideo(index)">×</view>
+          </view>
+          <view v-if="isEdit && !readOnly" class="add-btn" @click="chooseVideo">
+            <text class="add-icon">+</text>
+            <text class="add-text">添加视频</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="file-section">
+        <view class="section-title">附件</view>
+        <view class="file-list">
+          <view 
+            v-for="(file, index) in attachmentList" 
+            :key="'file-' + index"
+            class="file-item attachment-item"
+            @click="openFile(file)"
+          >
+            <text class="file-name">{{ getFileName(file) }}</text>
+            <view v-if="isEdit && !readOnly" class="delete-btn" @click.stop="deleteAttachment(index)">×</view>
+          </view>
+          <view v-if="isEdit && !readOnly" class="add-btn" @click="chooseFile">
+            <text class="add-icon">+</text>
+            <text class="add-text">添加附件</text>
+          </view>
+        </view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, onMounted } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+
+interface FileItem {
+  checkItemId: string
+  reportName: string
+  image?: string
+  video?: string
+  attachment?: string
+}
+
+const checkItemId = ref('')
+const fileList = ref<FileItem[]>([])
+const selectPropject = ref<FileItem | null>(null)
+const isEdit = ref(true)
+const useOnline = ref('1')
+const readOnly = ref(false)
+
+const imageList = ref<string[]>([])
+const videoList = ref<string[]>([])
+const attachmentList = ref<string[]>([])
+
+const isOnlineMode = computed(() => useOnline.value === '1')
+
+onLoad((options: any) => {
+  checkItemId.value = options.checkItemId || ''
+  isEdit.value = options.isEdit === 'true'
+  useOnline.value = options.useOnline || '1'
+  readOnly.value = options.readOnly === 'true'
+  
+  if (options.fileList) {
+    try {
+      const parsed = JSON.parse(decodeURIComponent(options.fileList))
+      fileList.value = parsed
+      
+      const currentItem = parsed.find((item: FileItem) => item.checkItemId === checkItemId.value)
+      if (currentItem) {
+        selectPropject.value = currentItem
+        loadFileData(currentItem)
+      }
+    } catch (error) {
+      console.error('解析fileList失败:', error)
+    }
+  }
+})
+
+const loadFileData = (item: FileItem) => {
+  const handleData = (data: string | undefined): string[] => {
+    if (data && data !== 'null') {
+      return data.split(',').map(value => {
+        if (value.match(/^https?:\/\//)) {
+          return value
+        }
+        return isOnlineMode.value ? buildFileUrl(value) : value
+      })
+    }
+    return []
+  }
+
+  imageList.value = handleData(item.image)
+  videoList.value = handleData(item.video)
+  attachmentList.value = handleData(item.attachment)
+}
+
+const buildFileUrl = (path: string): string => {
+  const baseUrl = 'https://api3.boot.jeecg.com'
+  return path.startsWith('http') ? path : `${baseUrl}${path}`
+}
+
+const chooseImage = () => {
+  if (readOnly.value) return
+  
+  uni.chooseImage({
+    count: 9,
+    success: (res) => {
+      imageList.value = [...imageList.value, ...res.tempFilePaths]
+    }
+  })
+}
+
+const deleteImage = (index: number) => {
+  imageList.value.splice(index, 1)
+}
+
+const previewImage = (index: number) => {
+  uni.previewImage({
+    urls: imageList.value,
+    current: index
+  })
+}
+
+const chooseVideo = () => {
+  if (readOnly.value) return
+  
+  uni.chooseVideo({
+    success: (res) => {
+      if (res.tempFilePath) {
+        videoList.value.push(res.tempFilePath)
+      }
+    }
+  })
+}
+
+const deleteVideo = (index: number) => {
+  videoList.value.splice(index, 1)
+}
+
+const previewVideo = (index: number) => {
+  uni.previewVideo({
+    urls: videoList.value,
+    current: index
+  })
+}
+
+const chooseFile = () => {
+  if (readOnly.value) return
+  
+  uni.chooseFile({
+    success: (res) => {
+      attachmentList.value.push(...res.tempFilePaths)
+    }
+  })
+}
+
+const deleteAttachment = (index: number) => {
+  attachmentList.value.splice(index, 1)
+}
+
+const openFile = (url: string) => {
+  uni.downloadFile({
+    url,
+    success: (res) => {
+      uni.openDocument({
+        filePath: res.tempFilePath,
+        fail: () => {
+          uni.showToast({ title: '无法打开此文件', icon: 'none' })
+        }
+      })
+    }
+  })
+}
+
+const getFileName = (url: string): string => {
+  const parts = url.split('/')
+  return parts[parts.length - 1] || '未知文件'
+}
+
+const handleBack = async () => {
+  if (!isEdit.value || readOnly.value) {
+    uni.navigateBack()
+    return
+  }
+
+  try {
+    uni.showLoading({ title: '正在保存...' })
+
+    if (isOnlineMode.value) {
+      await uploadOnline()
+    } else {
+      await saveOffline()
+    }
+  } catch (error) {
+    uni.hideLoading()
+    console.error('保存失败:', error)
+    uni.showToast({ title: '保存失败', icon: 'error' })
+  }
+}
+
+const uploadOnline = async () => {
+  const { reportUploadApi } = await import('@/api/task')
+
+  const uploadFile = async (filePath: string): Promise<string> => {
+    if (!filePath || filePath.startsWith('http')) {
+      return filePath
+    }
+
+    return new Promise((resolve, reject) => {
+      uni.uploadFile({
+        url: 'https://api3.boot.jeecg.com/infra/file/upload',
+        filePath,
+        name: 'file',
+        success: (res) => {
+          try {
+            const data = JSON.parse(res.data)
+            if (data.code === 0 && data.url) {
+              resolve(data.url)
+            } else {
+              resolve(filePath)
+            }
+          } catch {
+            resolve(filePath)
+          }
+        },
+        fail: () => {
+          resolve(filePath)
+        }
+      })
+    })
+  }
+
+  const processFiles = async (files: string[]): Promise<string> => {
+    if (!files || files.length === 0) return ''
+    
+    const uploadedUrls = []
+    for (const file of files) {
+      if (!file.startsWith('file://')) {
+        uploadedUrls.push(file)
+      } else {
+        const url = await uploadFile(file)
+        uploadedUrls.push(url)
+      }
+    }
+    return uploadedUrls.join(',')
+  }
+
+  const fileParams = {
+    checkItemId: checkItemId.value,
+    image: await processFiles(imageList.value),
+    video: await processFiles(videoList.value),
+    attachment: await processFiles(attachmentList.value),
+  }
+
+  const result = await reportUploadApi({ items: [fileParams] })
+  
+  if (result.code === 0) {
+    uni.hideLoading()
+    uni.showToast({ title: '文件上传成功', icon: 'success' })
+    setTimeout(() => {
+      uni.navigateBack()
+    }, 1500)
+  } else {
+    throw new Error(result.msg || '上传失败')
+  }
+}
+
+const saveOffline = async () => {
+  const dic = {
+    image: imageList.value.join(','),
+    video: videoList.value.join(','),
+    attachment: attachmentList.value.join(','),
+  }
+
+  const result = await uni.request({
+    url: 'https://api3.boot.jeecg.com/pressure/task-order-item/update',
+    method: 'PUT',
+    data: {
+      id: checkItemId.value,
+      ...dic
+    }
+  })
+
+  if (result.data?.code === 0) {
+    uni.hideLoading()
+    uni.showToast({ title: '离线数据保存成功', icon: 'success' })
+    setTimeout(() => {
+      uni.navigateBack()
+    }, 1000)
+  } else {
+    throw new Error('保存失败')
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.upload-file-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #f5f5f5;
+}
+
+.navigate-view {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  height: 44px;
+  padding: 0 15px;
+  background-color: #fff;
+  border-bottom: 1px solid #eee;
+}
+
+.navigate-left {
+  display: flex;
+  align-items: center;
+}
+
+.back-icon {
+  width: 20px;
+  height: 20px;
+}
+
+.navigate-title {
+  font-size: 15px;
+  font-weight: 500;
+  color: #333;
+}
+
+.navigate-right {
+  width: 20px;
+}
+
+.readonly-tip {
+  margin: 10px 15px;
+  padding: 12px;
+  background-color: #fff3cd;
+  border-radius: 6px;
+  border-left: 4px solid #ffc107;
+}
+
+.readonly-text {
+  color: #856404;
+  font-size: 14px;
+}
+
+.scroll-content {
+  flex: 1;
+  padding: 15px;
+}
+
+.file-section {
+  margin-bottom: 20px;
+  padding: 15px;
+  background-color: #fff;
+  border-radius: 8px;
+}
+
+.section-title {
+  margin-bottom: 10px;
+  font-size: 16px;
+  font-weight: 500;
+  color: #333;
+}
+
+.file-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.file-item {
+  position: relative;
+  width: 100px;
+  height: 100px;
+  border-radius: 8px;
+  overflow: hidden;
+  background-color: #f0f0f0;
+}
+
+.file-preview {
+  width: 100%;
+  height: 100%;
+}
+
+.delete-btn {
+  position: absolute;
+  top: 5px;
+  right: 5px;
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  background-color: rgba(0, 0, 0, 0.6);
+  color: #fff;
+  font-size: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.add-btn {
+  width: 100px;
+  height: 100px;
+  border: 2px dashed #ddd;
+  border-radius: 8px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.add-icon {
+  font-size: 24px;
+  color: #999;
+}
+
+.add-text {
+  font-size: 12px;
+  color: #999;
+  margin-top: 5px;
+}
+
+.attachment-item {
+  height: auto;
+  min-height: 50px;
+  padding: 10px;
+  display: flex;
+  align-items: center;
+}
+
+.file-name {
+  font-size: 14px;
+  color: #333;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>

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

@@ -21,6 +21,7 @@ interface NavigateToOptions {
        "/pages/unClaim/unClaimList" |
        "/pages/unitQuery/unitQuery" |
        "/pages/unitQuery/unitQueryDetail" |
+       "/pages/uploadFile/UploadFile" |
        "/pages/user/index" |
        "/pages/user/people" |
        "/pages/webview/generic-webview-v2" |

+ 100 - 0
src/utils/imageExtractor.ts

@@ -0,0 +1,100 @@
+export interface ImageInfo {
+  sheetName: string
+  imgX: number
+  imgY: number
+  imgHeight: number
+  imgWidth: number
+  imgUrl: string
+}
+
+export async function saveBase64ImageAsLocal(
+  base64Data: string,
+  userId: string,
+  orderItemId: string,
+  checkItemId: string,
+  imageName: string
+): Promise<string> {
+  try {
+    let imageExt = 'jpeg'
+    const mimeType = base64Data.match(/^data:image\/([^;]+);base64,/)
+    if (mimeType && mimeType[1]) {
+      imageExt = mimeType[1]
+    }
+
+    const cleanBase64 = base64Data.replace(/^data:image\/[^;]+;base64,/, '')
+
+    const relativePath = `user_${userId}/order_item_${orderItemId}/check_item_${checkItemId}/images/${imageName}.${imageExt}`
+
+    const fs = uni.getFileSystemManager()
+    const baseDir = uni.env.USER_DATA_PATH || ''
+    const fullPath = `${baseDir}/${relativePath}`
+
+    const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/'))
+    
+    try {
+      fs.mkdirSync(dirPath, true)
+    } catch (error) {
+      console.warn('目录创建失败或已存在:', error)
+    }
+
+    const buffer = uni.base64ToArrayBuffer(cleanBase64)
+    fs.writeFileSync(fullPath, buffer)
+
+    console.log(`图片保存成功: ${fullPath}`)
+    return fullPath
+  } catch (error) {
+    console.error('保存base64图片失败:', error)
+    throw error
+  }
+}
+
+export async function extractAndSaveImages(
+  images: ImageInfo[],
+  userId: string,
+  orderItemId: string,
+  checkItemId: string
+): Promise<ImageInfo[]> {
+  try {
+    if (!images || !Array.isArray(images)) {
+      return []
+    }
+
+    const processedImages: ImageInfo[] = []
+
+    for (let i = 0; i < images.length; i++) {
+      const image = images[i]
+
+      const processedImage: ImageInfo = {
+        sheetName: image.sheetName,
+        imgX: image.imgX,
+        imgY: image.imgY,
+        imgHeight: image.imgHeight,
+        imgWidth: image.imgWidth,
+        imgUrl: image.imgUrl
+      }
+
+      if (processedImage.imgUrl && processedImage.imgUrl.startsWith('data:image/')) {
+        try {
+          const localPath = await saveBase64ImageAsLocal(
+            processedImage.imgUrl,
+            userId,
+            orderItemId,
+            checkItemId,
+            `image_${i + 1}`
+          )
+          processedImage.imgUrl = localPath
+          console.log(`图片 ${i + 1} 保存成功`)
+        } catch (error) {
+          console.error(`图片 ${i + 1} 保存失败:`, error)
+        }
+      }
+
+      processedImages.push(processedImage)
+    }
+
+    return processedImages
+  } catch (error) {
+    console.error('批量处理图片失败:', error)
+    return images
+  }
+}