微信分享功能

This commit is contained in:
2026-07-14 17:48:07 +08:00
parent 5222ec82b0
commit 9b13d02dde
9 changed files with 271 additions and 19 deletions
@@ -0,0 +1,32 @@
import { Button, Text, View } from '@tarojs/components';
import { handleShareButtonClick, type PageSharePayload } from '../lib/wechat-share';
type ShareNavButtonProps = {
payload?: PageSharePayload;
};
/**
* 顶栏分享:
* - 小程序:open-type=share 弹出微信分享面板
* - H5 微信:JSSDK 配置后提示点右上角 ···
*/
export default function ShareNavButton({ payload }: ShareNavButtonProps) {
if (process.env.TARO_ENV === 'weapp') {
return (
<Button className="page-nav-bar__btn page-nav-bar__share-btn" openType="share" hoverClass="none">
<Text className="page-nav-bar__icon page-nav-bar__icon--share"></Text>
</Button>
);
}
return (
<View
className="page-nav-bar__btn"
onClick={() => {
void handleShareButtonClick(payload);
}}
>
<Text className="page-nav-bar__icon page-nav-bar__icon--share"></Text>
</View>
);
}
@@ -0,0 +1,46 @@
import { useEffect } from 'react';
import Taro, { useDidShow } from '@tarojs/taro';
import { captureIosJssdkEntryUrl } from '@dukang/weixin-sdk';
import { applyWechatShare, type PageSharePayload } from '../lib/wechat-share';
/**
* H5:进入页面时刷新微信分享卡片;
* 小程序:开启右上角分享菜单。
*/
export default function WechatShareReady({ payload }: { payload?: PageSharePayload }) {
useEffect(() => {
if (process.env.TARO_ENV === 'h5') {
captureIosJssdkEntryUrl();
}
}, []);
useDidShow(() => {
if (process.env.TARO_ENV === 'weapp') {
void Taro.showShareMenu({
withShareTicket: true,
showShareItems: ['shareAppMessage', 'shareTimeline'],
}).catch(() => {
void Taro.showShareMenu({ withShareTicket: true }).catch(() => {});
});
return;
}
void applyWechatShare({
title: payload?.title,
desc: payload?.desc,
imgUrl: payload?.imgUrl,
link: payload?.link,
}).catch(() => {});
});
useEffect(() => {
if (process.env.TARO_ENV !== 'h5') return;
void applyWechatShare({
title: payload?.title,
desc: payload?.desc,
imgUrl: payload?.imgUrl,
link: payload?.link,
}).catch(() => {});
}, [payload?.title, payload?.desc, payload?.imgUrl, payload?.link]);
return null;
}
+97
View File
@@ -0,0 +1,97 @@
import Taro from '@tarojs/taro';
import { BRAND_LOGO_URL } from '@dukang/shared-types';
import type { WechatShareData } from '@dukang/weixin-sdk';
import { getWechatShareLink, isWechatBrowser, isMiniProgram } from '@dukang/weixin-sdk';
import { toast } from './api';
import { isWechatEnv, weixinSdk } from './weixin';
export const DEFAULT_SHARE_TITLE = '你吃饭,杜康买单';
export const DEFAULT_SHARE_DESC = '杜康好客 · 买酒享权益,全城门店可用';
export const WECHAT_SHARE_HINT = '请点击右上角 ··· 分享给好友';
export function getDefaultShareImageUrl(): string {
return BRAND_LOGO_URL;
}
export function buildDefaultShareData(
overrides?: Partial<WechatShareData>,
): WechatShareData {
let link = overrides?.link;
if (!link) {
if (process.env.TARO_ENV === 'h5' && typeof window !== 'undefined') {
link = getWechatShareLink();
} else {
link = '';
}
}
return {
title: overrides?.title ?? DEFAULT_SHARE_TITLE,
desc: overrides?.desc ?? DEFAULT_SHARE_DESC,
link,
imgUrl: overrides?.imgUrl ?? getDefaultShareImageUrl(),
};
}
/** 配置 H5 微信内右上角分享卡片 */
export async function applyWechatShare(
overrides?: Partial<WechatShareData>,
): Promise<void> {
if (process.env.TARO_ENV !== 'h5') return;
if (!isWechatBrowser()) return;
await weixinSdk.setShare(buildDefaultShareData(overrides));
}
export type PageSharePayload = {
title?: string;
desc?: string;
/** 小程序分享 path,如 /pages/product-detail/index?id=1 */
path?: string;
imgUrl?: string;
/** H5 自定义分享 link,默认当前页 */
link?: string;
};
/** 点击「分享」按钮 */
export async function handleShareButtonClick(payload?: PageSharePayload): Promise<void> {
// 小程序:由 Button open-type="share" 触发,无需在此拉起
if (process.env.TARO_ENV === 'weapp' || isMiniProgram()) {
try {
await Taro.showShareMenu({ withShareTicket: true, showShareItems: ['shareAppMessage', 'shareTimeline'] });
} catch {
/* 旧基础库可能不支持 showShareItems */
try {
await Taro.showShareMenu({ withShareTicket: true });
} catch {
/* ignore */
}
}
toast(WECHAT_SHARE_HINT);
return;
}
if (!isWechatEnv()) {
toast('请在微信内打开后分享');
return;
}
try {
await applyWechatShare({
title: payload?.title,
desc: payload?.desc,
imgUrl: payload?.imgUrl,
link: payload?.link,
});
toast(WECHAT_SHARE_HINT);
} catch {
toast('分享配置失败,请刷新后重试');
}
}
/** 供 useShareAppMessage 使用的标题/路径/图 */
export function toWeappShareMessage(payload?: PageSharePayload) {
return {
title: payload?.title || DEFAULT_SHARE_TITLE,
path: payload?.path || '/pages/home/index',
imageUrl: payload?.imgUrl || getDefaultShareImageUrl(),
};
}
+15
View File
@@ -0,0 +1,15 @@
import { ClientApp } from '@dukang/shared-types';
import { createWeixinSdk, isWechatEnv } from '@dukang/weixin-sdk';
import { API_BASE, getToken } from './api';
/**
* H5 公众号内 JSSDK(分享 / 定位等)走 USER_H5 对应的公众号 appId。
* 小程序原生能力不经此门面。
*/
export const weixinSdk = createWeixinSdk({
apiBase: API_BASE,
clientApp: process.env.TARO_ENV === 'h5' ? ClientApp.USER_H5 : ClientApp.USER_MINI,
getAccessToken: () => getToken() || null,
});
export { isWechatEnv };
@@ -1,4 +1,6 @@
export default definePageConfig({ export default definePageConfig({
navigationStyle: 'custom', navigationStyle: 'custom',
navigationBarTitleText: '商品详情', navigationBarTitleText: '商品详情',
enableShareAppMessage: true,
enableShareTimeline: true,
}); });
@@ -1,18 +1,26 @@
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { View, Text, Image } from '@tarojs/components'; import { View, Text, Image } from '@tarojs/components';
import Taro, { usePageScroll, useRouter } from '@tarojs/taro'; import Taro, { usePageScroll, useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
import type { ProductDetailContentDto } from '@dukang/shared-types'; import type { ProductDetailContentDto } from '@dukang/shared-types';
import PageShell from '../../components/PageShell'; import PageShell from '../../components/PageShell';
import PageNavBar from '../../components/PageNavBar'; import PageNavBar from '../../components/PageNavBar';
import ProductCarousel from '../../components/ProductCarousel'; import ProductCarousel from '../../components/ProductCarousel';
import ShareNavButton from '../../components/ShareNavButton';
import WechatShareReady from '../../components/WechatShareReady';
import { goLogin } from '../../lib/auth-nav'; import { goLogin } from '../../lib/auth-nav';
import { ensurePayReady } from '../../lib/pay-ready'; import { ensurePayReady } from '../../lib/pay-ready';
import { isLoggedIn, request, toast } from '../../lib/api'; import { isLoggedIn, request, toast } from '../../lib/api';
import { import {
getProductCarouselImages, getProductCarouselImages,
getProductDetailImages, getProductDetailImages,
getProductMainImage,
type ProductImageSource, type ProductImageSource,
} from '../../lib/product-images'; } from '../../lib/product-images';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
toWeappShareMessage,
} from '../../lib/wechat-share';
import iconHome from '../../assets/tabbar/home.png'; import iconHome from '../../assets/tabbar/home.png';
type Product = ProductImageSource & { type Product = ProductImageSource & {
@@ -42,6 +50,23 @@ export default function ProductDetailPage() {
.catch((e) => toast(e instanceof Error ? e.message : '加载失败')); .catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
}, [productId]); }, [productId]);
const sharePayload = useMemo(
() => ({
title: product?.name || DEFAULT_SHARE_TITLE,
desc: product?.subtitle || DEFAULT_SHARE_DESC,
path: `/pages/product-detail/index?id=${productId}`,
imgUrl: (product ? getProductMainImage(product) : '') || undefined,
}),
[product, productId],
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: productId ? `id=${productId}` : '',
imageUrl: sharePayload.imgUrl,
}));
function goBack() { function goBack() {
const pages = Taro.getCurrentPages(); const pages = Taro.getCurrentPages();
if (pages.length > 1) { if (pages.length > 1) {
@@ -84,16 +109,13 @@ export default function ProductDetailPage() {
return ( return (
<PageShell variant="scroll" className="product-detail-page" hasFixedFooter> <PageShell variant="scroll" className="product-detail-page" hasFixedFooter>
<WechatShareReady payload={sharePayload} />
<PageNavBar <PageNavBar
title={product.name} title={product.name}
solid={headerSolid} solid={headerSolid}
titleVisible={headerSolid} titleVisible={headerSolid}
onBack={goBack} onBack={goBack}
right={( right={<ShareNavButton payload={sharePayload} />}
<View className="page-nav-bar__btn" onClick={() => toast('分享功能开发中')}>
<Text className="page-nav-bar__icon page-nav-bar__icon--share"></Text>
</View>
)}
/> />
<View className="product-detail-main"> <View className="product-detail-main">
@@ -1,4 +1,6 @@
export default definePageConfig({ export default definePageConfig({
navigationStyle: 'custom', navigationStyle: 'custom',
navigationBarTitleText: '门店详情', navigationBarTitleText: '门店详情',
enableShareAppMessage: true,
enableShareTimeline: true,
}); });
+30 -12
View File
@@ -1,10 +1,17 @@
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { View, Text } from '@tarojs/components'; import { View, Text } from '@tarojs/components';
import Taro, { usePageScroll, useRouter } from '@tarojs/taro'; import Taro, { usePageScroll, useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
import PageShell from '../../components/PageShell'; import PageShell from '../../components/PageShell';
import PageNavBar from '../../components/PageNavBar'; import PageNavBar from '../../components/PageNavBar';
import ProductCarousel from '../../components/ProductCarousel'; import ProductCarousel from '../../components/ProductCarousel';
import ShareNavButton from '../../components/ShareNavButton';
import WechatShareReady from '../../components/WechatShareReady';
import { request, toast } from '../../lib/api'; import { request, toast } from '../../lib/api';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
toWeappShareMessage,
} from '../../lib/wechat-share';
type Store = { type Store = {
id: string; id: string;
@@ -44,6 +51,23 @@ export default function StoreDetailPage() {
}); });
}, [storeId]); }, [storeId]);
const sharePayload = useMemo(
() => ({
title: store?.name || DEFAULT_SHARE_TITLE,
desc: store?.address || DEFAULT_SHARE_DESC,
path: `/pages/store-detail/index?id=${storeId}`,
imgUrl: store?.coverUrl || store?.carouselUrls?.[0] || undefined,
}),
[store, storeId],
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: storeId ? `id=${storeId}` : '',
imageUrl: sharePayload.imgUrl,
}));
function goBack() { function goBack() {
const pages = Taro.getCurrentPages(); const pages = Taro.getCurrentPages();
if (pages.length > 1) Taro.navigateBack(); if (pages.length > 1) Taro.navigateBack();
@@ -68,11 +92,13 @@ export default function StoreDetailPage() {
return ( return (
<PageShell variant="scroll" className="store-detail-page" hasFixedFooter> <PageShell variant="scroll" className="store-detail-page" hasFixedFooter>
<WechatShareReady payload={sharePayload} />
<PageNavBar <PageNavBar
title={store.name} title={store.name}
solid={headerSolid} solid={headerSolid}
titleVisible={headerSolid} titleVisible={headerSolid}
onBack={goBack} onBack={goBack}
right={<ShareNavButton payload={sharePayload} />}
/> />
<View className="store-detail-hero full-bleed"> <View className="store-detail-hero full-bleed">
@@ -98,17 +124,9 @@ export default function StoreDetailPage() {
</View> </View>
</View> </View>
<View className="store-detail-section">
<Text className="store-detail-section-title"></Text>
<Text className="store-detail-meta"></Text>
</View>
<View className="store-detail-bar"> <View className="store-detail-bar">
<View <View className="u-btn u-btn--block" onClick={() => toast('核销请前往「好客权益」')}>
className="store-detail-bar-btn store-detail-bar-btn--primary store-detail-bar-btn--full" <Text></Text>
onClick={() => Taro.navigateTo({ url: '/pages/redeem/index' })}
>
<Text></Text>
</View> </View>
</View> </View>
</PageShell> </PageShell>
+18
View File
@@ -169,6 +169,24 @@
font-weight: 600; font-weight: 600;
} }
/* 小程序分享 Button 去默认样式,对齐圆形胶囊按钮 */
.page-nav-bar__share-btn {
padding: 0 !important;
margin: 0 !important;
border: none !important;
line-height: 1 !important;
background: rgba(255, 255, 255, 0.8) !important;
box-sizing: border-box;
}
.page-nav-bar__share-btn::after {
border: none !important;
}
.page-nav-bar--solid .page-nav-bar__share-btn {
background: transparent !important;
}
.page-nav-bar__title { .page-nav-bar__title {
position: absolute; position: absolute;
left: 0; left: 0;