From 3f60c37265b04bd967bc738069d18cbff6f1c936 Mon Sep 17 00:00:00 2001 From: jacy-dukang Date: Thu, 9 Jul 2026 17:14:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=88=86=E4=BA=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/h5-user/src/App.tsx | 2 + .../src/components/WechatShareBootstrap.tsx | 14 ++++ apps/h5-user/src/lib/wechat-share.ts | 47 ++++++++++++ apps/h5-user/src/pages/OrderDetailPage.tsx | 7 +- apps/h5-user/src/pages/ProductDetailPage.tsx | 7 +- apps/h5-user/src/pages/StoreDetailPage.tsx | 7 +- packages/weixin-sdk/src/index.ts | 9 +++ packages/weixin-sdk/src/share.ts | 75 +++++++++++++++++++ packages/weixin-sdk/src/types.ts | 15 ++++ 9 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 apps/h5-user/src/components/WechatShareBootstrap.tsx create mode 100644 apps/h5-user/src/lib/wechat-share.ts create mode 100644 packages/weixin-sdk/src/share.ts diff --git a/apps/h5-user/src/App.tsx b/apps/h5-user/src/App.tsx index 53e0e19..3e051a4 100644 --- a/apps/h5-user/src/App.tsx +++ b/apps/h5-user/src/App.tsx @@ -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 ( + } /> }> diff --git a/apps/h5-user/src/components/WechatShareBootstrap.tsx b/apps/h5-user/src/components/WechatShareBootstrap.tsx new file mode 100644 index 0000000..d49165c --- /dev/null +++ b/apps/h5-user/src/components/WechatShareBootstrap.tsx @@ -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; +} diff --git a/apps/h5-user/src/lib/wechat-share.ts b/apps/h5-user/src/lib/wechat-share.ts new file mode 100644 index 0000000..8ff447d --- /dev/null +++ b/apps/h5-user/src/lib/wechat-share.ts @@ -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 { + 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, +): Promise { + 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('分享配置失败,请刷新页面后重试')); +} diff --git a/apps/h5-user/src/pages/OrderDetailPage.tsx b/apps/h5-user/src/pages/OrderDetailPage.tsx index ddb79b5..f2c2b3f 100644 --- a/apps/h5-user/src/pages/OrderDetailPage.tsx +++ b/apps/h5-user/src/pages/OrderDetailPage.tsx @@ -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(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() {

杜康好客

-
+ +
diff --git a/apps/h5-user/src/pages/ProductDetailPage.tsx b/apps/h5-user/src/pages/ProductDetailPage.tsx index 4c5038e..0ceab31 100644 --- a/apps/h5-user/src/pages/ProductDetailPage.tsx +++ b/apps/h5-user/src/pages/ProductDetailPage.tsx @@ -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(null); const [headerSolid, setHeaderSolid] = useState(false); + const [toast, setToast] = useState(''); useEffect(() => { if (id) request('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)} > share + +
diff --git a/apps/h5-user/src/pages/StoreDetailPage.tsx b/apps/h5-user/src/pages/StoreDetailPage.tsx index ac7b9ad..6e28ccb 100644 --- a/apps/h5-user/src/pages/StoreDetailPage.tsx +++ b/apps/h5-user/src/pages/StoreDetailPage.tsx @@ -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(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() { arrow_back

门店详情

- + +
diff --git a/packages/weixin-sdk/src/index.ts b/packages/weixin-sdk/src/index.ts index 47c8d7c..7f66347 100644 --- a/packages/weixin-sdk/src/index.ts +++ b/packages/weixin-sdk/src/index.ts @@ -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[1]) => + setWechatShareData(config, data), }; } diff --git a/packages/weixin-sdk/src/share.ts b/packages/weixin-sdk/src/share.ts new file mode 100644 index 0000000..fb8a70e --- /dev/null +++ b/packages/weixin-sdk/src/share.ts @@ -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 { + 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 { + 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); +} diff --git a/packages/weixin-sdk/src/types.ts b/packages/weixin-sdk/src/types.ts index 5842c5a..43a2902 100644 --- a/packages/weixin-sdk/src/types.ts +++ b/packages/weixin-sdk/src/types.ts @@ -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 = {