|
|
@@ -1,177 +1,347 @@
|
|
|
<template>
|
|
|
- <view class="app-container pb-24">
|
|
|
- <TopBar title="任务详情" showBack />
|
|
|
-
|
|
|
- <view v-if="task" class="px-4 py-4 gap-y-4">
|
|
|
- <!-- 施工流程进度 -->
|
|
|
- <ConstructionFlowBar :status="task.status" :timeline="task.timeline" />
|
|
|
-
|
|
|
- <!-- 操作按钮区 -->
|
|
|
- <view class="card p-4">
|
|
|
- <!-- 待确认 -->
|
|
|
- <view v-if="task.status === 'pending'" class="gap-y-3">
|
|
|
- <view class="bg-blue-50 rounded-xl p-4 mb-4 text-center">
|
|
|
- <text class="uni-icons uniui-compose text-4xl block mb-2 text-blue-500"></text>
|
|
|
- <text class="text-lg font-semibold text-gray-800 block">待确认任务</text>
|
|
|
- <text class="text-gray-500 text-sm mt-1 block">请查看任务详情,确认是否接受</text>
|
|
|
- </view>
|
|
|
- <button class="w-full py-3 btn-success rounded-xl font-medium flex items-center justify-center gap-2" @click="onConfirm">
|
|
|
- <text>确认任务</text>
|
|
|
- </button>
|
|
|
- <button class="w-full py-3 bg-orange-50 border border-orange-200 text-orange-600 rounded-xl font-medium flex items-center justify-center gap-2" @click="onFeedback">
|
|
|
- <text>反馈问题</text>
|
|
|
- </button>
|
|
|
- <button class="w-full py-3 bg-red-50 border border-red-200 text-red-500 rounded-xl font-medium flex items-center justify-center gap-2" @click="onReject">
|
|
|
- <text>拒绝任务</text>
|
|
|
- </button>
|
|
|
+ <view class="sub-page">
|
|
|
+ <TopBar title="任务详情" show-back />
|
|
|
+
|
|
|
+ <view v-if="loading" class="py-10 text-center">
|
|
|
+ <text class="empty-text">加载中...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-else-if="task" class="px-4 pt-3">
|
|
|
+ <!-- 标题卡片 -->
|
|
|
+ <view class="glass-card p-4">
|
|
|
+ <view class="flex items-center justify-between mb-2">
|
|
|
+ <text class="text-base font-semibold text-gray-800 flex-1 mr-2">{{ taskName }}</text>
|
|
|
+ <StatusTag :status="task.status" />
|
|
|
</view>
|
|
|
+ <view class="flex items-center">
|
|
|
+ <text class="text-xs text-gray-400">任务编号:{{ task.taskNo || '-' }}</text>
|
|
|
+ <text v-if="urgencyText" class="text-xs ml-3" :class="task.urgencyLevel === 'very_urgent' ? 'text-red-500' : 'text-gray-400'">{{ urgencyText }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
|
|
|
- <!-- 待出车 -->
|
|
|
- <view v-else-if="task.status === 'scheduled'" class="gap-y-3">
|
|
|
- <view class="bg-orange-50 rounded-xl p-4 mb-4 text-center">
|
|
|
- <text class="uni-icons uniui-cart-filled text-4xl block mb-2 text-orange-500"></text>
|
|
|
- <text class="text-lg font-semibold text-gray-800 block">准备出发</text>
|
|
|
- <text class="text-gray-500 text-sm mt-1 block">请确认设备准备完毕后出发</text>
|
|
|
- </view>
|
|
|
- <view class="bg-white rounded-lg p-3 border border-gray-100 mb-3">
|
|
|
- <view class="flex justify-between text-sm">
|
|
|
- <text class="text-gray-400">目的地</text>
|
|
|
- <text class="text-gray-800">{{ task.projectName }}</text>
|
|
|
- </view>
|
|
|
- <view v-if="task.vehicle" class="flex justify-between text-sm mt-2">
|
|
|
- <text class="text-gray-400">车辆</text>
|
|
|
- <text class="text-gray-800">{{ task.vehicle }}</text>
|
|
|
+ <!-- SOP 8步流程条 -->
|
|
|
+ <view class="glass-card p-4">
|
|
|
+ <view class="flow-row">
|
|
|
+ <view v-for="(label, i) in stepLabels" :key="i" class="flow-step">
|
|
|
+ <view class="flow-node-wrap">
|
|
|
+ <view v-if="i !== 0" class="flow-line" :class="i <= currentStep ? 'flow-line-active' : ''" />
|
|
|
+ <view class="flow-node" :class="nodeClass(i + 1)">
|
|
|
+ <text v-if="i + 1 <= currentStep" class="uni-icons uniui-checkmarkempty flow-check"></text>
|
|
|
+ <text v-else class="flow-node-text">{{ i + 1 }}</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
+ <text class="flow-label" :class="i + 1 <= currentStep ? 'flow-label-active' : ''">{{ label }}</text>
|
|
|
</view>
|
|
|
- <button class="w-full py-3 btn-primary rounded-xl font-semibold flex items-center justify-center gap-2" @click="onDepart">
|
|
|
- <text>出发</text>
|
|
|
- </button>
|
|
|
</view>
|
|
|
+ </view>
|
|
|
|
|
|
- <!-- 已出车 -->
|
|
|
- <view v-else-if="task.status === 'departed'" class="gap-y-3">
|
|
|
- <view class="bg-purple-50 rounded-xl p-4 mb-4 text-center">
|
|
|
- <text class="uni-icons uniui-location-filled text-4xl block mb-2 text-purple-500"></text>
|
|
|
- <text class="text-lg font-semibold text-gray-800 block">前往现场</text>
|
|
|
- <text class="text-gray-500 text-sm mt-1 block">正在前往施工地点</text>
|
|
|
- </view>
|
|
|
- <button class="w-full py-3 btn-success rounded-xl font-semibold flex items-center justify-center gap-2" @click="onArrive">
|
|
|
- <text>到达现场</text>
|
|
|
+ <!-- 待确认操作区 -->
|
|
|
+ <view v-if="isPendingConfirm" class="glass-card p-4">
|
|
|
+ <view class="banner banner-green mb-4">
|
|
|
+ <text class="banner-title">待确认任务</text>
|
|
|
+ <text class="banner-desc">{{ constructionTypeText }} · 服务时间 {{ serviceTimeText }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="action-group">
|
|
|
+ <button class="action-btn action-btn--confirm" hover-class="action-btn--hover" :disabled="submitting" @click="onConfirm">
|
|
|
+ <text class="uni-icons uniui-checkmarkempty action-btn__icon"></text>
|
|
|
+ <text>确认任务</text>
|
|
|
</button>
|
|
|
+ <view class="action-row">
|
|
|
+ <button class="action-btn action-btn--feedback" hover-class="action-btn--hover" @click="onFeedback">
|
|
|
+ <text class="uni-icons uniui-compose action-btn__icon"></text>
|
|
|
+ <text>反馈问题</text>
|
|
|
+ </button>
|
|
|
+ <button class="action-btn action-btn--reject" hover-class="action-btn--hover" @click="onReject">
|
|
|
+ <text class="uni-icons uniui-close action-btn__icon"></text>
|
|
|
+ <text>拒绝任务</text>
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 已拒绝 / 已取消 -->
|
|
|
+ <view v-if="task.status === 'rejected' || task.status === 'cancelled'" class="glass-card p-4">
|
|
|
+ <view class="banner banner-red">
|
|
|
+ <text class="banner-title">{{ task.status === 'rejected' ? '任务已拒绝' : '任务已取消' }}</text>
|
|
|
+ <text v-if="task.cancelReason" class="banner-desc">原因:{{ task.cancelReason }}</text>
|
|
|
</view>
|
|
|
+ </view>
|
|
|
|
|
|
- <!-- 已到达(勘验) -->
|
|
|
- <view v-else-if="task.status === 'arrived'" class="gap-y-3">
|
|
|
- <view class="bg-green-50 rounded-xl p-4 mb-4 text-center">
|
|
|
- <text class="uni-icons uniui-location text-4xl block mb-2 text-green-500"></text>
|
|
|
- <text class="text-lg font-semibold text-gray-800 block">已到达现场</text>
|
|
|
- <text class="text-gray-500 text-sm mt-1 block">请完成现场勘验后开始施工作业</text>
|
|
|
+ <!-- 出车准备(SOP:推进到第2步前) -->
|
|
|
+ <view v-if="currentStep === 1" class="glass-card p-4">
|
|
|
+ <text class="text-base font-semibold text-gray-800 mb-4 block">出车准备(SOP)</text>
|
|
|
+ <view class="gap-y-3 mb-4">
|
|
|
+ <view
|
|
|
+ v-for="item in checklist"
|
|
|
+ :key="item.key"
|
|
|
+ class="flex items-center justify-between py-2 border-b border-gray-100"
|
|
|
+ @click="item.checked = !item.checked"
|
|
|
+ >
|
|
|
+ <text class="text-sm text-gray-700">{{ item.label }}</text>
|
|
|
+ <view class="check-box" :class="item.checked ? 'check-box-on' : ''">
|
|
|
+ <text v-if="item.checked" class="uni-icons uniui-checkmarkempty check-icon"></text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- <button class="w-full py-3 btn-primary rounded-xl font-semibold flex items-center justify-center gap-2" @click="onStartWork">
|
|
|
- <text>开始施工</text>
|
|
|
- </button>
|
|
|
</view>
|
|
|
+ <text class="text-xs text-gray-400 block">请完成全部车辆点检后方可出发</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- SOP 推进操作区 -->
|
|
|
+ <view v-if="canAdvance" class="glass-card p-4">
|
|
|
+ <view class="banner banner-green mb-4">
|
|
|
+ <text class="banner-title">当前步骤:{{ stepLabels[currentStep] || '待开始' }}</text>
|
|
|
+ <text class="banner-desc">下一步:{{ stepLabels[currentStep + 1] }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="mb-4">
|
|
|
+ <text class="text-sm text-gray-500 mb-2 block">现场照片(可选)</text>
|
|
|
+ <ImageUploader v-model="stepPhotos" :max-count="6" tip="上传本步骤现场照片" />
|
|
|
+ </view>
|
|
|
+ <view class="mb-4">
|
|
|
+ <text class="text-sm text-gray-500 mb-2 block">备注(可选)</text>
|
|
|
+ <textarea v-model="stepRemark" class="form-textarea" placeholder="如:堵塞物情况、设施反馈等"></textarea>
|
|
|
+ </view>
|
|
|
+ <button
|
|
|
+ class="advance-btn"
|
|
|
+ :class="advanceDisabled ? 'advance-btn-disabled' : ''"
|
|
|
+ hover-class="advance-btn-hover"
|
|
|
+ :hover-start-time="0"
|
|
|
+ :hover-stay-time="120"
|
|
|
+ :disabled="submitting || advanceDisabled"
|
|
|
+ @click="onAdvance"
|
|
|
+ >
|
|
|
+ {{ nextActionText }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
|
|
|
- <!-- 施工中 -->
|
|
|
- <view v-else-if="task.status === 'constructing'" class="gap-y-3">
|
|
|
- <view class="bg-blue-50 rounded-xl p-4 mb-4 text-center">
|
|
|
- <text class="uni-icons uniui-gear-filled text-4xl block mb-2 text-blue-500"></text>
|
|
|
- <text class="text-lg font-semibold text-gray-800 block">施工中</text>
|
|
|
- <text class="text-gray-500 text-sm mt-1 block">请按规范完成施工作业</text>
|
|
|
+ <!-- 已完成 -->
|
|
|
+ <view v-if="task.status === 'completed'" class="glass-card p-4">
|
|
|
+ <view class="banner banner-green">
|
|
|
+ <text class="banner-title">任务已完成</text>
|
|
|
+ <text v-if="task.completionTime" class="banner-desc">完成时间:{{ formatDate(task.completionTime, 'YYYY-MM-DD HH:mm') }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 项目信息 -->
|
|
|
+ <view v-if="project" class="glass-card p-4">
|
|
|
+ <view class="section-header" @click="sections.project = !sections.project">
|
|
|
+ <text class="text-base font-semibold text-gray-800">项目信息</text>
|
|
|
+ <text class="uni-icons text-gray-400" :class="sections.project ? 'uniui-arrowup' : 'uniui-arrowdown'"></text>
|
|
|
+ </view>
|
|
|
+ <view v-if="sections.project" class="gap-y-3 mt-3">
|
|
|
+ <view class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">项目名称</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ project.projectName }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="coopTypeText" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">合作类型</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ coopTypeText }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="projectAddress" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">项目地址</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ projectAddress }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="project.primaryContactName" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">联系人</text>
|
|
|
+ <view class="flex-1 flex items-center justify-between">
|
|
|
+ <text class="text-sm text-gray-800">{{ project.primaryContactName }} {{ project.primaryContactPhone || '' }}</text>
|
|
|
+ <button
|
|
|
+ v-if="project.primaryContactPhone"
|
|
|
+ class="phone-btn"
|
|
|
+ @click="callPhone(project.primaryContactPhone)"
|
|
|
+ >
|
|
|
+ <text class="uni-icons uniui-phone-filled phone-icon"></text>
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- <button class="w-full py-3 btn-success rounded-xl font-semibold flex items-center justify-center gap-2" @click="onComplete">
|
|
|
- <text>完成施工</text>
|
|
|
- </button>
|
|
|
</view>
|
|
|
+ </view>
|
|
|
|
|
|
- <!-- 待验收 -->
|
|
|
- <view v-else-if="task.status === 'inspecting'" class="gap-y-3">
|
|
|
- <view class="bg-warning-light rounded-xl p-4 mb-4 text-center">
|
|
|
- <text class="uni-icons uniui-person-filled text-4xl block mb-2 text-warning"></text>
|
|
|
- <text class="text-lg font-semibold text-gray-800 block">等待验收</text>
|
|
|
- <text class="text-gray-500 text-sm mt-1 block">请等待客户或调度验收</text>
|
|
|
+ <!-- 施工信息 -->
|
|
|
+ <view class="glass-card p-4">
|
|
|
+ <view class="section-header" @click="sections.work = !sections.work">
|
|
|
+ <text class="text-base font-semibold text-gray-800">施工信息</text>
|
|
|
+ <text class="uni-icons text-gray-400" :class="sections.work ? 'uniui-arrowup' : 'uniui-arrowdown'"></text>
|
|
|
+ </view>
|
|
|
+ <view v-if="sections.work" class="gap-y-3 mt-3">
|
|
|
+ <view class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">施工类型</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ constructionTypeText }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="task.facilityType" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">设施类型</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.facilityType }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="task.serviceMethod" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">服务方式</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.serviceMethod }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">服务时间</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ serviceTimeText }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="task.address" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">施工地址</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.address }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="geoLocationText" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">地理定位</text>
|
|
|
+ <text class="text-sm link-text flex-1" @click="openMap">{{ geoLocationText }}(点击导航)</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="task.constructionInstruction" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">施工指令</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.constructionInstruction }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="task.riskTip" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">风险提示</text>
|
|
|
+ <text class="text-sm text-red-500 flex-1">{{ task.riskTip }}</text>
|
|
|
</view>
|
|
|
- <button class="w-full py-3 btn-success rounded-xl font-semibold flex items-center justify-center gap-2" @click="onAccept">
|
|
|
- <text>完成验收</text>
|
|
|
- </button>
|
|
|
</view>
|
|
|
+ </view>
|
|
|
|
|
|
- <!-- 已完成 -->
|
|
|
- <view v-else-if="task.status === 'completed'" class="gap-y-3">
|
|
|
- <view class="bg-green-50 rounded-xl p-4 text-center">
|
|
|
- <text class="uni-icons uniui-checkmarkempty text-4xl block mb-2 text-green-500"></text>
|
|
|
- <text class="text-lg font-semibold text-green-700 block">任务已完成</text>
|
|
|
- <text class="text-gray-500 text-sm mt-1 block">该任务已验收通过</text>
|
|
|
+ <!-- 故障信息 -->
|
|
|
+ <view v-if="faultDescText || task.faultLocation || faultMedia.length > 0" class="glass-card p-4">
|
|
|
+ <view class="section-header" @click="sections.fault = !sections.fault">
|
|
|
+ <text class="text-base font-semibold text-gray-800">故障信息</text>
|
|
|
+ <text class="uni-icons text-gray-400" :class="sections.fault ? 'uniui-arrowup' : 'uniui-arrowdown'"></text>
|
|
|
+ </view>
|
|
|
+ <view v-if="sections.fault" class="gap-y-3 mt-3">
|
|
|
+ <view v-if="task.faultLocation" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">故障位置</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.faultLocation }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="faultDescText" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">故障描述</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ faultDescText }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="task.customerWords" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">客户原话</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.customerWords }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="faultMedia.length > 0" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">故障图片</text>
|
|
|
+ <view class="flex-1 flex flex-wrap gap-2">
|
|
|
+ <image
|
|
|
+ v-for="(url, index) in faultMedia"
|
|
|
+ :key="index"
|
|
|
+ :src="url"
|
|
|
+ class="w-16 h-16 rounded-lg bg-gray-100"
|
|
|
+ mode="aspectFill"
|
|
|
+ @click="previewMedia(index)"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 任务信息 -->
|
|
|
- <view class="card p-4">
|
|
|
- <view class="detail-section-title mb-4">
|
|
|
- <text class="uni-icons uniui-flag-filled text-primary mr-2"></text>
|
|
|
- <text>{{ task.projectName }}</text>
|
|
|
- <StatusTag :status="task.status" class="ml-auto" />
|
|
|
+ <!-- 施工要求 -->
|
|
|
+ <view class="glass-card p-4">
|
|
|
+ <view class="section-header" @click="sections.require = !sections.require">
|
|
|
+ <text class="text-base font-semibold text-gray-800">施工要求</text>
|
|
|
+ <text class="uni-icons text-gray-400" :class="sections.require ? 'uniui-arrowup' : 'uniui-arrowdown'"></text>
|
|
|
</view>
|
|
|
- <view class="space-y-3">
|
|
|
- <view class="detail-row">
|
|
|
- <text class="detail-label">任务类型</text>
|
|
|
- <text class="detail-value">{{ getTypeText(task.type) }}</text>
|
|
|
+ <view v-if="sections.require" class="gap-y-3 mt-3">
|
|
|
+ <view v-if="task.waterDistance" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">取水距离</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.waterDistance }}</text>
|
|
|
</view>
|
|
|
- <view class="detail-row">
|
|
|
- <text class="detail-label">紧急程度</text>
|
|
|
- <text :class="task.urgent === 'high' ? 'text-danger' : 'text-gray-800'">{{ getUrgentText(task.urgent) }}</text>
|
|
|
+ <view v-if="task.outletNormal !== undefined && task.outletNormal !== null" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">化粪池排口</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.outletNormal === 1 ? '正常' : '异常' }}</text>
|
|
|
</view>
|
|
|
- <view v-if="task.serviceType" class="detail-row">
|
|
|
- <text class="detail-label">施工类型</text>
|
|
|
- <text class="detail-value">{{ task.serviceType }}</text>
|
|
|
+ <view v-if="task.assistantInfo" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">施工助力</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.assistantInfo }}</text>
|
|
|
</view>
|
|
|
- <view v-if="task.facilityType" class="detail-row">
|
|
|
- <text class="detail-label">设施类型</text>
|
|
|
- <text class="detail-value">{{ task.facilityType }}</text>
|
|
|
+ <view v-if="task.estimatedWorkload" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">预计作业量</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.estimatedWorkload }}</text>
|
|
|
</view>
|
|
|
- <view v-if="task.faultLocation" class="detail-row">
|
|
|
- <text class="detail-label">故障位置</text>
|
|
|
- <text class="detail-value">{{ task.faultLocation }}</text>
|
|
|
+ <view v-if="task.estimatedDuration" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">预计工时</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.estimatedDuration }}</text>
|
|
|
</view>
|
|
|
- <view v-if="task.faultDesc" class="detail-row">
|
|
|
- <text class="detail-label">故障描述</text>
|
|
|
- <text class="detail-value">{{ task.faultDesc }}</text>
|
|
|
+ <view v-if="task.acceptanceRequirement" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">验收要求</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.acceptanceRequirement }}</text>
|
|
|
</view>
|
|
|
- <view v-if="task.assignee" class="detail-row">
|
|
|
- <text class="detail-label">负责人</text>
|
|
|
- <text class="detail-value">{{ task.assignee }}</text>
|
|
|
+ <view v-if="task.acceptanceMethod" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">验收方式</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.acceptanceMethod }}</text>
|
|
|
</view>
|
|
|
- <view v-if="task.vehicle" class="detail-row">
|
|
|
- <text class="detail-label">车辆</text>
|
|
|
- <text class="detail-value">{{ task.vehicle }}</text>
|
|
|
+ <view v-if="task.attentionPoint" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">注意要点</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.attentionPoint }}</text>
|
|
|
</view>
|
|
|
- <view v-if="task.team" class="detail-row">
|
|
|
- <text class="detail-label">班组</text>
|
|
|
- <text class="detail-value">{{ task.team }}</text>
|
|
|
+ <view v-if="task.toolRequirement" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">工具要求</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ task.toolRequirement }}</text>
|
|
|
</view>
|
|
|
- <view v-if="task.scheduledDate" class="detail-row">
|
|
|
- <text class="detail-label">计划日期</text>
|
|
|
- <text class="detail-value">{{ task.scheduledDate }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 甲方确认人 -->
|
|
|
+ <view v-if="partyAContacts.length > 0" class="glass-card p-4">
|
|
|
+ <text class="text-base font-semibold text-gray-800 mb-4 block">甲方确认人</text>
|
|
|
+ <view class="gap-y-2">
|
|
|
+ <view
|
|
|
+ v-for="(contact, index) in partyAContacts"
|
|
|
+ :key="index"
|
|
|
+ class="flex items-center justify-between py-2 border-b border-gray-100"
|
|
|
+ >
|
|
|
+ <text class="text-sm text-gray-800">{{ contact.name }} {{ contact.phone }}</text>
|
|
|
+ <button
|
|
|
+ v-if="contact.phone"
|
|
|
+ class="phone-btn"
|
|
|
+ @click="callPhone(contact.phone)"
|
|
|
+ >
|
|
|
+ <text class="uni-icons uniui-phone-filled phone-icon"></text>
|
|
|
+ </button>
|
|
|
</view>
|
|
|
- <view v-if="task.address" class="detail-row">
|
|
|
- <text class="detail-label">项目地址</text>
|
|
|
- <text class="detail-value text-right flex-1">{{ task.address }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 调度信息 -->
|
|
|
+ <view v-if="isDispatched" class="glass-card p-4">
|
|
|
+ <text class="text-base font-semibold text-gray-800 mb-4 block">调度信息</text>
|
|
|
+ <view class="gap-y-3">
|
|
|
+ <view class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">执行班组</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ dispatchedTeamName }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">调度车辆</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ dispatchedVehicleName }}</text>
|
|
|
</view>
|
|
|
- <view class="detail-row">
|
|
|
- <text class="detail-label">创建时间</text>
|
|
|
- <text class="detail-value">{{ formatDate(task.createTime, 'YYYY-MM-DD HH:mm') }}</text>
|
|
|
+ <view v-if="task.departureTime" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">出车时间</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ formatDate(task.departureTime, 'MM-DD HH:mm') }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="task.arrivalTime" class="flex items-start">
|
|
|
+ <text class="text-gray-500 text-sm w-24">到达时间</text>
|
|
|
+ <text class="text-sm text-gray-800 flex-1">{{ formatDate(task.arrivalTime, 'MM-DD HH:mm') }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 时间轴 -->
|
|
|
- <view class="card p-4">
|
|
|
- <view class="detail-section-title mb-4">
|
|
|
- <text class="uni-icons uniui-time-filled text-primary mr-2"></text>
|
|
|
- <text>操作记录</text>
|
|
|
+ <!-- 操作日志 -->
|
|
|
+ <view v-if="taskLogs.length > 0" class="glass-card p-4">
|
|
|
+ <text class="text-base font-semibold text-gray-800 mb-4 block">任务日志</text>
|
|
|
+ <view class="gap-y-4">
|
|
|
+ <view v-for="(log, index) in taskLogs" :key="log.logId || index" class="flex items-start">
|
|
|
+ <view class="flex flex-col items-center mr-3">
|
|
|
+ <view class="w-3 h-3 rounded-full" :class="index === 0 ? 'log-dot-active' : 'log-dot'"></view>
|
|
|
+ <view v-if="index < taskLogs.length - 1" class="w-px h-8 bg-gray-200 mt-1"></view>
|
|
|
+ </view>
|
|
|
+ <view class="flex-1 pb-2">
|
|
|
+ <text class="text-sm font-medium text-gray-800">{{ log.operation }}</text>
|
|
|
+ <view class="flex justify-between mt-1">
|
|
|
+ <text class="text-xs text-gray-500">{{ log.operatorName || '系统' }}</text>
|
|
|
+ <text class="text-xs text-gray-400">{{ formatDate(log.createTime, 'MM-DD HH:mm') }}</text>
|
|
|
+ </view>
|
|
|
+ <text v-if="log.remark" class="text-xs text-gray-400 block mt-1">{{ log.remark }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- <Timeline :records="sortedTimeline" />
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
@@ -180,95 +350,568 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { computed } from 'vue'
|
|
|
+import { computed, reactive, ref } from 'vue'
|
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
|
import TopBar from '../../components/common/TopBar.vue'
|
|
|
import StatusTag from '../../components/common/StatusTag.vue'
|
|
|
import EmptyState from '../../components/common/EmptyState.vue'
|
|
|
-import Timeline from '../../components/common/Timeline.vue'
|
|
|
-import ConstructionFlowBar from '../../components/common/ConstructionFlowBar.vue'
|
|
|
-import { useTaskStore } from '../../stores/task'
|
|
|
-import { formatDate, getTypeText, getUrgentText, showToast, showModal, goBack } from '../../utils'
|
|
|
-import { updateTask } from '../../api/task'
|
|
|
-
|
|
|
-const taskStore = useTaskStore()
|
|
|
-const task = computed(() => taskStore.currentTask)
|
|
|
-
|
|
|
-const sortedTimeline = computed(() => {
|
|
|
- if (!task.value?.timeline) return []
|
|
|
- return [...task.value.timeline].sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime())
|
|
|
+import ImageUploader from '../../components/common/ImageUploader.vue'
|
|
|
+import { formatDate, showToast } from '../../utils'
|
|
|
+import { getTaskDetail, advanceTaskStep, updateTask } from '../../api/task'
|
|
|
+import { getProjectDetail } from '../../api/project'
|
|
|
+import { getTeamSimpleList } from '../../api/team'
|
|
|
+import { getVehicleSimpleList } from '../../api/vehicle'
|
|
|
+
|
|
|
+const STEP_LABELS = ['确认', '准备', '现场', '施工', '拍照', '清洗', '签字', '清理']
|
|
|
+const stepLabels = STEP_LABELS
|
|
|
+
|
|
|
+const task = ref<any>(null)
|
|
|
+const project = ref<any>(null)
|
|
|
+const loading = ref(false)
|
|
|
+const submitting = ref(false)
|
|
|
+const taskId = ref<number>(0)
|
|
|
+
|
|
|
+const stepPhotos = ref<string[]>([])
|
|
|
+const stepRemark = ref('')
|
|
|
+
|
|
|
+const checklist = reactive([
|
|
|
+ { key: 'oil', label: '机油检查', checked: false },
|
|
|
+ { key: 'tire', label: '轮胎检查', checked: false },
|
|
|
+ { key: 'tools', label: '灯光、水管、工具检查', checked: false },
|
|
|
+])
|
|
|
+
|
|
|
+const sections = reactive({
|
|
|
+ project: true,
|
|
|
+ work: true,
|
|
|
+ fault: true,
|
|
|
+ require: false,
|
|
|
})
|
|
|
|
|
|
-async function advanceStatus(step: number, successMsg: string) {
|
|
|
- if (!task.value) return
|
|
|
+const teamOptions = ref<Array<{ value: number; label: string }>>([])
|
|
|
+const vehicleOptions = ref<Array<{ value: number; label: string }>>([])
|
|
|
+
|
|
|
+const currentStep = computed(() => Number(task.value?.currentStep) || 0)
|
|
|
+const taskLogs = computed(() => task.value?.logList || [])
|
|
|
+
|
|
|
+const isPendingConfirm = computed(() => task.value?.status === 'assigned' && currentStep.value === 0)
|
|
|
+const isDispatched = computed(() => ['assigned', 'in_progress', 'completed'].includes(task.value?.status))
|
|
|
+const canAdvance = computed(() => {
|
|
|
+ if (!task.value) return false
|
|
|
+ if (!['assigned', 'in_progress'].includes(task.value.status)) return false
|
|
|
+ if (currentStep.value === 0) return false
|
|
|
+ return currentStep.value < 8
|
|
|
+})
|
|
|
+
|
|
|
+const advanceDisabled = computed(() => currentStep.value === 1 && !checklist.every(c => c.checked))
|
|
|
+
|
|
|
+const nextActionText = computed(() => {
|
|
|
+ const next = currentStep.value + 1
|
|
|
+ const map: Record<number, string> = {
|
|
|
+ 2: '完成准备,出发',
|
|
|
+ 3: '到达现场',
|
|
|
+ 4: '开始施工',
|
|
|
+ 5: '完成拍照记录',
|
|
|
+ 6: '完成清洗',
|
|
|
+ 7: '完成签字验收',
|
|
|
+ 8: '完成清理,结束任务',
|
|
|
+ }
|
|
|
+ return map[next] || '推进到下一步'
|
|
|
+})
|
|
|
+
|
|
|
+const extra = computed(() => {
|
|
|
+ if (!task.value?.extraData) return {} as any
|
|
|
try {
|
|
|
- await taskStore.advanceStep(Number(task.value.id), step)
|
|
|
- showToast(successMsg)
|
|
|
- await taskStore.fetchTaskDetail(Number(task.value.id))
|
|
|
- } catch (error) {
|
|
|
- showToast('操作失败')
|
|
|
+ return JSON.parse(task.value.extraData)
|
|
|
+ } catch {
|
|
|
+ return {} as any
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const taskName = computed(() => {
|
|
|
+ if (!task.value) return ''
|
|
|
+ return task.value.taskName || task.value.serviceContent || task.value.faultLocation || taskTypeText(task.value.taskType) || '未命名任务'
|
|
|
+})
|
|
|
+
|
|
|
+const constructionTypeText = computed(() => {
|
|
|
+ if (!task.value) return ''
|
|
|
+ return task.value.constructionType || taskTypeText(task.value.taskType) || '-'
|
|
|
+})
|
|
|
+
|
|
|
+const serviceTimeText = computed(() => {
|
|
|
+ if (!task.value) return '待定'
|
|
|
+ const date = task.value.planDate || ''
|
|
|
+ const rangeMap: Record<string, string> = { morning: '上午', afternoon: '下午', evening: '晚上' }
|
|
|
+ const range = rangeMap[task.value.planTimeRange] || task.value.planTimeRange || ''
|
|
|
+ return date || range ? `${date} ${range}`.trim() : '待定'
|
|
|
+})
|
|
|
+
|
|
|
+const urgencyText = computed(() => {
|
|
|
+ const map: Record<string, string> = { normal: '普通', urgent: '紧急', very_urgent: '特急' }
|
|
|
+ return task.value?.urgencyLevel ? map[task.value.urgencyLevel] || task.value.urgencyLevel : ''
|
|
|
+})
|
|
|
+
|
|
|
+const coopTypeText = computed(() => {
|
|
|
+ const map: Record<string, string> = { annual: '包年', monthly: '包月', once: '单次', contract: '合同制' }
|
|
|
+ const v = project.value?.cooperationType
|
|
|
+ return v ? map[v] || v : ''
|
|
|
+})
|
|
|
+
|
|
|
+const projectAddress = computed(() => {
|
|
|
+ if (!project.value) return ''
|
|
|
+ const p = project.value
|
|
|
+ return [p.province, p.city, p.district, p.street, p.detailAddress || p.address].filter(Boolean).join('') || p.address || ''
|
|
|
+})
|
|
|
+
|
|
|
+const faultDescText = computed(() => {
|
|
|
+ if (!task.value) return ''
|
|
|
+ return [task.value.faultType, task.value.customerWords].filter(Boolean).join(';')
|
|
|
+})
|
|
|
+
|
|
|
+const geoLocationText = computed(() => {
|
|
|
+ if (extra.value.geoLocation) return extra.value.geoLocation
|
|
|
+ if (extra.value.latitude && extra.value.longitude) return `${extra.value.latitude}, ${extra.value.longitude}`
|
|
|
+ return ''
|
|
|
+})
|
|
|
+
|
|
|
+const faultMedia = computed<string[]>(() => {
|
|
|
+ const photos = extra.value.photos || extra.value.faultPhotos || extra.value.media
|
|
|
+ if (!photos) return []
|
|
|
+ if (Array.isArray(photos)) return photos
|
|
|
+ return String(photos).split(',').filter(Boolean)
|
|
|
+})
|
|
|
+
|
|
|
+const partyAContacts = computed<Array<{ name: string; phone: string }>>(() => {
|
|
|
+ const list = extra.value.partyAContacts || extra.value.partyA
|
|
|
+ if (!list) return []
|
|
|
+ if (Array.isArray(list)) return list
|
|
|
+ return []
|
|
|
+})
|
|
|
+
|
|
|
+const dispatchedTeamName = computed(() => {
|
|
|
+ if (!task.value?.dispatchTeamId) return '未安排'
|
|
|
+ return teamOptions.value.find(t => t.value === Number(task.value.dispatchTeamId))?.label || `班组#${task.value.dispatchTeamId}`
|
|
|
+})
|
|
|
+
|
|
|
+const dispatchedVehicleName = computed(() => {
|
|
|
+ if (!task.value?.dispatchVehicleId) return '未安排'
|
|
|
+ return vehicleOptions.value.find(v => v.value === Number(task.value.dispatchVehicleId))?.label || `车辆#${task.value.dispatchVehicleId}`
|
|
|
+})
|
|
|
+
|
|
|
+function nodeClass(step: number): string {
|
|
|
+ if (step <= currentStep.value) return 'flow-node-active'
|
|
|
+ if (step === currentStep.value + 1) return 'flow-node-current'
|
|
|
+ return ''
|
|
|
+}
|
|
|
+
|
|
|
+function taskTypeText(type: string): string {
|
|
|
+ const map: Record<string, string> = {
|
|
|
+ dredge: '疏通', repair: '维修', clean: '清掏', inspect: '检查', emergency: '应急',
|
|
|
+ '疏通': '疏通', '维修': '维修', '清掏': '清掏', '检查': '检查', '应急': '应急',
|
|
|
}
|
|
|
+ return type ? map[type] || type : ''
|
|
|
}
|
|
|
|
|
|
-function onConfirm() {
|
|
|
- advanceStatus(1, '任务已确认')
|
|
|
+function callPhone(phone: string) {
|
|
|
+ if (!phone) return
|
|
|
+ uni.makePhoneCall({ phoneNumber: phone })
|
|
|
}
|
|
|
|
|
|
-function onDepart() {
|
|
|
- advanceStatus(3, '已出车')
|
|
|
+function openMap() {
|
|
|
+ const lat = Number(extra.value.latitude)
|
|
|
+ const lng = Number(extra.value.longitude)
|
|
|
+ if (!lat || !lng) return
|
|
|
+ uni.openLocation({ latitude: lat, longitude: lng, name: task.value?.faultLocation || '故障位置' })
|
|
|
}
|
|
|
|
|
|
-function onArrive() {
|
|
|
- advanceStatus(4, '已到达现场')
|
|
|
+function previewMedia(index: number) {
|
|
|
+ uni.previewImage({ urls: faultMedia.value, current: index })
|
|
|
}
|
|
|
|
|
|
-function onStartWork() {
|
|
|
- advanceStatus(5, '开始施工')
|
|
|
+onLoad((options) => {
|
|
|
+ if (options?.id) {
|
|
|
+ taskId.value = Number(options.id)
|
|
|
+ loadDetail()
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+async function loadDetail() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const res: any = await getTaskDetail(taskId.value)
|
|
|
+ task.value = res
|
|
|
+ if (res?.projectId) {
|
|
|
+ getProjectDetail(res.projectId).then(p => { project.value = p }).catch(() => {})
|
|
|
+ }
|
|
|
+ if (teamOptions.value.length === 0) {
|
|
|
+ getTeamSimpleList().then(list => { teamOptions.value = list }).catch(() => {})
|
|
|
+ }
|
|
|
+ if (vehicleOptions.value.length === 0) {
|
|
|
+ getVehicleSimpleList().then(list => { vehicleOptions.value = list }).catch(() => {})
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载任务详情失败', e)
|
|
|
+ showToast('加载任务详情失败')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-function onComplete() {
|
|
|
- advanceStatus(6, '施工完成')
|
|
|
+async function doAdvance(step: number, successMsg: string) {
|
|
|
+ submitting.value = true
|
|
|
+ try {
|
|
|
+ const data: Record<string, any> = {}
|
|
|
+ if (stepRemark.value.trim()) data.remark = stepRemark.value.trim()
|
|
|
+ if (stepPhotos.value.length > 0) data.photos = stepPhotos.value
|
|
|
+ await advanceTaskStep(taskId.value, step, Object.keys(data).length > 0 ? data : undefined)
|
|
|
+ showToast(successMsg)
|
|
|
+ stepPhotos.value = []
|
|
|
+ stepRemark.value = ''
|
|
|
+ await loadDetail()
|
|
|
+ } catch (e) {
|
|
|
+ console.error('推进SOP步骤失败', e)
|
|
|
+ } finally {
|
|
|
+ submitting.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onConfirm() {
|
|
|
+ doAdvance(1, '任务已确认')
|
|
|
}
|
|
|
|
|
|
-function onAccept() {
|
|
|
- advanceStatus(7, '验收完成')
|
|
|
+function onAdvance() {
|
|
|
+ if (advanceDisabled.value) {
|
|
|
+ showToast('请先完成全部出车点检')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const next = currentStep.value + 1
|
|
|
+ if (next > 8) return
|
|
|
+ doAdvance(next, next === 8 ? '任务已完成' : `已推进到「${STEP_LABELS[next - 1]}」`)
|
|
|
}
|
|
|
|
|
|
async function onReject() {
|
|
|
- const res = await showModal({
|
|
|
- title: '拒绝任务',
|
|
|
- content: '确定要拒绝此任务吗?',
|
|
|
- showCancel: true,
|
|
|
- confirmText: '确认拒绝',
|
|
|
+ const res = await new Promise<any>((resolve) => {
|
|
|
+ uni.showModal({
|
|
|
+ title: '拒绝任务',
|
|
|
+ editable: true,
|
|
|
+ placeholderText: '请输入拒绝原因(可选)',
|
|
|
+ confirmText: '确认拒绝',
|
|
|
+ success: resolve,
|
|
|
+ fail: () => resolve({ confirm: false }),
|
|
|
+ })
|
|
|
})
|
|
|
- if (res.confirm && task.value) {
|
|
|
- try {
|
|
|
- await updateTask(Number(task.value.id), { status: 'rejected' } as any)
|
|
|
- showToast('已拒绝任务')
|
|
|
- goBack()
|
|
|
- } catch {
|
|
|
- showToast('操作失败')
|
|
|
- }
|
|
|
+ if (!res.confirm) return
|
|
|
+ submitting.value = true
|
|
|
+ try {
|
|
|
+ const t = task.value
|
|
|
+ await updateTask(taskId.value, {
|
|
|
+ taskNo: t.taskNo,
|
|
|
+ address: t.address,
|
|
|
+ taskType: t.taskType,
|
|
|
+ projectId: t.projectId,
|
|
|
+ planDate: t.planDate,
|
|
|
+ status: 'rejected',
|
|
|
+ remark: res.content || t.remark || '',
|
|
|
+ } as any)
|
|
|
+ showToast('已拒绝任务')
|
|
|
+ await loadDetail()
|
|
|
+ } catch (e) {
|
|
|
+ console.error('拒绝任务失败', e)
|
|
|
+ } finally {
|
|
|
+ submitting.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function onFeedback() {
|
|
|
- showModal({
|
|
|
+ uni.showModal({
|
|
|
title: '反馈问题',
|
|
|
- content: '请描述遇到的问题,将提交给调度中心处理',
|
|
|
- showCancel: true,
|
|
|
+ editable: true,
|
|
|
+ placeholderText: '请描述遇到的问题,将提交给调度中心处理',
|
|
|
confirmText: '提交',
|
|
|
success: (res) => {
|
|
|
if (res.confirm) {
|
|
|
showToast('反馈已提交')
|
|
|
}
|
|
|
- }
|
|
|
+ },
|
|
|
})
|
|
|
}
|
|
|
-
|
|
|
-onLoad((options) => {
|
|
|
- if (options?.id) {
|
|
|
- taskStore.setCurrentTask(options.id)
|
|
|
- }
|
|
|
-})
|
|
|
</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* 页面外壳 */
|
|
|
+.sub-page {
|
|
|
+ max-width: 430px;
|
|
|
+ margin: 0 auto;
|
|
|
+ min-height: 100vh;
|
|
|
+ background-color: #f6fbf9;
|
|
|
+ padding-bottom: 96px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 磨砂玻璃卡 */
|
|
|
+.glass-card {
|
|
|
+ background-color: rgba(255, 255, 255, 0.85);
|
|
|
+ backdrop-filter: blur(14px);
|
|
|
+ -webkit-backdrop-filter: blur(14px);
|
|
|
+ border: 1px solid rgba(164, 216, 152, 0.35);
|
|
|
+ border-radius: 24px;
|
|
|
+ box-shadow: 0 18px 50px -12px rgba(54, 143, 111, 0.28);
|
|
|
+ overflow: hidden;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-text {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+.link-text {
|
|
|
+ color: #368f6f;
|
|
|
+}
|
|
|
+
|
|
|
+/* 电话按钮 */
|
|
|
+.phone-btn {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background-color: rgba(164, 216, 152, 0.2);
|
|
|
+ line-height: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+.phone-btn::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.phone-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #368f6f;
|
|
|
+}
|
|
|
+
|
|
|
+/* 日志圆点 */
|
|
|
+.log-dot {
|
|
|
+ background-color: #c4d0cb;
|
|
|
+}
|
|
|
+
|
|
|
+.log-dot-active {
|
|
|
+ background-color: #368f6f;
|
|
|
+ box-shadow: 0 0 0 3px rgba(164, 216, 152, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+/* 推进按钮 */
|
|
|
+.advance-btn {
|
|
|
+ width: 100%;
|
|
|
+ padding: 12px 0;
|
|
|
+ border-radius: 14px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #ffffff;
|
|
|
+ background: linear-gradient(135deg, #368f6f 0%, #4ba98a 100%);
|
|
|
+ box-shadow: 0 10px 24px -10px rgba(54, 143, 111, 0.7);
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+.advance-btn::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.advance-btn-hover {
|
|
|
+ opacity: 0.88;
|
|
|
+}
|
|
|
+
|
|
|
+.advance-btn-disabled {
|
|
|
+ background: #e5e7eb;
|
|
|
+ color: #9ca3af;
|
|
|
+ box-shadow: none;
|
|
|
+}
|
|
|
+
|
|
|
+/* SOP 流程条 */
|
|
|
+.flow-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+.flow-step {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+.flow-node-wrap {
|
|
|
+ width: 100%;
|
|
|
+ height: 24px;
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+.flow-line {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 0;
|
|
|
+ right: 50%;
|
|
|
+ height: 2px;
|
|
|
+ background-color: #e5e7eb;
|
|
|
+ transform: translateY(-50%);
|
|
|
+}
|
|
|
+.flow-line-active {
|
|
|
+ background-color: #368f6f;
|
|
|
+}
|
|
|
+.flow-node {
|
|
|
+ width: 22px;
|
|
|
+ height: 22px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #f3f4f6;
|
|
|
+ border: 2px solid #e5e7eb;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+.flow-node-active {
|
|
|
+ background-color: #368f6f;
|
|
|
+ border-color: #368f6f;
|
|
|
+}
|
|
|
+.flow-node-current {
|
|
|
+ background-color: #fff;
|
|
|
+ border-color: #368f6f;
|
|
|
+ box-shadow: 0 0 0 3px rgba(54, 143, 111, 0.2);
|
|
|
+}
|
|
|
+.flow-check {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 11px;
|
|
|
+}
|
|
|
+.flow-node-text {
|
|
|
+ font-size: 10px;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+.flow-node-current .flow-node-text {
|
|
|
+ color: #368f6f;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.flow-label {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 10px;
|
|
|
+ color: #9ca3af;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.flow-label-active {
|
|
|
+ color: #368f6f;
|
|
|
+}
|
|
|
+
|
|
|
+/* 状态横幅 */
|
|
|
+.banner {
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 16px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.banner-green {
|
|
|
+ background-color: rgba(164, 216, 152, 0.16);
|
|
|
+}
|
|
|
+.banner-red {
|
|
|
+ background-color: #fef2f2;
|
|
|
+}
|
|
|
+.banner-title {
|
|
|
+ display: block;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
+}
|
|
|
+.banner-desc {
|
|
|
+ display: block;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7280;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 待确认操作按钮 */
|
|
|
+.action-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+.action-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+.action-row .action-btn {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+.action-btn {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 44px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 44px;
|
|
|
+ border: 1px solid transparent;
|
|
|
+ transition: transform 0.15s ease, opacity 0.15s ease;
|
|
|
+}
|
|
|
+.action-btn::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+.action-btn__icon {
|
|
|
+ font-size: 16px;
|
|
|
+ margin-right: 6px;
|
|
|
+}
|
|
|
+.action-btn--hover {
|
|
|
+ transform: scale(0.97);
|
|
|
+ opacity: 0.85;
|
|
|
+}
|
|
|
+.action-btn--confirm {
|
|
|
+ background: linear-gradient(135deg, #368f6f 0%, #5ab8d0 100%);
|
|
|
+ color: #ffffff;
|
|
|
+ box-shadow: 0 8px 20px -8px rgba(54, 143, 111, 0.6);
|
|
|
+}
|
|
|
+.action-btn--feedback {
|
|
|
+ background-color: #fffbeb;
|
|
|
+ border-color: #fcd34d;
|
|
|
+ color: #d97706;
|
|
|
+}
|
|
|
+.action-btn--reject {
|
|
|
+ background-color: #fef2f2;
|
|
|
+ border-color: #fca5a5;
|
|
|
+ color: #dc2626;
|
|
|
+}
|
|
|
+
|
|
|
+/* 点检勾选框 */
|
|
|
+.check-box {
|
|
|
+ width: 22px;
|
|
|
+ height: 22px;
|
|
|
+ border-radius: 6px;
|
|
|
+ border: 2px solid #d1d5db;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+.check-box-on {
|
|
|
+ background-color: #368f6f;
|
|
|
+ border-color: #368f6f;
|
|
|
+}
|
|
|
+.check-icon {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 折叠区块标题 */
|
|
|
+.section-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单 */
|
|
|
+.form-textarea {
|
|
|
+ width: 100%;
|
|
|
+ padding: 12px 16px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #333;
|
|
|
+ background: #fff;
|
|
|
+ min-height: 72px;
|
|
|
+}
|
|
|
+</style>
|