Files
hfb_sys/frontend/src/views/mobile/MobileOrdersView.vue
T
2026-06-03 12:21:23 +08:00

496 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
import { showDialog, showToast } from "vant";
import MobileBottomNav from "@/components/MobileBottomNav.vue";
import { fetchOrders, payOrder, queryOrderPayment, type Order, type PaymentOrder } from "@/api/orders";
import { useSessionStore } from "@/stores/session";
import { formatDateMinute } from "@/utils/time";
const router = useRouter();
const route = useRoute();
const session = useSessionStore();
const loading = ref(false);
const orders = ref<Order[]>([]);
const activeTab = ref("all");
const payingOrderId = ref<number | null>(null);
onMounted(() => {
loadOrders();
if (route.query.tab) {
activeTab.value = String(route.query.tab);
}
});
async function loadOrders() {
loading.value = true;
try {
orders.value = await fetchOrders();
} 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: "pending_checkout_confirm", label: "待结账" },
{ key: "completed", label: "已完成" },
];
const displayOrders = computed(() => {
if (activeTab.value === "all") return orders.value;
return orders.value.filter((o) => o.status === activeTab.value);
});
function statusLabel(status: string) {
const map: Record<string, string> = {
pending_payment: "待支付",
pending_handoff: "待交接",
renting: "使用中",
overdue: "已逾期",
pending_return_confirm: "待结账",
pending_checkout_confirm: "待号主确认",
pending_checkout_accept: "待租客确认",
checkout_disputing: "结账争议中",
completed: "已完成",
cancelled: "已取消",
disputing: "申诉中",
abnormal: "异常",
closed: "已关闭",
};
return map[status] || status;
}
function goDetail(id: number) {
router.push(`/m/orders/${id}`);
}
async function handlePay(order: Order) {
payingOrderId.value = order.id;
try {
const payment = await payOrder(order.id);
if (payment.paid) {
showToast({ message: "支付成功", icon: "passed" });
} else {
await showPaymentDialog(order, payment);
}
await loadOrders();
} catch (error) {
showToast({ message: readError(error, "支付失败"), icon: "cross" });
} finally {
payingOrderId.value = null;
}
}
async function showPaymentDialog(order: Order, payment: PaymentOrder) {
const payURL = payment.jspay_url || payment.td_code || payment.jspay_info || "";
const message = payURL
? `支付单已创建,金额 ¥${(payment.amount_cent / 100).toFixed(2)}。请复制或打开支付链接完成付款,付款后可刷新订单状态。\n\n${payURL}`
: "支付单已创建,请完成付款后刷新订单状态。";
await showDialog({
title: "去支付",
message,
confirmButtonText: payURL ? "打开/复制" : "刷新状态",
cancelButtonText: "刷新状态",
showCancelButton: true,
})
.then(async () => {
if (payURL) {
await openOrCopyPayURL(payURL);
}
await refreshPayment(order.id);
})
.catch(async () => {
await refreshPayment(order.id);
});
}
async function openOrCopyPayURL(payURL: string) {
if (/^https?:\/\//.test(payURL)) {
window.open(payURL, "_blank");
return;
}
try {
await navigator.clipboard?.writeText(payURL);
showToast({ message: "支付链接已复制", icon: "passed" });
} catch {
showToast({ message: "请手动复制支付链接", icon: "warning-o" });
}
}
async function refreshPayment(orderId: number) {
const payment = await queryOrderPayment(orderId);
if (payment.paid) {
showToast({ message: "支付成功", icon: "passed" });
} else {
showToast({ message: "支付未完成", icon: "clock-o" });
}
}
function readError(error: unknown, fallback: string) {
if (typeof error === "object" && error && "response" in error) {
const response = (error as { response?: { data?: { message?: string } } })
.response;
return response?.data?.message || fallback;
}
return fallback;
}
function money(value: unknown) {
return Math.round(Number(value || 0));
}
</script>
<template>
<main class="mobile-orders">
<!-- 顶部导航 -->
<header class="page-header">
<button class="back-btn" @click="router.back()">
<van-icon name="arrow-left" :size="20" />
</button>
<h1>我的订单</h1>
<span class="header-spacer"></span>
</header>
<!-- 状态筛选条 - Vant 滑动标签页 -->
<van-tabs
v-model:active="activeTab"
class="custom-tabs"
line-width="20px"
line-height="3px"
color="#1477ff"
title-active-color="#1477ff"
title-inactive-color="#6b7280"
:border="false"
swipeable
animated
>
<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>
加载中...
</van-loading>
<div v-else-if="displayOrders.length === 0" class="empty-state-wrap">
<van-empty description="暂无相关订单" image="search" />
</div>
<div
v-else
v-for="order in displayOrders"
:key="order.id"
class="order-card"
@click="goDetail(order.id)"
>
<!-- 卡片头:订单号 + 状态 -->
<div class="card-header">
<div class="header-left">
<span class="order-no">{{ order.order_no }}</span>
</div>
<span class="status-badge" :class="'badge-' + order.status">
{{ statusLabel(order.status) }}
</span>
</div>
<!-- 卡片体:关键信息 -->
<div class="card-body">
<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>
</div>
<div class="price-row">
<div class="price-item">
<span class="price-label">我的金额</span>
<span class="price-val">¥{{ money(order.display_amount) }}</span>
</div>
<div class="price-item">
<span class="price-label">押金金额</span>
<span class="price-val deposit">¥{{ money(order.deposit_amount) }}</span>
</div>
</div>
</div>
<!-- 卡片底:时间 + 操作 -->
<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>
</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>
</div>
</div>
</section>
<!-- 底部导航 -->
<MobileBottomNav />
</main>
</template>
<style scoped>
.mobile-orders {
min-height: 100dvh;
background: #f6f8fa;
padding-bottom: calc(64px + env(safe-area-inset-bottom));
}
/* ========== 顶部导航 ========== */
.page-header {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
height: 48px;
padding: 0 12px;
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(243, 244, 246, 0.8);
}
.page-header h1 {
flex: 1;
margin: 0;
font-size: 17px;
font-weight: 700;
text-align: center;
color: #111827;
}
.back-btn {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border: none;
background: none;
color: #374151;
cursor: pointer;
}
.header-spacer {
width: 36px;
}
/* ========== Vant Tabs 自定义样式 ========== */
.custom-tabs {
position: sticky;
top: 48px;
z-index: 99;
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(243, 244, 246, 0.6);
}
:deep(.van-tabs__nav) {
background: transparent;
padding-bottom: 4px;
}
:deep(.van-tab) {
font-size: 13px;
font-weight: 600;
}
/* ========== 订单列表 ========== */
.order-list {
padding: 14px 16px;
}
.center-loading {
display: flex;
justify-content: center;
padding: 60px 0;
}
.empty-state-wrap {
padding: 40px 0;
}
/* ========== 订单卡片 ========== */
.order-card {
background: #ffffff;
border-radius: 16px;
margin-bottom: 14px;
padding: 16px;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.02), 0 1px 4px rgba(0, 0, 0, 0.02);
transition: transform 0.1s ease, box-shadow 0.1s ease;
border: 1px solid rgba(243, 244, 246, 0.9);
cursor: pointer;
}
.order-card:active {
transform: scale(0.98);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 10px;
border-bottom: 1px dashed #f3f4f6;
}
.order-no {
font-size: 11px;
color: #9ca3af;
font-family: monospace;
}
.status-badge {
padding: 4px 8px;
border-radius: 8px;
font-size: 11px;
font-weight: 700;
line-height: 1;
}
/* 状态徽章颜色 - 现代轻量化配色 */
.badge-pending_payment { color: #ff6a00; background: rgba(255, 106, 0, 0.08); }
.badge-pending_handoff { color: #d97706; background: rgba(217, 119, 6, 0.08); }
.badge-renting { color: #1477ff; background: rgba(20, 119, 255, 0.08); }
.badge-overdue { color: #ef4444; background: rgba(239, 68, 68, 0.08); }
.badge-pending_return_confirm { color: #8b5cf6; background: rgba(139, 92, 246, 0.08); }
.badge-pending_checkout_confirm { color: #8b5cf6; background: rgba(139, 92, 246, 0.08); }
.badge-pending_checkout_accept { color: #a855f7; background: rgba(168, 85, 247, 0.08); }
.badge-checkout_disputing { color: #ef4444; background: rgba(239, 68, 68, 0.08); }
.badge-completed { color: #10b981; background: rgba(16, 185, 129, 0.08); }
.badge-cancelled { 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;
}
.order-title {
margin: 0 0 8px;
font-size: 15px;
font-weight: 700;
color: #111827;
line-height: 1.4;
}
.tag-row {
display: flex;
gap: 6px;
margin-bottom: 12px;
}
.info-tag {
font-size: 11px;
color: #6b7280;
background: #f3f4f6;
padding: 3px 8px;
border-radius: 6px;
font-weight: 500;
}
.price-row {
display: flex;
background: #f9fafb;
border-radius: 12px;
padding: 10px 14px;
justify-content: space-between;
gap: 12px;
}
.price-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.price-label {
font-size: 10px;
color: #9ca3af;
}
.price-val {
font-size: 15px;
font-weight: 800;
color: #ff5f00;
}
.price-val.deposit {
color: #374151;
}
/* ========== 卡片底部 ========== */
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid #f3f4f6;
}
.time-box {
display: flex;
align-items: center;
gap: 4px;
color: #9ca3af;
}
.clock-icon {
font-size: 13px;
}
.order-time {
font-size: 11px;
}
.detail-link {
font-size: 12px;
color: #1477ff;
font-weight: 600;
display: flex;
align-items: center;
gap: 2px;
}
.pay-btn {
height: 28px !important;
padding: 0 16px !important;
font-size: 12px !important;
font-weight: 700 !important;
background: linear-gradient(135deg, #ff8c00, #ff5f00) !important;
border: none !important;
box-shadow: 0 4px 10px rgba(255, 95, 0, 0.2) !important;
}
</style>