|
|
@@ -0,0 +1,954 @@
|
|
|
+<template>
|
|
|
+ <el-drawer v-model="drawerVisible" title="订单详情" direction="rtl" size="60%" class="order-detail-drawer">
|
|
|
+ <div class="detail-container" v-if="order">
|
|
|
+ <!-- 1. Header Status -->
|
|
|
+ <div class="detail-header">
|
|
|
+ <div class="left-head">
|
|
|
+ <span class="order-no">{{ order.orderNo }}</span>
|
|
|
+ <el-tag :type="getStatusTag(order.status)" effect="dark" class="status-tag">{{
|
|
|
+ getStatusName(order.status) }}</el-tag>
|
|
|
+ <el-tag effect="plain" class="type-tag"
|
|
|
+ :type="order.type === 'transport' ? '' : (order.type === 'feeding' ? 'warning' : 'danger')">
|
|
|
+ {{ getTypeName(order.type) }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="right-head">
|
|
|
+ <!-- Action Buttons Group -->
|
|
|
+ <div class="detail-actions">
|
|
|
+ <template v-if="[0, 1, 2].includes(order.status)">
|
|
|
+ <el-button type="success" icon="Bicycle" @click="emit('dispatch', order)">立即派单</el-button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="order.status === 0">
|
|
|
+ <el-button type="danger" plain icon="CircleClose"
|
|
|
+ @click="emit('cancel', order)">取消订单</el-button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="order.status === 3">
|
|
|
+ <el-button type="primary" icon="CircleCheck"
|
|
|
+ @click="emit('command', 'complete', order)">确认完成</el-button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="[3, 4].includes(order.status)">
|
|
|
+ <el-button icon="Notebook" @click="emit('care-summary', order)">护理小结</el-button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-dropdown trigger="click" @command="(cmd) => emit('command', cmd, order)"
|
|
|
+ style="margin-left: 12px;">
|
|
|
+ <el-button icon="More">更多操作</el-button>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item command="reward" icon="Trophy">奖惩操作</el-dropdown-item>
|
|
|
+ <el-dropdown-item command="remark" icon="EditPen">订单备注</el-dropdown-item>
|
|
|
+ <el-dropdown-item command="delete" v-if="[5, 4].includes(order.status)" divided
|
|
|
+ icon="Delete" style="color: #f56c6c;">删除订单</el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="detail-scroll-area">
|
|
|
+ <!-- 2. Progress Section -->
|
|
|
+ <div class="progress-section">
|
|
|
+ <el-steps :active="currentOrderSteps.active" finish-status="success" align-center
|
|
|
+ class="custom-steps">
|
|
|
+ <el-step v-for="(step, index) in currentOrderSteps.steps" :key="index" :title="step.title"
|
|
|
+ :description="step.time" />
|
|
|
+ </el-steps>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 3. Top Info: Pet & User -->
|
|
|
+ <div class="top-info-row">
|
|
|
+ <!-- Left: Pet Info -->
|
|
|
+ <div class="info-section pet-section">
|
|
|
+ <div class="sec-header">
|
|
|
+ <span class="label">宠物档案</span>
|
|
|
+ <el-tag size="small" effect="plain">{{ order.petBreed }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="pet-basic-row">
|
|
|
+ <el-avatar :size="50" :src="order.petAvatar" shape="square" class="pet-avatar-lg">{{
|
|
|
+ (order.petName || '').charAt(0) }}</el-avatar>
|
|
|
+ <div class="pet-names">
|
|
|
+ <div class="b-name">{{ order.petName }}
|
|
|
+ <el-icon v-if="order.petGender === 'male'" color="#409eff">
|
|
|
+ <Male />
|
|
|
+ </el-icon>
|
|
|
+ <el-icon v-else color="#f56c6c">
|
|
|
+ <Female />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="b-tags">
|
|
|
+ <el-tag size="small" type="info">{{ order.petAge || '未知年龄' }}</el-tag>
|
|
|
+ <el-tag size="small" type="info">{{ order.petWeight || '未知体重' }}</el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-descriptions :column="2" size="small" class="pet-desc" border>
|
|
|
+ <el-descriptions-item label="绝育状态">{{ order.petSterilized ? '已绝育' : '未绝育'
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ order.petVaccine || '未知'
|
|
|
+ }}</span></el-descriptions-item>
|
|
|
+ <el-descriptions-item label="性格特点">{{ order.petCharacter || '温顺' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="健康状况">{{ order.petHealth || '健康' }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Right: User Info -->
|
|
|
+ <div class="info-section user-section">
|
|
|
+ <div class="sec-header">
|
|
|
+ <span class="label">用户信息</span>
|
|
|
+ </div>
|
|
|
+ <div class="user-content">
|
|
|
+ <div class="u-row">
|
|
|
+ <el-avatar :size="40" :src="order.userAvatar">{{ (order.userName || '').charAt(0)
|
|
|
+ }}</el-avatar>
|
|
|
+ <div class="u-info">
|
|
|
+ <div class="nm">{{ order.userName }}</div>
|
|
|
+ <div class="ph">{{ order.contactPhone }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="addr-box">
|
|
|
+ <div class="addr-label">服务地址</div>
|
|
|
+ <div class="addr-txt">{{ order.city }}{{ order.district }} {{ order.address || '' }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 4. Bottom Tabs -->
|
|
|
+ <el-tabs v-model="activeDetailTab" class="detail-tabs type-card">
|
|
|
+ <!-- Tab 1: Basic Info -->
|
|
|
+ <el-tab-pane label="订单基础信息" name="basic">
|
|
|
+ <div class="tab-pane-content">
|
|
|
+ <div class="section-block">
|
|
|
+ <div class="sec-title-bar">基础业务信息</div>
|
|
|
+ <el-descriptions :column="3" border size="default" class="custom-desc">
|
|
|
+ <el-descriptions-item label="系统单号">{{ order.orderNo }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="服务类型">
|
|
|
+ {{ getTypeName(order.type) }}
|
|
|
+ <el-tag size="small" v-if="order.type === 'transport'" style="margin-left:5px"
|
|
|
+ effect="light">{{ getTransportModeName(order.transportType) }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="归属门店">{{ order.merchantName }}
|
|
|
+ ({{ Number(order.platformId) === 1 ? '门店下单' : '平台代下单' }})</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="宠主信息">{{ order.userName }} / {{ order.contactPhone
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="服务费用" label-class-name="money-label">
|
|
|
+ <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.fulfillerFee }}</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="预约时间">{{ getServiceTimeRange(order.serviceTime)
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="团购套餐">{{ order.groupBuyPackage || '未使用团购套餐'
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="创建时间">{{ order.createTime }}</el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="订单备注" :span="3">
|
|
|
+ {{ order.remark || '暂无备注' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="order.type === 'transport'" class="section-block transport-split-block">
|
|
|
+ <div class="sec-title-bar">接送任务详情</div>
|
|
|
+ <div class="transport-one">
|
|
|
+ <div class="t-row">
|
|
|
+ <el-tag size="small" effect="plain" class="sub-badge">{{
|
|
|
+ getTransportLabel(order.subOrderType) }}</el-tag>
|
|
|
+ <span class="time">{{ order.serviceTime }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="t-row">
|
|
|
+ <span class="t-k">起点</span>
|
|
|
+ <span class="t-v">{{ order.detail?.fromAddress || order.detail?.pickAddr || '--'
|
|
|
+ }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="t-row">
|
|
|
+ <span class="t-k">终点</span>
|
|
|
+ <span class="t-v">{{ order.detail?.toAddress || order.detail?.dropAddr ||
|
|
|
+ order.toAddress ||
|
|
|
+ '--' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="t-row sub">
|
|
|
+ <span class="t-v">{{ order.contact || order.userName || '--' }} {{
|
|
|
+ order.contactPhoneNumber
|
|
|
+ || order.contactPhone || '--' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="['feeding', 'washing'].includes(order.type)" class="section-block">
|
|
|
+ <div class="sec-title-bar">服务执行要求</div>
|
|
|
+ <el-descriptions :column="2" border size="default" class="custom-desc">
|
|
|
+ <el-descriptions-item label="服务地址" :span="2">{{ order.detail.area || order.address
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="服务套餐">{{ order.detail.packageName
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="特殊要求">{{ order.detail.petStatus || '无'
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- Tab 2: Fulfiller Info -->
|
|
|
+ <el-tab-pane label="指派履约者" name="fulfiller">
|
|
|
+ <div class="tab-pane-content">
|
|
|
+ <div v-if="order.fulfillerName" class="fulfiller-card">
|
|
|
+ <div class="f-left">
|
|
|
+ <el-avatar :size="60" :src="order.fulfillerAvatar">{{ order.fulfillerName.charAt(0)
|
|
|
+ }}</el-avatar>
|
|
|
+ </div>
|
|
|
+ <div class="f-right">
|
|
|
+ <div class="f-row1">
|
|
|
+ <span class="f-name">{{ order.fulfillerName }}</span>
|
|
|
+ <el-tag size="small" type="primary" effect="plain" round>Lv1 普通</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="f-row2">
|
|
|
+ <span>联系电话:{{ order.fulfillerPhone || '138****0000' }}</span>
|
|
|
+ <span class="sep">|</span>
|
|
|
+ <span>归属区域:{{ order.fulfillerStation || '朝阳一站' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="f-row3"
|
|
|
+ style="margin-top: 8px; font-size: 13px; color: #606266; background: #f9fafe; padding: 8px; border-radius: 4px; display: flex; gap: 20px;">
|
|
|
+ <span><span style="color:#909399;">指派时间:</span>{{ order.createTime }}</span>
|
|
|
+ <span><span style="color:#909399;">接单时间:</span>{{ order.detail?.receiveTime ||
|
|
|
+ order.serviceTime }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-else class="empty-state">
|
|
|
+ <el-result icon="info" title="暂无履约者" sub-title="该订单尚未指派履约人员"></el-result>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- Tab 3: Service Progress -->
|
|
|
+ <el-tab-pane label="服务进度" name="service">
|
|
|
+ <div class="tab-pane-content">
|
|
|
+ <div v-if="serviceProgressSteps.length === 0" class="empty-progress"
|
|
|
+ style="padding:40px; text-align:center; color:#909399;">
|
|
|
+ <el-result icon="info" title="待接单" sub-title="履约者接单后将在此记录服务进度"></el-result>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-timeline style="padding: 10px 20px;" v-else>
|
|
|
+ <el-timeline-item v-for="(step, index) in serviceProgressSteps" :key="index"
|
|
|
+ :timestamp="step.time" placement="top" :color="step.color" :icon="step.icon"
|
|
|
+ size="large">
|
|
|
+ <div class="progress-card">
|
|
|
+ <h4 class="p-title">{{ step.title }}</h4>
|
|
|
+ <p class="p-desc">{{ step.desc }}</p>
|
|
|
+ <div class="p-media" v-if="step.media && step.media.length">
|
|
|
+ <div v-for="(item, i) in step.media" :key="i" class="media-item">
|
|
|
+ <el-image v-if="item.type === 'image'" :src="item.url"
|
|
|
+ :preview-src-list="step.media.map(m => m.url)" fit="cover"
|
|
|
+ class="p-img" :preview-teleported="true" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-timeline-item>
|
|
|
+ </el-timeline>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- Tab 4: Logs -->
|
|
|
+ <el-tab-pane label="订单日志" name="logs">
|
|
|
+ <div class="tab-pane-content">
|
|
|
+ <div style="display: flex; justify-content: flex-end; margin-bottom: 15px;">
|
|
|
+ <el-button type="primary" size="small" icon="Download"
|
|
|
+ @click="handleExportLogs">导出日志Excel</el-button>
|
|
|
+ </div>
|
|
|
+ <el-timeline>
|
|
|
+ <el-timeline-item v-for="(log, index) in (order.orderLogs || [])" :key="index"
|
|
|
+ :timestamp="log.time" :type="log.type || 'primary'" :icon="log.icon"
|
|
|
+ placement="top">
|
|
|
+ <div class="log-card">
|
|
|
+ <div class="l-tit">{{ log.title }}</div>
|
|
|
+ <div class="l-txt">{{ log.content }}</div>
|
|
|
+ </div>
|
|
|
+ </el-timeline-item>
|
|
|
+
|
|
|
+ <el-timeline-item
|
|
|
+ v-if="(!order.orderLogs || order.orderLogs.length === 0) && order.timeline"
|
|
|
+ v-for="(log, idx) in order.timeline" :key="'old-' + idx" :timestamp="log.time"
|
|
|
+ :type="log.type">
|
|
|
+ {{ log.content }}
|
|
|
+ </el-timeline-item>
|
|
|
+ </el-timeline>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, watch } from 'vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import { getPet } from '@/api/archieves/pet'
|
|
|
+import { getCustomer } from '@/api/archieves/customer'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ visible: Boolean,
|
|
|
+ order: Object
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits(['update:visible', 'dispatch', 'cancel', 'command', 'care-summary'])
|
|
|
+
|
|
|
+const drawerVisible = computed({
|
|
|
+ get: () => props.visible,
|
|
|
+ set: (val) => emit('update:visible', val)
|
|
|
+})
|
|
|
+
|
|
|
+const orderDetail = ref(null)
|
|
|
+const order = computed(() => orderDetail.value || props.order)
|
|
|
+
|
|
|
+const loadSeq = ref(0)
|
|
|
+
|
|
|
+const loadPetAndCustomer = async (order) => {
|
|
|
+ const seq = ++loadSeq.value
|
|
|
+ const next = { ...(order || {}) }
|
|
|
+
|
|
|
+ const petId = next?.pet || next?.petId
|
|
|
+ if (petId) {
|
|
|
+ try {
|
|
|
+ const res = await getPet(petId)
|
|
|
+ const pet = res?.data
|
|
|
+ if (pet) {
|
|
|
+ next.petName = pet.name ?? next.petName
|
|
|
+ next.petAvatar = pet.avatarUrl ?? next.petAvatar
|
|
|
+ next.petGender = pet.gender ?? next.petGender
|
|
|
+ next.petAge = (pet.age !== undefined && pet.age !== null) ? `${pet.age}岁` : next.petAge
|
|
|
+ next.petWeight = (pet.weight !== undefined && pet.weight !== null) ? `${pet.weight}kg` : next.petWeight
|
|
|
+ next.petBreed = pet.breed ?? next.petBreed
|
|
|
+ next.petSterilized = (pet.isSterilized !== undefined && pet.isSterilized !== null)
|
|
|
+ ? (Number(pet.isSterilized) === 1)
|
|
|
+ : next.petSterilized
|
|
|
+ next.petVaccine = pet.vaccineStatus ?? next.petVaccine
|
|
|
+ next.petCharacter = pet.personality ?? next.petCharacter
|
|
|
+ next.petHealth = pet.healthStatus ?? next.petHealth
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const customerId = next?.customer || next?.customerId
|
|
|
+ if (customerId) {
|
|
|
+ try {
|
|
|
+ const res = await getCustomer(customerId)
|
|
|
+ const customer = res?.data
|
|
|
+ if (customer) {
|
|
|
+ next.userName = customer.name ?? next.userName
|
|
|
+ next.userAvatar = customer.avatarUrl ?? next.userAvatar
|
|
|
+ next.contactPhone = customer.phone ?? next.contactPhone
|
|
|
+ next.city = customer.areaName ?? next.city
|
|
|
+ next.address = customer.address ?? next.address
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (seq !== loadSeq.value) return
|
|
|
+ orderDetail.value = next
|
|
|
+}
|
|
|
+
|
|
|
+watch(() => props.order, (val) => {
|
|
|
+ if (!val) {
|
|
|
+ orderDetail.value = null
|
|
|
+ return
|
|
|
+ }
|
|
|
+ loadPetAndCustomer(val)
|
|
|
+}, { immediate: true, deep: true })
|
|
|
+
|
|
|
+const activeDetailTab = ref('basic')
|
|
|
+
|
|
|
+const getStatusName = (status) => {
|
|
|
+ const map = { 0: '待派单', 1: '待接单', 2: '服务中', 3: '待商家确认', 4: '已完成', 5: '已取消' }
|
|
|
+ return map[status] || '未知'
|
|
|
+}
|
|
|
+const getStatusTag = (status) => {
|
|
|
+ const map = { 0: 'danger', 1: 'warning', 2: 'primary', 3: 'warning', 4: 'success', 5: 'info' }
|
|
|
+ return map[status] || 'info'
|
|
|
+}
|
|
|
+const getTypeName = (type) => {
|
|
|
+ const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
|
|
|
+ return map[type]
|
|
|
+}
|
|
|
+const getTransportModeName = (type) => {
|
|
|
+ const map = { round: '往返接送', pick: '单程接(到店)', drop: '单程送(回家)' }
|
|
|
+ return map[type] || '接送服务'
|
|
|
+}
|
|
|
+
|
|
|
+const getTransportLabel = (t) => {
|
|
|
+ if (t === 0 || t === '0') return '接'
|
|
|
+ if (t === 1 || t === '1') return '送'
|
|
|
+ if (t === 2 || t === '2') return '单程接'
|
|
|
+ if (t === 3 || t === '3') return '单程送'
|
|
|
+ return '接送'
|
|
|
+}
|
|
|
+const getServiceTimeRange = (timeStr) => {
|
|
|
+ if (!timeStr) return '--'
|
|
|
+ try {
|
|
|
+ if (timeStr.length < 16) return timeStr
|
|
|
+ let timePart = timeStr.substring(11, 16)
|
|
|
+ let [hh, mm] = timePart.split(':').map(Number)
|
|
|
+ let endH = hh + 2
|
|
|
+ if (endH >= 24) endH -= 24
|
|
|
+ let endHStr = endH.toString().padStart(2, '0')
|
|
|
+ return `${timeStr}-${endHStr}:${mm.toString().padStart(2, '0')}`
|
|
|
+ } catch (e) {
|
|
|
+ return timeStr
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const currentOrderSteps = computed(() => {
|
|
|
+ if (!props.order) return { active: 0, steps: [] }
|
|
|
+ const steps = [
|
|
|
+ { title: '商户下单', status: 'created', time: '' },
|
|
|
+ { title: '运营派单', status: 'dispatched', time: '' },
|
|
|
+ { title: '履约接单', status: 'accepted', time: '' },
|
|
|
+ { title: '服务中', status: 'serving', time: '' },
|
|
|
+ { title: '待商家确认', status: 'confirming', time: '' },
|
|
|
+ { title: '已完成', status: 'completed', time: '' }
|
|
|
+ ]
|
|
|
+ const logs = props.order.orderLogs || []
|
|
|
+ const status = props.order.status
|
|
|
+ let active = 0
|
|
|
+ const findTime = (keyword) => {
|
|
|
+ const log = logs.find(l => l.title.includes(keyword) || l.content.includes(keyword))
|
|
|
+ return log ? log.time : ''
|
|
|
+ }
|
|
|
+ steps[0].time = props.order.createTime || findTime('下单') || findTime('创建')
|
|
|
+ if (steps[0].time) active = 1
|
|
|
+ if ([0].includes(status)) {
|
|
|
+ steps[1].time = findTime('派单') || steps[0].time
|
|
|
+ } else {
|
|
|
+ steps[1].time = findTime('派单') || ''
|
|
|
+ }
|
|
|
+ if ([1, 2, 3, 4].includes(status)) active = 2
|
|
|
+ steps[2].time = findTime('接单')
|
|
|
+ if ([1].includes(status)) {
|
|
|
+ steps[2].title = '待履约者接单'
|
|
|
+ } else if ([2, 3, 4].includes(status)) {
|
|
|
+ steps[2].title = '履约者已接单'
|
|
|
+ active = 3
|
|
|
+ }
|
|
|
+ steps[3].time = findTime('到达') || findTime('出发')
|
|
|
+ if ([2].includes(status)) {
|
|
|
+ steps[3].title = '服务进行中'
|
|
|
+ } else if ([3, 4].includes(status)) {
|
|
|
+ steps[3].title = '服务已完成'
|
|
|
+ active = 4
|
|
|
+ }
|
|
|
+ steps[4].time = findTime('等待商家确认') || findTime('待验收')
|
|
|
+ if ([3].includes(status)) {
|
|
|
+ steps[4].title = '待商家确认'
|
|
|
+ } else if ([4].includes(status)) {
|
|
|
+ steps[4].title = '商家已确认'
|
|
|
+ active = 5
|
|
|
+ }
|
|
|
+ if (status === 4) {
|
|
|
+ steps[5].time = findTime('完成')
|
|
|
+ active = 6
|
|
|
+ }
|
|
|
+ if (status === 5) {
|
|
|
+ return {
|
|
|
+ active: 1,
|
|
|
+ steps: [
|
|
|
+ { title: '商户下单', time: steps[0].time },
|
|
|
+ { title: '已取消', time: findTime('取消') || '订单已取消' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { active, steps }
|
|
|
+})
|
|
|
+
|
|
|
+const serviceProgressSteps = computed(() => {
|
|
|
+ const order = props.order
|
|
|
+ if (!order) return []
|
|
|
+ if ([0, 1, 5].includes(order.status)) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+ const baseTime = order.serviceTime || '2024-02-10 10:00'
|
|
|
+ const datePart = baseTime.split(' ')[0]
|
|
|
+ const isTransport = order.type === 'transport'
|
|
|
+ let steps = []
|
|
|
+ steps.push({
|
|
|
+ title: '已接单', time: `${datePart} 09:30`, icon: 'Bicycle', color: '#ff9900',
|
|
|
+ desc: `履约者 ${order.fulfillerName || '当前履约者'} 已确认接单,准备前往服务地点`, media: []
|
|
|
+ })
|
|
|
+ steps.push({
|
|
|
+ title: '到达打卡', time: `${datePart} 09:50`, icon: 'Location', color: '#ff9900',
|
|
|
+ desc: '已到达指定位置,打卡确认', media: [{ type: 'image', url: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg' }]
|
|
|
+ })
|
|
|
+ if (isTransport) {
|
|
|
+ steps.push({
|
|
|
+ title: '确认出发', time: `${datePart} 10:10`, icon: 'Van', color: '#ff9900',
|
|
|
+ desc: '接到宠物,状态良好,开始运输。备注:宠物很乖,已放入航空箱。',
|
|
|
+ media: [
|
|
|
+ { type: 'image', url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg' },
|
|
|
+ { type: 'image', url: 'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg' }
|
|
|
+ ]
|
|
|
+ })
|
|
|
+ if ([3, 4].includes(order.status)) {
|
|
|
+ steps.push({
|
|
|
+ title: '送达打卡', time: `${datePart} 10:50`, icon: 'Place', color: '#ff9900',
|
|
|
+ desc: '宠物已安全送达目的地,等待商家验收',
|
|
|
+ media: [{ type: 'image', url: 'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg' }]
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ steps.push({
|
|
|
+ title: '开始服务', time: `${datePart} 10:00`, icon: 'VideoPlay', color: '#ff9900',
|
|
|
+ desc: '已确认宠物状态,开始进行服务视频录制',
|
|
|
+ media: [{ type: 'image', url: 'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg' }]
|
|
|
+ })
|
|
|
+ if ([3, 4].includes(order.status)) {
|
|
|
+ steps.push({
|
|
|
+ title: '服务结束', time: `${datePart} 10:50`, icon: 'VideoPause', color: '#ff9900',
|
|
|
+ desc: '服务项目已全部完成,清理现场完毕。备注:狗狗今天很配合,完成了梳毛和喂食。',
|
|
|
+ media: [
|
|
|
+ { type: 'image', url: 'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg' },
|
|
|
+ { type: 'image', url: 'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg' }
|
|
|
+ ]
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ([3, 4].includes(order.status)) {
|
|
|
+ steps.push({ title: '待商家确认', time: `${datePart} 10:55`, icon: 'Clock', color: '#ff9900', desc: '履约者已提交完成信息,等待商家确认订单', media: [] })
|
|
|
+ }
|
|
|
+ if (order.status === 4) {
|
|
|
+ steps.push({ title: '订单完成', time: `${datePart} 11:00`, icon: 'Select', color: '#67C23A', desc: '用户/商家已确认,服务圆满结束', media: [] })
|
|
|
+ }
|
|
|
+ return steps
|
|
|
+})
|
|
|
+
|
|
|
+const handleExportLogs = () => {
|
|
|
+ const logs = props.order.orderLogs || []
|
|
|
+ if (logs.length === 0) {
|
|
|
+ ElMessage.warning('暂无日志可导出')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let csvContent = "时间,类型,标题,内容\n"
|
|
|
+ logs.forEach(log => {
|
|
|
+ const time = log.time || ''
|
|
|
+ const type = log.type || ''
|
|
|
+ const title = (log.title || '').replace(/"/g, '""')
|
|
|
+ const content = (log.content || '').replace(/"/g, '""')
|
|
|
+ csvContent += `${time},${type},"${title}","${content}"\n`
|
|
|
+ })
|
|
|
+ const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' })
|
|
|
+ const url = URL.createObjectURL(blob)
|
|
|
+ const link = document.createElement("a")
|
|
|
+ link.href = url
|
|
|
+ link.download = `OrderLogs_${props.order.orderNo}.csv`
|
|
|
+ link.click()
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
+ ElMessage.success('导出成功')
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* Detail Styles */
|
|
|
+.order-detail-drawer :deep(.el-drawer__body) {
|
|
|
+ padding: 0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-container {
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background: #f5f7fa;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-header {
|
|
|
+ background: #fff;
|
|
|
+ padding: 20px 24px;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.left-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.order-no {
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.type-tag {
|
|
|
+ font-weight: normal;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-scroll-area {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 20px 24px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Progress */
|
|
|
+.progress-section {
|
|
|
+ background: #fff;
|
|
|
+ padding: 30px 20px 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.custom-steps :deep(.el-step__title) {
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Top Info Row */
|
|
|
+.top-info-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ align-items: stretch;
|
|
|
+}
|
|
|
+
|
|
|
+.info-section {
|
|
|
+ flex: 1;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 15px;
|
|
|
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.sec-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ border-bottom: 1px solid #f2f2f2;
|
|
|
+}
|
|
|
+
|
|
|
+.sec-header .label {
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 15px;
|
|
|
+ color: #303133;
|
|
|
+ border-left: 3px solid #409eff;
|
|
|
+ padding-left: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Pet Section */
|
|
|
+.pet-basic-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 15px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.pet-avatar-lg {
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #ecf5ff;
|
|
|
+ color: #409eff;
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.pet-names {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.b-name {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: bold;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.b-tags {
|
|
|
+ display: flex;
|
|
|
+ gap: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.pet-desc :deep(.el-descriptions__label) {
|
|
|
+ width: 70px;
|
|
|
+}
|
|
|
+
|
|
|
+/* User Section */
|
|
|
+.u-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.u-info .nm {
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 15px;
|
|
|
+ color: #303133;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.u-info .ph {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.addr-box {
|
|
|
+ background: #fdf6ec;
|
|
|
+ padding: 8px 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.addr-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #e6a23c;
|
|
|
+ margin-bottom: 2px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.addr-txt {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+/* Tabs */
|
|
|
+.detail-tabs {
|
|
|
+ background: #fff;
|
|
|
+ padding: 10px 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
|
|
+ min-height: 400px;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-pane-content {
|
|
|
+ padding: 15px 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* Fulfiller Card inside Tab */
|
|
|
+.fulfiller-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20px;
|
|
|
+ padding: 20px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.f-right {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.f-row1 {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.f-name {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.f-row2 {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.sep {
|
|
|
+ color: #e4e7ed;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-state {
|
|
|
+ padding: 40px 0;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+/* Progress Card Styles */
|
|
|
+.progress-card {
|
|
|
+ background: #f8fcfb;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 12px;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+}
|
|
|
+
|
|
|
+.p-title {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.p-desc {
|
|
|
+ margin: 0 0 12px;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.p-media {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.media-item {
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+.p-img {
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+/* New Transport Split Styles */
|
|
|
+.transport-split-block {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-grid {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-card {
|
|
|
+ flex: 1;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 6px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
|
|
|
+}
|
|
|
+
|
|
|
+.transport-card .t-header {
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding: 10px 15px;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-card .t-header .time {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-card .t-body {
|
|
|
+ padding: 15px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-card .row {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-card .row.sub {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 13px;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-card .row .el-icon {
|
|
|
+ margin-top: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-one {
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
|
|
|
+ padding: 14px 16px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-one .t-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-one .t-row .time {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-one .t-k {
|
|
|
+ width: 40px;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-one .t-v {
|
|
|
+ color: #303133;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+.transport-one .t-row.sub .t-v {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Logs */
|
|
|
+.log-card {
|
|
|
+ background: #f4f4f5;
|
|
|
+ padding: 10px 15px;
|
|
|
+ border-radius: 4px;
|
|
|
+ position: relative;
|
|
|
+ top: -5px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.l-tit {
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 14px;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.l-txt {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+</style>
|