|
|
@@ -0,0 +1,2108 @@
|
|
|
+<template>
|
|
|
+ <div class="page-container">
|
|
|
+ <el-card shadow="never" class="table-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span class="title">订单列表</span>
|
|
|
+ <div class="right-panel">
|
|
|
+ <el-radio-group v-model="filters.orderType" size="default" @change="handleSearch">
|
|
|
+ <el-radio-button label="">全部类型</el-radio-button>
|
|
|
+ <el-radio-button label="transport">宠物接送</el-radio-button>
|
|
|
+ <el-radio-button label="feeding">上门喂遛</el-radio-button>
|
|
|
+ <el-radio-button label="washing">上门洗护</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ <el-input
|
|
|
+ v-model="filters.keyword"
|
|
|
+ placeholder="订单号/商户/宠主/手机号"
|
|
|
+ class="search-input"
|
|
|
+ prefix-icon="Search"
|
|
|
+ clearable
|
|
|
+ @clear="handleSearch"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ <el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-tabs v-model="filters.status" class="status-tabs" @tab-click="handleSearch">
|
|
|
+ <el-tab-pane label="全部订单" name="" />
|
|
|
+ <el-tab-pane label="待派单" name="pending_dispatch" />
|
|
|
+ <el-tab-pane label="待接单" name="pending_accept" />
|
|
|
+ <el-tab-pane label="服务中" name="serving" />
|
|
|
+ <el-tab-pane label="待商家确认" name="pending_confirm" />
|
|
|
+ <el-tab-pane label="已完成" name="completed" />
|
|
|
+ <el-tab-pane label="已取消" name="cancelled" />
|
|
|
+ </el-tabs>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-table :data="tableData" style="width: 100%" v-loading="loading" :header-cell-style="{ background: '#f5f7fa' }">
|
|
|
+ <el-table-column prop="orderNo" label="订单号" width="170" fixed="left" />
|
|
|
+
|
|
|
+ <el-table-column label="服务类型" width="190">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="service-type-cell">
|
|
|
+ <el-tag :type="getTypeTag(row.type)">{{ getTypeName(row.type) }}</el-tag>
|
|
|
+ <!-- 接送细分标签 -->
|
|
|
+ <el-tag v-if="row.type === 'transport' && row.transportType === 'round'" size="small" effect="plain" type="warning" class="sub-tag">往返</el-tag>
|
|
|
+ <!-- Split Tags -->
|
|
|
+ <el-tag v-if="row.splitType === 'pick'" size="small" effect="dark" color="#409eff" style="border:none; color:white;" class="sub-tag">接</el-tag>
|
|
|
+ <el-tag v-if="row.splitType === 'drop'" size="small" effect="dark" color="#67c23a" style="border:none; color:white;" class="sub-tag">送</el-tag>
|
|
|
+
|
|
|
+ <el-tag v-if="row.type === 'transport' && row.transportType === 'pick' && !row.splitType" size="small" effect="plain" class="sub-tag">单程接</el-tag>
|
|
|
+ <el-tag v-if="row.type === 'transport' && row.transportType === 'drop' && !row.splitType" size="small" effect="plain" type="success" class="sub-tag">单程送</el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="宠物信息" min-width="150">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="pet-info">
|
|
|
+ <el-avatar :size="30" :src="row.petAvatar" :class="'avatar-' + row.type">{{ row.petName?.charAt(0) }}</el-avatar>
|
|
|
+ <div class="pet-detail">
|
|
|
+ <div class="pet-name">
|
|
|
+ {{ row.petName }}
|
|
|
+ <span class="pet-breed">{{ row.petBreed }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="所属用户" width="120" prop="userName">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span style="font-weight: 500">{{ row.userName }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="城市/区域" width="140">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div>{{ row.city }}</div>
|
|
|
+ <div class="sub-text">{{ row.district }}</div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="接单门店/下单人" min-width="160">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="merchant-info">
|
|
|
+ <div>{{ row.merchantName }}</div>
|
|
|
+ <div class="sub-text">{{ row.contactPhone }}</div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column prop="createTime" label="下单时间" width="165" sortable>
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span class="time-text">{{ row.createTime }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column prop="serviceTime" label="预约服务时间" width="165" sortable>
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span class="time-text">{{ row.serviceTime }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="订单状态" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="status-cell">
|
|
|
+ <div class="status-dot" :class="row.status"></div>
|
|
|
+ <span>{{ getStatusName(row.status) }}</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="履约信息" width="140">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div v-if="row.fulfillerName" class="fulfiller-info">
|
|
|
+ <span class="fulfiller-name">{{ row.fulfillerName }}</span>
|
|
|
+ <span class="fulfiller-fee" v-if="row.fulfillerFee">¥{{ row.fulfillerFee }}</span>
|
|
|
+ </div>
|
|
|
+ <span v-else class="text-gray">暂未指派</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="操作" width="200" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="op-cell">
|
|
|
+ <el-button link type="primary" size="small" @click="handleDetail(row)">详情</el-button>
|
|
|
+ <el-button v-if="['pending_dispatch', 'pending_accept'].includes(row.status)" link type="danger" size="small" @click="handleCancel(row)">取消</el-button>
|
|
|
+
|
|
|
+ <el-dropdown v-if="['serving', 'pending_confirm', 'completed'].includes(row.status)" trigger="click" @command="(cmd) => handleCommand(cmd, row)">
|
|
|
+ <span class="el-dropdown-link">
|
|
|
+ 更多<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
|
|
+ </span>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item v-if="row.status === 'pending_confirm'" command="complete">确认完成</el-dropdown-item>
|
|
|
+ <el-dropdown-item v-if="['pending_confirm', 'completed'].includes(row.status)" command="care_summary">护理小结</el-dropdown-item>
|
|
|
+ <el-dropdown-item command="reward">奖惩</el-dropdown-item>
|
|
|
+ <el-dropdown-item command="remark">备注</el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div class="pagination-container">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pagination.current"
|
|
|
+ v-model:page-size="pagination.size"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ :total="pagination.total"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handleCurrentChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 订单详情侧滑栏 -->
|
|
|
+ <el-drawer
|
|
|
+ v-model="detailVisible"
|
|
|
+ title="订单详情"
|
|
|
+ direction="rtl"
|
|
|
+ size="60%"
|
|
|
+ class="order-detail-drawer"
|
|
|
+ >
|
|
|
+ <div class="detail-container" v-if="currentOrder">
|
|
|
+ <!-- 1. Header Status -->
|
|
|
+ <!-- 1. Header Status -->
|
|
|
+ <div class="detail-header">
|
|
|
+ <div class="left-head">
|
|
|
+ <span class="order-no">{{ currentOrder.orderNo }}</span>
|
|
|
+ <el-tag :type="getStatusTag(currentOrder.status)" effect="dark" class="status-tag">{{ getStatusName(currentOrder.status) }}</el-tag>
|
|
|
+ <el-tag effect="plain" class="type-tag" :type="currentOrder.type === 'transport' ? '' : (currentOrder.type === 'feeding' ? 'warning' : 'danger')">
|
|
|
+ {{ getTypeName(currentOrder.type) }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="right-head">
|
|
|
+ <!-- Action Buttons Group -->
|
|
|
+ <div class="detail-actions">
|
|
|
+ <template v-if="currentOrder.status === 'pending_dispatch'">
|
|
|
+ <el-button type="danger" plain icon="CircleClose" @click="handleCancel(currentOrder)">取消订单</el-button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="currentOrder.status === 'pending_confirm'">
|
|
|
+ <el-button type="primary" icon="CircleCheck" @click="handleCommand('complete', currentOrder)">确认完成</el-button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="['pending_confirm', 'completed'].includes(currentOrder.status)">
|
|
|
+ <el-button icon="Notebook" @click="openCareSummary(currentOrder)">护理小结</el-button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, currentOrder)" 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="['cancelled', 'completed'].includes(currentOrder.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 (Enhanced) -->
|
|
|
+ <div class="info-section pet-section">
|
|
|
+ <div class="sec-header">
|
|
|
+ <span class="label">宠物档案</span>
|
|
|
+ <el-tag size="small" effect="plain">{{ currentOrder.petBreed }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="pet-basic-row">
|
|
|
+ <el-avatar :size="50" :src="currentOrder.petAvatar" shape="square" class="pet-avatar-lg">{{ (currentOrder.petName||'').charAt(0) }}</el-avatar>
|
|
|
+ <div class="pet-names">
|
|
|
+ <div class="b-name">{{ currentOrder.petName }}
|
|
|
+ <el-icon v-if="currentOrder.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">{{ currentOrder.petAge || '未知年龄' }}</el-tag>
|
|
|
+ <el-tag size="small" type="info">{{ currentOrder.petWeight || '未知体重' }}</el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-descriptions :column="2" size="small" class="pet-desc" border>
|
|
|
+ <el-descriptions-item label="绝育状态">{{ currentOrder.petSterilized ? '已绝育' : '未绝育' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ currentOrder.petVaccine || '未知' }}</span></el-descriptions-item>
|
|
|
+ <el-descriptions-item label="性格特点">{{ currentOrder.petCharacter || '温顺' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="健康状况">{{ currentOrder.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="currentOrder.userAvatar">{{ (currentOrder.userName||'').charAt(0) }}</el-avatar>
|
|
|
+ <div class="u-info">
|
|
|
+ <div class="nm">{{ currentOrder.userName }}</div>
|
|
|
+ <div class="ph">{{ currentOrder.contactPhone }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="addr-box">
|
|
|
+ <div class="addr-label">服务地址</div>
|
|
|
+ <div class="addr-txt">{{ currentOrder.city }}{{ currentOrder.district }} {{ currentOrder.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">
|
|
|
+ <!-- A. General Order Info -->
|
|
|
+ <div class="section-block">
|
|
|
+ <div class="sec-title-bar">基础业务信息</div>
|
|
|
+ <el-descriptions :column="3" border size="default" class="custom-desc">
|
|
|
+ <el-descriptions-item label="系统单号">{{ currentOrder.orderNo }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="服务类型">
|
|
|
+ {{ getTypeName(currentOrder.type) }}
|
|
|
+ <el-tag size="small" v-if="currentOrder.type==='transport'" style="margin-left:5px" effect="light">{{ getTransportModeName(currentOrder.transportType) }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="归属门店">{{ currentOrder.merchantName }} (平台代下单)</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="宠主信息">{{ currentOrder.userName }} / {{ currentOrder.contactPhone }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="服务费用" label-class-name="money-label">
|
|
|
+ <span style="color:#f56c6c; font-weight:bold;">¥ {{ currentOrder.fulfillerFee }}</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="预约时间">{{ getServiceTimeRange(currentOrder.serviceTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="团购套餐">{{ currentOrder.groupBuyPackage || '未使用团购套餐' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="创建时间">{{ currentOrder.createTime }}</el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="订单备注" :span="3">
|
|
|
+ {{ currentOrder.remark || '暂无备注' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- B. Transport Specifics (Single Display) -->
|
|
|
+ <div v-if="currentOrder.type === 'transport'" class="section-block transport-split-block">
|
|
|
+ <div class="sec-title-bar">接送任务详情</div>
|
|
|
+ <div class="transport-grid">
|
|
|
+ <!-- Pick Up Info (Only if pick or round) -->
|
|
|
+ <div class="transport-card pick-card" v-if="['round', 'pick'].includes(currentOrder.transportType)">
|
|
|
+ <div class="t-header">
|
|
|
+ <div class="left-badges">
|
|
|
+ <el-tag :type="currentOrder.type === 'feeding' ? 'warning' : ''" effect="dark">{{ getTypeName(currentOrder.type) }}</el-tag>
|
|
|
+ <el-tag size="small" effect="plain" class="sub-badge">接</el-tag>
|
|
|
+ </div>
|
|
|
+ <span class="time">{{ currentOrder.detail.pickTime || currentOrder.serviceTime }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="t-body">
|
|
|
+ <div class="row"><el-icon><Location /></el-icon> <span class="addr">{{ currentOrder.detail.pickAddr }}</span></div>
|
|
|
+ <div class="row sub"><el-icon><User /></el-icon> {{ currentOrder.detail.pickContact || currentOrder.userName }} <el-icon><Phone /></el-icon> {{ currentOrder.detail.pickPhone || currentOrder.contactPhone }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- Drop Off Info (Only if drop or round) -->
|
|
|
+ <div class="transport-card drop-card" v-if="['round', 'drop'].includes(currentOrder.transportType)">
|
|
|
+ <div class="t-header">
|
|
|
+ <div class="left-badges">
|
|
|
+ <el-tag :type="currentOrder.type === 'feeding' ? 'warning' : 'success'" effect="dark">{{ getTypeName(currentOrder.type) }}</el-tag>
|
|
|
+ <el-tag size="small" effect="plain" class="sub-badge">送</el-tag>
|
|
|
+ </div>
|
|
|
+ <span class="time">{{ currentOrder.detail.dropTime || '待定' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="t-body">
|
|
|
+ <div class="row"><el-icon><Location /></el-icon> <span class="addr">{{ currentOrder.detail.dropAddr }}</span></div>
|
|
|
+ <div class="row sub"><el-icon><User /></el-icon> {{ currentOrder.detail.dropContact || currentOrder.userName }} <el-icon><Phone /></el-icon> {{ currentOrder.detail.dropPhone || currentOrder.contactPhone }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- C. Other Service Specifics -->
|
|
|
+ <div v-if="['feeding', 'washing'].includes(currentOrder.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">{{ currentOrder.detail.area || currentOrder.address }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="服务套餐">{{ currentOrder.detail.packageName }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="特殊要求">{{ currentOrder.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="currentOrder.fulfillerName" class="fulfiller-card">
|
|
|
+ <div class="f-left">
|
|
|
+ <el-avatar :size="60" :src="currentOrder.fulfillerAvatar">{{ currentOrder.fulfillerName.charAt(0) }}</el-avatar>
|
|
|
+ </div>
|
|
|
+ <div class="f-right">
|
|
|
+ <div class="f-row1">
|
|
|
+ <span class="f-name">{{ currentOrder.fulfillerName }}</span>
|
|
|
+ <el-tag size="small" type="primary" effect="plain" round>Lv1 普通</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="f-row2">
|
|
|
+ <span>联系电话:{{ currentOrder.fulfillerPhone || '138****0000' }}</span>
|
|
|
+ <span class="sep">|</span>
|
|
|
+ <span>归属区域:{{ currentOrder.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>{{ currentOrder.createTime }}</span>
|
|
|
+ <span><span style="color:#909399;">接单时间:</span>{{ currentOrder.detail?.receiveTime || currentOrder.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">
|
|
|
+ <!-- Empty State for Pending Accept -->
|
|
|
+ <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 (currentOrder.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>
|
|
|
+
|
|
|
+ <!-- Fallback Legacy Mock -->
|
|
|
+ <el-timeline-item
|
|
|
+ v-if="(!currentOrder.orderLogs || currentOrder.orderLogs.length === 0) && currentOrder.timeline"
|
|
|
+ v-for="(log, idx) in currentOrder.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>
|
|
|
+
|
|
|
+ <!-- 派单弹窗 (Enhanced) -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="dispatchDialogVisible"
|
|
|
+ title="派单调度"
|
|
|
+ width="900px"
|
|
|
+ top="5vh"
|
|
|
+ destroy-on-close
|
|
|
+ append-to-body
|
|
|
+ >
|
|
|
+ <div class="dispatch-dialog-content">
|
|
|
+ <!-- Top: Order Info (OrderDispatch Style) -->
|
|
|
+ <div class="dispatch-order-info" v-if="currentDispatchOrder">
|
|
|
+ <div class="list-card order-card" style="margin:0; box-shadow:none; cursor:default; border:none;">
|
|
|
+ <div class="card-left">
|
|
|
+ <div class="type-tag" :class="currentDispatchOrder.typeCode">
|
|
|
+ {{ getShortType(currentDispatchOrder.typeCode) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card-main">
|
|
|
+ <template v-if="currentDispatchOrder.typeCode === 'transport'">
|
|
|
+ <div class="row-addr" v-if="['round', 'pick'].includes(currentDispatchOrder.transportType)" :title="currentDispatchOrder.pickAddr">
|
|
|
+ <span class="tag pick">取</span> {{ currentDispatchOrder.pickAddr }}
|
|
|
+ </div>
|
|
|
+ <div class="row-addr" v-if="['round', 'drop'].includes(currentDispatchOrder.transportType)" :title="currentDispatchOrder.dropAddr">
|
|
|
+ <span class="tag drop">送</span> {{ currentDispatchOrder.dropAddr }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="row-addr" :title="currentDispatchOrder.address">
|
|
|
+ <span class="tag home">址</span> {{ currentDispatchOrder.address }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="row-time" style="margin-top: 4px;">
|
|
|
+ <el-icon><Clock /></el-icon> {{ currentDispatchOrder.time }}
|
|
|
+ <span class="days-tag" v-if="currentDispatchOrder.daysLater">{{ currentDispatchOrder.daysLater }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Current Rider Info (If Exists) -->
|
|
|
+ <div class="current-rider-section" v-if="currentRider">
|
|
|
+ <div class="select-header" style="margin-bottom:8px;">
|
|
|
+ <span class="tit">当前派单履约者</span>
|
|
|
+ </div>
|
|
|
+ <div class="list-card rider-card" style="margin-bottom: 20px; border: 1px solid #e4e7ed; background:#fafafa; cursor:default;">
|
|
|
+ <div class="card-left relative">
|
|
|
+ <el-avatar :src="currentRider.avatar" :size="40" />
|
|
|
+ <div class="dot" :class="currentRider.status"></div>
|
|
|
+ </div>
|
|
|
+ <div class="card-main">
|
|
|
+ <div class="row-1" style="justify-content: space-between; align-items: flex-start; display: flex;">
|
|
|
+ <div style="display:flex; align-items:baseline; gap:8px;">
|
|
|
+ <span class="r-name">{{ currentRider.name }}</span>
|
|
|
+ <span class="r-phone">{{ currentRider.maskPhone }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="status-right">
|
|
|
+ <span class="status-badge" :class="currentRider.status">{{ getRiderStatusText(currentRider.status) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="row-2 categories-row" style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
|
|
|
+ <span v-for="cat in currentRider.categories" :key="cat" class="cat-tag" :class="getCategoryClass(cat)">{{ cat }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="row-3 time-row" style="margin-top: 4px;">
|
|
|
+ <span class="last-time">下一单: {{ (currentRider.status === 'offline' || currentRider.status === 'disabled') ? '--' : currentRider.lastServiceTime }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Middle: Rider Selection -->
|
|
|
+ <div class="dispatch-rider-select">
|
|
|
+ <div class="select-header">
|
|
|
+ <span class="tit">选择履约者 (下一单时间由近及远排序)</span>
|
|
|
+ <el-input
|
|
|
+ v-model="dispatchSearchQuery"
|
|
|
+ placeholder="搜索履约者姓名/手机号"
|
|
|
+ prefix-icon="Search"
|
|
|
+ clearable
|
|
|
+ style="width: 240px"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="rider-grid-wrapper">
|
|
|
+ <el-scrollbar height="400px">
|
|
|
+ <div class="rider-grid">
|
|
|
+ <div
|
|
|
+ v-for="rider in filteredDispatchRiders"
|
|
|
+ :key="rider.id"
|
|
|
+ class="list-card rider-card select-card"
|
|
|
+ :class="{active: selectedRiderId === rider.id}"
|
|
|
+ @click="selectedRiderId = rider.id"
|
|
|
+ >
|
|
|
+ <!-- Reusing Rider Card Layout -->
|
|
|
+ <div class="card-left relative">
|
|
|
+ <el-avatar :src="rider.avatar" :size="40" />
|
|
|
+ <div class="dot" :class="rider.status"></div>
|
|
|
+ </div>
|
|
|
+ <div class="card-main">
|
|
|
+ <div class="row-1" style="justify-content: space-between; align-items: flex-start; display: flex;">
|
|
|
+ <div style="display:flex; align-items:baseline; gap:8px;">
|
|
|
+ <span class="r-name">{{ rider.name }}</span>
|
|
|
+ <span class="r-phone">{{ rider.maskPhone }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="status-right">
|
|
|
+ <span class="status-badge" :class="rider.status">{{ getRiderStatusText(rider.status) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="row-2 categories-row" style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
|
|
|
+ <span v-for="cat in rider.categories" :key="cat" class="cat-tag" :class="getCategoryClass(cat)">{{ cat }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="row-3 time-row" style="margin-top: 4px;">
|
|
|
+ <span class="last-time">下一单: {{ (rider.status === 'offline' || rider.status === 'disabled') ? '--' : rider.lastServiceTime }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Selected Check -->
|
|
|
+ <div class="selected-mark" v-if="selectedRiderId === rider.id">
|
|
|
+ <el-icon><Check /></el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="filteredDispatchRiders.length === 0" class="empty-text">暂无符合条件的履约者</div>
|
|
|
+ </div>
|
|
|
+ </el-scrollbar>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Bottom: Fee & Submit -->
|
|
|
+ <div class="dispatch-footer">
|
|
|
+ <div class="fee-input">
|
|
|
+ <span class="label">服务费用:</span>
|
|
|
+ <el-input-number v-model="dispatchFee" :min="0" :precision="2" :step="10" placeholder="请输入" style="width: 140px;" />
|
|
|
+ <span class="unit">元</span>
|
|
|
+ </div>
|
|
|
+ <div class="btns">
|
|
|
+ <el-button @click="dispatchDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleDispatchSubmit">确认派单</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 护理小结侧滑栏 -->
|
|
|
+ <el-drawer
|
|
|
+ v-model="careSummaryVisible"
|
|
|
+ title="宠物护理工作小结"
|
|
|
+ direction="rtl"
|
|
|
+ size="750px"
|
|
|
+ destroy-on-close
|
|
|
+ class="care-summary-drawer"
|
|
|
+ >
|
|
|
+ <div class="care-summary-container" v-if="careSummaryOrder">
|
|
|
+ <!-- Pet Header -->
|
|
|
+ <div class="summary-header">
|
|
|
+ <div class="avatar-wrapper">
|
|
|
+ <el-avatar :size="80" :src="careSummaryOrder.petAvatar" shape="circle" class="pet-summary-avatar">{{ careSummaryOrder.petName?.charAt(0) }}</el-avatar>
|
|
|
+ </div>
|
|
|
+ <div class="pet-summary-info">
|
|
|
+ <div class="summary-name-row">
|
|
|
+ <span class="name">{{ careSummaryOrder.petName }}</span>
|
|
|
+ <div class="tags-group">
|
|
|
+ <el-tag :type="careSummaryOrder.petGender==='male'?'':'danger'" effect="light" round>
|
|
|
+ <el-icon><component :is="careSummaryOrder.petGender==='male'?'Male':'Female'" /></el-icon>
|
|
|
+ {{ careSummaryOrder.petAge }}
|
|
|
+ </el-tag>
|
|
|
+ <el-tag v-for="tag in (careSummaryOrder.petTags||[])" :key="tag" type="warning" effect="plain" round>{{ tag }}</el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="summary-sub-row">
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="lbl">品种</span>
|
|
|
+ <span class="val">{{ careSummaryOrder.petBreed || '未知' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="divider-v"></div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="lbl">体重</span>
|
|
|
+ <span class="val">{{ careSummaryOrder.petWeight }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="divider-v"></div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="lbl">主人</span>
|
|
|
+ <span class="val">{{ careSummaryOrder.userName || '未知' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Info Groups -->
|
|
|
+ <div class="summary-section">
|
|
|
+ <div class="sec-title">
|
|
|
+ <span class="icon-box"><el-icon><List /></el-icon></span>
|
|
|
+ 基本信息
|
|
|
+ </div>
|
|
|
+ <el-descriptions :column="2" border class="spacious-desc">
|
|
|
+ <el-descriptions-item label="性格关键词">{{ careSummaryOrder.petPersonality }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="健康状况">
|
|
|
+ <el-tag :type="careSummaryOrder.healthStatus==='健康'?'success':'danger'" effect="light" size="small">{{ careSummaryOrder.healthStatus }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="疫苗情况">
|
|
|
+ <div class="flex-align">
|
|
|
+ <span style="color:#67c23a; margin-right:8px;" v-if="careSummaryOrder.vaccineImg"><el-icon><CircleCheckFilled /></el-icon> 已接种</span>
|
|
|
+ <span v-else style="color:#909399;">未接种</span>
|
|
|
+ <el-image
|
|
|
+ v-if="careSummaryOrder.vaccineImg"
|
|
|
+ style="width: 24px; height: 24px; border-radius:4px; vertical-align:middle; cursor:zoom-in;"
|
|
|
+ :src="careSummaryOrder.vaccineImg"
|
|
|
+ :preview-src-list="[careSummaryOrder.vaccineImg]"
|
|
|
+ :preview-teleported="true"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="过敏史">
|
|
|
+ <span :style="{color: careSummaryOrder.allergy ? '#f56c6c' : 'inherit'}">{{ careSummaryOrder.allergy || '无' }}</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="summary-section">
|
|
|
+ <div class="sec-title">
|
|
|
+ <span class="icon-box text-blue"><el-icon><HomeFilled /></el-icon></span>
|
|
|
+ 服务环境
|
|
|
+ </div>
|
|
|
+ <el-descriptions :column="2" border class="spacious-desc">
|
|
|
+ <el-descriptions-item label="到家时间">{{ careSummaryOrder.homeTime }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="房屋类型">{{ careSummaryOrder.houseType }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="入户方式" :span="2">
|
|
|
+ <span style="font-weight:bold;">{{ careSummaryOrder.entryMethod }}</span>
|
|
|
+ <span style="margin-left:8px; color:#909399;">({{ careSummaryOrder.entryDetail }})</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Service Log -->
|
|
|
+ <div class="summary-section main-log">
|
|
|
+ <div class="sec-title" style="border:none; padding-left:0; margin-bottom:16px;">
|
|
|
+ <div class="left">
|
|
|
+ <span class="icon-box text-orange"><el-icon><Notebook /></el-icon></span>
|
|
|
+ 服务内容记录
|
|
|
+ </div>
|
|
|
+ <el-button v-if="!isEditingSummary" type="primary" link icon="Edit" @click="isEditingSummary = true">编辑</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="isEditingSummary" class="edit-area">
|
|
|
+ <el-input
|
|
|
+ v-model="careSummaryText"
|
|
|
+ type="textarea"
|
|
|
+ :rows="12"
|
|
|
+ placeholder="请输入详细的护理服务小结..."
|
|
|
+ resize="none"
|
|
|
+ />
|
|
|
+ <div class="edit-actions">
|
|
|
+ <el-button @click="isEditingSummary = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="saveCareSummary">保存内容</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-else class="log-content-box">
|
|
|
+ <pre class="log-text">{{ careSummaryText }}</pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Footer Info -->
|
|
|
+ <div class="summary-footer">
|
|
|
+ <div class="footer-info">
|
|
|
+ <div class="f-row">
|
|
|
+ <span class="lbl">护宠师</span>
|
|
|
+ <span class="val user-active">{{ careSummaryOrder.fulfillerName || '当前履约者' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="f-row">
|
|
|
+ <span class="lbl">提交时间</span>
|
|
|
+ <span class="val">{{ careSummaryOrder.summaryTime || '2024-02-04 17:00' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="footer-action">
|
|
|
+ <el-button size="large" @click="careSummaryVisible = false">关闭</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <!-- 奖惩弹窗 -->
|
|
|
+ <el-dialog v-model="rewardDialogVisible" title="奖惩操作" width="500px">
|
|
|
+ <div v-if="currentOperateRow" style="padding: 0 10px;">
|
|
|
+ <div style="margin-bottom: 20px; font-size: 14px; color: #606266; line-height: 1.6; background: #fdf6ec; padding: 10px; border-radius: 4px;">
|
|
|
+ <div>奖惩履约者:<span style="font-weight: bold; color: #303133;">{{ currentOperateRow.fulfillerName || '未指派' }}</span></div>
|
|
|
+ <div style="font-size: 13px; margin-top: 4px;">订单号:{{ currentOperateRow.orderNo }}</div>
|
|
|
+ <div style="font-size: 13px; margin-top: 4px; display:flex; align-items:center; gap:6px;">
|
|
|
+ 服务类型:
|
|
|
+ <el-tag :type="getTypeTag(currentOperateRow.type)" size="small">{{ getTypeName(currentOperateRow.type) }}</el-tag>
|
|
|
+ <el-tag v-if="currentOperateRow.type === 'transport' && currentOperateRow.transportType === 'round'" size="small" effect="plain" type="warning">往返</el-tag>
|
|
|
+ <el-tag v-if="currentOperateRow.splitType === 'pick'" size="small" effect="dark" color="#409eff" style="border:none; color:white;">接</el-tag>
|
|
|
+ <el-tag v-if="currentOperateRow.splitType === 'drop'" size="small" effect="dark" color="#67c23a" style="border:none; color:white;">送</el-tag>
|
|
|
+ <el-tag v-if="currentOperateRow.type === 'transport' && currentOperateRow.transportType === 'pick' && !currentOperateRow.splitType" size="small" effect="plain">单程接</el-tag>
|
|
|
+ <el-tag v-if="currentOperateRow.type === 'transport' && currentOperateRow.transportType === 'drop' && !currentOperateRow.splitType" size="small" effect="plain" type="success">单程送</el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-form :model="rewardForm" label-width="80px">
|
|
|
+ <el-form-item label="操作类型">
|
|
|
+ <el-radio-group v-model="rewardForm.type">
|
|
|
+ <el-radio label="reward">奖励 (增加)</el-radio>
|
|
|
+ <el-radio label="punish">惩罚 (扣除)</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="调整项目">
|
|
|
+ <el-radio-group v-model="rewardForm.item">
|
|
|
+ <el-radio label="points">积分</el-radio>
|
|
|
+ <el-radio label="amount">金额 (元)</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="数额" required>
|
|
|
+ <el-input-number v-model="rewardForm.value" :min="1" :step="10" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="原因备注" required>
|
|
|
+ <el-input
|
|
|
+ v-model="rewardForm.reason"
|
|
|
+ type="textarea"
|
|
|
+ :rows="3"
|
|
|
+ placeholder="请输入奖惩原因..."
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="rewardDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleRewardSubmit">确认执行</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 备注弹窗 -->
|
|
|
+ <el-dialog v-model="remarkDialogVisible" title="订单备注" width="500px">
|
|
|
+ <div style="margin-bottom:10px; font-size:13px; color:#909399;">
|
|
|
+ <span v-if="currentOperateRow">订单号:{{ currentOperateRow.orderNo }}</span>
|
|
|
+ </div>
|
|
|
+ <el-input
|
|
|
+ v-model="remarkForm"
|
|
|
+ type="textarea"
|
|
|
+ :rows="5"
|
|
|
+ placeholder="请输入订单备注信息..."
|
|
|
+ />
|
|
|
+ <template #footer>
|
|
|
+ <span class="dialog-footer">
|
|
|
+ <el-button @click="remarkDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleRemarkSubmit">保存备注</el-button>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive, onMounted, computed } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const detailVisible = ref(false)
|
|
|
+const dispatchVisible = ref(false)
|
|
|
+const currentOrder = ref(null)
|
|
|
+
|
|
|
+const filters = reactive({
|
|
|
+ orderType: '',
|
|
|
+ status: '',
|
|
|
+ keyword: ''
|
|
|
+})
|
|
|
+
|
|
|
+const pagination = reactive({
|
|
|
+ current: 1,
|
|
|
+ size: 10,
|
|
|
+ total: 100
|
|
|
+})
|
|
|
+
|
|
|
+const dispatchForm = reactive({
|
|
|
+ orderId: null,
|
|
|
+ fulfillerId: null,
|
|
|
+ fee: 0,
|
|
|
+ remark: ''
|
|
|
+})
|
|
|
+
|
|
|
+// Mocks
|
|
|
+const fulfillerOptions = ref([
|
|
|
+ { id: 101, name: '王大力', distance: '1.2km' },
|
|
|
+ { id: 102, name: '张小美', distance: '3.5km' },
|
|
|
+ { id: 103, name: '李建国', distance: '0.8km' }
|
|
|
+])
|
|
|
+
|
|
|
+const tableData = ref([])
|
|
|
+
|
|
|
+const mockData = [
|
|
|
+ // 宠物接送 - 待派单 (往返)
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ orderNo: 'ORD202402048801',
|
|
|
+ type: 'transport',
|
|
|
+ transportType: 'round', // 往返
|
|
|
+ petName: '旺财',
|
|
|
+ petBreed: '金毛',
|
|
|
+ petAvatar: '',
|
|
|
+ userName: '李先生',
|
|
|
+ city: '北京市',
|
|
|
+ district: '朝阳区',
|
|
|
+ createTime: '2024-02-04 09:30',
|
|
|
+ merchantName: '爱宠生活馆 (三里屯店)',
|
|
|
+ contactPhone: '13812345678',
|
|
|
+ serviceTime: '2024-02-05 10:00',
|
|
|
+ status: 'pending_dispatch',
|
|
|
+ fulfillerName: '',
|
|
|
+ fulfillerFee: 0,
|
|
|
+ detail: {
|
|
|
+ pickAddr: '朝阳区三里屯SOHO B座',
|
|
|
+ pickContact: '张先生',
|
|
|
+ pickPhone: '138xxxx',
|
|
|
+ pickTime: '10:00',
|
|
|
+ dropAddr: '朝阳区某某宠物医院',
|
|
|
+ dropContact: '前台',
|
|
|
+ dropPhone: '010-xxxx',
|
|
|
+ },
|
|
|
+ timeline: [{ time: '2024-02-04 09:30', content: '商户下单成功', type: 'primary' }]
|
|
|
+ },
|
|
|
+ // 宠物接送 - 待接单 (单程接)
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ orderNo: 'ORD202402048802',
|
|
|
+ type: 'transport',
|
|
|
+ transportType: 'pick', // 单程接
|
|
|
+ petName: 'Bella',
|
|
|
+ petBreed: '拉布拉多',
|
|
|
+ petAvatar: '',
|
|
|
+ userName: '赵女士',
|
|
|
+ city: '北京市',
|
|
|
+ district: '海淀区',
|
|
|
+ createTime: '2024-02-04 10:00',
|
|
|
+ merchantName: '爱宠生活馆 (国贸店)',
|
|
|
+ contactPhone: '13911112222',
|
|
|
+ serviceTime: '2024-02-05 14:00',
|
|
|
+ status: 'pending_accept',
|
|
|
+ fulfillerName: '王大力',
|
|
|
+ fulfillerFee: 35.00,
|
|
|
+ detail: {
|
|
|
+ pickAddr: '海淀区万柳书院',
|
|
|
+ pickContact: '李女士',
|
|
|
+ pickPhone: '139xxxx',
|
|
|
+ pickTime: '14:00',
|
|
|
+ dropAddr: '',
|
|
|
+ },
|
|
|
+ timeline: [
|
|
|
+ { time: '2024-02-04 10:00', content: '下单成功', type: 'info' },
|
|
|
+ { time: '2024-02-04 10:30', content: '已派单给 王大力', type: 'primary' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ // 宠物接送 - 服务中 (单程送)
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ orderNo: 'ORD202402048803',
|
|
|
+ type: 'transport',
|
|
|
+ transportType: 'drop', // 单程送
|
|
|
+ petName: 'Cookie',
|
|
|
+ petBreed: '柯基',
|
|
|
+ petAvatar: '',
|
|
|
+ userName: '王先生',
|
|
|
+ city: '北京市',
|
|
|
+ district: '朝阳区',
|
|
|
+ createTime: '2024-02-04 12:00',
|
|
|
+ merchantName: '爱宠生活馆 (中关村店)',
|
|
|
+ contactPhone: '13612345678',
|
|
|
+ serviceTime: '2024-02-04 18:00',
|
|
|
+ status: 'serving',
|
|
|
+ fulfillerName: '张小美',
|
|
|
+ fulfillerFee: 40.00,
|
|
|
+ detail: {
|
|
|
+ pickAddr: '', // 单程送不需要接的详细起始点,实际业务中是已在店
|
|
|
+ dropAddr: '朝阳公园西门',
|
|
|
+ dropContact: '王先生',
|
|
|
+ dropPhone: '136xxxx',
|
|
|
+ },
|
|
|
+ timeline: [
|
|
|
+ { time: '2024-02-04 12:00', content: '下单成功', type: 'info' },
|
|
|
+ { time: '2024-02-04 17:50', content: '履约者出发', type: 'primary' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ // 上门喂遛 - 待派单
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ orderNo: 'ORD202402048804',
|
|
|
+ type: 'feeding',
|
|
|
+ petName: '咪咪',
|
|
|
+ petBreed: '布偶猫',
|
|
|
+ petAvatar: '',
|
|
|
+ userName: '李女士',
|
|
|
+ city: '上海市',
|
|
|
+ district: '徐汇区',
|
|
|
+ createTime: '2024-02-04 12:00',
|
|
|
+ merchantName: '爱宠生活馆 (三里屯店)',
|
|
|
+ contactPhone: '13987654321',
|
|
|
+ serviceTime: '2024-02-06 12:00',
|
|
|
+ status: 'pending_dispatch',
|
|
|
+ fulfillerName: '',
|
|
|
+ fulfillerFee: 0,
|
|
|
+ detail: {
|
|
|
+ packageName: '春节上门喂猫7天套餐',
|
|
|
+ currentCount: 1,
|
|
|
+ totalCount: 7,
|
|
|
+ area: '客厅、阳台',
|
|
|
+ itemLoc: '厨房柜子里',
|
|
|
+ cleanLoc: '卫生间洗手台',
|
|
|
+ foodAmount: '每次一罐头 + 半碗粮',
|
|
|
+ },
|
|
|
+ timeline: [{ time: '2024-02-04 12:00', content: '订单创建', type: 'info' }]
|
|
|
+ },
|
|
|
+ // 上门喂遛 - 服务中
|
|
|
+ {
|
|
|
+ id: 5,
|
|
|
+ orderNo: 'ORD202402048805',
|
|
|
+ type: 'feeding',
|
|
|
+ petName: '大黄',
|
|
|
+ petBreed: '中华田园犬',
|
|
|
+ petAvatar: '',
|
|
|
+ userName: '张先生',
|
|
|
+ city: '杭州市',
|
|
|
+ district: '西湖区',
|
|
|
+ createTime: '2024-02-04 12:30',
|
|
|
+ merchantName: '爱宠生活馆 (国贸店)',
|
|
|
+ contactPhone: '13555555555',
|
|
|
+ serviceTime: '2024-02-04 14:00',
|
|
|
+ status: 'serving',
|
|
|
+ fulfillerName: '李建国',
|
|
|
+ fulfillerFee: 50.00,
|
|
|
+ detail: {
|
|
|
+ packageName: '上门遛狗1小时',
|
|
|
+ currentCount: 1,
|
|
|
+ totalCount: 1,
|
|
|
+ area: '小区公园',
|
|
|
+ itemLoc: '门口鞋柜处',
|
|
|
+ cleanLoc: '楼下垃圾桶',
|
|
|
+ foodAmount: '-',
|
|
|
+ },
|
|
|
+ timeline: [
|
|
|
+ { time: '2024-02-04 12:00', content: '张小美 已接单', type: 'primary' },
|
|
|
+ { time: '2024-02-04 13:55', content: '履约者到达并在门口打卡', type: 'success', media: ['https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg'] }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ // 上门洗护 - 待接单
|
|
|
+ {
|
|
|
+ id: 6,
|
|
|
+ orderNo: 'ORD202402048806',
|
|
|
+ type: 'washing',
|
|
|
+ petName: '豆豆',
|
|
|
+ petBreed: '泰迪',
|
|
|
+ petAvatar: '',
|
|
|
+ userName: '刘小姐',
|
|
|
+ city: '深圳市',
|
|
|
+ district: '南山区',
|
|
|
+ createTime: '2024-02-04 15:00',
|
|
|
+ merchantName: '爱宠生活馆 (三里屯店)',
|
|
|
+ contactPhone: '13666666666',
|
|
|
+ serviceTime: '2024-02-04 16:00',
|
|
|
+ status: 'pending_accept',
|
|
|
+ fulfillerName: '王大力',
|
|
|
+ fulfillerFee: 80.00,
|
|
|
+ detail: {
|
|
|
+ packageName: '精致洗护套餐',
|
|
|
+ petStatus: '胆小,怕吹风机',
|
|
|
+ area: '浴室',
|
|
|
+ cleanLoc: '浴室淋浴间',
|
|
|
+ toolLoc: '自带工具箱',
|
|
|
+ },
|
|
|
+ timeline: [
|
|
|
+ { time: '2024-02-04 15:00', content: '订单创建', type: 'info' },
|
|
|
+ { time: '2024-02-04 15:05', content: '派单给 王大力', type: 'primary' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ // 上门洗护 - 已取消
|
|
|
+ {
|
|
|
+ id: 7,
|
|
|
+ orderNo: 'ORD202402047701',
|
|
|
+ type: 'washing',
|
|
|
+ petName: 'Snow',
|
|
|
+ petBreed: '萨摩耶',
|
|
|
+ petAvatar: '',
|
|
|
+ userName: 'Zhao',
|
|
|
+ city: '北京市',
|
|
|
+ district: '朝阳区',
|
|
|
+ createTime: '2024-02-02 10:00',
|
|
|
+ merchantName: '爱宠生活馆 (国贸店)',
|
|
|
+ contactPhone: '13777777777',
|
|
|
+ serviceTime: '2024-02-03 09:00',
|
|
|
+ status: 'cancelled',
|
|
|
+ fulfillerName: '',
|
|
|
+ fulfillerFee: 0,
|
|
|
+ detail: { packageName: '洗澡+美容', petStatus: '温顺' },
|
|
|
+ timeline: [
|
|
|
+ { time: '2024-02-02 10:00', content: '订单创建', type: 'info' },
|
|
|
+ { time: '2024-02-02 12:00', content: '用户取消订单', type: 'warning' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ // 宠物接送 - 已完成 (往返)
|
|
|
+ {
|
|
|
+ id: 8,
|
|
|
+ orderNo: 'ORD202402038899',
|
|
|
+ type: 'transport',
|
|
|
+ transportType: 'round',
|
|
|
+ petName: 'Cooper',
|
|
|
+ petBreed: '法斗',
|
|
|
+ petAvatar: '',
|
|
|
+ userName: '孙女士',
|
|
|
+ city: '北京市',
|
|
|
+ district: '通州区',
|
|
|
+ createTime: '2024-02-03 09:00',
|
|
|
+ merchantName: '爱宠生活馆 (三里屯店)',
|
|
|
+ contactPhone: '13333333333',
|
|
|
+ serviceTime: '2024-02-03 10:00',
|
|
|
+ status: 'completed',
|
|
|
+ fulfillerName: '张小美',
|
|
|
+ fulfillerFee: 65.00,
|
|
|
+ detail: {
|
|
|
+ pickAddr: '朝阳大悦城',
|
|
|
+ dropAddr: '瑞鹏宠物医院',
|
|
|
+ },
|
|
|
+ timeline: [
|
|
|
+ { time: '2024-02-03 09:00', content: '订单创建', type: 'info' },
|
|
|
+ { time: '2024-02-03 11:30', content: '服务完成', type: 'success' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ // 待商家确认
|
|
|
+ {
|
|
|
+ id: 99,
|
|
|
+ orderNo: 'ORD202402048999',
|
|
|
+ type: 'feeding',
|
|
|
+ petName: '小黑',
|
|
|
+ petBreed: '拉布拉多',
|
|
|
+ petAvatar: '',
|
|
|
+ userName: '周先生',
|
|
|
+ city: '北京市',
|
|
|
+ district: '海淀区',
|
|
|
+ createTime: '2024-02-04 14:00',
|
|
|
+ merchantName: '爱宠生活馆 (三里屯店)',
|
|
|
+ contactPhone: '13811112222',
|
|
|
+ serviceTime: '2024-02-04 16:00',
|
|
|
+ status: 'pending_confirm',
|
|
|
+ fulfillerName: '赵铁柱',
|
|
|
+ fulfillerFee: 45.00,
|
|
|
+ detail: {
|
|
|
+ packageName: '上门喂遛',
|
|
|
+ area: '小区内部',
|
|
|
+ },
|
|
|
+ timeline: [
|
|
|
+ { time: '2024-02-04 14:00', content: '订单创建', type: 'info' },
|
|
|
+ { time: '2024-02-04 14:05', content: '赵铁柱 已接单', type: 'primary' },
|
|
|
+ { time: '2024-02-04 16:30', content: '履约者完成服务,等待商家确认', type: 'warning' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ handleSearch()
|
|
|
+})
|
|
|
+
|
|
|
+const handleSearch = () => {
|
|
|
+ loading.value = true
|
|
|
+ setTimeout(() => {
|
|
|
+ let res = mockData
|
|
|
+ if(filters.orderType) res = res.filter(i => i.type === filters.orderType)
|
|
|
+ if(filters.status) res = res.filter(i => i.status === filters.status)
|
|
|
+ if(filters.keyword) {
|
|
|
+ const k = filters.keyword
|
|
|
+ res = res.filter(i => i.orderNo.includes(k) || i.merchantName.includes(k) || i.petName.includes(k))
|
|
|
+ }
|
|
|
+
|
|
|
+ // Split Round type orders
|
|
|
+ let processedData = []
|
|
|
+ res.forEach(item => {
|
|
|
+ if (item.type === 'transport' && item.transportType === 'round') {
|
|
|
+ // Split into Pick (接) and Drop (送)
|
|
|
+ // We clone the item. Ideally IDs should be unique for keys, but for display it's ok or append suffix.
|
|
|
+ let pickObj = { ...item, id: item.id + '_1', splitType: 'pick' }
|
|
|
+ let dropObj = { ...item, id: item.id + '_2', splitType: 'drop' }
|
|
|
+ processedData.push(pickObj)
|
|
|
+ processedData.push(dropObj)
|
|
|
+ } else {
|
|
|
+ processedData.push(item)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ tableData.value = processedData
|
|
|
+ pagination.total = processedData.length
|
|
|
+ loading.value = false
|
|
|
+ }, 500)
|
|
|
+}
|
|
|
+
|
|
|
+const handleSizeChange = (val) => { console.log(val) }
|
|
|
+const handleCurrentChange = (val) => { console.log(val) }
|
|
|
+
|
|
|
+const getTypeTag = (type) => {
|
|
|
+ const map = { transport: '', feeding: 'warning', washing: 'success' }
|
|
|
+ return map[type]
|
|
|
+}
|
|
|
+const getTypeName = (type) => {
|
|
|
+ const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
|
|
|
+ return map[type]
|
|
|
+}
|
|
|
+const getStatusName = (status) => {
|
|
|
+ const map = { pending_dispatch: '待派单', pending_accept: '待接单', serving: '服务中', pending_confirm: '待商家确认', completed: '已完成', cancelled: '已取消' }
|
|
|
+ return map[status]
|
|
|
+}
|
|
|
+
|
|
|
+const getStatusTag = (status) => {
|
|
|
+ const map = {
|
|
|
+ pending_dispatch: 'danger',
|
|
|
+ pending_accept: 'warning',
|
|
|
+ serving: 'primary',
|
|
|
+ pending_confirm: 'warning',
|
|
|
+ completed: 'success',
|
|
|
+ cancelled: 'info'
|
|
|
+ }
|
|
|
+ return map[status] || 'info'
|
|
|
+}
|
|
|
+
|
|
|
+const getStepActive = (status) => {
|
|
|
+ // Kept for table list if needed, but detail now uses computed steps below
|
|
|
+ if(status === 'pending_dispatch') return 1
|
|
|
+ if(status === 'pending_accept') return 2
|
|
|
+ if(status === 'serving') return 3
|
|
|
+ if(status === 'pending_confirm') return 4
|
|
|
+ if(status === 'completed') return 6
|
|
|
+ if(status === 'cancelled') return 0
|
|
|
+ return 0
|
|
|
+}
|
|
|
+
|
|
|
+const getTransportModeName = (type) => {
|
|
|
+ const map = { round: '往返接送', pick: '单程接(到店)', drop: '单程送(回家)' }
|
|
|
+ return map[type] || '接送服务'
|
|
|
+}
|
|
|
+
|
|
|
+// Generate dynamic steps with times for detail view
|
|
|
+const currentOrderSteps = computed(() => {
|
|
|
+ if(!currentOrder.value) return { active: 0, steps: [] }
|
|
|
+
|
|
|
+ // Base timeline nodes
|
|
|
+ const steps = [
|
|
|
+ { title: '商户下单', status: 'created', time: '' },
|
|
|
+ { title: '运营派单', status: 'dispatched', time: '' },
|
|
|
+ { title: '履约接单', status: 'accepted', time: '' },
|
|
|
+ { title: '服务中', status: 'serving', time: '' }, // Includes arrival/start
|
|
|
+ { title: '待商家确认', status: 'confirming', time: '' }, // Finished service
|
|
|
+ { title: '已完成', status: 'completed', time: '' }
|
|
|
+ ]
|
|
|
+
|
|
|
+ const logs = currentOrder.value.orderLogs || []
|
|
|
+ const status = currentOrder.value.status
|
|
|
+
|
|
|
+ let active = 0
|
|
|
+
|
|
|
+ // Map logs to steps time
|
|
|
+ // This is a simple mapper, in real app would match specific log types/codes
|
|
|
+ const findTime = (keyword) => {
|
|
|
+ const log = logs.find(l => l.title.includes(keyword) || l.content.includes(keyword))
|
|
|
+ return log ? log.time : ''
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. Created
|
|
|
+ steps[0].time = currentOrder.value.createTime || findTime('下单') || findTime('创建')
|
|
|
+ if (steps[0].time) active = 1
|
|
|
+
|
|
|
+ // 2. Dispatched
|
|
|
+ steps[1].time = findTime('派单') || (status !== 'pending_dispatch' ? steps[0].time : '') // Mock if passed
|
|
|
+ if (['pending_accept', 'serving', 'pending_confirm', 'completed'].includes(status)) active = 2
|
|
|
+
|
|
|
+ // 3. Accepted
|
|
|
+ steps[2].time = findTime('接单')
|
|
|
+ if (['pending_accept'].includes(status)) {
|
|
|
+ steps[2].title = '待履约者接单'
|
|
|
+ } else if (['serving', 'pending_confirm', 'completed'].includes(status)) {
|
|
|
+ steps[2].title = '履约者已接单'
|
|
|
+ active = 3
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. Serving (Arrival/Start)
|
|
|
+ steps[3].time = findTime('到达') || findTime('出发')
|
|
|
+ if (['serving'].includes(status)) {
|
|
|
+ steps[3].title = '服务进行中'
|
|
|
+ } else if (['pending_confirm', 'completed'].includes(status)) {
|
|
|
+ steps[3].title = '服务已完成'
|
|
|
+ active = 4
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. Confirming
|
|
|
+ steps[4].time = findTime('等待商家确认') || findTime('待验收')
|
|
|
+ if (['pending_confirm'].includes(status)) {
|
|
|
+ steps[4].title = '待商家确认'
|
|
|
+ } else if (['completed'].includes(status)) {
|
|
|
+ steps[4].title = '商家已确认'
|
|
|
+ active = 5
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. Completed
|
|
|
+ if (status === 'completed') {
|
|
|
+ steps[5].time = findTime('完成') // or calculate based on logic
|
|
|
+ active = 6
|
|
|
+ }
|
|
|
+
|
|
|
+ if (status === 'cancelled') {
|
|
|
+ // Handle cancelled state simply or insert a cancelled step
|
|
|
+ return {
|
|
|
+ active: 1,
|
|
|
+ steps: [
|
|
|
+ { title: '商户下单', time: steps[0].time },
|
|
|
+ { title: '已取消', time: findTime('取消') || '订单已取消' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { active, steps }
|
|
|
+})
|
|
|
+
|
|
|
+const activeDetailTab = ref('basic')
|
|
|
+
|
|
|
+const handleDetail = (row) => {
|
|
|
+ // Inject rich mock detail data
|
|
|
+ currentOrder.value = {
|
|
|
+ ...row,
|
|
|
+ // Mock extended user info
|
|
|
+ userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
|
|
|
+ userLevel: 'Lv3',
|
|
|
+ userRegDays: '210天',
|
|
|
+ userTotalSpend: '3580',
|
|
|
+ address: '某小区5号楼2单元101',
|
|
|
+ address: '某小区5号楼2单元101',
|
|
|
+ groupBuyPackage: '新客体验9.9元洗护套餐',
|
|
|
+ transportType: 'pick', // 模拟单程接
|
|
|
+ detail: {
|
|
|
+ ...row.detail,
|
|
|
+ pickTime: '2024-02-05 09:30',
|
|
|
+ pickAddr: '北京市朝阳区某小区5号楼2单元101',
|
|
|
+ pickContact: '李先生',
|
|
|
+ pickPhone: '13812345678',
|
|
|
+ dropTime: '2024-02-05 18:30',
|
|
|
+ dropAddr: '北京市朝阳区某小区5号楼2单元101',
|
|
|
+ dropContact: '李先生',
|
|
|
+ dropPhone: '13812345678',
|
|
|
+ packageName: '精细洗护套餐A',
|
|
|
+ petStatus: '胆小,需安抚',
|
|
|
+ area: '北京市朝阳区某小区5号楼2单元101'
|
|
|
+ },
|
|
|
+ petGender: 'male',
|
|
|
+ petAge: '2岁',
|
|
|
+ petWeight: '15kg',
|
|
|
+ petVaccine: '已接种',
|
|
|
+ petSterilized: true,
|
|
|
+ petCharacter: '活泼好动,喜欢球类玩具',
|
|
|
+ petHealth: '健康良好',
|
|
|
+ // Mock fulfiller info
|
|
|
+ fulfillerAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
|
|
+ fulfillerPhone: '13812345678',
|
|
|
+ fulfillerStation: '朝阳服务站',
|
|
|
+ fulfillerRating: 4.8,
|
|
|
+ // Mock order logs
|
|
|
+ orderLogs: [
|
|
|
+ { time: '2024-02-04 09:30', title: '订单创建', content: '商户提交订单', icon: 'Document' },
|
|
|
+ { time: '2024-02-04 10:00', title: '系统派单', content: '指派给 王大力', icon: 'Bicycle' },
|
|
|
+ { time: '2024-02-04 10:05', title: '接单成功', content: '履约者已确认接单', icon: 'CircleCheck' },
|
|
|
+ { time: '2024-02-04 13:55', title: '到达服务点', content: '履约者已打卡', icon: 'Location' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ activeDetailTab.value = 'basic'
|
|
|
+ detailVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+// --- Dispatch Dialog Logic (Copied from OrderDispatch) ---
|
|
|
+const ridersList = ref([
|
|
|
+ {
|
|
|
+ id: 101, name: '王大力', station: '朝阳站', phone: '13800138000', maskPhone: '138****8000',
|
|
|
+ status: 'online', categories: ['接送', '喂遛'], lastServiceTime: '2024-02-07 11:00',
|
|
|
+ avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
|
|
|
+ pendingCount: 0, todoCount: 2, lng: 116.460, lat: 39.920
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 102, name: '李小龙', station: '海淀站', phone: '13912345678', maskPhone: '139****5678',
|
|
|
+ status: 'online', categories: ['接送', '洗护'], lastServiceTime: '2024-02-07 10:30',
|
|
|
+ avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
|
|
+ pendingCount: 1, todoCount: 3, lng: 116.450, lat: 39.915
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 103, name: '张小美', station: '望京站', phone: '13666666666', maskPhone: '136****6666',
|
|
|
+ status: 'online', categories: ['喂遛'], lastServiceTime: '2024-02-07 10:45',
|
|
|
+ avatar: '',
|
|
|
+ pendingCount: 0, todoCount: 0, lng: 116.470, lat: 39.930
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 104, name: '赵铁柱', station: '通州站', phone: '13555555555', maskPhone: '135****5555',
|
|
|
+ status: 'offline', categories: ['接送'], lastServiceTime: '2024-02-06 18:00',
|
|
|
+ avatar: '',
|
|
|
+ pendingCount: 0, todoCount: 0, lng: 116.440, lat: 39.910
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 105, name: '孙悟空', station: '花果山', phone: '13888888888', maskPhone: '138****8888',
|
|
|
+ status: 'online', categories: ['接送', '喂遛', '洗护'], lastServiceTime: '2024-02-07 09:30',
|
|
|
+ avatar: '',
|
|
|
+ pendingCount: 0, todoCount: 1, lng: 116.480, lat: 39.925
|
|
|
+ }
|
|
|
+])
|
|
|
+
|
|
|
+const serviceProgressSteps = computed(() => {
|
|
|
+ const order = currentOrder.value
|
|
|
+ if (!order) return []
|
|
|
+
|
|
|
+ // 1. Pending / Waiting for Rider: No progress yet
|
|
|
+ // Strict requirement: "待接单时应该为空"
|
|
|
+ if (['pending', 'pending_dispatch', 'pending_accept', 'cancelled'].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 = []
|
|
|
+
|
|
|
+ // --- Step 1: Accepted (Start for Serving+) ---
|
|
|
+ steps.push({
|
|
|
+ title: '已接单',
|
|
|
+ time: `${datePart} 09:30`,
|
|
|
+ icon: 'Bicycle',
|
|
|
+ color: '#ff9900', // Active color
|
|
|
+ desc: `履约者 ${order.fulfillerName || '当前履约者'} 已确认接单,准备前往服务地点`,
|
|
|
+ media: []
|
|
|
+ })
|
|
|
+
|
|
|
+ // --- Step 2: Arrived (Assume arrived if serving) ---
|
|
|
+ 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) {
|
|
|
+ // Transport Flow
|
|
|
+ // --- Step 3: Depart (Active in Serving) ---
|
|
|
+ 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' }
|
|
|
+ ]
|
|
|
+ })
|
|
|
+
|
|
|
+ // --- Step 4: Deliver (Only if Confirming or Completed) ---
|
|
|
+ if (['pending_confirm', 'completed'].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 {
|
|
|
+ // Service Flow
|
|
|
+ // --- Step 3: Start Service (Active in Serving) ---
|
|
|
+ steps.push({
|
|
|
+ title: '开始服务',
|
|
|
+ time: `${datePart} 10:00`,
|
|
|
+ icon: 'VideoPlay',
|
|
|
+ color: '#ff9900',
|
|
|
+ desc: '已确认宠物状态,开始进行服务视频录制',
|
|
|
+ media: [
|
|
|
+ { type: 'image', url: 'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg' }
|
|
|
+ ]
|
|
|
+ })
|
|
|
+
|
|
|
+ // --- Step 4: End Service (Only if Confirming or Completed) ---
|
|
|
+ if (['pending_confirm', 'completed'].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' }
|
|
|
+ ]
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- Step 5: Wait Confirm (Only if Confirming or Completed) ---
|
|
|
+ if (['pending_confirm', 'completed'].includes(order.status)) {
|
|
|
+ steps.push({
|
|
|
+ title: '待商家确认',
|
|
|
+ time: `${datePart} 10:55`,
|
|
|
+ icon: 'Clock',
|
|
|
+ color: '#ff9900',
|
|
|
+ desc: '履约者已提交完成信息,等待商家确认订单',
|
|
|
+ media: []
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- Step 6: Completed (Only if Completed) ---
|
|
|
+ if (order.status === 'completed') {
|
|
|
+ steps.push({
|
|
|
+ title: '订单完成',
|
|
|
+ time: `${datePart} 11:00`,
|
|
|
+ icon: 'Select',
|
|
|
+ color: '#67C23A',
|
|
|
+ desc: '用户/商家已确认,服务圆满结束',
|
|
|
+ media: []
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ return steps
|
|
|
+})
|
|
|
+
|
|
|
+const dispatchDialogVisible = ref(false)
|
|
|
+const currentDispatchOrder = ref(null)
|
|
|
+const currentRider = ref(null)
|
|
|
+const dispatchSearchQuery = ref('')
|
|
|
+const selectedRiderId = ref(null)
|
|
|
+const dispatchFee = ref(0) // Default fee
|
|
|
+
|
|
|
+const openDispatchDialog = (row) => {
|
|
|
+ // Map table row to order card structure if needed, or use row directly if fields match
|
|
|
+ // OrderDispatch uses: pickAddr, dropAddr, address, typeCode, daysLater, time
|
|
|
+ // OrderList row has: detail.pickAddr/dropAddr, type, serviceTime (as time), etc.
|
|
|
+ // We need to adapt the row to 'currentDispatchOrder' structure expected by dialog template
|
|
|
+
|
|
|
+ // Construct dispatch order object
|
|
|
+ let orderObj = {
|
|
|
+ id: row.id,
|
|
|
+ typeCode: row.type,
|
|
|
+ transportType: row.splitType || row.transportType, // Add transportType
|
|
|
+ time: row.serviceTime,
|
|
|
+ status: row.status,
|
|
|
+ daysLater: getDaysLater(row.serviceTime), // Need helper
|
|
|
+ address: '', // generic address for feeding/washing
|
|
|
+ pickAddr: '',
|
|
|
+ dropAddr: '',
|
|
|
+ riderId: (row.status === 'pending_accept' || row.status === 'serving') ? getRiderIdByName(row.fulfillerName) : null // Mock finding rider ID
|
|
|
+ }
|
|
|
+
|
|
|
+ if (row.type === 'transport') {
|
|
|
+ orderObj.pickAddr = row.detail.pickAddr
|
|
|
+ orderObj.dropAddr = row.detail.dropAddr
|
|
|
+ } else {
|
|
|
+ // Feeding/Washing address isn't in main row usually, let's assume city+district or from detail if we had it
|
|
|
+ // row.city + row.district is available.
|
|
|
+ // We will just use row.city + row.district for display in card for now, or add address to mock data details.
|
|
|
+ // Actually, OrderList mock has detailed addresses implicit in description?
|
|
|
+ // Let's use City + District + "详细地址" placeholder or row.detail.address if strictly needed.
|
|
|
+ // For visual, let's just use City+District.
|
|
|
+ orderObj.address = row.city + row.district
|
|
|
+ }
|
|
|
+
|
|
|
+ currentDispatchOrder.value = orderObj
|
|
|
+
|
|
|
+ // Set Current Rider Logic
|
|
|
+ if (orderObj.riderId) {
|
|
|
+ currentRider.value = ridersList.value.find(r => r.id === orderObj.riderId) || null
|
|
|
+ } else {
|
|
|
+ currentRider.value = null
|
|
|
+ }
|
|
|
+
|
|
|
+ dispatchDialogVisible.value = true
|
|
|
+ dispatchSearchQuery.value = ''
|
|
|
+ selectedRiderId.value = null
|
|
|
+ dispatchFee.value = 0
|
|
|
+}
|
|
|
+
|
|
|
+const handleDispatchSubmit = () => {
|
|
|
+ if (!selectedRiderId.value) {
|
|
|
+ ElMessage.warning('请选择履约者')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!dispatchFee.value) {
|
|
|
+ ElMessage.warning('请输入服务费用')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ dispatchDialogVisible.value = false
|
|
|
+ ElMessage.success('派单成功')
|
|
|
+
|
|
|
+ // Update local list
|
|
|
+ if (currentDispatchOrder.value) {
|
|
|
+ const row = tableData.value.find(r => r.id === currentDispatchOrder.value.id)
|
|
|
+ if (row) {
|
|
|
+ row.status = 'pending_accept'
|
|
|
+ const rider = ridersList.value.find(r => r.id === selectedRiderId.value)
|
|
|
+ row.fulfillerName = rider ? rider.name : 'Unknown'
|
|
|
+ row.fulfillerFee = dispatchFee.value
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleExportLogs = () => {
|
|
|
+ const logs = currentOrder.value.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_${currentOrder.value.orderNo}.csv`
|
|
|
+ link.click()
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
+
|
|
|
+ ElMessage.success('导出成功')
|
|
|
+}
|
|
|
+
|
|
|
+// Helpers
|
|
|
+const getShortType = (code) => {
|
|
|
+ const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' }
|
|
|
+ return map[code] || '订单'
|
|
|
+}
|
|
|
+const getRiderStatusText = (status) => {
|
|
|
+ const map = { 'online': '接单中', 'busy': '接单中', 'offline': '休息中', 'disabled': '禁用' }
|
|
|
+ return map[status]
|
|
|
+}
|
|
|
+const getCategoryClass = (cat) => {
|
|
|
+ const map = { '接送': 'cat-transport', '喂遛': 'cat-feeding', '洗护': 'cat-washing' }
|
|
|
+ return map[cat] || ''
|
|
|
+}
|
|
|
+
|
|
|
+const getServiceTimeRange = (timeStr) => {
|
|
|
+ if (!timeStr) return '--'
|
|
|
+ try {
|
|
|
+ // Assume format YYYY-MM-DD HH:mm
|
|
|
+ if (timeStr.length < 16) return timeStr
|
|
|
+
|
|
|
+ let timePart = timeStr.substring(11, 16)
|
|
|
+ let [hh, mm] = timePart.split(':').map(Number)
|
|
|
+ let endH = hh + 2 // Assume 2 hours duration
|
|
|
+ if (endH >= 24) endH -= 24
|
|
|
+
|
|
|
+ let endHStr = endH.toString().padStart(2, '0')
|
|
|
+ return `${timeStr}-${endHStr}:${mm.toString().padStart(2, '0')}`
|
|
|
+ } catch (e) {
|
|
|
+ return timeStr
|
|
|
+ }
|
|
|
+}
|
|
|
+// Mock helper to get days later text
|
|
|
+const getDaysLater = (dateStr) => {
|
|
|
+ // Simple mock logic
|
|
|
+ if (dateStr.includes('02-07')) return '今天'
|
|
|
+ if (dateStr.includes('02-08')) return '明天'
|
|
|
+ return ''
|
|
|
+}
|
|
|
+// Mock helper to find rider ID by name (since table has name only)
|
|
|
+const getRiderIdByName = (name) => {
|
|
|
+ const rider = ridersList.value.find(r => r.name === name)
|
|
|
+ return rider ? rider.id : null
|
|
|
+}
|
|
|
+const filteredDispatchRiders = computed(() => {
|
|
|
+ let result = ridersList.value.filter(r => r.status === 'online' || r.status === 'busy')
|
|
|
+ if (dispatchSearchQuery.value) {
|
|
|
+ const q = dispatchSearchQuery.value.toLowerCase()
|
|
|
+ result = result.filter(r => r.name.includes(q) || r.phone.includes(q))
|
|
|
+ }
|
|
|
+ result.sort((a,b) => {
|
|
|
+ return a.lastServiceTime.localeCompare(b.lastServiceTime)
|
|
|
+ })
|
|
|
+ return result
|
|
|
+})
|
|
|
+
|
|
|
+
|
|
|
+const handleCancel = (row) => {
|
|
|
+ ElMessageBox.confirm('确认取消该订单吗?', '提示', { type: 'warning' })
|
|
|
+ .then(() => {
|
|
|
+ ElMessage.success('订单已取消')
|
|
|
+ handleSearch()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const careSummaryVisible = ref(false)
|
|
|
+const careSummaryOrder = ref(null)
|
|
|
+const careSummaryText = ref('')
|
|
|
+const isEditingSummary = ref(false)
|
|
|
+
|
|
|
+const openCareSummary = (row) => {
|
|
|
+ // Inject rich mock data for the detailed profile view
|
|
|
+ careSummaryOrder.value = {
|
|
|
+ ...row,
|
|
|
+ petAge: '3岁',
|
|
|
+ petGender: 'male',
|
|
|
+ petTags: ['易过敏', '胆小'],
|
|
|
+ petWeight: '30 kg',
|
|
|
+ petSize: '大型',
|
|
|
+ petPersonality: '活泼,超级粘人,喜欢玩球',
|
|
|
+ homeTime: '2023-01-01',
|
|
|
+ houseType: '电梯',
|
|
|
+ entryMethod: '密码开门',
|
|
|
+ entryDetail: '密码: 123456 (仅限服务期间使用)',
|
|
|
+ healthStatus: '健康',
|
|
|
+ aggression: '无',
|
|
|
+ vaccineImg: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
|
|
|
+ medicalHistory: '无',
|
|
|
+ allergy: '海鲜'
|
|
|
+ }
|
|
|
+
|
|
|
+ isEditingSummary.value = false
|
|
|
+ // Mock default summary if empty
|
|
|
+ if (!row.careSummary) {
|
|
|
+ careSummaryText.value = `1. 精神/身体状态:${row.petName}精神状态良好,愿意互动。
|
|
|
+2. 进食/饮水:食欲正常,饮水适当,已清洗碗具。
|
|
|
+3. 排泄情况:排便正常,颜色形状正常,已清理。
|
|
|
+4. 卫生情况:猫砂盆/地面已清理干净,无异味。
|
|
|
+5. 互动情况:陪玩了20分钟,${row.petName}很开心。
|
|
|
+6. 特殊情况/备注:无特殊异常。`
|
|
|
+ } else {
|
|
|
+ careSummaryText.value = row.careSummary
|
|
|
+ }
|
|
|
+ careSummaryVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const saveCareSummary = () => {
|
|
|
+ if(careSummaryOrder.value) {
|
|
|
+ careSummaryOrder.value.careSummary = careSummaryText.value
|
|
|
+ // Update time if not exists or update it on save? Usually maintain original submission time unless updated.
|
|
|
+ // Let's set a mock time if missing.
|
|
|
+ if (!careSummaryOrder.value.summaryTime) {
|
|
|
+ careSummaryOrder.value.summaryTime = '2024-02-04 17:00' // Mock
|
|
|
+ }
|
|
|
+ ElMessage.success('护理小结已保存')
|
|
|
+ isEditingSummary.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Reward / Punish Logic
|
|
|
+const rewardDialogVisible = ref(false)
|
|
|
+const remarkDialogVisible = ref(false)
|
|
|
+const currentOperateRow = ref(null)
|
|
|
+
|
|
|
+const rewardForm = reactive({
|
|
|
+ type: 'reward', // reward | punish
|
|
|
+ item: 'points', // points | amount
|
|
|
+ value: 10,
|
|
|
+ reason: ''
|
|
|
+})
|
|
|
+
|
|
|
+const remarkForm = ref('')
|
|
|
+
|
|
|
+const openRewardDialog = (row) => {
|
|
|
+ currentOperateRow.value = row
|
|
|
+ // Reset form
|
|
|
+ rewardForm.type = 'reward'
|
|
|
+ rewardForm.item = 'points'
|
|
|
+ rewardForm.value = 10
|
|
|
+ rewardForm.reason = ''
|
|
|
+ rewardDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const handleRewardSubmit = () => {
|
|
|
+ if(!rewardForm.reason) {
|
|
|
+ ElMessage.warning('请输入奖惩原因')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ElMessage.success(`操作成功:${rewardForm.type === 'reward' ? '奖励' : '惩罚'}已执行`)
|
|
|
+ rewardDialogVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const openRemarkDialog = (row) => {
|
|
|
+ currentOperateRow.value = row
|
|
|
+ remarkForm.value = row.remark || ''
|
|
|
+ remarkDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const handleRemarkSubmit = () => {
|
|
|
+ if(currentOperateRow.value) {
|
|
|
+ currentOperateRow.value.remark = remarkForm.value
|
|
|
+ ElMessage.success('备注已更新')
|
|
|
+ remarkDialogVisible.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleCommand = (cmd, row) => {
|
|
|
+ if(cmd === 'reward') openRewardDialog(row)
|
|
|
+ if(cmd === 'remark') openRemarkDialog(row)
|
|
|
+ if(cmd === 'care_summary') openCareSummary(row)
|
|
|
+
|
|
|
+ if(cmd === 'complete') {
|
|
|
+ ElMessageBox.confirm('确认将该订单手动标记为完成吗?', '提示', { type: 'warning' })
|
|
|
+ .then(() => {
|
|
|
+ row.status = 'completed'
|
|
|
+ ElMessage.success('订单已标记完成')
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if(cmd === 'delete') {
|
|
|
+ ElMessageBox.confirm('确认删除该订单吗?此操作不可恢复', '警告', { type: 'error' })
|
|
|
+ .then(() => {
|
|
|
+ tableData.value = tableData.value.filter(item => item.id !== row.id)
|
|
|
+ ElMessage.success('订单已删除')
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.page-container { padding: 20px; }
|
|
|
+.card-header { display: flex; justify-content: space-between; align-items: center; }
|
|
|
+.title { font-weight: bold; font-size: 18px; }
|
|
|
+.right-panel { display: flex; gap: 10px; align-items: center; }
|
|
|
+.search-input { width: 220px; }
|
|
|
+.status-tabs { margin-top: 10px; margin-bottom: -10px; }
|
|
|
+.pagination-container { display: flex; justify-content: flex-end; margin-top: 20px; }
|
|
|
+
|
|
|
+/* Table Content Styles */
|
|
|
+.service-type-cell { display: flex; flex-direction: row; gap: 4px; align-items: center; } /* Changed to row */
|
|
|
+.sub-tag { font-size: 11px; height: 20px; padding: 0 5px; }
|
|
|
+
|
|
|
+.pet-info { display: flex; align-items: center; gap: 10px; }
|
|
|
+.pet-info .el-avatar { background: #e0eaff; color: #409eff; font-weight: bold; flex-shrink: 0; }
|
|
|
+.pet-info .avatar-feeding { background: #fdf6ec; color: #e6a23c; }
|
|
|
+.pet-info .avatar-washing { background: #f0f9eb; color: #67c23a; }
|
|
|
+
|
|
|
+.pet-detail { display: flex; flex-direction: column; line-height: 1.4; }
|
|
|
+.pet-name { font-weight: bold; font-size: 14px; color: #303133; }
|
|
|
+.pet-breed { color: #909399; font-weight: normal; font-size: 12px; margin-left: 4px; }
|
|
|
+
|
|
|
+.merchant-info { display: flex; flex-direction: column; line-height: 1.4; }
|
|
|
+.sub-text { font-size: 12px; color: #999; }
|
|
|
+.text-gray { color: #ccc; font-style: italic; }
|
|
|
+.time-text { font-size: 13px; color: #606266; }
|
|
|
+
|
|
|
+.status-cell { display: flex; align-items: center; }
|
|
|
+.status-dot { width: 6px; height: 6px; border-radius: 50%; margin-right: 6px; background-color: #909399; }
|
|
|
+.status-dot.pending_dispatch { background-color: #f56c6c; box-shadow: 0 0 4px rgba(245, 108, 108, 0.4); }
|
|
|
+.status-dot.pending_accept { background-color: #e6a23c; }
|
|
|
+.status-dot.serving { background-color: #409eff; }
|
|
|
+.status-dot.pending_confirm { background-color: #bf24e8; }
|
|
|
+.status-dot.completed { background-color: #67c23a; }
|
|
|
+.status-dot.cancelled { background-color: #909399; }
|
|
|
+
|
|
|
+.fulfiller-info { display: flex; flex-direction: column; }
|
|
|
+.fulfiller-name { font-weight: 500; color: #333; }
|
|
|
+.fulfiller-fee { font-size: 12px; color: #e6a23c; }
|
|
|
+
|
|
|
+.op-cell { display: flex; align-items: center; gap: 8px; } /* Added flex for operation column */
|
|
|
+.el-dropdown-link { cursor: pointer; color: #409eff; font-size: 12px; display: flex; align-items: center; line-height: 1; height: 24px;} /* Ensure alignment */
|
|
|
+
|
|
|
+/* Detail Styles */
|
|
|
+.detail-content { padding: 0 10px; }
|
|
|
+.order-detail-drawer .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; }
|
|
|
+.crt-time { font-size: 13px; color: #909399; }
|
|
|
+
|
|
|
+.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; }
|
|
|
+.vip-badge { background: linear-gradient(90deg, #f3d19e, #e6a23c); color: #fff; font-size: 10px; padding: 0 5px; border-radius: 10px; height: 16px; line-height: 16px; font-weight: bold; }
|
|
|
+.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; }
|
|
|
+.stat-row { font-size: 12px; color: #909399; display: flex; gap: 10px; }
|
|
|
+
|
|
|
+/* 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; }
|
|
|
+.order-desc :deep(.el-descriptions__label) { width: 90px; font-weight: bold; color: #606266; }
|
|
|
+
|
|
|
+/* 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; }
|
|
|
+.f-row3 { display: flex; align-items: center; gap: 8px; }
|
|
|
+.score-text { font-weight: bold; color: #ff9900; }
|
|
|
+.empty-state { padding: 40px 0; text-align: center; }
|
|
|
+
|
|
|
+/* Service Block */
|
|
|
+.service-block { margin-bottom: 25px; }
|
|
|
+.block-title { font-weight: bold; font-size: 15px; margin-bottom: 15px; padding-left: 8px; border-left: 4px solid #409eff; }
|
|
|
+
|
|
|
+/* New Dispatch Styles */
|
|
|
+/* Dispatch Dialog Styles from OrderDispatch */
|
|
|
+.list-card {
|
|
|
+ background: #fff; border: 1px solid #ebeef5; border-radius: 8px;
|
|
|
+ padding: 12px; margin-bottom: 10px;
|
|
|
+ display: flex; align-items: stretch; gap: 12px; transition: all 0.2s;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+.list-card:hover { border-color: #c6e2ff; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
|
|
|
+
|
|
|
+.card-left { flex-shrink: 0; display: flex; align-items: center; }
|
|
|
+.order-card .type-tag {
|
|
|
+ width: 40px; height: 40px; border-radius: 8px; color: #fff;
|
|
|
+ display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold;
|
|
|
+}
|
|
|
+.type-tag.transport { background: #e6a23c; }
|
|
|
+.type-tag.feeding { background: #67c23a; }
|
|
|
+.type-tag.washing { background: #409eff; }
|
|
|
+
|
|
|
+.card-main { flex: 1; overflow: hidden; display: flex; flex-direction: column; justify-content: center; gap: 4px; }
|
|
|
+.row-addr {
|
|
|
+ font-size: 13px; color: #303133;
|
|
|
+ display: flex; align-items: center; gap: 4px;
|
|
|
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+.row-addr .tag {
|
|
|
+ font-size: 11px; color: #fff; padding: 1px 4px; border-radius: 4px; flex-shrink: 0; transform: scale(0.9);
|
|
|
+}
|
|
|
+.tag.pick { background: #409eff; }
|
|
|
+.tag.drop { background: #e6a23c; }
|
|
|
+.tag.home { background: #67c23a; }
|
|
|
+
|
|
|
+.row-time {
|
|
|
+ font-size: 12px; color: #909399; display: flex; align-items: center; gap: 4px;
|
|
|
+}
|
|
|
+.days-tag {
|
|
|
+ color: #f56c6c; background: #fef0f0; padding: 0 4px; border-radius: 4px; font-size: 11px; border: 1px solid #fde2e2;
|
|
|
+ transform: scale(0.95);
|
|
|
+}
|
|
|
+
|
|
|
+.dispatch-order-info {
|
|
|
+ background: #f5f7fa; padding: 10px; border-radius: 4px; margin-bottom: 20px; border:1px solid #e4e7ed;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+.dispatch-rider-select .select-header {
|
|
|
+ display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;
|
|
|
+}
|
|
|
+.dispatch-rider-select .tit { font-weight: bold; font-size: 14px; }
|
|
|
+
|
|
|
+.rider-grid {
|
|
|
+ display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; padding-right: 10px;
|
|
|
+}
|
|
|
+.rider-card.select-card {
|
|
|
+ cursor: pointer; border: 1px solid #dcdfe6; position: relative; transition: all 0.2s;
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+.rider-card.select-card:hover { border-color: #409eff; }
|
|
|
+.rider-card.select-card.active {
|
|
|
+ border-color: #409eff; background-color: #ecf5ff;
|
|
|
+}
|
|
|
+.selected-mark {
|
|
|
+ position: absolute; top: 0; right: 0;
|
|
|
+ background: #409eff; color: #fff; border-bottom-left-radius: 6px;
|
|
|
+ width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.rider-card .card-left .dot {
|
|
|
+ position: absolute; bottom: 0; right: 0; width: 10px; height: 10px; border-radius: 50%; border: 2px solid #fff;
|
|
|
+}
|
|
|
+.dot.online { background: #67c23a; }
|
|
|
+.dot.busy { background: #409eff; }
|
|
|
+.dot.offline { background: #909399; }
|
|
|
+
|
|
|
+.r-name { font-weight: bold; font-size: 14px; color: #303133; margin-right: 8px; }
|
|
|
+.r-phone { font-size: 12px; color: #909399; }
|
|
|
+.status-badge { font-size: 11px; padding: 2px 6px; border-radius: 4px; display: inline-block; font-weight: bold; }
|
|
|
+.status-badge.online { background: #f0f9eb; color: #67c23a; }
|
|
|
+.status-badge.busy { background: #ecf5ff; color: #409eff; }
|
|
|
+.status-badge.offline { background: #f4f4f5; color: #909399; }
|
|
|
+
|
|
|
+.cat-tag {
|
|
|
+ background: #f4f4f5; color: #909399; font-size: 10px;
|
|
|
+ padding: 1px 4px; border-radius: 2px;
|
|
|
+ margin-right: 4px;
|
|
|
+}
|
|
|
+.cat-tag.cat-transport { background: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
|
|
|
+.cat-tag.cat-feeding { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
|
|
|
+.cat-tag.cat-washing { background: #fff0f6; color: #eb2f96; border: 1px solid #ffadd2; }
|
|
|
+
|
|
|
+.last-time { font-size: 11px; color: #999; }
|
|
|
+.empty-text { text-align: center; color: #909399; padding: 20px; width: 100%; grid-column: span 2; }
|
|
|
+.dispatch-footer {
|
|
|
+ margin-top: 20px; padding-top: 20px; border-top: 1px solid #ebeef5;
|
|
|
+ display: flex; justify-content: space-between; align-items: center;
|
|
|
+}
|
|
|
+.dispatch-footer .fee-input { display: flex; align-items: center; gap: 8px; font-size: 14px; }
|
|
|
+
|
|
|
+
|
|
|
+/* Enhanced Care Summary Styles */
|
|
|
+.care-summary-drawer :deep(.el-drawer__header) { margin-bottom: 0; padding: 20px 24px; border-bottom: 1px solid #f0f0f0; }
|
|
|
+.care-summary-drawer :deep(.el-drawer__body) { padding: 0; overflow-y: auto; background: #fff; }
|
|
|
+
|
|
|
+.care-summary-container { padding: 32px 40px; }
|
|
|
+
|
|
|
+/* 1. Header */
|
|
|
+.summary-header { display: flex; gap: 24px; align-items: flex-start; margin-bottom: 32px; padding-bottom: 24px; border-bottom: 1px dashed #e4e7ed; }
|
|
|
+.avatar-wrapper { border: 4px solid #f2f6fc; border-radius: 50%; }
|
|
|
+.pet-summary-info { flex: 1; display:flex; flex-direction:column; gap:12px; padding-top: 4px; }
|
|
|
+
|
|
|
+.summary-name-row { display: flex; align-items: center; gap: 16px; margin-bottom: 4px; }
|
|
|
+.summary-name-row .name { font-size: 24px; font-weight: 800; color: #303133; letter-spacing: 0.5px; }
|
|
|
+.tags-group { display: flex; gap: 8px; align-items: center; }
|
|
|
+
|
|
|
+.summary-sub-row { display: flex; align-items: center; background: #f9fafe; padding: 10px 16px; border-radius: 8px; align-self: flex-start; }
|
|
|
+.info-item { display: flex; flex-direction: column; gap: 2px; }
|
|
|
+.info-item .lbl { font-size: 11px; color: #909399; text-transform: uppercase; }
|
|
|
+.info-item .val { font-size: 14px; font-weight: bold; color: #606266; }
|
|
|
+.divider-v { width: 1px; height: 24px; background: #ebeef5; margin: 0 16px; }
|
|
|
+
|
|
|
+/* 2. Sections */
|
|
|
+.summary-section { margin-bottom: 40px; }
|
|
|
+.sec-title {
|
|
|
+ font-size: 16px; font-weight: 700; color: #303133; margin-bottom: 16px;
|
|
|
+ display:flex; align-items:center; gap:8px;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+.sec-title .left { display: flex; align-items: center; gap: 8px; }
|
|
|
+.icon-box {
|
|
|
+ width: 28px; height: 28px; background: #ecf5ff; color: #409eff; border-radius: 6px;
|
|
|
+ display: flex; align-items: center; justify-content: center; font-size: 16px;
|
|
|
+}
|
|
|
+.icon-box.text-blue { background: #ecf5ff; color: #409eff; }
|
|
|
+.icon-box.text-orange { background: #fdf6ec; color: #e6a23c; }
|
|
|
+
|
|
|
+/* 3. Descriptions */
|
|
|
+.spacious-desc :deep(.el-descriptions__cell) { padding: 12px 16px!important; }
|
|
|
+.spacious-desc :deep(.el-descriptions__label) { width: 100px; color: #606266; font-weight: 500; background: #fafafa; }
|
|
|
+.flex-align { display: flex; align-items: center; }
|
|
|
+
|
|
|
+/* 4. Log Area */
|
|
|
+.main-log { background: #fff; }
|
|
|
+.log-content-box {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #ebeef5; border-radius: 8px;
|
|
|
+ padding: 24px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0,0,0,0.02);
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+.log-content-box::before {
|
|
|
+ content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: #e6a23c; border-top-left-radius: 8px; border-bottom-left-radius: 8px;
|
|
|
+}
|
|
|
+.log-text {
|
|
|
+ white-space: pre-wrap; font-family: 'Inter', system-ui, sans-serif; margin: 0; line-height: 1.8; font-size: 15px; color: #303133; text-align: justify;
|
|
|
+}
|
|
|
+.edit-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 12px; }
|
|
|
+
|
|
|
+/* 5. Footer */
|
|
|
+.summary-footer {
|
|
|
+ margin-top: 60px; padding-top: 24px; border-top: 1px solid #ebeef5;
|
|
|
+ display: flex; justify-content: space-between; align-items: center;
|
|
|
+}
|
|
|
+.footer-info { display: flex; gap: 32px; }
|
|
|
+.f-row { display: flex; flex-direction: column; gap: 4px; }
|
|
|
+.f-row .lbl { font-size: 12px; color: #909399; }
|
|
|
+.f-row .val { font-size: 15px; font-weight: 600; color: #303133; }
|
|
|
+.f-row .val.user-active { color: #409eff; }
|
|
|
+
|
|
|
+
|
|
|
+/* 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; }
|
|
|
+
|
|
|
+/* Route Graph */
|
|
|
+.route-graph { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; background: #f9f9f9; padding: 20px; border-radius: 6px; }
|
|
|
+.route-node { display: flex; gap: 10px; align-items: flex-start; background: #fff; padding: 12px; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.04); flex: 1; min-width: 200px; }
|
|
|
+.node-icon { width: 30px; height: 30px; border-radius: 50%; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; flex-shrink: 0; }
|
|
|
+.node-icon.pick { background: #409eff; }
|
|
|
+.node-icon.drop { background: #67c23a; }
|
|
|
+
|
|
|
+/* 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; }
|
|
|
+.node-icon.shop { background: #e6a23c; font-size: 16px; }
|
|
|
+.node-content { display: flex; flex-direction: column; line-height: 1.4; flex: 1; }
|
|
|
+.addr-t { font-weight: bold; font-size: 14px; color: #303133; margin-bottom: 4px; }
|
|
|
+.contact-t { font-size: 12px; color: #909399; }
|
|
|
+.time-t { font-size: 12px; color: #f56c6c; margin-top: 4px; font-weight: 500; }
|
|
|
+.route-arrow-lg { color: #c0c4cc; font-size: 20px; }
|
|
|
+
|
|
|
+/* 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>
|