|
@@ -0,0 +1,742 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="page">
|
|
|
|
|
+ <!-- 顶部导航 -->
|
|
|
|
|
+ <view class="nav-bar">
|
|
|
|
|
+ <view class="nav-back" @click="goBack">
|
|
|
|
|
+ <text class="nav-arrow">‹</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="nav-title">{{ isDetailMode ? '订单详情' : '退单管理' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ==================== 退单列表模式 ==================== -->
|
|
|
|
|
+ <view v-if="!isDetailMode" class="content">
|
|
|
|
|
+ <view v-if="!refundOrders.length && !loading" class="empty-state">
|
|
|
|
|
+ <text class="empty-icon"></text>
|
|
|
|
|
+ <text class="empty-text">暂无退单记录</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-for="order in refundOrders" :key="order.id" class="order-card" @click="openDetail(order)">
|
|
|
|
|
+ <view class="oc-header">
|
|
|
|
|
+ <view class="oc-room">
|
|
|
|
|
+ <text class="oc-room-icon">🏠</text>
|
|
|
|
|
+ <text class="oc-room-no">{{ order.roomNo || '--' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="oc-status" :class="statusTagClass(order.status)">
|
|
|
|
|
+ <text>{{ statusLabel(order.status) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="oc-items">{{ buildItemSummary(order) }}</text>
|
|
|
|
|
+ <view class="oc-footer">
|
|
|
|
|
+ <text class="oc-amount">¥{{ order.totalAmount }}</text>
|
|
|
|
|
+ <text class="oc-meta">{{ formatShortTime(order.createTime) }} · {{ order.orderNo }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="order.refundReason" class="oc-reason">
|
|
|
|
|
+ <text class="oc-reason-label">退单原因:</text>
|
|
|
|
|
+ <text class="oc-reason-text">{{ order.refundReason }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="order.status === 8" class="oc-actions">
|
|
|
|
|
+ <view class="btn btn--success" @click.stop="handleApprove(order)">同意退单</view>
|
|
|
|
|
+ <view class="btn btn--error" @click.stop="handleReject(order)">驳回</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-if="loading" class="loading-more"><text>加载中...</text></view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ==================== 订单详情模式 ==================== -->
|
|
|
|
|
+ <scroll-view v-else class="content" scroll-y>
|
|
|
|
|
+ <view v-if="loading" class="loading-state"><text>加载中...</text></view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-if="orderData" class="detail-content">
|
|
|
|
|
+ <!-- 订单信息卡片 -->
|
|
|
|
|
+ <view class="info-card">
|
|
|
|
|
+ <view class="card-header">
|
|
|
|
|
+ <view>
|
|
|
|
|
+ <text class="room-name">{{ orderData.roomNo }} {{ orderData.roomName || '' }}</text>
|
|
|
|
|
+ <text class="order-meta">{{ orderData.orderNo }} · {{ formatShortTime(orderData.createTime) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="oc-status" :class="statusTagClass(orderData.status)">
|
|
|
|
|
+ <text>{{ statusLabel(orderData.status) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="card-info-grid">
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">联系人</text>
|
|
|
|
|
+ <text class="info-value">{{ orderData.contactName || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">电话</text>
|
|
|
|
|
+ <text class="info-value">{{ orderData.contactPhone || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">支付方式</text>
|
|
|
|
|
+ <text class="info-value">{{ payMethodLabel(orderData.payMethod) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">金额</text>
|
|
|
|
|
+ <text class="info-value info-amount">¥{{ orderData.totalAmount }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 菜品明细 -->
|
|
|
|
|
+ <view class="items-section">
|
|
|
|
|
+ <text class="items-title">菜品明细</text>
|
|
|
|
|
+ <view v-for="(item, idx) in (orderData.items || [])" :key="idx" class="item-row">
|
|
|
|
|
+ <text class="item-name">{{ item.dishName }} ×{{ item.quantity }} — ¥{{ item.subtotal }}</text>
|
|
|
|
|
+ <text v-if="item.specDesc" class="item-spec">({{ item.specDesc }})</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="items-total">
|
|
|
|
|
+ <text>合计 ¥{{ orderData.totalAmount }} · 免配送费</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 退单操作区域 -->
|
|
|
|
|
+ <view v-if="canRefund" class="refund-card">
|
|
|
|
|
+ <text class="refund-title"> 退单操作</text>
|
|
|
|
|
+ <text class="refund-desc">宾馆前台可在任何阶段发起退单。退款将原路返回至客人支付账户。</text>
|
|
|
|
|
+ <textarea class="refund-input" v-model="refundReason" placeholder="请输入退单原因..." :maxlength="200" />
|
|
|
|
|
+ <view class="refund-btn" @click="handleRefundConfirm">
|
|
|
|
|
+ <text>确认退单 · ¥{{ orderData.totalAmount }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 退单审核操作(退单待审核状态) -->
|
|
|
|
|
+ <view v-if="orderData.status === 8" class="refund-card refund-card--audit">
|
|
|
|
|
+ <text class="refund-title">⚠ 退单审核</text>
|
|
|
|
|
+ <text class="refund-desc">顾客已发起退单申请,请选择处理方式。</text>
|
|
|
|
|
+ <textarea class="refund-input" v-model="auditReason" placeholder="审核备注(选填)..." :maxlength="200" />
|
|
|
|
|
+ <view class="audit-btns">
|
|
|
|
|
+ <view class="btn btn--success btn--block" @click="handleApproveConfirm">同意退单</view>
|
|
|
|
|
+ <view class="btn btn--error btn--block" @click="handleRejectConfirm">驳回退单</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 状态时间线 -->
|
|
|
|
|
+ <view v-if="hasTimeInfo" class="timeline-section">
|
|
|
|
|
+ <text class="timeline-title">📋 状态时间线</text>
|
|
|
|
|
+ <view class="timeline-list">
|
|
|
|
|
+ <view class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot" :class="getDotClass('create')" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.createTime) }} · 顾客下单</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="orderData.payTime" class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot" :class="getDotClass('pay')" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.payTime) }} · 支付成功 · ¥{{ orderData.paidAmount || orderData.totalAmount }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="orderData.acceptTime" class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot" :class="getDotClass('accept')" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.acceptTime) }} · 餐厅前台接单</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="orderData.prepareTime" class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot" :class="getDotClass('prepare')" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.prepareTime) }} · 开始备餐</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="orderData.readyTime" class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot" :class="getDotClass('ready')" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.readyTime) }} · 出餐完成</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="orderData.deliverTime" class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot" :class="getDotClass('deliver')" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.deliverTime) }} · 开始配送</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="orderData.completeTime" class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot" :class="getDotClass('complete')" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.completeTime) }} · 配送完成</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="orderData.rejectTime" class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot reject" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.rejectTime) }} · 已拒单{{ orderData.rejectReason ? ':' + orderData.rejectReason : '' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="orderData.refundApplyTime" class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot" :class="getDotClass('refundApply')" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.refundApplyTime) }} · 发起退单申请{{ orderData.refundReason ? ':' + orderData.refundReason : '' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="orderData.refundAuditTime" class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot" :class="getDotClass('refundAudit')" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.refundAuditTime) }} · 退单审核{{ orderData.status === 9 ? '通过' : '驳回' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="orderData.refundTime" class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot refund" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.refundTime) }} · 退款完成 · ¥{{ orderData.refundAmount || orderData.totalAmount }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="orderData.cancelTime" class="timeline-item">
|
|
|
|
|
+ <view class="timeline-dot cancel" />
|
|
|
|
|
+ <text class="timeline-text">{{ formatTime(orderData.cancelTime) }} · 订单已取消</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </scroll-view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 底部 TabBar -->
|
|
|
|
|
+ <view class="bottom-tab">
|
|
|
|
|
+ <view class="tab-item" @click="goOverview">
|
|
|
|
|
+ <text class="tab-icon">📋</text>
|
|
|
|
|
+ <text class="tab-label">订单总览</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="tab-item" :class="{ 'tab-item--active': !isDetailMode }" @click="goRefundList">
|
|
|
|
|
+ <text class="tab-icon">💰</text>
|
|
|
|
|
+ <text class="tab-label">退单管理</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { computed, onMounted, ref } from 'vue'
|
|
|
|
|
+import api from '@/api'
|
|
|
|
|
+
|
|
|
|
|
+defineOptions({ name: 'HotelOrderRefund' })
|
|
|
|
|
+
|
|
|
|
|
+// ==================== 路由参数 ====================
|
|
|
|
|
+const orderId = ref('')
|
|
|
|
|
+const isDetailMode = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+// ==================== 数据 ====================
|
|
|
|
|
+const orderData = ref(null)
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+const refundOrders = ref([])
|
|
|
|
|
+const refundReason = ref('')
|
|
|
|
|
+const auditReason = ref('')
|
|
|
|
|
+
|
|
|
|
|
+// ==================== 工具 ====================
|
|
|
|
|
+function buildItemSummary(order) {
|
|
|
|
|
+ if (!order.items || !order.items.length) return ''
|
|
|
|
|
+ return order.items.map(i => i.dishName).join('、')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatShortTime(timeStr) {
|
|
|
|
|
+ if (!timeStr) return ''
|
|
|
|
|
+ const match = String(timeStr).match(/(\d{2}:\d{2})/)
|
|
|
|
|
+ return match ? match[1] : timeStr
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatTime(timeStr) {
|
|
|
|
|
+ if (!timeStr) return ''
|
|
|
|
|
+ const match = String(timeStr).match(/(\d{2}:\d{2})/)
|
|
|
|
|
+ return match ? match[1] : timeStr
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function payMethodLabel(method) {
|
|
|
|
|
+ const map = { ALIPAY: '支付宝', WECHAT: '微信支付', MOCK: '模拟支付', CASH: '现金' }
|
|
|
|
|
+ return map[method] || method || '-'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function statusLabel(s) {
|
|
|
|
|
+ const map = { 0: '待支付', 1: '待接单', 2: '已接单', 3: '备餐中', 4: '已出餐', 5: '配送中', 6: '已完成', 7: '已拒单', 8: '退单审核中', 9: '已退单', 10: '已超时取消', 11: '已取消' }
|
|
|
|
|
+ return map[s] || '未知'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function statusTagClass(s) {
|
|
|
|
|
+ if (s === 1) return 'tag--pending'
|
|
|
|
|
+ if (s >= 2 && s <= 3) return 'tag--preparing'
|
|
|
|
|
+ if (s === 4) return 'tag--ready'
|
|
|
|
|
+ if (s === 5) return 'tag--delivering'
|
|
|
|
|
+ if (s === 6) return 'tag--done'
|
|
|
|
|
+ if (s === 7) return 'tag--rejected'
|
|
|
|
|
+ if (s === 8) return 'tag--refund'
|
|
|
|
|
+ if (s === 9) return 'tag--refunded'
|
|
|
|
|
+ return 'tag--default'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 终态订单不允许退单
|
|
|
|
|
+const canRefund = computed(() => {
|
|
|
|
|
+ if (!orderData.value) return false
|
|
|
|
|
+ const s = orderData.value.status
|
|
|
|
|
+ return s !== 6 && s !== 7 && s !== 9 && s !== 10 && s !== 11
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const hasTimeInfo = computed(() => {
|
|
|
|
|
+ if (!orderData.value) return false
|
|
|
|
|
+ const d = orderData.value
|
|
|
|
|
+ return d.payTime || d.acceptTime || d.prepareTime || d.readyTime || d.deliverTime
|
|
|
|
|
+ || d.completeTime || d.rejectTime || d.refundApplyTime || d.refundAuditTime
|
|
|
|
|
+ || d.refundTime || d.cancelTime
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 时间线圆点颜色
|
|
|
|
|
+function getDotClass(stepType) {
|
|
|
|
|
+ if (!orderData.value) return ''
|
|
|
|
|
+ const status = orderData.value.status
|
|
|
|
|
+ if (stepType === 'reject') return 'reject'
|
|
|
|
|
+ if (stepType === 'cancel') return 'cancel'
|
|
|
|
|
+
|
|
|
|
|
+ const stepStatusMap = {
|
|
|
|
|
+ create: 0, pay: 1, accept: 2, prepare: 3,
|
|
|
|
|
+ ready: 4, deliver: 5, complete: 6,
|
|
|
|
|
+ refundApply: 8, refundAudit: 9,
|
|
|
|
|
+ }
|
|
|
|
|
+ const stepStatus = stepStatusMap[stepType]
|
|
|
|
|
+ if (stepStatus === undefined) return ''
|
|
|
|
|
+ if (status > stepStatus) return 'completed'
|
|
|
|
|
+ return 'active'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ==================== 数据加载 ====================
|
|
|
|
|
+async function loadOrderDetail() {
|
|
|
|
|
+ if (!orderId.value) return
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await api.hotelStaffOrderDetail(orderId.value)
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ orderData.value = res.data
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ uni.showToast({ title: '加载订单详情失败', icon: 'none' })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadRefundOrders() {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 加载退单审核中(8) + 已退单(9) 的订单
|
|
|
|
|
+ const [res8, res9] = await Promise.all([
|
|
|
|
|
+ api.hotelStaffOrderPage({ pageNum: 1, pageSize: 50, status: 8 }),
|
|
|
|
|
+ api.hotelStaffOrderPage({ pageNum: 1, pageSize: 50, status: 9 }),
|
|
|
|
|
+ ])
|
|
|
|
|
+ let list = []
|
|
|
|
|
+ if (res8.code === 200 && res8.data?.records) list = list.concat(res8.data.records)
|
|
|
|
|
+ if (res9.code === 200 && res9.data?.records) list = list.concat(res9.data.records)
|
|
|
|
|
+ // 按创建时间倒序
|
|
|
|
|
+ list.sort((a, b) => new Date(b.createTime) - new Date(a.createTime))
|
|
|
|
|
+ refundOrders.value = list
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('[退单管理] 加载失败:', e)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ==================== 退单操作 ====================
|
|
|
|
|
+async function handleRefundConfirm() {
|
|
|
|
|
+ if (!refundReason.value || refundReason.value.trim() === '') {
|
|
|
|
|
+ uni.showToast({ title: '请输入退单原因', icon: 'none' }); return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ await api.hotelHotelRefund(orderData.value.id, refundReason.value.trim())
|
|
|
|
|
+ uni.showToast({ title: '退单成功', icon: 'success' })
|
|
|
|
|
+ setTimeout(() => goBack(), 1500)
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ uni.showToast({ title: '退单失败', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleApproveConfirm() {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '同意退单',
|
|
|
|
|
+ content: `确认同意订单 ${orderData.value.orderNo} 的退单申请?退款将原路返回。`,
|
|
|
|
|
+ success: async (res) => {
|
|
|
|
|
+ if (res.confirm) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await api.hotelApproveRefund(orderData.value.id, auditReason.value || undefined)
|
|
|
|
|
+ uni.showToast({ title: '已同意退单', icon: 'success' })
|
|
|
|
|
+ loadOrderDetail()
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ uni.showToast({ title: '操作失败', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleRejectConfirm() {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '驳回退单',
|
|
|
|
|
+ content: `确认驳回订单 ${orderData.value.orderNo} 的退单申请?`,
|
|
|
|
|
+ success: async (res) => {
|
|
|
|
|
+ if (res.confirm) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await api.hotelRejectRefund(orderData.value.id, auditReason.value || undefined)
|
|
|
|
|
+ uni.showToast({ title: '已驳回退单', icon: 'success' })
|
|
|
|
|
+ loadOrderDetail()
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ uni.showToast({ title: '操作失败', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 列表页的审核操作
|
|
|
|
|
+function handleApprove(order) {
|
|
|
|
|
+ uni.navigateTo({ url: `/pages/hotel/hotel-order-refund?id=${order.id}` })
|
|
|
|
|
+}
|
|
|
|
|
+function handleReject(order) {
|
|
|
|
|
+ uni.navigateTo({ url: `/pages/hotel/hotel-order-refund?id=${order.id}` })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ==================== 导航 ====================
|
|
|
|
|
+function openDetail(order) {
|
|
|
|
|
+ uni.navigateTo({ url: `/pages/hotel/hotel-order-refund?id=${order.id}` })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goBack() {
|
|
|
|
|
+ uni.navigateBack({ fail: () => { uni.navigateTo({ url: '/pages/hotel/hotel-order' }) } })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goOverview() {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/hotel/hotel-order' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goRefundList() {
|
|
|
|
|
+ if (isDetailMode.value) {
|
|
|
|
|
+ uni.redirectTo({ url: '/pages/hotel/hotel-order-refund?mode=refund' })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ==================== 生命周期 ====================
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ // 获取页面参数
|
|
|
|
|
+ const pages = getCurrentPages()
|
|
|
|
|
+ const currentPage = pages[pages.length - 1]
|
|
|
|
|
+ const options = currentPage.$page?.options || currentPage.options || {}
|
|
|
|
|
+ orderId.value = options.id || ''
|
|
|
|
|
+ isDetailMode.value = !!orderId.value
|
|
|
|
|
+
|
|
|
|
|
+ if (isDetailMode.value) {
|
|
|
|
|
+ loadOrderDetail()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ loadRefundOrders()
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.page {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ background: #f0f2f5;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ padding-bottom: 120rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ==================== 顶部导航 ==================== */
|
|
|
|
|
+.nav-bar {
|
|
|
|
|
+ background: #1a365d;
|
|
|
|
|
+ padding: 20rpx 24rpx 24rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.nav-back {
|
|
|
|
|
+ width: 56rpx;
|
|
|
|
|
+ height: 56rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+.nav-arrow {
|
|
|
|
|
+ font-size: 44rpx;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-weight: 300;
|
|
|
|
|
+}
|
|
|
|
|
+.nav-title {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ==================== 内容区 ==================== */
|
|
|
|
|
+.content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ padding: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ==================== 订单卡片(列表模式) ==================== */
|
|
|
|
|
+.order-card {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
|
|
|
|
+}
|
|
|
|
|
+.oc-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.oc-room {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.oc-room-icon { font-size: 32rpx; }
|
|
|
|
|
+.oc-room-no { font-size: 32rpx; font-weight: 700; color: #1a1a1a; }
|
|
|
|
|
+.oc-status {
|
|
|
|
|
+ padding: 6rpx 16rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+.tag--pending { background: #fff0f0; color: #d03050; }
|
|
|
|
|
+.tag--preparing { background: #fff8e6; color: #f0a020; }
|
|
|
|
|
+.tag--ready { background: #e8f0fe; color: #2080f0; }
|
|
|
|
|
+.tag--delivering { background: #e8f8ee; color: #18a058; }
|
|
|
|
|
+.tag--done { background: #f0f0f0; color: #999; }
|
|
|
|
|
+.tag--rejected { background: #f0f0f0; color: #999; }
|
|
|
|
|
+.tag--refund { background: #fff3e0; color: #e67e22; }
|
|
|
|
|
+.tag--refunded { background: #f0f0f0; color: #999; }
|
|
|
|
|
+.tag--default { background: #f0f0f0; color: #999; }
|
|
|
|
|
+
|
|
|
|
|
+.oc-items {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ margin-bottom: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.oc-footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+.oc-amount { font-size: 30rpx; font-weight: 700; color: #f0a020; }
|
|
|
|
|
+.oc-meta { font-size: 22rpx; color: #bbb; }
|
|
|
|
|
+.oc-reason {
|
|
|
|
|
+ margin-top: 12rpx;
|
|
|
|
|
+ padding: 12rpx 16rpx;
|
|
|
|
|
+ background: #fff8f8;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.oc-reason-label { font-size: 22rpx; color: #999; }
|
|
|
|
|
+.oc-reason-text { font-size: 22rpx; color: #d03050; }
|
|
|
|
|
+.oc-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+ margin-top: 16rpx;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ==================== 按钮 ==================== */
|
|
|
|
|
+.btn {
|
|
|
|
|
+ padding: 12rpx 28rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+.btn--success { background: #e8f8ee; color: #18a058; }
|
|
|
|
|
+.btn--error { background: #fff0f0; color: #d03050; }
|
|
|
|
|
+.btn--block { width: 100%; padding: 20rpx; font-size: 28rpx; }
|
|
|
|
|
+
|
|
|
|
|
+/* ==================== 详情卡片 ==================== */
|
|
|
|
|
+.info-card {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
|
|
|
|
+}
|
|
|
|
|
+.card-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ padding-bottom: 16rpx;
|
|
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.room-name {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #1a1a1a;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+}
|
|
|
|
|
+.order-meta {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ margin-top: 6rpx;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+}
|
|
|
|
|
+.card-info-grid {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 20rpx;
|
|
|
|
|
+ padding-bottom: 16rpx;
|
|
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.info-item {
|
|
|
|
|
+ width: calc(50% - 10rpx);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 6rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.info-label { font-size: 22rpx; color: #999; }
|
|
|
|
|
+.info-value { font-size: 26rpx; color: #333; }
|
|
|
|
|
+.info-amount { font-weight: 700; color: #f0a020; font-size: 28rpx; }
|
|
|
|
|
+
|
|
|
|
|
+/* 菜品明细 */
|
|
|
|
|
+.items-section {
|
|
|
|
|
+ background: #f8f9fa;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ padding: 16rpx 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.items-title {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ margin-bottom: 12rpx;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+}
|
|
|
|
|
+.item-row {
|
|
|
|
|
+ padding: 6rpx 0;
|
|
|
|
|
+}
|
|
|
|
|
+.item-name { font-size: 26rpx; color: #333; }
|
|
|
|
|
+.item-spec { font-size: 22rpx; color: #999; margin-left: 8rpx; }
|
|
|
|
|
+.items-total {
|
|
|
|
|
+ border-top: 1rpx dashed #e0e0e0;
|
|
|
|
|
+ margin-top: 12rpx;
|
|
|
|
|
+ padding-top: 12rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ==================== 退单操作 ==================== */
|
|
|
|
|
+.refund-card {
|
|
|
|
|
+ background: #fff5f5;
|
|
|
|
|
+ border: 1rpx solid #ffd0d0;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.refund-card--audit {
|
|
|
|
|
+ background: #fff8f0;
|
|
|
|
|
+ border-color: #ffe0b2;
|
|
|
|
|
+}
|
|
|
|
|
+.refund-title {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #d03050;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-bottom: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.refund-desc {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #d03050;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+}
|
|
|
|
|
+.refund-input {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ min-height: 120rpx;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1rpx solid #ffd0d0;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ padding: 16rpx;
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.refund-btn {
|
|
|
|
|
+ background: #d03050;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+.refund-btn text {
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+.audit-btns {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ==================== 时间线 ==================== */
|
|
|
|
|
+.timeline-section {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
|
|
|
|
+}
|
|
|
|
|
+.timeline-title {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #1a1a1a;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.timeline-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 20rpx;
|
|
|
|
|
+ padding-left: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.timeline-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.timeline-dot {
|
|
|
|
|
+ width: 16rpx;
|
|
|
|
|
+ height: 16rpx;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #f0a020;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.timeline-dot.completed { background: #18a058; }
|
|
|
|
|
+.timeline-dot.active { background: #f0a020; }
|
|
|
|
|
+.timeline-dot.reject { background: #f0a020; }
|
|
|
|
|
+.timeline-dot.refund { background: #d03050; }
|
|
|
|
|
+.timeline-dot.cancel { background: #999; }
|
|
|
|
|
+.timeline-text {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ==================== 空状态 / 加载 ==================== */
|
|
|
|
|
+.empty-state {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 120rpx 0;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.empty-icon { font-size: 80rpx; }
|
|
|
|
|
+.empty-text { font-size: 28rpx; color: #bbb; }
|
|
|
|
|
+.loading-state, .loading-more {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 60rpx 0;
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #bbb;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ==================== 底部 TabBar ==================== */
|
|
|
|
|
+.bottom-tab {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ height: 100rpx;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-top: 1rpx solid #eee;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+}
|
|
|
|
|
+.tab-item {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 4rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.tab-item--active .tab-label {
|
|
|
|
|
+ color: #1a365d;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+.tab-icon { font-size: 36rpx; }
|
|
|
|
|
+.tab-label { font-size: 22rpx; color: #999; }
|
|
|
|
|
+</style>
|