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
@@ -16,35 +16,43 @@ function dialPhone(phone: string) {
}
export default function CustomerServicePage() {
const [phone, setPhone] = useState(() => getBrandAssetsSync().customerServicePhone);
const [brand, setBrand] = useState(() => getBrandAssetsSync());
useEffect(() => {
void loadBrandAssets().then((b) => setPhone(b.customerServicePhone));
void loadBrandAssets().then(setBrand);
}, []);
const phone = brand.customerServicePhone;
const wecomReady = !!brand.customerServiceWecomUrl.trim() && !!brand.wecomCorpId.trim();
const wecomUrlReady = !!brand.customerServiceWecomUrl.trim();
const hint = isWeapp
? wecomReady
? '点击下方按钮,进入企业微信客服会话'
: '点击下方按钮,进入在线客服会话'
: wecomUrlReady
? '点击下方按钮进入企业微信客服,或拨打客服电话'
: '请在微信小程序内打开以使用在线客服,或拨打客服电话';
return (
<PageShell variant="sub" className="cs-page">
<SubPageHeader title="联系客服" />
<View className="sub-page-body inset-page cs-body">
<Text className="cs-brand"></Text>
<Text className="cs-hint">
{isWeapp
? '点击下方按钮,进入小程序在线客服会话'
: '请在微信小程序内打开以使用在线客服,或拨打客服电话'}
</Text>
<Text className="cs-hint">{hint}</Text>
<Text className="cs-hours">9:00 - 21:00</Text>
{isWeapp ? (
{isWeapp || wecomUrlReady ? (
<ContactCsButton className="cs-online-btn" session={{ from: 'customer-service' }} />
) : null}
<View className={isWeapp ? 'cs-phone-link' : 'cs-call-btn'} onClick={() => dialPhone(phone)}>
<View className={isWeapp || wecomUrlReady ? 'cs-phone-link' : 'cs-call-btn'} onClick={() => dialPhone(phone)}>
<Text>
{isWeapp ? `或拨打客服电话 ${phone}` : '拨打客服电话'}
{isWeapp || wecomUrlReady ? `或拨打客服电话 ${phone}` : '拨打客服电话'}
</Text>
</View>
{!isWeapp ? (
{!isWeapp && !wecomUrlReady ? (
<Text className="cs-phone-display">{phone}</Text>
) : null}
</View>
@@ -11,7 +11,9 @@ import { maskPhone } from '../../lib/phone';
import { ensurePayReady } from '../../lib/pay-ready';
import { fetchUserProfile } from '../../lib/pay-wechat';
import { request, toast } from '../../lib/api';
import DeliveryHintHtml from '../../components/DeliveryHintHtml';
import { canCrossCity, isCrossCityAddress } from '../../lib/product-fulfillment';
import { loadLocalDeliveries, matchLocalDelivery, resolveLocalDeliveryHintHtml } from '../../lib/local-delivery';
import { getProductMainImage } from '../../lib/product-images';
import BenefitFigure from '../../components/BenefitFigure';
@@ -75,6 +77,7 @@ export default function OrderConfirmPage() {
const [addresses, setAddresses] = useState<Address[]>([]);
const [addressId, setAddressId] = useState(checkoutCtx.addressId || '');
const [preview, setPreview] = useState<OrderPreview | null>(null);
const [localHintHtml, setLocalHintHtml] = useState('');
const [previewLoading, setPreviewLoading] = useState(false);
const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState('');
@@ -151,6 +154,18 @@ export default function OrderConfirmPage() {
[addresses, addressId],
);
useEffect(() => {
const cityName = selectedAddress?.city;
if (!cityName) {
setLocalHintHtml('');
return;
}
void loadLocalDeliveries().then((list) => {
const row = matchLocalDelivery(list, { cityName });
setLocalHintHtml(row ? resolveLocalDeliveryHintHtml(row) : '');
});
}, [selectedAddress?.city]);
const allowCross =
preview?.allowCrossCityDelivery !== undefined
? canCrossCity({ allowCrossCityDelivery: preview.allowCrossCityDelivery })
@@ -339,6 +354,12 @@ export default function OrderConfirmPage() {
</View>
) : null}
{!isCross && addressOk && addressId && localHintHtml ? (
<View className="order-card">
<DeliveryHintHtml className="u-muted" html={localHintHtml} />
</View>
) : null}
{preview ? (
<>
<View className="order-card">
@@ -10,6 +10,8 @@ import ContactCsButton from '../../components/ContactCsButton';
import LogisticsRichText from '../../components/LogisticsRichText';
import { request, toast } from '../../lib/api';
import { buildPayUrl } from '../../lib/checkout-nav';
import DeliveryHintHtml from '../../components/DeliveryHintHtml';
import { loadLocalDeliveries, matchLocalDelivery, resolveLocalDeliveryHintHtml } from '../../lib/local-delivery';
import {
formatEstimatedArrival,
isLogisticsNotArrived,
@@ -83,6 +85,22 @@ const STATUS_LABELS: Record<string, string> = {
REFUNDED: '已退款',
};
function applyLocalDeliveryHint(
data: OrderDetail,
setLocalHintHtml: (html: string) => void,
) {
if (data.deliveryType !== 'LOCAL') {
setLocalHintHtml('');
return;
}
void loadLocalDeliveries().then((list) => {
const row = data.receiverCity
? matchLocalDelivery(list, { cityName: data.receiverCity })
: null;
setLocalHintHtml(resolveLocalDeliveryHintHtml(row));
});
}
function fullReceiverAddress(order: OrderDetail) {
const detail = (order.receiverAddress || '').trim();
const region = [order.receiverProvince, order.receiverCity, order.receiverDistrict]
@@ -102,6 +120,7 @@ export default function OrderDetailPage() {
const [latestTrack, setLatestTrack] = useState<OrderTrackNode | null>(null);
const [estimatedArrival, setEstimatedArrival] = useState<OrderTrackEstimatedArrival | null>(null);
const [trackLoading, setTrackLoading] = useState(false);
const [localHintHtml, setLocalHintHtml] = useState('');
async function loadOrderTrack(delivery: OrderDetail['delivery'], orderStatus?: string) {
if (!orderId || !shouldLoadOrderTrack(delivery)) {
@@ -133,6 +152,7 @@ export default function OrderDetailPage() {
.then((data) => {
setOrder(data);
void loadOrderTrack(data.delivery, data.status);
applyLocalDeliveryHint(data, setLocalHintHtml);
})
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
}, [orderId]);
@@ -144,6 +164,7 @@ export default function OrderDetailPage() {
.then((data) => {
setOrder(data);
void loadOrderTrack(data.delivery, data.status);
applyLocalDeliveryHint(data, setLocalHintHtml);
})
.catch(() => {});
});
@@ -361,6 +382,12 @@ export default function OrderDetailPage() {
{order.createdAt ? String(order.createdAt).slice(0, 19).replace('T', ' ') : '-'}
</Text>
</View>
{order.deliveryType === 'LOCAL' && localHintHtml ? (
<View className="order-row">
<Text className="order-row-label"></Text>
<DeliveryHintHtml className="order-row-value" html={localHintHtml} />
</View>
) : null}
</View>
</>
)}
@@ -14,6 +14,7 @@ import PageNavBar from '../../components/PageNavBar';
import ProductCarousel from '../../components/ProductCarousel';
import WechatShareReady from '../../components/WechatShareReady';
import BenefitFigure from '../../components/BenefitFigure';
import DeliveryHintHtml from '../../components/DeliveryHintHtml';
import { goLogin } from '../../lib/auth-nav';
import { ensurePayReady } from '../../lib/pay-ready';
import { isLoggedIn, request, toast } from '../../lib/api';
@@ -28,6 +29,8 @@ import {
canPickupOnSite,
normalizeFulfillmentFlags,
} from '../../lib/product-fulfillment';
import { loadLocalDeliveries, matchLocalDelivery, resolveLocalDeliveryHintHtml } from '../../lib/local-delivery';
import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location';
import {
buildSceneSharePayload,
toWeappShareMessage,
@@ -90,6 +93,7 @@ export default function ProductDetailPage() {
const [product, setProduct] = useState<Product | null>(null);
const [headerSolid, setHeaderSolid] = useState(false);
const [selected, setSelected] = useState<Record<string, string>>({});
const [localHintHtml, setLocalHintHtml] = useState('');
usePageScroll(({ scrollTop }) => {
setHeaderSolid(scrollTop > 100);
@@ -136,6 +140,17 @@ export default function ProductDetailPage() {
useDidShow(() => {
loadProduct();
void resolveUserCity()
.then((city) =>
loadLocalDeliveries().then((list) => {
const row = matchLocalDelivery(list, {
cityCode: getCityCodeForCatalog(city),
cityName: city.cityName || city.displayCity || city.city,
});
setLocalHintHtml(resolveLocalDeliveryHintHtml(row));
}),
)
.catch(() => setLocalHintHtml(resolveLocalDeliveryHintHtml(null)));
});
const attrs = product?.specAttrs ?? [];
@@ -317,6 +332,10 @@ export default function ProductDetailPage() {
</View>
) : null}
{allowOnline && localHintHtml ? (
<DeliveryHintHtml className="product-detail-fulfillment" html={localHintHtml} />
) : null}
<View className="product-detail-promo">
<View className="product-detail-promo-glow" />
<View className="product-detail-promo-head">
@@ -12,7 +12,6 @@ import Taro, {
import PageShell from '../../components/PageShell';
import PageNavBar from '../../components/PageNavBar';
import ProductCarousel from '../../components/ProductCarousel';
import ShareNavButton from '../../components/ShareNavButton';
import StoreRedeemMarquee from '../../components/StoreRedeemMarquee';
import WechatShareReady from '../../components/WechatShareReady';
import { request, toast } from '../../lib/api';
@@ -356,7 +355,6 @@ export default function StoreDetailPage() {
solid={headerSolid}
titleVisible={headerSolid}
onBack={goBack}
right={<ShareNavButton payload={sharePayload} />}
/>
<View className="store-detail-hero full-bleed">