|
|
@@ -0,0 +1,710 @@
|
|
|
+<template>
|
|
|
+ <el-dialog v-model="dialogVisible" 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="order">
|
|
|
+ <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="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>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="row-addr" :title="order.address">
|
|
|
+ <span class="tag home">址</span> {{ order.address }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
+ <!-- 新增右侧按钮组 -->
|
|
|
+ <div class="card-right" style="display: flex; align-items: center; gap: 10px; padding-left: 20px;">
|
|
|
+ <el-button type="primary" size="small" plain round icon="User" @click="openCustomerDetail" :loading="orderInfoLoading">用户档案</el-button>
|
|
|
+ <el-button type="success" size="small" plain round @click="openPetDetail" :loading="orderInfoLoading" style="margin-left: 0;">宠物档案</el-button>
|
|
|
+ </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>
|
|
|
+ <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.phone || '--' }}</span>
|
|
|
+ <dict-tag :options="sys_user_sex" :value="currentRider.gender" />
|
|
|
+ <el-tag v-if="currentRider.status" size="small" :type="getStatusType(currentRider.status)" effect="plain">
|
|
|
+ {{ getStatusText(currentRider.status) }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="row-2 categories-row"
|
|
|
+ style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
|
|
|
+ <el-tag v-for="typeId in (currentRider.serviceTypes ? String(currentRider.serviceTypes).split(',') : [])" :key="typeId" size="small"
|
|
|
+ type="primary" effect="plain">{{ getServiceTypeText(typeId) }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="row-3 time-row" style="margin-top: 4px;">
|
|
|
+ <span class="last-time">下一单: {{ currentRider.nextOrderTime || '-' }}</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 class="rider-scroll">
|
|
|
+ <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>
|
|
|
+ <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.phone || '--' }}</span>
|
|
|
+ <dict-tag :options="sys_user_sex" :value="rider.gender" />
|
|
|
+ </div>
|
|
|
+ <el-tag v-if="rider.status" size="small" :type="getStatusType(rider.status)" effect="plain">
|
|
|
+ {{ getStatusText(rider.status) }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="row-2 categories-row"
|
|
|
+ style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
|
|
|
+ <el-tag v-for="typeId in (rider.serviceTypes ? String(rider.serviceTypes).split(',') : [])" :key="typeId" size="small"
|
|
|
+ type="primary" effect="plain">{{ getServiceTypeText(typeId) }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="row-3 time-row" style="margin-top: 4px">
|
|
|
+ <span class="last-time">下一单: {{ rider.nextOrderTime || '-' }}</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 class="rider-pagination" style="margin-top: 20px;">
|
|
|
+ <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize"
|
|
|
+ :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" :total="total"
|
|
|
+ @current-change="loadRiders" @size-change="handlePageSizeChange" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="dispatch-footer">
|
|
|
+ <div class="fee-inputs" style="display: flex; align-items: center;">
|
|
|
+ <div class="fee-input">
|
|
|
+ <span class="label">履约佣金:</span>
|
|
|
+ <el-input-number v-model="dispatchFee" :min="0" :precision="2" :step="10" placeholder="请输入"
|
|
|
+ style="width: 130px;" />
|
|
|
+ <span class="unit">元</span>
|
|
|
+ </div>
|
|
|
+ <div class="fee-input" style="margin-left: 20px;">
|
|
|
+ <span class="label">订单佣金:</span>
|
|
|
+ <el-input-number v-model="orderCommission" :min="0" :precision="2" :step="10" placeholder="请输入"
|
|
|
+ style="width: 130px;" />
|
|
|
+ <span class="unit">元</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="btns">
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :disabled="!canSubmit" @click="handleDispatchSubmit">确认派单</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <CustomerDetailDrawer v-model:visible="customerDialogVisible" :customer-id="customerId" :area-station-list="areaStationList" />
|
|
|
+ <PetDetailDrawer v-model:visible="petDialogVisible" :pet-id="petId" />
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, watch, getCurrentInstance, toRefs } from 'vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import { pageFulfillerOnOrder } from '@/api/fulfiller/pool'
|
|
|
+import { listAllTag } from '@/api/fulfiller/tag'
|
|
|
+import { getSubOrderInfo } from '@/api/order/subOrder/index'
|
|
|
+import { listAllService } from '@/api/service/list/index'
|
|
|
+import { listAreaStation } from '@/api/system/areaStation/index'
|
|
|
+import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
|
|
|
+import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ visible: Boolean,
|
|
|
+ order: Object
|
|
|
+})
|
|
|
+const emit = defineEmits(['update:visible', 'submit'])
|
|
|
+
|
|
|
+const { proxy } = getCurrentInstance();
|
|
|
+const { sys_user_sex } = toRefs(proxy.useDict('sys_user_sex'));
|
|
|
+
|
|
|
+const dialogVisible = computed({
|
|
|
+ get: () => props.visible,
|
|
|
+ set: (val) => emit('update:visible', val)
|
|
|
+})
|
|
|
+
|
|
|
+const ridersList = ref([])
|
|
|
+const total = ref(0)
|
|
|
+const pageNum = ref(1)
|
|
|
+const pageSize = ref(10)
|
|
|
+
|
|
|
+const allTags = ref([])
|
|
|
+const tagMap = computed(() => {
|
|
|
+ const map = {}
|
|
|
+ for (const t of (allTags.value || [])) {
|
|
|
+ if (t && t.id !== undefined && t.id !== null) map[t.id] = t
|
|
|
+ }
|
|
|
+ return map
|
|
|
+})
|
|
|
+
|
|
|
+const currentRider = ref(null)
|
|
|
+const dispatchSearchQuery = ref('')
|
|
|
+const selectedRiderId = ref(null)
|
|
|
+const dispatchFee = ref(0)
|
|
|
+const orderCommission = ref(0)
|
|
|
+
|
|
|
+const customerDialogVisible = ref(false)
|
|
|
+const petDialogVisible = ref(false)
|
|
|
+const customerId = ref(null)
|
|
|
+const petId = ref(null)
|
|
|
+const orderInfoLoading = ref(false)
|
|
|
+
|
|
|
+const loadAllTags = async () => {
|
|
|
+ if (allTags.value && allTags.value.length > 0) return
|
|
|
+ try {
|
|
|
+ const res = await listAllTag({ category: 'fulfiller' })
|
|
|
+ allTags.value = res?.data || []
|
|
|
+ } catch {
|
|
|
+ allTags.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const serviceOptions = ref([])
|
|
|
+const loadServiceOptions = async () => {
|
|
|
+ if (serviceOptions.value.length > 0) return
|
|
|
+ try {
|
|
|
+ const res = await listAllService()
|
|
|
+ serviceOptions.value = res?.data || []
|
|
|
+ } catch { /* ignore */ }
|
|
|
+}
|
|
|
+
|
|
|
+const areaStationList = ref([])
|
|
|
+const loadAreaStationList = async () => {
|
|
|
+ if (areaStationList.value.length > 0) return
|
|
|
+ try {
|
|
|
+ const res = await listAreaStation()
|
|
|
+ areaStationList.value = res.data || []
|
|
|
+ } catch { /* ignore */ }
|
|
|
+}
|
|
|
+
|
|
|
+const getServiceTypeText = (id) => {
|
|
|
+ const s = serviceOptions.value.find(item => String(item.id) === String(id))
|
|
|
+ return s ? s.name : String(id)
|
|
|
+}
|
|
|
+
|
|
|
+const loadRiders = async () => {
|
|
|
+ try {
|
|
|
+ const res = await pageFulfillerOnOrder({
|
|
|
+ content: dispatchSearchQuery.value || undefined,
|
|
|
+ pageNum: pageNum.value,
|
|
|
+ pageSize: pageSize.value,
|
|
|
+ service: props.order?.service
|
|
|
+ })
|
|
|
+ const list = res?.rows || []
|
|
|
+ ridersList.value = list.map(r => ({
|
|
|
+ ...r,
|
|
|
+ nextOrderTime: r.nextOrderTime || '-',
|
|
|
+ gender: r.gender ?? r.sex
|
|
|
+ }))
|
|
|
+ total.value = res?.total || 0
|
|
|
+
|
|
|
+ if (props.order?.riderId) {
|
|
|
+ currentRider.value = ridersList.value.find(r => r.id === props.order.riderId) || null
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ridersList.value = []
|
|
|
+ total.value = 0
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handlePageSizeChange = (size) => {
|
|
|
+ pageSize.value = size
|
|
|
+ pageNum.value = 1
|
|
|
+ loadRiders()
|
|
|
+}
|
|
|
+
|
|
|
+watch(() => props.visible, (val) => {
|
|
|
+ if (val && props.order) {
|
|
|
+ currentRider.value = null
|
|
|
+ dispatchSearchQuery.value = ''
|
|
|
+ selectedRiderId.value = null
|
|
|
+ // price 单位为分,转成元显示
|
|
|
+ dispatchFee.value = props.order?.fulfillmentCommission ? Number((props.order.fulfillmentCommission / 100).toFixed(2)) : 0
|
|
|
+ orderCommission.value = props.order?.orderCommission ? Number((props.order.orderCommission / 100).toFixed(2)) : 0
|
|
|
+ if (props.order?.riderId) {
|
|
|
+ currentRider.value = {
|
|
|
+ id: props.order.riderId,
|
|
|
+ gender: props.order.riderGender ?? props.order.riderSex
|
|
|
+ }
|
|
|
+ }
|
|
|
+ pageNum.value = 1
|
|
|
+ loadAllTags()
|
|
|
+ loadServiceOptions()
|
|
|
+ loadAreaStationList()
|
|
|
+ loadRiders()
|
|
|
+
|
|
|
+ // 获取订单详细信息
|
|
|
+ customerId.value = null
|
|
|
+ petId.value = null
|
|
|
+ orderInfoLoading.value = true
|
|
|
+ getSubOrderInfo(props.order.id).then((res) => {
|
|
|
+ if(res.data) {
|
|
|
+ // 如果 usrCustomer / usrPet 是对象则取其 id,如果是 ID 直接取
|
|
|
+ customerId.value = res.data.usrCustomer?.id || res.data.usrCustomer
|
|
|
+ petId.value = res.data.usrPet?.id || res.data.usrPet
|
|
|
+
|
|
|
+ // 接到详情后,把真实的金额放进去(后端金额单位为分)
|
|
|
+ if (res.data.fulfillmentCommission !== undefined && res.data.fulfillmentCommission !== null) {
|
|
|
+ dispatchFee.value = Number((res.data.fulfillmentCommission / 100).toFixed(2))
|
|
|
+ }
|
|
|
+ if (res.data.orderCommission !== undefined && res.data.orderCommission !== null) {
|
|
|
+ orderCommission.value = Number((res.data.orderCommission / 100).toFixed(2))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果已经有履约者且不是在列表中找到的,从详情中补全性别
|
|
|
+ if (props.order?.riderId && !currentRider.value) {
|
|
|
+ currentRider.value = {
|
|
|
+ id: props.order.riderId,
|
|
|
+ name: res.data.fulfillerName,
|
|
|
+ gender: res.data.fulfillerGender ?? res.data.fulfillerSex,
|
|
|
+ status: res.data.fulfillerStatus
|
|
|
+ }
|
|
|
+ } else if (currentRider.value && (res.data.fulfillerGender || res.data.fulfillerSex)) {
|
|
|
+ currentRider.value.gender = res.data.fulfillerGender ?? res.data.fulfillerSex
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }).catch((e) => {
|
|
|
+ console.error('获取订单详细信息失败', e)
|
|
|
+ }).finally(() => {
|
|
|
+ orderInfoLoading.value = false
|
|
|
+ })
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const openCustomerDetail = () => {
|
|
|
+ if (!customerId.value) {
|
|
|
+ ElMessage.warning('未能获取到用户信息')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ customerDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const openPetDetail = () => {
|
|
|
+ if (!petId.value) {
|
|
|
+ ElMessage.warning('未能获取到宠物信息')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ petDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const getTagText = (tagId) => {
|
|
|
+ const t = tagMap.value?.[tagId]
|
|
|
+ return t?.name || String(tagId)
|
|
|
+}
|
|
|
+
|
|
|
+const getTagType = (tagId) => {
|
|
|
+ const t = tagMap.value?.[tagId]
|
|
|
+ const type = t?.colorType
|
|
|
+ if (type === 'success' || type === 'warning' || type === 'danger' || type === 'info') return type
|
|
|
+ return ''
|
|
|
+}
|
|
|
+
|
|
|
+watch(dispatchSearchQuery, () => {
|
|
|
+ pageNum.value = 1
|
|
|
+ loadRiders()
|
|
|
+})
|
|
|
+
|
|
|
+const getShortType = (code) => {
|
|
|
+ const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' }
|
|
|
+ return map[code] || '订单'
|
|
|
+}
|
|
|
+
|
|
|
+const getStatusText = (status) => {
|
|
|
+ const statusMap = {
|
|
|
+ resting: '休息',
|
|
|
+ busy: '接单中',
|
|
|
+ disabled: '禁用'
|
|
|
+ }
|
|
|
+ return statusMap[status] || status
|
|
|
+}
|
|
|
+
|
|
|
+const getStatusType = (status) => {
|
|
|
+ const typeMap = {
|
|
|
+ resting: 'info',
|
|
|
+ busy: 'success',
|
|
|
+ disabled: 'danger'
|
|
|
+ }
|
|
|
+ return typeMap[status] || 'info'
|
|
|
+}
|
|
|
+
|
|
|
+const filteredDispatchRiders = computed(() => {
|
|
|
+ return ridersList.value || []
|
|
|
+})
|
|
|
+
|
|
|
+const canSubmit = computed(() => {
|
|
|
+ return !!selectedRiderId.value && !!dispatchFee.value
|
|
|
+})
|
|
|
+
|
|
|
+const handleDispatchSubmit = () => {
|
|
|
+ if (!selectedRiderId.value) {
|
|
|
+ ElMessage.warning('请选择履约者')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!dispatchFee.value) {
|
|
|
+ ElMessage.warning('请输入服务费用')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const rider = ridersList.value.find(r => r.id === selectedRiderId.value)
|
|
|
+ emit('submit', {
|
|
|
+ riderId: rider.id,
|
|
|
+ riderName: rider.name,
|
|
|
+ fee: dispatchFee.value,
|
|
|
+ orderCommission: orderCommission.value
|
|
|
+ })
|
|
|
+ dialogVisible.value = false
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* Dispatch Dialog Styles */
|
|
|
+.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-scroll {
|
|
|
+ max-height: 45vh;
|
|
|
+}
|
|
|
+
|
|
|
+.rider-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 12px;
|
|
|
+ padding-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.rider-pagination {
|
|
|
+ margin-top: 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;
|
|
|
+}
|
|
|
+</style>
|