wlzboy
2026-02-01 0ffdf00009b0bede0859fa33deddefb55c075a7b
app/pagesTask/create-emergency.vue
@@ -8,8 +8,149 @@
      <view class="smart-parse-btn" @click="showSmartParsePopup">
        <uni-icons type="compose" size="20" color="#007AFF"></uni-icons>
        <text>智能识别</text>
      </view>
      <view class="multi-photo-ocr-btn" @click="showMultiPhotoOCRPopup">
        <uni-icons type="images" size="20" color="#FF6B00"></uni-icons>
        <text>拍照识别</text>
      </view>
    </view>
    <!-- 多图片拍照识别弹窗 -->
    <uni-popup ref="multiPhotoOCRPopup" type="bottom" :safe-area="true">
      <view class="photo-ocr-popup">
        <view class="popup-header">
          <view class="popup-title">
            <uni-icons type="images" size="22" color="#FF6B00"></uni-icons>
            <text>拍照识别 - 知情同意书</text>
          </view>
          <view class="popup-close" @click="closeMultiPhotoOCRPopup">
            <uni-icons type="closeempty" size="24" color="#333"></uni-icons>
          </view>
        </view>
        <view class="ocr-content">
          <view class="ocr-tip">
            <uni-icons type="info" size="18" color="#FF6B00"></uni-icons>
            <text>请分别上传知情同意书的第一页和第二页,系统将自动识别并填充到表单中</text>
          </view>
          <!-- 知情同意书第一页 -->
          <view class="page-upload-section first-page">
            <view class="page-header">
              <view class="page-title">
                <view class="page-badge">
                  <uni-icons type="compose" size="18" color="#52c41a"></uni-icons>
                  <text class="page-number">1</text>
                </view>
                <view class="page-info">
                  <text class="page-main-title">知情同意书第一页</text>
                  <text class="page-sub-title">患者基本信息页</text>
                </view>
              </view>
              <!-- 只有未上传时显示+号 -->
              <view class="upload-btn green" @click="selectPage1Images" v-if="!page1Image">
                <uni-icons type="plusempty" size="30" color="#52c41a"></uni-icons>
              </view>
            </view>
            <!-- 识别字段提示 -->
            <view class="field-hint" v-if="!page1Image">
              <text>识别字段:患者姓名、性别、年龄、身份证号、诊断、转运费用、行程、开始时间、结束时间、家属签名</text>
            </view>
            <!-- 单图预览 -->
            <view class="single-image-container" v-if="page1Image">
              <image :src="page1Image" mode="aspectFit" class="preview-image"></image>
              <view class="delete-btn" @click="deletePage1Image">
                <uni-icons type="closeempty" size="20" color="#fff"></uni-icons>
              </view>
              <view class="image-status">
                <uni-icons type="checkmarkempty" size="16" color="#fff"></uni-icons>
                <text>已上传</text>
              </view>
            </view>
            <!-- 第一页识别结果 -->
            <view class="recognition-result" v-if="Object.keys(page1Fields).length > 0">
              <view class="result-title">
                <uni-icons type="checkmarkempty" size="18" color="#52c41a"></uni-icons>
                <text>识别结果</text>
                <text class="result-count">(共{{ Object.keys(page1Fields).length }}个字段)</text>
              </view>
              <view class="field-list">
                <view class="field-item" v-for="(value, key) in page1Fields" :key="key">
                  <text class="field-name">{{ key }}</text>
                  <text class="field-value">{{ value || '未识别' }}</text>
                </view>
              </view>
            </view>
          </view>
          <!-- 知情同意书第二页 -->
          <view class="page-upload-section second-page">
            <view class="page-header">
              <view class="page-title">
                <view class="page-badge">
                  <uni-icons type="compose" size="18" color="#FF6B00"></uni-icons>
                  <text class="page-number">2</text>
                </view>
                <view class="page-info">
                  <text class="page-main-title">知情同意书第二页</text>
                  <text class="page-sub-title">签名与联系信息页</text>
                </view>
              </view>
              <!-- 只有未上传时显示+号 -->
              <view class="upload-btn orange" @click="selectPage2Images" v-if="!page2Image">
                <uni-icons type="plusempty" size="30" color="#FF6B00"></uni-icons>
              </view>
            </view>
            <!-- 识别字段提示 -->
            <view class="field-hint" v-if="!page2Image">
              <text>识别字段:患者签名、签字人身份证号、签字日期、联系电话、签字人关系</text>
            </view>
            <!-- 单图预览 -->
            <view class="single-image-container" v-if="page2Image">
              <image :src="page2Image" mode="aspectFit" class="preview-image"></image>
              <view class="delete-btn" @click="deletePage2Image">
                <uni-icons type="closeempty" size="20" color="#fff"></uni-icons>
              </view>
              <view class="image-status">
                <uni-icons type="checkmarkempty" size="16" color="#fff"></uni-icons>
                <text>已上传</text>
              </view>
            </view>
            <!-- 第二页识别结果 -->
            <view class="recognition-result" v-if="Object.keys(page2Fields).length > 0">
              <view class="result-title">
                <uni-icons type="checkmarkempty" size="18" color="#FF6B00"></uni-icons>
                <text>识别结果</text>
                <text class="result-count">(共{{ Object.keys(page2Fields).length }}个字段)</text>
              </view>
              <view class="field-list">
                <view class="field-item" v-for="(value, key) in page2Fields" :key="key">
                  <text class="field-name">{{ key }}</text>
                  <text class="field-value">{{ value || '未识别' }}</text>
                </view>
              </view>
            </view>
          </view>
        </view>
        <view class="popup-footer">
          <button class="cancel-btn" @click="closeMultiPhotoOCRPopup">取消</button>
          <button
            class="confirm-btn"
            @click="applyOcrResult"
            :disabled="Object.keys(page1Fields).length === 0 && Object.keys(page2Fields).length === 0"
          >
            应用到表单
          </button>
        </view>
      </view>
    </uni-popup>
    
    <view class="form-section">
      <view class="form-item">
@@ -65,7 +206,7 @@
        label="执行任务人员"
        :required="false"
        :auto-add-current-user="true"
        :current-user-removable="false"
        :current-user-removable="true"
        :branch-dept-ids="allOrganizationIds"
        @change="onStaffChange"
      />
@@ -266,15 +407,18 @@
import { mapState } from 'vuex'
import uniDatetimePicker from '@/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue'
import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue'
import { addTask } from "@/api/task"
import { addTask, checkTaskDuplicate } from "@/api/task"
import { listAvailableVehicles, getUserBoundVehicle } from "@/api/vehicle"
import { searchHospitals, searchHospitalsByDeptRegion } from "@/api/hospital"
import { searchHospitals, searchHospitalsByDeptRegion, searchHospitalsByKeywords } from "@/api/hospital"
import DepartureSelector from './components/DepartureSelector.vue'
import { calculateTianDiTuDistance } from "@/api/map"
import { listBranchUsers } from "@/api/system/user"
import { searchIcd10 } from "@/api/icd10"
import { calculateTransferPrice } from "@/api/price"
import { checkVehicleActiveTasks } from "@/api/task"
import { recognizeImage, batchRecognizeImages, DEFAULT_TRANSFER_ITEM_NAMES } from "@/api/ocr"
import config from '@/config'
import { getToken } from '@/utils/auth'
import { getDicts } from "@/api/dict"
import { getServiceOrdAreaTypes, getServiceOrderTypes } from "@/api/dictionary"
@@ -357,7 +501,18 @@
      loading: false,
      // 智能识别相关
      rawText: '',
      parseLoading: false
      parseLoading: false,
      // 多图片拍照识别相关
      multiOcrImages: [],
      multiOcrLoading: false,
      // 分页OCR识别相关
      currentOcrPage: 1, // 当前上传的页码:1=第一页,2=第二页
      page1Image: '', // 第一页图片(单图)
      page2Image: '', // 第二页图片(单图)
      page1Fields: {}, // 第一页识别结果
      page2Fields: {}, // 第二页识别结果
      // 附件临时存储(OCR图片)
      pendingAttachments: [] // 待上传的附件列表 [{ filePath: '', category: '1' }]
    }
  },
  computed: {
@@ -567,11 +722,7 @@
      console.log('转出医院变化:', hospitalData)
      // 组件已经通过 v-model 更新了 taskForm.hospitalOut
      
      // 如果选择的是"家中",自动设置科室为"其它"
      if (hospitalData.name === '家中') {
        this.taskForm.hospitalOut.department = '其它'
        this.taskForm.hospitalOut.departmentId = null
      }
      // 科室的设置由 DepartmentSelector 组件自动处理(通过 isHome 属性)
      
      // 如果转入地址已填写,自动计算距离
      if (this.taskForm.hospitalIn.address) {
@@ -597,11 +748,7 @@
      console.log('转入医院变化:', hospitalData)
      // 组件已经通过 v-model 更新了 taskForm.hospitalIn
      
      // 如果选择的是"家中",自动设置科室为"其它"
      if (hospitalData.name === '家中') {
        this.taskForm.hospitalIn.department = '其它'
        this.taskForm.hospitalIn.departmentId = null
      }
      // 科室的设置由 DepartmentSelector 组件自动处理(通过 isHome 属性)
      
      // 如果转出地址已填写,自动计算距离
      if (this.taskForm.hospitalOut.address) {
@@ -813,6 +960,18 @@
        return false
      }
      
      // 如果转入医院是“家中”,必须填写地址
      if (this.taskForm.hospitalIn.name === '家中' && !this.taskForm.hospitalIn.address) {
        this.$modal.showToast('请输入转入地址(家中)')
        return false
      }
      // 如果转出医院是“家中”,必须填写地址
      if (this.taskForm.hospitalOut.name === '家中' && !this.taskForm.hospitalOut.address) {
        this.$modal.showToast('请输入转出地址(家中)')
        return false
      }
      if (!this.taskForm.transferDistance) {
        this.$modal.showToast('请输入转运公里数')
        return false
@@ -846,6 +1005,10 @@
          conditionText = this.taskForm.patient.condition
        }
      }
      // 调试日志:检查转入医院地址
      console.log('构建提交数据 - 转入医院:', this.taskForm.hospitalIn.name)
      console.log('构建提交数据 - 转入医院地址:', this.taskForm.hospitalIn.address)
      
      const submitData = {
        taskType: 'EMERGENCY_TRANSFER',
@@ -908,32 +1071,134 @@
      if (!this.validateForm()) {
        return
      }
      this.doSubmitTask();
    },
    checkTaskDp(){
   // 获取当前日期(格式YYYY-MM-DD)
      const today = new Date()
      const createDate = today.getFullYear() + '-' +
                        String(today.getMonth() + 1).padStart(2, '0') + '-' +
                        String(today.getDate()).padStart(2, '0')
      const phone = this.taskForm.patient.phone
      
      // 先检查是否重复
      uni.showLoading({
        title: '检查中...'
      })
      checkTaskDuplicate(phone, createDate).then(response => {
        uni.hideLoading()
        // 如果接口返回错误,说明存在重复
        if (response.code !== 200) {
          this.$modal.showToast(response.msg || '该联系电话在当天已有任务,不能重复提交')
          return
        }
        // 检查通过,继续提交
        this.doSubmitTask()
      }).catch(error => {
        uni.hideLoading()
        console.error('重复性检查失败:', error)
        this.$modal.showToast('检查失败,请重试')
      })
    },
    // 执行实际的提交操作
    doSubmitTask() {
      this.$modal.confirm('确定要保存任务吗?').then(() => {
        this.loading = true
        const submitData = this.buildSubmitData()
        
        addTask(submitData).then(response => {
          this.loading = false
          this.$modal.showToast('任务创建成功')
          
          // 延迟跳转,让用户看到成功提示
          setTimeout(() => {
            // 跳转到任务列表并触发刷新
            uni.switchTab({
              url: '/pages/task/index',
              success: () => {
                // 使用事件总线通知任务列表页面刷新
                uni.$emit('refreshTaskList')
              }
          // 获取创建的任务ID
          const taskId = response.taskId || (response.data && response.data.taskId) || null
          console.log('任务创建成功,taskId:', taskId)
          // 如果有待上传的附件(OCR图片),先上传附件
          if (taskId && this.pendingAttachments.length > 0) {
            this.uploadPendingAttachments(taskId).then(() => {
              this.$modal.showToast('任务创建成功,附件已上传')
              this.navigateToTaskList()
            }).catch(error => {
              console.error('附件上传失败:', error)
              this.$modal.showToast('任务创建成功,但附件上传失败')
              this.navigateToTaskList()
            })
          }, 1000)
          } else {
            this.$modal.showToast('任务创建成功')
            this.navigateToTaskList()
          }
        }).catch(error => {
          this.loading = false
          console.error('任务创建失败:', error)
          this.$modal.showToast('任务创建失败,请重试')
        })
      }).catch(() => {})
    },
    // 上传待上传的附件(OCR图片)
    uploadPendingAttachments(taskId) {
      console.log('开始上传附件,taskId:', taskId, '附件数量:', this.pendingAttachments.length)
      // 使用 Promise.all 并发上传所有附件
      const uploadPromises = this.pendingAttachments.map(attachment => {
        return this.uploadSingleAttachment(taskId, attachment)
      })
      return Promise.all(uploadPromises)
    },
    // 上传单个附件
    uploadSingleAttachment(taskId, attachment) {
      return new Promise((resolve, reject) => {
        uni.uploadFile({
          url: config.baseUrl + '/task/attachment/upload/' + taskId,
          filePath: attachment.filePath,
          name: 'file',
          formData: {
            'category': attachment.category
          },
          header: {
            'Authorization': 'Bearer ' + getToken()
          },
          success: function(uploadRes) {
            if (uploadRes.statusCode === 200) {
              const result = JSON.parse(uploadRes.data)
              if (result.code === 200) {
                console.log('附件上传成功:', attachment.description)
                resolve(result)
              } else {
                console.error('附件上传失败:', attachment.description, result.msg)
                reject(result)
              }
            } else {
              console.error('附件上传失败:', attachment.description, uploadRes)
              reject(uploadRes)
            }
          },
          fail: function(err) {
            console.error('附件上传失败:', attachment.description, err)
            reject(err)
          }
        })
      })
    },
    // 跳转到任务列表
    navigateToTaskList() {
      setTimeout(() => {
        // 跳转到任务列表并触发刷新
        uni.switchTab({
          url: '/pages/task/index',
          success: () => {
            // 使用事件总线通知任务列表页面刷新
            uni.$emit('refreshTaskList')
          }
        })
      }, 1000)
    },
    
   goBack() {
@@ -1238,7 +1503,7 @@
    findHospitalByName(name, type, restrictRegion = true) {
      if (!name) return Promise.resolve(null)
      const normalized = name.trim()
      // 特殊处理"家中"
      if (normalized === '家中') {
        // 查询医院库中的"家中"记录
@@ -1246,7 +1511,7 @@
        const queryPromise = restrictRegion && deptId
          ? searchHospitalsByDeptRegion('家中', deptId, 50)
          : searchHospitals('家中', null, 50)
        return queryPromise.then(res => {
          const list = res.data || []
          // 查找名称为"家中"的医院记录
@@ -1271,20 +1536,23 @@
          }
        })
      }
      // restrictRegion=false 时走全量查询;true 且有 deptId 时走区域接口
      // OCR识别后的医院名称,使用新的分词匹配接口
      const deptId = this.selectedOrganizationId || null
      const queryPromise = (restrictRegion && deptId)
        ? searchHospitalsByDeptRegion(normalized, deptId, 50)
        : searchHospitals(normalized, null, 50)
      return queryPromise.then(res => {
        const list = res.data || []
        if (!list.length) return null
        // 自动选择第一个非"家中"的区院,如果全是"家中"则选第一个
        const best = this.pickBestHospitalMatch(list, normalized)
        return best || null
      return searchHospitalsByKeywords(normalized, deptId, 50).then(res => {
        const rawData = res.data || []
        if (!rawData.length) return null
        // 提取 hospital 对象(接口返回格式:{hospital: {...}, matchScore: ...})
        const firstItem = rawData[0]
        const firstHospital = firstItem.hospital || firstItem
        console.log(`OCR识别医院"${normalized}",自动选中:${firstHospital.hospName}(匹配分数:${firstItem.matchScore || 'N/A'})`)
        return firstHospital
      }).catch(error => {
        console.error(`搜索医院"${normalized}"失败:`, error)
        return null
      })
    },
@@ -1382,6 +1650,448 @@
          console.error('距离计算失败:', error)
          this.$modal.showToast('距离计算失败,请手动输入')
        })
    },
    // ==================== 拍照识别相关方法 ====================
    // 显示多图拍照识别弹窗
    showMultiPhotoOCRPopup() {
      this.page1Image = ''
      this.page2Image = ''
      this.page1Fields = {}
      this.page2Fields = {}
      this.$refs.multiPhotoOCRPopup.open()
    },
    // 关闭多图拍照识别弹窗
    closeMultiPhotoOCRPopup() {
      this.$refs.multiPhotoOCRPopup.close()
    },
    // 选择第一页图片
    selectPage1Images() {
      uni.showActionSheet({
        itemList: ['从相册选择', '拍照'],
        success: (res) => {
          if (res.tapIndex === 0) {
            this.chooseImageForPage(1, 'album')
          } else if (res.tapIndex === 1) {
            this.chooseImageForPage(1, 'camera')
          }
        }
      })
    },
    // 选择第二页图片
    selectPage2Images() {
      uni.showActionSheet({
        itemList: ['从相册选择', '拍照'],
        success: (res) => {
          if (res.tapIndex === 0) {
            this.chooseImageForPage(2, 'album')
          } else if (res.tapIndex === 1) {
            this.chooseImageForPage(2, 'camera')
          }
        }
      })
    },
    // 删除第一页图片
    deletePage1Image() {
      this.page1Image = ''
      this.page1Fields = {}
    },
    // 删除第二页图片
    deletePage2Image() {
      this.page2Image = ''
      this.page2Fields = {}
    },
    // 为指定页码选择图片(单图)
    chooseImageForPage(page, sourceType) {
      uni.chooseImage({
        count: 1, // 只选择一张图片
        sizeType: ['compressed'],
        sourceType: [sourceType],
        success: (res) => {
          const imagePath = res.tempFilePaths[0]
          if (page === 1) {
            this.page1Image = imagePath
          } else {
            this.page2Image = imagePath
          }
          // 选择完图片后,立即进行OCR识别
          this.$modal.showToast(`已选择图片,开始识别...`)
          this.recognizeSinglePage(page)
        },
        fail: (err) => {
          console.error('选择图片失败:', err)
          this.$modal.showToast('选择图片失败')
        }
      })
    },
    // 应用OCR识别结果到表单
    applyOcrResult() {
      // 合并两页的识别结果
      const mergedFields = { ...this.page1Fields, ...this.page2Fields }
      // 处理合并后的识别结果
      this.processMultiOCRResult(mergedFields)
      // 保存OCR图片到待上传附件列表(知情同意书,分类为'1')
      this.pendingAttachments = []
      if (this.page1Image) {
        this.pendingAttachments.push({
          filePath: this.page1Image,
          category: '1', // 知情同意书
          description: '知情同意书第一页'
        })
      }
      if (this.page2Image) {
        this.pendingAttachments.push({
          filePath: this.page2Image,
          category: '1', // 知情同意书
          description: '知情同意书第二页'
        })
      }
      console.log('保存待上传附件:', this.pendingAttachments)
      // 关闭弹窗
      this.closeMultiPhotoOCRPopup()
      // 清空图片和结果(但保留 pendingAttachments)
      this.page1Image = ''
      this.page2Image = ''
      this.page1Fields = {}
      this.page2Fields = {}
      this.$modal.showToast('已应用到表单,知情同意书将在任务创建后上传')
    },
    // 识别单个页码的图片(选择后立即识别)
    recognizeSinglePage(page) {
      const image = page === 1 ? this.page1Image : this.page2Image
      if (!image) {
        return
      }
      // 显示加载提示
      uni.showLoading({
        title: `识别第${page}页...`
      })
      // 第一页的itemNames
      const page1ItemNames = [
        "患者姓名", "性别", "年龄", "身份证号", "诊断",
        "需支付转运费用", "行程", "开始时间", "结束时间", "家属签名"
      ]
      // 第二页的itemNames
      const page2ItemNames = [
        "患者签名(手印)", "签字人身份证号码", "签字人身份证号", "日期",
        "联系电话", "本人", "签字人与患者关系"
      ]
      const itemNames = page === 1 ? page1ItemNames : page2ItemNames
      // 调用批量OCR API(传入单个图片)
      batchRecognizeImages([image], itemNames)
        .then(result => {
          uni.hideLoading()
          if (result.success && result.successCount > 0) {
            // 保存识别结果
            if (page === 1) {
              this.page1Fields = result.fields
            } else {
              this.page2Fields = result.fields
            }
            console.log(`第${page}页识别结果:`, result.fields)
            this.$modal.showToast(`第${page}页识别成功`)
          } else {
            this.$modal.showToast(`第${page}页识别失败`)
          }
        })
        .catch(error => {
          uni.hideLoading()
          console.error(`第${page}页OCR识别失败:`, error)
          this.$modal.showToast(error.msg || `第${page}页识别失败`)
        })
    },
    // 处理多图OCR识别结果
    processMultiOCRResult(fields) {
      console.log('多图OCR识别结果:', fields)
      // 提取患者姓名
      if (fields['患者姓名']) {
        this.taskForm.patient.name = fields['患者姓名'].trim()
      }
      // 提取联系人(优先患者签名,其次家属签名,最后本人)
      if (fields['患者签名(手印)']) {
        this.taskForm.patient.contact = fields['患者签名(手印)'].trim()
      } else if (fields['家属签名']) {
        this.taskForm.patient.contact = fields['家属签名'].trim()
      } else if (fields['本人']) {
        this.taskForm.patient.contact = fields['本人'].trim()
      }
      // 提取性别
      if (fields['性别']) {
        const gender = fields['性别'].trim()
        if (gender.includes('男')) {
          this.taskForm.patient.gender = 'male'
        } else if (gender.includes('女')) {
          this.taskForm.patient.gender = 'female'
        }
      }
      // 提取身份证号(优先身份证号,其次签字人身份证号码)
      const patientIdCard = fields['身份证号'] || fields['患者身份证号']
      const signerIdCard = fields['签字人身份证号码'] || fields['签字人身份证号']
      if (patientIdCard) {
        this.taskForm.patient.idCard = patientIdCard.trim()
      } else if (signerIdCard) {
        this.taskForm.patient.idCard = signerIdCard.trim()
      }
      // 提取日期(转运时间)
      if (fields['日期']) {
        const dateString = fields['日期'].trim()
        const dateFormatted = this.formatDateString(dateString)
        if (dateFormatted) {
          this.taskForm.transferTime = dateFormatted
        }
      }
      // 提取联系电话
      if (fields['联系电话']) {
        this.taskForm.patient.phone = fields['联系电话'].trim()
      }
      // 提取需支付转运费用(成交价)
      if (fields['需支付转运费用']) {
        const priceText = fields['需支付转运费用'].trim()
        const priceNumber = priceText.match(/\d+(?:\.\d{1,2})?/)
        if (priceNumber) {
          this.taskForm.price = parseFloat(priceNumber[0]).toFixed(2)
        }
      }
      // 提取诊断(病情)(限制10个字符以内,避免误识别)
      if (fields['诊断']) {
        const diagnosis = fields['诊断'].trim()
        // 只有当10个字符以内的诊断信息才填入表单
        if (diagnosis.length <= 10) {
          this.taskForm.patient.condition = diagnosis
        } else {
          console.log('诊断信息过长,可能为误识别,已忽略:', diagnosis)
        }
      }
      // 提取行程(转出医院和转入医院)
      if (fields['行程']) {
        const route = fields['行程'].trim()
        console.log('【行程】OCR识别的行程信息:', route)
        // 按多种分隔符分割行程:-、—、---、-->、→、空格等
        const hospitals = route.split(/[-—]{1,3}|-->|→|\s{2,}/).map(h => h.trim()).filter(h => h.length > 0)
        console.log('【行程】分割后的医院列表:', hospitals)
        if (hospitals.length >= 2) {
          // 第一个是转出医院
          const outHospital = hospitals[0]
          this.taskForm.hospitalOut.name = outHospital
          // 如枟转出医院是“家中”,提取后面的详细地址
          if (outHospital === '家中' && hospitals.length >= 3) {
            // 将剩余部分作为详细地址
            const detailAddress = hospitals.slice(1, hospitals.length - 1).join(' ')
            if (detailAddress) {
              this.taskForm.hospitalOut.address = detailAddress
              console.log('【行程】转出家中地址:', detailAddress)
            }
            // 最后一个是转入医院
            this.taskForm.hospitalIn.name = hospitals[hospitals.length - 1]
            console.log('【行程】转入医院:', hospitals[hospitals.length - 1])
          } else {
            // 第二个是转入医院
            const inHospital = hospitals[1]
            this.taskForm.hospitalIn.name = inHospital
            // 如果转入医院是“家中”,提取后面的详细地址
            if (inHospital === '家中' && hospitals.length >= 3) {
              // 将剩余部分作为详细地址
              const detailAddress = hospitals.slice(2).join(' ')
              if (detailAddress) {
                this.taskForm.hospitalIn.address = detailAddress
                console.log('【行程】转入家中地址:', detailAddress)
              }
            }
          }
          // 尝试从医院库中匹配并补全地址(只在非“家中”时)
          const outHospitalName = this.taskForm.hospitalOut.name
          const inHospitalName = this.taskForm.hospitalIn.name
          Promise.all([
            outHospitalName !== '家中' ? this.findHospitalByName(outHospitalName, 'out', false) : Promise.resolve(null),
            inHospitalName !== '家中' ? this.findHospitalByName(inHospitalName, 'in', false) : Promise.resolve(null)
          ]).then(([outHosp, inHosp]) => {
            // 处理转出医院
            if (outHospitalName !== '家中') {
              if (outHosp) {
                // 找到医院,使用医院库中的信息
                this.taskForm.hospitalOut.id = outHosp.hospId
                this.taskForm.hospitalOut.name = outHosp.hospName
                this.taskForm.hospitalOut.address = this.buildFullAddress(outHosp)
                this.taskForm.hospitalOut.city = outHosp.hopsCity || ''
                console.log('【行程】转出医院匹配成功:', outHosp.hospName)
              } else {
                // 找不到医院,默认设置为"家中",原医院名称作为地址
                console.log('【行程】转出医院未找到,默认设置为家中,地址为:', outHospitalName)
                this.taskForm.hospitalOut.name = '家中'
                this.taskForm.hospitalOut.address = outHospitalName
                this.taskForm.hospitalOut.id = null
                this.taskForm.hospitalOut.city = ''
              }
            }
            // 处理转入医院
            if (inHospitalName !== '家中') {
              if (inHosp) {
                // 找到医院,使用医院库中的信息
                this.taskForm.hospitalIn.id = inHosp.hospId
                this.taskForm.hospitalIn.name = inHosp.hospName
                this.taskForm.hospitalIn.address = this.buildFullAddress(inHosp)
                this.taskForm.hospitalIn.city = inHosp.hopsCity || ''
                console.log('【行程】转入医院匹配成功:', inHosp.hospName)
              } else {
                // 找不到医院,默认设置为"家中",原医院名称作为地址
                console.log('【行程】转入医院未找到,默认设置为家中,地址为:', inHospitalName)
                this.taskForm.hospitalIn.name = '家中'
                this.taskForm.hospitalIn.address = inHospitalName
                this.taskForm.hospitalIn.id = null
                this.taskForm.hospitalIn.city = ''
              }
            }
            // 如果两个医院地址都有,自动计算距离
            if (this.taskForm.hospitalOut.address && this.taskForm.hospitalIn.address &&
                this.taskForm.hospitalOut.name !== '家中' && this.taskForm.hospitalIn.name !== '家中') {
              this.calculateHospitalDistance()
            }
          })
        }
      }
      console.log('多图OCR结果处理完成,表单数据更新')
    },
    // 格式化日期字符串(返回 yyyy-MM-dd HH:mm:ss 格式)
    formatDateString(dateStr) {
      // 尝试不同的日期格式
      // 支持格式:
      // - yyyy年MM月dd日HH时mm分
      // - yyyy年MM月dd日
      // - YYYYMMDD
      // - yyyy-MM-dd HH:mm:ss
      console.log('尝试格式化日期字符串:', dateStr)
      // 如果输入为空或无效,返回空字符串
      if (!dateStr || typeof dateStr !== 'string') {
        console.warn('日期字符串无效:', dateStr)
        return ''
      }
      // 清洗日期字符串
      let cleaned = dateStr
        .replace(/[年月]/g, '-')
        .replace(/[日号]/g, ' ')  // 日/号 → 空格,保留日期和时间的分隔
        .replace(/时/g, ':')
        .replace(/分/g, ':')
        .replace(/秒/g, '')
        .replace(/\s+/g, ' ')  // 多个空格合并为一个
        .trim()
      console.log('清理后的日期字符串:', cleaned)
      // 分离日期和时间部分
      const parts = cleaned.split(' ')
      let datePart = parts[0] || ''
      let timePart = parts[1] || ''
      let dateResult = ''
      // 处理日期部分
      // 如果是YYMMDD格式
      if (/^\d{6}$/.test(datePart)) {
        const year = '20' + datePart.substring(0, 2)
        const month = datePart.substring(2, 4)
        const day = datePart.substring(4, 6)
        dateResult = `${year}-${month}-${day}`
      }
      // 如果是YYYYMMDD格式
      else if (/^\d{8}$/.test(datePart)) {
        const year = datePart.substring(0, 4)
        const month = datePart.substring(4, 6)
        const day = datePart.substring(6, 8)
        dateResult = `${year}-${month}-${day}`
      }
      // 如果是yyyy-MM-dd或yyyy/MM/dd格式
      else if (datePart.match(/^\d{4}[-\/]\d{1,2}[-\/]\d{1,2}$/)) {
        dateResult = datePart.replace(/\//g, '-')
      }
      else {
        dateResult = datePart
      }
      // 验证日期部分是否有效
      if (!dateResult.match(/^\d{4}-\d{1,2}-\d{1,2}$/)) {
        console.warn('日期格式不正确:', dateResult)
        return ''
      }
      // 处理时间部分
      let timeResult = '00:00:00'  // 默认时间
      if (timePart) {
        // 移除末尾多余的冒号
        timePart = timePart.replace(/:+$/, '')
        // 分割时、分、秒
        const timeParts = timePart.split(':')
        const hour = timeParts[0] || '00'
        const minute = timeParts[1] || '00'
        const second = timeParts[2] || '00'
        // 验证时间数字是否有效
        const hourNum = parseInt(hour, 10)
        const minuteNum = parseInt(minute, 10)
        const secondNum = parseInt(second, 10)
        if (!isNaN(hourNum) && !isNaN(minuteNum) && !isNaN(secondNum) &&
            hourNum >= 0 && hourNum < 24 && minuteNum >= 0 && minuteNum < 60 && secondNum >= 0 && secondNum < 60) {
          // 补齐两位数
          timeResult = `${String(hourNum).padStart(2, '0')}:${String(minuteNum).padStart(2, '0')}:${String(secondNum).padStart(2, '0')}`
        } else {
          console.warn('时间数值超出范围,使用默认值00:00:00')
        }
      }
      const finalResult = `${dateResult} ${timeResult}`
      console.log('最终格式化结果:', finalResult)
      return finalResult
    }
  }
}
@@ -1417,7 +2127,9 @@
      color: #333;
    }
    
    .smart-parse-btn {
    .smart-parse-btn,
    .ocr-page-btn {
      position: relative;
      display: flex;
      flex-direction: column;
      align-items: center;
@@ -1426,7 +2138,53 @@
      
      text {
        font-size: 22rpx;
        margin-top: 4rpx;
      }
      .badge {
        position: absolute;
        top: 0;
        right: 10rpx;
        min-width: 32rpx;
        height: 32rpx;
        line-height: 32rpx;
        text-align: center;
        background-color: #ff4d4f;
        color: white;
        font-size: 20rpx;
        border-radius: 16rpx;
        padding: 0 8rpx;
      }
    }
    .smart-parse-btn {
      text {
        color: #007AFF;
      }
    }
    .ocr-page-btn:first-of-type {
      text {
        color: #52c41a;
      }
    }
    .ocr-page-btn:last-of-type {
      text {
        color: #FF6B00;
      }
    }
    .multi-photo-ocr-btn {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 10rpx 20rpx;
      text {
        font-size: 22rpx;
        color: #FF6B00;
        margin-top: 4rpx;
      }
    }
@@ -1794,4 +2552,493 @@
    }
  }
}
// 拍照识别弹窗样式
.photo-ocr-popup {
  background-color: white;
  border-radius: 20rpx 20rpx 0 0;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  .popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 30rpx;
    border-bottom: 1rpx solid #f0f0f0;
    flex-shrink: 0;
    .popup-title {
      display: flex;
      align-items: center;
      gap: 10rpx;
      font-size: 32rpx;
      font-weight: bold;
      color: #333;
      text {
        margin-left: 8rpx;
      }
    }
    .popup-close {
      padding: 10rpx;
    }
  }
  .ocr-content {
    flex: 1;
    padding: 30rpx;
    overflow-y: auto;
    .ocr-tip {
      display: flex;
      align-items: flex-start;
      padding: 20rpx;
      background-color: #f0f7ff;
      border-radius: 10rpx;
      margin-bottom: 20rpx;
      text {
        flex: 1;
        margin-left: 10rpx;
        font-size: 24rpx;
        color: #666;
        line-height: 1.6;
      }
    }
    .image-preview {
      margin-bottom: 20rpx;
      border: 1rpx solid #eee;
      border-radius: 10rpx;
      overflow: hidden;
    }
    .multi-image-preview {
      display: flex;
      flex-wrap: wrap;
      gap: 15rpx;
      margin-bottom: 20rpx;
      .image-item {
        position: relative;
        width: 200rpx;
        height: 200rpx;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        overflow: hidden;
        image {
          width: 100%;
          height: 100%;
        }
        .delete-btn {
          position: absolute;
          top: 5rpx;
          right: 5rpx;
          width: 40rpx;
          height: 40rpx;
          background-color: rgba(0, 0, 0, 0.6);
          border-radius: 50%;
          display: flex;
          align-items: center;
          justify-content: center;
        }
      }
    }
    .ocr-actions {
      display: flex;
      gap: 20rpx;
      button {
        flex: 1;
        height: 80rpx;
        border-radius: 10rpx;
        font-size: 28rpx;
      }
      .select-btn {
        background-color: #f5f5f5;
        color: #333;
      }
      .capture-btn {
        background-color: #007AFF;
        color: white;
      }
    }
    .image-count {
      text-align: center;
      margin-top: 20rpx;
      font-size: 26rpx;
      color: #FF6B00;
      font-weight: bold;
    }
    // 页面上传区域
    .page-upload-section {
      background: linear-gradient(135deg, #f9f9f9 0%, #ffffff 100%);
      border-radius: 15rpx;
      padding: 25rpx;
      margin-bottom: 25rpx;
      border: 2rpx solid #f0f0f0;
      transition: all 0.3s;
      &.first-page {
        border-left: 4rpx solid #52c41a;
        .page-badge {
          background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
          .page-number {
            color: #52c41a;
          }
        }
      }
      &.second-page {
        border-left: 4rpx solid #FF6B00;
        .page-badge {
          background: linear-gradient(135deg, #FF6B00 0%, #ff8c3a 100%);
          .page-number {
            color: #FF6B00;
          }
        }
      }
      &:last-child {
        margin-bottom: 0;
      }
      .page-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 20rpx;
        .page-title {
          display: flex;
          align-items: center;
          gap: 15rpx;
          flex: 1;
          .page-badge {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 60rpx;
            height: 60rpx;
            background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
            border-radius: 50%;
            position: relative;
            .page-number {
              position: absolute;
              bottom: -2rpx;
              right: -2rpx;
              width: 24rpx;
              height: 24rpx;
              background-color: #fff;
              border-radius: 50%;
              font-size: 16rpx;
              font-weight: bold;
              color: #52c41a;
              display: flex;
              align-items: center;
              justify-content: center;
              box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
            }
          }
          .page-info {
            display: flex;
            flex-direction: column;
            gap: 4rpx;
            .page-main-title {
              font-size: 28rpx;
              font-weight: bold;
              color: #333;
            }
            .page-sub-title {
              font-size: 22rpx;
              color: #999;
            }
          }
        }
        .upload-btn {
          width: 60rpx;
          height: 60rpx;
          border-radius: 50%;
          display: flex;
          align-items: center;
          justify-content: center;
          transition: all 0.3s;
          box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
          &.green {
            border: 2rpx dashed #52c41a;
            background-color: rgba(82, 196, 26, 0.05);
          }
          &.orange {
            border: 2rpx dashed #FF6B00;
            background-color: rgba(255, 107, 0, 0.05);
          }
          &:active {
            transform: scale(0.95);
            opacity: 0.8;
          }
        }
      }
      // 识别字段提示
      .field-hint {
        background-color: #fffbe6;
        border: 1rpx solid #ffe58f;
        border-radius: 8rpx;
        padding: 15rpx;
        margin-bottom: 15rpx;
        text {
          font-size: 22rpx;
          color: #d48806;
          line-height: 1.6;
        }
      }
      .images-container {
        display: flex;
        flex-wrap: wrap;
        gap: 10rpx;
        margin-bottom: 15rpx;
        .image-item {
          position: relative;
          width: 150rpx;
          height: 150rpx;
          border: 1rpx solid #eee;
          border-radius: 10rpx;
          overflow: hidden;
          image {
            width: 100%;
            height: 100%;
          }
          .delete-btn {
            position: absolute;
            top: 5rpx;
            right: 5rpx;
            width: 35rpx;
            height: 35rpx;
            background-color: rgba(0, 0, 0, 0.6);
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
          }
        }
      }
      // 单图预览区域
      .single-image-container {
        position: relative;
        width: 100%;
        height: 400rpx;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        overflow: hidden;
        margin-bottom: 15rpx;
        background-color: #f5f5f5;
        .preview-image {
          width: 100%;
          height: 100%;
        }
        .delete-btn {
          position: absolute;
          top: 10rpx;
          right: 10rpx;
          width: 50rpx;
          height: 50rpx;
          background-color: rgba(0, 0, 0, 0.6);
          border-radius: 50%;
          display: flex;
          align-items: center;
          justify-content: center;
          z-index: 2;
          &:active {
            transform: scale(0.95);
            opacity: 0.9;
          }
        }
        // 图片状态标签
        .image-status {
          position: absolute;
          bottom: 10rpx;
          left: 10rpx;
          display: flex;
          align-items: center;
          gap: 5rpx;
          padding: 8rpx 15rpx;
          background-color: rgba(82, 196, 26, 0.9);
          border-radius: 20rpx;
          z-index: 2;
          text {
            font-size: 22rpx;
            color: #fff;
            font-weight: 500;
          }
        }
      }
      .recognition-result {
        background-color: white;
        border-radius: 10rpx;
        padding: 15rpx;
        border: 1rpx solid #e0e0e0;
        .result-title {
          display: flex;
          align-items: center;
          margin-bottom: 15rpx;
          padding-bottom: 10rpx;
          border-bottom: 1rpx solid #f0f0f0;
          text {
            margin-left: 8rpx;
            font-size: 26rpx;
            font-weight: bold;
            color: #333;
          }
          .result-count {
            margin-left: 8rpx;
            font-size: 22rpx;
            color: #999;
            font-weight: normal;
          }
        }
        .field-list {
          .field-item {
            display: flex;
            padding: 10rpx 0;
            border-bottom: 1rpx solid #f9f9f9;
            &:last-child {
              border-bottom: none;
            }
            .field-name {
              min-width: 150rpx;
              font-size: 24rpx;
              color: #666;
              font-weight: 500;
              &::after {
                content: ':';
                margin-left: 4rpx;
              }
            }
            .field-value {
              flex: 1;
              font-size: 24rpx;
              color: #333;
              word-break: break-all;
            }
          }
        }
      }
    }
    .page-indicator {
      display: flex;
      justify-content: center;
      gap: 30rpx;
      margin-top: 30rpx;
      .page-dot {
        padding: 15rpx 30rpx;
        border: 2rpx solid #ddd;
        border-radius: 30rpx;
        font-size: 26rpx;
        color: #999;
        background-color: #f5f5f5;
        transition: all 0.3s;
        &.active {
          border-color: #007AFF;
          background-color: #007AFF;
          color: white;
        }
        &.completed {
          border-color: #52c41a;
          color: #52c41a;
          &.active {
            background-color: #007AFF;
            border-color: #007AFF;
            color: white;
          }
        }
      }
    }
  }
  .popup-footer {
    display: flex;
    padding: 20rpx 30rpx;
    border-top: 1rpx solid #f0f0f0;
    gap: 20rpx;
    flex-shrink: 0;
    flex-wrap: wrap;
    button {
      flex: 1;
      min-width: 160rpx;
      height: 80rpx;
      border-radius: 10rpx;
      font-size: 30rpx;
    }
    .cancel-btn {
      background-color: #f5f5f5;
      color: #666;
    }
    .next-btn,
    .prev-btn {
      background-color: #52c41a;
      color: white;
    }
    .confirm-btn {
      background-color: #007AFF;
      color: white;
      &[disabled] {
        background-color: #ccc;
        color: #999;
      }
    }
  }
}
</style>