diff --git a/apps/mini-user/src/components/ShareNavButton.tsx b/apps/mini-user/src/components/ShareNavButton.tsx new file mode 100644 index 0000000..30e83ae --- /dev/null +++ b/apps/mini-user/src/components/ShareNavButton.tsx @@ -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 ( + + ); + } + + return ( + { + void handleShareButtonClick(payload); + }} + > + + + ); +} diff --git a/apps/mini-user/src/components/WechatShareReady.tsx b/apps/mini-user/src/components/WechatShareReady.tsx new file mode 100644 index 0000000..4618218 --- /dev/null +++ b/apps/mini-user/src/components/WechatShareReady.tsx @@ -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; +} diff --git a/apps/mini-user/src/lib/wechat-share.ts b/apps/mini-user/src/lib/wechat-share.ts new file mode 100644 index 0000000..0c7aa17 --- /dev/null +++ b/apps/mini-user/src/lib/wechat-share.ts @@ -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 { + 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, +): Promise { + 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 { + // 小程序:由 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(), + }; +} diff --git a/apps/mini-user/src/lib/weixin.ts b/apps/mini-user/src/lib/weixin.ts new file mode 100644 index 0000000..bca5759 --- /dev/null +++ b/apps/mini-user/src/lib/weixin.ts @@ -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 }; diff --git a/apps/mini-user/src/pages/product-detail/index.config.ts b/apps/mini-user/src/pages/product-detail/index.config.ts index 0603472..ddf25e2 100644 --- a/apps/mini-user/src/pages/product-detail/index.config.ts +++ b/apps/mini-user/src/pages/product-detail/index.config.ts @@ -1,4 +1,6 @@ export default definePageConfig({ navigationStyle: 'custom', navigationBarTitleText: '商品详情', + enableShareAppMessage: true, + enableShareTimeline: true, }); diff --git a/apps/mini-user/src/pages/product-detail/index.tsx b/apps/mini-user/src/pages/product-detail/index.tsx index fbc8626..0b312d4 100644 --- a/apps/mini-user/src/pages/product-detail/index.tsx +++ b/apps/mini-user/src/pages/product-detail/index.tsx @@ -1,18 +1,26 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; 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 PageShell from '../../components/PageShell'; import PageNavBar from '../../components/PageNavBar'; import ProductCarousel from '../../components/ProductCarousel'; +import ShareNavButton from '../../components/ShareNavButton'; +import WechatShareReady from '../../components/WechatShareReady'; import { goLogin } from '../../lib/auth-nav'; import { ensurePayReady } from '../../lib/pay-ready'; import { isLoggedIn, request, toast } from '../../lib/api'; import { getProductCarouselImages, getProductDetailImages, + getProductMainImage, type ProductImageSource, } from '../../lib/product-images'; +import { + DEFAULT_SHARE_DESC, + DEFAULT_SHARE_TITLE, + toWeappShareMessage, +} from '../../lib/wechat-share'; import iconHome from '../../assets/tabbar/home.png'; type Product = ProductImageSource & { @@ -42,6 +50,23 @@ export default function ProductDetailPage() { .catch((e) => toast(e instanceof Error ? e.message : '加载失败')); }, [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() { const pages = Taro.getCurrentPages(); if (pages.length > 1) { @@ -84,16 +109,13 @@ export default function ProductDetailPage() { return ( + toast('分享功能开发中')}> - - - )} + right={} /> diff --git a/apps/mini-user/src/pages/store-detail/index.config.ts b/apps/mini-user/src/pages/store-detail/index.config.ts index e0bc464..b9aea6b 100644 --- a/apps/mini-user/src/pages/store-detail/index.config.ts +++ b/apps/mini-user/src/pages/store-detail/index.config.ts @@ -1,4 +1,6 @@ export default definePageConfig({ navigationStyle: 'custom', navigationBarTitleText: '门店详情', + enableShareAppMessage: true, + enableShareTimeline: true, }); diff --git a/apps/mini-user/src/pages/store-detail/index.tsx b/apps/mini-user/src/pages/store-detail/index.tsx index 8a1ee81..2a68d97 100644 --- a/apps/mini-user/src/pages/store-detail/index.tsx +++ b/apps/mini-user/src/pages/store-detail/index.tsx @@ -1,10 +1,17 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; 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 PageNavBar from '../../components/PageNavBar'; import ProductCarousel from '../../components/ProductCarousel'; +import ShareNavButton from '../../components/ShareNavButton'; +import WechatShareReady from '../../components/WechatShareReady'; import { request, toast } from '../../lib/api'; +import { + DEFAULT_SHARE_DESC, + DEFAULT_SHARE_TITLE, + toWeappShareMessage, +} from '../../lib/wechat-share'; type Store = { id: string; @@ -44,6 +51,23 @@ export default function StoreDetailPage() { }); }, [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() { const pages = Taro.getCurrentPages(); if (pages.length > 1) Taro.navigateBack(); @@ -68,11 +92,13 @@ export default function StoreDetailPage() { return ( + } /> @@ -98,17 +124,9 @@ export default function StoreDetailPage() { - - 门店服务 - 支持好客权益到店核销,部分门店提供包间预约。 - - - Taro.navigateTo({ url: '/pages/redeem/index' })} - > - 去核销 + toast('核销请前往「好客权益」')}> + 到店核销 diff --git a/apps/mini-user/src/styles/nav-bar.css b/apps/mini-user/src/styles/nav-bar.css index 10a0d3a..00f3b96 100644 --- a/apps/mini-user/src/styles/nav-bar.css +++ b/apps/mini-user/src/styles/nav-bar.css @@ -169,6 +169,24 @@ 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 { position: absolute; left: 0;