Files
medical-mall/pages/mall/delivery/profile.uvue
not-like-juvenile b1c845d571 添加新页面
2026-01-23 17:13:39 +08:00

813 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 配送端 - 个人中心 -->
<template>
<view class="delivery-profile">
<!-- 1. 蓝色头像条profile-header -->
<view class="profile-header">
<!-- 返回按钮:最左边垂直居中 -->
<view class="back-box" @click="backToIndex">
<text class="back-icon"></text>
</view>
<image :src="driverInfo.avatar_url || '/static/default-avatar.png'" class="driver-avatar" @click="editProfile" />
<view class="driver-info">
<text class="driver-name">{{ driverInfo.real_name }}</text>
<text class="driver-status">{{ getWorkStatus() }}</text>
<view class="driver-stats">
<text class="stat-item">评分: {{ driverInfo.rating }}/5.0</text>
<text class="stat-item">总单数: {{ driverInfo.total_orders }}</text>
</view>
</view>
<view class="settings-icon" @click="goToSettings">⚙️</view>
</view>
<!-- 2. 工作状态切换 -->
<view class="work-status">
<view class="section-title">工作状态</view>
<view class="status-controls">
<view class="status-toggle" @click="toggleWorkStatus">
<text class="toggle-label">{{ workStatus === 1 ? '工作中' : '休息中' }}</text>
<view class="toggle-switch" :class="{ active: workStatus === 1 }">
<view class="toggle-handle"></view>
</view>
</view>
<view v-if="workStatus === 1" class="current-location">
<text class="location-text">📍 {{ currentLocation }}</text>
</view>
</view>
</view>
<!-- 3. 配送任务快捷入口 -->
<view class="task-shortcuts">
<view class="section-title">配送任务</view>
<view class="task-tabs">
<view class="task-tab" @click="goToTasks('all')">
<text class="tab-icon">📋</text>
<text class="tab-text">全部任务</text>
<text v-if="taskCounts.total > 0" class="tab-badge">{{ taskCounts.total }}</text>
</view>
<view class="task-tab" @click="goToTasks('pending')">
<text class="tab-icon">⏳</text>
<text class="tab-text">待接单</text>
<text v-if="taskCounts.pending > 0" class="tab-badge alert">{{ taskCounts.pending }}</text>
</view>
<view class="task-tab" @click="goToTasks('ongoing')">
<text class="tab-icon">🚚</text>
<text class="tab-text">配送中</text>
<text v-if="taskCounts.ongoing > 0" class="tab-badge">{{ taskCounts.ongoing }}</text>
</view>
<view class="task-tab" @click="goToTasks('completed')">
<text class="tab-icon">✅</text>
<text class="tab-text">已完成</text>
<text v-if="taskCounts.completed > 0" class="tab-badge">{{ taskCounts.completed }}</text>
</view>
</view>
</view>
<!-- 4. 今日配送数据 -->
<view class="today-stats">
<view class="section-title">今日配送</view>
<view class="stats-grid">
<view class="stat-card">
<text class="stat-value">{{ todayStats.deliveries }}</text>
<text class="stat-label">完成单数</text>
</view>
<view class="stat-card">
<text class="stat-value">¥{{ todayStats.earnings }}</text>
<text class="stat-label">配送收入</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ todayStats.distance }}km</text>
<text class="stat-label">配送里程</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ todayStats.efficiency }}%</text>
<text class="stat-label">准时率</text>
</view>
</view>
</view>
<!-- 5. 当前任务 -->
<view v-if="currentTask" class="current-task">
<view class="section-title">当前任务</view>
<view class="task-card">
<view class="task-header">
<text class="task-id">任务 #{{ currentTask.id.slice(-6) }}</text>
<text class="task-status">{{ getTaskStatusText(currentTask.status) }}</text>
</view>
<view class="task-route">
<view class="route-point">
<text class="point-icon">📍</text>
<view class="point-info">
<text class="point-label">取货地址</text>
<text class="point-address">{{ getAddressText(currentTask.pickup_address) }}</text>
</view>
</view>
<view class="route-line"></view>
<view class="route-point">
<text class="point-icon">🏠</text>
<view class="point-info">
<text class="point-label">送达地址</text>
<text class="point-address">{{ getAddressText(currentTask.delivery_address) }}</text>
</view>
</view>
</view>
<view class="task-actions">
<button class="action-btn" @click="contactCustomer">联系客户</button>
<button class="action-btn primary" @click="viewTaskDetail">查看详情</button>
</view>
</view>
</view>
<!-- 6. 最近任务 -->
<view class="recent-tasks">
<view class="section-header">
<text class="section-title">最近任务</text>
<text class="view-all" @click="goToTasks('all')">查看全部 ></text>
</view>
<view v-if="recentTasks.length > 0" class="task-list">
<view v-for="task in recentTasks" :key="task.id" class="task-item" @click="viewTaskDetail(task.id)">
<view class="task-info">
<text class="task-order">订单: {{ task.order_id.slice(-6) }}</text>
<text class="task-fee">配送费: ¥{{ task.delivery_fee }}</text>
</view>
<view class="task-time">
<text class="time-text">{{ formatTime(task.created_at) }}</text>
<text class="status-text" :class="'status-' + task.status">{{ getTaskStatusText(task.status) }}</text>
</view>
</view>
</view>
<view v-else class="no-data">
<text class="no-data-text">暂无最近任务</text>
</view>
</view>
<!-- 7. 收入统计 -->
<view class="earnings-chart">
<view class="section-header">
<text class="section-title">收入统计</text>
<text class="view-more" @click="goToEarnings">详细报表 ></text>
</view>
<view class="chart-container">
<view class="chart-bar">
<view v-for="(day, index) in weeklyEarnings" :key="index"
class="bar-item"
:style="{ height: (day.amount / maxEarnings * 100) + '%' }">
<text class="bar-label">{{ day.day }}</text>
</view>
</view>
</view>
</view>
<!-- 8. 功能菜单 -->
<view class="function-menu">
<view class="menu-group">
<view class="menu-item" @click="goToEarnings">
<text class="menu-icon">💰</text>
<text class="menu-label">收入明细</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="goToVehicle">
<text class="menu-icon">🚗</text>
<text class="menu-label">车辆管理</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="goToRatings">
<text class="menu-icon">⭐</text>
<text class="menu-label">评价记录</text>
<text class="menu-arrow">></text>
</view>
</view>
<view class="menu-group">
<view class="menu-item" @click="goToHelp">
<text class="menu-icon">❓</text>
<text class="menu-label">帮助中心</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="goToFeedback">
<text class="menu-icon">💬</text>
<text class="menu-label">意见反馈</text>
<text class="menu-arrow">></text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import type { DeliveryDriverType, DeliveryTaskType, ApiResponseType } from '@/types/mall-types'
/* ----------------- 返回按钮 ----------------- */
function backToIndex() {
uni.navigateBack({ url: '/pages/mall/delivery/index' })
}
/* ----------------- 数据 ----------------- */
const driverInfo = ref({
id: '',
real_name: '配送员',
avatar_url: '',
rating: 4.9,
total_orders: 368,
work_status: 1
})
const workStatus = ref(1) // 1 工作中 0 休息中
const currentLocation = ref('朝阳区建国门附近')
const taskCounts = ref({ total: 0, pending: 0, ongoing: 0, completed: 0 })
const todayStats = ref({
deliveries: 12,
earnings: '156.80',
distance: 45.6,
efficiency: 96.5
})
const currentTask = ref<DeliveryTaskType | null>(null)
const recentTasks = ref<DeliveryTaskType[]>([])
const weeklyEarnings = ref([
{ day: '周一', amount: 120 },
{ day: '周二', amount: 156 },
{ day: '周三', amount: 189 },
{ day: '周四', amount: 145 },
{ day: '周五', amount: 203 },
{ day: '周六', amount: 245 },
{ day: '周日', amount: 198 }
])
const maxEarnings = computed(() => Math.max(...weeklyEarnings.value.map(i => i.amount)))
/* ----------------- 生命周期 ----------------- */
onMounted(() => {
loadDriverInfo()
loadTaskCounts()
loadCurrentTask()
loadRecentTasks()
})
/* ----------------- 方法 ----------------- */
function loadDriverInfo() {
driverInfo.value = {
id: 'driver001',
user_id: 'user001',
real_name: '李师傅',
id_card: '110101199001011234',
driver_license: 'C1',
vehicle_type: 1,
vehicle_number: '京A12345',
work_status: 1,
current_location: { lat: 39.9042, lng: 116.4074 },
service_areas: ['朝阳区', '东城区'],
rating: 4.9,
total_orders: 368,
auth_status: 1,
created_at: '2024-01-01',
updated_at: '2024-12-01'
}
}
function loadTaskCounts() {
taskCounts.value = { total: 25, pending: 3, ongoing: 1, completed: 21 }
}
function loadCurrentTask() {
currentTask.value = {
id: 'task001',
order_id: 'order001',
driver_id: 'driver001',
pickup_address: { address: '朝阳区建国门大街1号', lat: 39.9042, lng: 116.4074 },
delivery_address: { address: '朝阳区CBD核心区2号', lat: 39.9142, lng: 116.4174 },
distance: 2.5,
estimated_time: 15,
delivery_fee: 8.0,
status: 2,
pickup_time: null,
delivered_time: null,
delivery_code: 'DEL123',
remark: '联系电话: 13888888888',
created_at: '2024-12-01 14:30:00',
updated_at: '2024-12-01 14:30:00'
}
}
function loadRecentTasks() {
recentTasks.value = [
{
id: 'task002',
order_id: 'order002',
driver_id: 'driver001',
pickup_address: { address: '朝阳区国贸中心' },
delivery_address: { address: '朝阳区望京SOHO' },
distance: 12.5,
estimated_time: 35,
delivery_fee: 12.0,
status: 4,
pickup_time: '2024-12-01 13:00:00',
delivered_time: '2024-12-01 13:30:00',
delivery_code: 'DEL122',
remark: '',
created_at: '2024-12-01 12:45:00',
updated_at: '2024-12-01 13:30:00'
},
{
id: 'task003',
order_id: 'order003',
driver_id: 'driver001',
pickup_address: { address: '朝阳区三里屯太古里' },
delivery_address: { address: '朝阳区工体北路' },
distance: 1.8,
estimated_time: 8,
delivery_fee: 6.0,
status: 4,
pickup_time: '2024-12-01 11:30:00',
delivered_time: '2024-12-01 11:45:00',
delivery_code: 'DEL121',
remark: '',
created_at: '2024-12-01 11:15:00',
updated_at: '2024-12-01 11:45:00'
}
]
}
function getWorkStatus(): string {
const m: Record<number, string> = { 0: '休息中', 1: '工作中', 2: '忙碌中' }
return m[driverInfo.value.work_status] || '未知状态'
}
function getTaskStatusText(status: number): string {
const m: Record<number, string> = { 1: '待接单', 2: '已接单', 3: '配送中', 4: '已完成', 5: '已取消' }
return m[status] || '未知'
}
function getAddressText(address: UTSJSONObject): string {
return (address['address'] as string) || '地址信息'
}
function formatTime(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime()
const hours = Math.floor(diff / 36e5)
if (hours < 1) return '刚刚'
if (hours < 24) return `${hours}小时前`
return `${Math.floor(hours / 24)}天前`
}
/* ----------------- 交互 ----------------- */
function toggleWorkStatus() {
workStatus.value = workStatus.value === 1 ? 0 : 1
driverInfo.value.work_status = workStatus.value
uni.showToast({
title: workStatus.value === 1 ? '已开始工作' : '已停止工作',
icon: 'success'
})
}
function contactCustomer() {
uni.showActionSheet({
itemList: ['拨打电话', '发送短信'],
success: res => {
if (res.tapIndex === 0) uni.makePhoneCall({ phoneNumber: '13888888888' })
}
})
}
function viewTaskDetail(taskId = '') {
const id = taskId || currentTask.value?.id || ''
uni.navigateTo({ url: `/pages/mall/delivery/task-detail?id=${id}` })
}
/* ----------------- 导航 ----------------- */
function editProfile() {
uni.navigateTo({ url: '/pages/mall/delivery/profile-edit' })
}
function goToSettings() {
uni.navigateTo({ url: '/pages/mall/delivery/settings' })
}
function goToTasks(type: string) {
uni.navigateTo({ url: `/pages/mall/delivery/tasks?type=${type}` })
}
function goToEarnings() {
uni.navigateTo({ url: '/pages/mall/delivery/earnings' })
}
function goToVehicle() {
uni.navigateTo({ url: '/pages/mall/delivery/vehicle' })
}
function goToRatings() {
uni.navigateTo({ url: '/pages/mall/delivery/ratings' })
}
function goToHelp() {
uni.navigateTo({ url: '/pages/mall/common/help' })
}
function goToFeedback() {
uni.navigateTo({ url: '/pages/mall/common/feedback' })
}
</script>
<style scoped>
/* ---------- 返回按钮:蓝色条最左边垂直居中 ---------- */
.profile-header {
position: relative;
}
.back-box {
position: absolute;
left: 30rpx;
top: 50%;
transform: translateY(-50%);
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: rgba(0, 0, 0, .15);
display: flex;
align-items: center;
justify-content: center;
}
.back-box:active {
background: rgba(0, 0, 0, .3);
}
.back-icon {
font-size: 40rpx;
color: #fff;
}
/* ---------- 以下与原样式一致 ---------- */
.delivery-profile {
padding: 0 0 120rpx 0;
background-color: #f5f5f5;
min-height: 100vh;
}
.profile-header {
display: flex;
align-items: center;
padding: 40rpx 30rpx;
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
}
.driver-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
margin-left: 80rpx; /* 给返回按钮留位置 */
margin-right: 30rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
}
.driver-info {
flex: 1;
}
.driver-name {
font-size: 36rpx;
font-weight: bold;
color: white;
margin-bottom: 10rpx;
}
.driver-status {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 15rpx;
}
.driver-stats {
display: flex;
gap: 30rpx;
}
.stat-item {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.9);
}
.settings-icon {
font-size: 36rpx;
color: white;
padding: 10rpx;
}
.work-status, .task-shortcuts, .today-stats, .current-task, .recent-tasks, .earnings-chart, .function-menu {
margin: 20rpx 30rpx;
background: white;
border-radius: 20rpx;
padding: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.view-all, .view-more {
font-size: 24rpx;
color: #74b9ff;
}
.status-controls {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.status-toggle {
display: flex;
justify-content: space-between;
align-items: center;
}
.toggle-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.toggle-switch {
position: relative;
width: 100rpx;
height: 50rpx;
background: #ddd;
border-radius: 25rpx;
transition: all 0.3s;
}
.toggle-switch.active {
background: #74b9ff;
}
.toggle-handle {
position: absolute;
top: 5rpx;
left: 5rpx;
width: 40rpx;
height: 40rpx;
background: white;
border-radius: 50%;
transition: all 0.3s;
}
.toggle-switch.active .toggle-handle {
left: 55rpx;
}
.current-location {
padding: 15rpx 20rpx;
background: #e8f4fd;
border-radius: 15rpx;
}
.location-text {
font-size: 24rpx;
color: #74b9ff;
}
.task-tabs {
display: flex;
justify-content: space-between;
}
.task-tab {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
}
.tab-icon {
font-size: 48rpx;
margin-bottom: 10rpx;
}
.tab-text {
font-size: 24rpx;
color: #666;
}
.tab-badge {
position: absolute;
top: -10rpx;
right: 20rpx;
background: #ff6b6b;
color: white;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 10rpx;
min-width: 30rpx;
text-align: center;
}
.tab-badge.alert {
background: #ff4757;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.stats-grid {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 20rpx;
}
.stat-card {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
min-width: 150rpx;
padding: 20rpx;
background: #e8f4fd;
border-radius: 15rpx;
}
.stat-value {
font-size: 36rpx;
font-weight: bold;
color: #74b9ff;
margin-bottom: 5rpx;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.task-card {
padding: 25rpx;
background: #e8f4fd;
border-radius: 15rpx;
border-left: 6rpx solid #74b9ff;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.task-id {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.task-status {
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 20rpx;
background: #74b9ff;
color: white;
}
.task-route {
margin-bottom: 25rpx;
}
.route-point {
display: flex;
align-items: flex-start;
margin-bottom: 15rpx;
}
.point-icon {
font-size: 32rpx;
margin-right: 15rpx;
margin-top: 5rpx;
}
.point-info {
flex: 1;
}
.point-label {
font-size: 22rpx;
color: #666;
margin-bottom: 5rpx;
}
.point-address {
font-size: 26rpx;
color: #333;
line-height: 1.4;
}
.route-line {
width: 2rpx;
height: 30rpx;
background: #ddd;
margin-left: 16rpx;
margin-bottom: 5rpx;
}
.task-actions {
display: flex;
gap: 20rpx;
}
.action-btn {
flex: 1;
padding: 20rpx;
border-radius: 15rpx;
font-size: 26rpx;
background: #f0f0f0;
color: #333;
border: none;
}
.action-btn.primary {
background: #74b9ff;
color: white;
}
.task-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.task-item {
padding: 25rpx;
background: #f8f9ff;
border-radius: 15rpx;
border-left: 6rpx solid #74b9ff;
}
.task-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.task-order {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.task-fee {
font-size: 24rpx;
color: #74b9ff;
font-weight: bold;
}
.task-time {
display: flex;
justify-content: space-between;
align-items: center;
}
.time-text {
font-size: 22rpx;
color: #999;
}
.status-text {
font-size: 22rpx;
padding: 4rpx 8rpx;
border-radius: 15rpx;
background: #e3f2fd;
color: #1976d2;
}
.status-4 {
background: #e8f5e8;
color: #388e3c;
}
.chart-container {
padding: 20rpx 0;
}
.chart-bar {
display: flex;
justify-content: space-between;
align-items: flex-end;
height: 200rpx;
gap: 10rpx;
}
.bar-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
}
.bar-item::before {
content: '';
width: 100%;
background: linear-gradient(180deg, #74b9ff 0%, #0984e3 100%);
border-radius: 8rpx 8rpx 0 0;
min-height: 20rpx;
}
.bar-label {
font-size: 20rpx;
color: #666;
margin-top: 10rpx;
}
.menu-group {
margin-bottom: 30rpx;
}
.menu-group:last-child {
margin-bottom: 0;
}
.menu-item {
display: flex;
align-items: center;
padding: 25rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-icon {
font-size: 36rpx;
width: 60rpx;
margin-right: 25rpx;
}
.menu-label {
flex: 1;
font-size: 28rpx;
color: #333;
}
.menu-arrow {
font-size: 24rpx;
color: #ccc;
}
.no-data {
text-align: center;
padding: 60rpx 0;
}
.no-data-text {
font-size: 24rpx;
color: #999;
}
</style>