微信分享

This commit is contained in:
2026-07-09 17:14:00 +08:00
parent 3d3588babb
commit 3f60c37265
9 changed files with 180 additions and 3 deletions
+2
View File
@@ -21,6 +21,7 @@ import PayPage from './pages/PayPage';
import CustomerServicePage from './pages/CustomerServicePage';
import { UserSessionProvider } from './contexts/UserSessionContext';
import { capturePromoFromUrl, touchPromoIfNeeded } from './lib/promo';
import WechatShareBootstrap from './components/WechatShareBootstrap';
function PromoBootstrap() {
useEffect(() => {
@@ -34,6 +35,7 @@ export default function App() {
return (
<UserSessionProvider>
<PromoBootstrap />
<WechatShareBootstrap />
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<TabLayout />}>
@@ -0,0 +1,14 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { applyDefaultWechatShare } from '../lib/wechat-share';
/** 路由变化时刷新微信右上角分享卡片 */
export default function WechatShareBootstrap() {
const location = useLocation();
useEffect(() => {
void applyDefaultWechatShare().catch(() => {});
}, [location.pathname, location.search]);
return null;
}
+47
View File
@@ -0,0 +1,47 @@
import type { WechatShareData } from '@dukang/weixin-sdk';
import { getWechatShareLink, toAppPath } from '@dukang/weixin-sdk';
import { isWechatEnv, weixinSdk } from './weixin';
export const DEFAULT_SHARE_TITLE = '你吃饭,杜康买单';
export const DEFAULT_SHARE_DESC = '杜康好客 · 买酒享权益,全城门店可用';
export const WECHAT_SHARE_HINT = '请点击右上角 ··· 分享给好友';
export function getDefaultShareImageUrl(): string {
if (typeof window === 'undefined') return toAppPath('/logo.png');
return new URL(toAppPath('/logo.png'), window.location.origin).href;
}
export function buildDefaultShareData(
overrides?: Partial<WechatShareData>,
): WechatShareData {
return {
title: overrides?.title ?? DEFAULT_SHARE_TITLE,
desc: overrides?.desc ?? DEFAULT_SHARE_DESC,
link: overrides?.link ?? getWechatShareLink(),
imgUrl: overrides?.imgUrl ?? getDefaultShareImageUrl(),
};
}
export async function applyDefaultWechatShare(
overrides?: Partial<WechatShareData>,
): Promise<void> {
if (!isWechatEnv()) return;
await weixinSdk.setShare(buildDefaultShareData(overrides));
}
export function handleShareButtonClick(onHint: (message: string) => void): void {
const showHint = (message: string) => {
onHint(message);
if (message) {
window.setTimeout(() => onHint(''), 2500);
}
};
if (!isWechatEnv()) {
showHint('请在微信内打开后分享');
return;
}
void applyDefaultWechatShare()
.then(() => showHint(WECHAT_SHARE_HINT))
.catch(() => showHint('分享配置失败,请刷新页面后重试'));
}
+6 -1
View File
@@ -1,9 +1,11 @@
import { useEffect, useMemo, useState } from 'react';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage';
import AppToast from '../components/AppToast';
import { request } from '../lib/api';
import { buildOrderAddressSelectUrl } from '../lib/navigation';
import { STITCH_ORDER_PRODUCT_IMAGE } from '../lib/order-images';
import { handleShareButtonClick } from '../lib/wechat-share';
import ContactCustomerSheet from '../components/ContactCustomerSheet';
type OrderItem = {
@@ -121,6 +123,7 @@ export default function OrderDetailPage() {
const [order, setOrder] = useState<Order | null>(null);
const [showCs, setShowCs] = useState(false);
const [copyHint, setCopyHint] = useState('');
const [shareToast, setShareToast] = useState('');
const [confirming, setConfirming] = useState(false);
const isReship = order?.orderType === 'RESHIPMENT' || params.get('type') === 'reship';
@@ -206,7 +209,7 @@ export default function OrderDetailPage() {
</button>
<h1 className="order-detail-topbar-title"></h1>
<div className="order-detail-topbar-actions">
<button type="button" className="order-detail-topbar-btn" aria-label="分享" onClick={() => {}}>
<button type="button" className="order-detail-topbar-btn" aria-label="分享" onClick={() => handleShareButtonClick(setShareToast)}>
<span className="material-symbols-outlined">share</span>
</button>
<button type="button" className="order-detail-topbar-btn" aria-label="更多" onClick={() => {}}>
@@ -215,6 +218,8 @@ export default function OrderDetailPage() {
</div>
</header>
<AppToast message={shareToast} />
<main className="order-detail-main order-detail-main--stitch">
<section className="order-detail-status-card">
<div className="order-detail-status-deco" aria-hidden>
+6 -1
View File
@@ -3,8 +3,10 @@ import { Link, useNavigate, useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import AppImage from '@dukang/shared-ui/AppImage';
import ProductCarousel from '../components/ProductCarousel';
import AppToast from '../components/AppToast';
import { request } from '../lib/api';
import { track } from '../lib/analytics';
import { handleShareButtonClick } from '../lib/wechat-share';
import { getProductCarouselImages, getProductDetailImages } from '../lib/product-images';
import type { ProductImageSource } from '../lib/product-images';
@@ -21,6 +23,7 @@ export default function ProductDetailPage() {
const navigate = useNavigate();
const [product, setProduct] = useState<Product | null>(null);
const [headerSolid, setHeaderSolid] = useState(false);
const [toast, setToast] = useState('');
useEffect(() => {
if (id) request<Product>('USER_H5', `/catalog/products/${id}`).then(setProduct);
@@ -66,12 +69,14 @@ export default function ProductDetailPage() {
type="button"
className="product-detail-header-btn"
aria-label="分享"
onClick={() => {}}
onClick={() => handleShareButtonClick(setToast)}
>
<span className="material-symbols-outlined">share</span>
</button>
</header>
<AppToast message={toast} />
<main className="product-detail-main">
<section className="product-detail-hero">
<ProductCarousel images={carouselImages} alt={product.name} variant="detail" />
+6 -1
View File
@@ -2,8 +2,10 @@ import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage';
import ProductCarousel from '../components/ProductCarousel';
import AppToast from '../components/AppToast';
import { request } from '../lib/api';
import { track } from '../lib/analytics';
import { handleShareButtonClick } from '../lib/wechat-share';
import { STITCH_STORE_MAP, getStoreGalleryImages } from '../lib/store-images';
type StoreMedia = { url: string; mediaType?: string; sortOrder?: number };
@@ -54,6 +56,7 @@ export default function StoreDetailPage() {
const [store, setStore] = useState<StoreDetail | null>(null);
const [benefitBalance, setBenefitBalance] = useState(0);
const [headerSolid, setHeaderSolid] = useState(false);
const [toast, setToast] = useState('');
useEffect(() => {
if (id) {
@@ -114,11 +117,13 @@ export default function StoreDetailPage() {
<span className="material-symbols-outlined">arrow_back</span>
</button>
<h1 className={`app-page-title store-detail-header-title${headerSolid ? ' visible' : ''}`}></h1>
<button type="button" className="store-detail-header-btn" aria-label="分享" onClick={() => {}}>
<button type="button" className="store-detail-header-btn" aria-label="分享" onClick={() => handleShareButtonClick(setToast)}>
<span className="material-symbols-outlined">share</span>
</button>
</header>
<AppToast message={toast} />
<main className="store-detail-main">
<section className="store-detail-hero">
<ProductCarousel images={galleryImages} alt={store.name} variant="store" />
+9
View File
@@ -21,6 +21,12 @@ export { scanQrCode } from './scan';
export { invokeWechatPay } from './pay';
export { chooseWechatImages, canUseWechatChooseImage } from './chooseImage';
export type { ChooseWechatImageOptions } from './chooseImage';
export {
setWechatShareData,
canUseWechatShare,
getWechatShareLink,
} from './share';
export type { WechatShareData } from './share';
export {
getWechatOAuthUrl,
startWechatOAuthLogin,
@@ -41,6 +47,7 @@ import { getWechatLocation, getWechatLocationDetailed } from './location';
import { scanQrCode } from './scan';
import { invokeWechatPay } from './pay';
import { chooseWechatImages } from './chooseImage';
import { setWechatShareData } from './share';
import {
wechatLogin,
handleWechatOAuthCallback,
@@ -68,5 +75,7 @@ export function createWeixinSdk(config: WeixinSdkConfig) {
clientApp: config.clientApp,
getAccessToken: config.getAccessToken,
}),
setShare: (data: Parameters<typeof setWechatShareData>[1]) =>
setWechatShareData(config, data),
};
}
+75
View File
@@ -0,0 +1,75 @@
import { ensureJssdkReady, normalizeJssdkPageUrl } from './jssdk';
import { getRuntimePlatform, isWechatBrowser } from './env';
import type { WeixinSdkConfig } from './types';
export type WechatShareData = {
title: string;
desc: string;
link: string;
imgUrl: string;
};
export function canUseWechatShare(): boolean {
return isWechatBrowser() || getRuntimePlatform() === 'mini';
}
function applyShareData(data: WechatShareData): Promise<void> {
return new Promise((resolve, reject) => {
const wx = window.wx;
if (!wx?.updateAppMessageShareData || !wx.updateTimelineShareData) {
reject(new Error('当前微信版本不支持分享,请升级微信后重试'));
return;
}
let pending = 2;
let failed = false;
const done = (err?: Error) => {
if (err && !failed) {
failed = true;
reject(err);
return;
}
pending -= 1;
if (pending === 0 && !failed) resolve();
};
wx.updateAppMessageShareData({
title: data.title,
desc: data.desc,
link: data.link,
imgUrl: data.imgUrl,
success: () => done(),
fail: (res) => done(new Error(res.errMsg || '设置分享给朋友失败')),
});
wx.updateTimelineShareData({
title: data.title,
link: data.link,
imgUrl: data.imgUrl,
success: () => done(),
fail: (res) => done(new Error(res.errMsg || '设置分享到朋友圈失败')),
});
});
}
/** 配置微信内分享卡片(好友 / 朋友圈) */
export async function setWechatShareData(
config: WeixinSdkConfig,
data: WechatShareData,
): Promise<void> {
if (!canUseWechatShare()) return;
await ensureJssdkReady({
apiBase: config.apiBase ?? '/api/v1',
clientApp: config.clientApp,
getAccessToken: config.getAccessToken,
});
await applyShareData(data);
}
/** 获取当前页分享链接(去掉 hash、OAuth 回跳参数) */
export function getWechatShareLink(rawUrl?: string): string {
if (typeof window === 'undefined') return rawUrl ?? '';
return normalizeJssdkPageUrl(rawUrl ?? window.location.href);
}
+15
View File
@@ -53,6 +53,21 @@ export type WxApi = {
success?: (res: { localData: string }) => void;
fail?: (res: { errMsg: string }) => void;
}) => void;
updateAppMessageShareData: (options: {
title: string;
desc: string;
link: string;
imgUrl: string;
success?: () => void;
fail?: (res: { errMsg: string }) => void;
}) => void;
updateTimelineShareData: (options: {
title: string;
link: string;
imgUrl: string;
success?: () => void;
fail?: (res: { errMsg: string }) => void;
}) => void;
};
export type MiniProgramWx = {