| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- <template>
- <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: currentTab === 'All' }" @click="currentTab = 'All'">
- <span class="txt">全部</span>
- <span class="num">{{ stats.all }}</span>
- </span>
- <span class="h-tab-item" :class="{ active: currentTab === 'Working' }" @click="currentTab = 'Working'">
- <span class="txt">接单中</span>
- <span class="num success">{{ stats.working }}</span>
- </span>
- <span class="h-tab-item" :class="{ active: currentTab === 'Resting' }" @click="currentTab = 'Resting'">
- <span class="txt">休息中</span>
- <span class="num info">{{ stats.resting }}</span>
- </span>
- <span class="h-tab-item" :class="{ active: currentTab === 'Disabled' }" @click="currentTab = 'Disabled'">
- <span class="txt">禁用</span>
- <span class="num danger">{{ stats.disabled }}</span>
- </span>
- </div>
- </div>
- <!-- Rider List -->
- <div class="list-wrapper">
- <el-scrollbar>
- <div v-for="rider in riders" :key="rider.id" class="list-card rider-card" @click="$emit('focus', rider.lng, rider.lat)">
- <div class="card-left relative">
- <el-avatar :src="rider.avatar" :size="40" />
- <div class="dot" :class="rider.uiStatus"></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>
- <dict-tag :options="sys_user_sex" :value="rider.gender ?? rider.sex" />
- <span class="r-phone">{{ rider.maskPhone }}</span>
- </div>
- <div class="status-right">
- <span class="status-badge" :class="rider.uiStatus">{{ getRiderStatusText(rider.uiStatus) }}</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.nextOrderTime || '14:30' }}</span>
- </div>
- </div>
- <div class="card-right-stats">
- <!-- <el-button link type="primary" size="small" style="margin-top: 4px; padding: 0" @click.stop="$emit('view-orders', rider)"
- >查看订单</el-button> -->
- </div>
- </div>
- </el-scrollbar>
- </div>
- </div>
- </template>
- <script setup>
- import { computed, ref, toRefs } from 'vue';
- import { useDict } from '@/utils/dict';
- import DictTag from '@/components/DictTag/index.vue';
- const { sys_user_sex } = toRefs(useDict('sys_user_sex'));
- const props = defineProps({
- modelValue: { type: String, default: 'All' },
- riders: { type: Array, default: () => [] },
- stats: { type: Object, default: () => ({ all: 0, working: 0, resting: 0, disabled: 0 }) }
- });
- const emit = defineEmits(['update:modelValue', 'focus', 'view-orders']);
- const currentTab = computed({
- get: () => props.modelValue,
- set: (val) => emit('update:modelValue', val)
- });
- const getRiderStatusText = (status) => {
- const map = { 'busy': '接单中', 'offline': '休息中', 'disabled': '禁用' };
- return map[status] || '未知';
- };
- const getCategoryClass = (cat) => {
- const map = { '接送': 'cat-transport', '喂遛': 'cat-feeding', '洗护': 'cat-washing' };
- return map[cat] || '';
- };
- </script>
- <style scoped>
- .fulfiller-mgmt {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- height: 50%;
- }
- .sec-header {
- height: 48px;
- padding: 0 16px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .sec-header.no-border {
- border-bottom: none;
- height: 40px;
- }
- .sec-header .tit {
- font-weight: bold;
- font-size: 15px;
- color: #1f2f3d;
- }
- .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 .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.success {
- color: #67c23a;
- }
- .h-tab-item .num.info {
- color: #909399;
- }
- .list-wrapper {
- flex: 1;
- overflow: hidden;
- padding: 8px 12px;
- }
- .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;
- }
- .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;
- }
- .card-main {
- flex: 1;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- gap: 4px;
- }
- .row-1 {
- display: flex;
- align-items: center;
- }
- .r-name {
- font-weight: bold;
- font-size: 14px;
- color: #303133;
- }
- .r-phone {
- font-size: 12px;
- color: #909399;
- }
- .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;
- }
- .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;
- }
- </style>
|