feat: E1002 六色版改版为 AI 用量看板+Claude 用量新增 Fable 7d 行与重置时刻
- 版面重排:主体左 Claude Usage(5h/7d/Fable 7d 三行),右 ChatGPT Usage(目前 mock,标「示例数据」);每行为一张用量卡:标签+F24 百分比+pace/全宽进度条/「重置 HH:MM」+「已过 NN%」 - 移除上一版的橘喵今日实时与 30 天趋势图及 trend totals 合计(业务数据留给 400×300 小屏),绿色暂不再使用 - data 用量结构新增 scoped(按模型限定额度)、reset(重置时刻文本)、updated(快照落盘时刻),新增 _usage_gpt mock 数据源,接入真实数据时保持结构替换即可 - usage_local 读取 statusline 新增的 seven_day_fable 与 updated_at;400×300 小屏 render 跳过 scoped 行 - 同步更新 CLAUDE.md 分支说明 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
reTerminal E1002 渲染层 —— 复用 renderer.DashboardRenderer 的全部组件(**k=1,字号与 400x300 版完全相同**),
|
||||
利用 800x480 的四倍面积重新排版,输出 **E Ink Spectra 6 原生六色**(黑/白/黄/红/绿/蓝)纯色图,
|
||||
reTerminal E1002 渲染层 —— 复用 renderer.DashboardRenderer 的组件(**k=1,字号与 400x300 版完全相同**),
|
||||
利用 800x480 的四倍面积重新排版为「AI 用量看板」,输出 **E Ink Spectra 6 原生六色**(黑/白/黄/红/绿/蓝)纯色图,
|
||||
并生成一张固定 800x480 的 HTML 页面供 SenseCraft HMI 的 HTML 控件云端截图拉取。
|
||||
|
||||
版式(k=1,12/24/48px 点阵字体,不放大):
|
||||
版式(k=1,12/24/48px 点阵字体,不放大;四段,三道横规):
|
||||
A 顶部(0~90) :大时钟+日期(左)|天气(右,贴 790)——与三色版同一套组件与尺寸。
|
||||
B 中段(96~192) :左半 Claude Usage(10~390)|竖规 x=400|右半 橘喵·今日实时三列(410~790)。
|
||||
C 下段(206~462):橘喵·30 天趋势(全宽,三指标各一色:流水红 / 单量蓝 / 毛利绿),三色版里隐藏的图在此恢复。
|
||||
D 可选(470~479):六色测试色条(config.E1002_COLOR_STRIP,默认关;真机校准色彩量化时打开)。
|
||||
B 用量(96~206) :左半 **Claude Usage**(5h / 7d / Fable 7d)|竖规 x=400|右半 **ChatGPT Usage**(mock:5h / 7d / Codex 7d)。
|
||||
行组件沿用三色版的紧凑单行 `_usage_row`(行距 31),标签列宽按最长标签自适应。
|
||||
C 今日(218~314):**橘喵·今日实时** 三列全宽(名 / 值 F24 粗 / 环比·同比),版式同三色版。
|
||||
D 近7天(328~438):**橘喵·近7天** 数据表(真实数据:30 天趋势缓存的最后 7 天;不画折线)——
|
||||
列 = 指标名 + 7 个日期 + 合计,行 = 单量 / 流水 / 毛利;合计列伪粗体。
|
||||
可选(470~479):六色测试色条(config.E1002_COLOR_STRIP,默认关;真机校准色彩量化时打开)。
|
||||
|
||||
六色的语义分工(每种颜色只有一个含义,不为「有」而用):
|
||||
- 黑/白:结构与正文。红:预警、涨、定位、太阳/闪电、趋势主指标(流水)。
|
||||
- 黑/白:结构与正文。红:预警(pct≥warn)、pace 超前、定位、太阳/闪电。
|
||||
- 黄:用量进度条「注意」档(caution ≤ pct < warn)填充,边框与百分比仍黑(黄字压白底不可读,故黄只作填充)。
|
||||
- 绿:趋势「毛利」线。蓝:趋势「单量」线、天气雨滴/雪花。
|
||||
- 蓝:天气雨滴/雪花。绿:暂未使用(保留给后续语义,例如「节余」)。
|
||||
- 整图只允许六种纯色;渲染后可用 `img.getcolors()` 自查。
|
||||
|
||||
为什么是「固定 800x480 + 一张 PNG」而不是自适应 HTML:
|
||||
@@ -32,19 +35,16 @@ from renderer import DashboardRenderer, BLACK, WHITE, RED
|
||||
YELLOW, GREEN, BLUE = (255, 255, 0), (0, 255, 0), (0, 0, 255)
|
||||
SPECTRA6 = [BLACK, WHITE, YELLOW, RED, GREEN, BLUE]
|
||||
|
||||
# 趋势三指标配色:主指标流水红领读,单量蓝、毛利绿;未列出的指标回退黑
|
||||
SERIES_COLOR = {"流水": RED, "单量": BLUE, "毛利": GREEN}
|
||||
|
||||
|
||||
class E1002Renderer(DashboardRenderer):
|
||||
"""800x480 六色版。组件全部继承(k=1,字号不变),只重写版式与配色钩子。"""
|
||||
"""800x480 六色版。组件继承(k=1,字号不变),只重写版式与配色钩子。"""
|
||||
|
||||
def __init__(self, font_path=config.FONT_PATH):
|
||||
super().__init__(font_path, size=(config.E1002_WIDTH, config.E1002_HEIGHT), k=1)
|
||||
|
||||
# ---------- 配色钩子 ----------
|
||||
def _usage_colors(self, ok, pct, warn, bar):
|
||||
"""三档:≥warn 整体红|caution≤pct<warn 黄填充+黑框黑字|其余黑。"""
|
||||
"""三档:≥warn 整体红|caution≤pct<warn 黄填充+黑框黑字|其余黑。返回(填充,边框,文字)。"""
|
||||
if not ok:
|
||||
return BLACK, BLACK, BLACK
|
||||
if pct >= warn:
|
||||
@@ -66,66 +66,62 @@ class E1002Renderer(DashboardRenderer):
|
||||
if c == WHITE:
|
||||
self.d.rectangle([bx0, y0, bx1, y1], outline=BLACK, width=1)
|
||||
|
||||
def _trend_chart6(self, y0, y1, trend):
|
||||
"""近30天三指标折线(六色版):标题(左)+ 彩色图例(右)+ 折线 + X 轴。
|
||||
与三色版 `_trend_chart` 的差别:用**颜色**而非线型区分指标(全部实线,主指标 2px),
|
||||
绘图区高度由 y0~y1 决定以填满下半屏。各指标按自身极值独立归一化,只表达趋势形状。"""
|
||||
dates, series = trend.get("dates") or [], trend.get("series") or []
|
||||
title = "橘喵·30天趋势"
|
||||
self._label(y0, title)
|
||||
if not series or not dates or not any(s.get("data") for s in series):
|
||||
self.ctext(self.W / 2, (y0 + y1) / 2 - 12, "--", self.F24)
|
||||
return
|
||||
# ---- 合计(紧随标题):「合计 单量 12,345 流水 123.45万 毛利 12.34万」,数值伪粗体 ----
|
||||
totals = trend.get("totals") or []
|
||||
if totals:
|
||||
x = 10 + 8 + 6 + self._tw(title, self.F12) + 24
|
||||
self.text(x, y0, "合计", self.F12); x += self._tw("合计", self.F12) + 12
|
||||
for name, val in totals:
|
||||
self.text(x, y0, name, self.F12); x += self._tw(name, self.F12) + 4
|
||||
self.text(x, y0, val, self.F12, bold=1); x += self._tw(val, self.F12) + 1 + 14
|
||||
# ---- 图例(右):色块样线 + 指标名,从右往左排布 ----
|
||||
SAMP, G1, GAP = 16, 8, 12
|
||||
x = self.W - 10
|
||||
for s in reversed(series):
|
||||
c = SERIES_COLOR.get(s.get("k"), BLACK)
|
||||
self.rtext(x, y0, s["k"], self.F12, fill=c)
|
||||
x -= self._tw(s["k"], self.F12) + G1
|
||||
self.d.line([x - SAMP, y0 + 7, x, y0 + 7], fill=c, width=2)
|
||||
x -= SAMP + GAP
|
||||
# ---- 绘图区 ----
|
||||
px0, px1 = 14, self.W - 10
|
||||
py0, py1 = y0 + 24, y1 - 16 # 底部留 16px 给 X 轴日期
|
||||
self.hdash(px0, px1, py1, dash=2, gap=3) # 基线
|
||||
n = len(dates)
|
||||
xstep = (px1 - px0) / (n - 1) if n > 1 else 0
|
||||
# 周界竖向点规:每 7 个点一道,帮助读「第几周」,不与折线争抢
|
||||
for i in range(7, n, 7):
|
||||
self.vdash(px0 + xstep * i, py0, py1, dash=1, gap=3)
|
||||
# 先画次要指标,最后画主指标(流水)压顶
|
||||
order = sorted(series, key=lambda s: s.get("k") == "流水")
|
||||
for s in order:
|
||||
color = SERIES_COLOR.get(s.get("k"), BLACK)
|
||||
data = s["data"]
|
||||
lo, hi = min(data), max(data)
|
||||
span = (hi - lo) or 1
|
||||
pad = 6
|
||||
pts = []
|
||||
for i, v in enumerate(data):
|
||||
xv = px0 + xstep * i
|
||||
yv = py1 - pad - (v - lo) / span * (py1 - py0 - 2 * pad)
|
||||
pts.append((xv, yv))
|
||||
w = 2 if color == RED else 1
|
||||
for (ax, ay), (bx, by) in zip(pts, pts[1:]):
|
||||
self.d.line([ax, ay, bx, by], fill=color, width=w)
|
||||
if color == RED and pts: # 主指标最新值标点:“今日 · 你在此处”
|
||||
ex, ey = pts[-1]
|
||||
self.d.ellipse([ex - 3, ey - 3, ex + 3, ey + 3], fill=RED)
|
||||
# ---- X 轴标签:首 / 中 / 末 ----
|
||||
xl_y = py1 + 3
|
||||
self.text(px0, xl_y, dates[0], self.F12)
|
||||
self.ctext((px0 + px1) / 2, xl_y, dates[n // 2], self.F12)
|
||||
self.rtext(px1, xl_y, dates[-1], self.F12)
|
||||
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)
|
||||
self._label(y0, usage.get("title", "Usage"), right=right, x0=x0, x1=x1)
|
||||
warn = usage.get("warn", 80)
|
||||
bars = usage.get("bars", [])
|
||||
label_w = max([self._tw(b["k"], self.F13) + 1 for b in bars] + [26]) + 8
|
||||
y = y0 + 27
|
||||
for b in bars:
|
||||
self._usage_row(x0, x1, y, b, warn, label_w=label_w)
|
||||
y += 31
|
||||
return y - 31 + 20
|
||||
|
||||
def _realtime(self, y0, mao, x0=8, x1=None):
|
||||
"""橘喵·今日实时三列(版式同三色版:标题+18=名/+34=值/+64=环比/+82=同比)。返回墨迹底部 y。"""
|
||||
x1 = self.W - 8 if x1 is None else x1
|
||||
self._label(y0, "橘喵·今日实时", right=f'上次更新 {mao["upd"]}', x0=x0 + 2, x1=x1 - 2)
|
||||
cols = mao["cols"]; n = len(cols)
|
||||
cw = (x1 - x0) / n
|
||||
for i in range(1, n):
|
||||
self.vdash(x0 + cw * i, y0 + 32, y0 + 96)
|
||||
for i, c in enumerate(cols):
|
||||
cx = x0 + cw * (i + 0.5)
|
||||
self.ctext(cx, y0 + 18, c["k"], self.F12)
|
||||
self.ctext(cx, y0 + 34, c["v"], self.F24, bold=1)
|
||||
self._cmp_line(cx, y0 + 64, "环比", c["hb"])
|
||||
self._cmp_line(cx, y0 + 82, "同比", c["tb"])
|
||||
return y0 + 96
|
||||
|
||||
def _week_table(self, y0, week, x0=10, x1=None):
|
||||
"""橘喵·近7天数据表:表头 = 指标名列 + 7 个日期 + 合计;行 = 单量/流水/毛利。
|
||||
全部 F12、右对齐数字;合计列伪粗体;表头下一道点规。无数据居中 "--"。返回底部 y。"""
|
||||
x1 = self.W - 10 if x1 is None else x1
|
||||
self._label(y0, "橘喵·近7天", right="不含今日", x0=x0, x1=x1)
|
||||
dates, rows = (week or {}).get("dates") or [], (week or {}).get("rows") or []
|
||||
if not dates or not rows:
|
||||
self.ctext((x0 + x1) / 2, y0 + 40, "--", self.F24)
|
||||
return y0 + 80
|
||||
NAME_W, ROW = 44, 22
|
||||
ncol = len(dates) + 1 # 日期列 + 合计列
|
||||
cw = (x1 - x0 - NAME_W) / ncol
|
||||
col_r = lambda i: x0 + NAME_W + cw * (i + 1) - 4 # 第 i 列右端(含右内边距)
|
||||
hy = y0 + 24
|
||||
for i, d in enumerate(dates):
|
||||
self.rtext(col_r(i), hy, d, self.F12)
|
||||
self.rtext(col_r(len(dates)), hy, "合计", self.F12)
|
||||
self.hdash(x0, x1, hy + 17, dash=2, gap=3)
|
||||
y = hy + ROW
|
||||
for name, vals, total in rows:
|
||||
self.text(x0, y, name, self.F12)
|
||||
for i, v in enumerate(vals):
|
||||
self.rtext(col_r(i), y, v, self.F12)
|
||||
self.rtext(col_r(len(dates)), y, total, self.F12, bold=1)
|
||||
y += ROW
|
||||
return y - ROW + 14
|
||||
|
||||
# ---------- 入口 ----------
|
||||
def render(self, data):
|
||||
@@ -135,7 +131,8 @@ class E1002Renderer(DashboardRenderer):
|
||||
self.d.fontmode = "1"
|
||||
R = self.W - 10 # 右页边 790
|
||||
|
||||
dt, wt, mao, usage = data["date"], data["weather"], data["mao"], data["usage"]
|
||||
dt, wt, mao = data["date"], data["weather"], data["mao"]
|
||||
usage, gpt = data["usage"], data.get("usage_gpt") or {}
|
||||
self._caution = usage.get("caution", config.USAGE_CAUTION)
|
||||
|
||||
# ---- A. 顶部:大时钟+日期(左)/ 天气(右)——与三色版同尺寸 ----
|
||||
@@ -156,38 +153,24 @@ class E1002Renderer(DashboardRenderer):
|
||||
self._weather_category(wt.get("icon")), s=s, accent=RED, wet=BLUE)
|
||||
self.hdash(8, self.W - 8, DIV_Y)
|
||||
|
||||
# ---- B. 中段:左 Claude Usage | 右 橘喵·今日实时 ----
|
||||
# ---- B. 用量:左 Claude | 右 ChatGPT(紧凑单行,与三色版同款)----
|
||||
MID = self.W // 2 # 400
|
||||
self._label(96, usage.get("title", "Claude Usage"))
|
||||
warn = usage.get("warn", 80)
|
||||
ry = 123
|
||||
for b in usage.get("bars", []):
|
||||
self._usage_row(10, MID - 10, ry, b, warn)
|
||||
ry += 31
|
||||
self.vdash(MID, 96, 194)
|
||||
|
||||
lx0, lx1 = MID + 10, R
|
||||
self._label(96, "橘喵·今日实时", right=f'上次更新 {mao["upd"]}', x0=lx0, x1=lx1)
|
||||
cols = mao["cols"]; n = len(cols)
|
||||
ax0, ax1 = MID + 8, self.W - 8
|
||||
cw = (ax1 - ax0) / n
|
||||
for i in range(1, n):
|
||||
self.vdash(ax0 + cw * i, 128, 192)
|
||||
for i, c in enumerate(cols):
|
||||
cx = ax0 + cw * (i + 0.5)
|
||||
self.ctext(cx, 114, c["k"], self.F12) # 指标名(标题+18)
|
||||
self.ctext(cx, 130, c["v"], self.F24, bold=1) # 数值 伪粗体(+34)
|
||||
self._cmp_line(cx, 160, "环比", c["hb"]) # +64
|
||||
self._cmp_line(cx, 178, "同比", c["tb"]) # +82
|
||||
DIV2_Y = 200
|
||||
y_l = self._usage_block(10, MID - 14, DIV_Y + 6, usage)
|
||||
y_r = self._usage_block(MID + 14, R, DIV_Y + 6, gpt) if gpt else y_l
|
||||
self.vdash(MID, DIV_Y + 6, max(y_l, y_r))
|
||||
DIV2_Y = max(y_l, y_r) + 6
|
||||
self.hdash(8, self.W - 8, DIV2_Y)
|
||||
|
||||
# ---- C. 下段:近 30 天趋势(全宽)----
|
||||
strip = config.E1002_COLOR_STRIP
|
||||
self._trend_chart6(DIV2_Y + 6, self.H - (18 if strip else 8), mao.get("trend") or {})
|
||||
# ---- C. 橘喵·今日实时(全宽三列)----
|
||||
end_c = self._realtime(DIV2_Y + 6, mao)
|
||||
DIV3_Y = end_c + 8
|
||||
self.hdash(8, self.W - 8, DIV3_Y)
|
||||
|
||||
# ---- D. 可选:六色测试色条 ----
|
||||
if strip:
|
||||
# ---- D. 橘喵·近7天 数据表 ----
|
||||
self._week_table(DIV3_Y + 6, mao.get("week"))
|
||||
|
||||
# ---- C. 可选:六色测试色条 ----
|
||||
if config.E1002_COLOR_STRIP:
|
||||
self._color_strip(self.H - 10, self.H - 1)
|
||||
return self.img
|
||||
|
||||
|
||||
Reference in New Issue
Block a user