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:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dukang/mini-user",
|
||||
"version": "0.1.0",
|
||||
"version": "3.4.13",
|
||||
"private": true,
|
||||
"description": "杜康好客 · C 端用户微信小程序(Taro)",
|
||||
"scripts": {
|
||||
|
||||
@@ -11,6 +11,7 @@ export default defineAppConfig({
|
||||
'pages/pay/index',
|
||||
'pages/orders/index',
|
||||
'pages/order-detail/index',
|
||||
'pages/order-logistics/index',
|
||||
'pages/pickup-receive/index',
|
||||
'pages/addresses/index',
|
||||
'pages/address-edit/index',
|
||||
|
||||
@@ -20,6 +20,7 @@ export default function ProductCarousel({
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const prefix =
|
||||
variant === 'store' ? 'store-detail-carousel' : variant === 'home' ? 'home-carousel' : 'detail-carousel';
|
||||
const imageMode = variant === 'store' ? 'aspectFit' : 'aspectFill';
|
||||
|
||||
function previewAt(index: number) {
|
||||
const urls = slides.filter(Boolean);
|
||||
@@ -41,7 +42,7 @@ export default function ProductCarousel({
|
||||
<Image
|
||||
className={`${prefix}-image`}
|
||||
src={src}
|
||||
mode="aspectFill"
|
||||
mode={imageMode}
|
||||
alt={alt}
|
||||
onClick={previewable ? () => previewAt(index) : undefined}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react';
|
||||
import { finishLoginNavigate, forceReloadAfterAccountMerge, goLogin } from '../lib/auth-nav';
|
||||
import { toast } from '../lib/api';
|
||||
import { capturePromoSceneAndTouchScan } from '../lib/promo';
|
||||
import { initClientVersionChecks } from '../lib/client-version';
|
||||
import { saveWechatLoginResult } from '../lib/pay-wechat';
|
||||
import { applyWechatShare } from '../lib/wechat-share';
|
||||
import { handleWechatAuthCallback } from '../lib/wechat-auth';
|
||||
@@ -38,6 +39,7 @@ export default function WechatShareBootstrap() {
|
||||
|
||||
useEffect(() => {
|
||||
void capturePromoSceneAndTouchScan();
|
||||
initClientVersionChecks();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { fetchClientConfig } from './pay-wechat';
|
||||
|
||||
/** 与 package.json version 同步,供服务端 minClientVersion 比对 */
|
||||
export const APP_VERSION = '3.4.13';
|
||||
|
||||
function parseSemver(v: string): number[] {
|
||||
return v.split('.').map((n) => parseInt(n, 10) || 0);
|
||||
}
|
||||
|
||||
export function compareSemver(a: string, b: string): number {
|
||||
const pa = parseSemver(a);
|
||||
const pb = parseSemver(b);
|
||||
const len = Math.max(pa.length, pb.length);
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
const da = pa[i] ?? 0;
|
||||
const db = pb[i] ?? 0;
|
||||
if (da !== db) return da - db;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function checkClientVersionGate() {
|
||||
try {
|
||||
const config = await fetchClientConfig();
|
||||
const min = config.minClientVersion?.trim();
|
||||
if (min && compareSemver(APP_VERSION, min) < 0) {
|
||||
await Taro.showModal({
|
||||
title: '版本过低',
|
||||
content: '当前小程序版本过低,请更新至最新版本后继续使用。',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
/* 配置拉取失败不阻塞启动 */
|
||||
}
|
||||
}
|
||||
|
||||
export function setupWeappUpdateManager() {
|
||||
if (process.env.TARO_ENV !== 'weapp') return;
|
||||
if (typeof Taro.getUpdateManager !== 'function') return;
|
||||
|
||||
const manager = Taro.getUpdateManager();
|
||||
manager.onCheckForUpdate((res) => {
|
||||
if (!res.hasUpdate) return;
|
||||
manager.onUpdateReady(() => {
|
||||
void Taro.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本已准备好,是否重启应用?',
|
||||
confirmText: '立即更新',
|
||||
success: (r) => {
|
||||
if (r.confirm) manager.applyUpdate();
|
||||
},
|
||||
});
|
||||
});
|
||||
manager.onUpdateFailed(() => {
|
||||
void Taro.showModal({
|
||||
title: '更新失败',
|
||||
content: '新版本下载失败,请删除小程序后重新搜索打开。',
|
||||
showCancel: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function initClientVersionChecks() {
|
||||
setupWeappUpdateManager();
|
||||
void checkClientVersionGate();
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -488,3 +488,42 @@
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.order-logistics-page {
|
||||
background: var(--color-background);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.order-logistics-link {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.order-row-value--link {
|
||||
color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.order-logistics-node {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.order-logistics-node:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.order-logistics-node-time {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--color-on-surface-variant);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.order-logistics-node-info {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: var(--color-on-surface);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,15 @@
|
||||
.store-detail-carousel-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
min-height: 200px;
|
||||
max-height: 360px;
|
||||
overflow: hidden;
|
||||
background: var(--color-surface-container);
|
||||
}
|
||||
|
||||
.store-detail-carousel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.store-detail-carousel-item,
|
||||
@@ -241,6 +243,27 @@
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.store-detail-package-card--collapsed .store-detail-package-body,
|
||||
.store-detail-package-card--collapsed .store-detail-package-meta {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.store-detail-package-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.store-detail-package-toggle {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.store-detail-package-img {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
|
||||
Reference in New Issue
Block a user