Files
medical-mall/pages/mall/delivery/orders/index.uvue
2026-06-12 10:16:27 +08:00

595 lines
14 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>
<ServicePageScaffold title="订单" fallback-url="/pages/mall/delivery/home/index" :hide-header="true">
<view class="page">
<view class="hero">
<text class="hero-title">服务订单</text>
<text class="hero-subtitle">待接单、今日订单、历史订单统一查看</text>
</view>
<view class="tab-row">
<view v-for="item in tabs" :key="item.value" class="tab-item" :class="currentTab == item.value ? 'tab-item-active' : ''" @click="switchTab(item.value)">
<text class="tab-text" :class="currentTab == item.value ? 'tab-text-active' : ''">{{ item.label }}</text>
</view>
</view>
<view class="search-section">
<view class="search-bar">
<view class="search-input-wrap">
<text class="search-icon">🔍</text>
<input class="search-input" placeholder="搜索订单号" :value="searchKeyword" @input="onSearchInput" @confirm="onSearchConfirm" />
<view v-if="searchKeyword != ''" class="search-clear" @tap="clearSearch">
<text class="clear-icon">✕</text>
</view>
</view>
<view class="sort-wrap">
<view class="sort-btn" @tap="openSortActionSheet">
<text class="sort-check-inline">✔</text>
<text class="sort-btn-text">{{ currentSortLabel }}</text>
<text class="sort-arrow">⌄</text>
</view>
</view>
</view>
<view v-if="searchKeyword != ''" class="search-count-row">
<text class="search-count-text">{{ filteredOrders.length }} 条结果</text>
</view>
</view>
<view class="list-card">
<view v-if="filteredOrders.length == 0" class="empty-box"><text class="empty-text">{{ emptyText }}</text></view>
<view v-for="item in filteredOrders" :key="item.id" class="order-card">
<view class="order-top" @click="goDetail(item.id)">
<view class="order-main">
<text class="order-title">{{ item.serviceName }}</text>
<text class="order-subtitle">{{ item.elderName }} · {{ formatDateTime(item.appointmentTime) }}</text>
<text class="order-id">订单号:{{ item.orderNo }}</text>
</view>
<text class="order-status">{{ item.statusText }}</text>
</view>
<view class="meta-box" @click="goDetail(item.id)">
<text class="meta-text">地址:{{ item.address }} {{ item.addressDetail }}</text>
<text class="meta-text">时长:{{ item.duration }} 分钟 · 预计收入:¥{{ item.staffIncome }}</text>
<text class="meta-text">风险:{{ formatTags(item.riskTags) }}</text>
<text v-if="item.acceptedByName != ''" class="meta-text accepted-by">接单人:{{ item.acceptedByName }}</text>
</view>
<view class="action-row">
<view class="outline-btn" @click="goDetail(item.id)"><text class="outline-btn-text">查看详情</text></view>
<view v-if="showReject(item.status)" class="warn-btn" @click="rejectOrder(item.id)"><text class="warn-btn-text">拒单</text></view>
<view class="primary-btn" @click="handleAction(item.id, item.status)"><text class="primary-btn-text">{{ getActionText(item.status) }}</text></view>
</view>
</view>
</view>
</view>
</ServicePageScaffold>
</template>
<script setup lang="uts">
import { computed, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import ServicePageScaffold from '@/components/homeService/ServicePageScaffold.uvue'
import type { DeliveryOrderStatus, DeliveryOrderType } from '@/types/delivery.uts'
import {
acceptServiceOrder,
getHistoryServiceOrders,
getPendingServiceOrders,
getTodayServiceOrders,
rejectServiceOrder
} from '@/services/deliveryService.uts'
import { getDeliveryOrderTabs, getPrimaryActionText } from '@/utils/deliveryCareUi.uts'
import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts'
import { formatDateTime } from '@/utils/utils.uts'
const tabs = getDeliveryOrderTabs()
const currentTab = ref('pending')
const orders = ref([] as Array<DeliveryOrderType>)
const searchKeyword = ref('')
const sortMode = ref('appointment_desc')
const sortIndex = ref(0)
const currentSortLabel = ref('晚到早')
const sortLabels = ['晚到早', '早到晚', '最新接单'] as Array<string>
const sortValues = ['appointment_desc', 'appointment_asc', 'created_desc'] as Array<string>
function onSortPickerCancel() {
console.warn('[orders/sort] picker cancel')
}
function openSortActionSheet() {
console.warn('[orders/sort] open action sheet')
uni.showActionSheet({
itemList: sortLabels,
success: (res) => {
const index = res.tapIndex
if (index < 0 || index >= sortValues.length) {
console.warn('[orders/sort] invalid action sheet index:', index)
return
}
sortIndex.value = index
sortMode.value = sortValues[index]
currentSortLabel.value = sortLabels[index]
console.warn('[orders/sort] action sheet change:', sortMode.value, currentSortLabel.value)
},
fail: (err) => {
console.warn('[orders/sort] action sheet cancel/fail:', err)
}
})
}
function parseTime(value: string | null | undefined): number {
if (value == null) return 0
const raw = value.trim()
if (raw == '') return 0
let normalized = raw
// 接口返回 "2026-06-10 15:28:21"iOS/小程序不兼容,统一转成 "2026/06/10 15:28:21"
// ISO 格式 "2026-06-10T15:28:21" 不要替换
if (normalized.indexOf('T') < 0) {
normalized = normalized.replace(/-/g, '/')
}
const time = new Date(normalized).getTime()
// NaN 判断NaN 不等于自己
if (time != time) {
console.warn('[orders/sort] invalid time:', value)
return 0
}
return time
}
function getSortTime(item: DeliveryOrderType): number {
if (sortMode.value == 'created_desc') {
if (item.acceptTime != '') return parseTime(item.acceptTime)
if (item.updatedAt != '') return parseTime(item.updatedAt)
return parseTime(item.createdAt)
}
if (item.appointmentTime != '') return parseTime(item.appointmentTime)
if (item.appointmentStartTime != '') return parseTime(item.appointmentStartTime)
return 0
}
function sortOrderList(list: Array<DeliveryOrderType>): Array<DeliveryOrderType> {
const wrapped = list.map((item, index) => {
return {
item,
index,
time: getSortTime(item)
} as { item: DeliveryOrderType, index: number, time: number }
})
wrapped.sort((a, b) => {
if (a.time == 0 && b.time != 0) return 1
if (a.time != 0 && b.time == 0) return -1
if (sortMode.value == 'appointment_asc') {
if (a.time != b.time) return a.time - b.time
} else {
if (a.time != b.time) return b.time - a.time
}
return a.index - b.index
})
return wrapped.map((entry) => entry.item)
}
const filteredOrders = computed((): Array<DeliveryOrderType> => {
const keyword = searchKeyword.value.trim().toLowerCase()
let result: Array<DeliveryOrderType>
if (keyword == '') {
result = orders.value.slice()
} else {
result = orders.value.filter((item) => item.orderNo.toLowerCase().includes(keyword))
}
return sortOrderList(result)
})
const emptyText = computed((): string => {
if (searchKeyword.value.trim() != '' && filteredOrders.value.length == 0) {
return '暂无匹配订单'
}
if (currentTab.value == 'pending') return '暂无待接单订单'
if (currentTab.value == 'today') return '暂无今日订单'
return '暂无历史订单'
})
async function loadData() {
const authResult = await requireDeliveryAuth({ redirectOnFail: true, toastOnFail: true })
if (!authResult.ok) {
return
}
if (currentTab.value == 'pending') {
orders.value = await getPendingServiceOrders()
console.warn('[orders/index] pending orders:', JSON.stringify(orders.value))
return
}
if (currentTab.value == 'history') {
orders.value = await getHistoryServiceOrders()
console.warn('[orders/index] history orders:', JSON.stringify(orders.value))
return
}
orders.value = await getTodayServiceOrders()
console.warn('[orders/index] today orders:', JSON.stringify(orders.value))
}
function consumeStoredTab(): void {
const storedTab = uni.getStorageSync('delivery_order_tab') as string | null
if (storedTab != null && storedTab != '') {
currentTab.value = storedTab
uni.removeStorageSync('delivery_order_tab')
}
}
function switchTab(tab: string) {
currentTab.value = tab
searchKeyword.value = ''
loadData()
}
function onSearchInput(e: any) {
searchKeyword.value = e.detail.value as string
}
function onSearchConfirm() {
uni.showToast({ title: '已搜索', icon: 'none' })
}
function clearSearch() {
searchKeyword.value = ''
}
function formatTags(tags: Array<string>): string {
if (tags.length == 0) return '常规'
return tags.join(' / ')
}
function showReject(status: DeliveryOrderStatus): boolean {
return status == 'pending_assignment' || status == 'pending_accept'
}
function getActionText(status: DeliveryOrderStatus): string {
return getPrimaryActionText(status)
}
function goDetail(orderId: string) {
uni.navigateTo({ url: '/pages/mall/delivery/orders/detail?id=' + orderId })
}
function goRecord(orderId: string) {
uni.navigateTo({ url: '/pages/mall/delivery/service-record/index?id=' + orderId })
}
function rejectOrder(orderId: string) {
uni.showActionSheet({
itemList: ['时间冲突', '距离过远', '技能不匹配', '其他原因'],
success: async (result) => {
const reasons = ['时间冲突', '距离过远', '技能不匹配', '其他原因']
const reason = reasons[result.tapIndex]
await rejectServiceOrder(orderId, reason)
uni.showToast({ title: '已拒单', icon: 'success' })
loadData()
}
})
}
async function handleAction(orderId: string, status: DeliveryOrderStatus) {
if (status == 'pending_assignment' || status == 'pending_accept') {
await acceptServiceOrder(orderId)
uni.showToast({ title: '接单成功', icon: 'success' })
loadData()
return
}
if (status == 'accepted' || status == 'waiting_departure') {
uni.navigateTo({ url: '/pages/mall/delivery/orders/route?id=' + orderId })
return
}
if (status == 'departed' || status == 'on_the_way') {
uni.navigateTo({ url: '/pages/mall/delivery/orders/route?id=' + orderId })
return
}
if (status == 'arrived') {
uni.navigateTo({ url: '/pages/mall/delivery/orders/checkin?id=' + orderId })
return
}
if (status == 'in_service' || status == 'serving' || status == 'completed') {
goRecord(orderId)
return
}
if (status == 'pending_confirm' || status == 'pending_acceptance' || status == 'pending_submit') {
uni.showToast({ title: '已完成服务,等待用户验收', icon: 'none' })
return
}
goDetail(orderId)
}
onLoad(() => {
consumeStoredTab()
loadData()
})
onShow(() => {
consumeStoredTab()
loadData()
})
</script>
<style scoped>
.page {
min-height: 100%;
margin-left: -24rpx;
margin-right: -24rpx;
margin-top: -24rpx;
padding: 0 24rpx 36rpx;
background-color: #f3f8fb;
}
.hero {
padding: 68rpx 28rpx 26rpx;
border-bottom-left-radius: 32rpx;
border-bottom-right-radius: 32rpx;
background: linear-gradient(180deg, #1f7db4 0%, #1aa67f 100%);
}
.hero-title {
font-size: 40rpx;
font-weight: 700;
color: #ffffff;
}
.hero-subtitle,
.meta-text,
.empty-text,
.outline-btn-text,
.tab-text {
font-size: 24rpx;
line-height: 36rpx;
}
.hero-subtitle {
margin-top: 10rpx;
color: rgba(255, 255, 255, 0.9);
}
.tab-row,
.order-top,
.action-row {
display: flex;
flex-direction: row;
}
.tab-row {
justify-content: space-between;
margin-top: 20rpx;
padding: 10rpx;
border-radius: 24rpx;
background-color: #ffffff;
box-shadow: 0 10rpx 28rpx rgba(15, 35, 55, 0.06);
}
.search-section {
position: relative;
z-index: 30;
margin-top: 16rpx;
}
.search-bar {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 4rpx;
}
.search-input-wrap {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
padding: 14rpx 20rpx;
border-radius: 24rpx;
background-color: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(15, 35, 55, 0.04);
}
.sort-wrap {
margin-left: 16rpx;
flex-shrink: 0;
}
.sort-btn {
display: flex;
flex-direction: row;
align-items: center;
padding: 14rpx 18rpx;
border-radius: 24rpx;
background-color: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(15, 35, 55, 0.04);
}
.sort-check-inline {
font-size: 24rpx;
color: #18a058;
font-weight: 700;
margin-right: 6rpx;
}
.sort-btn-text {
font-size: 24rpx;
color: #16324f;
margin-right: 6rpx;
}
.sort-arrow {
font-size: 22rpx;
color: #5e758c;
}
.search-icon {
font-size: 28rpx;
margin-right: 12rpx;
}
.search-input {
flex: 1;
font-size: 26rpx;
color: #16324f;
background-color: transparent;
}
.search-clear {
padding: 4rpx;
margin-left: 8rpx;
}
.clear-icon {
font-size: 24rpx;
color: #9aa8b8;
}
.search-count {
font-size: 22rpx;
color: #176e97;
margin-left: 12rpx;
padding: 8rpx 16rpx;
background-color: #e6f3fa;
border-radius: 16rpx;
}
.search-count-row {
margin-top: 10rpx;
padding-left: 8rpx;
}
.search-count-text {
font-size: 22rpx;
color: #176e97;
}
.tab-item {
width: 32%;
padding: 18rpx 0;
border-radius: 18rpx;
align-items: center;
justify-content: center;
}
.tab-item-active {
background-color: #e6f3fa;
}
.tab-text {
color: #5e758c;
}
.tab-text-active {
font-weight: 700;
color: #176e97;
}
.list-card {
margin-top: 22rpx;
padding: 24rpx;
border-radius: 28rpx;
background-color: #ffffff;
box-shadow: 0 10rpx 28rpx rgba(15, 35, 55, 0.06);
position: relative;
z-index: 1;
}
.order-card {
padding: 24rpx;
margin-bottom: 18rpx;
border-radius: 22rpx;
background-color: #f7fbfd;
}
.order-top,
.action-row {
justify-content: space-between;
align-items: center;
}
.order-main {
flex: 1;
padding-right: 18rpx;
}
.order-title {
font-size: 30rpx;
font-weight: 700;
color: #16324f;
}
.order-subtitle,
.order-status,
.meta-text,
.empty-text,
.outline-btn-text,
.warn-btn-text,
.primary-btn-text,
.order-id {
margin-top: 10rpx;
}
.order-subtitle,
.meta-text,
.empty-text,
.outline-btn-text {
color: #5e758c;
}
.order-id {
font-size: 22rpx;
color: #8a9bae;
font-family: 'Courier New', monospace;
}
.order-status {
font-size: 24rpx;
color: #176e97;
}
.meta-box {
margin-top: 14rpx;
padding: 18rpx;
border-radius: 18rpx;
background-color: #eef7fb;
}
.action-row {
margin-top: 18rpx;
flex-wrap: wrap;
}
.outline-btn,
.warn-btn,
.primary-btn {
width: 31%;
padding: 18rpx 0;
border-radius: 18rpx;
align-items: center;
justify-content: center;
}
.outline-btn {
background-color: #eef6fa;
}
.warn-btn {
background-color: #fff3e6;
}
.warn-btn-text {
font-size: 26rpx;
font-weight: 700;
color: #c77413;
}
.primary-btn {
background-color: #1f7db4;
}
.primary-btn-text {
font-size: 26rpx;
font-weight: 700;
color: #ffffff;
}
.empty-box {
padding: 24rpx 0;
}
.accepted-by {
color: #1f7db4;
font-weight: 600;
}
</style>