| | |
| | | <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> |
| | | <text class="edit-text">修改</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <scroll-view class="detail-content" scroll-y="true" v-if="taskDetail"> |
| | |
| | | <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.emergencyInfo && taskDetail.emergencyInfo.serviceOrdVip === '1'" class="vip-tag">VIP</text> |
| | | <text v-if="taskDetail.emergencyInfo && taskDetail.emergencyInfo.fromHq2Is === '1'" class="hq-tag">广总</text> |
| | | </view> |
| | | </view> |
| | | <view class="info-item"> |
| | | <view class="label">任务类型</view> |
| | |
| | | <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 |
| | | v-if="showAssigneeReadyFeature() && isAssigneeSelf(assignee) && !isAssigneeReady(assignee) && taskDetail.taskStatus === 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> |
| | | <view v-else class="empty-assignee"> |
| | | <uni-icons type="info" size="40" color="#ccc"></uni-icons> |
| | | <text>暂无执行人员</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="detail-section"> |
| | | <view class="section-title">时间信息</view> |
| | | <view class="info-item"> |
| | | <view class="label">计划开始时间</view> |
| | | <view class="label">预约时间</view> |
| | | <view class="value">{{ displayPlannedStartTime }}</view> |
| | | </view> |
| | | <view class="info-item"> |
| | | <view class="info-item" v-if="taskDetail.plannedEndTime"> |
| | | <view class="label">计划结束时间</view> |
| | | <view class="value">{{ displayPlannedEndTime }}</view> |
| | | </view> |
| | |
| | | <view class="section-title">位置信息</view> |
| | | <!-- 转运任务:显示转出/转入医院地址 --> |
| | | <template v-if="taskDetail.taskType === 'EMERGENCY_TRANSFER' && taskDetail.emergencyInfo"> |
| | | <view class="info-item" v-if="taskDetail.emergencyInfo.hospitalOutAddress"> |
| | | <view class="info-item" v-if="taskDetail.emergencyInfo.hospitalOutName"> |
| | | <view class="label">转出医院</view> |
| | | <view class="value">{{ taskDetail.emergencyInfo.hospitalOutAddress }}</view> |
| | | <view class="value">{{ taskDetail.emergencyInfo.hospitalOutName }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="taskDetail.emergencyInfo.hospitalInAddress"> |
| | | <view class="info-item" v-if="taskDetail.emergencyInfo.hospitalInName"> |
| | | <view class="label">转入医院</view> |
| | | <view class="value">{{ taskDetail.emergencyInfo.hospitalInAddress }}</view> |
| | | <view class="value">{{ taskDetail.emergencyInfo.hospitalInName }}</view> |
| | | </view> |
| | | </template> |
| | | <!-- 福祉车任务:显示接送/目的地址 --> |
| | |
| | | </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> |
| | |
| | | |
| | | <!-- 转运 - 费用信息 --> |
| | | <view class="detail-section" v-if="taskDetail.taskType === 'EMERGENCY_TRANSFER' && taskDetail.emergencyInfo"> |
| | | <view class="section-title">费用信息</view> |
| | | <view class="section-title"> |
| | | 费用信息 |
| | | <!-- 已完成且未申请发票时显示申请发票按钮 --> |
| | | <button |
| | | v-if="canApplyInvoice" |
| | | class="apply-invoice-btn" |
| | | @click="handleApplyInvoice" |
| | | > |
| | | <text class="cuIcon-form"></text> 申请发票 |
| | | </button> |
| | | </view> |
| | | <view class="info-item" v-if="taskDetail.emergencyInfo.transferDistance"> |
| | | <view class="label">转运公里数</view> |
| | | <view class="value">{{ taskDetail.emergencyInfo.transferDistance }}公里</view> |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 取消信息(仅在任务已取消且有取消原因时显示) --> |
| | | <view class="detail-section" v-if="taskDetail.taskStatus === TaskStatus.CANCELLED && taskDetail.emergencyInfo && taskDetail.emergencyInfo.cancelReason"> |
| | | <view class="section-title">取消信息</view> |
| | | <view class="info-item"> |
| | | <view class="label">取消原因</view> |
| | | <view class="value">{{ getCancelReasonLabel(taskDetail.emergencyInfo.cancelReason) }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="taskDetail.emergencyInfo.cancelBy"> |
| | | <view class="label">取消人</view> |
| | | <view class="value">{{ taskDetail.emergencyInfo.cancelBy }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="taskDetail.emergencyInfo.cancelTime"> |
| | | <view class="label">取消时间</view> |
| | | <view class="value">{{ formatTime(taskDetail.emergencyInfo.cancelTime) }}</view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 支付记录明细 --> |
| | | <view class="detail-section" v-if="paymentInfo && paymentInfo.paidPayments && paymentInfo.paidPayments.length > 0"> |
| | | <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 |
| | |
| | | <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"> |
| | | <view class="dialog-title">请选择取消原因</view> |
| | | <picker mode="selector" :range="cancelReasonList" range-key="label" @change="selectCancelReason"> |
| | | <view class="reason-picker"> |
| | | <view class="picker-label">取消原因</view> |
| | | <view class="picker-value"> |
| | | {{ selectedCancelReasonLabel }} |
| | | </view> |
| | | <uni-icons type="arrowright" size="16"></uni-icons> |
| | | </view> |
| | | </picker> |
| | | <view class="dialog-buttons"> |
| | | <button class="cancel-btn" @click="closeCancelDialog">取消</button> |
| | | <button class="confirm-btn" @click="confirmCancelTask">确定</button> |
| | | </view> |
| | | </view> |
| | | </uni-popup> |
| | | |
| | | <!-- 操作按钮区域 --> |
| | | <view class="action-buttons" v-if="taskDetail"> |
| | | <!-- 待处理状态: 显示编辑、出发、取消 --> |
| | | <template v-if="taskDetail.taskStatus === 'PENDING'"> |
| | | <!-- 待处理状态:显示出发、取消、强制完成 --> |
| | | <template v-if="taskDetail.taskStatus === TaskStatus.PENDING |
| | | || taskDetail.taskStatus === TaskStatus.NOT_DEPARTED |
| | | || taskDetail.taskStatus === TaskStatus.NOT_CONFIRMED |
| | | || taskDetail.taskStatus === TaskStatus.PARTIALLY_CONFIRMED"> |
| | | <button |
| | | class="action-btn edit" |
| | | @click="handleEdit" |
| | | > |
| | | 修改 |
| | | </button> |
| | | <button |
| | | v-if="canOperateTask()" |
| | | class="action-btn primary" |
| | | @click="handleTaskAction('depart')" |
| | | @click="handleDepartAction()" |
| | | > |
| | | 出发 |
| | | </button> |
| | |
| | | > |
| | | 取消 |
| | | </button> |
| | | </template> |
| | | |
| | | <!-- 出发中状态: 显示编辑、已到达、强制结束 --> |
| | | <template v-else-if="taskDetail.taskStatus === 'DEPARTING'"> |
| | | <button |
| | | class="action-btn edit" |
| | | @click="handleEdit" |
| | | v-if="canOperateTask() && showForceCompleteFeature()" |
| | | class="action-btn force-complete" |
| | | @click="showForceCompleteTimeDialog()" |
| | | > |
| | | 修改 |
| | | </button> |
| | | <button |
| | | class="action-btn primary" |
| | | @click="handleTaskAction('arrive')" |
| | | > |
| | | 已到达 |
| | | </button> |
| | | <button |
| | | class="action-btn cancel" |
| | | @click="handleTaskAction('forceCancel')" |
| | | > |
| | | 强制结束 |
| | | 强制完成 |
| | | </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')" |
| | | > |
| | | 已返程 |
| | | </button> |
| | | |
| | | <!-- 出发中状态:显示已到达、强制结束、强制完成 --> |
| | | <template v-else-if="taskDetail.taskStatus === TaskStatus.DEPARTING"> |
| | | <template v-if="canOperateTask()"> |
| | | <button |
| | | class="action-btn primary" |
| | | @click="handleTaskAction('arrive')" |
| | | > |
| | | 已到达 |
| | | </button> |
| | | <button |
| | | class="action-btn cancel" |
| | | @click="handleTaskAction('forceCancel')" |
| | | > |
| | | 强制结束 |
| | | </button> |
| | | <button |
| | | v-if="showForceCompleteFeature()" |
| | | class="action-btn force-complete" |
| | | @click="showForceCompleteTimeDialog()" |
| | | > |
| | | 强制完成 |
| | | </button> |
| | | </template> |
| | | </template> |
| | | |
| | | <!-- 返程中状态: 显示编辑、已完成 --> |
| | | <template v-else-if="taskDetail.taskStatus === 'RETURNING'"> |
| | | <button |
| | | class="action-btn edit" |
| | | @click="handleEdit" |
| | | > |
| | | 修改 |
| | | </button> |
| | | <button |
| | | class="action-btn primary" |
| | | @click="handleTaskAction('complete')" |
| | | > |
| | | 已完成 |
| | | </button> |
| | | <!-- 已到达状态:显示已返程 --> |
| | | <template v-else-if="taskDetail.taskStatus === TaskStatus.ARRIVED"> |
| | | <template v-if="canOperateTask()"> |
| | | <button |
| | | class="action-btn primary" |
| | | @click="handleTaskAction('return')" |
| | | > |
| | | 已返程 |
| | | </button> |
| | | </template> |
| | | </template> |
| | | |
| | | <!-- 返程中状态:显示已完成 --> |
| | | <template v-else-if="taskDetail.taskStatus === TaskStatus.RETURNING"> |
| | | <template v-if="canOperateTask()"> |
| | | <button |
| | | class="action-btn primary" |
| | | @click="handleTaskAction('complete')" |
| | | > |
| | | 已完成 |
| | | </button> |
| | | </template> |
| | | </template> |
| | | |
| | | <!-- 处理中状态:显示强制完成、取消 --> |
| | | <template v-else-if="taskDetail.taskStatus === TaskStatus.IN_PROGRESS"> |
| | | <template v-if="canOperateTask()"> |
| | | <button |
| | | class="action-btn primary" |
| | | @click="handleTaskAction('arrive')" |
| | | > |
| | | 已到达 |
| | | </button> |
| | | <button |
| | | v-if="showForceCompleteFeature()" |
| | | class="action-btn force-complete" |
| | | @click="showForceCompleteTimeDialog()" |
| | | > |
| | | 强制完成 |
| | | </button> |
| | | </template> |
| | | </template> |
| | | |
| | | <!-- 已完成/已取消: 不显示按钮,但如果是转运任务则显示结算按钮 --> |
| | |
| | | </template> |
| | | |
| | | <script> |
| | | import { getTask, changeTaskStatus } from '@/api/task' |
| | | import { getTask, changeTaskStatus, setAssigneeReady, checkTaskConsentAttachment } from '@/api/task' |
| | | import { checkVehicleActiveTasks } from '@/api/task' |
| | | import { getPaymentInfo } from '@/api/payment' |
| | | import { getDicts } from '@/api/dict' |
| | | import { checkTaskInvoice } from '@/api/invoice' |
| | | import { formatDateTime } from '@/utils/common' |
| | | import { validateTaskForDepart, validateTaskForSettlement, getTaskVehicleId, checkTaskCanDepart } from '@/utils/taskValidator' |
| | | import { getStatusText as getTaskStatusText, getTaskTypeText as getTaskTypeTextUtil, TaskStatus } from '@/utils/TaskUtil' |
| | | import AttachmentUpload from './components/AttachmentUpload.vue' |
| | | import config from '@/config' |
| | | |
| | | export default { |
| | | components: { |
| | |
| | | }, |
| | | data() { |
| | | return { |
| | | TaskStatus, // 暴露 TaskStatus 给模板使用 |
| | | taskDetail: null, |
| | | taskId: null, |
| | | paymentInfo: null // 支付信息 |
| | | paymentInfo: null, // 支付信息 |
| | | cancelReasonList: [], // 取消原因列表 |
| | | showCancelDialog: false, // 显示取消原因对话框 |
| | | selectedCancelReason: '', // 选中的取消原因 |
| | | showForceCompleteDialog: false, // 显示强制完成对话框 |
| | | forceCompleteForm: { |
| | | actualStartTime: '', |
| | | actualEndTime: '' |
| | | }, |
| | | hasInvoiceApplied: false, // 是否已申请发票 |
| | | invoiceStatus: null // 发票状态:0-待审核, 1-已通过, 2-已驳回 |
| | | } |
| | | }, |
| | | computed: { |
| | |
| | | return false |
| | | } |
| | | return ['COMPLETED', 'CANCELLED'].includes(this.taskDetail.taskStatus) |
| | | }, |
| | | |
| | | // 是否可以申请发票 |
| | | canApplyInvoice() { |
| | | // 仅急救转运任务 |
| | | if (this.taskDetail?.taskType !== 'EMERGENCY_TRANSFER') return false |
| | | // 任务必须已完成 |
| | | if (this.taskDetail?.taskStatus !== 'COMPLETED') return false |
| | | // 未申请过发票,或曾被驳回 |
| | | return !this.hasInvoiceApplied || this.invoiceStatus === 2 |
| | | }, |
| | | |
| | | // 生成执行人员角色标签的类名 |
| | | getRoleTagClass() { |
| | | return (userType) => { |
| | | const baseClass = 'role-tag' |
| | | const roleClasses = { |
| | | 'driver': 'role-driver', |
| | | 'doctor': 'role-doctor', |
| | | 'nurse': 'role-nurse' |
| | | } |
| | | return [baseClass, roleClasses[userType] || ''] |
| | | } |
| | | }, |
| | | |
| | | // 获取选中的取消原因标签(用于弹窗显示) |
| | | selectedCancelReasonLabel() { |
| | | if (!this.selectedCancelReason || !this.cancelReasonList.length) { |
| | | return '请选择' |
| | | } |
| | | const reason = this.cancelReasonList.find(r => r.value === this.selectedCancelReason) |
| | | return reason ? reason.label : '请选择' |
| | | }, |
| | | // 显示任务类型 |
| | | displayTaskType() { |
| | |
| | | return '' |
| | | } |
| | | const status = this.taskDetail.taskStatus |
| | | if (status === 'PENDING') return 'pending' |
| | | if (['DEPARTING', 'ARRIVED', 'RETURNING', 'IN_PROGRESS'].includes(status)) return 'in_progress' |
| | | if (status === 'COMPLETED') return 'completed' |
| | | if (status === 'CANCELLED') return 'cancelled' |
| | | if (status === TaskStatus.PENDING || status === TaskStatus.NOT_CONFIRMED || status === TaskStatus.NOT_DEPARTED || status === TaskStatus.PARTIALLY_CONFIRMED) return 'pending' |
| | | if ([TaskStatus.DEPARTING, TaskStatus.ARRIVED, TaskStatus.RETURNING, TaskStatus.IN_PROGRESS].includes(status)) return 'in_progress' |
| | | if (status === TaskStatus.COMPLETED) return 'completed' |
| | | if (status === TaskStatus.CANCELLED) return 'cancelled' |
| | | return '' |
| | | }, |
| | | // 显示计划开始时间 |
| | |
| | | if (!this.taskDetail || !this.taskDetail.plannedStartTime) { |
| | | return '未设置' |
| | | } |
| | | return formatDateTime(this.taskDetail.plannedStartTime, 'YYYY-MM-DD HH:mm') |
| | | const formatted = formatDateTime(this.taskDetail.plannedStartTime, 'YYYY-MM-DD HH:mm') |
| | | // 如果年份是1900或1970,表示无效日期,显示为未分配时间 |
| | | if (formatted && (formatted.startsWith('1900') || formatted.startsWith('1970'))) { |
| | | return '未分配时间' |
| | | } |
| | | return formatted |
| | | }, |
| | | // 显示计划结束时间 |
| | | displayPlannedEndTime() { |
| | | if (!this.taskDetail || !this.taskDetail.plannedEndTime) { |
| | | return '未设置' |
| | | } |
| | | return formatDateTime(this.taskDetail.plannedEndTime, 'YYYY-MM-DD HH:mm') |
| | | const formatted = formatDateTime(this.taskDetail.plannedEndTime, 'YYYY-MM-DD HH:mm') |
| | | // 如果年份是1900或1970,表示无效日期,显示为未分配时间 |
| | | if (formatted && (formatted.startsWith('1900') || formatted.startsWith('1970'))) { |
| | | return '未分配时间' |
| | | } |
| | | return formatted |
| | | }, |
| | | // 显示实际开始时间 |
| | | displayActualStartTime() { |
| | | if (!this.taskDetail || !this.taskDetail.actualStartTime) { |
| | | return '未设置' |
| | | } |
| | | return formatDateTime(this.taskDetail.actualStartTime, 'YYYY-MM-DD HH:mm') |
| | | const formatted = formatDateTime(this.taskDetail.actualStartTime, 'YYYY-MM-DD HH:mm') |
| | | // 如果年份是1900或1970,表示无效日期,显示为未分配时间 |
| | | if (formatted && (formatted.startsWith('1900') || formatted.startsWith('1970'))) { |
| | | return '未分配时间' |
| | | } |
| | | return formatted |
| | | }, |
| | | // 显示实际结束时间 |
| | | displayActualEndTime() { |
| | | if (!this.taskDetail || !this.taskDetail.actualEndTime) { |
| | | return '未设置' |
| | | } |
| | | return formatDateTime(this.taskDetail.actualEndTime, 'YYYY-MM-DD HH:mm') |
| | | const formatted = formatDateTime(this.taskDetail.actualEndTime, 'YYYY-MM-DD HH:mm') |
| | | // 如果年份是1900或1970,表示无效日期,显示为未分配时间 |
| | | if (formatted && (formatted.startsWith('1900') || formatted.startsWith('1970'))) { |
| | | return '未分配时间' |
| | | } |
| | | return formatted |
| | | } |
| | | }, |
| | | onLoad(options) { |
| | | this.taskId = options.id |
| | | this.loadTaskDetail() |
| | | this.loadCancelReasonDict() // 加载取消原因字典 |
| | | // 检查发票申请状态 |
| | | this.checkInvoiceStatus() |
| | | }, |
| | | onShow() { |
| | | // 每次页面显示时重新加载数据,确保从编辑页面返回后能看到最新数据 |
| | |
| | | |
| | | 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) |
| | | |
| | | |
| | | // 如果是转运任务,加载支付信息 |
| | | if (this.taskDetail.taskType === 'EMERGENCY_TRANSFER') { |
| | |
| | | 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) { |
| | |
| | | |
| | | // 返回上一页 |
| | | goBack() { |
| | | uni.navigateBack() |
| | | // 检查是否有页面可以返回 |
| | | uni.navigateBack({ |
| | | delta: 1, |
| | | fail: () => { |
| | | // 如果无法返回,则跳转到任务列表页面 |
| | | uni.switchTab({ |
| | | url: '/pages/task/index' |
| | | }) |
| | | } |
| | | }) |
| | | }, |
| | | |
| | | // 处理编辑按钮 |
| | |
| | | |
| | | // 获取状态文本 |
| | | getStatusText(status) { |
| | | const statusMap = { |
| | | 'PENDING': '待处理', |
| | | 'DEPARTING': '出发中', |
| | | 'ARRIVED': '已到达', |
| | | 'RETURNING': '返程中', |
| | | 'COMPLETED': '已完成', |
| | | 'CANCELLED': '已取消', |
| | | 'IN_PROGRESS': '处理中' // 兼容旧数据 |
| | | } |
| | | return statusMap[status] || '未知' |
| | | return getTaskStatusText(status) |
| | | }, |
| | | |
| | | // 获取任务类型文本 |
| | | getTaskTypeText(type) { |
| | | return getTaskTypeTextUtil(type) |
| | | }, |
| | | |
| | | // 获取用户类型标签 |
| | | getUserTypeLabel(userType) { |
| | | const typeMap = { |
| | | 'MAINTENANCE': '维修保养', |
| | | 'FUEL': '加油', |
| | | 'OTHER': '其他', |
| | | 'EMERGENCY_TRANSFER': '转运任务', |
| | | 'WELFARE': '福祉车' |
| | | 'driver': '司机', |
| | | 'doctor': '医生', |
| | | 'nurse': '护士' |
| | | } |
| | | return typeMap[type] || '未知类型' |
| | | 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 |
| | | }) |
| | |
| | | handleTaskAction(action) { |
| | | switch (action) { |
| | | case 'depart': |
| | | // 出发 -> 检查车辆是否有其他正在进行中的任务 |
| | | this.checkVehicleAndDepart(); |
| | | this.ensureReadyThenDepart(); |
| | | break; |
| | | |
| | | case 'cancel': |
| | | // 取消 -> 二次确认后状态变为已取消 |
| | | this.$modal.confirm('确定要取消此任务吗?').then(() => { |
| | | this.updateTaskStatus('CANCELLED', '任务已取消') |
| | | }).catch(() => {}); |
| | | // 取消 -> 显示取消原因选择对话框 |
| | | this.showCancelReasonDialog(); |
| | | break; |
| | | |
| | | case 'arrive': |
| | |
| | | break; |
| | | |
| | | case 'forceCancel': |
| | | // 强制结束 -> 状态变为已取消 |
| | | this.$modal.confirm('确定要强制结束此任务吗?').then(() => { |
| | | this.updateTaskStatus('CANCELLED', '任务已强制结束') |
| | | }).catch(() => {}); |
| | | // 强制结束 -> 显示取消原因选择对话框 |
| | | this.showCancelReasonDialog(); |
| | | break; |
| | | |
| | | case 'return': |
| | |
| | | }, |
| | | |
| | | // 检查车辆状态并出发 |
| | | 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 |
| | |
| | | return null; |
| | | }, |
| | | |
| | | // 检查发票申请状态 |
| | | checkInvoiceStatus() { |
| | | if (!this.taskId) return; |
| | | |
| | | // 调用后端接口检查该任务是否已申请发票 |
| | | checkTaskInvoice(this.taskId).then(response => { |
| | | if (response.code === 200 && response.data) { |
| | | this.hasInvoiceApplied = true; |
| | | this.invoiceStatus = response.data.status; |
| | | } |
| | | }).catch(error => { |
| | | console.error('检查发票申请状态失败:', error); |
| | | // 忽略错误,默认未申请 |
| | | }); |
| | | }, |
| | | |
| | | // 申请发票 |
| | | handleApplyInvoice() { |
| | | // 准备任务信息 |
| | | const taskInfo = { |
| | | taskId: this.taskDetail.taskId, |
| | | taskCode: this.taskDetail.showTaskCode || this.taskDetail.taskCode, |
| | | legacyServiceOrderId: this.taskDetail.emergencyInfo?.legacyServiceOrdId, |
| | | serviceCode: this.taskDetail.emergencyInfo?.serviceCode, |
| | | departure: this.taskDetail.departureAddress, |
| | | destination: this.taskDetail.destinationAddress, |
| | | completionTime: this.formatTime(this.taskDetail.actualEndTime), |
| | | transferPrice: this.paymentInfo?.transferPrice || this.paymentInfo?.totalAmount |
| | | }; |
| | | |
| | | // 将任务信息序列化为 URL 参数 |
| | | const taskInfoParam = encodeURIComponent(JSON.stringify(taskInfo)); |
| | | |
| | | // 跳转到发票申请页面,传递任务信息 |
| | | uni.navigateTo({ |
| | | url: `/pages/mine/invoice/apply?taskInfo=${taskInfoParam}` |
| | | }); |
| | | }, |
| | | |
| | | // 更新任务状态 |
| | | updateTaskStatus(status, remark) { |
| | | // 获取GPS位置信息 |
| | | this.getLocationAndUpdateStatus(status, remark) |
| | | // 如果是完成状态,需要检查是否上传了知情同意书 |
| | | if (status === 'COMPLETED') { |
| | | this.checkConsentAttachmentAndThen(status, remark); |
| | | } else { |
| | | // 获取GPS位置信息 |
| | | this.getLocationAndUpdateStatus(status, remark); |
| | | } |
| | | }, |
| | | |
| | | // 检查知情同意书附件并更新状态 |
| | | async checkConsentAttachmentAndThen(status, remark) { |
| | | try { |
| | | uni.showLoading({ |
| | | title: '检查附件...' |
| | | }); |
| | | |
| | | // 注意:这里会被请求拦截器处理,code !== 200 时会 reject |
| | | const response = await checkTaskConsentAttachment(this.taskId).catch(err => { |
| | | // 拦截器 reject 的情况,返回一个默认对象 |
| | | console.log('请求被拦截器 reject,err:', err); |
| | | return { code: -1, msg: '未上传知情同意书' }; |
| | | }); |
| | | |
| | | uni.hideLoading(); |
| | | console.log('检查附件结果:', response); |
| | | |
| | | // 后台返回 code: 200 表示已上传,code: -1 表示未上传 |
| | | if (response && response.code === 200) { |
| | | // 已上传知情同意书,继续更新状态 |
| | | console.log('已上传知情同意书,继续完成任务'); |
| | | this.getLocationAndUpdateStatus(status, remark); |
| | | } else { |
| | | // 未上传知情同意书或其他错误,阻止完成 |
| | | const message = (response && response.msg) || '任务未上传知情同意书,无法完成任务'; |
| | | console.log('未上传知情同意书,阻止完成'); |
| | | |
| | | this.$modal.confirm(message + '。是否现在去上传?').then(() => { |
| | | // 滚动到附件上传区域 |
| | | this.$nextTick(() => { |
| | | uni.pageScrollTo({ |
| | | scrollTop: 9999, // 滚动到底部 |
| | | duration: 300 |
| | | }); |
| | | }); |
| | | }).catch(() => {}); |
| | | } |
| | | } catch (error) { |
| | | uni.hideLoading(); |
| | | console.error('检查附件异常:', error); |
| | | |
| | | // 如果检查失败(网络异常等),不允许完成任务 |
| | | this.$modal.showToast('检查附件状态失败,无法完成任务'); |
| | | } |
| | | }, |
| | | |
| | | // 获取位置信息并更新状态 |
| | | getLocationAndUpdateStatus(status, remark) { |
| | | getLocationAndUpdateStatus(status, remark, cancelReason) { |
| | | const that = this |
| | | |
| | | // 使用uni.getLocation获取GPS位置 |
| | |
| | | heading: res.direction || res.heading |
| | | } |
| | | |
| | | // 如果有取消原因,添加到请求数据中 |
| | | if (cancelReason) { |
| | | statusData.cancelReason = cancelReason |
| | | } |
| | | |
| | | changeTaskStatus(that.taskId, statusData).then(response => { |
| | | that.$modal.showToast('状态更新成功') |
| | | // 重新加载任务详情 |
| | |
| | | const statusData = { |
| | | taskStatus: status, |
| | | remark: remark |
| | | } |
| | | |
| | | // 如果有取消原因,添加到请求数据中 |
| | | if (cancelReason) { |
| | | statusData.cancelReason = cancelReason |
| | | } |
| | | |
| | | changeTaskStatus(that.taskId, statusData).then(response => { |
| | |
| | | // 附件删除成功回调 |
| | | onAttachmentDeleted(attachmentId) { |
| | | console.log('附件删除成功:', attachmentId) |
| | | } |
| | | }, |
| | | |
| | | // 是否显示"就绪"功能(配置开关) |
| | | showAssigneeReadyFeature() { |
| | | return !!(config && config.features && config.features.showAssigneeReadyButton) |
| | | }, |
| | | |
| | | // 是否显示"强制完成"功能(配置开关) |
| | | showForceCompleteFeature() { |
| | | return !!(config && config.features && config.features.showForceCompleteButton) |
| | | }, |
| | | |
| | | // 当前用户是否为该执行人 |
| | | 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) |
| | | }, |
| | | |
| | | // 处理就绪按钮点击(通过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() |
| | | 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)) |
| | | }, |
| | | |
| | | // 是否当前用户可以操作任务(执行人或管理员) |
| | | canOperateTask() { |
| | | // 检查是否是管理员(canViewAllConsult === '1') |
| | | |
| | | const canViewAllConsult = this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.canViewAllConsult |
| | | console.log("当前用户是否是管理员:", canViewAllConsult) |
| | | if (canViewAllConsult === '1') { |
| | | return true |
| | | } |
| | | // 检查是否是任务执行人 |
| | | return this.isCurrentUserAssignee() |
| | | }, |
| | | |
| | | // 是否多人执行 |
| | | 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); |
| | | }, |
| | | |
| | | // 加载取消原因字典 |
| | | loadCancelReasonDict() { |
| | | // 从后端获取取消原因字典 |
| | | getDicts('task_cancel_reason').then(response => { |
| | | if (response.code === 200 && response.data) { |
| | | this.cancelReasonList = response.data.map(item => ({ |
| | | value: item.dictValue, |
| | | label: item.dictLabel |
| | | })) |
| | | } |
| | | }).catch(error => { |
| | | console.error('加载取消原因字典失败:', error) |
| | | }) |
| | | }, |
| | | |
| | | // 显示取消原因对话框 |
| | | showCancelReasonDialog() { |
| | | this.selectedCancelReason = '' |
| | | this.$refs.cancelPopup.open() |
| | | }, |
| | | |
| | | // 确认取消任务 |
| | | confirmCancelTask() { |
| | | if (!this.selectedCancelReason) { |
| | | this.$modal.showToast('请选择取消原因') |
| | | return |
| | | } |
| | | |
| | | this.$refs.cancelPopup.close() |
| | | |
| | | // 调用更新状态方法,传递取消原因 |
| | | this.updateTaskStatusWithCancelReason('CANCELLED', '任务已取消', this.selectedCancelReason) |
| | | }, |
| | | |
| | | // 取消对话框关闭 |
| | | closeCancelDialog() { |
| | | this.$refs.cancelPopup.close() |
| | | this.selectedCancelReason = '' |
| | | }, |
| | | |
| | | // 选择取消原因 |
| | | selectCancelReason(e) { |
| | | const index = parseInt(e.detail.value) |
| | | if (this.cancelReasonList && this.cancelReasonList[index]) { |
| | | this.selectedCancelReason = this.cancelReasonList[index].value |
| | | } |
| | | }, |
| | | |
| | | // 带取消原因的状态更新 |
| | | updateTaskStatusWithCancelReason(status, remark, cancelReason) { |
| | | this.getLocationAndUpdateStatus(status, remark, cancelReason) |
| | | }, |
| | | |
| | | // 根据取消原因value获取label |
| | | getCancelReasonLabel(value) { |
| | | if (!value || !this.cancelReasonList.length) { |
| | | return value || '未知' |
| | | } |
| | | 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}` |
| | | }, |
| | | |
| | | } |
| | | } |
| | | </script> |
| | |
| | | } |
| | | |
| | | .title { |
| | | flex: 1; |
| | | font-size: 36rpx; |
| | | font-weight: bold; |
| | | color: #333; |
| | | } |
| | | |
| | | .edit-btn { |
| | | width: 120rpx; |
| | | height: 60rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: pointer; |
| | | |
| | | .edit-text { |
| | | margin-left: 8rpx; |
| | | font-size: 28rpx; |
| | | color: #007AFF; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | color: white; |
| | | border-radius: 8rpx; |
| | | border: none; |
| | | } |
| | | } |
| | | |
| | | // 执行人员列表样式 |
| | | .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 { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | .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; |
| | | } |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | } |
| | | |
| | |
| | | flex: 1; |
| | | height: 80rpx; |
| | | border-radius: 10rpx; |
| | | font-size: 30rpx; |
| | | font-size: 28rpx; |
| | | margin: 0 10rpx; |
| | | background-color: #f0f0f0; |
| | | color: #333; |
| | | white-space: nowrap; |
| | | padding: 0 10rpx; |
| | | min-width: 0; |
| | | |
| | | &.edit { |
| | | background-color: #ff9500; |
| | |
| | | color: white; |
| | | } |
| | | |
| | | &.force-end { |
| | | background-color: #ff6b22; |
| | | color: white; |
| | | } |
| | | |
| | | &.settlement { |
| | | background-color: #34C759; |
| | | color: white; |
| | | } |
| | | |
| | | &.force-complete { |
| | | background-color: #5856d6; |
| | | color: white; |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | .vip-tag { |
| | | display: inline-block; |
| | | padding: 2rpx 8rpx; |
| | | font-size: 20rpx; |
| | | color: #fff; |
| | | background-color: #ff0000; |
| | | border-radius: 4rpx; |
| | | margin-left: 10rpx; |
| | | vertical-align: middle; |
| | | } |
| | | |
| | | .hq-tag { |
| | | display: inline-block; |
| | | padding: 2rpx 8rpx; |
| | | font-size: 20rpx; |
| | | color: #fff; |
| | | background-color: #5856d6; |
| | | border-radius: 4rpx; |
| | | margin-left: 10rpx; |
| | | vertical-align: middle; |
| | | } |
| | | |
| | | .apply-invoice-btn { |
| | | padding: 8rpx 16rpx; |
| | | font-size: 24rpx; |
| | | color: #fff; |
| | | background-color: #34C759; |
| | | border: none; |
| | | border-radius: 6rpx; |
| | | margin-left: 20rpx; |
| | | } |
| | | |
| | | .apply-invoice-btn::after { |
| | | border: none; |
| | | } |
| | | |
| | | // 取消原因对话框样式 |
| | | .cancel-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; |
| | | } |
| | | |
| | | .reason-picker { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 20rpx 30rpx; |
| | | background-color: #f5f5f5; |
| | | border-radius: 10rpx; |
| | | margin-bottom: 30rpx; |
| | | |
| | | .picker-label { |
| | | font-size: 28rpx; |
| | | color: #666; |
| | | } |
| | | |
| | | .picker-value { |
| | | flex: 1; |
| | | text-align: right; |
| | | font-size: 28rpx; |
| | | color: #333; |
| | | margin: 0 10rpx; |
| | | } |
| | | } |
| | | |
| | | .dialog-buttons { |
| | | display: flex; |
| | | gap: 20rpx; |
| | | |
| | | button { |
| | | flex: 1; |
| | | height: 80rpx; |
| | | border-radius: 10rpx; |
| | | font-size: 30rpx; |
| | | border: none; |
| | | |
| | | &.cancel-btn { |
| | | background-color: #f0f0f0; |
| | | color: #666; |
| | | } |
| | | |
| | | &.confirm-btn { |
| | | background-color: #007AFF; |
| | | color: white; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 强制完成对话框样式 |
| | | .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> |