首页加夜晚模式:右上角月亮/太阳切换,样式由 tools/gen-night.py 自动翻色 (v26100460)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 11:48:54 +00:00
parent e9cfa10fca
commit 57e845eb2b
4 changed files with 1568 additions and 2 deletions

View File

@@ -7,8 +7,11 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>兜爸心选 · 孕育陪伴</title>
<!-- V:26100459 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<!-- V:26100460 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<link rel="stylesheet" href="mama-box.css">
<link rel="stylesheet" href="night.css">
<!-- 夜晚模式:首屏绘制前就按上次的选择加 class,避免先白后黑闪一下(开关在首页右上角 #nightBtn,逻辑见 app.js) -->
<script>try{if(localStorage.getItem('mmbox.night.v1')==='1'){document.documentElement.classList.add('night');document.querySelector('meta[name=theme-color]').content='#2A2340'}}catch(e){}</script>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- 共享助手:授权/跳商城/位置上报(分享卡片同步,见 docs/h5-auth-standard.md 第四节) -->
<script src="js/wx-auth.js"></script>
@@ -102,6 +105,13 @@
所以抠成白色透明 PNG(按红色浓度转 alpha,3x 高清),和旁边白字 slogan、宝宝插画同一套白色语言。 -->
<h1 class="hero-brand"><img class="hero-logo" src="assets/img/db2f99232b39.png" alt="兜爸心选" width="107" height="30"></h1>
<p class="hero-slogan">全家都爱用的孕育平台</p>
<!-- 2026-10-05 夜晚模式开关(用户:「做一个夜晚模式,右上角有个切换按钮」) -->
<div class="hero-actions">
<button class="icon-btn glass night-btn" id="nightBtn" aria-label="切换夜晚模式" aria-pressed="false">
<svg class="nb-moon" viewBox="0 0 24 24" width="18" height="18"><path d="M20.2 14.6A8.3 8.3 0 0 1 9.4 3.8a8.3 8.3 0 1 0 10.8 10.8Z"/></svg>
<svg class="nb-sun" viewBox="0 0 24 24" width="19" height="19"><circle cx="12" cy="12" r="4.3"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.6v2.2M12 19.2v2.2M2.6 12h2.2M19.2 12h2.2M5.4 5.4l1.5 1.5M17.1 17.1l1.5 1.5M5.4 18.6l1.5-1.5M17.1 6.9l1.5-1.5"/></g></svg>
</button>
</div>
</div>
<!-- 2026-10-01 头部左侧改版(用户:「LOGO 下边、小孩左边那块太单一,好好设计一下」)。

View File

@@ -3,7 +3,7 @@
(function () {
'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
var VER = '26100459';
var VER = '26100460';
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
@@ -559,6 +559,26 @@
}
$$('[data-sheet="open"]').forEach(b => b.addEventListener('click', openSheet));
/* 「今日脚印」右上角掀角里的祈福树 → 宝宝祈福树(2026-10-05)。stopPropagation:卡片本身点了会跳档案。走业务域名反代 /qifu-shu/,jumpApp 负责带身份和上报。 */
/* 夜晚模式开关(2026-10-05):<html class="night"> + localStorage 'mmbox.night.v1',样式全在 night.css。
首屏的 class 由 index.html <head> 的内联脚本先加好,这里只管点击切换和按钮状态。 */
const NIGHT_KEY = 'mmbox.night.v1';
const nb = $('#nightBtn');
const syncNightBtn = () => {
const on = document.documentElement.classList.contains('night');
if (nb) { nb.setAttribute('aria-pressed', on ? 'true' : 'false'); nb.setAttribute('aria-label', on ? '切换到白天模式' : '切换到夜晚模式'); }
const tc = document.querySelector('meta[name=theme-color]'); if (tc) tc.content = on ? '#2A2340' : '#F4593C';
};
syncNightBtn();
if (nb) nb.addEventListener('click', e => {
e.stopPropagation();
const root = document.documentElement;
root.classList.add('nt-anim');
const on = root.classList.toggle('night');
try { localStorage.setItem(NIGHT_KEY, on ? '1' : '0'); } catch (err) {}
syncNightBtn();
setTimeout(() => root.classList.remove('nt-anim'), 450);
});
const QIFU_URL = location.origin + '/qifu-shu/';
const fq = $('#fpQifu');
if (fq) fq.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); jumpApp(QIFU_URL); });

1316
night.css Normal file

File diff suppressed because it is too large Load Diff

220
tools/gen-night.py Normal file
View File

@@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""夜晚模式样式生成器:读 mama-box.css,把浅色底 / 深色字自动翻成夜色,写进 night.css 的「自动生成」段。
为什么用生成而不是手写:mama-box.css 有 4700 行、几百处写死的 #fff / 暖白,手写一份夜间版
既漏得多、又会和日间版各改各的。这里只做「颜色翻转」这一件机械的事:
· 背景 / 边框类属性里,亮且不太艳的颜色(暖白、米色、浅灰线)→ 同色相的深色(卡片比页面底略亮);
· color / fill / stroke 里,暗的颜色(正文墨色、灰字)→ 同色相的浅色;
· 品牌橙、工具卡的绿 / 紫 / 蓝这些高饱和色、白字、透明度低的玻璃白,一律不动。
每条规则原样复制选择器、前面加 html.night,所以优先级比日间版恰好高一级,顺序关系不变。
自动翻不好的地方写在 night.css 的「手调」段(在生成段之后,同优先级后写的赢)。
用法:python3 tools/gen-night.py [mama-box.css] 改了 mama-box.css 的颜色后重跑一次。
"""
import re, sys, colorsys, pathlib
ROOT = pathlib.Path(__file__).resolve().parent.parent
SRC = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / 'mama-box.css'
OUT = ROOT / 'night.css'
MARK_A = '/* ==== 自动生成段:tools/gen-night.py 写入,别手改 ==== */'
MARK_B = '/* ==== 自动生成段结束 ==== */'
BG_PROPS = {'background', 'background-color', 'background-image', 'border', 'border-color', 'border-top',
'border-bottom', 'border-left', 'border-right', 'border-top-color', 'border-bottom-color',
'border-left-color', 'border-right-color', 'outline', 'outline-color', 'box-shadow', 'caret-color'}
FG_PROPS = {'color', 'fill', 'stroke', '-webkit-text-fill-color', 'text-decoration-color'}
SKIP_SEL = re.compile(r'home-hero|hero-|\.hi-|\.hd-|\.letter-card|zd-|ar-hero|page-hero|::selection|\.skip')
COL = re.compile(r'#([0-9a-fA-F]{3,8})\b|rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+%?)\s*)?\)')
def parse_hex(h):
if len(h) in (3, 4):
h = ''.join(c * 2 for c in h)
r, g, b = (int(h[i:i + 2], 16) / 255 for i in (0, 2, 4))
a = int(h[6:8], 16) / 255 if len(h) == 8 else 1
return r, g, b, a
def fmt(r, g, b, a):
R, G, B = (round(max(0, min(1, x)) * 255) for x in (r, g, b))
return f'#{R:02X}{G:02X}{B:02X}' if a >= .999 else f'rgba({R},{G},{B},{round(a, 3)})'
NIGHT_H = 280 / 360
def bg_map(r, g, b, a):
h, l, s = colorsys.rgb_to_hls(r, g, b)
if l < .80: # 艳色 / 中间调:品牌色、工具卡底,不动(浅粉浅黄这类高饱和的淡色照样翻)
return None
if a < .2: # 几乎透明的白:只是渐变收尾,不动
return None
nl = max(.115, .17 - (1 - l) * .5) # #fff→.17(卡片) 米色浅底 .93→.135 浅线→.12;页底另由 --bg 定
if s < .3: # 近中性的暖白 / 灰:统一染成夜色的一点紫,不然一片死灰
h, ns = NIGHT_H, .10
else:
ns = min(s, .5) * .45
return fmt(*colorsys.hls_to_rgb(h, nl, ns), a)
def fg_map(r, g, b, a):
h, l, s = colorsys.rgb_to_hls(r, g, b)
if l > .55 or (s > .55 and l > .3): # 已经够亮 / 艳色强调字,不动
return None
nl = min(.93, .95 - l * .55) # 墨色 .15→.87 灰字 .42→.72
ns = min(s, .3) * .5
if s < .3:
h, ns = 25 / 360, .12 # 字色带一点暖,跟日间的暖墨色同一气质
return fmt(*colorsys.hls_to_rgb(h, nl, ns), a)
def vivid(val):
"""这条背景里有没有「主色」(品牌橙、工具卡的绿紫蓝):有就整条不动,否则渐变里的白高光被翻黑、主色还在,成了脏灰。"""
for m in COL.finditer(val):
if m.group(1):
if len(m.group(1)) not in (3, 4, 6, 8):
continue
r, g, b, a = parse_hex(m.group(1))
else:
r, g, b = (float(m.group(i)) / 255 for i in (2, 3, 4))
a = m.group(5); a = 1 if a is None else (float(a[:-1]) / 100 if a.endswith('%') else float(a))
h, l, s = colorsys.rgb_to_hls(r, g, b)
if a > .5 and s > .45 and .3 < l < .78:
return True
return False
def recolor(val, fn):
changed = False
def sub(m):
nonlocal changed
if m.group(1):
if len(m.group(1)) not in (3, 4, 6, 8):
return m.group(0)
c = parse_hex(m.group(1))
else:
a = m.group(5)
a = 1 if a is None else (float(a[:-1]) / 100 if a.endswith('%') else float(a))
c = (float(m.group(2)) / 255, float(m.group(3)) / 255, float(m.group(4)) / 255, a)
n = fn(*c)
if n is None:
return m.group(0)
changed = True
return n
out = COL.sub(sub, val)
return out if changed else None
def map_keyword(prop, val):
v = val.strip().lower().replace('!important', '').strip()
if prop in BG_PROPS and v in ('white',):
return val.lower().replace('white', '#2A2530')
return None
def strip_comments(s):
return re.sub(r'/\*.*?\*/', '', s, flags=re.S)
def split_decls(body):
out, depth, cur = [], 0, ''
for ch in body:
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
if ch == ';' and depth == 0:
out.append(cur); cur = ''
else:
cur += ch
out.append(cur)
return [d.strip() for d in out if d.strip()]
def prefix_sel(sel):
parts, depth, cur = [], 0, ''
for ch in sel:
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
if ch == ',' and depth == 0:
parts.append(cur); cur = ''
else:
cur += ch
parts.append(cur)
res = []
for p in parts:
p = p.strip()
if p.startswith(':root'):
continue
if re.match(r'html\b', p):
p = 'html.night' + p[4:]
else:
p = 'html.night ' + p
res.append(p)
return ','.join(res)
def walk(css):
"""返回 [(prelude 或 None, 内容)]。嵌套 @media / @supports 递归,@keyframes / @font-face 跳过。"""
i, n, out = 0, len(css), []
while i < n:
j = css.find('{', i)
if j < 0:
break
pre = css[i:j].strip()
depth, k = 1, j + 1
while k < n and depth:
if css[k] == '{':
depth += 1
elif css[k] == '}':
depth -= 1
k += 1
body = css[j + 1:k - 1]
i = k
if pre.startswith('@'):
if re.match(r'@(media|supports)', pre):
inner = walk(body)
if inner:
out.append(pre + '{\n' + '\n'.join(inner) + '\n}')
continue
# ⚠️ 所有规则的**所有**颜色声明(含没翻的)都抄一份:
# 日间版里后写的规则会盖掉先写的;只抄翻过色的那几条,先写的规则加了 html.night 反而赢过后写的(踩过:工具卡变灰)。
# 同理,头图那些不翻色的规则也照抄,免得被前面翻过色的通用规则压住。
skip = bool(SKIP_SEL.search(pre))
changed = skip
decls = []
for d in split_decls(body):
if ':' not in d or d.startswith('--'):
continue
prop, val = d.split(':', 1)
prop = prop.strip().lower()
if prop not in BG_PROPS and prop not in FG_PROPS:
continue
nv = None
if not skip and prop in BG_PROPS and not vivid(val):
nv = recolor(val, bg_map) or map_keyword(prop, val)
elif not skip and prop in FG_PROPS:
nv = recolor(val, fg_map)
if nv:
changed = True
decls.append(f'{prop}:{(nv or val).strip()}')
sel = prefix_sel(pre)
if decls and sel: # 没翻色的也抄:它可能是在覆盖前面某条翻过色的规则(工具卡彩色底就是这么丢的)
out.append(sel + '{' + ';'.join(decls) + '}')
return out
rules = walk(strip_comments(SRC.read_text()))
gen = MARK_A + '\n' + '\n'.join(rules) + '\n' + MARK_B
old = OUT.read_text() if OUT.exists() else ''
if MARK_A in old:
a, b = old.index(MARK_A), old.index(MARK_B) + len(MARK_B)
new = old[:a] + gen + old[b:]
else:
new = old + '\n' + gen + '\n'
OUT.write_text(new)
print(f'{len(rules)} 条规则 → {OUT}')