wlzboy
2025-11-22 fd047fa7234dc11643dab8ecbf38e8d7a8ba0854
feat:修改任务
10个文件已修改
10个文件已添加
1个文件已删除
6318 ■■■■ 已修改文件
app/api/map.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/api/task.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/components/DiseaseSelector.vue 471 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/components/HospitalSelector.vue 586 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/components/OrganizationSelector.vue 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/components/TaskTypeSelector.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/components/VehicleSelector.vue 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/mixins/distanceCalculator.js 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages.json 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/create-emergency.vue 1177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/create-normal.vue 451 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/detail.vue 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/edit-emergency.vue 1015 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/edit-welfare.vue 569 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/edit.vue 520 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskUpdateVO.java 326 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/areacode.json 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/test/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImplTest.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/api/map.js
@@ -249,7 +249,7 @@
  })
}
// 天地图地址搜索提示API(输入联想)
// 天地图普通搜索服务API(地址搜索)
export function tiandituPlaceSuggestion(keyWord, region, city, count) {
  // 参数验证
  if (!keyWord) {
@@ -266,4 +266,4 @@
      count: count || 10
    }
  })
}
}
app/api/task.js
@@ -26,7 +26,7 @@
export function updateTask(data) {
  return request({
    url: '/task/' + data.taskId,
    url: '/task',
    method: 'put',
    data: data
  })
app/components/DiseaseSelector.vue
New file
@@ -0,0 +1,471 @@
<template>
  <view class="disease-selector">
    <view class="form-item">
      <view class="form-label" :class="{ required: required }" v-if="showLabel">{{ label }}</view>
      <view class="disease-container">
        <view class="disease-tags" v-if="selectedDiseases.length > 0">
          <view
            class="disease-tag"
            v-for="(disease, index) in selectedDiseases"
            :key="index"
          >
            <text class="disease-name">{{ disease.icdName }}</text>
            <uni-icons
              type="closeempty"
              size="16"
              color="#fff"
              @click="removeDisease(index)"
            ></uni-icons>
          </view>
        </view>
        <view class="add-disease-btn" @click="showSelector">
          <uni-icons type="plusempty" size="20" color="#007AFF"></uni-icons>
          <text>添加病情</text>
        </view>
        <textarea
          v-if="showOtherDescription"
          class="form-textarea"
          placeholder="其他病情描述(选填)"
          :value="otherDescription"
          @input="onOtherDescriptionInput"
          style="margin-top: 20rpx;"
        />
      </view>
    </view>
    <!-- 病情选择弹窗 -->
    <uni-popup ref="diseasePopup" type="bottom" :safe-area="true">
      <view class="disease-selector-popup">
        <view class="popup-header">
          <view class="popup-title">选择病情(ICD-10)</view>
          <view class="popup-close" @click="closeSelector">
            <uni-icons type="closeempty" size="24" color="#333"></uni-icons>
          </view>
        </view>
        <view class="search-box">
          <uni-icons type="search" size="18" color="#999"></uni-icons>
          <input
            class="search-input"
            placeholder="搜索疾病名称、编码或助记码"
            v-model="diseaseSearchKeyword"
            @input="onDiseaseSearch"
          />
        </view>
        <scroll-view class="disease-list-popup" scroll-y="true">
          <view
            class="disease-item-popup"
            v-for="disease in diseaseSearchResults"
            :key="disease.id"
            @click="toggleDiseaseSelection(disease)"
          >
            <view class="disease-info">
              <view class="disease-name-row">
                <text class="disease-name">{{ disease.icdName }}</text>
                <text class="disease-code">[{{ disease.icdCode }}]</text>
              </view>
              <view class="disease-detail-row" v-if="disease.sm">
                <text class="disease-desc">{{ disease.sm }}</text>
              </view>
            </view>
            <uni-icons
              v-if="isDiseaseSelected(disease.id)"
              type="checkmarkempty"
              size="24"
              color="#007AFF"
            ></uni-icons>
            <view v-else class="checkbox-empty"></view>
          </view>
          <view class="no-data" v-if="diseaseSearchResults.length === 0">
            <uni-icons type="info" size="40" color="#ccc"></uni-icons>
            <text>{{ diseaseSearchKeyword ? '未找到相关疾病' : '暂无病情数据' }}</text>
          </view>
        </scroll-view>
        <view class="popup-footer">
          <button class="cancel-btn" @click="closeSelector">取消</button>
          <button class="confirm-btn" @click="confirmSelection">确定(已选{{ tempSelectedDiseases.length }})</button>
        </view>
      </view>
    </uni-popup>
  </view>
</template>
<script>
import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue'
import { searchIcd10 } from "@/api/icd10"
export default {
  name: 'DiseaseSelector',
  components: {
    uniPopup
  },
  props: {
    // 标签文本
    label: {
      type: String,
      default: '病情'
    },
    // 是否显示标签
    showLabel: {
      type: Boolean,
      default: true
    },
    // 是否必填
    required: {
      type: Boolean,
      default: false
    },
    // 已选择的病情列表
    value: {
      type: Array,
      default: () => []
    },
    // 其他病情描述
    otherDescription: {
      type: String,
      default: ''
    },
    // 是否显示其他病情描述框
    showOtherDescription: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      selectedDiseases: [],
      tempSelectedDiseases: [],
      diseaseSearchKeyword: '',
      diseaseSearchResults: [],
      diseaseSearchTimer: null
    }
  },
  watch: {
    value: {
      immediate: true,
      handler(newVal) {
        if (newVal && Array.isArray(newVal)) {
          this.selectedDiseases = [...newVal]
        }
      }
    }
  },
  methods: {
    // 显示病情选择弹窗
    showSelector() {
      // 初始化临时选择列表(复制当前已选择的病情)
      this.tempSelectedDiseases = [...this.selectedDiseases]
      this.diseaseSearchKeyword = ''
      // 默认加载所有病情
      this.loadAllDiseases()
      this.$refs.diseasePopup.open()
    },
    // 关闭病情选择弹窗
    closeSelector() {
      this.$refs.diseasePopup.close()
      this.diseaseSearchKeyword = ''
      this.diseaseSearchResults = []
      this.tempSelectedDiseases = []
    },
    // 病情搜索
    onDiseaseSearch(e) {
      const keyword = e.detail.value
      this.diseaseSearchKeyword = keyword
      // 防抖处理
      if (this.diseaseSearchTimer) {
        clearTimeout(this.diseaseSearchTimer)
      }
      // 如果关键词为空,加载所有病情
      if (!keyword || keyword.trim() === '') {
        this.loadAllDiseases()
        return
      }
      // 有关键词时进行搜索
      this.diseaseSearchTimer = setTimeout(() => {
        this.searchDiseaseByKeyword(keyword)
      }, 300)
    },
    // 加载所有病情(默认显示)
    loadAllDiseases() {
      searchIcd10('').then(response => {
        this.diseaseSearchResults = response.data || []
      }).catch(error => {
        console.error('加载病情列表失败:', error)
        this.diseaseSearchResults = []
      })
    },
    // 根据关键词搜索病情
    searchDiseaseByKeyword(keyword) {
      searchIcd10(keyword).then(response => {
        this.diseaseSearchResults = response.data || []
      }).catch(error => {
        console.error('搜索病情失败:', error)
        this.diseaseSearchResults = []
      })
    },
    // 切换病情选中状态
    toggleDiseaseSelection(disease) {
      const index = this.tempSelectedDiseases.findIndex(d => d.id === disease.id)
      if (index > -1) {
        // 已选中,移除
        this.tempSelectedDiseases.splice(index, 1)
      } else {
        // 未选中,添加
        this.tempSelectedDiseases.push({
          id: disease.id,
          icdCode: disease.icdCode,
          icdName: disease.icdName,
          sm: disease.sm
        })
      }
    },
    // 判断病情是否已选中
    isDiseaseSelected(diseaseId) {
      return this.tempSelectedDiseases.some(d => d.id === diseaseId)
    },
    // 确认病情选择
    confirmSelection() {
      // 将临时选择的病情复制到正式列表
      this.selectedDiseases = [...this.tempSelectedDiseases]
      this.$emit('input', this.selectedDiseases)
      this.$emit('change', this.selectedDiseases)
      this.closeSelector()
    },
    // 移除病情
    removeDisease(index) {
      this.selectedDiseases.splice(index, 1)
      this.$emit('input', this.selectedDiseases)
      this.$emit('change', this.selectedDiseases)
    },
    // 其他病情描述输入
    onOtherDescriptionInput(e) {
      const value = e.detail.value
      this.$emit('update:otherDescription', value)
      this.$emit('other-description-change', value)
    }
  }
}
</script>
<style lang="scss" scoped>
.disease-selector {
  .form-item {
    margin-bottom: 40rpx;
    .form-label {
      font-size: 28rpx;
      margin-bottom: 15rpx;
      color: #333;
      &.required::before {
        content: '*';
        color: #ff4d4f;
        margin-right: 4rpx;
        font-weight: bold;
      }
    }
    .disease-container {
      .disease-tags {
        display: flex;
        flex-wrap: wrap;
        gap: 15rpx;
        margin-bottom: 20rpx;
        .disease-tag {
          display: flex;
          align-items: center;
          padding: 10rpx 20rpx;
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          border-radius: 30rpx;
          .disease-name {
            font-size: 26rpx;
            color: white;
            margin-right: 10rpx;
          }
        }
      }
      .add-disease-btn {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 20rpx;
        border: 1rpx dashed #007AFF;
        border-radius: 10rpx;
        color: #007AFF;
        font-size: 28rpx;
        text {
          margin-left: 10rpx;
        }
      }
      .form-textarea {
        width: 100%;
        min-height: 150rpx;
        padding: 20rpx;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        font-size: 28rpx;
      }
    }
  }
}
// 病情选择弹窗样式
.disease-selector-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 {
      font-size: 32rpx;
      font-weight: bold;
      color: #333;
    }
    .popup-close {
      padding: 10rpx;
    }
  }
  .search-box {
    display: flex;
    align-items: center;
    margin: 20rpx 30rpx;
    padding: 15rpx 20rpx;
    background-color: #f5f5f5;
    border-radius: 10rpx;
    flex-shrink: 0;
    .search-input {
      flex: 1;
      margin-left: 10rpx;
      font-size: 28rpx;
    }
  }
  .disease-list-popup {
    flex: 1;
    overflow-y: auto;
    padding: 0 30rpx;
    .disease-item-popup {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 25rpx 20rpx;
      border-bottom: 1rpx solid #f0f0f0;
      &:active {
        background-color: #f5f5f5;
      }
      .disease-info {
        flex: 1;
        .disease-name-row {
          display: flex;
          align-items: center;
          margin-bottom: 8rpx;
          .disease-name {
            font-size: 30rpx;
            font-weight: bold;
            color: #333;
            margin-right: 15rpx;
          }
          .disease-code {
            font-size: 24rpx;
            color: #007AFF;
            background-color: #e6f2ff;
            padding: 4rpx 12rpx;
            border-radius: 6rpx;
          }
        }
        .disease-detail-row {
          .disease-desc {
            font-size: 24rpx;
            color: #999;
            line-height: 1.5;
          }
        }
      }
      .checkbox-empty {
        width: 40rpx;
        height: 40rpx;
        border: 2rpx solid #ddd;
        border-radius: 50%;
      }
    }
    .no-data {
      text-align: center;
      padding: 100rpx 0;
      color: #999;
      text {
        display: block;
        margin-top: 20rpx;
        font-size: 28rpx;
      }
    }
  }
  .popup-footer {
    display: flex;
    padding: 20rpx 30rpx;
    border-top: 1rpx solid #f0f0f0;
    gap: 20rpx;
    flex-shrink: 0;
    button {
      flex: 1;
      height: 80rpx;
      border-radius: 10rpx;
      font-size: 30rpx;
    }
    .cancel-btn {
      background-color: #f5f5f5;
      color: #666;
    }
    .confirm-btn {
      background-color: #007AFF;
      color: white;
    }
  }
}
</style>
app/components/HospitalSelector.vue
New file
@@ -0,0 +1,586 @@
<template>
  <view class="hospital-selector">
    <view class="form-item">
      <view class="form-label" :class="{ required: required }" v-if="showLabel">{{ label }}</view>
      <view class="hospital-search-container">
        <input
          class="form-input"
          :placeholder="placeholder"
          v-model="searchKeyword"
          @input="onSearch"
          @focus="onFocus"
        />
        <view class="search-results" v-if="showResults && searchResults.length > 0">
          <view
            class="search-result-item"
            v-for="hospital in searchResults"
            :key="hospital.hospId"
            @click="selectHospital(hospital)"
          >
            <view class="hospital-name">
              {{ hospital.hospName }}
              <text class="hospital-short" v-if="hospital.hospShort">{{ hospital.hospShort }}</text>
            </view>
            <view class="hospital-address">{{ buildFullAddress(hospital) }}</view>
          </view>
        </view>
      </view>
    </view>
    <view class="form-item" v-if="showDepartment">
      <view class="form-label" :class="{ required: departmentRequired }">科室</view>
      <picker
        v-if="selectedHospitalName !== '家中' && departmentOptions.length > 0"
        mode="selector"
        :range="departmentOptions"
        range-key="text"
        @change="onDepartmentChange"
      >
        <view class="form-input picker-input">
          {{ departmentValue || '请选择科室' }}
          <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
        </view>
      </picker>
      <input
        v-else-if="selectedHospitalName !== '家中'"
        class="form-input"
        placeholder="请输入科室"
        :value="departmentValue"
        @input="onDepartmentInput"
      />
      <view v-else class="form-input picker-input disabled">
        其它
      </view>
    </view>
    <view class="form-item" v-if="showBedNumber">
      <view class="form-label">床号</view>
      <input
        class="form-input"
        placeholder="请输入床号"
        :value="bedNumberValue"
        @input="onBedNumberInput"
      />
    </view>
    <view class="form-item">
      <view class="form-label" :class="{ required: required }">{{ addressLabel }}</view>
      <view class="address-input-container" v-if="selectedHospitalName === '家中'">
        <input
          class="form-input"
          placeholder="请输入详细地址"
          :value="addressValue"
          @input="onAddressInput"
          @focus="onAddressFocus"
        />
        <view class="address-suggestions" v-if="showAddressSuggestions && addressSuggestions.length > 0">
          <view
            class="address-suggestion-item"
            v-for="(item, index) in addressSuggestions"
            :key="index"
            @click="selectAddressSuggestion(item)"
          >
            <view class="suggestion-name">{{ item.name }}</view>
            <view class="suggestion-address">{{ item.district }}{{ item.address }}</view>
          </view>
        </view>
      </view>
      <view v-else class="form-input picker-input disabled">
        {{ addressValue || '选择医院后自动填充' }}
      </view>
    </view>
  </view>
</template>
<script>
import { searchHospitals } from "@/api/hospital"
import { baiduPlaceSuggestion } from "@/api/map"
export default {
  name: 'HospitalSelector',
  props: {
    // 标签文本
    label: {
      type: String,
      default: '医院名称'
    },
    // 是否显示标签
    showLabel: {
      type: Boolean,
      default: true
    },
    // 是否必填
    required: {
      type: Boolean,
      default: false
    },
    // 地址标签
    addressLabel: {
      type: String,
      default: '医院地址'
    },
    // 占位符
    placeholder: {
      type: String,
      default: '请输入医院名称或地址搜索'
    },
    // 当前选中的医院信息
    value: {
      type: Object,
      default: () => ({
        id: null,
        name: '',
        department: '',
        departmentId: null,
        bedNumber: '',
        address: ''
      })
    },
    // 是否显示科室
    showDepartment: {
      type: Boolean,
      default: true
    },
    // 科室是否必填
    departmentRequired: {
      type: Boolean,
      default: false
    },
    // 科室选项列表(用于 picker)
    departmentOptions: {
      type: Array,
      default: () => []
    },
    // 是否显示床号
    showBedNumber: {
      type: Boolean,
      default: true
    },
    // 部门ID(用于区域过滤)
    deptId: {
      type: [Number, String],
      default: null
    },
    // 当前区域(用于地址搜索)
    region: {
      type: String,
      default: '广州'
    }
  },
  data() {
    return {
      searchKeyword: '',
      searchResults: [],
      showResults: false,
      searchTimer: null,
      selectedHospitalName: '',
      // 地址建议相关
      addressSuggestions: [],
      showAddressSuggestions: false,
      addressSearchTimer: null,
      // 默认医院列表(用于首次加载)
      defaultHospitals: [],
      hasLoadedDefault: false  // 标记是否已加载过默认列表
    }
  },
  computed: {
    addressValue() {
      return this.value.address || ''
    },
    departmentValue() {
      return this.value.department || ''
    },
    bedNumberValue() {
      return this.value.bedNumber || ''
    }
  },
  watch: {
    'value.name': {
      immediate: true,
      handler(newVal) {
        if (newVal) {
          this.searchKeyword = newVal
          this.selectedHospitalName = newVal
        }
      }
    },
    // 监听 deptId 变化,清除已加载的默认列表(但不自动加载)
    deptId(newVal, oldVal) {
      if (newVal !== oldVal && newVal) {
        this.hasLoadedDefault = false
        this.defaultHospitals = []
        // 不自动加载,等待用户点击输入框时再加载
      }
    }
  },
  methods: {
    // 搜索输入监听
    onSearch(e) {
      const keyword = e.detail.value
      this.searchKeyword = keyword
      if (this.searchTimer) {
        clearTimeout(this.searchTimer)
      }
      if (!keyword || keyword.trim() === '') {
        // 关键词为空时,显示默认医院列表
        if (this.defaultHospitals.length > 0) {
          this.searchResults = this.defaultHospitals
          this.showResults = true
        } else {
          this.searchResults = []
          this.showResults = false
        }
        return
      }
      // 防抖处理
      this.searchTimer = setTimeout(() => {
        this.searchHospital(keyword)
      }, 300)
    },
    // 搜索医院
    searchHospital(keyword) {
      searchHospitals(keyword, this.deptId).then(response => {
        this.searchResults = response.data || []
        this.showResults = true
      }).catch(error => {
        console.error('搜索医院失败:', error)
        this.searchResults = []
      })
    },
    // 输入框获得焦点
    onFocus() {
      // 如果已经有搜索结果,直接显示
      if (this.searchResults.length > 0) {
        this.showResults = true
        return
      }
      // 如果还没有加载过默认医院列表,则加载
      if (!this.hasLoadedDefault) {
        this.loadDefaultHospitals()
      } else if (this.defaultHospitals.length > 0) {
        // 如果已经加载过,直接显示默认列表
        this.searchResults = this.defaultHospitals
        this.showResults = true
      }
    },
    // 加载默认医院列表
    loadDefaultHospitals() {
      searchHospitals('', this.deptId).then(response => {
        this.defaultHospitals = response.data || []
        this.searchResults = this.defaultHospitals
        this.showResults = true
        this.hasLoadedDefault = true
        console.log('加载默认医院列表,数量:', this.defaultHospitals.length)
      }).catch(error => {
        console.error('加载默认医院列表失败:', error)
        this.defaultHospitals = []
      })
    },
    // 选择医院
    selectHospital(hospital) {
      this.selectedHospitalName = hospital.hospName
      this.searchKeyword = hospital.hospName
      const hospitalData = {
        id: hospital.hospId,
        name: hospital.hospName,
        department: this.value.department || '',
        departmentId: this.value.departmentId || null,
        bedNumber: this.value.bedNumber || '',
        address: hospital.hospName === '家中' ? '' : this.buildFullAddress(hospital)
      }
      // 如果选择的是"家中",科室设置为"其它"
      if (hospital.hospName === '家中') {
        hospitalData.department = '其它'
        hospitalData.departmentId = null
      }
      this.showResults = false
      this.searchResults = []
      // 触发更新事件
      this.$emit('input', hospitalData)
      this.$emit('change', hospitalData)
    },
    // 合并医院地址(省 + 市 + 区 + 详细地址)
    buildFullAddress(hospital) {
      const parts = []
      if (hospital.hopsProvince) {
        parts.push(hospital.hopsProvince)
      }
      if (hospital.hopsCity) {
        parts.push(hospital.hopsCity)
      }
      if (hospital.hopsArea) {
        parts.push(hospital.hopsArea)
      }
      if (hospital.hospAddress) {
        parts.push(hospital.hospAddress)
      }
      return parts.join('')
    },
    // 地址输入(仅在选择“家中”时可用)
    onAddressInput(e) {
      const address = e.detail.value
      this.$emit('input', {
        ...this.value,
        address: address
      })
      this.$emit('address-change', address)
      // 防抖处理地址搜索
      if (this.addressSearchTimer) {
        clearTimeout(this.addressSearchTimer)
      }
      if (!address || address.trim() === '') {
        this.addressSuggestions = []
        this.showAddressSuggestions = false
        return
      }
      this.addressSearchTimer = setTimeout(() => {
        this.searchAddress(address)
      }, 300)
    },
    // 搜索地址建议
    searchAddress(query) {
      baiduPlaceSuggestion(query, this.region).then(response => {
        if (response.code === 200 && response.data) {
          this.addressSuggestions = response.data
          this.showAddressSuggestions = true
        } else {
          this.addressSuggestions = []
          this.showAddressSuggestions = false
        }
      }).catch(error => {
        console.error('搜索地址失败:', error)
        this.addressSuggestions = []
        this.showAddressSuggestions = false
      })
    },
    // 地址输入框获得焦点
    onAddressFocus() {
      if (this.addressValue && this.addressSuggestions.length > 0) {
        this.showAddressSuggestions = true
      }
    },
    // 选择地址建议
    selectAddressSuggestion(item) {
      const fullAddress = item.district + item.address
      this.$emit('input', {
        ...this.value,
        address: fullAddress
      })
      this.$emit('address-selected', {
        address: fullAddress,
        location: item.location
      })
      this.showAddressSuggestions = false
      this.addressSuggestions = []
    },
    // 科室选择变化(picker)
    onDepartmentChange(e) {
      const index = e.detail.value
      const selected = this.departmentOptions[index]
      const updatedValue = {
        ...this.value,
        department: selected.text,
        departmentId: selected.id
      }
      this.$emit('input', updatedValue)
      this.$emit('department-change', {
        department: selected.text,
        departmentId: selected.id
      })
    },
    // 科室输入(手动输入)
    onDepartmentInput(e) {
      const department = e.detail.value
      this.$emit('input', {
        ...this.value,
        department: department
      })
      this.$emit('department-change', department)
    },
    // 床号输入
    onBedNumberInput(e) {
      const bedNumber = e.detail.value
      this.$emit('input', {
        ...this.value,
        bedNumber: bedNumber
      })
      this.$emit('bed-number-change', bedNumber)
    }
  }
}
</script>
<style lang="scss" scoped>
.hospital-selector {
  .form-item {
    margin-bottom: 40rpx;
    .form-label {
      font-size: 28rpx;
      margin-bottom: 15rpx;
      color: #333;
      &.required::before {
        content: '*';
        color: #ff0000;
        margin-right: 5rpx;
      }
    }
    .hospital-search-container {
      position: relative;
      .form-input {
        width: 100%;
        height: 70rpx;
        padding: 0 20rpx;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        font-size: 28rpx;
        box-sizing: border-box;
      }
      .search-results {
        position: absolute;
        top: 75rpx;
        left: 0;
        right: 0;
        max-height: 400rpx;
        overflow-y: auto;
        background-color: white;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
        z-index: 100;
        .search-result-item {
          padding: 20rpx;
          border-bottom: 1rpx solid #f0f0f0;
          &:last-child {
            border-bottom: none;
          }
          &:active {
            background-color: #f5f5f5;
          }
          .hospital-name {
            font-size: 28rpx;
            color: #333;
            margin-bottom: 8rpx;
            .hospital-short {
              margin-left: 10rpx;
              font-size: 24rpx;
              color: #999;
            }
          }
          .hospital-address {
            font-size: 24rpx;
            color: #999;
          }
        }
      }
    }
    .address-input-container {
      position: relative;
      .form-input {
        width: 100%;
        height: 70rpx;
        padding: 0 20rpx;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        font-size: 28rpx;
        box-sizing: border-box;
      }
      .address-suggestions {
        position: absolute;
        top: 75rpx;
        left: 0;
        right: 0;
        max-height: 400rpx;
        overflow-y: auto;
        background-color: white;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
        z-index: 100;
        .address-suggestion-item {
          padding: 20rpx;
          border-bottom: 1rpx solid #f0f0f0;
          &:last-child {
            border-bottom: none;
          }
          &:active {
            background-color: #f5f5f5;
          }
          .suggestion-name {
            font-size: 28rpx;
            color: #333;
            margin-bottom: 8rpx;
          }
          .suggestion-address {
            font-size: 24rpx;
            color: #999;
          }
        }
      }
    }
    .form-input {
      width: 100%;
      height: 70rpx;
      padding: 0 20rpx;
      border: 1rpx solid #eee;
      border-radius: 10rpx;
      font-size: 28rpx;
      box-sizing: border-box;
      &.picker-input {
        display: flex;
        align-items: center;
        justify-content: space-between;
      }
      &.disabled {
        background-color: #f5f5f5;
        color: #999;
      }
    }
  }
}
</style>
app/components/OrganizationSelector.vue
New file
@@ -0,0 +1,215 @@
<template>
  <view class="organization-selector">
    <view class="form-label" :class="{ required: required }" v-if="showLabel">{{ label }}</view>
    <picker
      mode="selector"
      :range="organizations"
      :value="selectedIndex"
      @change="onOrganizationChange"
      :disabled="disabled"
    >
      <view class="form-input picker-input" :class="{ disabled: disabled }">
        {{ displayText }}
        <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
      </view>
    </picker>
  </view>
</template>
<script>
import { listBranchCompany } from "@/api/system/dept"
import { mapState } from 'vuex'
export default {
  name: 'OrganizationSelector',
  props: {
    // 标签文本
    label: {
      type: String,
      default: '归属机构'
    },
    // 是否显示标签
    showLabel: {
      type: Boolean,
      default: true
    },
    // 是否必填
    required: {
      type: Boolean,
      default: false
    },
    // 当前选中的归属机构ID
    value: {
      type: [Number, String],
      default: null
    },
    // 占位符
    placeholder: {
      type: String,
      default: '请选择归属机构'
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      default: false
    },
    // 是否自动选择当前用户的分公司
    autoSelectUserDept: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      organizations: [],
      organizationOptions: [],
      selectedIndex: -1
    }
  },
  computed: {
    ...mapState({
      currentUser: state => ({
        branchCompanyId: state.user.branchCompanyId,
        branchCompanyName: state.user.branchCompanyName
      })
    }),
    displayText() {
      if (this.selectedIndex >= 0 && this.selectedIndex < this.organizations.length) {
        return this.organizations[this.selectedIndex]
      }
      return this.placeholder
    }
  },
  watch: {
    value: {
      immediate: true,
      handler(newVal) {
        if (newVal && this.organizationOptions.length > 0) {
          const index = this.organizationOptions.findIndex(org => org.deptId === newVal)
          this.selectedIndex = index
        }
      }
    }
  },
  mounted() {
    this.loadOrganizations()
  },
  methods: {
    // 加载分公司数据(parent_id=100的部门)
    loadOrganizations() {
      return listBranchCompany().then(response => {
        // console.log('加载分公司数据:', response.data);
        const list = response.data || []
        // 过滤出 parent_id = 100 的部门(分公司)
        this.organizationOptions = list.filter(dept => dept.parentId == "100")
        // 生成picker的数据源(只显示名称)
        this.organizations = this.organizationOptions.map(dept => dept.deptName)
        // 自动选择用户的分公司
        if (this.autoSelectUserDept && !this.value && this.currentUser.branchCompanyName) {
          this.selectUserBranchCompany()
        } else if (this.value) {
          // 如果有传入的value,设置选中索引
          const index = this.organizationOptions.findIndex(org => org.deptId === this.value)
          this.selectedIndex = index
        }
        return this.organizationOptions
      }).catch(error => {
        console.error('加载分公司数据失败:', error)
        this.organizations = []
        this.organizationOptions = []
        return []
      })
    },
    // 选择用户所属的分公司
    selectUserBranchCompany() {
      if (!this.currentUser.branchCompanyName) {
        return
      }
      const index = this.organizationOptions.findIndex(
        dept => dept.deptName === this.currentUser.branchCompanyName
      )
      if (index !== -1) {
        this.selectedIndex = index
        this.emitChange(this.organizationOptions[index])
      }
    },
    // 归属机构选择变化
    onOrganizationChange(e) {
      const index = e.detail.value
      this.selectedIndex = index
      const selectedOrg = this.organizationOptions[index]
      this.emitChange(selectedOrg)
    },
    // 触发事件
    emitChange(organization) {
      if (organization) {
        // 提取地域关键词(去除"分公司"、"总公司"、"总部"后缀)
        const region = organization.deptName.replace(/(分公司|总公司|总部)$/g, '').trim()
        this.$emit('input', organization.deptId)
        this.$emit('change', {
          deptId: organization.deptId,
          deptName: organization.deptName,
          serviceOrderClass: organization.serviceOrderClass || '',
          region: region
        })
      }
    },
    // 重新加载归属机构列表(供外部调用)
    reload() {
      return this.loadOrganizations()
    },
    // 获取当前选中的组织信息(供外部调用)
    getSelectedOrganization() {
      if (this.selectedIndex >= 0 && this.selectedIndex < this.organizationOptions.length) {
        return this.organizationOptions[this.selectedIndex]
      }
      return null
    }
  }
}
</script>
<style lang="scss" scoped>
.organization-selector {
  .form-label {
    font-size: 28rpx;
    margin-bottom: 15rpx;
    color: #333;
    &.required::before {
      content: '*';
      color: #ff0000;
      margin-right: 5rpx;
    }
  }
  .form-input {
    height: 70rpx;
    padding: 0 20rpx;
    border: 1rpx solid #eee;
    border-radius: 10rpx;
    font-size: 28rpx;
    &.picker-input {
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    &.disabled {
      background-color: #f5f5f5;
      color: #999;
    }
  }
}
</style>
app/components/TaskTypeSelector.vue
New file
@@ -0,0 +1,181 @@
<template>
  <view class="task-type-selector">
    <view class="form-label" v-if="label">{{ label }}</view>
    <picker
      mode="selector"
      :range="taskTypeLabels"
      :value="selectedIndex"
      @change="onTaskTypeChange"
      :disabled="disabled"
    >
      <view class="form-input picker-input" :class="{ disabled: disabled }">
        {{ displayText }}
        <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
      </view>
    </picker>
  </view>
</template>
<script>
import { getDicts } from "@/api/dict"
export default {
  name: 'TaskTypeSelector',
  props: {
    // 标签文本
    label: {
      type: String,
      default: '任务类型'
    },
    // 当前选中的任务类型值
    value: {
      type: String,
      default: ''
    },
    // 字典类型(sys_task_type)
    dictType: {
      type: String,
      default: 'sys_task_type'
    },
    // 占位符
    placeholder: {
      type: String,
      default: '请选择任务类型'
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      default: false
    },
    // 过滤条件(可选)- 只显示特定的任务类型
    filter: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      taskTypeOptions: [],
      taskTypeLabels: [],
      selectedIndex: -1
    }
  },
  computed: {
    displayText() {
      if (this.selectedIndex >= 0 && this.selectedIndex < this.taskTypeLabels.length) {
        return this.taskTypeLabels[this.selectedIndex]
      }
      return this.placeholder
    }
  },
  watch: {
    value: {
      immediate: true,
      handler(newVal) {
        if (newVal && this.taskTypeOptions.length > 0) {
          const index = this.taskTypeOptions.findIndex(item => item.value === newVal)
          this.selectedIndex = index
        }
      }
    }
  },
  mounted() {
    this.loadTaskTypes()
  },
  methods: {
    // 加载任务类型字典
    loadTaskTypes() {
      return getDicts(this.dictType).then(response => {
        const dictData = response.data || []
        let options = dictData.map(item => ({
          label: item.dictLabel,
          value: item.dictValue
        }))
        // 如果有过滤条件,只保留指定的类型
        if (this.filter && this.filter.length > 0) {
          options = options.filter(item => this.filter.includes(item.value))
        }
        this.taskTypeOptions = options
        this.taskTypeLabels = options.map(item => item.label)
        // 如果有预设值,设置选中索引
        if (this.value) {
          const index = this.taskTypeOptions.findIndex(item => item.value === this.value)
          this.selectedIndex = index
        }
        return options
      }).catch(error => {
        console.error('加载任务类型字典失败:', error)
        // 使用默认值
        this.taskTypeOptions = [
          { label: '维修保养', value: 'MAINTENANCE' },
          { label: '加油任务', value: 'FUEL' },
          { label: '其他', value: 'OTHER' }
        ]
        this.taskTypeLabels = this.taskTypeOptions.map(item => item.label)
        return this.taskTypeOptions
      })
    },
    // 任务类型选择变化
    onTaskTypeChange(e) {
      const index = e.detail.value
      this.selectedIndex = index
      const selectedOption = this.taskTypeOptions[index]
      if (selectedOption) {
        this.$emit('input', selectedOption.value)
        this.$emit('change', {
          value: selectedOption.value,
          label: selectedOption.label
        })
      }
    },
    // 重新加载任务类型(供外部调用)
    reload() {
      return this.loadTaskTypes()
    },
    // 获取当前选中的任务类型对象
    getSelected() {
      if (this.selectedIndex >= 0 && this.selectedIndex < this.taskTypeOptions.length) {
        return this.taskTypeOptions[this.selectedIndex]
      }
      return null
    }
  }
}
</script>
<style lang="scss" scoped>
.task-type-selector {
  .form-label {
    font-size: 28rpx;
    margin-bottom: 15rpx;
    color: #333;
  }
  .form-input {
    height: 70rpx;
    padding: 0 20rpx;
    border: 1rpx solid #eee;
    border-radius: 10rpx;
    font-size: 28rpx;
    &.picker-input {
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    &.disabled {
      background-color: #f5f5f5;
      color: #999;
    }
  }
}
</style>
app/components/VehicleSelector.vue
New file
@@ -0,0 +1,214 @@
<template>
  <view class="vehicle-selector">
    <view class="form-label" v-if="label">{{ label }}</view>
    <picker
      mode="selector"
      :range="vehicles"
      :value="selectedIndex"
      @change="onVehicleChange"
      :disabled="disabled"
    >
      <view class="form-input picker-input" :class="{ disabled: disabled }">
        {{ displayText }}
        <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
      </view>
    </picker>
  </view>
</template>
<script>
import { listAvailableVehicles, getUserBoundVehicle } from "@/api/vehicle"
export default {
  name: 'VehicleSelector',
  props: {
    // 标签文本
    label: {
      type: String,
      default: '任务车辆'
    },
    // 当前选中的车辆ID
    value: {
      type: [Number, String],
      default: null
    },
    // 车辆类型过滤(GENERAL/EMERGENCY/WELFARE)
    vehicleType: {
      type: String,
      default: 'GENERAL'
    },
    // 部门ID
    deptId: {
      type: [Number, String],
      default: null
    },
    // 占位符
    placeholder: {
      type: String,
      default: '请选择任务车辆'
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      default: false
    },
    // 是否自动选择绑定车辆
    autoSelectBound: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      vehicles: [],
      vehicleOptions: [],
      selectedIndex: -1
    }
  },
  computed: {
    displayText() {
      if (this.selectedIndex >= 0 && this.selectedIndex < this.vehicles.length) {
        return this.vehicles[this.selectedIndex]
      }
      return this.placeholder
    }
  },
  watch: {
    value: {
      immediate: true,
      handler(newVal) {
        if (newVal && this.vehicleOptions.length > 0) {
          const index = this.vehicleOptions.findIndex(v => v.id === newVal)
          this.selectedIndex = index
        }
      }
    },
    deptId: {
      immediate: true,
      handler(newVal) {
        if (newVal) {
          this.loadVehicles()
        }
      }
    }
  },
  mounted() {
    if (this.deptId) {
      this.loadVehicles()
    }
  },
  methods: {
    // 加载车辆列表
    loadVehicles() {
      if (!this.deptId) {
        console.warn('VehicleSelector: deptId is required')
        return Promise.reject('deptId is required')
      }
      return listAvailableVehicles(this.deptId, this.vehicleType).then(response => {
        const vehicleList = response.data || response.rows || []
        this.vehicleOptions = vehicleList.map(vehicle => ({
          id: vehicle.vehicleId,
          name: vehicle.vehicleNo,
          type: vehicle.vehicleType,
          status: vehicle.status
        }))
        this.vehicles = this.vehicleOptions.map(v => v.name)
        // 自动选择绑定车辆
        if (this.autoSelectBound && !this.value) {
          this.selectBoundVehicle()
        } else if (this.value) {
          // 如果有传入的value,设置选中索引
          const index = this.vehicleOptions.findIndex(v => v.id === this.value)
          this.selectedIndex = index
        }
        return vehicleList
      }).catch(error => {
        console.error('加载车辆列表失败:', error)
        this.vehicles = []
        this.vehicleOptions = []
        return []
      })
    },
    // 选择绑定的车辆
    selectBoundVehicle() {
      getUserBoundVehicle().then(response => {
        const userInfo = response.data || response
        if (userInfo.boundVehicle) {
          const boundVehicleNo = userInfo.boundVehicle.vehicleNumber
          const boundVehicleId = userInfo.boundVehicle.vehicleId
          const vehicleIndex = this.vehicleOptions.findIndex(v =>
            v.id === boundVehicleId || v.name === boundVehicleNo
          )
          if (vehicleIndex !== -1) {
            this.selectedIndex = vehicleIndex
            this.emitChange(this.vehicleOptions[vehicleIndex])
          }
        }
      }).catch(error => {
        console.error('获取用户绑定车辆失败:', error)
      })
    },
    // 车辆选择变化
    onVehicleChange(e) {
      const index = e.detail.value
      this.selectedIndex = index
      const selectedVehicle = this.vehicleOptions[index]
      this.emitChange(selectedVehicle)
    },
    // 触发事件
    emitChange(vehicle) {
      if (vehicle) {
        this.$emit('input', vehicle.id)
        this.$emit('change', {
          vehicleId: vehicle.id,
          vehicleName: vehicle.name,
          vehicleType: vehicle.type,
          vehicleStatus: vehicle.status
        })
      }
    },
    // 重新加载车辆列表(供外部调用)
    reload() {
      return this.loadVehicles()
    }
  }
}
</script>
<style lang="scss" scoped>
.vehicle-selector {
  .form-label {
    font-size: 28rpx;
    margin-bottom: 15rpx;
    color: #333;
  }
  .form-input {
    height: 70rpx;
    padding: 0 20rpx;
    border: 1rpx solid #eee;
    border-radius: 10rpx;
    font-size: 28rpx;
    &.picker-input {
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    &.disabled {
      background-color: #f5f5f5;
      color: #999;
    }
  }
}
</style>
app/mixins/distanceCalculator.js
New file
@@ -0,0 +1,141 @@
/**
 * 距离计算 Mixin
 * 提供地址选择和距离自动计算功能
 */
import { calculateDistance } from "@/api/map"
export default {
  data() {
    return {
      // 地址坐标存储
      addressCoordinates: {
        start: null,
        end: null
      },
      // 当前正在选择的地址类型
      currentAddressType: '',
      // 计算出的距离
      calculatedDistance: null
    }
  },
  methods: {
    /**
     * 设置起点坐标
     * @param {Object} location - 位置对象 { lat, lng }
     */
    setStartLocation(location) {
      this.addressCoordinates.start = location
      this.autoCalculateDistance()
    },
    /**
     * 设置终点坐标
     * @param {Object} location - 位置对象 { lat, lng }
     */
    setEndLocation(location) {
      this.addressCoordinates.end = location
      this.autoCalculateDistance()
    },
    /**
     * 通过地址选择结果设置坐标
     * @param {String} type - 地址类型 'start' 或 'end'
     * @param {Object} address - 地址对象 { title, address, lat, lng }
     */
    setLocationByAddress(type, address) {
      const location = {
        lat: address.lat,
        lng: address.lng
      }
      if (type === 'start') {
        this.setStartLocation(location)
      } else if (type === 'end') {
        this.setEndLocation(location)
      }
      return location
    },
    /**
     * 自动计算距离(当起点和终点都存在时)
     */
    autoCalculateDistance() {
      if (this.addressCoordinates.start && this.addressCoordinates.end) {
        return this.getDistanceBetweenPoints(
          this.addressCoordinates.start.lat,
          this.addressCoordinates.start.lng,
          this.addressCoordinates.end.lat,
          this.addressCoordinates.end.lng
        ).then(distance => {
          this.calculatedDistance = distance
          // 触发距离计算完成事件
          this.$emit && this.$emit('distance-calculated', distance)
          return distance
        }).catch(error => {
          console.error('距离计算失败:', error)
          return null
        })
      }
      return Promise.resolve(null)
    },
    /**
     * 计算两点之间的距离(公里)
     * @param {Number} lat1 - 起点纬度
     * @param {Number} lng1 - 起点经度
     * @param {Number} lat2 - 终点纬度
     * @param {Number} lng2 - 终点经度
     * @returns {Promise<Number>} 距离(公里)
     */
    getDistanceBetweenPoints(lat1, lng1, lat2, lng2) {
      return new Promise((resolve, reject) => {
        calculateDistance(lat1, lng1, lat2, lng2).then(response => {
          if (response.code === 200) {
            const responseData = typeof response.data === 'string'
              ? JSON.parse(response.data)
              : response.data
            if (responseData &&
                responseData.status === 0 &&
                responseData.result &&
                responseData.result.elements &&
                responseData.result.elements.length > 0) {
              const distanceInKm = responseData.result.elements[0].distance / 1000
              resolve(distanceInKm)
            } else {
              reject(new Error('距离计算接口返回数据格式不正确'))
            }
          } else {
            reject(new Error('距离计算接口调用失败'))
          }
        }).catch(error => {
          reject(error)
        })
      })
    },
    /**
     * 清空地址坐标
     */
    clearAddressCoordinates() {
      this.addressCoordinates = {
        start: null,
        end: null
      }
      this.calculatedDistance = null
    },
    /**
     * 格式化距离显示(保留2位小数)
     * @param {Number} distance - 距离值
     * @returns {String} 格式化后的距离
     */
    formatDistance(distance) {
      if (distance === null || distance === undefined) {
        return ''
      }
      return parseFloat(distance).toFixed(2)
    }
  }
}
app/pages.json
@@ -121,6 +121,21 @@
      "navigationBarTitleText": "任务详情"
    }
  }, {
    "path": "pages/task/edit",
    "style": {
      "navigationBarTitleText": "编辑任务"
    }
  }, {
    "path": "pages/task/edit-emergency",
    "style": {
      "navigationBarTitleText": "编辑转运任务"
    }
  }, {
    "path": "pages/task/edit-welfare",
    "style": {
      "navigationBarTitleText": "编辑福祗车任务"
    }
  }, {
    "path": "pages/task/settlement",
    "style": {
      "navigationBarTitleText": "任务结算"
app/pages/task/create-emergency.vue
@@ -22,13 +22,12 @@
        </picker>
      </view>
        <view class="form-item">
        <view class="form-label required">归属机构</view>
        <picker mode="selector" :range="organizations" @change="onOrganizationChange">
          <view class="form-input picker-input">
            {{ selectedOrganization || '请选择归属机构' }}
            <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
          </view>
        </picker>
        <OrganizationSelector
          v-model="selectedOrganizationId"
          :required="true"
          :auto-select-user-dept="true"
          @change="onOrganizationChange"
        />
      </view>
      
      <view class="form-item">
@@ -144,200 +143,38 @@
        />
      </view>
      
      <view class="form-item">
        <view class="form-label">病情</view>
        <view class="disease-container">
          <view class="disease-tags" v-if="selectedDiseases.length > 0">
            <view
              class="disease-tag"
              v-for="(disease, index) in selectedDiseases"
              :key="index"
            >
              <text class="disease-name">{{ disease.icdName }}</text>
              <uni-icons
                type="closeempty"
                size="16"
                color="#fff"
                @click="removeDisease(index)"
              ></uni-icons>
            </view>
          </view>
          <view class="add-disease-btn" @click="showDiseaseSelector">
            <uni-icons type="plusempty" size="20" color="#007AFF"></uni-icons>
            <text>添加病情</text>
          </view>
          <textarea
            class="form-textarea"
            placeholder="其他病情描述(选填)"
            v-model="taskForm.patient.condition"
            style="margin-top: 20rpx;"
          />
        </view>
      </view>
      <DiseaseSelector
        v-model="selectedDiseases"
        :other-description.sync="taskForm.patient.condition"
      />
      
      <view class="form-section-title">转出医院信息</view>
      <view class="form-item">
        <view class="form-label required">医院名称</view>
        <view class="hospital-search-container">
          <input
            class="form-input"
            placeholder="请输入医院名称或地址搜索"
            v-model="hospitalOutSearchKeyword"
            @input="onHospitalOutSearch"
            @focus="onHospitalOutFocus"
          />
          <view class="search-results" v-if="showHospitalOutResults && hospitalOutResults.length > 0">
            <view
              class="search-result-item"
              v-for="hospital in hospitalOutResults"
              :key="hospital.hospId"
              @click="selectHospitalOut(hospital)"
            >
              <view class="hospital-name">
                {{ hospital.hospName }}
                <text class="hospital-short" v-if="hospital.hospShort">{{ hospital.hospShort }}</text>
              </view>
              <view class="hospital-address">{{ buildFullAddress(hospital) }}</view>
            </view>
          </view>
        </view>
      </view>
      <view class="form-item">
        <view class="form-label required">科室</view>
        <picker
          v-if="taskForm.hospitalOut.name !== '家中'"
          mode="selector"
          :range="departmentOptions"
          range-key="text"
          @change="onHospitalOutDepartmentChange"
        >
          <view class="form-input picker-input">
            {{ taskForm.hospitalOut.department || '请选择科室' }}
            <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
          </view>
        </picker>
        <view v-else class="form-input picker-input disabled">
          其它
        </view>
      </view>
      <view class="form-item">
        <view class="form-label">床号</view>
        <input
          class="form-input"
          placeholder="请输入床号"
          v-model="taskForm.hospitalOut.bedNumber"
        />
      </view>
      <view class="form-item">
        <view class="form-label">转出地址</view>
        <view class="address-input-container" v-if="taskForm.hospitalOut.name === '家中'">
          <input
            class="form-input"
            placeholder="请输入详细地址"
            v-model="taskForm.hospitalOut.address"
            @input="onAddressOutInput"
            @focus="onAddressOutFocus"
          />
          <view class="address-suggestions" v-if="showAddressOutSuggestions && addressOutSuggestions.length > 0">
            <view
              class="address-suggestion-item"
              v-for="(item, index) in addressOutSuggestions"
              :key="index"
              @click="selectAddressOut(item)">
              <view class="suggestion-name">{{ item.name }}</view>
              <view class="suggestion-address">{{ item.district }}{{ item.address }}</view>
            </view>
          </view>
        </view>
        <view v-else class="form-input picker-input disabled">
          {{ taskForm.hospitalOut.address || '选择医院后自动填充' }}
        </view>
      </view>
      <HospitalSelector
        label="医院名称"
        address-label="转出地址"
        :required="true"
        :department-required="true"
        v-model="taskForm.hospitalOut"
        :dept-id="selectedOrganizationId"
        :region="selectedRegion"
        :department-options="departmentOptions"
        @change="onHospitalOutChange"
        @address-selected="onHospitalOutAddressSelected"
      />
      
      <view class="form-section-title">转入医院信息</view>
      <view class="form-item">
        <view class="form-label required">医院名称</view>
        <view class="hospital-search-container">
          <input
            class="form-input"
            placeholder="请输入医院名称或地址搜索"
            v-model="hospitalInSearchKeyword"
            @input="onHospitalInSearch"
            @focus="onHospitalInFocus"
          />
          <view class="search-results" v-if="showHospitalInResults && hospitalInResults.length > 0">
            <view
              class="search-result-item"
              v-for="hospital in hospitalInResults"
              :key="hospital.hospId"
              @click="selectHospitalIn(hospital)"
            >
              <view class="hospital-name">
                {{ hospital.hospName }}
                <text class="hospital-short" v-if="hospital.hospShort">{{ hospital.hospShort }}</text>
              </view>
              <view class="hospital-address">{{ buildFullAddress(hospital) }}</view>
            </view>
          </view>
        </view>
      </view>
      <view class="form-item">
        <view class="form-label required">科室</view>
        <picker
          v-if="taskForm.hospitalIn.name !== '家中'"
          mode="selector"
          :range="departmentOptions"
          range-key="text"
          @change="onHospitalInDepartmentChange"
        >
          <view class="form-input picker-input">
            {{ taskForm.hospitalIn.department || '请选择科室' }}
            <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
          </view>
        </picker>
        <view v-else class="form-input picker-input disabled">
          其它
        </view>
      </view>
      <view class="form-item">
        <view class="form-label">床号</view>
        <input
          class="form-input"
          placeholder="请输入床号"
          v-model="taskForm.hospitalIn.bedNumber"
        />
      </view>
      <view class="form-item">
        <view class="form-label">转入地址</view>
        <view class="address-input-container" v-if="taskForm.hospitalIn.name === '家中'">
          <input
            class="form-input"
            placeholder="请输入详细地址"
            v-model="taskForm.hospitalIn.address"
            @input="onAddressInInput"
            @focus="onAddressInFocus"
          />
          <view class="address-suggestions" v-if="showAddressInSuggestions && addressInSuggestions.length > 0">
            <view
              class="address-suggestion-item"
              v-for="(item, index) in addressInSuggestions"
              :key="index"
              @click="selectAddressIn(item)">
              <view class="suggestion-name">{{ item.name }}</view>
              <view class="suggestion-address">{{ item.district }}{{ item.address }}</view>
            </view>
          </view>
        </view>
        <view v-else class="form-input picker-input disabled">
          {{ taskForm.hospitalIn.address || '选择医院后自动填充' }}
        </view>
      </view>
      <HospitalSelector
        label="医院名称"
        address-label="转入地址"
        :required="true"
        :department-required="true"
        v-model="taskForm.hospitalIn"
        :dept-id="selectedOrganizationId"
        :region="selectedRegion"
        :department-options="departmentOptions"
        @change="onHospitalInChange"
        @address-selected="onHospitalInAddressSelected"
      />
      
      <view class="form-item">
        <view class="form-label required">转运公里数</view>
@@ -475,64 +312,6 @@
        </view>
      </view>
    </uni-popup>
    <!-- 病情选择弹窗 -->
    <uni-popup ref="diseasePopup" type="bottom" :safe-area="true">
      <view class="disease-selector-popup">
        <view class="popup-header">
          <view class="popup-title">选择病情(ICD-10)</view>
          <view class="popup-close" @click="closeDiseaseSelector">
            <uni-icons type="closeempty" size="24" color="#333"></uni-icons>
          </view>
        </view>
        <view class="search-box">
          <uni-icons type="search" size="18" color="#999"></uni-icons>
          <input
            class="search-input"
            placeholder="搜索疾病名称、编码或助记码"
            v-model="diseaseSearchKeyword"
            @input="onDiseaseSearch"
          />
        </view>
        <scroll-view class="disease-list-popup" scroll-y="true">
          <view
            class="disease-item-popup"
            v-for="disease in diseaseSearchResults"
            :key="disease.id"
            @click="toggleDiseaseSelection(disease)"
          >
            <view class="disease-info">
              <view class="disease-name-row">
                <text class="disease-name">{{ disease.icdName }}</text>
                <text class="disease-code">[{{ disease.icdCode }}]</text>
              </view>
              <view class="disease-detail-row" v-if="disease.sm">
                <text class="disease-desc">{{ disease.sm }}</text>
              </view>
            </view>
            <uni-icons
              v-if="isDiseaseSelected(disease.id)"
              type="checkmarkempty"
              size="24"
              color="#007AFF"
            ></uni-icons>
            <view v-else class="checkbox-empty"></view>
          </view>
          <view class="no-data" v-if="diseaseSearchResults.length === 0">
            <uni-icons type="info" size="40" color="#ccc"></uni-icons>
            <text>{{ diseaseSearchKeyword ? '未找到相关疾病' : '暂无病情数据' }}</text>
          </view>
        </scroll-view>
        <view class="popup-footer">
          <button class="cancel-btn" @click="closeDiseaseSelector">取消</button>
          <button class="confirm-btn" @click="confirmDiseaseSelection">确定(已选{{ tempSelectedDiseases.length }})</button>
        </view>
      </view>
    </uni-popup>
  </scroll-view>
</template>
@@ -553,18 +332,23 @@
import { getServiceOrdAreaTypes, getServiceOrderTypes, getHospitalDepartments } from "@/api/dictionary"
import { listBranchCompany, getDept } from "@/api/system/dept"
import MapSelector from '@/components/map-selector.vue'
import OrganizationSelector from '@/components/OrganizationSelector.vue'
import HospitalSelector from '@/components/HospitalSelector.vue'
import DiseaseSelector from '@/components/DiseaseSelector.vue'
export default {
  components: {
    uniDatetimePicker,
    uniPopup,
    MapSelector
    MapSelector,
    OrganizationSelector,
    HospitalSelector,
    DiseaseSelector
  },
  data() {
    return {
      selectedVehicle: '',
      selectedVehicleId: null,
      selectedOrganization: '',
      selectedOrganizationId: null, // 归属机构ID(部门ID)
      selectedOrganizationServiceOrderClass: '', // 归属机构的服务单编码
      selectedRegion: '', // 从归属机构中提取的地域信息(如:广州、深圳等)
@@ -573,21 +357,6 @@
      selectedDocumentType: '', // 选中的单据类型文本
      selectedDocumentTypeId: null, // 选中的单据类型ID
      mapSelectorType: '',
      // 医院搜索相关
      hospitalOutSearchKeyword: '',
      hospitalOutResults: [],
      showHospitalOutResults: false,
      hospitalInSearchKeyword: '',
      hospitalInResults: [],
      showHospitalInResults: false,
      searchTimer: null,
      defaultHospitals: [], // 默认的100条医院数据
      // 地址搜索提示相关
      addressOutSuggestions: [], // 转出地址提示列表
      showAddressOutSuggestions: false,
      addressInSuggestions: [], // 转入地址提示列表
      showAddressInSuggestions: false,
      addressSearchTimer: null, // 地址搜索防抖定时器
      // 人员选择相关
      selectedStaff: [], // 已选择的人员列表
      allStaffList: [], // 所有人员列表
@@ -596,10 +365,6 @@
      staffFilterType: 'driver', // 人员筛选类型:driver/doctor/nurse,默认选中司机
      // 病情选择相关
      selectedDiseases: [], // 已选择的病情列表
      tempSelectedDiseases: [], // 临时选择的病情列表(用于弹窗)
      diseaseSearchKeyword: '', // 病情搜索关键词
      diseaseSearchResults: [], // 病情搜索结果
      diseaseSearchTimer: null, // 病情搜索防抖定时器
      taskForm: {
        transferTime: '',
        patient: {
@@ -631,8 +396,7 @@
      },
      vehicles: [],
      vehicleOptions: [],
      organizations: [], // 归属机构列表(从后台加载分公司数据)
      organizationOptions: [], // 归属机构选项(用于picker显示)
      emergencyTaskTypes: [], // 任务类型列表(从 SQL Server 动态加载)
      emergencyTaskTypeOptions: [], // 任务类型选项(用于picker显示)
      documentTypes: [], // 单据类型列表
@@ -680,8 +444,6 @@
    })
    this.initSelectedStaff()
    this.loadDeptStaff()
    // 加载分公司数据(会自动设置默认分公司并加载医院列表)
    this.loadBranchCompanies()
    // 加载科室字典数据
    this.loadDepartments()
    // 加载任务类型数据
@@ -754,53 +516,17 @@
      this.selectedVehicleId = this.vehicleOptions[index]?.id
    },
    
    onOrganizationChange(e) {
      const index = e.detail.value
      const selected = this.organizationOptions[index]
      this.selectedOrganization = selected.deptName
      this.selectedOrganizationId = selected.deptId // 保存部门ID
      this.selectedOrganizationServiceOrderClass = selected.serviceOrderClass || '' // 保存服务单编码
      // 从归属机构中提取地域关键词(去除“分公司”后缀)
      // 例如:“广州分公司” -> “广州”
      //如果出现广州总公司,也要去除“总公司”后缀
      this.selectedRegion = this.replaceRegion(selected.deptName);
      // 重新加载医院列表(带地域过滤)
      this.loadDefaultHospitals()
    onOrganizationChange(orgData) {
      // orgData 包含:deptId, deptName, serviceOrderClass, region
      this.selectedOrganizationId = orgData.deptId
      this.selectedOrganizationServiceOrderClass = orgData.serviceOrderClass
      this.selectedRegion = orgData.region
      console.log('选中归属机构:', orgData.deptName, '部门ID:', orgData.deptId, '服务单编码:', orgData.serviceOrderClass, '地域:', orgData.region)
    },
    replaceRegion(region){
        return region.replace(/(分公司|总公司|总部)$/g, '').trim();
    },
    // 加载分公司数据(parent_id=100的部门)
    loadBranchCompanies() {
      listBranchCompany().then(response => {
        const list = response.data || []
        // 过滤出 parent_id = 100 的部门(分公司)
        this.organizationOptions = list.filter(dept => dept.parentId === 100)
        // 生成picker的数据源(只显示名称)
        this.organizations = this.organizationOptions.map(dept => dept.deptName)
        // 默认设置为当前用户的分公司
        if (this.currentUser.branchCompanyName) {
          const index = this.organizationOptions.findIndex(
            dept => dept.deptName === this.currentUser.branchCompanyName
          )
          if (index !== -1) {
            this.selectedOrganization = this.currentUser.branchCompanyName
            this.selectedOrganizationId = this.organizationOptions[index].deptId // 保存部门ID
            this.selectedOrganizationServiceOrderClass = this.organizationOptions[index].serviceOrderClass || '' // 保存服务单编码
            // 提取地域关键词
            this.selectedRegion =this.replaceRegion(this.selectedOrganization);
            console.log('默认选中归属机构:', this.selectedOrganization, '部门ID:', this.selectedOrganizationId, '服务单编码:', this.selectedOrganizationServiceOrderClass, '地域:', this.selectedRegion)
            // 加载医院列表(带地域过滤)
            this.loadDefaultHospitals()
          }
        }
      }).catch(error => {
        console.error('加载分公司数据失败:', error)
        this.organizationOptions = []
        this.organizations = []
      })
    },
    
    // 加载科室数据(从 SQL Server 动态加载)
    loadDepartments() {
@@ -892,243 +618,15 @@
      this.selectedDocumentTypeId = selected.id
    },
    
    // 转出医院科室选择
    onHospitalOutDepartmentChange(e) {
      const index = e.detail.value
      const selected = this.departmentOptions[index]
      this.taskForm.hospitalOut.department = selected.text  // 保存科室名称
      this.taskForm.hospitalOut.departmentId = selected.id  // 保存科室ID
    },
    // 转入医院科室选择
    onHospitalInDepartmentChange(e) {
      const index = e.detail.value
      const selected = this.departmentOptions[index]
      this.taskForm.hospitalIn.department = selected.text  // 保存科室名称
      this.taskForm.hospitalIn.departmentId = selected.id  // 保存科室ID
    },
    // 加载默认医院列表(常用医院)
    loadDefaultHospitals() {
      // 检查是否有归属机构ID
      if (!this.selectedOrganizationId) {
        console.warn('未选择归属机构,无法加载医院列表')
        return
      }
      // 转出医院:根据归属机构的区域配置加载
      this.loadHospitalsByDeptRegion('out')
      // 转入医院:根据归属机构的区域配置加载
      this.loadHospitalsByDeptRegion('in')
    },
    // 降级加载医院(按地域过滤)
    loadDefaultHospitalsByRegion() {
      // 转出医院:只加载当前区域的医院(带地域过滤)
      searchHospitals('', this.selectedRegion).then(response => {
        this.hospitalOutResults = response.data || []
        console.log('加载转出医院(当前区域):', this.selectedRegion, '数量:', this.hospitalOutResults.length)
      }).catch(error => {
        console.error('加载转出医院列表失败:', error)
        this.hospitalOutResults = []
      })
      // 转入医院:加载所有医院(不带地域过滤,后续会按地域排序)
      searchHospitals('', '').then(response => {
        const allHospitals = response.data || []
        // 将医院按地域排序:本地区域优先
        this.hospitalInResults = this.sortHospitalsByRegion(allHospitals)
        console.log('加载转入医院(全部区域):', '数量:', this.hospitalInResults.length)
      }).catch(error => {
        console.error('加载转入医院列表失败:', error)
        this.hospitalInResults = []
      })
    },
    // 根据部门区域配置加载医院
    loadHospitalsByDeptRegion(type) {
      const deptId = this.selectedOrganizationId
      if (!deptId) {
        console.warn('部门ID不存在')
        return
      }
      // 调用后端接口,根据部门的区域配置查询医院
      searchHospitalsByDeptRegion('', deptId).then(response => {
        const hospitals = response.data || []
        if (type === 'out') {
          this.hospitalOutResults = hospitals
          console.log('加载转出医院(区域配置):部门', deptId, '数量:', this.hospitalOutResults.length)
        } else if (type === 'in') {
          // 转入医院按地域排序
          this.hospitalInResults = this.sortHospitalsByRegion(hospitals)
          console.log('加载转入医院(区域配置):部门', deptId, '数量:', this.hospitalInResults.length)
        }
      }).catch(error => {
        console.error('加载医院失败(区域配置):', error)
        // 失败后降级为普通搜索
        this.loadDefaultHospitalsByRegion()
      })
    },
    // 按地域排序医院:本地区域优先,"家中"始终在最前面
    sortHospitalsByRegion(hospitals) {
      if (!hospitals || hospitals.length === 0) {
        return hospitals
      }
      const region = this.selectedRegion
      const homeHospital = []  // "家中"
      const localHospitals = []  // 本地医院
      const otherHospitals = []  // 其他医院
      hospitals.forEach(hospital => {
        // "家中"优先处理,放在最前面
        if (hospital.hospName === '家中') {
          homeHospital.push(hospital)
          return
        }
        // 判断医院是否在本地区域(省、市、区任一包含地域关键词)
        const isLocal = region && (
          (hospital.hopsProvince && hospital.hopsProvince.includes(region)) ||
          (hospital.hopsCity && hospital.hopsCity.includes(region)) ||
          (hospital.hopsArea && hospital.hopsArea.includes(region))
        )
        if (isLocal) {
          localHospitals.push(hospital)
        } else {
          otherHospitals.push(hospital)
        }
      })
      // "家中"在最前,本地医院其次,其他医院在后
      return [...homeHospital, ...localHospitals, ...otherHospitals]
    },
    // 转出医院输入框获得焦点
    onHospitalOutFocus() {
      // 如果没有搜索关键词,显示常用转出医院
      if (!this.hospitalOutSearchKeyword || this.hospitalOutSearchKeyword.trim() === '') {
        // 如果已经加载过常用医院,直接显示
        if (this.hospitalOutResults.length > 0) {
          this.showHospitalOutResults = true
          return
        }
        // 否则重新加载常用医院
        if (this.selectedOrganizationServiceOrderClass) {
          getFrequentOutHospitals(this.selectedOrganizationServiceOrderClass, this.selectedRegion).then(response => {
            const hospitals = response.data || []
            // 确保"家中"在最前面
            this.hospitalOutResults = this.sortHospitalsByRegion(hospitals)
            // 如果没有常用医院,降级为普通搜索
            if (this.hospitalOutResults.length === 0) {
              searchHospitals('', this.selectedOrganizationId).then(res => {
                const hospitals = res.data || []
                this.hospitalOutResults = this.sortHospitalsByRegion(hospitals)
              })
            }
          }).catch(error => {
            console.error('加载常用转出医院失败:', error)
            searchHospitals('', this.selectedOrganizationId).then(res => {
              const hospitals = res.data || []
              this.hospitalOutResults = this.sortHospitalsByRegion(hospitals)
            })
          })
        } else {
          // 没有服务单编码,使用普通搜索
          searchHospitals('', this.selectedOrganizationId).then(response => {
            const hospitals = response.data || []
            this.hospitalOutResults = this.sortHospitalsByRegion(hospitals)
          }).catch(error => {
            console.error('加载转出医院失败:', error)
            this.hospitalOutResults = []
          })
        }
      }
      this.showHospitalOutResults = true
    },
    // 转出医院搜索
    onHospitalOutSearch(e) {
      const keyword = e.detail.value
      this.hospitalOutSearchKeyword = keyword
      // 防抖处理
      if (this.searchTimer) {
        clearTimeout(this.searchTimer)
      }
      // 如果关键词为空,显示当前区域的医院
      if (!keyword || keyword.trim() === '') {
        searchHospitals('', this.selectedOrganizationId).then(response => {
          const hospitals = response.data || []
          // 确保"家中"在最前面
          this.hospitalOutResults = this.sortHospitalsByRegion(hospitals)
        }).catch(error => {
          console.error('加载转出医院失败:', error)
          this.hospitalOutResults = []
        })
        this.showHospitalOutResults = true
        return
      }
      // 有关键词时,去服务端搜索(仅限当前区域)
      this.searchTimer = setTimeout(() => {
        this.searchHospitalOut(keyword)
      }, 300)
    },
    // 搜索转出医院(仅限当前区域)
    searchHospitalOut(keyword) {
      // 传入关键词和部门ID,只搜索当前区域的医院
      searchHospitals(keyword, this.selectedOrganizationId).then(response => {
        const hospitals = response.data || []
        // 确保"家中"在最前面
        this.hospitalOutResults = this.sortHospitalsByRegion(hospitals)
        this.showHospitalOutResults = true
        console.log('搜索转出医院:', keyword, '部门ID:', this.selectedOrganizationId, '结果数:', this.hospitalOutResults.length)
      }).catch(error => {
        console.error('搜索转出医院失败:', error)
        this.hospitalOutResults = []
      })
    },
    // 选择转出医院
    selectHospitalOut(hospital) {
      this.taskForm.hospitalOut.id = hospital.hospId  // 保存医院ID
      this.taskForm.hospitalOut.name = hospital.hospName
      // 如果选择的是"家中",清空地址让用户手动输入;否则自动填充地址
      if (hospital.hospName === '家中') {
        this.taskForm.hospitalOut.address = ''
        // 科室自动设置为"其它"
        this.taskForm.hospitalOut.department = '其它'
        this.taskForm.hospitalOut.departmentId = null
      } else {
        // 合并省市区 + 详细地址
        const fullAddress = this.buildFullAddress(hospital)
        this.taskForm.hospitalOut.address = fullAddress
        // 清空科室,让用户重新选择
        if (this.taskForm.hospitalOut.department === '其它') {
          this.taskForm.hospitalOut.department = ''
          this.taskForm.hospitalOut.departmentId = null
        }
      }
      this.hospitalOutSearchKeyword = hospital.hospName
      this.showHospitalOutResults = false
      this.hospitalOutResults = []
      // 保存转出医院的城市信息
      this.taskForm.hospitalOut.city = hospital.hopsCity || ''
    // 转出医院变化
    onHospitalOutChange(hospitalData) {
      console.log('转出医院变化:', hospitalData)
      // 组件已经通过 v-model 更新了 taskForm.hospitalOut
      
      // 如果转入地址已填写,自动计算距离
      if (this.taskForm.hospitalIn.address) {
        // 如果两个都不是"家中",使用医院距离计算
        if (hospital.hospName !== '家中' && this.taskForm.hospitalIn.name !== '家中') {
        if (hospitalData.name !== '家中' && this.taskForm.hospitalIn.name !== '家中') {
          this.calculateHospitalDistance()
        } else {
          // 有一个是"家中",使用地址计算
@@ -1137,130 +635,44 @@
      }
    },
    
    // 转入医院输入框获得焦点
    onHospitalInFocus() {
      // 如果没有搜索关键词,显示常用转入医院
      if (!this.hospitalInSearchKeyword || this.hospitalInSearchKeyword.trim() === '') {
        // 如果已经加载过常用医院,直接显示
        if (this.hospitalInResults.length > 0) {
          this.showHospitalInResults = true
          return
        }
    // 转出医院地址选择(当选择"家中"时使用百度地图地址建议)
    onHospitalOutAddressSelected(data) {
      // data 包含:address, location
      if (data.location) {
        this.addressCoordinates.hospitalOutAddress = data.location
        
        // 否则重新加载常用医院
        if (this.selectedOrganizationServiceOrderClass) {
          getFrequentInHospitals(this.selectedOrganizationServiceOrderClass, '').then(response => {
            const allHospitals = response.data || []
            // 按地域排序:本地区域优先
            this.hospitalInResults = this.sortHospitalsByRegion(allHospitals)
            // 如果没有常用医院,降级为普通搜索
            if (this.hospitalInResults.length === 0) {
              searchHospitals('', null).then(res => {
                const allHospitals = res.data || []
                this.hospitalInResults = this.sortHospitalsByRegion(allHospitals)
              })
            }
          }).catch(error => {
            console.error('加载常用转入医院失败:', error)
            searchHospitals('', null).then(res => {
              const allHospitals = res.data || []
              this.hospitalInResults = this.sortHospitalsByRegion(allHospitals)
            })
          })
        } else {
          // 没有服务单编码,使用普通搜索
          searchHospitals('', null).then(response => {
            const allHospitals = response.data || []
            // 按地域排序:本地区域优先
            this.hospitalInResults = this.sortHospitalsByRegion(allHospitals)
          }).catch(error => {
            console.error('加载转入医院失败:', error)
            this.hospitalInResults = []
          })
        // 如果转入地址也已填写,自动计算距离
        if (this.taskForm.hospitalIn.address) {
          this.calculateDistanceByManualAddress()
        }
      }
      this.showHospitalInResults = true
    },
    
    // 转入医院搜索
    onHospitalInSearch(e) {
      const keyword = e.detail.value
      this.hospitalInSearchKeyword = keyword
      // 防抖处理
      if (this.searchTimer) {
        clearTimeout(this.searchTimer)
      }
      // 如果关键词为空,显示所有医院(本地区域优先)
      if (!keyword || keyword.trim() === '') {
        searchHospitals('', null).then(response => {
          const allHospitals = response.data || []
          // 按地域排序:"家中"最前,本地区域优先
          this.hospitalInResults = this.sortHospitalsByRegion(allHospitals)
        }).catch(error => {
          console.error('加载转入医院失败:', error)
          this.hospitalInResults = []
        })
        this.showHospitalInResults = true
        return
      }
      // 有关键词时,去服务端搜索(不限区域,但结果按地域排序)
      this.searchTimer = setTimeout(() => {
        this.searchHospitalIn(keyword)
      }, 300)
    },
    // 搜索转入医院(不限区域,但本地区域优先)
    searchHospitalIn(keyword) {
      // 传入关键词,不传部门ID(搜索所有区域)
      searchHospitals(keyword, null).then(response => {
        const allHospitals = response.data || []
        // 按地域排序:"家中"最前,本地区域优先
        this.hospitalInResults = this.sortHospitalsByRegion(allHospitals)
        this.showHospitalInResults = true
        console.log('搜索转入医院:', keyword, '结果数:', this.hospitalInResults.length)
      }).catch(error => {
        console.error('搜索转入医院失败:', error)
        this.hospitalInResults = []
      })
    },
    // 选择转入医院
    selectHospitalIn(hospital) {
      this.taskForm.hospitalIn.id = hospital.hospId  // 保存医院ID
      this.taskForm.hospitalIn.name = hospital.hospName
      // 如果选择的是"家中",清空地址让用户手动输入;否则自动填充地址
      if (hospital.hospName === '家中') {
        this.taskForm.hospitalIn.address = ''
        // 科室自动设置为"其它"
        this.taskForm.hospitalIn.department = '其它'
        this.taskForm.hospitalIn.departmentId = null
      } else {
        // 合并省市区 + 详细地址
        const fullAddress = this.buildFullAddress(hospital)
        this.taskForm.hospitalIn.address = fullAddress
        // 清空科室,让用户重新选择
        if (this.taskForm.hospitalIn.department === '其它') {
          this.taskForm.hospitalIn.department = ''
          this.taskForm.hospitalIn.departmentId = null
        }
      }
      this.hospitalInSearchKeyword = hospital.hospName
      this.showHospitalInResults = false
      this.hospitalInResults = []
      // 保存转入医院的城市信息
      this.taskForm.hospitalIn.city = hospital.hopsCity || ''
    // 转入医院变化
    onHospitalInChange(hospitalData) {
      console.log('转入医院变化:', hospitalData)
      // 组件已经通过 v-model 更新了 taskForm.hospitalIn
      
      // 如果转出地址已填写,自动计算距离
      if (this.taskForm.hospitalOut.address) {
        // 如果两个都不是"家中",使用医院距离计算
        if (hospital.hospName !== '家中' && this.taskForm.hospitalOut.name !== '家中') {
        if (hospitalData.name !== '家中' && this.taskForm.hospitalOut.name !== '家中') {
          this.calculateHospitalDistance()
        } else {
          // 有一个是"家中",使用地址计算
          this.calculateDistanceByManualAddress()
        }
      }
    },
    // 转入医院地址选择(当选择"家中"时使用百度地图地址建议)
    onHospitalInAddressSelected(data) {
      // data 包含:address, location
      if (data.location) {
        this.addressCoordinates.hospitalInAddress = data.location
        // 如果转出地址也已填写,自动计算距离
        if (this.taskForm.hospitalOut.address) {
          this.calculateDistanceByManualAddress()
        }
      }
@@ -1396,7 +808,7 @@
      console.log('按关键词过滤后,数量:', list.length)
      
      this.filteredStaffList = list
      console.log('最终过滤结果:', this.filteredStaffList)
      // console.log('最终过滤结果:', this.filteredStaffList)
    },
    
    // 切换人员选中状态
@@ -1443,168 +855,6 @@
    addStaff() {
      this.showStaffSelector()
    },
    // ==================== 地址输入联想相关方法 ====================
    // 转出地址输入监听
    onAddressOutInput(e) {
      const query = e.detail.value
      this.taskForm.hospitalOut.address = query
      // 防抖处理
      if (this.addressSearchTimer) {
        clearTimeout(this.addressSearchTimer)
      }
      // 如果输入为空,隐藏提示列表
      if (!query || query.trim() === '') {
        this.showAddressOutSuggestions = false
        this.addressOutSuggestions = []
        return
      }
      // 输入长度大于2才开始搜索
      if (query.trim().length < 2) {
        this.showAddressOutSuggestions = false
        return
      }
      // 延迟300ms搜索
      this.addressSearchTimer = setTimeout(() => {
        this.searchAddressOut(query)
      }, 300)
    },
    // 转出地址输入框获得焦点
    onAddressOutFocus() {
      // 如果有地址且有搜索结果,显示提示列表
      if (this.taskForm.hospitalOut.address && this.addressOutSuggestions.length > 0) {
        this.showAddressOutSuggestions = true
      }
    },
    // 搜索转出地址
    searchAddressOut(query) {
      // 获取当前区域(优先使用归属机构的区域)
      const region = this.selectedRegion || '广州'
      baiduPlaceSuggestion(query, region).then(response => {
        if (response.code === 200 && response.data) {
          this.addressOutSuggestions = response.data
          this.showAddressOutSuggestions = true
        } else {
          this.addressOutSuggestions = []
          this.showAddressOutSuggestions = false
        }
      }).catch(error => {
        console.error('搜索转出地址失败:', error)
        this.addressOutSuggestions = []
        this.showAddressOutSuggestions = false
      })
    },
    // 选择转出地址
    selectAddressOut(item) {
      // 填充完整地址
      const fullAddress = item.district + item.address
      this.taskForm.hospitalOut.address = fullAddress
      // 保存经纬度(如果有)
      if (item.location) {
        this.taskForm.hospitalOut.latitude = item.location.lat
        this.taskForm.hospitalOut.longitude = item.location.lng
      }
      // 隐藏提示列表
      this.showAddressOutSuggestions = false
      this.addressOutSuggestions = []
      // 如果转入地址也已填写,自动计算距离
      if (this.taskForm.hospitalIn.address) {
        this.calculateDistanceByManualAddress()
      }
    },
    // 转入地址输入监听
    onAddressInInput(e) {
      const query = e.detail.value
      this.taskForm.hospitalIn.address = query
      // 防抖处理
      if (this.addressSearchTimer) {
        clearTimeout(this.addressSearchTimer)
      }
      // 如果输入为空,隐藏提示列表
      if (!query || query.trim() === '') {
        this.showAddressInSuggestions = false
        this.addressInSuggestions = []
        return
      }
      // 输入长度大于2才开始搜索
      if (query.trim().length < 2) {
        this.showAddressInSuggestions = false
        return
      }
      // 延迟300ms搜索
      this.addressSearchTimer = setTimeout(() => {
        this.searchAddressIn(query)
      }, 300)
    },
    // 转入地址输入框获得焦点
    onAddressInFocus() {
      // 如果有地址且有搜索结果,显示提示列表
      if (this.taskForm.hospitalIn.address && this.addressInSuggestions.length > 0) {
        this.showAddressInSuggestions = true
      }
    },
    // 搜索转入地址
    searchAddressIn(query) {
      // 获取当前区域(优先使用归属机构的区域)
      const region = this.selectedRegion || '广州'
      baiduPlaceSuggestion(query, region).then(response => {
        if (response.code === 200 && response.data) {
          this.addressInSuggestions = response.data
          this.showAddressInSuggestions = true
        } else {
          this.addressInSuggestions = []
          this.showAddressInSuggestions = false
        }
      }).catch(error => {
        console.error('搜索转入地址失败:', error)
        this.addressInSuggestions = []
        this.showAddressInSuggestions = false
      })
    },
    // 选择转入地址
    selectAddressIn(item) {
      // 填充完整地址
      const fullAddress = item.district + item.address
      this.taskForm.hospitalIn.address = fullAddress
      // 保存经纬度(如果有)
      if (item.location) {
        this.taskForm.hospitalIn.latitude = item.location.lat
        this.taskForm.hospitalIn.longitude = item.location.lng
      }
      // 隐藏提示列表
      this.showAddressInSuggestions = false
      this.addressInSuggestions = []
      // 如果转出地址也已填写,自动计算距离
      if (this.taskForm.hospitalOut.address) {
        this.calculateDistanceByManualAddress()
      }
    },
    // 手动输入地址时计算距离
    calculateDistanceByManualAddress() {
      const fromAddress = this.taskForm.hospitalOut.address
      const toAddress = this.taskForm.hospitalIn.address
@@ -1694,107 +944,7 @@
        // 计算失败时不提示用户,允许用户手动输入
      })
    },
    // ==================== 病情选择相关方法 ====================
    // 显示病情选择弹窗
    showDiseaseSelector() {
      // 初始化临时选择列表(复制当前已选择的病情)
      this.tempSelectedDiseases = [...this.selectedDiseases]
      this.diseaseSearchKeyword = ''
      // 默认加载所有病情
      this.loadAllDiseases()
      this.$refs.diseasePopup.open()
    },
    // 关闭病情选择弹窗
    closeDiseaseSelector() {
      this.$refs.diseasePopup.close()
      this.diseaseSearchKeyword = ''
      this.diseaseSearchResults = []
      this.tempSelectedDiseases = []
    },
    // 病情搜索
    onDiseaseSearch(e) {
      const keyword = e.detail.value
      this.diseaseSearchKeyword = keyword
      // 防抖处理
      if (this.diseaseSearchTimer) {
        clearTimeout(this.diseaseSearchTimer)
      }
      // 如果关键词为空,加载所有病情
      if (!keyword || keyword.trim() === '') {
        this.loadAllDiseases()
        return
      }
      // 有关键词时进行搜索
      this.diseaseSearchTimer = setTimeout(() => {
        this.searchDiseaseByKeyword(keyword)
      }, 300)
    },
    // 加载所有病情(默认显示)
    loadAllDiseases() {
      // 使用空字符串或特殊标识符来获取所有病情
      // 如果后端不支持空查询,可以传入一个通配符如'%'或者修改后端接口
      searchIcd10('').then(response => {
        this.diseaseSearchResults = response.data || []
      }).catch(error => {
        console.error('加载病情列表失败:', error)
        this.diseaseSearchResults = []
      })
    },
    // 根据关键词搜索病情
    searchDiseaseByKeyword(keyword) {
      searchIcd10(keyword).then(response => {
        this.diseaseSearchResults = response.data || []
      }).catch(error => {
        console.error('搜索病情失败:', error)
        this.diseaseSearchResults = []
      })
    },
    // 切换病情选中状态
    toggleDiseaseSelection(disease) {
      const index = this.tempSelectedDiseases.findIndex(d => d.id === disease.id)
      if (index > -1) {
        // 已选中,移除
        this.tempSelectedDiseases.splice(index, 1)
      } else {
        // 未选中,添加
        this.tempSelectedDiseases.push({
          id: disease.id,
          icdCode: disease.icdCode,
          icdName: disease.icdName,
          sm: disease.sm
        })
      }
    },
    // 判断病情是否已选中
    isDiseaseSelected(diseaseId) {
      return this.tempSelectedDiseases.some(d => d.id === diseaseId)
    },
    // 确认病情选择
    confirmDiseaseSelection() {
      // 将临时选择的病情复制到正式列表
      this.selectedDiseases = [...this.tempSelectedDiseases]
      this.closeDiseaseSelector()
    },
    // 移除病情
    removeDisease(index) {
      this.selectedDiseases.splice(index, 1)
    },
    // 设置默认转运时间为当前时间
    // 手动输入地址时计算距离
    setDefaultTransferTime() {
      const now = new Date()
      const year = now.getFullYear()
@@ -1886,8 +1036,14 @@
      // 合并病情信息:选中的ICD-10疾病 + 其他描述
      let conditionText = ''
      if (this.selectedDiseases.length > 0) {
        const diseaseNames = this.selectedDiseases.map(d => `${d.icdName}(${d.icdCode})`).join('、')
        conditionText = diseaseNames
        // 过滤掉病情名称为空的项,并构建病情字符串
        const diseaseNames = this.selectedDiseases
          .filter(d => d.icdName && d.icdName.trim())
          .map(d => `${d.icdName}(${d.icdCode})`)
          .join('、')
        if (diseaseNames) {
          conditionText = diseaseNames
        }
      }
      if (this.taskForm.patient.condition && this.taskForm.patient.condition.trim()) {
        if (conditionText) {
@@ -1914,18 +1070,20 @@
        documentTypeId: this.selectedDocumentTypeId, // 单据类型ID
        taskTypeId: this.selectedEmergencyTaskTypeId, // 任务类型ID
        // 病情ID列表(用于同步调度单的OrdICD_ID参数)
        diseaseIds: this.selectedDiseases.map(d => d.id),
        diseaseIds: this.selectedDiseases.filter(d => d.id !== null).map(d => d.id),
        // 将转出医院地址作为出发地,转入医院地址作为目的地
        departureAddress: this.taskForm.hospitalOut.address || '',
        destinationAddress: this.taskForm.hospitalIn.address || '',
        patient: {
          ...this.taskForm.patient,
          condition: conditionText, // 使用合并后的病情信息
          diseases: this.selectedDiseases.map(d => ({
            icdId: d.id,
            icdCode: d.icdCode,
            icdName: d.icdName
          }))
          diseases: this.selectedDiseases
            .filter(d => d.icdName && d.icdName.trim())
            .map(d => ({
              icdId: d.id,
              icdCode: d.icdCode,
              icdName: d.icdName
            }))
        },
        // 医院信息(包含医院ID、科室名称、科室ID等完整信息)
        hospitalOut: this.taskForm.hospitalOut,  // 包含: id, name, department, departmentId, bedNumber, address
@@ -2989,145 +2147,6 @@
        background-color: #ccc;
        color: #999;
      }
    }
  }
}
// 病情选择弹窗样式
.disease-selector-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 {
      font-size: 32rpx;
      font-weight: bold;
      color: #333;
    }
    .popup-close {
      padding: 10rpx;
    }
  }
  .search-box {
    display: flex;
    align-items: center;
    margin: 20rpx 30rpx;
    padding: 15rpx 20rpx;
    background-color: #f5f5f5;
    border-radius: 10rpx;
    flex-shrink: 0;
    .search-input {
      flex: 1;
      margin-left: 10rpx;
      font-size: 28rpx;
    }
  }
  .disease-list-popup {
    flex: 1;
    overflow-y: auto;
    padding: 0 30rpx;
    .disease-item-popup {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 25rpx 20rpx;
      border-bottom: 1rpx solid #f0f0f0;
      &:active {
        background-color: #f5f5f5;
      }
      .disease-info {
        flex: 1;
        .disease-name-row {
          display: flex;
          align-items: center;
          margin-bottom: 8rpx;
          .disease-name {
            font-size: 30rpx;
            font-weight: bold;
            color: #333;
            margin-right: 15rpx;
          }
          .disease-code {
            font-size: 24rpx;
            color: #007AFF;
            background-color: #e6f2ff;
            padding: 4rpx 12rpx;
            border-radius: 6rpx;
          }
        }
        .disease-detail-row {
          .disease-desc {
            font-size: 24rpx;
            color: #999;
            line-height: 1.5;
          }
        }
      }
      .checkbox-empty {
        width: 40rpx;
        height: 40rpx;
        border: 2rpx solid #ddd;
        border-radius: 50%;
      }
    }
    .no-data {
      text-align: center;
      padding: 100rpx 0;
      color: #999;
      text {
        display: block;
        margin-top: 20rpx;
        font-size: 28rpx;
      }
    }
  }
  .popup-footer {
    display: flex;
    padding: 20rpx 30rpx;
    border-top: 1rpx solid #f0f0f0;
    gap: 20rpx;
    flex-shrink: 0;
    button {
      flex: 1;
      height: 80rpx;
      border-radius: 10rpx;
      font-size: 30rpx;
    }
    .cancel-btn {
      background-color: #f5f5f5;
      color: #666;
    }
    .confirm-btn {
      background-color: #007AFF;
      color: white;
    }
  }
}
app/pages/task/create-normal.vue
@@ -39,17 +39,49 @@
      
      <view class="form-item">
        <view class="form-label">任务出发地</view>
        <view class="form-input picker-input" @click="selectStartLocation">
          {{ taskForm.startLocation || '请选择任务出发地' }}
          <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
        <view class="address-input-container">
          <input
            class="form-input"
            placeholder="请输入任务出发地"
            v-model="taskForm.startLocation"
            @input="onStartLocationInput"
            @focus="onStartLocationFocus"
          />
          <view class="address-suggestions" v-if="showStartSuggestions && startSuggestions.length > 0">
            <view
              class="address-suggestion-item"
              v-for="(item, index) in startSuggestions"
              :key="index"
              @click="selectStartSuggestion(item)"
            >
              <view class="suggestion-name">{{ item.name }}</view>
              <view class="suggestion-address">{{ item.address }}</view>
            </view>
          </view>
        </view>
      </view>
      
      <view class="form-item">
        <view class="form-label">任务目的地</view>
        <view class="form-input picker-input" @click="selectEndLocation">
          {{ taskForm.endLocation || '请选择任务目的地' }}
          <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
        <view class="address-input-container">
          <input
            class="form-input"
            placeholder="请输入任务目的地"
            v-model="taskForm.endLocation"
            @input="onEndLocationInput"
            @focus="onEndLocationFocus"
          />
          <view class="address-suggestions" v-if="showEndSuggestions && endSuggestions.length > 0">
            <view
              class="address-suggestion-item"
              v-for="(item, index) in endSuggestions"
              :key="index"
              @click="selectEndSuggestion(item)"
            >
              <view class="suggestion-name">{{ item.name }}</view>
              <view class="suggestion-address">{{ item.address }}</view>
            </view>
          </view>
        </view>
      </view>
      
@@ -109,40 +141,22 @@
      </view>
    </view>
    
    <!-- 地图选择器弹窗 -->
    <uni-popup ref="mapPopup" type="bottom" :mask-click="false">
      <view class="map-popup-container">
        <view class="popup-header">
          <view class="popup-title">选择地址</view>
          <view class="close-btn" @click="closeMapSelector">
            <uni-icons type="closeempty" size="20" color="#999"></uni-icons>
          </view>
        </view>
        <map-selector
          :initial-address="getInitialAddress()"
          @addressSelected="onAddressSelected"
        ></map-selector>
      </view>
    </uni-popup>
  </scroll-view>
</template>
<script>
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 { getUserProfile } from "@/api/system/user"
import { addTask } from "@/api/task"
import { listAvailableVehicles } from "@/api/vehicle"
import { calculateDistance } from "@/api/map"
import { baiduPlaceSuggestion, baiduGeocoding, baiduDistanceByAddress } from "@/api/map"
import { getDicts } from "@/api/dict"
import MapSelector from '@/components/map-selector.vue'
export default {
  components: {
    uniDatetimePicker,
    uniPopup,
    MapSelector
    uniDatetimePicker
  },
  data() {
    return {
@@ -157,7 +171,13 @@
      boundVehicleId: null,
      taskTypeOptions: [],
      taskTypeLabels: [],
      mapSelectorType: '',
      // 地址搜索相关
      startSuggestions: [],
      endSuggestions: [],
      showStartSuggestions: false,
      showEndSuggestions: false,
      startSearchTimer: null,
      endSearchTimer: null,
      taskForm: {
        taskDescription: '',
        taskType: '',
@@ -173,14 +193,17 @@
      vehicleOptions: [],
      loading: false,
      addressCoordinates: {
        startLocation: null,
        endLocation: null
      }
        startLocation: { lon: null, lat: null },
        endLocation: { lon: null, lat: null }
      },
      // 搜索区域(可根据用户所在城市调整)
      searchRegion: '广州市'
    }
  },
  computed: {
    ...mapState({
      currentUser: state => ({
        id: state.user.userId,
        name: state.user.nickName || '张三',
        position: '司机',
        deptId: state.user.deptId || 100
@@ -300,77 +323,202 @@
      this.taskForm.vehicleId = this.selectedVehicleId
    },
    
    selectStartLocation() {
      this.mapSelectorType = 'startLocation'
      this.$refs.mapPopup.open()
    },
    selectEndLocation() {
      this.mapSelectorType = 'endLocation'
      this.$refs.mapPopup.open()
    },
    getInitialAddress() {
      return this.mapSelectorType === 'startLocation' ? this.taskForm.startLocation : this.taskForm.endLocation
    },
    onAddressSelected(address) {
      if (this.mapSelectorType === 'startLocation') {
        this.taskForm.startLocation = address.title + ' - ' + address.address
        this.addressCoordinates.startLocation = {
          lat: address.lat,
          lng: address.lng
        }
      } else if (this.mapSelectorType === 'endLocation') {
        this.taskForm.endLocation = address.title + ' - ' + address.address
        this.addressCoordinates.endLocation = {
          lat: address.lat,
          lng: address.lng
        }
    // 出发地输入监听
    onStartLocationInput(e) {
      const keyword = e.detail.value
      this.taskForm.startLocation = keyword
      if (this.startSearchTimer) {
        clearTimeout(this.startSearchTimer)
      }
      
      this.calculateDistance()
      this.closeMapSelector()
    },
    calculateDistance() {
      if (this.addressCoordinates.startLocation && this.addressCoordinates.endLocation) {
        this.getDistanceBetweenPoints(
          this.addressCoordinates.startLocation.lat,
          this.addressCoordinates.startLocation.lng,
          this.addressCoordinates.endLocation.lat,
          this.addressCoordinates.endLocation.lng
        ).then(distance => {
          this.taskForm.distance = distance.toFixed(2)
        }).catch(error => {
          console.error('距离计算失败:', error)
        })
      if (!keyword || keyword.trim() === '') {
        this.startSuggestions = []
        this.showStartSuggestions = false
        return
      }
      // 防抖处理
      this.startSearchTimer = setTimeout(() => {
        this.searchStartAddress(keyword)
      }, 300)
    },
    
    getDistanceBetweenPoints(lat1, lng1, lat2, lng2) {
      return new Promise((resolve, reject) => {
        calculateDistance(lat1, lng1, lat2, lng2).then(response => {
          if (response.code === 200) {
            const responseData = typeof response.data === 'string' ? JSON.parse(response.data) : response.data
            if (responseData && responseData.status === 0 && responseData.result && responseData.result.elements && responseData.result.elements.length > 0) {
              const distanceInKm = responseData.result.elements[0].distance / 1000
              resolve(distanceInKm)
            } else {
              reject(new Error('距离计算接口返回数据格式不正确'))
            }
          } else {
            reject(new Error('距离计算接口调用失败'))
          }
        }).catch(error => {
          reject(error)
        })
    // 搜索出发地地址
    searchStartAddress(keyword) {
      baiduPlaceSuggestion(keyword, this.searchRegion).then(response => {
        if (response.code === 200 && response.data) {
          this.startSuggestions = response.data
          this.showStartSuggestions = true
        } else {
          this.startSuggestions = []
          this.showStartSuggestions = false
        }
      }).catch(error => {
        console.error('搜索出发地地址失败:', error)
        this.startSuggestions = []
        this.showStartSuggestions = false
      })
    },
    
    closeMapSelector() {
      this.$refs.mapPopup.close()
      this.mapSelectorType = ''
    // 出发地输入框获得焦点
    onStartLocationFocus() {
      if (this.taskForm.startLocation && this.startSuggestions.length > 0) {
        this.showStartSuggestions = true
      }
    },
    // 选择出发地地址建议
    selectStartSuggestion(item) {
      this.taskForm.startLocation = item.name
      this.showStartSuggestions = false
      this.startSuggestions = []
      // 获取地址坐标
      if (item.location && item.location.lng && item.location.lat) {
        this.addressCoordinates.startLocation = {
          lon: item.location.lng,
          lat: item.location.lat
        }
      } else {
        // 如果没有坐标,需要通过地址获取坐标
        this.getCoordinatesByAddress(item.name, 'start')
      }
      // 如果两个地址都已选择,自动计算距离
      if (this.taskForm.endLocation && this.addressCoordinates.endLocation.lon) {
        this.calculateDistance()
      }
    },
    // 目的地输入监听
    onEndLocationInput(e) {
      const keyword = e.detail.value
      this.taskForm.endLocation = keyword
      if (this.endSearchTimer) {
        clearTimeout(this.endSearchTimer)
      }
      if (!keyword || keyword.trim() === '') {
        this.endSuggestions = []
        this.showEndSuggestions = false
        return
      }
      // 防抖处理
      this.endSearchTimer = setTimeout(() => {
        this.searchEndAddress(keyword)
      }, 300)
    },
    // 搜索目的地地址
    searchEndAddress(keyword) {
      baiduPlaceSuggestion(keyword, this.searchRegion).then(response => {
        if (response.code === 200 && response.data) {
          this.endSuggestions = response.data
          this.showEndSuggestions = true
        } else {
          this.endSuggestions = []
          this.showEndSuggestions = false
        }
      }).catch(error => {
        console.error('搜索目的地地址失败:', error)
        this.endSuggestions = []
        this.showEndSuggestions = false
      })
    },
    // 目的地输入框获得焦点
    onEndLocationFocus() {
      if (this.taskForm.endLocation && this.endSuggestions.length > 0) {
        this.showEndSuggestions = true
      }
    },
    // 选择目的地地址建议
    selectEndSuggestion(item) {
      this.taskForm.endLocation = item.name
      this.showEndSuggestions = false
      this.endSuggestions = []
      // 获取地址坐标
      if (item.location && item.location.lng && item.location.lat) {
        this.addressCoordinates.endLocation = {
          lon: item.location.lng,
          lat: item.location.lat
        }
      } else {
        // 如果没有坐标,需要通过地址获取坐标
        this.getCoordinatesByAddress(item.name, 'end')
      }
      // 如果两个地址都已选择,自动计算距离
      if (this.taskForm.startLocation && this.addressCoordinates.startLocation.lon) {
        this.calculateDistance()
      }
    },
    // 通过地址获取坐标
    getCoordinatesByAddress(address, type) {
      baiduGeocoding(address, this.searchRegion).then(response => {
        if (response.code === 200 && response.data && response.data.location) {
          if (type === 'start') {
            this.addressCoordinates.startLocation = {
              lon: response.data.location.lng,
              lat: response.data.location.lat
            }
          } else if (type === 'end') {
            this.addressCoordinates.endLocation = {
              lon: response.data.location.lng,
              lat: response.data.location.lat
            }
          }
          // 如果两个地址都已有坐标,自动计算距离
          if (this.addressCoordinates.startLocation.lon && this.addressCoordinates.endLocation.lon) {
            this.calculateDistance()
          }
        }
      }).catch(error => {
        console.error('获取地址坐标失败:', error)
      })
    },
    // 计算两地之间的距离
    calculateDistance() {
      if (!this.taskForm.startLocation || !this.taskForm.endLocation) {
        return
      }
      // 使用百度地图计算距离(组合接口)
      baiduDistanceByAddress(this.taskForm.startLocation, this.searchRegion, this.taskForm.endLocation, this.searchRegion).then(response => {
        if (response.code === 200 && response.data) {
          // 百度地图返回的距离单位是米,需要转换为公里
          const distanceInMeters = response.data.distance
          const distanceInKm = distanceInMeters / 1000
          this.taskForm.distance = distanceInKm.toFixed(2)
          console.log('距离计算成功:', distanceInMeters, '米 =', distanceInKm, '公里')
          // 同时更新坐标信息
          if (response.data.fromLocation) {
            this.addressCoordinates.startLocation = {
              lon: response.data.fromLocation.lng,
              lat: response.data.fromLocation.lat
            }
          }
          if (response.data.toLocation) {
            this.addressCoordinates.endLocation = {
              lon: response.data.toLocation.lng,
              lat: response.data.toLocation.lat
            }
          }
        }
      }).catch(error => {
        console.error('计算距离失败:', error)
        this.$modal.showToast('计算距离失败,请手动输入')
      })
    },
    
    validateForm() {
@@ -413,10 +561,17 @@
    },
    
    buildSubmitData() {
      // 调试:打印当前用户信息
      console.log('当前用户信息:', this.currentUser)
      console.log('用户ID:', this.currentUser.id)
      console.log('Vuex State:', this.$store.state.user)
      const submitData = {
        taskDescription: this.taskForm.taskDescription,
        taskType: this.taskForm.taskType,
        vehicleIds: this.taskForm.vehicleId ? [this.taskForm.vehicleId] : [],
        assigneeId: this.currentUser.id || this.$store.state.user.userId, // 主要执行人
        assigneeIds: (this.currentUser.id || this.$store.state.user.userId) ? [this.currentUser.id || this.$store.state.user.userId] : [], // 执行人员ID列表
        plannedStartTime: this.taskForm.plannedStartTime,
        plannedEndTime: this.taskForm.plannedEndTime,
        departureAddress: this.taskForm.startLocation,
@@ -425,13 +580,16 @@
        remark: this.taskForm.remark
      }
      
      if (this.addressCoordinates.startLocation) {
        submitData.departureLongitude = this.addressCoordinates.startLocation.lng
      // 调试:打印提交数据
      console.log('提交数据:', submitData)
      if (this.addressCoordinates.startLocation && this.addressCoordinates.startLocation.lon) {
        submitData.departureLongitude = this.addressCoordinates.startLocation.lon
        submitData.departureLatitude = this.addressCoordinates.startLocation.lat
      }
      
      if (this.addressCoordinates.endLocation) {
        submitData.destinationLongitude = this.addressCoordinates.endLocation.lng
      if (this.addressCoordinates.endLocation && this.addressCoordinates.endLocation.lon) {
        submitData.destinationLongitude = this.addressCoordinates.endLocation.lon
        submitData.destinationLatitude = this.addressCoordinates.endLocation.lat
      }
      
@@ -450,9 +608,18 @@
        addTask(submitData).then(response => {
          this.loading = false
          this.$modal.showToast('任务创建成功')
          // 延迟跳转,让用户看到成功提示
          setTimeout(() => {
            this.$tab.navigateTo('/pages/task/index')
          }, 1500)
            // 跳转到任务列表并触发刷新
            uni.switchTab({
              url: '/pages/task/index',
              success: () => {
                // 使用事件总线通知任务列表页面刷新
                uni.$emit('refreshTaskList')
              }
            })
          }, 1000)
        }).catch(error => {
          this.loading = false
          console.error('任务创建失败:', error)
@@ -540,6 +707,60 @@
        border-radius: 10rpx;
        font-size: 28rpx;
      }
      // 地址输入容器
      .address-input-container {
        position: relative;
        .form-input {
          width: 100%;
          height: 70rpx;
          padding: 0 20rpx;
          border: 1rpx solid #eee;
          border-radius: 10rpx;
          font-size: 28rpx;
          box-sizing: border-box;
        }
        .address-suggestions {
          position: absolute;
          top: 75rpx;
          left: 0;
          right: 0;
          max-height: 400rpx;
          overflow-y: auto;
          background-color: white;
          border: 1rpx solid #eee;
          border-radius: 10rpx;
          box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
          z-index: 100;
          .address-suggestion-item {
            padding: 20rpx;
            border-bottom: 1rpx solid #f0f0f0;
            &:last-child {
              border-bottom: none;
            }
            &:active {
              background-color: #f5f5f5;
            }
            .suggestion-name {
              font-size: 28rpx;
              color: #333;
              margin-bottom: 8rpx;
              font-weight: 500;
            }
            .suggestion-address {
              font-size: 24rpx;
              color: #999;
            }
          }
        }
      }
    }
    
    .form-actions {
@@ -562,34 +783,6 @@
    }
  }
  
  .map-popup-container {
    height: 80vh;
    background-color: white;
    border-top-left-radius: 20rpx;
    border-top-right-radius: 20rpx;
    overflow: hidden;
    .popup-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 20rpx 30rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .popup-title {
        font-size: 32rpx;
        font-weight: bold;
        color: #333;
      }
      .close-btn {
        width: 50rpx;
        height: 50rpx;
        display: flex;
        align-items: center;
        justify-content: center;
      }
    }
  }
}
</style>
app/pages/task/detail.vue
@@ -135,7 +135,7 @@
        </view>
        <view class="info-item" v-if="taskDetail.emergencyInfo.patientCondition">
          <view class="label">病情描述</view>
          <view class="value">{{ taskDetail.emergencyInfo.patientCondition }}</view>
          <view class="value" style="white-space: pre-line;">{{ taskDetail.emergencyInfo.patientCondition }}</view>
        </view>
      </view>
      
@@ -269,8 +269,14 @@
    
    <!-- 操作按钮区域 -->
    <view class="action-buttons" v-if="taskDetail">
      <!-- 待处理状态: 显示出发、取消 -->
      <!-- 待处理状态: 显示编辑、出发、取消 -->
      <template v-if="taskDetail.taskStatus === 'PENDING'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          修改
        </button>
        <button 
          class="action-btn primary" 
          @click="handleTaskAction('depart')"
@@ -285,8 +291,14 @@
        </button>
      </template>
      
      <!-- 出发中状态: 显示已到达、强制结束 -->
      <!-- 出发中状态: 显示编辑、已到达、强制结束 -->
      <template v-else-if="taskDetail.taskStatus === 'DEPARTING'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          修改
        </button>
        <button 
          class="action-btn primary" 
          @click="handleTaskAction('arrive')"
@@ -301,8 +313,14 @@
        </button>
      </template>
      
      <!-- 已到达状态: 显示已返程 -->
      <!-- 已到达状态: 显示编辑、已返程 -->
      <template v-else-if="taskDetail.taskStatus === 'ARRIVED'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          修改
        </button>
        <button 
          class="action-btn primary" 
          @click="handleTaskAction('return')"
@@ -311,8 +329,14 @@
        </button>
      </template>
      
      <!-- 返程中状态: 显示已完成 -->
      <!-- 返程中状态: 显示编辑、已完成 -->
      <template v-else-if="taskDetail.taskStatus === 'RETURNING'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          修改
        </button>
        <button 
          class="action-btn primary" 
          @click="handleTaskAction('complete')"
@@ -409,6 +433,12 @@
      this.taskId = options.id
      this.loadTaskDetail()
    },
    onShow() {
      // 每次页面显示时重新加载数据,确保从编辑页面返回后能看到最新数据
      if (this.taskId) {
        this.loadTaskDetail()
      }
    },
    methods: {
      // 加载任务详情
      loadTaskDetail() {
@@ -420,11 +450,12 @@
        getTask(this.taskId).then(response => {
          this.taskDetail = response.data || response
          // 调试:打印返回的数据
          // console.log('任务详情完整数据:', JSON.stringify(this.taskDetail, null, 2))
          // console.log('任务类型字段值:', this.taskDetail.taskType)
          // console.log('任务状态字段值:', this.taskDetail.taskStatus)
          // console.log('出发地址:', this.taskDetail.departureAddress)
          // console.log('目的地址:', this.taskDetail.destinationAddress)
          console.log('任务详情完整数据:', JSON.stringify(this.taskDetail, null, 2))
          console.log('任务类型字段值:', this.taskDetail.taskType)
          console.log('任务状态字段值:', this.taskDetail.taskStatus)
          console.log('出发地址:', this.taskDetail.departureAddress)
          console.log('目的地址:', this.taskDetail.destinationAddress)
          console.log('转运任务信息 (emergencyInfo):', this.taskDetail.emergencyInfo)
        }).catch(error => {
          console.error('加载任务详情失败:', error)
          this.$modal.showToast('加载任务详情失败')
@@ -473,6 +504,41 @@
      // 返回上一页
      goBack() {
        uni.navigateBack()
      },
      // 处理编辑按钮
      handleEdit() {
        if (!this.taskDetail) {
          this.$modal.showToast('任务信息不存在')
          return
        }
        // 检查任务状态,已完成或已取消的任务不能编辑
        if (this.isTaskFinished) {
          this.$modal.showToast('已完成或已取消的任务不能编辑')
          return
        }
        const taskType = this.taskDetail.taskType
        const taskId = this.taskDetail.taskId
        // 根据任务类型跳转到不同的编辑页面
        if (taskType === 'EMERGENCY_TRANSFER') {
          // 转运任务:跳转到转运任务编辑页面
          uni.navigateTo({
            url: `/pages/task/edit-emergency?id=${taskId}`
          })
        } else if (taskType === 'WELFARE') {
          // 福祗车任务:跳转到福祗车编辑页面
          uni.navigateTo({
            url: `/pages/task/edit-welfare?id=${taskId}`
          })
        } else {
          // 其他任务:跳转到通用任务编辑页面
          uni.navigateTo({
            url: `/pages/task/edit?id=${taskId}`
          })
        }
      },
      
      // 获取状态文本
@@ -1095,6 +1161,11 @@
        background-color: #f0f0f0;
        color: #333;
        
        &.edit {
          background-color: #ff9500;
          color: white;
        }
        &.primary {
          background-color: #007AFF;
          color: white;
app/pages/task/edit-emergency.vue
New file
@@ -0,0 +1,1015 @@
<template>
  <scroll-view class="edit-emergency-task-container" scroll-y="true">
    <view class="form-header">
      <view class="back-btn" @click="goBack">
        <uni-icons type="arrowleft" size="20"></uni-icons>
      </view>
      <view class="title">编辑转运任务</view>
    </view>
    <view class="form-section" v-if="taskDetail">
      <!-- 使用车辆选择器组件 -->
      <VehicleSelector
        label="任务车辆"
        v-model="selectedVehicleId"
        :dept-id="currentUser.deptId"
        vehicle-type="EMERGENCY"
        :auto-select-bound="false"
        @change="onVehicleChange"
      />
      <view class="form-item">
        <OrganizationSelector
          v-model="selectedOrganizationId"
          :required="true"
          :auto-select-user-dept="false"
          @change="onOrganizationChange"
        />
      </view>
      <view class="form-item">
        <view class="form-label required">转运时间</view>
        <uni-datetime-picker
          v-model="taskForm.transferTime"
          type="datetime"
          :placeholder="'请选择转运时间'"
          class="form-input"
        />
      </view>
      <view class="form-section-title">患者信息</view>
      <view class="form-item">
        <view class="form-label required">联系人</view>
        <input
          class="form-input"
          placeholder="请输入联系人"
          v-model="taskForm.patient.contact"
        />
      </view>
      <view class="form-item">
        <view class="form-label required">联系电话</view>
        <input
          class="form-input"
          type="number"
          placeholder="请输入联系电话"
          v-model="taskForm.patient.phone"
        />
      </view>
      <view class="form-item">
        <view class="form-label required">患者姓名</view>
        <input
          class="form-input"
          placeholder="请输入患者姓名"
          v-model="taskForm.patient.name"
        />
      </view>
      <view class="form-item">
        <view class="form-label">性别</view>
        <view class="radio-group">
          <label class="radio-item">
            <radio value="male" :checked="taskForm.patient.gender === 'male'" @click="taskForm.patient.gender = 'male'" />
            <text>男</text>
          </label>
          <label class="radio-item">
            <radio value="female" :checked="taskForm.patient.gender === 'female'" @click="taskForm.patient.gender = 'female'" />
            <text>女</text>
          </label>
        </view>
      </view>
      <view class="form-item">
        <view class="form-label">患者身份证</view>
        <input
          class="form-input"
          type="idcard"
          placeholder="请输入患者身份证号"
          v-model="taskForm.patient.idCard"
        />
      </view>
      <DiseaseSelector
        v-model="selectedDiseases"
        :other-description.sync="taskForm.patient.otherCondition"
        @change="onDiseaseChange"
      />
      <view class="form-section-title">转出医院信息</view>
      <HospitalSelector
        label="区院名称"
        address-label="转出地址"
        :required="true"
        v-model="taskForm.hospitalOut"
        :dept-id="selectedOrganizationId"
        @change="onHospitalOutChange"
        @address-selected="onHospitalOutAddressSelected"
      />
      <view class="form-section-title">转入医院信息</view>
      <HospitalSelector
        label="医院名称"
        address-label="转入地址"
        :required="true"
        v-model="taskForm.hospitalIn"
        :dept-id="selectedOrganizationId"
        @change="onHospitalInChange"
        @address-selected="onHospitalInAddressSelected"
      />
      <view class="form-item">
        <view class="form-label">转运距离</view>
        <input
          class="form-input"
          type="digit"
          placeholder="自动计算"
          v-model="taskForm.transferDistance"
          disabled
        />
      </view>
      <view class="form-item">
        <view class="form-label">成交价</view>
        <input
          class="form-input"
          type="digit"
          placeholder="请输入成交价"
          v-model="taskForm.price"
        />
      </view>
      <view class="form-actions">
        <button class="submit-btn" @click="submitTask" :disabled="loading">
          {{ loading ? '保存中...' : '保存' }}
        </button>
      </view>
    </view>
    <view class="loading" v-else>
      <uni-icons type="spinner-cycle" size="40" color="#007AFF"></uni-icons>
      <text>加载中...</text>
    </view>
    <!-- 地图选择器弹窗 -->
    <uni-popup ref="mapPopup" type="bottom" :mask-click="false">
      <view class="map-popup-container">
        <view class="popup-header">
          <view class="popup-title">选择地址</view>
          <view class="close-btn" @click="closeMapSelector">
            <uni-icons type="closeempty" size="20" color="#999"></uni-icons>
          </view>
        </view>
        <map-selector
          :initial-address="getInitialAddress()"
          @addressSelected="onAddressSelected"
        ></map-selector>
      </view>
    </uni-popup>
  </scroll-view>
</template>
<script>
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 { getTask, updateTask } from "@/api/task"
import { baiduDistanceByAddress } from "@/api/map"
import { calculateTransferPrice } from "@/api/price"
import MapSelector from '@/components/map-selector.vue'
import VehicleSelector from '@/components/VehicleSelector.vue'
import OrganizationSelector from '@/components/OrganizationSelector.vue'
import HospitalSelector from '@/components/HospitalSelector.vue'
import DiseaseSelector from '@/components/DiseaseSelector.vue'
import distanceCalculator from '@/mixins/distanceCalculator.js'
export default {
  components: {
    uniDatetimePicker,
    uniPopup,
    MapSelector,
    VehicleSelector,
    OrganizationSelector,
    HospitalSelector,
    DiseaseSelector
  },
  mixins: [distanceCalculator],
  data() {
    return {
      taskId: null,
      taskDetail: null,
      selectedVehicleId: null,
      selectedOrganizationId: null,
      selectedRegion: '',
      mapSelectorType: '',
      // 地址坐标(用于手动输入地址时计算距离)
      addressCoordinates: {
        hospitalOutAddress: null,
        hospitalInAddress: null
      },
      selectedDiseases: [], // 已选择的病情列表
      taskForm: {
        transferTime: '',
        patient: {
          contact: '',
          phone: '',
          name: '',
          gender: 'male',
          idCard: '',
          condition: '',
          otherCondition: '' // 其他病情描述
        },
        hospitalOut: {
          id: null,
          name: '',
          department: '',
          departmentId: null,
          bedNumber: '',
          address: ''
        },
        hospitalIn: {
          id: null,
          name: '',
          department: '',
          departmentId: null,
          bedNumber: '',
          address: ''
        },
        transferDistance: '',
        price: ''
      },
      loading: false
    }
  },
  computed: {
    ...mapState({
      currentUser: state => ({
        name: state.user.nickName || '张三',
        position: '司机',
        deptId: state.user.deptId || 100,
        branchCompanyName: state.user.branchCompanyName
      })
    })
  },
  onLoad(options) {
    if (options.id) {
      this.taskId = options.id
      this.loadTaskDetail()
    } else {
      this.$modal.showToast('任务ID不能为空')
      setTimeout(() => {
        uni.navigateBack()
      }, 1500)
    }
  },
  methods: {
    // 加载任务详情
    loadTaskDetail() {
      if (!this.taskId) {
        this.$modal.showToast('任务ID不能为空')
        return
      }
      console.log('开始加载任务详情,taskId:', this.taskId)
      getTask(this.taskId).then(response => {
        console.log('任务详情API响应:', response)
        this.taskDetail = response.data || response
        if (!this.taskDetail) {
          console.error('任务详情为空')
          this.$modal.showToast('任务数据加载失败')
          return
        }
        console.log('任务详情数据:', this.taskDetail)
        // 填充表单数据 - 转运任务特有字段
        if (this.taskDetail.emergencyInfo) {
          const info = this.taskDetail.emergencyInfo
          console.log('转运任务信息:', info)
          // 转运时间
          this.taskForm.transferTime = this.taskDetail.plannedStartTime || ''
          // 患者信息
          this.taskForm.patient.contact = info.patientContact || ''
          this.taskForm.patient.phone = info.patientPhone || ''
          this.taskForm.patient.name = info.patientName || ''
          this.taskForm.patient.gender = info.patientGender || 'male'
          this.taskForm.patient.idCard = info.patientIdCard || ''
          this.taskForm.patient.condition = info.patientCondition || ''
          // 解析病情信息
          this.parseDiseaseInfo(info.patientCondition, info.diseaseIds)
          // 转出医院信息
          this.taskForm.hospitalOut.id = info.hospitalOutId || null
          this.taskForm.hospitalOut.name = info.hospitalOutName || ''
          this.taskForm.hospitalOut.department = info.hospitalOutDepartment || ''
          this.taskForm.hospitalOut.bedNumber = info.hospitalOutBedNumber || ''
          this.taskForm.hospitalOut.address = info.hospitalOutAddress || ''
          // 转入医院信息
          this.taskForm.hospitalIn.id = info.hospitalInId || null
          this.taskForm.hospitalIn.name = info.hospitalInName || ''
          this.taskForm.hospitalIn.department = info.hospitalInDepartment || ''
          this.taskForm.hospitalIn.bedNumber = info.hospitalInBedNumber || ''
          this.taskForm.hospitalIn.address = info.hospitalInAddress || ''
          // 转运距离和价格
          this.taskForm.transferDistance = info.transferDistance ? String(info.transferDistance) : ''
          this.taskForm.price = info.transferPrice ? String(info.transferPrice) : ''
        } else {
          console.warn('任务详情中没有emergencyInfo字段,尝试从主对象获取数据')
          // 兼容处理:如果emergencyInfo不存在,尝试从主对象获取
          this.taskForm.transferTime = this.taskDetail.plannedStartTime || ''
          this.taskForm.transferDistance = this.taskDetail.estimatedDistance ? String(this.taskDetail.estimatedDistance) : ''
        }
        // 设置车辆信息
        if (this.taskDetail.assignedVehicles && this.taskDetail.assignedVehicles.length > 0) {
          const firstVehicle = this.taskDetail.assignedVehicles[0]
          this.selectedVehicleId = firstVehicle.vehicleId
          console.log('设置车辆ID:', this.selectedVehicleId)
        } else {
          console.warn('任务没有分配车辆')
        }
        // 设置归属机构
        if (this.taskDetail.deptId) {
          this.selectedOrganizationId = this.taskDetail.deptId
          console.log('设置归属机构ID:', this.selectedOrganizationId)
        } else {
          console.warn('未找到归属机构信息')
        }
        // 设置地址坐标(使用mixin中的方法)
        if (this.taskDetail.departureLongitude && this.taskDetail.departureLatitude) {
          this.setStartLocation({
            lng: this.taskDetail.departureLongitude,
            lat: this.taskDetail.departureLatitude
          })
          console.log('设置出发地坐标')
        }
        if (this.taskDetail.destinationLongitude && this.taskDetail.destinationLatitude) {
          this.setEndLocation({
            lng: this.taskDetail.destinationLongitude,
            lat: this.taskDetail.destinationLatitude
          })
          console.log('设置目的地坐标')
        }
        console.log('表单数据填充完成:', this.taskForm)
      }).catch(error => {
        console.error('加载任务详情失败:', error)
        console.error('错误详情:', error.response || error.message || error)
        this.$modal.showToast('加载任务详情失败: ' + (error.message || '网络错误'))
        setTimeout(() => {
          uni.navigateBack()
        }, 1500)
      })
    },
    // 车辆选择变化
    onVehicleChange(vehicle) {
      console.log('选中车辆:', vehicle)
    },
    // 归属机构选择变化
    onOrganizationChange(orgData) {
      // orgData 包含:deptId, deptName, serviceOrderClass, region
      this.selectedOrganizationId = orgData.deptId
      console.log('选中归属机构:', orgData.deptName, '部门ID:', orgData.deptId)
    },
    // 转出医院变化
    onHospitalOutChange(hospitalData) {
      console.log('转出医院变化:', hospitalData)
      // 组件已经通过 v-model 更新了 taskForm.hospitalOut
      // 如果转入地址已填写,自动计算距离
      if (this.taskForm.hospitalIn.address) {
        // 如果两个都不是"家中",使用医院距离计算
        if (hospitalData.name !== '家中' && this.taskForm.hospitalIn.name !== '家中') {
          this.calculateHospitalDistance()
        } else {
          // 有一个是"家中",使用地址计算
          this.calculateDistanceByManualAddress()
        }
      }
    },
    // 转出医院地址选择(当选择"家中"时使用百度地图地址建议)
    onHospitalOutAddressSelected(data) {
      // data 包含:address, location
      if (data.location) {
        this.addressCoordinates.hospitalOutAddress = data.location
        // 如果转入地址也已填写,自动计算距离
        if (this.taskForm.hospitalIn.address) {
          this.calculateDistanceByManualAddress()
        }
      }
    },
    // 转入医院变化
    onHospitalInChange(hospitalData) {
      console.log('转入医院变化:', hospitalData)
      // 组件已经通过 v-model 更新了 taskForm.hospitalIn
      // 如果转出地址已填写,自动计算距离
      if (this.taskForm.hospitalOut.address) {
        // 如果两个都不是"家中",使用医院距离计算
        if (hospitalData.name !== '家中' && this.taskForm.hospitalOut.name !== '家中') {
          this.calculateHospitalDistance()
        } else {
          // 有一个是"家中",使用地址计算
          this.calculateDistanceByManualAddress()
        }
      }
    },
    // 转入医院地址选择(当选择"家中"时使用百度地图地址建议)
    onHospitalInAddressSelected(data) {
      // data 包含:address, location
      if (data.location) {
        this.addressCoordinates.hospitalInAddress = data.location
        // 如果转出地址也已填写,自动计算距离
        if (this.taskForm.hospitalOut.address) {
          this.calculateDistanceByManualAddress()
        }
      }
    },
    // 病情变化
    onDiseaseChange(diseases) {
      console.log('病情变化:', diseases)
      // 组件已经通过 v-model 更新了 selectedDiseases
    },
    // 解析病情信息(从字符串解析出ICD-10疾病列表)
    parseDiseaseInfo(conditionText, diseaseIds) {
      if (!conditionText) {
        this.selectedDiseases = []
        this.taskForm.patient.otherCondition = ''
        return
      }
      // 解析diseaseIds(逗号分隔的字符串转为数组)
      const diseaseIdArray = diseaseIds ? diseaseIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)) : []
      // 如果包含"其他:"分隔符,拆分病情和其他描述
      if (conditionText.includes('\n其他:')) {
        const parts = conditionText.split('\n其他:')
        const diseasePart = parts[0]
        this.taskForm.patient.otherCondition = parts[1] || ''
        // 解析病情部分
        this.parseDiseaseList(diseasePart, diseaseIdArray)
      } else {
        // 没有"其他"部分,全部作为其他描述
        this.taskForm.patient.otherCondition = conditionText
        this.selectedDiseases = []
      }
    },
    // 解析病情列表(格式:疾病名(编码)、疾病名(编码))
    parseDiseaseList(diseasePart, diseaseIdArray) {
      if (!diseasePart) {
        this.selectedDiseases = []
        return
      }
      // 匹配格式:疾病名(编码)
      const regex = /([^(]+)\(([^)]+)\)/g
      const diseases = []
      let match
      let index = 0
      while ((match = regex.exec(diseasePart)) !== null) {
        const icdName = match[1].replace(/[、,\s]+$/, '').replace(/^[、,\s]+/, '').trim()
        const icdCode = match[2].trim()
        // 只添加病情名称不为空的项
        if (icdName) {
          diseases.push({
            icdName: icdName,
            icdCode: icdCode,
            id: diseaseIdArray && index < diseaseIdArray.length ? diseaseIdArray[index] : null
          })
          index++
        }
      }
      this.selectedDiseases = diseases
    },
    // 选择转出医院地址
    selectHospitalOutAddress() {
      this.mapSelectorType = 'hospitalOut'
      this.$refs.mapPopup.open()
    },
    // 选择转入医院地址
    selectHospitalInAddress() {
      this.mapSelectorType = 'hospitalIn'
      this.$refs.mapPopup.open()
    },
    getInitialAddress() {
      if (this.mapSelectorType === 'hospitalOut') {
        return this.taskForm.hospitalOut.address
      } else if (this.mapSelectorType === 'hospitalIn') {
        return this.taskForm.hospitalIn.address
      }
      return ''
    },
    onAddressSelected(address) {
      const formattedAddress = address.title + ' - ' + address.address
      if (this.mapSelectorType === 'hospitalOut') {
        this.taskForm.hospitalOut.address = formattedAddress
        this.setLocationByAddress('start', address)
      } else if (this.mapSelectorType === 'hospitalIn') {
        this.taskForm.hospitalIn.address = formattedAddress
        this.setLocationByAddress('end', address)
      }
      // 监听mixin中的距离计算完成事件
      this.$once('distance-calculated', (distance) => {
        this.taskForm.transferDistance = this.formatDistance(distance)
      })
      this.closeMapSelector()
    },
    closeMapSelector() {
      this.$refs.mapPopup.close()
      this.mapSelectorType = ''
    },
    validateForm() {
      if (!this.selectedVehicleId) {
        this.$modal.showToast('请选择任务车辆')
        return false
      }
      if (!this.selectedOrganizationId) {
        this.$modal.showToast('请选择归属机构')
        return false
      }
      if (!this.taskForm.transferTime) {
        this.$modal.showToast('请选择转运时间')
        return false
      }
      if (!this.taskForm.patient.contact) {
        this.$modal.showToast('请输入联系人')
        return false
      }
      if (!this.taskForm.patient.phone) {
        this.$modal.showToast('请输入联系电话')
        return false
      }
      if (!this.taskForm.patient.name) {
        this.$modal.showToast('请输入患者姓名')
        return false
      }
      if (!this.taskForm.hospitalOut.name) {
        this.$modal.showToast('请输入转出医院名称')
        return false
      }
      if (!this.taskForm.hospitalOut.address) {
        this.$modal.showToast('请选择转出医院地址')
        return false
      }
      if (!this.taskForm.hospitalIn.name) {
        this.$modal.showToast('请输入转入医院名称')
        return false
      }
      if (!this.taskForm.hospitalIn.address) {
        this.$modal.showToast('请选择转入医院地址')
        return false
      }
      return true
    },
    buildSubmitData() {
      // 合并病情信息:选中的ICD-10疾病 + 其他描述
      let conditionText = ''
      if (this.selectedDiseases.length > 0) {
        // 过滤掉病情名称为空的项,并构建病情字符串
        const diseaseNames = this.selectedDiseases
          .filter(d => d.icdName && d.icdName.trim())
          .map(d => `${d.icdName}(${d.icdCode})`)
          .join('、')
        if (diseaseNames) {
          conditionText = diseaseNames
        }
      }
      if (this.taskForm.patient.otherCondition && this.taskForm.patient.otherCondition.trim()) {
        if (conditionText) {
          conditionText += '\n其他:' + this.taskForm.patient.otherCondition
        } else {
          conditionText = this.taskForm.patient.otherCondition
        }
      }
      const submitData = {
        taskId: this.taskId,
        taskType: 'EMERGENCY_TRANSFER',
        deptId: this.selectedOrganizationId,
        vehicleIds: this.selectedVehicleId ? [this.selectedVehicleId] : [],
        plannedStartTime: this.taskForm.transferTime,
        departureAddress: this.taskForm.hospitalOut.address,
        destinationAddress: this.taskForm.hospitalIn.address,
        // 病情ID列表(用于同步调度单的OrdICD_ID参数)
        diseaseIds: this.selectedDiseases.map(d => d.id).filter(id => id !== null),
        emergencyInfo: {
          patientContact: this.taskForm.patient.contact,
          patientPhone: this.taskForm.patient.phone,
          patientName: this.taskForm.patient.name,
          patientGender: this.taskForm.patient.gender,
          patientIdCard: this.taskForm.patient.idCard,
          patientCondition: conditionText, // 使用合并后的病情信息
          hospitalOutName: this.taskForm.hospitalOut.name,
          hospitalOutDepartment: this.taskForm.hospitalOut.department,
          hospitalOutBedNumber: this.taskForm.hospitalOut.bedNumber,
          hospitalOutAddress: this.taskForm.hospitalOut.address,
          hospitalInName: this.taskForm.hospitalIn.name,
          hospitalInDepartment: this.taskForm.hospitalIn.department,
          hospitalInBedNumber: this.taskForm.hospitalIn.bedNumber,
          hospitalInAddress: this.taskForm.hospitalIn.address,
          transferDistance: this.taskForm.transferDistance ? parseFloat(this.taskForm.transferDistance) : null,
          transferPrice: this.taskForm.price ? parseFloat(this.taskForm.price) : null,
          // 病情详细信息(过滤掉空的病情名称)
          diseases: this.selectedDiseases
            .filter(d => d.icdName && d.icdName.trim())
            .map(d => ({
              icdId: d.id,
              icdCode: d.icdCode,
              icdName: d.icdName
            }))
        }
      }
      // 添加GPS坐标
      if (this.addressCoordinates.start) {
        submitData.departureLongitude = this.addressCoordinates.start.lng
        submitData.departureLatitude = this.addressCoordinates.start.lat
      }
      if (this.addressCoordinates.end) {
        submitData.destinationLongitude = this.addressCoordinates.end.lng
        submitData.destinationLatitude = this.addressCoordinates.end.lat
      }
      return submitData
    },
    submitTask() {
      if (!this.validateForm()) {
        return
      }
      this.$modal.confirm('确定要保存修改吗?').then(() => {
        this.loading = true
        const submitData = this.buildSubmitData()
        console.log('提交数据:', JSON.stringify(submitData, null, 2))
        updateTask(submitData).then(response => {
          this.loading = false
          console.log('任务更新响应:', response)
          this.$modal.showToast('任务更新成功')
          setTimeout(() => {
            uni.navigateBack()
          }, 1500)
        }).catch(error => {
          this.loading = false
          console.error('任务更新失败:', error)
          console.error('错误详情:', error.response || error.message || error)
          const errorMsg = error.response?.data?.msg || error.message || '请重试'
          this.$modal.showToast('任务更新失败: ' + errorMsg)
        })
      }).catch(() => {})
    },
    goBack() {
      uni.navigateBack()
    },
    // 自动计算两个医院之间的距离
    calculateHospitalDistance() {
      const fromAddress = this.taskForm.hospitalOut.address
      const toAddress = this.taskForm.hospitalIn.address
      if (!fromAddress || !toAddress) {
        console.log('地址信息不完整,无法计算距离')
        return
      }
      console.log('开始计算距离:', fromAddress, '->', toAddress)
      // 显示加载提示
      uni.showLoading({
        title: '计算距离中...'
      })
      // 调用百度地图API计算距离
      const region = this.selectedRegion || '广州'
      baiduDistanceByAddress(fromAddress, region, toAddress, region)
        .then(response => {
          uni.hideLoading()
          if (response.code === 200 && response.data) {
            const distanceInMeters = response.data.distance
            // 转换为公里,保留1位小数
            const distanceInKm = (distanceInMeters / 1000).toFixed(1)
            this.taskForm.transferDistance = distanceInKm
            console.log('距离计算成功:', distanceInKm, 'km')
            this.$modal.showToast(`距离: ${distanceInKm}公里`)
            // 距离计算成功后,自动计算成交价
            this.calculatePrice()
          } else {
            console.error('距离计算失败:', response.msg)
            this.$modal.showToast('距离计算失败,请手动输入')
          }
        })
        .catch(error => {
          uni.hideLoading()
          console.error('距离计算失败:', error)
          this.$modal.showToast('距离计算失败,请手动输入')
        })
    },
    // 手动输入地址时计算距离
    calculateDistanceByManualAddress() {
      const fromAddress = this.taskForm.hospitalOut.address
      const toAddress = this.taskForm.hospitalIn.address
      if (!fromAddress || !toAddress) {
        return
      }
      console.log('计算手动输入地址距离:', fromAddress, '->', toAddress)
      // 显示加载提示
      uni.showLoading({
        title: '计算距离中...'
      })
      // 调用百度地图API计算距离
      const region = this.selectedRegion || '广州'
      baiduDistanceByAddress(fromAddress, region, toAddress, region)
        .then(response => {
          uni.hideLoading()
          if (response.code === 200 && response.data) {
            const distanceInMeters = response.data.distance
            // 转换为公里,保留1位小数
            const distanceInKm = (distanceInMeters / 1000).toFixed(1)
            this.taskForm.transferDistance = distanceInKm
            console.log('距离计算成功:', distanceInKm, 'km')
            this.$modal.showToast(`距离: ${distanceInKm}公里`)
            // 距离计算成功后,自动计算成交价
            this.calculatePrice()
          } else {
            console.error('距离计算失败:', response.msg)
            this.$modal.showToast('距离计算失败,请手动输入')
          }
        })
        .catch(error => {
          uni.hideLoading()
          console.error('距离计算失败:', error)
          this.$modal.showToast('距离计算失败,请手动输入')
        })
    },
    // 计算成交价
    calculatePrice() {
      const fromAddress = this.taskForm.hospitalOut.address
      const toAddress = this.taskForm.hospitalIn.address
      const distance = this.taskForm.transferDistance
      // 如果地址或距离不完整,不进行计算
      if (!fromAddress || !toAddress || !distance || parseFloat(distance) <= 0) {
        console.log('地址或距离信息不完整,跳过成交价计算')
        return
      }
      console.log('开始计算成交价:', fromAddress, '->', toAddress, '距离:', distance)
      // 调用成交价计算接口
      calculateTransferPrice({
        fromAddress: fromAddress,
        toAddress: toAddress,
        distance: parseFloat(distance),
        region: this.selectedRegion || ''
      }).then(response => {
        if (response.code === 200 && response.data) {
          const price = response.data.price
          // 只有当返回的价格大于0时,才自动填充成交价
          if (price && price > 0) {
            this.taskForm.price = price.toFixed(2)
            console.log('成交价计算成功:', price)
            this.$modal.showToast(`成交价: ¥${price.toFixed(2)}`)
          } else {
            console.log('成交价为0,不自动填充')
          }
        } else {
          console.log('成交价计算失败,保持手动输入')
        }
      }).catch(error => {
        console.error('成交价计算失败:', error)
        // 计算失败时不提示用户,允许用户手动输入
      })
    }
  }
}
</script>
<style lang="scss" scoped>
.edit-emergency-task-container {
  padding: 20rpx;
  background-color: #f5f5f5;
  min-height: 100vh;
  .form-header {
    display: flex;
    align-items: center;
    padding: 20rpx 0;
    margin-bottom: 30rpx;
    .back-btn {
      width: 60rpx;
      height: 60rpx;
      border-radius: 50%;
      background-color: #f0f0f0;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-right: 20rpx;
    }
    .title {
      font-size: 36rpx;
      font-weight: bold;
      color: #333;
    }
  }
  .form-section {
    background-color: white;
    border-radius: 15rpx;
    padding: 30rpx;
    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
    .form-section-title {
      font-size: 32rpx;
      font-weight: bold;
      margin: 40rpx 0 20rpx 0;
      padding-bottom: 10rpx;
      border-bottom: 1rpx solid #f0f0f0;
    }
    .form-item {
      margin-bottom: 40rpx;
      .form-label {
        font-size: 28rpx;
        margin-bottom: 15rpx;
        color: #333;
        &.required::before {
          content: '*';
          color: #ff3b30;
          margin-right: 4rpx;
        }
      }
      .form-input {
        height: 70rpx;
        padding: 0 20rpx;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        font-size: 28rpx;
        &.picker-input {
          display: flex;
          align-items: center;
          justify-content: space-between;
        }
        &[disabled] {
          background-color: #f5f5f5;
          color: #999;
        }
      }
      .form-textarea {
        width: 100%;
        min-height: 150rpx;
        padding: 20rpx;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        font-size: 28rpx;
      }
      .radio-group {
        display: flex;
        gap: 40rpx;
        .radio-item {
          display: flex;
          align-items: center;
          gap: 10rpx;
          font-size: 28rpx;
          color: #333;
        }
      }
    }
    .form-actions {
      margin-top: 50rpx;
      text-align: center;
      .submit-btn {
        width: 80%;
        height: 80rpx;
        background-color: #007AFF;
        color: white;
        border-radius: 10rpx;
        font-size: 32rpx;
        &[disabled] {
          background-color: #ccc;
          color: #999;
        }
      }
    }
  }
  .loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 400rpx;
    color: #999;
    text {
      margin-top: 20rpx;
      font-size: 28rpx;
    }
  }
  .map-popup-container {
    height: 80vh;
    background-color: white;
    border-top-left-radius: 20rpx;
    border-top-right-radius: 20rpx;
    overflow: hidden;
    .popup-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 20rpx 30rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .popup-title {
        font-size: 32rpx;
        font-weight: bold;
        color: #333;
      }
      .close-btn {
        width: 50rpx;
        height: 50rpx;
        display: flex;
        align-items: center;
        justify-content: center;
      }
    }
  }
}
</style>
app/pages/task/edit-welfare.vue
New file
@@ -0,0 +1,569 @@
<template>
  <scroll-view class="edit-welfare-task-container" scroll-y="true">
    <view class="form-header">
      <view class="back-btn" @click="goBack">
        <uni-icons type="arrowleft" size="20"></uni-icons>
      </view>
      <view class="title">编辑福祉车任务</view>
    </view>
    <view class="form-section" v-if="taskDetail">
      <!-- 使用车辆选择器组件 -->
      <VehicleSelector
        label="任务车辆"
        v-model="selectedVehicleId"
        :dept-id="currentUser.deptId"
        vehicle-type="WELFARE"
        :auto-select-bound="false"
        @change="onVehicleChange"
      />
      <view class="form-item">
        <view class="form-label">执行任务人员</view>
        <view class="staff-list">
          <view class="staff-item">
            <text>{{ currentUser.name }} ({{ currentUser.position }})</text>
            <uni-icons type="checkmarkempty" size="20" color="#007AFF"></uni-icons>
          </view>
          <view
            class="staff-item"
            v-for="(staff, index) in additionalStaff"
            :key="index"
            @click="removeStaff(index)"
          >
            <text>{{ staff.name }} ({{ staff.position }})</text>
            <uni-icons type="closeempty" size="20" color="#ff4d4f"></uni-icons>
          </view>
          <view class="add-staff" @click="addStaff">
            <uni-icons type="plusempty" size="20" color="#007AFF"></uni-icons>
            <text>添加人员</text>
          </view>
        </view>
      </view>
      <view class="form-item">
        <view class="form-label">归属机构</view>
        <picker mode="selector" :range="organizations" @change="onOrganizationChange">
          <view class="form-input picker-input">
            {{ selectedOrganization || '请选择归属机构' }}
            <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
          </view>
        </picker>
      </view>
      <view class="form-item">
        <view class="form-label">服务时间</view>
        <uni-datetime-picker
          v-model="taskForm.serviceTime"
          type="datetime"
          :placeholder="'请选择服务时间'"
          class="form-input"
        />
      </view>
      <view class="form-section-title">乘客信息</view>
      <view class="form-item">
        <view class="form-label">联系人</view>
        <input
          class="form-input"
          placeholder="请输入联系人"
          v-model="taskForm.passenger.contact"
        />
      </view>
      <view class="form-item">
        <view class="form-label">联系电话</view>
        <input
          class="form-input"
          type="number"
          placeholder="请输入联系电话"
          v-model="taskForm.passenger.phone"
        />
      </view>
      <view class="form-item">
        <view class="form-label">出发地址</view>
        <view class="form-input picker-input" @click="selectStartAddress">
          {{ taskForm.startAddress || '请选择出发地址' }}
          <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
        </view>
      </view>
      <view class="form-item">
        <view class="form-label">目的地址</view>
        <view class="form-input picker-input" @click="selectEndAddress">
          {{ taskForm.endAddress || '请选择目的地址' }}
          <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
        </view>
      </view>
      <view class="form-item">
        <view class="form-label">公里数</view>
        <input
          class="form-input"
          type="digit"
          placeholder="请输入公里数"
          v-model="taskForm.distance"
        />
      </view>
      <view class="form-item">
        <view class="form-label">成交价</view>
        <input
          class="form-input"
          type="digit"
          placeholder="请输入成交价"
          v-model="taskForm.price"
        />
      </view>
      <view class="form-actions">
        <button class="submit-btn" @click="submitTask" :disabled="loading">
          {{ loading ? '保存中...' : '保存' }}
        </button>
      </view>
    </view>
    <view class="loading" v-else>
      <uni-icons type="spinner-cycle" size="40" color="#007AFF"></uni-icons>
      <text>加载中...</text>
    </view>
    <!-- 地图选择器弹窗 -->
    <uni-popup ref="mapPopup" type="bottom" :mask-click="false">
      <view class="map-popup-container">
        <view class="popup-header">
          <view class="popup-title">选择地址</view>
          <view class="close-btn" @click="closeMapSelector">
            <uni-icons type="closeempty" size="20" color="#999"></uni-icons>
          </view>
        </view>
        <map-selector
          :initial-address="getInitialAddress()"
          @addressSelected="onAddressSelected"
        ></map-selector>
      </view>
    </uni-popup>
  </scroll-view>
</template>
<script>
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 { getTask, updateTask } from "@/api/task"
import MapSelector from '@/components/map-selector.vue'
import VehicleSelector from '@/components/VehicleSelector.vue'
import distanceCalculator from '@/mixins/distanceCalculator.js'
export default {
  components: {
    uniDatetimePicker,
    uniPopup,
    MapSelector,
    VehicleSelector
  },
  mixins: [distanceCalculator],
  data() {
    return {
      taskId: null,
      taskDetail: null,
      selectedVehicleId: null,
      selectedOrganization: '',
      mapSelectorType: '',
      taskForm: {
        serviceTime: '',
        passenger: {
          contact: '',
          phone: ''
        },
        startAddress: '',
        endAddress: '',
        distance: '',
        price: ''
      },
      organizations: ['广州分公司', '深圳分公司', '珠海分公司', '佛山分公司'],
      additionalStaff: [],
      loading: false
    }
  },
  computed: {
    ...mapState({
      currentUser: state => ({
        name: state.user.nickName || '张三',
        position: '司机',
        deptId: state.user.deptId || 100
      })
    })
  },
  onLoad(options) {
    if (options.id) {
      this.taskId = options.id
      this.loadTaskDetail()
    } else {
      this.$modal.showToast('任务ID不能为空')
      setTimeout(() => {
        uni.navigateBack()
      }, 1500)
    }
  },
  methods: {
    // 加载任务详情
    loadTaskDetail() {
      if (!this.taskId) {
        this.$modal.showToast('任务ID不能为空')
        return
      }
      getTask(this.taskId).then(response => {
        this.taskDetail = response.data || response
        // 填充表单数据 - 福祉车任务特有字段
        if (this.taskDetail.welfareInfo) {
          this.taskForm.serviceTime = this.taskDetail.plannedStartTime || ''
          this.taskForm.passenger.contact = this.taskDetail.welfareInfo.passengerContact || ''
          this.taskForm.passenger.phone = this.taskDetail.welfareInfo.passengerPhone || ''
          this.taskForm.startAddress = this.taskDetail.welfareInfo.pickupAddress || ''
          this.taskForm.endAddress = this.taskDetail.welfareInfo.destinationAddress || ''
          this.taskForm.distance = this.taskDetail.welfareInfo.serviceDistance || ''
          this.taskForm.price = this.taskDetail.welfareInfo.servicePrice || ''
        }
        // 设置车辆信息
        if (this.taskDetail.assignedVehicles && this.taskDetail.assignedVehicles.length > 0) {
          const firstVehicle = this.taskDetail.assignedVehicles[0]
          this.selectedVehicleId = firstVehicle.vehicleId
        }
        // 设置地址坐标(使用mixin中的方法)
        if (this.taskDetail.departureLongitude && this.taskDetail.departureLatitude) {
          this.setStartLocation({
            lng: this.taskDetail.departureLongitude,
            lat: this.taskDetail.departureLatitude
          })
        }
        if (this.taskDetail.destinationLongitude && this.taskDetail.destinationLatitude) {
          this.setEndLocation({
            lng: this.taskDetail.destinationLongitude,
            lat: this.taskDetail.destinationLatitude
          })
        }
      }).catch(error => {
        console.error('加载任务详情失败:', error)
        this.$modal.showToast('加载任务详情失败')
        setTimeout(() => {
          uni.navigateBack()
        }, 1500)
      })
    },
    // 车辆选择变化
    onVehicleChange(vehicle) {
      // 组件已经处理了selectedVehicleId的更新
      console.log('选中车辆:', vehicle)
    },
    onOrganizationChange(e) {
      this.selectedOrganization = this.organizations[e.detail.value]
    },
    selectStartAddress() {
      this.mapSelectorType = 'startAddress'
      this.$refs.mapPopup.open()
    },
    selectEndAddress() {
      this.mapSelectorType = 'endAddress'
      this.$refs.mapPopup.open()
    },
    getInitialAddress() {
      return this.mapSelectorType === 'startAddress'
        ? this.taskForm.startAddress
        : this.taskForm.endAddress
    },
    onAddressSelected(address) {
      const formattedAddress = address.title + ' - ' + address.address
      if (this.mapSelectorType === 'startAddress') {
        this.taskForm.startAddress = formattedAddress
        this.setLocationByAddress('start', address)
      } else if (this.mapSelectorType === 'endAddress') {
        this.taskForm.endAddress = formattedAddress
        this.setLocationByAddress('end', address)
      }
      // 监听mixin中的距离计算完成事件
      this.$once('distance-calculated', (distance) => {
        this.taskForm.distance = this.formatDistance(distance)
      })
      this.closeMapSelector()
    },
    closeMapSelector() {
      this.$refs.mapPopup.close()
      this.mapSelectorType = ''
    },
    addStaff() {
      this.$modal.showToast('添加人员功能开发中')
    },
    removeStaff(index) {
      this.additionalStaff.splice(index, 1)
    },
    validateForm() {
      if (!this.selectedVehicleId) {
        this.$modal.showToast('请选择任务车辆')
        return false
      }
      if (!this.taskForm.passenger.contact) {
        this.$modal.showToast('请输入联系人')
        return false
      }
      if (!this.taskForm.passenger.phone) {
        this.$modal.showToast('请输入联系电话')
        return false
      }
      if (!this.taskForm.startAddress) {
        this.$modal.showToast('请选择出发地址')
        return false
      }
      if (!this.taskForm.endAddress) {
        this.$modal.showToast('请选择目的地址')
        return false
      }
      return true
    },
    buildSubmitData() {
      const submitData = {
        taskId: this.taskId,
        taskType: 'WELFARE',
        vehicleIds: this.selectedVehicleId ? [this.selectedVehicleId] : [],
        plannedStartTime: this.taskForm.serviceTime,
        welfareInfo: {
          passengerContact: this.taskForm.passenger.contact,
          passengerPhone: this.taskForm.passenger.phone,
          pickupAddress: this.taskForm.startAddress,
          destinationAddress: this.taskForm.endAddress,
          serviceDistance: this.taskForm.distance ? parseFloat(this.taskForm.distance) : null,
          servicePrice: this.taskForm.price ? parseFloat(this.taskForm.price) : null
        }
      }
      if (this.addressCoordinates.startAddress) {
        submitData.departureLongitude = this.addressCoordinates.startAddress.lng
        submitData.departureLatitude = this.addressCoordinates.startAddress.lat
      }
      if (this.addressCoordinates.endAddress) {
        submitData.destinationLongitude = this.addressCoordinates.endAddress.lng
        submitData.destinationLatitude = this.addressCoordinates.endAddress.lat
      }
      return submitData
    },
    submitTask() {
      if (!this.validateForm()) {
        return
      }
      this.$modal.confirm('确定要保存修改吗?').then(() => {
        this.loading = true
        const submitData = this.buildSubmitData()
        updateTask(submitData).then(response => {
          this.loading = false
          this.$modal.showToast('任务更新成功')
          setTimeout(() => {
            uni.navigateBack()
          }, 1500)
        }).catch(error => {
          this.loading = false
          console.error('任务更新失败:', error)
          this.$modal.showToast('任务更新失败,请重试')
        })
      }).catch(() => {})
    },
    goBack() {
      uni.navigateBack()
    }
  }
}
</script>
<style lang="scss" scoped>
.edit-welfare-task-container {
  padding: 20rpx;
  background-color: #f5f5f5;
  min-height: 100vh;
  .form-header {
    display: flex;
    align-items: center;
    padding: 20rpx 0;
    margin-bottom: 30rpx;
    .back-btn {
      width: 60rpx;
      height: 60rpx;
      border-radius: 50%;
      background-color: #f0f0f0;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-right: 20rpx;
    }
    .title {
      font-size: 36rpx;
      font-weight: bold;
      color: #333;
    }
  }
  .form-section {
    background-color: white;
    border-radius: 15rpx;
    padding: 30rpx;
    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
    .form-section-title {
      font-size: 32rpx;
      font-weight: bold;
      margin: 40rpx 0 20rpx 0;
      padding-bottom: 10rpx;
      border-bottom: 1rpx solid #f0f0f0;
    }
    .form-item {
      margin-bottom: 40rpx;
      .form-label {
        font-size: 28rpx;
        margin-bottom: 15rpx;
        color: #333;
      }
      .form-input {
        height: 70rpx;
        padding: 0 20rpx;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        font-size: 28rpx;
        &.picker-input {
          display: flex;
          align-items: center;
          justify-content: space-between;
        }
      }
      .form-textarea {
        width: 100%;
        min-height: 150rpx;
        padding: 20rpx;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        font-size: 28rpx;
      }
      .staff-list {
        .staff-item {
          display: flex;
          justify-content: space-between;
          align-items: center;
          padding: 20rpx;
          background-color: #f9f9f9;
          border-radius: 10rpx;
          margin-bottom: 20rpx;
        }
        .add-staff {
          display: flex;
          align-items: center;
          justify-content: center;
          padding: 20rpx;
          border: 1rpx dashed #007AFF;
          border-radius: 10rpx;
          color: #007AFF;
        }
      }
    }
    .form-actions {
      margin-top: 50rpx;
      text-align: center;
      .submit-btn {
        width: 80%;
        height: 80rpx;
        background-color: #007AFF;
        color: white;
        border-radius: 10rpx;
        font-size: 32rpx;
        &[disabled] {
          background-color: #ccc;
          color: #999;
        }
      }
    }
  }
  .loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 400rpx;
    color: #999;
    text {
      margin-top: 20rpx;
      font-size: 28rpx;
    }
  }
  .map-popup-container {
    height: 80vh;
    background-color: white;
    border-top-left-radius: 20rpx;
    border-top-right-radius: 20rpx;
    overflow: hidden;
    .popup-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 20rpx 30rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .popup-title {
        font-size: 32rpx;
        font-weight: bold;
        color: #333;
      }
      .close-btn {
        width: 50rpx;
        height: 50rpx;
        display: flex;
        align-items: center;
        justify-content: center;
      }
    }
  }
}
</style>
app/pages/task/edit.vue
New file
@@ -0,0 +1,520 @@
<template>
  <scroll-view class="edit-task-container" scroll-y="true">
    <view class="form-header">
      <view class="back-btn" @click="goBack">
        <uni-icons type="arrowleft" size="20"></uni-icons>
      </view>
      <view class="title">编辑任务</view>
    </view>
    <view class="form-section" v-if="taskDetail">
      <!-- 使用车辆选择器组件 -->
      <VehicleSelector
        v-model="taskForm.vehicleId"
        :dept-id="currentUser.deptId"
        :auto-select-bound="false"
        @change="onVehicleChange"
      />
      <!-- 使用任务类型选择器组件 -->
      <TaskTypeSelector
        v-model="taskForm.taskType"
        @change="onTaskTypeChange"
      />
      <view class="form-item">
        <view class="form-label">任务描述</view>
        <textarea
          class="form-textarea"
          placeholder="请输入任务描述"
          v-model="taskForm.taskDescription"
        />
      </view>
      <view class="form-item">
        <view class="form-label">任务出发地</view>
        <view class="form-input picker-input" @click="selectStartLocation">
          {{ taskForm.startLocation || '请选择任务出发地' }}
          <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
        </view>
      </view>
      <view class="form-item">
        <view class="form-label">任务目的地</view>
        <view class="form-input picker-input" @click="selectEndLocation">
          {{ taskForm.endLocation || '请选择任务目的地' }}
          <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
        </view>
      </view>
      <view class="form-item">
        <view class="form-label">行驶公里数</view>
        <input
          class="form-input"
          type="digit"
          placeholder="请输入行驶公里数"
          v-model="taskForm.distance"
        />
      </view>
      <view class="form-item">
        <view class="form-label">计划开始时间</view>
        <uni-datetime-picker
          v-model="taskForm.plannedStartTime"
          type="datetime"
          :placeholder="'请选择计划开始时间'"
          class="form-input"
        />
      </view>
      <view class="form-item">
        <view class="form-label">计划结束时间</view>
        <uni-datetime-picker
          v-model="taskForm.plannedEndTime"
          type="datetime"
          :placeholder="'请选择计划结束时间'"
          class="form-input"
        />
      </view>
      <view class="form-item">
        <view class="form-label">执行人</view>
        <input
          class="form-input"
          :value="currentUser.name"
          disabled
        />
      </view>
      <view class="form-item">
        <view class="form-label">备注</view>
        <textarea
          class="form-textarea"
          placeholder="请输入备注信息(最多200字)"
          v-model="taskForm.remark"
          maxlength="200"
        />
      </view>
      <view class="form-actions">
        <button class="submit-btn" @click="submitTask" :disabled="loading">
          {{ loading ? '保存中...' : '保存' }}
        </button>
      </view>
    </view>
    <view class="loading" v-else>
      <uni-icons type="spinner-cycle" size="40" color="#007AFF"></uni-icons>
      <text>加载中...</text>
    </view>
    <!-- 地图选择器弹窗 -->
    <uni-popup ref="mapPopup" type="bottom" :mask-click="false">
      <view class="map-popup-container">
        <view class="popup-header">
          <view class="popup-title">选择地址</view>
          <view class="close-btn" @click="closeMapSelector">
            <uni-icons type="closeempty" size="20" color="#999"></uni-icons>
          </view>
        </view>
        <map-selector
          :initial-address="getInitialAddress()"
          @addressSelected="onAddressSelected"
        ></map-selector>
      </view>
    </uni-popup>
  </scroll-view>
</template>
<script>
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 { getTask, updateTask } from "@/api/task"
import MapSelector from '@/components/map-selector.vue'
import VehicleSelector from '@/components/VehicleSelector.vue'
import TaskTypeSelector from '@/components/TaskTypeSelector.vue'
import distanceCalculator from '@/mixins/distanceCalculator.js'
export default {
  components: {
    uniDatetimePicker,
    uniPopup,
    MapSelector,
    VehicleSelector,
    TaskTypeSelector
  },
  mixins: [distanceCalculator],
  data() {
    return {
      taskId: null,
      taskDetail: null,
      mapSelectorType: '',
      taskForm: {
        taskDescription: '',
        taskType: '',
        vehicleId: null,
        plannedStartTime: '',
        plannedEndTime: '',
        startLocation: '',
        endLocation: '',
        distance: '',
        remark: ''
      },
      loading: false
    }
  },
  computed: {
    ...mapState({
      currentUser: state => ({
        name: state.user.nickName || '张三',
        position: '司机',
        deptId: state.user.deptId || 100
      })
    })
  },
  onLoad(options) {
    if (options.id) {
      this.taskId = options.id
      this.loadTaskDetail()
    } else {
      this.$modal.showToast('任务ID不能为空')
      setTimeout(() => {
        uni.navigateBack()
      }, 1500)
    }
  },
  methods: {
    // 加载任务详情
    loadTaskDetail() {
      if (!this.taskId) {
        this.$modal.showToast('任务ID不能为空')
        return
      }
      getTask(this.taskId).then(response => {
        this.taskDetail = response.data || response
        // 填充表单数据
        this.taskForm.taskDescription = this.taskDetail.taskDescription || ''
        this.taskForm.taskType = this.taskDetail.taskType || ''
        this.taskForm.plannedStartTime = this.taskDetail.plannedStartTime || ''
        this.taskForm.plannedEndTime = this.taskDetail.plannedEndTime || ''
        this.taskForm.startLocation = this.taskDetail.departureAddress || ''
        this.taskForm.endLocation = this.taskDetail.destinationAddress || ''
        this.taskForm.distance = this.taskDetail.estimatedDistance || ''
        this.taskForm.remark = this.taskDetail.remark || ''
        // 设置车辆信息
        if (this.taskDetail.assignedVehicles && this.taskDetail.assignedVehicles.length > 0) {
          const firstVehicle = this.taskDetail.assignedVehicles[0]
          this.taskForm.vehicleId = firstVehicle.vehicleId
        }
        // 设置地址坐标(使用mixin中的方法)
        if (this.taskDetail.departureLongitude && this.taskDetail.departureLatitude) {
          this.setStartLocation({
            lng: this.taskDetail.departureLongitude,
            lat: this.taskDetail.departureLatitude
          })
        }
        if (this.taskDetail.destinationLongitude && this.taskDetail.destinationLatitude) {
          this.setEndLocation({
            lng: this.taskDetail.destinationLongitude,
            lat: this.taskDetail.destinationLatitude
          })
        }
      }).catch(error => {
        console.error('加载任务详情失败:', error)
        this.$modal.showToast('加载任务详情失败')
        setTimeout(() => {
          uni.navigateBack()
        }, 1500)
      })
    },
    // 车辆选择变化
    onVehicleChange(vehicle) {
      // 组件已经处理了vehicleId的更新
      console.log('选中车辆:', vehicle)
    },
    // 任务类型选择变化
    onTaskTypeChange(taskType) {
      // 组件已经处理了taskType的更新
      console.log('选中任务类型:', taskType)
    },
    selectStartLocation() {
      this.mapSelectorType = 'startLocation'
      this.$refs.mapPopup.open()
    },
    selectEndLocation() {
      this.mapSelectorType = 'endLocation'
      this.$refs.mapPopup.open()
    },
    getInitialAddress() {
      return this.mapSelectorType === 'startLocation' ? this.taskForm.startLocation : this.taskForm.endLocation
    },
    onAddressSelected(address) {
      const formattedAddress = address.title + ' - ' + address.address
      if (this.mapSelectorType === 'startLocation') {
        this.taskForm.startLocation = formattedAddress
        this.setLocationByAddress('start', address)
      } else if (this.mapSelectorType === 'endLocation') {
        this.taskForm.endLocation = formattedAddress
        this.setLocationByAddress('end', address)
      }
      // 监听mixin中的距离计算完成事件
      this.$once('distance-calculated', (distance) => {
        this.taskForm.distance = this.formatDistance(distance)
      })
      this.closeMapSelector()
    },
    closeMapSelector() {
      this.$refs.mapPopup.close()
      this.mapSelectorType = ''
    },
    validateForm() {
      if (!this.taskForm.vehicleId) {
        this.$modal.showToast('请选择任务车辆')
        return false
      }
      if (!this.taskForm.taskType) {
        this.$modal.showToast('请选择任务类型')
        return false
      }
      if (!this.taskForm.taskDescription) {
        this.$modal.showToast('请输入任务描述')
        return false
      }
      if (!this.taskForm.startLocation) {
        this.$modal.showToast('请选择任务出发地')
        return false
      }
      if (!this.taskForm.endLocation) {
        this.$modal.showToast('请选择任务目的地')
        return false
      }
      if (!this.taskForm.plannedStartTime) {
        this.$modal.showToast('请选择开始时间')
        return false
      }
      if (!this.taskForm.plannedEndTime) {
        this.$modal.showToast('请选择结束时间')
        return false
      }
      return true
    },
    buildSubmitData() {
      const submitData = {
        taskId: this.taskId,
        taskDescription: this.taskForm.taskDescription,
        taskType: this.taskForm.taskType,
        vehicleIds: this.taskForm.vehicleId ? [this.taskForm.vehicleId] : [],
        plannedStartTime: this.taskForm.plannedStartTime,
        plannedEndTime: this.taskForm.plannedEndTime,
        departureAddress: this.taskForm.startLocation,
        destinationAddress: this.taskForm.endLocation,
        estimatedDistance: this.taskForm.distance ? parseFloat(this.taskForm.distance) : null,
        remark: this.taskForm.remark
      }
      if (this.addressCoordinates.startLocation) {
        submitData.departureLongitude = this.addressCoordinates.startLocation.lng
        submitData.departureLatitude = this.addressCoordinates.startLocation.lat
      }
      if (this.addressCoordinates.endLocation) {
        submitData.destinationLongitude = this.addressCoordinates.endLocation.lng
        submitData.destinationLatitude = this.addressCoordinates.endLocation.lat
      }
      return submitData
    },
    submitTask() {
      if (!this.validateForm()) {
        return
      }
      this.$modal.confirm('确定要保存修改吗?').then(() => {
        this.loading = true
        const submitData = this.buildSubmitData()
        updateTask(submitData).then(response => {
          this.loading = false
          this.$modal.showToast('任务更新成功')
          setTimeout(() => {
            uni.navigateBack()
          }, 1500)
        }).catch(error => {
          this.loading = false
          console.error('任务更新失败:', error)
          this.$modal.showToast('任务更新失败,请重试')
        })
      }).catch(() => {})
    },
    goBack() {
      uni.navigateBack()
    }
  }
}
</script>
<style lang="scss" scoped>
.edit-task-container {
  padding: 20rpx;
  background-color: #f5f5f5;
  min-height: 100vh;
  .form-header {
    display: flex;
    align-items: center;
    padding: 20rpx 0;
    margin-bottom: 30rpx;
    .back-btn {
      width: 60rpx;
      height: 60rpx;
      border-radius: 50%;
      background-color: #f0f0f0;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-right: 20rpx;
    }
    .title {
      font-size: 36rpx;
      font-weight: bold;
      color: #333;
    }
  }
  .form-section {
    background-color: white;
    border-radius: 15rpx;
    padding: 30rpx;
    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
    .form-item {
      margin-bottom: 40rpx;
      .form-label {
        font-size: 28rpx;
        margin-bottom: 15rpx;
        color: #333;
      }
      .form-input {
        height: 70rpx;
        padding: 0 20rpx;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        font-size: 28rpx;
        &.picker-input {
          display: flex;
          align-items: center;
          justify-content: space-between;
        }
        &[disabled] {
          background-color: #f5f5f5;
          color: #999;
        }
      }
      .form-textarea {
        width: 100%;
        min-height: 150rpx;
        padding: 20rpx;
        border: 1rpx solid #eee;
        border-radius: 10rpx;
        font-size: 28rpx;
      }
    }
    .form-actions {
      margin-top: 50rpx;
      text-align: center;
      .submit-btn {
        width: 80%;
        height: 80rpx;
        background-color: #007AFF;
        color: white;
        border-radius: 10rpx;
        font-size: 32rpx;
        &[disabled] {
          background-color: #ccc;
          color: #999;
        }
      }
    }
  }
  .loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 400rpx;
    color: #999;
    text {
      margin-top: 20rpx;
      font-size: 28rpx;
    }
  }
  .map-popup-container {
    height: 80vh;
    background-color: white;
    border-top-left-radius: 20rpx;
    border-top-right-radius: 20rpx;
    overflow: hidden;
    .popup-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 20rpx 30rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .popup-title {
        font-size: 32rpx;
        font-weight: bold;
        color: #333;
      }
      .close-btn {
        width: 50rpx;
        height: 50rpx;
        display: flex;
        align-items: center;
        justify-content: center;
      }
    }
  }
}
</style>
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java
@@ -1055,9 +1055,9 @@
    @GetMapping("/tianditu/place/suggestion")
    public AjaxResult tiandituPlaceSuggestion(String keyWord, String region, String city, Integer count) {
        try {
            // 检查参数
            // 检查必需参数
            if (keyWord == null || keyWord.trim().isEmpty()) {
                return AjaxResult.error("参数不完整,缺少搜索关键词");
                return AjaxResult.error("参数不完整,缺少搜索关键词(keyWord)");
            }
            
            // 设置默认值
@@ -1065,38 +1065,50 @@
                count = 10;
            }
            
            // 构建天地图输入提示API URL
            String url = "http://api.tianditu.gov.cn/search";
            // 构建天地图普通搜索API URL
            String url = "http://api.tianditu.gov.cn/v2/search";
            StringBuilder paramsBuilder = new StringBuilder();
            paramsBuilder.append("postStr={\"keyWord\":\"").append(keyWord).append("\"");
            // 硬编码中国地图范围
            paramsBuilder.append(",\"mapBound\":\"").append("73.66,3.86,135.05,53.55").append("\"");
            // 默认级别为18
            paramsBuilder.append(",\"level\":\"").append("18").append("\"");
            // 默认查询类型为普通搜索
            paramsBuilder.append(",\"queryType\":\"").append("1").append("\"");
            // 默认起始位置为0
            paramsBuilder.append(",\"start\":\"").append("0").append("\"");
            paramsBuilder.append(",\"count\":\"").append(count).append("\"");
            // 添加可选参数
            if (region != null && !region.trim().isEmpty()) {
                paramsBuilder.append(",\"region\":\"").append(region).append("\"");
                paramsBuilder.append(",\"specify\":\"").append(region).append("\"");
            }
            if (city != null && !city.trim().isEmpty()) {
                paramsBuilder.append(",\"city\":\"").append(city).append("\"");
                paramsBuilder.append(",\"dataTypes\":\"").append(city).append("\"");
            }
            paramsBuilder.append(",\"count\":\"").append(count).append("\"");
            paramsBuilder.append("}" );
            paramsBuilder.append("&type=suggest");
            paramsBuilder.append("}");
            paramsBuilder.append("&type=query");
            paramsBuilder.append("&tk=").append(tiandituMapConfig.getTk());
            
            String params = paramsBuilder.toString();
            
            logger.info("天地图输入提示请求: keyWord={}, region={}", keyWord, region);
            logger.info("天地图普通搜索请求: keyWord={}, count={}", keyWord, count);
            logger.debug("地图请求接口参数:{}", params);
            
            // 发送HTTP请求
            String response = HttpUtils.sendGet(url, params);
            logger.debug("天地图输入提示响应: {}", response);
            logger.debug("天地图普通搜索响应: {}", response);
            
            // 解析响应
            com.alibaba.fastjson2.JSONObject jsonResponse = com.alibaba.fastjson2.JSONObject.parseObject(response);
            if (!"0".equals(jsonResponse.getString("status"))) {
                logger.error("输入提示失败: {}", response);
                logger.error("普通搜索失败: {}", response);
                return AjaxResult.error("地址搜索失败");
            }
            
            // 提取提示列表
            com.alibaba.fastjson2.JSONArray results = jsonResponse.getJSONArray("suggests");
            // 提取搜索结果
            com.alibaba.fastjson2.JSONArray results = jsonResponse.getJSONArray("pois");
            if (results == null || results.isEmpty()) {
                logger.info("未找到匹配的地址");
                return AjaxResult.success("查询成功", new ArrayList<>());
@@ -1109,13 +1121,16 @@
                
                Map<String, Object> suggestion = new HashMap<>();
                suggestion.put("name", item.getString("name")); // 名称
                suggestion.put("address", item.getString("address")); // 地址
                suggestion.put("province", item.getString("province")); // 省
                suggestion.put("address", item.getString("addr")); // 地址
                suggestion.put("province", item.getString("prov")); // 省
                suggestion.put("city", item.getString("city")); // 市
                suggestion.put("district", item.getString("district")); // 区
                suggestion.put("district", item.getString("county")); // 区县
                suggestion.put("tel", item.getString("tel")); // 电话
                suggestion.put("postcode", item.getString("postcode")); // 邮编
                suggestion.put("type", item.getString("type")); // 类型
                
                // 经纬度信息
                com.alibaba.fastjson2.JSONObject location = item.getJSONObject("location");
                com.alibaba.fastjson2.JSONObject location = item.getJSONObject("lonlat");
                if (location != null) {
                    Map<String, Object> locationMap = new HashMap<>();
                    locationMap.put("lon", location.getDouble("lon"));
@@ -1126,10 +1141,10 @@
                suggestions.add(suggestion);
            }
            
            logger.info("地址搜索提示成功: 找到{}条结果", suggestions.size());
            logger.info("地址搜索成功: 找到{}条结果", suggestions.size());
            return AjaxResult.success("查询成功", suggestions);
        } catch (Exception e) {
            logger.error("地址搜索提示失败", e);
            logger.error("地址搜索失败", e);
            return AjaxResult.error("地址搜索失败:" + e.getMessage());
        }
    }
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskUpdateVO.java
@@ -2,6 +2,7 @@
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
@@ -49,6 +50,21 @@
    /** 目的地纬度 */
    private BigDecimal destinationLatitude;
    /** 任务类型 */
    private String taskType;
    /** 部门ID */
    private Long deptId;
    /** 车辆ID列表 */
    private List<Long> vehicleIds;
    /** 病情ID列表(用于同步调度单的OrdICD_ID参数) */
    private List<Long> diseaseIds;
    /** 急救转运任务扩展信息 */
    private EmergencyInfoVO emergencyInfo;
    public Long getTaskId() {
        return taskId;
@@ -145,4 +161,314 @@
    public void setDestinationLatitude(BigDecimal destinationLatitude) {
        this.destinationLatitude = destinationLatitude;
    }
    public String getTaskType() {
        return taskType;
    }
    public void setTaskType(String taskType) {
        this.taskType = taskType;
    }
    public Long getDeptId() {
        return deptId;
    }
    public void setDeptId(Long deptId) {
        this.deptId = deptId;
    }
    public List<Long> getVehicleIds() {
        return vehicleIds;
    }
    public void setVehicleIds(List<Long> vehicleIds) {
        this.vehicleIds = vehicleIds;
    }
    public List<Long> getDiseaseIds() {
        return diseaseIds;
    }
    public void setDiseaseIds(List<Long> diseaseIds) {
        this.diseaseIds = diseaseIds;
    }
    public EmergencyInfoVO getEmergencyInfo() {
        return emergencyInfo;
    }
    public void setEmergencyInfo(EmergencyInfoVO emergencyInfo) {
        this.emergencyInfo = emergencyInfo;
    }
    /**
     * 急救转运任务扩展信息内部类
     */
    public static class EmergencyInfoVO {
        /** 患者联系人 */
        private String patientContact;
        /** 患者联系电话 */
        private String patientPhone;
        /** 患者姓名 */
        private String patientName;
        /** 患者性别 */
        private String patientGender;
        /** 患者身份证号 */
        private String patientIdCard;
        /** 患者病情描述 */
        private String patientCondition;
        /** 转出医院ID */
        private Long hospitalOutId;
        /** 转出医院名称 */
        private String hospitalOutName;
        /** 转出医院科室 */
        private String hospitalOutDepartment;
        /** 转出医院科室ID */
        private String hospitalOutDepartmentId;
        /** 转出医院床号 */
        private String hospitalOutBedNumber;
        /** 转出医院地址 */
        private String hospitalOutAddress;
        /** 转出医院经度 */
        private BigDecimal hospitalOutLongitude;
        /** 转出医院纬度 */
        private BigDecimal hospitalOutLatitude;
        /** 转入医院ID */
        private Long hospitalInId;
        /** 转入医院名称 */
        private String hospitalInName;
        /** 转入医院科室 */
        private String hospitalInDepartment;
        /** 转入医院科室ID */
        private String hospitalInDepartmentId;
        /** 转入医院床号 */
        private String hospitalInBedNumber;
        /** 转入医院地址 */
        private String hospitalInAddress;
        /** 转入医院经度 */
        private BigDecimal hospitalInLongitude;
        /** 转入医院纬度 */
        private BigDecimal hospitalInLatitude;
        /** 转运公里数 */
        private BigDecimal transferDistance;
        /** 转运费用 */
        private BigDecimal transferPrice;
        // Getters and Setters
        public String getPatientContact() {
            return patientContact;
        }
        public void setPatientContact(String patientContact) {
            this.patientContact = patientContact;
        }
        public String getPatientPhone() {
            return patientPhone;
        }
        public void setPatientPhone(String patientPhone) {
            this.patientPhone = patientPhone;
        }
        public String getPatientName() {
            return patientName;
        }
        public void setPatientName(String patientName) {
            this.patientName = patientName;
        }
        public String getPatientGender() {
            return patientGender;
        }
        public void setPatientGender(String patientGender) {
            this.patientGender = patientGender;
        }
        public String getPatientIdCard() {
            return patientIdCard;
        }
        public void setPatientIdCard(String patientIdCard) {
            this.patientIdCard = patientIdCard;
        }
        public String getPatientCondition() {
            return patientCondition;
        }
        public void setPatientCondition(String patientCondition) {
            this.patientCondition = patientCondition;
        }
        public Long getHospitalOutId() {
            return hospitalOutId;
        }
        public void setHospitalOutId(Long hospitalOutId) {
            this.hospitalOutId = hospitalOutId;
        }
        public String getHospitalOutName() {
            return hospitalOutName;
        }
        public void setHospitalOutName(String hospitalOutName) {
            this.hospitalOutName = hospitalOutName;
        }
        public String getHospitalOutDepartment() {
            return hospitalOutDepartment;
        }
        public void setHospitalOutDepartment(String hospitalOutDepartment) {
            this.hospitalOutDepartment = hospitalOutDepartment;
        }
        public String getHospitalOutDepartmentId() {
            return hospitalOutDepartmentId;
        }
        public void setHospitalOutDepartmentId(String hospitalOutDepartmentId) {
            this.hospitalOutDepartmentId = hospitalOutDepartmentId;
        }
        public String getHospitalOutBedNumber() {
            return hospitalOutBedNumber;
        }
        public void setHospitalOutBedNumber(String hospitalOutBedNumber) {
            this.hospitalOutBedNumber = hospitalOutBedNumber;
        }
        public String getHospitalOutAddress() {
            return hospitalOutAddress;
        }
        public void setHospitalOutAddress(String hospitalOutAddress) {
            this.hospitalOutAddress = hospitalOutAddress;
        }
        public BigDecimal getHospitalOutLongitude() {
            return hospitalOutLongitude;
        }
        public void setHospitalOutLongitude(BigDecimal hospitalOutLongitude) {
            this.hospitalOutLongitude = hospitalOutLongitude;
        }
        public BigDecimal getHospitalOutLatitude() {
            return hospitalOutLatitude;
        }
        public void setHospitalOutLatitude(BigDecimal hospitalOutLatitude) {
            this.hospitalOutLatitude = hospitalOutLatitude;
        }
        public Long getHospitalInId() {
            return hospitalInId;
        }
        public void setHospitalInId(Long hospitalInId) {
            this.hospitalInId = hospitalInId;
        }
        public String getHospitalInName() {
            return hospitalInName;
        }
        public void setHospitalInName(String hospitalInName) {
            this.hospitalInName = hospitalInName;
        }
        public String getHospitalInDepartment() {
            return hospitalInDepartment;
        }
        public void setHospitalInDepartment(String hospitalInDepartment) {
            this.hospitalInDepartment = hospitalInDepartment;
        }
        public String getHospitalInDepartmentId() {
            return hospitalInDepartmentId;
        }
        public void setHospitalInDepartmentId(String hospitalInDepartmentId) {
            this.hospitalInDepartmentId = hospitalInDepartmentId;
        }
        public String getHospitalInBedNumber() {
            return hospitalInBedNumber;
        }
        public void setHospitalInBedNumber(String hospitalInBedNumber) {
            this.hospitalInBedNumber = hospitalInBedNumber;
        }
        public String getHospitalInAddress() {
            return hospitalInAddress;
        }
        public void setHospitalInAddress(String hospitalInAddress) {
            this.hospitalInAddress = hospitalInAddress;
        }
        public BigDecimal getHospitalInLongitude() {
            return hospitalInLongitude;
        }
        public void setHospitalInLongitude(BigDecimal hospitalInLongitude) {
            this.hospitalInLongitude = hospitalInLongitude;
        }
        public BigDecimal getHospitalInLatitude() {
            return hospitalInLatitude;
        }
        public void setHospitalInLatitude(BigDecimal hospitalInLatitude) {
            this.hospitalInLatitude = hospitalInLatitude;
        }
        public BigDecimal getTransferDistance() {
            return transferDistance;
        }
        public void setTransferDistance(BigDecimal transferDistance) {
            this.transferDistance = transferDistance;
        }
        public BigDecimal getTransferPrice() {
            return transferPrice;
        }
        public void setTransferPrice(BigDecimal transferPrice) {
            this.transferPrice = transferPrice;
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -2,10 +2,7 @@
import java.io.*;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.util.*;
import java.util.stream.Collectors;
import java.net.HttpURLConnection;
import java.net.URL;
@@ -490,10 +487,51 @@
        task.setUpdateTime(DateUtils.getNowDate());
        task.setRemark(updateVO.getRemark());
        
        // 如果更新了部门ID
        if (updateVO.getDeptId() != null) {
            task.setDeptId(updateVO.getDeptId());
        }
        // 重新计算预计公里数
        calculateEstimatedDistance(task);
        
        int result = sysTaskMapper.updateSysTask(task);
        // 更新车辆关联
        if (result > 0 && updateVO.getVehicleIds() != null && !updateVO.getVehicleIds().isEmpty()) {
            // 查询现有的车辆关联
            List<SysTaskVehicle> existingVehicles = sysTaskVehicleMapper.selectSysTaskVehicleByTaskId(updateVO.getTaskId());
            List<Long> existingVehicleIds = existingVehicles.stream()
                .map(SysTaskVehicle::getVehicleId)
                .collect(Collectors.toList());
            // 比较新旧车辆ID列表,判断是否有变化
            boolean vehiclesChanged = !new HashSet<>(existingVehicleIds).equals(new HashSet<>(updateVO.getVehicleIds()));
            // 只有车辆发生变化时才更新
            if (vehiclesChanged) {
                // 删除旧的车辆关联
                sysTaskVehicleMapper.deleteSysTaskVehicleByTaskId(updateVO.getTaskId());
                // 添加新的车辆关联
                Date now = DateUtils.getNowDate();
                String currentUser = SecurityUtils.getUsername();
                for (Long vehicleId : updateVO.getVehicleIds()) {
                    SysTaskVehicle taskVehicle = new SysTaskVehicle();
                    taskVehicle.setTaskId(updateVO.getTaskId());
                    taskVehicle.setVehicleId(vehicleId);
                    taskVehicle.setAssignTime(now);
                    taskVehicle.setAssignBy(currentUser);
                    taskVehicle.setCreateTime(now);
                    sysTaskVehicleMapper.insertSysTaskVehicle(taskVehicle);
                }
            }
        }
        // 更新急救转运扩展信息
        if (result > 0 && "EMERGENCY_TRANSFER".equals(oldTask.getTaskType()) && updateVO.getEmergencyInfo() != null) {
            updateEmergencyInfo(updateVO.getTaskId(), updateVO);
        }
        
        // 记录操作日志
        if (result > 0) {
@@ -1454,6 +1492,124 @@
    }
    /**
     * 更新急救转运任务扩展信息
     *
     * @param taskId 任务ID
     * @param updateVO 任务更新对象
     */
    private void updateEmergencyInfo(Long taskId, TaskUpdateVO updateVO) {
        // 查询现有的扩展信息
        SysTaskEmergency existingInfo = sysTaskEmergencyMapper.selectSysTaskEmergencyByTaskId(taskId);
        if (existingInfo == null) {
            // 如果不存在,则创建新的
            existingInfo = new SysTaskEmergency();
            existingInfo.setTaskId(taskId);
            existingInfo.setCreateTime(DateUtils.getNowDate());
            existingInfo.setCreateBy(SecurityUtils.getUsername());
        }
        TaskUpdateVO.EmergencyInfoVO emergencyInfo = updateVO.getEmergencyInfo();
        // 更新患者信息
        if (emergencyInfo.getPatientContact() != null) {
            existingInfo.setPatientContact(emergencyInfo.getPatientContact());
        }
        if (emergencyInfo.getPatientPhone() != null) {
            existingInfo.setPatientPhone(emergencyInfo.getPatientPhone());
        }
        if (emergencyInfo.getPatientName() != null) {
            existingInfo.setPatientName(emergencyInfo.getPatientName());
        }
        if (emergencyInfo.getPatientGender() != null) {
            existingInfo.setPatientGender(emergencyInfo.getPatientGender());
        }
        if (emergencyInfo.getPatientIdCard() != null) {
            existingInfo.setPatientIdCard(emergencyInfo.getPatientIdCard());
        }
        if (emergencyInfo.getPatientCondition() != null) {
            existingInfo.setPatientCondition(emergencyInfo.getPatientCondition());
        }
        // 更新转出医院信息
        if (emergencyInfo.getHospitalOutId() != null) {
            existingInfo.setHospitalOutId(emergencyInfo.getHospitalOutId());
        }
        if (emergencyInfo.getHospitalOutName() != null) {
            existingInfo.setHospitalOutName(emergencyInfo.getHospitalOutName());
        }
        if (emergencyInfo.getHospitalOutDepartment() != null) {
            existingInfo.setHospitalOutDepartment(emergencyInfo.getHospitalOutDepartment());
        }
        if (emergencyInfo.getHospitalOutDepartmentId() != null) {
            existingInfo.setHospitalOutDepartmentId(emergencyInfo.getHospitalOutDepartmentId());
        }
        if (emergencyInfo.getHospitalOutBedNumber() != null) {
            existingInfo.setHospitalOutBedNumber(emergencyInfo.getHospitalOutBedNumber());
        }
        if (emergencyInfo.getHospitalOutAddress() != null) {
            existingInfo.setHospitalOutAddress(emergencyInfo.getHospitalOutAddress());
        }
        if (emergencyInfo.getHospitalOutLongitude() != null) {
            existingInfo.setHospitalOutLongitude(emergencyInfo.getHospitalOutLongitude());
        }
        if (emergencyInfo.getHospitalOutLatitude() != null) {
            existingInfo.setHospitalOutLatitude(emergencyInfo.getHospitalOutLatitude());
        }
        // 更新转入医院信息
        if (emergencyInfo.getHospitalInId() != null) {
            existingInfo.setHospitalInId(emergencyInfo.getHospitalInId());
        }
        if (emergencyInfo.getHospitalInName() != null) {
            existingInfo.setHospitalInName(emergencyInfo.getHospitalInName());
        }
        if (emergencyInfo.getHospitalInDepartment() != null) {
            existingInfo.setHospitalInDepartment(emergencyInfo.getHospitalInDepartment());
        }
        if (emergencyInfo.getHospitalInDepartmentId() != null) {
            existingInfo.setHospitalInDepartmentId(emergencyInfo.getHospitalInDepartmentId());
        }
        if (emergencyInfo.getHospitalInBedNumber() != null) {
            existingInfo.setHospitalInBedNumber(emergencyInfo.getHospitalInBedNumber());
        }
        if (emergencyInfo.getHospitalInAddress() != null) {
            existingInfo.setHospitalInAddress(emergencyInfo.getHospitalInAddress());
        }
        if (emergencyInfo.getHospitalInLongitude() != null) {
            existingInfo.setHospitalInLongitude(emergencyInfo.getHospitalInLongitude());
        }
        if (emergencyInfo.getHospitalInLatitude() != null) {
            existingInfo.setHospitalInLatitude(emergencyInfo.getHospitalInLatitude());
        }
        // 更新费用信息
        if (emergencyInfo.getTransferDistance() != null) {
            existingInfo.setTransferDistance(emergencyInfo.getTransferDistance());
        }
        if (emergencyInfo.getTransferPrice() != null) {
            existingInfo.setTransferPrice(emergencyInfo.getTransferPrice());
        }
        // 更新病情ID列表
        if (updateVO.getDiseaseIds() != null && !updateVO.getDiseaseIds().isEmpty()) {
            String diseaseIdsStr = updateVO.getDiseaseIds().stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
            existingInfo.setDiseaseIds(diseaseIdsStr);
        } else {
            // 如果病情ID列表为空,清空该字段
            existingInfo.setDiseaseIds(null);
        }
        // 系统字段
        existingInfo.setUpdateTime(DateUtils.getNowDate());
        existingInfo.setUpdateBy(SecurityUtils.getUsername());
        // 执行更新
        sysTaskEmergencyMapper.updateSysTaskEmergency(existingInfo);
    }
    /**
     * 保存福祉车任务扩展信息
     * 
     * @param taskId 任务ID
ruoyi-system/src/main/resources/areacode.json
ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml
@@ -186,10 +186,6 @@
            <if test="creatorId != null">creator_id,</if>
            <if test="assigneeId != null">assignee_id,</if>
            <if test="deptId != null">dept_id,</if>
            <if test="plannedStartTime!=null">planned_start_time,</if>
            <if test="plannedEndTime!=null">planned_end_time,</if>
            <if test="plannedStartTime!=null">planned_start_time,</if>
            <if test="plannedEndTime!=null">planned_end_time,</if>
            <if test="createTime != null">create_time,</if>
            update_time,
            <if test="createBy != null">create_by,</if>
@@ -216,10 +212,6 @@
            <if test="creatorId != null">#{creatorId},</if>
            <if test="assigneeId != null">#{assigneeId},</if>
            <if test="deptId != null">#{deptId},</if>
            <if test="plannedStartTime!=null">#{plannedStartTime},</if>
            <if test="plannedEndTime!=null">#{plannedEndTime},</if>
            <if test="actualStartTime!=null">#{actualStartTime},</if>
            <if test="actualEndTime!=null">#{actualEndTime},</if>
            <if test="createTime != null">#{createTime},</if>
            now(),
            <if test="createBy != null">#{createBy},</if>
ruoyi-system/src/test/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImplTest.java
File was deleted