wlzboy
1 天以前 08f95b2f159b56fa3bd4f4b348855989de8aa456
app/pagesTask/detail.vue
@@ -12,7 +12,10 @@
        <view class="section-title">基本信息</view>
        <view class="info-item">
          <view class="label">任务编号</view>
          <view class="value">{{ taskDetail.taskCode }}</view>
          <view class="value">
            {{ taskDetail.showTaskCode }}
            <text v-if="taskDetail.isHeadPush === '1'" class="head-push-tag">总</text>
          </view>
        </view>
        <view class="info-item">
          <view class="label">任务类型</view>
@@ -28,9 +31,53 @@
          <view class="label">执行车辆</view>
          <view class="value">{{ getVehicleInfo(taskDetail) }}</view>
        </view>
        <view class="info-item">
          <view class="label">执行人员</view>
          <view class="value">{{ taskDetail.assigneeName || '未分配' }}</view>
      </view>
      <!-- 执行人员列表 -->
      <view class="detail-section">
        <view class="section-title">执行人员</view>
        <view v-if="taskDetail.assignees && taskDetail.assignees.length > 0" class="assignee-list">
          <view
            class="assignee-item"
            v-for="(assignee, index) in taskDetail.assignees"
            :key="getAssigneeKey(assignee, index)"
          >
            <view class="assignee-index">{{ index + 1 }}</view>
            <view class="assignee-info">
              <view class="assignee-name">
                {{ assignee.userName }}
                <view v-if="assignee.isPrimary === '1'" class="primary-badge">
                  <uni-icons type="star-filled" size="12" color="#ff9500"></uni-icons>
                  <text>负责人</text>
                </view>
              </view>
              <view class="assignee-role">
                <view
                  class="role-tag"
                  :class="{
                    'role-driver': assignee.userType === 'driver',
                    'role-doctor': assignee.userType === 'doctor',
                    'role-nurse': assignee.userType === 'nurse'
                  }"
                >
                  {{ getUserTypeLabel(assignee.userType) }}
                </view>
                <view
                  class="ready-badge"
                  :class="{
                    'ready': isAssigneeReady(assignee),
                    'unready': !isAssigneeReady(assignee)
                  }"
                >
                  {{ isAssigneeReady(assignee) ? '已就绪' : '未就绪' }}
                </view>
              </view>
            </view>
          </view>
        </view>
        <view v-else class="empty-assignee">
          <uni-icons type="info" size="40" color="#ccc"></uni-icons>
          <text>暂无执行人员</text>
        </view>
      </view>
      
@@ -96,12 +143,12 @@
        </view>
      </view>
      
      <view class="detail-section" v-if="taskDetail.taskDescription">
      <view class="detail-section" v-if="taskDetail.taskDescription && taskDetail.taskType !== 'EMERGENCY_TRANSFER'">
        <view class="section-title">任务描述</view>
        <view class="description">{{ taskDetail.taskDescription }}</view>
      </view>
      
      <view class="detail-section" v-if="taskDetail.remark">
      <view class="detail-section" v-if="taskDetail.remark && taskDetail.taskType !== 'EMERGENCY_TRANSFER'">
        <view class="section-title">备注信息</view>
        <view class="description">{{ taskDetail.remark }}</view>
      </view>
@@ -215,8 +262,8 @@
        <view class="section-title">支付记录</view>
        <view 
          class="payment-record-item" 
          v-for="payment in paymentInfo.paidPayments"
          :key="payment.id"
          v-for="(payment, index) in paymentInfo.paidPayments"
          :key="getPaymentKey(payment, index)"
        >
          <view class="payment-header">
            <view 
@@ -327,18 +374,27 @@
        >
          修改
        </button>
        <button
          class="action-btn primary"
          @click="handleTaskAction('depart')"
        >
          出发
        </button>
        <button
          class="action-btn cancel"
          @click="handleTaskAction('cancel')"
        >
          取消
        </button>
        <template v-if="isCurrentUserAssignee()">
          <button
            v-if="showAssigneeReadyFeature() && isMultipleAssignees() && !isCurrentUserReady()"
            class="action-btn primary"
            @click="handleReadyAction()"
          >
            就绪
          </button>
          <button
            class="action-btn primary"
            @click="handleDepartAction()"
          >
            出发
          </button>
          <button
            class="action-btn cancel"
            @click="handleTaskAction('cancel')"
          >
            取消
          </button>
        </template>
      </template>
      
      <!-- 出发中状态: 显示编辑、已到达、强制结束 -->
@@ -349,18 +405,20 @@
        >
          修改
        </button>
        <button
          class="action-btn primary"
          @click="handleTaskAction('arrive')"
        >
          已到达
        </button>
        <button
          class="action-btn cancel"
          @click="handleTaskAction('forceCancel')"
        >
          强制结束
        </button>
        <template v-if="isCurrentUserAssignee()">
          <button
            class="action-btn primary"
            @click="handleTaskAction('arrive')"
          >
            已到达
          </button>
          <button
            class="action-btn cancel"
            @click="handleTaskAction('forceCancel')"
          >
            强制结束
          </button>
        </template>
      </template>
      
      <!-- 已到达状态: 显示编辑、已返程 -->
@@ -371,12 +429,14 @@
        >
          修改
        </button>
        <button
          class="action-btn primary"
          @click="handleTaskAction('return')"
        >
          已返程
        </button>
        <template v-if="isCurrentUserAssignee()">
          <button
            class="action-btn primary"
            @click="handleTaskAction('return')"
          >
            已返程
          </button>
        </template>
      </template>
      
      <!-- 返程中状态: 显示编辑、已完成 -->
@@ -387,12 +447,14 @@
        >
          修改
        </button>
        <button
          class="action-btn primary"
          @click="handleTaskAction('complete')"
        >
          已完成
        </button>
        <template v-if="isCurrentUserAssignee()">
          <button
            class="action-btn primary"
            @click="handleTaskAction('complete')"
          >
            已完成
          </button>
        </template>
      </template>
      
      <!-- 已完成/已取消: 不显示按钮,但如果是转运任务则显示结算按钮 -->
@@ -410,11 +472,13 @@
</template>
<script>
  import { getTask, changeTaskStatus } from '@/api/task'
  import { getTask, changeTaskStatus, setAssigneeReady } from '@/api/task'
  import { checkVehicleActiveTasks } from '@/api/task'
  import { getPaymentInfo } from '@/api/payment'
  import { formatDateTime } from '@/utils/common'
  import { validateTaskForDepart, validateTaskForSettlement, getTaskVehicleId, checkTaskCanDepart } from '@/utils/taskValidator'
  import AttachmentUpload from './components/AttachmentUpload.vue'
  import config from '@/config'
  
  export default {
    components: {
@@ -531,12 +595,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('转运任务信息 (emergencyInfo):', this.taskDetail.emergencyInfo)
          // 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)
          
          // 如果是转运任务,加载支付信息
          if (this.taskDetail.taskType === 'EMERGENCY_TRANSFER') {
@@ -600,9 +664,9 @@
        return remaining > 0 ? remaining.toFixed(2) : '0.00'
      },
      
      // 获取车辆信息
      // 获取车辆信息(修复:防止 assignedVehicles 为 null)
      getVehicleInfo(task) {
        if (task.assignedVehicles && task.assignedVehicles.length > 0) {
        if (task.assignedVehicles && Array.isArray(task.assignedVehicles) && task.assignedVehicles.length > 0) {
          const firstVehicle = task.assignedVehicles[0]
          let vehicleInfo = firstVehicle.vehicleNo || '未知车牌'
          if (task.assignedVehicles.length > 1) {
@@ -641,7 +705,16 @@
      
      // 返回上一页
      goBack() {
        uni.navigateBack()
        // 检查是否有页面可以返回
        uni.navigateBack({
          delta: 1,
          fail: () => {
            // 如果无法返回,则跳转到任务列表页面
            uni.switchTab({
              url: '/pages/task/index'
            })
          }
        })
      },
      
      // 处理编辑按钮
@@ -705,8 +778,27 @@
        return typeMap[type] || '未知类型'
      },
      
      // 获取用户类型标签
      getUserTypeLabel(userType) {
        const typeMap = {
          'driver': '司机',
          'doctor': '医生',
          'nurse': '护士'
        }
        return typeMap[userType] || userType || '未知'
      },
      // 处理结算
      handleSettlement() {
        // 校验任务是否可以结算
        const validation = validateTaskForSettlement(this.taskDetail)
        if (!validation.valid) {
          this.$modal.confirm(`${validation.message},需要先修改任务后才能结算。是否现在去修改?`).then(() => {
            this.handleEdit()
          }).catch(() => {})
          return
        }
        uni.navigateTo({
          url: '/pagesTask/settlement?taskId=' + this.taskId
        })
@@ -716,8 +808,7 @@
      handleTaskAction(action) {
        switch (action) {
          case 'depart':
            // 出发 -> 检查车辆是否有其他正在进行中的任务
            this.checkVehicleAndDepart();
            this.ensureReadyThenDepart();
            break;
            
          case 'cancel':
@@ -758,60 +849,84 @@
      },
      
      // 检查车辆状态并出发
      checkVehicleAndDepart() {
        // 获取任务车辆ID
        const vehicleId = this.getVehicleId();
        if (!vehicleId) {
          this.$modal.showToast('未找到任务车辆信息');
          return;
        }
      async checkVehicleAndDepart() {
        // 显示加载提示
        uni.showLoading({
          title: '检查车辆状态...'
          title: '检查任务状态...'
        });
        
        checkVehicleActiveTasks(vehicleId).then(response => {
        try {
          // 调用工具类检查任务是否可以出发(包含基本校验和冲突检查)
          const checkResult = await checkTaskCanDepart(this.taskDetail)
          uni.hideLoading();
          
          const activeTasks = response.data || [];
          console.log('出发检查结果:', checkResult);
          console.log('valid:', checkResult.valid);
          console.log('conflicts:', checkResult.conflicts);
          
          // 过滤掉当前任务本身
          const otherActiveTasks = activeTasks.filter(task => task.taskId !== this.taskId);
          if (otherActiveTasks.length > 0) {
            // 车辆有其他正在进行中的任务
            const task = otherActiveTasks[0];
            const taskStatus = this.getStatusText(task.taskStatus);
            const message = `该车辆已有正在转运中的任务!
任务单号:${task.taskCode}
任务状态:${taskStatus}
请先完成当前任务后再出发新任务。`;
          if (!checkResult.valid) {
            // 校验失败,显示提示信息并提供跳转选项
            const conflicts = checkResult.conflicts || [];
            const conflictInfo = conflicts.length > 0 ? conflicts[0] : null;
            
            uni.showModal({
              title: '提示',
              content: message,
              showCancel: false,
              confirmText: '我知道了'
            });
            console.log('冲突信息:', conflictInfo);
            // 如果有冲突任务信息,提供跳转按钮
            if (conflictInfo && conflictInfo.taskId) {
              console.log('显示带跳转按钮的弹窗,任务ID:', conflictInfo.taskId);
              const conflictTaskId = conflictInfo.taskId;
              const message = checkResult.message || conflictInfo.message || '存在冲突任务';
              uni.showModal({
                title: '提示',
                content: message,
                confirmText: '去处理',
                cancelText: '知道了',
                success: function(res) {
                  console.log('弹窗点击结果:', res);
                  if (res.confirm) {
                    // 用户点击"现在去处理",跳转到冲突任务详情页
                    console.log('准备跳转到任务详情页:', conflictTaskId);
                    uni.redirectTo({
                      url: `/pagesTask/detail?id=${conflictTaskId}`
                    });
                  }
                },
                fail: function(err) {
                  console.error('显示弹窗失败:', err);
                }
              });
            } else {
              // 没有冲突任务ID,只显示提示
              console.log('显示普通提示弹窗');
              uni.showModal({
                title: '提示',
                content: checkResult.message || '任务校验失败',
                showCancel: false,
                confirmText: '知道了',
                fail: function(err) {
                  console.error('显示弹窗失败:', err);
                }
              });
            }
            return;
          }
          
          // 车辆没有其他正在进行中的任务,可以出发
          // 所有检查通过,可以出发
          this.$modal.confirm('确定要出发吗?').then(() => {
            this.updateTaskStatus('DEPARTING', '任务已出发')
          }).catch(() => {});
          
        }).catch(error => {
        } catch (error) {
          uni.hideLoading();
          console.error('检查车辆状态失败:', error);
          console.error('检查任务状态失败:', error);
          // 检查失败时,仍然允许出发
          this.$modal.confirm('检查车辆状态失败,是否继续出发?').then(() => {
          this.$modal.confirm('检查任务状态失败,是否继续出发?').then(() => {
            this.updateTaskStatus('DEPARTING', '任务已出发')
          }).catch(() => {});
        });
        }
      },
      
      // 获取任务车辆ID
@@ -1156,7 +1271,165 @@
      // 附件删除成功回调
      onAttachmentDeleted(attachmentId) {
        console.log('附件删除成功:', attachmentId)
      }
      },
      // 是否显示“就绪”功能(配置开关)
      showAssigneeReadyFeature() {
        return !!(config && config.features && config.features.showAssigneeReadyButton)
      },
      // 当前用户是否为该执行人
      isAssigneeSelf(assignee) {
        const userId = this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.userId
        return assignee && (assignee.userId === userId || assignee.oaUserId === userId)
      },
      // 执行人点击“就绪”
      markAssigneeReady(assignee) {
        if (!assignee || !this.taskDetail) {
          this.$modal.showToast('执行人或任务信息不存在')
          return
        }
        const userId = assignee.userId || assignee.oaUserId
        if (!userId) {
          this.$modal.showToast('无法识别执行人ID')
          return
        }
        this.$modal.showLoading && this.$modal.showLoading('提交中...')
        setAssigneeReady(this.taskId).then(() => {
          this.$modal.hideLoading && this.$modal.hideLoading()
          this.$modal.showToast('已就绪')
          // 刷新任务详情
          this.loadTaskDetail()
        }).catch(err => {
          this.$modal.hideLoading && this.$modal.hideLoading()
          console.error('标记就绪失败:', err)
          this.$modal.showToast('标记就绪失败')
        })
      },
      // 是否当前用户是任务执行人
      isCurrentUserAssignee() {
        const userId = this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.userId;
        console.log("当前用户ID:", userId)
        const list = (this.taskDetail && Array.isArray(this.taskDetail.assignees)) ? this.taskDetail.assignees : []
        return list.some(a => a && (a.userId === userId || a.oaUserId === userId))
      },
      // 是否多人执行
      isMultipleAssignees() {
        const list = (this.taskDetail && Array.isArray(this.taskDetail.assignees)) ? this.taskDetail.assignees : []
        return list.length > 1
      },
      // 执行人是否已就绪
      isAssigneeReady(assignee) {
        if (!assignee) return false
        return assignee.isReady === '1' || assignee.ready === true || assignee.readyStatus === 'READY' || assignee.readyFlag === 'Y'
      },
      // 所有执行人是否已就绪
      areAllAssigneesReady() {
        const list = (this.taskDetail && Array.isArray(this.taskDetail.assignees)) ? this.taskDetail.assignees : []
        if (list.length === 0) return false
        return list.every(a => this.isAssigneeReady(a))
      },
      // 获取当前用户对应的执行人记录
      getCurrentUserAssignee() {
        const userId = this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.userId
        console.log('userId', userId)
        const list = (this.taskDetail && Array.isArray(this.taskDetail.assignees)) ? this.taskDetail.assignees : []
        return list.find(a => a && (a.userId === userId || a.oaUserId === userId)) || null
      },
      // 操作区就绪按钮(多人任务)
      markCurrentAssigneeReady() {
        const me = this.getCurrentUserAssignee()
        if (!me) {
          this.$modal.showToast('仅任务执行人可操作')
          return
        }
        this.markAssigneeReady(me)
      },
      // 当前用户是否已就绪
      isCurrentUserReady() {
        const me = this.getCurrentUserAssignee()
        return me ? this.isAssigneeReady(me) : false
      },
      // 处理就绪按钮点击
      async handleReadyAction() {
        const me = this.getCurrentUserAssignee()
        if (!me) {
          this.$modal.showToast('仅任务执行人可操作')
          return
        }
        try {
          await setAssigneeReady(this.taskId)
          this.$modal.showToast('已就绪')
          // 刷新任务详情
          await this.loadTaskDetail()
        } catch (err) {
          console.error('标记就绪失败:', err)
          this.$modal.showToast('标记就绪失败')
        }
      },
      // 处理出发按钮点击
      async handleDepartAction() {
        if (!this.taskDetail) return
        const list = (this.taskDetail && Array.isArray(this.taskDetail.assignees)) ? this.taskDetail.assignees : []
        // 如果开启了就绪功能且是多人任务,需要检查所有人是否就绪
        if (this.showAssigneeReadyFeature() && list.length > 1) {
          if (!this.areAllAssigneesReady()) {
            this.$modal.showToast('其他人未就绪,所有人就绪后才能出发')
            return
          }
        }
        // 单人任务或未开启就绪功能:自动标记就绪
        if (this.showAssigneeReadyFeature() && list.length === 1) {
          const me = this.getCurrentUserAssignee()
          if (me && !this.isAssigneeReady(me)) {
            try {
              await setAssigneeReady(this.taskId)
            } catch (e) {
              console.error('自动就绪失败:', e)
            }
          }
        }
        // 执行出发流程
        this.checkVehicleAndDepart()
      },
      // 出发前保证就绪(保留向后兼容)
      async ensureReadyThenDepart() {
        this.handleDepartAction()
      },
      // 获取执行人员的key值
      getAssigneeKey(assignee, index) {
        // 确保返回有效的字符串key
        if (!assignee) return 'assignee-' + index;
        // 优先使用userId,其次是userName,最后使用index
        const key = assignee.userId || assignee.userName || index;
        return 'assignee-' + (key !== null && key !== undefined ? key : index);
      },
      // 获取支付记录的key值
      getPaymentKey(payment, index) {
        // 确保返回有效的字符串key
        if (!payment) return 'payment-' + index;
        // 优先使用id,其次使用index
        const key = payment.id || index;
        return 'payment-' + (key !== null && key !== undefined ? key : index);
      },
    }
  }
</script>
@@ -1191,6 +1464,17 @@
      }
    }
    
    // 总部推送标记样式
    .head-push-tag {
      color: #ff0000;
      font-size: 24rpx;
      font-weight: bold;
      margin-left: 10rpx;
      padding: 2rpx 8rpx;
      border: 1rpx solid #ff0000;
      border-radius: 4rpx;
    }
    .detail-content {
      padding: 20rpx;
      height: calc(100vh - 220rpx); // 减去header(100rpx)和按钮区域(120rpx)的高度
@@ -1225,6 +1509,129 @@
        }
      }
      
      // 执行人员列表样式
      .assignee-list {
        .assignee-item {
          display: flex;
          align-items: center;
          padding: 20rpx;
          margin-bottom: 15rpx;
          background-color: #f9f9f9;
          border-radius: 10rpx;
          &:last-child {
            margin-bottom: 0;
          }
          .assignee-index {
            width: 50rpx;
            height: 50rpx;
            border-radius: 50%;
            background-color: #007AFF;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24rpx;
            font-weight: bold;
            margin-right: 20rpx;
            flex-shrink: 0;
          }
          .assignee-info {
            flex: 1;
            display: flex;
            flex-direction: column;
            gap: 10rpx;
            .assignee-name {
              display: flex;
              align-items: center;
              font-size: 30rpx;
              color: #333;
              font-weight: 500;
              .primary-badge {
                display: inline-flex;
                align-items: center;
                gap: 4rpx;
                margin-left: 12rpx;
                padding: 4rpx 12rpx;
                background-color: #fff3e0;
                border-radius: 6rpx;
                text {
                  font-size: 20rpx;
                  color: #ff9500;
                  font-weight: normal;
                }
              }
            }
            .assignee-role {
              .role-tag {
                display: inline-block;
                padding: 4rpx 12rpx;
                border-radius: 6rpx;
                font-size: 22rpx;
                color: white;
                &.role-driver {
                  background-color: #007AFF;
                }
                &.role-doctor {
                  background-color: #34C759;
                }
                &.role-nurse {
                  background-color: #AF52DE;
                }
              }
              .assignee-ready-btn {
                margin-left: 12rpx;
                padding: 8rpx 16rpx;
                font-size: 24rpx;
                border-radius: 6rpx;
                background-color: #34C759;
                color: #fff;
                border: none;
              }
              .ready-badge {
                display: inline-block;
                margin-left: 12rpx;
                padding: 4rpx 12rpx;
                font-size: 22rpx;
                border-radius: 6rpx;
                &.ready {
                  background-color: #e6ffed;
                  color: #34C759;
                }
                &.unready {
                  background-color: #f0f0f0;
                  color: #999;
                }
              }
            }
          }
        }
      }
      .empty-assignee {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 60rpx 0;
        color: #999;
        text {
          margin-top: 20rpx;
          font-size: 28rpx;
        }
      }
      .info-item {
        display: flex;
        margin-bottom: 20rpx;