mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 14:50:45 +00:00
用户要求:"官网样式和特效模仿 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 截图:内容都完整
82 lines
2.6 KiB
Python
82 lines
2.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
r'''找出 SITE_CSS 里没转义的 %(% 格式化会把它们当占位符)。
|
||
|
||
规则:既然 SITE_CSS 最后要过 `% {...}`,那么**所有字面的百分号**
|
||
都必须写成 %%。CSS 里最常见的漏网之鱼是:
|
||
width:50% 渐变里的 60% 60%
|
||
rgba(...) 不会
|
||
'''
|
||
from __future__ import annotations
|
||
|
||
import ast
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
SRC = Path(__file__).resolve().parent.parent / "relay" / "landing.py"
|
||
text = SRC.read_text(encoding="utf-8")
|
||
tree = ast.parse(text)
|
||
|
||
target = None
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.Assign):
|
||
for t in node.targets:
|
||
if isinstance(t, ast.Name) and t.id == "SITE_CSS":
|
||
target = node.value
|
||
break
|
||
|
||
if target is None:
|
||
print(" 找不到 SITE_CSS 赋值")
|
||
sys.exit(1)
|
||
|
||
# 第一段是那个大三引号字符串(后面还接了 showcase)
|
||
first = target
|
||
while isinstance(first, ast.BinOp):
|
||
first = first.left
|
||
if isinstance(first, ast.Constant):
|
||
css = first.value
|
||
offset = first.col_offset
|
||
print(" 找到 SITE_CSS 的字面量部分:%d 字符,起始行 %d"
|
||
% (len(css), first.lineno))
|
||
else:
|
||
print(" 结构不认识:%s" % type(first).__name__)
|
||
sys.exit(1)
|
||
|
||
# 逐行找单个 % (不是 %%)
|
||
print()
|
||
print(" ════ 可疑的单 % ════")
|
||
bad = 0
|
||
for i, line in enumerate(css.splitlines(), 1):
|
||
# 去掉 %(name)s 这类合法占位符,再看还有没有单 %
|
||
stripped = line
|
||
import re
|
||
stripped = re.sub(r"%\([a-zA-Z_]+\)[sd]", "", stripped)
|
||
# 现在剩下的 % 都必须是 %%
|
||
for m in re.finditer(r"%(.)", stripped):
|
||
if m.group(1) != "%":
|
||
print(" 第 %d 行: ...%s..." % (i, line.strip()[:90]))
|
||
print(" 这里有个单 %% 后面跟了 %r" % m.group(1))
|
||
bad += 1
|
||
break
|
||
|
||
if not bad:
|
||
print(" (没找到)")
|
||
|
||
# 直接试一下格式化,把出错位置算出来
|
||
print()
|
||
print(" ════ 试格式化 ════")
|
||
try:
|
||
css % {"BG": "#fff", "SURFACE": "#fafafa", "SURFACE2": "#f5f5f5",
|
||
"BORDER": "#eee", "TEXT": "#111", "MUTED": "#666",
|
||
"FAINT": "#999", "ACCENT": "#07e", "ACCENT_DARK": "#06d"}
|
||
print(" ✓ 能格式化")
|
||
except ValueError as exc:
|
||
print(" ✗ %s" % exc)
|
||
# 把出错位置附近打出来
|
||
import re
|
||
m = re.search(r"index (\d+)", str(exc))
|
||
if m:
|
||
pos = int(m.group(1))
|
||
lo = max(0, pos - 90)
|
||
print(" 位置 %d 附近:" % pos)
|
||
print(" ...%s..." % css[lo:pos + 30].replace("\n", "\\n"))
|