Files
medical-mall/pages/mall/delivery/orders/checkin.uvue
2026-06-12 13:03:13 +08:00

587 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>
<ServicePageScaffold title="到达签到" fallback-url="/pages/mall/delivery/orders/route">
<ServicePanel title="居家服务认证" subtitle="使用居家服务系统账号登录">
<view v-if="!isHomecareLoggedIn">
<text class="info-text">状态:未登录</text>
<text class="info-text warning-text">居家服务认证服务暂不可用,请跳过此步骤继续操作</text>
</view>
<view v-else>
<text class="info-text">已登录:{{ homecareUserEmail }}</text>
<text class="info-text success-text">居家服务认证通过</text>
</view>
</ServicePanel>
<ServicePanel title="签到要求" subtitle="定位签到或扫码签到,要求在 50 米范围内并上传现场图片。">
<text v-if="order != null" class="info-text">服务地址:{{ order.fullAddress }}</text>
<text class="info-text">当前定位:{{ locationText }}</text>
<text class="info-text">定位精度:{{ accuracyText }}</text>
<text class="info-text">现场图片:{{ photos.length }} 张</text>
</ServicePanel>
<ServicePanel title="距离预校验" subtitle="获取定位后校验是否在允许签到范围内">
<text class="info-text">距离服务点:{{ distanceText }}</text>
<text class="info-text">允许半径:{{ allowedRadiusText }}</text>
<text class="info-text">校验状态:{{ precheckStatusText }}</text>
<text v-if="reasonText !== ''" class="info-text warning-text">{{ reasonText }}</text>
<view class="button-stack">
<button class="secondary-btn" :disabled="prechecking" @click="handlePrecheck">{{ prechecking ? '预校验中...' : '距离预校验' }}</button>
</view>
</ServicePanel>
<ServicePanel title="签到操作" subtitle="定位失败时要给出清晰提示。">
<view class="button-stack">
<button class="secondary-btn" @click="getCurrentLocation">获取 GPS 定位</button>
<button class="secondary-btn" @click="choosePhoto">拍照上传现场图片</button>
<input class="note-input" v-model="note" placeholder="补充签到说明,例如入户前核验联系人" />
<button class="primary-btn" :disabled="submitting" @click="submitCheckin">{{ submitting ? '签到中...' : '完成签到' }}</button>
</view>
</ServicePanel>
</ServicePageScaffold>
</template>
<script setup lang="uts">
import { ref } from 'vue'
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 supa from '@/components/supadb/aksupainstance.uts'
import { 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,
getReasonText
} from '@/utils/homecareAuth.uts'
import { debugCurrentUser, getCurrentAkUserId } from '@/utils/akUserMapping.uts'
const orderId = ref('')
const order = ref<DeliveryOrderType | null>(null)
const currentLocation = ref<DeliveryLocationType | null>(null)
const locationText = ref('未获取')
const accuracyText = ref('未知')
const photos = ref([] as Array<string>)
const evidenceFileIds = ref([] as Array<string>)
const note = ref('')
const submitting = ref(false)
// 居家服务认证状态
const isHomecareLoggedIn = ref(false)
const homecareUserEmail = ref('')
// 距离预校验状态
const prechecking = ref(false)
const canCheckin = ref(false)
const distanceText = ref('未校验')
const allowedRadiusText = ref('未校验')
const precheckStatusText = ref('未校验')
const reasonText = ref('')
async function updateHomecareLoginStatus(): Promise<void> {
console.warn('[CHECKIN AUTH] ========== 登录状态检查 START ==========')
// 打印认证相关日志
const authUserId = getCurrentUserId()
const akUserId = await getCurrentAkUserId()
const storageAkUserId = uni.getStorageSync('ak_user_id')
const storageCurrentAkUser = uni.getStorageSync('current_ak_user')
console.warn('[CHECKIN AUTH] getCurrentUserId =', authUserId)
console.warn('[CHECKIN AUTH] getCurrentAkUserId =', akUserId)
console.warn('[CHECKIN AUTH] storage ak_user_id =', storageAkUserId)
console.warn('[CHECKIN AUTH] current_ak_user =', storageCurrentAkUser)
console.warn('[CHECKIN AUTH] ========== 登录状态检查 END ==========')
if (akUserId !== '') {
isHomecareLoggedIn.value = true
homecareUserEmail.value = '已登录 (uid: ' + akUserId.substring(0, 8) + '...)'
console.warn('[CHECKIN AUTH] 居家服务认证通过')
} else {
isHomecareLoggedIn.value = false
homecareUserEmail.value = ''
console.warn('[CHECKIN AUTH] 居家服务认证失败akUserId 为空')
}
}
async function loginHomecare(): Promise<void> {
const token = getHomecareToken()
if (token !== '') {
uni.showToast({ title: '已登录,无需重复登录', icon: 'success' })
return
}
uni.showToast({ title: '居家服务认证服务暂不可用', icon: 'none' })
}
function toRadians(value: number): number {
return value * 3.1415926 / 180
}
function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
const earthRadius = 6378137
const deltaLat = toRadians(lat2 - lat1)
const deltaLng = toRadians(lng2 - lng1)
const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return earthRadius * c
}
function wrapLocation(): Promise<DeliveryLocationType> {
console.warn('[CHECKIN DEBUG] wrapLocation: starting...')
return new Promise((resolve, reject) => {
uni.getLocation({
type: 'gcj02',
isHighAccuracy: true,
enableHighAccuracy: true,
success: (res) => {
console.warn('[CHECKIN DEBUG] wrapLocation SUCCESS:', JSON.stringify({
latitude: res.latitude,
longitude: res.longitude,
accuracy: res.accuracy,
speed: res.speed,
altitude: res.altitude,
altitudeAccuracy: res.altitudeAccuracy,
heading: res.heading,
timestamp: res.timestamp
}))
resolve({
latitude: res.latitude,
longitude: res.longitude,
address: '当前位置',
time: new Date().toISOString().replace('T', ' ').substring(0, 19)
})
},
fail: (err) => {
console.warn('[CHECKIN DEBUG] wrapLocation FAIL:', JSON.stringify(err))
reject(new Error('定位失败'))
}
})
})
}
type LocationFullResult = {
location: DeliveryLocationType
accuracy: number
}
function wrapLocationFull(): Promise<LocationFullResult> {
console.warn('[CHECKIN DEBUG] wrapLocationFull: starting...')
return new Promise((resolve, reject) => {
uni.getLocation({
type: 'gcj02',
isHighAccuracy: true,
enableHighAccuracy: true,
success: (res) => {
console.warn('[CHECKIN DEBUG] wrapLocationFull SUCCESS:', JSON.stringify({
latitude: res.latitude,
longitude: res.longitude,
accuracy: res.accuracy,
speed: res.speed,
altitude: res.altitude,
altitudeAccuracy: res.altitudeAccuracy,
heading: res.heading,
timestamp: res.timestamp
}))
const loc: DeliveryLocationType = {
latitude: res.latitude,
longitude: res.longitude,
address: '当前位置',
time: new Date().toISOString().replace('T', ' ').substring(0, 19)
}
const acc = res.accuracy != null ? res.accuracy : 0
console.warn('[CHECKIN DEBUG] wrapLocationFull resolved with accuracy:', acc)
resolve({ location: loc, accuracy: acc })
},
fail: (err) => {
console.warn('[CHECKIN DEBUG] wrapLocationFull FAIL:', JSON.stringify(err))
reject(new Error('定位失败'))
}
})
})
}
function wrapChooseImage(): Promise<Array<string>> {
return new Promise((resolve, reject) => {
uni.chooseImage({
count: 3,
sourceType: ['camera'],
success: (res) => {
resolve(res.tempFilePaths)
},
fail: () => {
reject(new Error('拍照失败'))
}
})
})
}
async function loadData() {
console.warn('[CHECKIN DEBUG] loadData: called, orderId:', orderId.value)
const authResult = await requireDeliveryAuth({ redirectOnFail: true, toastOnFail: true })
if (!authResult.ok || orderId.value == '') {
console.warn('[CHECKIN DEBUG] loadData: auth failed or orderId empty')
return
}
console.warn('[CHECKIN DEBUG] loadData: fetching order detail')
order.value = await getDeliveryOrderDetail(orderId.value)
console.warn('[CHECKIN DEBUG] loadData: order fetched:', JSON.stringify({
id: order.value?.id,
fullAddress: order.value?.fullAddress,
latitude: order.value?.latitude,
longitude: order.value?.longitude,
allowCheckinRadiusMeters: order.value?.allowCheckinRadiusMeters
}))
updateHomecareLoginStatus()
}
async function getCurrentLocation() {
console.warn('[CHECKIN DEBUG] getCurrentLocation: called')
try {
currentLocation.value = await wrapLocation()
locationText.value = '纬度 ' + String(currentLocation.value.latitude) + ' / 经度 ' + String(currentLocation.value.longitude)
console.warn('[CHECKIN DEBUG] getCurrentLocation completed, location:', JSON.stringify(currentLocation.value))
} catch (error) {
console.warn('[CHECKIN DEBUG] getCurrentLocation error:', error)
uni.showToast({ title: '签到定位失败,请检查定位权限', icon: 'none' })
}
}
async function choosePhoto() {
try {
const selected = await wrapChooseImage()
photos.value = selected
// 上传现场图片并创建 hc_evidence_files 记录
if (selected.length > 0 && orderId.value != '') {
const akUserId = await getCurrentAkUserId()
console.warn('[CHOOSE PHOTO] akUserId:', akUserId)
if (akUserId == '') {
uni.showToast({ title: '未获取到业务用户 ID', icon: 'none' })
return
}
// 上传每张照片
for (let i = 0; i < selected.length; i++) {
const tempPath = selected[i]
try {
// 上传到 Supabase Storage
const { data: uploadData, error: uploadError } = await supa.storage
.from('evidence')
.upload('checkin/' + orderId.value + '/' + Date.now() + '_' + i + '.jpg', {
base64: tempPath,
contentType: 'image/jpeg'
})
if (uploadError == null && uploadData != null) {
// 获取公开 URL
const { data: urlData } = supa.storage
.from('evidence')
.getPublicUrl(uploadData.path)
if (urlData != null) {
const fileUrl = urlData.publicUrl
// 创建 hc_evidence_files 记录
const { data: evidenceRecord, error: evidenceError } = await supa
.from('hc_evidence_files')
.insert({
work_order_id: orderId.value,
uploader_id: akUserId,
file_url: fileUrl,
upload_status: 'UPLOADED',
file_type: 'image',
created_at: new Date().toISOString()
})
.select('id')
.single()
if (evidenceError == null && evidenceRecord != null) {
const evidenceId = evidenceRecord.getString('id') ?? ''
if (evidenceId != '') {
evidenceFileIds.value.push(evidenceId)
console.warn('[CHOOSE PHOTO] 证据记录创建成功, evidenceId:', evidenceId)
}
} else {
console.warn('[CHOOSE PHOTO] 证据记录创建失败:', evidenceError)
}
}
} else {
console.warn('[CHOOSE PHOTO] 图片上传失败:', uploadError)
}
} catch (uploadErr) {
console.warn('[CHOOSE PHOTO] 上传异常:', uploadErr)
}
}
console.warn('[CHOOSE PHOTO] 当前 evidenceFileIds:', evidenceFileIds.value)
}
} catch (error) {
uni.showToast({ title: '拍照失败,请重试', icon: 'none' })
}
}
async function handlePrecheck(): Promise<void> {
console.warn('[CHECKIN PAGE] ========== handlePrecheck START ==========')
console.warn('[CHECKIN PAGE] 当前 orderId:', orderId.value)
if (prechecking.value) {
console.warn('[CHECKIN PAGE] 已经在预校验中,直接返回')
return
}
if (orderId.value === '') {
console.warn('[CHECKIN PAGE] orderId 为空')
uni.showToast({ title: '工单信息缺失', icon: 'none' })
return
}
// 预校验前打印认证日志
const authUserId = getCurrentUserId()
let akUserId = await getCurrentAkUserId()
const storageAkUserId = uni.getStorageSync('ak_user_id')
const storageCurrentAkUser = uni.getStorageSync('current_ak_user')
console.warn('[CHECKIN AUTH] getCurrentUserId =', authUserId)
console.warn('[CHECKIN AUTH] getCurrentAkUserId =', akUserId)
console.warn('[CHECKIN AUTH] storage ak_user_id =', storageAkUserId)
console.warn('[CHECKIN AUTH] current_ak_user =', storageCurrentAkUser)
// 兜底逻辑:如果 akUserId 为空但 authUserId 非空,尝试写入缓存
if ((akUserId == '' || akUserId == null) && authUserId != null && String(authUserId).length > 0) {
console.warn('[CHECKIN AUTH] 兜底akUserId 为空但 authUserId 非空,尝试写入缓存')
console.warn('[CHECKIN AUTH] authUserId =', authUserId)
// 直接写入 authUserId 作为 ak_user_id
uni.setStorageSync('ak_user_id', authUserId)
uni.setStorageSync('current_ak_user', JSON.stringify({ id: authUserId }))
console.warn('[CHECKIN AUTH] 已写入缓存: ak_user_id =', authUserId)
// 重新读取
akUserId = await getCurrentAkUserId()
console.warn('[CHECKIN AUTH] 重新读取 akUserId =', akUserId)
}
// 如果 akUserId 仍然为空,提示重新登录
if (akUserId == '' || akUserId == null) {
uni.showToast({ title: '业务账号未初始化,请重新登录', icon: 'none' })
console.warn('[CHECKIN AUTH] akUserId 仍为空,阻止预校验')
return
}
console.warn('[CHECKIN PAGE] 开始预校验流程')
// 调试:打印当前 ak_user 信息
debugCurrentUser()
prechecking.value = true
canCheckin.value = false
distanceText.value = '定位中...'
allowedRadiusText.value = '定位中...'
precheckStatusText.value = '定位中...'
reasonText.value = ''
try {
console.warn('[CHECKIN PAGE] 步骤 1: 调用 wrapLocationFull 获取位置')
const fullResult = await wrapLocationFull()
const location = fullResult.location
const accuracy = fullResult.accuracy
console.warn('[CHECKIN PAGE] 位置信息: lat=', location.latitude, ' lng=', location.longitude, ' accuracy=', accuracy)
currentLocation.value = location
locationText.value = '纬度 ' + String(location.latitude) + ' / 经度 ' + String(location.longitude)
accuracyText.value = accuracy > 0 ? String(accuracy) + ' 米' : '未知'
// 调用 RPC 预校验(现在直接走 Supabase不需要本地后端
console.warn('[CHECKIN PAGE] 步骤 2: 调用 checkinPrecheck RPC')
const result = await checkinPrecheck(
orderId.value,
location.latitude,
location.longitude,
accuracy
)
console.warn('[CHECKIN PAGE] RPC 返回结果: distance=', result.distanceMeters, ' radius=', result.allowedRadiusMeters, ' canCheckin=', result.canCheckin, ' reason=', result.reasonCode)
if (result.distanceMeters != null) {
distanceText.value = String(result.distanceMeters) + ' 米'
} else {
distanceText.value = '未知'
}
allowedRadiusText.value = String(result.allowedRadiusMeters) + ' 米'
if (result.canCheckin) {
canCheckin.value = true
precheckStatusText.value = '可以签到 / 下一步拍照'
reasonText.value = ''
console.warn('[CHECKIN PAGE] 可以签到')
} else {
canCheckin.value = false
precheckStatusText.value = '不可签到'
reasonText.value = getReasonText(result.reasonCode)
console.warn('[CHECKIN PAGE] 不可签到,原因:', result.reasonCode, getReasonText(result.reasonCode))
}
} catch (error) {
console.warn('[CHECKIN PAGE] ========== 捕获异常 ==========')
console.warn('[CHECKIN PAGE] 异常类型:', error instanceof Error ? 'Error' : typeof error)
console.warn('[CHECKIN PAGE] 异常信息:', error)
console.warn('[CHECKIN PAGE] ========== handlePrecheck END (ERROR) ==========')
uni.showToast({ title: '预校验失败,请重试', icon: 'none' })
precheckStatusText.value = '预校验失败'
} finally {
prechecking.value = false
console.warn('[CHECKIN PAGE] ========== handlePrecheck END (FINISHED) ==========')
}
}
async function submitCheckin() {
console.warn('[CHECKIN DEBUG] submitCheckin: called')
if (submitting.value) {
console.warn('[CHECKIN DEBUG] submitCheckin: already submitting, returning')
return
}
if (order.value == null) {
console.warn('[CHECKIN DEBUG] submitCheckin: order is null')
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()
if (currentLocation.value == null) {
console.warn('[CHECKIN DEBUG] submitCheckin: still null after getCurrentLocation')
return
}
}
if (photos.value.length == 0) {
console.warn('[CHECKIN DEBUG] submitCheckin: no photos uploaded')
uni.showToast({ title: '请至少上传一张现场图片', icon: 'none' })
return
}
// 服务坐标以后端 precheck 返回为准,不再以前端 order.latitude/order.longitude 为准
// 删除了之前的坐标检查
console.warn('[CHECKIN DEBUG] submitCheckin: proceeding with checkin, photos count:', photos.value.length, ' evidenceFileIds:', evidenceFileIds.value)
doCheckin()
}
async function doCheckin() {
submitting.value = true
try {
// 获取 akUserId
const akUserId = await getCurrentAkUserId()
console.warn('[CHECKIN RPC] akUserId:', akUserId)
if (akUserId == '') {
uni.showToast({ title: '未获取到业务用户 ID请重新登录', icon: 'none' })
return
}
// 调用居家服务正式签到 RPCrpc_homecare_checkin_submit
const { submitHomecareCheckin } = await import('@/api/delivery.uts')
const result = await submitHomecareCheckin(
orderId.value,
akUserId,
currentLocation.value!.latitude,
currentLocation.value!.longitude,
'gcj02',
parseFloat(accuracyText.value) || 0,
new Date().toISOString(),
evidenceFileIds.value,
null,
'worker_arrived'
)
console.warn('[CHECKIN RPC] result:', result)
// 签到成功后显示等待消费者确认
uni.showModal({
title: '签到成功',
content: '已提交到达签到,等待消费者确认',
showCancel: false,
success: () => {
// 返回订单列表
uni.navigateBack()
}
})
} catch (error) {
console.warn('[CHECKIN DEBUG] submitCheckin error:', error)
uni.showToast({ title: '签到失败,请重试', icon: 'none' })
} finally {
submitting.value = false
}
}
onLoad((options) => {
console.warn('[CHECKIN DEBUG] onLoad: called, options:', JSON.stringify(options))
if (options != null) {
orderId.value = getDeliveryRouteParam(options as UTSJSONObject, 'id')
console.warn('[CHECKIN DEBUG] onLoad: extracted orderId:', orderId.value)
}
console.warn('[CHECKIN DEBUG] onLoad: calling loadData')
loadData()
})
</script>
<style scoped>
.info-text {
display: block;
margin-bottom: 14rpx;
font-size: 26rpx;
line-height: 38rpx;
color: #16324f;
}
.success-text {
color: #0f766e;
font-weight: bold;
}
.warning-text {
color: #dc2626;
}
.button-stack {
flex-direction: column;
}
.primary-btn,
.secondary-btn {
margin-bottom: 18rpx;
border-radius: 18rpx;
font-size: 28rpx;
}
.primary-btn {
background: #0f766e;
color: #ffffff;
}
.primary-btn[disabled] {
background: #9ca3af;
}
.secondary-btn {
background: #eaf2f0;
color: #0f766e;
}
.note-input {
height: 84rpx;
padding: 0 24rpx;
margin-bottom: 18rpx;
border-radius: 18rpx;
background: #f2f7f6;
font-size: 28rpx;
}
</style>