547 lines
13 KiB
Vue
547 lines
13 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref } from "vue";
|
||
import { onShow } from "@dcloudio/uni-app";
|
||
import { appApi, type OrderListItem } from "../../api/app";
|
||
import { isLoggedIn, redirectToLogin } from "../../utils/auth";
|
||
import { showErrorToast } from "../../utils/feedback";
|
||
import { getPrivacyMode, maskOrderNo } from "../../utils/privacy";
|
||
|
||
const orders = ref<OrderListItem[]>([]);
|
||
const privacyMode = ref(getPrivacyMode());
|
||
const orderHeroBackground = ref("");
|
||
const defaultOrderHeroBackground = "/static/order/order-reference.jpg";
|
||
let orderRefreshTicket = 0;
|
||
|
||
const orderHeroStyle = computed(() => ({
|
||
backgroundImage: `url("${orderHeroBackground.value || defaultOrderHeroBackground}")`,
|
||
}));
|
||
|
||
const heroStats = computed(() => {
|
||
const stats = {
|
||
pending: 0,
|
||
processing: 0,
|
||
completed: 0,
|
||
};
|
||
|
||
for (const item of orders.value) {
|
||
if (["report_published", "completed"].includes(item.order_status)) {
|
||
stats.completed += 1;
|
||
continue;
|
||
}
|
||
if (["pending_payment", "pending_supplement"].includes(item.order_status)) {
|
||
stats.pending += 1;
|
||
continue;
|
||
}
|
||
stats.processing += 1;
|
||
}
|
||
|
||
return stats;
|
||
});
|
||
|
||
const emptyState = computed(() => ({
|
||
title: "还没有鉴定订单",
|
||
desc: "发起第一笔鉴定后,订单进度、补资料提醒和报告状态都会集中展示在这里。",
|
||
}));
|
||
|
||
function openOrder(id: number) {
|
||
uni.navigateTo({ url: `/pages/order/detail?id=${id}` });
|
||
}
|
||
|
||
function goStartAppraisal() {
|
||
uni.navigateTo({ url: "/pages/appraisal/service?provider=anxinyan&start=1" });
|
||
}
|
||
|
||
function goHome() {
|
||
uni.switchTab({ url: "/pages/home/index" });
|
||
}
|
||
|
||
function goHelp() {
|
||
uni.navigateTo({ url: "/pages/help/index" });
|
||
}
|
||
|
||
async function fetchPageVisuals() {
|
||
try {
|
||
const data = await appApi.getPageVisuals();
|
||
orderHeroBackground.value = data.order_background_image_url || "";
|
||
} catch (error) {
|
||
console.warn("order page visuals fallback", error);
|
||
}
|
||
}
|
||
|
||
function shouldSyncPaymentStatus(item: OrderListItem) {
|
||
return item.order_status === "pending_payment" && item.payment_status !== "paid";
|
||
}
|
||
|
||
async function syncPendingPaymentOrders(list: OrderListItem[]) {
|
||
const pendingOrders = list.filter(shouldSyncPaymentStatus);
|
||
if (!pendingOrders.length) {
|
||
return false;
|
||
}
|
||
|
||
const results = await Promise.all(
|
||
pendingOrders.map(async (item) => {
|
||
try {
|
||
const data = await appApi.getOrderPaymentStatus(item.order_id);
|
||
return data.order_status !== item.order_status
|
||
|| data.payment_status !== item.payment_status
|
||
|| data.display_status !== item.display_status;
|
||
} catch (error) {
|
||
console.warn("order payment status sync skipped", error);
|
||
return false;
|
||
}
|
||
}),
|
||
);
|
||
|
||
return results.some(Boolean);
|
||
}
|
||
|
||
async function refreshOrdersWithPaymentSync() {
|
||
const ticket = ++orderRefreshTicket;
|
||
const data = await appApi.getOrders();
|
||
if (ticket !== orderRefreshTicket) return;
|
||
|
||
orders.value = data.list;
|
||
const needsRefresh = await syncPendingPaymentOrders(data.list);
|
||
if (!needsRefresh || ticket !== orderRefreshTicket) return;
|
||
|
||
const refreshed = await appApi.getOrders();
|
||
if (ticket === orderRefreshTicket) {
|
||
orders.value = refreshed.list;
|
||
}
|
||
}
|
||
|
||
onShow(async () => {
|
||
privacyMode.value = getPrivacyMode();
|
||
void fetchPageVisuals();
|
||
if (!isLoggedIn()) {
|
||
orderRefreshTicket += 1;
|
||
orders.value = [];
|
||
redirectToLogin("/pages/order/index");
|
||
return;
|
||
}
|
||
try {
|
||
await refreshOrdersWithPaymentSync();
|
||
} catch (error) {
|
||
orders.value = [];
|
||
showErrorToast(error, "订单加载失败");
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<view class="order-page">
|
||
<view :class="['order-hero', orders.length === 0 ? 'order-hero--empty' : 'order-hero--list']">
|
||
<view class="order-hero__bg" :style="orderHeroStyle"></view>
|
||
<view class="order-nav">
|
||
<view class="order-nav__home" @click="goHome">
|
||
<view class="order-nav__home-roof"></view>
|
||
<view class="order-nav__home-body"></view>
|
||
</view>
|
||
<view class="order-nav__title">订单中心</view>
|
||
<view class="order-nav__capsule">
|
||
<text class="order-nav__dots">•••</text>
|
||
<view class="order-nav__divider"></view>
|
||
<view class="order-nav__circle"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="order-hero__content">
|
||
<view class="order-hero__title">订单中心</view>
|
||
<view class="order-hero__desc">查看每一笔鉴定从下单、寄送、补资料到出报告的完整进度。</view>
|
||
|
||
<view class="order-stat-grid">
|
||
<view class="order-stat-card">
|
||
<view class="order-stat-card__value">{{ heroStats.pending }}</view>
|
||
<view class="order-stat-card__label">待处理</view>
|
||
</view>
|
||
<view class="order-stat-card">
|
||
<view class="order-stat-card__value">{{ heroStats.processing }}</view>
|
||
<view class="order-stat-card__label">进行中</view>
|
||
</view>
|
||
<view class="order-stat-card">
|
||
<view class="order-stat-card__value">{{ heroStats.completed }}</view>
|
||
<view class="order-stat-card__label">已完成</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="orders.length === 0" class="order-empty-state">
|
||
<view class="order-empty-state__title">{{ emptyState.title }}</view>
|
||
<view class="order-empty-state__desc">{{ emptyState.desc }}</view>
|
||
<view class="order-empty-state__actions">
|
||
<view class="order-empty-state__button order-empty-state__button--primary" @click="goStartAppraisal">发起鉴定</view>
|
||
<view class="order-empty-state__button order-empty-state__button--secondary" @click="goHelp">查看帮助</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-else class="order-list">
|
||
<view
|
||
v-for="item in orders"
|
||
:key="item.order_id"
|
||
class="order-card"
|
||
@click="openOrder(item.order_id)"
|
||
>
|
||
<view class="order-card__top">
|
||
<view>
|
||
<view class="order-card__title">{{ item.product_name }}</view>
|
||
<view class="order-card__no">订单号:{{ maskOrderNo(item.order_no, privacyMode) }}</view>
|
||
</view>
|
||
<text
|
||
class="order-card__status"
|
||
:class="['pending_payment', 'pending_supplement'].includes(item.order_status) ? 'order-card__status--warning' : ['report_published', 'completed'].includes(item.order_status) ? 'order-card__status--success' : 'order-card__status--info'"
|
||
>
|
||
{{ item.display_status }}
|
||
</text>
|
||
</view>
|
||
<view class="order-card__desc">{{ item.status_desc }}</view>
|
||
<view v-if="item.order_status === 'report_published'" class="order-card__desc">平台待安排寄回,请先确认寄回地址。</view>
|
||
<view v-if="item.order_status === 'completed' && item.display_status === '物品已寄回'" class="order-card__desc">平台已回寄商品,请留意签收物流。</view>
|
||
<view class="order-card__footer">
|
||
<view class="order-card__provider">
|
||
{{ item.service_provider === "zhongjian" ? "中检鉴定" : "安心验鉴定" }}
|
||
<text v-if="item.price_package_name"> / {{ item.price_package_name }}</text>
|
||
</view>
|
||
<view v-if="item.primary_action" class="order-card__action">{{ item.primary_action }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.order-page {
|
||
width: 100vw;
|
||
min-height: 100vh;
|
||
overflow-x: hidden;
|
||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||
background: #f2f2f4;
|
||
color: #2d2d2f;
|
||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||
}
|
||
|
||
.order-page,
|
||
.order-page view,
|
||
.order-page text {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.order-hero {
|
||
position: relative;
|
||
overflow: hidden;
|
||
width: 100vw;
|
||
padding: 88rpx 32rpx 0;
|
||
}
|
||
|
||
.order-hero--empty {
|
||
height: 900rpx;
|
||
}
|
||
|
||
.order-hero--list {
|
||
height: 790rpx;
|
||
}
|
||
|
||
.order-hero__bg {
|
||
position: absolute;
|
||
inset: -18rpx;
|
||
background-size: 100vw auto;
|
||
background-position: top center;
|
||
background-repeat: no-repeat;
|
||
filter: blur(8rpx) saturate(1.08);
|
||
transform: scale(1.04);
|
||
opacity: 0.82;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.order-hero::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background:
|
||
linear-gradient(180deg, rgba(245, 239, 232, 0.08) 0%, rgba(243, 243, 245, 0.22) 42%, #f2f2f4 88%),
|
||
linear-gradient(0deg, rgba(242, 242, 244, 0.36), rgba(255, 255, 255, 0.04));
|
||
pointer-events: none;
|
||
}
|
||
|
||
.order-nav,
|
||
.order-hero__content {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.order-nav {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 76rpx;
|
||
}
|
||
|
||
.order-nav__title {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
color: #272729;
|
||
font-size: 34rpx;
|
||
line-height: 1;
|
||
font-weight: 700;
|
||
transform: translate(-50%, -50%);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.order-nav__home {
|
||
position: relative;
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
}
|
||
|
||
.order-nav__home-roof {
|
||
position: absolute;
|
||
left: 10rpx;
|
||
top: 7rpx;
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
border-top: 4rpx solid #2a2a2c;
|
||
border-left: 4rpx solid #2a2a2c;
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
.order-nav__home-body {
|
||
position: absolute;
|
||
left: 12rpx;
|
||
bottom: 8rpx;
|
||
width: 34rpx;
|
||
height: 30rpx;
|
||
border: 4rpx solid #2a2a2c;
|
||
border-top: 0;
|
||
border-radius: 0 0 8rpx 8rpx;
|
||
background: linear-gradient(135deg, transparent 0 36%, #f0c000 36% 100%);
|
||
}
|
||
|
||
.order-nav__capsule {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-around;
|
||
width: 174rpx;
|
||
height: 64rpx;
|
||
padding: 0 20rpx;
|
||
border-radius: 34rpx;
|
||
background: rgba(255, 255, 255, 0.72);
|
||
box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.05);
|
||
backdrop-filter: blur(12rpx);
|
||
}
|
||
|
||
.order-nav__dots {
|
||
color: #050505;
|
||
font-size: 36rpx;
|
||
line-height: 1;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.order-nav__divider {
|
||
width: 1rpx;
|
||
height: 34rpx;
|
||
background: rgba(20, 20, 20, 0.68);
|
||
}
|
||
|
||
.order-nav__circle {
|
||
width: 38rpx;
|
||
height: 38rpx;
|
||
border: 7rpx solid #070707;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.order-hero__content {
|
||
margin-top: 214rpx;
|
||
}
|
||
|
||
.order-hero__title {
|
||
color: #262628;
|
||
font-size: 70rpx;
|
||
line-height: 1.08;
|
||
font-weight: 800;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.order-hero__desc {
|
||
width: 650rpx;
|
||
max-width: 100%;
|
||
margin-top: 28rpx;
|
||
color: #666;
|
||
font-size: 29rpx;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.order-stat-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 42rpx;
|
||
margin: 26rpx 22rpx 0;
|
||
}
|
||
|
||
.order-stat-card {
|
||
height: 176rpx;
|
||
border-radius: 10rpx;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
text-align: center;
|
||
box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.02);
|
||
}
|
||
|
||
.order-stat-card__value {
|
||
margin-top: 35rpx;
|
||
color: #2b2b2d;
|
||
font-size: 68rpx;
|
||
line-height: 1;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.order-stat-card__label {
|
||
margin-top: 18rpx;
|
||
color: #686868;
|
||
font-size: 25rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.order-empty-state {
|
||
margin: 190rpx 32rpx 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.order-empty-state__title {
|
||
color: #202022;
|
||
font-size: 40rpx;
|
||
line-height: 1.2;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.order-empty-state__desc {
|
||
width: 560rpx;
|
||
max-width: 100%;
|
||
margin: 28rpx auto 0;
|
||
color: #818181;
|
||
font-size: 29rpx;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.order-empty-state__actions {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 38rpx;
|
||
margin-top: 50rpx;
|
||
}
|
||
|
||
.order-empty-state__button {
|
||
height: 68rpx;
|
||
border-radius: 36rpx;
|
||
font-size: 25rpx;
|
||
font-weight: 700;
|
||
line-height: 68rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.order-empty-state__button--primary {
|
||
background: #edbd00;
|
||
color: #fff;
|
||
}
|
||
|
||
.order-empty-state__button--secondary {
|
||
background: #fff;
|
||
color: #29292b;
|
||
}
|
||
|
||
.order-list {
|
||
display: grid;
|
||
gap: 22rpx;
|
||
margin: -22rpx 32rpx 0;
|
||
padding-bottom: 36rpx;
|
||
}
|
||
|
||
.order-card {
|
||
padding: 28rpx;
|
||
border-radius: 16rpx;
|
||
background: #fff;
|
||
box-shadow: 0 12rpx 26rpx rgba(0, 0, 0, 0.035);
|
||
}
|
||
|
||
.order-card__top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.order-card__title {
|
||
color: #242426;
|
||
font-size: 31rpx;
|
||
line-height: 1.25;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.order-card__no,
|
||
.order-card__desc {
|
||
margin-top: 12rpx;
|
||
color: #777;
|
||
font-size: 24rpx;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.order-card__status {
|
||
flex-shrink: 0;
|
||
height: 42rpx;
|
||
padding: 0 16rpx;
|
||
border-radius: 22rpx;
|
||
font-size: 22rpx;
|
||
line-height: 42rpx;
|
||
}
|
||
|
||
.order-card__status--warning {
|
||
background: #fff4d9;
|
||
color: #a97700;
|
||
}
|
||
|
||
.order-card__status--success {
|
||
background: #eaf7ef;
|
||
color: #2d7652;
|
||
}
|
||
|
||
.order-card__status--info {
|
||
background: #edf4ff;
|
||
color: #28669f;
|
||
}
|
||
|
||
.order-card__footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 22rpx;
|
||
}
|
||
|
||
.order-card__provider {
|
||
color: #9a9a9a;
|
||
font-size: 23rpx;
|
||
}
|
||
|
||
.order-card__action {
|
||
min-width: 126rpx;
|
||
height: 48rpx;
|
||
padding: 0 20rpx;
|
||
border-radius: 25rpx;
|
||
background: #f1bd00;
|
||
color: #fff;
|
||
font-size: 23rpx;
|
||
font-weight: 700;
|
||
line-height: 48rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
@media (max-width: 360px) {
|
||
.order-stat-grid {
|
||
gap: 22rpx;
|
||
margin-left: 12rpx;
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.order-empty-state__actions {
|
||
gap: 22rpx;
|
||
}
|
||
}
|
||
</style>
|