wlzboy
2025-12-26 4fdde57a837b47b0a04aa17a7627c21b7425eda2
app/pagesTask/detail.vue
@@ -5,6 +5,9 @@
        <uni-icons type="arrowleft" size="20"></uni-icons>
      </view>
      <view class="title">任务详情</view>
      <view class="edit-btn" @click="handleEdit" v-if="taskDetail && !isTaskFinished">
        <uni-icons type="compose" size="20" color="#007AFF"></uni-icons>
      </view>
    </view>
    
    <scroll-view class="detail-content" scroll-y="true" v-if="taskDetail">
@@ -64,6 +67,17 @@
                  {{ isAssigneeReady(assignee) ? '已就绪' : '未就绪' }}
                </view>
              </view>
            </view>
            <!-- 当前登录人是该执行人且未就绪时显示就绪按钮 -->
            <view
              v-if="showAssigneeReadyFeature() && isAssigneeSelf(assignee) && !isAssigneeReady(assignee) && taskDetail.taskStatus === 'PENDING'"
              class="assignee-ready-btn"
              :data-user-id="assignee.userId || assignee.oaUserId"
              :data-user-name="assignee.userName"
              :data-index="index"
              @click="handleReadyClick"
            >
              点击就绪
            </view>
          </view>
        </view>
@@ -373,6 +387,51 @@
      <text>加载中...</text>
    </view>
    
    <!-- 强制完成对话框 -->
    <uni-popup ref="forceCompletePopup" type="center" :is-mask-click="false">
      <view class="force-complete-dialog">
        <view class="dialog-title">请输入时间</view>
        <view class="time-picker-item">
          <view class="time-label">转运开始时间</view>
          <picker
            mode="date"
            :value="getDateFromDateTime(forceCompleteForm.actualStartTime)"
            @change="selectStartDate"
          >
            <view class="picker-value">{{ getDateFromDateTime(forceCompleteForm.actualStartTime) || '请选择日期' }}</view>
          </picker>
          <picker
            mode="time"
            :value="getTimeFromDateTime(forceCompleteForm.actualStartTime)"
            @change="selectStartTime"
          >
            <view class="picker-value">{{ getTimeFromDateTime(forceCompleteForm.actualStartTime) || '请选择时间' }}</view>
          </picker>
        </view>
        <view class="time-picker-item">
          <view class="time-label">转运结束时间</view>
          <picker
            mode="date"
            :value="getDateFromDateTime(forceCompleteForm.actualEndTime)"
            @change="selectEndDate"
          >
            <view class="picker-value">{{ getDateFromDateTime(forceCompleteForm.actualEndTime) || '请选择日期' }}</view>
          </picker>
          <picker
            mode="time"
            :value="getTimeFromDateTime(forceCompleteForm.actualEndTime)"
            @change="selectEndTime"
          >
            <view class="picker-value">{{ getTimeFromDateTime(forceCompleteForm.actualEndTime) || '请选择时间' }}</view>
          </picker>
        </view>
        <view class="dialog-buttons">
          <button class="cancel-btn" @click="closeForceCompleteDialog">取消</button>
          <button class="confirm-btn" @click="confirmForceComplete">确定</button>
        </view>
      </view>
    </uni-popup>
    <!-- 取消原因选择对话框 -->
    <uni-popup ref="cancelPopup" type="center" :is-mask-click="false">
      <view class="cancel-dialog">
@@ -395,22 +454,9 @@
    
    <!-- 操作按钮区域 -->
    <view class="action-buttons" v-if="taskDetail">
      <!-- 待处理状态: 显示编辑、出发、取消 -->
      <!-- 待处理状态: 显示出发、取消、强制完成 -->
      <template v-if="taskDetail.taskStatus === 'PENDING'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          修改
        </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()"
@@ -423,17 +469,17 @@
          >
            取消
          </button>
          <button
            class="action-btn force-complete"
            @click="showForceCompleteTimeDialog()"
          >
            强制完成
          </button>
        </template>
      </template>
      
      <!-- 出发中状态: 显示编辑、已到达、强制结束 -->
      <!-- 出发中状态: 显示已到达、强制结束 -->
      <template v-else-if="taskDetail.taskStatus === 'DEPARTING'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          修改
        </button>
        <template v-if="isCurrentUserAssignee()">
          <button 
            class="action-btn primary" 
@@ -450,14 +496,8 @@
        </template>
      </template>
      
      <!-- 已到达状态: 显示编辑、已返程 -->
      <!-- 已到达状态: 显示已返程 -->
      <template v-else-if="taskDetail.taskStatus === 'ARRIVED'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          修改
        </button>
        <template v-if="isCurrentUserAssignee()">
          <button 
            class="action-btn primary" 
@@ -468,14 +508,8 @@
        </template>
      </template>
      
      <!-- 返程中状态: 显示编辑、已完成 -->
      <!-- 返程中状态: 显示已完成 -->
      <template v-else-if="taskDetail.taskStatus === 'RETURNING'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          修改
        </button>
        <template v-if="isCurrentUserAssignee()">
          <button 
            class="action-btn primary" 
@@ -521,7 +555,12 @@
        paymentInfo: null, // 支付信息
        cancelReasonList: [], // 取消原因列表
        showCancelDialog: false, // 显示取消原因对话框
        selectedCancelReason: '' // 选中的取消原因
        selectedCancelReason: '', // 选中的取消原因
        showForceCompleteDialog: false, // 显示强制完成对话框
        forceCompleteForm: {
          actualStartTime: '',
          actualEndTime: ''
        }
      }
    },
    computed: {
@@ -1393,17 +1432,52 @@
        return assignee && (assignee.userId === userId || assignee.oaUserId === userId)
      },
      // 执行人点击“就绪”
      markAssigneeReady(assignee) {
        if (!assignee || !this.taskDetail) {
          this.$modal.showToast('执行人或任务信息不存在')
      // 处理就绪按钮点击(通过data属性获取执行人信息)
      handleReadyClick(e) {
        const dataset = e.currentTarget.dataset
        const index = dataset.index
        console.log('handleReadyClick - dataset:', dataset)
        console.log('handleReadyClick - index:', index)
        if (index === undefined || index === null) {
          this.$modal.showToast('无法获取执行人索引')
          return
        }
        const assignee = this.taskDetail.assignees[index]
        console.log('handleReadyClick - assignee:', assignee)
        if (!assignee) {
          this.$modal.showToast('执行人信息不存在')
          return
        }
        this.markAssigneeReady(assignee)
      },
      // 执行人点击"就绪"
      markAssigneeReady(assignee) {
        console.log('markAssigneeReady 被调用,参数:', assignee)
        console.log('taskDetail:', this.taskDetail)
        if (!this.taskDetail) {
          this.$modal.showToast('任务信息不存在')
          return
        }
        if (!assignee) {
          this.$modal.showToast('执行人信息不存在')
          return
        }
        const userId = assignee.userId || assignee.oaUserId
        console.log('执行人ID:', userId)
        if (!userId) {
          this.$modal.showToast('无法识别执行人ID')
          return
        }
        this.$modal.showLoading && this.$modal.showLoading('提交中...')
        setAssigneeReady(this.taskId).then(() => {
          this.$modal.hideLoading && this.$modal.hideLoading()
@@ -1581,7 +1655,10 @@
      
      // 选择取消原因
      selectCancelReason(e) {
        this.selectedCancelReason = e.detail.value
        const index = parseInt(e.detail.value)
        if (this.cancelReasonList && this.cancelReasonList[index]) {
          this.selectedCancelReason = this.cancelReasonList[index].value
        }
      },
      
      // 带取消原因的状态更新
@@ -1596,6 +1673,167 @@
        }
        const reason = this.cancelReasonList.find(r => r.value === value)
        return reason ? reason.label : value
      },
      // 显示强制完成时间对话框
      showForceCompleteTimeDialog() {
        // 校验任务是否满足强制完成条件
        const validation = this.validateForceComplete()
        if (!validation.valid) {
          this.$modal.showToast(validation.message)
          return
        }
        // 重置表单
        const now = new Date()
        const nowDateStr = this.formatDateForPicker(now)
        const nowTimeStr = this.formatTimeForPicker(now)
        // 开始时间:优先使用预约时间,如果没有则使用当前时间
        let startTimeStr = nowDateStr + ' ' + nowTimeStr
        if (this.taskDetail && this.taskDetail.plannedStartTime) {
          const plannedTime = new Date(this.taskDetail.plannedStartTime)
          const year = plannedTime.getFullYear()
          // 检查是否是有效时间(排除1900、1970等无效年份)
          if (year > 2000) {
            const plannedDateStr = this.formatDateForPicker(plannedTime)
            const plannedTimeStr = this.formatTimeForPicker(plannedTime)
            startTimeStr = plannedDateStr + ' ' + plannedTimeStr
          }
        }
        this.forceCompleteForm = {
          actualStartTime: startTimeStr,
          actualEndTime: nowDateStr + ' ' + nowTimeStr
        }
        this.$refs.forceCompletePopup.open()
      },
      // 校验是否可以强制完成
      validateForceComplete() {
        if (!this.taskDetail) {
          return { valid: false, message: '任务信息不存在' }
        }
        // 检查是否有执行人
        if (!this.taskDetail.assignees || this.taskDetail.assignees.length === 0) {
          return { valid: false, message: '请先分配执行人' }
        }
        // 检查是否有车辆
        if (!this.taskDetail.assignedVehicles || this.taskDetail.assignedVehicles.length === 0) {
          return { valid: false, message: '请先分配执行车辆' }
        }
        return { valid: true }
      },
      // 关闭强制完成对话框
      closeForceCompleteDialog() {
        this.$refs.forceCompletePopup.close()
      },
      // 确认强制完成
      confirmForceComplete() {
        // 校验时间
        if (!this.forceCompleteForm.actualStartTime || !this.forceCompleteForm.actualEndTime) {
          this.$modal.showToast('请选择开始和结束时间')
          return
        }
        const startTime = new Date(this.forceCompleteForm.actualStartTime)
        const endTime = new Date(this.forceCompleteForm.actualEndTime)
        if (startTime >= endTime) {
          this.$modal.showToast('结束时间必须大于开始时间')
          return
        }
        this.$refs.forceCompletePopup.close()
        // 调用API更新任务
        this.forceCompleteTask()
      },
      // 强制完成任务
      forceCompleteTask() {
        uni.showLoading({
          title: '处理中...'
        })
        const statusData = {
          taskStatus: 'COMPLETED',
          actualStartTime: this.forceCompleteForm.actualStartTime,
          actualEndTime: this.forceCompleteForm.actualEndTime,
          remark: '强制完成任务'
        }
        changeTaskStatus(this.taskId, statusData).then(response => {
          uni.hideLoading()
          this.$modal.showToast('任务已完成')
          // 重新加载任务详情
          this.loadTaskDetail()
        }).catch(error => {
          uni.hideLoading()
          console.error('强制完成任务失败:', error)
          this.$modal.showToast('操作失败,请重试')
        })
      },
      // 选择开始日期
      selectStartDate(e) {
        const date = e.detail.value
        const time = this.getTimeFromDateTime(this.forceCompleteForm.actualStartTime) || '00:00'
        this.forceCompleteForm.actualStartTime = date + ' ' + time
      },
      // 选择开始时间
      selectStartTime(e) {
        const time = e.detail.value
        const date = this.getDateFromDateTime(this.forceCompleteForm.actualStartTime) || this.formatDateForPicker(new Date())
        this.forceCompleteForm.actualStartTime = date + ' ' + time
      },
      // 选择结束日期
      selectEndDate(e) {
        const date = e.detail.value
        const time = this.getTimeFromDateTime(this.forceCompleteForm.actualEndTime) || '00:00'
        this.forceCompleteForm.actualEndTime = date + ' ' + time
      },
      // 选择结束时间
      selectEndTime(e) {
        const time = e.detail.value
        const date = this.getDateFromDateTime(this.forceCompleteForm.actualEndTime) || this.formatDateForPicker(new Date())
        this.forceCompleteForm.actualEndTime = date + ' ' + time
      },
      // 从日期时间字符串中提取日期
      getDateFromDateTime(dateTimeStr) {
        if (!dateTimeStr) return ''
        return dateTimeStr.split(' ')[0] || ''
      },
      // 从日期时间字符串中提取时间
      getTimeFromDateTime(dateTimeStr) {
        if (!dateTimeStr) return ''
        return dateTimeStr.split(' ')[1] || ''
      },
      // 格式化日期为 picker 需要的格式 (YYYY-MM-DD)
      formatDateForPicker(date) {
        const year = date.getFullYear()
        const month = String(date.getMonth() + 1).padStart(2, '0')
        const day = String(date.getDate()).padStart(2, '0')
        return `${year}-${month}-${day}`
      },
      // 格式化时间为 picker 需要的格式 (HH:mm)
      formatTimeForPicker(date) {
        const hour = String(date.getHours()).padStart(2, '0')
        const minute = String(date.getMinutes()).padStart(2, '0')
        return `${hour}:${minute}`
      },
      
    }
@@ -1626,9 +1864,19 @@
      }
      
      .title {
        flex: 1;
        font-size: 36rpx;
        font-weight: bold;
        color: #333;
      }
      .edit-btn {
        width: 60rpx;
        height: 60rpx;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
      }
    }
    
@@ -1726,6 +1974,9 @@
            }
            
            .assignee-role {
              display: flex;
              align-items: center;
              .role-tag {
                display: inline-block;
                padding: 4rpx 12rpx;
@@ -1746,15 +1997,6 @@
                }
              }
              
              .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;
@@ -1771,6 +2013,17 @@
                }
              }
            }
          }
          .assignee-ready-btn {
            margin-left: 12rpx;
            padding: 8rpx 16rpx;
            font-size: 24rpx;
            border-radius: 6rpx;
            background-color: #34C759;
            color: #fff;
            border: none;
            flex-shrink: 0;
          }
        }
      }
@@ -1964,6 +2217,11 @@
          color: white;
        }
        
        &.force-complete {
          background-color: #5856d6;
          color: white;
        }
        &:first-child {
          margin-left: 0;
        }
@@ -2057,5 +2315,74 @@
        }
      }
    }
    // 强制完成对话框样式
    .force-complete-dialog {
      width: 600rpx;
      background-color: white;
      border-radius: 20rpx;
      padding: 40rpx;
      .dialog-title {
        font-size: 32rpx;
        font-weight: bold;
        text-align: center;
        margin-bottom: 30rpx;
        color: #333;
      }
      .time-picker-item {
        margin-bottom: 30rpx;
        .time-label {
          font-size: 28rpx;
          color: #333;
          margin-bottom: 15rpx;
          font-weight: 500;
        }
        picker {
          display: inline-block;
          width: 48%;
          &:first-of-type {
            margin-right: 4%;
          }
          .picker-value {
            padding: 20rpx;
            background-color: #f5f5f5;
            border-radius: 10rpx;
            font-size: 26rpx;
            color: #333;
            text-align: center;
          }
        }
      }
      .dialog-buttons {
        display: flex;
        gap: 20rpx;
        margin-top: 40rpx;
        button {
          flex: 1;
          height: 80rpx;
          border-radius: 10rpx;
          font-size: 30rpx;
          border: none;
          &.cancel-btn {
            background-color: #f0f0f0;
            color: #666;
          }
          &.confirm-btn {
            background-color: #5856d6;
            color: white;
          }
        }
      }
    }
  }
</style>