feat: update appraisal ordering and payment flows
This commit is contained in:
@@ -130,14 +130,38 @@ export interface MineOverviewData {
|
||||
};
|
||||
}
|
||||
|
||||
export interface PaymentLaunchInfo {
|
||||
status: string;
|
||||
channel: "h5" | "mini_program" | string;
|
||||
check_sn: string;
|
||||
order_token: string;
|
||||
cashier_url: string;
|
||||
order_sn: string;
|
||||
plugin_url?: string;
|
||||
plugin_provider?: string;
|
||||
}
|
||||
|
||||
export interface OrderPaymentStatusData {
|
||||
order_id: number;
|
||||
order_no: string;
|
||||
payment_status: string;
|
||||
order_status: string;
|
||||
display_status: string;
|
||||
payment: PaymentLaunchInfo | null;
|
||||
}
|
||||
|
||||
export interface OrderListItem {
|
||||
order_id: number;
|
||||
order_no: string;
|
||||
appraisal_no: string;
|
||||
payment_status: string;
|
||||
order_status: string;
|
||||
product_name: string;
|
||||
product_cover: string;
|
||||
service_provider: string;
|
||||
price_package_name: string;
|
||||
price_package_code: string;
|
||||
price_package_price: number;
|
||||
display_status: string;
|
||||
status_desc: string;
|
||||
estimated_finish_time: string;
|
||||
@@ -150,6 +174,10 @@ export interface OrderDetailData {
|
||||
order_no: string;
|
||||
appraisal_no: string;
|
||||
service_provider: string;
|
||||
price_package_name: string;
|
||||
price_package_code: string;
|
||||
price_package_price: number;
|
||||
payment_status: string;
|
||||
order_status: string;
|
||||
display_status: string;
|
||||
status_desc: string;
|
||||
@@ -239,6 +267,7 @@ export interface OrderDetailData {
|
||||
primary_action: string;
|
||||
secondary_action: string;
|
||||
};
|
||||
payment: PaymentLaunchInfo | null;
|
||||
}
|
||||
|
||||
export interface ShippingDetailData {
|
||||
@@ -650,6 +679,23 @@ export const appApi = {
|
||||
params: { id },
|
||||
});
|
||||
},
|
||||
retryOrderPayment(orderId: number) {
|
||||
return request<{ order_id: number; payment: PaymentLaunchInfo }>("/api/app/order/pay/retry", {
|
||||
method: "POST",
|
||||
data: { order_id: orderId },
|
||||
});
|
||||
},
|
||||
getOrderPaymentStatus(orderId: number) {
|
||||
return request<OrderPaymentStatusData>("/api/app/order/payment/status", {
|
||||
params: { id: orderId },
|
||||
});
|
||||
},
|
||||
cancelOrder(orderId: number) {
|
||||
return request<OrderPaymentStatusData>("/api/app/order/cancel", {
|
||||
method: "POST",
|
||||
data: { order_id: orderId },
|
||||
});
|
||||
},
|
||||
getOrderShippingDetail(orderId: number) {
|
||||
return request<ShippingDetailData>("/api/app/order/shipping", {
|
||||
params: { order_id: orderId },
|
||||
|
||||
@@ -2,10 +2,12 @@ import { parseUploadResponse, request } from "../utils/request";
|
||||
import { buildAuthHeaders } from "../utils/auth";
|
||||
import { resolveApiBaseUrl } from "../utils/env";
|
||||
import { resolveOrderSourceChannel } from "../utils/order-source";
|
||||
import type { PaymentLaunchInfo } from "./app";
|
||||
|
||||
export interface CatalogOption {
|
||||
brand_id?: number;
|
||||
brand_name?: string;
|
||||
brand_en_name?: string;
|
||||
}
|
||||
|
||||
export interface CategoryOption {
|
||||
@@ -36,6 +38,10 @@ export interface DraftDetail {
|
||||
draft_id: number;
|
||||
service_provider: string;
|
||||
service_mode: string;
|
||||
price_package_id: number;
|
||||
price_package_name: string;
|
||||
price_package_code: string;
|
||||
price_package_price: number;
|
||||
current_step: number;
|
||||
product_info: Record<string, any>;
|
||||
extra_info: Record<string, any>;
|
||||
@@ -48,6 +54,9 @@ export interface PreviewData {
|
||||
service_summary: {
|
||||
service_provider: string;
|
||||
service_provider_text: string;
|
||||
price_package_id: number;
|
||||
price_package_name: string;
|
||||
price_package_code: string;
|
||||
};
|
||||
product_summary: {
|
||||
product_name: string;
|
||||
@@ -71,23 +80,60 @@ export interface PreviewData {
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AppraisalServicePackage {
|
||||
id: number;
|
||||
service_provider: string;
|
||||
service_provider_text: string;
|
||||
package_name: string;
|
||||
package_code: string;
|
||||
price: number;
|
||||
description: string;
|
||||
is_enabled: boolean;
|
||||
is_default: boolean;
|
||||
sort_order: number;
|
||||
sla_hours: number;
|
||||
}
|
||||
|
||||
export interface AppraisalServiceConfig {
|
||||
service_provider: string;
|
||||
service_provider_text: string;
|
||||
price: number;
|
||||
sla_hours: number;
|
||||
default_package_id: number;
|
||||
default_package: AppraisalServicePackage | null;
|
||||
packages: AppraisalServicePackage[];
|
||||
}
|
||||
|
||||
export interface SubmitResult {
|
||||
order_id: number;
|
||||
order_no: string;
|
||||
appraisal_no: string;
|
||||
pay_amount: number;
|
||||
next_status: string;
|
||||
payment: PaymentLaunchInfo | null;
|
||||
payment_launch_failed?: boolean;
|
||||
payment_error?: string;
|
||||
}
|
||||
|
||||
export const appraisalApi = {
|
||||
createDraft(serviceProvider: string) {
|
||||
return request<{ draft_id: number; service_provider: string; service_mode: string }>(
|
||||
createDraft(serviceProvider: string, pricePackageId?: number, pricePackageCode?: string) {
|
||||
return request<{
|
||||
draft_id: number;
|
||||
service_provider: string;
|
||||
service_mode: string;
|
||||
price_package_id: number;
|
||||
price_package_name: string;
|
||||
price_package_code: string;
|
||||
price_package_price: number;
|
||||
}>(
|
||||
"/api/app/appraisal/draft/create",
|
||||
{
|
||||
method: "POST",
|
||||
data: {
|
||||
service_provider: serviceProvider,
|
||||
service_mode: "physical",
|
||||
price_package_id: pricePackageId || undefined,
|
||||
price_package_code: pricePackageCode || undefined,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -111,6 +157,9 @@ export const appraisalApi = {
|
||||
getCategories() {
|
||||
return request<{ list: CategoryOption[] }>("/api/app/catalog/categories");
|
||||
},
|
||||
getServiceConfigs() {
|
||||
return request<{ list: AppraisalServiceConfig[] }>("/api/app/appraisal/service-configs");
|
||||
},
|
||||
getUploadTemplate(categoryId: number, serviceProvider: string) {
|
||||
return request<{ template_id: number; required_items: UploadItem[]; optional_items: UploadItem[] }>(
|
||||
"/api/app/appraisal/upload-template",
|
||||
|
||||
@@ -46,6 +46,11 @@ export interface WechatBindMobileResult extends LoginResult {
|
||||
status: "logged_in";
|
||||
}
|
||||
|
||||
export interface MiniProgramBindResult {
|
||||
openid: string;
|
||||
unionid: string;
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
sendLoginCode(mobile: string) {
|
||||
return request<SendLoginCodeResult>("/api/app/auth/send-code", {
|
||||
@@ -84,6 +89,12 @@ export const authApi = {
|
||||
data: payload,
|
||||
});
|
||||
},
|
||||
bindMiniProgramOpenid(code: string) {
|
||||
return request<MiniProgramBindResult>("/api/app/auth/mini-program/bind", {
|
||||
method: "POST",
|
||||
data: { code },
|
||||
});
|
||||
},
|
||||
getMe() {
|
||||
return request<{ user_info: AuthUserInfo }>("/api/app/auth/me");
|
||||
},
|
||||
|
||||
@@ -50,11 +50,18 @@
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx1234567890test",
|
||||
"appid" : "wx8da234b813b3c9ac",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
"usingComponents" : true,
|
||||
"plugins" : {
|
||||
"lite-pos-plugin" : {
|
||||
"version" : "2.4.7",
|
||||
"provider" : "wx7903bb295ac26ac7",
|
||||
"export" : "miniprogram_npm/lite-pos-plugin-mate/utils.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
|
||||
@@ -81,10 +81,14 @@ export const ordersFallback: OrderListItem[] = [
|
||||
order_id: 1,
|
||||
order_no: "AXY202604200001",
|
||||
appraisal_no: "AXY-APP-20260420-0001",
|
||||
payment_status: "paid",
|
||||
order_status: "pending_supplement",
|
||||
product_name: "Louis Vuitton Neverfull MM",
|
||||
product_cover: "",
|
||||
service_provider: "zhongjian",
|
||||
price_package_name: "中检基础套餐",
|
||||
price_package_code: "zhongjian_basic",
|
||||
price_package_price: 199,
|
||||
display_status: "等待您补充资料",
|
||||
status_desc: "还差 2 项必传资料",
|
||||
estimated_finish_time: "2026-04-21 18:00:00",
|
||||
@@ -94,10 +98,14 @@ export const ordersFallback: OrderListItem[] = [
|
||||
order_id: 2,
|
||||
order_no: "AXY202604190012",
|
||||
appraisal_no: "AXY-APP-20260419-0012",
|
||||
payment_status: "paid",
|
||||
order_status: "pending_shipping",
|
||||
product_name: "Air Jordan 1 High OG",
|
||||
product_cover: "",
|
||||
service_provider: "anxinyan",
|
||||
price_package_name: "安心验基础套餐",
|
||||
price_package_code: "anxinyan_basic",
|
||||
price_package_price: 99,
|
||||
display_status: "鉴定师处理中",
|
||||
status_desc: "鉴定师正在处理,预计 24 小时内出具报告",
|
||||
estimated_finish_time: "2026-04-20 20:00:00",
|
||||
@@ -107,10 +115,14 @@ export const ordersFallback: OrderListItem[] = [
|
||||
order_id: 3,
|
||||
order_no: "AXY202604180088",
|
||||
appraisal_no: "AXY-APP-20260418-0088",
|
||||
payment_status: "paid",
|
||||
order_status: "completed",
|
||||
product_name: "Rolex Datejust 36",
|
||||
product_cover: "",
|
||||
service_provider: "zhongjian",
|
||||
price_package_name: "中检基础套餐",
|
||||
price_package_code: "zhongjian_basic",
|
||||
price_package_price: 199,
|
||||
display_status: "报告已出具",
|
||||
status_desc: "正式报告可查看并验真",
|
||||
estimated_finish_time: "2026-04-18 20:00:00",
|
||||
@@ -124,6 +136,10 @@ export const orderDetailFallback: OrderDetailData = {
|
||||
order_no: "AXY202604200001",
|
||||
appraisal_no: "AXY-APP-20260420-0001",
|
||||
service_provider: "zhongjian",
|
||||
price_package_name: "中检基础套餐",
|
||||
price_package_code: "zhongjian_basic",
|
||||
price_package_price: 199,
|
||||
payment_status: "paid",
|
||||
order_status: "pending_supplement",
|
||||
display_status: "等待您补充资料",
|
||||
status_desc: "鉴定师需要您补充 2 项资料后继续处理,建议尽快完成。",
|
||||
@@ -228,6 +244,7 @@ export const orderDetailFallback: OrderDetailData = {
|
||||
primary_action: "去补资料",
|
||||
secondary_action: "联系客服",
|
||||
},
|
||||
payment: null,
|
||||
};
|
||||
|
||||
export const shippingDetailFallback: ShippingDetailData = {
|
||||
@@ -391,7 +408,6 @@ export const reportDetailFallback: ReportDetailData = {
|
||||
{ label: "品牌", value: "Rolex" },
|
||||
{ label: "主体颜色", value: "银盘" },
|
||||
{ label: "服务类型", value: "中检鉴定" },
|
||||
{ label: "鉴定师", value: "张师傅" },
|
||||
],
|
||||
},
|
||||
trace_info: {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/home/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "安心验",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/login",
|
||||
"style": {
|
||||
@@ -13,29 +20,25 @@
|
||||
"navigationBarTitleText": "绑定手机号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/home/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "安心验",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/appraisal/service",
|
||||
"style": {
|
||||
"navigationBarTitleText": "选择鉴定服务"
|
||||
"navigationBarTitleText": "鉴定服务",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/appraisal/product",
|
||||
"style": {
|
||||
"navigationBarTitleText": "选择商品信息"
|
||||
"navigationBarTitleText": "选择品牌",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/appraisal/confirm",
|
||||
"style": {
|
||||
"navigationBarTitleText": "确认订单"
|
||||
"navigationBarTitleText": "确认订单",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,26 +3,37 @@ import { computed, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { appraisalApi } from "../../api/appraisal";
|
||||
import { appApi, type UserAddressItem } from "../../api/app";
|
||||
import FlowStepHeader from "../../components/FlowStepHeader.vue";
|
||||
import { useAppraisalStore } from "../../stores/appraisal";
|
||||
import { isMissingDraftError, rebuildDraftFromStore } from "../../utils/appraisal-flow";
|
||||
import { showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
|
||||
import { launchOrderPayment, prepareMiniProgramPaymentIdentity } from "../../utils/payment";
|
||||
|
||||
const store = useAppraisalStore();
|
||||
const preview = computed(() => store.preview);
|
||||
const loading = computed(() => !store.preview);
|
||||
const submitting = computed(() => false);
|
||||
const productCategoryBrandText = computed(() => {
|
||||
const categoryName = preview.value?.product_summary.category_name || "";
|
||||
const brandName = preview.value?.product_summary.brand_name || "未填写";
|
||||
return categoryName ? `${categoryName} / ${brandName}` : "";
|
||||
});
|
||||
const submitting = ref(false);
|
||||
const addressSheetVisible = ref(false);
|
||||
const addressesLoading = ref(false);
|
||||
const addressOptions = ref<UserAddressItem[]>([]);
|
||||
const selectedReturnAddress = computed(() => store.returnAddress);
|
||||
const recentCreatedAddressStorageKey = "anxinyan_recent_created_address_id";
|
||||
|
||||
const serviceProviderText = computed(() => preview.value?.service_summary.service_provider_text || "安心验鉴定");
|
||||
const packageNameText = computed(() => preview.value?.service_summary.price_package_name || store.pricePackageName || "");
|
||||
const categoryText = computed(() => preview.value?.product_summary.category_name || store.product.categoryName || "-");
|
||||
const brandText = computed(() => preview.value?.product_summary.brand_name || store.product.brandName || "未填写");
|
||||
const productNameText = computed(() => preview.value?.product_summary.product_name || `${categoryText.value} ${brandText.value === "未填写" ? "" : brandText.value}`.trim());
|
||||
const serviceFeeText = computed(() => formatMoney(preview.value?.fee_detail.service_fee || 0));
|
||||
const discountFeeText = computed(() => formatMoney(preview.value?.fee_detail.discount_fee || 0));
|
||||
const payAmountText = computed(() => formatMoney(preview.value?.fee_detail.pay_amount || 0));
|
||||
const canSubmit = computed(() => Boolean(store.draftId && store.returnAddress.id && !loading.value && !submitting.value));
|
||||
|
||||
function formatMoney(value: number | string) {
|
||||
const amount = Number(value || 0);
|
||||
if (!Number.isFinite(amount)) return "0";
|
||||
return amount % 1 === 0 ? String(amount) : amount.toFixed(2);
|
||||
}
|
||||
|
||||
function applySelectedAddress(item: UserAddressItem) {
|
||||
store.setReturnAddress({
|
||||
id: item.id,
|
||||
@@ -94,51 +105,12 @@ function chooseAddress(item: UserAddressItem) {
|
||||
showInfoToast("寄回地址已选择");
|
||||
}
|
||||
|
||||
async function goSuccess() {
|
||||
async function loadPreview() {
|
||||
if (!store.draftId) {
|
||||
showInfoToast("订单信息未准备完成,请返回上一步检查");
|
||||
return;
|
||||
}
|
||||
if (!store.returnAddress.id) {
|
||||
showInfoToast("请先确认寄回地址");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await withLoading("正在创建订单", async () => {
|
||||
try {
|
||||
return await appraisalApi.submit(store.draftId, store.returnAddress.id);
|
||||
} catch (error) {
|
||||
if (!isMissingDraftError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await rebuildDraftFromStore(store);
|
||||
return appraisalApi.submit(store.draftId, store.returnAddress.id);
|
||||
}
|
||||
});
|
||||
const successUrl = `/pages/appraisal/success?order_no=${encodeURIComponent(result.order_no)}&appraisal_no=${encodeURIComponent(result.appraisal_no)}&order_id=${result.order_id}`;
|
||||
store.resetForNewFlow();
|
||||
uni.navigateTo({
|
||||
url: successUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorToast(error, "创建订单失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
function openAgreement(url: string) {
|
||||
if (!url) return;
|
||||
uni.navigateTo({ url });
|
||||
}
|
||||
|
||||
onLoad(async () => {
|
||||
store.hydrate();
|
||||
await fetchAddresses();
|
||||
if (!store.draftId) return;
|
||||
try {
|
||||
let data;
|
||||
try {
|
||||
@@ -156,101 +128,165 @@ onLoad(async () => {
|
||||
} catch (error) {
|
||||
showErrorToast(error, "订单预览加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function goSuccess() {
|
||||
if (submitting.value) return;
|
||||
if (!store.draftId) {
|
||||
showInfoToast("订单信息未准备完成,请返回上一步检查");
|
||||
return;
|
||||
}
|
||||
if (!store.returnAddress.id) {
|
||||
showInfoToast("请先确认寄回地址");
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await prepareMiniProgramPaymentIdentity();
|
||||
const result = await withLoading("正在创建订单", async () => {
|
||||
try {
|
||||
return await appraisalApi.submit(store.draftId, store.returnAddress.id);
|
||||
} catch (error) {
|
||||
if (!isMissingDraftError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await rebuildDraftFromStore(store);
|
||||
return appraisalApi.submit(store.draftId, store.returnAddress.id);
|
||||
}
|
||||
});
|
||||
store.resetForNewFlow();
|
||||
if (!result.payment) {
|
||||
showInfoToast("订单已生成,可稍后在详情页继续支付");
|
||||
uni.navigateTo({ url: `/pages/order/detail?id=${result.order_id}` });
|
||||
return;
|
||||
}
|
||||
if (result.payment?.status === "paid") {
|
||||
uni.navigateTo({ url: `/pages/order/detail?id=${result.order_id}` });
|
||||
return;
|
||||
}
|
||||
launchOrderPayment(result.payment);
|
||||
} catch (error) {
|
||||
showErrorToast(error, "支付发起失败,请稍后重试");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
function openAgreement(url: string) {
|
||||
if (!url) return;
|
||||
uni.navigateTo({ url });
|
||||
}
|
||||
|
||||
onLoad(async () => {
|
||||
store.hydrate();
|
||||
await fetchAddresses();
|
||||
await loadPreview();
|
||||
});
|
||||
|
||||
onShow(fetchAddresses);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="app-page app-page--tight">
|
||||
<FlowStepHeader
|
||||
:step="3"
|
||||
:total="3"
|
||||
title="确认订单并支付"
|
||||
desc="提交前请再次确认服务、商品信息与寄回地址,提交后会生成正式鉴定单并进入履约流程。"
|
||||
:chips="['确认服务', '确认商品', '提交后生成正式鉴定单']"
|
||||
/>
|
||||
|
||||
<view v-if="loading" class="notice-card">
|
||||
<view class="notice-card__title">正在准备订单预览</view>
|
||||
<view class="notice-card__desc">请稍候,系统正在汇总服务、商品与费用信息。</view>
|
||||
</view>
|
||||
|
||||
<view class="section summary-card">
|
||||
<view class="summary-card__title">服务摘要</view>
|
||||
<view class="summary-card__row">
|
||||
<text class="summary-card__label">鉴定服务</text>
|
||||
<text class="summary-card__value">{{ preview?.service_summary.service_provider_text || '中检鉴定' }}</text>
|
||||
<view class="confirm-page">
|
||||
<view class="confirm-nav">
|
||||
<view class="confirm-nav__home" @click="goBack">
|
||||
<view class="confirm-nav__home-roof"></view>
|
||||
<view class="confirm-nav__home-body"></view>
|
||||
</view>
|
||||
<view class="confirm-nav__title">确认订单</view>
|
||||
<view class="confirm-nav__capsule">
|
||||
<text class="confirm-nav__dots">•••</text>
|
||||
<view class="confirm-nav__divider"></view>
|
||||
<view class="confirm-nav__circle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section summary-card">
|
||||
<view class="summary-card__title">商品摘要</view>
|
||||
<view class="summary-card__row">
|
||||
<text class="summary-card__label">商品名称</text>
|
||||
<text class="summary-card__value">{{ preview?.product_summary.product_name || '' }}</text>
|
||||
</view>
|
||||
<view class="summary-card__row">
|
||||
<text class="summary-card__label">品类 / 品牌</text>
|
||||
<text class="summary-card__value">{{ productCategoryBrandText }}</text>
|
||||
</view>
|
||||
<view v-if="loading" class="confirm-state">
|
||||
<view class="confirm-state__title">正在准备订单预览</view>
|
||||
<view class="confirm-state__desc">请稍候,系统正在汇总服务、商品与费用信息。</view>
|
||||
</view>
|
||||
|
||||
<view class="section summary-card">
|
||||
<view class="summary-card__title">费用明细</view>
|
||||
<view class="summary-card__row">
|
||||
<text class="summary-card__label">鉴定服务费</text>
|
||||
<text class="summary-card__value">¥{{ preview?.fee_detail.service_fee || 0 }}</text>
|
||||
</view>
|
||||
<view class="summary-card__row">
|
||||
<text class="summary-card__label">优惠抵扣</text>
|
||||
<text class="summary-card__value">- ¥{{ preview?.fee_detail.discount_fee || 0 }}</text>
|
||||
</view>
|
||||
<view class="summary-card__row">
|
||||
<text class="summary-card__label">实付金额</text>
|
||||
<text class="summary-card__value">¥{{ preview?.fee_detail.pay_amount || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section summary-card">
|
||||
<view class="summary-card__title">寄回地址</view>
|
||||
<view v-if="selectedReturnAddress.id" class="address-preview-card">
|
||||
<view class="address-preview-card__top">
|
||||
<view>
|
||||
<view class="address-preview-card__name">{{ selectedReturnAddress.consignee }}</view>
|
||||
<view class="address-preview-card__mobile">{{ selectedReturnAddress.mobile }}</view>
|
||||
<view class="confirm-card confirm-address" @click="openAddressSheet">
|
||||
<view class="confirm-address__icon"></view>
|
||||
<view class="confirm-address__body">
|
||||
<view class="confirm-card__title">寄回地址</view>
|
||||
<template v-if="selectedReturnAddress.id">
|
||||
<view class="confirm-address__person">
|
||||
{{ selectedReturnAddress.consignee }} {{ selectedReturnAddress.mobile }}
|
||||
<text v-if="selectedReturnAddress.isDefault" class="confirm-address__tag">默认</text>
|
||||
</view>
|
||||
<text v-if="selectedReturnAddress.isDefault" class="certificate-meta-chip">默认地址</text>
|
||||
</view>
|
||||
<view class="address-preview-card__address">{{ selectedReturnAddress.fullAddress }}</view>
|
||||
<view class="address-preview-card__action" @click="openAddressSheet">更换地址</view>
|
||||
<view class="confirm-address__detail">{{ selectedReturnAddress.fullAddress }}</view>
|
||||
</template>
|
||||
<view v-else class="confirm-card__muted">请添加收货人信息</view>
|
||||
</view>
|
||||
<view v-else class="address-preview-card address-preview-card--empty">
|
||||
<view class="address-preview-card__name">暂未选择寄回地址</view>
|
||||
<view class="section__desc">鉴定完成后,平台会按这里的地址回寄商品,请先确认。</view>
|
||||
<view class="address-preview-card__action" @click="openAddressSheet">选择地址</view>
|
||||
<view class="confirm-address__edit"></view>
|
||||
</view>
|
||||
|
||||
<view class="confirm-card">
|
||||
<view class="confirm-card__title">商品摘要</view>
|
||||
<view class="product-summary">
|
||||
<view class="product-summary__thumb">
|
||||
<view class="product-summary__mark">{{ brandText === "未填写" ? "AXY" : brandText.slice(0, 2) }}</view>
|
||||
</view>
|
||||
<view class="product-summary__info">
|
||||
<view class="product-summary__row">服务:{{ serviceProviderText }}</view>
|
||||
<view v-if="packageNameText" class="product-summary__row">套餐:{{ packageNameText }}</view>
|
||||
<view class="product-summary__row">品类:{{ categoryText }}</view>
|
||||
<view class="product-summary__row">品牌:{{ brandText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="confirm-card__muted confirm-card__muted--top">{{ productNameText }}</view>
|
||||
</view>
|
||||
|
||||
<view class="confirm-card">
|
||||
<view class="fee-head">
|
||||
<view class="confirm-card__title">费用明细</view>
|
||||
<view class="fee-head__amount">¥{{ payAmountText }}</view>
|
||||
</view>
|
||||
<view class="fee-line">
|
||||
<view class="fee-line__service-logo">安心验</view>
|
||||
<view class="fee-line__name">{{ packageNameText || `${serviceProviderText}服务费` }}</view>
|
||||
<view class="fee-line__amount">¥{{ serviceFeeText }}</view>
|
||||
</view>
|
||||
<view v-if="Number(preview?.fee_detail.discount_fee || 0) > 0" class="fee-line fee-line--plain">
|
||||
<view class="fee-line__name">优惠抵扣</view>
|
||||
<view class="fee-line__amount">- ¥{{ discountFeeText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section agreement-card">
|
||||
<view class="section__title">提交前确认</view>
|
||||
<view class="section__desc">提交即表示您已阅读并同意以下协议与说明,请在提交前再次确认。</view>
|
||||
<view v-if="preview?.agreements?.length" class="agreement-card__list">
|
||||
<view
|
||||
v-for="item in preview.agreements"
|
||||
:key="item.code"
|
||||
class="agreement-card__item"
|
||||
@click="openAgreement(item.target_url)"
|
||||
>
|
||||
<view class="agreement-card__item-title">{{ item.title }}</view>
|
||||
<view class="agreement-card__item-desc">{{ item.desc }}</view>
|
||||
<view class="confirm-section-title">提交前请确认</view>
|
||||
<view class="confirm-section-desc">提交即表示您已阅读并同意以下协议与说明,请在提交前再次确认。</view>
|
||||
|
||||
<view v-if="preview?.agreements?.length" class="agreement-list">
|
||||
<view
|
||||
v-for="item in preview.agreements"
|
||||
:key="item.code"
|
||||
class="agreement-item"
|
||||
@click="openAgreement(item.target_url)"
|
||||
>
|
||||
<view class="agreement-item__icon"></view>
|
||||
<view class="agreement-item__body">
|
||||
<view class="agreement-item__title">{{ item.title }}</view>
|
||||
<view class="agreement-item__desc">{{ item.desc }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="fixed-action-bar">
|
||||
<view class="btn btn--secondary" @click="goBack">返回</view>
|
||||
<view :class="['btn', 'btn--primary', loading ? 'btn--disabled' : '']" @click="goSuccess">立即支付</view>
|
||||
<view class="confirm-bottom">
|
||||
<view class="confirm-bottom__total">
|
||||
<view class="confirm-bottom__label">总金额</view>
|
||||
<view class="confirm-bottom__price">¥{{ payAmountText }}</view>
|
||||
<view class="confirm-bottom__tax">含税价</view>
|
||||
</view>
|
||||
<view :class="['confirm-bottom__submit', !canSubmit ? 'confirm-bottom__submit--disabled' : '']" @click="goSuccess">
|
||||
{{ submitting ? "提交中..." : "立即支付" }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="addressSheetVisible" class="picker-sheet">
|
||||
@@ -269,15 +305,16 @@ onShow(fetchAddresses);
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="picker-sheet__list">
|
||||
<view v-if="addressesLoading" class="picker-sheet__empty">正在加载地址...</view>
|
||||
<view
|
||||
v-for="item in addressOptions"
|
||||
:key="item.id"
|
||||
:class="['picker-sheet__option', selectedReturnAddress.id === item.id ? 'picker-sheet__option--selected' : '']"
|
||||
@click="chooseAddress(item)"
|
||||
>
|
||||
<view style="flex:1">
|
||||
<view class="picker-sheet__option-main">
|
||||
<view class="picker-sheet__option-title">{{ item.consignee }} / {{ item.mobile }}</view>
|
||||
<view class="selector-card__meta" style="margin-top: 10rpx;">{{ item.full_address }}</view>
|
||||
<view class="picker-sheet__option-desc">{{ item.full_address }}</view>
|
||||
</view>
|
||||
<view class="picker-sheet__option-action">{{ selectedReturnAddress.id === item.id ? "已选地址" : "选择" }}</view>
|
||||
</view>
|
||||
@@ -287,79 +324,456 @@ onShow(fetchAddresses);
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.address-preview-card {
|
||||
margin-top: 16rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--card-border);
|
||||
<style scoped lang="scss">
|
||||
.confirm-page {
|
||||
min-height: 100vh;
|
||||
padding: 28rpx 30rpx calc(148rpx + env(safe-area-inset-bottom));
|
||||
background: #f2f2f4;
|
||||
color: #252527;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.address-preview-card--empty {
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
.confirm-page,
|
||||
.confirm-page view,
|
||||
.confirm-page text {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.address-preview-card__top {
|
||||
.confirm-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
align-items: flex-start;
|
||||
min-height: 72rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.address-preview-card__name {
|
||||
color: var(--color-heading);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1.6;
|
||||
.confirm-nav__home {
|
||||
position: relative;
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
}
|
||||
|
||||
.address-preview-card__mobile {
|
||||
.confirm-nav__home-roof {
|
||||
position: absolute;
|
||||
left: 10rpx;
|
||||
top: 4rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-left: 5rpx solid #252527;
|
||||
border-top: 5rpx solid #252527;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.confirm-nav__home-body {
|
||||
position: absolute;
|
||||
left: 12rpx;
|
||||
bottom: 6rpx;
|
||||
width: 30rpx;
|
||||
height: 28rpx;
|
||||
border: 5rpx solid #252527;
|
||||
border-top: 0;
|
||||
border-radius: 4rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.confirm-nav__home-body::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 2rpx;
|
||||
bottom: 2rpx;
|
||||
width: 18rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 12rpx 12rpx 12rpx 4rpx;
|
||||
background: #edbd00;
|
||||
}
|
||||
|
||||
.confirm-nav__title {
|
||||
color: #252527;
|
||||
font-size: 38rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.confirm-nav__capsule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150rpx;
|
||||
height: 64rpx;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.confirm-nav__dots {
|
||||
color: #111111;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.confirm-nav__divider {
|
||||
width: 1px;
|
||||
height: 36rpx;
|
||||
margin: 0 18rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.confirm-nav__circle {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
border: 7rpx solid #111111;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.confirm-state,
|
||||
.confirm-card,
|
||||
.agreement-item {
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.confirm-state {
|
||||
padding: 28rpx 30rpx;
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
.confirm-state__title {
|
||||
color: #252527;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.confirm-state__desc {
|
||||
margin-top: 8rpx;
|
||||
color: var(--color-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.6;
|
||||
color: #9a9a9d;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.address-preview-card__address {
|
||||
margin-top: 14rpx;
|
||||
color: var(--color-body);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.8;
|
||||
.confirm-card {
|
||||
margin-top: 22rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.address-preview-card__action {
|
||||
margin-top: 16rpx;
|
||||
color: var(--color-accent);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
.confirm-card__title {
|
||||
color: #252527;
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.agreement-card__list {
|
||||
display: grid;
|
||||
gap: 14rpx;
|
||||
.confirm-card__muted {
|
||||
color: #a0a0a4;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.confirm-card__muted--top {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.agreement-card__item {
|
||||
padding: 18rpx 20rpx;
|
||||
border-radius: 22rpx;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1px solid var(--card-border);
|
||||
.confirm-address {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22rpx;
|
||||
}
|
||||
|
||||
.agreement-card__item-title {
|
||||
color: var(--color-heading);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
.confirm-address__icon {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 48rpx;
|
||||
height: 42rpx;
|
||||
border: 4rpx solid #252527;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.confirm-address__icon::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 7rpx;
|
||||
top: -14rpx;
|
||||
width: 30rpx;
|
||||
height: 24rpx;
|
||||
border: 4rpx solid #252527;
|
||||
border-bottom: 0;
|
||||
border-radius: 12rpx 12rpx 0 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.confirm-address__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.confirm-address__person {
|
||||
margin-top: 8rpx;
|
||||
color: #252527;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.confirm-address__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 34rpx;
|
||||
margin-left: 12rpx;
|
||||
padding: 0 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(237, 189, 0, 0.12);
|
||||
color: #c89b00;
|
||||
font-size: 21rpx;
|
||||
line-height: 34rpx;
|
||||
}
|
||||
|
||||
.confirm-address__detail {
|
||||
margin-top: 8rpx;
|
||||
color: #7a7a7d;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.confirm-address__edit {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
border: 3rpx dashed #8d8d91;
|
||||
}
|
||||
|
||||
.confirm-address__edit::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 12rpx;
|
||||
top: 22rpx;
|
||||
width: 24rpx;
|
||||
height: 4rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #5d5d61;
|
||||
transform: rotate(-35deg);
|
||||
}
|
||||
|
||||
.product-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.product-summary__thumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 104rpx;
|
||||
height: 104rpx;
|
||||
border: 2rpx dashed #8d8d91;
|
||||
background: #f7f7f8;
|
||||
}
|
||||
|
||||
.product-summary__mark {
|
||||
max-width: 86rpx;
|
||||
color: #252527;
|
||||
font-size: 20rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.product-summary__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.product-summary__row {
|
||||
color: #3d3d40;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.8;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.fee-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.fee-head__amount {
|
||||
color: #e01b24;
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.fee-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
min-height: 80rpx;
|
||||
margin-top: 24rpx;
|
||||
padding: 0 18rpx;
|
||||
border-radius: 10rpx;
|
||||
background: #f5f5f6;
|
||||
}
|
||||
|
||||
.fee-line--plain {
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f2;
|
||||
}
|
||||
|
||||
.fee-line__service-logo {
|
||||
flex-shrink: 0;
|
||||
color: #252527;
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.fee-line__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #3d3d40;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.fee-line__amount {
|
||||
flex-shrink: 0;
|
||||
color: #3d3d40;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.confirm-section-title {
|
||||
margin-top: 34rpx;
|
||||
color: #252527;
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.confirm-section-desc {
|
||||
margin-top: 10rpx;
|
||||
color: #a0a0a4;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.agreement-list {
|
||||
display: grid;
|
||||
gap: 18rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.agreement-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22rpx;
|
||||
min-height: 104rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.agreement-item__icon {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border: 3rpx dashed #8d8d91;
|
||||
}
|
||||
|
||||
.agreement-item__icon::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 15rpx;
|
||||
top: 10rpx;
|
||||
width: 22rpx;
|
||||
height: 30rpx;
|
||||
border: 4rpx solid #252527;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.agreement-item__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agreement-item__title {
|
||||
color: #252527;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.agreement-item__desc {
|
||||
margin-top: 4rpx;
|
||||
color: #a0a0a4;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.agreement-card__item-desc {
|
||||
margin-top: 8rpx;
|
||||
color: var(--color-body);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.7;
|
||||
.confirm-bottom {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 90;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 28rpx;
|
||||
padding: 18rpx 30rpx calc(18rpx + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #e9e9eb;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.confirm-bottom__total {
|
||||
flex-shrink: 0;
|
||||
min-width: 220rpx;
|
||||
}
|
||||
|
||||
.confirm-bottom__label {
|
||||
display: inline-block;
|
||||
color: #3d3d40;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.confirm-bottom__price {
|
||||
display: inline-block;
|
||||
margin-left: 8rpx;
|
||||
color: #edbd00;
|
||||
font-size: 42rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.confirm-bottom__tax {
|
||||
margin-top: 2rpx;
|
||||
color: #a0a0a4;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.confirm-bottom__submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
height: 84rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #edbd00;
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.confirm-bottom__submit--disabled {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.picker-sheet {
|
||||
@@ -394,32 +808,25 @@ onShow(fetchAddresses);
|
||||
}
|
||||
|
||||
.picker-sheet__title {
|
||||
color: var(--color-heading);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: #252527;
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.picker-sheet__desc {
|
||||
margin-top: 8rpx;
|
||||
color: var(--color-body);
|
||||
font-size: var(--font-size-xs);
|
||||
.picker-sheet__desc,
|
||||
.picker-sheet__option-desc,
|
||||
.picker-sheet__empty {
|
||||
color: #9a9a9d;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.picker-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;
|
||||
.picker-sheet__close,
|
||||
.picker-sheet__create,
|
||||
.picker-sheet__option-action {
|
||||
color: #edbd00;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.picker-sheet__list {
|
||||
@@ -432,46 +839,36 @@ onShow(fetchAddresses);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.picker-sheet__create {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 144rpx;
|
||||
padding: 14rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, rgba(237, 189, 0, 0.16) 0%, rgba(237, 189, 0, 0.08) 100%);
|
||||
color: var(--color-accent);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.picker-sheet__option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 6rpx;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
border-bottom: 1px solid #eeeeef;
|
||||
}
|
||||
|
||||
.picker-sheet__option--selected {
|
||||
padding: 24rpx 18rpx;
|
||||
border-radius: 22rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(237, 189, 0, 0.08);
|
||||
border: 1px solid rgba(237, 189, 0, 0.22);
|
||||
box-shadow: 0 10rpx 24rpx rgba(237, 189, 0, 0.08);
|
||||
}
|
||||
|
||||
.picker-sheet__option-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.picker-sheet__option-title {
|
||||
color: var(--color-heading);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.7;
|
||||
color: #252527;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.picker-sheet__option-action {
|
||||
color: var(--color-accent);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
.picker-sheet__empty {
|
||||
padding: 48rpx 0 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,212 +1,214 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { appraisalApi, type CategoryOption } from "../../api/appraisal";
|
||||
import FlowStepHeader from "../../components/FlowStepHeader.vue";
|
||||
import { appraisalApi, type CatalogOption, type CategoryOption } from "../../api/appraisal";
|
||||
import { useAppraisalStore } from "../../stores/appraisal";
|
||||
import { isMissingDraftError, rebuildDraftFromStore } from "../../utils/appraisal-flow";
|
||||
import { showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
|
||||
|
||||
type PickerType = "category";
|
||||
type CategoryPickerItem = {
|
||||
type ProductPageOptions = Record<string, string | undefined>;
|
||||
type BrandOption = {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
desc: string;
|
||||
enName: string;
|
||||
displayName: string;
|
||||
searchText: string;
|
||||
group: string;
|
||||
};
|
||||
|
||||
const store = useAppraisalStore();
|
||||
|
||||
const categories = ref<CategoryPickerItem[]>([]);
|
||||
const pageLoading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const loadError = ref("");
|
||||
const activePicker = ref<PickerType | "">("");
|
||||
const pickerKeyword = ref("");
|
||||
const keyword = ref("");
|
||||
const brands = ref<BrandOption[]>([]);
|
||||
|
||||
const canContinue = computed(() => Boolean(store.product.categoryId));
|
||||
const brandNameForSave = computed(() => store.product.brandName.trim());
|
||||
const selectedBrandId = computed(() => store.product.brandId || 0);
|
||||
const categoryTitle = computed(() => store.product.categoryName || "已选品类");
|
||||
const serviceProviderText = computed(() => store.serviceProvider === "zhongjian" ? "中检鉴定" : "安心验鉴定");
|
||||
const hasCategory = computed(() => Boolean(store.product.categoryId));
|
||||
|
||||
const productSummary = computed(() => [
|
||||
store.product.categoryName || "未选品类",
|
||||
brandNameForSave.value || "未填品牌",
|
||||
].join(" / "));
|
||||
|
||||
const pickerMeta = computed(() => {
|
||||
return {
|
||||
title: "选择品类",
|
||||
desc: "品类将持续扩展,建议统一通过搜索选择器完成选择,后续更便于拓展更多鉴定业务。",
|
||||
options: categories.value,
|
||||
label: (item: CategoryPickerItem) => item.name || "",
|
||||
emptyText: "暂无可用品类",
|
||||
searchPlaceholder: "搜索品类名称",
|
||||
};
|
||||
const filteredBrands = computed(() => {
|
||||
const normalized = normalizeSearchText(keyword.value);
|
||||
if (!normalized) return brands.value;
|
||||
return brands.value.filter((item) => item.searchText.includes(normalized));
|
||||
});
|
||||
|
||||
const filteredPickerOptions = computed(() => {
|
||||
const keyword = pickerKeyword.value.trim().toLowerCase();
|
||||
if (!keyword) {
|
||||
return pickerMeta.value.options;
|
||||
const groupedBrands = computed(() => {
|
||||
const groups: Array<{ letter: string; list: BrandOption[] }> = [];
|
||||
for (const item of filteredBrands.value) {
|
||||
const last = groups[groups.length - 1];
|
||||
if (last?.letter === item.group) {
|
||||
last.list.push(item);
|
||||
continue;
|
||||
}
|
||||
groups.push({ letter: item.group, list: [item] });
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
|
||||
function normalizeSearchText(value = "") {
|
||||
return value.replace(/[\s\u200B-\u200D\uFEFF]+/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
function firstGroupLetter(item: CatalogOption) {
|
||||
const source = (item.brand_en_name || item.brand_name || "").trim();
|
||||
const first = source.charAt(0).toUpperCase();
|
||||
return /^[A-Z]$/.test(first) ? first : "#";
|
||||
}
|
||||
|
||||
function formatBrandName(item: CatalogOption) {
|
||||
const name = String(item.brand_name || "").trim();
|
||||
const enName = String(item.brand_en_name || "").trim();
|
||||
if (enName && name && normalizeSearchText(enName) !== normalizeSearchText(name)) {
|
||||
return `${enName}/${name}`;
|
||||
}
|
||||
return enName || name || "未命名品牌";
|
||||
}
|
||||
|
||||
function mapBrand(item: CatalogOption): BrandOption {
|
||||
const name = String(item.brand_name || "").trim();
|
||||
const enName = String(item.brand_en_name || "").trim();
|
||||
const displayName = formatBrandName(item);
|
||||
return {
|
||||
id: Number(item.brand_id || 0),
|
||||
name,
|
||||
enName,
|
||||
displayName,
|
||||
searchText: normalizeSearchText(`${name}${enName}${displayName}`),
|
||||
group: firstGroupLetter(item),
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureCategoryNameFromCatalog(categoryId: number) {
|
||||
if (!categoryId || store.product.categoryName) return;
|
||||
const data = await appraisalApi.getCategories();
|
||||
const match = data.list.find((item: CategoryOption) => Number(item.category_id) === categoryId);
|
||||
if (match) {
|
||||
store.setProduct({ categoryName: match.category_name });
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDraftAndBrands(options: ProductPageOptions = {}) {
|
||||
const queryDraftId = Number(options.draft_id || 0);
|
||||
const queryCategoryId = Number(options.category_id || 0);
|
||||
if (queryDraftId && queryDraftId !== store.draftId) {
|
||||
store.setDraft(queryDraftId);
|
||||
}
|
||||
if (queryCategoryId && queryCategoryId !== store.product.categoryId) {
|
||||
store.setProduct({
|
||||
categoryId: queryCategoryId,
|
||||
brandId: 0,
|
||||
brandName: "",
|
||||
});
|
||||
}
|
||||
|
||||
const options = pickerMeta.value.options;
|
||||
return options.filter((item) =>
|
||||
pickerMeta.value.label(item).toLowerCase().includes(keyword),
|
||||
);
|
||||
});
|
||||
if (!store.draftId) {
|
||||
throw new Error("订单草稿不存在,请返回服务介绍页重新发起鉴定");
|
||||
}
|
||||
|
||||
function pickerOptionKey(item: CategoryPickerItem) {
|
||||
return String(item.id);
|
||||
const draft = await appraisalApi.getDraft(store.draftId);
|
||||
store.setServiceProvider(draft.service_provider || store.serviceProvider);
|
||||
if (draft.product_info?.category_id) {
|
||||
const categoryId = Number(draft.product_info.category_id || 0);
|
||||
store.setProduct({
|
||||
categoryId,
|
||||
brandId: Number(draft.product_info.brand_id || 0),
|
||||
brandName: draft.product_info.brand_name || "",
|
||||
});
|
||||
}
|
||||
|
||||
await ensureCategoryNameFromCatalog(store.product.categoryId);
|
||||
|
||||
if (!store.product.categoryId) {
|
||||
throw new Error("请先选择鉴定品类");
|
||||
}
|
||||
|
||||
const data = await appraisalApi.getBrands(store.product.categoryId);
|
||||
brands.value = data.list
|
||||
.map(mapBrand)
|
||||
.filter((item) => item.id > 0 || item.displayName)
|
||||
.sort((a, b) => {
|
||||
if (a.group !== b.group) return a.group.localeCompare(b.group);
|
||||
return a.displayName.localeCompare(b.displayName);
|
||||
});
|
||||
|
||||
loadError.value = "";
|
||||
}
|
||||
|
||||
function selectCategory(item: CategoryPickerItem) {
|
||||
store.setProduct({
|
||||
categoryId: item.id,
|
||||
categoryName: item.name,
|
||||
brandId: 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
const data = await appraisalApi.getCategories();
|
||||
categories.value = data.list.map((item: CategoryOption) => ({
|
||||
id: item.category_id,
|
||||
name: item.category_name,
|
||||
code: item.category_code,
|
||||
desc: "来自后台品类配置,支持后续继续扩展。",
|
||||
}));
|
||||
}
|
||||
|
||||
function openPicker(type: PickerType) {
|
||||
activePicker.value = type;
|
||||
pickerKeyword.value = "";
|
||||
}
|
||||
|
||||
function closePicker() {
|
||||
activePicker.value = "";
|
||||
pickerKeyword.value = "";
|
||||
}
|
||||
|
||||
function confirmPicker(item: CategoryPickerItem) {
|
||||
selectCategory(item);
|
||||
closePicker();
|
||||
}
|
||||
|
||||
function isPickerCurrent(item: CategoryPickerItem) {
|
||||
return Number(item.id) === Number(store.product.categoryId || 0);
|
||||
}
|
||||
|
||||
function setBrandName(event: unknown) {
|
||||
const value = typeof event === "string"
|
||||
? event
|
||||
: (event as { detail?: { value?: string } })?.detail?.value || "";
|
||||
store.setProduct({
|
||||
brandId: 0,
|
||||
brandName: value,
|
||||
});
|
||||
}
|
||||
|
||||
async function goNext() {
|
||||
async function saveProductAndGoConfirm(payload: { brandId: number; brandName: string }) {
|
||||
if (submitting.value) return;
|
||||
if (!canContinue.value) {
|
||||
if (!hasCategory.value) {
|
||||
showInfoToast("请先选择品类");
|
||||
return;
|
||||
}
|
||||
|
||||
store.setProduct({
|
||||
brandId: 0,
|
||||
brandName: brandNameForSave.value,
|
||||
});
|
||||
submitting.value = true;
|
||||
try {
|
||||
await withLoading("正在保存商品信息", async () => {
|
||||
await withLoading("正在保存品牌", async () => {
|
||||
store.setProduct({
|
||||
brandId: payload.brandId,
|
||||
brandName: payload.brandName,
|
||||
});
|
||||
await appraisalApi.saveDraft({
|
||||
draft_id: store.draftId,
|
||||
current_step: 3,
|
||||
service_provider: store.serviceProvider,
|
||||
product_info: {
|
||||
category_id: store.product.categoryId,
|
||||
brand_id: 0,
|
||||
brand_name: store.product.brandName,
|
||||
brand_id: payload.brandId,
|
||||
brand_name: payload.brandName,
|
||||
},
|
||||
});
|
||||
});
|
||||
loadError.value = "";
|
||||
store.setCurrentStep(3);
|
||||
store.setPreview(null);
|
||||
uni.navigateTo({ url: "/pages/appraisal/confirm" });
|
||||
} catch (error) {
|
||||
loadError.value = "商品信息保存失败,请稍后重试或联系管理员检查服务。";
|
||||
showErrorToast(error, "商品信息保存失败");
|
||||
loadError.value = "品牌信息保存失败,请稍后重试。";
|
||||
showErrorToast(error, "品牌信息保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectBrand(item: BrandOption) {
|
||||
void saveProductAndGoConfirm({
|
||||
brandId: item.id,
|
||||
brandName: item.name || item.enName || item.displayName,
|
||||
});
|
||||
}
|
||||
|
||||
function skipBrand() {
|
||||
void saveProductAndGoConfirm({
|
||||
brandId: 0,
|
||||
brandName: "",
|
||||
});
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
onLoad(async () => {
|
||||
onLoad(async (options: ProductPageOptions = {}) => {
|
||||
store.hydrate();
|
||||
pageLoading.value = true;
|
||||
try {
|
||||
await loadCategories();
|
||||
if (!categories.value.length) {
|
||||
loadError.value = "后台暂未启用品类,请先在商品资料中心配置并启用品类。";
|
||||
store.setProduct({
|
||||
categoryId: 0,
|
||||
categoryName: "",
|
||||
brandId: 0,
|
||||
brandName: "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!store.draftId) {
|
||||
const draft = await appraisalApi.createDraft(store.serviceProvider);
|
||||
store.setDraft(draft.draft_id);
|
||||
}
|
||||
|
||||
const draft = await appraisalApi.getDraft(store.draftId);
|
||||
if (draft.product_info?.category_id) {
|
||||
const categoryId = Number(draft.product_info.category_id);
|
||||
const categoryName = categories.value.find((item) => item.id === categoryId)?.name || "";
|
||||
store.setProduct({
|
||||
categoryId,
|
||||
categoryName,
|
||||
brandId: Number(draft.product_info.brand_id || 0),
|
||||
brandName: draft.product_info.brand_name || "",
|
||||
});
|
||||
} else if (store.product.categoryId) {
|
||||
const currentCategory = categories.value.find((item) => item.id === store.product.categoryId);
|
||||
if (!currentCategory) {
|
||||
store.setProduct({
|
||||
categoryId: 0,
|
||||
categoryName: "",
|
||||
brandId: 0,
|
||||
brandName: "",
|
||||
});
|
||||
} else if (store.product.categoryName !== currentCategory.name) {
|
||||
store.setProduct({ categoryName: currentCategory.name });
|
||||
}
|
||||
}
|
||||
|
||||
loadError.value = "";
|
||||
await loadDraftAndBrands(options);
|
||||
} catch (error) {
|
||||
if (isMissingDraftError(error)) {
|
||||
try {
|
||||
await rebuildDraftFromStore(store);
|
||||
loadError.value = "";
|
||||
showInfoToast("草稿已自动恢复,可继续填写商品信息。");
|
||||
await loadDraftAndBrands(options);
|
||||
showInfoToast("草稿已自动恢复,可继续选择品牌。");
|
||||
} catch (recoverError) {
|
||||
store.resetForNewFlow();
|
||||
loadError.value = "商品草稿已失效,请重新选择鉴定服务。";
|
||||
showErrorToast(recoverError, "商品信息加载失败");
|
||||
loadError.value = "商品草稿已失效,请重新发起鉴定。";
|
||||
showErrorToast(recoverError, "品牌加载失败");
|
||||
}
|
||||
} else {
|
||||
loadError.value = "商品字典加载失败,请稍后重试或联系管理员检查接口。";
|
||||
showErrorToast(error, "商品信息加载失败");
|
||||
loadError.value = error instanceof Error ? error.message : "品牌加载失败,请稍后重试。";
|
||||
showErrorToast(error, "品牌加载失败");
|
||||
}
|
||||
} finally {
|
||||
pageLoading.value = false;
|
||||
@@ -215,297 +217,289 @@ onLoad(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="app-page app-page--tight">
|
||||
<FlowStepHeader
|
||||
:step="2"
|
||||
:total="3"
|
||||
title="选择商品信息"
|
||||
desc="只需确认本次送检商品的品类,品牌可按实际情况选填,其他细节可在后续补充说明中填写。"
|
||||
:chips="['选择品类', '品牌选填']"
|
||||
/>
|
||||
|
||||
<view class="section form-panel">
|
||||
<view class="form-panel__title">当前识别路径</view>
|
||||
<view class="form-panel__desc">当前步骤用于确定商品大类,便于匹配资料模板与服务流程。</view>
|
||||
<view class="selection-summary-card">
|
||||
<view class="selection-summary-card__title">{{ productSummary }}</view>
|
||||
<view class="selection-summary-card__desc">
|
||||
{{ canContinue ? "已完成基础商品信息" : "请选择品类后继续" }}
|
||||
</view>
|
||||
<view class="brand-page">
|
||||
<view class="brand-search">
|
||||
<view class="brand-search__box">
|
||||
<view class="brand-search__icon"></view>
|
||||
<input
|
||||
v-model="keyword"
|
||||
class="brand-search__input"
|
||||
confirm-type="search"
|
||||
placeholder="请输入您要搜索的品牌名称"
|
||||
/>
|
||||
</view>
|
||||
<view class="brand-search__cancel" @click="goBack">取消</view>
|
||||
</view>
|
||||
|
||||
<view class="section form-panel">
|
||||
<view class="form-panel__title">商品识别信息</view>
|
||||
<view class="form-panel__desc">品类来源于后台资料配置,品牌由用户按实际商品自行填写。</view>
|
||||
|
||||
<view v-if="pageLoading" class="notice-card">
|
||||
<view class="notice-card__title">正在加载商品字典</view>
|
||||
<view class="notice-card__desc">品类数据加载完成后即可继续选择。</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loadError" class="notice-card">
|
||||
<view class="notice-card__title">商品信息加载失败</view>
|
||||
<view class="notice-card__desc">{{ loadError }}</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<view class="form-group__label">品类</view>
|
||||
<view :class="['selector-card', store.product.categoryId ? 'selector-card--selected' : '']" @click="openPicker('category')">
|
||||
<view class="selector-card__main">
|
||||
<view class="selector-card__value">{{ store.product.categoryName || "请选择品类" }}</view>
|
||||
<view class="selector-card__meta">当前已配置 {{ categories.length }} 个品类,来源于后台商品资料配置。</view>
|
||||
</view>
|
||||
<view class="selector-card__side">
|
||||
<view v-if="store.product.categoryId" class="selector-card__badge">已选品类</view>
|
||||
<view class="selector-card__action">选择</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<view class="form-group__label">品牌(选填)</view>
|
||||
<view class="field-box">
|
||||
<input
|
||||
:value="store.product.brandName"
|
||||
class="field-input"
|
||||
maxlength="128"
|
||||
placeholder="请输入品牌名称,可不填"
|
||||
@input="setBrandName"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-group__hint">不确定品牌时可留空,后续补充说明中仍可描述商品细节。</view>
|
||||
</view>
|
||||
<view class="brand-summary">
|
||||
<view class="brand-summary__service">{{ serviceProviderText }}</view>
|
||||
<view class="brand-summary__category">{{ categoryTitle }}</view>
|
||||
<view class="brand-summary__skip" @click="skipBrand">跳过品牌</view>
|
||||
</view>
|
||||
|
||||
<view v-if="activePicker" class="picker-sheet">
|
||||
<view class="picker-sheet__mask" @click="closePicker"></view>
|
||||
<view class="picker-sheet__panel">
|
||||
<view class="picker-sheet__header">
|
||||
<view>
|
||||
<view class="picker-sheet__title">{{ pickerMeta.title }}</view>
|
||||
<view class="picker-sheet__desc">{{ pickerMeta.desc }}</view>
|
||||
</view>
|
||||
<view class="picker-sheet__close" @click="closePicker">关闭</view>
|
||||
</view>
|
||||
<view v-if="pageLoading" class="brand-state">
|
||||
<view class="brand-state__title">正在加载品牌</view>
|
||||
<view class="brand-state__desc">请稍候</view>
|
||||
</view>
|
||||
|
||||
<view class="field-box">
|
||||
<input v-model="pickerKeyword" class="field-input" :placeholder="pickerMeta.searchPlaceholder" />
|
||||
</view>
|
||||
<view v-else-if="loadError" class="brand-state">
|
||||
<view class="brand-state__title">品牌加载失败</view>
|
||||
<view class="brand-state__desc">{{ loadError }}</view>
|
||||
<view class="brand-state__button" @click="skipBrand">跳过品牌</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="picker-sheet__list">
|
||||
<scroll-view v-else scroll-y class="brand-list">
|
||||
<view v-if="groupedBrands.length === 0" class="brand-state brand-state--embedded">
|
||||
<view class="brand-state__title">暂无匹配品牌</view>
|
||||
<view class="brand-state__desc">可跳过品牌后继续确认订单</view>
|
||||
<view class="brand-state__button" @click="skipBrand">跳过品牌</view>
|
||||
</view>
|
||||
|
||||
<view v-for="group in groupedBrands" :key="group.letter" class="brand-group">
|
||||
<view class="brand-group__letter">{{ group.letter }}</view>
|
||||
<view class="brand-group__items">
|
||||
<view
|
||||
v-for="item in filteredPickerOptions"
|
||||
:key="pickerOptionKey(item)"
|
||||
:class="['picker-sheet__option', isPickerCurrent(item) ? 'picker-sheet__option--selected' : '']"
|
||||
@click="confirmPicker(item)"
|
||||
v-for="item in group.list"
|
||||
:key="`${group.letter}-${item.id}-${item.displayName}`"
|
||||
:class="['brand-item', selectedBrandId === item.id ? 'brand-item--selected' : '']"
|
||||
@click="selectBrand(item)"
|
||||
>
|
||||
<view class="picker-sheet__option-title">{{ pickerMeta.label(item) }}</view>
|
||||
<view class="picker-sheet__option-action">{{ isPickerCurrent(item) ? '已选' : '选择' }}</view>
|
||||
<text class="brand-item__name">{{ item.displayName }}</text>
|
||||
<text v-if="selectedBrandId === item.id" class="brand-item__selected">已选</text>
|
||||
</view>
|
||||
|
||||
<view v-if="filteredPickerOptions.length === 0" class="picker-sheet__empty">
|
||||
{{ pickerMeta.emptyText }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="fixed-action-bar">
|
||||
<view class="btn btn--secondary" @click="goBack">返回</view>
|
||||
<view :class="['btn', 'btn--primary', (!canContinue || submitting) ? 'btn--disabled' : '']" @click="goNext">
|
||||
{{ submitting ? "保存中..." : "下一步" }}
|
||||
<view class="brand-bottom">
|
||||
<view :class="['brand-bottom__button', submitting ? 'brand-bottom__button--disabled' : '']" @click="skipBrand">
|
||||
{{ submitting ? "保存中..." : "跳过品牌,继续确认" }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.selection-summary-card {
|
||||
margin-top: 20rpx;
|
||||
padding: 24rpx 26rpx;
|
||||
border-radius: 26rpx;
|
||||
<style scoped lang="scss">
|
||||
.brand-page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(132rpx + env(safe-area-inset-bottom));
|
||||
background: #f2f2f4;
|
||||
color: #252527;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.brand-page,
|
||||
.brand-page view,
|
||||
.brand-page text,
|
||||
.brand-page input {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.brand-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22rpx;
|
||||
padding: 28rpx 30rpx 22rpx;
|
||||
background: #f2f2f4;
|
||||
}
|
||||
|
||||
.brand-search__box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
border: 2rpx solid #a9a9ad;
|
||||
border-radius: 999rpx;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(237, 189, 0, 0.18);
|
||||
}
|
||||
|
||||
.selection-summary-card__title {
|
||||
color: var(--color-heading);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.selection-summary-card__desc {
|
||||
margin-top: 10rpx;
|
||||
color: var(--color-body);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.selector-card {
|
||||
.brand-search__icon {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
margin-right: 14rpx;
|
||||
border: 5rpx solid #b7b7bb;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.brand-search__icon::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -10rpx;
|
||||
bottom: -8rpx;
|
||||
width: 18rpx;
|
||||
height: 5rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #b7b7bb;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.brand-search__input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 68rpx;
|
||||
color: #252527;
|
||||
font-size: 28rpx;
|
||||
line-height: 68rpx;
|
||||
}
|
||||
|
||||
.brand-search__cancel {
|
||||
flex-shrink: 0;
|
||||
color: #252527;
|
||||
font-size: 30rpx;
|
||||
line-height: 72rpx;
|
||||
}
|
||||
|
||||
.brand-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
padding: 0 30rpx 20rpx;
|
||||
background: #f2f2f4;
|
||||
}
|
||||
|
||||
.brand-summary__service,
|
||||
.brand-summary__category {
|
||||
min-height: 44rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ffffff;
|
||||
color: #6a6a6d;
|
||||
font-size: 23rpx;
|
||||
line-height: 44rpx;
|
||||
}
|
||||
|
||||
.brand-summary__category {
|
||||
color: #8a6d00;
|
||||
}
|
||||
|
||||
.brand-summary__skip {
|
||||
margin-left: auto;
|
||||
color: #edbd00;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
line-height: 44rpx;
|
||||
}
|
||||
|
||||
.brand-list {
|
||||
height: calc(100vh - 252rpx);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.brand-group__letter {
|
||||
height: 72rpx;
|
||||
padding: 0 30rpx;
|
||||
background: #f2f2f4;
|
||||
color: #6a6a6d;
|
||||
font-size: 32rpx;
|
||||
line-height: 72rpx;
|
||||
}
|
||||
|
||||
.brand-group__items {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.brand-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
margin-top: 12rpx;
|
||||
padding: 24rpx 24rpx;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--input-border);
|
||||
background: #ffffff;
|
||||
min-height: 82rpx;
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 1px solid #f0f0f2;
|
||||
}
|
||||
|
||||
.selector-card--disabled {
|
||||
opacity: 0.56;
|
||||
.brand-item--selected {
|
||||
background: rgba(237, 189, 0, 0.08);
|
||||
}
|
||||
|
||||
.selector-card--selected {
|
||||
border-color: rgba(237, 189, 0, 0.36);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(237, 189, 0, 0.18), transparent 32%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(247, 238, 223, 0.98) 100%);
|
||||
box-shadow:
|
||||
0 14rpx 30rpx rgba(237, 189, 0, 0.1),
|
||||
0 0 0 2rpx rgba(237, 189, 0, 0.08) inset;
|
||||
}
|
||||
|
||||
.selector-card__badge {
|
||||
position: static;
|
||||
min-height: 38rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: var(--radius-pill);
|
||||
background: linear-gradient(135deg, rgba(237, 189, 0, 0.16) 0%, rgba(237, 189, 0, 0.22) 100%);
|
||||
color: var(--color-brand-gold-deep);
|
||||
font-size: 22rpx;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 38rpx;
|
||||
border: 1px solid rgba(237, 189, 0, 0.18);
|
||||
}
|
||||
|
||||
.selector-card__main {
|
||||
.brand-item__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #a0a0a4;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.selector-card__side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 12rpx;
|
||||
.brand-item--selected .brand-item__name {
|
||||
color: #252527;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.brand-item__selected {
|
||||
flex-shrink: 0;
|
||||
color: #edbd00;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.selector-card__value {
|
||||
color: var(--color-heading);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
.brand-state {
|
||||
margin: 40rpx 30rpx;
|
||||
padding: 46rpx 32rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.brand-state--embedded {
|
||||
margin-top: 68rpx;
|
||||
}
|
||||
|
||||
.brand-state__title {
|
||||
color: #252527;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.brand-state__desc {
|
||||
margin-top: 10rpx;
|
||||
color: #9a9a9d;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.selector-card__meta {
|
||||
margin-top: 8rpx;
|
||||
color: var(--color-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.6;
|
||||
.brand-state__button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 220rpx;
|
||||
height: 68rpx;
|
||||
margin-top: 26rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #edbd00;
|
||||
color: #ffffff;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.selector-card__action {
|
||||
min-width: 96rpx;
|
||||
color: var(--color-accent);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.picker-sheet {
|
||||
.brand-bottom {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 160;
|
||||
}
|
||||
|
||||
.picker-sheet__mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(20, 20, 18, 0.38);
|
||||
}
|
||||
|
||||
.picker-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);
|
||||
z-index: 80;
|
||||
padding: 18rpx 30rpx calc(18rpx + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #e9e9eb;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.picker-sheet__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.picker-sheet__title {
|
||||
color: var(--color-heading);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.picker-sheet__desc {
|
||||
margin-top: 8rpx;
|
||||
color: var(--color-body);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.picker-sheet__close {
|
||||
color: var(--color-accent);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.picker-sheet__list {
|
||||
max-height: 58vh;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.picker-sheet__option {
|
||||
.brand-bottom__button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 6rpx;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
justify-content: center;
|
||||
height: 82rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #252527;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.picker-sheet__option--selected {
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 18rpx;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(237, 189, 0, 0.22);
|
||||
box-shadow: 0 10rpx 24rpx rgba(237, 189, 0, 0.08);
|
||||
}
|
||||
|
||||
.picker-sheet__option-title {
|
||||
color: var(--color-heading);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.picker-sheet__option-action {
|
||||
color: var(--color-accent);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.picker-sheet__empty {
|
||||
padding: 48rpx 0 24rpx;
|
||||
color: var(--color-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
text-align: center;
|
||||
.brand-bottom__button--disabled {
|
||||
opacity: 0.58;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -83,13 +83,10 @@ const categoryCards = computed(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
function goService(provider = "anxinyan") {
|
||||
uni.navigateTo({ url: `/pages/appraisal/service?provider=${provider}` });
|
||||
}
|
||||
|
||||
function goCategory(category: string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/appraisal/service?provider=anxinyan&category=${encodeURIComponent(category)}`,
|
||||
function goService() {
|
||||
uni.showToast({
|
||||
title: "暂不支持自助下单",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -160,7 +157,7 @@ onShow(fetchHome);
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="home-platform" @click="goService()">
|
||||
<view class="home-platform">
|
||||
<view class="home-platform__content">
|
||||
<view class="home-platform__title">{{ heroBanner.subtitle }}</view>
|
||||
<view class="home-platform__desc">{{ heroBanner.description }}</view>
|
||||
@@ -170,7 +167,6 @@ onShow(fetchHome);
|
||||
<text class="home-platform__chip">进度可追踪</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="home-platform__button">立即鉴定</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -181,7 +177,7 @@ onShow(fetchHome);
|
||||
v-for="card in homeServiceCards"
|
||||
:key="card.service_provider"
|
||||
:class="['home-service-card', `home-service-card--${card.theme}`]"
|
||||
@click="goService(card.service_provider)"
|
||||
@click="goService"
|
||||
>
|
||||
<view class="home-service-card__watermark">{{ card.theme === "blue" ? "CIC" : "" }}</view>
|
||||
<view class="home-service-card__title">
|
||||
@@ -201,7 +197,6 @@ onShow(fetchHome);
|
||||
v-for="item in categoryCards"
|
||||
:key="item.categoryId || item.categoryName"
|
||||
class="home-category-card"
|
||||
@click="goCategory(item.categoryName)"
|
||||
>
|
||||
<view class="home-category-card__title">{{ item.displayName }}</view>
|
||||
<image
|
||||
@@ -426,8 +421,7 @@ onShow(fetchHome);
|
||||
background-size: 34rpx 34rpx;
|
||||
}
|
||||
|
||||
.home-platform__content,
|
||||
.home-platform__button {
|
||||
.home-platform__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -466,20 +460,6 @@ onShow(fetchHome);
|
||||
line-height: 32rpx;
|
||||
}
|
||||
|
||||
.home-platform__button {
|
||||
flex-shrink: 0;
|
||||
min-width: 132rpx;
|
||||
height: 58rpx;
|
||||
margin-left: 14rpx;
|
||||
border-radius: 30rpx;
|
||||
background: #e8b700;
|
||||
color: #fff;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
line-height: 58rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
@@ -719,14 +699,5 @@ onShow(fetchHome);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.home-platform {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.home-platform__button {
|
||||
margin-top: 22rpx;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { appApi, type OrderDetailData, type UserAddressItem } from "../../api/app";
|
||||
import { orderDetailFallback } from "../../mocks/app";
|
||||
import { resolveErrorMessage, showErrorToast, showInfoToast } from "../../utils/feedback";
|
||||
import { launchOrderPayment, prepareMiniProgramPaymentIdentity } from "../../utils/payment";
|
||||
import { getPrivacyMode, maskOrderNo } from "../../utils/privacy";
|
||||
|
||||
const detail = ref<OrderDetailData>(orderDetailFallback);
|
||||
@@ -16,6 +17,8 @@ const addressOptions = ref<UserAddressItem[]>([]);
|
||||
const loading = ref(false);
|
||||
const pageReady = ref(false);
|
||||
const loadError = ref("");
|
||||
const paymentLaunching = ref(false);
|
||||
const cancelSubmitting = ref(false);
|
||||
|
||||
const serviceProviderText = computed(() =>
|
||||
detail.value.order_info.service_provider === "zhongjian" ? "中检鉴定" : "实物鉴定",
|
||||
@@ -68,8 +71,10 @@ const returnLogisticsStatusText = computed(
|
||||
() => detail.value.return_logistics?.provider_status_text || detail.value.return_logistics?.tracking_status_text || "",
|
||||
);
|
||||
const canEditReturnAddress = computed(() => detail.value.order_info.can_edit_return_address);
|
||||
const isPendingPayment = computed(() => detail.value.order_info.order_status === "pending_payment");
|
||||
|
||||
const heroTagClass = computed(() => {
|
||||
if (detail.value.order_info.order_status === "pending_payment") return "tag tag--warning";
|
||||
if (detail.value.order_info.order_status === "pending_supplement") return "tag tag--warning";
|
||||
if (detail.value.order_info.order_status === "pending_shipping" || detail.value.order_info.order_status === "pending_submission") {
|
||||
return "tag tag--accent";
|
||||
@@ -82,6 +87,8 @@ const heroTagClass = computed(() => {
|
||||
|
||||
const heroTagText = computed(() => {
|
||||
switch (detail.value.order_info.order_status) {
|
||||
case "pending_payment":
|
||||
return "待支付";
|
||||
case "pending_shipping":
|
||||
return shippingSubmitted.value ? "待签收" : "待寄送";
|
||||
case "pending_submission":
|
||||
@@ -93,6 +100,8 @@ const heroTagText = computed(() => {
|
||||
case "report_published":
|
||||
case "completed":
|
||||
return "已出报告";
|
||||
case "cancelled":
|
||||
return "已取消";
|
||||
default:
|
||||
return "处理中";
|
||||
}
|
||||
@@ -100,6 +109,8 @@ const heroTagText = computed(() => {
|
||||
|
||||
const focusTitle = computed(() => {
|
||||
switch (detail.value.order_info.order_status) {
|
||||
case "pending_payment":
|
||||
return "下一步请完成支付";
|
||||
case "pending_shipping":
|
||||
return shippingSubmitted.value ? "下一步等待鉴定中心签收" : "下一步请先寄送商品";
|
||||
case "pending_supplement":
|
||||
@@ -108,6 +119,8 @@ const focusTitle = computed(() => {
|
||||
return hasReturnAddress.value ? "下一步等待平台寄回物品" : "下一步请先确认寄回地址";
|
||||
case "completed":
|
||||
return returnReceived.value ? "本次订单已完成" : hasReturnLogistics.value ? "物品已寄回,请留意签收" : "结果已经可以查看";
|
||||
case "cancelled":
|
||||
return "订单已取消";
|
||||
default:
|
||||
return "当前订单正在继续处理";
|
||||
}
|
||||
@@ -119,6 +132,8 @@ const focusDesc = computed(() => {
|
||||
}
|
||||
|
||||
switch (detail.value.order_info.order_status) {
|
||||
case "pending_payment":
|
||||
return "支付成功后,订单会进入待寄送商品环节,平台再开始安排后续鉴定作业。";
|
||||
case "pending_shipping":
|
||||
return shippingSubmitted.value
|
||||
? "运单已登记成功,无需再次寄送商品,后续等待鉴定中心签收后继续处理。"
|
||||
@@ -133,6 +148,8 @@ const focusDesc = computed(() => {
|
||||
: hasReturnLogistics.value
|
||||
? "平台已登记回寄运单,可在本页查看回寄物流进度和最新节点。"
|
||||
: "正式报告已生成,可前往报告页查看结果并完成验真。";
|
||||
case "cancelled":
|
||||
return "该订单已取消,不会继续进入鉴定流程。如仍需服务,请重新发起鉴定。";
|
||||
default:
|
||||
return detail.value.order_info.status_desc;
|
||||
}
|
||||
@@ -157,6 +174,8 @@ const progressSectionDesc = computed(() =>
|
||||
|
||||
const supportHelpText = computed(() => {
|
||||
switch (detail.value.order_info.order_status) {
|
||||
case "pending_payment":
|
||||
return "如果支付页面无法打开或支付后状态未更新,可先刷新本页,也可以联系客服协助核查。";
|
||||
case "pending_supplement":
|
||||
return "如果暂时无法补图,可先联系客服说明情况,我们会协助判断下一步处理方式。";
|
||||
case "pending_shipping":
|
||||
@@ -173,6 +192,9 @@ const supportHelpText = computed(() => {
|
||||
|
||||
const primaryActionText = computed(() =>
|
||||
{
|
||||
if (isPendingPayment.value && paymentLaunching.value) {
|
||||
return "支付中...";
|
||||
}
|
||||
if (detail.value.order_info.order_status === "report_published" && !hasReturnAddress.value) {
|
||||
return "确认寄回地址";
|
||||
}
|
||||
@@ -183,6 +205,10 @@ const primaryActionText = computed(() =>
|
||||
},
|
||||
);
|
||||
|
||||
const secondaryActionText = computed(() =>
|
||||
isPendingPayment.value ? (cancelSubmitting.value ? "取消中..." : "取消订单") : detail.value.available_actions.secondary_action,
|
||||
);
|
||||
|
||||
async function fetchDetail() {
|
||||
if (!orderId.value) return;
|
||||
loading.value = true;
|
||||
@@ -205,6 +231,24 @@ async function fetchDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
async function syncPendingPaymentStatus(silent = true) {
|
||||
if (!orderId.value || !isPendingPayment.value) return;
|
||||
try {
|
||||
const data = await appApi.getOrderPaymentStatus(orderId.value);
|
||||
if (data.order_status !== detail.value.order_info.order_status || data.payment_status !== detail.value.order_info.payment_status) {
|
||||
await fetchDetail();
|
||||
return;
|
||||
}
|
||||
detail.value.payment = data.payment;
|
||||
detail.value.order_info.display_status = data.display_status;
|
||||
detail.value.order_info.status_desc = data.order_status === "pending_payment" ? "请完成支付后继续本次鉴定服务" : detail.value.order_info.status_desc;
|
||||
} catch (error) {
|
||||
if (!silent) {
|
||||
showErrorToast(error, "支付状态同步失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAddressOptions() {
|
||||
addressesLoading.value = true;
|
||||
try {
|
||||
@@ -217,7 +261,31 @@ async function fetchAddressOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrimaryAction() {
|
||||
async function payCurrentOrder() {
|
||||
if (paymentLaunching.value || !orderId.value) return;
|
||||
paymentLaunching.value = true;
|
||||
try {
|
||||
await prepareMiniProgramPaymentIdentity();
|
||||
const data = await appApi.retryOrderPayment(orderId.value);
|
||||
if (data.payment.status === "paid") {
|
||||
showInfoToast("订单已支付");
|
||||
await fetchDetail();
|
||||
return;
|
||||
}
|
||||
launchOrderPayment(data.payment);
|
||||
} catch (error) {
|
||||
showErrorToast(error, "支付发起失败");
|
||||
await syncPendingPaymentStatus(true);
|
||||
} finally {
|
||||
paymentLaunching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePrimaryAction() {
|
||||
if (isPendingPayment.value) {
|
||||
await payCurrentOrder();
|
||||
return;
|
||||
}
|
||||
if (detail.value.order_info.order_status === "report_published" && !hasReturnAddress.value) {
|
||||
openReturnAddressSheet();
|
||||
return;
|
||||
@@ -249,12 +317,46 @@ function handlePrimaryAction() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!action) {
|
||||
uni.pageScrollTo({
|
||||
selector: "#order-progress",
|
||||
duration: 260,
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.showToast({
|
||||
title: action,
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
|
||||
function handleSecondaryAction() {
|
||||
if (!isPendingPayment.value) {
|
||||
contactService();
|
||||
return;
|
||||
}
|
||||
if (cancelSubmitting.value) return;
|
||||
|
||||
uni.showModal({
|
||||
title: "取消订单",
|
||||
content: "确认取消这笔待支付订单吗?取消后不会进入鉴定流程。",
|
||||
success: async (result) => {
|
||||
if (!result.confirm) return;
|
||||
cancelSubmitting.value = true;
|
||||
try {
|
||||
await appApi.cancelOrder(detail.value.order_info.order_id);
|
||||
showInfoToast("订单已取消");
|
||||
await fetchDetail();
|
||||
} catch (error) {
|
||||
showErrorToast(error, "订单取消失败");
|
||||
await syncPendingPaymentStatus(true);
|
||||
} finally {
|
||||
cancelSubmitting.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function contactService() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/support/create?ticket_type=order_issue&order_id=${detail.value.order_info.order_id}&prefill_title=${encodeURIComponent("订单问题咨询")}`,
|
||||
@@ -314,7 +416,10 @@ onLoad((options) => {
|
||||
}
|
||||
});
|
||||
|
||||
onShow(fetchDetail);
|
||||
onShow(async () => {
|
||||
await fetchDetail();
|
||||
await syncPendingPaymentStatus(true);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -342,6 +447,7 @@ onShow(fetchDetail);
|
||||
<text class="meta-chip">订单号 {{ maskOrderNo(detail.order_info.order_no, privacyMode) }}</text>
|
||||
<text class="meta-chip">鉴定单号 {{ maskOrderNo(detail.order_info.appraisal_no, privacyMode) }}</text>
|
||||
<text class="meta-chip">{{ serviceProviderText }}</text>
|
||||
<text v-if="detail.order_info.price_package_name" class="meta-chip">{{ detail.order_info.price_package_name }}</text>
|
||||
<text v-if="detail.order_info.estimated_finish_time" class="meta-chip">预计 {{ detail.order_info.estimated_finish_time }}</text>
|
||||
</view>
|
||||
|
||||
@@ -563,7 +669,7 @@ onShow(fetchDetail);
|
||||
</view>
|
||||
|
||||
<view class="fixed-action-bar">
|
||||
<view class="btn btn--secondary" @click="contactService">联系客服</view>
|
||||
<view class="btn btn--secondary" @click="handleSecondaryAction">{{ secondaryActionText }}</view>
|
||||
<view class="btn btn--primary" @click="handlePrimaryAction">{{ primaryActionText }}</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const heroStats = computed(() => {
|
||||
stats.completed += 1;
|
||||
continue;
|
||||
}
|
||||
if (item.order_status === "pending_supplement") {
|
||||
if (["pending_payment", "pending_supplement"].includes(item.order_status)) {
|
||||
stats.pending += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -146,7 +146,7 @@ onShow(async () => {
|
||||
</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'"
|
||||
: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>
|
||||
@@ -155,7 +155,10 @@ onShow(async () => {
|
||||
<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__provider">
|
||||
{{ item.service_provider === "zhongjian" ? "中检鉴定" : "安心验鉴定" }}
|
||||
<text v-if="item.price_package_name"> / {{ item.price_package_name }}</text>
|
||||
</view>
|
||||
<view class="order-card__action">{{ item.primary_action }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -75,11 +75,6 @@ const productItems = computed(() => {
|
||||
detail.value.report_header.service_provider_text || serviceProviderText(detail.value.report_header.service_provider),
|
||||
);
|
||||
|
||||
const appraiserName = textValue(detail.value.appraisal_info?.appraiser_name)
|
||||
|| textValue(detail.value.appraisal_info?.reviewer_name)
|
||||
|| textValue(detail.value.report_header.report_entry_admin_name);
|
||||
appendProductItem(items, "鉴定师", appraiserName);
|
||||
|
||||
return items;
|
||||
});
|
||||
const publishTime = computed(() => detail.value.report_header.publish_time || "-");
|
||||
@@ -133,7 +128,7 @@ function appendProductItem(items: ProductDisplayItem[], label: unknown, value: u
|
||||
const labelText = textValue(label);
|
||||
const valueText = textValue(value);
|
||||
const remarkText = textValue(remark);
|
||||
if (!labelText || (!valueText && !remarkText) || items.some((item) => item.label === labelText)) return;
|
||||
if (labelText === "鉴定师" || !labelText || (!valueText && !remarkText) || items.some((item) => item.label === labelText)) return;
|
||||
items.push({
|
||||
label: labelText,
|
||||
value: valueText || "-",
|
||||
|
||||
BIN
user-app/src/static/appraisal/service-anxinyan-hero.png
Normal file
BIN
user-app/src/static/appraisal/service-anxinyan-hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
user-app/src/static/appraisal/service-step-pay.png
Normal file
BIN
user-app/src/static/appraisal/service-step-pay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 736 KiB |
@@ -96,6 +96,10 @@ export const useAppraisalStore = defineStore("appraisal", {
|
||||
draftId: 0,
|
||||
serviceProvider: "anxinyan",
|
||||
serviceMode: "physical",
|
||||
pricePackageId: 0,
|
||||
pricePackageName: "",
|
||||
pricePackageCode: "",
|
||||
pricePackagePrice: 0,
|
||||
currentStep: 1,
|
||||
product: initialProduct(),
|
||||
extra: initialExtra(),
|
||||
@@ -128,6 +132,10 @@ export const useAppraisalStore = defineStore("appraisal", {
|
||||
draftId: this.draftId,
|
||||
serviceProvider: this.serviceProvider,
|
||||
serviceMode: this.serviceMode,
|
||||
pricePackageId: this.pricePackageId,
|
||||
pricePackageName: this.pricePackageName,
|
||||
pricePackageCode: this.pricePackageCode,
|
||||
pricePackagePrice: this.pricePackagePrice,
|
||||
currentStep: this.currentStep,
|
||||
product: this.product,
|
||||
extra: this.extra,
|
||||
@@ -144,6 +152,18 @@ export const useAppraisalStore = defineStore("appraisal", {
|
||||
this.serviceProvider = serviceProvider;
|
||||
this.persist();
|
||||
},
|
||||
setPricePackage(payload: {
|
||||
id: number;
|
||||
packageName: string;
|
||||
packageCode: string;
|
||||
price: number;
|
||||
}) {
|
||||
this.pricePackageId = payload.id;
|
||||
this.pricePackageName = payload.packageName;
|
||||
this.pricePackageCode = payload.packageCode;
|
||||
this.pricePackagePrice = payload.price;
|
||||
this.persist();
|
||||
},
|
||||
setDraft(id: number) {
|
||||
this.draftId = id;
|
||||
this.persist();
|
||||
@@ -255,6 +275,10 @@ export const useAppraisalStore = defineStore("appraisal", {
|
||||
resetForNewFlow() {
|
||||
this.serviceProvider = "anxinyan";
|
||||
this.serviceMode = "physical";
|
||||
this.pricePackageId = 0;
|
||||
this.pricePackageName = "";
|
||||
this.pricePackageCode = "";
|
||||
this.pricePackagePrice = 0;
|
||||
this.draftId = 0;
|
||||
this.currentStep = 1;
|
||||
this.product = initialProduct();
|
||||
|
||||
@@ -9,14 +9,22 @@ export function isMissingDraftError(error: unknown) {
|
||||
}
|
||||
|
||||
export async function rebuildDraftFromStore(store: AppraisalStore) {
|
||||
const draft = await appraisalApi.createDraft(store.serviceProvider || "anxinyan");
|
||||
const draft = await appraisalApi.createDraft(store.serviceProvider || "anxinyan", store.pricePackageId || undefined, store.pricePackageCode || undefined);
|
||||
store.setDraft(draft.draft_id);
|
||||
store.setPricePackage({
|
||||
id: draft.price_package_id,
|
||||
packageName: draft.price_package_name,
|
||||
packageCode: draft.price_package_code,
|
||||
price: draft.price_package_price,
|
||||
});
|
||||
|
||||
if (store.product.categoryId) {
|
||||
await appraisalApi.saveDraft({
|
||||
draft_id: draft.draft_id,
|
||||
current_step: 3,
|
||||
service_provider: store.serviceProvider,
|
||||
price_package_id: store.pricePackageId,
|
||||
price_package_code: store.pricePackageCode,
|
||||
product_info: {
|
||||
category_id: store.product.categoryId,
|
||||
brand_id: store.product.brandId || 0,
|
||||
|
||||
@@ -23,6 +23,14 @@ const PUBLIC_PAGES = new Set([
|
||||
"/pages/auth/wechat-bind",
|
||||
]);
|
||||
|
||||
const AUTH_REQUIRED_PAGES = new Set([
|
||||
"/pages/appraisal/product",
|
||||
"/pages/appraisal/confirm",
|
||||
"/pages/appraisal/success",
|
||||
"/pages/appraisal/upload",
|
||||
"/pages/appraisal/extra",
|
||||
]);
|
||||
|
||||
let redirecting = false;
|
||||
|
||||
function normalizePath(path: string) {
|
||||
@@ -146,6 +154,11 @@ export function isPublicPage(urlOrPath: string) {
|
||||
return PUBLIC_PAGES.has(path);
|
||||
}
|
||||
|
||||
export function isAuthRequiredPage(urlOrPath: string) {
|
||||
const { path } = splitUrl(urlOrPath);
|
||||
return AUTH_REQUIRED_PAGES.has(path);
|
||||
}
|
||||
|
||||
export function getCurrentPageUrl() {
|
||||
const pages = getCurrentPages();
|
||||
const current = pages[pages.length - 1] as
|
||||
@@ -181,7 +194,7 @@ export function redirectToLogin(targetUrl?: string) {
|
||||
|
||||
export function ensureAuthenticatedPageAccess() {
|
||||
const currentUrl = getCurrentPageUrl();
|
||||
if (!currentUrl || isPublicPage(currentUrl) || isLoggedIn()) {
|
||||
if (!currentUrl || !isAuthRequiredPage(currentUrl) || isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
45
user-app/src/utils/payment.ts
Normal file
45
user-app/src/utils/payment.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { authApi } from "../api/auth";
|
||||
import type { PaymentLaunchInfo } from "../api/app";
|
||||
|
||||
function miniProgramLoginCode() {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: "weixin",
|
||||
success: (result) => {
|
||||
const code = String(result.code || "");
|
||||
if (!code) {
|
||||
reject(new Error("小程序登录 code 为空"));
|
||||
return;
|
||||
}
|
||||
resolve(code);
|
||||
},
|
||||
fail: (error) => reject(error),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function prepareMiniProgramPaymentIdentity() {
|
||||
// #ifdef MP-WEIXIN
|
||||
const code = await miniProgramLoginCode();
|
||||
await authApi.bindMiniProgramOpenid(code);
|
||||
// #endif
|
||||
}
|
||||
|
||||
export function launchOrderPayment(payment: PaymentLaunchInfo) {
|
||||
if (payment.channel === "mini_program") {
|
||||
if (!payment.plugin_url) {
|
||||
throw new Error("小程序支付链接未生成");
|
||||
}
|
||||
uni.navigateTo({ url: payment.plugin_url });
|
||||
return;
|
||||
}
|
||||
|
||||
if (payment.cashier_url) {
|
||||
// #ifdef H5
|
||||
window.location.href = payment.cashier_url;
|
||||
return;
|
||||
// #endif
|
||||
}
|
||||
|
||||
throw new Error("支付链接未生成");
|
||||
}
|
||||
Reference in New Issue
Block a user