feat(mini): v4.0.8 客服卡片主包落地、定位误报修复,合伙人微信内预览保存海报

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 15:23:52 +08:00
parent cdc4b82931
commit 283e6651aa
25 changed files with 291 additions and 93 deletions
+1
View File
@@ -4,6 +4,7 @@ export default defineAppConfig({
'pages/stores/index',
'pages/benefit/index',
'pages/mine/index',
'pages/open/index',
],
subPackages: [
{ root: 'pages/product-detail', pages: ['index'] },
@@ -3,7 +3,7 @@ import { Button, Text } from '@tarojs/components';
import type { ReactNode } from 'react';
import { toast } from '../lib/api';
import { getBrandAssetsSync, loadBrandAssets } from '../lib/brand-assets';
import { formatOpenCsError, openWecomCustomerServiceChat } from '../lib/wecom-cs';
import { buildCsSharePath, buildCsShareTitle, formatOpenCsError, openWecomCustomerServiceChat } from '../lib/wecom-cs';
export type ContactCsSessionContext = {
orderId?: string;
@@ -91,6 +91,9 @@ export default function ContactCsButton({
className={className}
openType="contact"
sessionFrom={buildCsSessionFrom(session)}
showMessageCard
sendMessageTitle={buildCsShareTitle(session?.orderNo)}
sendMessagePath={buildCsSharePath(session?.orderId)}
hoverClass="none"
>
{typeof children === 'string' ? <Text>{children}</Text> : children}
+4 -2
View File
@@ -130,10 +130,12 @@ export function toast(title: string, icon: 'success' | 'error' | 'none' = 'none'
const text = title.trim();
if (!text) return;
if (icon !== 'success' && showFloatingToast(text)) {
void Taro.hideToast();
void Promise.resolve(Taro.hideToast() as void | Promise<unknown>).catch(() => {});
return;
}
Taro.showToast({ title: text, icon, duration: 1800 });
void Promise.resolve(
Taro.showToast({ title: text, icon, duration: 1800 }) as void | Promise<unknown>,
).catch(() => {});
}
export type SessionPayload = {
+17 -6
View File
@@ -21,6 +21,14 @@ export type ClientErrorPayload = {
extra?: Record<string, unknown>;
};
/** 微信预期失败,不应按 P1 未捕获上报 */
const IGNORED_REJECTION =
/getLocation:fail|hideToast:fail:toast can't be found|showToast:fail|authorize:fail auth deny/i;
function shouldIgnoreRejection(message: string): boolean {
return IGNORED_REJECTION.test(message);
}
function currentPagePath(): string | undefined {
try {
const pages = Taro.getCurrentPages();
@@ -97,6 +105,7 @@ export function installClientErrorReporting(): void {
: typeof reason === 'string'
? reason
: JSON.stringify(reason);
if (shouldIgnoreRejection(message || '')) return;
const stack = reason instanceof Error ? reason.stack : undefined;
reportClientError({
level: 'error',
@@ -121,15 +130,17 @@ export function installClientErrorReporting(): void {
});
window.addEventListener('unhandledrejection', (ev) => {
const reason = ev.reason;
const message =
reason instanceof Error
? reason.message
: typeof reason === 'string'
? reason
: 'unhandledrejection';
if (shouldIgnoreRejection(message)) return;
reportClientError({
level: 'error',
category: 'unhandled_rejection',
message:
reason instanceof Error
? reason.message
: typeof reason === 'string'
? reason
: 'unhandledrejection',
message,
stack: reason instanceof Error ? reason.stack : undefined,
});
});
+1 -7
View File
@@ -14,13 +14,7 @@ export async function tryGetClientGpsLocation(): Promise<ClientGpsLocation | nul
if (process.env.TARO_ENV === 'weapp') {
try {
const Taro = (await import('@tarojs/taro')).default;
const loc = await new Promise<{ latitude: number; longitude: number }>((resolve, reject) => {
Taro.getLocation({
type: 'gcj02',
success: resolve,
fail: reject,
});
});
const loc = await Taro.getLocation({ type: 'gcj02' });
return { latitude: loc.latitude, longitude: loc.longitude };
} catch {
return null;
@@ -11,13 +11,7 @@ export type ClientGpsLocation = {
export async function tryGetClientGpsLocation(): Promise<ClientGpsLocation | null> {
try {
const loc = await new Promise<{ latitude: number; longitude: number }>((resolve, reject) => {
Taro.getLocation({
type: 'gcj02',
success: resolve,
fail: reject,
});
});
const loc = await Taro.getLocation({ type: 'gcj02' });
return { latitude: loc.latitude, longitude: loc.longitude };
} catch {
return null;
+10 -5
View File
@@ -1,13 +1,18 @@
import Taro from '@tarojs/taro';
import { fetchClientConfig } from './pay-wechat';
/** 与 package.json version 同步,供服务端 minClientVersion 比对 */
export const APP_VERSION = '3.5.16';
/** 与 package.json version 同步Taro defineConstants 注入),供 minClientVersion 比对 */
export const APP_VERSION =
(typeof TARO_APP_VERSION !== 'undefined' && String(TARO_APP_VERSION).trim()) || '4.0.4';
export const APP_VERSION_LABEL = `v${APP_VERSION}`;
export const APP_VERSION_LABEL = `v${APP_VERSION.replace(/^v/i, '')}`;
function normalizeVersion(v: string): string {
return String(v || '').trim().replace(/^v/i, '');
}
function parseSemver(v: string): number[] {
return v.split('.').map((n) => parseInt(n, 10) || 0);
return normalizeVersion(v).split('.').map((n) => parseInt(n, 10) || 0);
}
export function compareSemver(a: string, b: string): number {
@@ -89,7 +94,7 @@ async function enforceMinClientVersion(min: string) {
export async function checkClientVersionGate() {
try {
const config = await fetchClientConfig();
const min = config.minClientVersion?.trim();
const min = normalizeVersion(config.minClientVersion ?? '');
if (min && compareSemver(APP_VERSION, min) < 0) {
await enforceMinClientVersion(min);
}
+25 -9
View File
@@ -59,6 +59,23 @@ function clearLocationDenied() {
}
}
function wxErrorMessage(err: unknown): string {
if (!err) return '';
if (typeof err === 'string') return err;
if (err instanceof Error) return err.message;
if (typeof err === 'object') {
const o = err as { errMsg?: unknown; message?: unknown };
if (typeof o.errMsg === 'string' && o.errMsg) return o.errMsg;
if (typeof o.message === 'string' && o.message) return o.message;
try {
return JSON.stringify(err);
} catch {
return '';
}
}
return String(err);
}
function isDenyMessage(errMsg?: string): boolean {
return /auth deny|authorize|permission|denied|拒绝|用户拒绝|getLocation:fail/i.test(
errMsg || '',
@@ -179,14 +196,13 @@ async function promptLocationAuthOnce() {
}).catch(() => {});
}
function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
return new Promise((resolve, reject) => {
Taro.getLocation({
type: 'gcj02',
success: resolve,
fail: reject,
});
});
async function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
const setting = await Taro.getSetting().catch(() => null);
if (setting?.authSetting?.['scope.userLocation'] === false) {
throw new Error('getLocation:fail auth deny');
}
// 只用返回的 Promise,避免 callback + Promise 双重 reject 变成未捕获
return Taro.getLocation({ type: 'gcj02' });
}
async function resolveViaH5Jssdk(): Promise<ResolvedUserCity | null> {
@@ -263,7 +279,7 @@ export async function resolveUserCity(force = false): Promise<ResolvedUserCity>
return resolved;
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
const errMsg = wxErrorMessage(err);
const denied = isDenyMessage(errMsg);
if (denied && !isLocationDenied()) {
await promptLocationAuthOnce();
+25 -9
View File
@@ -57,6 +57,23 @@ function clearLocationDenied() {
}
}
function wxErrorMessage(err: unknown): string {
if (!err) return '';
if (typeof err === 'string') return err;
if (err instanceof Error) return err.message;
if (typeof err === 'object') {
const o = err as { errMsg?: unknown; message?: unknown };
if (typeof o.errMsg === 'string' && o.errMsg) return o.errMsg;
if (typeof o.message === 'string' && o.message) return o.message;
try {
return JSON.stringify(err);
} catch {
return '';
}
}
return String(err);
}
function isDenyMessage(errMsg?: string): boolean {
return /auth deny|authorize|permission|denied|拒绝|用户拒绝|getLocation:fail/i.test(
errMsg || '',
@@ -175,14 +192,13 @@ async function promptLocationAuthOnce() {
}).catch(() => {});
}
function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
return new Promise((resolve, reject) => {
Taro.getLocation({
type: 'gcj02',
success: resolve,
fail: reject,
});
});
async function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
const setting = await Taro.getSetting().catch(() => null);
if (setting?.authSetting?.['scope.userLocation'] === false) {
throw new Error('getLocation:fail auth deny');
}
// 只用返回的 Promise,避免 callback + Promise 双重 reject 变成未捕获
return Taro.getLocation({ type: 'gcj02' });
}
export async function resolveUserCity(force = false): Promise<ResolvedUserCity> {
@@ -211,7 +227,7 @@ export async function resolveUserCity(force = false): Promise<ResolvedUserCity>
return resolved;
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
const errMsg = wxErrorMessage(err);
const denied = isDenyMessage(errMsg);
if (denied && !isLocationDenied()) {
await promptLocationAuthOnce();
+14 -9
View File
@@ -4,6 +4,17 @@ export type WecomCsSessionContext = {
from?: string;
};
/** 客服消息卡片必须落主包页;分包(订单详情)作启动页会白屏 */
export function buildCsSharePath(orderId?: string): string {
const id = (orderId || '').trim();
return id ? `pages/open/index?id=${encodeURIComponent(id)}` : 'pages/home/index';
}
export function buildCsShareTitle(orderNo?: string): string {
const no = (orderNo || '').trim();
return no ? `订单 ${no}` : '杜康好客';
}
type OpenCsChatOption = {
extInfo: { url: string };
corpId: string;
@@ -97,16 +108,10 @@ export async function openWecomCustomerServiceChat(params: {
const option: OpenCsChatOption = {
extInfo: { url },
corpId,
showMessageCard: true,
sendMessageTitle: buildCsShareTitle(params.session?.orderNo),
sendMessagePath: buildCsSharePath(params.session?.orderId),
};
if (params.session?.orderNo || params.session?.orderId) {
option.showMessageCard = true;
option.sendMessageTitle = params.session.orderNo
? `订单 ${params.session.orderNo}`
: '订单咨询';
if (params.session.orderId) {
option.sendMessagePath = `pages/order-detail/index?id=${params.session.orderId}`;
}
}
await new Promise<void>((resolve, reject) => {
wxApi({
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '杜康好客',
});
+42
View File
@@ -0,0 +1,42 @@
import { View, Text } from '@tarojs/components';
import Taro, { useLoad } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import { goLogin } from '../../lib/auth-nav';
import { isLoggedIn } from '../../lib/api';
function goHome() {
Taro.switchTab({ url: '/pages/home/index' }).catch(() => {
Taro.reLaunch({ url: '/pages/home/index' });
});
}
function openOrder(orderId: string) {
const detail = `/pages/order-detail/index?id=${encodeURIComponent(orderId)}`;
if (!isLoggedIn()) {
goLogin(detail);
return;
}
Taro.redirectTo({ url: detail }).catch(() => {
Taro.reLaunch({ url: detail });
});
}
/** 主包落地:客服卡片 / 外链打开分包页(订单详情)前先落到这里,避免白屏 */
export default function OpenPage() {
useLoad((query?: Record<string, string | undefined>) => {
const orderId = String(query?.id || query?.orderId || '').trim();
if (!orderId) {
goHome();
return;
}
openOrder(orderId);
});
return (
<PageShell variant="plain">
<View className="u-empty">
<Text></Text>
</View>
</PageShell>
);
}
@@ -116,6 +116,7 @@ export default function OrderDetailPage() {
const orderId = router.params.id ?? '';
usePageView('order_detail_view', orderId ? { orderId } : undefined);
const [order, setOrder] = useState<OrderDetail | null>(null);
const [loadError, setLoadError] = useState('');
const [confirming, setConfirming] = useState(false);
const [latestTrack, setLatestTrack] = useState<OrderTrackNode | null>(null);
const [estimatedArrival, setEstimatedArrival] = useState<OrderTrackEstimatedArrival | null>(null);
@@ -147,14 +148,18 @@ export default function OrderDetailPage() {
}
useEffect(() => {
if (!orderId) return;
if (!orderId) {
setLoadError('订单信息缺失');
return;
}
request<OrderDetail>(`/trade/orders/${orderId}`)
.then((data) => {
setOrder(data);
setLoadError('');
void loadOrderTrack(data.delivery, data.status);
applyLocalDeliveryHint(data, setLocalHintHtml);
})
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
.catch((e) => setLoadError(e instanceof Error ? e.message : '加载失败'));
}, [orderId]);
useDidShow(() => {
@@ -293,7 +298,9 @@ export default function OrderDetailPage() {
/>
<View className="sub-page-body">
{!order ? (
<View className="u-empty"></View>
<View className="u-empty" onClick={loadError ? () => Taro.switchTab({ url: '/pages/home/index' }) : undefined}>
<Text>{loadError || '加载中…'}</Text>
</View>
) : (
<>
<View className="order-card">