@@ -200,7 +202,7 @@ export default function ActivityPosterPicker({ initialId }: Props) {
{selectedId === item.id && (
-

+

{previewLoading && !previewUrl && (
正在贴入二维码…
)}
diff --git a/apps/h5-partner/src/lib/wechat-save-image.ts b/apps/h5-partner/src/lib/wechat-save-image.ts
new file mode 100644
index 0000000..6fed121
--- /dev/null
+++ b/apps/h5-partner/src/lib/wechat-save-image.ts
@@ -0,0 +1,31 @@
+import { isHttpImageUrl } from '@dukang/weixin-sdk';
+import { toastSuccess } from './toast';
+import { isWechatEnv, weixinSdk } from './weixin';
+
+export { isHttpImageUrl };
+
+export function blobToDataUrl(blob: Blob): Promise
{
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(String(reader.result || ''));
+ reader.onerror = () => reject(new Error('读取图片失败'));
+ reader.readAsDataURL(blob);
+ });
+}
+
+/**
+ * 微信内用 previewImage 打开可公网访问的图片,用户长按即可保存。
+ * blob / data URL 不能走该接口。
+ */
+export async function previewSaveableImage(url: string): Promise {
+ const href = url.trim();
+ if (!isWechatEnv() || !isHttpImageUrl(href)) return false;
+ try {
+ await weixinSdk.previewImage(href);
+ toastSuccess('请长按图片保存到相册');
+ return true;
+ } catch {
+ toastSuccess('请长按上方图片保存到相册');
+ return false;
+ }
+}
diff --git a/apps/h5-partner/src/pages/AssocQrcodePage.tsx b/apps/h5-partner/src/pages/AssocQrcodePage.tsx
index 78f24fb..9983b99 100644
--- a/apps/h5-partner/src/pages/AssocQrcodePage.tsx
+++ b/apps/h5-partner/src/pages/AssocQrcodePage.tsx
@@ -4,6 +4,7 @@ import PageHeader from '@dukang/shared-ui/PageHeader';
import type { PartnerAssocSummary } from '@dukang/shared-types';
import { request } from '../lib/api';
import { toastError, toastSuccess } from '../lib/toast';
+import { isHttpImageUrl, previewSaveableImage } from '../lib/wechat-save-image';
import { isWechatEnv } from '../lib/weixin';
import { usePartnerPageView } from '../lib/usePageView';
@@ -11,7 +12,6 @@ export default function AssocQrcodePage() {
usePartnerPageView('partner_assoc_qrcode_view');
const navigate = useNavigate();
const [summary, setSummary] = useState(null);
- const [previewUrl, setPreviewUrl] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
@@ -21,11 +21,12 @@ export default function AssocQrcodePage() {
.finally(() => setLoading(false));
}, []);
- useEffect(() => () => {
- if (previewUrl) URL.revokeObjectURL(previewUrl);
- }, [previewUrl]);
-
async function downloadQr() {
+ const publicUrl = summary?.qrcodeUrl;
+ if (isWechatEnv() && isHttpImageUrl(publicUrl)) {
+ await previewSaveableImage(publicUrl);
+ return;
+ }
const token = localStorage.getItem('accessToken');
try {
const res = await fetch('/api/v1/partner/assoc/qrcode', {
@@ -37,11 +38,6 @@ export default function AssocQrcodePage() {
if (!res.ok) throw new Error('下载失败');
const blob = await res.blob();
const url = URL.createObjectURL(blob);
- if (isWechatEnv()) {
- setPreviewUrl(url);
- toastSuccess('请长按图片保存到相册');
- return;
- }
const a = document.createElement('a');
a.href = url;
a.download = `partner-assoc-${summary?.partnerId || 'qr'}.png`;
@@ -49,9 +45,8 @@ export default function AssocQrcodePage() {
URL.revokeObjectURL(url);
toastSuccess('已开始下载');
} catch (e) {
- if (summary?.qrcodeUrl) {
- setPreviewUrl(summary.qrcodeUrl);
- toastSuccess('请长按图片保存到相册');
+ if (isWechatEnv() && isHttpImageUrl(publicUrl)) {
+ await previewSaveableImage(publicUrl);
return;
}
toastError(e instanceof Error ? e.message : '下载失败');
@@ -70,7 +65,8 @@ export default function AssocQrcodePage() {
{summary?.qrcodeUrl ? (
@@ -83,11 +79,11 @@ export default function AssocQrcodePage() {
- {previewUrl && isWechatEnv() && (
+ {isWechatEnv() && summary?.qrcodeUrl ? (
- 微信内请长按上方图片保存
+ 微信内请点「下载二维码」后长按保存
- )}
+ ) : null}
)}
diff --git a/apps/h5-partner/src/pages/UsersManagePage.tsx b/apps/h5-partner/src/pages/UsersManagePage.tsx
index f893893..aeedf07 100644
--- a/apps/h5-partner/src/pages/UsersManagePage.tsx
+++ b/apps/h5-partner/src/pages/UsersManagePage.tsx
@@ -13,6 +13,7 @@ import {
fetchAssocQrcodeImage,
} from '../lib/activity-posters';
import { toastError, toastSuccess } from '../lib/toast';
+import { blobToDataUrl, isHttpImageUrl, previewSaveableImage } from '../lib/wechat-save-image';
import { isWechatEnv } from '../lib/weixin';
import { usePartnerPageView } from '../lib/usePageView';
@@ -58,7 +59,7 @@ export default function UsersManagePage() {
}, []);
useEffect(() => () => {
- if (previewUrl) URL.revokeObjectURL(previewUrl);
+ if (previewUrl?.startsWith('blob:')) URL.revokeObjectURL(previewUrl);
}, [previewUrl]);
useEffect(() => {
@@ -131,12 +132,17 @@ export default function UsersManagePage() {
async function downloadMainImage() {
const posterId = summary?.activityPosterId;
try {
+ if (isWechatEnv() && !posterId && isHttpImageUrl(summary?.qrcodeUrl)) {
+ await previewSaveableImage(summary.qrcodeUrl);
+ return;
+ }
const blob = posterId
? await fetchActivityPosterImage(posterId)
: await fetchAssocQrcodeImage();
if (isWechatEnv()) {
- setPreviewUrl(URL.createObjectURL(blob));
- toastSuccess('请长按图片保存到相册');
+ if (previewUrl?.startsWith('blob:')) URL.revokeObjectURL(previewUrl);
+ setPreviewUrl(await blobToDataUrl(blob));
+ toastSuccess('请长按上方图片保存到相册');
return;
}
downloadBlob(blob, posterId
@@ -144,7 +150,11 @@ export default function UsersManagePage() {
: `partner-assoc-${summary?.partnerId || 'qr'}.png`);
toastSuccess('已开始下载');
} catch (e) {
- if (!posterId && summary?.qrcodeUrl) {
+ if (!posterId && isHttpImageUrl(summary?.qrcodeUrl)) {
+ if (isWechatEnv()) {
+ await previewSaveableImage(summary.qrcodeUrl);
+ return;
+ }
setPreviewUrl(summary.qrcodeUrl);
toastSuccess('请长按图片保存到相册');
return;
@@ -182,6 +192,7 @@ export default function UsersManagePage() {
{summary?.activityPosterId && (previewUrl || heroUrl) ? (

正在生成活动图…
) : previewUrl || summary?.qrcodeUrl ? (

void downloadMainImage()}>
{summary?.activityPosterId ? '下载活动图' : '下载二维码'}
- {previewUrl && isWechatEnv() && (
-
微信内请长按上方图片保存
- )}
+ {isWechatEnv() && (previewUrl || summary?.qrcodeUrl) ? (
+
+ 微信内请点「{summary?.activityPosterId ? '下载活动图' : '下载二维码'}」后长按保存
+
+ ) : null}
diff --git a/apps/h5-partner/src/styles.css b/apps/h5-partner/src/styles.css
index 914374a..7cf6371 100644
--- a/apps/h5-partner/src/styles.css
+++ b/apps/h5-partner/src/styles.css
@@ -4087,3 +4087,11 @@ header:has(> .app-page-title:only-child),
width: 200px;
height: 200px;
}
+
+/* 微信内长按保存:不要禁用系统菜单 */
+img.partner-longpress-img {
+ -webkit-touch-callout: default;
+ -webkit-user-select: none;
+ user-select: none;
+ pointer-events: auto;
+}
diff --git a/apps/mini-user/config/index.ts b/apps/mini-user/config/index.ts
index a928fbf..25a38ca 100644
--- a/apps/mini-user/config/index.ts
+++ b/apps/mini-user/config/index.ts
@@ -14,6 +14,9 @@ const API_ORIGIN =
(isDevMode ? 'http://192.168.1.10:3010' : 'https://api.dukanghaoke.com');
const requireFromApp = createRequire(path.resolve(__dirname, '../package.json'));
+const { version: APP_PKG_VERSION } = requireFromApp(path.resolve(__dirname, '../package.json')) as {
+ version: string;
+};
/** 解析包目录/文件,避免 pnpm/Vite 把同一 runtime 打成两份 → useDidShow 读到空 reactMeta */
function resolvePkgFile(id: string): string {
@@ -54,6 +57,7 @@ export default defineConfig(async () => ({
},
defineConstants: {
TARO_APP_API_ORIGIN: JSON.stringify(API_ORIGIN),
+ TARO_APP_VERSION: JSON.stringify(APP_PKG_VERSION),
},
copy: {
patterns: [
diff --git a/apps/mini-user/package.json b/apps/mini-user/package.json
index 4577f9a..20db33b 100644
--- a/apps/mini-user/package.json
+++ b/apps/mini-user/package.json
@@ -1,6 +1,6 @@
{
"name": "@dukang/mini-user",
- "version": "3.5.16",
+ "version": "4.0.4",
"private": true,
"description": "杜康好客 · C 端用户微信小程序(Taro)",
"scripts": {
diff --git a/apps/mini-user/src/app.config.ts b/apps/mini-user/src/app.config.ts
index 3184325..62d0c90 100644
--- a/apps/mini-user/src/app.config.ts
+++ b/apps/mini-user/src/app.config.ts
@@ -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'] },
diff --git a/apps/mini-user/src/components/ContactCsButton.tsx b/apps/mini-user/src/components/ContactCsButton.tsx
index d03df90..11ab976 100644
--- a/apps/mini-user/src/components/ContactCsButton.tsx
+++ b/apps/mini-user/src/components/ContactCsButton.tsx
@@ -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' ?
{children} : children}
diff --git a/apps/mini-user/src/lib/api.ts b/apps/mini-user/src/lib/api.ts
index f7a0821..69b90ea 100644
--- a/apps/mini-user/src/lib/api.ts
+++ b/apps/mini-user/src/lib/api.ts
@@ -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
).catch(() => {});
return;
}
- Taro.showToast({ title: text, icon, duration: 1800 });
+ void Promise.resolve(
+ Taro.showToast({ title: text, icon, duration: 1800 }) as void | Promise,
+ ).catch(() => {});
}
export type SessionPayload = {
diff --git a/apps/mini-user/src/lib/client-error.ts b/apps/mini-user/src/lib/client-error.ts
index e46a1f6..1cb653a 100644
--- a/apps/mini-user/src/lib/client-error.ts
+++ b/apps/mini-user/src/lib/client-error.ts
@@ -21,6 +21,14 @@ export type ClientErrorPayload = {
extra?: Record;
};
+/** 微信预期失败,不应按 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,
});
});
diff --git a/apps/mini-user/src/lib/client-location.ts b/apps/mini-user/src/lib/client-location.ts
index c9c772e..267103e 100644
--- a/apps/mini-user/src/lib/client-location.ts
+++ b/apps/mini-user/src/lib/client-location.ts
@@ -14,13 +14,7 @@ export async function tryGetClientGpsLocation(): Promise((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;
diff --git a/apps/mini-user/src/lib/client-location.weapp.ts b/apps/mini-user/src/lib/client-location.weapp.ts
index 3735458..d3fb039 100644
--- a/apps/mini-user/src/lib/client-location.weapp.ts
+++ b/apps/mini-user/src/lib/client-location.weapp.ts
@@ -11,13 +11,7 @@ export type ClientGpsLocation = {
export async function tryGetClientGpsLocation(): Promise {
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;
diff --git a/apps/mini-user/src/lib/client-version.ts b/apps/mini-user/src/lib/client-version.ts
index 0175aa6..5b441aa 100644
--- a/apps/mini-user/src/lib/client-version.ts
+++ b/apps/mini-user/src/lib/client-version.ts
@@ -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);
}
diff --git a/apps/mini-user/src/lib/user-location.ts b/apps/mini-user/src/lib/user-location.ts
index 29a0f72..867e933 100644
--- a/apps/mini-user/src/lib/user-location.ts
+++ b/apps/mini-user/src/lib/user-location.ts
@@ -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 {
- return new Promise((resolve, reject) => {
- Taro.getLocation({
- type: 'gcj02',
- success: resolve,
- fail: reject,
- });
- });
+async function getMiniLocation(): Promise {
+ 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 {
@@ -263,7 +279,7 @@ export async function resolveUserCity(force = false): Promise
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();
diff --git a/apps/mini-user/src/lib/user-location.weapp.ts b/apps/mini-user/src/lib/user-location.weapp.ts
index a3ec5c7..ddb94c6 100644
--- a/apps/mini-user/src/lib/user-location.weapp.ts
+++ b/apps/mini-user/src/lib/user-location.weapp.ts
@@ -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 {
- return new Promise((resolve, reject) => {
- Taro.getLocation({
- type: 'gcj02',
- success: resolve,
- fail: reject,
- });
- });
+async function getMiniLocation(): Promise {
+ 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 {
@@ -211,7 +227,7 @@ export async function resolveUserCity(force = false): Promise
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();
diff --git a/apps/mini-user/src/lib/wecom-cs.ts b/apps/mini-user/src/lib/wecom-cs.ts
index 74f3e6f..dfdbdf3 100644
--- a/apps/mini-user/src/lib/wecom-cs.ts
+++ b/apps/mini-user/src/lib/wecom-cs.ts
@@ -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((resolve, reject) => {
wxApi({
diff --git a/apps/mini-user/src/pages/open/index.config.ts b/apps/mini-user/src/pages/open/index.config.ts
new file mode 100644
index 0000000..0d044a3
--- /dev/null
+++ b/apps/mini-user/src/pages/open/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '杜康好客',
+});
diff --git a/apps/mini-user/src/pages/open/index.tsx b/apps/mini-user/src/pages/open/index.tsx
new file mode 100644
index 0000000..0a41e28
--- /dev/null
+++ b/apps/mini-user/src/pages/open/index.tsx
@@ -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) => {
+ const orderId = String(query?.id || query?.orderId || '').trim();
+ if (!orderId) {
+ goHome();
+ return;
+ }
+ openOrder(orderId);
+ });
+
+ return (
+
+
+ 正在打开…
+
+
+ );
+}
diff --git a/apps/mini-user/src/pages/order-detail/index.tsx b/apps/mini-user/src/pages/order-detail/index.tsx
index 67991ce..c80fcbd 100644
--- a/apps/mini-user/src/pages/order-detail/index.tsx
+++ b/apps/mini-user/src/pages/order-detail/index.tsx
@@ -116,6 +116,7 @@ export default function OrderDetailPage() {
const orderId = router.params.id ?? '';
usePageView('order_detail_view', orderId ? { orderId } : undefined);
const [order, setOrder] = useState(null);
+ const [loadError, setLoadError] = useState('');
const [confirming, setConfirming] = useState(false);
const [latestTrack, setLatestTrack] = useState(null);
const [estimatedArrival, setEstimatedArrival] = useState(null);
@@ -147,14 +148,18 @@ export default function OrderDetailPage() {
}
useEffect(() => {
- if (!orderId) return;
+ if (!orderId) {
+ setLoadError('订单信息缺失');
+ return;
+ }
request(`/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() {
/>
{!order ? (
- 加载中…
+ Taro.switchTab({ url: '/pages/home/index' }) : undefined}>
+ {loadError || '加载中…'}
+
) : (
<>
diff --git a/apps/mini-user/types/global.d.ts b/apps/mini-user/types/global.d.ts
index 49d6fdd..1a4eb4a 100644
--- a/apps/mini-user/types/global.d.ts
+++ b/apps/mini-user/types/global.d.ts
@@ -13,6 +13,7 @@ declare const defineAppConfig: (config: Record) => Record) => Record;
declare const TARO_APP_API_ORIGIN: string;
+declare const TARO_APP_VERSION: string;
/** 微信小程序全局(Taro 未封装的 API 如 openBusinessView 需直接调用) */
declare const wx: {
diff --git a/packages/weixin-sdk/src/index.ts b/packages/weixin-sdk/src/index.ts
index 91b6814..2c39aca 100644
--- a/packages/weixin-sdk/src/index.ts
+++ b/packages/weixin-sdk/src/index.ts
@@ -25,6 +25,7 @@ export { scanQrCode } from './scan';
export { invokeWechatPay } from './pay';
export { chooseWechatImages, canUseWechatChooseImage, formatChooseImageFailMessage } from './chooseImage';
export type { ChooseWechatImageOptions } from './chooseImage';
+export { previewWechatImage, isHttpImageUrl } from './previewImage';
export {
setWechatShareData,
canUseWechatShare,
@@ -53,6 +54,7 @@ import { getWechatLocation, getWechatLocationDetailed } from './location';
import { scanQrCode } from './scan';
import { invokeWechatPay } from './pay';
import { chooseWechatImages } from './chooseImage';
+import { previewWechatImage } from './previewImage';
import { setWechatShareData, shareViaWechatSdk } from './share';
import {
wechatLogin,
@@ -76,6 +78,7 @@ export function createWeixinSdk(config: WeixinSdkConfig) {
scanQrCode: (options?: Parameters[1]) => scanQrCode(config, options),
chooseImages: (options?: Parameters[1]) =>
chooseWechatImages(config, options),
+ previewImage: (url: string) => previewWechatImage(config, url),
pay: (prepay: Parameters[0]) =>
invokeWechatPay(prepay, {
apiBase: config.apiBase ?? '/api/v1',
diff --git a/packages/weixin-sdk/src/previewImage.ts b/packages/weixin-sdk/src/previewImage.ts
new file mode 100644
index 0000000..08544f2
--- /dev/null
+++ b/packages/weixin-sdk/src/previewImage.ts
@@ -0,0 +1,33 @@
+import { ensureJssdkReady } from './jssdk';
+import type { WeixinSdkConfig } from './types';
+
+export function isHttpImageUrl(url?: string | null): url is string {
+ return !!url && /^https?:\/\//i.test(url.trim());
+}
+
+/** 微信内预览图片,便于长按保存到相册(须是可公网访问的 http(s) 地址) */
+export async function previewWechatImage(config: WeixinSdkConfig, url: string): Promise {
+ const href = url.trim();
+ if (!isHttpImageUrl(href)) {
+ throw new Error('图片地址无效');
+ }
+
+ await ensureJssdkReady({
+ apiBase: config.apiBase ?? '/api/v1',
+ clientApp: config.clientApp,
+ getAccessToken: config.getAccessToken,
+ });
+
+ if (!window.wx?.previewImage) {
+ throw new Error('微信预览图片不可用');
+ }
+
+ await new Promise((resolve, reject) => {
+ window.wx!.previewImage!({
+ current: href,
+ urls: [href],
+ success: () => resolve(),
+ fail: (res) => reject(new Error(res?.errMsg || '预览失败')),
+ });
+ });
+}
diff --git a/packages/weixin-sdk/src/types.ts b/packages/weixin-sdk/src/types.ts
index 28647f6..2e0eae6 100644
--- a/packages/weixin-sdk/src/types.ts
+++ b/packages/weixin-sdk/src/types.ts
@@ -58,6 +58,12 @@ export type WxApi = {
success?: (res: { localData: string }) => void;
fail?: (res: { errMsg: string }) => void;
}) => void;
+ previewImage: (options: {
+ current: string;
+ urls: string[];
+ success?: () => void;
+ fail?: (res: { errMsg: string }) => void;
+ }) => void;
updateAppMessageShareData: (options: {
title: string;
desc: string;
@@ -121,6 +127,7 @@ export const DEFAULT_JS_API_LIST = [
'chooseWXPay',
'chooseImage',
'getLocalImgData',
+ 'previewImage',
'updateAppMessageShareData',
'updateTimelineShareData',
'onMenuShareAppMessage',
diff --git a/server/dukang-api/src/common/system-config/system-config.registry.ts b/server/dukang-api/src/common/system-config/system-config.registry.ts
index 58ae029..900b9fb 100644
--- a/server/dukang-api/src/common/system-config/system-config.registry.ts
+++ b/server/dukang-api/src/common/system-config/system-config.registry.ts
@@ -144,8 +144,8 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
group: G.wechat_mini,
type: 'string',
requiresRestart: false,
- placeholder: '3.4.15',
- description: 'semver 格式;客户端低于此版本时提示更新',
+ placeholder: '4.0.4',
+ description: 'semver 格式(可带或不带 v);客户端低于此版本时提示更新',
},
{
key: 'USER_H5_URL',