feat(h5): 首页 feed 真接入 /meituan/feed+top-sales(渲染/翻页/缓存/三态/抢)
删 7 张写死 mock 卡改 CouponCard 动态渲染(随原生:feed-tag 只用 price_label/rank_label,去掉「比团购省」);三排序 tab 智能推荐/距离最近→feed、销量最高→top-sales(删 mock 残留 asc/desc);触底翻页 + 每 tab 5min 缓存 + loading/empty/degraded 三态 + 去重 + 切 tab 防竞态 + 未授权只切高亮不打接口;抢→referral-link([1,3])→SGBridge.openMeituan(deeplink||h5)。含原 H2a 定位空态 WIP 一并提交。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+343
-159
@@ -4798,6 +4798,15 @@ html,body{background:#fff;display:block;height:100%;margin:0;cursor:auto;overflo
|
||||
.screen,.home-scroll{overscroll-behavior:none;-webkit-overflow-scrolling:touch}
|
||||
#variantToggle,#debugBack{display:none!important}
|
||||
.tab-bar{display:none!important} /* 底栏归原生壳:隐藏 H5 原型自带 tabbar,修双底栏 */
|
||||
/* 顶部品牌带吸顶(去框后).第一版用 flex 固定头(#home overflow:hidden + .home-scroll min-height:0)
|
||||
在本机 WebView 上把页面整个卡死、无法滚动,放弃。改用与已验证可用的 records 页同款「绝对定位」法:
|
||||
.screen(position:absolute 定位容器)内,品牌带绝对贴顶、home-scroll 绝对占满其下并自身滚动——
|
||||
不依赖 flex 高度链或 position:sticky,在本机 WebView 上最稳(对齐 records .hist-scroll 的 top/bottom 定位)。
|
||||
margin-top:0 去框后无假状态栏,贴系统状态栏下(原生壳已 statusBarsPadding);feed-sort top:0 紧贴品牌带下。 */
|
||||
#home{overflow:hidden}
|
||||
#home .brand-strip{position:absolute; top:0; left:0; right:0; margin-top:0; z-index:5}
|
||||
#home .home-scroll{position:absolute; top:38px; left:0; right:0; bottom:0}
|
||||
#home .feed-sort{top:0}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -4840,8 +4849,7 @@ TODO: 补充 @owner / @from / @to / @notes(参照 FLOW.md)
|
||||
|
||||
<!-- ===== HOME TAB ===== -->
|
||||
<div class="screen active" id="home" style="background: var(--bg);">
|
||||
<div class="home-scroll" style="flex: 1;overflow-y: auto;">
|
||||
<!-- 顶部品牌带 wordmark -->
|
||||
<!-- 顶部品牌带 wordmark(吸顶:移出滚动区,作 .screen 固定头,不随内容滚动) -->
|
||||
<div class="brand-strip">
|
||||
<div class="brand-strip-left">
|
||||
<img class="brand-strip-logo" src="assets/shared/logos/sb-brand.png" alt="">
|
||||
@@ -4849,6 +4857,7 @@ TODO: 补充 @owner / @from / @to / @notes(参照 FLOW.md)
|
||||
</div>
|
||||
<span class="brand-strip-tagline">跨平台比价,用傻瓜</span>
|
||||
</div>
|
||||
<div class="home-scroll" style="flex: 1;overflow-y: auto;">
|
||||
<!-- 顶部 Banner —— 3 个点位的可滑动轮播 -->
|
||||
<div class="home-banner" id="homeBanner">
|
||||
<div class="home-banner-track" id="homeBannerTrack">
|
||||
@@ -5034,121 +5043,18 @@ TODO: 补充 @owner / @from / @to / @notes(参照 FLOW.md)
|
||||
<div style="font-size: 14px;color: #999;line-height: 1.5;">附近暂无优惠商品~去外卖平台解锁底价</div>
|
||||
<button onclick="onHomeCompareClick()" style="margin-top: 12px;height: 32px;padding: 0 20px;border: none;border-radius: 18px;font-size: 13px;font-weight: 600;background: linear-gradient(180deg,#FFE066 0%,#FFC400 100%);color: #1A1A1A;box-shadow: inset 0 1px 0 rgba(255,255,255,.7), 0 2px 6px rgba(255,179,0,.35);cursor: pointer;display: inline-flex;align-items: center;justify-content: center;">去比价</button>
|
||||
</div>
|
||||
<div class="home-feed">
|
||||
<div class="feed-card">
|
||||
<div class="feed-img"><img src="assets/home/index/feed/chabaidao.png" alt=""><span class="feed-cat-badge cat-waimai">外卖</span></div>
|
||||
<div class="feed-body">
|
||||
<div class="feed-title"><span class="shop-name">茶百道</span><span class="title-sep">|</span>【新品】杨梅系列双杯兑换券·1张|208店通用</div>
|
||||
<div class="feed-meta">
|
||||
<span class="meta-left">600m 国贸店</span>
|
||||
<span class="meta-right">热销8.5万+</span>
|
||||
<!-- feed 首次加载中(转圈占位) -->
|
||||
<div class="home-feed-empty" id="homeFeedLoading" style="display: none;flex-direction: column;align-items: center;justify-content: flex-start;background: #fff;padding: 32px 24px;text-align: center;">
|
||||
<div style="font-size: 14px;color: #999;">加载中…</div>
|
||||
</div>
|
||||
<div class="feed-tag">比团购省 5.5 元</div>
|
||||
<div class="feed-pricerow">
|
||||
<div class="feed-price"><span class="yuan">¥</span><span class="num">22<small>.5</small></span></div>
|
||||
<span class="feed-original">¥28.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed-grab">抢</div>
|
||||
</div>
|
||||
<div class="feed-card">
|
||||
<div class="feed-img"><img src="assets/home/index/feed/subway.png" alt=""><span class="feed-cat-badge cat-waimai">外卖</span></div>
|
||||
<div class="feed-body">
|
||||
<div class="feed-title"><span class="shop-name">SUBWAY赛百味</span><span class="title-sep">|</span>香溢金枪鱼双拼三明治小食两件套兑换券·1张|159店通用</div>
|
||||
<div class="feed-meta">
|
||||
<span class="meta-left">1.2km 大悦城店</span>
|
||||
<span class="meta-right">热销3.2万+</span>
|
||||
</div>
|
||||
<div class="feed-tag">比团购省 19.1 元</div>
|
||||
<div class="feed-pricerow">
|
||||
<div class="feed-price"><span class="yuan">¥</span><span class="num">27<small>.9</small></span></div>
|
||||
<span class="feed-original">¥47.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed-grab">抢</div>
|
||||
</div>
|
||||
<div class="feed-card">
|
||||
<div class="feed-img"><img src="assets/home/index/feed/luckin.png" alt=""><span class="feed-cat-badge cat-waimai">外卖</span></div>
|
||||
<div class="feed-body">
|
||||
<div class="feed-title"><span class="shop-name">瑞幸咖啡</span><span class="title-sep">|</span>【双杯】人气拿铁-含生椰-城市专享兑换券·1张|27店通用</div>
|
||||
<div class="feed-meta">
|
||||
<span class="meta-left">800m 朝阳店</span>
|
||||
<span class="meta-right">热销28万+</span>
|
||||
</div>
|
||||
<div class="feed-tag">外卖榜第 1</div>
|
||||
<div class="feed-pricerow">
|
||||
<div class="feed-price"><span class="yuan">¥</span><span class="num">20<small>.9</small></span></div>
|
||||
<span class="feed-original">¥44.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed-grab">抢</div>
|
||||
</div>
|
||||
<div class="feed-card">
|
||||
<div class="feed-img"><img src="assets/home/index/feed/pizzahut.png" alt=""><span class="feed-cat-badge cat-waimai">外卖</span></div>
|
||||
<div class="feed-body">
|
||||
<div class="feed-title"><span class="shop-name">必胜客</span><span class="title-sep">|</span>超级大满贯双拼披萨手拍普装兑换券·1张|13店通用</div>
|
||||
<div class="feed-meta">
|
||||
<span class="meta-left">1.5km 西单店</span>
|
||||
<span class="meta-right">热销5.6万+</span>
|
||||
</div>
|
||||
<div class="feed-tag">比团购省 34.1 元</div>
|
||||
<div class="feed-pricerow">
|
||||
<div class="feed-price"><span class="yuan">¥</span><span class="num">34<small>.9</small></span></div>
|
||||
<span class="feed-original">¥69.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed-grab">抢</div>
|
||||
</div>
|
||||
<div class="feed-card">
|
||||
<div class="feed-img"><img src="assets/home/index/feed/jubaoyuan.png" alt=""><span class="feed-cat-badge cat-tuangou">团购</span></div>
|
||||
<div class="feed-body">
|
||||
<div class="feed-title"><span class="shop-name">聚宝源</span><span class="title-sep">|</span>300元代金券|不限张数|54店通用</div>
|
||||
<div class="feed-meta">
|
||||
<span class="meta-left">2.3km 簋街店</span>
|
||||
<span class="meta-right">热销1.2万+</span>
|
||||
</div>
|
||||
<div class="feed-tag">全年低价</div>
|
||||
<div class="feed-pricerow">
|
||||
<div class="feed-price"><span class="yuan">¥</span><span class="num">218</span></div>
|
||||
<span class="feed-original">¥300.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed-grab">抢</div>
|
||||
</div>
|
||||
<div class="feed-card">
|
||||
<div class="feed-img"><img src="assets/home/index/feed/mcdonalds.png" alt=""><span class="feed-cat-badge cat-waimai">外卖</span></div>
|
||||
<div class="feed-body">
|
||||
<div class="feed-title"><span class="shop-name">麦当劳</span><span class="title-sep">|</span>【翅堡喝足】堡堡双人餐6件套|601店通用</div>
|
||||
<div class="feed-meta">
|
||||
<span class="meta-left">450m 国贸店</span>
|
||||
<span class="meta-right">热销50万+</span>
|
||||
</div>
|
||||
<div class="feed-tag">外卖榜第 1</div>
|
||||
<div class="feed-pricerow">
|
||||
<div class="feed-price"><span class="yuan">¥</span><span class="num">37<small>.1</small></span></div>
|
||||
<span class="feed-original">¥87.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed-grab">抢</div>
|
||||
</div>
|
||||
<div class="feed-card">
|
||||
<div class="feed-img"><img src="assets/home/index/feed/yoshinoya.png" alt=""><span class="feed-cat-badge cat-tuangou">团购</span></div>
|
||||
<div class="feed-body">
|
||||
<div class="feed-title"><span class="shop-name">吉野家</span><span class="title-sep">|</span>【吉划算】1+1对对碰随心餐|174店通用</div>
|
||||
<div class="feed-meta">
|
||||
<span class="meta-left">1.1km 王府井店</span>
|
||||
<span class="meta-right">热销12万+</span>
|
||||
</div>
|
||||
<div class="feed-tag">全年低价</div>
|
||||
<div class="feed-pricerow">
|
||||
<div class="feed-price"><span class="yuan">¥</span><span class="num">19<small>.9</small></span></div>
|
||||
<span class="feed-original">¥27.5</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed-grab">抢</div>
|
||||
</div>
|
||||
<div class="feed-end">已经到底了</div>
|
||||
<!-- feed 服务繁忙(degraded:上游/库失败已降级,可重试) -->
|
||||
<div class="home-feed-empty" id="homeFeedDegraded" style="display: none;flex-direction: column;align-items: center;justify-content: flex-start;background: #fff;padding: 18px 24px 24px;text-align: center;">
|
||||
<div style="font-size: 14px;color: #999;line-height: 1.5;">服务繁忙,下拉重试~</div>
|
||||
<button onclick="HomeFeed.loadFirst()" style="margin-top: 12px;height: 32px;padding: 0 20px;border: none;border-radius: 18px;font-size: 13px;font-weight: 600;background: linear-gradient(180deg,#FFE066 0%,#FFC400 100%);color: #1A1A1A;cursor: pointer;">重试</button>
|
||||
</div>
|
||||
<!-- H2b: feed 卡片由 JS 按 /api/v1/meituan/feed|top-sales 动态渲染(原 mock 静态卡已移除) -->
|
||||
<div class="home-feed"></div>
|
||||
<div class="feed-end" style="display:none">已经到底了</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-bar">
|
||||
@@ -6648,30 +6554,22 @@ function startFlipBanner() {
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', startFlipBanner);
|
||||
|
||||
// ===== feed 排序 tab(智能推荐/距离最近/销量最高) =====
|
||||
// 点未激活 tab → 激活 + 默认 desc;再点已激活 tab → desc⇄asc 翻转
|
||||
// 「智能推荐」tab 无方向。data-dir 仅记录状态,无 UI 表达
|
||||
// 点击任意 tab 后将 .feed-sort 整条 scrollIntoView 到 home-scroll 顶部
|
||||
// ===== feed 排序 tab(智能推荐/距离最近/销量最高)→ 接真实数据(H2b) =====
|
||||
// 点未激活 tab → 切高亮 + 拉真实数据(原 asc/desc 方向翻转是 mock 残留、后端不支持方向, 已删)
|
||||
document.addEventListener('click', function (e) {
|
||||
var item = e.target.closest('#home .feed-sort .fs-item');
|
||||
if (!item) return;
|
||||
if (!item || item.classList.contains('active')) return;
|
||||
var group = item.closest('.feed-sort');
|
||||
var isRec = item.dataset.key === 'rec';
|
||||
if (!item.classList.contains('active')) {
|
||||
group.querySelectorAll('.fs-item').forEach(function (o) {
|
||||
o.classList.remove('active');
|
||||
delete o.dataset.dir;
|
||||
});
|
||||
group.querySelectorAll('.fs-item').forEach(function (o) { o.classList.remove('active'); });
|
||||
item.classList.add('active');
|
||||
if (!isRec) item.dataset.dir = 'desc';
|
||||
} else if (!isRec) {
|
||||
item.dataset.dir = item.dataset.dir === 'desc' ? 'asc' : 'desc';
|
||||
// 未授权定位:只切高亮、不打接口(对齐原生 selectTabWithoutLoad);授权后由 applyLocationState 拉
|
||||
if (window.HomeFeed) {
|
||||
if (HomeFeed.locationGranted()) HomeFeed.switchTab(item.dataset.key);
|
||||
else HomeFeed.selectTabWithoutLoad(item.dataset.key);
|
||||
}
|
||||
// 自动滚到顶部:仅当 sort 还未贴顶时才滚(贴顶位置是 device y=44 + sticky top=38 = y=82)
|
||||
// 已贴顶时若再调 scrollIntoView,浏览器会做一次 0 距离的 smooth scroll,造成可见的微位移
|
||||
// 自动滚到顶部:仅当 sort 还未贴顶时才滚(贴顶位置 device y=44 + sticky top=38 = 82)
|
||||
var stickY = 82;
|
||||
var topNow = group.getBoundingClientRect().top;
|
||||
if (topNow > stickY + 2) {
|
||||
if (group.getBoundingClientRect().top > stickY + 2) {
|
||||
group.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
@@ -7713,10 +7611,12 @@ function couponChipShortName(name) {
|
||||
function renderCouponPromptPlatforms() {
|
||||
const grid = document.getElementById('couponPlatformGrid');
|
||||
if (!grid) return;
|
||||
const installedList = getInstalledPkgSet(); // 真装机态(同去点餐,替换写死的 mock app.installed)
|
||||
const platforms = getVisibleApps(); // 3 张外卖版卡片
|
||||
grid.innerHTML = platforms.map(app => {
|
||||
const shortName = couponChipShortName(app.name);
|
||||
if (app.installed) {
|
||||
const installed = (PICKER_PACKAGES[app.name] || []).some(function(p){ return installedList.indexOf(p) !== -1; });
|
||||
if (installed) {
|
||||
return '<div data-checked="true" onclick="toggleCouponPlatform(this)" class="cp-card cp-card-yellow" style="background: #fff;border: 2px solid #FF5630;border-radius: 14px;padding: 12px 4px 8px;position: relative;">' +
|
||||
'<img src="' + app.icon + '" style="width: 42px;height: 42px;border-radius: 8px;margin: 0 auto;display: block;"/>' +
|
||||
'<div style="font-size: 12px;font-weight: 700;color: var(--gray-900);margin-top: 8px;">' + shortName + '</div>' +
|
||||
@@ -8860,11 +8760,11 @@ function demoNext() {
|
||||
|
||||
// ===== 一键领券弹窗按钮 =====
|
||||
function onClaimCouponConfirm() {
|
||||
// 首页弹窗确认 → 跳 coupon 模块跑领券流程(#claim 跳过 phase-prompt,因为 home 弹窗已经选过平台)
|
||||
// H4:确认 → 触发原生领券(权限校验→缺则 OEM 引导→前台服务领券)。[] = 全领(对齐原生 platformSelectable=false);
|
||||
// 领券时用户在外卖 App,进度由原生悬浮窗展示(D2),H5 不接事件流、也不再跳原型 coupon 页。
|
||||
const prompt = document.getElementById('demoCouponPrompt');
|
||||
if (prompt) prompt.style.display = 'none';
|
||||
try { localStorage.setItem('proto_homeClaimMode', 'true'); } catch(e) {}
|
||||
location.href = '../coupon/index.html#claim';
|
||||
if (window.SGBridge && typeof SGBridge.startCouponClaim === 'function') SGBridge.startCouponClaim([]);
|
||||
}
|
||||
|
||||
// ===== 首页"一键领优惠"卡片:去领取 → 弹窗 → agent 领券 → 完成 → 回首页 =====
|
||||
@@ -8883,22 +8783,14 @@ function renderCouponDoneFromLogos() {
|
||||
}
|
||||
|
||||
function onTriClaimClick() {
|
||||
// 未登录:先弹登录页(登录成功后回首页)
|
||||
if (!isLoggedIn) { try{localStorage.setItem('proto_loginFrom','home');}catch(e){} window.loginFrom='home'; go('login'); return; }
|
||||
// 已领取过:直接弹结算/战报弹窗,不重启 agent 流
|
||||
if (homeCouponClaimed) {
|
||||
if (homeCouponClaimSummary) {
|
||||
couponTotal = homeCouponClaimSummary.total || 0;
|
||||
}
|
||||
const toast = document.getElementById('demoCouponDoneToast');
|
||||
if (toast) {
|
||||
toast.style.display = 'block';
|
||||
renderCouponDoneFromLogos();
|
||||
if (typeof runKCountUp === 'function') runKCountUp('demoCouponDoneCount', couponTotal, 1500, 200);
|
||||
if (typeof startMidnightCountdown === 'function') startMidnightCountdown();
|
||||
}
|
||||
// H4:登录门槛(领券要归属用户)→ 未登录拉极光一键登录
|
||||
if (window.SGBridge && typeof SGBridge.getToken === 'function' && !SGBridge.getToken()) { SGBridge.requestLogin(); return; }
|
||||
// 今日已领 → 提示,不重复触发(完整「上次结算卡」回显留后续)
|
||||
if (window.SGBridge && typeof SGBridge.getCouponClaimedToday === 'function' && SGBridge.getCouponClaimedToday()) {
|
||||
if (typeof showToast === 'function') showToast('今日已领取~');
|
||||
return;
|
||||
}
|
||||
// 弹领券确认弹窗:「一键自动领取」→ onClaimCouponConfirm → SGBridge.startCouponClaim
|
||||
homeClaimMode = true;
|
||||
couponTotal = 0;
|
||||
renderCouponPromptPlatforms();
|
||||
@@ -9064,6 +8956,8 @@ function getHomeLocationMode() {
|
||||
return '';
|
||||
}
|
||||
function isHomeLocationGranted() {
|
||||
// H2a:优先读真·系统定位权限;无桥(浏览器无 bridge)兜底原 localStorage 模式。
|
||||
try { if (window.SGBridge && typeof SGBridge.isLocationGranted === 'function') return !!SGBridge.isLocationGranted(); } catch(e) {}
|
||||
var mode = getHomeLocationMode();
|
||||
if (mode === 'whileUsing') return true;
|
||||
if (mode === 'once') {
|
||||
@@ -9098,8 +8992,12 @@ function openLocSysPopup() {
|
||||
if (s) s.style.display = 'flex';
|
||||
}
|
||||
function goAuthLocation() {
|
||||
// H2a:接真·系统定位权限窗(永久拒绝由原生跳设置);无桥兜底原型假弹窗。
|
||||
if (window.SGBridge && typeof SGBridge.requestLocationPermission === 'function') { SGBridge.requestLocationPermission(); return; }
|
||||
openLocSysPopup();
|
||||
}
|
||||
// 回前台(定位授权窗 / 设置返回)重核授权态并刷新 feed 区(纯 H5,不依赖原生事件)。
|
||||
document.addEventListener('visibilitychange', function(){ if (document.visibilityState === 'visible') { try { applyLocationState(); } catch(e){} } });
|
||||
function isHomeNearbyEmpty(feed) {
|
||||
try {
|
||||
var params = new URLSearchParams(location.search);
|
||||
@@ -9139,10 +9037,19 @@ function applyLocationState() {
|
||||
var wrap = document.querySelector('#home .home-feed-wrap');
|
||||
if (wrap) wrap.style.borderColor = (locationGranted && !nearbyEmpty) ? '' : 'transparent';
|
||||
// 空态时让白色 feed 区撑满到底栏,下方不留灰
|
||||
if (!locationGranted) { syncHomeFeedEmptyHeight(empty); return; }
|
||||
if (!locationGranted) {
|
||||
var fl0 = document.getElementById('homeFeedLoading'); if (fl0) fl0.style.display = 'none';
|
||||
var fd0 = document.getElementById('homeFeedDegraded'); if (fd0) fd0.style.display = 'none';
|
||||
syncHomeFeedEmptyHeight(empty); return;
|
||||
}
|
||||
empty.style.minHeight = '';
|
||||
if (nearbyEmpty && noDealEmpty) syncHomeFeedEmptyHeight(noDealEmpty);
|
||||
else if (noDealEmpty) noDealEmpty.style.minHeight = '';
|
||||
// H2b: 已授权(未授权上面已 return)→ 首次拉 feed / 坐标变化刷新;loading/empty/degraded 由 HomeFeedStates 接管
|
||||
if (window.HomeFeed) {
|
||||
if (!HomeFeed.state.loadedOnce && !HomeFeed.state.loading) HomeFeed.loadFirst();
|
||||
else HomeFeed.refreshIfCoordsChanged();
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', applyLocationState);
|
||||
window.addEventListener('load', applyLocationState);
|
||||
@@ -10077,13 +9984,26 @@ function onPlatformStatsClick() {
|
||||
if (typeof go === 'function') go('profile');
|
||||
}
|
||||
|
||||
// 比价入口 picker:动态渲染 3 平台(已装/未装区分)
|
||||
// 平台候选包(对齐原生 HOME_PICKER_APPS):任一已装即「已装」;点击拉起把整组候选包交原生取首个已装。
|
||||
var PICKER_PACKAGES = {
|
||||
'美团': ['com.sankuai.meituan', 'com.sankuai.meituan.takeoutnew'],
|
||||
'淘宝': ['com.taobao.taobao', 'me.ele'],
|
||||
'京东': ['com.jingdong.app.mall'],
|
||||
};
|
||||
function getInstalledPkgSet() {
|
||||
try { if (window.SGBridge && typeof SGBridge.getInstalledApps === 'function') return SGBridge.getInstalledApps() || []; } catch(e) {}
|
||||
return [];
|
||||
}
|
||||
// 比价入口 picker:动态渲染 3 平台(真装机态:getInstalledApps 判候选包是否已装)
|
||||
function renderHomePickerPlatforms() {
|
||||
var grid = document.getElementById('homePickerGrid');
|
||||
if (!grid || typeof getVisibleApps !== 'function') return;
|
||||
var installedList = getInstalledPkgSet();
|
||||
var platforms = getVisibleApps();
|
||||
grid.innerHTML = platforms.map(function(app) {
|
||||
if (app.installed) {
|
||||
var pkgs = PICKER_PACKAGES[app.name] || [];
|
||||
var installed = pkgs.some(function(p){ return installedList.indexOf(p) !== -1; });
|
||||
if (installed) {
|
||||
return '<div class="home-picker-item" data-platform="' + app.name + '">' +
|
||||
'<div class="home-picker-logo"><img src="' + app.icon + '" alt=""></div>' +
|
||||
'<div class="home-picker-name">' + app.name + '</div>' +
|
||||
@@ -10101,8 +10021,7 @@ function renderHomePickerPlatforms() {
|
||||
|
||||
// 比价入口 onclick → 渲染平台 + 弹 picker
|
||||
function onHomeCompareClick() {
|
||||
// 未登录:先弹登录页(登录成功后回首页)
|
||||
if (!isLoggedIn) { try{localStorage.setItem('proto_loginFrom','home');}catch(e){} window.loginFrom='home'; go('login'); return; }
|
||||
// H3:拉起外卖不需登录态;「未登录引导」连同 getAuthState 留后续登录态子项。直接渲染 + 弹平台选择。
|
||||
renderHomePickerPlatforms();
|
||||
var mask = document.getElementById('homePickerMask');
|
||||
if (mask) mask.classList.add('show');
|
||||
@@ -10130,7 +10049,9 @@ function closeHomePicker() {
|
||||
if (app) onPlatformInstallClick(app.id);
|
||||
}
|
||||
} else {
|
||||
if (typeof showToast === 'function') showToast('已跳「' + name + '」');
|
||||
// 已装:拉起对应平台 App(整组候选包交原生取首个已装,NEW_TASK|CLEAR_TASK 冷启首页)
|
||||
var pkgs = PICKER_PACKAGES[name] || [];
|
||||
if (pkgs.length && window.SGBridge && typeof SGBridge.launchApp === 'function') SGBridge.launchApp(pkgs);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -10153,9 +10074,272 @@ function closeHomePicker() {
|
||||
else updateWelfareCheckinTabDot();
|
||||
})();
|
||||
</script>
|
||||
<script src="../shared/bridge.js"></script>
|
||||
<script>
|
||||
/* H1:首页静态壳,导航/触发类先 no-op,H2-H4 接 SGBridge */
|
||||
window.go=function(){};window.onTriClaimClick=function(){};window.onHomeCompareClick=function(){};window.goAuthLocation=function(){};
|
||||
/* H2b 首页 feed:rec/distance→/meituan/feed,sales→/meituan/top-sales;触底翻页 + 每 tab 5min 缓存
|
||||
+ loading/empty/degraded 三态 + 去重 + 切 tab 防竞态 + 未授权不打接口。对齐原生 HomeViewModel。 */
|
||||
(function (global) {
|
||||
'use strict';
|
||||
var BEIJING = { longitude: 116.404, latitude: 39.928 };
|
||||
var ENDPOINT = { rec: '/api/v1/meituan/feed', distance: '/api/v1/meituan/feed', sales: '/api/v1/meituan/top-sales' };
|
||||
var CACHE_TTL = 5 * 60 * 1000;
|
||||
var SCROLL_THRESHOLD = 400; // 距底 px 触发翻页
|
||||
|
||||
var st = { tab: 'rec', page: 0, hasNext: true, loading: false, loadedOnce: false,
|
||||
items: [], seq: 0, coords: null, cache: {} };
|
||||
|
||||
function $feed() { return document.querySelector('#home .home-feed'); }
|
||||
function $end() { return document.querySelector('#home .feed-end'); }
|
||||
function $scroll() { return document.querySelector('#home .home-scroll'); }
|
||||
|
||||
function locationGranted() {
|
||||
try { if (global.SGBridge && SGBridge.isLocationGranted) return !!SGBridge.isLocationGranted(); } catch (e) {}
|
||||
return false;
|
||||
}
|
||||
function getCoords() {
|
||||
try {
|
||||
if (global.SGBridge && SGBridge.getLocation) {
|
||||
var c = SGBridge.getLocation();
|
||||
if (c && typeof c.longitude === 'number' && typeof c.latitude === 'number') return c;
|
||||
}
|
||||
} catch (e) {}
|
||||
return BEIJING;
|
||||
}
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (m) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m];
|
||||
});
|
||||
}
|
||||
function splitPrice(p) {
|
||||
var s = String(p == null ? '' : p), i = s.indexOf('.');
|
||||
return i < 0 ? [s, ''] : [s.slice(0, i), s.slice(i)];
|
||||
}
|
||||
|
||||
// CouponCard → .feed-card(1:1 照原生 FeedCard;feed-tag 只用 price_label/rank_label,不显示「比团购省」)
|
||||
function renderCard(card) {
|
||||
var isWaimai = card.platform === 1;
|
||||
var shop = card.brand_name
|
||||
? '<span class="shop-name">' + esc(card.brand_name) + '</span><span class="title-sep">|</span>' : '';
|
||||
var title = esc(card.name || '');
|
||||
if (card.available_poi_num) title += '|' + card.available_poi_num + '店通用';
|
||||
var metaLeft = '';
|
||||
if (card.distance_text) metaLeft += esc(card.distance_text);
|
||||
if (card.poi_name) metaLeft += (metaLeft ? ' ' : '') + esc(card.poi_name);
|
||||
var saleVol = card.sale_volume ? '<span class="meta-right">' + esc(card.sale_volume) + '</span>' : '';
|
||||
var labels = [];
|
||||
if (card.price_label && card.price_label.trim()) labels.push(card.price_label.trim());
|
||||
if (card.rank_label && card.rank_label.trim()) labels.push(card.rank_label.trim());
|
||||
var tag = labels.length
|
||||
? '<div class="feed-tag">' + esc(labels[Math.floor(Math.random() * labels.length)]) + '</div>' : '';
|
||||
var pr = splitPrice(card.sell_price);
|
||||
|
||||
var el = document.createElement('div');
|
||||
el.className = 'feed-card';
|
||||
el.dataset.sign = card.product_view_sign || '';
|
||||
el.dataset.platform = card.platform;
|
||||
if (card.biz_line != null) el.dataset.bizLine = card.biz_line;
|
||||
el.innerHTML =
|
||||
'<div class="feed-img"><img src="' + esc(card.head_image_url || '') + '" alt="">' +
|
||||
'<span class="feed-cat-badge ' + (isWaimai ? 'cat-waimai">外卖' : 'cat-tuangou">团购') + '</span></div>' +
|
||||
'<div class="feed-body">' +
|
||||
'<div class="feed-title">' + shop + title + '</div>' +
|
||||
'<div class="feed-meta"><span class="meta-left">' + metaLeft + '</span>' + saleVol + '</div>' +
|
||||
tag +
|
||||
'<div class="feed-pricerow">' +
|
||||
'<div class="feed-price"><span class="yuan">¥</span><span class="num">' + esc(pr[0]) +
|
||||
(pr[1] ? '<small>' + esc(pr[1]) + '</small>' : '') + '</span></div>' +
|
||||
'<span class="feed-original">¥' + esc(card.original_price || '') + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="feed-grab">抢</div>';
|
||||
return el;
|
||||
}
|
||||
|
||||
function renderList(newItems, append) {
|
||||
var feed = $feed(); if (!feed) return;
|
||||
if (!append) feed.innerHTML = '';
|
||||
newItems.forEach(function (c) { feed.appendChild(renderCard(c)); });
|
||||
var end = $end();
|
||||
if (end) end.style.display = (!st.hasNext && st.items.length) ? '' : 'none';
|
||||
}
|
||||
|
||||
function reqBody(tab, page, coords) {
|
||||
return tab === 'sales'
|
||||
? { page: page }
|
||||
: { longitude: coords.longitude, latitude: coords.latitude, page: page, tab: tab };
|
||||
}
|
||||
|
||||
function loadPage(page, append) {
|
||||
var tab = st.tab, coords = getCoords(), mySeq = ++st.seq;
|
||||
st.coords = coords; st.loading = true;
|
||||
if (global.HomeFeedStates) HomeFeedStates(append ? 'loadingMore' : 'loading');
|
||||
fetch(ENDPOINT[tab], {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqBody(tab, page, coords)),
|
||||
}).then(function (r) { return r.ok ? r.json() : null; }).then(function (d) {
|
||||
if (mySeq !== st.seq) return; // 竞态:已切 tab/重载 → 丢弃旧响应
|
||||
st.loading = false; st.loadedOnce = true;
|
||||
if (!d) { if (global.HomeFeedStates) HomeFeedStates('degraded'); return; }
|
||||
var items = d.items || [];
|
||||
if (append) {
|
||||
var seen = {}; st.items.forEach(function (c) { seen[c.product_view_sign] = 1; });
|
||||
items = items.filter(function (c) { return c.product_view_sign && !seen[c.product_view_sign]; });
|
||||
st.items = st.items.concat(items);
|
||||
} else { st.items = items; }
|
||||
st.page = page; st.hasNext = !!d.has_next;
|
||||
st.cache[tab] = { items: st.items.slice(), page: st.page, hasNext: st.hasNext,
|
||||
loadedAt: (append && st.cache[tab]) ? st.cache[tab].loadedAt : Date.now() };
|
||||
renderList(append ? items : st.items, append);
|
||||
if (global.HomeFeedStates) HomeFeedStates(d.status === 'degraded' ? 'degraded' : (st.items.length ? 'ok' : 'empty'));
|
||||
}).catch(function () {
|
||||
if (mySeq !== st.seq) return;
|
||||
st.loading = false; st.loadedOnce = true;
|
||||
if (global.HomeFeedStates) HomeFeedStates('degraded');
|
||||
});
|
||||
}
|
||||
|
||||
function loadFirst() { st.page = 0; st.hasNext = true; st.items = []; renderList([], false); loadPage(1, false); }
|
||||
function loadNext() { if (!st.hasNext || st.loading) return; loadPage(st.page + 1, true); }
|
||||
|
||||
function switchTab(tab) {
|
||||
if (tab === st.tab) return;
|
||||
st.seq++; // 作废旧 tab 在飞的请求
|
||||
st.tab = tab;
|
||||
var c = st.cache[tab];
|
||||
if (c && Date.now() - c.loadedAt < CACHE_TTL) { // 命中新鲜缓存 → 直接恢复,不转圈
|
||||
st.items = c.items.slice(); st.page = c.page; st.hasNext = c.hasNext; st.loading = false;
|
||||
renderList(st.items, false);
|
||||
if (global.HomeFeedStates) HomeFeedStates(st.items.length ? 'ok' : 'empty');
|
||||
} else { loadFirst(); }
|
||||
}
|
||||
function selectTabWithoutLoad(tab) { // 未授权:只切状态,不打接口
|
||||
if (tab === st.tab) return;
|
||||
st.seq++; st.tab = tab; st.page = 0; st.hasNext = true; st.items = []; renderList([], false);
|
||||
}
|
||||
|
||||
// 坐标变化(rec/distance 依赖坐标)→ 清全部缓存并重拉当前 tab
|
||||
function refreshIfCoordsChanged() {
|
||||
var c = getCoords();
|
||||
if (st.coords && (st.coords.longitude !== c.longitude || st.coords.latitude !== c.latitude)) {
|
||||
st.cache = {};
|
||||
if (locationGranted()) loadFirst();
|
||||
}
|
||||
}
|
||||
|
||||
function bindScroll() { // 触底翻页
|
||||
var sc = $scroll(); if (!sc || sc._feedBound) return; sc._feedBound = true;
|
||||
sc.addEventListener('scroll', function () {
|
||||
if (sc.scrollTop + sc.clientHeight >= sc.scrollHeight - SCROLL_THRESHOLD) loadNext();
|
||||
});
|
||||
}
|
||||
|
||||
global.HomeFeed = {
|
||||
loadFirst: loadFirst, switchTab: switchTab, selectTabWithoutLoad: selectTabWithoutLoad,
|
||||
refreshIfCoordsChanged: refreshIfCoordsChanged, bindScroll: bindScroll,
|
||||
locationGranted: locationGranted, state: st,
|
||||
};
|
||||
document.addEventListener('DOMContentLoaded', bindScroll);
|
||||
})(window);
|
||||
</script>
|
||||
<script>
|
||||
/* H2b feed 三态 + 抢:loading 首次转圈 / empty 复用「附近暂无…去比价」/ degraded 服务繁忙;
|
||||
抢 → referral-link([1,3]) → openMeituan(deeplink||h5)。与 H2a applyLocationState 协作切态。 */
|
||||
(function (global) {
|
||||
'use strict';
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function hide(n) { if (n) n.style.display = 'none'; }
|
||||
|
||||
// feed 子态机:loading/degraded 本函数管;ok/empty 交回 applyLocationState 按卡片数切 feed/附近暂无空态。
|
||||
global.HomeFeedStates = function (kind) {
|
||||
var feed = document.querySelector('#home .home-feed');
|
||||
var loading = el('homeFeedLoading');
|
||||
var degraded = el('homeFeedDegraded');
|
||||
var noDeal = el('homeFeedNoDealEmpty');
|
||||
var hasItems = !!(global.HomeFeed && HomeFeed.state.items.length);
|
||||
if (kind === 'loading' && !hasItems) { // 首次加载中:转圈,压住空态/feed
|
||||
if (loading) loading.style.display = 'flex';
|
||||
hide(degraded); hide(noDeal); if (feed) feed.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
if (kind === 'degraded' && !hasItems) { // 上游失败且无数据:服务繁忙
|
||||
if (degraded) degraded.style.display = 'flex';
|
||||
hide(loading); hide(noDeal); if (feed) feed.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
// ok / empty / loadingMore / 有数据时的 degraded:收起 loading+degraded,交 H2a 按卡片数定 feed/附近暂无
|
||||
hide(loading); hide(degraded);
|
||||
try { applyLocationState(); } catch (e) {}
|
||||
};
|
||||
|
||||
// 抢:换链(linkTypeList=[1,3])→ deeplink 优先, 退 h5 → SGBridge.openMeituan(事件委托,适配动态卡)
|
||||
document.addEventListener('click', function (e) {
|
||||
var grab = e.target.closest('#home .feed-card .feed-grab');
|
||||
if (!grab) return;
|
||||
var card = grab.closest('.feed-card');
|
||||
if (!card || card._grabbing) return;
|
||||
card._grabbing = true;
|
||||
var prev = grab.textContent; grab.textContent = '…';
|
||||
fetch('/api/v1/meituan/referral-link', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
product_view_sign: card.dataset.sign,
|
||||
platform: Number(card.dataset.platform) || 1,
|
||||
biz_line: card.dataset.bizLine != null ? Number(card.dataset.bizLine) : null,
|
||||
link_type_list: [1, 3],
|
||||
}),
|
||||
}).then(function (r) { return r.ok ? r.json() : null; }).then(function (d) {
|
||||
var map = (d && d.link_map) || {};
|
||||
var uri = map['3'] || map['1'] || (d && d.link) || '';
|
||||
if (uri && global.SGBridge && SGBridge.openMeituan) SGBridge.openMeituan(uri);
|
||||
else if (global.SGBridge && SGBridge.toast) SGBridge.toast('暂时无法跳转,请稍后再试');
|
||||
else console.log('[抢] openMeituan →', uri);
|
||||
}).catch(function () {
|
||||
if (global.SGBridge && SGBridge.toast) SGBridge.toast('暂时无法跳转,请稍后再试');
|
||||
}).then(function () { grab.textContent = prev; card._grabbing = false; });
|
||||
});
|
||||
})(window);
|
||||
</script>
|
||||
<script>
|
||||
/* H1→收尾:导航类 go() 已接 SGBridge.navigate(原生壳跳 教程/邀请/比价结果);去领取/去点餐/去授权仍占位(H3/H4)。
|
||||
占位 = 轻量 toast + console,不触发原型多屏切换(其它 .screen 已删,原型切屏会 JS 报错)。
|
||||
仅真实点击才弹 toast:pointerdown 记最近手势,过滤加载时的自动调用(如领券提醒 setTimeout(onTriClaimClick))。 */
|
||||
(function(){
|
||||
var lastGesture = 0;
|
||||
document.addEventListener('pointerdown', function(){ lastGesture = Date.now(); }, true);
|
||||
var hideTimer;
|
||||
function placeholderToast(){
|
||||
var el = document.getElementById('h1PlaceholderToast');
|
||||
if (!el){
|
||||
el = document.createElement('div');
|
||||
el.id = 'h1PlaceholderToast';
|
||||
el.style.cssText = 'position:fixed;left:50%;bottom:96px;transform:translateX(-50%);max-width:78%;'
|
||||
+ 'background:rgba(0,0,0,.8);color:#fff;font-size:13px;font-weight:500;line-height:1.4;text-align:center;'
|
||||
+ 'padding:9px 16px;border-radius:10px;z-index:9999;opacity:0;transition:opacity .18s;pointer-events:none;';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
el.textContent = '功能开发中,敬请期待';
|
||||
el.style.opacity = '1';
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = setTimeout(function(){ el.style.opacity = '0'; }, 1400);
|
||||
}
|
||||
function placeholder(label){
|
||||
return function(){
|
||||
try { console.log('[H1 占位] ' + label + ' — H2-H4 接桥'); } catch(e){}
|
||||
if (Date.now() - lastGesture < 1200) placeholderToast(); // 仅手势触发才弹,滤掉加载时自动调用
|
||||
};
|
||||
}
|
||||
// 导航类按钮 → SGBridge.navigate(原生壳跳真实页);其余目标本轮未接,维持占位
|
||||
var NAV_ROUTES = { guideVideo: 'guideVideo', invite: 'invite', compresult: 'compareResult' };
|
||||
window.go = function(target){
|
||||
var route = NAV_ROUTES[target];
|
||||
if (route && window.SGBridge && typeof SGBridge.navigate === 'function') { SGBridge.navigate(route); return; }
|
||||
try { console.log('[H1 占位] go(' + (target == null ? '' : target) + ') — 未接线/无桥,占位'); } catch(e){}
|
||||
if (Date.now() - lastGesture < 1200) placeholderToast();
|
||||
};
|
||||
// onTriClaimClick(去领取)已接真实领券(见上方定义),从占位移除
|
||||
// onHomeCompareClick(去点餐)已接真实 picker + launchApp(见上方定义),从占位移除;去领取/去授权仍占位
|
||||
// goAuthLocation(去授权)已接真·定位权限(见上方定义),从占位移除;去领取仍占位
|
||||
})();
|
||||
|
||||
/* H1:三数据卡 + 跑马灯接同源 /api/v1/platform/*(均不鉴权;失败回落原型 mock) */
|
||||
(function(){
|
||||
|
||||
@@ -56,3 +56,17 @@ def test_h5_bridge_has_get_location(client) -> None:
|
||||
body = resp.text
|
||||
assert "getLocation" in body
|
||||
assert "116.404" in body and "39.928" in body # 浏览器 mock 北京兜底
|
||||
|
||||
|
||||
def test_h5_home_feed_wired(client) -> None:
|
||||
# H2b: 首页 feed 接真实美团端点 + 抢换链;mock 静态卡已移除
|
||||
resp = client.get("/h5/home/index.html")
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
assert "/api/v1/meituan/feed" in body # rec/distance
|
||||
assert "/api/v1/meituan/top-sales" in body # 销量最高
|
||||
assert "/api/v1/meituan/referral-link" in body # 抢
|
||||
assert "HomeFeed" in body # feed 模块已挂
|
||||
assert "茶百道" not in body # 写死的 mock 卡(店名)已移除
|
||||
assert "chabaidao.png" not in body # 写死的 mock 头图已移除
|
||||
assert "比团购省 5.5 元" not in body # 随原生:mock 卡的「比团购省」标签已删(渲染只用 price_label/rank_label)
|
||||
|
||||
Reference in New Issue
Block a user