hotel-order-refund.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. <template>
  2. <view class="page">
  3. <!-- 顶部导航 -->
  4. <view class="nav-bar">
  5. <view class="nav-back" @click="goBack">
  6. <text class="nav-arrow">‹</text>
  7. </view>
  8. <text class="nav-title">{{ isDetailMode ? '订单详情' : '退单管理' }}</text>
  9. </view>
  10. <!-- ==================== 退单列表模式 ==================== -->
  11. <view v-if="!isDetailMode" class="content">
  12. <view v-if="!refundOrders.length && !loading" class="empty-state">
  13. <text class="empty-icon"></text>
  14. <text class="empty-text">暂无退单记录</text>
  15. </view>
  16. <view v-for="order in refundOrders" :key="order.id" class="order-card" @click="openDetail(order)">
  17. <view class="oc-header">
  18. <view class="oc-room">
  19. <text class="oc-room-icon">🏠</text>
  20. <text class="oc-room-no">{{ order.roomNo || '--' }}</text>
  21. </view>
  22. <view class="oc-status" :class="statusTagClass(order.status)">
  23. <text>{{ statusLabel(order.status) }}</text>
  24. </view>
  25. </view>
  26. <text class="oc-items">{{ buildItemSummary(order) }}</text>
  27. <view class="oc-footer">
  28. <text class="oc-amount">¥{{ order.totalAmount }}</text>
  29. <text class="oc-meta">{{ formatShortTime(order.createTime) }} · {{ order.orderNo }}</text>
  30. </view>
  31. <view v-if="order.refundReason" class="oc-reason">
  32. <text class="oc-reason-label">退单原因:</text>
  33. <text class="oc-reason-text">{{ order.refundReason }}</text>
  34. </view>
  35. <view v-if="order.status === 8" class="oc-actions">
  36. <view class="btn btn--success" @click.stop="handleApprove(order)">同意退单</view>
  37. <view class="btn btn--error" @click.stop="handleReject(order)">驳回</view>
  38. </view>
  39. </view>
  40. <view v-if="loading" class="loading-more"><text>加载中...</text></view>
  41. </view>
  42. <!-- ==================== 订单详情模式 ==================== -->
  43. <scroll-view v-else class="content" scroll-y>
  44. <view v-if="loading" class="loading-state"><text>加载中...</text></view>
  45. <view v-if="orderData" class="detail-content">
  46. <!-- 订单信息卡片 -->
  47. <view class="info-card">
  48. <view class="card-header">
  49. <view>
  50. <text class="room-name">{{ orderData.roomNo }} {{ orderData.roomName || '' }}</text>
  51. <text class="order-meta">{{ orderData.orderNo }} · {{ formatShortTime(orderData.createTime) }}</text>
  52. </view>
  53. <view class="oc-status" :class="statusTagClass(orderData.status)">
  54. <text>{{ statusLabel(orderData.status) }}</text>
  55. </view>
  56. </view>
  57. <view class="card-info-grid">
  58. <view class="info-item">
  59. <text class="info-label">联系人</text>
  60. <text class="info-value">{{ orderData.contactName || '-' }}</text>
  61. </view>
  62. <view class="info-item">
  63. <text class="info-label">电话</text>
  64. <text class="info-value">{{ orderData.contactPhone || '-' }}</text>
  65. </view>
  66. <view class="info-item">
  67. <text class="info-label">支付方式</text>
  68. <text class="info-value">{{ payMethodLabel(orderData.payMethod) }}</text>
  69. </view>
  70. <view class="info-item">
  71. <text class="info-label">金额</text>
  72. <text class="info-value info-amount">¥{{ orderData.totalAmount }}</text>
  73. </view>
  74. </view>
  75. <!-- 菜品明细 -->
  76. <view class="items-section">
  77. <text class="items-title">菜品明细</text>
  78. <view v-for="(item, idx) in (orderData.items || [])" :key="idx" class="item-row">
  79. <text class="item-name">{{ item.dishName }} ×{{ item.quantity }} — ¥{{ item.subtotal }}</text>
  80. <text v-if="item.specDesc" class="item-spec">({{ item.specDesc }})</text>
  81. </view>
  82. <view class="items-total">
  83. <text>合计 ¥{{ orderData.totalAmount }} · 免配送费</text>
  84. </view>
  85. </view>
  86. </view>
  87. <!-- 退单操作区域 -->
  88. <view v-if="canRefund" class="refund-card">
  89. <text class="refund-title"> 退单操作</text>
  90. <text class="refund-desc">宾馆前台可在任何阶段发起退单。退款将原路返回至客人支付账户。</text>
  91. <textarea class="refund-input" v-model="refundReason" placeholder="请输入退单原因..." :maxlength="200" />
  92. <view class="refund-btn" @click="handleRefundConfirm">
  93. <text>确认退单 · ¥{{ orderData.totalAmount }}</text>
  94. </view>
  95. </view>
  96. <!-- 退单审核操作(退单待审核状态) -->
  97. <view v-if="orderData.status === 8" class="refund-card refund-card--audit">
  98. <text class="refund-title">⚠ 退单审核</text>
  99. <text class="refund-desc">顾客已发起退单申请,请选择处理方式。</text>
  100. <textarea class="refund-input" v-model="auditReason" placeholder="审核备注(选填)..." :maxlength="200" />
  101. <view class="audit-btns">
  102. <view class="btn btn--success btn--block" @click="handleApproveConfirm">同意退单</view>
  103. <view class="btn btn--error btn--block" @click="handleRejectConfirm">驳回退单</view>
  104. </view>
  105. </view>
  106. <!-- 状态时间线 -->
  107. <view v-if="hasTimeInfo" class="timeline-section">
  108. <text class="timeline-title">📋 状态时间线</text>
  109. <view class="timeline-list">
  110. <view class="timeline-item">
  111. <view class="timeline-dot" :class="getDotClass('create')" />
  112. <text class="timeline-text">{{ formatTime(orderData.createTime) }} · 顾客下单</text>
  113. </view>
  114. <view v-if="orderData.payTime" class="timeline-item">
  115. <view class="timeline-dot" :class="getDotClass('pay')" />
  116. <text class="timeline-text">{{ formatTime(orderData.payTime) }} · 支付成功 · ¥{{ orderData.paidAmount || orderData.totalAmount }}</text>
  117. </view>
  118. <view v-if="orderData.acceptTime" class="timeline-item">
  119. <view class="timeline-dot" :class="getDotClass('accept')" />
  120. <text class="timeline-text">{{ formatTime(orderData.acceptTime) }} · 餐厅前台接单</text>
  121. </view>
  122. <view v-if="orderData.prepareTime" class="timeline-item">
  123. <view class="timeline-dot" :class="getDotClass('prepare')" />
  124. <text class="timeline-text">{{ formatTime(orderData.prepareTime) }} · 开始备餐</text>
  125. </view>
  126. <view v-if="orderData.readyTime" class="timeline-item">
  127. <view class="timeline-dot" :class="getDotClass('ready')" />
  128. <text class="timeline-text">{{ formatTime(orderData.readyTime) }} · 出餐完成</text>
  129. </view>
  130. <view v-if="orderData.deliverTime" class="timeline-item">
  131. <view class="timeline-dot" :class="getDotClass('deliver')" />
  132. <text class="timeline-text">{{ formatTime(orderData.deliverTime) }} · 开始配送</text>
  133. </view>
  134. <view v-if="orderData.completeTime" class="timeline-item">
  135. <view class="timeline-dot" :class="getDotClass('complete')" />
  136. <text class="timeline-text">{{ formatTime(orderData.completeTime) }} · 配送完成</text>
  137. </view>
  138. <view v-if="orderData.rejectTime" class="timeline-item">
  139. <view class="timeline-dot reject" />
  140. <text class="timeline-text">{{ formatTime(orderData.rejectTime) }} · 已拒单{{ orderData.rejectReason ? ':' + orderData.rejectReason : '' }}</text>
  141. </view>
  142. <view v-if="orderData.refundApplyTime" class="timeline-item">
  143. <view class="timeline-dot" :class="getDotClass('refundApply')" />
  144. <text class="timeline-text">{{ formatTime(orderData.refundApplyTime) }} · 发起退单申请{{ orderData.refundReason ? ':' + orderData.refundReason : '' }}</text>
  145. </view>
  146. <view v-if="orderData.refundAuditTime" class="timeline-item">
  147. <view class="timeline-dot" :class="getDotClass('refundAudit')" />
  148. <text class="timeline-text">{{ formatTime(orderData.refundAuditTime) }} · 退单审核{{ orderData.status === 9 ? '通过' : '驳回' }}</text>
  149. </view>
  150. <view v-if="orderData.refundTime" class="timeline-item">
  151. <view class="timeline-dot refund" />
  152. <text class="timeline-text">{{ formatTime(orderData.refundTime) }} · 退款完成 · ¥{{ orderData.refundAmount || orderData.totalAmount }}</text>
  153. </view>
  154. <view v-if="orderData.cancelTime" class="timeline-item">
  155. <view class="timeline-dot cancel" />
  156. <text class="timeline-text">{{ formatTime(orderData.cancelTime) }} · 订单已取消</text>
  157. </view>
  158. </view>
  159. </view>
  160. </view>
  161. </scroll-view>
  162. <!-- 底部 TabBar -->
  163. <view class="bottom-tab">
  164. <view class="tab-item" @click="goOverview">
  165. <text class="tab-icon">📋</text>
  166. <text class="tab-label">订单总览</text>
  167. </view>
  168. <view class="tab-item" :class="{ 'tab-item--active': !isDetailMode }" @click="goRefundList">
  169. <text class="tab-icon">💰</text>
  170. <text class="tab-label">退单管理</text>
  171. </view>
  172. </view>
  173. </view>
  174. </template>
  175. <script setup>
  176. import { computed, onMounted, ref } from 'vue'
  177. import api from '@/api'
  178. defineOptions({ name: 'HotelOrderRefund' })
  179. // ==================== 路由参数 ====================
  180. const orderId = ref('')
  181. const isDetailMode = ref(false)
  182. // ==================== 数据 ====================
  183. const orderData = ref(null)
  184. const loading = ref(false)
  185. const refundOrders = ref([])
  186. const refundReason = ref('')
  187. const auditReason = ref('')
  188. // ==================== 工具 ====================
  189. function buildItemSummary(order) {
  190. if (!order.items || !order.items.length) return ''
  191. return order.items.map(i => i.dishName).join('、')
  192. }
  193. function formatShortTime(timeStr) {
  194. if (!timeStr) return ''
  195. const match = String(timeStr).match(/(\d{2}:\d{2})/)
  196. return match ? match[1] : timeStr
  197. }
  198. function formatTime(timeStr) {
  199. if (!timeStr) return ''
  200. const match = String(timeStr).match(/(\d{2}:\d{2})/)
  201. return match ? match[1] : timeStr
  202. }
  203. function payMethodLabel(method) {
  204. const map = { ALIPAY: '支付宝', WECHAT: '微信支付', MOCK: '模拟支付', CASH: '现金' }
  205. return map[method] || method || '-'
  206. }
  207. function statusLabel(s) {
  208. const map = { 0: '待支付', 1: '待接单', 2: '已接单', 3: '备餐中', 4: '已出餐', 5: '配送中', 6: '已完成', 7: '已拒单', 8: '退单审核中', 9: '已退单', 10: '已超时取消', 11: '已取消' }
  209. return map[s] || '未知'
  210. }
  211. function statusTagClass(s) {
  212. if (s === 1) return 'tag--pending'
  213. if (s >= 2 && s <= 3) return 'tag--preparing'
  214. if (s === 4) return 'tag--ready'
  215. if (s === 5) return 'tag--delivering'
  216. if (s === 6) return 'tag--done'
  217. if (s === 7) return 'tag--rejected'
  218. if (s === 8) return 'tag--refund'
  219. if (s === 9) return 'tag--refunded'
  220. return 'tag--default'
  221. }
  222. // 终态订单不允许退单
  223. const canRefund = computed(() => {
  224. if (!orderData.value) return false
  225. const s = orderData.value.status
  226. return s !== 6 && s !== 7 && s !== 9 && s !== 10 && s !== 11
  227. })
  228. const hasTimeInfo = computed(() => {
  229. if (!orderData.value) return false
  230. const d = orderData.value
  231. return d.payTime || d.acceptTime || d.prepareTime || d.readyTime || d.deliverTime
  232. || d.completeTime || d.rejectTime || d.refundApplyTime || d.refundAuditTime
  233. || d.refundTime || d.cancelTime
  234. })
  235. // 时间线圆点颜色
  236. function getDotClass(stepType) {
  237. if (!orderData.value) return ''
  238. const status = orderData.value.status
  239. if (stepType === 'reject') return 'reject'
  240. if (stepType === 'cancel') return 'cancel'
  241. const stepStatusMap = {
  242. create: 0, pay: 1, accept: 2, prepare: 3,
  243. ready: 4, deliver: 5, complete: 6,
  244. refundApply: 8, refundAudit: 9,
  245. }
  246. const stepStatus = stepStatusMap[stepType]
  247. if (stepStatus === undefined) return ''
  248. if (status > stepStatus) return 'completed'
  249. return 'active'
  250. }
  251. // ==================== 数据加载 ====================
  252. async function loadOrderDetail() {
  253. if (!orderId.value) return
  254. loading.value = true
  255. try {
  256. const res = await api.hotelStaffOrderDetail(orderId.value)
  257. if (res.code === 200) {
  258. orderData.value = res.data
  259. }
  260. } catch (e) {
  261. uni.showToast({ title: '加载订单详情失败', icon: 'none' })
  262. } finally {
  263. loading.value = false
  264. }
  265. }
  266. async function loadRefundOrders() {
  267. loading.value = true
  268. try {
  269. // 加载退单审核中(8) + 已退单(9) 的订单
  270. const [res8, res9] = await Promise.all([
  271. api.hotelStaffOrderPage({ pageNum: 1, pageSize: 50, status: 8 }),
  272. api.hotelStaffOrderPage({ pageNum: 1, pageSize: 50, status: 9 }),
  273. ])
  274. let list = []
  275. if (res8.code === 200 && res8.data?.records) list = list.concat(res8.data.records)
  276. if (res9.code === 200 && res9.data?.records) list = list.concat(res9.data.records)
  277. // 按创建时间倒序
  278. list.sort((a, b) => new Date(b.createTime) - new Date(a.createTime))
  279. refundOrders.value = list
  280. } catch (e) {
  281. console.error('[退单管理] 加载失败:', e)
  282. } finally {
  283. loading.value = false
  284. }
  285. }
  286. // ==================== 退单操作 ====================
  287. async function handleRefundConfirm() {
  288. if (!refundReason.value || refundReason.value.trim() === '') {
  289. uni.showToast({ title: '请输入退单原因', icon: 'none' }); return
  290. }
  291. try {
  292. await api.hotelHotelRefund(orderData.value.id, refundReason.value.trim())
  293. uni.showToast({ title: '退单成功', icon: 'success' })
  294. setTimeout(() => goBack(), 1500)
  295. } catch (e) {
  296. uni.showToast({ title: '退单失败', icon: 'none' })
  297. }
  298. }
  299. async function handleApproveConfirm() {
  300. uni.showModal({
  301. title: '同意退单',
  302. content: `确认同意订单 ${orderData.value.orderNo} 的退单申请?退款将原路返回。`,
  303. success: async (res) => {
  304. if (res.confirm) {
  305. try {
  306. await api.hotelApproveRefund(orderData.value.id, auditReason.value || undefined)
  307. uni.showToast({ title: '已同意退单', icon: 'success' })
  308. loadOrderDetail()
  309. } catch (e) {
  310. uni.showToast({ title: '操作失败', icon: 'none' })
  311. }
  312. }
  313. },
  314. })
  315. }
  316. async function handleRejectConfirm() {
  317. uni.showModal({
  318. title: '驳回退单',
  319. content: `确认驳回订单 ${orderData.value.orderNo} 的退单申请?`,
  320. success: async (res) => {
  321. if (res.confirm) {
  322. try {
  323. await api.hotelRejectRefund(orderData.value.id, auditReason.value || undefined)
  324. uni.showToast({ title: '已驳回退单', icon: 'success' })
  325. loadOrderDetail()
  326. } catch (e) {
  327. uni.showToast({ title: '操作失败', icon: 'none' })
  328. }
  329. }
  330. },
  331. })
  332. }
  333. // 列表页的审核操作
  334. function handleApprove(order) {
  335. uni.navigateTo({ url: `/pages/hotel/hotel-order-refund?id=${order.id}` })
  336. }
  337. function handleReject(order) {
  338. uni.navigateTo({ url: `/pages/hotel/hotel-order-refund?id=${order.id}` })
  339. }
  340. // ==================== 导航 ====================
  341. function openDetail(order) {
  342. uni.navigateTo({ url: `/pages/hotel/hotel-order-refund?id=${order.id}` })
  343. }
  344. function goBack() {
  345. uni.navigateBack({ fail: () => { uni.navigateTo({ url: '/pages/hotel/hotel-order' }) } })
  346. }
  347. function goOverview() {
  348. uni.navigateTo({ url: '/pages/hotel/hotel-order' })
  349. }
  350. function goRefundList() {
  351. if (isDetailMode.value) {
  352. uni.redirectTo({ url: '/pages/hotel/hotel-order-refund?mode=refund' })
  353. }
  354. }
  355. // ==================== 生命周期 ====================
  356. onMounted(() => {
  357. // 获取页面参数
  358. const pages = getCurrentPages()
  359. const currentPage = pages[pages.length - 1]
  360. const options = currentPage.$page?.options || currentPage.options || {}
  361. orderId.value = options.id || ''
  362. isDetailMode.value = !!orderId.value
  363. if (isDetailMode.value) {
  364. loadOrderDetail()
  365. } else {
  366. loadRefundOrders()
  367. }
  368. })
  369. </script>
  370. <style lang="scss" scoped>
  371. .page {
  372. min-height: 100vh;
  373. background: #f0f2f5;
  374. display: flex;
  375. flex-direction: column;
  376. padding-bottom: 120rpx;
  377. }
  378. /* ==================== 顶部导航 ==================== */
  379. .nav-bar {
  380. background: #1a365d;
  381. padding: 20rpx 24rpx 24rpx;
  382. display: flex;
  383. align-items: center;
  384. gap: 16rpx;
  385. }
  386. .nav-back {
  387. width: 56rpx;
  388. height: 56rpx;
  389. display: flex;
  390. align-items: center;
  391. justify-content: center;
  392. }
  393. .nav-arrow {
  394. font-size: 44rpx;
  395. color: #fff;
  396. font-weight: 300;
  397. }
  398. .nav-title {
  399. font-size: 32rpx;
  400. font-weight: 700;
  401. color: #fff;
  402. }
  403. /* ==================== 内容区 ==================== */
  404. .content {
  405. flex: 1;
  406. padding: 20rpx;
  407. }
  408. /* ==================== 订单卡片(列表模式) ==================== */
  409. .order-card {
  410. background: #fff;
  411. border-radius: 16rpx;
  412. padding: 24rpx;
  413. margin-bottom: 16rpx;
  414. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  415. }
  416. .oc-header {
  417. display: flex;
  418. justify-content: space-between;
  419. align-items: center;
  420. margin-bottom: 12rpx;
  421. }
  422. .oc-room {
  423. display: flex;
  424. align-items: center;
  425. gap: 8rpx;
  426. }
  427. .oc-room-icon { font-size: 32rpx; }
  428. .oc-room-no { font-size: 32rpx; font-weight: 700; color: #1a1a1a; }
  429. .oc-status {
  430. padding: 6rpx 16rpx;
  431. border-radius: 8rpx;
  432. font-size: 22rpx;
  433. font-weight: 600;
  434. }
  435. .tag--pending { background: #fff0f0; color: #d03050; }
  436. .tag--preparing { background: #fff8e6; color: #f0a020; }
  437. .tag--ready { background: #e8f0fe; color: #2080f0; }
  438. .tag--delivering { background: #e8f8ee; color: #18a058; }
  439. .tag--done { background: #f0f0f0; color: #999; }
  440. .tag--rejected { background: #f0f0f0; color: #999; }
  441. .tag--refund { background: #fff3e0; color: #e67e22; }
  442. .tag--refunded { background: #f0f0f0; color: #999; }
  443. .tag--default { background: #f0f0f0; color: #999; }
  444. .oc-items {
  445. font-size: 26rpx;
  446. color: #666;
  447. display: block;
  448. overflow: hidden;
  449. text-overflow: ellipsis;
  450. white-space: nowrap;
  451. margin-bottom: 12rpx;
  452. }
  453. .oc-footer {
  454. display: flex;
  455. justify-content: space-between;
  456. align-items: center;
  457. }
  458. .oc-amount { font-size: 30rpx; font-weight: 700; color: #f0a020; }
  459. .oc-meta { font-size: 22rpx; color: #bbb; }
  460. .oc-reason {
  461. margin-top: 12rpx;
  462. padding: 12rpx 16rpx;
  463. background: #fff8f8;
  464. border-radius: 8rpx;
  465. display: flex;
  466. gap: 8rpx;
  467. }
  468. .oc-reason-label { font-size: 22rpx; color: #999; }
  469. .oc-reason-text { font-size: 22rpx; color: #d03050; }
  470. .oc-actions {
  471. display: flex;
  472. gap: 16rpx;
  473. margin-top: 16rpx;
  474. justify-content: flex-end;
  475. }
  476. /* ==================== 按钮 ==================== */
  477. .btn {
  478. padding: 12rpx 28rpx;
  479. border-radius: 8rpx;
  480. font-size: 24rpx;
  481. font-weight: 600;
  482. text-align: center;
  483. }
  484. .btn--success { background: #e8f8ee; color: #18a058; }
  485. .btn--error { background: #fff0f0; color: #d03050; }
  486. .btn--block { width: 100%; padding: 20rpx; font-size: 28rpx; }
  487. /* ==================== 详情卡片 ==================== */
  488. .info-card {
  489. background: #fff;
  490. border-radius: 16rpx;
  491. padding: 24rpx;
  492. margin-bottom: 16rpx;
  493. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  494. }
  495. .card-header {
  496. display: flex;
  497. justify-content: space-between;
  498. align-items: flex-start;
  499. padding-bottom: 16rpx;
  500. border-bottom: 1rpx solid #f0f0f0;
  501. margin-bottom: 16rpx;
  502. }
  503. .room-name {
  504. font-size: 32rpx;
  505. font-weight: 700;
  506. color: #1a1a1a;
  507. display: block;
  508. }
  509. .order-meta {
  510. font-size: 22rpx;
  511. color: #999;
  512. margin-top: 6rpx;
  513. display: block;
  514. }
  515. .card-info-grid {
  516. display: flex;
  517. flex-wrap: wrap;
  518. gap: 20rpx;
  519. padding-bottom: 16rpx;
  520. border-bottom: 1rpx solid #f0f0f0;
  521. margin-bottom: 16rpx;
  522. }
  523. .info-item {
  524. width: calc(50% - 10rpx);
  525. display: flex;
  526. flex-direction: column;
  527. gap: 6rpx;
  528. }
  529. .info-label { font-size: 22rpx; color: #999; }
  530. .info-value { font-size: 26rpx; color: #333; }
  531. .info-amount { font-weight: 700; color: #f0a020; font-size: 28rpx; }
  532. /* 菜品明细 */
  533. .items-section {
  534. background: #f8f9fa;
  535. border-radius: 12rpx;
  536. padding: 16rpx 20rpx;
  537. }
  538. .items-title {
  539. font-size: 24rpx;
  540. color: #999;
  541. margin-bottom: 12rpx;
  542. display: block;
  543. }
  544. .item-row {
  545. padding: 6rpx 0;
  546. }
  547. .item-name { font-size: 26rpx; color: #333; }
  548. .item-spec { font-size: 22rpx; color: #999; margin-left: 8rpx; }
  549. .items-total {
  550. border-top: 1rpx dashed #e0e0e0;
  551. margin-top: 12rpx;
  552. padding-top: 12rpx;
  553. font-size: 28rpx;
  554. font-weight: 700;
  555. color: #333;
  556. }
  557. /* ==================== 退单操作 ==================== */
  558. .refund-card {
  559. background: #fff5f5;
  560. border: 1rpx solid #ffd0d0;
  561. border-radius: 16rpx;
  562. padding: 24rpx;
  563. margin-bottom: 16rpx;
  564. }
  565. .refund-card--audit {
  566. background: #fff8f0;
  567. border-color: #ffe0b2;
  568. }
  569. .refund-title {
  570. font-size: 28rpx;
  571. font-weight: 700;
  572. color: #d03050;
  573. display: block;
  574. margin-bottom: 8rpx;
  575. }
  576. .refund-desc {
  577. font-size: 24rpx;
  578. color: #d03050;
  579. display: block;
  580. margin-bottom: 16rpx;
  581. line-height: 1.5;
  582. }
  583. .refund-input {
  584. width: 100%;
  585. min-height: 120rpx;
  586. background: #fff;
  587. border: 1rpx solid #ffd0d0;
  588. border-radius: 8rpx;
  589. padding: 16rpx;
  590. font-size: 26rpx;
  591. color: #333;
  592. box-sizing: border-box;
  593. margin-bottom: 16rpx;
  594. }
  595. .refund-btn {
  596. background: #d03050;
  597. border-radius: 12rpx;
  598. padding: 24rpx;
  599. text-align: center;
  600. }
  601. .refund-btn text {
  602. color: #fff;
  603. font-size: 28rpx;
  604. font-weight: 700;
  605. }
  606. .audit-btns {
  607. display: flex;
  608. gap: 16rpx;
  609. }
  610. /* ==================== 时间线 ==================== */
  611. .timeline-section {
  612. background: #fff;
  613. border-radius: 16rpx;
  614. padding: 24rpx;
  615. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  616. }
  617. .timeline-title {
  618. font-size: 28rpx;
  619. font-weight: 700;
  620. color: #1a1a1a;
  621. display: block;
  622. margin-bottom: 20rpx;
  623. }
  624. .timeline-list {
  625. display: flex;
  626. flex-direction: column;
  627. gap: 20rpx;
  628. padding-left: 8rpx;
  629. }
  630. .timeline-item {
  631. display: flex;
  632. align-items: center;
  633. gap: 16rpx;
  634. }
  635. .timeline-dot {
  636. width: 16rpx;
  637. height: 16rpx;
  638. border-radius: 50%;
  639. background: #f0a020;
  640. flex-shrink: 0;
  641. }
  642. .timeline-dot.completed { background: #18a058; }
  643. .timeline-dot.active { background: #f0a020; }
  644. .timeline-dot.reject { background: #f0a020; }
  645. .timeline-dot.refund { background: #d03050; }
  646. .timeline-dot.cancel { background: #999; }
  647. .timeline-text {
  648. font-size: 24rpx;
  649. color: #666;
  650. }
  651. /* ==================== 空状态 / 加载 ==================== */
  652. .empty-state {
  653. display: flex;
  654. flex-direction: column;
  655. align-items: center;
  656. padding: 120rpx 0;
  657. gap: 16rpx;
  658. }
  659. .empty-icon { font-size: 80rpx; }
  660. .empty-text { font-size: 28rpx; color: #bbb; }
  661. .loading-state, .loading-more {
  662. text-align: center;
  663. padding: 60rpx 0;
  664. font-size: 26rpx;
  665. color: #bbb;
  666. }
  667. /* ==================== 底部 TabBar ==================== */
  668. .bottom-tab {
  669. position: fixed;
  670. bottom: 0;
  671. left: 0;
  672. right: 0;
  673. height: 100rpx;
  674. background: #fff;
  675. border-top: 1rpx solid #eee;
  676. display: flex;
  677. z-index: 100;
  678. }
  679. .tab-item {
  680. flex: 1;
  681. display: flex;
  682. flex-direction: column;
  683. align-items: center;
  684. justify-content: center;
  685. gap: 4rpx;
  686. }
  687. .tab-item--active .tab-label {
  688. color: #1a365d;
  689. font-weight: 600;
  690. }
  691. .tab-icon { font-size: 36rpx; }
  692. .tab-label { font-size: 22rpx; color: #999; }
  693. </style>