#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 方案二:800x480 的**现代网页**版仪表盘(server.py 的 HTML_PATH 每次请求现生成),供 SenseCraft HTML 控件用无头浏览器截图。 与 PNG 版(renderer_e1002)的差别: - 不限点阵字体:系统中文字体栈(PingFang / Microsoft YaHei / Noto Sans SC)+ Google Fonts 的 Noto Sans SC 兜底 (平台截图机若无中文字体会掉字,网络字体是保险;能否加载取决于其出网能力)。 - 纯 HTML+CSS+内联 SVG,**不依赖 JavaScript**(平台是否执行 JS 未证实),数据全部服务端写死进 HTML。 - 颜色只用六色纯值 #000/#fff/#f00/#0f0(黄/蓝未用),抗锯齿边缘交给平台抖动——这正是要和 PNG 版对比的点。 - 版式与 PNG 版同构:顶部时钟/天气;左 Claude Usage、右 Codex Usage;橘喵今日实时三列;BTC 时 K(SVG)。 - 配色规则与 PNG 版一致:用量条 ≥warn 红 / ≤ok 绿 / 其间黑;pace ≥+tol 红 / ≤−tol 绿 / 其间黑;环比同比 涨绿 跌红(与 K 线一致);K 线 涨绿 跌红。 """ from html import escape as _e RED, GREEN, BLACK = "#f00", "#0f0", "#000" _CSS = """ *{box-sizing:border-box;margin:0;padding:0} html,body{width:800px;height:480px;overflow:hidden;background:#fff;color:#000; font-family:"PingFang SC","Microsoft YaHei","Noto Sans SC","Helvetica Neue",Arial,sans-serif; -webkit-font-smoothing:antialiased} .page{width:800px;height:480px;padding:12px 16px 10px;display:flex;flex-direction:column;gap:6px} .hd{display:flex;justify-content:space-between;align-items:flex-end;height:92px;border-bottom:2px solid #000;padding-bottom:6px} .clock{font-size:72px;font-weight:800;line-height:1;letter-spacing:-2px;font-variant-numeric:tabular-nums} .date{font-size:15px;margin-top:4px;color:#000} .wx{text-align:right;display:flex;align-items:flex-end;gap:14px} .wx .t{font-size:44px;font-weight:800;line-height:1} .wx .c{font-size:15px;line-height:1.45} .wx .loc{font-size:13px;color:#000} .pin{display:inline-block;width:9px;height:9px;border-radius:50% 50% 50% 0;background:#f00;transform:rotate(-45deg);margin-right:4px;vertical-align:-1px} .row{display:flex;gap:16px} .card{flex:1;min-width:0} .card h3{font-size:14px;font-weight:700;display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px} .card h3 em{font-style:normal;font-weight:400;font-size:12px} .card h3 b::before{content:"";display:inline-block;width:9px;height:9px;background:#000;margin-right:6px;vertical-align:-1px} .u{display:grid;grid-template-columns:64px 1fr 58px 44px;align-items:center;column-gap:8px;height:26px} .u .k{font-size:13px;font-weight:700;font-family:"SF Mono",Menlo,Consolas,monospace} .bar{height:11px;border:1.5px solid #000;position:relative;background:#fff} .bar i{position:absolute;left:0;top:0;bottom:0;background:#000} .u .p{font-size:19px;font-weight:800;text-align:right;font-variant-numeric:tabular-nums;line-height:1} .u .pace{font-size:13px;font-weight:700;font-variant-numeric:tabular-nums;white-space:nowrap} .tri{display:inline-block;width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;vertical-align:1px;margin-right:2px} .tri.up{border-bottom:8px solid currentColor}.tri.dn{border-top:8px solid currentColor} .rt{border-top:2px solid #000;border-bottom:2px solid #000;padding:6px 0 4px} .rt .cols{display:flex} .rt .col{flex:1;text-align:center;border-left:1px dashed #000;padding:2px 0} .rt .col:first-child{border-left:0} .rt .name{font-size:13px;margin-bottom:2px} .rt .grp{display:inline-flex;align-items:center;gap:12px} .rt .v{font-size:36px;font-weight:800;line-height:1;font-variant-numeric:tabular-nums} .rt .cmp{text-align:left;font-size:12px;line-height:1.5;font-variant-numeric:tabular-nums} .btc h3{margin-bottom:2px} .btc svg{display:block} """ def _tier_bar(bar, usage): """用量条颜色三档 绿/黑/红(与 renderer_e1002 一致)。返回 (填充/边框色, 百分比文字色),两者同色。""" pct = bar.get("pct") if not isinstance(pct, (int, float)): 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): tol = usage.get("pace_tol", 10) return RED if diff >= tol else GREEN if diff <= -tol else BLACK def _usage_card(usage): 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") ok = isinstance(pct, (int, float)) fill, ptxt = _tier_bar(b, usage) width = max(0, min(100, int(pct))) if ok else 0 pace = "" if ok and "time_pct" in b: diff = int(pct) - int(b["time_pct"]) col = _tier_pace(diff, usage) tri = '' if diff > 0 else ('' if diff < 0 else "") pace = f'{tri}{abs(diff):02d}' else: pace = '' rows.append( f'
{_e(b.get("label") or b["k"])}' f'' f'{f"{int(pct):02d}%" if ok else "--"}{pace}
') return (f'

{_e(usage.get("title", "Usage"))}{_e(note)}

' + "".join(rows) + "
") def _cmp(label, pair): direction, val = pair col = GREEN if direction == "up" else RED if direction == "down" else BLACK tri = '' if direction == "up" else ('' if direction == "down" else "") return f'
{_e(label)} {tri}{_e(val)}
' def _realtime(mao): cols = "".join( f'
{_e(c["k"])}
{_e(c["v"])}' f'{_cmp("环比", c["hb"])}{_cmp("同比", c["tb"])}
' for c in mao["cols"]) return (f'

橘喵·今日实时上次更新 {_e(mao["upd"])}

' f'
{cols}
') def _fmt_price(v): return f"{v:,.0f}" if abs(v) >= 1000 else f"{v:.2f}" def _btc(btc, W=768, H=118): """BTC K 线 → 内联 SVG(涨绿跌红),坐标逻辑同 renderer_e1002._btc_chart。""" btc = btc or {} candles = btc.get("candles") or [] kind = "时K" if btc.get("bar", "1H") == "1H" else "日K" head = f'{_e(btc.get("symbol") or "BTC/USDT")}·{kind}' right = "" if candles and btc.get("last") is not None: chg = btc.get("chg") or 0.0 col = GREEN if chg >= 0 else RED tri = '' if chg >= 0 else '' right = (f'{_fmt_price(btc["last"])}  {_e(btc.get("chg_label") or "")}' f' {tri}{abs(chg):.2f}%') if not candles: return f'

{head}

--
' LBL, TOP, BOT = 66, 6, 16 px0, px1 = 0, W - LBL py0, py1 = TOP, H - BOT hi_raw, lo_raw = max(c["h"] for c in candles), min(c["l"] for c in candles) pad = (hi_raw - lo_raw or 1) * 0.04 lo, hi = lo_raw - pad, hi_raw + pad Y = lambda v: py1 - (v - lo) / (hi - lo) * (py1 - py0) n = len(candles) slot = (px1 - px0) / n bw = max(3, int(slot * 0.6)) | 1 parts = [f'', f''] for i, c in enumerate(candles): cx = round(px0 + slot * (i + 0.5)) col = GREEN if c["c"] >= c["o"] else RED top, bot = sorted((round(Y(c["o"])), round(Y(c["c"])))) parts.append(f'') parts.append(f'') last = candles[-1]["c"] ly = round(Y(last)) parts.append(f'') hi_y, lo_y = py0 + 5, py1 - 1 ty = min(max(ly + 4, hi_y + 13), lo_y - 13) txt = 'font-size="12" font-family="SF Mono,Menlo,Consolas,monospace" text-anchor="end"' parts.append(f'{_fmt_price(hi_raw)}') parts.append(f'{_fmt_price(lo_raw)}') parts.append(f'{_fmt_price(last)}') lab = lambda c: _e(c.get("t") or c.get("d") or "") ax = 'font-size="11" font-family="SF Mono,Menlo,Consolas,monospace"' parts.append(f'{lab(candles[0])}') parts.append(f'{lab(candles[n // 2])}') parts.append(f'{lab(candles[-1])}') parts.append("") return f'

{head}{right}

{"".join(parts)}
' def render_html(d): """data dict(同 renderer_e1002.render 的输入)→ 完整 HTML 字符串。""" dt, wt = d["date"], d["weather"] head = ( f'
{_e(dt["time"])}
' f'
{_e(dt["week"])}  {_e(dt["greg"])}
' f'
{_e(wt["loc"])}
' f'{_e(wt["cond"])}
{_e(wt["range"])}
{_e(wt["temp"])}
') body = (head + f'
{_usage_card(d["usage"])}{_usage_card(d.get("usage_gpt") or {"title": "Codex Usage"})}
' + _realtime(d["mao"]) + _btc(d.get("btc"))) return ("" "" "" "" "" f"eink dashboard
{body}
")