增加展示选择
This commit is contained in:
@@ -47,14 +47,14 @@ import ServicePanel from '@/components/homeService/ServicePanel.uvue'
|
||||
import type { DeliveryLocationType, DeliveryOrderType } from '@/types/delivery.uts'
|
||||
import { checkinOrder, getDeliveryOrderDetail } from '@/services/deliveryService.uts'
|
||||
import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts'
|
||||
import { getCurrentUserId } from '@/utils/store.uts'
|
||||
import { getDeliveryRouteParam } from '@/utils/deliveryRoute.uts'
|
||||
import {
|
||||
emailLogin,
|
||||
checkinPrecheck,
|
||||
getHomecareToken,
|
||||
getHomecareUser,
|
||||
getReasonText
|
||||
} from '@/utils/homecareAuth.uts'
|
||||
import { debugCurrentUser } from '@/utils/akUserMapping.uts'
|
||||
|
||||
const orderId = ref('')
|
||||
const order = ref<DeliveryOrderType | null>(null)
|
||||
@@ -79,16 +79,13 @@ const reasonText = ref('')
|
||||
|
||||
function updateHomecareLoginStatus(): void {
|
||||
console.warn('[CHECKIN DEBUG] updateHomecareLoginStatus: called')
|
||||
const token = getHomecareToken()
|
||||
console.warn('[CHECKIN DEBUG] updateHomecareLoginStatus: token length:', token.length)
|
||||
if (token !== '') {
|
||||
// 改用 Supabase 当前用户判断是否已登录(homecare 专属 token 已废弃,从未被写入)
|
||||
const userId = getCurrentUserId()
|
||||
console.warn('[CHECKIN DEBUG] updateHomecareLoginStatus: token length:', userId.length)
|
||||
if (userId !== '') {
|
||||
isHomecareLoggedIn.value = true
|
||||
const user = getHomecareUser()
|
||||
if (user != null) {
|
||||
const email = user.getString('email')
|
||||
homecareUserEmail.value = email != null ? email : ''
|
||||
console.warn('[CHECKIN DEBUG] updateHomecareLoginStatus: logged in as', homecareUserEmail.value)
|
||||
}
|
||||
homecareUserEmail.value = '已登录 (uid: ' + userId.substring(0, 8) + '...)'
|
||||
console.warn('[CHECKIN DEBUG] updateHomecareLoginStatus: logged in as', homecareUserEmail.value)
|
||||
} else {
|
||||
isHomecareLoggedIn.value = false
|
||||
homecareUserEmail.value = ''
|
||||
@@ -263,6 +260,10 @@ async function handlePrecheck(): Promise<void> {
|
||||
}
|
||||
|
||||
console.warn('[CHECKIN PAGE] 开始预校验流程')
|
||||
|
||||
// 调试:打印当前 ak_user 信息
|
||||
debugCurrentUser()
|
||||
|
||||
prechecking.value = true
|
||||
canCheckin.value = false
|
||||
distanceText.value = '定位中...'
|
||||
@@ -334,6 +335,13 @@ async function submitCheckin() {
|
||||
uni.showToast({ title: '订单信息缺失', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 必须通过预校验才能提交
|
||||
if (!canCheckin.value) {
|
||||
uni.showToast({ title: '请先完成距离预校验', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (currentLocation.value == null) {
|
||||
console.warn('[CHECKIN DEBUG] submitCheckin: currentLocation is null, calling getCurrentLocation')
|
||||
await getCurrentLocation()
|
||||
@@ -373,8 +381,17 @@ async function doCheckin() {
|
||||
checkinMode: 'gps'
|
||||
})
|
||||
console.warn('[CHECKIN DEBUG] submitCheckin: checkinOrder succeeded')
|
||||
uni.showToast({ title: '签到成功', icon: 'success' })
|
||||
uni.redirectTo({ url: '/pages/mall/delivery/service-record/index?id=' + orderId.value })
|
||||
|
||||
// 签到成功后显示等待消费者确认
|
||||
uni.showModal({
|
||||
title: '签到成功',
|
||||
content: '已提交到达签到,等待消费者确认',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
// 返回订单列表或详情页
|
||||
uni.navigateBack()
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('[CHECKIN DEBUG] submitCheckin error:', error)
|
||||
uni.showToast({ title: '签到失败,请重试', icon: 'none' })
|
||||
|
||||
@@ -59,6 +59,13 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<text class="section-title">服务人员</text>
|
||||
<text class="row-text">配送员:{{ order.deliveryStaffName }}</text>
|
||||
<text v-if="order.acceptedByName != ''" class="row-text">接单人:{{ order.acceptedByName }} (ID: {{ order.acceptedBy }})</text>
|
||||
<text v-if="order.acceptTime != ''" class="row-text">接单时间:{{ formatDateTime(order.acceptTime) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<text class="section-title">用户备注</text>
|
||||
<text class="row-text">特殊需求:{{ order.remark }}</text>
|
||||
|
||||
@@ -10,13 +10,35 @@
|
||||
<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="orders.length == 0" class="empty-box"><text class="empty-text">{{ emptyText }}</text></view>
|
||||
<view v-for="item in orders" :key="item.id" class="order-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>
|
||||
@@ -24,6 +46,7 @@
|
||||
<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>
|
||||
@@ -55,8 +78,105 @@ 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 '暂无历史订单'
|
||||
@@ -91,9 +211,22 @@ function consumeStoredTab(): void {
|
||||
|
||||
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(' / ')
|
||||
@@ -222,6 +355,104 @@ onShow(() => {
|
||||
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;
|
||||
@@ -249,6 +480,8 @@ onShow(() => {
|
||||
border-radius: 28rpx;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 10rpx 28rpx rgba(15, 35, 55, 0.06);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
@@ -281,7 +514,8 @@ onShow(() => {
|
||||
.empty-text,
|
||||
.outline-btn-text,
|
||||
.warn-btn-text,
|
||||
.primary-btn-text {
|
||||
.primary-btn-text,
|
||||
.order-id {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
@@ -292,6 +526,12 @@ onShow(() => {
|
||||
color: #5e758c;
|
||||
}
|
||||
|
||||
.order-id {
|
||||
font-size: 22rpx;
|
||||
color: #8a9bae;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
font-size: 24rpx;
|
||||
color: #176e97;
|
||||
@@ -346,4 +586,9 @@ onShow(() => {
|
||||
.empty-box {
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
.accepted-by {
|
||||
color: #1f7db4;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<button class="secondary-btn" @click="getCurrentLocation">获取当前位置</button>
|
||||
<button class="primary-btn" :disabled="submitting" @click="startDepartAction">{{ submitting ? '处理中...' : '点击出发' }}</button>
|
||||
<button class="secondary-btn" @click="openMap">打开地图导航</button>
|
||||
<button class="primary-btn" :disabled="submitting" @click="arriveAction">标记到达</button>
|
||||
<button class="primary-btn" :disabled="submitting" @click="goToCheckin">标记到达签到</button>
|
||||
</view>
|
||||
</ServicePanel>
|
||||
</ServicePageScaffold>
|
||||
@@ -22,7 +22,7 @@ import { onLoad } from '@dcloudio/uni-app'
|
||||
import ServicePageScaffold from '@/components/homeService/ServicePageScaffold.uvue'
|
||||
import ServicePanel from '@/components/homeService/ServicePanel.uvue'
|
||||
import type { DeliveryLocationType, DeliveryOrderType } from '@/types/delivery.uts'
|
||||
import { arriveOrder, getDeliveryOrderDetail, startDepart } from '@/services/deliveryService.uts'
|
||||
import { getDeliveryOrderDetail, startDepart } from '@/services/deliveryService.uts'
|
||||
import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts'
|
||||
import { getDeliveryRouteParam } from '@/utils/deliveryRoute.uts'
|
||||
import { formatDateTime } from '@/utils/utils.uts'
|
||||
@@ -87,22 +87,20 @@ async function startDepartAction() {
|
||||
}
|
||||
}
|
||||
|
||||
async function arriveAction() {
|
||||
/**
|
||||
* 跳转到签到页面
|
||||
* 不再直接调用 arriveOrder RPC,而是进入签到页面执行距离预校验、拍照上传、正式签到
|
||||
*/
|
||||
function goToCheckin() {
|
||||
if (submitting.value) return
|
||||
|
||||
// 先获取当前位置(签到页面会再次获取)
|
||||
if (currentLocation.value == null) {
|
||||
await getCurrentLocation()
|
||||
if (currentLocation.value == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await arriveOrder(orderId.value, currentLocation.value as DeliveryLocationType)
|
||||
uni.showToast({ title: '已标记到达', icon: 'success' })
|
||||
uni.navigateTo({ url: '/pages/mall/delivery/orders/checkin?id=' + orderId.value })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
getCurrentLocation()
|
||||
}
|
||||
|
||||
// 直接跳转到签到页面
|
||||
uni.navigateTo({ url: '/pages/mall/delivery/orders/checkin?id=' + orderId.value })
|
||||
}
|
||||
|
||||
function openMap() {
|
||||
|
||||
@@ -150,6 +150,7 @@ import { getCurrentUser, logout, setIsLoggedIn, setUserProfile } from '@/utils/s
|
||||
import { checkMerchantAccount, clearAuth, goMerchantHome, isMerchantRole, requireMerchantAuth, setMerchantInfo } from '@/utils/merchantAuth.uts'
|
||||
import { clearDeliveryAuth, goDeliveryHome, requireDeliveryAuth, saveDeliverySession } from '@/utils/deliveryAuth.uts'
|
||||
import { loginDelivery } from '@/services/deliveryService.uts'
|
||||
import { loadCurrentAkUser } from '@/utils/akUserMapping.uts'
|
||||
import type { UserProfile } from '@/types/mall-types.uts'
|
||||
import { AkReq } from '@/uni_modules/ak-req/index.uts'
|
||||
|
||||
@@ -498,6 +499,15 @@ const handleLogin = async () => {
|
||||
clearDeliveryAuth()
|
||||
const result = await loginDelivery({ account: account.value.trim(), password: password.value.trim() })
|
||||
saveDeliverySession(result.token, result.userInfo, result.deliveryInfo)
|
||||
|
||||
// 加载 ak_users 映射(Supabase Auth ID -> 业务用户 ID)
|
||||
try {
|
||||
await loadCurrentAkUser()
|
||||
console.log('[Login] ak_users 映射加载成功')
|
||||
} catch (akErr) {
|
||||
console.warn('[Login] ⚠️ ak_users 映射加载失败(不影响登录):', akErr)
|
||||
}
|
||||
|
||||
const authResult = await requireDeliveryAuth({ redirectOnFail: false, toastOnFail: false })
|
||||
if (!authResult.ok) {
|
||||
clearDeliveryAuth()
|
||||
|
||||
Reference in New Issue
Block a user