feat(mini-user): add configurable home banner and footer
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
HQ system settings WeChat mini config with OSS swiper/footer uploads; client-config exposes miniHome. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import TabMainHeader from '../../components/TabMainHeader';
|
||||
@@ -12,6 +12,7 @@ import { ensurePayReady } from '../../lib/pay-ready';
|
||||
import { capturePromoSceneAndTouchScan } from '../../lib/promo';
|
||||
import { getProductImages } from '../../lib/product-images';
|
||||
import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location';
|
||||
|
||||
type Product = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -24,6 +25,11 @@ type Product = {
|
||||
allowOnSitePickup?: boolean;
|
||||
};
|
||||
|
||||
type MiniHomeConfig = {
|
||||
banners: string[];
|
||||
footerUrl: string | null;
|
||||
};
|
||||
|
||||
const AROMA_TABS = [
|
||||
{ key: 'QINGXIANG', label: '清香型' },
|
||||
{ key: 'JIANGXIANG', label: '酱香型' },
|
||||
@@ -36,10 +42,29 @@ export default function HomePage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [displayCity, setDisplayCity] = useState('郑州市');
|
||||
const [cityCode, setCityCode] = useState('410100');
|
||||
const [miniHome, setMiniHome] = useState<MiniHomeConfig>({ banners: [], footerUrl: null });
|
||||
|
||||
const loadMiniHome = useCallback(() => {
|
||||
return request<{ miniHome?: MiniHomeConfig }>('/common/client-config')
|
||||
.then((cfg) => {
|
||||
const banners = Array.isArray(cfg.miniHome?.banners)
|
||||
? cfg.miniHome!.banners.filter((u) => typeof u === 'string' && !!u.trim())
|
||||
: [];
|
||||
const footerUrl =
|
||||
typeof cfg.miniHome?.footerUrl === 'string' && cfg.miniHome.footerUrl.trim()
|
||||
? cfg.miniHome.footerUrl.trim()
|
||||
: null;
|
||||
setMiniHome({ banners, footerUrl });
|
||||
})
|
||||
.catch(() => {
|
||||
/* 首页装饰图失败不阻断商品列表 */
|
||||
});
|
||||
}, []);
|
||||
|
||||
useDidShow(() => {
|
||||
syncTabBarSelected(0);
|
||||
void capturePromoSceneAndTouchScan();
|
||||
void loadMiniHome();
|
||||
void resolveUserCity().then((resolved) => {
|
||||
setDisplayCity(resolved.displayCity);
|
||||
setCityCode(getCityCodeForCatalog(resolved));
|
||||
@@ -66,9 +91,10 @@ export default function HomePage() {
|
||||
const nextCode = getCityCodeForCatalog(resolved);
|
||||
setCityCode(nextCode);
|
||||
setLoading(true);
|
||||
const list = await request<Product[]>(
|
||||
`/catalog/products?cityCode=${encodeURIComponent(nextCode)}`,
|
||||
);
|
||||
const [list] = await Promise.all([
|
||||
request<Product[]>(`/catalog/products?cityCode=${encodeURIComponent(nextCode)}`),
|
||||
loadMiniHome(),
|
||||
]);
|
||||
setProducts(Array.isArray(list) ? list : []);
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : '加载失败');
|
||||
@@ -110,6 +136,8 @@ export default function HomePage() {
|
||||
}
|
||||
|
||||
const filtered = products.filter((p) => p.aromaType === tab);
|
||||
const banners = miniHome.banners;
|
||||
const footerUrl = miniHome.footerUrl;
|
||||
|
||||
return (
|
||||
<PageShell variant="tab" className="home-page no-tab-header">
|
||||
@@ -130,6 +158,24 @@ export default function HomePage() {
|
||||
<Text className="home-aroma-city">{displayCity}</Text>
|
||||
</View>
|
||||
|
||||
{banners.length > 0 ? (
|
||||
<View className="home-promo-banner">
|
||||
<Swiper
|
||||
className="home-promo-banner-swiper"
|
||||
indicatorDots={banners.length > 1}
|
||||
autoplay={banners.length > 1}
|
||||
circular={banners.length > 1}
|
||||
interval={4000}
|
||||
>
|
||||
{banners.map((url) => (
|
||||
<SwiperItem key={url}>
|
||||
<Image className="home-promo-banner-img" src={url} mode="aspectFill" />
|
||||
</SwiperItem>
|
||||
))}
|
||||
</Swiper>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View className="home-product-list">
|
||||
{loading ? <View className="home-empty">加载中…</View> : null}
|
||||
{!loading && products.length === 0 ? (
|
||||
@@ -173,6 +219,12 @@ export default function HomePage() {
|
||||
})}
|
||||
</View>
|
||||
|
||||
{footerUrl ? (
|
||||
<View className="home-promo-footer">
|
||||
<Image className="home-promo-footer-img" src={footerUrl} mode="aspectFill" />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{shouldRenderPageTabBar() ? <UserTabBar selected={0} /> : null}
|
||||
</PageShell>
|
||||
);
|
||||
|
||||
@@ -81,6 +81,42 @@
|
||||
border-bottom-color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.home-promo-banner {
|
||||
margin: 16px var(--space-page) 0;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
background: var(--color-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
aspect-ratio: 15 / 8;
|
||||
}
|
||||
|
||||
.home-promo-banner-swiper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.home-promo-banner-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.home-promo-footer {
|
||||
margin: 0 var(--space-page) 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
background: var(--color-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
aspect-ratio: 15 / 4;
|
||||
}
|
||||
|
||||
.home-promo-footer-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.home-product-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user