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:
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -104,6 +104,97 @@ export function resolveMockSmsFixedCode(env?: Record<string, string | undefined>
|
||||
return code || MOCK_SMS_FIXED_CODE;
|
||||
}
|
||||
|
||||
/** 小程序 / H5 默认分享文案与引导(系统设置「小程序分享配置」可覆盖) */
|
||||
export const DEFAULT_SHARE_TITLE = '你吃饭,我买单';
|
||||
export const DEFAULT_SHARE_DESC = '杜康好客 · 买美酒,享好礼,全城门店可用';
|
||||
export const DEFAULT_SHARE_HINT = '请点击右上角 ··· 分享给好友';
|
||||
export const DEFAULT_SHARE_STORES_TITLE = '杜康好客门店';
|
||||
export const DEFAULT_SHARE_BENEFIT_TITLE = '好客权益 · 杜康好客';
|
||||
export const DEFAULT_SHARE_MINE_TITLE = '杜康好客 · 我的';
|
||||
/** 订单详情分享标题模板,可用 {productName} */
|
||||
export const DEFAULT_SHARE_ORDER_TITLE = '我买了{productName} · 杜康好客';
|
||||
|
||||
export type MiniShareSceneConfig = {
|
||||
title: string;
|
||||
desc: string;
|
||||
imageUrl: string;
|
||||
};
|
||||
|
||||
/** 各场景分享文案/图(空字符串表示该字段走动态内容或默认分享) */
|
||||
export type MiniShareRuntime = {
|
||||
hint: string;
|
||||
default: MiniShareSceneConfig;
|
||||
home: MiniShareSceneConfig;
|
||||
stores: MiniShareSceneConfig;
|
||||
storeDetail: MiniShareSceneConfig;
|
||||
benefit: MiniShareSceneConfig;
|
||||
mine: MiniShareSceneConfig;
|
||||
productDetail: MiniShareSceneConfig;
|
||||
orderDetail: MiniShareSceneConfig;
|
||||
};
|
||||
|
||||
function pickShareScene(
|
||||
e: Record<string, string | undefined>,
|
||||
prefix: string,
|
||||
fallback: Partial<MiniShareSceneConfig> = {},
|
||||
): MiniShareSceneConfig {
|
||||
return {
|
||||
title: (e[`${prefix}_TITLE`] || '').trim() || fallback.title || '',
|
||||
desc: (e[`${prefix}_DESC`] || '').trim() || fallback.desc || '',
|
||||
imageUrl: (e[`${prefix}_IMAGE_URL`] || '').trim() || fallback.imageUrl || '',
|
||||
};
|
||||
}
|
||||
|
||||
/** 从 env / 系统设置解析小程序分享配置 */
|
||||
export function resolveMiniShareRuntime(
|
||||
env?: Record<string, string | undefined>,
|
||||
): MiniShareRuntime {
|
||||
const e = readEnv(env);
|
||||
const brand = resolveClientBrandRuntime(e);
|
||||
const defaults: MiniShareSceneConfig = {
|
||||
title: (e.SHARE_DEFAULT_TITLE || '').trim() || DEFAULT_SHARE_TITLE,
|
||||
desc: (e.SHARE_DEFAULT_DESC || '').trim() || DEFAULT_SHARE_DESC,
|
||||
imageUrl: (e.SHARE_DEFAULT_IMAGE_URL || '').trim() || brand.brandLogoUrl,
|
||||
};
|
||||
return {
|
||||
hint: (e.SHARE_HINT || '').trim() || DEFAULT_SHARE_HINT,
|
||||
default: defaults,
|
||||
home: pickShareScene(e, 'SHARE_HOME', {
|
||||
title: defaults.title,
|
||||
desc: defaults.desc,
|
||||
}),
|
||||
stores: pickShareScene(e, 'SHARE_STORES', {
|
||||
title: DEFAULT_SHARE_STORES_TITLE,
|
||||
desc: defaults.desc,
|
||||
}),
|
||||
storeDetail: pickShareScene(e, 'SHARE_STORE_DETAIL'),
|
||||
benefit: pickShareScene(e, 'SHARE_BENEFIT', {
|
||||
title: DEFAULT_SHARE_BENEFIT_TITLE,
|
||||
desc: defaults.desc,
|
||||
}),
|
||||
mine: pickShareScene(e, 'SHARE_MINE', {
|
||||
title: DEFAULT_SHARE_MINE_TITLE,
|
||||
desc: defaults.desc,
|
||||
}),
|
||||
productDetail: pickShareScene(e, 'SHARE_PRODUCT_DETAIL'),
|
||||
orderDetail: pickShareScene(e, 'SHARE_ORDER_DETAIL', {
|
||||
title: DEFAULT_SHARE_ORDER_TITLE,
|
||||
desc: defaults.desc,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** 订单等标题模板:替换 {productName} 等占位符 */
|
||||
export function applyShareTitleTemplate(
|
||||
template: string,
|
||||
vars: Record<string, string | undefined | null>,
|
||||
): string {
|
||||
return template.replace(/\{(\w+)\}/g, (_, key: string) => {
|
||||
const v = vars[key];
|
||||
return v != null && String(v).trim() ? String(v).trim() : '';
|
||||
}).replace(/\s{2,}/g, ' ').trim();
|
||||
}
|
||||
|
||||
function readEnv(env?: Record<string, string | undefined>) {
|
||||
return (
|
||||
env ??
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { MiniShareRuntime } from './config';
|
||||
|
||||
/** 微信 JSSDK 初始化参数(后端签名下发) */
|
||||
export interface WechatJssdkConfig {
|
||||
appId: string;
|
||||
@@ -62,6 +64,8 @@ export type ClientRuntimeConfig = {
|
||||
qualificationDisclosureUrl?: string;
|
||||
/** 总部客服电话 */
|
||||
customerServicePhone?: string;
|
||||
/** 小程序各场景分享文案/图 */
|
||||
share?: MiniShareRuntime;
|
||||
};
|
||||
|
||||
/** 是否展示微信授权入口 */
|
||||
|
||||
@@ -5,6 +5,13 @@ import {
|
||||
BRAND_LOGO_URL,
|
||||
BRAND_LOGO_WIDE_URL,
|
||||
CUSTOMER_SERVICE_PHONE,
|
||||
DEFAULT_SHARE_BENEFIT_TITLE,
|
||||
DEFAULT_SHARE_DESC,
|
||||
DEFAULT_SHARE_HINT,
|
||||
DEFAULT_SHARE_MINE_TITLE,
|
||||
DEFAULT_SHARE_ORDER_TITLE,
|
||||
DEFAULT_SHARE_STORES_TITLE,
|
||||
DEFAULT_SHARE_TITLE,
|
||||
DEFAULT_USER_H5_URL,
|
||||
MINI_USER_STATIC_OSS_BASE,
|
||||
MOCK_SMS_FIXED_CODE,
|
||||
@@ -17,6 +24,7 @@ export const SYSTEM_CONFIG_GROUPS: SystemConfigGroupMeta[] = [
|
||||
{ key: 'sms', label: '短信' },
|
||||
{ key: 'wechat', label: '微信' },
|
||||
{ key: 'wechat_mini', label: '微信小程序配置' },
|
||||
{ key: 'wechat_mini_share', label: '小程序分享配置' },
|
||||
{ key: 'oss', label: '对象存储 OSS' },
|
||||
{ key: 'app', label: '应用链接' },
|
||||
{ key: 'deploy', label: '发布部署' },
|
||||
@@ -29,6 +37,7 @@ const G = {
|
||||
sms: 'sms',
|
||||
wechat: 'wechat',
|
||||
wechat_mini: 'wechat_mini',
|
||||
wechat_mini_share: 'wechat_mini_share',
|
||||
oss: 'oss',
|
||||
app: 'app',
|
||||
deploy: 'deploy',
|
||||
@@ -160,7 +169,7 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
|
||||
group: G.wechat_mini,
|
||||
type: 'image',
|
||||
requiresRestart: false,
|
||||
description: '分享卡片默认图、首页等品牌展示',
|
||||
description: '品牌方形 Logo;分享默认图未单独配置时回退此图',
|
||||
},
|
||||
{
|
||||
key: 'BRAND_LOGO_WIDE_URL',
|
||||
@@ -214,6 +223,202 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
|
||||
description: '仅 MOCK_SMS 开启时生效;留空则用默认 999888',
|
||||
},
|
||||
|
||||
// —— 小程序分享配置 ——
|
||||
{
|
||||
key: 'SHARE_HINT',
|
||||
label: '分享引导文案',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
placeholder: DEFAULT_SHARE_HINT,
|
||||
description: '点击分享按钮后的 Toast 提示(小程序内)',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_DEFAULT_TITLE',
|
||||
label: '默认分享标题',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
placeholder: DEFAULT_SHARE_TITLE,
|
||||
description: '各场景未单独配置标题时使用',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_DEFAULT_DESC',
|
||||
label: '默认分享描述',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
placeholder: DEFAULT_SHARE_DESC,
|
||||
description: 'H5 分享卡片描述;小程序朋友圈/好友卡片副文案回退',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_DEFAULT_IMAGE_URL',
|
||||
label: '默认分享图',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'image',
|
||||
requiresRestart: false,
|
||||
description: '各场景未配置图且无业务图时使用;建议接近 5:4',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_HOME_TITLE',
|
||||
label: '首页 · 标题',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_HOME_DESC',
|
||||
label: '首页 · 描述',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_HOME_IMAGE_URL',
|
||||
label: '首页 · 分享图',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'image',
|
||||
requiresRestart: false,
|
||||
description: '留空则优先用首页首张轮播图,再回退默认分享图',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_STORES_TITLE',
|
||||
label: '门店列表 · 标题',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
placeholder: DEFAULT_SHARE_STORES_TITLE,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_STORES_DESC',
|
||||
label: '门店列表 · 描述',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_STORES_IMAGE_URL',
|
||||
label: '门店列表 · 分享图',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'image',
|
||||
requiresRestart: false,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_STORE_DETAIL_TITLE',
|
||||
label: '门店详情 · 标题',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
description: '留空则用门店名称',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_STORE_DETAIL_DESC',
|
||||
label: '门店详情 · 描述',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
description: '留空则用门店简介/地址',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_STORE_DETAIL_IMAGE_URL',
|
||||
label: '门店详情 · 分享图',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'image',
|
||||
requiresRestart: false,
|
||||
description: '留空则用门头图/环境图',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_BENEFIT_TITLE',
|
||||
label: '权益页 · 标题',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
placeholder: DEFAULT_SHARE_BENEFIT_TITLE,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_BENEFIT_DESC',
|
||||
label: '权益页 · 描述',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_BENEFIT_IMAGE_URL',
|
||||
label: '权益页 · 分享图',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'image',
|
||||
requiresRestart: false,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_MINE_TITLE',
|
||||
label: '我的 · 标题',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
placeholder: DEFAULT_SHARE_MINE_TITLE,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_MINE_DESC',
|
||||
label: '我的 · 描述',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_MINE_IMAGE_URL',
|
||||
label: '我的 · 分享图',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'image',
|
||||
requiresRestart: false,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_PRODUCT_DETAIL_TITLE',
|
||||
label: '商品详情 · 标题',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
description: '留空则用商品名称',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_PRODUCT_DETAIL_DESC',
|
||||
label: '商品详情 · 描述',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
description: '留空则用商品副标题',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_PRODUCT_DETAIL_IMAGE_URL',
|
||||
label: '商品详情 · 分享图',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'image',
|
||||
requiresRestart: false,
|
||||
description: '留空则用商品主图',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_ORDER_DETAIL_TITLE',
|
||||
label: '订单详情 · 标题',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
placeholder: DEFAULT_SHARE_ORDER_TITLE,
|
||||
description: '支持 {productName} 占位符',
|
||||
},
|
||||
{
|
||||
key: 'SHARE_ORDER_DETAIL_DESC',
|
||||
label: '订单详情 · 描述',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
},
|
||||
{
|
||||
key: 'SHARE_ORDER_DETAIL_IMAGE_URL',
|
||||
label: '订单详情 · 分享图',
|
||||
group: G.wechat_mini_share,
|
||||
type: 'image',
|
||||
requiresRestart: false,
|
||||
},
|
||||
|
||||
{ key: 'OSS_ACCESS_KEY_ID', label: 'OSS AccessKey ID', group: G.oss, type: 'password', secret: true, requiresRestart: true },
|
||||
{ key: 'OSS_ACCESS_KEY_SECRET', label: 'OSS AccessKey Secret', group: G.oss, type: 'password', secret: true, requiresRestart: true },
|
||||
{ key: 'OSS_BUCKET', label: 'OSS Bucket', group: G.oss, type: 'string', requiresRestart: true },
|
||||
@@ -345,6 +550,20 @@ export const SYSTEM_CONFIG_DEFAULTS: Record<string, string> = {
|
||||
QUALIFICATION_DISCLOSURE_URL: QUALIFICATION_DISCLOSURE_URL,
|
||||
CUSTOMER_SERVICE_PHONE: CUSTOMER_SERVICE_PHONE,
|
||||
MOCK_SMS_FIXED_CODE: MOCK_SMS_FIXED_CODE,
|
||||
SHARE_HINT: DEFAULT_SHARE_HINT,
|
||||
SHARE_DEFAULT_TITLE: DEFAULT_SHARE_TITLE,
|
||||
SHARE_DEFAULT_DESC: DEFAULT_SHARE_DESC,
|
||||
SHARE_DEFAULT_IMAGE_URL: BRAND_LOGO_URL,
|
||||
SHARE_HOME_TITLE: DEFAULT_SHARE_TITLE,
|
||||
SHARE_HOME_DESC: DEFAULT_SHARE_DESC,
|
||||
SHARE_STORES_TITLE: DEFAULT_SHARE_STORES_TITLE,
|
||||
SHARE_STORES_DESC: DEFAULT_SHARE_DESC,
|
||||
SHARE_BENEFIT_TITLE: DEFAULT_SHARE_BENEFIT_TITLE,
|
||||
SHARE_BENEFIT_DESC: DEFAULT_SHARE_DESC,
|
||||
SHARE_MINE_TITLE: DEFAULT_SHARE_MINE_TITLE,
|
||||
SHARE_MINE_DESC: DEFAULT_SHARE_DESC,
|
||||
SHARE_ORDER_DETAIL_TITLE: DEFAULT_SHARE_ORDER_TITLE,
|
||||
SHARE_ORDER_DETAIL_DESC: DEFAULT_SHARE_DESC,
|
||||
};
|
||||
|
||||
export function getSystemConfigField(key: string): SystemConfigFieldMeta | undefined {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { parseMiniHomeBanners, resolveClientBrandRuntime } from '@dukang/shared-types';
|
||||
import {
|
||||
parseMiniHomeBanners,
|
||||
resolveClientBrandRuntime,
|
||||
resolveMiniShareRuntime,
|
||||
} from '@dukang/shared-types';
|
||||
import { SystemConfigService } from '../../common/system-config/system-config.service';
|
||||
|
||||
@Controller('common')
|
||||
@@ -13,6 +17,7 @@ export class ClientConfigController {
|
||||
const footer = (env.MINI_HOME_FOOTER_URL ?? '').trim();
|
||||
const minClientVersion = (env.MINI_USER_MIN_VERSION ?? '').trim() || null;
|
||||
const brand = resolveClientBrandRuntime(env);
|
||||
const share = resolveMiniShareRuntime(env);
|
||||
return {
|
||||
mockPay: cfg.mockPay,
|
||||
wechatPayEnabled: cfg.wechatPayEnabled,
|
||||
@@ -32,6 +37,7 @@ export class ClientConfigController {
|
||||
brandLogoMarkUrl: brand.brandLogoMarkUrl,
|
||||
qualificationDisclosureUrl: brand.qualificationDisclosureUrl,
|
||||
customerServicePhone: brand.customerServicePhone,
|
||||
share,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user