From 3656d8d93385404219d6bcd4f4cb00c2baa461dd Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Mon, 14 Sep 2026 17:34:41 +0800 Subject: [PATCH] =?UTF-8?q?fix:=2012px=20=E4=BC=AA=E7=B2=97=E4=BD=93?= =?UTF-8?q?=E6=96=87=E5=AD=97=E8=87=AA=E5=8A=A8=E5=8A=A0=201px=20=E5=AD=97?= =?UTF-8?q?=E8=B7=9D=EF=BC=8C=E6=B6=88=E9=99=A4=E7=9B=B8=E9=82=BB=E5=AD=97?= =?UTF-8?q?=E7=B2=98=E8=BF=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 12px 点阵字形间只有 1 列空隙,伪粗体右偏 1px 正好填满导致左右重叠;13/24px 空隙 2 列不受影响 - 基类新增 _track():12px 且加粗时自动加 k 字距,text/rtext/ctext/_mid 统一走它,宽版映射到 12px 的 F13/F16/FPACE 同样命中 - 新增 _tw_b() 取含粗体偏移与字距的实际占宽,替换环比/同比居中、金额前后缀、状态栏推进、BTC 涨跌幅、用量行进度条右端的手写宽度 - 夹具回归:小屏 F13/F16 部分逐像素不变,各行进度条右端仍齐平 Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 4 ++-- renderer.py | 32 ++++++++++++++++++++++---------- renderer_e1002.py | 8 ++++---- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9b9aa91..ec7aafe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,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:///` 调试同一套接口)。`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 与三色版一致),只重写版式与配色钩子(基类抽出类属性 `PCT_TRACK`,宽版置 0——百分比已是整数倍字号不需手动加字距;基类另抽 `FPACE` 让 pace 字体与标签 F13 解耦)。四段、三道横规,各段 y 由上一段实际底部推算:A 顶部 0–90 **大时钟 F84 粗 3**(按 `getbbox` 把墨迹顶沿放到 16、底沿 79,占满整段且保住原上/左/下边距),其右 14px 起一列三行左缘对齐:电池行、温度湿度行(`_status_battery`/`_status_env`)、星期/日期 F12(按日期数字底沿与时钟**基线对齐**;三行中心约 30.5/52.5/74.5,行距 22);天气右(贴 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 / 值 `_value` 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)+ 涨跌三角 + 涨跌幅(F12 粗,2026-09-14 加粗),涨绿跌红;每根蜡烛 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`(小屏 F16 百分比的手动字距),宽版置 0——其 F16 映射到 12px、加粗后由 `_track()` 自动加 1px 字距;基类另抽 `FPACE` 让 pace 字体与标签 F13 解耦)。四段、三道横规,各段 y 由上一段实际底部推算:A 顶部 0–90 **大时钟 F84 粗 3**(按 `getbbox` 把墨迹顶沿放到 16、底沿 79,占满整段且保住原上/左/下边距),其右 14px 起一列三行左缘对齐:电池行、温度湿度行(`_status_battery`/`_status_env`)、星期/日期 F12(按日期数字底沿与时钟**基线对齐**;三行中心约 30.5/52.5/74.5,行距 22);天气右(贴 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 / 值 `_value` 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)+ 涨跌三角 + 涨跌幅(F12 粗,2026-09-14 加粗),涨绿跌红;每根蜡烛 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` 时区现算;四路上游(天气/橘喵/BTC/设备遥测)经 `_cached(key, ttl, fetch)`——`STATE.last_good[key]` 未过期(`time - STATE.fetched_at[key] < ttl`)直接用;过期的**并行**提交到线程池 `_POOL`(`_refresh` 跑 `data.weather_fetch()`/`data.mao_fetch(now, with_trend=False)`/`data.btc_fetch()`,成功更新数据与时间戳),`build_data` 用 `concurrent.futures.wait` 整体最多等 **`FETCH_BUDGET`** 秒(默认 5,`.env` 可调),再由 `_settle` 结算:完成→`fetched(0.27s)`(带耗时)/失败沿用旧值 `stale`/从未成功占位 `none`;**未完成→ `slow→stale`(用上一次成功值)或 `slow→none`(占位)**,拉取线程继续跑完写入缓存供下次请求(stale-while-revalidate),`STATE.inflight[key]` 保证同一路进行中不重复实拉。响应上限 ≈ 预算 + 渲染,任一路上游卡死(socket 超时和风/橘喵 30s、BTC 8s)不再拖垮整图。**2026-09-14 前为串行实拉**(三路全拉约 1.1~2.6s,单路卡住即整体超时);改并行后三路全拉约 0.75s(取最慢一路)。日志每行末尾 `[weather:cache(12s) mao:fetched(0.06s) btc:slow→stale]` 可直接看命中/耗时/超预算情况,`/health` 有 `cache_age_s`/`ttl_s`/`fetch_budget_s`/`inflight`;用量取 `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 线标题右侧价格用 ``(`h3 b::before` 的方块装饰只给标题)。 @@ -76,7 +76,7 @@ EPD_HOST=epd400a44.local python3 main.py - **`DashboardRenderer(k=…)` 缩放系数**:所有组件(`_label`/`_cmp_line`/`_usage_row`/`_pace`/`tri` 等)的像素常量与伪粗体偏移都乘 `k`,字体按 `12*k/24*k/48*k` 创建;`k=1` 为 400×300 原版(改组件后务必用固定夹具比对 `k=1` 输出逐像素不变),E1002 版当前用 `k=1`(k=2 整体放大在 800×480 上字过大,已否决),k 参数保留供更高分辩率设备使用。字体属性名(`F12/F24/F48`)表达层级而非绝对像素。 - 字体固定用 **Fusion Pixel 12px 点阵字体**(`fonts/`,OFL 协议),首选整数倍尺寸 **12/24/48**(`F12/F24/F48`)最锐利;用量区为拿到合适的字号层级另用了 **F13/F16**(1.08x/1.33x 非整数倍,笔画略不均,属有意取舍),靠 `d.fontmode = "1"` 关抗锯齿仍保持纯色不糊。跨字号竖直居中统一用 `_mid()`(按 `getbbox` 墨迹中心对齐)。 -- 加粗用「伪粗体」:`_draw()` 按 1~bold px 水平偏移叠绘笔画(点阵字体只有单一字重)。 +- 加粗用「伪粗体」:`_draw()` 按 1~bold px 水平偏移叠绘笔画(点阵字体只有单一字重)。**12px 加粗自动加 1px 字距**(`_track()`,2026-09-14):12px 字形之间只有 1 列空隙,伪粗体右偏 1px 正好填满、相邻字粘连(13/24px 空隙 2 列不受影响);`text/rtext/ctext/_mid` 内部都走它,宽版映射到 12px 的 F13/F16/FPACE 同样命中。手动推进 x 或居中时用 `_tw_b(s, ft, bold)` 取含粗体偏移与字距的实际占宽,别再手写 `_tw + bold`。 - 推送时 `DITHER = False`(硬阈值),纯黑白图最锐利,勿改成 `true`。 - 坐标、字号均为像素网格上的硬编码常量;调布局时注意各区分隔线 `hdash` 的 y 值与下方组件位置联动。 diff --git a/renderer.py b/renderer.py index 1ff014f..62efc6c 100644 --- a/renderer.py +++ b/renderer.py @@ -75,14 +75,25 @@ class DashboardRenderer: self.d.text((cx + dx, y), ch, font=ft, fill=fill) cx += self.d.textlength(ch, font=ft) + track + def _track(self, ft, bold, track=0): + """实际字距:显式 track 优先;否则「12px 且加粗」自动加 k——12px 点阵字形之间只有 1 列空隙,伪粗体右偏 1px 正好填满、 + 相邻字左右粘连(13/24px 空隙有 2 列所以正常),2026-09-14 用户要求拉开一点点。宽版 F13/F16/FPACE 都映射到 12px,同样命中。""" + if track or not bold: + return track + return self.k if getattr(ft, "size", 0) == 12 * self.k else 0 + + def _tw_b(self, s, ft, bold=0, track=0): + """含伪粗体偏移与自动字距的实际占宽(手动推进 x / 居中时用它,别再手写 _tw + bold)。""" + return self._tw(s, ft, self._track(ft, bold, track)) + bold + def text(self, x, y, s, ft, fill=BLACK, bold=0): - self._draw(x, y, s, ft, fill, bold) + self._draw(x, y, s, ft, fill, bold, self._track(ft, bold)) def rtext(self, xr, y, s, ft, fill=BLACK, bold=0): - self._draw(xr - self._tw(s, ft) - bold, y, s, ft, fill, bold) + self._draw(xr - self._tw_b(s, ft, bold), y, s, ft, fill, bold, self._track(ft, bold)) def ctext(self, cx, y, s, ft, fill=BLACK, bold=0): - self._draw(cx - (self._tw(s, ft) + bold) / 2, y, s, ft, fill, bold) + self._draw(cx - self._tw_b(s, ft, bold) / 2, y, s, ft, fill, bold, self._track(ft, bold)) def hdash(self, x0, x1, y, dash=4, gap=4, fill=BLACK, width=1): x = x0 @@ -108,8 +119,8 @@ class DashboardRenderer: """环比/同比一行的总宽度(标签 + 间隙 + 三角 + 数值),供居中/右对齐排布。""" direction, val = pair k = self.k - trw, tgap, vb = (8 * k, 4 * k, k) if direction else (0, 0, 0) # vb:数值伪粗体的偏移宽度(占位 -- 不加粗) - return self._tw(label, self.F12) + 5 * k + trw + tgap + self._tw(val, self.F12) + vb + trw, tgap, vb = (8 * k, 4 * k, k) if direction else (0, 0, 0) # vb:数值伪粗体(占位 -- 不加粗) + return self._tw(label, self.F12) + 5 * k + trw + tgap + self._tw_b(val, self.F12, vb) def _cmp_line(self, cx, y, label, pair, fg=BLACK): """环比/同比一行:标签 + 涨跌三角 + 数值,整体居中于 cx。direction 为 None 时不画三角。""" @@ -151,9 +162,9 @@ class DashboardRenderer: k = self.k GAP = 1 * k # 小号段与主数字的间距 sb = bold # 小号段同样伪粗体,否则挨着粗体主数字显得单薄 - wp = self._tw(pre, self.F12) + sb + GAP if pre else 0 - ws = GAP + self._tw(suf, self.F12) + sb if suf else 0 - wn = self._tw(num, self.F24) + bold + wp = self._tw_b(pre, self.F12, sb) + GAP if pre else 0 + ws = GAP + self._tw_b(suf, self.F12, sb) if suf else 0 + wn = self._tw_b(num, self.F24, bold) x = cx - (wp + wn + ws) / 2.0 # 基线对齐:F12/F24 数字墨迹底沿分别为 13k/26k(getbbox),小号段下移其差值 ys = y + (self.F24.getbbox("0")[3] - self.F12.getbbox("0")[3]) @@ -182,6 +193,7 @@ class DashboardRenderer: track:见 _tw,用于给紧贴的字符手动加间距。""" _, t, _, b = ft.getbbox(s) # 该串墨迹在字身框内的上/下沿 y = cy - (t + b) / 2.0 + track = self._track(ft, bold, track) w = self._tw(s, ft, track) if align == "r": x -= w + bold @@ -196,7 +208,7 @@ class DashboardRenderer: up = diff > 0 color = self._pace_color(diff) txt = f"{abs(int(diff)):02d}" - tw = self._tw(txt, self.FPACE) + tw = self._tw(txt, self.FPACE, self._track(self.FPACE, self.k)) # 宽版 FPACE=12px 加粗 → 含自动字距 self._mid(xr, cy, txt, self.FPACE, fill=color, bold=self.k, align="r") TRI_GAP, TRI = 4 * self.k, 6 * self.k cx = xr - tw - TRI_GAP - TRI / 2 @@ -234,7 +246,7 @@ class DashboardRenderer: pct_txt = f"{pct:02d}%" if ok else "--" self._mid(pct_r, cy, pct_txt, self.F16, fill=color, bold=k, align="r", track=PCT_TRACK) # 百分比 F16(主) bx0 = x0 + LABEL_W - bx1 = pct_r - self._tw(pct_txt, self.F16, PCT_TRACK) - G1 + bx1 = pct_r - self._tw(pct_txt, self.F16, self._track(self.F16, k, PCT_TRACK)) - G1 by = cy - H // 2 self.d.rectangle([bx0, by, bx1, by + H], outline=outline, width=k) # 轨道边框随条色 fw = (bx1 - bx0) * pct / 100.0 diff --git a/renderer_e1002.py b/renderer_e1002.py index 7b7ff5e..6d82d5b 100644 --- a/renderer_e1002.py +++ b/renderer_e1002.py @@ -111,7 +111,7 @@ class E1002Renderer(DashboardRenderer): txt = f"{lvl}%" if lvl is not None else "--" b = 1 if lvl is not None else 0 # 数字伪粗体、字号不变(2026-09-14 用户要求);占位不加粗 self._mid(x, cy, txt, F, bold=b) - return x + self._tw(txt, F) + b + return x + self._tw_b(txt, F, b) def _status_env(self, x, cy, dev): """状态项·环境(F12,数字伪粗体,墨迹中心 cy):温度计(细管 3×8 + 底部球 7×7,管内水银线)+温度 | 蓝色水滴(上尖下圆)+湿度, @@ -125,7 +125,7 @@ class E1002Renderer(DashboardRenderer): t = dev.get("temp") txt = f"{t:.1f}℃" if t is not None else "--" b = 1 if t is not None else 0 # 数字伪粗体、字号不变(2026-09-14 用户要求);占位不加粗 - self._mid(x, cy, txt, F, bold=b); x += self._tw(txt, F) + b + GAP + self._mid(x, cy, txt, F, bold=b); x += self._tw_b(txt, F, b) + GAP self.d.polygon([(x + 4, cy - 6), (x, cy + 2), (x + 8, cy + 2)], fill=BLUE) self.d.ellipse([x, cy - 1, x + 8, cy + 6], fill=BLUE) x += 9 + GAP_I @@ -133,7 +133,7 @@ class E1002Renderer(DashboardRenderer): txt = f"{h:.0f}%" if h is not None else "--" b = 1 if h is not None else 0 self._mid(x, cy, txt, F, bold=b) - return x + self._tw(txt, F) + b + return x + self._tw_b(txt, F, b) def _color_strip(self, y0, y1, x0=None, x1=None): """六色测试色条:六块等宽实心色块,白块加 1px 黑框以便可见。真机校准用。""" @@ -239,7 +239,7 @@ class E1002Renderer(DashboardRenderer): col = GREEN if chg >= 0 else RED xr = x1 pct_txt = f"{abs(chg):.2f}%" - self.rtext(xr, y0, pct_txt, self.F12, fill=col, bold=1); xr -= self._tw(pct_txt, self.F12) + 1 + 4 # 涨跌幅伪粗体(+1 为粗体偏移) + self.rtext(xr, y0, pct_txt, self.F12, fill=col, bold=1); xr -= self._tw_b(pct_txt, self.F12, 1) + 4 # 涨跌幅伪粗体 self.tri(xr - 4, y0 + 8.5, 7, up=chg >= 0, fill=col); xr -= 8 + 6 if btc.get("chg_label"): # 涨跌幅口径(24h / 1d) self.rtext(xr, y0, btc["chg_label"], self.F12); xr -= self._tw(btc["chg_label"], self.F12) + 10