统一订单列表混排租赁与撞车,后台撞车订单展示真实手机号
PC/移动端「我的订单」并行拉取两类订单并按时间混排展示;后台撞车订单买家手机号不再脱敏,便于履约联系。
This commit is contained in:
@@ -102,8 +102,9 @@ func toOrderDTO(row model.MohongOrder, snap productSnapshot, items []orderItemSn
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
if includeAdmin {
|
||||
// 后台展示真实手机号,便于履约联系买家
|
||||
dto.BuyerNickname = buyerNickname
|
||||
dto.BuyerPhone = maskPhone(buyerPhone)
|
||||
dto.BuyerPhone = strings.TrimSpace(buyerPhone)
|
||||
dto.AdminRemark = row.AdminRemark
|
||||
} else {
|
||||
dto.BuyerNickname = buyerNickname
|
||||
|
||||
@@ -7,6 +7,10 @@ const { unreadCount, unreadLabel } = useChatUnreadCount(route)
|
||||
|
||||
function isNavActive(path: string) {
|
||||
if (path === '/m') return route.path === '/m'
|
||||
// 订单 Tab 同时覆盖租赁订单与撞车订单路径
|
||||
if (path === '/m/orders') {
|
||||
return route.path.startsWith('/m/orders') || route.path.startsWith('/m/crash/orders')
|
||||
}
|
||||
return route.path.startsWith(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -224,7 +224,7 @@ async function handleSupportClick() {
|
||||
>
|
||||
{{ supportLoading ? '...' : '客服' }}
|
||||
</button>
|
||||
<button type="button" class="orders-btn" @click="router.push('/crash/orders')">订单</button>
|
||||
<button type="button" class="orders-btn" @click="router.push('/m/orders')">订单</button>
|
||||
</header>
|
||||
|
||||
<div class="shop-body">
|
||||
|
||||
@@ -46,7 +46,7 @@ async function loadOrders() {
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="card"
|
||||
@click="router.push(`/crash/orders/${item.id}`)"
|
||||
@click="router.push(`/m/crash/orders/${item.id}`)"
|
||||
>
|
||||
<div class="top">
|
||||
<span>{{ item.order_no }}</span>
|
||||
|
||||
@@ -228,7 +228,7 @@ async function handleSupportClick() {
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
<span>购物车{{ cartCount > 0 ? `(${cartCount})` : '' }}</span>
|
||||
</button>
|
||||
<button type="button" class="toolbar-btn outline" @click="router.push('/crash/orders')">
|
||||
<button type="button" class="toolbar-btn outline" @click="router.push('/orders')">
|
||||
<el-icon><Tickets /></el-icon>
|
||||
<span>我的订单</span>
|
||||
</button>
|
||||
|
||||
@@ -135,7 +135,7 @@ async function copyText() {
|
||||
<h1>{{ statusText }}</h1>
|
||||
<p class="sub">订单号 {{ order.order_no }} · {{ formatDateTime(order.created_at) }}</p>
|
||||
</div>
|
||||
<button type="button" class="toolbar-btn outline" @click="router.push('/crash/orders')">
|
||||
<button type="button" class="toolbar-btn outline" @click="router.push('/orders')">
|
||||
返回列表
|
||||
</button>
|
||||
</header>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { readError } from '@/shared/utils/error'
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import { onMounted, ref, computed, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||||
@@ -11,16 +11,40 @@ import MobilePayWaySelectPopup from '@/features/orders/components/MobilePayWaySe
|
||||
import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier'
|
||||
import { useMobilePayWaySelect } from '@/features/orders/composables/useMobilePayWaySelect'
|
||||
import { fetchOrderChat } from '@/features/chats/api/chats'
|
||||
import { centToYuan, formatMoney } from '@/shared/utils/money'
|
||||
import {
|
||||
fetchMyMohongOrders,
|
||||
mohongOrderStatusLabel,
|
||||
type MohongOrder,
|
||||
} from '@/features/mohong/api/mohong'
|
||||
import { centToYuan, formatCent, formatMoney } from '@/shared/utils/money'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { formatDateMinute } from '@/shared/utils/time'
|
||||
import { formatListingNo } from '@/shared/utils/listingDisplay'
|
||||
|
||||
type BizType = 'rental' | 'crash'
|
||||
|
||||
interface UnifiedOrderItem {
|
||||
key: string
|
||||
bizType: BizType
|
||||
id: number
|
||||
orderNo: string
|
||||
title: string
|
||||
status: string
|
||||
createdAt: string
|
||||
coverUrl?: string
|
||||
quantity?: number
|
||||
unit?: string
|
||||
amountText: string
|
||||
rental?: Order
|
||||
crash?: MohongOrder
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const session = useSessionStore()
|
||||
const loading = ref(false)
|
||||
const orders = ref<Order[]>([])
|
||||
const rentalOrders = ref<Order[]>([])
|
||||
const crashOrders = ref<MohongOrder[]>([])
|
||||
const activeTab = ref('all')
|
||||
const payingOrderId = ref<number | null>(null)
|
||||
const {
|
||||
@@ -34,45 +58,113 @@ const {
|
||||
refreshPaymentStatus,
|
||||
} = useMobilePaymentCashier()
|
||||
|
||||
// 支付方式选择底部弹层。
|
||||
const payWaySelect = useMobilePayWaySelect()
|
||||
const selectPayWay = payWaySelect.select
|
||||
|
||||
onMounted(() => {
|
||||
loadOrders()
|
||||
if (route.query.tab) {
|
||||
activeTab.value = String(route.query.tab)
|
||||
}
|
||||
})
|
||||
|
||||
async function loadOrders() {
|
||||
loading.value = true
|
||||
try {
|
||||
const items = await fetchOrders()
|
||||
orders.value = Array.isArray(items) ? items : []
|
||||
} catch {
|
||||
showToast({ message: '订单加载失败,请稍后重试', icon: 'warning-o' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* 状态筛选 */
|
||||
const statusTabs = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'pending_payment', label: '待支付' },
|
||||
{ key: 'pending_handoff', label: '待交接' },
|
||||
{ key: 'renting', label: '使用中' },
|
||||
{ key: 'paid', label: '已支付' },
|
||||
{ key: 'pending_checkout_confirm', label: '待结账' },
|
||||
{ key: 'completed', label: '已完成' },
|
||||
{ key: 'cancelled', label: '已取消' },
|
||||
]
|
||||
|
||||
const displayOrders = computed(() => {
|
||||
if (activeTab.value === 'all') return orders.value
|
||||
return orders.value.filter(o => o.status === activeTab.value)
|
||||
const unifiedOrders = computed<UnifiedOrderItem[]>(() => {
|
||||
const rentalItems: UnifiedOrderItem[] = rentalOrders.value.map(order => ({
|
||||
key: `rental-${order.id}`,
|
||||
bizType: 'rental',
|
||||
id: order.id,
|
||||
orderNo: order.order_no,
|
||||
title: order.title,
|
||||
status: order.status,
|
||||
createdAt: order.created_at,
|
||||
amountText: money(orderRentAmount(order)),
|
||||
rental: order,
|
||||
}))
|
||||
|
||||
const crashItems: UnifiedOrderItem[] = crashOrders.value.map(order => ({
|
||||
key: `crash-${order.id}`,
|
||||
bizType: 'crash',
|
||||
id: order.id,
|
||||
orderNo: order.order_no,
|
||||
title: order.product_title,
|
||||
status: order.status,
|
||||
createdAt: order.created_at,
|
||||
coverUrl: order.product_cover_url,
|
||||
quantity: order.quantity,
|
||||
unit: order.product_unit,
|
||||
amountText: order.amount || formatCent(order.amount_cent),
|
||||
crash: order,
|
||||
}))
|
||||
|
||||
return [...rentalItems, ...crashItems].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)
|
||||
})
|
||||
|
||||
function statusLabel(status: string) {
|
||||
const displayOrders = computed(() => {
|
||||
if (activeTab.value === 'all') return unifiedOrders.value
|
||||
return unifiedOrders.value.filter(item => item.status === activeTab.value)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.tab) {
|
||||
activeTab.value = String(route.query.tab)
|
||||
}
|
||||
loadOrders()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.query.tab,
|
||||
tab => {
|
||||
if (tab) activeTab.value = String(tab)
|
||||
}
|
||||
)
|
||||
|
||||
function onStatusTabChange(name: string | number) {
|
||||
activeTab.value = String(name)
|
||||
router.replace({
|
||||
path: '/m/orders',
|
||||
query: {
|
||||
tab: activeTab.value === 'all' ? undefined : activeTab.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function loadOrders() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [rentalResult, crashResult] = await Promise.allSettled([
|
||||
fetchOrders(),
|
||||
fetchMyMohongOrders({ page: 1, page_size: 50 }),
|
||||
])
|
||||
|
||||
if (rentalResult.status === 'fulfilled') {
|
||||
rentalOrders.value = Array.isArray(rentalResult.value) ? rentalResult.value : []
|
||||
} else {
|
||||
rentalOrders.value = []
|
||||
showToast({ message: '租赁订单加载失败', icon: 'warning-o' })
|
||||
}
|
||||
|
||||
if (crashResult.status === 'fulfilled') {
|
||||
crashOrders.value = crashResult.value?.items || []
|
||||
} else {
|
||||
crashOrders.value = []
|
||||
showToast({
|
||||
message: readError(crashResult.reason, '撞车订单加载失败'),
|
||||
icon: 'warning-o',
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function statusLabel(item: UnifiedOrderItem) {
|
||||
if (item.bizType === 'crash') return mohongOrderStatusLabel(item.status)
|
||||
const map: Record<string, string> = {
|
||||
pending_payment: '待支付',
|
||||
pending_handoff: '待交接',
|
||||
@@ -88,11 +180,19 @@ function statusLabel(status: string) {
|
||||
abnormal: '异常',
|
||||
closed: '已关闭',
|
||||
}
|
||||
return map[status] || status
|
||||
return map[item.status] || item.status
|
||||
}
|
||||
|
||||
function goDetail(id: number) {
|
||||
router.push(`/m/orders/${id}`)
|
||||
function bizTypeLabel(bizType: BizType) {
|
||||
return bizType === 'crash' ? '撞车' : '租赁'
|
||||
}
|
||||
|
||||
function goDetail(item: UnifiedOrderItem) {
|
||||
if (item.bizType === 'crash') {
|
||||
router.push(`/m/crash/orders/${item.id}`)
|
||||
return
|
||||
}
|
||||
router.push(`/m/orders/${item.id}`)
|
||||
}
|
||||
|
||||
async function handlePay(order: Order) {
|
||||
@@ -124,8 +224,6 @@ async function openOrderChatAfterPayment(order: Order) {
|
||||
}
|
||||
}
|
||||
|
||||
// selectPayWay 在列表快捷支付前让用户选择支付宝或微信(见 payWaySelect)。
|
||||
|
||||
function money(value: unknown) {
|
||||
return formatMoney(Number(value || 0))
|
||||
}
|
||||
@@ -171,7 +269,6 @@ async function copyListingCode(order: Order) {
|
||||
|
||||
<template>
|
||||
<main class="mobile-orders">
|
||||
<!-- 顶部导航 -->
|
||||
<header class="page-header">
|
||||
<button class="back-btn" @click="router.back()">
|
||||
<van-icon name="arrow-left" :size="20" />
|
||||
@@ -180,7 +277,6 @@ async function copyListingCode(order: Order) {
|
||||
<span class="header-spacer"></span>
|
||||
</header>
|
||||
|
||||
<!-- 状态筛选条 - Vant 滑动标签页 -->
|
||||
<van-tabs
|
||||
v-model:active="activeTab"
|
||||
class="custom-tabs"
|
||||
@@ -192,11 +288,11 @@ async function copyListingCode(order: Order) {
|
||||
:border="false"
|
||||
swipeable
|
||||
animated
|
||||
@change="onStatusTabChange"
|
||||
>
|
||||
<van-tab v-for="tab in statusTabs" :key="tab.key" :title="tab.label" :name="tab.key" />
|
||||
</van-tabs>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<section class="order-list">
|
||||
<van-loading v-if="loading" class="center-loading" size="24px" vertical>
|
||||
加载中...
|
||||
@@ -208,77 +304,124 @@ async function copyListingCode(order: Order) {
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-for="order in displayOrders"
|
||||
:key="order.id"
|
||||
v-for="item in displayOrders"
|
||||
:key="item.key"
|
||||
class="order-card"
|
||||
@click="goDetail(order.id)"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<!-- 卡片头:订单号 + 状态 -->
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<span class="order-no">{{ order.order_no }}</span>
|
||||
<span class="biz-tag" :class="item.bizType">{{ bizTypeLabel(item.bizType) }}</span>
|
||||
<span class="order-no">{{ item.orderNo }}</span>
|
||||
</div>
|
||||
<span class="status-badge" :class="'badge-' + order.status">
|
||||
{{ statusLabel(order.status) }}
|
||||
<span class="status-badge" :class="'badge-' + item.status">
|
||||
{{ statusLabel(item) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 卡片体:关键信息 -->
|
||||
<div class="card-body">
|
||||
<button class="listing-code-chip" type="button" @click.stop="copyListingCode(order)">
|
||||
商品编号 {{ formatListingCode(order) }}
|
||||
</button>
|
||||
<h3 class="order-title">{{ order.title }}</h3>
|
||||
<div class="tag-row">
|
||||
<span class="info-tag">{{ order.server_region }}</span>
|
||||
<span class="info-tag">{{ order.login_platform }}</span>
|
||||
<!-- 租赁卡片 -->
|
||||
<template v-if="item.bizType === 'rental' && item.rental">
|
||||
<div class="card-body">
|
||||
<button
|
||||
class="listing-code-chip"
|
||||
type="button"
|
||||
@click.stop="copyListingCode(item.rental)"
|
||||
>
|
||||
商品编号 {{ formatListingCode(item.rental) }}
|
||||
</button>
|
||||
<h3 class="order-title">{{ item.title }}</h3>
|
||||
<div class="tag-row">
|
||||
<span class="info-tag">{{ item.rental.server_region }}</span>
|
||||
<span class="info-tag">{{ item.rental.login_platform }}</span>
|
||||
</div>
|
||||
|
||||
<div class="price-row">
|
||||
<div class="price-item">
|
||||
<span class="price-label">{{ amountLabel(item.rental) }}</span>
|
||||
<span class="price-val">¥{{ money(orderRentAmount(item.rental)) }}</span>
|
||||
<span v-if="ownerActualIncome(item.rental) !== null" class="price-sub"
|
||||
>实际到手 ¥{{ money(ownerActualIncome(item.rental)) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="price-item">
|
||||
<span class="price-label">押金金额</span>
|
||||
<span class="price-val deposit">
|
||||
¥{{ money(amountYuan(item.rental.deposit_amount_cent)) }}
|
||||
<em v-if="amountYuan(item.rental.deposit_waived_amount_cent) > 0"
|
||||
>免 ¥{{ money(amountYuan(item.rental.deposit_waived_amount_cent)) }}</em
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-row">
|
||||
<div class="price-item">
|
||||
<span class="price-label">{{ amountLabel(order) }}</span>
|
||||
<span class="price-val">¥{{ money(orderRentAmount(order)) }}</span>
|
||||
<span v-if="ownerActualIncome(order) !== null" class="price-sub"
|
||||
>实际到手 ¥{{ money(ownerActualIncome(order)) }}</span
|
||||
>
|
||||
<div class="card-footer">
|
||||
<div class="time-box">
|
||||
<van-icon name="clock-o" class="clock-icon" />
|
||||
<span class="order-time">{{ formatDateMinute(item.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="price-item">
|
||||
<span class="price-label">押金金额</span>
|
||||
<span class="price-val deposit">
|
||||
¥{{ money(amountYuan(order.deposit_amount_cent)) }}
|
||||
<em v-if="amountYuan(order.deposit_waived_amount_cent) > 0"
|
||||
>免 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</em
|
||||
>
|
||||
<div class="footer-action">
|
||||
<van-button
|
||||
v-if="
|
||||
item.rental.status === 'pending_payment' &&
|
||||
item.rental.renter_id === session.userId
|
||||
"
|
||||
size="small"
|
||||
type="warning"
|
||||
round
|
||||
class="pay-btn"
|
||||
:loading="payingOrderId === item.rental.id"
|
||||
@click.stop="handlePay(item.rental)"
|
||||
>
|
||||
去支付
|
||||
</van-button>
|
||||
<span v-else class="detail-link">
|
||||
查看详情 <van-icon name="arrow" :size="10" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 卡片底:时间 + 操作 -->
|
||||
<div class="card-footer">
|
||||
<div class="time-box">
|
||||
<van-icon name="clock-o" class="clock-icon" />
|
||||
<span class="order-time">{{ formatDateMinute(order.created_at) }}</span>
|
||||
<!-- 撞车卡片 -->
|
||||
<template v-else>
|
||||
<div class="card-body crash-body">
|
||||
<div class="crash-cover">
|
||||
<img v-if="item.coverUrl" :src="item.coverUrl" alt="" />
|
||||
<span v-else>图</span>
|
||||
</div>
|
||||
<div class="crash-info">
|
||||
<h3 class="order-title">{{ item.title }}</h3>
|
||||
<p class="crash-meta">
|
||||
x{{ item.quantity || 1 }}
|
||||
<template v-if="item.unit"> · {{ item.unit }}</template>
|
||||
</p>
|
||||
<div class="price-row crash-price">
|
||||
<div class="price-item">
|
||||
<span class="price-label">订单金额</span>
|
||||
<span class="price-val">¥{{ item.amountText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-action">
|
||||
<van-button
|
||||
v-if="order.status === 'pending_payment' && order.renter_id === session.userId"
|
||||
size="small"
|
||||
type="warning"
|
||||
round
|
||||
class="pay-btn"
|
||||
:loading="payingOrderId === order.id"
|
||||
@click.stop="handlePay(order)"
|
||||
>
|
||||
去支付
|
||||
</van-button>
|
||||
<span v-else class="detail-link"> 查看详情 <van-icon name="arrow" :size="10" /> </span>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="time-box">
|
||||
<van-icon name="clock-o" class="clock-icon" />
|
||||
<span class="order-time">{{ formatDateMinute(item.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="footer-action">
|
||||
<span v-if="item.status === 'pending_payment'" class="detail-link pay-hint">
|
||||
去支付 <van-icon name="arrow" :size="10" />
|
||||
</span>
|
||||
<span v-else class="detail-link">
|
||||
查看详情 <van-icon name="arrow" :size="10" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<MobilePayWaySelectPopup
|
||||
v-model:show="payWaySelect.visible.value"
|
||||
@choose="payWaySelect.choose"
|
||||
@@ -306,7 +449,6 @@ async function copyListingCode(order: Order) {
|
||||
padding-bottom: calc(64px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* ========== 顶部导航 ========== */
|
||||
.page-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -344,7 +486,6 @@ async function copyListingCode(order: Order) {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
/* ========== Vant Tabs 自定义样式 ========== */
|
||||
.custom-tabs {
|
||||
position: sticky;
|
||||
top: 48px;
|
||||
@@ -364,7 +505,6 @@ async function copyListingCode(order: Order) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ========== 订单列表 ========== */
|
||||
.order-list {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
@@ -379,7 +519,6 @@ async function copyListingCode(order: Order) {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/* ========== 订单卡片 ========== */
|
||||
.order-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
@@ -406,15 +545,46 @@ async function copyListingCode(order: Order) {
|
||||
justify-content: space-between;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px dashed #f3f4f6;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.biz-tag {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.biz-tag.rental {
|
||||
color: #1477ff;
|
||||
background: rgba(20, 119, 255, 0.1);
|
||||
}
|
||||
|
||||
.biz-tag.crash {
|
||||
color: #ff6a00;
|
||||
background: rgba(255, 106, 0, 0.1);
|
||||
}
|
||||
|
||||
.order-no {
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
font-family: monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
@@ -433,7 +603,6 @@ async function copyListingCode(order: Order) {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* 状态徽章颜色 - 现代轻量化配色 */
|
||||
.badge-pending_payment {
|
||||
color: #ff6a00;
|
||||
background: rgba(255, 106, 0, 0.08);
|
||||
@@ -450,10 +619,7 @@ async function copyListingCode(order: Order) {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
.badge-pending_return_confirm {
|
||||
color: #8b5cf6;
|
||||
background: rgba(139, 92, 246, 0.08);
|
||||
}
|
||||
.badge-pending_return_confirm,
|
||||
.badge-pending_checkout_confirm {
|
||||
color: #8b5cf6;
|
||||
background: rgba(139, 92, 246, 0.08);
|
||||
@@ -462,30 +628,26 @@ async function copyListingCode(order: Order) {
|
||||
color: #a855f7;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
}
|
||||
.badge-checkout_disputing {
|
||||
.badge-checkout_disputing,
|
||||
.badge-disputing,
|
||||
.badge-abnormal {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
.badge-paid {
|
||||
color: #1477ff;
|
||||
background: rgba(20, 119, 255, 0.08);
|
||||
}
|
||||
.badge-completed {
|
||||
color: #10b981;
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
}
|
||||
.badge-cancelled {
|
||||
.badge-cancelled,
|
||||
.badge-closed,
|
||||
.badge-refunded {
|
||||
color: #9ca3af;
|
||||
background: rgba(156, 163, 175, 0.08);
|
||||
}
|
||||
.badge-disputing {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
.badge-abnormal {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
.badge-closed {
|
||||
color: #6b7280;
|
||||
background: rgba(107, 114, 128, 0.08);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 12px 0 0;
|
||||
@@ -560,7 +722,49 @@ async function copyListingCode(order: Order) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ========== 卡片底部 ========== */
|
||||
.crash-body {
|
||||
display: grid;
|
||||
grid-template-columns: 64px 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.crash-cover {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #f0f3f8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #a0aec0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.crash-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.crash-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.crash-meta {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
color: #8a94a6;
|
||||
}
|
||||
|
||||
.crash-price {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.crash-price .price-val {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -594,6 +798,10 @@ async function copyListingCode(order: Order) {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.pay-hint {
|
||||
color: #ff5f00;
|
||||
}
|
||||
|
||||
.pay-btn {
|
||||
height: 28px !important;
|
||||
padding: 0 16px !important;
|
||||
|
||||
@@ -6,33 +6,93 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { CopyDocument, Search } from '@element-plus/icons-vue'
|
||||
|
||||
import { fetchOrders, type Order } from '@/features/orders'
|
||||
import { centToYuan, formatMoney } from '@/shared/utils/money'
|
||||
import {
|
||||
fetchMyMohongOrders,
|
||||
mohongOrderStatusLabel,
|
||||
type MohongOrder,
|
||||
} from '@/features/mohong/api/mohong'
|
||||
import { centToYuan, formatCent, formatMoney } from '@/shared/utils/money'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { orderStatusLabel } from '@/shared/utils/statusLabels'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { formatListingNo } from '@/shared/utils/listingDisplay'
|
||||
|
||||
type BizType = 'rental' | 'crash'
|
||||
|
||||
interface UnifiedOrderItem {
|
||||
key: string
|
||||
bizType: BizType
|
||||
id: number
|
||||
orderNo: string
|
||||
title: string
|
||||
status: string
|
||||
createdAt: string
|
||||
amountText: string
|
||||
coverUrl?: string
|
||||
quantity?: number
|
||||
unit?: string
|
||||
rental?: Order
|
||||
crash?: MohongOrder
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const orders = ref<Order[]>([])
|
||||
const rentalOrders = ref<Order[]>([])
|
||||
const crashOrders = ref<MohongOrder[]>([])
|
||||
const payingOrderId = ref<number | null>(null)
|
||||
const session = useSessionStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const searchKeyword = ref('')
|
||||
const fallbackPendingPaymentMinutes = 15
|
||||
|
||||
const statusTabs = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'pending_payment', label: '待支付' },
|
||||
{ key: 'pending_handoff', label: '待交接' },
|
||||
{ key: 'renting', label: '使用中' },
|
||||
{ key: 'paid', label: '已支付' },
|
||||
{ key: 'pending_checkout_confirm', label: '待结账' },
|
||||
{ key: 'completed', label: '已完成' },
|
||||
{ key: 'cancelled', label: '已取消' },
|
||||
]
|
||||
const tabKeys = new Set(statusTabs.map(item => item.key))
|
||||
const activeTab = ref(readTab(route.query.tab))
|
||||
const fallbackPendingPaymentMinutes = 15
|
||||
|
||||
const unifiedOrders = computed<UnifiedOrderItem[]>(() => {
|
||||
const rentalItems: UnifiedOrderItem[] = rentalOrders.value.map(order => ({
|
||||
key: `rental-${order.id}`,
|
||||
bizType: 'rental',
|
||||
id: order.id,
|
||||
orderNo: order.order_no,
|
||||
title: order.title,
|
||||
status: order.status,
|
||||
createdAt: order.created_at,
|
||||
amountText: money(orderRentAmount(order)),
|
||||
rental: order,
|
||||
}))
|
||||
|
||||
const crashItems: UnifiedOrderItem[] = crashOrders.value.map(order => ({
|
||||
key: `crash-${order.id}`,
|
||||
bizType: 'crash',
|
||||
id: order.id,
|
||||
orderNo: order.order_no,
|
||||
title: order.product_title,
|
||||
status: order.status,
|
||||
createdAt: order.created_at,
|
||||
amountText: order.amount || formatCent(order.amount_cent),
|
||||
coverUrl: order.product_cover_url,
|
||||
quantity: order.quantity,
|
||||
unit: order.product_unit,
|
||||
crash: order,
|
||||
}))
|
||||
|
||||
return [...rentalItems, ...crashItems].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)
|
||||
})
|
||||
|
||||
const displayOrders = computed(() => {
|
||||
let filtered = orders.value
|
||||
let filtered = unifiedOrders.value
|
||||
|
||||
if (activeTab.value !== 'all') {
|
||||
filtered = filtered.filter(order => order.status === activeTab.value)
|
||||
@@ -40,24 +100,28 @@ const displayOrders = computed(() => {
|
||||
|
||||
if (searchKeyword.value.trim()) {
|
||||
const keyword = searchKeyword.value.trim().toLowerCase()
|
||||
filtered = filtered.filter(
|
||||
order =>
|
||||
textValue(order.order_no).toLowerCase().includes(keyword) ||
|
||||
formatListingCode(order).toLowerCase().includes(keyword) ||
|
||||
String(order.listing_id).includes(keyword) ||
|
||||
String(order.account_id).includes(keyword) ||
|
||||
textValue(order.title).toLowerCase().includes(keyword)
|
||||
)
|
||||
filtered = filtered.filter(order => {
|
||||
if (textValue(order.orderNo).toLowerCase().includes(keyword)) return true
|
||||
if (textValue(order.title).toLowerCase().includes(keyword)) return true
|
||||
if (order.bizType === 'rental' && order.rental) {
|
||||
return (
|
||||
formatListingCode(order.rental).toLowerCase().includes(keyword) ||
|
||||
String(order.rental.listing_id).includes(keyword) ||
|
||||
String(order.rental.account_id).includes(keyword)
|
||||
)
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
const tabCounts = computed(() => {
|
||||
const counts: Record<string, number> = { all: orders.value.length }
|
||||
const counts: Record<string, number> = { all: unifiedOrders.value.length }
|
||||
statusTabs.forEach(tab => {
|
||||
if (tab.key !== 'all') {
|
||||
counts[tab.key] = orders.value.filter(order => order.status === tab.key).length
|
||||
counts[tab.key] = unifiedOrders.value.filter(order => order.status === tab.key).length
|
||||
}
|
||||
})
|
||||
return counts
|
||||
@@ -79,19 +143,44 @@ function readTab(tab: unknown) {
|
||||
|
||||
function handleTabChange(tab: string | number) {
|
||||
const nextTab = readTab(String(tab))
|
||||
activeTab.value = nextTab
|
||||
router.replace({ path: '/orders', query: nextTab === 'all' ? {} : { tab: nextTab } })
|
||||
}
|
||||
|
||||
async function loadOrders() {
|
||||
loading.value = true
|
||||
try {
|
||||
const items = await fetchOrders()
|
||||
orders.value = Array.isArray(items) ? items : []
|
||||
const [rentalResult, crashResult] = await Promise.allSettled([
|
||||
fetchOrders(),
|
||||
fetchMyMohongOrders({ page: 1, page_size: 50 }),
|
||||
])
|
||||
|
||||
if (rentalResult.status === 'fulfilled') {
|
||||
rentalOrders.value = Array.isArray(rentalResult.value) ? rentalResult.value : []
|
||||
} else {
|
||||
rentalOrders.value = []
|
||||
ElMessage.error('租赁订单加载失败')
|
||||
}
|
||||
|
||||
if (crashResult.status === 'fulfilled') {
|
||||
crashOrders.value = crashResult.value?.items || []
|
||||
} else {
|
||||
crashOrders.value = []
|
||||
ElMessage.error(readError(crashResult.reason, '撞车订单加载失败'))
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goDetail(item: UnifiedOrderItem) {
|
||||
if (item.bizType === 'crash') {
|
||||
router.push(`/crash/orders/${item.id}`)
|
||||
return
|
||||
}
|
||||
router.push(`/orders/${item.id}`)
|
||||
}
|
||||
|
||||
async function handlePay(order: Order) {
|
||||
payingOrderId.value = order.id
|
||||
try {
|
||||
@@ -202,6 +291,15 @@ function getCountdownMinutes(order: Order) {
|
||||
const diff = deadline.getTime() - now.getTime()
|
||||
return Math.max(0, Math.ceil(diff / 60000))
|
||||
}
|
||||
|
||||
function statusText(item: UnifiedOrderItem) {
|
||||
if (item.bizType === 'crash') return mohongOrderStatusLabel(item.status)
|
||||
return orderStatusLabel(item.status)
|
||||
}
|
||||
|
||||
function bizTypeLabel(bizType: BizType) {
|
||||
return bizType === 'crash' ? '撞车' : '租赁'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -209,13 +307,13 @@ function getCountdownMinutes(order: Order) {
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">ORDERS</p>
|
||||
<h1>我的订单</h1>
|
||||
<p>跟踪待支付、待交接、使用中、待归还、申诉中和已完成订单。</p>
|
||||
<p>租赁与撞车订单统一展示,按创建时间倒序排列。</p>
|
||||
</div>
|
||||
|
||||
<div class="orders-toolbar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索订单号 / 商品编号 / 账号"
|
||||
placeholder="搜索订单号 / 商品 / 账号"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
class="search-input"
|
||||
@@ -241,89 +339,130 @@ function getCountdownMinutes(order: Order) {
|
||||
|
||||
<div v-else class="orders-container">
|
||||
<el-table v-loading="loading" class="table-panel orders-table" :data="displayOrders">
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="biz-tag" :class="row.bizType">{{ bizTypeLabel(row.bizType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单号" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="order-no-cell">
|
||||
<span class="order-no-text" :title="row.order_no">{{
|
||||
shortenOrderNo(row.order_no)
|
||||
<span class="order-no-text" :title="row.orderNo">{{
|
||||
shortenOrderNo(row.orderNo)
|
||||
}}</span>
|
||||
<el-icon class="copy-icon" @click="copyOrderNo(row.order_no)">
|
||||
<el-icon class="copy-icon" @click="copyOrderNo(row.orderNo)">
|
||||
<CopyDocument />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品编号" width="150">
|
||||
<el-table-column label="商品 / 账号" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="order-no-cell">
|
||||
<span class="order-no-text">{{ formatListingCode(row) }}</span>
|
||||
<el-icon class="copy-icon" @click="copyListingCode(row)">
|
||||
<CopyDocument />
|
||||
</el-icon>
|
||||
<div v-if="row.bizType === 'crash'" class="crash-product-cell">
|
||||
<div class="crash-cover">
|
||||
<img v-if="row.coverUrl" :src="row.coverUrl" alt="" />
|
||||
<span v-else>图</span>
|
||||
</div>
|
||||
<div class="crash-product-info">
|
||||
<strong>{{ row.title }}</strong>
|
||||
<span
|
||||
>x{{ row.quantity || 1 }}{{ row.unit ? ` · ${row.unit}` : '' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="rental-product-cell">
|
||||
<button
|
||||
v-if="row.rental"
|
||||
class="listing-code-line compact"
|
||||
type="button"
|
||||
@click="copyListingCode(row.rental)"
|
||||
>
|
||||
{{ formatListingCode(row.rental) }}
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</button>
|
||||
<span class="rental-title">{{ row.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="账号" min-width="180" />
|
||||
<el-table-column label="金额" width="132">
|
||||
<el-table-column label="金额" width="140">
|
||||
<template #default="{ row }">
|
||||
<div class="amount-cell">
|
||||
<span class="amount-value">¥{{ money(orderRentAmount(row)) }}</span>
|
||||
<span class="amount-label">{{ amountLabel(row) }}</span>
|
||||
<span v-if="ownerActualIncome(row) !== null" class="amount-sub"
|
||||
>实际到手 ¥{{ money(ownerActualIncome(row)) }}</span
|
||||
<div v-if="row.bizType === 'rental' && row.rental" class="amount-cell">
|
||||
<span class="amount-value">¥{{ money(orderRentAmount(row.rental)) }}</span>
|
||||
<span class="amount-label">{{ amountLabel(row.rental) }}</span>
|
||||
<span v-if="ownerActualIncome(row.rental) !== null" class="amount-sub"
|
||||
>实际到手 ¥{{ money(ownerActualIncome(row.rental)) }}</span
|
||||
>
|
||||
</div>
|
||||
<div v-else class="amount-cell">
|
||||
<span class="amount-value">¥{{ row.amountText }}</span>
|
||||
<span class="amount-label">订单金额</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="押金" width="100">
|
||||
<el-table-column label="押金 / 数量" width="110">
|
||||
<template #default="{ row }">
|
||||
<div class="amount-cell">
|
||||
<span class="amount-value">¥{{ money(amountYuan(row.deposit_amount_cent)) }}</span>
|
||||
<span v-if="amountYuan(row.deposit_waived_amount_cent) > 0" class="amount-label"
|
||||
>免 ¥{{ money(amountYuan(row.deposit_waived_amount_cent)) }}</span
|
||||
<div v-if="row.bizType === 'rental' && row.rental" class="amount-cell">
|
||||
<span class="amount-value">¥{{ money(amountYuan(row.rental.deposit_amount_cent)) }}</span>
|
||||
<span v-if="amountYuan(row.rental.deposit_waived_amount_cent) > 0" class="amount-label"
|
||||
>免 ¥{{ money(amountYuan(row.rental.deposit_waived_amount_cent)) }}</span
|
||||
>
|
||||
</div>
|
||||
<span v-else class="amount-value"
|
||||
>x{{ row.quantity || 1 }}{{ row.unit ? row.unit : '' }}</span
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="身份" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="isRenter(row) ? 'primary' : 'success'" size="small">
|
||||
{{ orderRole(row) }}
|
||||
<el-tag
|
||||
v-if="row.bizType === 'rental' && row.rental"
|
||||
:type="isRenter(row.rental) ? 'primary' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
{{ orderRole(row.rental) }}
|
||||
</el-tag>
|
||||
<span v-else class="muted">买家</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="140">
|
||||
<template #default="{ row }">
|
||||
<div class="status-cell">
|
||||
<span>{{ orderStatusLabel(row.status) }}</span>
|
||||
<span>{{ statusText(row) }}</span>
|
||||
<span
|
||||
v-if="row.status === 'pending_payment' && getCountdownMinutes(row) > 0"
|
||||
v-if="
|
||||
row.bizType === 'rental' &&
|
||||
row.rental &&
|
||||
row.rental.status === 'pending_payment' &&
|
||||
getCountdownMinutes(row.rental) > 0
|
||||
"
|
||||
class="countdown-badge"
|
||||
>
|
||||
{{ getCountdownMinutes(row) }}分钟后超时
|
||||
{{ getCountdownMinutes(row.rental) }}分钟后超时
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
<template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
v-if="row.status === 'pending_payment'"
|
||||
v-if="
|
||||
row.bizType === 'rental' &&
|
||||
row.rental &&
|
||||
row.rental.status === 'pending_payment'
|
||||
"
|
||||
size="small"
|
||||
type="primary"
|
||||
:disabled="row.renter_id !== session.userId"
|
||||
:loading="payingOrderId === row.id"
|
||||
@click="handlePay(row)"
|
||||
:disabled="row.rental.renter_id !== session.userId"
|
||||
:loading="payingOrderId === row.rental.id"
|
||||
@click="handlePay(row.rental)"
|
||||
>
|
||||
使用中
|
||||
去支付
|
||||
</el-button>
|
||||
<RouterLink :to="`/orders/${row.id}`">
|
||||
<el-button size="small">详情</el-button>
|
||||
</RouterLink>
|
||||
<el-button size="small" @click="goDetail(row)">详情</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -331,72 +470,115 @@ function getCountdownMinutes(order: Order) {
|
||||
|
||||
<div class="mobile-cards">
|
||||
<div
|
||||
v-for="order in displayOrders"
|
||||
:key="order.id"
|
||||
v-for="item in displayOrders"
|
||||
:key="item.key"
|
||||
class="order-card"
|
||||
@click="router.push(`/orders/${order.id}`)"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="order-no-row">
|
||||
<span class="order-no-text" :title="order.order_no">{{
|
||||
shortenOrderNo(order.order_no)
|
||||
<span class="biz-tag" :class="item.bizType">{{ bizTypeLabel(item.bizType) }}</span>
|
||||
<span class="order-no-text" :title="item.orderNo">{{
|
||||
shortenOrderNo(item.orderNo)
|
||||
}}</span>
|
||||
<el-icon class="copy-icon" @click.stop="copyOrderNo(order.order_no)">
|
||||
<el-icon class="copy-icon" @click.stop="copyOrderNo(item.orderNo)">
|
||||
<CopyDocument />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-tag :type="isRenter(order) ? 'primary' : 'success'" size="small">
|
||||
{{ orderRole(order) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<button class="listing-code-line" type="button" @click.stop="copyListingCode(order)">
|
||||
商品编号 {{ formatListingCode(order) }}
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</button>
|
||||
<div class="card-title">{{ order.title }}</div>
|
||||
<div class="card-meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">{{ amountLabel(order) }}</span>
|
||||
<span class="meta-value amount">¥{{ money(orderRentAmount(order)) }}</span>
|
||||
</div>
|
||||
<div v-if="ownerActualIncome(order) !== null" class="meta-row">
|
||||
<span class="meta-label">实际到手</span>
|
||||
<span class="meta-value amount">¥{{ money(ownerActualIncome(order)) }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">押金</span>
|
||||
<span class="meta-value">
|
||||
¥{{ money(amountYuan(order.deposit_amount_cent)) }}
|
||||
<em v-if="amountYuan(order.deposit_waived_amount_cent) > 0"
|
||||
>免 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</em
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">创建时间</span>
|
||||
<span class="meta-value">{{ formatDateTime(order.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="status-info">
|
||||
<span class="status-label">{{ orderStatusLabel(order.status) }}</span>
|
||||
<span
|
||||
v-if="order.status === 'pending_payment' && getCountdownMinutes(order) > 0"
|
||||
class="countdown-badge"
|
||||
>
|
||||
{{ getCountdownMinutes(order) }}分钟后超时
|
||||
</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="order.status === 'pending_payment' && isRenter(order)"
|
||||
<el-tag
|
||||
v-if="item.bizType === 'rental' && item.rental"
|
||||
:type="isRenter(item.rental) ? 'primary' : 'success'"
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="payingOrderId === order.id"
|
||||
@click.stop="handlePay(order)"
|
||||
>
|
||||
立即支付
|
||||
</el-button>
|
||||
{{ orderRole(item.rental) }}
|
||||
</el-tag>
|
||||
<span v-else class="crash-status" :class="item.status">{{ statusText(item) }}</span>
|
||||
</div>
|
||||
|
||||
<template v-if="item.bizType === 'rental' && item.rental">
|
||||
<button
|
||||
class="listing-code-line"
|
||||
type="button"
|
||||
@click.stop="copyListingCode(item.rental)"
|
||||
>
|
||||
商品编号 {{ formatListingCode(item.rental) }}
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</button>
|
||||
<div class="card-title">{{ item.title }}</div>
|
||||
<div class="card-meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">{{ amountLabel(item.rental) }}</span>
|
||||
<span class="meta-value amount">¥{{ money(orderRentAmount(item.rental)) }}</span>
|
||||
</div>
|
||||
<div v-if="ownerActualIncome(item.rental) !== null" class="meta-row">
|
||||
<span class="meta-label">实际到手</span>
|
||||
<span class="meta-value amount">¥{{ money(ownerActualIncome(item.rental)) }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">押金</span>
|
||||
<span class="meta-value">
|
||||
¥{{ money(amountYuan(item.rental.deposit_amount_cent)) }}
|
||||
<em v-if="amountYuan(item.rental.deposit_waived_amount_cent) > 0"
|
||||
>免 ¥{{ money(amountYuan(item.rental.deposit_waived_amount_cent)) }}</em
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">创建时间</span>
|
||||
<span class="meta-value">{{ formatDateTime(item.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="status-info">
|
||||
<span class="status-label">{{ statusText(item) }}</span>
|
||||
<span
|
||||
v-if="
|
||||
item.rental.status === 'pending_payment' && getCountdownMinutes(item.rental) > 0
|
||||
"
|
||||
class="countdown-badge"
|
||||
>
|
||||
{{ getCountdownMinutes(item.rental) }}分钟后超时
|
||||
</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="item.rental.status === 'pending_payment' && isRenter(item.rental)"
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="payingOrderId === item.rental.id"
|
||||
@click.stop="handlePay(item.rental)"
|
||||
>
|
||||
立即支付
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="crash-card-body">
|
||||
<div class="crash-cover lg">
|
||||
<img v-if="item.coverUrl" :src="item.coverUrl" alt="" />
|
||||
<span v-else>图</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="card-title">{{ item.title }}</div>
|
||||
<div class="card-meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">数量</span>
|
||||
<span class="meta-value"
|
||||
>x{{ item.quantity || 1 }}{{ item.unit ? ` · ${item.unit}` : '' }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">金额</span>
|
||||
<span class="meta-value amount">¥{{ item.amountText }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">创建时间</span>
|
||||
<span class="meta-value">{{ formatDateTime(item.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -472,6 +654,27 @@ function getCountdownMinutes(order: Order) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.biz-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.biz-tag.rental {
|
||||
color: #1477ff;
|
||||
background: rgba(20, 119, 255, 0.1);
|
||||
}
|
||||
|
||||
.biz-tag.crash {
|
||||
color: #ff6a00;
|
||||
background: rgba(255, 106, 0, 0.1);
|
||||
}
|
||||
|
||||
.order-no-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -500,6 +703,11 @@ function getCountdownMinutes(order: Order) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.listing-code-line.compact {
|
||||
margin-top: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
@@ -554,6 +762,105 @@ function getCountdownMinutes(order: Order) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #9ca3af;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.crash-product-cell,
|
||||
.rental-product-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.crash-product-cell {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.crash-cover {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #f0f3f8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #a0aec0;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.crash-cover.lg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.crash-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.crash-product-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.crash-product-info strong,
|
||||
.rental-title {
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.crash-product-info span {
|
||||
font-size: 12px;
|
||||
color: #8a94a6;
|
||||
}
|
||||
|
||||
.crash-status {
|
||||
display: inline-flex;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.crash-status.pending_payment {
|
||||
background: #fff7ed;
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.crash-status.paid {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.crash-status.completed {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.crash-status.cancelled,
|
||||
.crash-status.refunded {
|
||||
background: #f8fafc;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.crash-card-body {
|
||||
display: grid;
|
||||
grid-template-columns: 64px 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
@@ -580,6 +887,7 @@ function getCountdownMinutes(order: Order) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
|
||||
@@ -98,10 +98,10 @@ export const mobileRoutes: RouteRecordRaw[] = [
|
||||
meta: { layout: 'blank' },
|
||||
},
|
||||
{
|
||||
// 撞车订单列表统一到底栏「订单」页(与租赁订单混排)
|
||||
path: '/m/crash/orders',
|
||||
name: 'mobile-crash-orders',
|
||||
component: () => import('@/features/mohong/views/MobileMohongOrdersView.vue'),
|
||||
meta: { layout: 'blank', requiresAuth: true },
|
||||
redirect: '/m/orders',
|
||||
},
|
||||
{
|
||||
path: '/m/crash/orders/:id',
|
||||
|
||||
@@ -23,10 +23,10 @@ export const publicRoutes: RouteRecordRaw[] = [
|
||||
component: () => import('@/features/mohong/views/MohongListView.vue'),
|
||||
},
|
||||
{
|
||||
// 撞车订单列表统一到「我的订单」页(与租赁订单混排)
|
||||
path: '/crash/orders',
|
||||
name: 'crash-orders',
|
||||
component: () => import('@/features/mohong/views/MohongOrdersView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
redirect: '/orders',
|
||||
},
|
||||
{
|
||||
path: '/crash/orders/:id',
|
||||
|
||||
Reference in New Issue
Block a user