首页加夜晚模式:右上角月亮/太阳切换,样式由 tools/gen-night.py 自动翻色 (v26100460)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
12
index.html
12
index.html
@@ -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 下边、小孩左边那块太单一,好好设计一下」)。
|
||||
|
||||
22
js/app.js
22
js/app.js
@@ -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); });
|
||||
|
||||
220
tools/gen-night.py
Normal file
220
tools/gen-night.py
Normal 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}')
|
||||
Reference in New Issue
Block a user