feat(mini-user): configurable WeChat mini share scenes in system settings

Add HQ group for default and per-page share title/desc/image; expose via client-config and wire all mini-user share entry points.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-06 15:45:57 +08:00
parent a2abbb6169
commit 83b1b0e5f6
12 changed files with 526 additions and 106 deletions
+128 -11
View File
@@ -1,21 +1,126 @@
import Taro from '@tarojs/taro';
import type { WechatShareData } from '@dukang/weixin-sdk';
import { getWechatShareLink, isWechatBrowser, isMiniProgram } from '@dukang/weixin-sdk';
import {
applyShareTitleTemplate,
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_HINT,
DEFAULT_SHARE_TITLE,
resolveMiniShareRuntime,
type ClientRuntimeConfig,
type MiniShareRuntime,
type MiniShareSceneConfig,
} from '@dukang/shared-types';
import { toast } from './api';
import { getBrandAssetsSync, loadBrandAssets } from './brand-assets';
import { fetchClientConfig } from './pay-wechat';
import { isWechatEnv, weixinSdk } from './weixin';
export const DEFAULT_SHARE_TITLE = '你吃饭,我买单';
export const DEFAULT_SHARE_DESC = '杜康好客 · 买美酒,享好礼,全城门店可用';
export const WECHAT_SHARE_HINT = '请点击右上角 ··· 分享给好友';
export type ShareScene =
| 'home'
| 'stores'
| 'storeDetail'
| 'benefit'
| 'mine'
| 'productDetail'
| 'orderDetail';
export function getDefaultShareImageUrl(): string {
return getBrandAssetsSync().brandLogoUrl;
export { DEFAULT_SHARE_TITLE, DEFAULT_SHARE_DESC };
export const WECHAT_SHARE_HINT = DEFAULT_SHARE_HINT;
const FALLBACK_SHARE: MiniShareRuntime = resolveMiniShareRuntime({});
let shareCached: MiniShareRuntime | null = null;
export function getShareRuntimeSync(): MiniShareRuntime {
return shareCached ?? FALLBACK_SHARE;
}
/** 预热分享默认图(来自系统设置) */
export function applyShareFromClientConfig(config?: ClientRuntimeConfig | null) {
if (config?.share) {
shareCached = config.share;
return shareCached;
}
return getShareRuntimeSync();
}
export async function loadShareConfig(force = false): Promise<MiniShareRuntime> {
if (!force && shareCached) return shareCached;
try {
const cfg = await fetchClientConfig();
if (cfg?.share) {
shareCached = cfg.share;
return shareCached;
}
} catch {
/* ignore */
}
shareCached = FALLBACK_SHARE;
return shareCached;
}
export function getDefaultShareImageUrl(): string {
const share = getShareRuntimeSync();
return share.default.imageUrl || getBrandAssetsSync().brandLogoUrl;
}
export function getShareHint(): string {
return getShareRuntimeSync().hint || DEFAULT_SHARE_HINT;
}
/** 预热分享配置与品牌图 */
export function prefetchShareBrandAssets() {
void loadBrandAssets();
void loadShareConfig();
}
function sceneConfig(scene?: ShareScene): MiniShareSceneConfig {
const runtime = getShareRuntimeSync();
if (!scene) return runtime.default;
return runtime[scene] ?? runtime.default;
}
/**
* 组装页面分享:场景配置优先;空字段用 dynamic → 默认分享。
* orderDetail 标题支持 {productName}。
*/
export function buildSceneSharePayload(
scene: ShareScene,
options?: {
path?: string;
/** 业务动态标题(场景配置为空时使用) */
dynamicTitle?: string | null;
dynamicDesc?: string | null;
dynamicImageUrl?: string | null;
titleVars?: Record<string, string | undefined | null>;
},
): PageSharePayload {
const def = getShareRuntimeSync().default;
const sc = sceneConfig(scene);
let title = (sc.title || '').trim();
if (title && options?.titleVars) {
const vars = options.titleVars;
const missingRequired = Object.entries(vars).some(
([key, value]) => title.includes(`{${key}}`) && !(value != null && String(value).trim()),
);
title = missingRequired ? '' : applyShareTitleTemplate(title, vars);
}
if (!title) {
title = (options?.dynamicTitle || '').trim() || def.title;
}
const desc = (sc.desc || '').trim() || (options?.dynamicDesc || '').trim() || def.desc;
const imgUrl =
(sc.imageUrl || '').trim() ||
(options?.dynamicImageUrl || '').trim() ||
def.imageUrl ||
getDefaultShareImageUrl();
return {
title,
desc,
path: options?.path,
imgUrl,
};
}
export function buildDefaultShareData(
@@ -29,9 +134,10 @@ export function buildDefaultShareData(
link = '';
}
}
const def = getShareRuntimeSync().default;
return {
title: overrides?.title ?? DEFAULT_SHARE_TITLE,
desc: overrides?.desc ?? DEFAULT_SHARE_DESC,
title: overrides?.title ?? def.title,
desc: overrides?.desc ?? def.desc,
link,
imgUrl: overrides?.imgUrl ?? getDefaultShareImageUrl(),
};
@@ -73,7 +179,7 @@ export async function handleShareButtonClick(
/* ignore */
}
}
toast(WECHAT_SHARE_HINT);
toast(getShareHint());
return { showGuide: false };
}
@@ -94,7 +200,7 @@ export async function handleShareButtonClick(
if (result.invoked) {
return { showGuide: false };
}
toast(WECHAT_SHARE_HINT);
toast(getShareHint());
return { showGuide: true };
} catch (e) {
toast(e instanceof Error ? e.message : '分享配置失败,请刷新后重试');
@@ -104,9 +210,20 @@ export async function handleShareButtonClick(
/** 供 useShareAppMessage 使用的标题/路径/图 */
export function toWeappShareMessage(payload?: PageSharePayload) {
const def = getShareRuntimeSync().default;
return {
title: payload?.title || DEFAULT_SHARE_TITLE,
title: payload?.title || def.title,
path: payload?.path || '/pages/home/index',
imageUrl: payload?.imgUrl || getDefaultShareImageUrl(),
};
}
/** 供 useShareTimeline */
export function toWeappShareTimeline(payload?: PageSharePayload, query = '') {
const def = getShareRuntimeSync().default;
return {
title: payload?.title || def.title,
query,
imageUrl: payload?.imgUrl || getDefaultShareImageUrl(),
};
}
+7 -12
View File
@@ -8,9 +8,9 @@ import { goLogin } from '../../lib/auth-nav';
import { isLoggedIn, request, toast } from '../../lib/api';
import { navBarStyle, tabNavContentStyle, useNavBarMetrics } from '../../lib/nav-bar';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
buildSceneSharePayload,
toWeappShareMessage,
toWeappShareTimeline,
} from '../../lib/wechat-share';
import { formatMoney } from '../../lib/money';
import iconBenefit from '../../assets/tabbar/benefit-active.png';
@@ -104,20 +104,15 @@ export default function BenefitPage() {
const available = coupons.filter((c) => c.status === 'ACTIVE' && c.balance > 0);
const sharePayload = useMemo(
() => ({
title: '好客权益 · 杜康好客',
desc: DEFAULT_SHARE_DESC,
path: '/pages/benefit/index',
}),
() =>
buildSceneSharePayload('benefit', {
path: '/pages/benefit/index',
}),
[],
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: '',
imageUrl: sharePayload.imgUrl,
}));
useShareTimeline(() => toWeappShareTimeline(sharePayload));
return (
<PageShell variant="tab" className="benefit-page">
+12 -14
View File
@@ -28,10 +28,12 @@ import {
normalizeFulfillmentFlags,
} from '../../lib/product-fulfillment';
import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location';
import type { ClientRuntimeConfig } from '@dukang/shared-types';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
applyShareFromClientConfig,
buildSceneSharePayload,
toWeappShareMessage,
toWeappShareTimeline,
} from '../../lib/wechat-share';
import { trackPageView } from '../../lib/analytics';
type Product = {
@@ -85,8 +87,9 @@ export default function HomePage() {
}, [cityCode]);
const loadMiniHome = useCallback(() => {
return request<{ miniHome?: MiniHomeConfig }>('/common/client-config')
return request<ClientRuntimeConfig>('/common/client-config')
.then((cfg) => {
applyShareFromClientConfig(cfg);
const banners = Array.isArray(cfg.miniHome?.banners)
? cfg.miniHome!.banners.filter((u) => typeof u === 'string' && !!u.trim())
: [];
@@ -221,21 +224,16 @@ export default function HomePage() {
const footerUrl = miniHome.footerUrl;
const sharePayload = useMemo(
() => ({
title: DEFAULT_SHARE_TITLE,
desc: DEFAULT_SHARE_DESC,
path: '/pages/home/index',
imgUrl: banners[0] || undefined,
}),
() =>
buildSceneSharePayload('home', {
path: '/pages/home/index',
dynamicImageUrl: banners[0] || undefined,
}),
[banners],
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: '',
imageUrl: sharePayload.imgUrl,
}));
useShareTimeline(() => toWeappShareTimeline(sharePayload));
function scrollToAroma(key: AromaKey) {
setActiveAroma(key);
+12 -13
View File
@@ -29,9 +29,10 @@ import { isWechatEnv } from '../../lib/weixin';
import { APP_VERSION_LABEL } from '../../lib/client-version';
import { maskPhone } from '../../lib/phone';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
applyShareFromClientConfig,
buildSceneSharePayload,
toWeappShareMessage,
toWeappShareTimeline,
} from '../../lib/wechat-share';
import iconPendingPay from '../../assets/icons/待付款.png';
import iconPaid from '../../assets/icons/已付款.png';
@@ -77,7 +78,7 @@ export default function MinePage() {
const [qualificationUrl, setQualificationUrl] = useState(
() => getBrandAssetsSync().qualificationDisclosureUrl,
);
const [shareTick, setShareTick] = useState(0);
function resetGuestState() {
setProfile(null);
setBenefitBalance(0);
@@ -151,8 +152,10 @@ export default function MinePage() {
.then((config) => {
setWxAuthorize(isWxAuthorizeEnabled(config));
const brand = applyBrandFromClientConfig(config);
applyShareFromClientConfig(config);
setBrandMarkUrl(brand.brandLogoMarkUrl);
setQualificationUrl(brand.qualificationDisclosureUrl);
setShareTick((n) => n + 1);
})
.catch(() => {
setWxAuthorize(true);
@@ -161,19 +164,15 @@ export default function MinePage() {
}, []);
const sharePayload = useMemo(
() => ({
title: '杜康好客 · 我的',
desc: DEFAULT_SHARE_DESC,
path: '/pages/mine/index',
}),
[],
() =>
buildSceneSharePayload('mine', {
path: '/pages/mine/index',
}),
[shareTick],
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: '',
}));
useShareTimeline(() => toWeappShareTimeline(sharePayload));
async function ensureWechatBound(): Promise<boolean> {
if (profile?.hasWechat) return true;
+12 -11
View File
@@ -22,9 +22,9 @@ import { fetchOrderTrack } from '../../lib/order-logistics';
import { maskPhone } from '../../lib/phone';
import { confirmOrderReceive, type WechatConfirmPayload } from '../../lib/wechat-order-confirm';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
buildSceneSharePayload,
toWeappShareMessage,
toWeappShareTimeline,
} from '../../lib/wechat-share';
import { usePageView } from '../../lib/usePageView';
@@ -174,19 +174,20 @@ export default function OrderDetailPage() {
: '';
const sharePayload = useMemo(
() => ({
title: productName !== '杜康商品' ? `我买了${productName} · 杜康好客` : DEFAULT_SHARE_TITLE,
desc: DEFAULT_SHARE_DESC,
path: orderId ? `/pages/order-detail/index?id=${orderId}` : '/pages/home/index',
}),
() =>
buildSceneSharePayload('orderDetail', {
path: orderId ? `/pages/order-detail/index?id=${orderId}` : '/pages/home/index',
titleVars: {
productName: productName && productName !== '杜康商品' ? productName : '',
},
}),
[productName, orderId],
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: orderId ? `id=${orderId}` : '',
}));
useShareTimeline(() =>
toWeappShareTimeline(sharePayload, orderId ? `id=${orderId}` : ''),
);
function goPay() {
if (!order) return;
@@ -27,9 +27,9 @@ import {
normalizeFulfillmentFlags,
} from '../../lib/product-fulfillment';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
buildSceneSharePayload,
toWeappShareMessage,
toWeappShareTimeline,
} from '../../lib/wechat-share';
import iconHome from '../../assets/tabbar/home.png';
import { usePageView } from '../../lib/usePageView';
@@ -88,21 +88,20 @@ export default function ProductDetailPage() {
});
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,
}),
() =>
buildSceneSharePayload('productDetail', {
path: `/pages/product-detail/index?id=${productId}`,
dynamicTitle: product?.name,
dynamicDesc: product?.subtitle,
dynamicImageUrl: product ? getProductMainImage(product) : undefined,
}),
[product, productId],
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: productId ? `id=${productId}` : '',
imageUrl: sharePayload.imgUrl,
}));
useShareTimeline(() =>
toWeappShareTimeline(sharePayload, productId ? `id=${productId}` : ''),
);
function goBack() {
const pages = Taro.getCurrentPages();
+14 -19
View File
@@ -19,9 +19,9 @@ import { maskPhone } from '../../lib/phone';
import { track } from '../../lib/analytics';
import { formatShanghaiDateTime } from '../../lib/datetime';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
buildSceneSharePayload,
toWeappShareMessage,
toWeappShareTimeline,
} from '../../lib/wechat-share';
type StoreMedia = {
@@ -237,18 +237,15 @@ export default function StoreDetailPage() {
void loadRecentRedeems(id);
});
const sharePayload = useMemo(
() => {
const envFirst = store ? envPhotoUrls(store)[0] : undefined;
return {
title: store?.name || DEFAULT_SHARE_TITLE,
desc: store?.intro?.trim() || store?.address || DEFAULT_SHARE_DESC,
path: `/pages/store-detail/index?id=${storeId}`,
imgUrl: store?.coverUrl || envFirst || store?.carouselUrls?.[0] || undefined,
};
},
[store, storeId],
);
const sharePayload = useMemo(() => {
const envFirst = store ? envPhotoUrls(store)[0] : undefined;
return buildSceneSharePayload('storeDetail', {
path: `/pages/store-detail/index?id=${storeId}`,
dynamicTitle: store?.name,
dynamicDesc: store?.intro?.trim() || store?.address,
dynamicImageUrl: store?.coverUrl || envFirst || store?.carouselUrls?.[0],
});
}, [store, storeId]);
const marqueeLines = useMemo(
() => recentRedeems.map(formatRecentRedeemLine).filter(Boolean),
@@ -256,11 +253,9 @@ export default function StoreDetailPage() {
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: storeId ? `id=${storeId}` : '',
imageUrl: sharePayload.imgUrl,
}));
useShareTimeline(() =>
toWeappShareTimeline(sharePayload, storeId ? `id=${storeId}` : ''),
);
function goBack() {
const pages = Taro.getCurrentPages();
+7 -11
View File
@@ -36,9 +36,9 @@ import {
setStoresListCache,
} from '../../lib/stores-session';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
buildSceneSharePayload,
toWeappShareMessage,
toWeappShareTimeline,
} from '../../lib/wechat-share';
type Store = {
@@ -350,19 +350,15 @@ export default function StoresPage() {
}
const sharePayload = useMemo(
() => ({
title: '杜康好客门店',
desc: DEFAULT_SHARE_DESC,
path: '/pages/stores/index',
}),
() =>
buildSceneSharePayload('stores', {
path: '/pages/stores/index',
}),
[],
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: '',
}));
useShareTimeline(() => toWeappShareTimeline(sharePayload));
return (
<PageShell variant="tab" className="store-page no-tab-header">