Files
medical-mall/pages/mall/merchant/order-detail.uvue
2026-04-10 11:42:02 +08:00

1678 lines
42 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="detail-page">
<!-- #ifdef MP-WEIXIN -->
<view class="detail-navbar">
<view class="detail-navbar-back" @click="uni.navigateBack()">
<text class="back-arrow"></text>
<text class="back-text">返回</text>
</view>
<text class="detail-navbar-title">服务订单详情</text>
<view style="width: 120rpx;"></view>
</view>
<!-- #endif -->
<scroll-view class="detail-scroll" direction="vertical">
<!-- ① 状态头部 -->
<view class="status-header" :class="'sh-' + statusKey">
<view class="sh-main-row">
<text class="sh-icon">{{ statusIcon }}</text>
<view class="sh-titles">
<text class="sh-main-text">{{ statusMainText }}</text>
<text class="sh-desc-text">{{ statusDescText }}</text>
</view>
</view>
<view class="sh-tags-row">
<view class="sh-tag" :class="'sht-pay-' + payTagType">
<text class="sh-tag-label">支付</text>
<text class="sh-tag-val">{{ payStatusText }}</text>
</view>
<view class="sh-tag sht-fulfill">
<text class="sh-tag-label">服务</text>
<text class="sh-tag-val">{{ serviceStatusText }}</text>
</view>
<view v-if="isAftersale" class="sh-tag sht-aftersale">
<text class="sh-tag-label">退款</text>
<text class="sh-tag-val">{{ aftersaleStatusText }}</text>
</view>
</view>
</view>
<!-- ② 上门地址 & 服务安排 -->
<view class="section-card mt16">
<view class="section-card-title">
<text class="sct-icon">📍</text>
<text class="sct-text">上门地址</text>
</view>
<view class="addr-block">
<view class="addr-person-row">
<text class="addr-name">{{ addressData.recipient_name || '服务对象未知' }}</text>
<text class="addr-phone">{{ maskPhone(addressData.phone) }}</text>
</view>
<text class="addr-detail">
{{ addressData.province }}{{ addressData.city }}{{ addressData.district }}{{ addressData.detail_address }}
</text>
<text v-if="!addressData.recipient_name && !addressData.detail_address" class="addr-empty">暂无上门地址</text>
</view>
<!-- 服务安排信息,已接单后显示 -->
<view v-if="order.order_status === 3 || order.order_status === 4" class="logistics-divider"></view>
<view v-if="order.order_status === 3 || order.order_status === 4" class="logistics-block">
<view class="logi-left">
<text class="logi-icon">👨‍⚕️</text>
<view class="logi-info">
<text class="logi-company">{{ order.carrier_name || '服务人员待分配' }}</text>
<view class="logi-no-row">
<text class="logi-no">工单号:{{ order.tracking_no || '待生成' }}</text>
<view v-if="order.tracking_no" class="copy-tag" @click="copyText(order.tracking_no)">复制</view>
</view>
<text v-if="order.shipped_at" class="logi-time">出发时间:{{ formatTime(order.shipped_at) }}</text>
</view>
</view>
</view>
</view>
<!-- ③ 服务/商品项目 -->
<view class="section-card mt16">
<view class="section-card-title">
<text class="sct-icon">🏥</text>
<text class="sct-text">服务/商品项目</text>
<text class="sct-count">共 {{ order.items.length }} 项</text>
</view>
<view v-if="order.items.length === 0" class="items-empty">
<text class="items-empty-text">暂无服务项目信息</text>
</view>
<view v-for="item in order.items" :key="item.id" class="product-item">
<image
:src="item.image_url || '/static/images/default-product.png'"
class="product-img"
mode="aspectFill"
/>
<view class="product-main">
<text class="product-name">{{ item.product_name }}</text>
<view v-if="item.sku_name" class="product-spec-wrap">
<text class="product-spec">{{ item.sku_name }}</text>
</view>
<view class="product-price-row">
<text class="product-price">¥{{ formatMoney(item.price) }}</text>
<text class="product-qty">× {{ item.quantity }}</text>
</view>
</view>
<view class="product-subtotal">
<text class="product-subtotal-label">小计</text>
<text class="product-subtotal-val">¥{{ formatMoney(item.price * item.quantity) }}</text>
</view>
</view>
</view>
<!-- ④ 费用明细 -->
<view class="section-card mt16">
<view class="section-card-title">
<text class="sct-icon">💰</text>
<text class="sct-text">费用明细</text>
</view>
<view class="fee-row">
<text class="fee-label">服务金额</text>
<text class="fee-val">¥{{ formatMoney(order.product_amount) }}</text>
</view>
<view class="fee-row">
<text class="fee-label">上门服务费</text>
<text class="fee-val">¥{{ formatMoney(order.shipping_fee) }}</text>
</view>
<view v-if="safeNum(order.discount_amount) > 0" class="fee-row">
<text class="fee-label">优惠/补贴</text>
<text class="fee-val fee-red">-¥{{ formatMoney(order.discount_amount) }}</text>
</view>
<view class="fee-divider"></view>
<view class="fee-total-row">
<text class="fee-total-label">实付金额</text>
<text class="fee-total-val">¥{{ formatMoney(order.total_amount) }}</text>
</view>
</view>
<!-- ⑤ 订单信息 -->
<view class="section-card mt16">
<view class="section-card-title">
<text class="sct-icon">📋</text>
<text class="sct-text">订单信息</text>
</view>
<view class="info-row">
<text class="info-label">订单编号</text>
<view class="info-right">
<text class="info-val info-code">{{ order.order_no }}</text>
<view class="copy-tag" @click="copyText(order.order_no)">复制</view>
</view>
</view>
<view v-if="order.payment_method" class="info-row">
<text class="info-label">支付方式</text>
<text class="info-val">{{ getPayMethodText(order.payment_method) }}</text>
</view>
<view class="info-row">
<text class="info-label">下单时间</text>
<text class="info-val">{{ formatTime(order.created_at) }}</text>
</view>
<view v-if="order.paid_at" class="info-row">
<text class="info-label">付款时间</text>
<text class="info-val">{{ formatTime(order.paid_at) }}</text>
</view>
<view v-if="order.shipped_at" class="info-row">
<text class="info-label">派单时间</text>
<text class="info-val">{{ formatTime(order.shipped_at) }}</text>
</view>
<view v-if="order.completed_at" class="info-row">
<text class="info-label">完成时间</text>
<text class="info-val">{{ formatTime(order.completed_at) }}</text>
</view>
</view>
<!-- ⑥ 备注 & 操作日志 -->
<view class="section-card mt16">
<view class="section-card-title">
<text class="sct-icon">🗒</text>
<text class="sct-text">备注与日志</text>
</view>
<view v-if="order.remark" class="remark-block buyer-remark">
<view class="remark-header-row">
<text class="remark-tag buyer">家属备注</text>
</view>
<text class="remark-content">{{ order.remark }}</text>
</view>
<view v-if="!order.remark" class="remark-block remark-empty-row">
<text class="remark-tag buyer">家属备注</text>
<text class="remark-none">无</text>
</view>
<view class="remark-block merchant-remark">
<view class="remark-header-row">
<text class="remark-tag merchant">机构备注</text>
<text class="remark-edit-btn" @click="editMerchantRemark">编辑</text>
</view>
<text v-if="order.merchant_remark" class="remark-content">{{ order.merchant_remark }}</text>
<text v-else class="remark-none">暂无机构备注(仅自己可见)</text>
</view>
<view class="timeline-wrap">
<view class="timeline-title-row">
<text class="timeline-title">操作日志</text>
</view>
<view v-if="timeline.length === 0" class="timeline-empty">
<text class="timeline-empty-text">暂无操作记录</text>
</view>
<view v-for="(log, idx) in timeline" :key="idx" class="timeline-item">
<view class="tl-dot-wrap">
<view class="tl-dot" :class="idx === 0 ? 'tl-dot-active' : ''"></view>
<view v-if="idx < timeline.length - 1" class="tl-line"></view>
</view>
<view class="tl-content">
<text class="tl-action">{{ log.action }}</text>
<text class="tl-time">{{ formatTime(log.created_at) }}</text>
<text v-if="log.remark" class="tl-remark">{{ log.remark }}</text>
</view>
</view>
</view>
</view>
<!-- ⑦ 退款信息(有售后时显示) -->
<view v-if="isAftersale" class="section-card mt16 aftersale-card">
<view class="section-card-title">
<text class="sct-icon">🔄</text>
<text class="sct-text">退款信息</text>
<view class="sct-status-tag">{{ aftersaleStatusText }}</view>
</view>
<view class="info-row">
<text class="info-label">退款类型</text>
<text class="info-val">{{ order.refund_type || '退款' }}</text>
</view>
<view v-if="safeNum(order.refund_amount) > 0" class="info-row">
<text class="info-label">退款金额</text>
<text class="info-val fee-red">¥{{ formatMoney(order.refund_amount) }}</text>
</view>
<view v-if="order.refund_reason" class="info-row">
<text class="info-label">退款原因</text>
<text class="info-val">{{ order.refund_reason }}</text>
</view>
<view class="aftersale-btn-row">
<view class="aftersale-enter-btn" @click="processAftersale">处理退款 </view>
</view>
</view>
<!-- 底部安全区占位 -->
<view style="height: 180rpx;"></view>
</scroll-view>
<!-- 底部固定操作栏 -->
<view class="action-bar">
<view class="action-bar-inner">
<view
v-for="btn in actionButtons"
:key="btn.key"
class="action-btn"
:class="'ab-' + btn.type"
@click="handleActionBtn(btn.key)"
>
{{ btn.label }}
</view>
</view>
</view>
<!-- 安排服务上门弹窗 -->
<view v-if="showShipModal" class="modal-mask" @click="closeShipModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">安排服务上门</text>
<text class="modal-close" @click="closeShipModal">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<text class="form-label">服务人员</text>
<picker
class="form-picker"
:range="serviceStaff"
range-key="name"
@change="onStaffChange"
>
<view class="picker-value">
{{ selectedStaff.name || '请选择服务人员' }}
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">服务工单号</text>
<input
class="form-input"
v-model="serviceCode"
placeholder="请输入服务工单号"
/>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel" @click="closeShipModal">取消</view>
<view class="modal-btn confirm" @click="confirmShip">确认派单</view>
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
type OrderItemType = {
id: string
order_id: string
product_id: string
sku_id: string
product_name: string
sku_name: string
price: number
quantity: number
image_url: string
sku_snapshot: string
}
type AddressType = {
recipient_name: string
phone: string
province: string
city: string
district: string
detail_address: string
}
type ServiceStaffType = {
name: string
code: string
}
type ActionBtnType = {
key: string
label: string
type: string
}
type TimelineItemType = {
action: string
created_at: string
remark: string
}
// ===== 状态映射常量(医养语义) =====
const STATUS_KEY_MAP : UTSJSONObject = {
'1': 'pay', '2': 'pending', '3': 'service', '4': 'done',
'5': 'cancel', '-1': 'cancel', '0': 'refund', '6': 'refund'
}
const STATUS_MAIN_TEXT : UTSJSONObject = {
'1': '待支付', '2': '待接单', '3': '服务进行中', '4': '服务完成',
'5': '已取消', '-1': '已取消', '0': '退款处理中', '6': '退款处理中'
}
const STATUS_DESC : UTSJSONObject = {
'1': '家属已下单,等待支付',
'2': '家属已支付,请尽快安排服务人员上门',
'3': '服务人员已上门,服务进行中',
'4': '服务已完成,感谢您的服务',
'5': '订单已取消',
'-1': '订单已取消',
'0': '家属申请退款,请及时处理',
'6': '家属申请退款,请及时处理'
}
const STATUS_ICON : UTSJSONObject = {
'1': '💳', '2': '📋', '3': '🏥', '4': '✅',
'5': '❌', '-1': '❌', '0': '🔄', '6': '🔄'
}
const PAY_STATUS_TEXT : UTSJSONObject = {
'1': '待付款', '2': '已付款', '3': '已付款', '4': '已付款',
'5': '未付款', '-1': '未付款', '0': '退款中', '6': '退款中'
}
const SERVICE_STATUS_TEXT : UTSJSONObject = {
'1': '待支付', '2': '待接单', '3': '服务中', '4': '已完成',
'5': '已取消', '-1': '已取消', '0': '退款处理', '6': '退款处理'
}
const PAY_METHOD_TEXT : UTSJSONObject = {
'wechat': '微信支付', 'alipay': '支付宝', 'balance': '余额支付',
'wxpay': '微信支付', 'wx': '微信支付'
}
// 按钮矩阵配置(医养对应语义)
const ACTION_BUTTONS_MAP : UTSJSONObject = {
'1': [
{ key: 'contact', label: '联系家属', type: 'default' },
{ key: 'close_order', label: '取消订单', type: 'danger' }
],
'2': [
{ key: 'contact', label: '联系家属', type: 'default' },
{ key: 'ship', label: '安排服务上门', type: 'primary' }
],
'3': [
{ key: 'contact', label: '联系家属', type: 'default' },
{ key: 'view_progress', label: '服务进度', type: 'default' },
{ key: 'complete_service', label: '完成服务', type: 'primary' }
],
'4': [
{ key: 'contact', label: '联系家属', type: 'default' },
{ key: 'service_record', label: '服务记录', type: 'default' }
],
'5': [
{ key: 'close_reason', label: '取消原因', type: 'default' },
{ key: 'delete', label: '删除订单', type: 'danger' }
],
'-1': [
{ key: 'close_reason', label: '取消原因', type: 'default' },
{ key: 'delete', label: '删除订单', type: 'danger' }
],
'0': [
{ key: 'contact', label: '联系家属', type: 'default' },
{ key: 'process_refund', label: '处理退款', type: 'primary' }
],
'6': [
{ key: 'contact', label: '联系家属', type: 'default' },
{ key: 'process_refund', label: '处理退款', type: 'primary' }
]
}
export default {
data() {
return {
orderId: '' as string,
order: {
id: '',
order_no: '',
user_id: '',
merchant_id: '',
order_status: 1,
total_amount: 0,
product_amount: 0,
shipping_fee: 0,
discount_amount: 0,
paid_amount: 0,
refund_amount: 0,
refund_type: '',
refund_reason: '',
shipping_address: '',
remark: '',
merchant_remark: '',
carrier_name: '',
tracking_no: '',
payment_method: '',
paid_at: '',
shipped_at: '',
completed_at: '',
created_at: '',
updated_at: '',
items: [] as OrderItemType[]
},
addressData: {
recipient_name: '',
phone: '',
province: '',
city: '',
district: '',
detail_address: ''
} as AddressType,
timeline: [] as TimelineItemType[],
// 派单弹窗
showShipModal: false as boolean,
serviceStaff: [
{ name: '张医师', code: 'ZYS001' },
{ name: '李护士', code: 'LHS002' },
{ name: '王康复师', code: 'WKF003' },
{ name: '陈营养师', code: 'CYY004' },
{ name: '刘家政员', code: 'LJZ005' }
] as ServiceStaffType[],
selectedStaff: { name: '', code: '' } as ServiceStaffType,
serviceCode: '' as string
}
},
computed: {
statusKey(): string {
const k = String(this.order.order_status)
return (STATUS_KEY_MAP[k] as string) || 'pay'
},
statusMainText(): string {
const k = String(this.order.order_status)
return (STATUS_MAIN_TEXT[k] as string) || '未知'
},
statusDescText(): string {
const k = String(this.order.order_status)
return (STATUS_DESC[k] as string) || ''
},
statusIcon(): string {
const k = String(this.order.order_status)
return (STATUS_ICON[k] as string) || '📋'
},
payStatusText(): string {
const k = String(this.order.order_status)
return (PAY_STATUS_TEXT[k] as string) || '-'
},
payTagType(): string {
const s = this.order.order_status
if (s === 1) return 'pending'
if (s === 5 || s === -1) return 'none'
return 'done'
},
serviceStatusText(): string {
const k = String(this.order.order_status)
return (SERVICE_STATUS_TEXT[k] as string) || '-'
},
isAftersale(): boolean {
return this.order.order_status === 0 || this.order.order_status === 6
},
aftersaleStatusText(): string {
if (this.order.order_status === 0) return '退款中'
if (this.order.order_status === 6) return '退款处理中'
return ''
},
actionButtons(): ActionBtnType[] {
const k = String(this.order.order_status)
const btns = ACTION_BUTTONS_MAP[k]
if (btns != null && Array.isArray(btns)) {
return btns as ActionBtnType[]
}
return [{ key: 'contact', label: '联系家属', type: 'default' }] as ActionBtnType[]
}
},
onLoad(options: any) {
let id = ''
if (options['id'] != null) {
id = options['id'] as string
} else if (options.id != null) {
id = options.id as string
}
if (id !== '') {
this.orderId = id
this.loadOrderDetail()
}
},
methods: {
// ===== 工具方法 =====
copyText(text: string) {
if (!text) return
uni.setClipboardData({
data: text,
success: () => { uni.showToast({ title: '复制成功', icon: 'success' }) }
})
},
formatMoney(amount: any): string {
const num = Number(amount)
if (isNaN(num)) return '0.00'
return num.toFixed(2)
},
safeNum(val: any): number {
const n = Number(val)
return isNaN(n) ? 0 : n
},
formatTime(timeStr: string): string {
if (!timeStr) return '-'
try {
const date = new Date(timeStr)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
} catch (_e) { return timeStr }
},
maskPhone(phone: string): string {
if (!phone || phone.length < 7) return phone || ''
return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4)
},
getPayMethodText(method: string): string {
if (!method) return '-'
const t = PAY_METHOD_TEXT[method] as string
return t || method
},
// ===== 数据加载 =====
async loadOrderDetail() {
try {
const response = await supa
.from('ml_orders')
.select(`*,items:ml_order_items(id,order_id,product_id,sku_id,product_name,sku_name,price,quantity,image_url,specifications)`)
.eq('id', this.orderId)
.single()
.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
console.error('获取订单详情失败:', response.error)
uni.showToast({ title: '加载失败', icon: 'none' })
return
}
let realData = response.data
if (response.data != null && (response.data as any)['0'] != null) {
realData = (response.data as any)['0']
}
const rawData = realData as UTSJSONObject
if (rawData == null) return
this.order = {
id: String(rawData['id'] ?? '') || '',
order_no: String(rawData['order_no'] ?? '') || '',
user_id: String(rawData['user_id'] ?? '') || '',
merchant_id: String(rawData['merchant_id'] ?? '') || '',
order_status: Number(rawData['order_status'] ?? 0) || 1,
total_amount: Number(rawData['total_amount'] ?? 0) || 0,
product_amount: Number(rawData['product_amount'] ?? 0) || 0,
shipping_fee: Number(rawData['shipping_fee'] ?? 0) || 0,
discount_amount: Number(rawData['discount_amount'] ?? 0) || 0,
paid_amount: Number(rawData['paid_amount'] ?? 0) || 0,
refund_amount: Number(rawData['refund_amount'] ?? 0) || 0,
refund_type: String(rawData['refund_type'] ?? '') || '',
refund_reason: String(rawData['refund_reason'] ?? '') || '',
shipping_address: String(rawData['shipping_address'] ?? '') || '{}',
remark: String(rawData['remark'] ?? '') || '',
merchant_remark: String(rawData['merchant_remark'] ?? '') || '',
carrier_name: String(rawData['carrier_name'] ?? '') || '',
tracking_no: String(rawData['tracking_no'] ?? '') || '',
payment_method: String(rawData['payment_method'] ?? '') || '',
paid_at: String(rawData['paid_at'] ?? '') || '',
shipped_at: String(rawData['shipped_at'] ?? '') || '',
completed_at: String(rawData['completed_at'] ?? '') || '',
created_at: String(rawData['created_at'] ?? '') || '',
updated_at: String(rawData['updated_at'] ?? '') || '',
items: []
}
// 多策略解析服务项目数组
let itemsArr : Array<any> | null = rawData.getArray('items')
if (itemsArr == null || itemsArr.length === 0) {
itemsArr = rawData.getArray('ml_order_items')
}
if (itemsArr == null || itemsArr.length === 0) {
try {
const rawStr = JSON.stringify(rawData)
const parsedObj = JSON.parse(rawStr) as UTSJSONObject
const fromParsed = parsedObj.getArray('items') ?? parsedObj.getArray('ml_order_items')
if (fromParsed != null && fromParsed.length > 0) {
itemsArr = fromParsed
}
} catch (_e) { console.warn('[OD] fallback JSON parse 失败', _e) }
}
const tempItems : OrderItemType[] = []
if (itemsArr != null && itemsArr.length > 0) {
for (let i = 0; i < itemsArr.length; i++) {
const _rawItem = itemsArr[i]
const orderItem = (_rawItem instanceof UTSJSONObject ? _rawItem : new UTSJSONObject(_rawItem)) as UTSJSONObject
tempItems.push({
id: String(orderItem['id'] ?? '') || '',
order_id: String(orderItem['order_id'] ?? '') || '',
product_id: String(orderItem['product_id'] ?? '') || '',
sku_id: String(orderItem['sku_id'] ?? '') || '',
product_name: String(orderItem['product_name'] ?? '') || '',
sku_name: String(orderItem['sku_name'] ?? '') || '',
price: Number(orderItem['price'] ?? 0) || 0,
quantity: Number(orderItem['quantity'] ?? 0) || 0,
image_url: String(orderItem['image_url'] ?? '') || '',
sku_snapshot: String(orderItem['specifications'] ?? orderItem['sku_name'] ?? '') || ''
} as OrderItemType)
}
}
this.order.items = tempItems
this.parseAddress()
this.buildTimeline()
} catch (e) {
console.error('获取订单详情异常:', e)
}
},
parseAddress() {
try {
const addrStr = this.order.shipping_address
if (!addrStr || addrStr === '{}') return
const raw = JSON.parse(addrStr) as UTSJSONObject
this.addressData = {
recipient_name: String(raw['recipient_name'] ?? raw['name'] ?? '') || '',
phone: String(raw['phone'] ?? raw['mobile'] ?? '') || '',
province: String(raw['province'] ?? '') || '',
city: String(raw['city'] ?? '') || '',
district: String(raw['district'] ?? '') || '',
detail_address: String(raw['detail_address'] ?? raw['address'] ?? raw['detail'] ?? '') || ''
}
} catch (e) {
console.error('解析地址失败:', e)
}
},
buildTimeline() {
const tl : TimelineItemType[] = []
if (this.order.completed_at) {
tl.push({ action: '服务完成', created_at: this.order.completed_at, remark: '' })
}
if (this.order.shipped_at) {
const rm = this.order.carrier_name ? `服务人员:${this.order.carrier_name}` : ''
tl.push({ action: '服务人员已出发', created_at: this.order.shipped_at, remark: rm })
}
if (this.order.paid_at) {
tl.push({ action: '家属完成付款', created_at: this.order.paid_at, remark: '' })
}
if (this.order.created_at) {
tl.push({ action: '订单创建', created_at: this.order.created_at, remark: '' })
}
this.timeline = tl
},
// ===== 按钮统一分发 =====
handleActionBtn(key: string) {
if (key === 'contact') { this.contactBuyer(); return }
if (key === 'ship') { this.openShipModal(); return }
if (key === 'view_progress') { this.viewProgress(); return }
if (key === 'complete_service') { this.completeService(); return }
if (key === 'service_record') { uni.showToast({ title: '服务记录开发中', icon: 'none' }); return }
if (key === 'close_order') { this.cancelOrder(); return }
if (key === 'delete') { this.deleteOrder(); return }
if (key === 'process_refund') { this.processAftersale(); return }
if (key === 'close_reason') {
uni.showModal({
title: '取消原因',
content: this.order.remark || '暂无取消原因记录',
showCancel: false
})
return
}
},
// ===== 具体操作方法 =====
contactBuyer() {
uni.navigateTo({ url: `/pages/mall/merchant/chat?userId=${this.order.user_id}` })
},
openShipModal() {
this.showShipModal = true
},
closeShipModal() {
this.showShipModal = false
this.selectedStaff = { name: '', code: '' }
this.serviceCode = ''
},
onStaffChange(e: any) {
const index = e.detail.value as number
this.selectedStaff = this.serviceStaff[index]
},
async confirmShip() {
if (!this.selectedStaff.name) {
uni.showToast({ title: '请选择服务人员', icon: 'none' }); return
}
if (!this.serviceCode) {
uni.showToast({ title: '请输入服务工单号', icon: 'none' }); return
}
try {
const response = await supa
.from('ml_orders')
.update({
order_status: 3,
shipping_status: 2,
carrier_name: this.selectedStaff.name,
tracking_no: this.serviceCode,
shipped_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.eq('id', this.order.id)
.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
const msg = response.error?.message ?? '请检查网络或登录状态'
uni.showToast({ title: '派单失败:' + msg, icon: 'none', duration: 4000 })
return
}
uni.showToast({ title: '派单成功', icon: 'success' })
this.closeShipModal()
this.loadOrderDetail()
} catch (e) {
uni.showToast({ title: '派单发生异常', icon: 'none' })
}
},
viewProgress() {
uni.navigateTo({ url: `/pages/mall/merchant/logistics?orderId=${this.order.id}` })
},
async completeService() {
uni.showModal({
title: '确认完成服务',
content: '确认服务已完成?完成后将通知家属确认。',
success: async (res) => {
if (res.confirm) {
try {
const response = await supa
.from('ml_orders')
.update({
order_status: 4,
completed_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.eq('id', this.order.id)
.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
uni.showToast({ title: '操作失败', icon: 'none' }); return
}
uni.showToast({ title: '服务完成', icon: 'success' })
this.loadOrderDetail()
} catch (e) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
}
})
},
cancelOrder() {
uni.showModal({
title: '取消订单',
content: '确认取消此服务订单吗?',
success: async (res) => {
if (res.confirm) {
try {
const response = await supa
.from('ml_orders')
.update({
order_status: 5,
updated_at: new Date().toISOString()
})
.eq('id', this.order.id)
.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
uni.showToast({ title: '操作失败', icon: 'none' }); return
}
uni.showToast({ title: '订单已取消', icon: 'success' })
this.loadOrderDetail()
} catch (e) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
}
})
},
processAftersale() {
uni.showToast({ title: '退款处理开发中', icon: 'none' })
},
editMerchantRemark() {
uni.showModal({
title: '机构备注',
content: '该功能需要后端接口支持,待接入',
showCancel: false
})
},
async deleteOrder() {
uni.showModal({
title: '确认删除',
content: '删除后不可恢复,确定要删除吗?',
success: async (res) => {
if (res.confirm) {
try {
const response = await supa
.from('ml_orders')
.delete()
.eq('id', this.order.id)
.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
uni.showToast({ title: '删除失败', icon: 'none' }); return
}
uni.showToast({ title: '删除成功', icon: 'success' })
setTimeout(() => { uni.navigateBack() }, 1500)
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
}
}
}
</script>
<style>
/* ===== 页面基础 ===== */
.detail-page {
background-color: #f0f0f0;
min-height: 100vh;
}
.detail-scroll {
flex: 1;
}
.mt16 { margin-top: 16rpx; }
/* ===== 导航栏 ===== */
.detail-navbar {
display: flex;
flex-direction: row;
align-items: flex-end;
background-color: #ffffff;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #eeeeee;
box-sizing: border-box;
padding-top: var(--status-bar-height);
height: calc(88rpx + var(--status-bar-height));
}
.detail-navbar-back {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 30rpx;
height: 88rpx;
width: 120rpx;
}
.back-arrow {
font-size: 44rpx;
color: #333333;
line-height: 1;
margin-right: 4rpx;
}
.back-text {
font-size: 28rpx;
color: #333333;
}
.detail-navbar-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: #1a1a1a;
height: 88rpx;
line-height: 88rpx;
}
/* ===== 状态头部 ===== */
.status-header {
padding: 32rpx 30rpx 24rpx 30rpx;
}
/* 医养状态色系 */
.sh-pay { background: linear-gradient(135deg, #FF7800 0%, #ff9500 100%); }
.sh-pending { background: linear-gradient(135deg, #A6F1E4 0%, #69DFC2 100%); }
.sh-service { background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%); }
.sh-done { background: linear-gradient(135deg, #26A69A 0%, #00796B 100%); }
.sh-cancel { background: linear-gradient(135deg, #90A4AE 0%, #607D8B 100%); }
.sh-refund { background: linear-gradient(135deg, #FF7043 0%, #E64A19 100%); }
.sh-main-row {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20rpx;
}
.sh-icon {
font-size: 56rpx;
margin-right: 20rpx;
}
.sh-titles {
flex: 1;
display: flex;
flex-direction: column;
}
.sh-main-text {
font-size: 38rpx;
font-weight: 700;
color: #ffffff;
margin-bottom: 6rpx;
}
.sh-desc-text {
font-size: 24rpx;
color: rgba(255,255,255,0.85);
}
.sh-tags-row {
display: flex;
flex-direction: row;
align-items: center;
}
.sh-tag {
display: flex;
flex-direction: row;
align-items: center;
background-color: rgba(255,255,255,0.18);
border-radius: 6rpx;
padding: 6rpx 14rpx;
margin-right: 12rpx;
}
.sh-tag-label {
font-size: 20rpx;
color: rgba(255,255,255,0.75);
margin-right: 6rpx;
}
.sh-tag-val {
font-size: 22rpx;
color: #ffffff;
font-weight: 500;
}
.sht-pay-pending .sh-tag-val { color: #ffe082; }
.sht-pay-done .sh-tag-val { color: #b9fbc0; }
.sht-pay-none .sh-tag-val { color: rgba(255,255,255,0.5); }
.sht-aftersale { background-color: rgba(255,255,255,0.25); }
/* ===== 通用卡片 ===== */
.section-card {
background-color: #ffffff;
border-radius: 0;
}
.section-card-title {
display: flex;
flex-direction: row;
align-items: center;
padding: 22rpx 30rpx 18rpx 30rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f0f0f0;
}
.sct-icon {
font-size: 30rpx;
margin-right: 10rpx;
}
.sct-text {
font-size: 28rpx;
font-weight: 600;
color: #1a1a1a;
flex: 1;
}
.sct-count {
font-size: 24rpx;
color: #999;
}
.sct-status-tag {
font-size: 22rpx;
color: #E64A19;
background-color: #fff3e0;
padding: 4rpx 14rpx;
border-radius: 20rpx;
}
/* ===== 上门地址块 ===== */
.addr-block {
padding: 20rpx 30rpx;
}
.addr-person-row {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 10rpx;
}
.addr-name {
font-size: 30rpx;
font-weight: 600;
color: #1a1a1a;
margin-right: 20rpx;
}
.addr-phone {
font-size: 28rpx;
color: #555;
}
.addr-detail {
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
.addr-empty {
font-size: 26rpx;
color: #bbb;
}
/* ===== 服务安排块 ===== */
.logistics-divider {
height: 1rpx;
background-color: #f0f0f0;
margin: 0 30rpx;
}
.logistics-block {
display: flex;
flex-direction: row;
align-items: center;
padding: 20rpx 30rpx;
}
.logi-left {
flex: 1;
display: flex;
flex-direction: row;
align-items: flex-start;
}
.logi-icon {
font-size: 36rpx;
margin-right: 16rpx;
padding-top: 4rpx;
}
.logi-info {
flex: 1;
display: flex;
flex-direction: column;
}
.logi-company {
font-size: 28rpx;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 6rpx;
}
.logi-no-row {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 4rpx;
}
.logi-no {
font-size: 24rpx;
color: #555;
margin-right: 12rpx;
}
.logi-time {
font-size: 22rpx;
color: #999;
}
/* ===== 商品/服务列表 ===== */
.product-item {
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 20rpx 30rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f5f5f5;
}
.product-img {
width: 160rpx;
height: 160rpx;
border-radius: 8rpx;
margin-right: 20rpx;
background-color: #f5f5f5;
flex-shrink: 0;
}
.product-main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.product-name {
font-size: 28rpx;
color: #1a1a1a;
line-height: 1.5;
}
.product-spec-wrap {
display: inline-flex;
margin-top: 8rpx;
margin-bottom: 8rpx;
}
.product-spec {
font-size: 22rpx;
color: #888;
background-color: #f7f7f7;
padding: 4rpx 12rpx;
border-radius: 4rpx;
}
.product-price-row {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 8rpx;
}
.product-price {
font-size: 26rpx;
color: #1a1a1a;
font-weight: 500;
margin-right: 16rpx;
}
.product-qty {
font-size: 24rpx;
color: #999;
}
.product-subtotal {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: 16rpx;
flex-shrink: 0;
}
.product-subtotal-label {
font-size: 20rpx;
color: #bbb;
margin-bottom: 4rpx;
}
.product-subtotal-val {
font-size: 28rpx;
color: #1a1a1a;
font-weight: 600;
}
.items-empty {
padding: 40rpx 30rpx;
display: flex;
flex-direction: row;
justify-content: center;
}
.items-empty-text {
font-size: 26rpx;
color: #bbb;
}
/* ===== 金额明细 ===== */
.fee-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 14rpx 30rpx;
}
.fee-label {
font-size: 26rpx;
color: #666;
}
.fee-val {
font-size: 26rpx;
color: #333;
}
.fee-red { color: #e64a19; }
.fee-divider {
height: 1rpx;
background-color: #f0f0f0;
margin: 6rpx 30rpx;
}
.fee-total-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 18rpx 30rpx 22rpx 30rpx;
}
.fee-total-label {
font-size: 28rpx;
font-weight: 600;
color: #1a1a1a;
}
.fee-total-val {
font-size: 36rpx;
font-weight: 700;
color: #e64a19;
}
/* ===== 订单信息行 ===== */
.info-row {
display: flex;
flex-direction: row;
align-items: center;
padding: 16rpx 30rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f5f5f5;
}
.info-label {
font-size: 26rpx;
color: #999;
min-width: 150rpx;
flex-shrink: 0;
}
.info-right {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
.info-val {
font-size: 26rpx;
color: #333;
flex: 1;
text-align: right;
}
.info-code {
font-size: 22rpx;
color: #555;
}
.copy-tag {
font-size: 20rpx;
color: #09C39D;
border-width: 1rpx;
border-style: solid;
border-color: #09C39D;
border-radius: 4rpx;
padding: 2rpx 10rpx;
margin-left: 12rpx;
flex-shrink: 0;
}
/* ===== 备注区 ===== */
.remark-block {
padding: 16rpx 30rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f5f5f5;
}
.remark-empty-row {
display: flex;
flex-direction: row;
align-items: center;
}
.remark-header-row {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 10rpx;
}
.remark-tag {
font-size: 20rpx;
border-radius: 4rpx;
padding: 2rpx 10rpx;
margin-right: 12rpx;
}
.remark-tag.buyer {
background-color: #E3F7ED;
color: #09C39D;
}
.remark-tag.merchant {
background-color: #fff3e0;
color: #e64a19;
}
.remark-content {
font-size: 26rpx;
color: #333;
line-height: 1.6;
}
.remark-none {
font-size: 24rpx;
color: #bbb;
}
.remark-edit-btn {
font-size: 22rpx;
color: #09C39D;
margin-left: auto;
}
/* ===== 时间线 ===== */
.timeline-wrap {
padding: 16rpx 30rpx 20rpx 30rpx;
}
.timeline-title-row {
margin-bottom: 16rpx;
}
.timeline-title {
font-size: 24rpx;
color: #999;
}
.timeline-empty {
padding: 10rpx 0;
}
.timeline-empty-text {
font-size: 24rpx;
color: #ccc;
}
.timeline-item {
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 4rpx;
}
.tl-dot-wrap {
display: flex;
flex-direction: column;
align-items: center;
width: 32rpx;
margin-right: 16rpx;
flex-shrink: 0;
}
.tl-dot {
width: 16rpx;
height: 16rpx;
border-radius: 8rpx;
background-color: #d0d0d0;
margin-top: 6rpx;
}
.tl-dot-active {
background-color: #09C39D;
}
.tl-line {
width: 2rpx;
flex: 1;
min-height: 32rpx;
background-color: #e8e8e8;
margin-top: 4rpx;
margin-bottom: 4rpx;
}
.tl-content {
flex: 1;
display: flex;
flex-direction: column;
padding-bottom: 24rpx;
}
.tl-action {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.tl-time {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
.tl-remark {
font-size: 22rpx;
color: #aaa;
margin-top: 4rpx;
}
/* ===== 退款卡片 ===== */
.aftersale-card {
border-top-width: 4rpx;
border-top-style: solid;
border-top-color: #E64A19;
}
.aftersale-btn-row {
padding: 16rpx 30rpx 20rpx 30rpx;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.aftersale-enter-btn {
font-size: 26rpx;
color: #E64A19;
border-width: 1rpx;
border-style: solid;
border-color: #E64A19;
border-radius: 30rpx;
padding: 10rpx 30rpx;
}
/* ===== 底部固定操作栏 ===== */
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
box-shadow: 0 -2rpx 16rpx rgba(0,0,0,0.10);
padding-bottom: env(safe-area-inset-bottom);
z-index: 100;
}
.action-bar-inner {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
padding: 16rpx 24rpx;
min-height: 104rpx;
box-sizing: border-box;
}
.action-btn {
height: 72rpx;
line-height: 72rpx;
padding: 0 32rpx;
font-size: 28rpx;
border-radius: 36rpx;
margin-left: 16rpx;
text-align: center;
flex-shrink: 0;
}
.ab-default {
color: #555;
border-width: 1rpx;
border-style: solid;
border-color: #d0d0d0;
background-color: #ffffff;
}
.ab-primary {
background-color: #09C39D;
color: #ffffff;
}
.ab-danger {
background-color: #ffffff;
color: #e64a19;
border-width: 1rpx;
border-style: solid;
border-color: #e64a19;
}
/* ===== 弹窗 ===== */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
z-index: 200;
}
.modal-content {
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
}
.modal-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f5f5f5;
}
.modal-title {
font-size: 32rpx;
font-weight: 700;
color: #1a1a1a;
}
.modal-close {
font-size: 44rpx;
color: #999;
line-height: 1;
}
.modal-body {
padding: 30rpx;
}
.form-item {
margin-bottom: 24rpx;
}
.form-label {
font-size: 26rpx;
color: #333;
display: block;
margin-bottom: 14rpx;
}
.form-picker {
height: 72rpx;
border-width: 1rpx;
border-style: solid;
border-color: #e5e5e5;
border-radius: 8rpx;
padding: 0 20rpx;
background-color: #fafafa;
}
.form-input {
height: 72rpx;
border-width: 1rpx;
border-style: solid;
border-color: #e5e5e5;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
background-color: #fafafa;
}
.picker-value {
height: 72rpx;
line-height: 72rpx;
color: #333;
font-size: 28rpx;
}
.modal-footer {
display: flex;
flex-direction: row;
border-top-width: 1rpx;
border-top-style: solid;
border-top-color: #f5f5f5;
}
.modal-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
font-size: 28rpx;
}
.modal-btn.cancel {
color: #666;
border-right-width: 1rpx;
border-right-style: solid;
border-right-color: #f5f5f5;
}
.modal-btn.confirm {
color: #09C39D;
font-weight: bold;
}
</style>