|
|
@@ -1,2572 +0,0 @@
|
|
|
-<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>
|