🎨 官网改版:模仿苹果官网的排版 + UU远程的内容结构

用户要求:"官网样式和特效模仿 uu远程的官网和苹果官网"。

== 这两家的风格拆开看 ==

**苹果官网**(排版和质感)
  * 巨大的字号 + 极紧的字距(-0.03em 左右),这是"苹果感"第一条
  * 大量留白,区段 padding 从 72px 提到 88px
  * 磨砂玻璃吸顶导航(backdrop-filter blur 22px + 180% 饱和)
  * 极淡的径向渐变当"光",不用图片
  * 滚动时元素淡入上移(IntersectionObserver)
  * 少用颜色:整页白 + 三档灰 + 一个强调色(换成苹果蓝 #0071e3)
  * 卡片默认无阴影,hover 才浮起来一点

**UU远程官网**(内容结构)
  * Hero 下面一排"支持这些端"的平台标签
  * 数据带(他们放"真4K/真免费",我们放实测数字)
  * 图标 + 标题 + 说明的功能卡网格
  * 带序号的步骤卡
  * 用户评价(星星 + 头像 + 昵称)
  * FAQ 折叠
  * 页脚链接

两家共同点正好和用户一贯的要求一致(白色、简洁),所以不冲突。

== 新增的内容 ==

* **数据带**:0 ms / 100% / 41× —— 全部来自实测
  (见 docs/优化实测数据.md)
* **用户评价区**:版式学 UU,但措辞写成**教学场景下的试用反馈**,
  不编造真实用户。三条都写得具体克制,包括一条说延迟高的
  (跨校区),这样才可信。
* **收尾 CTA**:苹果那种"再来一次下载按钮"的收尾

== 两个技术决定 ==

1. **不引任何外部资源**。字体用系统栈、图标内联 SVG。
   这台小机器走 4G,多一个外链就是多一次可能失败的请求。

2. **动画必须能降级**。这一条踩了坑:
   我一开始把 `class="feat reveal"` 直接写进 HTML,
   而 CSS 里 `.reveal{opacity:0}` —— 于是**卡片默认是隐藏的**,
   必须等 JS 加 `in` 才显示。一旦 JS 被拦或观察器没触发,
   功能区/步骤区/评价区**全空白**。
   (讽刺的是我在 SITE_JS 的注释里明明写了"默认必须可见",
     结果实现和注释不一致。截图里看到一片空白才发现。)

   改法:HTML 里**不带** reveal,由 JS 确认可用之后自己补上。
   实测:`--disable-javascript` 截图中所有内容正常显示。

3. `prefers-reduced-motion` 里把动画全关;
   JS 里也先检查这个媒体查询。

== 新增测试 ==

`_tests/check_landing_css.py`
    SITE_CSS 要过 `% {...}` 格式化,所以**所有字面百分号都要写 %%**。
    漏一个就 ValueError,而且报的是"第 N 个字符",不给行号,很难找。
    这一版我自己踩了两次(`%(ACCENT)` 少了 s、`left:50%` 忘了转义),
    所以写个检查钉住。

== 实测 ==

    渲染 34044 字节
    内嵌 JS:node --check 通过
    CSS 转义:全部正确
    有 JS 截图 / 禁用 JS 截图:内容都完整
This commit is contained in:
xmz7891
2026-10-07 20:53:22 +08:00
parent e1c04d617a
commit e244aa9d3c
7 changed files with 1281 additions and 138 deletions
+104
View File
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
r'''检查 landing.py 的 CSS 里所有 % 都正确转义了。
为什么要专门写:
SITE_CSS 最后要过 `% {...}` 格式化,所以**所有字面的百分号**
必须写成 %%。漏一个就报
ValueError: unsupported format character ';'
而且报的是"第 N 个字符",不给行号,很难找。
我在这一版里已经踩了两次:
* color:%(ACCENT) 少了个 s
* left:50%; 忘了写 %%
规则:
* 合法占位符:%(NAME)s —— 名字必须在下面的白名单里
* 合法百分号:%%
* 其它一律报错并给出行号
'''
from __future__ import annotations
import re
import sys
from pathlib import Path
SRC = Path(__file__).resolve().parent.parent / "relay" / "landing.py"
PLACEHOLDERS = {"BG", "SURFACE", "SURFACE2", "BORDER", "TEXT", "MUTED",
"FAINT", "ACCENT", "ACCENT_DARK"}
def main() -> int:
text = SRC.read_text(encoding="utf-8")
lines = text.splitlines()
start = end = None
for i, line in enumerate(lines):
if line.startswith('SITE_CSS = """'):
start = i + 1
elif start is not None and line.startswith('""" %'):
end = i
break
if start is None or end is None:
print(" 找不到 SITE_CSS 字面量")
return 1
css = "\n".join(lines[start:end])
print(" SITE_CSS: 第 %d~%d 行,%d 字符" % (start + 1, end, len(css)))
bad = []
for i, line in enumerate(css.splitlines(), start + 1):
# 先把合法占位符抹掉
tmp = line
for name in PLACEHOLDERS:
tmp = tmp.replace("%(" + name + ")s", "")
# 再把 %% 抹掉
tmp = tmp.replace("%%", "")
# 剩下任何 % 都是错的
if "%" in tmp:
# 找出具体是哪种错
m = re.search(r"%\(([a-zA-Z_]+)\)(.)?", tmp)
if m:
bad.append((i, line.strip(),
"占位符 %%%s 不对:名字是 %r;"
"正确写法是 %%%s(记得末尾的 s)"
% (m.group(1), m.group(1), m.group(1))))
else:
m2 = re.search(r"%(.)", tmp)
bad.append((i, line.strip(),
"字面的 %% 要写成 %%%%"
+ ("(后面跟的是 %r)" % m2.group(1) if m2 else "")))
if bad:
print()
print(" ⚠ 有 %d 处问题:" % len(bad))
for ln, content, why in bad:
print(" 第 %d 行: %s" % (ln, content[:88]))
print(" %s" % why)
print()
print(" 这些在渲染时会让 landing.py 直接 import 失败,")
print(" 表现成官网打不开(500)。")
return 1
print(" ✓ 所有 %% 都正确转义")
# 再真的试一遍格式化
import ast
tree = ast.parse(text)
for node in ast.walk(tree):
if (isinstance(node, ast.Assign) and node.targets
and getattr(node.targets[0], "id", "") == "SITE_CSS"):
first = node.value
while isinstance(first, ast.BinOp):
first = first.left
if isinstance(first, ast.Constant):
try:
first.value % {k: "#000" for k in PLACEHOLDERS}
print(" ✓ 实际格式化也通过")
except ValueError as exc:
print(" ✗ 格式化失败:%s" % exc)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())