feat(v3.4.14): mini-user store UX, brand config, client settings
Store list/detail package flow, configurable WeChat mini brand assets and customer service, shop H5 home refresh. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,19 +1,26 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { CUSTOMER_SERVICE_PHONE } from '@dukang/shared-types';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import SubPageHeader from '../../components/SubPageHeader';
|
||||
import ContactCsButton from '../../components/ContactCsButton';
|
||||
import { toast } from '../../lib/api';
|
||||
import { getBrandAssetsSync, loadBrandAssets } from '../../lib/brand-assets';
|
||||
|
||||
const isWeapp = process.env.TARO_ENV === 'weapp';
|
||||
const DIAL_PHONE = CUSTOMER_SERVICE_PHONE.replace(/-/g, '');
|
||||
|
||||
function dialPhone() {
|
||||
Taro.makePhoneCall({ phoneNumber: DIAL_PHONE }).catch(() => toast('无法拨打电话'));
|
||||
function dialPhone(phone: string) {
|
||||
const tel = phone.replace(/-/g, '');
|
||||
Taro.makePhoneCall({ phoneNumber: tel }).catch(() => toast('无法拨打电话'));
|
||||
}
|
||||
|
||||
export default function CustomerServicePage() {
|
||||
const [phone, setPhone] = useState(() => getBrandAssetsSync().customerServicePhone);
|
||||
|
||||
useEffect(() => {
|
||||
void loadBrandAssets().then((b) => setPhone(b.customerServicePhone));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageShell variant="sub" className="cs-page">
|
||||
<SubPageHeader title="联系客服" />
|
||||
@@ -30,14 +37,14 @@ export default function CustomerServicePage() {
|
||||
<ContactCsButton className="cs-online-btn" session={{ from: 'customer-service' }} />
|
||||
) : null}
|
||||
|
||||
<View className={isWeapp ? 'cs-phone-link' : 'cs-call-btn'} onClick={dialPhone}>
|
||||
<View className={isWeapp ? 'cs-phone-link' : 'cs-call-btn'} onClick={() => dialPhone(phone)}>
|
||||
<Text>
|
||||
{isWeapp ? `或拨打客服电话 ${CUSTOMER_SERVICE_PHONE}` : '拨打客服电话'}
|
||||
{isWeapp ? `或拨打客服电话 ${phone}` : '拨打客服电话'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{!isWeapp ? (
|
||||
<Text className="cs-phone-display">{CUSTOMER_SERVICE_PHONE}</Text>
|
||||
<Text className="cs-phone-display">{phone}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</PageShell>
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
import PageShell from '../../components/PageShell';
|
||||
import WechatLoginButton from '../../components/WechatLoginButton';
|
||||
import PhoneQuickLoginButton from '../../components/PhoneQuickLoginButton';
|
||||
import { BRAND_LOGO_WIDE_URL } from '@dukang/shared-types';
|
||||
import {
|
||||
applyBrandFromClientConfig,
|
||||
getBrandAssetsSync,
|
||||
loadBrandAssets,
|
||||
} from '../../lib/brand-assets';
|
||||
import { finishLoginNavigate, forceReloadAfterAccountMerge } from '../../lib/auth-nav';
|
||||
import {
|
||||
bindWechatForUser,
|
||||
@@ -98,11 +102,18 @@ export default function LoginPage() {
|
||||
const [wxAuthorize, setWxAuthorize] = useState(true);
|
||||
const [completeMode, setCompleteMode] = useState<'phone' | 'wechat' | null>(null);
|
||||
const [showSmsForm, setShowSmsForm] = useState(!IS_WEAPP);
|
||||
const [logoWideUrl, setLogoWideUrl] = useState(() => getBrandAssetsSync().brandLogoWideUrl);
|
||||
|
||||
useEffect(() => {
|
||||
request<ClientRuntimeConfig>('/common/client-config')
|
||||
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
||||
.catch(() => setWxAuthorize(true));
|
||||
.then((config) => {
|
||||
setWxAuthorize(isWxAuthorizeEnabled(config));
|
||||
setLogoWideUrl(applyBrandFromClientConfig(config).brandLogoWideUrl);
|
||||
})
|
||||
.catch(() => {
|
||||
setWxAuthorize(true);
|
||||
void loadBrandAssets().then((b) => setLogoWideUrl(b.brandLogoWideUrl));
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -394,7 +405,7 @@ export default function LoginPage() {
|
||||
<View className="login-header">
|
||||
<View className="login-logo-wrap">
|
||||
<View className="login-logo">
|
||||
<Image className="login-logo-img" src={BRAND_LOGO_WIDE_URL} mode="aspectFit" />
|
||||
<Image className="login-logo-img" src={logoWideUrl} mode="aspectFit" />
|
||||
</View>
|
||||
<Text className="login-logo-badge">官方</Text>
|
||||
</View>
|
||||
|
||||
@@ -2,8 +2,6 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { View, Text, Image, Button, Input, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
|
||||
import {
|
||||
BRAND_LOGO_MARK_URL,
|
||||
QUALIFICATION_DISCLOSURE_URL,
|
||||
isWxAuthorizeEnabled,
|
||||
type ClientRuntimeConfig,
|
||||
} from '@dukang/shared-types';
|
||||
@@ -13,6 +11,11 @@ import WechatShareReady from '../../components/WechatShareReady';
|
||||
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
||||
import { goLogin } from '../../lib/auth-nav';
|
||||
import { bindWechatForUser } from '../../lib/wechat-auth';
|
||||
import {
|
||||
applyBrandFromClientConfig,
|
||||
getBrandAssetsSync,
|
||||
loadBrandAssets,
|
||||
} from '../../lib/brand-assets';
|
||||
import {
|
||||
fetchMiniWechatUserInfo,
|
||||
isDefaultMiniNickname,
|
||||
@@ -70,6 +73,10 @@ export default function MinePage() {
|
||||
const [savingProfile, setSavingProfile] = useState(false);
|
||||
const [profileLoadError, setProfileLoadError] = useState('');
|
||||
const [qualificationOpen, setQualificationOpen] = useState(false);
|
||||
const [brandMarkUrl, setBrandMarkUrl] = useState(() => getBrandAssetsSync().brandLogoMarkUrl);
|
||||
const [qualificationUrl, setQualificationUrl] = useState(
|
||||
() => getBrandAssetsSync().qualificationDisclosureUrl,
|
||||
);
|
||||
|
||||
function resetGuestState() {
|
||||
setProfile(null);
|
||||
@@ -141,8 +148,16 @@ export default function MinePage() {
|
||||
|
||||
useEffect(() => {
|
||||
request<ClientRuntimeConfig>('/common/client-config')
|
||||
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
||||
.catch(() => setWxAuthorize(true));
|
||||
.then((config) => {
|
||||
setWxAuthorize(isWxAuthorizeEnabled(config));
|
||||
const brand = applyBrandFromClientConfig(config);
|
||||
setBrandMarkUrl(brand.brandLogoMarkUrl);
|
||||
setQualificationUrl(brand.qualificationDisclosureUrl);
|
||||
})
|
||||
.catch(() => {
|
||||
setWxAuthorize(true);
|
||||
void loadBrandAssets();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const sharePayload = useMemo(
|
||||
@@ -304,7 +319,7 @@ export default function MinePage() {
|
||||
if (displayAvatarUrl) {
|
||||
return <Image className="mine-avatar-img" src={displayAvatarUrl} mode="aspectFill" />;
|
||||
}
|
||||
return <Image className="mine-avatar-img" src={BRAND_LOGO_MARK_URL} mode="aspectFit" />;
|
||||
return <Image className="mine-avatar-img" src={brandMarkUrl} mode="aspectFit" />;
|
||||
}
|
||||
|
||||
if (!authed) {
|
||||
@@ -317,7 +332,7 @@ export default function MinePage() {
|
||||
<View className="mine-profile">
|
||||
<View className="mine-avatar-wrap mine-avatar-wrap--action" onClick={() => goLogin('/pages/mine/index')}>
|
||||
<View className="mine-avatar mine-avatar--wx-pending">
|
||||
<Image className="mine-avatar-img" src={BRAND_LOGO_MARK_URL} mode="aspectFit" />
|
||||
<Image className="mine-avatar-img" src={brandMarkUrl} mode="aspectFit" />
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
@@ -526,7 +541,7 @@ export default function MinePage() {
|
||||
{previewAvatar ? (
|
||||
<Image className="mine-avatar-img" src={previewAvatar} mode="aspectFill" />
|
||||
) : (
|
||||
<Image className="mine-avatar-img" src={BRAND_LOGO_MARK_URL} mode="aspectFit" />
|
||||
<Image className="mine-avatar-img" src={brandMarkUrl} mode="aspectFit" />
|
||||
)}
|
||||
</View>
|
||||
<Text className="mine-profile-avatar-tip">点击选择头像</Text>
|
||||
@@ -579,7 +594,7 @@ export default function MinePage() {
|
||||
<View className="mine-qualification-body">
|
||||
<Image
|
||||
className="mine-qualification-img"
|
||||
src={QUALIFICATION_DISCLOSURE_URL}
|
||||
src={qualificationUrl}
|
||||
mode="widthFix"
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components';
|
||||
import { View, Text, Image } from '@tarojs/components';
|
||||
import Taro, {
|
||||
useDidShow,
|
||||
useLoad,
|
||||
@@ -156,14 +156,9 @@ export default function StoreDetailPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState('');
|
||||
const [headerSolid, setHeaderSolid] = useState(false);
|
||||
const [activePackageIndex, setActivePackageIndex] = useState(0);
|
||||
const storeRef = useRef<Store | null>(null);
|
||||
storeRef.current = store;
|
||||
|
||||
useEffect(() => {
|
||||
setActivePackageIndex(0);
|
||||
}, [store?.id]);
|
||||
|
||||
usePageScroll(({ scrollTop }) => {
|
||||
setHeaderSolid(scrollTop > 100);
|
||||
});
|
||||
@@ -326,13 +321,18 @@ export default function StoreDetailPage() {
|
||||
const envPhotos = envPhotoUrls(store);
|
||||
const heroImages = uniqueUrls([store.coverUrl, ...(store.carouselUrls || [])]);
|
||||
const packages = store.packages ?? [];
|
||||
const activePackage = packages[activePackageIndex] ?? packages[0];
|
||||
|
||||
const intro = store.intro?.trim() || '';
|
||||
const benefitRuleRaw = store.benefitUsageRule?.trim() || '';
|
||||
const benefitRule =
|
||||
benefitRuleRaw && !/^null$/i.test(benefitRuleRaw) ? benefitRuleRaw : '';
|
||||
|
||||
function openPackageDetail(index: number) {
|
||||
Taro.navigateTo({
|
||||
url: `/pages/store-package-detail/index?storeId=${storeId}&index=${index}`,
|
||||
});
|
||||
}
|
||||
|
||||
function previewEnv(index: number) {
|
||||
if (!envPhotos.length) return;
|
||||
Taro.previewImage({
|
||||
@@ -353,7 +353,13 @@ export default function StoreDetailPage() {
|
||||
/>
|
||||
|
||||
<View className="store-detail-hero full-bleed">
|
||||
<ProductCarousel images={heroImages} alt={store.name} variant="store" previewable />
|
||||
<ProductCarousel
|
||||
images={heroImages}
|
||||
alt={store.name}
|
||||
variant="store"
|
||||
previewable
|
||||
imageFit="contain"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="store-detail-info-card">
|
||||
@@ -408,48 +414,19 @@ export default function StoreDetailPage() {
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{packages.length > 0 && activePackage ? (
|
||||
{packages.length > 0 ? (
|
||||
<View className="store-detail-section store-detail-section--packages">
|
||||
<Text className="store-detail-section-title">门店套餐</Text>
|
||||
{packages.length > 1 ? (
|
||||
<ScrollView className="store-detail-package-tabs" scrollX showScrollbar={false} enhanced>
|
||||
<View className="store-detail-package-tabs-inner">
|
||||
{packages.map((pkg, index) => (
|
||||
<View
|
||||
key={`${pkg.name}-${index}`}
|
||||
className={`store-detail-package-tab${
|
||||
index === activePackageIndex ? ' store-detail-package-tab--active' : ''
|
||||
}`}
|
||||
onClick={() => setActivePackageIndex(index)}
|
||||
>
|
||||
<Text className="store-detail-package-tab-text">{pkg.name}</Text>
|
||||
</View>
|
||||
))}
|
||||
<View className="store-detail-package-list">
|
||||
{packages.map((pkg, index) => (
|
||||
<View
|
||||
key={`${pkg.name}-${index}`}
|
||||
className="store-detail-package-list-item"
|
||||
onClick={() => openPackageDetail(index)}
|
||||
>
|
||||
<Text className="store-detail-package-list-title">{pkg.name}</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
) : null}
|
||||
<View className="store-detail-package-panel">
|
||||
{activePackage.imageUrl ? (
|
||||
<Image
|
||||
className="store-detail-package-thumb"
|
||||
src={activePackage.imageUrl}
|
||||
mode="aspectFill"
|
||||
/>
|
||||
) : null}
|
||||
<View className="store-detail-package-panel-body">
|
||||
{packages.length === 1 ? (
|
||||
<Text className="store-detail-package-name">{activePackage.name}</Text>
|
||||
) : null}
|
||||
<Text className="store-detail-package-body">
|
||||
{formatRedeemAmountYuan(activePackage.price)} 元 · {activePackage.dishes}
|
||||
</Text>
|
||||
{activePackage.usableTime ? (
|
||||
<Text className="store-detail-package-meta">使用时间:{activePackage.usableTime}</Text>
|
||||
) : null}
|
||||
{activePackage.otherNotes ? (
|
||||
<Text className="store-detail-package-meta">说明:{activePackage.otherNotes}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '套餐详情',
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { View, Text, Image } from '@tarojs/components';
|
||||
import Taro, { useLoad, useRouter } from '@tarojs/taro';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import PageNavBar from '../../components/PageNavBar';
|
||||
import { request, toast } from '../../lib/api';
|
||||
|
||||
type StorePackage = {
|
||||
name: string;
|
||||
price: string | number;
|
||||
dishes: string;
|
||||
usableTime?: string | null;
|
||||
otherNotes?: string | null;
|
||||
imageUrl?: string | null;
|
||||
};
|
||||
|
||||
type Store = {
|
||||
id: string;
|
||||
name: string;
|
||||
packages?: StorePackage[] | null;
|
||||
};
|
||||
|
||||
function pickStoreId(raw?: string | null) {
|
||||
return String(raw || '')
|
||||
.trim()
|
||||
.replace(/[^\d]/g, '');
|
||||
}
|
||||
|
||||
function parsePackageIndex(raw?: string | null) {
|
||||
const n = Number.parseInt(String(raw ?? ''), 10);
|
||||
return Number.isFinite(n) && n >= 0 ? n : -1;
|
||||
}
|
||||
|
||||
function formatPriceYuan(price: string | number) {
|
||||
const n = typeof price === 'number' ? price : Number(price);
|
||||
if (!Number.isFinite(n)) return '0';
|
||||
if (Math.abs(n - Math.round(n)) < 1e-9) return String(Math.round(n));
|
||||
return n.toFixed(2).replace(/\.?0+$/, '');
|
||||
}
|
||||
|
||||
export default function StorePackageDetailPage() {
|
||||
const router = useRouter();
|
||||
const [storeId, setStoreId] = useState(() => pickStoreId(router.params.storeId));
|
||||
const [storeName, setStoreName] = useState('');
|
||||
const [pkg, setPkg] = useState<StorePackage | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState('');
|
||||
|
||||
const loadPackage = useCallback(async (sid: string, index: number) => {
|
||||
if (!sid) {
|
||||
setLoading(false);
|
||||
setLoadError('缺少门店参数');
|
||||
return;
|
||||
}
|
||||
if (index < 0) {
|
||||
setLoading(false);
|
||||
setLoadError('套餐不存在');
|
||||
return;
|
||||
}
|
||||
setLoadError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await request<Store>(`/stores/${sid}`);
|
||||
const packages = data?.packages ?? [];
|
||||
const item = packages[index];
|
||||
if (!data?.id || !item) {
|
||||
setPkg(null);
|
||||
setLoadError('套餐不存在或已下架');
|
||||
toast('套餐不存在或已下架');
|
||||
return;
|
||||
}
|
||||
setStoreName(data.name);
|
||||
setPkg(item);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : '加载失败';
|
||||
setLoadError(msg);
|
||||
toast(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useLoad((options) => {
|
||||
const sid = pickStoreId(options?.storeId || router.params.storeId);
|
||||
const index = parsePackageIndex(options?.index ?? router.params.index);
|
||||
setStoreId(sid);
|
||||
void loadPackage(sid, index);
|
||||
});
|
||||
|
||||
function goBack() {
|
||||
const pages = Taro.getCurrentPages();
|
||||
if (pages.length > 1) Taro.navigateBack();
|
||||
else if (storeId) {
|
||||
Taro.redirectTo({ url: `/pages/store-detail/index?id=${storeId}` });
|
||||
} else {
|
||||
Taro.switchTab({ url: '/pages/stores/index' });
|
||||
}
|
||||
}
|
||||
|
||||
function previewImage(url: string) {
|
||||
Taro.previewImage({ urls: [url], current: url }).catch(() => toast('无法预览图片'));
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<PageShell variant="scroll" className="store-package-detail-page">
|
||||
<PageNavBar title="套餐详情" solid onBack={goBack} />
|
||||
<View className="page-with-nav-bar u-empty">加载中…</View>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
if (!pkg) {
|
||||
return (
|
||||
<PageShell variant="scroll" className="store-package-detail-page">
|
||||
<PageNavBar title="套餐详情" solid onBack={goBack} />
|
||||
<View className="page-with-nav-bar u-empty">{loadError || '套餐不存在'}</View>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
const imageUrl = (pkg.imageUrl || '').trim();
|
||||
|
||||
return (
|
||||
<PageShell variant="scroll" className="store-package-detail-page">
|
||||
<PageNavBar title={pkg.name} solid titleVisible onBack={goBack} />
|
||||
|
||||
<View className="store-package-detail-body">
|
||||
<View className="store-package-detail-inner">
|
||||
<View className="store-package-detail-header">
|
||||
<View className="store-package-detail-title-row">
|
||||
<Text className="store-package-detail-title">{pkg.name}</Text>
|
||||
<Text className="store-package-detail-price">¥{formatPriceYuan(pkg.price)}</Text>
|
||||
</View>
|
||||
{storeName ? (
|
||||
<Text className="store-package-detail-store">{storeName}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{imageUrl ? (
|
||||
<View
|
||||
className="store-package-detail-photo"
|
||||
onClick={() => previewImage(imageUrl)}
|
||||
>
|
||||
<Image
|
||||
className="store-package-detail-photo-image"
|
||||
src={imageUrl}
|
||||
mode="widthFix"
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View className="store-package-detail-content">
|
||||
<View className="store-detail-package-field">
|
||||
<Text className="store-detail-package-field-label">菜品</Text>
|
||||
<Text className="store-detail-package-field-value">{pkg.dishes || '—'}</Text>
|
||||
</View>
|
||||
{pkg.usableTime ? (
|
||||
<View className="store-detail-package-field">
|
||||
<Text className="store-detail-package-field-label">使用时间</Text>
|
||||
<Text className="store-detail-package-field-value">{pkg.usableTime}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{pkg.otherNotes ? (
|
||||
<View className="store-detail-package-field">
|
||||
<Text className="store-detail-package-field-label">其他说明</Text>
|
||||
<Text className="store-detail-package-field-value">{pkg.otherNotes}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { View, Text, Image, Input } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
|
||||
import { StoreStatus, STORE_STATUS_LABELS } from '@dukang/shared-types';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import TabMainHeader from '../../components/TabMainHeader';
|
||||
import WechatShareReady from '../../components/WechatShareReady';
|
||||
@@ -342,17 +341,12 @@ export default function StoresPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function formatHours(store: Store) {
|
||||
function hoursLines(store: Store): string[] {
|
||||
const parts: string[] = [];
|
||||
if (store.openTime && store.closeTime) parts.push(`${store.openTime}-${store.closeTime}`);
|
||||
if (store.openTime2 && store.closeTime2) parts.push(`${store.openTime2}-${store.closeTime2}`);
|
||||
return parts.length ? `营业时间: ${parts.join(',')}` : '营业时间: 10:00-22:00';
|
||||
}
|
||||
|
||||
function formatStatus(store: Store) {
|
||||
const status = store.status as StoreStatus | undefined;
|
||||
if (status && STORE_STATUS_LABELS[status]) return STORE_STATUS_LABELS[status];
|
||||
return STORE_STATUS_LABELS[StoreStatus.OPEN];
|
||||
if (!parts.length) parts.push('10:00-22:00');
|
||||
return parts.map((p, i) => (i === 0 ? `营业时间: ${p}` : p));
|
||||
}
|
||||
|
||||
const sharePayload = useMemo(
|
||||
@@ -446,14 +440,17 @@ export default function StoresPage() {
|
||||
{formatDistanceMeters(s.distanceMeters)}
|
||||
</Text>
|
||||
</View>
|
||||
{/* 第2行:状态 + 营业时间(含第二段) */}
|
||||
<View className="store-card-row store-card-row--meta">
|
||||
<Text className="store-card-status">{formatStatus(s)}</Text>
|
||||
<Text className="store-card-hours">{formatHours(s)}</Text>
|
||||
{/* 第2行:营业时间(多段各占一行,居左) */}
|
||||
<View className="store-card-hours">
|
||||
{hoursLines(s).map((line) => (
|
||||
<Text key={line} className="store-card-hours-line">
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
{/* 第3行:地址 + 去核销 */}
|
||||
{/* 第3行:地址(最多两行)+ 去核销 */}
|
||||
<View className="store-card-row store-card-row--foot">
|
||||
<Text className="store-card-address" numberOfLines={1}>
|
||||
<Text className="store-card-address" numberOfLines={2}>
|
||||
{s.address || (s.district ? `${s.district}` : '地址待完善')}
|
||||
</Text>
|
||||
<View
|
||||
|
||||
Reference in New Issue
Block a user