feat(h5): 首页 H5 壳从定稿原型抽取(#home 满屏,静态 mock,零桥) (#96)
首页webview改造合并 --------- Co-authored-by: guke <guke@autohome.com.cn> Reviewed-on: #96
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 664 KiB |
|
After Width: | Height: | Size: 614 KiB |
|
After Width: | Height: | Size: 773 KiB |
|
After Width: | Height: | Size: 695 KiB |
|
After Width: | Height: | Size: 732 KiB |
|
After Width: | Height: | Size: 778 KiB |
|
After Width: | Height: | Size: 960 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 975 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 570 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 89 KiB |
@@ -0,0 +1,34 @@
|
||||
<svg width="172" height="148" viewBox="0 0 172 148" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<filter id="buttonShadow" x="0" y="0" width="172" height="148" color-interpolation-filters="sRGB">
|
||||
<feDropShadow dx="0" dy="10" stdDeviation="7" flood-color="#D79A1E" flood-opacity=".22"/>
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#FFFFFF" flood-opacity=".88"/>
|
||||
</filter>
|
||||
<linearGradient id="buttonFill" x1="43" y1="16" x2="126" y2="135" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#FFFCE3"/>
|
||||
<stop offset=".38" stop-color="#FFF2A8"/>
|
||||
<stop offset=".72" stop-color="#FFE27A"/>
|
||||
<stop offset="1" stop-color="#FFD15A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="buttonStroke" x1="35" y1="11" x2="136" y2="139" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#FFF7C2"/>
|
||||
<stop offset=".48" stop-color="#FFE484"/>
|
||||
<stop offset="1" stop-color="#EAB237"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="buttonGlow" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(70 44) rotate(62) scale(86 66)">
|
||||
<stop stop-color="#FFFFFF" stop-opacity=".86"/>
|
||||
<stop offset=".58" stop-color="#FFFFFF" stop-opacity=".24"/>
|
||||
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="arrowStroke" x1="50" y1="49" x2="125" y2="98" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#FFAE16"/>
|
||||
<stop offset="1" stop-color="#F08300"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g filter="url(#buttonShadow)">
|
||||
<path d="M45 13H127C151.8 13 166 35.8 166 62V86C166 112.2 151.8 135 127 135H45C20.2 135 6 112.2 6 86V62C6 35.8 20.2 13 45 13Z" fill="url(#buttonFill)" stroke="url(#buttonStroke)" stroke-width="3"/>
|
||||
<path d="M48 19H124C146.1 19 159 38.7 159 63V84C159 108.3 146.1 128 124 128H48C25.9 128 13 108.3 13 84V63C13 38.7 25.9 19 48 19Z" fill="url(#buttonGlow)"/>
|
||||
<path d="M58 51L81 74L58 97" stroke="url(#arrowStroke)" stroke-width="15" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M93 51L116 74L93 97" stroke="url(#arrowStroke)" stroke-width="15" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 621 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 917 KiB |
|
After Width: | Height: | Size: 654 KiB |
|
After Width: | Height: | Size: 564 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 899 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 899 KiB |
|
After Width: | Height: | Size: 482 KiB |
@@ -0,0 +1,43 @@
|
||||
# Shared Icons
|
||||
|
||||
通用图标的单一事实源(canonical source)。每个图标都是独立 SVG 文件,但**实际页面里必须内联 SVG markup**(不用 `<img src>` 或 `<svg src>` 引用),符合 prototypes 仓库"每个 HTML 自包含"原则。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 改图标 → 改这里的 SVG 文件
|
||||
2. 在所有用到的 HTML 里同步替换内联 SVG markup
|
||||
3. DESIGN.md 引用本目录路径作为基准
|
||||
|
||||
## 现有图标
|
||||
|
||||
| 文件 | 用途 | 推荐内联尺寸 |
|
||||
|---|---|---|
|
||||
| `back-chevron.svg` | 顶部导航返回按钮 | 24×24 |
|
||||
| `tab-home-outline.svg` | 底 tab "首页" 未选中态 | 24×24 |
|
||||
| `tab-home-filled.svg` | 底 tab "首页" 选中态(黄填充)| 24×24 |
|
||||
| `tab-welfare-outline.svg` | 底 tab "福利" 未选中态 | 24×24 |
|
||||
| `tab-welfare-filled.svg` | 底 tab "福利" 选中态(黄填充)| 24×24 |
|
||||
| `tab-profile-outline.svg` | 底 tab "我的" 未选中态 | 24×24 |
|
||||
| `tab-profile-filled.svg` | 底 tab "我的" 选中态(黄填充)| 24×24 |
|
||||
|
||||
## 标准 button 包装
|
||||
|
||||
```html
|
||||
<button class="btn-back" onclick="goBack()" aria-label="返回">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 6L9 12L15 18" stroke="#1A1A1A" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
```
|
||||
|
||||
```css
|
||||
.btn-back {
|
||||
width: 44px; height: 44px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: transparent; border: none; padding: 0;
|
||||
cursor: pointer; -webkit-tap-highlight-color: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.btn-back:active { opacity: 0.5; }
|
||||
.btn-back svg { width: 24px; height: 24px; display: block; }
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 6L9 12L15 18" stroke="#1A1A1A" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 12l9-8 9 8" stroke="#1A1A1A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 10v9a1 1 0 001 1h3v-5h6v5h3a1 1 0 001-1v-9" fill="#FFD600" stroke="#1A1A1A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 368 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#1A1A1A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 12l9-8 9 8"/>
|
||||
<path d="M5 10v9a1 1 0 001 1h3v-5h6v5h3a1 1 0 001-1v-9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 272 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="8" r="4" fill="#FFD600" stroke="#1A1A1A" stroke-width="2"/>
|
||||
<path d="M4 20c0-4 4-6 8-6s8 2 8 6" fill="#FFD600" stroke="#1A1A1A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 321 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#1A1A1A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="8" r="4"/>
|
||||
<path d="M4 20c0-4 4-6 8-6s8 2 8 6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 257 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="8" width="18" height="4" rx="1" fill="#FFD600" stroke="#1A1A1A" stroke-width="2"/>
|
||||
<path d="M5 12v7a1 1 0 001 1h12a1 1 0 001-1v-7" fill="#FFD600" stroke="#1A1A1A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 8v12" stroke="#1A1A1A" stroke-width="2"/>
|
||||
<path d="M7.5 8C6 8 5 6.5 6 5.5S9 4 9.5 5.5c.4 1.2 2.5 2.5 2.5 2.5s2.1-1.3 2.5-2.5C15 4 17 4.5 18 5.5s-1 2.5-2.5 2.5" stroke="#1A1A1A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 611 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#1A1A1A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="8" width="18" height="4" rx="1"/>
|
||||
<path d="M12 8v12"/>
|
||||
<path d="M5 12v7a1 1 0 001 1h12a1 1 0 001-1v-7"/>
|
||||
<path d="M7.5 8C6 8 5 6.5 6 5.5S9 4 9.5 5.5c.4 1.2 2.5 2.5 2.5 2.5s2.1-1.3 2.5-2.5C15 4 17 4.5 18 5.5s-1 2.5-2.5 2.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 627 KiB |
|
After Width: | Height: | Size: 200 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<defs>
|
||||
<clipPath id="appIconClip">
|
||||
<rect x="0" y="0" width="512" height="512" rx="105" ry="105"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="512" height="512" rx="105" ry="105" fill="rgb(255, 217, 61)"/>
|
||||
<image href="sb-brand.png" x="0" y="0" width="512" height="512" preserveAspectRatio="none" clip-path="url(#appIconClip)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 446 B |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 35 KiB |
@@ -124,6 +124,29 @@
|
||||
else console.log('[SGBridge mock] closePage');
|
||||
}
|
||||
|
||||
/** H5「去授权」→ 触发系统定位权限窗(原生侧永久拒绝跳设置)。结果异步,H5 回前台 visibilitychange 自行重查。 */
|
||||
function requestLocationPermission() {
|
||||
if (hasNative && native.requestLocationPermission) native.requestLocationPermission();
|
||||
else console.log('[SGBridge mock] requestLocationPermission');
|
||||
}
|
||||
|
||||
/** 定位权限是否已授予(FINE/COARSE)。H5 据此切「未授权空态 / feed」。浏览器无原生 → mock 返 true(便于预览 feed)。 */
|
||||
function isLocationGranted() {
|
||||
if (hasNative && native.isLocationGranted) return !!native.isLocationGranted();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 取设备当前经纬度 → {longitude, latitude};原生拿不到(未授权/无缓存)返 null,浏览器无原生 mock 北京。
|
||||
* 原生侧同步返宿主缓存的 lastLocation(见 SGBridge.kt getLocation);H5 拿到 null 自行落默认坐标。 */
|
||||
function getLocation() {
|
||||
if (hasNative && native.getLocation) {
|
||||
var o = safeParse(native.getLocation(), null);
|
||||
if (o && typeof o.longitude === 'number' && typeof o.latitude === 'number') return o;
|
||||
return null;
|
||||
}
|
||||
return { longitude: 116.404, latitude: 39.928 };
|
||||
}
|
||||
|
||||
// ====== 原生 → H5 事件总线 ======
|
||||
|
||||
var listeners = {}; // event → [fn]
|
||||
@@ -153,6 +176,8 @@
|
||||
getAppVersion: getAppVersion,
|
||||
getInstalledApps: getInstalledApps,
|
||||
getCouponClaimedToday: getCouponClaimedToday,
|
||||
isLocationGranted: isLocationGranted,
|
||||
getLocation: getLocation,
|
||||
// 动作
|
||||
navigate: navigate,
|
||||
requestLogin: requestLogin,
|
||||
@@ -162,6 +187,7 @@
|
||||
startCouponClaim: startCouponClaim,
|
||||
launchApp: launchApp,
|
||||
closePage: closePage,
|
||||
requestLocationPermission: requestLocationPermission,
|
||||
// 事件
|
||||
on: on,
|
||||
_emit: _emit,
|
||||
|
||||
@@ -30,3 +30,72 @@ def test_h5_records_served_and_migrated(client) -> None:
|
||||
assert "NativeBridge" not in body # 旧桥名清零
|
||||
assert "getApiBase" not in body # 同源:绝对地址拼接已移除
|
||||
assert "../shared/bridge.js" in body
|
||||
|
||||
|
||||
def test_h5_home_served(client) -> None:
|
||||
resp = client.get("/h5/home/index.html")
|
||||
assert resp.status_code == 200
|
||||
assert resp.headers.get("cache-control") == "no-cache"
|
||||
body = resp.text
|
||||
assert 'id="home"' in body
|
||||
assert "data-cc-id" not in body # 原型编辑器留痕已剥
|
||||
assert "../../assets/" not in body # 素材路径已本地化
|
||||
assert "/api/v1/platform/stats" in body # 已接同源数据
|
||||
assert body.count('class="screen') == 1 # 只剩首页一屏
|
||||
|
||||
|
||||
def test_h5_home_asset_served(client) -> None:
|
||||
resp = client.get("/h5/home/assets/shared/logos/sb-brand.png")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_h5_bridge_has_get_location(client) -> None:
|
||||
# H2b: 首页 feed 需经纬度, bridge 暴露同步 getLocation()(无桥 mock 北京)
|
||||
resp = client.get("/h5/shared/bridge.js")
|
||||
assert resp.status_code == 200
|
||||
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)
|
||||
|
||||
|
||||
def test_h5_home_last_compare_wired(client) -> None:
|
||||
# 「上次比价」横幅改接真实 /api/v1/compare/records + 4 分钟新鲜窗口;原写死 mock 默认值已移除
|
||||
resp = client.get("/h5/home/index.html")
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
assert "/api/v1/compare/records" in body # 接真实比价记录端点
|
||||
assert "pickFreshLastCompare" in body # 4 分钟新鲜窗口逻辑
|
||||
assert "DEFAULT_LAST_COMPARE_RECORD" not in body # 写死 mock 默认已移除
|
||||
assert 'id="lastCompareShop">窑鸡王' not in body # 横幅店名静态 mock 占位已清空
|
||||
|
||||
|
||||
def test_compare_records_requires_auth(client) -> None:
|
||||
# 横幅靠 Bearer token 才拿得到数据;无鉴权必须 401(无桥/未登录时横幅隐藏)
|
||||
resp = client.get("/api/v1/compare/records?limit=5")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_h5_home_coupon_popup_coin_icon_served(client) -> None:
|
||||
# 「去领取」弹窗右上角金币图标:welfare 资源子树曾漏本地化 → 404 不显示;补齐后须 200
|
||||
resp = client.get("/h5/home/assets/welfare/index/coin-icon.png")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_h5_home_coupon_ad_reward_coins_served(client) -> None:
|
||||
# 领券「看广告赚金币」到账 toast(showCouponAdRewardToast)的金币堆图标:同属漏拷的 welfare 子树,补齐后须 200
|
||||
resp = client.get("/h5/home/assets/welfare/index/checkin-coins-600.png")
|
||||
assert resp.status_code == 200
|
||||
|
||||