Files
hfb_sys/frontend/src/views/mobile/MobileOrdersView.vue
T

442 lines
9.1 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 { showToast } from "vant";
import { fetchOrders, type Order } from "@/api/orders";
import { formatDateMinute } from "@/utils/time";
const router = useRouter();
const route = useRoute();
const loading = ref(false);
const orders = ref<Order[]>([]);
const activeTab = ref("all");
/** 判断当前底部导航是否激活 */
function isNavActive(path: string) {
if (path === "/m") return route.path === "/m";
return route.path.startsWith(path);
}
onMounted(loadOrders);
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_handoff", label: "待交接" },
{ key: "renting", label: "使用中" },
{ key: "pending_return_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 statusColor(status: string) {
const map: Record<string, string> = {
pending_handoff: "#ff9800",
renting: "#1477ff",
overdue: "#e91e63",
pending_return_confirm: "#e91e63",
completed: "#4caf50",
cancelled: "#999",
disputing: "#f44336",
abnormal: "#f44336",
closed: "#666",
};
return map[status] || "#999";
}
function statusLabel(status: string) {
const map: Record<string, string> = {
pending_handoff: "待交接",
renting: "使用中",
overdue: "已逾期",
pending_return_confirm: "待归还",
completed: "已完成",
cancelled: "已取消",
disputing: "申诉中",
abnormal: "异常",
closed: "已关闭",
};
return map[status] || status;
}
function goDetail(id: number) {
router.push(`/m/orders/${id}`);
}
</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>
<!-- 状态筛选条 -->
<div class="status-tabs">
<button
v-for="tab in statusTabs"
:key="tab.key"
class="tab-btn"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</button>
</div>
<!-- 订单列表 -->
<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">
<van-icon name="orders-o" :size="48" color="#ccc" />
<p>暂无订单</p>
</div>
<div
v-for="order in displayOrders"
:key="order.id"
class="order-card"
@click="goDetail(order.id)"
>
<!-- 卡片头:订单号 + 状态 -->
<div class="card-header">
<span class="order-no">{{ order.order_no }}</span>
<span
class="status-badge"
:style="{ background: statusColor(order.status) }"
>
{{ statusLabel(order.status) }}
</span>
</div>
<!-- 卡片体:关键信息 -->
<div class="card-body">
<h3 class="order-title">{{ order.title }}</h3>
<div class="info-grid">
<div class="info-item">
<span class="info-label">订单金额</span>
<span class="info-value price">¥{{ order.rent_amount }}</span>
</div>
<div class="info-item">
<span class="info-label">押金</span>
<span class="info-value">¥{{ order.deposit_amount }}</span>
</div>
<div class="info-item">
<span class="info-label">区服</span>
<span class="info-value">{{ order.server_region }}</span>
</div>
</div>
</div>
<!-- 卡片底:时间 + 操作 -->
<div class="card-footer">
<span class="order-time">{{ formatDateMinute(order.created_at) }}</span>
<span class="detail-link">查看详情 ›</span>
</div>
</div>
</section>
<!-- 底部导航 -->
<nav class="bottom-nav">
<RouterLink
to="/m"
class="nav-item"
:class="{ active: isNavActive('/m') }"
>
<van-icon name="home-o" :size="22" />
<span>首页</span>
</RouterLink>
<RouterLink
to="/m/messages"
class="nav-item"
:class="{ active: isNavActive('/m/messages') }"
>
<van-icon name="chat-o" :size="22" />
<span>消息</span>
</RouterLink>
<RouterLink to="/m/seller/listings/create" class="nav-item nav-publish">
<div class="publish-pill">+</div>
<span>发布</span>
</RouterLink>
<RouterLink
to="/m/orders"
class="nav-item"
:class="{ active: isNavActive('/m/orders') }"
>
<van-icon name="orders-o" :size="22" />
<span>订单</span>
</RouterLink>
<RouterLink
to="/m/profile"
class="nav-item"
:class="{ active: isNavActive('/m/profile') }"
>
<van-icon name="manager-o" :size="22" />
<span>我的</span>
</RouterLink>
</nav>
</main>
</template>
<style scoped>
.mobile-orders {
min-height: 100dvh;
background: #f5f7fa;
padding-bottom: 56px;
}
/* ========== 顶部导航 ========== */
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
height: 48px;
padding: 0 12px;
background: #fff;
border-bottom: 1px solid #eee;
}
.page-header h1 {
flex: 1;
margin: 0;
font-size: 17px;
font-weight: 700;
text-align: center;
}
.back-btn {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border: none;
background: none;
color: #333;
cursor: pointer;
}
.header-spacer {
width: 36px;
}
/* ========== 状态筛选条 ========== */
.status-tabs {
display: flex;
gap: 6px;
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.tab-btn {
flex-shrink: 0;
padding: 6px 14px;
border-radius: 16px;
border: 1px solid #ddd;
background: #fff;
color: #666;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.tab-btn.active {
background: #1477ff;
color: #fff;
border-color: #1477ff;
}
/* ========== 订单列表 ========== */
.order-list {
padding: 12px 16px;
}
.center-loading {
display: flex;
justify-content: center;
padding: 60px 0;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 80px 0;
color: #999;
font-size: 14px;
}
/* ========== 订单卡片 ========== */
.order-card {
background: #fff;
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
overflow: hidden;
cursor: pointer;
transition: box-shadow 0.15s;
}
.order-card:active {
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px 0;
}
.order-no {
font-size: 11px;
color: #999;
font-family: monospace;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 8px;
color: #fff;
font-size: 11px;
font-weight: 600;
}
.card-body {
padding: 10px 14px;
}
.order-title {
margin: 0 0 8px;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
line-height: 1.3;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 16px;
}
.info-item {
display: flex;
justify-content: space-between;
font-size: 12px;
}
.info-label {
color: #999;
}
.info-value {
color: #333;
font-weight: 500;
}
.info-value.price {
color: #ff5f00;
font-weight: 700;
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 14px 10px;
border-top: 1px solid #f5f5f5;
}
.order-time {
font-size: 11px;
color: #bbb;
}
.detail-link {
font-size: 12px;
color: #1477ff;
font-weight: 600;
}
/* ========== 底部导航 ========== */
.bottom-nav {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
display: flex;
height: 50px;
background: #fff;
border-top: 1px solid #eee;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
color: #999;
font-size: 10px;
text-decoration: none;
}
.nav-item.active {
color: #1477ff;
}
.nav-item span {
font-weight: 600;
}
.publish-pill {
width: 36px;
height: 26px;
display: grid;
place-items: center;
border-radius: 13px;
background: #ff6a00;
color: #fff;
font-size: 18px;
font-weight: 900;
}
.nav-publish span {
color: #ff6a00;
}
</style>