Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04d903c554 | |||
| 30bfcc0d93 | |||
| 283e6651aa |
@@ -11,6 +11,7 @@ import {
|
|||||||
saveActivityPosterSelection,
|
saveActivityPosterSelection,
|
||||||
} from '../lib/activity-posters';
|
} from '../lib/activity-posters';
|
||||||
import { toastError, toastSuccess } from '../lib/toast';
|
import { toastError, toastSuccess } from '../lib/toast';
|
||||||
|
import { blobToDataUrl, isHttpImageUrl, previewSaveableImage } from '../lib/wechat-save-image';
|
||||||
import { isWechatEnv } from '../lib/weixin';
|
import { isWechatEnv } from '../lib/weixin';
|
||||||
|
|
||||||
const NONE_ID = 'none';
|
const NONE_ID = 'none';
|
||||||
@@ -98,11 +99,13 @@ export default function ActivityPosterPicker({ initialId }: Props) {
|
|||||||
setDownloading(true);
|
setDownloading(true);
|
||||||
try {
|
try {
|
||||||
if (selectedId === NONE_ID) {
|
if (selectedId === NONE_ID) {
|
||||||
|
if (isWechatEnv() && isHttpImageUrl(qrcodeUrl)) {
|
||||||
|
await previewSaveableImage(qrcodeUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const blob = await fetchAssocQrcodeImage();
|
const blob = await fetchAssocQrcodeImage();
|
||||||
if (isWechatEnv()) {
|
if (isWechatEnv()) {
|
||||||
const url = URL.createObjectURL(blob);
|
setQrcodeUrl(await blobToDataUrl(blob));
|
||||||
urlsRef.current.push(url);
|
|
||||||
setQrcodeUrl(url);
|
|
||||||
toastSuccess('请长按上方图片保存到相册');
|
toastSuccess('请长按上方图片保存到相册');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -118,9 +121,8 @@ export default function ActivityPosterPicker({ initialId }: Props) {
|
|||||||
}
|
}
|
||||||
if (isWechatEnv()) {
|
if (isWechatEnv()) {
|
||||||
let url = previewUrls[selected.id];
|
let url = previewUrls[selected.id];
|
||||||
if (!url) {
|
if (!url || url.startsWith('blob:')) {
|
||||||
url = URL.createObjectURL(blob);
|
url = await blobToDataUrl(blob);
|
||||||
urlsRef.current.push(url);
|
|
||||||
setPreviewUrls((prev) => ({ ...prev, [selected.id]: url }));
|
setPreviewUrls((prev) => ({ ...prev, [selected.id]: url }));
|
||||||
}
|
}
|
||||||
toastSuccess('请长按上方图片保存到相册');
|
toastSuccess('请长按上方图片保存到相册');
|
||||||
@@ -172,7 +174,7 @@ export default function ActivityPosterPicker({ initialId }: Props) {
|
|||||||
<div className="partner-activity-panel">
|
<div className="partner-activity-panel">
|
||||||
{qrcodeUrl ? (
|
{qrcodeUrl ? (
|
||||||
<div className="partner-activity-preview partner-activity-preview--qr">
|
<div className="partner-activity-preview partner-activity-preview--qr">
|
||||||
<img src={qrcodeUrl} alt="关联码" />
|
<img className="partner-longpress-img" src={qrcodeUrl} alt="关联码" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="body-md text-muted" style={{ marginBottom: 12 }}>关联码尚未生成</p>
|
<p className="body-md text-muted" style={{ marginBottom: 12 }}>关联码尚未生成</p>
|
||||||
@@ -200,7 +202,7 @@ export default function ActivityPosterPicker({ initialId }: Props) {
|
|||||||
{selectedId === item.id && (
|
{selectedId === item.id && (
|
||||||
<div className="partner-activity-panel">
|
<div className="partner-activity-panel">
|
||||||
<div className="partner-activity-preview">
|
<div className="partner-activity-preview">
|
||||||
<img src={previewUrl || item.imageUrl} alt={item.title} />
|
<img className="partner-longpress-img" src={previewUrl || item.imageUrl} alt={item.title} />
|
||||||
{previewLoading && !previewUrl && (
|
{previewLoading && !previewUrl && (
|
||||||
<div className="partner-activity-preview-mask">正在贴入二维码…</div>
|
<div className="partner-activity-preview-mask">正在贴入二维码…</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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<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,6 +4,7 @@ import PageHeader from '@dukang/shared-ui/PageHeader';
|
|||||||
import type { PartnerAssocSummary } from '@dukang/shared-types';
|
import type { PartnerAssocSummary } from '@dukang/shared-types';
|
||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
import { toastError, toastSuccess } from '../lib/toast';
|
import { toastError, toastSuccess } from '../lib/toast';
|
||||||
|
import { isHttpImageUrl, previewSaveableImage } from '../lib/wechat-save-image';
|
||||||
import { isWechatEnv } from '../lib/weixin';
|
import { isWechatEnv } from '../lib/weixin';
|
||||||
import { usePartnerPageView } from '../lib/usePageView';
|
import { usePartnerPageView } from '../lib/usePageView';
|
||||||
|
|
||||||
@@ -11,7 +12,6 @@ export default function AssocQrcodePage() {
|
|||||||
usePartnerPageView('partner_assoc_qrcode_view');
|
usePartnerPageView('partner_assoc_qrcode_view');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [summary, setSummary] = useState<PartnerAssocSummary | null>(null);
|
const [summary, setSummary] = useState<PartnerAssocSummary | null>(null);
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -21,11 +21,12 @@ export default function AssocQrcodePage() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => () => {
|
|
||||||
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
|
||||||
}, [previewUrl]);
|
|
||||||
|
|
||||||
async function downloadQr() {
|
async function downloadQr() {
|
||||||
|
const publicUrl = summary?.qrcodeUrl;
|
||||||
|
if (isWechatEnv() && isHttpImageUrl(publicUrl)) {
|
||||||
|
await previewSaveableImage(publicUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const token = localStorage.getItem('accessToken');
|
const token = localStorage.getItem('accessToken');
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/partner/assoc/qrcode', {
|
const res = await fetch('/api/v1/partner/assoc/qrcode', {
|
||||||
@@ -37,11 +38,6 @@ export default function AssocQrcodePage() {
|
|||||||
if (!res.ok) throw new Error('下载失败');
|
if (!res.ok) throw new Error('下载失败');
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
if (isWechatEnv()) {
|
|
||||||
setPreviewUrl(url);
|
|
||||||
toastSuccess('请长按图片保存到相册');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `partner-assoc-${summary?.partnerId || 'qr'}.png`;
|
a.download = `partner-assoc-${summary?.partnerId || 'qr'}.png`;
|
||||||
@@ -49,9 +45,8 @@ export default function AssocQrcodePage() {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
toastSuccess('已开始下载');
|
toastSuccess('已开始下载');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (summary?.qrcodeUrl) {
|
if (isWechatEnv() && isHttpImageUrl(publicUrl)) {
|
||||||
setPreviewUrl(summary.qrcodeUrl);
|
await previewSaveableImage(publicUrl);
|
||||||
toastSuccess('请长按图片保存到相册');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toastError(e instanceof Error ? e.message : '下载失败');
|
toastError(e instanceof Error ? e.message : '下载失败');
|
||||||
@@ -70,7 +65,8 @@ export default function AssocQrcodePage() {
|
|||||||
</p>
|
</p>
|
||||||
{summary?.qrcodeUrl ? (
|
{summary?.qrcodeUrl ? (
|
||||||
<img
|
<img
|
||||||
src={previewUrl || summary.qrcodeUrl}
|
className="partner-longpress-img"
|
||||||
|
src={summary.qrcodeUrl}
|
||||||
alt="关联码"
|
alt="关联码"
|
||||||
style={{ width: 220, height: 220, background: '#fff' }}
|
style={{ width: 220, height: 220, background: '#fff' }}
|
||||||
/>
|
/>
|
||||||
@@ -83,11 +79,11 @@ export default function AssocQrcodePage() {
|
|||||||
<button type="button" className="partner-btn-primary" style={{ marginTop: 20 }} onClick={() => void downloadQr()}>
|
<button type="button" className="partner-btn-primary" style={{ marginTop: 20 }} onClick={() => void downloadQr()}>
|
||||||
下载二维码
|
下载二维码
|
||||||
</button>
|
</button>
|
||||||
{previewUrl && isWechatEnv() && (
|
{isWechatEnv() && summary?.qrcodeUrl ? (
|
||||||
<p className="label-md text-muted" style={{ marginTop: 12 }}>
|
<p className="label-md text-muted" style={{ marginTop: 12 }}>
|
||||||
微信内请长按上方图片保存
|
微信内请点「下载二维码」后长按保存
|
||||||
</p>
|
</p>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
fetchAssocQrcodeImage,
|
fetchAssocQrcodeImage,
|
||||||
} from '../lib/activity-posters';
|
} from '../lib/activity-posters';
|
||||||
import { toastError, toastSuccess } from '../lib/toast';
|
import { toastError, toastSuccess } from '../lib/toast';
|
||||||
|
import { blobToDataUrl, isHttpImageUrl, previewSaveableImage } from '../lib/wechat-save-image';
|
||||||
import { isWechatEnv } from '../lib/weixin';
|
import { isWechatEnv } from '../lib/weixin';
|
||||||
import { usePartnerPageView } from '../lib/usePageView';
|
import { usePartnerPageView } from '../lib/usePageView';
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ export default function UsersManagePage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
if (previewUrl?.startsWith('blob:')) URL.revokeObjectURL(previewUrl);
|
||||||
}, [previewUrl]);
|
}, [previewUrl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -131,12 +132,17 @@ export default function UsersManagePage() {
|
|||||||
async function downloadMainImage() {
|
async function downloadMainImage() {
|
||||||
const posterId = summary?.activityPosterId;
|
const posterId = summary?.activityPosterId;
|
||||||
try {
|
try {
|
||||||
|
if (isWechatEnv() && !posterId && isHttpImageUrl(summary?.qrcodeUrl)) {
|
||||||
|
await previewSaveableImage(summary.qrcodeUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const blob = posterId
|
const blob = posterId
|
||||||
? await fetchActivityPosterImage(posterId)
|
? await fetchActivityPosterImage(posterId)
|
||||||
: await fetchAssocQrcodeImage();
|
: await fetchAssocQrcodeImage();
|
||||||
if (isWechatEnv()) {
|
if (isWechatEnv()) {
|
||||||
setPreviewUrl(URL.createObjectURL(blob));
|
if (previewUrl?.startsWith('blob:')) URL.revokeObjectURL(previewUrl);
|
||||||
toastSuccess('请长按图片保存到相册');
|
setPreviewUrl(await blobToDataUrl(blob));
|
||||||
|
toastSuccess('请长按上方图片保存到相册');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
downloadBlob(blob, posterId
|
downloadBlob(blob, posterId
|
||||||
@@ -144,7 +150,11 @@ export default function UsersManagePage() {
|
|||||||
: `partner-assoc-${summary?.partnerId || 'qr'}.png`);
|
: `partner-assoc-${summary?.partnerId || 'qr'}.png`);
|
||||||
toastSuccess('已开始下载');
|
toastSuccess('已开始下载');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!posterId && summary?.qrcodeUrl) {
|
if (!posterId && isHttpImageUrl(summary?.qrcodeUrl)) {
|
||||||
|
if (isWechatEnv()) {
|
||||||
|
await previewSaveableImage(summary.qrcodeUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setPreviewUrl(summary.qrcodeUrl);
|
setPreviewUrl(summary.qrcodeUrl);
|
||||||
toastSuccess('请长按图片保存到相册');
|
toastSuccess('请长按图片保存到相册');
|
||||||
return;
|
return;
|
||||||
@@ -182,6 +192,7 @@ export default function UsersManagePage() {
|
|||||||
</p>
|
</p>
|
||||||
{summary?.activityPosterId && (previewUrl || heroUrl) ? (
|
{summary?.activityPosterId && (previewUrl || heroUrl) ? (
|
||||||
<img
|
<img
|
||||||
|
className="partner-longpress-img"
|
||||||
src={previewUrl || heroUrl || ''}
|
src={previewUrl || heroUrl || ''}
|
||||||
alt="活动图"
|
alt="活动图"
|
||||||
style={{ width: '100%', maxWidth: 360, background: '#f5f5f5' }}
|
style={{ width: '100%', maxWidth: 360, background: '#f5f5f5' }}
|
||||||
@@ -190,6 +201,7 @@ export default function UsersManagePage() {
|
|||||||
<p className="body-md text-muted">正在生成活动图…</p>
|
<p className="body-md text-muted">正在生成活动图…</p>
|
||||||
) : previewUrl || summary?.qrcodeUrl ? (
|
) : previewUrl || summary?.qrcodeUrl ? (
|
||||||
<img
|
<img
|
||||||
|
className="partner-longpress-img"
|
||||||
src={previewUrl || summary?.qrcodeUrl || ''}
|
src={previewUrl || summary?.qrcodeUrl || ''}
|
||||||
alt="关联码"
|
alt="关联码"
|
||||||
style={{ width: 200, height: 200, background: '#fff' }}
|
style={{ width: 200, height: 200, background: '#fff' }}
|
||||||
@@ -203,9 +215,11 @@ export default function UsersManagePage() {
|
|||||||
<button type="button" className="partner-btn-primary" style={{ marginTop: 16 }} onClick={() => void downloadMainImage()}>
|
<button type="button" className="partner-btn-primary" style={{ marginTop: 16 }} onClick={() => void downloadMainImage()}>
|
||||||
{summary?.activityPosterId ? '下载活动图' : '下载二维码'}
|
{summary?.activityPosterId ? '下载活动图' : '下载二维码'}
|
||||||
</button>
|
</button>
|
||||||
{previewUrl && isWechatEnv() && (
|
{isWechatEnv() && (previewUrl || summary?.qrcodeUrl) ? (
|
||||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>微信内请长按上方图片保存</p>
|
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||||
)}
|
微信内请点「{summary?.activityPosterId ? '下载活动图' : '下载二维码'}」后长按保存
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Link to="/center/activity-posters" className="partner-menu-card" style={{ display: 'block', marginBottom: 20, textDecoration: 'none', color: 'inherit' }}>
|
<Link to="/center/activity-posters" className="partner-menu-card" style={{ display: 'block', marginBottom: 20, textDecoration: 'none', color: 'inherit' }}>
|
||||||
|
|||||||
@@ -4087,3 +4087,11 @@ header:has(> .app-page-title:only-child),
|
|||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 微信内长按保存:不要禁用系统菜单 */
|
||||||
|
img.partner-longpress-img {
|
||||||
|
-webkit-touch-callout: default;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const API_ORIGIN =
|
|||||||
(isDevMode ? 'http://192.168.1.10:3010' : 'https://api.dukanghaoke.com');
|
(isDevMode ? 'http://192.168.1.10:3010' : 'https://api.dukanghaoke.com');
|
||||||
|
|
||||||
const requireFromApp = createRequire(path.resolve(__dirname, '../package.json'));
|
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 */
|
/** 解析包目录/文件,避免 pnpm/Vite 把同一 runtime 打成两份 → useDidShow 读到空 reactMeta */
|
||||||
function resolvePkgFile(id: string): string {
|
function resolvePkgFile(id: string): string {
|
||||||
@@ -54,6 +57,7 @@ export default defineConfig(async () => ({
|
|||||||
},
|
},
|
||||||
defineConstants: {
|
defineConstants: {
|
||||||
TARO_APP_API_ORIGIN: JSON.stringify(API_ORIGIN),
|
TARO_APP_API_ORIGIN: JSON.stringify(API_ORIGIN),
|
||||||
|
TARO_APP_VERSION: JSON.stringify(APP_PKG_VERSION),
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
patterns: [
|
patterns: [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dukang/mini-user",
|
"name": "@dukang/mini-user",
|
||||||
"version": "3.5.16",
|
"version": "4.0.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "杜康好客 · C 端用户微信小程序(Taro)",
|
"description": "杜康好客 · C 端用户微信小程序(Taro)",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export default defineAppConfig({
|
|||||||
'pages/stores/index',
|
'pages/stores/index',
|
||||||
'pages/benefit/index',
|
'pages/benefit/index',
|
||||||
'pages/mine/index',
|
'pages/mine/index',
|
||||||
|
'pages/open/index',
|
||||||
],
|
],
|
||||||
subPackages: [
|
subPackages: [
|
||||||
{ root: 'pages/product-detail', pages: ['index'] },
|
{ root: 'pages/product-detail', pages: ['index'] },
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button, Text } from '@tarojs/components';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { toast } from '../lib/api';
|
import { toast } from '../lib/api';
|
||||||
import { getBrandAssetsSync, loadBrandAssets } from '../lib/brand-assets';
|
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 = {
|
export type ContactCsSessionContext = {
|
||||||
orderId?: string;
|
orderId?: string;
|
||||||
@@ -91,6 +91,9 @@ export default function ContactCsButton({
|
|||||||
className={className}
|
className={className}
|
||||||
openType="contact"
|
openType="contact"
|
||||||
sessionFrom={buildCsSessionFrom(session)}
|
sessionFrom={buildCsSessionFrom(session)}
|
||||||
|
showMessageCard
|
||||||
|
sendMessageTitle={buildCsShareTitle(session?.orderNo)}
|
||||||
|
sendMessagePath={buildCsSharePath(session?.orderId)}
|
||||||
hoverClass="none"
|
hoverClass="none"
|
||||||
>
|
>
|
||||||
{typeof children === 'string' ? <Text>{children}</Text> : children}
|
{typeof children === 'string' ? <Text>{children}</Text> : children}
|
||||||
|
|||||||
@@ -130,10 +130,12 @@ export function toast(title: string, icon: 'success' | 'error' | 'none' = 'none'
|
|||||||
const text = title.trim();
|
const text = title.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
if (icon !== 'success' && showFloatingToast(text)) {
|
if (icon !== 'success' && showFloatingToast(text)) {
|
||||||
void Taro.hideToast();
|
void Promise.resolve(Taro.hideToast() as void | Promise<unknown>).catch(() => {});
|
||||||
return;
|
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 = {
|
export type SessionPayload = {
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ export type ClientErrorPayload = {
|
|||||||
extra?: Record<string, unknown>;
|
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 {
|
function currentPagePath(): string | undefined {
|
||||||
try {
|
try {
|
||||||
const pages = Taro.getCurrentPages();
|
const pages = Taro.getCurrentPages();
|
||||||
@@ -97,6 +105,7 @@ export function installClientErrorReporting(): void {
|
|||||||
: typeof reason === 'string'
|
: typeof reason === 'string'
|
||||||
? reason
|
? reason
|
||||||
: JSON.stringify(reason);
|
: JSON.stringify(reason);
|
||||||
|
if (shouldIgnoreRejection(message || '')) return;
|
||||||
const stack = reason instanceof Error ? reason.stack : undefined;
|
const stack = reason instanceof Error ? reason.stack : undefined;
|
||||||
reportClientError({
|
reportClientError({
|
||||||
level: 'error',
|
level: 'error',
|
||||||
@@ -121,15 +130,17 @@ export function installClientErrorReporting(): void {
|
|||||||
});
|
});
|
||||||
window.addEventListener('unhandledrejection', (ev) => {
|
window.addEventListener('unhandledrejection', (ev) => {
|
||||||
const reason = ev.reason;
|
const reason = ev.reason;
|
||||||
|
const message =
|
||||||
|
reason instanceof Error
|
||||||
|
? reason.message
|
||||||
|
: typeof reason === 'string'
|
||||||
|
? reason
|
||||||
|
: 'unhandledrejection';
|
||||||
|
if (shouldIgnoreRejection(message)) return;
|
||||||
reportClientError({
|
reportClientError({
|
||||||
level: 'error',
|
level: 'error',
|
||||||
category: 'unhandled_rejection',
|
category: 'unhandled_rejection',
|
||||||
message:
|
message,
|
||||||
reason instanceof Error
|
|
||||||
? reason.message
|
|
||||||
: typeof reason === 'string'
|
|
||||||
? reason
|
|
||||||
: 'unhandledrejection',
|
|
||||||
stack: reason instanceof Error ? reason.stack : undefined,
|
stack: reason instanceof Error ? reason.stack : undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,13 +14,7 @@ export async function tryGetClientGpsLocation(): Promise<ClientGpsLocation | nul
|
|||||||
if (process.env.TARO_ENV === 'weapp') {
|
if (process.env.TARO_ENV === 'weapp') {
|
||||||
try {
|
try {
|
||||||
const Taro = (await import('@tarojs/taro')).default;
|
const Taro = (await import('@tarojs/taro')).default;
|
||||||
const loc = await new Promise<{ latitude: number; longitude: number }>((resolve, reject) => {
|
const loc = await Taro.getLocation({ type: 'gcj02' });
|
||||||
Taro.getLocation({
|
|
||||||
type: 'gcj02',
|
|
||||||
success: resolve,
|
|
||||||
fail: reject,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return { latitude: loc.latitude, longitude: loc.longitude };
|
return { latitude: loc.latitude, longitude: loc.longitude };
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -11,13 +11,7 @@ export type ClientGpsLocation = {
|
|||||||
|
|
||||||
export async function tryGetClientGpsLocation(): Promise<ClientGpsLocation | null> {
|
export async function tryGetClientGpsLocation(): Promise<ClientGpsLocation | null> {
|
||||||
try {
|
try {
|
||||||
const loc = await new Promise<{ latitude: number; longitude: number }>((resolve, reject) => {
|
const loc = await Taro.getLocation({ type: 'gcj02' });
|
||||||
Taro.getLocation({
|
|
||||||
type: 'gcj02',
|
|
||||||
success: resolve,
|
|
||||||
fail: reject,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return { latitude: loc.latitude, longitude: loc.longitude };
|
return { latitude: loc.latitude, longitude: loc.longitude };
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { fetchClientConfig } from './pay-wechat';
|
import { fetchClientConfig } from './pay-wechat';
|
||||||
|
|
||||||
/** 与 package.json version 同步,供服务端 minClientVersion 比对 */
|
/** 与 package.json version 同步(Taro defineConstants 注入),供 minClientVersion 比对 */
|
||||||
export const APP_VERSION = '3.5.16';
|
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[] {
|
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 {
|
export function compareSemver(a: string, b: string): number {
|
||||||
@@ -89,7 +94,7 @@ async function enforceMinClientVersion(min: string) {
|
|||||||
export async function checkClientVersionGate() {
|
export async function checkClientVersionGate() {
|
||||||
try {
|
try {
|
||||||
const config = await fetchClientConfig();
|
const config = await fetchClientConfig();
|
||||||
const min = config.minClientVersion?.trim();
|
const min = normalizeVersion(config.minClientVersion ?? '');
|
||||||
if (min && compareSemver(APP_VERSION, min) < 0) {
|
if (min && compareSemver(APP_VERSION, min) < 0) {
|
||||||
await enforceMinClientVersion(min);
|
await enforceMinClientVersion(min);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
function isDenyMessage(errMsg?: string): boolean {
|
||||||
return /auth deny|authorize|permission|denied|拒绝|用户拒绝|getLocation:fail/i.test(
|
return /auth deny|authorize|permission|denied|拒绝|用户拒绝|getLocation:fail/i.test(
|
||||||
errMsg || '',
|
errMsg || '',
|
||||||
@@ -179,14 +196,13 @@ async function promptLocationAuthOnce() {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
async function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
||||||
return new Promise((resolve, reject) => {
|
const setting = await Taro.getSetting().catch(() => null);
|
||||||
Taro.getLocation({
|
if (setting?.authSetting?.['scope.userLocation'] === false) {
|
||||||
type: 'gcj02',
|
throw new Error('getLocation:fail auth deny');
|
||||||
success: resolve,
|
}
|
||||||
fail: reject,
|
// 只用返回的 Promise,避免 callback + Promise 双重 reject 变成未捕获
|
||||||
});
|
return Taro.getLocation({ type: 'gcj02' });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveViaH5Jssdk(): Promise<ResolvedUserCity | null> {
|
async function resolveViaH5Jssdk(): Promise<ResolvedUserCity | null> {
|
||||||
@@ -263,7 +279,7 @@ export async function resolveUserCity(force = false): Promise<ResolvedUserCity>
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errMsg = err instanceof Error ? err.message : String(err);
|
const errMsg = wxErrorMessage(err);
|
||||||
const denied = isDenyMessage(errMsg);
|
const denied = isDenyMessage(errMsg);
|
||||||
if (denied && !isLocationDenied()) {
|
if (denied && !isLocationDenied()) {
|
||||||
await promptLocationAuthOnce();
|
await promptLocationAuthOnce();
|
||||||
|
|||||||
@@ -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 {
|
function isDenyMessage(errMsg?: string): boolean {
|
||||||
return /auth deny|authorize|permission|denied|拒绝|用户拒绝|getLocation:fail/i.test(
|
return /auth deny|authorize|permission|denied|拒绝|用户拒绝|getLocation:fail/i.test(
|
||||||
errMsg || '',
|
errMsg || '',
|
||||||
@@ -175,14 +192,13 @@ async function promptLocationAuthOnce() {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
async function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
||||||
return new Promise((resolve, reject) => {
|
const setting = await Taro.getSetting().catch(() => null);
|
||||||
Taro.getLocation({
|
if (setting?.authSetting?.['scope.userLocation'] === false) {
|
||||||
type: 'gcj02',
|
throw new Error('getLocation:fail auth deny');
|
||||||
success: resolve,
|
}
|
||||||
fail: reject,
|
// 只用返回的 Promise,避免 callback + Promise 双重 reject 变成未捕获
|
||||||
});
|
return Taro.getLocation({ type: 'gcj02' });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveUserCity(force = false): Promise<ResolvedUserCity> {
|
export async function resolveUserCity(force = false): Promise<ResolvedUserCity> {
|
||||||
@@ -211,7 +227,7 @@ export async function resolveUserCity(force = false): Promise<ResolvedUserCity>
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errMsg = err instanceof Error ? err.message : String(err);
|
const errMsg = wxErrorMessage(err);
|
||||||
const denied = isDenyMessage(errMsg);
|
const denied = isDenyMessage(errMsg);
|
||||||
if (denied && !isLocationDenied()) {
|
if (denied && !isLocationDenied()) {
|
||||||
await promptLocationAuthOnce();
|
await promptLocationAuthOnce();
|
||||||
|
|||||||
@@ -4,6 +4,17 @@ export type WecomCsSessionContext = {
|
|||||||
from?: string;
|
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 = {
|
type OpenCsChatOption = {
|
||||||
extInfo: { url: string };
|
extInfo: { url: string };
|
||||||
corpId: string;
|
corpId: string;
|
||||||
@@ -97,16 +108,10 @@ export async function openWecomCustomerServiceChat(params: {
|
|||||||
const option: OpenCsChatOption = {
|
const option: OpenCsChatOption = {
|
||||||
extInfo: { url },
|
extInfo: { url },
|
||||||
corpId,
|
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) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
wxApi({
|
wxApi({
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '杜康好客',
|
||||||
|
});
|
||||||
@@ -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 ?? '';
|
const orderId = router.params.id ?? '';
|
||||||
usePageView('order_detail_view', orderId ? { orderId } : undefined);
|
usePageView('order_detail_view', orderId ? { orderId } : undefined);
|
||||||
const [order, setOrder] = useState<OrderDetail | null>(null);
|
const [order, setOrder] = useState<OrderDetail | null>(null);
|
||||||
|
const [loadError, setLoadError] = useState('');
|
||||||
const [confirming, setConfirming] = useState(false);
|
const [confirming, setConfirming] = useState(false);
|
||||||
const [latestTrack, setLatestTrack] = useState<OrderTrackNode | null>(null);
|
const [latestTrack, setLatestTrack] = useState<OrderTrackNode | null>(null);
|
||||||
const [estimatedArrival, setEstimatedArrival] = useState<OrderTrackEstimatedArrival | null>(null);
|
const [estimatedArrival, setEstimatedArrival] = useState<OrderTrackEstimatedArrival | null>(null);
|
||||||
@@ -147,14 +148,18 @@ export default function OrderDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!orderId) return;
|
if (!orderId) {
|
||||||
|
setLoadError('订单信息缺失');
|
||||||
|
return;
|
||||||
|
}
|
||||||
request<OrderDetail>(`/trade/orders/${orderId}`)
|
request<OrderDetail>(`/trade/orders/${orderId}`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setOrder(data);
|
setOrder(data);
|
||||||
|
setLoadError('');
|
||||||
void loadOrderTrack(data.delivery, data.status);
|
void loadOrderTrack(data.delivery, data.status);
|
||||||
applyLocalDeliveryHint(data, setLocalHintHtml);
|
applyLocalDeliveryHint(data, setLocalHintHtml);
|
||||||
})
|
})
|
||||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
.catch((e) => setLoadError(e instanceof Error ? e.message : '加载失败'));
|
||||||
}, [orderId]);
|
}, [orderId]);
|
||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
@@ -293,7 +298,9 @@ export default function OrderDetailPage() {
|
|||||||
/>
|
/>
|
||||||
<View className="sub-page-body">
|
<View className="sub-page-body">
|
||||||
{!order ? (
|
{!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">
|
<View className="order-card">
|
||||||
|
|||||||
Vendored
+1
@@ -13,6 +13,7 @@ declare const defineAppConfig: (config: Record<string, unknown>) => Record<strin
|
|||||||
declare const definePageConfig: (config: Record<string, unknown>) => Record<string, unknown>;
|
declare const definePageConfig: (config: Record<string, unknown>) => Record<string, unknown>;
|
||||||
|
|
||||||
declare const TARO_APP_API_ORIGIN: string;
|
declare const TARO_APP_API_ORIGIN: string;
|
||||||
|
declare const TARO_APP_VERSION: string;
|
||||||
|
|
||||||
/** 微信小程序全局(Taro 未封装的 API 如 openBusinessView 需直接调用) */
|
/** 微信小程序全局(Taro 未封装的 API 如 openBusinessView 需直接调用) */
|
||||||
declare const wx: {
|
declare const wx: {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export { scanQrCode } from './scan';
|
|||||||
export { invokeWechatPay } from './pay';
|
export { invokeWechatPay } from './pay';
|
||||||
export { chooseWechatImages, canUseWechatChooseImage, formatChooseImageFailMessage } from './chooseImage';
|
export { chooseWechatImages, canUseWechatChooseImage, formatChooseImageFailMessage } from './chooseImage';
|
||||||
export type { ChooseWechatImageOptions } from './chooseImage';
|
export type { ChooseWechatImageOptions } from './chooseImage';
|
||||||
|
export { previewWechatImage, isHttpImageUrl } from './previewImage';
|
||||||
export {
|
export {
|
||||||
setWechatShareData,
|
setWechatShareData,
|
||||||
canUseWechatShare,
|
canUseWechatShare,
|
||||||
@@ -53,6 +54,7 @@ import { getWechatLocation, getWechatLocationDetailed } from './location';
|
|||||||
import { scanQrCode } from './scan';
|
import { scanQrCode } from './scan';
|
||||||
import { invokeWechatPay } from './pay';
|
import { invokeWechatPay } from './pay';
|
||||||
import { chooseWechatImages } from './chooseImage';
|
import { chooseWechatImages } from './chooseImage';
|
||||||
|
import { previewWechatImage } from './previewImage';
|
||||||
import { setWechatShareData, shareViaWechatSdk } from './share';
|
import { setWechatShareData, shareViaWechatSdk } from './share';
|
||||||
import {
|
import {
|
||||||
wechatLogin,
|
wechatLogin,
|
||||||
@@ -76,6 +78,7 @@ export function createWeixinSdk(config: WeixinSdkConfig) {
|
|||||||
scanQrCode: (options?: Parameters<typeof scanQrCode>[1]) => scanQrCode(config, options),
|
scanQrCode: (options?: Parameters<typeof scanQrCode>[1]) => scanQrCode(config, options),
|
||||||
chooseImages: (options?: Parameters<typeof chooseWechatImages>[1]) =>
|
chooseImages: (options?: Parameters<typeof chooseWechatImages>[1]) =>
|
||||||
chooseWechatImages(config, options),
|
chooseWechatImages(config, options),
|
||||||
|
previewImage: (url: string) => previewWechatImage(config, url),
|
||||||
pay: (prepay: Parameters<typeof invokeWechatPay>[0]) =>
|
pay: (prepay: Parameters<typeof invokeWechatPay>[0]) =>
|
||||||
invokeWechatPay(prepay, {
|
invokeWechatPay(prepay, {
|
||||||
apiBase: config.apiBase ?? '/api/v1',
|
apiBase: config.apiBase ?? '/api/v1',
|
||||||
|
|||||||
@@ -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<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,6 +58,12 @@ export type WxApi = {
|
|||||||
success?: (res: { localData: string }) => void;
|
success?: (res: { localData: string }) => void;
|
||||||
fail?: (res: { errMsg: string }) => void;
|
fail?: (res: { errMsg: string }) => void;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
previewImage: (options: {
|
||||||
|
current: string;
|
||||||
|
urls: string[];
|
||||||
|
success?: () => void;
|
||||||
|
fail?: (res: { errMsg: string }) => void;
|
||||||
|
}) => void;
|
||||||
updateAppMessageShareData: (options: {
|
updateAppMessageShareData: (options: {
|
||||||
title: string;
|
title: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
@@ -121,6 +127,7 @@ export const DEFAULT_JS_API_LIST = [
|
|||||||
'chooseWXPay',
|
'chooseWXPay',
|
||||||
'chooseImage',
|
'chooseImage',
|
||||||
'getLocalImgData',
|
'getLocalImgData',
|
||||||
|
'previewImage',
|
||||||
'updateAppMessageShareData',
|
'updateAppMessageShareData',
|
||||||
'updateTimelineShareData',
|
'updateTimelineShareData',
|
||||||
'onMenuShareAppMessage',
|
'onMenuShareAppMessage',
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
|
|||||||
group: G.wechat_mini,
|
group: G.wechat_mini,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
requiresRestart: false,
|
requiresRestart: false,
|
||||||
placeholder: '3.4.15',
|
placeholder: '4.0.4',
|
||||||
description: 'semver 格式;客户端低于此版本时提示更新',
|
description: 'semver 格式(可带或不带 v);客户端低于此版本时提示更新',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'USER_H5_URL',
|
key: 'USER_H5_URL',
|
||||||
|
|||||||
Reference in New Issue
Block a user