first
This commit is contained in:
1076
user-app/src/pages/order/detail.vue
Normal file
1076
user-app/src/pages/order/detail.vue
Normal file
File diff suppressed because it is too large
Load Diff
500
user-app/src/pages/order/index.vue
Normal file
500
user-app/src/pages/order/index.vue
Normal file
@@ -0,0 +1,500 @@
|
||||
<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";
|
||||
|
||||
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 (item.order_status === "pending_supplement") {
|
||||
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" });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
onShow(async () => {
|
||||
privacyMode.value = getPrivacyMode();
|
||||
void fetchPageVisuals();
|
||||
if (!isLoggedIn()) {
|
||||
orders.value = [];
|
||||
redirectToLogin("/pages/order/index");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await appApi.getOrders();
|
||||
orders.value = data.list;
|
||||
} 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="item.order_status === 'pending_supplement' ? '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" ? "中检鉴定" : "安心验鉴定" }}</view>
|
||||
<view 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>
|
||||
627
user-app/src/pages/order/shipping.vue
Normal file
627
user-app/src/pages/order/shipping.vue
Normal file
@@ -0,0 +1,627 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { appApi, type ShippingDetailData } from "../../api/app";
|
||||
import { shippingDetailFallback } from "../../mocks/app";
|
||||
import { resolveErrorMessage, showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
|
||||
import { getPrivacyMode, maskOrderNo } from "../../utils/privacy";
|
||||
|
||||
const detail = ref<ShippingDetailData>(shippingDetailFallback);
|
||||
const orderId = ref(0);
|
||||
const saving = ref(false);
|
||||
const expressCompany = ref("");
|
||||
const trackingNo = ref("");
|
||||
const privacyMode = ref(getPrivacyMode());
|
||||
const selectedWarehouseId = ref(0);
|
||||
const warehouseSheetVisible = ref(false);
|
||||
const loading = ref(false);
|
||||
const pageReady = ref(false);
|
||||
const loadError = ref("");
|
||||
|
||||
const submitted = computed(() => detail.value.logistics_info.is_submitted);
|
||||
const canEditTracking = computed(() => !submitted.value && detail.value.can_submit_tracking);
|
||||
const hasWarehouseChoices = computed(
|
||||
() => detail.value.shipping_options.can_select_warehouse && detail.value.shipping_options.list.length > 1,
|
||||
);
|
||||
const warehouseOptions = computed(() =>
|
||||
[...detail.value.shipping_options.list].sort((left, right) => {
|
||||
const leftSelected = left.id === selectedWarehouseId.value ? 1 : 0;
|
||||
const rightSelected = right.id === selectedWarehouseId.value ? 1 : 0;
|
||||
if (leftSelected !== rightSelected) {
|
||||
return rightSelected - leftSelected;
|
||||
}
|
||||
|
||||
const leftRecommended = left.is_recommended ? 1 : 0;
|
||||
const rightRecommended = right.is_recommended ? 1 : 0;
|
||||
if (leftRecommended !== rightRecommended) {
|
||||
return rightRecommended - leftRecommended;
|
||||
}
|
||||
|
||||
return left.sort_order - right.sort_order;
|
||||
}),
|
||||
);
|
||||
const selectedWarehouse = computed(() => {
|
||||
const matched = warehouseOptions.value.find((item) => item.id === selectedWarehouseId.value);
|
||||
return matched || null;
|
||||
});
|
||||
const alternativeWarehouses = computed(() => warehouseOptions.value.filter((item) => item.id !== selectedWarehouseId.value));
|
||||
const activeShippingAddress = computed(() => selectedWarehouse.value || detail.value.shipping_address);
|
||||
const selectionChanged = computed(() => {
|
||||
const initialWarehouseId = detail.value.shipping_options.current_warehouse_id || detail.value.shipping_address.warehouse_id || 0;
|
||||
return Boolean(initialWarehouseId && selectedWarehouseId.value && initialWarehouseId !== selectedWarehouseId.value);
|
||||
});
|
||||
const activeWarehouseServiceText = computed(
|
||||
() => selectedWarehouse.value?.service_provider_text || detail.value.order_info.service_provider || "当前鉴定服务",
|
||||
);
|
||||
const activeWarehouseHint = computed(() => {
|
||||
if (!hasWarehouseChoices.value) {
|
||||
return "该订单当前仅匹配这一家检测中心,可直接按下方收件信息寄送。";
|
||||
}
|
||||
if (selectionChanged.value) {
|
||||
return "你已切换检测中心,提交运单后将按当前选择的仓库收货。";
|
||||
}
|
||||
return "寄送前可重新选择检测中心,避免商品寄错仓库。";
|
||||
});
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
function openWarehouseSheet() {
|
||||
if (!hasWarehouseChoices.value) return;
|
||||
warehouseSheetVisible.value = true;
|
||||
}
|
||||
|
||||
function closeWarehouseSheet() {
|
||||
warehouseSheetVisible.value = false;
|
||||
}
|
||||
|
||||
function selectWarehouse(warehouseId: number) {
|
||||
const target = warehouseOptions.value.find((item) => item.id === warehouseId);
|
||||
if (!target) return;
|
||||
|
||||
const changed = selectedWarehouseId.value !== warehouseId;
|
||||
selectedWarehouseId.value = warehouseId;
|
||||
closeWarehouseSheet();
|
||||
|
||||
if (changed) {
|
||||
showInfoToast(`已切换至${target.warehouse_name}`);
|
||||
}
|
||||
}
|
||||
|
||||
function copyAddress() {
|
||||
const current = activeShippingAddress.value;
|
||||
const address = `${current.receiver_name} ${current.receiver_mobile} ${current.province}${current.city}${current.district}${current.detail_address}`;
|
||||
uni.setClipboardData({
|
||||
data: address,
|
||||
success: () => showInfoToast("收件信息已复制"),
|
||||
});
|
||||
}
|
||||
|
||||
function useCompany(name: string) {
|
||||
expressCompany.value = name;
|
||||
}
|
||||
|
||||
async function fetchDetail() {
|
||||
if (!orderId.value) return;
|
||||
loading.value = true;
|
||||
privacyMode.value = getPrivacyMode();
|
||||
if (!pageReady.value) {
|
||||
loadError.value = "";
|
||||
}
|
||||
try {
|
||||
detail.value = await appApi.getOrderShippingDetail(orderId.value);
|
||||
expressCompany.value = detail.value.logistics_info.express_company || expressCompany.value;
|
||||
trackingNo.value = detail.value.logistics_info.tracking_no || trackingNo.value;
|
||||
selectedWarehouseId.value = detail.value.shipping_options.current_warehouse_id || detail.value.shipping_address.warehouse_id || 0;
|
||||
pageReady.value = true;
|
||||
} catch (error) {
|
||||
console.warn("shipping detail fallback", error);
|
||||
if (!pageReady.value) {
|
||||
loadError.value = resolveErrorMessage(error, "寄送信息加载失败,请稍后重试。");
|
||||
} else {
|
||||
showErrorToast(error, "寄送信息刷新失败");
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitTracking() {
|
||||
if (submitted.value) {
|
||||
showInfoToast("运单已提交,如需更正请联系客服处理");
|
||||
return;
|
||||
}
|
||||
if (!detail.value.can_submit_tracking) {
|
||||
showInfoToast("当前订单暂不需要提交运单");
|
||||
return;
|
||||
}
|
||||
if (!expressCompany.value.trim() || !trackingNo.value.trim()) {
|
||||
showInfoToast("请填写快递公司和运单号");
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
await withLoading("正在提交运单", async () =>
|
||||
appApi.saveOrderShipping({
|
||||
order_id: detail.value.order_info.order_id,
|
||||
express_company: expressCompany.value.trim(),
|
||||
tracking_no: trackingNo.value.trim(),
|
||||
warehouse_id: selectedWarehouseId.value || undefined,
|
||||
}),
|
||||
);
|
||||
showInfoToast("运单已提交");
|
||||
await fetchDetail();
|
||||
} catch (error) {
|
||||
showErrorToast(error, "运单提交失败");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
orderId.value = Number(options?.order_id || 0);
|
||||
if (!orderId.value) {
|
||||
loadError.value = "缺少订单编号,无法查看寄送信息。";
|
||||
}
|
||||
});
|
||||
|
||||
onShow(fetchDetail);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="app-page app-page--tight">
|
||||
<view v-if="!pageReady && loading" class="section notice-card">
|
||||
<view class="notice-card__title">正在加载寄送信息</view>
|
||||
<view class="notice-card__desc">请稍候,我们正在同步收货仓库、物流状态与可选检测中心。</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!pageReady && loadError" class="section notice-card">
|
||||
<view class="notice-card__title">寄送信息加载失败</view>
|
||||
<view class="notice-card__desc">{{ loadError }}</view>
|
||||
<view style="margin-top: 20rpx">
|
||||
<text class="btn btn--ghost" @click="fetchDetail">重新加载</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="section-card">
|
||||
<view class="tag" :class="submitted ? 'tag--info' : 'tag--warning'">{{ detail.order_info.display_status }}</view>
|
||||
<view class="page-title" style="margin-top: 20rpx; font-size: var(--font-size-2xl); color: var(--color-heading);">
|
||||
查看寄送与提交运单
|
||||
</view>
|
||||
<view class="section__desc">请先寄出商品,再回到本页填写快递公司和运单号,我们会同步更新鉴定进度。</view>
|
||||
<view class="certificate-header__meta">
|
||||
<text class="certificate-meta-chip">订单号 {{ maskOrderNo(detail.order_info.order_no, privacyMode) }}</text>
|
||||
<text class="certificate-meta-chip">鉴定单号 {{ maskOrderNo(detail.order_info.appraisal_no, privacyMode) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section section-card section-card--soft">
|
||||
<view class="section__title">收件信息</view>
|
||||
<view v-if="detail.shipping_options.list.length" class="warehouse-picker">
|
||||
<view class="warehouse-current">
|
||||
<view class="warehouse-current__header">
|
||||
<view>
|
||||
<view class="warehouse-current__eyebrow">当前检测中心</view>
|
||||
<view class="warehouse-current__name">{{ activeShippingAddress.warehouse_name || "检测中心待确认" }}</view>
|
||||
</view>
|
||||
<text class="warehouse-current__badge">{{ selectionChanged ? "已切换待生效" : "当前选择" }}</text>
|
||||
</view>
|
||||
|
||||
<view class="warehouse-current__meta-row">
|
||||
<text class="warehouse-current__meta-pill">{{ activeWarehouseServiceText }}</text>
|
||||
<text v-if="hasWarehouseChoices" class="warehouse-current__meta-pill">{{ alternativeWarehouses.length }} 个可切换</text>
|
||||
</view>
|
||||
<view class="warehouse-current__notice">{{ activeWarehouseHint }}</view>
|
||||
<view v-if="hasWarehouseChoices" class="warehouse-current__action" @click="openWarehouseSheet">
|
||||
<text>更换检测中心</text>
|
||||
<text class="warehouse-current__action-arrow">去选择</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="shipping-detail__header">
|
||||
<view class="shipping-detail__title">详细收件资料</view>
|
||||
<view class="shipping-detail__desc">请按以下联系人、地址与签收时间完成寄送。</view>
|
||||
</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">联系人</text>
|
||||
<text class="report-meta__value">{{ activeShippingAddress.receiver_name }}</text>
|
||||
</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">联系电话</text>
|
||||
<text class="report-meta__value">{{ activeShippingAddress.receiver_mobile }}</text>
|
||||
</view>
|
||||
<view class="report-meta__row report-meta__row--stacked">
|
||||
<text class="report-meta__label">详细地址</text>
|
||||
<text class="report-meta__value">{{ activeShippingAddress.province }}{{ activeShippingAddress.city }}{{ activeShippingAddress.district }}{{ activeShippingAddress.detail_address }}</text>
|
||||
</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">签收时间</text>
|
||||
<text class="report-meta__value">{{ activeShippingAddress.service_time }}</text>
|
||||
</view>
|
||||
<view class="section__desc">{{ activeShippingAddress.notice }}</view>
|
||||
<view style="margin-top: 20rpx">
|
||||
<text class="btn btn--ghost" @click="copyAddress">复制收件信息</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section grid-2">
|
||||
<view class="metric-card">
|
||||
<view class="metric-card__value">{{ detail.order_info.product_name || "待确认商品" }}</view>
|
||||
<view class="metric-card__label">当前寄送商品,请确保与订单信息一致</view>
|
||||
</view>
|
||||
<view class="metric-card">
|
||||
<view class="metric-card__value">{{ submitted ? detail.logistics_info.tracking_status_text : "待提交" }}</view>
|
||||
<view class="metric-card__label">寄送状态,提交运单后我们会继续同步节点</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section section-card">
|
||||
<view class="section__title">寄送提醒</view>
|
||||
<view v-for="(item, index) in detail.shipping_notice.tips" :key="item" class="report-meta__row report-meta__row--stacked">
|
||||
<text class="report-meta__label">{{ index + 1 }}. {{ item }}</text>
|
||||
</view>
|
||||
<view class="chip-list">
|
||||
<view v-for="company in detail.shipping_notice.express_recommendations" :key="company" class="choice-chip" @click="useCompany(company)">
|
||||
{{ company }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view :class="['section', 'form-panel', submitted ? 'form-panel--readonly' : '']">
|
||||
<view class="form-panel__title">{{ submitted ? "运单信息" : "填写运单信息" }}</view>
|
||||
<view v-if="submitted" class="form-panel__readonly-tip">
|
||||
运单已提交,当前页面仅展示已登记信息,避免误触修改。
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<view class="form-group__label">快递公司</view>
|
||||
<view :class="['field-box', submitted ? 'field-box--readonly' : '']">
|
||||
<input v-model="expressCompany" class="field-input" maxlength="30" placeholder="例如:顺丰速运" :disabled="submitted" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<view class="form-group__label">运单号</view>
|
||||
<view :class="['field-box', submitted ? 'field-box--readonly' : '']">
|
||||
<input v-model="trackingNo" class="field-input" maxlength="40" placeholder="请输入快递单号" :disabled="submitted" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-group__hint">
|
||||
{{ submitted ? "如物流信息存在异常,请联系平台客服协助处理。" : "提交后将进入待签收跟踪状态,请确认信息无误后再提交。" }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="submitted" class="section timeline-panel">
|
||||
<view class="section__title">寄送轨迹</view>
|
||||
<view class="section__desc">{{ detail.logistics_info.latest_desc }}</view>
|
||||
<view class="timeline" style="margin-top: 24rpx">
|
||||
<view
|
||||
v-for="item in detail.logistics_nodes"
|
||||
:key="`${item.node_time}-${item.node_desc}`"
|
||||
class="timeline__item timeline__item--active"
|
||||
>
|
||||
<view class="timeline-card__title">{{ item.node_desc }}</view>
|
||||
<view class="timeline__time">{{ item.node_time }}</view>
|
||||
<view v-if="item.node_location" class="timeline-card__desc">{{ item.node_location }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="fixed-action-bar">
|
||||
<view :class="['btn', 'btn--primary', submitted || saving || !canEditTracking ? 'btn--disabled' : '']" @click="submitTracking">
|
||||
{{ saving ? "提交中..." : submitted ? "运单已提交" : "提交运单" }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="warehouseSheetVisible" class="warehouse-sheet">
|
||||
<view class="warehouse-sheet__mask" @click="closeWarehouseSheet"></view>
|
||||
<view class="warehouse-sheet__panel">
|
||||
<view class="warehouse-sheet__header">
|
||||
<view>
|
||||
<view class="warehouse-sheet__title">选择检测中心</view>
|
||||
<view class="warehouse-sheet__desc">切换后,下方收件资料会同步更新,提交运单时自动保存当前选择。</view>
|
||||
</view>
|
||||
<view class="warehouse-sheet__close" @click="closeWarehouseSheet">关闭</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="warehouse-sheet__list">
|
||||
<view
|
||||
v-for="item in warehouseOptions"
|
||||
:key="item.id"
|
||||
:class="['warehouse-sheet__option', selectedWarehouseId === item.id ? 'warehouse-sheet__option--selected' : '']"
|
||||
@click="selectWarehouse(item.id)"
|
||||
>
|
||||
<view class="warehouse-sheet__top">
|
||||
<view class="warehouse-sheet__head">
|
||||
<view class="warehouse-sheet__name">{{ item.warehouse_name }}</view>
|
||||
<view class="warehouse-sheet__badges">
|
||||
<text v-if="item.is_recommended" class="warehouse-sheet__badge warehouse-sheet__badge--accent">推荐</text>
|
||||
<text v-if="item.is_default" class="warehouse-sheet__badge">默认</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="warehouse-sheet__state">{{ selectedWarehouseId === item.id ? "已选中心" : "选择" }}</text>
|
||||
</view>
|
||||
<view class="warehouse-sheet__meta">{{ item.service_provider_text }} / {{ item.service_time }}</view>
|
||||
<view class="warehouse-sheet__meta">{{ item.full_address }}</view>
|
||||
<view v-if="item.is_recommended && item.recommended_reason" class="warehouse-sheet__reason">
|
||||
{{ item.recommended_reason }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.warehouse-picker {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.warehouse-current {
|
||||
padding: 26rpx;
|
||||
border-radius: 32rpx;
|
||||
background:
|
||||
linear-gradient(150deg, rgba(29, 29, 29, 0.98) 0%, rgba(52, 44, 31, 0.98) 100%);
|
||||
box-shadow: 0 14rpx 32rpx rgba(26, 20, 12, 0.16);
|
||||
}
|
||||
|
||||
.warehouse-current__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.warehouse-current__eyebrow {
|
||||
color: rgba(244, 215, 157, 0.82);
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.warehouse-current__name {
|
||||
margin-top: 10rpx;
|
||||
color: #fffaf0;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.warehouse-current__badge {
|
||||
flex-shrink: 0;
|
||||
padding: 10rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(244, 215, 157, 0.14);
|
||||
color: #f4d79d;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.warehouse-current__meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.warehouse-current__meta-pill {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 250, 240, 0.88);
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.warehouse-current__notice {
|
||||
margin-top: 18rpx;
|
||||
color: #f7e6bc;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.warehouse-current__action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: #fff6e3;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.warehouse-current__action-arrow {
|
||||
color: #f4d79d;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.shipping-detail__header {
|
||||
margin-top: 28rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1px solid var(--card-border);
|
||||
}
|
||||
|
||||
.shipping-detail__title {
|
||||
color: var(--color-heading);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.shipping-detail__desc {
|
||||
margin-top: 8rpx;
|
||||
color: var(--color-body);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.form-panel--readonly {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.form-panel__readonly-tip {
|
||||
margin-top: 12rpx;
|
||||
padding: 16rpx 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(237, 189, 0, 0.1);
|
||||
color: #c89b00;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.field-box--readonly {
|
||||
background: #f6f6f7;
|
||||
border-color: var(--card-border);
|
||||
}
|
||||
|
||||
.warehouse-sheet {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 180;
|
||||
}
|
||||
|
||||
.warehouse-sheet__mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(20, 20, 18, 0.38);
|
||||
}
|
||||
|
||||
.warehouse-sheet__panel {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 28rpx 28rpx calc(28rpx + env(safe-area-inset-bottom));
|
||||
border-radius: 36rpx 36rpx 0 0;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 -12rpx 36rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.warehouse-sheet__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.warehouse-sheet__title {
|
||||
color: var(--color-heading);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.warehouse-sheet__desc {
|
||||
margin-top: 8rpx;
|
||||
color: var(--color-body);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.warehouse-sheet__close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
min-width: 104rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(237, 189, 0, 0.12);
|
||||
color: var(--color-accent);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.warehouse-sheet__list {
|
||||
max-height: 58vh;
|
||||
}
|
||||
|
||||
.warehouse-sheet__option {
|
||||
padding: 22rpx 22rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(219, 206, 183, 0.9);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.warehouse-sheet__option + .warehouse-sheet__option {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.warehouse-sheet__option--selected {
|
||||
border-color: rgba(237, 189, 0, 0.38);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.warehouse-sheet__top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.warehouse-sheet__head {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.warehouse-sheet__name {
|
||||
color: var(--color-heading);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.warehouse-sheet__badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.warehouse-sheet__badge {
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f3efe6;
|
||||
color: #7d6540;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.warehouse-sheet__badge--accent {
|
||||
background: rgba(237, 189, 0, 0.12);
|
||||
color: #a36816;
|
||||
}
|
||||
|
||||
.warehouse-sheet__state {
|
||||
flex-shrink: 0;
|
||||
padding: 10rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(237, 189, 0, 0.12);
|
||||
color: var(--color-accent);
|
||||
font-size: 22rpx;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.warehouse-sheet__meta {
|
||||
margin-top: 8rpx;
|
||||
color: var(--color-body);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.warehouse-sheet__reason {
|
||||
margin-top: 12rpx;
|
||||
padding: 14rpx 16rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(237, 189, 0, 0.1);
|
||||
color: #9a6218;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
217
user-app/src/pages/order/supplement.vue
Normal file
217
user-app/src/pages/order/supplement.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { appApi, type SupplementDetailData } from "../../api/app";
|
||||
import { showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
|
||||
|
||||
const detail = ref<SupplementDetailData | null>(null);
|
||||
const loading = ref(true);
|
||||
const submitting = ref(false);
|
||||
const uploadingItemId = ref(0);
|
||||
|
||||
const completedCount = computed(
|
||||
() => detail.value?.items.filter((item) => item.files.length > 0).length || 0,
|
||||
);
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
async function fetchDetail(orderId: number) {
|
||||
loading.value = true;
|
||||
try {
|
||||
detail.value = await appApi.getSupplementDetail(orderId);
|
||||
} catch (error) {
|
||||
showErrorToast(error, "补资料任务加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function previewFiles(files: Array<{ file_url: string }>, current: string) {
|
||||
if (!files.length) return;
|
||||
uni.previewImage({
|
||||
urls: files.map((item) => item.file_url),
|
||||
current,
|
||||
});
|
||||
}
|
||||
|
||||
async function chooseAndUpload(uploadItemId: number) {
|
||||
try {
|
||||
const result = await uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ["compressed"],
|
||||
sourceType: ["album", "camera"],
|
||||
});
|
||||
const filePath = result.tempFilePaths?.[0];
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
uploadingItemId.value = uploadItemId;
|
||||
const file = await appApi.uploadSupplementFile({
|
||||
uploadItemId,
|
||||
filePath,
|
||||
});
|
||||
|
||||
const item = detail.value?.items.find((entry) => entry.upload_item_id === uploadItemId);
|
||||
if (item) {
|
||||
item.files.push(file);
|
||||
item.status = "uploaded";
|
||||
}
|
||||
|
||||
showInfoToast("补资料上传成功");
|
||||
} catch (error) {
|
||||
showErrorToast(error, "补资料上传失败");
|
||||
} finally {
|
||||
uploadingItemId.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFile(uploadItemId: number, fileId: string) {
|
||||
try {
|
||||
await appApi.deleteSupplementFile(fileId);
|
||||
const item = detail.value?.items.find((entry) => entry.upload_item_id === uploadItemId);
|
||||
if (item) {
|
||||
item.files = item.files.filter((file) => file.file_id !== fileId);
|
||||
item.status = item.files.length > 0 ? "uploaded" : "pending";
|
||||
}
|
||||
showInfoToast("已删除上传文件");
|
||||
} catch (error) {
|
||||
showErrorToast(error, "删除文件失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function submitSupplement() {
|
||||
if (!detail.value) return;
|
||||
|
||||
const missingItem = detail.value.items.find((item) => item.is_required && item.files.length === 0);
|
||||
if (missingItem) {
|
||||
showInfoToast(`请先上传${missingItem.item_name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await withLoading("正在提交补资料", async () => {
|
||||
await appApi.submitSupplement(detail.value!.order_id);
|
||||
});
|
||||
showInfoToast("补资料已提交");
|
||||
uni.redirectTo({
|
||||
url: `/pages/order/detail?id=${detail.value.order_id}`,
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorToast(error, "补资料提交失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async (options) => {
|
||||
const orderId = Number(options?.order_id || 0);
|
||||
if (!orderId) {
|
||||
showInfoToast("订单信息缺失");
|
||||
return;
|
||||
}
|
||||
await fetchDetail(orderId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="app-page app-page--tight">
|
||||
<view v-if="detail" class="section-card section-card--soft">
|
||||
<view class="tag tag--warning">补资料任务</view>
|
||||
<view class="page-title" style="margin-top: 20rpx; font-size: var(--font-size-2xl); color: var(--color-heading);">
|
||||
请补充鉴定资料
|
||||
</view>
|
||||
<view class="section__desc">{{ detail.reason }}</view>
|
||||
<view class="certificate-header__meta">
|
||||
<text class="certificate-meta-chip">订单号 {{ detail.order_no }}</text>
|
||||
<text class="certificate-meta-chip">截止 {{ detail.deadline || "尽快补充" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detail" class="section section-card section-card--soft">
|
||||
<view class="section__title">补充任务说明</view>
|
||||
<view class="section__desc">建议先把必传资料传完,再视情况补充辅助资料。提交后订单会重新进入鉴定流程,系统会继续同步新进展。</view>
|
||||
<view class="chip-list" style="margin-top: 18rpx;">
|
||||
<text class="choice-chip choice-chip--selected">{{ completedCount }} 项已完成</text>
|
||||
<text class="choice-chip">{{ detail.items.length }} 项资料任务</text>
|
||||
<text class="choice-chip">提交后重新进入鉴定</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detail" class="section progress-panel">
|
||||
<view class="progress-panel__top">
|
||||
<view class="section__title">当前完成进度</view>
|
||||
<view class="progress-panel__value">{{ completedCount }} / {{ detail.items.length }}</view>
|
||||
</view>
|
||||
<view class="progress-panel__hint">请优先完成所有必传资料上传,提交后订单会重新进入鉴定流程。</view>
|
||||
<view class="progress-panel__bar">
|
||||
<view
|
||||
class="progress-panel__fill"
|
||||
:style="{ width: `${detail.items.length ? Math.max(12, (completedCount / detail.items.length) * 100) : 0}%` }"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="notice-card">
|
||||
<view class="notice-card__title">正在加载补资料任务</view>
|
||||
<view class="notice-card__desc">请稍候,我们正在读取鉴定师给您的补充要求。</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!loading && !detail" class="notice-card">
|
||||
<view class="notice-card__title">暂无待补资料任务</view>
|
||||
<view class="notice-card__desc">当前订单没有可补交的资料项,您可以返回订单页稍后查看。</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detail" class="section">
|
||||
<view
|
||||
v-for="item in detail.items"
|
||||
:key="item.upload_item_id"
|
||||
class="task-card section-card"
|
||||
>
|
||||
<view class="task-card__row">
|
||||
<view class="task-card__title">{{ item.item_name }}</view>
|
||||
<text :class="item.files.length ? 'tag tag--success' : item.is_required ? 'tag tag--warning' : 'tag tag--neutral'">
|
||||
{{ item.files.length ? "已上传" : item.is_required ? "待上传" : "选传" }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="task-card__desc">{{ item.guide_text }}</view>
|
||||
<view class="task-card__row" style="margin-top: 12rpx">
|
||||
<text class="info-list__label">已上传</text>
|
||||
<text class="info-list__value">{{ item.files.length }} 张</text>
|
||||
</view>
|
||||
<view v-if="item.files.length" class="task-files">
|
||||
<view v-for="file in item.files" :key="file.file_id" class="task-file">
|
||||
<image
|
||||
class="task-file__img"
|
||||
:src="file.thumbnail_url"
|
||||
mode="aspectFill"
|
||||
@click="previewFiles(item.files, file.file_url)"
|
||||
/>
|
||||
<view class="task-file__remove" @click="removeFile(item.upload_item_id, file.file_id)">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="task-card__row" style="margin-top: 20rpx">
|
||||
<text class="info-list__label">{{ item.is_required ? "必传资料" : "辅助资料" }}</text>
|
||||
<text class="btn btn--ghost" @click="chooseAndUpload(item.upload_item_id)">
|
||||
{{ uploadingItemId === item.upload_item_id ? "上传中..." : item.files.length ? "继续补充" : "上传资料" }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detail" class="section section-note">
|
||||
<view class="support-banner__title">提交后会发生什么?</view>
|
||||
<view class="support-banner__desc">系统会将您的补充资料同步给鉴定师,订单重新进入鉴定流程。若资料仍不充分,客服会继续通过消息中心通知您。</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detail" class="fixed-action-bar">
|
||||
<view class="btn btn--secondary" @click="goBack">稍后再传</view>
|
||||
<view :class="['btn', 'btn--primary', submitting ? 'btn--disabled' : '']" @click="submitSupplement">
|
||||
{{ submitting ? "提交中..." : "提交补资料" }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
Reference in New Issue
Block a user