feat(mini-user): v3.5.10 同城配送提示可配置并支持换行
承运商 HTML 按收货市下发;textarea 回车在 C 端转成换行。含门店去掉分享按钮与小程序企微客服。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user