feat(mini-user): v3.5.10 同城配送提示可配置并支持换行

承运商 HTML 按收货市下发;textarea 回车在 C 端转成换行。含门店去掉分享按钮与小程序企微客服。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 14:29:48 +08:00
parent fed8ff3d3a
commit 797b20979d
29 changed files with 758 additions and 28 deletions
+15 -2
View File
@@ -30,11 +30,16 @@ export interface AppConfig {
tencentLbsSecretKey: string;
/** C 端 H5 落地页(推广码二维码链接前缀) */
userH5Url: string;
/** 门店 H5 落地页(用户核销码二维码链接前缀) */
shopH5Url: string;
}
/** 推广码 / C 端 H5 默认落地页(系统设置 USER_H5_URL 未配时回退;HQ 可改) */
export const DEFAULT_USER_H5_URL = 'https://user.runxian.top/user';
/** 门店 H5 默认落地页(系统设置 SHOP_H5_URL 未配时回退;HQ 可改) */
export const DEFAULT_SHOP_H5_URL = 'https://shop.dukanghaoke.com';
/** 品牌 Logo OSS 根路径(默认;系统设置 BRAND_LOGO_OSS_BASE 可覆盖) */
export const BRAND_LOGO_OSS_BASE = 'https://dukang-dev.oss-cn-beijing.aliyuncs.com/logo/';
@@ -58,12 +63,15 @@ export const QUALIFICATION_DISCLOSURE_URL = `${MINI_USER_STATIC_OSS_BASE}qualifi
export const CUSTOMER_SERVICE_PHONE = '13203801799';
/**
* 企业微信「微信客服」链接(C 端「在线客服」;微信内网页点击后进入原生客服会话
* 可在 h5-user 用 VITE_CS_WECOM_URL 覆盖
* 企业微信「微信客服」链接(C 端「在线客服」;微信内网页 / 小程序 openCustomerServiceChat
* 可在系统设置 CUSTOMER_SERVICE_WECOM_URL 覆盖
*/
export const CUSTOMER_SERVICE_WECOM_URL =
'https://work.weixin.qq.com/kfid/kfc8b88659a1dffa8cd';
/** 企业微信 CorpID(小程序 wx.openCustomerServiceChat 必填;系统设置 WECOM_CORP_ID */
export const WECOM_CORP_ID = '';
/** 从 env / 系统设置解析的 C 端品牌与客服展示配置(缺省回退常量) */
export type ClientBrandRuntime = {
userH5Url: string;
@@ -74,6 +82,8 @@ export type ClientBrandRuntime = {
miniUserStaticOssBase: string;
qualificationDisclosureUrl: string;
customerServicePhone: string;
customerServiceWecomUrl: string;
wecomCorpId: string;
};
export function resolveClientBrandRuntime(
@@ -95,6 +105,8 @@ export function resolveClientBrandRuntime(
(e.QUALIFICATION_DISCLOSURE_URL || '').trim() ||
`${staticBase}qualification-disclosure.png`,
customerServicePhone: (e.CUSTOMER_SERVICE_PHONE || CUSTOMER_SERVICE_PHONE).trim(),
customerServiceWecomUrl: (e.CUSTOMER_SERVICE_WECOM_URL || CUSTOMER_SERVICE_WECOM_URL).trim(),
wecomCorpId: (e.WECOM_CORP_ID || WECOM_CORP_ID).trim(),
};
}
@@ -265,6 +277,7 @@ export function loadAppConfig(env?: Record<string, string | undefined>): AppConf
tencentLbsKey: e.TENCENT_LBS_KEY ?? '',
tencentLbsSecretKey: e.TENCENT_LBS_SECRET_KEY ?? '',
userH5Url: (e.USER_H5_URL || DEFAULT_USER_H5_URL).replace(/\/$/, ''),
shopH5Url: (e.SHOP_H5_URL || DEFAULT_SHOP_H5_URL).replace(/\/$/, ''),
};
return {
...base,
@@ -55,6 +55,8 @@ export interface FulfillmentProviderDto {
settlementMethod: LogisticsSettlementMethod;
pricingRules?: LogisticsPricingRuleDto | null;
prepaidBalance: number;
/** C 端同城配送提示(已消毒 HTML) */
deliveryHintHtml?: string | null;
createdAt: string;
updatedAt: string;
}
@@ -74,6 +76,7 @@ export interface CreateFulfillmentProviderInput {
bankAccountNo?: string | null;
settlementMethod?: LogisticsSettlementMethod;
pricingRules?: LogisticsPricingRuleDto | null;
deliveryHintHtml?: string | null;
}
export interface UpdateFulfillmentProviderInput {
@@ -89,6 +92,7 @@ export interface UpdateFulfillmentProviderInput {
bankAccountNo?: string | null;
settlementMethod?: LogisticsSettlementMethod;
pricingRules?: LogisticsPricingRuleDto | null;
deliveryHintHtml?: string | null;
}
export { DEFAULT_XFX_LOGISTICS_PRICING };
@@ -113,3 +117,83 @@ export const XFX_PROVIDER_CODES = ['XFX', 'XIAOFEIXIA'] as const;
export function isXfxProviderCode(code: string): boolean {
return (XFX_PROVIDER_CODES as readonly string[]).includes(code.trim().toUpperCase());
}
/** 承运商未配置提示时 C 端回退文案 */
export const DEFAULT_LOCAL_DELIVERY_HINT = '同城配送,预计24小时内送到';
export const LOCAL_DELIVERY_HINT_MAX_LEN = 2000;
const HINT_ALLOWED_TAGS = new Set(['span', 'p', 'br', 'b', 'strong', 'i', 'em', 'font']);
const HINT_ALLOWED_STYLES = new Set(['color', 'font-weight', 'font-size', 'font-style']);
export type LocalDeliveryDto = {
city: { id: string; code: string; name: string };
warehouse: { id: string; name: string; fulfillmentMode: string } | null;
provider: { id: string; code: string; name: string } | null;
hintHtml: string | null;
};
function sanitizeHintStyle(raw: string): string {
return raw
.split(';')
.map((part) => part.trim())
.filter(Boolean)
.map((part) => {
const idx = part.indexOf(':');
if (idx <= 0) return '';
const key = part.slice(0, idx).trim().toLowerCase();
const value = part.slice(idx + 1).trim();
if (!HINT_ALLOWED_STYLES.has(key)) return '';
if (/url\s*\(|expression\s*\(|javascript\s*:/i.test(value)) return '';
return `${key}:${value}`;
})
.filter(Boolean)
.join(';');
}
/** 文本换行转成 br,供 C 端 RichText 使用(不改 HQ 存盘原文) */
export function deliveryHintHtmlToRichNodes(html: string): string {
return html
.replace(/\r\n|\r|\n/g, '<br/>')
.replace(/(?:<br\s*\/?>){3,}/gi, '<br/><br/>');
}
/** 承运商配送提示 HTML:去掉脚本/事件,只保留字号颜色粗细 */
export function sanitizeDeliveryHintHtml(raw?: string | null): string | null {
if (raw == null) return null;
let html = String(raw).trim();
if (!html) return null;
if (html.length > LOCAL_DELIVERY_HINT_MAX_LEN) {
html = html.slice(0, LOCAL_DELIVERY_HINT_MAX_LEN);
}
html = html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '');
html = html.replace(/on[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
html = html.replace(/javascript\s*:/gi, '');
html = html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>/g, (_full, tag: string, attrs: string) => {
const name = String(tag).toLowerCase();
const closing = String(_full).startsWith('</');
if (!HINT_ALLOWED_TAGS.has(name)) return '';
if (name === 'br') return closing ? '' : '<br/>';
if (closing) return `</${name}>`;
let style = '';
const styleMatch = String(attrs).match(/\sstyle\s*=\s*("([^"]*)"|'([^']*)')/i);
if (styleMatch) {
style = sanitizeHintStyle(styleMatch[2] ?? styleMatch[3] ?? '');
}
let color = '';
let size = '';
if (name === 'font') {
const colorMatch = String(attrs).match(/\scolor\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/i);
if (colorMatch) color = (colorMatch[2] ?? colorMatch[3] ?? colorMatch[4] ?? '').trim();
const sizeMatch = String(attrs).match(/\ssize\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/i);
if (sizeMatch) size = (sizeMatch[2] ?? sizeMatch[3] ?? sizeMatch[4] ?? '').trim();
}
const extra: string[] = [];
if (style) extra.push(`style="${style}"`);
if (color) extra.push(`color="${color.replace(/"/g, '')}"`);
if (size) extra.push(`size="${size.replace(/"/g, '')}"`);
return extra.length ? `<${name} ${extra.join(' ')}>` : `<${name}>`;
});
const cleaned = html.replace(/&nbsp;/g, ' ').trim();
return cleaned || null;
}
+4
View File
@@ -64,6 +64,10 @@ export type ClientRuntimeConfig = {
qualificationDisclosureUrl?: string;
/** 总部客服电话 */
customerServicePhone?: string;
/** 企业微信「微信客服」链接(kfid) */
customerServiceWecomUrl?: string;
/** 企业微信 CorpID(小程序调起企微客服) */
wecomCorpId?: string;
/** 合伙人入驻:企微客服二维码图片 URL */
partnerOnboardCsQrUrl?: string | null;
/** 合伙人入驻:企微客服提示文案 */