|
|
@@ -0,0 +1,2572 @@
|
|
|
+<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 v-for="item in serviceTypeList" :key="item.id" :label="item.id">{{ item.name
|
|
|
+ }}</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="0" />
|
|
|
+ <el-tab-pane label="待接单" name="1" />
|
|
|
+ <el-tab-pane label="服务中" name="2" />
|
|
|
+ <el-tab-pane label="待商家确认" name="3" />
|
|
|
+ <el-tab-pane label="已完成" name="4" />
|
|
|
+ <el-tab-pane label="已取消" name="5" />
|
|
|
+ </el-tabs>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-table :data="tableData" style="width: 100%" v-loading="loading"
|
|
|
+ :header-cell-style="{ background: '#f5f7fa' }">
|
|
|
+ <el-table-column prop="code" label="订单号" width="170" fixed="left" />
|
|
|
+
|
|
|
+ <el-table-column label="服务类型" width="190">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="service-type-cell">
|
|
|
+ <el-tag>{{ getServiceName(row.service) }}</el-tag>
|
|
|
+ <el-tag v-if="getServiceModeTag(row)" class="sub-tag" type="warning" effect="plain">{{
|
|
|
+ getServiceModeTag(row) }}</el-tag>
|
|
|
+ <el-tag v-if="getServiceOrderTypeTag(row)" class="sub-tag" :type="getServiceOrderTypeTag(row).type"
|
|
|
+ effect="dark">{{ getServiceOrderTypeTag(row).label }}</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="customerName">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span style="font-weight: 500">{{ row.customerName }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="城市/区域" width="140">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div>{{ getCityDistrictText(row).city || '-' }}</div>
|
|
|
+ <div class="sub-text">{{ getCityDistrictText(row).district || '-' }}</div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="商户/下单人" min-width="160">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="merchant-info">
|
|
|
+ <div>{{ row.storeName }}</div>
|
|
|
+ <div class="sub-text" v-if="row.placerUsername">{{ row.placerUsername }}</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="'status-' + 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.price !== null && row.price !== undefined">¥{{ (row.price /
|
|
|
+ 100).toFixed(2) }}</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="row.status === 0" link type="success" size="small"
|
|
|
+ @click="openDispatchDialog(row)">派单</el-button>
|
|
|
+ <el-button v-if="[1, 2].includes(row.status)" link type="warning" size="small"
|
|
|
+ @click="openDispatchDialog(row)">重新派单</el-button>
|
|
|
+ <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small"
|
|
|
+ @click="handleCancel(row)">取消</el-button>
|
|
|
+
|
|
|
+ <el-dropdown v-if="[2, 3, 4].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 === 3" command="complete">确认完成</el-dropdown-item>
|
|
|
+ <el-dropdown-item v-if="[3, 4].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 === 0">
|
|
|
+ <el-button type="danger" plain icon="CircleClose" @click="handleCancel(currentOrder)">取消订单</el-button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="currentOrder.status === 3">
|
|
|
+ <el-button type="primary" icon="CircleCheck"
|
|
|
+ @click="handleCommand('complete', currentOrder)">确认完成</el-button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="[3, 4].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="[5, 4].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>
|
|
|
+
|
|
|
+ <!-- 护理小结侧滑栏 -->
|
|
|
+ <CareSummaryDrawer v-model:visible="careSummaryVisible" :order="careSummaryOrder"
|
|
|
+ @success="handleCareSummarySuccess" />
|
|
|
+
|
|
|
+ <!-- 奖惩弹窗 -->
|
|
|
+ <RewardDialog v-model:visible="rewardDialogVisible" :data="currentOperateRow" />
|
|
|
+
|
|
|
+ <!-- 备注弹窗 -->
|
|
|
+ <RemarkDialog v-model:visible="remarkDialogVisible" :data="currentOperateRow" />
|
|
|
+
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive, onMounted, computed } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import OrderDetailDrawer from './components/OrderDetailDrawer.vue'
|
|
|
+import RewardDialog from './components/RewardDialog.vue'
|
|
|
+import RemarkDialog from './components/RemarkDialog.vue'
|
|
|
+import CareSummaryDrawer from './components/CareSummaryDrawer.vue'
|
|
|
+import { listServiceOnOrder } from '@/api/service/list'
|
|
|
+import { listSubOrderOnMerchant } from '@/api/order/subOrder/index'
|
|
|
+import { listOnStore } from '@/api/system/areaStation'
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const detailVisible = ref(false)
|
|
|
+const dispatchVisible = ref(false)
|
|
|
+const currentOrder = ref(null)
|
|
|
+const serviceTypeList = ref([])
|
|
|
+const areaStationList = ref([])
|
|
|
+
|
|
|
+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: 0,
|
|
|
+ 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: 1,
|
|
|
+ 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: 2,
|
|
|
+ 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: 0,
|
|
|
+ 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: 2,
|
|
|
+ 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: 1,
|
|
|
+ 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: 5,
|
|
|
+ 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: 4,
|
|
|
+ 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: 3,
|
|
|
+ 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' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+const fetchServiceTypes = async () => {
|
|
|
+ try {
|
|
|
+ const res = await listServiceOnOrder()
|
|
|
+ serviceTypeList.value = res.data || []
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取服务类型失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const fetchAreaStations = async () => {
|
|
|
+ try {
|
|
|
+ const res = await listOnStore()
|
|
|
+ areaStationList.value = res.data || []
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取区域站点失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchServiceTypes()
|
|
|
+ fetchAreaStations()
|
|
|
+ handleSearch()
|
|
|
+})
|
|
|
+
|
|
|
+const handleSearch = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ content: filters.keyword || undefined,
|
|
|
+ service: filters.orderType || undefined,
|
|
|
+ status: filters.status || undefined,
|
|
|
+ pageNum: pagination.current,
|
|
|
+ pageSize: pagination.size
|
|
|
+ }
|
|
|
+ const res = await listSubOrderOnMerchant(params)
|
|
|
+ if (res) {
|
|
|
+ tableData.value = res.rows || []
|
|
|
+ pagination.total = res.total || 0
|
|
|
+ } else {
|
|
|
+ tableData.value = []
|
|
|
+ pagination.total = 0
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取订单列表失败:', error)
|
|
|
+ tableData.value = []
|
|
|
+ pagination.total = 0
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleSizeChange = (val) => {
|
|
|
+ pagination.size = val
|
|
|
+ handleSearch()
|
|
|
+}
|
|
|
+const handleCurrentChange = (val) => {
|
|
|
+ pagination.current = val
|
|
|
+ handleSearch()
|
|
|
+}
|
|
|
+
|
|
|
+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 getServiceName = (serviceId) => {
|
|
|
+ const service = serviceTypeList.value.find(s => s.id === serviceId)
|
|
|
+ return service ? service.name : '未知服务'
|
|
|
+}
|
|
|
+
|
|
|
+const getServiceModeTag = (row) => {
|
|
|
+ if (row.mode === 1 || row.mode === '1') {
|
|
|
+ return '往返'
|
|
|
+ }
|
|
|
+ return null
|
|
|
+}
|
|
|
+
|
|
|
+const getServiceOrderTypeTag = (row) => {
|
|
|
+ const t = row.type
|
|
|
+ if (t === 0 || t === '0' || t === 1 || t === '1') return null
|
|
|
+ if (t === 2 || t === '2') return { label: '接', type: 'primary' }
|
|
|
+ if (t === 3 || t === '3') return { label: '送', type: 'success' }
|
|
|
+ return null
|
|
|
+}
|
|
|
+
|
|
|
+const getCityDistrictText = (row) => {
|
|
|
+ if (!row.site) return { city: '-', district: '-' }
|
|
|
+
|
|
|
+ const findArea = (id) => {
|
|
|
+ return areaStationList.value.find(item =>
|
|
|
+ item.id === id || item.id === String(id) || String(item.id) === String(id)
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // site是站点ID,type=2
|
|
|
+ const station = findArea(row.site)
|
|
|
+ if (!station) return { city: '-', district: '-' }
|
|
|
+
|
|
|
+ // 站点的parentId是区域,type=1
|
|
|
+ const district = findArea(station.parentId)
|
|
|
+ if (!district) return { city: station.name || '-', district: '-' }
|
|
|
+
|
|
|
+ // 区域的parentId是城市,type=0
|
|
|
+ const city = findArea(district.parentId)
|
|
|
+
|
|
|
+ return {
|
|
|
+ city: city ? city.name : '-',
|
|
|
+ district: district.name || '-'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+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 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 !== 0 ? steps[0].time : '') // Mock if passed
|
|
|
+ if ([1, 2, 3, 4].includes(status)) active = 2
|
|
|
+
|
|
|
+ // 3. Accepted
|
|
|
+ steps[2].time = findTime('接单')
|
|
|
+ if (status === 1) {
|
|
|
+ steps[2].title = '待履约者接单'
|
|
|
+ } else if ([2, 3, 4].includes(status)) {
|
|
|
+ steps[2].title = '履约者已接单'
|
|
|
+ active = 3
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. Serving (Arrival/Start)
|
|
|
+ steps[3].time = findTime('到达') || findTime('出发')
|
|
|
+ if (status === 2) {
|
|
|
+ steps[3].title = '服务进行中'
|
|
|
+ } else if ([3, 4].includes(status)) {
|
|
|
+ steps[3].title = '服务已完成'
|
|
|
+ active = 4
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. Confirming
|
|
|
+ steps[4].time = findTime('等待商家确认') || findTime('待验收')
|
|
|
+ if (status === 3) {
|
|
|
+ steps[4].title = '待商家确认'
|
|
|
+ } else if (status === 4) {
|
|
|
+ steps[4].title = '商家已确认'
|
|
|
+ active = 5
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. Completed
|
|
|
+ if (status === 4) {
|
|
|
+ steps[5].time = findTime('完成') // or calculate based on logic
|
|
|
+ active = 6
|
|
|
+ }
|
|
|
+
|
|
|
+ if (status === 5) {
|
|
|
+ // 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) => {
|
|
|
+ const typeName = getServiceName(row?.service)
|
|
|
+ const isTransport = row?.mode === 1 || row?.mode === '1'
|
|
|
+ const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing'
|
|
|
+
|
|
|
+ currentOrder.value = {
|
|
|
+ ...row,
|
|
|
+ orderNo: row?.code || row?.orderCode || row?.orderNo || row?.orderNumber || row?.no || '',
|
|
|
+ type: row?.typeCode || row?.type || typeCode,
|
|
|
+ serviceItem: getServiceName(row?.service) || row?.serviceName || row?.service || '',
|
|
|
+ userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
|
|
|
+ address: '某小区5号楼2单元101',
|
|
|
+ groupBuyPackage: '',
|
|
|
+ transportType: row.splitType || row.transportType,
|
|
|
+ detail: {
|
|
|
+ ...row.detail,
|
|
|
+ pickTime: '2024-02-05 09:30',
|
|
|
+ pickAddr: row.detail?.pickAddr || '北京市朝阳区某小区5号楼2单元101',
|
|
|
+ pickContact: '李先生',
|
|
|
+ pickPhone: '13812345678',
|
|
|
+ dropTime: '2024-02-05 18:30',
|
|
|
+ dropAddr: row.detail?.dropAddr || '北京市朝阳区某小区5号楼2单元101',
|
|
|
+ dropContact: '李先生',
|
|
|
+ dropPhone: '13812345678',
|
|
|
+ packageName: row.detail?.packageName || '精细洗护套餐A',
|
|
|
+ petStatus: '胆小,需安抚',
|
|
|
+ area: '北京市朝阳区某小区5号楼2单元101'
|
|
|
+ },
|
|
|
+ petGender: 'male',
|
|
|
+ petAge: '2岁',
|
|
|
+ petWeight: '15kg',
|
|
|
+ petVaccine: '已接种',
|
|
|
+ petSterilized: true,
|
|
|
+ petCharacter: '活泼好动,喜欢球类玩具',
|
|
|
+ petHealth: '健康良好',
|
|
|
+ fulfillerAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
|
|
+ fulfillerPhone: '13812345678',
|
|
|
+ fulfillerStation: '朝阳服务站',
|
|
|
+ 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 ([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 = []
|
|
|
+
|
|
|
+ // --- 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 ([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 {
|
|
|
+ // 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 ([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' }
|
|
|
+ ]
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- Step 5: Wait Confirm (Only if Confirming or Completed) ---
|
|
|
+ if ([3, 4].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 === 4) {
|
|
|
+ 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: ([1, 2].includes(row.status)) ? 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 = 1
|
|
|
+ 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 handleCareSummarySuccess = (summaryData) => {
|
|
|
+ if (careSummaryOrder.value) {
|
|
|
+ careSummaryOrder.value.careSummary = summaryData
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+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: '海鲜'
|
|
|
+ }
|
|
|
+ careSummaryVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// Reward / Punish Logic
|
|
|
+const rewardDialogVisible = ref(false)
|
|
|
+const remarkDialogVisible = ref(false)
|
|
|
+const currentOperateRow = ref(null)
|
|
|
+
|
|
|
+const openRewardDialog = (row) => {
|
|
|
+ currentOperateRow.value = row
|
|
|
+ rewardDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const openRemarkDialog = (row) => {
|
|
|
+ currentOperateRow.value = row
|
|
|
+ remarkDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+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 = 4
|
|
|
+ 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.status-0 {
|
|
|
+ background-color: #f56c6c;
|
|
|
+ box-shadow: 0 0 4px rgba(245, 108, 108, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+.status-dot.status-1 {
|
|
|
+ background-color: #e6a23c;
|
|
|
+}
|
|
|
+
|
|
|
+.status-dot.status-2 {
|
|
|
+ background-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.status-dot.status-3 {
|
|
|
+ background-color: #bf24e8;
|
|
|
+}
|
|
|
+
|
|
|
+.status-dot.status-4 {
|
|
|
+ background-color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.status-dot.status-5 {
|
|
|
+ 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;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+/* Removed Enhanced Care Summary Styles */
|
|
|
+
|
|
|
+/* 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>
|