| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- <template>
- <view
- class="task-card p-4 clickable"
- hover-class="scale-down"
- :class="{ 'is-emergency': (task.taskType || task.type) === 'emergency' }"
- @click="$emit('click', String((task as any).id ?? (task as any).taskId ?? ''))"
- >
- <view class="flex justify-between items-start mb-2">
- <view class="flex-1 min-w-0 mr-3">
- <text class="text-sm font-semibold text-gray-800 truncate block">{{ (task as any).taskName || task.projectName || '未命名任务' }}</text>
- </view>
- <StatusTag :status="task.status" />
- </view>
- <view class="flex flex-wrap gap-2 mb-3">
- <view v-if="task.urgent === 'high'" class="px-2 py-1 bg-danger-light text-danger rounded text-xs font-medium">
- 加急
- </view>
- <view v-if="task.coopType" class="chip-type px-2 py-1 rounded text-xs font-medium">
- {{ task.coopType }}
- </view>
- <view v-if="task.serviceMode" class="px-2 py-1 bg-success-light text-success rounded text-xs font-medium">
- {{ task.serviceMode }}
- </view>
- </view>
- <view class="gap-y-2 mb-3">
- <view class="flex items-center">
- <text class="uni-icons uniui-tools info-icon text-xs mr-1"></text>
- <text class="text-gray-500 text-xs">{{ task.serviceType || '施工服务' }}</text>
- </view>
- <view class="flex items-center">
- <text class="uni-icons uniui-clock-filled info-icon text-xs mr-1"></text>
- <text class="text-gray-500 text-xs">{{ task.serviceDate || task.scheduleDate || '待定' }} {{ task.serviceTime || task.scheduleTime || '' }}</text>
- </view>
- <view class="flex items-center">
- <text class="uni-icons uniui-location-filled info-icon text-xs mr-1"></text>
- <text class="text-gray-500 text-xs truncate">{{ task.address || '暂无地址' }}</text>
- </view>
- </view>
- <view class="pt-3 border-t border-gray-100">
- <view class="flow-row">
- <view
- v-for="(step, i) in steps"
- :key="step.key"
- class="flow-step"
- >
- <view class="flow-node-wrap">
- <view v-if="i !== 0" class="flow-line" :class="step.active ? 'flow-line-active' : ''" />
- <view class="flow-node" :class="step.active ? 'flow-node-active' : (step.current ? 'flow-node-current' : '')">
- <text v-if="step.active" 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="step.active ? 'flow-label-active' : ''">{{ step.label }}</text>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script setup lang="ts">
- import { computed } from 'vue'
- import StatusTag from '../common/StatusTag.vue'
- import type { Task } from '@/types'
- import { SOP_STEPS } from '@/constants/sop'
- const props = defineProps<{
- task: Task
- }>()
- defineEmits<{
- click: [taskId: string]
- }>()
- const steps = computed(() => {
- const current = Number(props.task.currentStep) || 0
- return SOP_STEPS.map((s, i) => ({
- key: s.code,
- label: s.title.replace('任务', '').replace('准备', '出车').replace('现场', '到达').replace('签字', '验收'),
- active: current >= s.step,
- current: current + 1 === s.step,
- }))
- })
- </script>
- <style scoped>
- .task-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;
- }
- .task-card.is-emergency {
- border-left: 4px solid #ef4444;
- }
- .info-icon {
- color: #368f6f;
- }
- .chip-type {
- background-color: rgba(164, 216, 152, 0.2);
- color: #368f6f;
- }
- /* 流程条:与任务详情 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;
- }
- .scale-down {
- transition: transform 0.15s ease;
- }
- .scale-down:active {
- transform: scale(0.95);
- }
- </style>
|