mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 12:30:44 +00:00
🎨 官网改版:模仿苹果官网的排版 + 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:
@@ -0,0 +1,68 @@
|
|||||||
|
🎨 官网改版:模仿苹果官网的排版 + 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 截图:内容都完整
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# -*- 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"))
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r'''直接把 SITE_CSS 那段字符串抠出来,报出出错位置附近的内容。'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SRC = Path(__file__).resolve().parent.parent / "relay" / "landing.py"
|
||||||
|
lines = SRC.read_text(encoding="utf-8").splitlines()
|
||||||
|
|
||||||
|
# 找 SITE_CSS = """ 到对应的 """
|
||||||
|
start = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith("SITE_CSS = \"\"\""):
|
||||||
|
start = i + 1
|
||||||
|
break
|
||||||
|
if start is None:
|
||||||
|
print(" 找不到 SITE_CSS")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 找结束的 """
|
||||||
|
end = None
|
||||||
|
for i in range(start, len(lines)):
|
||||||
|
if lines[i].startswith('""" %'):
|
||||||
|
end = i
|
||||||
|
break
|
||||||
|
if end is None:
|
||||||
|
print(" 找不到结束")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
css = "\n".join(lines[start:end])
|
||||||
|
print(" SITE_CSS 字面量: 第 %d~%d 行,共 %d 字符" % (start + 1, end, len(css)))
|
||||||
|
|
||||||
|
# 试格式化
|
||||||
|
PLACE = {"BG": "#fff", "SURFACE": "#fafafa", "SURFACE2": "#f5f5f5",
|
||||||
|
"BORDER": "#eee", "TEXT": "#111", "MUTED": "#666",
|
||||||
|
"FAINT": "#999", "ACCENT": "#07e", "ACCENT_DARK": "#06d"}
|
||||||
|
try:
|
||||||
|
css % PLACE
|
||||||
|
print(" ✓ 能格式化 —— 那问题在别处")
|
||||||
|
except ValueError as exc:
|
||||||
|
print(" ✗ %s" % exc)
|
||||||
|
m = re.search(r"index (\d+)", str(exc))
|
||||||
|
if m:
|
||||||
|
pos = int(m.group(1))
|
||||||
|
lo = max(0, pos - 120)
|
||||||
|
seg = css[lo:pos + 40]
|
||||||
|
print()
|
||||||
|
print(" ════ 出错位置 %d 附近 ════" % pos)
|
||||||
|
# 算出这是第几行
|
||||||
|
line_no = css[:pos].count("\n") + start + 1
|
||||||
|
print(" (大约在第 %d 行)" % line_no)
|
||||||
|
print()
|
||||||
|
for k, seg_line in enumerate(seg.split("\n")):
|
||||||
|
mark = " <<<<" if k == seg.count("\n") - 1 or (lo + sum(len(x) + 1 for x in seg.split("\n")[:k]) <= pos < lo + sum(len(x) + 1 for x in seg.split("\n")[:k + 1])) else ""
|
||||||
|
print(" %s%s" % (seg_line, mark))
|
||||||
|
|
||||||
|
# 另外全文列一遍「单 % 后跟非 % 非 ( 的地方」
|
||||||
|
print()
|
||||||
|
print(" ════ CSS 里的单 % (排除 %(name)s 和 %% )════")
|
||||||
|
found = 0
|
||||||
|
for i, line in enumerate(css.splitlines(), start + 1):
|
||||||
|
tmp = re.sub(r"%\([a-zA-Z_]+\)[sd]", "", line)
|
||||||
|
for m in re.finditer(r"%(.)", tmp):
|
||||||
|
if m.group(1) != "%":
|
||||||
|
print(" 第 %d 行: %s" % (i, line.strip()[:100]))
|
||||||
|
found += 1
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
print(" (没找到)")
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r'''把 HTML 模板里的 `reveal` 类去掉。
|
||||||
|
|
||||||
|
== 为什么 ==
|
||||||
|
|
||||||
|
我在 HTML 里写了:
|
||||||
|
<div class="feat reveal">
|
||||||
|
而 CSS 是:
|
||||||
|
.reveal{opacity:0;transform:translateY(22px)}
|
||||||
|
.reveal.in{opacity:1;transform:none}
|
||||||
|
|
||||||
|
于是**这些卡片默认是隐藏的**,必须等 JS 跑起来加上 `in` 才显示。
|
||||||
|
一旦 JS 被拦截 / 报错 / IntersectionObserver 没触发 ——
|
||||||
|
整页功能区、步骤区、评价区**全空白**。
|
||||||
|
|
||||||
|
这个坑我在 SITE_JS 的注释里明明写了
|
||||||
|
("默认必须是可见的,否则脚本一旦没跑起来整页就是空白"),
|
||||||
|
结果 HTML 里还是把 reveal 写死了 —— 注释和实现不一致。
|
||||||
|
|
||||||
|
正确做法:
|
||||||
|
* HTML 里**不带** reveal(默认可见)
|
||||||
|
* JS 确认可用之后,自己补上 reveal,再注册观察器
|
||||||
|
* 这样没 JS = 正常显示;有 JS = 滚动淡入
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SRC = Path(__file__).resolve().parent.parent / "relay" / "landing.py"
|
||||||
|
|
||||||
|
# (原样, 改成) —— 逐个精确替换,不用正则免得误伤 CSS
|
||||||
|
PAIRS = [
|
||||||
|
('<div class="dl-card reveal">', '<div class="dl-card">'),
|
||||||
|
('<div class="feat reveal"%s>', '<div class="feat"%s>'),
|
||||||
|
('<div class="step reveal">', '<div class="step">'),
|
||||||
|
('<div class="stat reveal">', '<div class="stat">'),
|
||||||
|
('<figure class="quote reveal">', '<figure class="quote">'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
text = SRC.read_text(encoding="utf-8")
|
||||||
|
ok = True
|
||||||
|
for old, new in PAIRS:
|
||||||
|
if old not in text:
|
||||||
|
print(" ✗ 找不到:%s" % old)
|
||||||
|
ok = False
|
||||||
|
continue
|
||||||
|
if text.count(old) != 1:
|
||||||
|
print(" ! %s 出现 %d 次" % (old, text.count(old)))
|
||||||
|
text = text.replace(old, new)
|
||||||
|
print(" ✓ %s -> %s" % (old, new))
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
print()
|
||||||
|
print(" 有锚点没匹配,没有写回")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# 再确认一遍 HTML 里没有裸的 reveal
|
||||||
|
leftovers = re.findall(r'class="[^"]*\breveal\b[^"]*"', text)
|
||||||
|
print()
|
||||||
|
if leftovers:
|
||||||
|
print(" ⚠ HTML 里还有 reveal:")
|
||||||
|
for item in leftovers:
|
||||||
|
print(" %s" % item)
|
||||||
|
return 1
|
||||||
|
print(" ✓ HTML 模板里没有裸的 reveal 了(默认可见)")
|
||||||
|
|
||||||
|
SRC.write_text(text, encoding="utf-8", newline="\n")
|
||||||
|
print(" 已写回")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<title>ykxmz · 用手表管教室那台电脑</title>
|
||||||
|
<meta name="description" content="老师用手表管教室里那台电脑:放课件、投图、下课关机,不用走回讲台。同 WiFi 时毫秒级直连,不经服务器。">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
|
||||||
|
|
||||||
|
/* 苹果的字体栈:优先系统 UI 字体,中文落到雅黑/苹方。
|
||||||
|
字距收紧是"苹果感"最关键的一条 —— 大标题用 -0.03em 左右。 */
|
||||||
|
body{font-family:-apple-system,BlinkMacSystemFont,"SF Pro SC","SF Pro Text",
|
||||||
|
"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
|
||||||
|
background:#ffffff;color:#1d1d1f;line-height:1.6;
|
||||||
|
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
|
||||||
|
a{color:inherit;text-decoration:none}
|
||||||
|
img,svg{display:block}
|
||||||
|
.wrap{max-width:1040px;margin:0 auto;padding:0 22px}
|
||||||
|
.narrow{max-width:760px;margin:0 auto}
|
||||||
|
|
||||||
|
/* ---------------- 磨砂吸顶导航(苹果那个味道)----------------
|
||||||
|
.72 透明 + 22px 模糊 + 180% 饱和,滚动时下层内容会"透出来但糊掉" */
|
||||||
|
nav{position:sticky;top:0;z-index:60;
|
||||||
|
background:rgba(255,255,255,.72);
|
||||||
|
-webkit-backdrop-filter:saturate(180%) blur(22px);
|
||||||
|
backdrop-filter:saturate(180%) blur(22px);
|
||||||
|
border-bottom:1px solid rgba(0,0,0,.07)}
|
||||||
|
.nav-in{display:flex;align-items:center;justify-content:space-between;
|
||||||
|
height:48px}
|
||||||
|
.brand{display:flex;align-items:center;gap:8px;font-weight:600;
|
||||||
|
font-size:15px;letter-spacing:-.01em}
|
||||||
|
.nav-links{display:flex;gap:26px;font-size:12.5px;color:rgba(0,0,0,.62)}
|
||||||
|
.nav-links a{transition:color .2s}
|
||||||
|
.nav-links a:hover{color:#1d1d1f}
|
||||||
|
@media(max-width:700px){.nav-links{display:none}}
|
||||||
|
|
||||||
|
/* ---------------- Hero ----------------
|
||||||
|
clamp 让字号在手机上也不至于爆掉。
|
||||||
|
背后一层极淡的径向渐变当"光",不用图片。 */
|
||||||
|
.hero{position:relative;padding:104px 0 84px;text-align:center;
|
||||||
|
overflow:hidden}
|
||||||
|
.hero::before{content:"";position:absolute;inset:-40% -20% auto -20%;
|
||||||
|
height:520px;pointer-events:none;
|
||||||
|
background:radial-gradient(60% 60% at 50% 0%,
|
||||||
|
rgba(0,113,227,.10),rgba(0,113,227,0) 70%)}
|
||||||
|
.hero>*{position:relative}
|
||||||
|
.hero h1{font-size:clamp(34px,6.4vw,68px);line-height:1.06;
|
||||||
|
letter-spacing:-.032em;font-weight:700;margin-bottom:20px}
|
||||||
|
.hero h1 .grad{background:linear-gradient(90deg,#0a84ff,#3aa0f5 50%,#64b5f6);
|
||||||
|
-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||||
|
.hero .lede{font-size:clamp(16px,2vw,21px);color:#6e6e73;
|
||||||
|
max-width:620px;margin:0 auto 32px;line-height:1.55;
|
||||||
|
letter-spacing:-.01em}
|
||||||
|
.cta{display:flex;gap:14px;justify-content:center;flex-wrap:wrap}
|
||||||
|
.btn{display:inline-flex;align-items:center;gap:7px;
|
||||||
|
padding:12px 26px;border-radius:980px;font-weight:500;font-size:15px;
|
||||||
|
letter-spacing:-.01em;transition:all .22s cubic-bezier(.4,0,.2,1)}
|
||||||
|
.btn-p{background:#0071e3;color:#fff}
|
||||||
|
.btn-p:hover{background:#0077ed;transform:translateY(-1px);
|
||||||
|
box-shadow:0 8px 22px rgba(0,113,227,.28)}
|
||||||
|
.btn-s{background:rgba(0,0,0,.05);color:#1d1d1f}
|
||||||
|
.btn-s:hover{background:rgba(0,0,0,.09)}
|
||||||
|
|
||||||
|
/* 平台行(UU远程那种"支持这些端"的一排) */
|
||||||
|
.plats{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;
|
||||||
|
margin-top:34px}
|
||||||
|
.plat{display:inline-flex;align-items:center;gap:7px;
|
||||||
|
padding:7px 15px;border-radius:980px;background:#f5f5f7;
|
||||||
|
color:#6e6e73;font-size:13px;font-weight:500}
|
||||||
|
.plat svg{width:15px;height:15px}
|
||||||
|
.badge{display:inline-block;margin-top:20px;padding:5px 15px;
|
||||||
|
border-radius:980px;background:rgba(0,113,227,.08);
|
||||||
|
color:#0071e3;font-size:12.5px;font-weight:600}
|
||||||
|
|
||||||
|
/* 滚动提示 —— 做明显一点,不然等于没做 */
|
||||||
|
.scroll-hint{margin:56px auto 0;width:26px;height:42px;
|
||||||
|
border:2px solid rgba(0,0,0,.22);border-radius:14px;
|
||||||
|
position:relative}
|
||||||
|
.scroll-hint::after{content:"";position:absolute;left:50%;top:8px;
|
||||||
|
width:4px;height:8px;margin-left:-2px;border-radius:2px;
|
||||||
|
background:rgba(0,0,0,.42);animation:sd 1.7s ease-in-out infinite}
|
||||||
|
@keyframes sd{0%{opacity:0;transform:translateY(0)}
|
||||||
|
40%{opacity:1}
|
||||||
|
80%{opacity:0;transform:translateY(13px)}
|
||||||
|
100%{opacity:0}}
|
||||||
|
|
||||||
|
/* ---------------- 通用区段 ---------------- */
|
||||||
|
section{padding:88px 0}
|
||||||
|
.alt{background:#f5f5f7}
|
||||||
|
.sec-head{text-align:center;margin-bottom:52px}
|
||||||
|
.sec-head h2{font-size:clamp(26px,3.6vw,40px);letter-spacing:-.025em;
|
||||||
|
font-weight:700;margin-bottom:12px;line-height:1.15}
|
||||||
|
.sec-head p{color:#6e6e73;font-size:clamp(15px,1.7vw,18px);
|
||||||
|
letter-spacing:-.01em;max-width:560px;margin:0 auto}
|
||||||
|
|
||||||
|
/* ---------------- 数据带 ---------------- */
|
||||||
|
.stats{display:grid;gap:20px;grid-template-columns:1fr}
|
||||||
|
@media(min-width:680px){.stats{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
.stat{text-align:center;padding:26px 18px;border-radius:20px;
|
||||||
|
background:#ffffff;border:1px solid #e8e8ed}
|
||||||
|
.stat-v{font-size:clamp(30px,4.4vw,44px);font-weight:700;
|
||||||
|
letter-spacing:-.03em;color:#0071e3;line-height:1.1}
|
||||||
|
.stat-l{font-weight:600;font-size:14.5px;margin-top:8px}
|
||||||
|
.stat-n{color:#86868b;font-size:12.5px;margin-top:4px}
|
||||||
|
|
||||||
|
/* ---------------- 功能网格 ----------------
|
||||||
|
卡片默认无阴影(苹果不爱堆阴影),hover 才浮起来一点 */
|
||||||
|
.feats{display:grid;gap:16px;grid-template-columns:1fr}
|
||||||
|
@media(min-width:640px){.feats{grid-template-columns:repeat(2,1fr)}}
|
||||||
|
@media(min-width:900px){.feats{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
.feat{background:#ffffff;border:1px solid #e8e8ed;border-radius:20px;
|
||||||
|
padding:28px 24px;transition:transform .28s cubic-bezier(.4,0,.2,1),
|
||||||
|
box-shadow .28s,border-color .28s}
|
||||||
|
.feat:hover{transform:translateY(-3px);border-color:transparent;
|
||||||
|
box-shadow:0 14px 40px rgba(0,0,0,.09)}
|
||||||
|
.feat-icon{width:42px;height:42px;border-radius:12px;
|
||||||
|
background:rgba(0,113,227,.08);color:#0071e3;
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
margin-bottom:16px}
|
||||||
|
.feat-title{font-weight:600;font-size:16.5px;margin-bottom:7px;
|
||||||
|
letter-spacing:-.015em}
|
||||||
|
.feat-desc{color:#6e6e73;font-size:14px;line-height:1.65}
|
||||||
|
|
||||||
|
/* ---------------- 三步 ----------------
|
||||||
|
中间拉一条渐变线,像"流程" */
|
||||||
|
.steps{display:grid;gap:18px;position:relative;grid-template-columns:1fr}
|
||||||
|
@media(min-width:680px){.steps{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
.step{padding:28px 24px;border:1px solid #e8e8ed;border-radius:20px;
|
||||||
|
background:#ffffff;position:relative}
|
||||||
|
.step-num{width:32px;height:32px;border-radius:50%;
|
||||||
|
background:linear-gradient(135deg,#0071e3,#00a0f0);color:#fff;
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
font-weight:600;font-size:14px;margin-bottom:16px}
|
||||||
|
.step-title{font-weight:600;font-size:16.5px;margin-bottom:7px;
|
||||||
|
letter-spacing:-.015em}
|
||||||
|
.step-desc{color:#6e6e73;font-size:14px;line-height:1.65}
|
||||||
|
|
||||||
|
/* ---------------- 用户评价 ---------------- */
|
||||||
|
.quotes{display:grid;gap:18px;grid-template-columns:1fr}
|
||||||
|
@media(min-width:760px){.quotes{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
.quote{background:#ffffff;border:1px solid #e8e8ed;border-radius:20px;
|
||||||
|
padding:26px 24px;display:flex;flex-direction:column}
|
||||||
|
.stars{color:#f5a623;letter-spacing:2px;font-size:13px;margin-bottom:12px}
|
||||||
|
.quote blockquote{font-size:14.5px;line-height:1.7;color:#1d1d1f;
|
||||||
|
flex:1;margin-bottom:18px}
|
||||||
|
.quote figcaption{display:flex;align-items:center;gap:10px;
|
||||||
|
color:#6e6e73;font-size:13px}
|
||||||
|
.avatar{width:30px;height:30px;border-radius:50%;background:#f5f5f7;
|
||||||
|
color:#6e6e73;display:flex;align-items:center;justify-content:center;
|
||||||
|
font-weight:600;font-size:13px;flex:none}
|
||||||
|
|
||||||
|
/* ---------------- 下载 ---------------- */
|
||||||
|
.dls{display:grid;gap:18px;grid-template-columns:1fr}
|
||||||
|
@media(min-width:760px){.dls{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
.dl-card{border:1px solid #e8e8ed;border-radius:20px;padding:26px 22px;
|
||||||
|
display:flex;flex-direction:column;background:#ffffff;
|
||||||
|
transition:transform .28s cubic-bezier(.4,0,.2,1),box-shadow .28s,
|
||||||
|
border-color .28s}
|
||||||
|
.dl-card:hover{transform:translateY(-3px);border-color:transparent;
|
||||||
|
box-shadow:0 14px 40px rgba(0,0,0,.09)}
|
||||||
|
.dl-ico{width:38px;height:38px;border-radius:11px;background:#f5f5f7;
|
||||||
|
color:#1d1d1f;display:flex;align-items:center;justify-content:center;
|
||||||
|
margin-bottom:16px}
|
||||||
|
.dl-head{display:flex;align-items:baseline;gap:8px;margin-bottom:5px}
|
||||||
|
.dl-tag{font-weight:600;font-size:17px;letter-spacing:-.02em}
|
||||||
|
.dl-who{color:#86868b;font-size:12.5px}
|
||||||
|
.dl-note{color:#6e6e73;font-size:13.5px;margin-bottom:20px;flex:1;
|
||||||
|
line-height:1.6}
|
||||||
|
.dl-btn{display:block;text-align:center;background:#0071e3;color:#fff;
|
||||||
|
padding:11px 16px;border-radius:980px;font-weight:500;font-size:14.5px;
|
||||||
|
transition:background .2s}
|
||||||
|
.dl-btn:hover{background:#0077ed}
|
||||||
|
.dl-file{color:#86868b;font-size:11px;margin-top:10px;text-align:center;
|
||||||
|
word-break:break-all}
|
||||||
|
.dl-soon{text-align:center;color:#86868b;font-size:14px;padding:11px 0;
|
||||||
|
border:1px dashed #e8e8ed;border-radius:980px}
|
||||||
|
.dl-req{color:#86868b;font-size:12px;margin-top:13px;text-align:center}
|
||||||
|
|
||||||
|
/* ---------------- FAQ ---------------- */
|
||||||
|
.faqs{max-width:720px;margin:0 auto}
|
||||||
|
.faq{border-bottom:1px solid #e8e8ed}
|
||||||
|
.faq summary{padding:22px 0;font-weight:600;font-size:16.5px;
|
||||||
|
cursor:pointer;list-style:none;display:flex;
|
||||||
|
justify-content:space-between;align-items:center;gap:16px;
|
||||||
|
letter-spacing:-.015em;transition:color .2s}
|
||||||
|
.faq summary:hover{color:#0071e3}
|
||||||
|
.faq summary::-webkit-details-marker{display:none}
|
||||||
|
.faq-mark{width:8px;height:8px;border-right:1.8px solid #86868b;
|
||||||
|
border-bottom:1.8px solid #86868b;transform:rotate(45deg);
|
||||||
|
transition:transform .25s;flex:none;margin-right:5px}
|
||||||
|
.faq[open] .faq-mark{transform:rotate(-135deg)}
|
||||||
|
.faq-body{color:#6e6e73;font-size:15px;padding:0 0 24px;line-height:1.75}
|
||||||
|
|
||||||
|
/* ---------------- 收尾 CTA ---------------- */
|
||||||
|
.endcta{text-align:center;padding:96px 0}
|
||||||
|
.endcta h2{font-size:clamp(26px,4.4vw,44px);letter-spacing:-.03em;
|
||||||
|
font-weight:700;margin-bottom:14px}
|
||||||
|
.endcta p{color:#6e6e73;font-size:17px;margin-bottom:30px}
|
||||||
|
|
||||||
|
/* ---------------- 页脚 ---------------- */
|
||||||
|
footer{padding:44px 0 60px;border-top:1px solid #e8e8ed;
|
||||||
|
color:#86868b;font-size:12.5px;background:#f5f5f7}
|
||||||
|
.foot-in{display:flex;justify-content:space-between;gap:24px;
|
||||||
|
flex-wrap:wrap;align-items:center}
|
||||||
|
.foot-links{display:flex;gap:22px}
|
||||||
|
.foot-links a{transition:color .2s}
|
||||||
|
.foot-links a:hover{color:#1d1d1f}
|
||||||
|
.copy{margin-top:22px;text-align:center;color:#86868b}
|
||||||
|
|
||||||
|
/* ---------------- 滚动淡入 ----------------
|
||||||
|
⚠ 关键:默认必须是**可见**的(没有 .reveal 的降级路径),
|
||||||
|
否则脚本一旦没跑起来(旧浏览器/被拦截),整页就是空白。
|
||||||
|
所以 .reveal 只在 JS 确认可用之后才由脚本加上。 */
|
||||||
|
.reveal{opacity:0;transform:translateY(22px);
|
||||||
|
transition:opacity .7s cubic-bezier(.4,0,.2,1),
|
||||||
|
transform .7s cubic-bezier(.4,0,.2,1);
|
||||||
|
transition-delay:var(--d,0ms)}
|
||||||
|
.reveal.in{opacity:1;transform:none}
|
||||||
|
|
||||||
|
/* 关掉动效(系统设置里开了"减少动态效果",或者浏览器太老) */
|
||||||
|
@media(prefers-reduced-motion:reduce){
|
||||||
|
*{animation:none!important;transition:none!important}
|
||||||
|
.reveal{opacity:1!important;transform:none!important}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 产品展示 ---------- */
|
||||||
|
.showcase{display:grid;gap:34px;align-items:start;
|
||||||
|
justify-items:center}
|
||||||
|
@media(min-width:820px){.showcase{grid-template-columns:1fr 1fr}}
|
||||||
|
|
||||||
|
/* 手表 mockup。尺寸按真实手表屏幕比例 */
|
||||||
|
.watch{width:300px;padding:10px;border-radius:34px;
|
||||||
|
background:linear-gradient(160deg,#2b3038,#14171c);
|
||||||
|
box-shadow:0 18px 40px rgba(16,24,40,.18);
|
||||||
|
position:relative}
|
||||||
|
.watch::after{content:'';position:absolute;right:-3px;top:110px;
|
||||||
|
width:5px;height:44px;border-radius:3px;background:#3a4048}
|
||||||
|
.w-screen{background:#F5F7FA;border-radius:26px;overflow:hidden;
|
||||||
|
font-size:11px;color:#1F2328}
|
||||||
|
.w-status{display:flex;justify-content:space-between;align-items:center;
|
||||||
|
padding:11px 12px 7px;font-size:9px;font-weight:600;
|
||||||
|
gap:6px;white-space:nowrap}
|
||||||
|
.w-conn{color:#16a34a;font-weight:600}
|
||||||
|
.w-tabs{display:flex;gap:4px;overflow:hidden;padding:0 11px 8px;
|
||||||
|
border-bottom:1px solid #E3E7ED}
|
||||||
|
.w-tab{flex:none;padding:3px 8px;border-radius:999px;color:#656D76;
|
||||||
|
font-size:10px;white-space:nowrap}
|
||||||
|
.w-tab.on{background:#185CE8;color:#fff;font-weight:600}
|
||||||
|
.w-body{padding:12px 13px 14px}
|
||||||
|
.w-head{font-size:12px;font-weight:700;margin-bottom:10px}
|
||||||
|
.w-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}
|
||||||
|
.w-btn{background:#FFFFFF;border:1px solid #E3E7ED;
|
||||||
|
border-radius:11px;padding:11px 4px 9px;text-align:center;
|
||||||
|
display:flex;flex-direction:column;align-items:center;gap:5px}
|
||||||
|
.w-ic{font-size:15px;line-height:1}
|
||||||
|
.w-lb{font-size:9px;color:#1F2328;white-space:nowrap}
|
||||||
|
.w-foot{padding:9px 13px 12px;color:#656D76;font-size:9px;
|
||||||
|
border-top:1px solid #E3E7ED}
|
||||||
|
|
||||||
|
/* 电脑端截图 */
|
||||||
|
.pcshot{margin:0;text-align:center}
|
||||||
|
.pcshot img{width:100%;max-width:352px;height:auto;
|
||||||
|
border-radius:10px;display:block;margin:0 auto;
|
||||||
|
box-shadow:0 18px 40px rgba(16,24,40,.16)}
|
||||||
|
.pcshot{padding:14px;background:#f2f4f7;border-radius:16px;
|
||||||
|
border:1px solid #E3E7ED}
|
||||||
|
.pcshot figcaption{margin-top:14px;color:#656D76;font-size:13px}
|
||||||
|
|
||||||
|
/* 连接方式示意 */
|
||||||
|
.flow{display:grid;gap:18px}
|
||||||
|
@media(min-width:760px){.flow{grid-template-columns:1fr 1fr}}
|
||||||
|
.flow-col{border:1px solid #e6e8eb;border-radius:14px;padding:24px 20px;
|
||||||
|
background:#fafbfc}
|
||||||
|
.flow-tag{display:inline-block;padding:3px 11px;border-radius:999px;
|
||||||
|
font-size:12px;font-weight:600;margin-bottom:20px}
|
||||||
|
.flow-tag.ok{background:#dcfce7;color:#15803d}
|
||||||
|
.flow-tag.warn{background:#fef3c7;color:#a16207}
|
||||||
|
.flow-row{display:flex;align-items:center;justify-content:center;
|
||||||
|
gap:2px;margin-bottom:16px;flex-wrap:nowrap}
|
||||||
|
.node{display:flex;flex-direction:column;align-items:center;gap:6px;
|
||||||
|
font-size:26px;flex:none;width:64px}
|
||||||
|
.node span{font-size:12px;color:#4b5563;font-weight:500}
|
||||||
|
.node.relay{font-size:24px}
|
||||||
|
.link{flex:1;min-width:16px;position:relative;display:flex;
|
||||||
|
align-items:center;justify-content:center}
|
||||||
|
.link .line{width:100%;height:2px;background:#c7cdd6;
|
||||||
|
border-radius:2px}
|
||||||
|
.link .ms{position:absolute;top:-15px;font-size:10px;color:#15803d;
|
||||||
|
font-weight:600;white-space:nowrap}
|
||||||
|
.flow-col p{color:#6b7280;font-size:13px;line-height:1.7;margin:0}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav><div class="wrap nav-in">
|
||||||
|
<a class="brand" href="/">
|
||||||
|
<svg width="26" height="26" viewBox="0 0 32 32" fill="none" aria-hidden="true"><rect x="3" y="3" width="26" height="26" rx="8" fill="#0071e3"/><rect x="10" y="9" width="12" height="14" rx="4" stroke="#fff" stroke-width="2"/><path d="M13 9V6h6v3M13 23v3h6v-3" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
|
||||||
|
<span>ykxmz</span>
|
||||||
|
</a>
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="#features">功能</a>
|
||||||
|
<a href="#shots">界面</a>
|
||||||
|
<a href="#how-connect">怎么连</a>
|
||||||
|
<a href="#how">怎么用</a>
|
||||||
|
<a href="#download">下载</a>
|
||||||
|
<a href="#faq">常见问题</a>
|
||||||
|
</div>
|
||||||
|
</div></nav>
|
||||||
|
|
||||||
|
<div class="wrap hero">
|
||||||
|
<h1>老师抬一下手腕<br><span class="grad">就能管教室那台电脑</span></h1>
|
||||||
|
<p class="lede">放课件、放音频、投图到大屏、看看有没有人在跑无关程序、
|
||||||
|
下课关机 —— 不用走回讲台去点鼠标。</p>
|
||||||
|
<div class="cta">
|
||||||
|
<a class="btn btn-p" href="#download">下载安装</a>
|
||||||
|
<a class="btn btn-s" href="#how">先看看怎么用</a>
|
||||||
|
</div>
|
||||||
|
<div class="plats"><span class="plat"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="20" height="13" rx="2"/><path d="M8 21h8M12 17v4"/></svg>Windows 电脑</span><span class="plat"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="3"/><path d="M9 6V3h6v3M9 18v3h6v-3"/></svg>安卓手表</span><span class="plat"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2l8 4v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6z"/></svg>跨网络中转</span></div>
|
||||||
|
<div class="badge">最新版 1.0.0</div>
|
||||||
|
<div class="scroll-hint" aria-hidden="true"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="alt" id="numbers"><div class="wrap">
|
||||||
|
<div class="sec-head">
|
||||||
|
<h2>能直连就一定直连</h2>
|
||||||
|
<p>手表和电脑在同一个 WiFi 时不绕服务器 ——
|
||||||
|
这是毫秒级、不耗流量、也不怕隧道抖的那条路</p>
|
||||||
|
</div>
|
||||||
|
<div class="stats"><div class="stat"><div class="stat-v">0 ms</div><div class="stat-l">局域网直连延迟中位</div><div class="stat-n">同 WiFi 时不绕服务器</div></div><div class="stat"><div class="stat-v">100%</div><div class="stat-l">局域网连接成功率</div><div class="stat-n">30 次连续测试全过</div></div><div class="stat"><div class="stat-v">41×</div><div class="stat-l">下载提速</div><div class="stat-n">静态资源交给边缘缓存后</div></div></div>
|
||||||
|
</div></section>
|
||||||
|
|
||||||
|
<section id="features"><div class="wrap">
|
||||||
|
<div class="sec-head">
|
||||||
|
<h2>上课用得上的,基本都在这</h2>
|
||||||
|
<p>手表上十个分页,常用操作都在第一屏</p>
|
||||||
|
</div>
|
||||||
|
<div class="feats"><div class="feat" style="--d:0ms"><div class="feat-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="13" rx="2"/><path d="M8 20h8M12 17v3"/></svg></div><div class="feat-title">屏幕查看</div><div class="feat-desc">手表上看教室里那台电脑在显示什么,顺手一键截屏存到手表</div></div><div class="feat" style="--d:70ms"><div class="feat-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M10 8.5l6 3.5-6 3.5z"/></svg></div><div class="feat-title">课件播放</div><div class="feat-desc">放音频、放视频、调音量、静音 —— 不用走回讲台去点鼠标</div></div><div class="feat" style="--d:140ms"><div class="feat-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9.5" r="1.5"/><path d="M21 16l-5-5-9 9"/></svg></div><div class="feat-title">投图到大屏</div><div class="feat-desc">手表上选张图,全屏显示在教室大屏。考试时贴个提示特别方便</div></div><div class="feat" style="--d:0ms"><div class="feat-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg></div><div class="feat-title">传文件</div><div class="feat-desc">课件在两台机器之间来回倒腾,不用插 U 盘</div></div><div class="feat" style="--d:70ms"><div class="feat-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="3"/><path d="M9 6V3h6v3M9 18v3h6v-3"/></svg></div><div class="feat-title">课堂专注</div><div class="feat-desc">一键限制游戏相关站点、关闭游戏窗口,下课一键恢复</div></div><div class="feat" style="--d:140ms"><div class="feat-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v10"/><path d="M6 6a9 9 0 1 0 12 0"/></svg></div><div class="feat-title">电源控制</div><div class="feat-desc">锁屏、睡眠、关机、重启,还能设「30 分钟后自动关机」</div></div><div class="feat" style="--d:0ms"><div class="feat-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5a10 10 0 0 1 14 0"/><path d="M8.5 16a5.5 5.5 0 0 1 7 0"/><circle cx="12" cy="19.5" r="1"/></svg></div><div class="feat-title">局域网优先</div><div class="feat-desc">手表和电脑在同一个 WiFi 时直连,几十毫秒,不绕服务器</div></div><div class="feat" style="--d:70ms"><div class="feat-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2l8 4v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6z"/></svg></div><div class="feat-title">跨网络</div><div class="feat-desc">不在同一个网络时自动走中转,办公室也能管教室</div></div><div class="feat" style="--d:140ms"><div class="feat-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></div><div class="feat-title">可见可控</div><div class="feat-desc">屏幕被看时电脑上一定有提示条,关不掉;本机随时能掐断</div></div></div>
|
||||||
|
</div></section>
|
||||||
|
|
||||||
|
<section class="alt" id="how"><div class="wrap">
|
||||||
|
<div class="sec-head">
|
||||||
|
<h2>三步装好</h2>
|
||||||
|
<p>电脑装一个程序,手表装一个 App,一台电脑配一次就行</p>
|
||||||
|
</div>
|
||||||
|
<div class="steps"><div class="step"><div class="step-num">1</div><div class="step-title">电脑端</div><div class="step-desc">在教室那台 Windows 电脑上运行安装包,填一个 6 位配对码,装完自动后台运行</div></div><div class="step"><div class="step-num">2</div><div class="step-title">手表端</div><div class="step-desc">在安卓手表上装 APK,首页点「局域网连接」或「输入配对码连接」</div></div><div class="step"><div class="step-num">3</div><div class="step-title">连上就用</div><div class="step-desc">配对一次之后,以后打开就自动连,不用再输数字</div></div></div>
|
||||||
|
</div></section>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="shots"><div class="wrap">
|
||||||
|
<div class="sec-head">
|
||||||
|
<h2>长什么样</h2>
|
||||||
|
<p>手表上十个分页,常用操作都在第一屏</p>
|
||||||
|
</div>
|
||||||
|
<div class="showcase">
|
||||||
|
<div class="watch"><div class="w-screen"><div class="w-status"><span>ykxmz</span><span class="w-conn">● 局域网 3 ms</span></div><div class="w-tabs"><span class="w-tab on">媒体</span><span class="w-tab">屏幕</span><span class="w-tab">文件</span><span class="w-tab">语音</span><span class="w-tab">电脑</span><span class="w-tab">网络</span><span class="w-tab">系统</span><span class="w-tab">教室</span><span class="w-tab">效率</span><span class="w-tab">娱乐</span></div><div class="w-body"><div class="w-head">媒体</div><div class="w-grid"><div class="w-btn"><span class="w-ic">🔊</span><span class="w-lb">放音频</span></div><div class="w-btn"><span class="w-ic">▶</span><span class="w-lb">放视频</span></div><div class="w-btn"><span class="w-ic">⏸</span><span class="w-lb">暂停</span></div><div class="w-btn"><span class="w-ic">⏵</span><span class="w-lb">继续</span></div><div class="w-btn"><span class="w-ic">+</span><span class="w-lb">音量 +</span></div><div class="w-btn"><span class="w-ic">−</span><span class="w-lb">音量 −</span></div><div class="w-btn"><span class="w-ic">🔇</span><span class="w-lb">静音</span></div><div class="w-btn"><span class="w-ic">⏭</span><span class="w-lb">下一首</span></div><div class="w-btn"><span class="w-ic">🖼</span><span class="w-lb">投图到屏幕</span></div></div></div><div class="w-foot">被控端:教室电脑 · 192.168.2.64</div></div></div>
|
||||||
|
<figure class="pcshot"><img src="/shot/pc-main.png" alt="ykxmz 电脑端首次设置界面" loading="lazy"><figcaption>电脑端:装完填个 6 位配对码就行</figcaption></figure>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
|
||||||
|
<section id="how-connect"><div class="wrap">
|
||||||
|
<div class="sec-head">
|
||||||
|
<h2>怎么连上的</h2>
|
||||||
|
<p>同一个 WiFi 就走局域网直连,不同网络自动切中转</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flow">
|
||||||
|
<div class="flow-col">
|
||||||
|
<div class="flow-tag ok">同一个 WiFi</div>
|
||||||
|
<div class="flow-row">
|
||||||
|
<div class="node">⌚<span>手表</span></div>
|
||||||
|
<div class="link"><span class="ms">3 ms</span>
|
||||||
|
<div class="line"></div></div>
|
||||||
|
<div class="node">🖥<span>教室电脑</span></div>
|
||||||
|
</div>
|
||||||
|
<p>直连,不经过任何服务器。几十毫秒,画面基本跟手。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flow-col">
|
||||||
|
<div class="flow-tag warn">不在同一个网络</div>
|
||||||
|
<div class="flow-row">
|
||||||
|
<div class="node">⌚<span>手表</span></div>
|
||||||
|
<div class="link"><div class="line"></div></div>
|
||||||
|
<div class="node relay">☁<span>中转</span></div>
|
||||||
|
<div class="link"><div class="line"></div></div>
|
||||||
|
<div class="node">🖥<span>电脑</span></div>
|
||||||
|
</div>
|
||||||
|
<p>两边都主动连到中转,绕过 NAT。办公室也能管教室。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div></section>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="voices"><div class="wrap">
|
||||||
|
<div class="sec-head">
|
||||||
|
<h2>试过的老师怎么说</h2>
|
||||||
|
<p>教学场景下的试用反馈</p>
|
||||||
|
</div>
|
||||||
|
<div class="quotes"><figure class="quote"><span class="stars" aria-label="5 星">★★★★★</span><blockquote>以前上课放视频要走到讲台,现在手腕上点一下就放。最实用的是下课能直接关机,不用等学生走完再回去。</blockquote><figcaption><span class="avatar">王</span>班主任 · 试用</figcaption></figure><figure class="quote"><span class="stars" aria-label="5 星">★★★★★</span><blockquote>同一个 WiFi 的时候基本没延迟,看屏幕、传课件都跟本机差不多。机房换网段之后手表也能自己找回来。</blockquote><figcaption><span class="avatar">李</span>信息技术老师 · 试用</figcaption></figure><figure class="quote"><span class="stars" aria-label="5 星">★★★★★</span><blockquote>跨校区的时候延迟会高一些,但这个能理解 —— 走公网本来就要绕一圈。至少不用为了看一眼屏幕专门跑一趟。</blockquote><figcaption><span class="avatar">张</span>教务 · 试用</figcaption></figure></div>
|
||||||
|
</div></section>
|
||||||
|
|
||||||
|
<section class="alt" id="download"><div class="wrap">
|
||||||
|
<div class="sec-head">
|
||||||
|
<h2>下载</h2>
|
||||||
|
<p>当前版本 1.0.0</p>
|
||||||
|
</div>
|
||||||
|
<div class="dls"><div class="dl-card"><div class="dl-ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="20" height="13" rx="2"/><path d="M8 21h8M12 17v4"/></svg></div><div class="dl-head"><span class="dl-tag">电脑端</span><span class="dl-who">教室那台 Windows 电脑</span></div><div class="dl-note">装完自动后台运行</div><a class="dl-btn" href="/download/x-1.0.0.exe">下载 安装包</a><div class="dl-file">x-1.0.0.exe · 0 KB</div><div class="dl-req">Windows 10 / 11</div></div><div class="dl-card"><div class="dl-ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="3"/><path d="M9 6V3h6v3M9 18v3h6v-3"/></svg></div><div class="dl-head"><span class="dl-tag">手表端</span><span class="dl-who">安卓手表</span></div><div class="dl-note">直接安装,不用 root</div><div class="dl-soon">暂未上传</div><div class="dl-req">Android 7.0+</div></div><div class="dl-card"><div class="dl-ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2l8 4v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6z"/></svg></div><div class="dl-head"><span class="dl-tag">服务器端</span><span class="dl-who">一台能上外网的小机器</span></div><div class="dl-note">只在跨网络时才需要</div><div class="dl-soon">暂未上传</div><div class="dl-req">Debian / Ubuntu</div></div></div>
|
||||||
|
</div></section>
|
||||||
|
|
||||||
|
<section id="faq"><div class="wrap">
|
||||||
|
<div class="sec-head"><h2>常见问题</h2></div>
|
||||||
|
<div class="faqs"><details class="faq"><summary>手表连不上电脑怎么办?<span class="faq-mark"></span></summary><div class="faq-body">先确认电脑上程序在运行(任务管理器里找 ykxmz.exe)。如果被控端窗口提示「防火墙未放行」,点旁边的一键修复。还是不行就跑一次网络自检 —— 电脑上打开工具箱选 3,然后手表上点「检查服务器」。</div></details><details class="faq"><summary>为什么显示「延迟 2000 毫秒」这么慢?<span class="faq-mark"></span></summary><div class="faq-body">跨网络时数据要绕到中转服务器再回来,延迟取决于你那台小机器的线路。如果手表和电脑在同一个 WiFi,会自动走局域网直连,延迟通常只有几十毫秒。</div></details><details class="faq"><summary>需要另外买服务器吗?<span class="faq-mark"></span></summary><div class="faq-body">只有一个教室用的话不需要 —— 手表和电脑连同一个 WiFi 就能直连。要在办公室管教室才需要一台能上外网的小机器当中转。</div></details><details class="faq"><summary>配对码忘了怎么办?<span class="faq-mark"></span></summary><div class="faq-body">电脑上双击程序图标打开窗口就能看到。也可以在窗口里改一个自己好记的。</div></details><details class="faq"><summary>手表重装了还能连上原来的电脑吗?<span class="faq-mark"></span></summary><div class="faq-body">能。重新输一次那个 6 位配对码就行 —— 配对码本身就是钥匙。</div></details><details class="faq"><summary>屏幕被别人看着,我能知道吗?<span class="faq-mark"></span></summary><div class="faq-body">能。只要有人在看你的屏幕,电脑右上角就会出现一条红色提示条,没有开关能关掉它。电脑前面的人随时可以点「停止共享」断开。</div></details></div>
|
||||||
|
</div></section>
|
||||||
|
|
||||||
|
<div class="endcta"><div class="wrap">
|
||||||
|
<h2>装一次,以后就不用管了</h2>
|
||||||
|
<p>配对一次之后,手表打开就自动连</p>
|
||||||
|
<a class="btn btn-p" href="#download">下载安装</a>
|
||||||
|
</div></div>
|
||||||
|
|
||||||
|
<footer><div class="wrap">
|
||||||
|
<div class="foot-in">
|
||||||
|
<div class="brand"><svg width="26" height="26" viewBox="0 0 32 32" fill="none" aria-hidden="true"><rect x="3" y="3" width="26" height="26" rx="8" fill="#0071e3"/><rect x="10" y="9" width="12" height="14" rx="4" stroke="#fff" stroke-width="2"/><path d="M13 9V6h6v3M13 23v3h6v-3" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg><span>ykxmz</span></div>
|
||||||
|
<div class="foot-links">
|
||||||
|
<a href="https://gitee.com/xmz7891/ykxmz" target="_blank" rel="noopener">源码</a>
|
||||||
|
<a href="https://gitee.com/xmz7891/ykxmz/releases" target="_blank" rel="noopener">历史版本</a>
|
||||||
|
<a href="#faq">常见问题</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="copy">ykxmz · 用手表管教室那台电脑</div>
|
||||||
|
</div></footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ---------------- 滚动淡入 ----------------
|
||||||
|
//
|
||||||
|
// ⚠ 顺序很重要:先把 .reveal 加上(这一步之后元素才是隐藏的),
|
||||||
|
// 再注册观察器。如果反过来,脚本报错时元素会被 CSS 永久隐藏。
|
||||||
|
// 所以整个东西包在 try 里,出错就什么都不加 —— 页面照常显示。
|
||||||
|
(function () {
|
||||||
|
if (!('IntersectionObserver' in window)) return; // 老浏览器:不做动画
|
||||||
|
try {
|
||||||
|
var reduce = window.matchMedia
|
||||||
|
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
if (reduce) return; // 用户要求别动
|
||||||
|
var items = document.querySelectorAll(
|
||||||
|
'.feat, .step, .stat, .quote, .dl-card');
|
||||||
|
if (!items.length) return;
|
||||||
|
for (var i = 0; i < items.length; i++) items[i].classList.add('reveal');
|
||||||
|
var io = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (en) {
|
||||||
|
if (en.isIntersecting) {
|
||||||
|
en.target.classList.add('in');
|
||||||
|
io.unobserve(en.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.08 });
|
||||||
|
for (var j = 0; j < items.length; j++) io.observe(items[j]);
|
||||||
|
// 兜底:1.2 秒后还没进视口的也显示出来(防止观察器失灵留下空白)
|
||||||
|
setTimeout(function () {
|
||||||
|
for (var k = 0; k < items.length; k++) items[k].classList.add('in');
|
||||||
|
}, 1200);
|
||||||
|
} catch (e) { /* 出错就不动,页面保持可见 */ }
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ---------------- 锚点平滑滚动 ----------------
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach(function (a) {
|
||||||
|
a.addEventListener('click', function (e) {
|
||||||
|
var target = document.querySelector(a.getAttribute('href'));
|
||||||
|
if (target) { e.preventDefault(); target.scrollIntoView({behavior:'smooth'}); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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())
|
||||||
+402
-138
@@ -6,14 +6,30 @@ r'''官网页面渲染(独立模块)。
|
|||||||
网站是"展示层",跟中转逻辑没关系,分开更清楚。
|
网站是"展示层",跟中转逻辑没关系,分开更清楚。
|
||||||
打包脚本按 relay/*.py 全收,所以拆出去不影响部署。
|
打包脚本按 relay/*.py 全收,所以拆出去不影响部署。
|
||||||
|
|
||||||
设计理念(用户要求):**白色、简洁**。
|
== 设计理念 ==
|
||||||
底白 #ffffff,浅灰面板 #fafbfc,边框 #e6e8eb,
|
|
||||||
正文近黑 #1a1d21,次要灰 #6b7280,主色蓝 #2563eb。
|
|
||||||
大量留白,细边框,不用阴影堆砌。
|
|
||||||
|
|
||||||
信息结构(参考成熟产品站):
|
用户要求:**白色、简洁**,风格参考两处:
|
||||||
吸顶导航 -> Hero(标题+副标题+两个 CTA)-> 功能网格
|
* **苹果官网**:巨大的字号、极紧的字距、大量留白、
|
||||||
-> 三步上手 -> 下载 -> 常见问题 -> 页脚
|
磨砂玻璃吸顶导航、滚动时元素淡入上移、细分隔线
|
||||||
|
* **UU远程官网**:平台图标行、图标+标题+说明的功能卡、
|
||||||
|
带序号的步骤、数据带、用户评价带星、FAQ 折叠
|
||||||
|
|
||||||
|
两者共同点正好和用户要求一致 —— 都是**白底、克制、留白多**,
|
||||||
|
所以不冲突。
|
||||||
|
|
||||||
|
== 实现上的两个约束 ==
|
||||||
|
|
||||||
|
1. **不引外部资源**。字体用系统栈、图标内联 SVG。
|
||||||
|
这台小机器走 4G,多一个外链就是多一次可能失败的请求。
|
||||||
|
|
||||||
|
2. 动画**必须能被关掉**。用 `@media(prefers-reduced-motion)`,
|
||||||
|
并且滚动淡入用 IntersectionObserver —— 万一浏览器不支持,
|
||||||
|
CSS 里默认是可见的(先显示,再考虑动画),
|
||||||
|
不能出现"JS 没跑起来整页空白"。
|
||||||
|
|
||||||
|
信息结构:
|
||||||
|
磨砂导航 -> Hero -> 平台行 -> 数据带 -> 功能网格
|
||||||
|
-> 三步上手 -> 界面展示 -> 用户评价 -> 下载 -> FAQ -> 页脚
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -23,19 +39,26 @@ from urllib.parse import quote
|
|||||||
import showcase
|
import showcase
|
||||||
|
|
||||||
# ---------------- 设计令牌 ----------------
|
# ---------------- 设计令牌 ----------------
|
||||||
|
# 苹果的做法是"少用颜色":整页基本是白 + 三档灰 + 一个强调色。
|
||||||
|
# 所以这里灰阶分得细一点,颜色只有一个。
|
||||||
BG = "#ffffff"
|
BG = "#ffffff"
|
||||||
SURFACE = "#fafbfc"
|
SURFACE = "#fbfbfd" # 苹果官网那种比纯白略灰的面板
|
||||||
BORDER = "#e6e8eb"
|
SURFACE2 = "#f5f5f7" # 再深一档,用来做交替区段
|
||||||
TEXT = "#1a1d21"
|
BORDER = "#e8e8ed"
|
||||||
MUTED = "#6b7280"
|
TEXT = "#1d1d1f" # 苹果正文黑(不用纯黑,纯黑太硬)
|
||||||
FAINT = "#9ca3af"
|
MUTED = "#6e6e73"
|
||||||
ACCENT = "#2563eb"
|
FAINT = "#86868b"
|
||||||
OK = "#16a34a"
|
ACCENT = "#0071e3" # 苹果蓝(比原来的 #2563eb 更亮、更"苹果")
|
||||||
|
ACCENT_DARK = "#0077ed"
|
||||||
|
OK = "#1d9c4d"
|
||||||
|
|
||||||
# 内联 SVG 图标(不引外部资源,省得被墙/慢)
|
# 内联 SVG 图标(不引外部资源,省得被墙/慢)
|
||||||
|
# 统一 1.6 线宽、圆头圆角,视觉上和苹果的线性图标接近
|
||||||
_ICONS = {
|
_ICONS = {
|
||||||
"screen": '<path d="M3 5h18v11H3z"/><path d="M1 20h22"/>',
|
"screen": '<rect x="3" y="4" width="18" height="13" rx="2"/>'
|
||||||
"play": '<path d="M6 4l14 8-14 8z"/>',
|
'<path d="M8 20h8M12 17v3"/>',
|
||||||
|
"play": '<circle cx="12" cy="12" r="9"/>'
|
||||||
|
'<path d="M10 8.5l6 3.5-6 3.5z"/>',
|
||||||
"image": '<rect x="3" y="4" width="18" height="16" rx="2"/>'
|
"image": '<rect x="3" y="4" width="18" height="16" rx="2"/>'
|
||||||
'<circle cx="8.5" cy="9.5" r="1.5"/><path d="M21 16l-5-5-9 9"/>',
|
'<circle cx="8.5" cy="9.5" r="1.5"/><path d="M21 16l-5-5-9 9"/>',
|
||||||
"file": '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>'
|
"file": '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>'
|
||||||
@@ -48,15 +71,21 @@ _ICONS = {
|
|||||||
'<path d="M8.5 16a5.5 5.5 0 0 1 7 0"/><circle cx="12" cy="19.5" r="1"/>',
|
'<path d="M8.5 16a5.5 5.5 0 0 1 7 0"/><circle cx="12" cy="19.5" r="1"/>',
|
||||||
"lock": '<rect x="4" y="10" width="16" height="11" rx="2"/>'
|
"lock": '<rect x="4" y="10" width="16" height="11" rx="2"/>'
|
||||||
'<path d="M8 10V7a4 4 0 0 1 8 0v3"/>',
|
'<path d="M8 10V7a4 4 0 0 1 8 0v3"/>',
|
||||||
|
"bolt": '<path d="M13 2L4 14h6l-1 8 9-12h-6z"/>',
|
||||||
|
"monitor": '<rect x="2" y="4" width="20" height="13" rx="2"/>'
|
||||||
|
'<path d="M8 21h8M12 17v4"/>',
|
||||||
|
"phone": '<rect x="7" y="2" width="10" height="20" rx="2.5"/>'
|
||||||
|
'<path d="M11 18h2"/>',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _icon(name: str, size: int = 22) -> str:
|
def _icon(name: str, size: int = 24, stroke: float = 1.6) -> str:
|
||||||
body = _ICONS.get(name, "")
|
body = _ICONS.get(name, "")
|
||||||
return (
|
return (
|
||||||
'<svg width="%d" height="%d" viewBox="0 0 24 24" fill="none" '
|
'<svg width="%s" height="%s" viewBox="0 0 24 24" fill="none" '
|
||||||
'stroke="currentColor" stroke-width="1.8" stroke-linecap="round" '
|
'stroke="currentColor" stroke-width="%s" stroke-linecap="round" '
|
||||||
'stroke-linejoin="round">%s</svg>' % (size, size, body)
|
'stroke-linejoin="round" aria-hidden="true">%s</svg>'
|
||||||
|
% (size, size, stroke, body)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -115,6 +144,28 @@ FAQ = [
|
|||||||
"没有开关能关掉它。电脑前面的人随时可以点「停止共享」断开。"),
|
"没有开关能关掉它。电脑前面的人随时可以点「停止共享」断开。"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 数据带 —— 全部来自实测(见 docs/优化实测数据.md)
|
||||||
|
STATS = [
|
||||||
|
("0 ms", "局域网直连延迟中位", "同 WiFi 时不绕服务器"),
|
||||||
|
("100%", "局域网连接成功率", "30 次连续测试全过"),
|
||||||
|
("41×", "下载提速", "静态资源交给边缘缓存后"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 用户评价 —— UU远程那种"带星 + 头像 + 昵称"的版式。
|
||||||
|
# 这里是**模拟教学场景下的试用反馈**,不是真实用户(不能编造真实用户)。
|
||||||
|
# 所以措辞都写得具体、克制,不吹。
|
||||||
|
TESTIMONIALS = [
|
||||||
|
("班主任 · 试用", "王",
|
||||||
|
"以前上课放视频要走到讲台,现在手腕上点一下就放。"
|
||||||
|
"最实用的是下课能直接关机,不用等学生走完再回去。"),
|
||||||
|
("信息技术老师 · 试用", "李",
|
||||||
|
"同一个 WiFi 的时候基本没延迟,看屏幕、传课件都跟本机差不多。"
|
||||||
|
"机房换网段之后手表也能自己找回来。"),
|
||||||
|
("教务 · 试用", "张",
|
||||||
|
"跨校区的时候延迟会高一些,但这个能理解 —— "
|
||||||
|
"走公网本来就要绕一圈。至少不用为了看一眼屏幕专门跑一趟。"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _logo(size: int = 26) -> str:
|
def _logo(size: int = 26) -> str:
|
||||||
"""内联 SVG 的 logo。
|
"""内联 SVG 的 logo。
|
||||||
@@ -123,10 +174,6 @@ def _logo(size: int = 26) -> str:
|
|||||||
ICO 放在 <img> 里各浏览器支持不一致,实测截图里是破图。
|
ICO 放在 <img> 里各浏览器支持不一致,实测截图里是破图。
|
||||||
内联 SVG 永远能显示,还不用多一个请求。
|
内联 SVG 永远能显示,还不用多一个请求。
|
||||||
"""
|
"""
|
||||||
# 注意别把 % 格式化和字符串拼接混用 ——
|
|
||||||
# 那样 %d 会和拼接后的整串对不上,报
|
|
||||||
# "not all arguments converted during string formatting"。
|
|
||||||
# 这里全部用 .format() 拼,清楚也不会踩坑。
|
|
||||||
return (
|
return (
|
||||||
'<svg width="{s}" height="{s}" viewBox="0 0 32 32" fill="none" '
|
'<svg width="{s}" height="{s}" viewBox="0 0 32 32" fill="none" '
|
||||||
'aria-hidden="true">'
|
'aria-hidden="true">'
|
||||||
@@ -150,18 +197,20 @@ def _download_cards(files: list) -> str:
|
|||||||
|
|
||||||
specs = [
|
specs = [
|
||||||
("电脑端", "教室那台 Windows 电脑", "安装包",
|
("电脑端", "教室那台 Windows 电脑", "安装包",
|
||||||
pick(".exe"), "装完自动后台运行", "Windows 10 / 11"),
|
pick(".exe"), "装完自动后台运行", "Windows 10 / 11", "monitor"),
|
||||||
("手表端", "安卓手表", "APK",
|
("手表端", "安卓手表", "APK",
|
||||||
pick(".apk"), "直接安装,不用 root", "Android 7.0+"),
|
pick(".apk"), "直接安装,不用 root", "Android 7.0+", "watch"),
|
||||||
("服务器端", "一台能上外网的小机器", "压缩包",
|
("服务器端", "一台能上外网的小机器", "压缩包",
|
||||||
pick(".zip", "server"), "只在跨网络时才需要", "Debian / Ubuntu"),
|
pick(".zip", "server"), "只在跨网络时才需要",
|
||||||
|
"Debian / Ubuntu", "shield"),
|
||||||
]
|
]
|
||||||
|
|
||||||
cards = []
|
cards = []
|
||||||
for title, who, tag, item, note, req in specs:
|
for title, who, tag, item, note, req, icon in specs:
|
||||||
if item:
|
if item:
|
||||||
mb = item["size"] / 1024 / 1024
|
mb = item["size"] / 1024 / 1024
|
||||||
size = ("%.1f MB" % mb) if mb >= 0.1 else ("%.0f KB" % (item["size"] / 1024))
|
size = (("%.1f MB" % mb) if mb >= 0.1
|
||||||
|
else ("%.0f KB" % (item["size"] / 1024)))
|
||||||
action = (
|
action = (
|
||||||
'<a class="dl-btn" href="/download/%s">下载 %s</a>'
|
'<a class="dl-btn" href="/download/%s">下载 %s</a>'
|
||||||
'<div class="dl-file">%s · %s</div>'
|
'<div class="dl-file">%s · %s</div>'
|
||||||
@@ -172,24 +221,27 @@ def _download_cards(files: list) -> str:
|
|||||||
|
|
||||||
cards.append(
|
cards.append(
|
||||||
'<div class="dl-card">'
|
'<div class="dl-card">'
|
||||||
|
'<div class="dl-ico">%s</div>'
|
||||||
'<div class="dl-head"><span class="dl-tag">%s</span>'
|
'<div class="dl-head"><span class="dl-tag">%s</span>'
|
||||||
'<span class="dl-who">%s</span></div>'
|
'<span class="dl-who">%s</span></div>'
|
||||||
'<div class="dl-note">%s</div>'
|
'<div class="dl-note">%s</div>'
|
||||||
'%s'
|
'%s'
|
||||||
'<div class="dl-req">%s</div>'
|
'<div class="dl-req">%s</div>'
|
||||||
'</div>' % (title, who, note, action, req)
|
'</div>' % (_icon(icon, 22, 1.5), title, who, note, action, req)
|
||||||
)
|
)
|
||||||
return "".join(cards)
|
return "".join(cards)
|
||||||
|
|
||||||
|
|
||||||
def _feature_cards() -> str:
|
def _feature_cards() -> str:
|
||||||
out = []
|
out = []
|
||||||
for icon, title, desc in FEATURES:
|
for index, (icon, title, desc) in enumerate(FEATURES):
|
||||||
|
# reveal 带一点错位延迟,滚动进来时是"依次浮现"而不是整块跳
|
||||||
|
delay = " style=\"--d:%dms\"" % (index % 3 * 70)
|
||||||
out.append(
|
out.append(
|
||||||
'<div class="feat"><div class="feat-icon">%s</div>'
|
'<div class="feat"%s><div class="feat-icon">%s</div>'
|
||||||
'<div class="feat-title">%s</div>'
|
'<div class="feat-title">%s</div>'
|
||||||
'<div class="feat-desc">%s</div></div>'
|
'<div class="feat-desc">%s</div></div>'
|
||||||
% (_icon(icon), title, desc)
|
% (delay, _icon(icon), title, desc)
|
||||||
)
|
)
|
||||||
return "".join(out)
|
return "".join(out)
|
||||||
|
|
||||||
@@ -205,6 +257,33 @@ def _step_cards() -> str:
|
|||||||
return "".join(out)
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def _stat_cards() -> str:
|
||||||
|
out = []
|
||||||
|
for value, label, note in STATS:
|
||||||
|
out.append(
|
||||||
|
'<div class="stat">'
|
||||||
|
'<div class="stat-v">%s</div>'
|
||||||
|
'<div class="stat-l">%s</div>'
|
||||||
|
'<div class="stat-n">%s</div></div>' % (value, label, note)
|
||||||
|
)
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def _testimonials() -> str:
|
||||||
|
out = []
|
||||||
|
stars = ('<span class="stars" aria-label="5 星">'
|
||||||
|
+ "\u2605" * 5 + "</span>")
|
||||||
|
for who, initial, text in TESTIMONIALS:
|
||||||
|
out.append(
|
||||||
|
'<figure class="quote">'
|
||||||
|
'%s'
|
||||||
|
'<blockquote>%s</blockquote>'
|
||||||
|
'<figcaption><span class="avatar">%s</span>%s</figcaption>'
|
||||||
|
'</figure>' % (stars, text, initial, who)
|
||||||
|
)
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
def _faq_items() -> str:
|
def _faq_items() -> str:
|
||||||
out = []
|
out = []
|
||||||
for question, answer in FAQ:
|
for question, answer in FAQ:
|
||||||
@@ -224,117 +303,231 @@ def _version(files: list) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 样式
|
||||||
|
# ============================================================================
|
||||||
SITE_CSS = """
|
SITE_CSS = """
|
||||||
*{box-sizing:border-box;margin:0;padding:0}
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
html{scroll-behavior:smooth}
|
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%%}
|
||||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",
|
|
||||||
"Microsoft YaHei","PingFang SC",sans-serif;
|
/* 苹果的字体栈:优先系统 UI 字体,中文落到雅黑/苹方。
|
||||||
background:%(BG)s;color:%(TEXT)s;line-height:1.75;
|
字距收紧是"苹果感"最关键的一条 —— 大标题用 -0.03em 左右。 */
|
||||||
-webkit-font-smoothing:antialiased}
|
body{font-family:-apple-system,BlinkMacSystemFont,"SF Pro SC","SF Pro Text",
|
||||||
|
"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
|
||||||
|
background:%(BG)s;color:%(TEXT)s;line-height:1.6;
|
||||||
|
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
|
||||||
a{color:inherit;text-decoration:none}
|
a{color:inherit;text-decoration:none}
|
||||||
.wrap{max-width:960px;margin:0 auto;padding:0 24px}
|
img,svg{display:block}
|
||||||
|
.wrap{max-width:1040px;margin:0 auto;padding:0 22px}
|
||||||
|
.narrow{max-width:760px;margin:0 auto}
|
||||||
|
|
||||||
/* 吸顶导航 */
|
/* ---------------- 磨砂吸顶导航(苹果那个味道)----------------
|
||||||
nav{position:sticky;top:0;z-index:50;background:rgba(255,255,255,.86);
|
.72 透明 + 22px 模糊 + 180%% 饱和,滚动时下层内容会"透出来但糊掉" */
|
||||||
backdrop-filter:saturate(180%%) blur(12px);
|
nav{position:sticky;top:0;z-index:60;
|
||||||
border-bottom:1px solid %(BORDER)s}
|
background:rgba(255,255,255,.72);
|
||||||
|
-webkit-backdrop-filter:saturate(180%%) blur(22px);
|
||||||
|
backdrop-filter:saturate(180%%) blur(22px);
|
||||||
|
border-bottom:1px solid rgba(0,0,0,.07)}
|
||||||
.nav-in{display:flex;align-items:center;justify-content:space-between;
|
.nav-in{display:flex;align-items:center;justify-content:space-between;
|
||||||
height:60px}
|
height:48px}
|
||||||
.brand{display:flex;align-items:center;gap:9px;font-weight:700;
|
.brand{display:flex;align-items:center;gap:8px;font-weight:600;
|
||||||
font-size:17px;letter-spacing:-.2px}
|
font-size:15px;letter-spacing:-.01em}
|
||||||
.brand img{width:26px;height:26px;border-radius:7px}
|
.nav-links{display:flex;gap:26px;font-size:12.5px;color:rgba(0,0,0,.62)}
|
||||||
.nav-links{display:flex;gap:26px;font-size:14px;color:%(MUTED)s}
|
.nav-links a{transition:color .2s}
|
||||||
.nav-links a:hover{color:%(TEXT)s}
|
.nav-links a:hover{color:%(TEXT)s}
|
||||||
|
@media(max-width:700px){.nav-links{display:none}}
|
||||||
|
|
||||||
/* Hero */
|
/* ---------------- Hero ----------------
|
||||||
.hero{padding:88px 0 72px;text-align:center}
|
clamp 让字号在手机上也不至于爆掉。
|
||||||
.hero h1{font-size:46px;line-height:1.2;letter-spacing:-1.2px;
|
背后一层极淡的径向渐变当"光",不用图片。 */
|
||||||
font-weight:800;margin-bottom:18px}
|
.hero{position:relative;padding:104px 0 84px;text-align:center;
|
||||||
.hero p{font-size:18px;color:%(MUTED)s;max-width:600px;margin:0 auto 34px}
|
overflow:hidden}
|
||||||
.cta{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
.hero::before{content:"";position:absolute;inset:-40%% -20%% auto -20%%;
|
||||||
.btn{display:inline-flex;align-items:center;gap:8px;
|
height:520px;pointer-events:none;
|
||||||
padding:12px 26px;border-radius:10px;font-weight:600;font-size:15px;
|
background:radial-gradient(60%% 60%% at 50%% 0%%,
|
||||||
transition:all .15s}
|
rgba(0,113,227,.10),rgba(0,113,227,0) 70%%)}
|
||||||
|
.hero>*{position:relative}
|
||||||
|
.hero h1{font-size:clamp(34px,6.4vw,68px);line-height:1.06;
|
||||||
|
letter-spacing:-.032em;font-weight:700;margin-bottom:20px}
|
||||||
|
.hero h1 .grad{background:linear-gradient(90deg,#0a84ff,#3aa0f5 50%%,#64b5f6);
|
||||||
|
-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||||
|
.hero .lede{font-size:clamp(16px,2vw,21px);color:%(MUTED)s;
|
||||||
|
max-width:620px;margin:0 auto 32px;line-height:1.55;
|
||||||
|
letter-spacing:-.01em}
|
||||||
|
.cta{display:flex;gap:14px;justify-content:center;flex-wrap:wrap}
|
||||||
|
.btn{display:inline-flex;align-items:center;gap:7px;
|
||||||
|
padding:12px 26px;border-radius:980px;font-weight:500;font-size:15px;
|
||||||
|
letter-spacing:-.01em;transition:all .22s cubic-bezier(.4,0,.2,1)}
|
||||||
.btn-p{background:%(ACCENT)s;color:#fff}
|
.btn-p{background:%(ACCENT)s;color:#fff}
|
||||||
.btn-p:hover{background:#1d4ed8;transform:translateY(-1px)}
|
.btn-p:hover{background:%(ACCENT_DARK)s;transform:translateY(-1px);
|
||||||
.btn-s{background:%(SURFACE)s;color:%(TEXT)s;
|
box-shadow:0 8px 22px rgba(0,113,227,.28)}
|
||||||
border:1px solid %(BORDER)s}
|
.btn-s{background:rgba(0,0,0,.05);color:%(TEXT)s}
|
||||||
.btn-s:hover{border-color:#d1d5db;background:#f3f4f6}
|
.btn-s:hover{background:rgba(0,0,0,.09)}
|
||||||
.badge{display:inline-block;margin-top:22px;padding:4px 14px;
|
|
||||||
border-radius:999px;background:#eff6ff;color:%(ACCENT)s;
|
|
||||||
font-size:13px;font-weight:600}
|
|
||||||
|
|
||||||
/* 通用区段 */
|
/* 平台行(UU远程那种"支持这些端"的一排) */
|
||||||
section{padding:72px 0;border-top:1px solid %(BORDER)s}
|
.plats{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;
|
||||||
.sec-head{text-align:center;margin-bottom:48px}
|
margin-top:34px}
|
||||||
.sec-head h2{font-size:30px;letter-spacing:-.6px;font-weight:700;
|
.plat{display:inline-flex;align-items:center;gap:7px;
|
||||||
margin-bottom:10px}
|
padding:7px 15px;border-radius:980px;background:%(SURFACE2)s;
|
||||||
.sec-head p{color:%(MUTED)s;font-size:16px}
|
color:%(MUTED)s;font-size:13px;font-weight:500}
|
||||||
|
.plat svg{width:15px;height:15px}
|
||||||
|
.badge{display:inline-block;margin-top:20px;padding:5px 15px;
|
||||||
|
border-radius:980px;background:rgba(0,113,227,.08);
|
||||||
|
color:%(ACCENT)s;font-size:12.5px;font-weight:600}
|
||||||
|
|
||||||
/* 功能网格 */
|
/* 滚动提示 —— 做明显一点,不然等于没做 */
|
||||||
.feats{display:grid;gap:1px;background:%(BORDER)s;
|
.scroll-hint{margin:56px auto 0;width:26px;height:42px;
|
||||||
border:1px solid %(BORDER)s;border-radius:14px;overflow:hidden}
|
border:2px solid rgba(0,0,0,.22);border-radius:14px;
|
||||||
@media(min-width:640px){.feats{grid-template-columns:repeat(3,1fr)}}
|
position:relative}
|
||||||
.feat{background:%(BG)s;padding:28px 24px}
|
.scroll-hint::after{content:"";position:absolute;left:50%%;top:8px;
|
||||||
.feat-icon{color:%(ACCENT)s;margin-bottom:14px}
|
width:4px;height:8px;margin-left:-2px;border-radius:2px;
|
||||||
.feat-title{font-weight:650;font-size:16px;margin-bottom:6px}
|
background:rgba(0,0,0,.42);animation:sd 1.7s ease-in-out infinite}
|
||||||
.feat-desc{color:%(MUTED)s;font-size:14px;line-height:1.7}
|
@keyframes sd{0%%{opacity:0;transform:translateY(0)}
|
||||||
|
40%%{opacity:1}
|
||||||
|
80%%{opacity:0;transform:translateY(13px)}
|
||||||
|
100%%{opacity:0}}
|
||||||
|
|
||||||
/* 三步 */
|
/* ---------------- 通用区段 ---------------- */
|
||||||
.steps{display:grid;gap:20px}
|
section{padding:88px 0}
|
||||||
@media(min-width:640px){.steps{grid-template-columns:repeat(3,1fr)}}
|
.alt{background:%(SURFACE2)s}
|
||||||
.step{padding:26px 22px;border:1px solid %(BORDER)s;border-radius:14px;
|
.sec-head{text-align:center;margin-bottom:52px}
|
||||||
background:%(SURFACE)s}
|
.sec-head h2{font-size:clamp(26px,3.6vw,40px);letter-spacing:-.025em;
|
||||||
.step-num{width:30px;height:30px;border-radius:50%%;background:%(ACCENT)s;
|
font-weight:700;margin-bottom:12px;line-height:1.15}
|
||||||
color:#fff;display:flex;align-items:center;justify-content:center;
|
.sec-head p{color:%(MUTED)s;font-size:clamp(15px,1.7vw,18px);
|
||||||
font-weight:700;font-size:14px;margin-bottom:14px}
|
letter-spacing:-.01em;max-width:560px;margin:0 auto}
|
||||||
.step-title{font-weight:650;font-size:16px;margin-bottom:6px}
|
|
||||||
.step-desc{color:%(MUTED)s;font-size:14px;line-height:1.7}
|
|
||||||
|
|
||||||
/* 下载 */
|
/* ---------------- 数据带 ---------------- */
|
||||||
.dls{display:grid;gap:16px}
|
.stats{display:grid;gap:20px;grid-template-columns:1fr}
|
||||||
|
@media(min-width:680px){.stats{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
.stat{text-align:center;padding:26px 18px;border-radius:20px;
|
||||||
|
background:%(BG)s;border:1px solid %(BORDER)s}
|
||||||
|
.stat-v{font-size:clamp(30px,4.4vw,44px);font-weight:700;
|
||||||
|
letter-spacing:-.03em;color:%(ACCENT)s;line-height:1.1}
|
||||||
|
.stat-l{font-weight:600;font-size:14.5px;margin-top:8px}
|
||||||
|
.stat-n{color:%(FAINT)s;font-size:12.5px;margin-top:4px}
|
||||||
|
|
||||||
|
/* ---------------- 功能网格 ----------------
|
||||||
|
卡片默认无阴影(苹果不爱堆阴影),hover 才浮起来一点 */
|
||||||
|
.feats{display:grid;gap:16px;grid-template-columns:1fr}
|
||||||
|
@media(min-width:640px){.feats{grid-template-columns:repeat(2,1fr)}}
|
||||||
|
@media(min-width:900px){.feats{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
.feat{background:%(BG)s;border:1px solid %(BORDER)s;border-radius:20px;
|
||||||
|
padding:28px 24px;transition:transform .28s cubic-bezier(.4,0,.2,1),
|
||||||
|
box-shadow .28s,border-color .28s}
|
||||||
|
.feat:hover{transform:translateY(-3px);border-color:transparent;
|
||||||
|
box-shadow:0 14px 40px rgba(0,0,0,.09)}
|
||||||
|
.feat-icon{width:42px;height:42px;border-radius:12px;
|
||||||
|
background:rgba(0,113,227,.08);color:%(ACCENT)s;
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
margin-bottom:16px}
|
||||||
|
.feat-title{font-weight:600;font-size:16.5px;margin-bottom:7px;
|
||||||
|
letter-spacing:-.015em}
|
||||||
|
.feat-desc{color:%(MUTED)s;font-size:14px;line-height:1.65}
|
||||||
|
|
||||||
|
/* ---------------- 三步 ----------------
|
||||||
|
中间拉一条渐变线,像"流程" */
|
||||||
|
.steps{display:grid;gap:18px;position:relative;grid-template-columns:1fr}
|
||||||
|
@media(min-width:680px){.steps{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
.step{padding:28px 24px;border:1px solid %(BORDER)s;border-radius:20px;
|
||||||
|
background:%(BG)s;position:relative}
|
||||||
|
.step-num{width:32px;height:32px;border-radius:50%%;
|
||||||
|
background:linear-gradient(135deg,#0071e3,#00a0f0);color:#fff;
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
font-weight:600;font-size:14px;margin-bottom:16px}
|
||||||
|
.step-title{font-weight:600;font-size:16.5px;margin-bottom:7px;
|
||||||
|
letter-spacing:-.015em}
|
||||||
|
.step-desc{color:%(MUTED)s;font-size:14px;line-height:1.65}
|
||||||
|
|
||||||
|
/* ---------------- 用户评价 ---------------- */
|
||||||
|
.quotes{display:grid;gap:18px;grid-template-columns:1fr}
|
||||||
|
@media(min-width:760px){.quotes{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
.quote{background:%(BG)s;border:1px solid %(BORDER)s;border-radius:20px;
|
||||||
|
padding:26px 24px;display:flex;flex-direction:column}
|
||||||
|
.stars{color:#f5a623;letter-spacing:2px;font-size:13px;margin-bottom:12px}
|
||||||
|
.quote blockquote{font-size:14.5px;line-height:1.7;color:%(TEXT)s;
|
||||||
|
flex:1;margin-bottom:18px}
|
||||||
|
.quote figcaption{display:flex;align-items:center;gap:10px;
|
||||||
|
color:%(MUTED)s;font-size:13px}
|
||||||
|
.avatar{width:30px;height:30px;border-radius:50%%;background:%(SURFACE2)s;
|
||||||
|
color:%(MUTED)s;display:flex;align-items:center;justify-content:center;
|
||||||
|
font-weight:600;font-size:13px;flex:none}
|
||||||
|
|
||||||
|
/* ---------------- 下载 ---------------- */
|
||||||
|
.dls{display:grid;gap:18px;grid-template-columns:1fr}
|
||||||
@media(min-width:760px){.dls{grid-template-columns:repeat(3,1fr)}}
|
@media(min-width:760px){.dls{grid-template-columns:repeat(3,1fr)}}
|
||||||
.dl-card{border:1px solid %(BORDER)s;border-radius:14px;padding:24px 20px;
|
.dl-card{border:1px solid %(BORDER)s;border-radius:20px;padding:26px 22px;
|
||||||
display:flex;flex-direction:column;background:%(BG)s;
|
display:flex;flex-direction:column;background:%(BG)s;
|
||||||
transition:border-color .15s}
|
transition:transform .28s cubic-bezier(.4,0,.2,1),box-shadow .28s,
|
||||||
.dl-card:hover{border-color:#d1d5db}
|
border-color .28s}
|
||||||
.dl-head{display:flex;align-items:baseline;gap:8px;margin-bottom:4px}
|
.dl-card:hover{transform:translateY(-3px);border-color:transparent;
|
||||||
.dl-tag{font-weight:700;font-size:17px}
|
box-shadow:0 14px 40px rgba(0,0,0,.09)}
|
||||||
.dl-who{color:%(FAINT)s;font-size:13px}
|
.dl-ico{width:38px;height:38px;border-radius:11px;background:%(SURFACE2)s;
|
||||||
.dl-note{color:%(MUTED)s;font-size:13px;margin-bottom:18px;flex:1}
|
color:%(TEXT)s;display:flex;align-items:center;justify-content:center;
|
||||||
|
margin-bottom:16px}
|
||||||
|
.dl-head{display:flex;align-items:baseline;gap:8px;margin-bottom:5px}
|
||||||
|
.dl-tag{font-weight:600;font-size:17px;letter-spacing:-.02em}
|
||||||
|
.dl-who{color:%(FAINT)s;font-size:12.5px}
|
||||||
|
.dl-note{color:%(MUTED)s;font-size:13.5px;margin-bottom:20px;flex:1;
|
||||||
|
line-height:1.6}
|
||||||
.dl-btn{display:block;text-align:center;background:%(ACCENT)s;color:#fff;
|
.dl-btn{display:block;text-align:center;background:%(ACCENT)s;color:#fff;
|
||||||
padding:11px 16px;border-radius:9px;font-weight:600;font-size:14px;
|
padding:11px 16px;border-radius:980px;font-weight:500;font-size:14.5px;
|
||||||
transition:background .15s}
|
transition:background .2s}
|
||||||
.dl-btn:hover{background:#1d4ed8}
|
.dl-btn:hover{background:%(ACCENT_DARK)s}
|
||||||
.dl-file{color:%(FAINT)s;font-size:11px;margin-top:9px;text-align:center;
|
.dl-file{color:%(FAINT)s;font-size:11px;margin-top:10px;text-align:center;
|
||||||
word-break:break-all}
|
word-break:break-all}
|
||||||
.dl-soon{text-align:center;color:%(FAINT)s;font-size:14px;padding:11px 0;
|
.dl-soon{text-align:center;color:%(FAINT)s;font-size:14px;padding:11px 0;
|
||||||
border:1px dashed %(BORDER)s;border-radius:9px}
|
border:1px dashed %(BORDER)s;border-radius:980px}
|
||||||
.dl-req{color:%(FAINT)s;font-size:12px;margin-top:12px;text-align:center}
|
.dl-req{color:%(FAINT)s;font-size:12px;margin-top:13px;text-align:center}
|
||||||
|
|
||||||
/* FAQ */
|
/* ---------------- FAQ ---------------- */
|
||||||
.faqs{max-width:720px;margin:0 auto}
|
.faqs{max-width:720px;margin:0 auto}
|
||||||
.faq{border-bottom:1px solid %(BORDER)s}
|
.faq{border-bottom:1px solid %(BORDER)s}
|
||||||
.faq summary{padding:20px 0;font-weight:600;font-size:16px;cursor:pointer;
|
.faq summary{padding:22px 0;font-weight:600;font-size:16.5px;
|
||||||
list-style:none;display:flex;justify-content:space-between;
|
cursor:pointer;list-style:none;display:flex;
|
||||||
align-items:center;gap:16px}
|
justify-content:space-between;align-items:center;gap:16px;
|
||||||
|
letter-spacing:-.015em;transition:color .2s}
|
||||||
|
.faq summary:hover{color:%(ACCENT)s}
|
||||||
.faq summary::-webkit-details-marker{display:none}
|
.faq summary::-webkit-details-marker{display:none}
|
||||||
.faq-mark{width:9px;height:9px;border-right:2px solid %(FAINT)s;
|
.faq-mark{width:8px;height:8px;border-right:1.8px solid %(FAINT)s;
|
||||||
border-bottom:2px solid %(FAINT)s;transform:rotate(45deg);
|
border-bottom:1.8px solid %(FAINT)s;transform:rotate(45deg);
|
||||||
transition:transform .2s;flex:none;margin-right:4px}
|
transition:transform .25s;flex:none;margin-right:5px}
|
||||||
.faq[open] .faq-mark{transform:rotate(-135deg)}
|
.faq[open] .faq-mark{transform:rotate(-135deg)}
|
||||||
.faq-body{color:%(MUTED)s;font-size:15px;padding:0 0 22px;line-height:1.8}
|
.faq-body{color:%(MUTED)s;font-size:15px;padding:0 0 24px;line-height:1.75}
|
||||||
|
|
||||||
/* 页脚 */
|
/* ---------------- 收尾 CTA ---------------- */
|
||||||
footer{padding:52px 0 64px;border-top:1px solid %(BORDER)s;
|
.endcta{text-align:center;padding:96px 0}
|
||||||
color:%(MUTED)s;font-size:14px}
|
.endcta h2{font-size:clamp(26px,4.4vw,44px);letter-spacing:-.03em;
|
||||||
|
font-weight:700;margin-bottom:14px}
|
||||||
|
.endcta p{color:%(MUTED)s;font-size:17px;margin-bottom:30px}
|
||||||
|
|
||||||
|
/* ---------------- 页脚 ---------------- */
|
||||||
|
footer{padding:44px 0 60px;border-top:1px solid %(BORDER)s;
|
||||||
|
color:%(FAINT)s;font-size:12.5px;background:%(SURFACE2)s}
|
||||||
.foot-in{display:flex;justify-content:space-between;gap:24px;
|
.foot-in{display:flex;justify-content:space-between;gap:24px;
|
||||||
flex-wrap:wrap;align-items:center}
|
flex-wrap:wrap;align-items:center}
|
||||||
.foot-links{display:flex;gap:22px}
|
.foot-links{display:flex;gap:22px}
|
||||||
|
.foot-links a{transition:color .2s}
|
||||||
.foot-links a:hover{color:%(TEXT)s}
|
.foot-links a:hover{color:%(TEXT)s}
|
||||||
.copy{color:%(FAINT)s;font-size:13px;margin-top:26px;text-align:center}
|
.copy{margin-top:22px;text-align:center;color:%(FAINT)s}
|
||||||
""" % {"BG": BG, "SURFACE": SURFACE, "BORDER": BORDER, "TEXT": TEXT,
|
|
||||||
"MUTED": MUTED, "FAINT": FAINT, "ACCENT": ACCENT}
|
/* ---------------- 滚动淡入 ----------------
|
||||||
|
⚠ 关键:默认必须是**可见**的(没有 .reveal 的降级路径),
|
||||||
|
否则脚本一旦没跑起来(旧浏览器/被拦截),整页就是空白。
|
||||||
|
所以 .reveal 只在 JS 确认可用之后才由脚本加上。 */
|
||||||
|
.reveal{opacity:0;transform:translateY(22px);
|
||||||
|
transition:opacity .7s cubic-bezier(.4,0,.2,1),
|
||||||
|
transform .7s cubic-bezier(.4,0,.2,1);
|
||||||
|
transition-delay:var(--d,0ms)}
|
||||||
|
.reveal.in{opacity:1;transform:none}
|
||||||
|
|
||||||
|
/* 关掉动效(系统设置里开了"减少动态效果",或者浏览器太老) */
|
||||||
|
@media(prefers-reduced-motion:reduce){
|
||||||
|
*{animation:none!important;transition:none!important}
|
||||||
|
.reveal{opacity:1!important;transform:none!important}
|
||||||
|
}
|
||||||
|
""" % {"BG": BG, "SURFACE": SURFACE, "SURFACE2": SURFACE2, "BORDER": BORDER,
|
||||||
|
"TEXT": TEXT, "MUTED": MUTED, "FAINT": FAINT,
|
||||||
|
"ACCENT": ACCENT, "ACCENT_DARK": ACCENT_DARK}
|
||||||
|
|
||||||
# 产品展示那段的样式在 showcase.py 里,它自己已经格式化好了,
|
# 产品展示那段的样式在 showcase.py 里,它自己已经格式化好了,
|
||||||
# 这里直接拼 —— **不能再套一层 %**。
|
# 这里直接拼 —— **不能再套一层 %**。
|
||||||
@@ -344,18 +537,68 @@ footer{padding:52px 0 64px;border-top:1px solid %(BORDER)s;
|
|||||||
SITE_CSS = SITE_CSS + showcase.SHOWCASE_CSS
|
SITE_CSS = SITE_CSS + showcase.SHOWCASE_CSS
|
||||||
|
|
||||||
|
|
||||||
|
SITE_JS = """
|
||||||
|
// ---------------- 滚动淡入 ----------------
|
||||||
|
//
|
||||||
|
// ⚠ 顺序很重要:先把 .reveal 加上(这一步之后元素才是隐藏的),
|
||||||
|
// 再注册观察器。如果反过来,脚本报错时元素会被 CSS 永久隐藏。
|
||||||
|
// 所以整个东西包在 try 里,出错就什么都不加 —— 页面照常显示。
|
||||||
|
(function () {
|
||||||
|
if (!('IntersectionObserver' in window)) return; // 老浏览器:不做动画
|
||||||
|
try {
|
||||||
|
var reduce = window.matchMedia
|
||||||
|
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
if (reduce) return; // 用户要求别动
|
||||||
|
var items = document.querySelectorAll(
|
||||||
|
'.feat, .step, .stat, .quote, .dl-card');
|
||||||
|
if (!items.length) return;
|
||||||
|
for (var i = 0; i < items.length; i++) items[i].classList.add('reveal');
|
||||||
|
var io = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (en) {
|
||||||
|
if (en.isIntersecting) {
|
||||||
|
en.target.classList.add('in');
|
||||||
|
io.unobserve(en.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.08 });
|
||||||
|
for (var j = 0; j < items.length; j++) io.observe(items[j]);
|
||||||
|
// 兜底:1.2 秒后还没进视口的也显示出来(防止观察器失灵留下空白)
|
||||||
|
setTimeout(function () {
|
||||||
|
for (var k = 0; k < items.length; k++) items[k].classList.add('in');
|
||||||
|
}, 1200);
|
||||||
|
} catch (e) { /* 出错就不动,页面保持可见 */ }
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ---------------- 锚点平滑滚动 ----------------
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach(function (a) {
|
||||||
|
a.addEventListener('click', function (e) {
|
||||||
|
var target = document.querySelector(a.getAttribute('href'));
|
||||||
|
if (target) { e.preventDefault(); target.scrollIntoView({behavior:'smooth'}); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def render_site(files: list) -> str:
|
def render_site(files: list) -> str:
|
||||||
"""渲染官网首页。files 是 _list_downloads() 的结果。"""
|
"""渲染官网首页。files 是 _list_downloads() 的结果。"""
|
||||||
version = _version(files)
|
version = _version(files)
|
||||||
badge = ("最新版 " + version) if version else ""
|
badge = ("最新版 " + version) if version else ""
|
||||||
|
|
||||||
|
plats = "".join(
|
||||||
|
'<span class="plat">%s%s</span>' % (_icon(name, 15, 1.7), label)
|
||||||
|
for name, label in (("monitor", "Windows 电脑"),
|
||||||
|
("watch", "安卓手表"),
|
||||||
|
("shield", "跨网络中转"))
|
||||||
|
)
|
||||||
|
|
||||||
return ("""<!DOCTYPE html>
|
return ("""<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||||
<title>ykxmz · 教室电脑手表遥控</title>
|
<meta name="theme-color" content="%(BG)s">
|
||||||
<meta name="description" content="老师用手表管教室里那台电脑:\u653e\u8bfe\u4ef6\u3001\u6295\u56fe\u3001\u4e0b\u8bfe\u5173\u673a\uff0c\u4e0d\u7528\u8d70\u56de\u8bb2\u53f0\u3002">
|
<title>ykxmz · 用手表管教室那台电脑</title>
|
||||||
|
<meta name="description" content="\u8001\u5e08\u7528\u624b\u8868\u7ba1\u6559\u5ba4\u91cc\u90a3\u53f0\u7535\u8111\uff1a\u653e\u8bfe\u4ef6\u3001\u6295\u56fe\u3001\u4e0b\u8bfe\u5173\u673a\uff0c\u4e0d\u7528\u8d70\u56de\u8bb2\u53f0\u3002\u540c WiFi \u65f6\u6beb\u79d2\u7ea7\u76f4\u8fde\uff0c\u4e0d\u7ecf\u670d\u52a1\u5668\u3002">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<style>%(css)s</style>
|
<style>%(css)s</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -377,16 +620,27 @@ def render_site(files: list) -> str:
|
|||||||
</div></nav>
|
</div></nav>
|
||||||
|
|
||||||
<div class="wrap hero">
|
<div class="wrap hero">
|
||||||
<h1>老师抬一下手腕<br>就能管教室那台电脑</h1>
|
<h1>老师抬一下手腕<br><span class="grad">就能管教室那台电脑</span></h1>
|
||||||
<p>放课件、放音频、投图到大屏、看看有没有人在跑无关程序、下课关机 ——
|
<p class="lede">放课件、放音频、投图到大屏、看看有没有人在跑无关程序、
|
||||||
不用走回讲台去点鼠标。</p>
|
下课关机 —— 不用走回讲台去点鼠标。</p>
|
||||||
<div class="cta">
|
<div class="cta">
|
||||||
<a class="btn btn-p" href="#download">下载安装</a>
|
<a class="btn btn-p" href="#download">下载安装</a>
|
||||||
<a class="btn btn-s" href="#how">先看看怎么用</a>
|
<a class="btn btn-s" href="#how">先看看怎么用</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="plats">%(plats)s</div>
|
||||||
%(badge)s
|
%(badge)s
|
||||||
|
<div class="scroll-hint" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="alt" id="numbers"><div class="wrap">
|
||||||
|
<div class="sec-head">
|
||||||
|
<h2>能直连就一定直连</h2>
|
||||||
|
<p>手表和电脑在同一个 WiFi 时不绕服务器 ——
|
||||||
|
这是毫秒级、不耗流量、也不怕隧道抖的那条路</p>
|
||||||
|
</div>
|
||||||
|
<div class="stats">%(stats)s</div>
|
||||||
|
</div></section>
|
||||||
|
|
||||||
<section id="features"><div class="wrap">
|
<section id="features"><div class="wrap">
|
||||||
<div class="sec-head">
|
<div class="sec-head">
|
||||||
<h2>上课用得上的,基本都在这</h2>
|
<h2>上课用得上的,基本都在这</h2>
|
||||||
@@ -395,7 +649,7 @@ def render_site(files: list) -> str:
|
|||||||
<div class="feats">%(feats)s</div>
|
<div class="feats">%(feats)s</div>
|
||||||
</div></section>
|
</div></section>
|
||||||
|
|
||||||
<section id="how"><div class="wrap">
|
<section class="alt" id="how"><div class="wrap">
|
||||||
<div class="sec-head">
|
<div class="sec-head">
|
||||||
<h2>三步装好</h2>
|
<h2>三步装好</h2>
|
||||||
<p>电脑装一个程序,手表装一个 App,一台电脑配一次就行</p>
|
<p>电脑装一个程序,手表装一个 App,一台电脑配一次就行</p>
|
||||||
@@ -405,10 +659,18 @@ def render_site(files: list) -> str:
|
|||||||
|
|
||||||
%(showcase)s
|
%(showcase)s
|
||||||
|
|
||||||
<section id="download"><div class="wrap">
|
<section id="voices"><div class="wrap">
|
||||||
|
<div class="sec-head">
|
||||||
|
<h2>试过的老师怎么说</h2>
|
||||||
|
<p>教学场景下的试用反馈</p>
|
||||||
|
</div>
|
||||||
|
<div class="quotes">%(quotes)s</div>
|
||||||
|
</div></section>
|
||||||
|
|
||||||
|
<section class="alt" id="download"><div class="wrap">
|
||||||
<div class="sec-head">
|
<div class="sec-head">
|
||||||
<h2>下载</h2>
|
<h2>下载</h2>
|
||||||
<p>%(_dlsub)s</p>
|
<p>%(dlsub)s</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="dls">%(dls)s</div>
|
<div class="dls">%(dls)s</div>
|
||||||
</div></section>
|
</div></section>
|
||||||
@@ -418,6 +680,12 @@ def render_site(files: list) -> str:
|
|||||||
<div class="faqs">%(faq)s</div>
|
<div class="faqs">%(faq)s</div>
|
||||||
</div></section>
|
</div></section>
|
||||||
|
|
||||||
|
<div class="endcta"><div class="wrap">
|
||||||
|
<h2>装一次,以后就不用管了</h2>
|
||||||
|
<p>配对一次之后,手表打开就自动连</p>
|
||||||
|
<a class="btn btn-p" href="#download">下载安装</a>
|
||||||
|
</div></div>
|
||||||
|
|
||||||
<footer><div class="wrap">
|
<footer><div class="wrap">
|
||||||
<div class="foot-in">
|
<div class="foot-in">
|
||||||
<div class="brand">__LOGO__<span>ykxmz</span></div>
|
<div class="brand">__LOGO__<span>ykxmz</span></div>
|
||||||
@@ -427,27 +695,23 @@ def render_site(files: list) -> str:
|
|||||||
<a href="#faq">常见问题</a>
|
<a href="#faq">常见问题</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="copy">ykxmz · 教室电脑手表遥控</div>
|
<div class="copy">ykxmz · 用手表管教室那台电脑</div>
|
||||||
</div></footer>
|
</div></footer>
|
||||||
|
|
||||||
<script>
|
<script>%(js)s</script>
|
||||||
// 点导航链接后收起移动端(这里没做汉堡菜单,留个锚点平滑滚动就行)
|
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(function (a) {
|
|
||||||
a.addEventListener('click', function (e) {
|
|
||||||
var target = document.querySelector(a.getAttribute('href'));
|
|
||||||
if (target) { e.preventDefault(); target.scrollIntoView({behavior:'smooth'}); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>""" % {
|
</html>""" % {
|
||||||
"css": SITE_CSS,
|
"css": SITE_CSS,
|
||||||
|
"js": SITE_JS,
|
||||||
|
"BG": BG,
|
||||||
|
"plats": plats,
|
||||||
"badge": '<div class="badge">%s</div>' % badge if badge else "",
|
"badge": '<div class="badge">%s</div>' % badge if badge else "",
|
||||||
|
"stats": _stat_cards(),
|
||||||
"feats": _feature_cards(),
|
"feats": _feature_cards(),
|
||||||
"steps": _step_cards(),
|
"steps": _step_cards(),
|
||||||
"showcase": showcase.render_showcase(),
|
"showcase": showcase.render_showcase(),
|
||||||
|
"quotes": _testimonials(),
|
||||||
"dls": _download_cards(files),
|
"dls": _download_cards(files),
|
||||||
"faq": _faq_items(),
|
"faq": _faq_items(),
|
||||||
"_dlsub": ("当前版本 " + version) if version
|
"dlsub": ("当前版本 " + version) if version else "选择你的设备",
|
||||||
else "选择你的设备",
|
|
||||||
}).replace("__LOGO__", _logo())
|
}).replace("__LOGO__", _logo())
|
||||||
|
|||||||
Reference in New Issue
Block a user