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;