微信分享
This commit is contained in:
@@ -21,6 +21,7 @@ import PayPage from './pages/PayPage';
|
|||||||
import CustomerServicePage from './pages/CustomerServicePage';
|
import CustomerServicePage from './pages/CustomerServicePage';
|
||||||
import { UserSessionProvider } from './contexts/UserSessionContext';
|
import { UserSessionProvider } from './contexts/UserSessionContext';
|
||||||
import { capturePromoFromUrl, touchPromoIfNeeded } from './lib/promo';
|
import { capturePromoFromUrl, touchPromoIfNeeded } from './lib/promo';
|
||||||
|
import WechatShareBootstrap from './components/WechatShareBootstrap';
|
||||||
|
|
||||||
function PromoBootstrap() {
|
function PromoBootstrap() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,6 +35,7 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<UserSessionProvider>
|
<UserSessionProvider>
|
||||||
<PromoBootstrap />
|
<PromoBootstrap />
|
||||||
|
<WechatShareBootstrap />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route element={<TabLayout />}>
|
<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;
|
||||||
|
}
|
||||||
@@ -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('分享配置失败,请刷新页面后重试'));
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import AppImage from '@dukang/shared-ui/AppImage';
|
import AppImage from '@dukang/shared-ui/AppImage';
|
||||||
|
import AppToast from '../components/AppToast';
|
||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
import { buildOrderAddressSelectUrl } from '../lib/navigation';
|
import { buildOrderAddressSelectUrl } from '../lib/navigation';
|
||||||
import { STITCH_ORDER_PRODUCT_IMAGE } from '../lib/order-images';
|
import { STITCH_ORDER_PRODUCT_IMAGE } from '../lib/order-images';
|
||||||
|
import { handleShareButtonClick } from '../lib/wechat-share';
|
||||||
import ContactCustomerSheet from '../components/ContactCustomerSheet';
|
import ContactCustomerSheet from '../components/ContactCustomerSheet';
|
||||||
|
|
||||||
type OrderItem = {
|
type OrderItem = {
|
||||||
@@ -121,6 +123,7 @@ export default function OrderDetailPage() {
|
|||||||
const [order, setOrder] = useState<Order | null>(null);
|
const [order, setOrder] = useState<Order | null>(null);
|
||||||
const [showCs, setShowCs] = useState(false);
|
const [showCs, setShowCs] = useState(false);
|
||||||
const [copyHint, setCopyHint] = useState('');
|
const [copyHint, setCopyHint] = useState('');
|
||||||
|
const [shareToast, setShareToast] = useState('');
|
||||||
const [confirming, setConfirming] = useState(false);
|
const [confirming, setConfirming] = useState(false);
|
||||||
|
|
||||||
const isReship = order?.orderType === 'RESHIPMENT' || params.get('type') === 'reship';
|
const isReship = order?.orderType === 'RESHIPMENT' || params.get('type') === 'reship';
|
||||||
@@ -206,7 +209,7 @@ export default function OrderDetailPage() {
|
|||||||
</button>
|
</button>
|
||||||
<h1 className="order-detail-topbar-title">杜康好客</h1>
|
<h1 className="order-detail-topbar-title">杜康好客</h1>
|
||||||
<div className="order-detail-topbar-actions">
|
<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>
|
<span className="material-symbols-outlined">share</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="order-detail-topbar-btn" aria-label="更多" onClick={() => {}}>
|
<button type="button" className="order-detail-topbar-btn" aria-label="更多" onClick={() => {}}>
|
||||||
@@ -215,6 +218,8 @@ export default function OrderDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<AppToast message={shareToast} />
|
||||||
|
|
||||||
<main className="order-detail-main order-detail-main--stitch">
|
<main className="order-detail-main order-detail-main--stitch">
|
||||||
<section className="order-detail-status-card">
|
<section className="order-detail-status-card">
|
||||||
<div className="order-detail-status-deco" aria-hidden>
|
<div className="order-detail-status-deco" aria-hidden>
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { Link, useNavigate, useParams } from 'react-router-dom';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import AppImage from '@dukang/shared-ui/AppImage';
|
import AppImage from '@dukang/shared-ui/AppImage';
|
||||||
import ProductCarousel from '../components/ProductCarousel';
|
import ProductCarousel from '../components/ProductCarousel';
|
||||||
|
import AppToast from '../components/AppToast';
|
||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
import { track } from '../lib/analytics';
|
import { track } from '../lib/analytics';
|
||||||
|
import { handleShareButtonClick } from '../lib/wechat-share';
|
||||||
import { getProductCarouselImages, getProductDetailImages } from '../lib/product-images';
|
import { getProductCarouselImages, getProductDetailImages } from '../lib/product-images';
|
||||||
import type { ProductImageSource } from '../lib/product-images';
|
import type { ProductImageSource } from '../lib/product-images';
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ export default function ProductDetailPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [product, setProduct] = useState<Product | null>(null);
|
const [product, setProduct] = useState<Product | null>(null);
|
||||||
const [headerSolid, setHeaderSolid] = useState(false);
|
const [headerSolid, setHeaderSolid] = useState(false);
|
||||||
|
const [toast, setToast] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) request<Product>('USER_H5', `/catalog/products/${id}`).then(setProduct);
|
if (id) request<Product>('USER_H5', `/catalog/products/${id}`).then(setProduct);
|
||||||
@@ -66,12 +69,14 @@ export default function ProductDetailPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="product-detail-header-btn"
|
className="product-detail-header-btn"
|
||||||
aria-label="分享"
|
aria-label="分享"
|
||||||
onClick={() => {}}
|
onClick={() => handleShareButtonClick(setToast)}
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined">share</span>
|
<span className="material-symbols-outlined">share</span>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<AppToast message={toast} />
|
||||||
|
|
||||||
<main className="product-detail-main">
|
<main className="product-detail-main">
|
||||||
<section className="product-detail-hero">
|
<section className="product-detail-hero">
|
||||||
<ProductCarousel images={carouselImages} alt={product.name} variant="detail" />
|
<ProductCarousel images={carouselImages} alt={product.name} variant="detail" />
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import AppImage from '@dukang/shared-ui/AppImage';
|
import AppImage from '@dukang/shared-ui/AppImage';
|
||||||
import ProductCarousel from '../components/ProductCarousel';
|
import ProductCarousel from '../components/ProductCarousel';
|
||||||
|
import AppToast from '../components/AppToast';
|
||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
import { track } from '../lib/analytics';
|
import { track } from '../lib/analytics';
|
||||||
|
import { handleShareButtonClick } from '../lib/wechat-share';
|
||||||
import { STITCH_STORE_MAP, getStoreGalleryImages } from '../lib/store-images';
|
import { STITCH_STORE_MAP, getStoreGalleryImages } from '../lib/store-images';
|
||||||
|
|
||||||
type StoreMedia = { url: string; mediaType?: string; sortOrder?: number };
|
type StoreMedia = { url: string; mediaType?: string; sortOrder?: number };
|
||||||
@@ -54,6 +56,7 @@ export default function StoreDetailPage() {
|
|||||||
const [store, setStore] = useState<StoreDetail | null>(null);
|
const [store, setStore] = useState<StoreDetail | null>(null);
|
||||||
const [benefitBalance, setBenefitBalance] = useState(0);
|
const [benefitBalance, setBenefitBalance] = useState(0);
|
||||||
const [headerSolid, setHeaderSolid] = useState(false);
|
const [headerSolid, setHeaderSolid] = useState(false);
|
||||||
|
const [toast, setToast] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -114,11 +117,13 @@ export default function StoreDetailPage() {
|
|||||||
<span className="material-symbols-outlined">arrow_back</span>
|
<span className="material-symbols-outlined">arrow_back</span>
|
||||||
</button>
|
</button>
|
||||||
<h1 className={`app-page-title store-detail-header-title${headerSolid ? ' visible' : ''}`}>门店详情</h1>
|
<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>
|
<span className="material-symbols-outlined">share</span>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<AppToast message={toast} />
|
||||||
|
|
||||||
<main className="store-detail-main">
|
<main className="store-detail-main">
|
||||||
<section className="store-detail-hero">
|
<section className="store-detail-hero">
|
||||||
<ProductCarousel images={galleryImages} alt={store.name} variant="store" />
|
<ProductCarousel images={galleryImages} alt={store.name} variant="store" />
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ export { scanQrCode } from './scan';
|
|||||||
export { invokeWechatPay } from './pay';
|
export { invokeWechatPay } from './pay';
|
||||||
export { chooseWechatImages, canUseWechatChooseImage } from './chooseImage';
|
export { chooseWechatImages, canUseWechatChooseImage } from './chooseImage';
|
||||||
export type { ChooseWechatImageOptions } from './chooseImage';
|
export type { ChooseWechatImageOptions } from './chooseImage';
|
||||||
|
export {
|
||||||
|
setWechatShareData,
|
||||||
|
canUseWechatShare,
|
||||||
|
getWechatShareLink,
|
||||||
|
} from './share';
|
||||||
|
export type { WechatShareData } from './share';
|
||||||
export {
|
export {
|
||||||
getWechatOAuthUrl,
|
getWechatOAuthUrl,
|
||||||
startWechatOAuthLogin,
|
startWechatOAuthLogin,
|
||||||
@@ -41,6 +47,7 @@ import { getWechatLocation, getWechatLocationDetailed } from './location';
|
|||||||
import { scanQrCode } from './scan';
|
import { scanQrCode } from './scan';
|
||||||
import { invokeWechatPay } from './pay';
|
import { invokeWechatPay } from './pay';
|
||||||
import { chooseWechatImages } from './chooseImage';
|
import { chooseWechatImages } from './chooseImage';
|
||||||
|
import { setWechatShareData } from './share';
|
||||||
import {
|
import {
|
||||||
wechatLogin,
|
wechatLogin,
|
||||||
handleWechatOAuthCallback,
|
handleWechatOAuthCallback,
|
||||||
@@ -68,5 +75,7 @@ export function createWeixinSdk(config: WeixinSdkConfig) {
|
|||||||
clientApp: config.clientApp,
|
clientApp: config.clientApp,
|
||||||
getAccessToken: config.getAccessToken,
|
getAccessToken: config.getAccessToken,
|
||||||
}),
|
}),
|
||||||
|
setShare: (data: Parameters<typeof setWechatShareData>[1]) =>
|
||||||
|
setWechatShareData(config, data),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -53,6 +53,21 @@ export type WxApi = {
|
|||||||
success?: (res: { localData: string }) => void;
|
success?: (res: { localData: string }) => void;
|
||||||
fail?: (res: { errMsg: string }) => void;
|
fail?: (res: { errMsg: string }) => void;
|
||||||
}) => 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 = {
|
export type MiniProgramWx = {
|
||||||
|
|||||||
Reference in New Issue
Block a user