|
|
@@ -41,338 +41,36 @@
|
|
|
|
|
|
<!-- Right: Dispatch Control Panel -->
|
|
|
<div class="right-panel">
|
|
|
- <!-- 1. Order Management Section -->
|
|
|
- <div class="panel-section order-mgmt">
|
|
|
- <div class="sec-header">
|
|
|
- <span class="tit">订单</span>
|
|
|
- <!-- Right Aligned Tabs -->
|
|
|
- <div class="header-right-tabs">
|
|
|
- <span class="h-tab-item" :class="{ active: currentOrderTab === 'PendingDispatch' }" @click="currentOrderTab = 'PendingDispatch'">
|
|
|
- <span class="txt">待派单</span>
|
|
|
- <span class="num danger">{{ orderStats.pendingDispatch }}</span>
|
|
|
- </span>
|
|
|
- <span class="h-tab-item" :class="{ active: currentOrderTab === 'PendingAccept' }" @click="currentOrderTab = 'PendingAccept'">
|
|
|
- <span class="txt">待接单</span>
|
|
|
- <span class="num warning">{{ orderStats.pendingAccept }}</span>
|
|
|
- </span>
|
|
|
- <span class="h-tab-item" :class="{ active: currentOrderTab === 'Processing' }" @click="currentOrderTab = 'Processing'">
|
|
|
- <span class="txt">进行中</span>
|
|
|
- <span class="num primary">{{ orderStats.processing }}</span>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- Order List -->
|
|
|
- <div class="list-wrapper">
|
|
|
- <el-scrollbar>
|
|
|
- <div v-if="filteredOrders.length === 0" class="empty-state">暂无数据</div>
|
|
|
- <div v-else v-for="order in filteredOrders" :key="order.id" class="list-card order-card" @click="focusMapPoint(order.lng, order.lat)">
|
|
|
- <div class="card-left">
|
|
|
- <div class="type-tag" :class="order.typeCode">
|
|
|
- {{ getShortType(order.typeCode) }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="card-main">
|
|
|
- <template v-if="order.typeCode === 'transport'">
|
|
|
- <div class="row-addr" :title="order.pickAddr"><span class="tag pick">取</span> {{ order.pickAddr }}</div>
|
|
|
- <div class="row-addr" :title="order.dropAddr"><span class="tag drop">送</span> {{ order.dropAddr }}</div>
|
|
|
- <div class="row-time">
|
|
|
- <el-icon><Clock /></el-icon> {{ order.time }}
|
|
|
- <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <template v-else>
|
|
|
- <div class="row-addr" :title="order.address"><span class="tag home">址</span> {{ order.address }}</div>
|
|
|
- <div class="row-time" style="margin-top: 4px">
|
|
|
- <el-icon><Clock /></el-icon> {{ order.time }}
|
|
|
- <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- Right: Status & Actions -->
|
|
|
- <div class="card-right">
|
|
|
- <el-tag size="small" :type="getOrderStatusType(order.status)" effect="plain">{{ getOrderStatusText(order.status) }}</el-tag>
|
|
|
- <div class="actions">
|
|
|
- <el-button v-if="order.status === 'pending_dispatch'" type="primary" size="small" @click.stop="openDispatchDialog(order)"
|
|
|
- >派单</el-button
|
|
|
- >
|
|
|
- <el-button
|
|
|
- v-else-if="['pending_accept', 'processing'].includes(order.status)"
|
|
|
- type="primary"
|
|
|
- size="small"
|
|
|
- plain
|
|
|
- @click.stop="openDispatchDialog(order)"
|
|
|
- >重新派单</el-button
|
|
|
- >
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-scrollbar>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 2. Fulfiller Management Section -->
|
|
|
- <div class="panel-section fulfiller-mgmt">
|
|
|
- <div class="sec-header no-border">
|
|
|
- <span class="tit">履约者</span>
|
|
|
- <!-- Right Aligned Tabs -->
|
|
|
- <div class="header-right-tabs">
|
|
|
- <span class="h-tab-item" :class="{ active: currentRiderTab === 'All' }" @click="currentRiderTab = 'All'">
|
|
|
- <span class="txt">全部</span>
|
|
|
- <span class="num">12</span>
|
|
|
- </span>
|
|
|
- <span class="h-tab-item" :class="{ active: currentRiderTab === 'Working' }" @click="currentRiderTab = 'Working'">
|
|
|
- <span class="txt">接单中</span>
|
|
|
- <span class="num success">8</span>
|
|
|
- </span>
|
|
|
- <span class="h-tab-item" :class="{ active: currentRiderTab === 'Resting' }" @click="currentRiderTab = 'Resting'">
|
|
|
- <span class="txt">休息中</span>
|
|
|
- <span class="num info">3</span>
|
|
|
- </span>
|
|
|
- <span class="h-tab-item" :class="{ active: currentRiderTab === 'Disabled' }" @click="currentRiderTab = 'Disabled'">
|
|
|
- <span class="txt">禁用</span>
|
|
|
- <span class="num danger">1</span>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- Rider List -->
|
|
|
- <div class="list-wrapper">
|
|
|
- <el-scrollbar>
|
|
|
- <div v-for="rider in filteredRiders" :key="rider.id" class="list-card rider-card" @click="focusMapPoint(rider.lng, rider.lat)">
|
|
|
- <div class="card-left relative">
|
|
|
- <el-avatar :src="rider.avatar" :size="40" />
|
|
|
- <div class="dot" :class="rider.status"></div>
|
|
|
- </div>
|
|
|
- <div class="card-main">
|
|
|
- <!-- Box 1: Name + Phone + Status (Right) -->
|
|
|
- <div class="row-1" style="justify-content: space-between; align-items: flex-start">
|
|
|
- <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>
|
|
|
-
|
|
|
- <!-- Box 2: Categories -->
|
|
|
- <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>
|
|
|
-
|
|
|
- <!-- Box 3: Last Service Time -->
|
|
|
- <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>
|
|
|
- <div class="card-right-stats">
|
|
|
- <div class="stat-box">
|
|
|
- <span class="lbl">待接</span>
|
|
|
- <span class="val danger">{{ rider.pendingCount }}</span>
|
|
|
- </div>
|
|
|
- <div class="stat-box">
|
|
|
- <span class="lbl">待服</span>
|
|
|
- <span class="val warning">{{ rider.todoCount }}</span>
|
|
|
- </div>
|
|
|
- <el-button link type="primary" size="small" style="margin-top: 4px; padding: 0" @click.stop="handleViewRiderOrders(rider)"
|
|
|
- >查看订单</el-button
|
|
|
- >
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-scrollbar>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <OrderListPanel
|
|
|
+ v-model="currentOrderTab"
|
|
|
+ :orders="filteredOrders"
|
|
|
+ :stats="orderStats"
|
|
|
+ @focus="focusMapPoint"
|
|
|
+ @dispatch="openDispatchDialog"
|
|
|
+ />
|
|
|
+
|
|
|
+ <RiderListPanel
|
|
|
+ v-model="currentRiderTab"
|
|
|
+ :riders="filteredRiders"
|
|
|
+ @focus="focusMapPoint"
|
|
|
+ @view-orders="handleViewRiderOrders"
|
|
|
+ />
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Rider Orders Dialog -->
|
|
|
- <el-dialog v-model="riderOrdersVisible" title="履约者订单详情" width="1100px" top="6vh" custom-class="rider-orders-dialog">
|
|
|
- <div class="dialog-content">
|
|
|
- <!-- 1. Rider Summary Info -->
|
|
|
- <div class="rider-summary" v-if="currentRiderInfo">
|
|
|
- <div class="summary-item">
|
|
|
- <span class="lbl">姓名:</span>
|
|
|
- <span class="val bold">{{ currentRiderInfo.name }}</span>
|
|
|
- </div>
|
|
|
- <div class="summary-item">
|
|
|
- <span class="lbl">手机号:</span>
|
|
|
- <span class="val">{{ currentRiderInfo.maskPhone }}</span>
|
|
|
- </div>
|
|
|
- <div class="summary-item">
|
|
|
- <span class="lbl">所属区域:</span>
|
|
|
- <span class="val">{{ currentRiderInfo.station }}</span>
|
|
|
- </div>
|
|
|
- <div class="summary-item">
|
|
|
- <span class="lbl">工作类型:</span>
|
|
|
- <span class="val">{{ currentRiderInfo.workType }}</span>
|
|
|
- </div>
|
|
|
- <div class="summary-item" style="flex: 1">
|
|
|
- <span class="lbl">服务类目:</span>
|
|
|
- <el-tag v-for="tag in currentRiderInfo.categories" :key="tag" size="small" effect="plain" style="margin-right: 4px">{{ tag }}</el-tag>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 2. Orders Table -->
|
|
|
- <div class="rider-orders-list">
|
|
|
- <el-table :data="currentRiderOrders" stripe style="width: 100%" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
|
|
|
- <el-table-column prop="orderNo" label="订单号" width="150" />
|
|
|
- <el-table-column prop="serviceName" label="服务项目" width="120" show-overflow-tooltip />
|
|
|
- <el-table-column prop="petName" label="服务宠物" width="100" />
|
|
|
- <el-table-column prop="customerName" label="客户" width="100" />
|
|
|
- <el-table-column prop="address" label="服务详细地址" min-width="200" show-overflow-tooltip />
|
|
|
- <el-table-column prop="time" label="预约时间" width="160" />
|
|
|
- <el-table-column label="状态" width="100">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-tag v-if="row.status === 'pending_accept'" type="warning" size="small">待接单</el-tag>
|
|
|
- <el-tag v-else-if="row.status === 'pending_service'" type="primary" size="small">待服务</el-tag>
|
|
|
- <el-tag v-else type="info" size="small">{{ row.status }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-dialog>
|
|
|
-
|
|
|
- <!-- Dispatch Dialog -->
|
|
|
- <el-dialog v-model="dispatchDialogVisible" title="派单调度" width="900px" top="5vh" destroy-on-close append-to-body>
|
|
|
- <div class="dispatch-dialog-content">
|
|
|
- <!-- Top: Order Info -->
|
|
|
- <div class="dispatch-order-info box-card" 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" :title="currentDispatchOrder.pickAddr">
|
|
|
- <span class="tag pick">取</span> {{ currentDispatchOrder.pickAddr }}
|
|
|
- </div>
|
|
|
- <div class="row-addr" :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 (For Re-dispatch) -->
|
|
|
- <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">
|
|
|
- <!-- Reusing Rider Card Layout -->
|
|
|
- <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">
|
|
|
- <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">
|
|
|
- <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>
|
|
|
+ <RiderOrdersDialog
|
|
|
+ v-model="riderOrdersVisible"
|
|
|
+ :riderInfo="currentRiderInfo"
|
|
|
+ :orders="currentRiderOrders"
|
|
|
+ />
|
|
|
+
|
|
|
+ <DispatchDialog
|
|
|
+ v-model="dispatchDialogVisible"
|
|
|
+ :order="currentDispatchOrder"
|
|
|
+ :currentRider="currentRider"
|
|
|
+ :ridersList="ridersList"
|
|
|
+ @submit="handleDispatchSubmit"
|
|
|
+ />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -380,53 +78,16 @@
|
|
|
import { ref, computed, reactive, onMounted, watch } from 'vue';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
|
|
|
-// --- Data & State ---
|
|
|
-const riderOrdersVisible = ref(false);
|
|
|
-const currentRiderOrders = ref([]);
|
|
|
-const currentRiderInfo = ref(null);
|
|
|
+import OrderListPanel from './components/OrderListPanel.vue';
|
|
|
+import RiderListPanel from './components/RiderListPanel.vue';
|
|
|
+import RiderOrdersDialog from './components/RiderOrdersDialog.vue';
|
|
|
+import DispatchDialog from './components/DispatchDialog.vue';
|
|
|
|
|
|
-const handleViewRiderOrders = (rider) => {
|
|
|
- // Mock Rider Info
|
|
|
- currentRiderInfo.value = {
|
|
|
- name: rider.name,
|
|
|
- maskPhone: rider.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'),
|
|
|
- station: rider.station + ' - 东城片区',
|
|
|
- workType: '全职专送',
|
|
|
- categories: ['宠物接送', '上门喂遛', '洗护套餐']
|
|
|
- };
|
|
|
+// Mock Data
|
|
|
+import dispatchMockData from '@/mock/dispatch.json';
|
|
|
+import riderOrdersMockData from '@/mock/RiderOrdersDialog.json';
|
|
|
|
|
|
- // Mock Data for specific rider
|
|
|
- currentRiderOrders.value = [
|
|
|
- {
|
|
|
- orderNo: 'ORD20240205001',
|
|
|
- serviceName: '上门喂遛',
|
|
|
- petName: '金毛(大黄)',
|
|
|
- customerName: '张先生',
|
|
|
- address: '北京市朝阳区三里屯SOHO A座 1202',
|
|
|
- time: '2024-02-05 14:00',
|
|
|
- status: 'pending_accept'
|
|
|
- },
|
|
|
- {
|
|
|
- orderNo: 'ORD20240205002',
|
|
|
- serviceName: '宠物接送',
|
|
|
- petName: '布偶(咪咪)',
|
|
|
- customerName: '李女士',
|
|
|
- address: '北京市朝阳区国贸三期 35层',
|
|
|
- time: '2024-02-05 16:30',
|
|
|
- status: 'pending_service'
|
|
|
- },
|
|
|
- {
|
|
|
- orderNo: 'ORD20240205003',
|
|
|
- serviceName: '洗护套餐',
|
|
|
- petName: '泰迪(可乐)',
|
|
|
- customerName: '王小姐',
|
|
|
- address: '北京市海淀区中关村软件园 5号楼',
|
|
|
- time: '2024-02-06 10:00',
|
|
|
- status: 'pending_service'
|
|
|
- }
|
|
|
- ];
|
|
|
- riderOrdersVisible.value = true;
|
|
|
-};
|
|
|
+// --- Data & State ---
|
|
|
const filters = reactive({
|
|
|
orderType: 'all',
|
|
|
city: 'beijing',
|
|
|
@@ -437,180 +98,29 @@ const currentOrderTab = ref('PendingDispatch');
|
|
|
const currentRiderTab = ref('All');
|
|
|
const activeMapFilter = ref('all');
|
|
|
|
|
|
-// Mock Stats
|
|
|
-const orderStats = reactive({
|
|
|
- pendingDispatch: 3,
|
|
|
- pendingAccept: 4,
|
|
|
- processing: 8,
|
|
|
- delivering: 2,
|
|
|
- merchants: 16
|
|
|
-});
|
|
|
-
|
|
|
-// Real Params (Beijing Chaoyang Area)
|
|
|
-// Center: 116.4551, 39.9255
|
|
|
+const ordersList = ref(dispatchMockData.ordersList);
|
|
|
+const ridersList = ref(dispatchMockData.ridersList);
|
|
|
+const merchantList = ref(dispatchMockData.merchantList);
|
|
|
+const orderStats = reactive(dispatchMockData.stats);
|
|
|
|
|
|
-// Mock Orders Data (With Real Coords)
|
|
|
-const ordersList = ref([
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- type: '宠物接送',
|
|
|
- typeCode: 'transport',
|
|
|
- status: 'pending_dispatch',
|
|
|
- pickAddr: '北京市朝阳区三里屯SOHO A座',
|
|
|
- dropAddr: '北京市朝阳区朝阳大悦城',
|
|
|
- time: '2024-02-07 14:30',
|
|
|
- daysLater: '今天',
|
|
|
- lng: 116.4552,
|
|
|
- lat: 39.9338
|
|
|
- },
|
|
|
- {
|
|
|
- id: 2,
|
|
|
- type: '上门喂遛',
|
|
|
- typeCode: 'feeding',
|
|
|
- status: 'pending_accept',
|
|
|
- address: '北京市海淀区中关村软件园',
|
|
|
- riderId: 101,
|
|
|
- time: '2024-02-08 15:00',
|
|
|
- daysLater: '明天',
|
|
|
- lng: 116.298,
|
|
|
- lat: 40.044
|
|
|
- },
|
|
|
- {
|
|
|
- id: 3,
|
|
|
- type: '上门洗护',
|
|
|
- typeCode: 'washing',
|
|
|
- status: 'processing',
|
|
|
- address: '北京市朝阳区望京SOHO T3',
|
|
|
- riderId: 102,
|
|
|
- time: '2024-02-07 10:00',
|
|
|
- daysLater: '进行中',
|
|
|
- lng: 116.486,
|
|
|
- lat: 39.998
|
|
|
- },
|
|
|
- {
|
|
|
- id: 4,
|
|
|
- type: '宠物接送',
|
|
|
- typeCode: 'transport',
|
|
|
- status: 'completed',
|
|
|
- pickAddr: '北京市通州区万达广场',
|
|
|
- dropAddr: '北京市通州区北苑',
|
|
|
- time: '2024-02-06 09:00',
|
|
|
- daysLater: '',
|
|
|
- lng: 116.643,
|
|
|
- lat: 39.905
|
|
|
- },
|
|
|
- {
|
|
|
- id: 5,
|
|
|
- type: '上门喂遛',
|
|
|
- typeCode: 'feeding',
|
|
|
- status: 'pending_dispatch',
|
|
|
- address: '北京市朝阳区朝阳公园南门',
|
|
|
- time: '2024-02-09 16:00',
|
|
|
- daysLater: '2天后',
|
|
|
- lng: 116.488,
|
|
|
- lat: 39.932
|
|
|
- },
|
|
|
- {
|
|
|
- id: 6,
|
|
|
- type: '宠物接送',
|
|
|
- typeCode: 'transport',
|
|
|
- status: 'pending_dispatch',
|
|
|
- pickAddr: '北京市朝阳区国贸三期',
|
|
|
- dropAddr: '北京市朝阳区双井富力城',
|
|
|
- time: '2024-02-07 16:30',
|
|
|
- daysLater: '今天',
|
|
|
- lng: 116.463,
|
|
|
- lat: 39.907
|
|
|
- }
|
|
|
-]);
|
|
|
-
|
|
|
-// Mock Riders Data (With Real Coords)
|
|
|
-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.46,
|
|
|
- lat: 39.92
|
|
|
- },
|
|
|
- {
|
|
|
- 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.45,
|
|
|
- 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.47,
|
|
|
- lat: 39.93
|
|
|
- },
|
|
|
- {
|
|
|
- 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.44,
|
|
|
- lat: 39.91
|
|
|
- },
|
|
|
- {
|
|
|
- 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.48,
|
|
|
- lat: 39.925
|
|
|
- }
|
|
|
-]);
|
|
|
+// Rider Orders State
|
|
|
+const riderOrdersVisible = ref(false);
|
|
|
+const currentRiderInfo = ref(null);
|
|
|
+const currentRiderOrders = ref([]);
|
|
|
|
|
|
-// Mock Merchants (Static Coords)
|
|
|
-const merchantList = ref([
|
|
|
- { id: 201, name: '萌它宠物', orders: 5, lng: 116.453, lat: 39.923, icon: '' },
|
|
|
- { id: 202, name: '宠爱国际', orders: 0, lng: 116.465, lat: 39.928, icon: '' },
|
|
|
- { id: 203, name: '顽皮狗', orders: 8, lng: 116.448, lat: 39.918, icon: '' }
|
|
|
-]);
|
|
|
+const handleViewRiderOrders = (rider) => {
|
|
|
+ currentRiderInfo.value = {
|
|
|
+ name: rider.name,
|
|
|
+ maskPhone: rider.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'),
|
|
|
+ station: rider.station + ' - 东城片区',
|
|
|
+ workType: '全职专送',
|
|
|
+ categories: ['宠物接送', '上门喂遛', '洗护套餐']
|
|
|
+ };
|
|
|
+ currentRiderOrders.value = riderOrdersMockData.orders;
|
|
|
+ riderOrdersVisible.value = true;
|
|
|
+};
|
|
|
|
|
|
-// --- Map Logic ---
|
|
|
+// Map Logic
|
|
|
let map = null;
|
|
|
const ak = 'E4805d16520de693a3fe707cdc962045'; // Public Key
|
|
|
|
|
|
@@ -658,12 +168,12 @@ const refreshMarkers = () => {
|
|
|
<div style="background:#fff; border-radius:8px; padding: 12px; display: flex; align-items: center; gap: 10px; position: relative;">
|
|
|
<!-- Close Icon -->
|
|
|
<div style="position: absolute; top: 4px; right: 8px; color: #999; font-size: 16px; cursor: pointer;">×</div>
|
|
|
-
|
|
|
+
|
|
|
<!-- Icon Ring -->
|
|
|
<div style="width: 48px; height: 48px; border-radius: 50%; border: 4px solid #F56C6C; padding: 2px; flex-shrink: 0; box-sizing: border-box; display: flex; align-items: center; justify-content: center;">
|
|
|
<img src="${iconImg}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">
|
|
|
</div>
|
|
|
-
|
|
|
+
|
|
|
<!-- Info -->
|
|
|
<div style="flex: 1; overflow: hidden;">
|
|
|
<div style="font-weight: bold; font-size: 14px; color: #333; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${m.name}</div>
|
|
|
@@ -695,12 +205,12 @@ const refreshMarkers = () => {
|
|
|
<div style="background:#fff; border-radius:8px; padding: 10px; display: flex; align-items: center; gap: 10px; position: relative;">
|
|
|
<!-- Close Icon -->
|
|
|
<div style="position: absolute; top: 2px; right: 6px; color: #999; font-size: 14px; cursor: pointer;">×</div>
|
|
|
-
|
|
|
+
|
|
|
<!-- Avatar Ring -->
|
|
|
<div style="width: 44px; height: 44px; border-radius: 50%; border: 3px solid ${borderColor}; padding: 1px; flex-shrink: 0; box-sizing: border-box;">
|
|
|
<img src="${avatar}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">
|
|
|
</div>
|
|
|
-
|
|
|
+
|
|
|
<!-- Info -->
|
|
|
<div style="flex: 1; overflow: hidden;">
|
|
|
<div style="font-weight: bold; font-size: 13px; color: #333; margin-bottom: 2px;">[履约者]${r.name}</div>
|
|
|
@@ -749,7 +259,6 @@ watch(activeMapFilter, () => {
|
|
|
refreshMarkers();
|
|
|
});
|
|
|
|
|
|
-// Also refresh when tab changes if logic depends on it (e.g. only showing active orders)
|
|
|
watch([currentOrderTab, currentRiderTab], () => {
|
|
|
refreshMarkers();
|
|
|
});
|
|
|
@@ -765,68 +274,33 @@ onMounted(() => {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
+// Dispatch Dialog State
|
|
|
const dispatchDialogVisible = ref(false);
|
|
|
const currentDispatchOrder = ref(null);
|
|
|
const currentRider = ref(null);
|
|
|
-const dispatchSearchQuery = ref('');
|
|
|
-const selectedRiderId = ref(null);
|
|
|
-const dispatchFee = ref(0);
|
|
|
|
|
|
const openDispatchDialog = (order) => {
|
|
|
currentDispatchOrder.value = order;
|
|
|
- dispatchDialogVisible.value = true;
|
|
|
- dispatchSearchQuery.value = '';
|
|
|
- selectedRiderId.value = null;
|
|
|
- dispatchFee.value = 0;
|
|
|
-
|
|
|
- // Find current rider for re-dispatch
|
|
|
if (order.riderId) {
|
|
|
currentRider.value = ridersList.value.find((r) => r.id === order.riderId) || null;
|
|
|
} else {
|
|
|
currentRider.value = null;
|
|
|
}
|
|
|
+ dispatchDialogVisible.value = true;
|
|
|
};
|
|
|
|
|
|
-const filteredDispatchRiders = computed(() => {
|
|
|
- // User said "Select Accepting Riders". Usually means "Online/Busy".
|
|
|
- // But the main list logic for "Working" includes online and busy.
|
|
|
- // Let's filter for riders who are NOT disabled or offline?
|
|
|
- // Or just show all eligible. Let's show 'online' and 'busy'.
|
|
|
- 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));
|
|
|
- }
|
|
|
- // Sort by Next Order Time
|
|
|
- // Mock sort: just string compare for now since format is YYYY-MM-DD HH:mm
|
|
|
- result.sort((a, b) => {
|
|
|
- return a.lastServiceTime.localeCompare(b.lastServiceTime);
|
|
|
- });
|
|
|
- return result;
|
|
|
-});
|
|
|
-
|
|
|
-const handleDispatchSubmit = () => {
|
|
|
- if (!selectedRiderId.value) {
|
|
|
- ElMessage.warning('请选择履约者');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (!dispatchFee.value) {
|
|
|
- ElMessage.warning('请输入服务费用');
|
|
|
- return;
|
|
|
- }
|
|
|
+const handleDispatchSubmit = (data) => {
|
|
|
dispatchDialogVisible.value = false;
|
|
|
ElMessage.success('派单成功');
|
|
|
|
|
|
- // update status locally
|
|
|
if (currentDispatchOrder.value && currentDispatchOrder.value.status === 'pending_dispatch') {
|
|
|
const idx = ordersList.value.findIndex((o) => o.id === currentDispatchOrder.value.id);
|
|
|
if (idx !== -1) ordersList.value[idx].status = 'pending_accept';
|
|
|
}
|
|
|
};
|
|
|
+
|
|
|
const filteredOrders = computed(() => {
|
|
|
let result = ordersList.value;
|
|
|
- // Tab Filter
|
|
|
const statusMap = {
|
|
|
'PendingDispatch': 'pending_dispatch',
|
|
|
'PendingAccept': 'pending_accept',
|
|
|
@@ -836,7 +310,6 @@ const filteredOrders = computed(() => {
|
|
|
if (targetStatus) {
|
|
|
result = result.filter((o) => o.status === targetStatus);
|
|
|
}
|
|
|
- // Type Filter
|
|
|
if (filters.orderType !== 'all') {
|
|
|
result = result.filter((o) => o.typeCode === filters.orderType);
|
|
|
}
|
|
|
@@ -853,29 +326,6 @@ const filteredRiders = computed(() => {
|
|
|
const allowed = map[currentRiderTab.value] || [];
|
|
|
return ridersList.value.filter((r) => allowed.includes(r.status));
|
|
|
});
|
|
|
-
|
|
|
-// --- Helpers ---
|
|
|
-const getShortType = (code) => {
|
|
|
- const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' };
|
|
|
- return map[code] || '订单';
|
|
|
-};
|
|
|
-const getOrderTabLabel = (key) => map[key]; // Simplified in template
|
|
|
-const getOrderStatusText = (status) => {
|
|
|
- const map = { 'pending_dispatch': '待派单', 'pending_accept': '待接单', 'processing': '进行中', 'completed': '已完成' };
|
|
|
- return map[status];
|
|
|
-};
|
|
|
-const getOrderStatusType = (status) => {
|
|
|
- const map = { 'pending_dispatch': 'danger', 'pending_accept': 'warning', 'processing': 'primary', 'completed': 'success' };
|
|
|
- return map[status];
|
|
|
-};
|
|
|
-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] || '';
|
|
|
-};
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
@@ -984,560 +434,4 @@ const getCategoryClass = (cat) => {
|
|
|
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.05);
|
|
|
z-index: 30;
|
|
|
}
|
|
|
-.panel-section {
|
|
|
- flex: 1;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-.order-mgmt {
|
|
|
- border-bottom: 8px solid #f5f7fa;
|
|
|
- flex: default;
|
|
|
- height: 50%;
|
|
|
-}
|
|
|
-.fulfiller-mgmt {
|
|
|
- flex: default;
|
|
|
- height: 50%;
|
|
|
-}
|
|
|
-
|
|
|
-.sec-header {
|
|
|
- height: 48px;
|
|
|
- padding: 0 16px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- border-bottom: 1px solid #f0f0f0;
|
|
|
-}
|
|
|
-.sec-header.no-border {
|
|
|
- border-bottom: none;
|
|
|
- height: 40px;
|
|
|
-}
|
|
|
-.sec-header .tit {
|
|
|
- font-weight: bold;
|
|
|
- font-size: 15px;
|
|
|
- color: #1f2f3d;
|
|
|
-}
|
|
|
-
|
|
|
-/* New Header Tabs Style */
|
|
|
-.header-tabs-wrapper {
|
|
|
- display: flex;
|
|
|
- padding: 0 16px 8px;
|
|
|
- gap: 12px;
|
|
|
- border-bottom: 1px solid #f5f7fa;
|
|
|
-}
|
|
|
-.header-tab {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- cursor: pointer;
|
|
|
- padding: 4px 8px;
|
|
|
- border-radius: 6px;
|
|
|
- transition: all 0.2s;
|
|
|
- min-width: 60px;
|
|
|
-}
|
|
|
-.header-tab:hover {
|
|
|
- background: #f5f7fa;
|
|
|
-}
|
|
|
-.header-tab.active {
|
|
|
- background: #ecf5ff;
|
|
|
-}
|
|
|
-.header-tab.active .lbl {
|
|
|
- color: #409eff;
|
|
|
- font-weight: bold;
|
|
|
-}
|
|
|
-
|
|
|
-.header-tab .num {
|
|
|
- font-size: 16px;
|
|
|
- font-weight: bold;
|
|
|
- line-height: 1.2;
|
|
|
-}
|
|
|
-.header-tab .lbl {
|
|
|
- font-size: 12px;
|
|
|
- color: #909399;
|
|
|
- margin-top: 2px;
|
|
|
-}
|
|
|
-
|
|
|
-.header-tab .num.danger {
|
|
|
- color: #f56c6c;
|
|
|
-}
|
|
|
-.header-tab .num.warning {
|
|
|
- color: #e6a23c;
|
|
|
-}
|
|
|
-.header-tab .num.primary {
|
|
|
- color: #409eff;
|
|
|
-}
|
|
|
-.header-tab .num.success {
|
|
|
- color: #67c23a;
|
|
|
-}
|
|
|
-
|
|
|
-.fulfiller-tabs .header-tab {
|
|
|
- flex-direction: row;
|
|
|
- gap: 4px;
|
|
|
- min-width: auto;
|
|
|
-}
|
|
|
-.fulfiller-tabs .header-tab .num {
|
|
|
- font-size: 14px;
|
|
|
-}
|
|
|
-.fulfiller-tabs .header-tab .lbl {
|
|
|
- margin-top: 0;
|
|
|
-}
|
|
|
-
|
|
|
-/* Header Right Tabs (Orders) */
|
|
|
-.header-right-tabs {
|
|
|
- display: flex;
|
|
|
- gap: 4px;
|
|
|
-}
|
|
|
-.h-tab-item {
|
|
|
- display: flex;
|
|
|
- flex-direction: row;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- cursor: pointer;
|
|
|
- position: relative;
|
|
|
- padding: 6px 12px;
|
|
|
- gap: 6px;
|
|
|
- border-radius: 4px;
|
|
|
- transition: all 0.2s;
|
|
|
- color: #606266;
|
|
|
-}
|
|
|
-.h-tab-item:hover {
|
|
|
- background: #f5f7fa;
|
|
|
-}
|
|
|
-.h-tab-item.active {
|
|
|
- background: #ecf5ff;
|
|
|
-}
|
|
|
-.h-tab-item.active .txt {
|
|
|
- color: #409eff;
|
|
|
- font-weight: bold;
|
|
|
-}
|
|
|
-.h-tab-item.active::after {
|
|
|
- display: none;
|
|
|
-}
|
|
|
-
|
|
|
-.h-tab-item .txt {
|
|
|
- font-size: 14px;
|
|
|
-}
|
|
|
-.h-tab-item .num {
|
|
|
- font-size: 14px;
|
|
|
- font-weight: bold;
|
|
|
- margin-bottom: 0;
|
|
|
-}
|
|
|
-.h-tab-item .num.danger {
|
|
|
- color: #f56c6c;
|
|
|
-}
|
|
|
-.h-tab-item .num.warning {
|
|
|
- color: #e6a23c;
|
|
|
-}
|
|
|
-.h-tab-item .num.primary {
|
|
|
- color: #409eff;
|
|
|
-}
|
|
|
-
|
|
|
-/* List */
|
|
|
-.list-wrapper {
|
|
|
- flex: 1;
|
|
|
- overflow: hidden;
|
|
|
- padding: 8px 12px;
|
|
|
-}
|
|
|
-.empty-state {
|
|
|
- text-align: center;
|
|
|
- color: #909399;
|
|
|
- padding: 20px;
|
|
|
- font-size: 13px;
|
|
|
-}
|
|
|
-
|
|
|
-.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-1 {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
-}
|
|
|
-.o-type {
|
|
|
- font-weight: bold;
|
|
|
- font-size: 14px;
|
|
|
- color: #303133;
|
|
|
-}
|
|
|
-.row-2 {
|
|
|
- font-size: 12px;
|
|
|
- color: #606266;
|
|
|
- white-space: nowrap;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
-}
|
|
|
-.row-3 {
|
|
|
- font-size: 12px;
|
|
|
- color: #909399;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.card-right {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: flex-end;
|
|
|
- justify-content: center;
|
|
|
- gap: 8px;
|
|
|
- margin-left: 8px;
|
|
|
- flex-shrink: 0;
|
|
|
-}
|
|
|
-.card-right .actions {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.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-station {
|
|
|
- font-size: 12px;
|
|
|
- color: #909399;
|
|
|
- background: #f4f4f5;
|
|
|
- padding: 1px 4px;
|
|
|
- border-radius: 2px;
|
|
|
-}
|
|
|
-.status-desc.online {
|
|
|
- color: #67c23a;
|
|
|
-}
|
|
|
-.status-desc.busy {
|
|
|
- color: #409eff;
|
|
|
-}
|
|
|
-.status-desc.offline {
|
|
|
- color: #909399;
|
|
|
-}
|
|
|
-
|
|
|
-.card-right-stats {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: flex-end;
|
|
|
- justify-content: center;
|
|
|
- gap: 6px;
|
|
|
-}
|
|
|
-.stat-box {
|
|
|
- font-size: 11px;
|
|
|
- color: #909399;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 4px;
|
|
|
-}
|
|
|
-.stat-box .val {
|
|
|
- font-weight: bold;
|
|
|
- font-size: 13px;
|
|
|
-}
|
|
|
-.stat-box .val.danger {
|
|
|
- color: #f56c6c;
|
|
|
-}
|
|
|
-.stat-box .val.warning {
|
|
|
- color: #e6a23c;
|
|
|
-}
|
|
|
-
|
|
|
-.rider-summary {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 24px;
|
|
|
- background: #fdfdfd;
|
|
|
- border: 1px solid #ebeef5;
|
|
|
- border-radius: 8px;
|
|
|
- padding: 20px;
|
|
|
- margin-bottom: 24px;
|
|
|
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
|
- flex-wrap: nowrap;
|
|
|
- overflow-x: auto;
|
|
|
-}
|
|
|
-.summary-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- font-size: 14px;
|
|
|
- color: #606266;
|
|
|
- white-space: nowrap;
|
|
|
- flex-shrink: 0;
|
|
|
-}
|
|
|
-.summary-item .lbl {
|
|
|
- color: #909399;
|
|
|
- margin-right: 6px;
|
|
|
-}
|
|
|
-.summary-item .val {
|
|
|
- color: #303133;
|
|
|
- font-weight: 500;
|
|
|
-}
|
|
|
-.summary-item .val.bold {
|
|
|
- font-weight: bold;
|
|
|
- font-size: 16px;
|
|
|
- color: #303133;
|
|
|
-}
|
|
|
-
|
|
|
-.rider-orders-list {
|
|
|
- border: 1px solid #ebeef5;
|
|
|
- border-radius: 8px;
|
|
|
- overflow: hidden; /* Hide potential scrollbars from container */
|
|
|
-}
|
|
|
-
|
|
|
-/* Order Card New Layout */
|
|
|
-.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);
|
|
|
-}
|
|
|
-
|
|
|
-/* Updated Rider Card Layout */
|
|
|
-.r-name {
|
|
|
- font-size: 14px;
|
|
|
- font-weight: bold;
|
|
|
- color: #303133;
|
|
|
-}
|
|
|
-.r-phone {
|
|
|
- font-size: 12px;
|
|
|
- color: #909399;
|
|
|
-}
|
|
|
-.categories-right {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 4px;
|
|
|
- justify-content: flex-end;
|
|
|
- max-width: 100px;
|
|
|
-}
|
|
|
-
|
|
|
-.cat-tag {
|
|
|
- background: #f4f4f5;
|
|
|
- color: #909399;
|
|
|
- font-size: 10px;
|
|
|
- padding: 1px 4px;
|
|
|
- border-radius: 2px;
|
|
|
-}
|
|
|
-.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;
|
|
|
-}
|
|
|
-
|
|
|
-.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;
|
|
|
-}
|
|
|
-.status-badge.disabled {
|
|
|
- background: #fef0f0;
|
|
|
- color: #f56c6c;
|
|
|
-}
|
|
|
-
|
|
|
-.last-time {
|
|
|
- font-size: 11px;
|
|
|
- color: #999;
|
|
|
-}
|
|
|
-
|
|
|
-/* Dispatch Dialog */
|
|
|
-.dispatch-order-info {
|
|
|
- background: #f5f7fa;
|
|
|
- padding: 10px;
|
|
|
- border-radius: 4px;
|
|
|
- margin-bottom: 20px;
|
|
|
- border: 1px solid #e4e7ed;
|
|
|
-}
|
|
|
-.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; /* Override default list card margin */
|
|
|
-}
|
|
|
-.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;
|
|
|
-}
|
|
|
-
|
|
|
-.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;
|
|
|
-}
|
|
|
-.empty-text {
|
|
|
- text-align: center;
|
|
|
- color: #909399;
|
|
|
- padding: 20px;
|
|
|
- width: 100%;
|
|
|
- grid-column: span 2;
|
|
|
-}
|
|
|
-
|
|
|
-.card-main {
|
|
|
- justify-content: flex-start;
|
|
|
-} /* Override default center */
|
|
|
</style>
|