Files
anxinyan/user-app/src/pages/order/index.vue

547 lines
13 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 { 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>