增加展示选择

This commit is contained in:
2026-06-12 10:16:27 +08:00
parent 389c22c627
commit 81f3e1d3b6
28 changed files with 5983 additions and 131 deletions

View File

@@ -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' })

View File

@@ -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>

View File

@@ -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>

View File

@@ -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() {

View File

@@ -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()