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

调起相机功能;特殊符号弹窗;回退文件重新提交功能

yangguanjin 4 дней назад
Родитель
Сommit
54c9a4c8f6

+ 241 - 2
src/components/SpreadDesigner/spreadDesigner.vue

@@ -42,6 +42,13 @@
           <button class="upload-image-btn" :disabled="!currentCell" @click="openCamera">
             拍摄图片
           </button>
+          <button
+            class="special-symbol-btn"
+            :disabled="!currentCell"
+            @click="openSpecialSymbolDialog"
+          >
+            特殊符号
+          </button>
           <button
             id="prevCellBtn"
             class="prev-cell-btn"
@@ -63,14 +70,43 @@
       <view class="floating-input-wrapper">
         <textarea
           id="floatingInput"
+          ref="floatingInputRef"
           v-model="floatingInputValue"
           placeholder="请选择单元格"
-          @input="backfillValueToCell"
+          @blur="updateFloatingInputCursor"
+          @click="updateFloatingInputCursor"
+          @focus="updateFloatingInputCursor"
+          @input="handleFloatingInput"
+          @keyup="updateFloatingInputCursor"
         />
         <button id="floatingClearBtn" class="clear-btn" @click="clearInput">×</button>
       </view>
     </view>
 
+    <view v-if="specialSymbolVisible" class="dialog-overlay" @click="specialSymbolVisible = false">
+      <view class="symbol-dialog-content" @click.stop>
+        <view class="symbol-dialog-header">
+          <text class="symbol-dialog-title">特殊符号</text>
+          <button class="symbol-dialog-close" @click="specialSymbolVisible = false">×</button>
+        </view>
+        <view class="symbol-groups">
+          <view v-for="group in specialSymbolGroups" :key="group.title" class="symbol-group">
+            <view class="symbol-group-title">{{ group.title }}</view>
+            <view class="symbol-grid">
+              <button
+                v-for="symbol in group.symbols"
+                :key="symbol"
+                class="symbol-item"
+                @click="insertSpecialSymbol(symbol)"
+              >
+                {{ symbol }}
+              </button>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
     <view v-if="dialogVisible" class="dialog-overlay" @click="dialogVisible = false">
       <view class="dialog-content" @click.stop>
         <view class="modal-message">{{ dialogMessage }}</view>
@@ -145,6 +181,61 @@ const currentFieldName = ref('-')
 const fieldNameMapping = ref([])
 const dialogVisible = ref(false)
 const dialogMessage = ref('')
+const floatingInputRef = ref(null)
+const floatingInputCursor = ref(0)
+const specialSymbolVisible = ref(false)
+const specialSymbolGroups = [
+  {
+    title: '数学符号',
+    symbols: [
+      '±',
+      '×',
+      '÷',
+      '≈',
+      '≠',
+      '≤',
+      '≥',
+      '∞',
+      '√',
+      '∑',
+      '∏',
+      'π',
+      'θ',
+      'α',
+      'β',
+      'γ',
+      'Δ',
+      '∠',
+      '⊥',
+      '∥',
+    ],
+  },
+  {
+    title: '物理单位',
+    symbols: [
+      '℃',
+      '℉',
+      'Ω',
+      'μ',
+      'm²',
+      'm³',
+      'N',
+      'Pa',
+      'kPa',
+      'MPa',
+      'J',
+      'W',
+      'kW',
+      'V',
+      'A',
+      'Hz',
+      'kg',
+      'mol',
+      'L',
+      'ppm',
+    ],
+  },
+]
 
 const designerConfig = computed(() => {
   const config = GC.Spread.Sheets.Designer.DefaultConfig
@@ -335,7 +426,10 @@ function deepMergeSchemaValue(target, source) {
       } else if (sourceValue !== undefined && sourceValue !== '') {
         const trimmedSourceValue = sourceValue.trim()
         // JSON字符串需要解析为对象或数组
-        if (typeof trimmedSourceValue === 'string' && (trimmedSourceValue.startsWith("{") || trimmedSourceValue.startsWith("["))) {
+        if (
+          typeof trimmedSourceValue === 'string' &&
+          (trimmedSourceValue.startsWith('{') || trimmedSourceValue.startsWith('['))
+        ) {
           result[key] = JSON.parse(trimmedSourceValue)
         } else {
           result[key] = trimmedSourceValue
@@ -652,6 +746,7 @@ function listenActiveSheetChange(spreadInstance) {
 }
 
 function updateFieldNameDisplay(fieldName) {
+  // console.log('fieldName.....', fieldName)
   let displayName = '-'
   if (fieldNameMapping.value && Array.isArray(fieldNameMapping.value)) {
     const fieldMapping = fieldNameMapping.value.find((item) => item.field === fieldName)
@@ -664,9 +759,51 @@ function updateFieldNameDisplay(fieldName) {
 
 function showFloatingInput() {
   floatingInputValue.value = currentCellInfo.value.value
+  floatingInputCursor.value = floatingInputValue.value.length
   floatingInputVisible.value = true
 }
 
+function getFloatingInputElement(event) {
+  return (
+    event?.target ||
+    floatingInputRef.value?.$el ||
+    floatingInputRef.value ||
+    document.getElementById('floatingInput')
+  )
+}
+
+function updateFloatingInputCursor(event) {
+  const inputElement = getFloatingInputElement(event)
+  const cursor = event?.detail?.cursor ?? inputElement?.selectionStart
+  if (typeof cursor === 'number') {
+    floatingInputCursor.value = cursor
+  }
+}
+
+function handleFloatingInput(event) {
+  updateFloatingInputCursor(event)
+  backfillValueToCell()
+}
+
+function openSpecialSymbolDialog() {
+  updateFloatingInputCursor()
+  specialSymbolVisible.value = true
+}
+
+function insertSpecialSymbol(symbol) {
+  const cursor = Math.min(floatingInputCursor.value, floatingInputValue.value.length)
+  floatingInputValue.value = `${floatingInputValue.value.slice(0, cursor)}${symbol}${floatingInputValue.value.slice(cursor)}`
+  floatingInputCursor.value = cursor + symbol.length
+  specialSymbolVisible.value = false
+  backfillValueToCell()
+
+  nextTick(() => {
+    const inputElement = getFloatingInputElement()
+    inputElement?.focus?.()
+    inputElement?.setSelectionRange?.(floatingInputCursor.value, floatingInputCursor.value)
+  })
+}
+
 function backfillValueToCell() {
   if (!currentCell.value || !currentCellInfo.value) return
   const { sheet, row, col } = currentCell.value
@@ -675,6 +812,9 @@ function backfillValueToCell() {
 
 function clearInput() {
   floatingInputValue.value = ''
+  if (!currentCell.value || !currentCellInfo.value) return
+  const { sheet, row, col } = currentCell.value
+  sheet.setValue(row, col, '')
 }
 
 function findNextEmptyBoundCell(direction = 'next') {
@@ -1290,6 +1430,35 @@ defineExpose({
   opacity: 0.7;
 }
 
+.special-symbol-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 60px;
+  height: 35px;
+  padding: 6px 12px;
+  font-size: 12px;
+  font-weight: 500;
+  color: white;
+  cursor: pointer;
+  background: #e6a23c;
+  border: none;
+  border-radius: 4px;
+  transition: all 0.2s ease;
+}
+
+.special-symbol-btn:hover:not(:disabled) {
+  background: #c87f0a;
+  transform: translateY(-1px);
+}
+
+.special-symbol-btn:disabled {
+  color: #999999;
+  cursor: not-allowed;
+  background: #cccccc;
+  opacity: 0.7;
+}
+
 .floating-input-wrapper {
   position: relative;
   width: 100%;
@@ -1494,6 +1663,76 @@ defineExpose({
   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
 }
 
+.symbol-dialog-content {
+  width: min(92vw, 560px);
+  max-height: 70vh;
+  padding: 16px;
+  overflow-y: auto;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.symbol-dialog-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+
+.symbol-dialog-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+}
+
+.symbol-dialog-close {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 28px;
+  height: 28px;
+  font-size: 18px;
+  color: #666;
+  cursor: pointer;
+  background: #f5f5f5;
+  border: none;
+  border-radius: 14px;
+}
+
+.symbol-group + .symbol-group {
+  margin-top: 14px;
+}
+
+.symbol-group-title {
+  margin-bottom: 8px;
+  font-size: 13px;
+  font-weight: 600;
+  color: #666;
+}
+
+.symbol-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));
+  gap: 8px;
+}
+
+.symbol-item {
+  height: 36px;
+  font-size: 15px;
+  color: #333;
+  cursor: pointer;
+  background: #f8f9fa;
+  border: 1px solid #dee2e6;
+  border-radius: 4px;
+}
+
+.symbol-item:hover {
+  color: #007aff;
+  background: #eef6ff;
+  border-color: #007aff;
+}
+
 .modal-message {
   font-size: 16px;
   line-height: 1.5;

+ 240 - 2
src/components/SpreadDesigner/spreadDesignerGeneric.vue

@@ -36,6 +36,21 @@
       <view class="floating-toolbar">
         <view class="floating-field-name-display">{{ currentFieldName }}</view>
         <view class="button-group">
+          <button
+            v-if="showTakePhotoButton"
+            class="upload-image-btn"
+            :disabled="!currentCell"
+            @click="openCamera"
+          >
+            拍摄图片
+          </button>
+          <button
+            class="special-symbol-btn"
+            :disabled="!currentCell"
+            @click="openSpecialSymbolDialog"
+          >
+            特殊符号
+          </button>
           <button
             id="prevCellBtn"
             class="prev-cell-btn"
@@ -57,14 +72,47 @@
       <view class="floating-input-wrapper">
         <textarea
           id="floatingInput"
+          ref="floatingInputRef"
           v-model="floatingInputValue"
           placeholder="请选择单元格"
-          @input="backfillValueToCell"
+          @blur="updateFloatingInputCursor"
+          @click="updateFloatingInputCursor"
+          @focus="updateFloatingInputCursor"
+          @input="handleFloatingInput"
+          @keyup="updateFloatingInputCursor"
         />
         <button id="floatingClearBtn" class="clear-btn" @click="clearInput">×</button>
       </view>
     </view>
 
+    <view
+      v-if="specialSymbolVisible"
+      class="dialog-overlay"
+      @click="specialSymbolVisible = false"
+    >
+      <view class="symbol-dialog-content" @click.stop>
+        <view class="symbol-dialog-header">
+          <text class="symbol-dialog-title">特殊符号</text>
+          <button class="symbol-dialog-close" @click="specialSymbolVisible = false">×</button>
+        </view>
+        <view class="symbol-groups">
+          <view v-for="group in specialSymbolGroups" :key="group.title" class="symbol-group">
+            <view class="symbol-group-title">{{ group.title }}</view>
+            <view class="symbol-grid">
+              <button
+                v-for="symbol in group.symbols"
+                :key="symbol"
+                class="symbol-item"
+                @click="insertSpecialSymbol(symbol)"
+              >
+                {{ symbol }}
+              </button>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
     <view v-if="dialogVisible" class="dialog-overlay" @click="dialogVisible = false">
       <view class="dialog-content" @click.stop>
         <view class="modal-message">{{ dialogMessage }}</view>
@@ -126,8 +174,22 @@ const keyboardHeight = ref(0)
 const selectedPicture = ref(null)
 const currentFieldName = ref('-')
 const fieldNameMapping = ref([])
+const showTakePhotoButton = ref(false)
 const dialogVisible = ref(false)
 const dialogMessage = ref('')
+const floatingInputRef = ref(null)
+const floatingInputCursor = ref(0)
+const specialSymbolVisible = ref(false)
+const specialSymbolGroups = [
+  {
+    title: '数学符号',
+    symbols: ['±', '×', '÷', '≈', '≠', '≤', '≥', '∞', '√', '∑', '∏', 'π', 'θ', 'α', 'β', 'γ', 'Δ', '∠', '⊥', '∥'],
+  },
+  {
+    title: '物理单位',
+    symbols: ['℃', '℉', 'Ω', 'μ', 'm²', 'm³', 'N', 'Pa', 'kPa', 'MPa', 'J', 'W', 'kW', 'V', 'A', 'Hz', 'kg', 'mol', 'L', 'ppm'],
+  },
+]
 
 const designerConfig = computed(() => {
   const config = GC.Spread.Sheets.Designer.DefaultConfig
@@ -331,7 +393,10 @@ function deepMergeSchemaValue(target, source) {
       } else if (sourceValue !== undefined && sourceValue !== '') {
         const trimmedSourceValue = sourceValue.trim()
         // JSON字符串需要解析为对象或数组
-        if (typeof trimmedSourceValue === 'string' && (trimmedSourceValue.startsWith("{") || trimmedSourceValue.startsWith("["))) {
+        if (
+          typeof trimmedSourceValue === 'string' &&
+          (trimmedSourceValue.startsWith('{') || trimmedSourceValue.startsWith('['))
+        ) {
           result[key] = JSON.parse(trimmedSourceValue)
         } else {
           result[key] = trimmedSourceValue
@@ -636,9 +701,46 @@ function updateFieldNameDisplay(fieldName) {
 
 function showFloatingInput() {
   floatingInputValue.value = currentCellInfo.value.value
+  floatingInputCursor.value = floatingInputValue.value.length
   floatingInputVisible.value = true
 }
 
+function getFloatingInputElement(event) {
+  return event?.target || floatingInputRef.value?.$el || floatingInputRef.value || document.getElementById('floatingInput')
+}
+
+function updateFloatingInputCursor(event) {
+  const inputElement = getFloatingInputElement(event)
+  const cursor = event?.detail?.cursor ?? inputElement?.selectionStart
+  if (typeof cursor === 'number') {
+    floatingInputCursor.value = cursor
+  }
+}
+
+function handleFloatingInput(event) {
+  updateFloatingInputCursor(event)
+  backfillValueToCell()
+}
+
+function openSpecialSymbolDialog() {
+  updateFloatingInputCursor()
+  specialSymbolVisible.value = true
+}
+
+function insertSpecialSymbol(symbol) {
+  const cursor = Math.min(floatingInputCursor.value, floatingInputValue.value.length)
+  floatingInputValue.value = `${floatingInputValue.value.slice(0, cursor)}${symbol}${floatingInputValue.value.slice(cursor)}`
+  floatingInputCursor.value = cursor + symbol.length
+  specialSymbolVisible.value = false
+  backfillValueToCell()
+
+  nextTick(() => {
+    const inputElement = getFloatingInputElement()
+    inputElement?.focus?.()
+    inputElement?.setSelectionRange?.(floatingInputCursor.value, floatingInputCursor.value)
+  })
+}
+
 function backfillValueToCell() {
   if (!currentCell.value || !currentCellInfo.value) return
   const { sheet, row, col } = currentCell.value
@@ -647,6 +749,9 @@ function backfillValueToCell() {
 
 function clearInput() {
   floatingInputValue.value = ''
+  if (!currentCell.value || !currentCellInfo.value) return
+  const { sheet, row, col } = currentCell.value
+  sheet.setValue(row, col, '')
 }
 
 function findNextEmptyBoundCell(direction = 'next') {
@@ -852,6 +957,7 @@ function handleWorkbookInitialized(spreadInstance) {
 
 function initGenericEditorUI(uiConfig) {
   navTitle.value = uiConfig.title
+  showTakePhotoButton.value = !!uiConfig.showTakePhotoButton
 
   navButtons.value = []
   const cancelBtn = {
@@ -1032,6 +1138,10 @@ function cancelEdit() {
   emit('cancel')
 }
 
+function openCamera() {
+  emit('openCamera')
+}
+
 function goBack() {
   uni.navigateBack({
     delta: 1,
@@ -1205,6 +1315,35 @@ defineExpose({
   border-color: #e6a23c;
 }
 
+.special-symbol-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 60px;
+  height: 35px;
+  padding: 6px 12px;
+  font-size: 12px;
+  font-weight: 500;
+  color: white;
+  cursor: pointer;
+  background: #e6a23c;
+  border: none;
+  border-radius: 4px;
+  transition: all 0.2s ease;
+}
+
+.special-symbol-btn:hover:not(:disabled) {
+  background: #c87f0a;
+  transform: translateY(-1px);
+}
+
+.special-symbol-btn:disabled {
+  color: #999999;
+  cursor: not-allowed;
+  background: #cccccc;
+  opacity: 0.7;
+}
+
 .floating-input-wrapper {
   position: relative;
   width: 100%;
@@ -1308,6 +1447,35 @@ defineExpose({
   align-items: center;
 }
 
+.upload-image-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 60px;
+  height: 35px;
+  padding: 6px 12px;
+  font-size: 12px;
+  font-weight: 500;
+  color: white;
+  cursor: pointer;
+  background: #28a745;
+  border: none;
+  border-radius: 4px;
+  transition: all 0.2s ease;
+}
+
+.upload-image-btn:hover:not(:disabled) {
+  background: #1e7e34;
+  transform: translateY(-1px);
+}
+
+.upload-image-btn:disabled {
+  color: #999999;
+  cursor: not-allowed;
+  background: #cccccc;
+  opacity: 0.7;
+}
+
 .prev-cell-btn {
   position: relative;
   display: flex;
@@ -1409,6 +1577,76 @@ defineExpose({
   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
 }
 
+.symbol-dialog-content {
+  width: min(92vw, 560px);
+  max-height: 70vh;
+  padding: 16px;
+  overflow-y: auto;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.symbol-dialog-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+
+.symbol-dialog-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+}
+
+.symbol-dialog-close {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 28px;
+  height: 28px;
+  font-size: 18px;
+  color: #666;
+  cursor: pointer;
+  background: #f5f5f5;
+  border: none;
+  border-radius: 14px;
+}
+
+.symbol-group + .symbol-group {
+  margin-top: 14px;
+}
+
+.symbol-group-title {
+  margin-bottom: 8px;
+  font-size: 13px;
+  font-weight: 600;
+  color: #666;
+}
+
+.symbol-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));
+  gap: 8px;
+}
+
+.symbol-item {
+  height: 36px;
+  font-size: 15px;
+  color: #333;
+  cursor: pointer;
+  background: #f8f9fa;
+  border: 1px solid #dee2e6;
+  border-radius: 4px;
+}
+
+.symbol-item:hover {
+  color: #007aff;
+  background: #eef6ff;
+  border-color: #007aff;
+}
+
 .modal-message {
   font-size: 16px;
   line-height: 1.5;

+ 92 - 2
src/pages/editor/equipCheckRecordEditor.vue

@@ -29,7 +29,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { ref, onUnmounted } from 'vue'
 import SpreadDesigner from '@/components/SpreadDesigner/spreadDesigner.vue'
 import { onLoad } from '@dcloudio/uni-app'
 import { getCheckerEquipmentDetailById, getDynamicTbVal, saveDynamicTbVal } from '@/api/task'
@@ -303,8 +303,88 @@ const handleCancel = () => {
   uni.navigateBack()
 }
 
+// 检测RN WebView环境
+const isRNEnv = () => {
+  return !!(window as any).ReactNativeWebView
+}
+
+// 注册RN拍照回调
+const registerRNTakePhotoCallback = () => {
+  ;(window as any).onTakePhotoFromRN = handleTakePhotoFromRN
+}
+
+// 处理RN回传的拍照数据
+/**
+ * {
+      canceled: false,
+      base64: readResult.data,
+      name: photo.fileName || 'photo.jpg',
+      type: photo.mimeType,
+      size: photo.fileSize,
+      uri: photo.uri,
+      width: photo.width,
+      height: photo.height,
+    }
+ */
+const handleTakePhotoFromRN = async (photoData: any) => {
+  if (photoData.canceled) return
+  if (photoData.error) {
+    uni.showToast({ title: photoData.error, icon: 'none' })
+    return
+  }
+  console.log('RN拍照数据:', photoData)
+}
+
+// 调起RN相机
+const takePhotoRN = () => {
+  ;(window as any).ReactNativeWebView.postMessage(
+    JSON.stringify({
+      type: 'takePhoto',
+    }),
+  )
+}
+
+// 非RN环境选择本地图片
+const pickPhotoH5 = () => {
+  const input = document.createElement('input')
+  input.type = 'file'
+  input.accept = 'image/*'
+  input.style.display = 'none'
+  input.onchange = async (e: Event) => {
+    const file = (e.target as HTMLInputElement).files?.[0]
+    if (!file) {
+      document.body.removeChild(input)
+      return
+    }
+
+    const reader = new FileReader()
+    reader.onload = async () => {
+      const base64 = (reader.result as string).split(',')[1]
+      await handleTakePhotoFromRN({
+        canceled: false,
+        base64,
+        name: file.name,
+        type: file.type,
+        size: file.size,
+      })
+      document.body.removeChild(input)
+    }
+    reader.onerror = () => {
+      uni.showToast({ title: '图片读取失败', icon: 'none' })
+      document.body.removeChild(input)
+    }
+    reader.readAsDataURL(file)
+  }
+  document.body.appendChild(input)
+  input.click()
+}
+
 const handleOpenCamera = () => {
-  console.log('打开相机')
+  if (isRNEnv()) {
+    takePhotoRN()
+    return
+  }
+  pickPhotoH5()
 }
 
 const handleSelectedReport = async (data: any) => {
@@ -331,6 +411,10 @@ onLoad((options: any) => {
   equipCode = options.equipCode || ''
   reportUrlParam = options.reportUrl || ''
 
+  if (isRNEnv()) {
+    registerRNTakePhotoCallback()
+  }
+
   if (!checkItemId) {
     console.error('checkItemId 为空')
     uni.navigateBack()
@@ -339,4 +423,10 @@ onLoad((options: any) => {
 
   init()
 })
+
+onUnmounted(() => {
+  if ((window as any).onTakePhotoFromRN) {
+    delete (window as any).onTakePhotoFromRN
+  }
+})
 </script>

+ 97 - 0
src/pages/editor/workInstructionEditor.vue

@@ -19,6 +19,7 @@
       :templateBlob="templateBlob"
       @save="handleSave"
       @cancel="handleCancel"
+      @openCamera="handleOpenCamera"
     >
       <template #navBar>
         <ReportNavBar>
@@ -100,6 +101,7 @@ const businessConfig = ref({
     saveButtonText: '保存',
     cancelButtonText: '取消',
     showAdditionalToolbar: true,
+    showTakePhotoButton: true,
   },
 })
 const equipType = useConfigStore().getEquipType()
@@ -123,6 +125,11 @@ onLoad((options: any) => {
   orderId = options.orderId
   mode = options.mode || 'edit'
   workInstructionId = options.refId || ''
+
+  if (isRNEnv()) {
+    registerRNTakePhotoCallback()
+  }
+
   init()
 })
 
@@ -230,6 +237,90 @@ const handleCancel = () => {
 
 const designerRef = ref<any>(null)
 
+// 检测RN WebView环境
+const isRNEnv = () => {
+  return !!(window as any).ReactNativeWebView
+}
+
+// 注册RN拍照回调
+const registerRNTakePhotoCallback = () => {
+  ;(window as any).onTakePhotoFromRN = handleTakePhotoFromRN
+}
+
+// 处理RN回传的拍照数据
+/**
+ * {
+      canceled: false,
+      base64: readResult.data,
+      name: photo.fileName || 'photo.jpg',
+      type: photo.mimeType,
+      size: photo.fileSize,
+      uri: photo.uri,
+      width: photo.width,
+      height: photo.height,
+    }
+ */
+const handleTakePhotoFromRN = async (photoData: any) => {
+  if (photoData.canceled) return
+  if (photoData.error) {
+    uni.showToast({ title: photoData.error, icon: 'none' })
+    return
+  }
+  console.log('RN拍照数据:', photoData)
+}
+
+// 调起RN相机
+const takePhotoRN = () => {
+  ;(window as any).ReactNativeWebView.postMessage(
+    JSON.stringify({
+      type: 'takePhoto',
+    }),
+  )
+}
+
+// 非RN环境选择本地图片
+const pickPhotoH5 = () => {
+  const input = document.createElement('input')
+  input.type = 'file'
+  input.accept = 'image/*'
+  input.style.display = 'none'
+  input.onchange = async (e: Event) => {
+    const file = (e.target as HTMLInputElement).files?.[0]
+    if (!file) {
+      document.body.removeChild(input)
+      return
+    }
+
+    const reader = new FileReader()
+    reader.onload = async () => {
+      const base64 = (reader.result as string).split(',')[1]
+      await handleTakePhotoFromRN({
+        canceled: false,
+        base64,
+        name: file.name,
+        type: file.type,
+        size: file.size,
+      })
+      document.body.removeChild(input)
+    }
+    reader.onerror = () => {
+      uni.showToast({ title: '图片读取失败', icon: 'none' })
+      document.body.removeChild(input)
+    }
+    reader.readAsDataURL(file)
+  }
+  document.body.appendChild(input)
+  input.click()
+}
+
+const handleOpenCamera = () => {
+  if (isRNEnv()) {
+    takePhotoRN()
+    return
+  }
+  pickPhotoH5()
+}
+
 // 打开审核人选择弹窗
 const openAuditorDialog = async () => {
   auditorVisible.value = true
@@ -289,6 +380,12 @@ const confirmSubmit = async () => {
     uni.hideLoading()
   }
 }
+
+onUnmounted(() => {
+  if ((window as any).onTakePhotoFromRN) {
+    delete (window as any).onTakePhotoFromRN
+  }
+})
 </script>
 
 <style scoped>

+ 46 - 9
src/pages/equipment/detail/components/OtherReport.vue

@@ -29,6 +29,9 @@
               <view v-if="canEdit(item)" class="action-btn edit-btn" @click.stop="handleEdit(item)">
                 <text>修改</text>
               </view>
+              <view v-if="canRecommit(item)" class="action-btn edit-btn" @click.stop="handleRecommit(item)">
+                <text>重新提交</text>
+              </view>
               <view class="action-btn view-btn" @click.stop="handleViewDetail(item)">
                 <text>查看详情</text>
               </view>
@@ -205,8 +208,8 @@ const showDelReportPopup = () => {
 
   const invalidItems = selectedProjects.value.filter(
     (item) =>
-      (item.reportType === PressureReportType.WORKINSTRUCTION && item.status !== 0) ||
-      (item.reportType === PressureReportType.INSPECTIONPLAN && item.status !== 0),
+      (item.reportType === PressureReportType.WORKINSTRUCTION && [100, 200].includes(item.status)) ||
+      (item.reportType === PressureReportType.INSPECTIONPLAN && [100, 200].includes(item.status)),
   )
   if (invalidItems.length) {
     uni.showToast({ title: '只能作废待提交或者被退回的操作指导书和检验方案', icon: 'error' })
@@ -280,11 +283,17 @@ const initSelected = () => {
 }
 
 const showStatusTag = (item: ReportItem): boolean => {
+  // return (
+  //   ([PressureReportType.WORKINSTRUCTION].includes(item.reportType) &&
+  //     item.status != null &&
+  //     item.status !== 200) ||
+  //   (item.reportType === PressureReportType.INSPECTIONPLAN && item.status !== 300)
+  // )
   return (
-    ([PressureReportType.WORKINSTRUCTION].includes(item.reportType) &&
-      item.status != null &&
-      item.status !== 200) ||
-    (item.reportType === PressureReportType.INSPECTIONPLAN && item.status !== 300)
+    item.status != null &&
+    [PressureReportType.WORKINSTRUCTION, PressureReportType.INSPECTIONPLAN].includes(
+      item.reportType,
+    )
   )
 }
 
@@ -296,9 +305,9 @@ const getStatusText = (item: ReportItem): string => {
       case 100:
         return '待批准'
       case 200:
-        return '待批准'
+        return '已通过'
       case 300:
-        return '退回'
+        return '退回'
       default:
         return ''
     }
@@ -311,7 +320,7 @@ const getStatusText = (item: ReportItem): string => {
       case 200:
         return '已通过'
       case 300:
-        return '退回'
+        return '退回'
       default:
         return ''
     }
@@ -329,6 +338,16 @@ const canEdit = (item: ReportItem): boolean => {
   return false
 }
 
+const canRecommit = (item: ReportItem): boolean => {
+  const { reportType, status } = item
+  if (reportType === PressureReportType.WORKINSTRUCTION) {
+    return [300].includes(status)
+  } else if (reportType === PressureReportType.INSPECTIONPLAN) {
+    return [300].includes(status)
+  }
+  return false
+}
+
 const handleEdit = (item: ReportItem) => {
   switch (item.reportType) {
     case PressureReportType.MAINQUESTION:
@@ -352,6 +371,24 @@ const handleEdit = (item: ReportItem) => {
   }
 }
 
+const handleRecommit = (item: ReportItem) => {
+  switch (item.reportType) {
+    case PressureReportType.WORKINSTRUCTION:
+      uni.navigateTo({
+        url: `/pages/editor/workInstructionEditor?templateId=${item?.templateId}&refId=${item?.id}`,
+      })
+      break
+    case PressureReportType.INSPECTIONPLAN:
+      uni.navigateTo({
+        url: `/pages/editor/inspectionPlanEditor?templateId=${item?.templateId}&refId=${item?.id}`,
+      })
+      break
+    default:
+      uni.showToast({ title: '不支持编辑该报告类型', icon: 'error' })
+      break
+  }
+}
+
 const handleViewDetail = async (item: ReportItem) => {
   switch (item.reportType) {
     case PressureReportType.WORKINSTRUCTION: