Compare commits

...

23 Commits

Author SHA1 Message Date
guke de707ec0a9 Merge branch 'feat/h5-sgbridge' of https://gitea.shaguabijia.com/WonderableAI/shaguabijia-app-server into feat/h5-sgbridge-gk 2026-06-30 11:11:44 +08:00
guke ddfe955e9c fix(h5): 补领券看广告金币到账 toast 的金币堆图标(welfare 漏拷致 404)
showCouponAdRewardToast(home/index.html:8841,领券奖励已到账 toast)引用 assets/welfare/index/checkin-coins-600.png,同属抽取时漏本地化的 welfare 子树。从原型 checkin-popup/reward-coins-600.png 补回该图。test: 断言 200。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:09:26 +08:00
guke 98cd915e2c fix(h5): 补「去领取」弹窗右上角金币图标(welfare 资源漏本地化致 404)
home/index.html 引用 assets/welfare/index/coin-icon.png,但 h5/home/assets/ 整个 welfare 子树在抽取时漏拷,弹窗右上角金币 404 不显示。从原型资源补回该图标。test: 断言 /h5/home/assets/welfare/index/coin-icon.png 返 200。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:05:42 +08:00
guke bf35a45793 chore(h5): 清空「上次比价」横幅静态 mock 占位文案(窑鸡王/¥57.9/淘宝)
横幅默认 display:none + 真实数据覆盖,占位本不显示;清空源码里的 mock 串,留结构与标签(最低价/去查看)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 10:43:09 +08:00
guke 452d7fef8f feat(h5): 首页「上次比价」横幅接真实 /api/v1/compare/records(4 分钟新鲜窗口)
删写死 mock(DEFAULT_LAST_COMPARE_RECORD + localStorage 读),改带 Bearer token 拉真实比价记录;取最近一条 status=success 且 created_at 在 4 分钟内才显示(对齐原生 HomeViewModel.loadLastCompare),否则/未登录/401 隐藏横幅;进页 + 回前台 visibilitychange 各拉一次;点击仍走 SGBridge.navigate('compareResult')。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 10:39:18 +08:00
guke 6682f9d0d4 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>
2026-06-30 10:18:45 +08:00
guke 674b8c5589 feat(h5): SGBridge 定位桥 getLocation()(首页 feed 取经纬度;无桥 mock 北京)
含 H2a 定位授权桥方法 isLocationGranted/requestLocationPermission(原 WIP 一并提交)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 09:54:38 +08:00
陈世睿 4d730169bf 真机验收修复:我的页去框沉浸式 + 三页字体加粗 + 卡片细节
我的页(h5/mine)整页去框满屏 + 顶部状态栏沉浸式(云朵铺到状态栏下/隐藏假状态栏/内容 safe-area/头像对齐原型);三页统一注入 faux-bold(安卓 WebView 中文回退字体不合成粗体兜底);上报最低价选记录态列表间距去框对齐(top 198→154);上报记录卡平台改主名+主logo(美团/京东/淘宝)+ 菜品名 2 行截断

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 09:52:07 +08:00
陈世睿 cdfe66dcee 接入上报记录列表 H5(第三个 session 的孤儿页,补提交整合)
h5/reports/index.html(审核中/已通过/未通过三态+计数,真接 /api/v1/report/records)+ logos;android REPORT_RECORDS 路由稍后接 H5

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:13:21 +08:00
陈世睿 8d08986c00 合并:上报最低价 H5 2026-06-29 17:12:53 +08:00
陈世睿 be77ffa721 我的页迁远程 H5
h5/mine/index.html改用SGBridge.getAuthState/navigate(同源相对API);拷入页面静态资源h5/mine/assets/

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:12:08 +08:00
陈世睿 0bed761c00 上报最低价流程对齐原型/原生 + 接桥
toast改感谢您的反馈/D校验文案/提交失败解析后端detail/模拟键盘补小数点后2位上限/取消改closePage真返回/上报记录入口接SGBridge.navigate(reportRecords)/头部未读红点默认隐藏

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:12:00 +08:00
guke 7fd84ce48f fix(h5): 首页锁外层滚动+关 overscroll 回弹(修可下拽致顶部下移)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:47:51 +08:00
guke 8fd2c5ee96 fix(h5): 隐藏首页 H5 原型自带底栏(底栏归原生壳,修双底栏)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:38:44 +08:00
guke 9ab4dfc6a4 test(h5): 首页托管 + 素材冒烟(6 passed)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:23:07 +08:00
guke 9484768321 feat(h5): 首页跑马灯/三数据卡接同源 /api/v1/platform/*(原地更新,mock 兜底)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:22:07 +08:00
guke 4be9848235 feat(h5): 首页 H5 壳从定稿原型抽取(#home 满屏,静态 mock,零桥)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:19:45 +08:00
guke 109aebaa16 chore(scripts): records 页比价记录 mock 数据脚本(真机验收用,幂等)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:33:35 +08:00
guke 84ae950ea7 feat(h5): records 页迁入 server 改用 SGBridge(同源相对 API,保留 multipart 上传)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:14:18 +08:00
guke 122766c911 feat(h5): SGBridge 新增 closePage(返回上一页)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:05:33 +08:00
guke cee4f3e0a7 fix(h5): no-cache 按响应 content-type 判定,覆盖目录式 URL(/h5/x/)
html=True 时 /h5/x/ 经 Starlette 回落 index.html,但 path 仍为 x/ 不带 .html,旧判定漏设 no-cache。改用响应 content-type=text/html 判定,并加目录式 URL 回归测试。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:03:36 +08:00
guke 4fb8f6447c feat(h5): FastAPI StaticFiles 同源托管 /h5(.html no-cache)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 12:51:42 +08:00
guke 46c650ecb8 docs(h5): 新增 H5/WebView 改造方案(方案1 SGBridge,服务端托管)
由 app-server 同源托管业务 H5(StaticFiles 挂 /h5);records/reports 从安卓迁入改用 SGBridge/SGApi;bridge.js 补 closePage;后端业务接口零改动;含远程开关、缓存热更、M1-M4 里程碑。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 12:14:15 +08:00
124 changed files with 13528 additions and 74 deletions
+19
View File
@@ -142,3 +142,22 @@ app.mount(
StaticFiles(directory=str(_media_root)),
name="media",
)
# 业务 H5 同源托管(/h5)。与 /api/v1 同 host → H5 用相对路径调后端,免 CORS。
# .html 加 no-cache:后端改完用户重开页即拉新(远程热更核心);js/图片走默认缓存。
class _NoCacheHTMLStaticFiles(StaticFiles):
async def get_response(self, path: str, scope):
resp = await super().get_response(path, scope)
# 按响应 content-type 判定,而非请求 path:html=True 时目录式 URL(/h5/x/)
# 经 Starlette 内部回落到 index.html,此刻 path 仍是 "x/"(不带 .html)。
if resp.headers.get("content-type", "").startswith("text/html"):
resp.headers["Cache-Control"] = "no-cache"
return resp
_h5_root = Path(__file__).resolve().parent.parent / "h5"
app.mount(
"/h5",
_NoCacheHTMLStaticFiles(directory=str(_h5_root), html=True),
name="h5",
)
+154
View File
@@ -0,0 +1,154 @@
# H5 / WebView 改造方案(app-server 端)
> 仓库:`shaguabijia-app-server` 分支:`feat/h5-sgbridge` 日期:2026-06-29
> 配套文档:`shaguabijia-app-android` 仓库 `docs/H5-WebView改造方案.md`(原生壳 + SGBridge.kt)
> 本方案 = **方案1:以本仓已有 `SGBridge` 为统一基准**,安卓退役其 `NativeBridge`。
---
## 1. 背景与现状
项目里同时存在**两套互不兼容**的 H5 方案,尚未收敛:
| | 本仓那套(已在 `main`,PR #89) | 安卓那套(`feat/records-h5`,未进 main) |
|---|---|---|
| 桥 | `SGBridge`(JS)↔ `SGBridgeNative`(原生),协议丰富 + 事件总线 | `NativeBridge`,仅 4 法 |
| 托管 | **设计为**后端同源托管(相对 `/api/v1`、免 CORS) | 本地 assets `file://`(跨域 + 放行 CORS) |
| 页面 | `h5/mine/index.html`(7771)+ `h5/shared/bridge.js`+`api.js` | `records`(1778)、`reports`(603) |
| 落地 | **半成品**:安卓从无 `SGBridge.kt`;且 `h5/` 实际没挂(StaticFiles 只挂了 `data/media` 头像上传) | 已接进安卓导航但只在分支上 |
**结论(已对齐):** 目标是「远程后端托管 + 四 tab 全 H5」,该方向天然契合本仓 SGBridge(同源相对路径、`mine` 已成型、`shared/` 已就绪)。故统一到 SGBridge,把安卓的 records/reports 页**迁入本仓** `h5/`
---
## 2. 目标与范围
**目标:** 由 app-server 同源托管全部业务 H5,配齐 `shared/` 桥与接口封装,支持安卓「原生壳 + 远程 H5」一套桥贯通,可不发版热更。
**本仓(app-server)范围:**
- H5 托管:新增 **FastAPI StaticFiles 挂载 `/h5`** → 指向仓库 `h5/` 目录(同源、免 CORS)
- 迁入 records / reports 两页(从安卓 `feat/records-h5` 移植),改用 `SGBridge` / `SGApi`
- `shared/bridge.js`**`closePage`(返回)**;`shared/api.js` 已同源就绪(必要时补 multipart 说明)
- 远程开关配置接口(返回 per-page `native`/`h5` 标志)
- 缓存 / 热更策略(cache-control + 版本)
**不在本仓(见 android 文档):** `SGBridge.kt`、WebView 宿主、原生/H5 切换接线。
**后端业务接口:零改动** —— H5 复用现有 `/api/v1/...`(`/compare/records``/report``/wallet/account``/coupon/stats` 等),只是请求方从 Kotlin 改成网页 fetch。
---
## 3. 关键决策(已锁定)
| # | 决策 | 选定 | 理由 |
|---|---|---|---|
| D1 | 桥基准 | **SGBridge**(安卓退役 NativeBridge) | 协议齐、mine 成型、三端对齐 |
| D2 | 托管 | **FastAPI StaticFiles 挂 `/h5`** | 同源免 CORS、复用现有 `/media` 方式、nginx 已反代 `/`→uvicorn,dev+prod 都能跑 |
| D3 | 离线兜底 | 纯远程 + 安卓侧重试页 | YAGNI;后端不需为离线做特殊处理 |
| D4 | 地址 | 安卓用 `BackendUrl.current` 推导 H5 地址(同源) | H5 与 `/api/v1` 同 host,自动跟随环境 |
| D5 | 迁移顺序 | records → reports → 首页/福利;mine 最后校准 | 用小页验证桥,风险分散 |
---
## 4. H5 托管(D2)
`app/main.py` 现有挂载(仅头像):
```python
app.mount(settings.MEDIA_URL_PREFIX, StaticFiles(directory=str(_media_root)), name="media")
```
**新增** H5 静态挂载(指向仓库根 `h5/`,`html=True` 让目录路径回落到 `index.html`):
```python
_h5_root = Path(__file__).resolve().parent.parent / "h5" # 仓库根 h5/
app.mount("/h5", StaticFiles(directory=str(_h5_root), html=True), name="h5")
```
- 访问:`https://app-api.shaguabijia.com/h5/records/index.html`(prod)/ `http://<dev-host>:8770/h5/...`(debug)。
-`/api/v1` **同 host → 同源**:H5 内 `SGApi` 用相对 `/api/v1`,免 CORS、免 `getApiBase`
- prod 可选改由 nginx 直发 `h5/`(绕过 uvicorn)以提性能——本期先用 StaticFiles,nginx 优化留后。
---
## 5. H5 工程结构
```
h5/
├── shared/
│ ├── bridge.js # SGBridge:H5↔原生桥(本期补 closePage)
│ └── api.js # SGApi:同源相对 /api/v1 封装(已就绪)
├── mine/index.html # 我的页(已成型,M4 按真实 SGBridge.kt 校准)
├── records/index.html # ← 从安卓迁入(M1)
└── reports/index.html # ← 从安卓迁入(M2)
```
各页头部引入:`<script src="../shared/bridge.js"></script>` + `<script src="../shared/api.js"></script>`
---
## 6. records / reports 页迁入(从安卓移植)
**搬运 + 改桥(迁移、非重写,UI/CSS/逻辑占绝大部分原样保留):**
| 安卓原写法(NativeBridge) | 迁入后(SGBridge / SGApi) |
|---|---|
| `NativeBridge.getApiBase()` 拼绝对 URL(8 处) | **删除**,改相对 `/api/v1/...`(同源) |
| `NativeBridge.getToken()`(7 处) | `SGBridge.getToken()`(同名) |
| `NativeBridge.onUnauthorized()`(6 处) | 走 `SGApi` 内置 401 → `SGBridge.requestLogin()` |
| `NativeBridge.closePage()`(5 处) | `SGBridge.closePage()`(**bridge.js 本期新增**) |
| 普通 GET/POST | 改用 `SGApi.get/post('/...')`(自动带 token、处理 401) |
**上报传图(records)特别说明:**`<input type="file">` + 客户端压缩 + **`FormData` multipart** POST `/api/v1/report``SGApi` 只发 JSON,**不覆盖 multipart**,故这段**保留自定义 `fetch`**:URL 改相对 `/api/v1/report``Authorization` 头取 `SGBridge.getToken()`。文件选择依赖**安卓宿主 `onShowFileChooser`**(见 android 文档),后端无需改动。
---
## 7. `shared/bridge.js` 改动
- **新增 `closePage()`**:`SGBridge.closePage()` → 调 `SGBridgeNative.closePage()`(无桥时 mock `console.log`)。records/reports 顶栏返回箭头、`?from=profile` 返回都用它。
- 其余方法(`getToken`/`requestLogin`/`navigate`/`getAuthState`/事件总线…)已就绪,保持签名,供 mine 及后续 tab 使用。
- 浏览器无 `SGBridgeNative` 时走 MOCK,便于本地 `python -m http.server` 或 StaticFiles 直接调样式。
---
## 8. SGBridge 接口契约(三端共享)
> 与 android 文档同表;`shared/bridge.js` 为权威。新增方法三端同步。
**查询类(同步返回 String):** `getAuthState` / `getToken` ✓ / `getDeviceId` / `getAppVersion` / `getInstalledApps` / `getCouponClaimedToday`
**动作类(无返回):** `navigate` / `requestLogin` ✓ / `toast` / `openMeituan` / `startCompare` / `startCouponClaim` / `launchApp` / **`closePage`【新增】✓**
**事件类(原生→H5,`_emit`):** `onAuthChange` / `onBalanceChange` / `onSigninChange` / `onResume`
(✓ = records 阶段 M1 最小子集)
---
## 9. 远程开关配置接口
- 提供一个轻量配置:返回各页渲染方式 `{ records: "h5"|"native", reports: ..., home: ..., welfare: ..., mine: ... }`
- 安卓 `AppNavHost` 据此决定加载 H5 还是原生页,**不发版即可灰度 / 一键回退**。
- 复用现有远程配置/开关基建(参考安卓 `ad-config-remote-delivery``comparing-ad-remote-flag` 的下发方式),无则新增一个 `GET /api/v1/app/ui-flags` 之类端点。
---
## 10. 缓存与热更
- H5 是后端托管 → **改完即生效**(用户重开页即拉新),这是远程方案的核心收益。
- `index.html``Cache-Control: no-cache`(或短 TTL)保证及时性;`shared/*.js`、图片等带版本/指纹便于长缓存。StaticFiles 可配响应头或交 nginx 处理。
---
## 11. 迁移顺序与风险
| 里程碑 | server 侧动作 |
|---|---|
| **M1 records** | 挂 `/h5`;`bridge.js``closePage`;records 迁入 + 改桥;配 `index.html` no-cache |
| **M2 reports** | reports 迁入 + 改桥 |
| **M3 首页/福利** | 首页/福利页 H5 化(需 SGBridge P2+ 协议,配合安卓) |
| **M4 mine + 收尾** | mine 按真实 SGBridge.kt 校准;远程开关接口上线 |
| 风险 | 缓解 |
|---|---|
| SGBridge 原生侧未验证 | M1 用 records 小页验证(详见 android 文档) |
| multipart 上传被误套进 SGApi | 文档明确:上报保留自定义 multipart fetch,不走 SGApi |
| 同源被破坏(H5 与 API 不同 host) | 坚持 StaticFiles 同源托管;若改独立前端域名,需回评 SGApi 相对路径策略 |
| 缓存导致改了不生效 | `index.html` no-cache + 资源指纹 |
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

+43
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+10397
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg width="1254" height="1254" viewBox="0 0 1254 1254" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="shadow" x="250" y="170" width="754" height="914" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feDropShadow dx="0" dy="34" stdDeviation="40" flood-color="#D56A00" flood-opacity="0.22"/>
<feDropShadow dx="0" dy="8" stdDeviation="14" flood-color="#8A3C00" flood-opacity="0.10"/>
</filter>
<linearGradient id="body" x1="372" y1="232" x2="866" y2="994" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFE780"/>
<stop offset="0.52" stop-color="#FFC12F"/>
<stop offset="1" stop-color="#FF9A1A"/>
</linearGradient>
<radialGradient id="shine" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(470 332) rotate(50) scale(254 150)">
<stop stop-color="white" stop-opacity="0.7"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
</defs>
<g filter="url(#shadow)">
<rect x="336" y="230" width="578" height="784" rx="168" fill="url(#body)"/>
<rect x="336" y="230" width="578" height="784" rx="168" fill="url(#shine)"/>
<path d="M468 424H786" stroke="white" stroke-width="72" stroke-linecap="round" opacity="0.94"/>
<path d="M468 586H786" stroke="white" stroke-width="58" stroke-linecap="round" opacity="0.72"/>
<path d="M468 730H704" stroke="white" stroke-width="58" stroke-linecap="round" opacity="0.58"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Some files were not shown because too many files have changed in this diff Show More