feat: E1002 顶部时钟改 F84+用量行 12px 三档配色、标签右对齐
- 顶部时钟 F48→F84 占满 A 段,星期/日期移到时钟右侧并与基线对齐 - 用量行:标签常规字重右对齐贴进度条,百分比与 pace 数字 12px 粗体 - 配色:进度条/边框/百分比同色,用量与 pace 各自三档绿/黑/红、独立取色 (曾按要求加过黄档,同日被否并移除) - 用量块标题右侧「快照」改为「上次更新」,与今日实时统一 - 今日实时标题与指标名间距加大 4px - 基类抽出 PCT_TRACK / LABEL_BOLD / LABEL_ALIGN / FPACE 供宽版覆写, 400×300 小屏输出逐像素不变 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ PUSH_STATE_PATH=
|
||||
PUSH_URL=
|
||||
# 时区(服务端可能在境外 VPS,所有时间按此时区)
|
||||
TZ_NAME=Asia/Shanghai
|
||||
# 用量三档阈值(宽版)与 BTC K 线
|
||||
# 用量/pace 三档阈值(宽版,颜色顺序 绿→黑→红,两者独立取色)与 BTC K 线
|
||||
USAGE_WARN=80
|
||||
USAGE_OK=30
|
||||
PACE_TOL=10
|
||||
|
||||
@@ -10,7 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
> **分支 `feature/colorful`**:新设备在黑白外支持红色,本分支把渲染升级为黑/白/红三色。当前版式为**上中下三段**:A 大时钟+日期 / 天气(定位南京·江宁);B **Claude Usage** 主角(5h/7d 用量进度条 + 时间维度对比 pace,用量吃紧转红预警);C 橘喵今日实时三列。红作**语义强调**(用量预警、涨、定位、天气太阳/闪电),仍**无真灰阶、不抖动**,全图只三种纯色。近30天趋势图当前**隐藏**(`renderer._trend_chart` 及依赖保留、未调用,随时可挂回)。
|
||||
|
||||
> **分支 `feature/reterminal-e1002`**(基于 `feature/colorful`):新增第二个目标设备 **Seeed reTerminal E1002**(7.3 寸 **800×480**,E Ink **Spectra 6 原生六色**:黑/白/黄/红/绿/蓝,真机已验证六色量化干净)。该设备不接受局域网直推,而是由 **SenseCraft HMI** 云端的 **HTML 控件**按设备刷新间隔(最小 5 分钟)**每周期重新抓取**一个公网 HTTPS URL、无头浏览器截图后量化下发;不支持 ETag/304,内容不变也会整屏重绘(全刷约 17 秒)。**真机验证(2026-09)**:Image 控件只在填写 URL 时抓一次快照、此后不再更新;HTML 控件填同一 URL 则每周期重抓——所以对外只用 HTML 控件。当前主形态是 **`server.py` 动态服务**(部署公网 HTTPS;局域网地址/明文 HTTP 云端抓不到):三条恒定 URL 并行供对比——方案一 `IMAGE_PATH`(默认 `/e1002.png`)每次请求**内存中现渲染 PNG**(Pillow 点阵版);方案二 `HTML_PATH`(默认 `/e1002.html`)每次请求生成 **800×480 现代网页**(`html_renderer.py`,系统/网络字体 + 内联 SVG K 线,无 JS)由平台截图——**真机实拍:文字抗锯齿灰边被平台抖动成毛边、小字与复杂汉字发糊**;方案三 `HTML_PNG_PATH`(默认 `/e1002-web.png`)同一网页由**服务端无头 Chrome 截图后按最近色(不抖动)固化为六色 PNG**(`html_shot.py`),平台拿到纯色图无从抖动,需服务器装 Chrome。**结论(2026-09-10):方案一为主力**——方案二/三的矢量字体在 125 PPI 下小字必然毛糙(抗锯齿灰边无论由平台还是我们二值化都只能变成锯齿/断笔,见 `output/e1002/font_compare.png`:矢量字体单色渲染 ≈ 抗锯齿后二值化,都不如点阵),Canvas 逐像素画也改变不了这点(Pillow 本身就是逐像素画)。方案二/三端点保留供对比,不再投入。宽版**只用点阵字体的整数倍字号 12/24/36/48**(`E1002Renderer.__init__` 把基类的 F13/F16 换成 12/24px),可用 `all(v % 12 == 0 for v in 字体尺寸)` 自查。天气/橘喵/BTC 走**服务端内存缓存 + 有效期**(`WEATHER_TTL`/`MAO_TTL`/`BTC_TTL`,默认 900/60/300 秒,0=每次实拉;请求时过期才实拉并更新时间戳,失败沿用旧值、不更新时间戳),服务**不读本机任何文件**;Claude/ChatGPT 用量**由本机 `main.py --target push` 推送原始快照**(`X-Push-Token` 鉴权),服务端内存暂存、渲染时现算 pace。静态形态 `main.py --target e1002`(`output/e1002/index.html` + `dashboard.png`)仍保留。渲染由 `renderer_e1002.py` 完成:继承 `DashboardRenderer` 组件、**k=1 字号与三色版完全相同(不放大,放大后字太大很傻)**,四段版式:A 时钟/天气;B 左 **Claude Usage 三行(all 5h / all 7d / fable 7d,标签沿用 statusline 写法、小写开头)**、右 **ChatGPT Usage(mock,标「示例数据」)**,紧凑单行样式(曾试过撑满高度的大卡片,被否);C **橘喵·今日实时**五列全宽(探店单量/探店流水/团购单量/团购流水/毛利,2026-09-11 接口新增团购字段后由三列改五列;列内四行逐行居中:名 F12 / 值 F24 粗 / 环比 / 同比,与小屏同构——五列每列仅 ~157px,此前「值 F36 粗 + 右侧叠放的环比/同比」并排组约 194px 放不下、降 F24 后 156px 仍贴分隔线,故改竖排;更早试过名与值同行、左上角角标,均已否);D **BTC/USDT 时 K 蜡烛图(近 48 小时 = 2 天,2026-09-14 由 72 根改 48 根)**(免费公开行情:OKX 主源、Coinbase/Huobi 备源,`btc_api.py`,5 分钟缓存;周期/根数 `BTC_BAR`/`BTC_LIMIT` 可配,日 K 也支持;**涨绿跌红**)。此前放过 30 天折线图(用户明确不要)和「近 7 天」数据表(已被 K 线替换)。六色语义:用量进度条(含边框)与 pace 各自**三档**——绿=健康(用量 ≤ `USAGE_OK` 30 / pace ≤ −`PACE_TOL` 10)、黑=正常、红=危险(用量 ≥ `USAGE_WARN` 80 / pace ≥ +10);百分比数字只在红档转红(绿小字在白底发虚)。红另用于涨/定位/太阳闪电,蓝=雨雪;**黄不用**——用户明确不要黄色进度条(曾有 60%~80% 黄「注意」档,已移除)。
|
||||
> **分支 `feature/reterminal-e1002`**(基于 `feature/colorful`):新增第二个目标设备 **Seeed reTerminal E1002**(7.3 寸 **800×480**,E Ink **Spectra 6 原生六色**:黑/白/黄/红/绿/蓝,真机已验证六色量化干净)。该设备不接受局域网直推,而是由 **SenseCraft HMI** 云端的 **HTML 控件**按设备刷新间隔(最小 5 分钟)**每周期重新抓取**一个公网 HTTPS URL、无头浏览器截图后量化下发;不支持 ETag/304,内容不变也会整屏重绘(全刷约 17 秒)。**真机验证(2026-09)**:Image 控件只在填写 URL 时抓一次快照、此后不再更新;HTML 控件填同一 URL 则每周期重抓——所以对外只用 HTML 控件。当前主形态是 **`server.py` 动态服务**(部署公网 HTTPS;局域网地址/明文 HTTP 云端抓不到):三条恒定 URL 并行供对比——方案一 `IMAGE_PATH`(默认 `/e1002.png`)每次请求**内存中现渲染 PNG**(Pillow 点阵版);方案二 `HTML_PATH`(默认 `/e1002.html`)每次请求生成 **800×480 现代网页**(`html_renderer.py`,系统/网络字体 + 内联 SVG K 线,无 JS)由平台截图——**真机实拍:文字抗锯齿灰边被平台抖动成毛边、小字与复杂汉字发糊**;方案三 `HTML_PNG_PATH`(默认 `/e1002-web.png`)同一网页由**服务端无头 Chrome 截图后按最近色(不抖动)固化为六色 PNG**(`html_shot.py`),平台拿到纯色图无从抖动,需服务器装 Chrome。**结论(2026-09-10):方案一为主力**——方案二/三的矢量字体在 125 PPI 下小字必然毛糙(抗锯齿灰边无论由平台还是我们二值化都只能变成锯齿/断笔,见 `output/e1002/font_compare.png`:矢量字体单色渲染 ≈ 抗锯齿后二值化,都不如点阵),Canvas 逐像素画也改变不了这点(Pillow 本身就是逐像素画)。方案二/三端点保留供对比,不再投入。宽版**只用点阵字体的整数倍字号 12/24/36/48/84**(`E1002Renderer.__init__` 把基类的 F13/F16/FPACE 都映射到 12px——用量行 标签常规字重、右对齐贴进度条(基类类属性 `LABEL_BOLD`/`LABEL_ALIGN`/`LABEL_GAP`),百分比与 pace 数字粗体;24px 试过被嫌大;另加 F84 给顶部大时钟),可用 `all(v % 12 == 0 for v in 字体尺寸)` 自查。天气/橘喵/BTC 走**服务端内存缓存 + 有效期**(`WEATHER_TTL`/`MAO_TTL`/`BTC_TTL`,默认 900/60/300 秒,0=每次实拉;请求时过期才实拉并更新时间戳,失败沿用旧值、不更新时间戳),服务**不读本机任何文件**;Claude/ChatGPT 用量**由本机 `main.py --target push` 推送原始快照**(`X-Push-Token` 鉴权),服务端内存暂存、渲染时现算 pace。静态形态 `main.py --target e1002`(`output/e1002/index.html` + `dashboard.png`)仍保留。渲染由 `renderer_e1002.py` 完成:继承 `DashboardRenderer` 组件、**k=1 字号与三色版完全相同(不放大,放大后字太大很傻)**,四段版式:A 时钟/天气;B 左 **Claude Usage 三行(all 5h / all 7d / fable 7d,标签沿用 statusline 写法、小写开头)**、右 **ChatGPT Usage(mock,标「示例数据」)**,紧凑单行样式(曾试过撑满高度的大卡片,被否);C **橘喵·今日实时**五列全宽(探店单量/探店流水/团购单量/团购流水/毛利,2026-09-11 接口新增团购字段后由三列改五列;列内四行逐行居中:名 F12 / 值 F24 粗 / 环比 / 同比,与小屏同构——五列每列仅 ~157px,此前「值 F36 粗 + 右侧叠放的环比/同比」并排组约 194px 放不下、降 F24 后 156px 仍贴分隔线,故改竖排;更早试过名与值同行、左上角角标,均已否);D **BTC/USDT 时 K 蜡烛图(近 48 小时 = 2 天,2026-09-14 由 72 根改 48 根)**(免费公开行情:OKX 主源、Coinbase/Huobi 备源,`btc_api.py`,5 分钟缓存;周期/根数 `BTC_BAR`/`BTC_LIMIT` 可配,日 K 也支持;**涨绿跌红**)。此前放过 30 天折线图(用户明确不要)和「近 7 天」数据表(已被 K 线替换)。六色语义:用量进度条(含边框、百分比数字三者同色——2026-09-14 用户要求百分比随条色)与 pace 各自**三档、颜色顺序固定 绿→黑→红、两者独立取色**:用量 ≤ `USAGE_OK` 30 绿「健康」|30~80 黑「正常」|≥ `USAGE_WARN` 80 红「危险」;pace ≤ −`PACE_TOL` 10 绿「节余」|±10 内黑「持平」|≥ +10 红「偏快」。**黄不用**:2026-09-14 曾按要求加过 60%~80% 黄档(含 `USAGE_NOTICE`/`PACE_WARN` 两个阈值),同日被否并整体移除,别再加回。红另用于涨/定位/太阳闪电,蓝=雨雪。
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -40,7 +40,7 @@ EPD_HOST=epd400a44.local python3 main.py
|
||||
|
||||
`main.py` 串联三层,各层职责单一、低耦合:
|
||||
|
||||
- **`data.py`** — 两种形态:本机 `get_dashboard_data()`(带文件缓存,用量读本地快照);服务端用纯拉取函数 `weather_fetch()`/`mao_fetch()`/`btc_fetch()` + `usage_from_raw(now, raw, windows, title)`(`_usage(now, raw=)`/`_usage_gpt(now, raw=)` 都基于它,raw=None 时前者读本地快照、后者用 `gpt_mock_raw()`)。所有「现在」用 `now_tz()`、时间戳格式化用 `_local()`(`TZ=ZoneInfo(config.TZ_NAME)`),本机与境外服务端结果一致。返回固定结构的 dict:`date`/`weather`/`mao`/`usage`/`usage_gpt`/`btc`。日期时间用真实系统时间;橘喵今日经营由 `_mao()` 经 `jm_api`、天气由 `_weather()` 经 `weather_api`(和风,定位南京·江宁)、Claude 用量由 `_usage()` 经 `usage_local`,**任何异常都回退占位 `"--"`,不回退 mock**。改数据保持返回结构不变即可(renderer 依赖其 key:`weather.icon` 为和风图标代码;`mao.cols[]` 为 `{k, v, hb, tb, wide}`,五列由 `REALTIME_COLS`(标签、接口字段前缀、格式化、是否仅宽版)驱动,`_col_from()` 用 `.get()` 取 `today{Base}`/`{base}WowRate`/`{base}YoyRate`,字段缺失(旧后端未上团购)该列占位 `--`;`wide=True`(团购两列)小屏 `render()` 跳过、宽版全显;`hb/tb` 为环比/同比 `(direction, text)`,direction 为 `None` 时不画三角;接口文档见 `../docs/首页看板接口文档-20260911.md`(毛利口径已含团购、金额与增长率为字符串、增长率可 `null`);`usage.bars[]` 为 `{k(短标签,小屏用), label(长标签 all 5h/fable 7d,宽版用), pct(或 None), time_pct?, reset?, scoped}`、`usage.warn`/`usage.ok`/`usage.pace_tol` 为红/绿/pace 阈值(小屏只用 warn)、`usage.updated` 为快照落盘时刻;`usage_gpt` 与 `usage` 同构,目前是 **mock**(`mock: True`;三行故意取 88/71/22 以常驻红/黑/绿三档),接真实数据源时替换 `_usage_gpt()` 即可)。注:`_mao` 仍会拉 `mao.trend`,但两个版面当前都不用它。宽版另有 `btc`:`_btc()` 经 `btc_api.get_candles(BTC_BAR, BTC_LIMIT)` 取 K 线(默认 1H×48),结构 `{symbol, bar, src, candles:[{t, o, h, l, c}], last, chg, chg_label}`,chg 时 K 取相对 24 根前收盘(24h)、日 K 取相对上一根;缓存 `.btc_cache.json`(TTL=`BTC_TTL` 默认 5 分钟,周期变更即失效,失败沿用旧缓存,仍无则 candles 为空 → 面板 `--`)。
|
||||
- **`data.py`** — 两种形态:本机 `get_dashboard_data()`(带文件缓存,用量读本地快照);服务端用纯拉取函数 `weather_fetch()`/`mao_fetch()`/`btc_fetch()` + `usage_from_raw(now, raw, windows, title)`(`_usage(now, raw=)`/`_usage_gpt(now, raw=)` 都基于它,raw=None 时前者读本地快照、后者用 `gpt_mock_raw()`)。所有「现在」用 `now_tz()`、时间戳格式化用 `_local()`(`TZ=ZoneInfo(config.TZ_NAME)`),本机与境外服务端结果一致。返回固定结构的 dict:`date`/`weather`/`mao`/`usage`/`usage_gpt`/`btc`。日期时间用真实系统时间;橘喵今日经营由 `_mao()` 经 `jm_api`、天气由 `_weather()` 经 `weather_api`(和风,定位南京·江宁)、Claude 用量由 `_usage()` 经 `usage_local`,**任何异常都回退占位 `"--"`,不回退 mock**。改数据保持返回结构不变即可(renderer 依赖其 key:`weather.icon` 为和风图标代码;`mao.cols[]` 为 `{k, v, hb, tb, wide}`,五列由 `REALTIME_COLS`(标签、接口字段前缀、格式化、是否仅宽版)驱动,`_col_from()` 用 `.get()` 取 `today{Base}`/`{base}WowRate`/`{base}YoyRate`,字段缺失(旧后端未上团购)该列占位 `--`;`wide=True`(团购两列)小屏 `render()` 跳过、宽版全显;`hb/tb` 为环比/同比 `(direction, text)`,direction 为 `None` 时不画三角;接口文档见 `../docs/首页看板接口文档-20260911.md`(毛利口径已含团购、金额与增长率为字符串、增长率可 `null`);`usage.bars[]` 为 `{k(短标签,小屏用), label(长标签 all 5h/fable 7d,宽版用), pct(或 None), time_pct?, reset?, scoped}`、`usage.warn`/`usage.ok`/`usage.pace_tol` 为用量与 pace 的三档阈值(小屏只用 warn)、`usage.updated` 为快照落盘时刻;`usage_gpt` 与 `usage` 同构,目前是 **mock**(`mock: True`;三行故意取 88/71/22 以常驻红/黑/绿三档),接真实数据源时替换 `_usage_gpt()` 即可)。注:`_mao` 仍会拉 `mao.trend`,但两个版面当前都不用它。宽版另有 `btc`:`_btc()` 经 `btc_api.get_candles(BTC_BAR, BTC_LIMIT)` 取 K 线(默认 1H×48),结构 `{symbol, bar, src, candles:[{t, o, h, l, c}], last, chg, chg_label}`,chg 时 K 取相对 24 根前收盘(24h)、日 K 取相对上一根;缓存 `.btc_cache.json`(TTL=`BTC_TTL` 默认 5 分钟,周期变更即失效,失败沿用旧缓存,仍无则 candles 为空 → 面板 `--`)。
|
||||
- **缓存**(为 launchd 每 5 分钟独立进程而设,必须落盘):天气 `.weather_cache.json`(TTL=`WEATHER_TTL`,默认 15min)、趋势 `.trend_cache.json`(按日期 key,每天只拉一次)、BTC K 线 `.btc_cache.json`(TTL=`BTC_TTL`,默认 5min);三者拉取失败沿用旧缓存。**用量不自建缓存**——直接读 statusline 生产的本地快照 `~/.claude/usage-snapshot.json`(见 `usage_local`),失败即 `"--"`。两个缓存文件已在 `.gitignore`。
|
||||
- **用量的 pace(时间维度对比)**:`_usage()` 每次按当前时间**现算** `time_pct=(窗口已流逝/窗口长)`、`pace=pct−time_pct`(>0 超前↑ / <0 节余↓);故即使 `utilization` 来自可能滞后的本地缓存,pace 仍随时钟准确推进。5h 窗口 18000s、7d 窗口 604800s。
|
||||
- **`jm_api.py`** — jm-devops 后端统计接口客户端,**设备密钥授权**(`grant_type=client_secret`,免验证码)。先用 `CLIENT_ID + CLIENT_SECRET` 调 `POST /auth/login` 换 token(进程内缓存、按 `expire_in` 提前刷新),再带 `Authorization: Bearer <token>` + `clientid` 头调 `/system/statistics/{realtime,recent-days}`;遇 401 清 token 重登一次重试。token 字段为蛇形 `access_token`/`expire_in`。注:历史上曾免鉴权直调、更早曾用账号密码+验证码 OCR 登录(ddddocr 不稳定),现统一为 client_secret 授权——密钥即长期凭证,务必走 HTTPS、优先用 `JM_CLIENT_SECRET` 环境变量、可在后端 `sys_client` 轮换。
|
||||
@@ -55,7 +55,7 @@ EPD_HOST=epd400a44.local python3 main.py
|
||||
- **`_trend_chart`(当前隐藏)**:近30天三指标折线,用 `_styled_polyline()` 按 solid/dashed/dotted 区分、主指标(流水)红线,各指标按自身极值独立归一化。已保留但 `render()` 未调用,想恢复即在 C 前挂回。
|
||||
- **天气图标全矢量手绘**(无图片资源):`_weather_category()` 把和风 icon 代码归并为 8 类(sunny/clear_night/cloudy/overcast/thunder/rain/snow/fog,识别不了回退 cloudy),再由 `_weather_icon()` 用 `_sun`/`_cloud` 等基元按 1bit 描边画出;改天气展示从这里动。
|
||||
- **`pusher.py`** — 直连局域网墨水屏设备(喵喵固件,开源:https://gitee.com/gxp666111/miaomiao,`web/` 即设备自带管理页,可浏览器打开 `http://<EPD_HOST>/` 调试同一套接口)。`push_image()` 先调 `GET /images` 拿画廊已存图片名单,逐个 `POST /delete_image?name=`清空(单张删除失败不阻塞后续),再 `POST /upload`(`Content-Type: image/png`,body 为原始 PNG 字节,非 multipart)触发设备转换+刷屏。**清空画廊是刻意设计**:设备把每次 `/upload` 都追加存一份到画廊(`GET /images` 可见),不清理会越推越多、且设备当前 `current_mode` 为 slideshow 会轮播到旧图;先清后传使画廊恒为 1 张,等价于始终静态展示最新一张。**清理非核心**:拉取画廊列表本身失败也不阻塞后续 `/upload`(跳过清理,直接推)。`_request()` 对连接类异常(`urllib.error.URLError`,含 mDNS `.local` 域名解析瞬时失败)内置重试(`RETRY_TIMES=2`,间隔 `RETRY_DELAY=2s`)——mDNS 基于组播 UDP,偶发丢包很正常,单次失败不该让整轮推送直接跳过。注:历史上曾走 Zectrix 公网 API(`POST /devices/{id}/display/image` + 设备 MAC + `ZECTRIX_API_KEY`),现统一为局域网直连。
|
||||
- **`renderer_e1002.py`** — reTerminal E1002 目标:`E1002Renderer(DashboardRenderer)` 以 **k=1** 复用组件(字号 12/24/48 与三色版一致),只重写版式与配色钩子。四段、三道横规,各段 y 由上一段实际底部推算:A 顶部 0–90 时钟/日期左、天气右(贴 x=790);B 用量 `_usage_block`×2(左 `usage` 10–386|竖规 x=400|右 `usage_gpt` 414–790):`_label` 标题 + 右侧「快照 HH:MM」或 mock 的「示例数据」,行用基类紧凑 `_usage_row`(行距 31,宽版把 `bar["label"]` 长标签代入 `k`,`label_w` 按本块最长标签自适应);C `_realtime` 今日实时五列全宽(`mao.cols` 全部,含 `wide` 列):列内四行 `ctext`/`_cmp_line` 逐行水平居中——名 F12 y0+17 / 值 F24 粗 y0+34 / 环比 y0+68 / 同比 y0+84(按墨迹框:名↔值 ~9px、值↔环比 ~6px),竖规 y0+22~98,区块底 y0+100;D `_btc_chart` BTC K 线(`data.btc`,标题按 `bar` 显示「时K/日K」:右侧最新价 F12 粗 + 口径 `chg_label`(24h/1d)+ 涨跌三角 + 涨跌幅,涨绿跌红;每根蜡烛 1px 影线 + 实心实体(宽≈槽位 60% 取奇数)、收≥开 绿 否则 红;右侧 66px 标签列放最高/最低/最新价,最新价横贯一道点规、标签避让顶底;底部基线 + 首/中/末时间标签 `t`;无数据居中 `--`)。用量块 `_usage_block` 返回末条进度条底部,B/C 分割线取其 +16px(用户反馈太挤)。配色钩子覆写:`_usage_colors` 三档(≥`USAGE_WARN` 红|≤`USAGE_OK` 绿|其间黑;填充与边框同色,百分比文字仅红档转红)、`_pace_color` 三档(≥+`PACE_TOL` 红|≤−`PACE_TOL` 绿|其间黑),阈值由 `usage.ok`/`usage.pace_tol`(来自 config)在 `_usage_block` 内注入。天气图标 `wet=BLUE` 让雨滴/雪花着蓝。可选六色测试色条 `_color_strip`(`E1002_COLOR_STRIP=1` 打开,默认关)。`write_page()` 写出固定 800×480 的 `index.html`(零边距、`image-rendering: pixelated`、PNG 带时间戳参数防缓存)。**为何固定尺寸而非自适应**:云端截图视口/DPR 不可控,自适应会重排+抗锯齿产生中间色再被平台抖动;固定页里的 1:1 PNG 每个像素本就是六色之一,截图零损失——已用无头 Chrome `--window-size=800,480` 截图验证与 PNG 逐像素一致。设备端 HTML 控件须铺满整个画布。历史(均已弃用):k=2 整体放大版;「左用量/右今日实时/下 30 天折线」版;「用量大卡片撑满」版。
|
||||
- **`renderer_e1002.py`** — reTerminal E1002 目标:`E1002Renderer(DashboardRenderer)` 以 **k=1** 复用组件(字号 12/24/48 与三色版一致),只重写版式与配色钩子(基类抽出类属性 `PCT_TRACK`,宽版置 0——百分比已是整数倍字号不需手动加字距;基类另抽 `FPACE` 让 pace 字体与标签 F13 解耦)。四段、三道横规,各段 y 由上一段实际底部推算:A 顶部 0–90 **大时钟 F84 粗 3**(按 `getbbox` 把墨迹顶沿放到 16、底沿 79,占满整段且保住原上/左/下边距),星期/日期 F12 紧随其右 14px、按日期数字底沿与时钟**基线对齐**(2026-09-14 改,此前 F48 + 日期另起一行);天气右(贴 x=790);B 用量 `_usage_block`×2(左 `usage` 10–386|竖规 x=400|右 `usage_gpt` 414–790):`_label` 标题 + 右侧「上次更新 HH:MM」(与今日实时同款文案)或 mock 的「示例数据」,行用基类紧凑 `_usage_row`(行距 31,宽版把 `bar["label"]` 长标签代入 `k`,`label_w` 按本块最长标签自适应);C `_realtime` 今日实时五列全宽(`mao.cols` 全部,含 `wide` 列):列内四行 `ctext`/`_cmp_line` 逐行水平居中——名 F12 y0+21 / 值 F24 粗 y0+38 / 环比 y0+72 / 同比 y0+88(按墨迹框:标题↔名 ~9px——2026-09-14 整列下移 4px,原 ~5px 用户觉得挤;名↔值 ~9px、值↔环比 ~6px),竖规 y0+26~102,区块底 y0+104;D `_btc_chart` BTC K 线(`data.btc`,标题按 `bar` 显示「时K/日K」:右侧最新价 F12 粗 + 口径 `chg_label`(24h/1d)+ 涨跌三角 + 涨跌幅,涨绿跌红;每根蜡烛 1px 影线 + 实心实体(宽≈槽位 60% 取奇数)、收≥开 绿 否则 红;右侧 66px 标签列放最高/最低/最新价,最新价横贯一道点规、标签避让顶底;底部基线 + 首/中/末时间标签 `t`;无数据居中 `--`)。用量块 `_usage_block` 返回末条进度条底部,B/C 分割线取其 +16px(用户反馈太挤)。配色钩子覆写:`_usage_colors` 三档(≥`USAGE_WARN` 红|≤`USAGE_OK` 绿|其间黑;填充、边框、百分比文字三者同色)、`_pace_color` 三档(≥+`PACE_TOL` 红|≤−`PACE_TOL` 绿|其间黑),阈值由 `usage.ok`/`usage.pace_tol`(来自 config)在 `_usage_block` 内注入。天气图标 `wet=BLUE` 让雨滴/雪花着蓝。可选六色测试色条 `_color_strip`(`E1002_COLOR_STRIP=1` 打开,默认关)。`write_page()` 写出固定 800×480 的 `index.html`(零边距、`image-rendering: pixelated`、PNG 带时间戳参数防缓存)。**为何固定尺寸而非自适应**:云端截图视口/DPR 不可控,自适应会重排+抗锯齿产生中间色再被平台抖动;固定页里的 1:1 PNG 每个像素本就是六色之一,截图零损失——已用无头 Chrome `--window-size=800,480` 截图验证与 PNG 逐像素一致。设备端 HTML 控件须铺满整个画布。历史(均已弃用):k=2 整体放大版;「左用量/右今日实时/下 30 天折线」版;「用量大卡片撑满」版。
|
||||
- **`server.py`** — E1002 动态服务(stdlib `ThreadingHTTPServer`,无第三方依赖)。`build_data()` → `(data, 各路状态)`:日期按 `config.TZ_NAME` 时区现算;三路上游经 `_cached(key, ttl, fetch, fallback)`——`STATE.last_good[key]` 未过期(`time - STATE.fetched_at[key] < ttl`)直接用,过期才调 `data.weather_fetch()`/`data.mao_fetch(now, with_trend=False)`/`data.btc_fetch()`,成功更新数据与时间戳,失败沿用旧值(状态 `stale`)、从未成功用占位(`none`);日志每行末尾 `[weather:cache(12s) mao:fetched btc:cache(3s)]` 可直接看命中情况,`/health` 有 `cache_age_s`/`ttl_s`;用量取 `STATE.raw`(推送暂存)→ `data._usage(now, raw=…)`/`data._usage_gpt(now, raw=…)`,未推送时 Claude 全 `--`、ChatGPT 用 mock。端点:`GET IMAGE_PATH` → `render_png_bytes()`(`_RENDER_LOCK` 串行化,Pillow 渲染器非线程安全);`GET HTML_PATH` → `html_renderer.render_html()`;`GET HTML_PNG_PATH` → `html_shot.screenshot_html()`+`quantize6()`(`?raw=1` 跳过量化);`GET /health` JSON;`POST /push/usage|/push/usage_gpt`(body=原始快照 JSON,≤64KB,头 `X-Push-Token`==`PUSH_TOKEN`,为空拒绝);`GET /push/*` 查看暂存(同令牌)。所有响应 `Cache-Control: no-store`;每请求打一行日志(含 `CF-Connecting-IP`)。`PUSH_STATE_PATH` 非空时推送数据落盘(原子替换),重启恢复;图片永不落盘。实测:三路全实拉约 1.1~1.3s(天气 0.28s、橘喵 0.06s、OKX 0.7s 串行),全部命中缓存约 0.02s(渲染 20ms + 编码 14ms)。
|
||||
- **`html_shot.py`** — 方案三:`screenshot_html(html)` 用无头 Chrome(`find_chrome()` 自动探测或 `CHROME_BIN`;Linux root 需 `CHROME_EXTRA_ARGS=--no-sandbox`)把 HTML 截成 800×480,临时目录用后即删,`_SHOT_LOCK` 串行化;**切勿传 `--user-data-dir` 指向全新目录**——新版 `--headless=new` 对空 profile 首启会卡死到超时(实测 A/B/D/E 全超时、只有不带它的 C 2 秒完成)。`quantize6(img)`:每通道二值化(阈值 `HTML_PNG_THRESHOLD` 默认 160,偏向保墨迹,细笔画浅灰边归黑而非丢白)→ Pillow 调色板 `quantize(dither=NONE)` 映射到 `SPECTRA6`,输出保证只含六色。服务端 `?raw=1` 返回未量化截图供对照。实测单次约 3.3s(Chrome 启动为主)。
|
||||
- **`html_renderer.py`** — 方案二网页版:纯 HTML+CSS+内联 SVG、无 JS,数据服务端写死;字体栈 PingFang/微软雅黑/Noto Sans SC + Google Fonts 兜底(平台截图机若无中文字体会掉字,能否加载取决于其出网能力);颜色只用六色纯值;配色规则与 PNG 版一致(用量/pace 三档、涨红跌黑、K 线涨绿跌红),版式同构。K 线标题右侧价格用 `<strong>`(`h3 b::before` 的方块装饰只给标题)。
|
||||
|
||||
@@ -111,6 +111,7 @@ BTC_CACHE_PATH = os.path.join(BASE_DIR, ".btc_cache.json")
|
||||
# 活跃使用时该快照几乎持续刷新、准确;空闲时停在最后一次。文件缺失时用量区显示 "--"。
|
||||
USAGE_LOCAL_PATH = os.environ.get("USAGE_LOCAL_PATH", os.path.expanduser("~/.claude/usage-snapshot.json"))
|
||||
USAGE_WARN = int(os.environ.get("USAGE_WARN", "80")) # 用量 ≥ 此百分比 → 进度条与百分比转红预警(两个版面统一)
|
||||
# 以下两项仅六色宽版(E1002)使用,三色小屏只有 黑/红 两档:
|
||||
USAGE_OK = int(os.environ.get("USAGE_OK", "30")) # 用量 ≤ 此百分比 → 进度条(含边框)绿色「健康」;介于 OK~WARN 之间为黑
|
||||
PACE_TOL = int(os.environ.get("PACE_TOL", "10")) # pace=用量%−时间%:≥ +TOL 红(超前过多)/ ≤ −TOL 绿(节余充足)/ 其间黑
|
||||
# 以下两项仅六色宽版(E1002)使用,三色小屏只有 黑/红 两档。用量与 pace 各自三档、独立取色,颜色顺序固定为 绿 → 黑 → 红
|
||||
# (黄不用:2026-09-14 曾按要求加过 60%~80% 黄档,随即被否——黄小字压白底发虚、且四档在三行里过于花):
|
||||
USAGE_OK = int(os.environ.get("USAGE_OK", "30")) # 用量 ≤ OK 绿「健康」|OK~WARN 黑「正常」|≥ WARN 红「危险」;进度条、边框与百分比数字同色
|
||||
PACE_TOL = int(os.environ.get("PACE_TOL", "10")) # pace=用量%−时间%:≤ −TOL 绿「节余」|±TOL 内黑「持平」|≥ +TOL 红「偏快」
|
||||
|
||||
3
data.py
3
data.py
@@ -333,7 +333,8 @@ def usage_from_raw(now, raw, windows, title):
|
||||
utilization 为已用百分比,resets_at 为 epoch 秒或 ISO;缺哪个窗口哪个显示 "--"。
|
||||
time_pct(时间已流逝%)与 pace 按传入的 now **现算**——所以推送端只需推原始快照,服务端渲染时 pace 仍随时钟准确推进。
|
||||
结构契约(renderer 依赖):{title, warn, ok, pace_tol, updated?, bars:[{k, label, pct(0~100 或 None), time_pct?, reset?, scoped}]};
|
||||
pct ≥ warn 时该条进度条与百分比转红(宽版另有 pct ≤ ok 绿档);pace = pct − time_pct(>0 超前↑ / <0 节余↓,宽版按 ±pace_tol 分三色);
|
||||
pct ≥ warn 时该条进度条与百分比转红(宽版另按 ok/warn 分 绿/黑/红 三档,百分比随条色);
|
||||
pace = pct − time_pct(>0 超前↑ / <0 节余↓,宽版按 ±pace_tol 分 绿/黑/红 三档);
|
||||
reset 为重置时刻文本;updated 为快照落盘时刻 HH:MM(活跃使用时持续刷新,空闲时停在最后一次)。"""
|
||||
raw = raw or {}
|
||||
bars = []
|
||||
|
||||
@@ -56,15 +56,12 @@ html,body{width:800px;height:480px;overflow:hidden;background:#fff;color:#000;
|
||||
|
||||
|
||||
def _tier_bar(bar, usage):
|
||||
"""用量条颜色三档(与 renderer_e1002 一致)。返回 (填充/边框色, 百分比文字色)。"""
|
||||
"""用量条颜色三档 绿/黑/红(与 renderer_e1002 一致)。返回 (填充/边框色, 百分比文字色),两者同色。"""
|
||||
pct = bar.get("pct")
|
||||
if not isinstance(pct, (int, float)):
|
||||
return BLACK, BLACK
|
||||
if pct >= usage.get("warn", 80):
|
||||
return RED, RED
|
||||
if pct <= usage.get("ok", 30):
|
||||
return GREEN, BLACK
|
||||
return BLACK, BLACK
|
||||
c = RED if pct >= usage.get("warn", 80) else GREEN if pct <= usage.get("ok", 30) else BLACK
|
||||
return c, c
|
||||
|
||||
|
||||
def _tier_pace(diff, usage):
|
||||
@@ -73,7 +70,7 @@ def _tier_pace(diff, usage):
|
||||
|
||||
|
||||
def _usage_card(usage):
|
||||
note = "示例数据" if usage.get("mock") else (f'快照 {usage["updated"]}' if usage.get("updated") else "")
|
||||
note = "示例数据" if usage.get("mock") else (f'上次更新 {usage["updated"]}' if usage.get("updated") else "")
|
||||
rows = []
|
||||
for b in usage.get("bars", []):
|
||||
pct = b.get("pct")
|
||||
|
||||
23
renderer.py
23
renderer.py
@@ -31,6 +31,14 @@ BLACK, WHITE, RED = (0, 0, 0), (255, 255, 255), (255, 0, 0)
|
||||
|
||||
|
||||
class DashboardRenderer:
|
||||
# 百分比数字的字符间距(px,乘 k):F16 为 16/12x 非整数倍缩放,数字墨迹无侧边距、相邻紧贴,需手动加距;
|
||||
# 子类若把百分比换成原生 12px 字号(宽版),字形自带侧边距,应覆写为 0。
|
||||
PCT_TRACK = 2
|
||||
# 用量行左标签的字重与对齐:三色版 粗体、左对齐(贴 x0);宽版覆写为 常规字重、右对齐(贴进度条左侧留 LABEL_GAP)。
|
||||
LABEL_BOLD = True
|
||||
LABEL_ALIGN = "l"
|
||||
LABEL_GAP = 8
|
||||
|
||||
def __init__(self, font_path=config.FONT_PATH, size=(config.WIDTH, config.HEIGHT), k=1):
|
||||
"""k:整体缩放系数(整数)。k=1 为 400x300 原版;k=2 供 800x480 设备(reTerminal E1002)复用组件:
|
||||
字体按 k 倍创建(点阵字体整数倍最锐利),组件内的像素常量一律乘 k,故各组件在两种尺寸下形态一致。
|
||||
@@ -40,6 +48,7 @@ class DashboardRenderer:
|
||||
self.F12 = ImageFont.truetype(font_path, 12 * k)
|
||||
self.F13 = ImageFont.truetype(font_path, 13 * k) # 5h/7d 与 pace 用;接近原生 12px,畸变很小
|
||||
self.F16 = ImageFont.truetype(font_path, 16 * k) # 非整数倍(1.33x):靠 fontmode='1' 保持纯色,笔画略不均可接受
|
||||
self.FPACE = self.F13 # pace 数值字体;与标签解耦,宽版把它换成 24px 而标签仍 12px
|
||||
self.F24 = ImageFont.truetype(font_path, 24 * k)
|
||||
self.F36 = ImageFont.truetype(font_path, 36 * k) # 3 倍整数缩放,仍锐利;宽版今日实时数值用
|
||||
self.F48 = ImageFont.truetype(font_path, 48 * k)
|
||||
@@ -149,12 +158,12 @@ class DashboardRenderer:
|
||||
|
||||
def _pace(self, xr, cy, diff):
|
||||
"""时间维度对比(pace = 用量% − 时间%):>0 超前↑红;<0 节余↓黑;=0 持平。
|
||||
数值(F13)两位补零、右端对齐 xr,小三角(6)紧贴其左;均竖直居中 cy。返回整体宽度供左侧定位。"""
|
||||
数值(FPACE,默认 F13)两位补零、右端对齐 xr,小三角(6)紧贴其左;均竖直居中 cy。返回整体宽度供左侧定位。"""
|
||||
up = diff > 0
|
||||
color = self._pace_color(diff)
|
||||
txt = f"{abs(int(diff)):02d}"
|
||||
tw = self._tw(txt, self.F13)
|
||||
self._mid(xr, cy, txt, self.F13, fill=color, bold=self.k, align="r")
|
||||
tw = self._tw(txt, self.FPACE)
|
||||
self._mid(xr, cy, txt, self.FPACE, fill=color, bold=self.k, align="r")
|
||||
if diff == 0:
|
||||
return tw
|
||||
TRI_GAP, TRI = 4 * self.k, 6 * self.k
|
||||
@@ -173,9 +182,13 @@ class DashboardRenderer:
|
||||
k = self.k
|
||||
LABEL_W = 26 * k if label_w is None else label_w
|
||||
G1, G2, H = 13 * k, 8 * k, 15 * k # G1 进度条↔百分比、G2 百分比↔pace(视觉约 12/11)
|
||||
PCT_TRACK = 2 * k # F16(16/12x 非整数倍缩放)数字墨迹无侧边距、紧贴,手动加字符间距
|
||||
PCT_TRACK = self.PCT_TRACK * k # 见类属性 PCT_TRACK
|
||||
cy = y + 10 * k # 行竖直中心
|
||||
self._mid(x0, cy, bar["k"], self.F13, bold=k) # 左标签 F13
|
||||
lb = k if self.LABEL_BOLD else 0 # 左标签 F13
|
||||
if self.LABEL_ALIGN == "r":
|
||||
self._mid(x0 + LABEL_W - self.LABEL_GAP * k, cy, bar["k"], self.F13, bold=lb, align="r")
|
||||
else:
|
||||
self._mid(x0, cy, bar["k"], self.F13, bold=lb)
|
||||
# 右侧从右往左:pace(最右) → 百分比 → 进度条
|
||||
pct_r = x1
|
||||
if ok and "time_pct" in bar:
|
||||
|
||||
@@ -5,11 +5,14 @@ reTerminal E1002 渲染层 —— 复用 renderer.DashboardRenderer 的组件(
|
||||
利用 800x480 的四倍面积重新排版为「AI 用量看板」,输出 **E Ink Spectra 6 原生六色**(黑/白/黄/红/绿/蓝)纯色图,
|
||||
并生成一张固定 800x480 的 HTML 页面供 SenseCraft HMI 的 HTML 控件云端截图拉取。
|
||||
|
||||
版式(k=1,点阵字体且**只用整数倍字号 12/24/36/48**,不放大;四段,三道横规):
|
||||
A 顶部(0~90) :大时钟+日期(左)|天气(右,贴 790)——与三色版同一套组件与尺寸。
|
||||
版式(k=1,点阵字体且**只用整数倍字号 12/24/36/48/84**,不放大;四段,三道横规):
|
||||
A 顶部(0~90) :**大时钟 F84**(墨迹 16~79,占满上下边距内的整段高度)+ 星期/日期 F12 紧随其右、与时钟数字**基线对齐**(左)
|
||||
|天气(右,贴 790)——天气组件与三色版同尺寸。
|
||||
B 用量(96~206) :左半 **Claude Usage**(all 5h / all 7d / fable 7d)|竖规 x=400|右半 **ChatGPT Usage**(mock:all 5h / all 7d / codex 7d)。
|
||||
行组件沿用三色版的紧凑单行 `_usage_row`(行距 31),标签用 bar["label"](statusline 同款写法),列宽按最长标签自适应。
|
||||
C 今日(218~318):**橘喵·今日实时** 五列全宽(探店单量/探店流水/团购单量/团购流水/毛利):列内四行逐行居中——
|
||||
行组件沿用三色版的紧凑单行 `_usage_row`(行距 31);**三处文字统一 12px**(基类 F13/F16/FPACE 都映射到 12px):
|
||||
标签**常规字重、在标签列内右对齐**(贴进度条),百分比与 pace 数字**粗体**(曾试过 24px,用户嫌大),
|
||||
标签用 bar["label"](statusline 同款写法),列宽按最长标签自适应;标题右侧「上次更新 HH:MM」与今日实时同款。
|
||||
C 今日(218~322):**橘喵·今日实时** 五列全宽(探店单量/探店流水/团购单量/团购流水/毛利):列内四行逐行居中——
|
||||
名 F12 / 值 F24 粗 / 环比 / 同比(五列每列 ~157px,原「值 F36 + 右侧叠放环比/同比」并排组放不下)。
|
||||
D 行情(~336~470):**BTC/USDT 时K(近 48 小时)** 蜡烛图(`_btc_chart`,数据来自 btc_api:OKX 主源、Coinbase/Huobi 备源,5 分钟缓存;
|
||||
周期/根数由 BTC_BAR/BTC_LIMIT 配置)——标题右侧最新价 + 24h 涨跌幅;**涨(收≥开)绿、跌红**,实心蜡烛 + 1px 影线;
|
||||
@@ -17,9 +20,12 @@ reTerminal E1002 渲染层 —— 复用 renderer.DashboardRenderer 的组件(
|
||||
可选(470~479):六色测试色条(config.E1002_COLOR_STRIP,默认关;真机校准色彩量化时打开)。
|
||||
|
||||
六色的语义分工(每种颜色只有一个含义,不为「有」而用):
|
||||
- 黑/白:结构与正文。红:危险(用量 ≥ warn 的进度条+边框+百分比、pace ≥ +tol)、涨、定位、太阳/闪电。
|
||||
- 绿:健康(用量 ≤ ok 的进度条+边框、pace ≤ −tol)、K 线上涨;红另用于 K 线下跌。用量与 pace 各自三档:绿 / 黑 / 红(阈值 USAGE_OK / USAGE_WARN / PACE_TOL)。
|
||||
- 蓝:天气雨滴/雪花。黄:不用(用户明确不要黄色进度条;曾试过 60%~80% 黄「注意」档,已移除)。
|
||||
- 黑/白:结构与正文。红:危险、涨、定位、太阳/闪电;绿:健康、K 线上涨(红另用于 K 线下跌);蓝:天气雨滴/雪花。
|
||||
黄:**不用**(2026-09-14 曾按要求加过四档含黄,随即被否)。
|
||||
- 用量与 pace **各自三档、颜色顺序固定 绿 → 黑 → 红**,两者独立计算(`_usage_colors` / `_pace_color`):
|
||||
用量 pct:≤ USAGE_OK(30) 绿「健康」|(30, 80) 黑「正常」|≥ USAGE_WARN(80) 红「危险」——进度条填充、边框与**百分比数字三者同色**;
|
||||
pace = 用量% − 时间%:≤ −PACE_TOL(10) 绿「节余」|(−10, +10) 黑「持平」|≥ +10 红「偏快」。
|
||||
注:绿色数字在白底上比黑字淡一些,24px 字号下可读;这是「百分比随条色」的已知取舍。
|
||||
- 整图只允许六种纯色;渲染后可用 `img.getcolors()` 自查。
|
||||
|
||||
为什么是「固定 800x480 + 一张 PNG」而不是自适应 HTML:
|
||||
@@ -33,6 +39,7 @@ import time
|
||||
import config
|
||||
from renderer import DashboardRenderer, BLACK, WHITE, RED
|
||||
import config as _cfg
|
||||
from PIL import ImageFont
|
||||
|
||||
# E Ink Spectra 6 原生六色。取纯饱和值作为给平台量化器的明确信号(与 ESPHome/GxEPD2 驱动的 BWYRGB 映射一致)。
|
||||
YELLOW, GREEN, BLUE = (255, 255, 0), (0, 255, 0), (0, 0, 255)
|
||||
@@ -42,30 +49,32 @@ SPECTRA6 = [BLACK, WHITE, YELLOW, RED, GREEN, BLUE]
|
||||
class E1002Renderer(DashboardRenderer):
|
||||
"""800x480 六色版。组件继承(k=1,字号不变),只重写版式与配色钩子。"""
|
||||
|
||||
PCT_TRACK = 0 # 百分比是整数倍字号(自带侧边距),不再手动加字符间距
|
||||
LABEL_BOLD = False # 标签常规字重(数值才加粗,层级:数值 > 标签)
|
||||
LABEL_ALIGN = "r" # 标签右对齐贴进度条,长短标签(all 5h / fable 7d)右缘齐平
|
||||
|
||||
def __init__(self, font_path=config.FONT_PATH):
|
||||
super().__init__(font_path, size=(config.E1002_WIDTH, config.E1002_HEIGHT), k=1)
|
||||
# 宽版只用点阵字体的**整数倍字号**(12 / 24 / 36 / 48):基类给用量行用的 F13(标签、pace)与 F16(百分比)
|
||||
# 是非整数倍缩放(笔画不均),这里分别替换成 12px 与 24px;行几何靠 _mid 按墨迹中心对齐,不受字号变化影响。
|
||||
from PIL import ImageFont
|
||||
# 宽版只用点阵字体的**整数倍字号**(12 / 24 / 36 / 48 / 84):基类给用量行用的 F13(标签)/ FPACE(pace)/ F16(百分比)
|
||||
# 是非整数倍缩放(笔画不均),这里统一映射到 12px(24px 试过、用户嫌大);标签常规字重、百分比与 pace 数字粗体;
|
||||
# 行几何靠 _mid 按墨迹中心对齐,不受字号变化影响。F84 给顶部大时钟(7 倍整数缩放,墨迹 63px 恰好占满 A 段)。
|
||||
self.F13 = ImageFont.truetype(font_path, 12)
|
||||
self.F16 = ImageFont.truetype(font_path, 24)
|
||||
self.F16 = self.F13
|
||||
self.FPACE = self.F13
|
||||
self.F84 = ImageFont.truetype(font_path, 84)
|
||||
|
||||
# ---------- 配色钩子(三档:绿 健康 / 黑 正常 / 红 危险)----------
|
||||
# ---------- 配色钩子(三档,顺序固定 绿 → 黑 → 红;用量与 pace 各自独立计算)----------
|
||||
_ok, _pace_tol = _cfg.USAGE_OK, _cfg.PACE_TOL # 由 _usage_block 按数据覆盖
|
||||
|
||||
def _usage_colors(self, ok, pct, warn, bar):
|
||||
"""进度条填充与边框同色:pct ≥ warn 红|pct ≤ ok 绿|其间黑。
|
||||
百分比文字只在红档转红,绿档仍黑——绿在白底上偏淡,做大块填充清楚、做小字发虚。"""
|
||||
"""进度条填充、边框、百分比数字三者同色:pct ≤ ok 绿「健康」|其间黑「正常」|pct ≥ warn 红「危险」。"""
|
||||
if not ok:
|
||||
return BLACK, BLACK, BLACK
|
||||
if pct >= warn:
|
||||
return RED, RED, RED
|
||||
if pct <= self._ok:
|
||||
return GREEN, GREEN, BLACK
|
||||
return BLACK, BLACK, BLACK
|
||||
c = RED if pct >= warn else GREEN if pct <= self._ok else BLACK
|
||||
return c, c, c
|
||||
|
||||
def _pace_color(self, diff):
|
||||
"""pace = 用量% − 时间%:≥ +tol 红(烧得比时间快,超前过多)|≤ −tol 绿(节余充足)|其间黑。"""
|
||||
"""pace = 用量% − 时间%(与用量档位独立):≤ −tol 绿「节余」|(−tol, +tol) 黑「持平」|≥ +tol 红「偏快」。"""
|
||||
if diff >= self._pace_tol:
|
||||
return RED
|
||||
if diff <= -self._pace_tol:
|
||||
@@ -86,14 +95,14 @@ class E1002Renderer(DashboardRenderer):
|
||||
self.d.rectangle([bx0, y0, bx1, y1], outline=BLACK, width=1)
|
||||
|
||||
def _usage_block(self, x0, x1, y0, usage):
|
||||
"""一个用量区块:标题(_label,右侧「快照 HH:MM」或 mock 的「示例数据」)+ 逐行紧凑 `_usage_row`(行距 31)。
|
||||
标签列宽按本块最长标签墨迹 + 8px 算,保证 "Fable 7d" 这类长标签不压进度条。返回末行底部 y。"""
|
||||
right = "示例数据" if usage.get("mock") else (f'快照 {usage["updated"]}' if usage.get("updated") else None)
|
||||
"""一个用量区块:标题(_label,右侧「上次更新 HH:MM」——与今日实时同款文案;mock 数据则标「示例数据」)
|
||||
+ 逐行紧凑 `_usage_row`(行距 31)。标签列宽按本块最长标签墨迹 + LABEL_GAP 算(标签右对齐贴进度条,短标签左侧留白)。返回末行底部 y。"""
|
||||
right = "示例数据" if usage.get("mock") else (f'上次更新 {usage["updated"]}' if usage.get("updated") else None)
|
||||
self._label(y0, usage.get("title", "Usage"), right=right, x0=x0, x1=x1)
|
||||
warn = usage.get("warn", 80)
|
||||
self._ok, self._pace_tol = usage.get("ok", _cfg.USAGE_OK), usage.get("pace_tol", _cfg.PACE_TOL)
|
||||
bars = [dict(b, k=b.get("label") or b["k"]) for b in usage.get("bars", [])] # 宽版用长标签(all 5h / fable 7d)
|
||||
label_w = max([self._tw(b["k"], self.F13) + 1 for b in bars] + [26]) + 8
|
||||
label_w = max([self._tw(b["k"], self.F13) for b in bars] + [26]) + self.LABEL_GAP
|
||||
y = y0 + 27
|
||||
for b in bars:
|
||||
self._usage_row(x0, x1, y, b, warn, label_w=label_w)
|
||||
@@ -102,7 +111,8 @@ class E1002Renderer(DashboardRenderer):
|
||||
|
||||
def _realtime(self, y0, mao, x0=8, x1=None):
|
||||
"""橘喵·今日实时五列(宽版):探店单量 / 探店流水 / 团购单量 / 团购流水 / 毛利,列内四行逐行水平居中:
|
||||
指标名 F12(y0+17)→ 值 F24 粗(y0+34)→ 环比(y0+68)→ 同比(y0+84,行距 16);按墨迹框算,名↔值 ~9px、值↔环比 ~6px、环比↔同比 5px。
|
||||
指标名 F12(y0+21)→ 值 F24 粗(y0+38)→ 环比(y0+72)→ 同比(y0+88,行距 16);按墨迹框算,标题↔名 ~9px(原 ~5px 太挤,整列下移 4px)、
|
||||
名↔值 ~9px、值↔环比 ~6px、环比↔同比 5px。
|
||||
为什么不再是「值 F36 + 右侧叠放环比/同比」的并排组:五列每列仅 ~157px,F36 并排组(如「2.12万」102+间隙+环比 77)
|
||||
近 194px 放不下;降到 F24 后并排组仍有 156px,贴着两侧分隔线太挤。竖排最宽元素(「1234.56%」一行 89px)余量充足,
|
||||
与 400×300 小屏同构。(更早曾试过名与值同行、名贴左上角角标,均已否。)返回区块底部 y。"""
|
||||
@@ -110,16 +120,16 @@ class E1002Renderer(DashboardRenderer):
|
||||
self._label(y0, "橘喵·今日实时", right=f'上次更新 {mao["upd"]}', x0=x0 + 2, x1=x1 - 2)
|
||||
cols = mao["cols"]; n = len(cols)
|
||||
cw = (x1 - x0) / n
|
||||
TAG_Y, VAL_Y, HB_Y, TB_Y = y0 + 17, y0 + 34, y0 + 68, y0 + 84
|
||||
TAG_Y, VAL_Y, HB_Y, TB_Y = y0 + 21, y0 + 38, y0 + 72, y0 + 88
|
||||
for i in range(1, n):
|
||||
self.vdash(x0 + cw * i, y0 + 22, y0 + 98)
|
||||
self.vdash(x0 + cw * i, y0 + 26, y0 + 102)
|
||||
for i, c in enumerate(cols):
|
||||
cx = x0 + cw * (i + 0.5)
|
||||
self.ctext(cx, TAG_Y, c["k"], self.F12)
|
||||
self.ctext(cx, VAL_Y, c["v"], self.F24, bold=1)
|
||||
self._cmp_line(cx, HB_Y, "环比", c["hb"])
|
||||
self._cmp_line(cx, TB_Y, "同比", c["tb"])
|
||||
return y0 + 100
|
||||
return y0 + 104
|
||||
|
||||
@staticmethod
|
||||
def _fmt_price(v):
|
||||
@@ -195,10 +205,16 @@ class E1002Renderer(DashboardRenderer):
|
||||
dt, wt, mao = data["date"], data["weather"], data["mao"]
|
||||
usage, gpt = data["usage"], data.get("usage_gpt") or {}
|
||||
|
||||
# ---- A. 顶部:大时钟+日期(左)/ 天气(右)——与三色版同尺寸 ----
|
||||
# ---- A. 顶部:大时钟 F84 占满整段(左)+ 星期/日期紧随其右、与时钟数字基线对齐 / 天气(右)----
|
||||
DIV_Y = 90
|
||||
self.text(10, 0, dt["time"], self.F48, bold=2)
|
||||
self.text(12, 64, f'{dt["week"]} {dt["greg"]}', self.F12)
|
||||
TIME_BOLD, INK_TOP = 3, 16 # 伪粗体 3px;墨迹顶沿 16 与旧版 F48 相同,保住上边距
|
||||
_, tt, _, tb = self.F84.getbbox(dt["time"]) # F84 数字墨迹 28~91 → 高 63,摆到 16~79,距横规 11px
|
||||
time_y = INK_TOP - tt
|
||||
base_y = time_y + tb # 时钟数字基线(墨迹底沿)
|
||||
self.text(10, time_y, dt["time"], self.F84, bold=TIME_BOLD)
|
||||
date_txt = f'{dt["week"]} {dt["greg"]}'
|
||||
_, _, _, db = self.F12.getbbox(dt["greg"]) # 用日期数字的底沿对齐基线(汉字底沿比数字低 1px,忽略)
|
||||
self.text(10 + self._tw(dt["time"], self.F84) + TIME_BOLD + 14, base_y - db, date_txt, self.F12)
|
||||
loc, temp, cr = wt["loc"], wt["temp"], f'{wt["cond"]} {wt["range"]}'
|
||||
self.rtext(R, 8, loc, self.F12)
|
||||
self.rtext(R, 30, temp, self.F24, bold=1)
|
||||
|
||||
Reference in New Issue
Block a user