Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e883d3e70 | |||
| 9a35c4aea9 | |||
| 971b8aaa7b | |||
| 4b9ab79acb | |||
| fbb92e00d5 |
@@ -11,7 +11,6 @@ import {
|
||||
saveActivityPosterSelection,
|
||||
} from '../lib/activity-posters';
|
||||
import { toastError, toastSuccess } from '../lib/toast';
|
||||
import { blobToDataUrl, isHttpImageUrl, previewSaveableImage } from '../lib/wechat-save-image';
|
||||
import { isWechatEnv } from '../lib/weixin';
|
||||
|
||||
const NONE_ID = 'none';
|
||||
@@ -99,13 +98,11 @@ export default function ActivityPosterPicker({ initialId }: Props) {
|
||||
setDownloading(true);
|
||||
try {
|
||||
if (selectedId === NONE_ID) {
|
||||
if (isWechatEnv() && isHttpImageUrl(qrcodeUrl)) {
|
||||
await previewSaveableImage(qrcodeUrl);
|
||||
return;
|
||||
}
|
||||
const blob = await fetchAssocQrcodeImage();
|
||||
if (isWechatEnv()) {
|
||||
setQrcodeUrl(await blobToDataUrl(blob));
|
||||
const url = URL.createObjectURL(blob);
|
||||
urlsRef.current.push(url);
|
||||
setQrcodeUrl(url);
|
||||
toastSuccess('请长按上方图片保存到相册');
|
||||
return;
|
||||
}
|
||||
@@ -121,8 +118,9 @@ export default function ActivityPosterPicker({ initialId }: Props) {
|
||||
}
|
||||
if (isWechatEnv()) {
|
||||
let url = previewUrls[selected.id];
|
||||
if (!url || url.startsWith('blob:')) {
|
||||
url = await blobToDataUrl(blob);
|
||||
if (!url) {
|
||||
url = URL.createObjectURL(blob);
|
||||
urlsRef.current.push(url);
|
||||
setPreviewUrls((prev) => ({ ...prev, [selected.id]: url }));
|
||||
}
|
||||
toastSuccess('请长按上方图片保存到相册');
|
||||
@@ -174,7 +172,7 @@ export default function ActivityPosterPicker({ initialId }: Props) {
|
||||
<div className="partner-activity-panel">
|
||||
{qrcodeUrl ? (
|
||||
<div className="partner-activity-preview partner-activity-preview--qr">
|
||||
<img className="partner-longpress-img" src={qrcodeUrl} alt="关联码" />
|
||||
<img src={qrcodeUrl} alt="关联码" />
|
||||
</div>
|
||||
) : (
|
||||
<p className="body-md text-muted" style={{ marginBottom: 12 }}>关联码尚未生成</p>
|
||||
@@ -202,7 +200,7 @@ export default function ActivityPosterPicker({ initialId }: Props) {
|
||||
{selectedId === item.id && (
|
||||
<div className="partner-activity-panel">
|
||||
<div className="partner-activity-preview">
|
||||
<img className="partner-longpress-img" src={previewUrl || item.imageUrl} alt={item.title} />
|
||||
<img src={previewUrl || item.imageUrl} alt={item.title} />
|
||||
{previewLoading && !previewUrl && (
|
||||
<div className="partner-activity-preview-mask">正在贴入二维码…</div>
|
||||
)}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { isHttpImageUrl } from '@dukang/weixin-sdk';
|
||||
import { toastSuccess } from './toast';
|
||||
import { isWechatEnv, weixinSdk } from './weixin';
|
||||
|
||||
export { isHttpImageUrl };
|
||||
|
||||
export function blobToDataUrl(blob: Blob): Promise<string> {
|
||||
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<boolean> {
|
||||
const href = url.trim();
|
||||
if (!isWechatEnv() || !isHttpImageUrl(href)) return false;
|
||||
try {
|
||||
await weixinSdk.previewImage(href);
|
||||
toastSuccess('请长按图片保存到相册');
|
||||
return true;
|
||||
} catch {
|
||||
toastSuccess('请长按上方图片保存到相册');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
|
||||
@@ -12,6 +11,7 @@ export default function AssocQrcodePage() {
|
||||
usePartnerPageView('partner_assoc_qrcode_view');
|
||||
const navigate = useNavigate();
|
||||
const [summary, setSummary] = useState<PartnerAssocSummary | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -21,12 +21,11 @@ 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', {
|
||||
@@ -38,6 +37,11 @@ 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`;
|
||||
@@ -45,8 +49,9 @@ export default function AssocQrcodePage() {
|
||||
URL.revokeObjectURL(url);
|
||||
toastSuccess('已开始下载');
|
||||
} catch (e) {
|
||||
if (isWechatEnv() && isHttpImageUrl(publicUrl)) {
|
||||
await previewSaveableImage(publicUrl);
|
||||
if (summary?.qrcodeUrl) {
|
||||
setPreviewUrl(summary.qrcodeUrl);
|
||||
toastSuccess('请长按图片保存到相册');
|
||||
return;
|
||||
}
|
||||
toastError(e instanceof Error ? e.message : '下载失败');
|
||||
@@ -65,8 +70,7 @@ export default function AssocQrcodePage() {
|
||||
</p>
|
||||
{summary?.qrcodeUrl ? (
|
||||
<img
|
||||
className="partner-longpress-img"
|
||||
src={summary.qrcodeUrl}
|
||||
src={previewUrl || summary.qrcodeUrl}
|
||||
alt="关联码"
|
||||
style={{ width: 220, height: 220, background: '#fff' }}
|
||||
/>
|
||||
@@ -79,11 +83,11 @@ export default function AssocQrcodePage() {
|
||||
<button type="button" className="partner-btn-primary" style={{ marginTop: 20 }} onClick={() => void downloadQr()}>
|
||||
下载二维码
|
||||
</button>
|
||||
{isWechatEnv() && summary?.qrcodeUrl ? (
|
||||
{previewUrl && isWechatEnv() && (
|
||||
<p className="label-md text-muted" style={{ marginTop: 12 }}>
|
||||
微信内请点「下载二维码」后长按保存
|
||||
微信内请长按上方图片保存
|
||||
</p>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,6 @@ 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';
|
||||
|
||||
@@ -59,7 +58,7 @@ export default function UsersManagePage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (previewUrl?.startsWith('blob:')) URL.revokeObjectURL(previewUrl);
|
||||
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
||||
}, [previewUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -132,17 +131,12 @@ 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()) {
|
||||
if (previewUrl?.startsWith('blob:')) URL.revokeObjectURL(previewUrl);
|
||||
setPreviewUrl(await blobToDataUrl(blob));
|
||||
toastSuccess('请长按上方图片保存到相册');
|
||||
setPreviewUrl(URL.createObjectURL(blob));
|
||||
toastSuccess('请长按图片保存到相册');
|
||||
return;
|
||||
}
|
||||
downloadBlob(blob, posterId
|
||||
@@ -150,11 +144,7 @@ export default function UsersManagePage() {
|
||||
: `partner-assoc-${summary?.partnerId || 'qr'}.png`);
|
||||
toastSuccess('已开始下载');
|
||||
} catch (e) {
|
||||
if (!posterId && isHttpImageUrl(summary?.qrcodeUrl)) {
|
||||
if (isWechatEnv()) {
|
||||
await previewSaveableImage(summary.qrcodeUrl);
|
||||
return;
|
||||
}
|
||||
if (!posterId && summary?.qrcodeUrl) {
|
||||
setPreviewUrl(summary.qrcodeUrl);
|
||||
toastSuccess('请长按图片保存到相册');
|
||||
return;
|
||||
@@ -192,7 +182,6 @@ export default function UsersManagePage() {
|
||||
</p>
|
||||
{summary?.activityPosterId && (previewUrl || heroUrl) ? (
|
||||
<img
|
||||
className="partner-longpress-img"
|
||||
src={previewUrl || heroUrl || ''}
|
||||
alt="活动图"
|
||||
style={{ width: '100%', maxWidth: 360, background: '#f5f5f5' }}
|
||||
@@ -201,7 +190,6 @@ export default function UsersManagePage() {
|
||||
<p className="body-md text-muted">正在生成活动图…</p>
|
||||
) : previewUrl || summary?.qrcodeUrl ? (
|
||||
<img
|
||||
className="partner-longpress-img"
|
||||
src={previewUrl || summary?.qrcodeUrl || ''}
|
||||
alt="关联码"
|
||||
style={{ width: 200, height: 200, background: '#fff' }}
|
||||
@@ -215,11 +203,9 @@ export default function UsersManagePage() {
|
||||
<button type="button" className="partner-btn-primary" style={{ marginTop: 16 }} onClick={() => void downloadMainImage()}>
|
||||
{summary?.activityPosterId ? '下载活动图' : '下载二维码'}
|
||||
</button>
|
||||
{isWechatEnv() && (previewUrl || summary?.qrcodeUrl) ? (
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||
微信内请点「{summary?.activityPosterId ? '下载活动图' : '下载二维码'}」后长按保存
|
||||
</p>
|
||||
) : null}
|
||||
{previewUrl && isWechatEnv() && (
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>微信内请长按上方图片保存</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<Link to="/center/activity-posters" className="partner-menu-card" style={{ display: 'block', marginBottom: 20, textDecoration: 'none', color: 'inherit' }}>
|
||||
|
||||
@@ -4087,11 +4087,3 @@ 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;
|
||||
}
|
||||
|
||||
@@ -14,9 +14,6 @@ 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 {
|
||||
@@ -57,7 +54,6 @@ export default defineConfig(async () => ({
|
||||
},
|
||||
defineConstants: {
|
||||
TARO_APP_API_ORIGIN: JSON.stringify(API_ORIGIN),
|
||||
TARO_APP_VERSION: JSON.stringify(APP_PKG_VERSION),
|
||||
},
|
||||
copy: {
|
||||
patterns: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dukang/mini-user",
|
||||
"version": "4.0.4",
|
||||
"version": "3.5.16",
|
||||
"private": true,
|
||||
"description": "杜康好客 · C 端用户微信小程序(Taro)",
|
||||
"scripts": {
|
||||
|
||||
@@ -4,7 +4,6 @@ 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 { buildCsSharePath, buildCsShareTitle, formatOpenCsError, openWecomCustomerServiceChat } from '../lib/wecom-cs';
|
||||
import { formatOpenCsError, openWecomCustomerServiceChat } from '../lib/wecom-cs';
|
||||
|
||||
export type ContactCsSessionContext = {
|
||||
orderId?: string;
|
||||
@@ -91,9 +91,6 @@ 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}
|
||||
|
||||
@@ -130,12 +130,10 @@ export function toast(title: string, icon: 'success' | 'error' | 'none' = 'none'
|
||||
const text = title.trim();
|
||||
if (!text) return;
|
||||
if (icon !== 'success' && showFloatingToast(text)) {
|
||||
void Promise.resolve(Taro.hideToast() as void | Promise<unknown>).catch(() => {});
|
||||
void Taro.hideToast();
|
||||
return;
|
||||
}
|
||||
void Promise.resolve(
|
||||
Taro.showToast({ title: text, icon, duration: 1800 }) as void | Promise<unknown>,
|
||||
).catch(() => {});
|
||||
Taro.showToast({ title: text, icon, duration: 1800 });
|
||||
}
|
||||
|
||||
export type SessionPayload = {
|
||||
|
||||
@@ -21,14 +21,6 @@ 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();
|
||||
@@ -105,7 +97,6 @@ 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',
|
||||
@@ -130,17 +121,15 @@ export function installClientErrorReporting(): void {
|
||||
});
|
||||
window.addEventListener('unhandledrejection', (ev) => {
|
||||
const reason = ev.reason;
|
||||
const message =
|
||||
reportClientError({
|
||||
level: 'error',
|
||||
category: 'unhandled_rejection',
|
||||
message:
|
||||
reason instanceof Error
|
||||
? reason.message
|
||||
: typeof reason === 'string'
|
||||
? reason
|
||||
: 'unhandledrejection';
|
||||
if (shouldIgnoreRejection(message)) return;
|
||||
reportClientError({
|
||||
level: 'error',
|
||||
category: 'unhandled_rejection',
|
||||
message,
|
||||
: 'unhandledrejection',
|
||||
stack: reason instanceof Error ? reason.stack : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,13 @@ export async function tryGetClientGpsLocation(): Promise<ClientGpsLocation | nul
|
||||
if (process.env.TARO_ENV === 'weapp') {
|
||||
try {
|
||||
const Taro = (await import('@tarojs/taro')).default;
|
||||
const loc = await Taro.getLocation({ type: 'gcj02' });
|
||||
const loc = await new Promise<{ latitude: number; longitude: number }>((resolve, reject) => {
|
||||
Taro.getLocation({
|
||||
type: 'gcj02',
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
return { latitude: loc.latitude, longitude: loc.longitude };
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -11,7 +11,13 @@ export type ClientGpsLocation = {
|
||||
|
||||
export async function tryGetClientGpsLocation(): Promise<ClientGpsLocation | null> {
|
||||
try {
|
||||
const loc = await Taro.getLocation({ type: 'gcj02' });
|
||||
const loc = await new Promise<{ latitude: number; longitude: number }>((resolve, reject) => {
|
||||
Taro.getLocation({
|
||||
type: 'gcj02',
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
return { latitude: loc.latitude, longitude: loc.longitude };
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { fetchClientConfig } from './pay-wechat';
|
||||
|
||||
/** 与 package.json version 同步(Taro defineConstants 注入),供 minClientVersion 比对 */
|
||||
export const APP_VERSION =
|
||||
(typeof TARO_APP_VERSION !== 'undefined' && String(TARO_APP_VERSION).trim()) || '4.0.4';
|
||||
/** 与 package.json version 同步,供服务端 minClientVersion 比对 */
|
||||
export const APP_VERSION = '3.5.16';
|
||||
|
||||
export const APP_VERSION_LABEL = `v${APP_VERSION.replace(/^v/i, '')}`;
|
||||
|
||||
function normalizeVersion(v: string): string {
|
||||
return String(v || '').trim().replace(/^v/i, '');
|
||||
}
|
||||
export const APP_VERSION_LABEL = `v${APP_VERSION}`;
|
||||
|
||||
function parseSemver(v: string): number[] {
|
||||
return normalizeVersion(v).split('.').map((n) => parseInt(n, 10) || 0);
|
||||
return v.split('.').map((n) => parseInt(n, 10) || 0);
|
||||
}
|
||||
|
||||
export function compareSemver(a: string, b: string): number {
|
||||
@@ -94,7 +89,7 @@ async function enforceMinClientVersion(min: string) {
|
||||
export async function checkClientVersionGate() {
|
||||
try {
|
||||
const config = await fetchClientConfig();
|
||||
const min = normalizeVersion(config.minClientVersion ?? '');
|
||||
const min = config.minClientVersion?.trim();
|
||||
if (min && compareSemver(APP_VERSION, min) < 0) {
|
||||
await enforceMinClientVersion(min);
|
||||
}
|
||||
|
||||
@@ -59,23 +59,6 @@ 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 || '',
|
||||
@@ -196,13 +179,14 @@ async function promptLocationAuthOnce() {
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
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' });
|
||||
function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
Taro.getLocation({
|
||||
type: 'gcj02',
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveViaH5Jssdk(): Promise<ResolvedUserCity | null> {
|
||||
@@ -279,7 +263,7 @@ export async function resolveUserCity(force = false): Promise<ResolvedUserCity>
|
||||
return resolved;
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = wxErrorMessage(err);
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
const denied = isDenyMessage(errMsg);
|
||||
if (denied && !isLocationDenied()) {
|
||||
await promptLocationAuthOnce();
|
||||
|
||||
@@ -57,23 +57,6 @@ 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 || '',
|
||||
@@ -192,13 +175,14 @@ async function promptLocationAuthOnce() {
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
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' });
|
||||
function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
Taro.getLocation({
|
||||
type: 'gcj02',
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveUserCity(force = false): Promise<ResolvedUserCity> {
|
||||
@@ -227,7 +211,7 @@ export async function resolveUserCity(force = false): Promise<ResolvedUserCity>
|
||||
return resolved;
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = wxErrorMessage(err);
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
const denied = isDenyMessage(errMsg);
|
||||
if (denied && !isLocationDenied()) {
|
||||
await promptLocationAuthOnce();
|
||||
|
||||
@@ -4,17 +4,6 @@ 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;
|
||||
@@ -108,10 +97,16 @@ 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({
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '杜康好客',
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
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,7 +116,6 @@ 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);
|
||||
@@ -148,18 +147,14 @@ export default function OrderDetailPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!orderId) {
|
||||
setLoadError('订单信息缺失');
|
||||
return;
|
||||
}
|
||||
if (!orderId) return;
|
||||
request<OrderDetail>(`/trade/orders/${orderId}`)
|
||||
.then((data) => {
|
||||
setOrder(data);
|
||||
setLoadError('');
|
||||
void loadOrderTrack(data.delivery, data.status);
|
||||
applyLocalDeliveryHint(data, setLocalHintHtml);
|
||||
})
|
||||
.catch((e) => setLoadError(e instanceof Error ? e.message : '加载失败'));
|
||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
||||
}, [orderId]);
|
||||
|
||||
useDidShow(() => {
|
||||
@@ -298,9 +293,7 @@ export default function OrderDetailPage() {
|
||||
/>
|
||||
<View className="sub-page-body">
|
||||
{!order ? (
|
||||
<View className="u-empty" onClick={loadError ? () => Taro.switchTab({ url: '/pages/home/index' }) : undefined}>
|
||||
<Text>{loadError || '加载中…'}</Text>
|
||||
</View>
|
||||
<View className="u-empty">加载中…</View>
|
||||
) : (
|
||||
<>
|
||||
<View className="order-card">
|
||||
|
||||
Vendored
-1
@@ -13,7 +13,6 @@ declare const defineAppConfig: (config: Record<string, unknown>) => Record<strin
|
||||
declare const definePageConfig: (config: Record<string, unknown>) => Record<string, unknown>;
|
||||
|
||||
declare const TARO_APP_API_ORIGIN: string;
|
||||
declare const TARO_APP_VERSION: string;
|
||||
|
||||
/** 微信小程序全局(Taro 未封装的 API 如 openBusinessView 需直接调用) */
|
||||
declare const wx: {
|
||||
|
||||
@@ -25,7 +25,6 @@ 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,
|
||||
@@ -54,7 +53,6 @@ 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,
|
||||
@@ -78,7 +76,6 @@ export function createWeixinSdk(config: WeixinSdkConfig) {
|
||||
scanQrCode: (options?: Parameters<typeof scanQrCode>[1]) => scanQrCode(config, options),
|
||||
chooseImages: (options?: Parameters<typeof chooseWechatImages>[1]) =>
|
||||
chooseWechatImages(config, options),
|
||||
previewImage: (url: string) => previewWechatImage(config, url),
|
||||
pay: (prepay: Parameters<typeof invokeWechatPay>[0]) =>
|
||||
invokeWechatPay(prepay, {
|
||||
apiBase: config.apiBase ?? '/api/v1',
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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<void> {
|
||||
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<void>((resolve, reject) => {
|
||||
window.wx!.previewImage!({
|
||||
current: href,
|
||||
urls: [href],
|
||||
success: () => resolve(),
|
||||
fail: (res) => reject(new Error(res?.errMsg || '预览失败')),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -58,12 +58,6 @@ 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;
|
||||
@@ -127,7 +121,6 @@ export const DEFAULT_JS_API_LIST = [
|
||||
'chooseWXPay',
|
||||
'chooseImage',
|
||||
'getLocalImgData',
|
||||
'previewImage',
|
||||
'updateAppMessageShareData',
|
||||
'updateTimelineShareData',
|
||||
'onMenuShareAppMessage',
|
||||
|
||||
@@ -144,8 +144,8 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
|
||||
group: G.wechat_mini,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
placeholder: '4.0.4',
|
||||
description: 'semver 格式(可带或不带 v);客户端低于此版本时提示更新',
|
||||
placeholder: '3.4.15',
|
||||
description: 'semver 格式;客户端低于此版本时提示更新',
|
||||
},
|
||||
{
|
||||
key: 'USER_H5_URL',
|
||||
|
||||
Reference in New Issue
Block a user