feat(release): v3.4.13 experience optimizations across admin, mini-user, and H5 login

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-05 22:34:29 +08:00
parent 490c26a7e1
commit 1c4b986924
29 changed files with 617 additions and 23 deletions
@@ -141,6 +141,14 @@ export default function OrderConfirmPickupPage() {
const ready = await ensurePayReady(returnPath);
if (!ready) return;
const { confirm } = await Taro.showModal({
title: '确认提交订单',
content: `确认提交现场提货订单?共 ${quantity} 瓶,应付 ¥${Number(preview?.payAmount ?? 0).toFixed(2)}`,
confirmText: '确认提交',
cancelText: '再想想',
});
if (!confirm) return;
setLoading(true);
setMsg('');
try {
@@ -44,8 +44,15 @@ type OrderDetail = {
orderType?: string;
isProxyOrder?: boolean;
proxyPartnerName?: string | null;
deliveryType?: string;
items?: OrderItem[];
wechatConfirm?: WechatConfirmPayload | null;
delivery?: {
provider?: string;
trackingNo?: string;
logisticsCompany?: string;
manualQueryUrl?: string;
} | null;
};
const STATUS_LABELS: Record<string, string> = {
@@ -99,6 +106,13 @@ export default function OrderDetailPage() {
const canPay = !!order && order.status === 'PENDING_PAY' && !isReship;
const canConfirmReceive =
!!order && !isReship && !isProxy && ['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || '');
const canViewLogistics =
!!order &&
order.deliveryType !== 'ON_SITE_PICKUP' &&
!isReship &&
['PENDING_SHIP', 'OUT_WAREHOUSE', 'SHIPPING', 'SHIPPED', 'PENDING_RECEIVE', 'DELIVERED', 'COMPLETED'].includes(
order.status || '',
);
const item = order?.items?.[0];
const productName = item?.productName || order?.productName || '杜康商品';
@@ -134,6 +148,11 @@ export default function OrderDetailPage() {
Taro.navigateTo({ url: '/pages/customer-service/index' });
}
function goLogistics() {
if (!order) return;
Taro.navigateTo({ url: `/pages/order-logistics/index?id=${order.id}` });
}
async function confirmReceive() {
if (!order || !canConfirmReceive || confirming) return;
@@ -211,6 +230,11 @@ export default function OrderDetailPage() {
{isProxy && order.proxyPartnerName ? (
<Text className="order-proxy-hint"> {order.proxyPartnerName} </Text>
) : null}
{canViewLogistics ? (
<Text className="order-logistics-link" onClick={goLogistics}>
</Text>
) : null}
</View>
<View className="order-card">
<Text className="order-card-title"></Text>
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '物流追踪',
});
@@ -0,0 +1,160 @@
import { useEffect, useState } from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import { request, toast } from '../../lib/api';
import { usePageView } from '../../lib/usePageView';
type OrderDelivery = {
provider?: string;
trackingNo?: string;
logisticsCompany?: string;
manualQueryUrl?: string;
};
type TrackNode = {
trackInfo?: string;
createdAt?: string;
};
type OrderDetail = {
id: string;
orderNo?: string;
status?: string;
delivery?: OrderDelivery | null;
};
const STATUS_LABELS: Record<string, string> = {
PENDING_SHIP: '待发货',
OUT_WAREHOUSE: '出库中',
SHIPPING: '配送中',
SHIPPED: '配送中',
PENDING_RECEIVE: '待签收',
DELIVERED: '待签收',
COMPLETED: '已完成',
};
function deliveryProviderLabel(provider?: string, company?: string) {
if (company) return company;
if (!provider || provider === 'MOCK' || provider === 'XFX' || provider === 'XIAOFEIXIA') {
return '小飞侠配送';
}
if (provider === 'LOGISTICS') return '快递配送';
return provider;
}
function formatDateTime(value: string) {
return String(value).slice(0, 19).replace('T', ' ');
}
export default function OrderLogisticsPage() {
const router = useRouter();
const orderId = router.params.id ?? '';
usePageView('order_logistics_view', orderId ? { orderId } : undefined);
const [order, setOrder] = useState<OrderDetail | null>(null);
const [trackNodes, setTrackNodes] = useState<TrackNode[]>([]);
useEffect(() => {
if (!orderId) return;
let cancelled = false;
(async () => {
try {
const data = await request<OrderDetail>(`/trade/orders/${orderId}`);
if (cancelled) return;
setOrder(data);
if (data.delivery?.trackingNo || data.delivery?.provider === 'XFX') {
try {
const track = await request<{ nodes?: TrackNode[] }>(`/trade/orders/${orderId}/track`);
if (!cancelled) setTrackNodes(track.nodes ?? []);
} catch {
if (!cancelled) setTrackNodes([]);
}
}
} catch (e) {
if (!cancelled) toast(e instanceof Error ? e.message : '加载失败');
}
})();
return () => {
cancelled = true;
};
}, [orderId]);
function openManualQuery() {
const url = order?.delivery?.manualQueryUrl?.trim();
if (!url) {
toast('暂无物流查询链接');
return;
}
if (process.env.TARO_ENV === 'h5' && typeof window !== 'undefined') {
window.open(url, '_blank');
return;
}
Taro.setClipboardData({ data: url })
.then(() => toast('查询链接已复制,请在浏览器中打开'))
.catch(() => toast('无法打开物流查询'));
}
const delivery = order?.delivery;
return (
<PageShell variant="sub" className="order-logistics-page">
<SubPageHeader title="物流追踪" />
<View className="sub-page-body">
{!order ? (
<View className="u-empty"></View>
) : (
<>
<View className="order-card">
<Text className="order-card-title"></Text>
<Text className="order-list-status">
{STATUS_LABELS[order.status || ''] || order.status || '处理中'}
</Text>
</View>
<View className="order-card">
<Text className="order-card-title"></Text>
<View className="order-row">
<Text className="order-row-label"></Text>
<Text className="order-row-value">
{deliveryProviderLabel(delivery?.provider, delivery?.logisticsCompany)}
</Text>
</View>
{delivery?.trackingNo ? (
<View className="order-row">
<Text className="order-row-label"></Text>
<Text className="order-row-value">{delivery.trackingNo}</Text>
</View>
) : null}
{delivery?.manualQueryUrl ? (
<View className="order-row">
<Text className="order-row-label"></Text>
<Text className="order-row-value order-row-value--link" onClick={openManualQuery}>
</Text>
</View>
) : null}
{!delivery?.trackingNo && !delivery?.manualQueryUrl ? (
<Text className="u-muted"></Text>
) : null}
</View>
{trackNodes.length > 0 ? (
<View className="order-card">
<Text className="order-card-title"></Text>
{trackNodes.map((node, index) => (
<View key={`${node.trackInfo}-${index}`} className="order-logistics-node">
<Text className="order-logistics-node-time">
{node.createdAt ? formatDateTime(node.createdAt) : '—'}
</Text>
<Text className="order-logistics-node-info">{node.trackInfo || '—'}</Text>
</View>
))}
</View>
) : null}
</>
)}
</View>
</PageShell>
);
}
@@ -11,7 +11,6 @@ import type { ProductDetailContentDto } from '@dukang/shared-types';
import PageShell from '../../components/PageShell';
import PageNavBar from '../../components/PageNavBar';
import ProductCarousel from '../../components/ProductCarousel';
import ShareNavButton from '../../components/ShareNavButton';
import WechatShareReady from '../../components/WechatShareReady';
import { goLogin } from '../../lib/auth-nav';
import { ensurePayReady } from '../../lib/pay-ready';
@@ -167,12 +166,11 @@ export default function ProductDetailPage() {
solid={headerSolid}
titleVisible={headerSolid}
onBack={goBack}
right={<ShareNavButton payload={sharePayload} />}
/>
<View className="product-detail-main">
<View className="product-detail-hero full-bleed">
<ProductCarousel images={carouselImages} alt={product.name} variant="detail" />
<ProductCarousel images={carouselImages} alt={product.name} variant="detail" previewable />
</View>
<View className="product-detail-info">
@@ -15,6 +15,8 @@ import ShareNavButton from '../../components/ShareNavButton';
import StoreRedeemMarquee from '../../components/StoreRedeemMarquee';
import WechatShareReady from '../../components/WechatShareReady';
import { request, toast } from '../../lib/api';
import { maskPhone } from '../../lib/phone';
import { track } from '../../lib/analytics';
import { formatShanghaiDateTime } from '../../lib/datetime';
import {
DEFAULT_SHARE_DESC,
@@ -154,6 +156,7 @@ export default function StoreDetailPage() {
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState('');
const [headerSolid, setHeaderSolid] = useState(false);
const [expandedPackages, setExpandedPackages] = useState<Record<number, boolean>>({});
const storeRef = useRef<Store | null>(null);
storeRef.current = store;
@@ -271,6 +274,7 @@ export default function StoreDetailPage() {
toast('暂无门店电话');
return;
}
track('store_phone_call', { storeId: store.id });
Taro.makePhoneCall({ phoneNumber: store.phone }).catch(() => toast('无法拨打电话'));
}
@@ -374,7 +378,9 @@ export default function StoreDetailPage() {
{store.phone ? (
<View className="store-detail-row">
<Text className="store-detail-meta store-detail-meta--flex">: {store.phone}</Text>
<Text className="store-detail-meta store-detail-meta--flex">
: {maskPhone(store.phone)}
</Text>
<Text className="store-detail-action" onClick={callStore}>
</Text>
@@ -399,15 +405,32 @@ export default function StoreDetailPage() {
{store.packages && store.packages.length > 0 ? (
<View className="store-detail-section">
<Text className="store-detail-section-title"></Text>
{store.packages.map((pkg, index) => (
<View key={`${pkg.name}-${index}`} className="store-detail-package-card">
{store.packages.map((pkg, index) => {
const expanded = !!expandedPackages[index];
const bodyText = `${formatRedeemAmountYuan(pkg.price)} 元 · ${pkg.dishes}`;
const longBody = bodyText.length > 48 || (pkg.otherNotes?.length ?? 0) > 40;
return (
<View
key={`${pkg.name}-${index}`}
className={`store-detail-package-card${expanded || !longBody ? '' : ' store-detail-package-card--collapsed'}`}
>
{pkg.imageUrl ? (
<Image className="store-detail-package-img" src={pkg.imageUrl} mode="aspectFill" />
) : null}
<Text className="store-detail-package-name">{pkg.name}</Text>
<Text className="store-detail-package-body">
{formatRedeemAmountYuan(pkg.price)} · {pkg.dishes}
</Text>
<View className="store-detail-package-head">
<Text className="store-detail-package-name">{pkg.name}</Text>
{longBody ? (
<Text
className="store-detail-package-toggle"
onClick={() =>
setExpandedPackages((prev) => ({ ...prev, [index]: !prev[index] }))
}
>
{expanded ? '收起' : '展开'}
</Text>
) : null}
</View>
<Text className="store-detail-package-body">{bodyText}</Text>
{pkg.usableTime ? (
<Text className="store-detail-package-meta">使{pkg.usableTime}</Text>
) : null}
@@ -415,7 +438,8 @@ export default function StoreDetailPage() {
<Text className="store-detail-package-meta">{pkg.otherNotes}</Text>
) : null}
</View>
))}
);
})}
</View>
) : null}