|
@@ -0,0 +1,732 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ :visible.sync="dialogVisible"
|
|
|
|
|
+ width="1100px"
|
|
|
|
|
+ custom-class="dispatch-order-dialog"
|
|
|
|
|
+ :close-on-click-modal="false"
|
|
|
|
|
+ :show-close="false"
|
|
|
|
|
+ append-to-body
|
|
|
|
|
+ destroy-on-close
|
|
|
|
|
+ @opened="onDialogOpened"
|
|
|
|
|
+ @closed="onDialogClosed"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="dispatch-order-panel">
|
|
|
|
|
+ <div class="panel-header">
|
|
|
|
|
+ <span class="panel-title">订单分配</span>
|
|
|
|
|
+ <span v-if="currentOrder" class="panel-subtitle">
|
|
|
|
|
+ 订单号:{{ currentOrder.orderNo }}
|
|
|
|
|
+ <template v-if="currentOrder.cAddress"> · {{ currentOrder.cAddress }}</template>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <i class="el-icon-close panel-close" @click="handleClose" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="filter-bar">
|
|
|
|
|
+ <el-select v-model="filterForm.region" placeholder="地区" clearable class="filter-item">
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="item in regionOptions"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ :label="item.label"
|
|
|
|
|
+ :value="item.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ <el-select v-model="filterForm.category" placeholder="服务类目" clearable class="filter-item">
|
|
|
|
|
+ <el-option label="按摩" value="按摩" />
|
|
|
|
|
+ <el-option label="陪玩" value="陪玩" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="filterForm.keyword"
|
|
|
|
|
+ placeholder="搜索技师名称或姓名"
|
|
|
|
|
+ prefix-icon="el-icon-search"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ class="filter-search"
|
|
|
|
|
+ @keyup.enter.native="handleSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="filter-actions">
|
|
|
|
|
+ <el-button type="primary" @click="handleSearch">搜索</el-button>
|
|
|
|
|
+ <el-button @click="handleClear">清空</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="panel-body">
|
|
|
|
|
+ <div class="technician-list">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="item in sortedTechnicians"
|
|
|
|
|
+ :key="item.id"
|
|
|
|
|
+ class="technician-card"
|
|
|
|
|
+ :class="{ 'is-active': selectedId === item.id }"
|
|
|
|
|
+ @click="selectTechnician(item)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="technician-radio" :class="{ 'is-checked': selectedId === item.id }">
|
|
|
|
|
+ <span class="technician-radio__inner" />
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <img :src="item.avatar" class="technician-avatar" alt="" />
|
|
|
|
|
+ <div class="technician-info">
|
|
|
|
|
+ <div class="technician-name-row">
|
|
|
|
|
+ <span class="technician-name">{{ item.nickname }}</span>
|
|
|
|
|
+ <span class="status-tag" :class="`status-tag--${item.status}`">{{ statusLabel(item.status) }}</span>
|
|
|
|
|
+ <span v-if="item.distance != null" class="technician-distance">距用户 {{ formatDistance(item.distance) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="technician-address">{{ item.address }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!sortedTechnicians.length" class="empty-tip">暂无符合条件的技师</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="map-wrap">
|
|
|
|
|
+ <div :id="mapId" class="map-container" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="panel-footer">
|
|
|
|
|
+ <el-button @click="handleClose">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="handleConfirm">确认派单</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import AMapLoader from '@amap/amap-jsapi-loader'
|
|
|
|
|
+import { calcDistance, formatDistance } from '@/utils/geo'
|
|
|
|
|
+import userDestIcon from '@/assets/images/map/user.png'
|
|
|
|
|
+
|
|
|
|
|
+const AMAP_KEY = '41bc0f647d4432045d472f554bf03d75'
|
|
|
|
|
+const AMAP_SECURITY = '80ba66cc0bba5b07de6a275d7aa02a39'
|
|
|
|
|
+
|
|
|
|
|
+const STATUS_MAP = {
|
|
|
|
|
+ online: '在线',
|
|
|
|
|
+ onway: '在路途中',
|
|
|
|
|
+ rest: '休息中'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const DEFAULT_FILTER = {
|
|
|
|
|
+ region: 'city',
|
|
|
|
|
+ category: '',
|
|
|
|
|
+ keyword: ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+let mapInstanceSeed = 0
|
|
|
|
|
+
|
|
|
|
|
+/** 示例技师数据 */
|
|
|
|
|
+const MOCK_TECHNICIANS = [
|
|
|
|
|
+ { id: 1, nickname: 'NaNny', name: '娜娜', category: '按摩', region: 'city', status: 'rest', lng: 116.397428, lat: 39.90923, address: '北京市东城区王府井大街88号附近', avatar: require('@/assets/images/map/user.png') },
|
|
|
|
|
+ { id: 2, nickname: '小艾', name: '艾琳', category: '按摩', region: 'city', status: 'onway', lng: 116.451328, lat: 39.908765, address: '北京市朝阳区建国门外大街1号附近', avatar: require('@/assets/images/map/user.png') },
|
|
|
|
|
+ { id: 3, nickname: '阿杰', name: '杰哥', category: '陪玩', region: 'city', status: 'online', lng: 116.280892, lat: 39.921234, address: '北京市海淀区中关村大街27号附近', avatar: require('@/assets/images/map/user.png') },
|
|
|
|
|
+ { id: 4, nickname: '小雨', name: '雨晴', category: '陪玩', region: 'city', status: 'online', lng: 116.385621, lat: 39.954812, address: '北京市西城区西单北大街120号附近', avatar: require('@/assets/images/map/user.png') },
|
|
|
|
|
+ { id: 5, nickname: '大明', name: '明明', category: '按摩', region: 'city', status: 'rest', lng: 116.420981, lat: 39.893456, address: '北京市丰台区南三环西路16号附近', avatar: require('@/assets/images/map/user.png') },
|
|
|
|
|
+ { id: 6, nickname: '小美', name: '美美', category: '按摩', region: 'city', status: 'online', lng: 116.362481, lat: 39.915612, address: '北京市东城区东直门内大街8号附近', avatar: require('@/assets/images/map/user.png') },
|
|
|
|
|
+ { id: 7, nickname: '阿强', name: '强子', category: '陪玩', region: 'city', status: 'onway', lng: 116.432156, lat: 39.928341, address: '北京市朝阳区三里屯路19号附近', avatar: require('@/assets/images/map/user.png') },
|
|
|
|
|
+ { id: 8, nickname: '莉莉', name: '丽丽', category: '按摩', region: 'city', status: 'rest', lng: 116.318765, lat: 39.876543, address: '北京市丰台区方庄路15号附近', avatar: require('@/assets/images/map/user.png') },
|
|
|
|
|
+ { id: 9, nickname: '小军', name: '军哥', category: '陪玩', region: 'city', status: 'online', lng: 116.468912, lat: 39.941234, address: '北京市朝阳区望京街10号附近', avatar: require('@/assets/images/map/user.png') },
|
|
|
|
|
+ { id: 10, nickname: '小芳', name: '芳芳', category: '按摩', region: 'city', status: 'onway', lng: 116.351234, lat: 39.902345, address: '北京市西城区金融大街35号附近', avatar: require('@/assets/images/map/user.png') }
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'DispatchOrderDialog',
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ dialogVisible: false,
|
|
|
|
|
+ mapId: `dispatch-order-map-${++mapInstanceSeed}`,
|
|
|
|
|
+ currentOrder: null,
|
|
|
|
|
+ orderDest: null,
|
|
|
|
|
+ filterForm: { ...DEFAULT_FILTER },
|
|
|
|
|
+ searchParams: { ...DEFAULT_FILTER },
|
|
|
|
|
+ regionOptions: [{ label: '城市运营中心', value: 'city' }],
|
|
|
|
|
+ technicians: MOCK_TECHNICIANS,
|
|
|
|
|
+ selectedId: null,
|
|
|
|
|
+ orderStatus: 'pending',
|
|
|
|
|
+ AMap: null,
|
|
|
|
|
+ map: null,
|
|
|
|
|
+ markerInstances: [],
|
|
|
|
|
+ destMarker: null
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ sortedTechnicians() {
|
|
|
|
|
+ const { region, category, keyword } = this.searchParams
|
|
|
|
|
+ const kw = keyword.trim().toLowerCase()
|
|
|
|
|
+ let list = this.technicians.filter((item) => {
|
|
|
|
|
+ if (region && item.region !== region) return false
|
|
|
|
|
+ if (category && item.category !== category) return false
|
|
|
|
|
+ if (kw && !item.nickname.toLowerCase().includes(kw) && !item.name.toLowerCase().includes(kw)) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if (this.orderDest) {
|
|
|
|
|
+ const { lng, lat } = this.orderDest
|
|
|
|
|
+ list = list
|
|
|
|
|
+ .map((item) => ({
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ distance: calcDistance(lng, lat, item.lng, item.lat)
|
|
|
|
|
+ }))
|
|
|
|
|
+ .sort((a, b) => a.distance - b.distance)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return list
|
|
|
|
|
+ },
|
|
|
|
|
+ mapMarkers() {
|
|
|
|
|
+ return this.sortedTechnicians.map((item) => ({
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ active: item.id === this.selectedId
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ watch: {
|
|
|
|
|
+ mapMarkers: {
|
|
|
|
|
+ deep: true,
|
|
|
|
|
+ handler() {
|
|
|
|
|
+ this.renderMarkers()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ this.destroyMap()
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ formatDistance,
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开派单弹窗
|
|
|
|
|
+ * @param {Object} order - 订单信息,需含 arrivalLongitude / arrivalLatitude(用户目的地经纬度)
|
|
|
|
|
+ */
|
|
|
|
|
+ open(order) {
|
|
|
|
|
+ if (!order) return
|
|
|
|
|
+ const lng = Number(order.arrivalLongitude)
|
|
|
|
|
+ const lat = Number(order.arrivalLatitude)
|
|
|
|
|
+ if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
|
|
|
|
|
+ this.$message.warning('该订单缺少用户目的地经纬度,无法分配')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.currentOrder = order
|
|
|
|
|
+ this.orderDest = { lng, lat }
|
|
|
|
|
+ this.orderStatus = order.dispatchStatus || 'pending'
|
|
|
|
|
+ this.filterForm = {
|
|
|
|
|
+ region: order.region || DEFAULT_FILTER.region,
|
|
|
|
|
+ category: order.category || '',
|
|
|
|
|
+ keyword: ''
|
|
|
|
|
+ }
|
|
|
|
|
+ this.searchParams = { ...this.filterForm }
|
|
|
|
|
+ this.selectedId = null
|
|
|
|
|
+ this.dialogVisible = true
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onDialogOpened() {
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.initMap()
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onDialogClosed() {
|
|
|
|
|
+ this.destroyMap()
|
|
|
|
|
+ this.currentOrder = null
|
|
|
|
|
+ this.orderDest = null
|
|
|
|
|
+ this.selectedId = null
|
|
|
|
|
+ this.filterForm = { ...DEFAULT_FILTER }
|
|
|
|
|
+ this.searchParams = { ...DEFAULT_FILTER }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ handleSearch() {
|
|
|
|
|
+ this.searchParams = { ...this.filterForm }
|
|
|
|
|
+ if (this.selectedId && !this.sortedTechnicians.some((item) => item.id === this.selectedId)) {
|
|
|
|
|
+ this.selectedId = null
|
|
|
|
|
+ }
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.fitMarkerView()
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ handleClear() {
|
|
|
|
|
+ this.filterForm = { region: '', category: '', keyword: '' }
|
|
|
|
|
+ this.searchParams = { region: '', category: '', keyword: '' }
|
|
|
|
|
+ this.selectedId = null
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.fitMarkerView()
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ statusLabel(status) {
|
|
|
|
|
+ return STATUS_MAP[status] || status
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ initMap() {
|
|
|
|
|
+ if (this.map) return
|
|
|
|
|
+
|
|
|
|
|
+ window._AMapSecurityConfig = { securityJsCode: AMAP_SECURITY }
|
|
|
|
|
+ const center = this.orderDest
|
|
|
|
|
+ ? [this.orderDest.lng, this.orderDest.lat]
|
|
|
|
|
+ : [116.397428, 39.90923]
|
|
|
|
|
+
|
|
|
|
|
+ AMapLoader.load({
|
|
|
|
|
+ key: AMAP_KEY,
|
|
|
|
|
+ version: '2.0'
|
|
|
|
|
+ }).then((AMap) => {
|
|
|
|
|
+ this.AMap = AMap
|
|
|
|
|
+ this.map = new AMap.Map(this.mapId, {
|
|
|
|
|
+ resizeEnable: true,
|
|
|
|
|
+ zoom: 13,
|
|
|
|
|
+ center,
|
|
|
|
|
+ viewMode: '2D'
|
|
|
|
|
+ })
|
|
|
|
|
+ this.renderDestinationMarker()
|
|
|
|
|
+ this.renderMarkers()
|
|
|
|
|
+ this.fitMarkerView()
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ if (this.map) this.map.resize()
|
|
|
|
|
+ })
|
|
|
|
|
+ }).catch((err) => {
|
|
|
|
|
+ console.error('地图加载失败', err)
|
|
|
|
|
+ this.$message.error('地图加载失败,请检查网络或 Key 配置')
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ createDestinationMarkerContent() {
|
|
|
|
|
+ const wrap = document.createElement('div')
|
|
|
|
|
+ wrap.className = 'dispatch-map-dest'
|
|
|
|
|
+ wrap.innerHTML = `
|
|
|
|
|
+ <div class="dispatch-map-dest__label">用户目的地</div>
|
|
|
|
|
+ <img class="dispatch-map-dest__icon" src="${userDestIcon}" alt="用户目的地" />
|
|
|
|
|
+ `
|
|
|
|
|
+ return wrap
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ renderDestinationMarker() {
|
|
|
|
|
+ if (!this.map || !this.AMap || !this.orderDest) return
|
|
|
|
|
+
|
|
|
|
|
+ if (this.destMarker) {
|
|
|
|
|
+ this.map.remove(this.destMarker)
|
|
|
|
|
+ this.destMarker = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.destMarker = new this.AMap.Marker({
|
|
|
|
|
+ position: [this.orderDest.lng, this.orderDest.lat],
|
|
|
|
|
+ content: this.createDestinationMarkerContent(),
|
|
|
|
|
+ offset: new this.AMap.Pixel(0, 0),
|
|
|
|
|
+ anchor: 'bottom-center',
|
|
|
|
|
+ zIndex: 300
|
|
|
|
|
+ })
|
|
|
|
|
+ this.map.add(this.destMarker)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ createMarkerContent(item) {
|
|
|
|
|
+ const wrap = document.createElement('div')
|
|
|
|
|
+ wrap.className = item.active
|
|
|
|
|
+ ? 'dispatch-map-marker dispatch-map-marker--active'
|
|
|
|
|
+ : 'dispatch-map-marker'
|
|
|
|
|
+ wrap.innerHTML = `
|
|
|
|
|
+ <div class="dispatch-map-marker__label">${item.nickname}</div>
|
|
|
|
|
+ <div class="dispatch-map-marker__avatar-wrap">
|
|
|
|
|
+ <img class="dispatch-map-marker__avatar" src="${item.avatar}" alt="${item.nickname}" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="dispatch-map-marker__pointer"></div>
|
|
|
|
|
+ `
|
|
|
|
|
+ return wrap
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ renderMarkers() {
|
|
|
|
|
+ if (!this.map || !this.AMap) return
|
|
|
|
|
+
|
|
|
|
|
+ this.clearMarkers()
|
|
|
|
|
+ this.mapMarkers.forEach((item) => {
|
|
|
|
|
+ const marker = new this.AMap.Marker({
|
|
|
|
|
+ position: [item.lng, item.lat],
|
|
|
|
|
+ content: this.createMarkerContent(item),
|
|
|
|
|
+ offset: new this.AMap.Pixel(0, 0),
|
|
|
|
|
+ anchor: 'bottom-center',
|
|
|
|
|
+ zIndex: item.active ? 200 : 100
|
|
|
|
|
+ })
|
|
|
|
|
+ marker.on('click', () => {
|
|
|
|
|
+ this.selectTechnician(item)
|
|
|
|
|
+ })
|
|
|
|
|
+ this.map.add(marker)
|
|
|
|
|
+ this.markerInstances.push(marker)
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ clearMarkers() {
|
|
|
|
|
+ if (this.markerInstances.length && this.map) {
|
|
|
|
|
+ this.map.remove(this.markerInstances)
|
|
|
|
|
+ }
|
|
|
|
|
+ this.markerInstances = []
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ fitMarkerView() {
|
|
|
|
|
+ if (!this.map) return
|
|
|
|
|
+ const allMarkers = [...this.markerInstances]
|
|
|
|
|
+ if (this.destMarker) allMarkers.push(this.destMarker)
|
|
|
|
|
+ if (!allMarkers.length) return
|
|
|
|
|
+ this.map.setFitView(allMarkers, false, [60, 60, 60, 60])
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ selectTechnician(item) {
|
|
|
|
|
+ if (this.selectedId === item.id) {
|
|
|
|
|
+ this.selectedId = null
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.selectedId = item.id
|
|
|
|
|
+ if (this.map) {
|
|
|
|
|
+ this.map.setZoomAndCenter(15, [item.lng, item.lat])
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ handleClose() {
|
|
|
|
|
+ this.dialogVisible = false
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ handleConfirm() {
|
|
|
|
|
+ if (!this.selectedId) {
|
|
|
|
|
+ this.$message.warning('请先选择技师')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.orderStatus !== 'pending') {
|
|
|
|
|
+ this.$message.warning('订单状态已变更,无法派单')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const tech = this.technicians.find((t) => t.id === this.selectedId)
|
|
|
|
|
+ this.$message.success(`订单 ${this.currentOrder.orderNo} 已成功派单给 ${tech.nickname}`)
|
|
|
|
|
+ this.$emit('success', {
|
|
|
|
|
+ order: this.currentOrder,
|
|
|
|
|
+ technician: tech
|
|
|
|
|
+ })
|
|
|
|
|
+ this.dialogVisible = false
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ destroyMap() {
|
|
|
|
|
+ this.clearMarkers()
|
|
|
|
|
+ if (this.destMarker && this.map) {
|
|
|
|
|
+ this.map.remove(this.destMarker)
|
|
|
|
|
+ this.destMarker = null
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.map) {
|
|
|
|
|
+ this.map.destroy()
|
|
|
|
|
+ this.map = null
|
|
|
|
|
+ }
|
|
|
|
|
+ this.AMap = null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.dispatch-order-panel {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ height: 560px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .panel-title {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .panel-subtitle {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .panel-close {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
|
|
+
|
|
|
|
|
+ .filter-item {
|
|
|
|
|
+ width: 160px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .filter-search {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 120px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .filter-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-body {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.technician-list {
|
|
|
|
|
+ width: 360px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ align-self: stretch;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ overflow-x: hidden;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ -webkit-overflow-scrolling: touch;
|
|
|
|
|
+ border-right: 1px solid #ebeef5;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.technician-card {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ padding: 14px 16px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
+ transition: background 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover,
|
|
|
|
|
+ &.is-active {
|
|
|
|
|
+ background: #ecf5ff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .technician-radio {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ width: 14px;
|
|
|
|
|
+ height: 14px;
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ margin-top: 14px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+
|
|
|
|
|
+ &__inner {
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+ height: 4px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ transform: scale(0);
|
|
|
|
|
+ transition: transform 0.15s ease-in;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.is-checked {
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+ background: #409eff;
|
|
|
|
|
+
|
|
|
|
|
+ .technician-radio__inner {
|
|
|
|
|
+ transform: scale(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .technician-avatar {
|
|
|
|
|
+ width: 44px;
|
|
|
|
|
+ height: 44px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ margin-right: 10px;
|
|
|
|
|
+ background: #eee;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .technician-info {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .technician-name-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .technician-name {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .technician-distance {
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .status-tag {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ padding: 0 6px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ line-height: 20px;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+
|
|
|
|
|
+ &--online {
|
|
|
|
|
+ color: #67c23a;
|
|
|
|
|
+ background: #f0f9eb;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &--onway {
|
|
|
|
|
+ color: #e6a23c;
|
|
|
|
|
+ background: #fdf6ec;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &--rest {
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ background: #f4f4f5;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .technician-address {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-tip {
|
|
|
|
|
+ padding: 40px 16px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-wrap {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ background: #eef1f5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-container {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ padding: 14px 20px;
|
|
|
|
|
+ border-top: 1px solid #ebeef5;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss">
|
|
|
|
|
+.dispatch-order-dialog {
|
|
|
|
|
+ .el-dialog__header {
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-dialog__body {
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dispatch-map-dest {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+
|
|
|
|
|
+ &__label {
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ background: #f56c6c;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__icon {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ width: auto;
|
|
|
|
|
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dispatch-map-marker {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+
|
|
|
|
|
+ &__label {
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.65);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__avatar-wrap {
|
|
|
|
|
+ width: 40px;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ border: 2px solid #fff;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #eee;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__avatar {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__pointer {
|
|
|
|
|
+ width: 0;
|
|
|
|
|
+ height: 0;
|
|
|
|
|
+ margin-top: -1px;
|
|
|
|
|
+ border-left: 6px solid transparent;
|
|
|
|
|
+ border-right: 6px solid transparent;
|
|
|
|
|
+ border-top: 8px solid #fff;
|
|
|
|
|
+ filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.15));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &--active {
|
|
|
|
|
+ .dispatch-map-marker__label {
|
|
|
|
|
+ background: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .dispatch-map-marker__avatar-wrap {
|
|
|
|
|
+ width: 48px;
|
|
|
|
|
+ height: 48px;
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+ border-width: 3px;
|
|
|
|
|
+ box-shadow: 0 2px 12px rgba(64, 158, 255, 0.5);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .dispatch-map-marker__pointer {
|
|
|
|
|
+ border-top-color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|