首页商品:对齐门店「当次会话」——首次进入 / 城市变化 / 登录态变化 / 下拉刷新才拉列表,切 Tab 不再重复请求;退出登录会清缓存。

核销成功时间:按 Asia/Shanghai 格式化为「2026年8月3日 13点45分」。
核销码编号:单行省略显示,双击复制(有「双击复制」提示)。
This commit is contained in:
2026-08-03 13:51:29 +08:00
parent ebd8c07147
commit 06cbcdeb9c
6 changed files with 185 additions and 30 deletions
+56 -24
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components';
import Taro, {
useDidShow,
@@ -13,7 +13,12 @@ import CouponBadge from '../../components/CouponBadge';
import WechatShareReady from '../../components/WechatShareReady';
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
import { goLogin } from '../../lib/auth-nav';
import { isLoggedIn, request, toast } from '../../lib/api';
import { getToken, isLoggedIn, request, toast } from '../../lib/api';
import {
getHomeCatalogCache,
isHomeCatalogBootstrapped,
setHomeCatalogCache,
} from '../../lib/home-catalog-session';
import { ensurePayReady } from '../../lib/pay-ready';
import { capturePromoSceneAndTouchScan } from '../../lib/promo';
import { getProductMainImage } from '../../lib/product-images';
@@ -29,7 +34,6 @@ import {
DEFAULT_SHARE_TITLE,
toWeappShareMessage,
} from '../../lib/wechat-share';
type Product = {
id: string;
name: string;
@@ -110,35 +114,60 @@ export default function HomePage() {
});
}, []);
const loadProducts = useCallback(() => {
setLoading(true);
return request<Product[]>(`/catalog/products?cityCode=${encodeURIComponent(cityCode)}`)
.then((list) =>
setProducts(Array.isArray(list) ? list.map((p) => normalizeFulfillmentFlags(p)) : []),
)
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
.finally(() => setLoading(false));
}, [cityCode]);
const applyProductList = useCallback((list: Product[], nextCode: string, authKey: string) => {
const normalized = Array.isArray(list) ? list.map((p) => normalizeFulfillmentFlags(p)) : [];
setProducts(normalized);
setHomeCatalogCache({ cityCode: nextCode, authKey, products: normalized });
}, []);
const fetchProducts = useCallback(
(nextCode: string, authKey: string) => {
setLoading(true);
return request<Product[]>(`/catalog/products?cityCode=${encodeURIComponent(nextCode)}`)
.then((list) => applyProductList(list, nextCode, authKey))
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
.finally(() => setLoading(false));
},
[applyProductList],
);
/**
* 首次进入 / 城市或登录态变化:拉商品。
* 同次再切 tab:只同步选中态,不重复请求(对齐门店页)。
*/
useDidShow(() => {
syncTabBarSelected(0);
void capturePromoSceneAndTouchScan();
void loadMiniHome();
// 白名单商品按登录手机号过滤;登录后 switchTab 回首页不会卸载页面,须重新拉列表
void loadProducts();
void resolveUserCity().then((resolved) => {
setDisplayCity(resolved.displayCity);
setCityCode(getCityCodeForCatalog(resolved));
});
});
useEffect(() => {
void loadProducts();
}, [loadProducts]);
const authKey = getToken() || '';
void (async () => {
const resolved = await resolveUserCity();
const nextCode = getCityCodeForCatalog(resolved);
setDisplayCity(resolved.displayCity);
setCityCode(nextCode);
const cache = getHomeCatalogCache();
if (
isHomeCatalogBootstrapped() &&
cache &&
cache.cityCode === nextCode &&
cache.authKey === authKey &&
Array.isArray(cache.products)
) {
setProducts(cache.products as Product[]);
setLoading(false);
return;
}
await fetchProducts(nextCode, authKey);
})();
});
usePullDownRefresh(() => {
void (async () => {
try {
const authKey = getToken() || '';
const resolved = await resolveUserCity();
setDisplayCity(resolved.displayCity);
const nextCode = getCityCodeForCatalog(resolved);
@@ -148,7 +177,11 @@ export default function HomePage() {
request<Product[]>(`/catalog/products?cityCode=${encodeURIComponent(nextCode)}`),
loadMiniHome(),
]);
setProducts(Array.isArray(list) ? list.map((p) => normalizeFulfillmentFlags(p)) : []);
applyProductList(
Array.isArray(list) ? list : [],
nextCode,
authKey,
);
} catch (e) {
toast(e instanceof Error ? e.message : '加载失败');
} finally {
@@ -157,7 +190,6 @@ export default function HomePage() {
}
})();
});
function openProductDetail(id: string) {
Taro.navigateTo({ url: `/pages/product-detail/index?id=${id}` });
}
+16 -1
View File
@@ -44,6 +44,20 @@ export default function RedeemCodePage() {
const [timerSec, setTimerSec] = useState(REDEEM_TOKEN_TTL_SECONDS);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const successHandled = useRef(false);
const lastTokenTapAt = useRef(0);
function onTokenTap() {
if (!token) return;
const now = Date.now();
if (now - lastTokenTapAt.current < 350) {
lastTokenTapAt.current = 0;
void Taro.setClipboardData({ data: token })
.then(() => toast('核销码编号已复制', 'success'))
.catch(() => toast('复制失败'));
return;
}
lastTokenTapAt.current = now;
}
const stopTimer = useCallback(() => {
if (timerRef.current != null) {
@@ -144,9 +158,10 @@ export default function RedeemCodePage() {
<Text className="u-muted"></Text>
<Text className="redeem-code-amount">¥ {formatMoney(amount)}</Text>
{token ? (
<View className="redeem-code-token-wrap">
<View className="redeem-code-token-wrap" onClick={onTokenTap}>
<Text className="redeem-code-token-label"></Text>
<Text className="redeem-code-token">{token}</Text>
<Text className="redeem-code-token-hint"></Text>
</View>
) : null}
</View>
@@ -20,6 +20,29 @@ function formatMoney(amount: number) {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
/** 中国时区展示:2026年8月3日 13点45分 */
function formatChinaDateTime(input?: string | null) {
const d = input ? new Date(input) : new Date();
if (Number.isNaN(d.getTime())) return '—';
const parts = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: false,
}).formatToParts(d);
const get = (type: Intl.DateTimeFormatPartTypes) =>
parts.find((p) => p.type === type)?.value ?? '';
const year = get('year');
const month = String(Number(get('month')));
const day = String(Number(get('day')));
const hour = String(Number(get('hour')));
const minute = get('minute').padStart(2, '0');
return `${year}${month}${day}${hour}${minute}`;
}
function StarRating({
label,
value,
@@ -65,9 +88,7 @@ export default function RedeemSuccessPage() {
const amount = Number(record?.amount ?? router.params.amount ?? 0);
const storeName = record?.storeName || '门店';
const redeemNo = record?.redeemNo || '—';
const redeemedAt = record?.createdAt
? new Date(record.createdAt).toLocaleString('zh-CN')
: new Date().toLocaleString('zh-CN');
const redeemedAt = formatChinaDateTime(record?.createdAt);
function clearCache() {
try {