diff --git a/.gitignore b/.gitignore index 322faa30..02020036 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ __pycache__/ # Claude Code local settings (do not publish) .claude/ + +# webapp 构建产物(由 app/build/build.py 从 data/exercises.json 生成) +app/public/data/ diff --git a/app/README.md b/app/README.md new file mode 100644 index 00000000..1663ba48 --- /dev/null +++ b/app/README.md @@ -0,0 +1,75 @@ +# 囚徒健身 · 徒手训练手册 + +基于本仓库 `data/exercises.json`(1324 个动作)构建的**手机端 webapp**,聚焦徒手训练(Convict Conditioning 体系)+ 挂墙单杠。 + +## 启动 + +```bash +python3 app/build/build.py # 构建数据(改了 catalog.py 后需重跑) +python3 app/server.py # 监听 0.0.0.0:9876 +``` + +`app/public/data/app-data.json` 是构建产物,不入库,首次使用必须先跑 `build.py`。 + +服务器默认监听 `0.0.0.0`,局域网内任意设备都能访问;启动时会打印本机的 +局域网地址,手机在同一网络下直接打开即可。也可以自己指定: + +```bash +python3 app/server.py --host 0.0.0.0 --port 9876 +``` + +iOS Safari / Android Chrome 可「添加到主屏幕」当作 App 用。 + +## 它做了什么 + +从 1324 个动作里筛出 **174 个**可徒手或挂墙单杠完成的动作,归入《Convict Conditioning》六艺体系: + +| 动作族 | 数量 | 说明 | +|---|---|---| +| 俯卧撑 | 21 | 推 · 靠墙 → 单臂 → 直臂支撑俯卧撑 | +| 引体向上 | 26 | 拉 · 肩胛引体 → 标准引体 → 单臂 / 双力臂 / 前后水平 | +| 深蹲 | 17 | 腿 · 扶物深蹲 → 手枪蹲 | +| 举腿 | 21 | 核心 · 死虫 → 悬垂直腿举 → 悬垂折体 | +| 桥 | 16 | 后链 · 骨盆后倾 → 反向平板桥 | +| 倒立撑 | 6 | 肩 · 肩胛俯卧撑 → 倒立撑 | +| 臂屈伸 / 核心稳定 / 小腿 / 代谢体能 / 拉伸灵活 | 67 | 辅助与恢复 | + +每个动作都带教练视角的内容:**发力要点**(按执行顺序)、**常见错误**、**三级推荐组数**(打基础 / 进阶 / 精通)、**节奏 · 组间休息 · 每周频率**。 + +三个入口: +- **六艺** — 六条进阶链,点进去是从入门到大师的完整阶梯 +- **动作库** — 按部位 / 难度 / 动作族筛选 + 搜索 +- **今日训练** — 选水平 + 重点 + 可用器械,自动排一节课(热身 / 主项 / 收尾) + +## 目录结构 + +``` +app/ +├── build/ +│ ├── catalog.py ← 精选目录:难度、部位、发力要点、组数(改这里) +│ └── build.py ← 与 exercises.json 合并,产出 app-data.json +├── public/ ← 前端(原生 JS,零依赖) +│ ├── index.html style.css app.js +│ └── data/app-data.json ← 构建产物 +└── server.py ← 静态服务器,同时映射仓库根的 images/ videos/ +``` + +## 修改内容 + +编辑 `app/build/catalog.py` 里的动作条目,然后重跑 `build.py`。 +`build.py` 会校验每个 `id` 是否存在于数据集并检查重复收录,写错会直接报错退出。 + +新增一个动作: + +```python +E("0652", "标准引体向上", "pull", 3, ["背", "手臂", "核心"], + cues=["正握,握距略宽于肩", "先肩胛下沉后收,再屈肘", "..."], + errors=["借摆动起身", "不放到底"], + std=("1组×5次", "2组×10次", "2组×15次"), # 可省略,按难度自动生成 + need="单杠", note="挂墙单杠请先确认膨胀螺栓牢固。"), +``` + +## 说明 + +- 训练内容为一般性健身参考,不构成医疗建议;有伤病史请先咨询专业人士。 +- 动作图片 / 动图 © [Gym visual](https://gymvisual.com/),用途受本仓库 [LICENSE](../LICENSE) 中的媒体条款约束。 diff --git a/app/build/build.py b/app/build/build.py new file mode 100644 index 00000000..362a26c0 --- /dev/null +++ b/app/build/build.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +"""把精选目录 catalog.py 与 data/exercises.json 合并,产出前端用的 app-data.json。 + +用法: python3 app/build/build.py +""" +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from catalog import CATALOG, FAMILIES, DIFFICULTY, PARTS # noqa: E402 + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +SRC = os.path.join(ROOT, "data", "exercises.json") +OUT = os.path.join(ROOT, "app", "public", "data", "app-data.json") + +# 未显式指定 std 时,按 (scheme, 难度) 给的兜底推荐组数 +DEFAULT_STD = { + ("rep", 1): ("1组×10次", "2组×20次", "3组×30次"), + ("rep", 2): ("1组×8次", "2组×15次", "3组×25次"), + ("rep", 3): ("1组×5次", "2组×12次", "3组×20次"), + ("rep", 4): ("1组×3次", "2组×8次", "2组×12次"), + ("rep", 5): ("1次", "3次", "2组×5次"), + ("hold", 1): ("20秒", "45秒", "60秒"), + ("hold", 2): ("20秒", "45秒", "90秒"), + ("hold", 3): ("15秒", "30秒", "60秒"), + ("hold", 4): ("10秒", "20秒", "40秒"), + ("hold", 5): ("5秒", "10秒", "20秒"), + ("time", 1): ("20秒×3轮", "30秒×4轮", "40秒×5轮"), + ("time", 2): ("20秒×4轮", "30秒×5轮", "45秒×6轮"), + ("time", 3): ("20秒×4轮", "30秒×6轮", "40秒×8轮"), + ("time", 4): ("15秒×4轮", "20秒×6轮", "30秒×8轮"), + ("time", 5): ("15秒×3轮", "20秒×4轮", "30秒×5轮"), +} + +# 节奏与组间休息:教练处方的另外两个维度 +TEMPO = { + "rep": { + 1: "下放 2 秒 · 顶端停 1 秒 · 推起 1 秒", + 2: "下放 2 秒 · 顶端停 1 秒 · 推起 1 秒", + 3: "下放 3 秒 · 停 1 秒 · 发力 1 秒", + 4: "下放 3-4 秒 · 停 1 秒 · 全力发力", + 5: "全程最慢可控速度,宁可少做也不失控", + }, + "hold": { + 1: "均匀呼吸,全程保持张力,不憋气", + 2: "均匀呼吸,全程保持张力,不憋气", + 3: "达到时间就停,宁可分多组也不要姿势变形", + 4: "姿势一变形立刻结束该组", + 5: "只做能保持标准形态的秒数,多组累积", + }, + "time": { + 1: "中等节奏,能说完整句子", + 2: "快节奏,但动作质量优先于速度", + 3: "接近全力,质量下滑即结束该轮", + 4: "全力,轮间充分恢复", + 5: "全力,轮间充分恢复", + }, +} + +REST = {1: "组间休息 30-45 秒", 2: "组间休息 45-60 秒", 3: "组间休息 60-90 秒", + 4: "组间休息 2-3 分钟", 5: "组间休息 3-5 分钟"} + +FREQ = {1: "每周 3-5 次,几乎不会练过量", 2: "每周 3 次,隔天练", + 3: "每周 2-3 次,注意恢复", 4: "每周 2 次,保证 48 小时以上间隔", + 5: "每周 1-2 次,永远留有余力,绝不练到力竭"} + +GOALS = ["打基础", "进阶", "精通"] + +# 数据集的肌肉名是英文,这里统一中译 +MUSCLE_ZH = { + "abductors": "髋外展肌", "abs": "腹肌", "adductors": "内收肌", + "ankle stabilizers": "踝稳定肌", "ankles": "踝关节", "back": "背部", + "biceps": "肱二头肌", "calves": "小腿", "cardiovascular system": "心肺", + "chest": "胸部", "core": "核心", "deltoids": "三角肌", "delts": "三角肌", + "feet": "足部", "forearms": "前臂", "glutes": "臀大肌", "groin": "腹股沟", + "hamstrings": "腘绳肌", "hands": "手部", "hip flexors": "髋屈肌", + "lats": "背阔肌", "levator scapulae": "肩胛提肌", "lower abs": "下腹", + "lower back": "下背", "obliques": "腹斜肌", "pectorals": "胸大肌", + "quadriceps": "股四头肌", "quads": "股四头肌", "rear deltoids": "三角肌后束", + "rhomboids": "菱形肌", "serratus anterior": "前锯肌", "shoulders": "肩部", + "spine": "脊柱", "sternocleidomastoid": "胸锁乳突肌", "trapezius": "斜方肌", + "traps": "斜方肌", "triceps": "肱三头肌", "upper back": "上背", "wrists": "腕关节", +} + +# 数据集中文步骤是机翻,这些术语在徒手动作语境下是错的(顺序敏感,长的在前) +STEP_FIX = [ + ("杠铃或杠铃表面", "横杠"), + ("杠铃或使用悬挂训练器", "横杠或悬挂带"), + ("杠铃或悬吊训练器", "横杠或悬挂带"), + ("杠铃或手柄", "横杠"), + ("引体向上杆", "单杠"), + ("杠铃杆", "横杠"), + ("一个杠铃", "一根横杠"), + ("杠铃", "横杠"), + ("将肩胛骨挤压在一起", "肩胛骨向中间收紧"), + ("肩胛骨挤压在一起", "肩胛骨向中间收紧"), + ("重复所需的重复次数", "重复至目标次数"), + ("所需的重复次数", "目标次数"), +] + + +def clean_step(s): + for a, b in STEP_FIX: + s = s.replace(a, b) + return s + + +def zh_muscle(name): + return MUSCLE_ZH.get(name, name) + + +def main(): + raw = json.load(open(SRC, encoding="utf-8")) + by_id = {x["id"]: x for x in raw} + + seen, errors, out = set(), [], [] + for order, item in enumerate(CATALOG): + eid = item["id"] + if eid not in by_id: + errors.append(f"ID 不存在于数据集: {eid} ({item['zh']})") + continue + if eid in seen: + errors.append(f"重复收录: {eid} ({item['zh']})") + continue + seen.add(eid) + + src = by_id[eid] + d, scheme = item["d"], item["scheme"] + std = item["std"] or DEFAULT_STD[(scheme, d)] + steps = src.get("instruction_steps", {}).get("zh") or [] + if not steps: + zh = src.get("instructions", {}).get("zh", "") + steps = [s.strip() for s in zh.split("。") if s.strip()] + + out.append({ + "_ord": order, + "id": eid, + "zh": item["zh"], + "en": src["name"], + "fam": item["fam"], + "d": d, + "parts": item["parts"], + "cues": item["cues"], + "errors": item["errors"], + "scheme": scheme, + "std": [{"goal": g, "text": t} for g, t in zip(GOALS, std)], + "tempo": TEMPO[scheme][d], + "rest": REST[d], + "freq": FREQ[d], + "note": item["note"], + "need": item["need"] or "徒手", + "steps": [clean_step(s) for s in steps], + "gif": src["gif_url"], + "img": src["image"], + "target": zh_muscle(src.get("target", "")), + "secondary": [zh_muscle(m) for m in src.get("secondary_muscles", [])], + }) + + if errors: + print("构建失败:") + for e in errors: + print(" -", e) + sys.exit(1) + + # 动作族内先按难度、同难度再按目录中编排的进阶顺序,形成阶梯 + fam_order = {f["key"]: i for i, f in enumerate(FAMILIES)} + out.sort(key=lambda x: (fam_order[x["fam"]], x["d"], x["_ord"])) + for f in FAMILIES: + chain = [x for x in out if x["fam"] == f["key"]] + for i, x in enumerate(chain): + x["step"] = i + 1 + x["chain_len"] = len(chain) + for x in out: + del x["_ord"] + + payload = { + "families": FAMILIES, + "difficulty": DIFFICULTY, + "parts": PARTS, + "goals": GOALS, + "exercises": out, + "attribution": "动作图片/动图 © Gym visual — https://gymvisual.com/", + } + os.makedirs(os.path.dirname(OUT), exist_ok=True) + with open(OUT, "w", encoding="utf-8") as f: + json.dump(payload, f, ensure_ascii=False, separators=(",", ":")) + + print(f"✅ 收录 {len(out)} 个动作 → {os.path.relpath(OUT, ROOT)}") + for fam in FAMILIES: + n = sum(1 for x in out if x["fam"] == fam["key"]) + print(f" {fam['icon']} {fam['zh']:<6} {n:>3} 个") + print(f" 文件大小 {os.path.getsize(OUT)/1024:.0f} KB") + + +if __name__ == "__main__": + main() diff --git a/app/build/catalog.py b/app/build/catalog.py new file mode 100644 index 00000000..0c68b38d --- /dev/null +++ b/app/build/catalog.py @@ -0,0 +1,1477 @@ +# -*- coding: utf-8 -*- +""" +囚徒健身(徒手训练)精选动作目录。 + +从 exercises-dataset 的 1324 个动作中筛出可徒手 + 挂墙单杠完成的动作, +按《Convict Conditioning》"六艺"体系归入六条主进阶链 + 辅助链, +并为每个动作标注难度、参与部位、发力要点、常见错误与推荐组数。 + +字段说明: + id 对应 data/exercises.json 的 id(构建时校验,错了会报错) + zh 中文动作名 + fam 所属动作族(见 FAMILIES) + d 难度 1-5(1=入门 2=初级 3=中级 4=高级 5=大师) + parts 主要参与部位(用于"按部位"筛选) + cues 发力要点(教练视角,按执行顺序排列) + errors 常见错误 + scheme "rep" 计次 / "hold" 计秒(静力) / "time" 计时(有氧/代谢) + std 可选,覆盖默认的三级推荐组数 (初级, 中级, 进阶) + note 可选,补充说明 / 安全提示 + need 可选,所需器械标签(默认徒手) +""" + + +def E(id, zh, fam, d, parts, cues, errors=(), scheme="rep", std=None, note=None, need=None): + return { + "id": id, "zh": zh, "fam": fam, "d": d, "parts": list(parts), + "cues": list(cues), "errors": list(errors), "scheme": scheme, + "std": std, "note": note, "need": need, + } + + +# ---------------------------------------------------------------- 动作族定义 +FAMILIES = [ + {"key": "push", "zh": "俯卧撑", "sub": "推 · 胸肩三头", "icon": "💪", "big6": True}, + {"key": "pull", "zh": "引体向上", "sub": "拉 · 背与二头", "icon": "🔗", "big6": True}, + {"key": "squat", "zh": "深蹲", "sub": "腿 · 股四头与臀", "icon": "🦵", "big6": True}, + {"key": "legraise", "zh": "举腿", "sub": "核心 · 腹与髋屈", "icon": "🧗", "big6": True}, + {"key": "bridge", "zh": "桥", "sub": "后链 · 脊柱与臀", "icon": "🌉", "big6": True}, + {"key": "handstand", "zh": "倒立撑", "sub": "肩 · 垂直推", "icon": "🤸", "big6": True}, + {"key": "dip", "zh": "臂屈伸", "sub": "辅助 · 三头与下胸", "icon": "🪑", "big6": False}, + {"key": "core", "zh": "核心稳定", "sub": "辅助 · 抗旋抗伸", "icon": "🧱", "big6": False}, + {"key": "calf", "zh": "小腿", "sub": "辅助 · 踝与跟腱", "icon": "🐾", "big6": False}, + {"key": "cardio", "zh": "代谢体能", "sub": "辅助 · 心肺", "icon": "🔥", "big6": False}, + {"key": "mobility", "zh": "拉伸灵活", "sub": "恢复 · 关节活动度", "icon": "🧘", "big6": False}, +] + +DIFFICULTY = [ + {"level": 1, "zh": "入门", "desc": "零基础可做,重在建立动作模式"}, + {"level": 2, "zh": "初级", "desc": "标准自重动作,打牢力量地基"}, + {"level": 3, "zh": "中级", "desc": "单侧/悬垂/增大行程,力量转化期"}, + {"level": 4, "zh": "高级", "desc": "接近单侧极限,需长期积累"}, + {"level": 5, "zh": "大师", "desc": "街健高阶技巧,数年功夫"}, +] + +# 部位标签(筛选用,顺序即展示顺序) +PARTS = ["胸", "背", "肩", "手臂", "核心", "臀", "腿", "小腿", "全身"] + + +# ================================================================ 一、俯卧撑链 +PUSH = [ + E("0659", "靠墙俯卧撑", "push", 1, ["胸", "肩", "手臂"], + ["双手与肩同宽撑墙,身体到脚跟连成一条直线,臀不要塌也不要撅", + "肩胛先主动下沉后收(把肩'插进'背里),再屈肘", + "肘部约 45° 夹向身体,不要外展成'T'字", + "推起时想象'把墙推远',顶端再把肩胛向前送一点"], + ["塌腰撅臀,靠腰发力", "含胸耸肩,颈部前伸", "手掌离墙太近导致行程过短"], + std=("1组×10次", "2组×25次", "3组×50次"), + note="六艺第一式起点。这一步是给肩肘做'适应性铺垫',别急着跳过。"), + + E("0493", "上斜俯卧撑", "push", 1, ["胸", "肩", "手臂"], + ["手撑在稳固台面(桌沿/窗台/单杠低位),越高越省力", + "全程绷紧臀和腹,肋骨下沉不外翻", + "下放到胸口轻触台面,停半拍再推", + "推起过程始终保持肩胛稳定,不要先抬头再抬身"], + ["台面不稳导致手滑", "只下放一半行程", "腰部先塌下去,身体像'折刀'"], + std=("1组×10次", "2组×20次", "3组×40次"), + note="通过调整台面高度可以无级调节难度,是最好用的进阶工具。"), + + E("3211", "跪姿俯卧撑", "push", 2, ["胸", "肩", "手臂", "核心"], + ["膝盖着地,膝-髋-肩三点保持一条直线,不要坐在小腿上", + "手掌位于乳头连线略下方,指尖朝前,五指抓地", + "下放时肘窝朝前、大臂贴近躯干约 45°", + "触地前停住,用胸大肌'夹'的感觉推起"], + ["髋部后坐,变成半程动作", "手放得太靠上,肩前侧代偿", "速度过快靠弹性反弹"], + std=("1组×10次", "2组×15次", "2组×30次")), + + E("0662", "标准俯卧撑", "push", 2, ["胸", "肩", "手臂", "核心"], + ["起始位:手在肩正下方偏外一拳,脚并拢或与髋同宽", + "臀腹同时收紧,把身体当成一块'木板'整体升降", + "下放 2 拍,胸口距地约一拳时停住;推起 1 拍,全程呼吸不憋", + "顶端不要锁死肘关节,保持张力立刻进入下一次"], + ["塌腰(腰椎受压最常见问题)", "手肘完全外展 90°,肩袖压力大", "颈部前伸用下巴够地"], + std=("1组×8次", "2组×20次", "2组×30次"), + note="能标准做满 2×20 再考虑进阶到窄距/单侧,急不得。"), + + E("3145", "俯卧撑推肩加强", "push", 2, ["胸", "肩", "核心"], + ["做完一个标准俯卧撑后,在顶端继续把肩胛向前推出去", + "推出的幅度不靠塌腰,而靠前锯肌把肩胛'贴'到肋骨上", + "感受腋下后方(前锯肌)发热,顶端停 1 秒", + "回落时肩胛主动后收回到中立位"], + ["用腰弓起来假装推肩", "顶端耸肩到耳朵旁"], + std=("1组×10次", "2组×15次", "3组×20次"), + note="前锯肌是倒立和高阶推的地基,别跳过这一步。"), + + E("1311", "宽距俯卧撑", "push", 2, ["胸", "肩"], + ["双手比肩宽约 1.5 倍,指尖略外旋", + "下放时重点感受胸大肌外侧被拉长", + "肘部走向与手掌朝向一致,避免手腕扭转", + "推起时想象'把两只手往中间挤'"], + ["手距过宽,肩关节前侧刺痛", "行程过短只做上半程"], + std=("1组×8次", "2组×15次", "2组×25次")), + + E("3217", "印度俯卧撑", "push", 2, ["肩", "胸", "手臂", "核心"], + ["从下犬式(臀部顶高、手脚成三角)开始", + "头部沿一条弧线'钻'过双手之间,像从桥洞下穿过", + "到底后展胸抬头进入上犬式,肩胛下沉不要耸肩", + "原路返回或直接抬臀回到下犬式"], + ["肩活动度不足硬做,腰椎代偿过度后伸", "速度过快,失去对轨迹的控制"], + std=("1组×5次", "2组×10次", "3组×15次"), + note="兼顾肩活动度与力量,很适合作为推日的热身。"), + + E("3662", "眼镜蛇俯卧撑", "push", 2, ["肩", "胸", "核心"], + ["下犬式起,屈肘让身体贴地前滑", + "靠胸和肩把上身'穿'过去,而不是靠腰甩", + "顶端展胸,肩下沉远离耳朵,颈部延长", + "抬臀回到起始位,全程保持流畅不停顿"], + ["腰部代偿过度反弓", "手腕压力过大(可垫毛巾或用拳撑)"], + std=("1组×5次", "2组×10次", "2组×15次")), + + E("0279", "下斜俯卧撑", "push", 3, ["胸", "肩", "核心"], + ["双脚垫高(凳/床沿),脚越高上胸和肩承担越多", + "身体保持一条直线,防止髋部下坠", + "下放时胸口指向地面,肘约 45°", + "推起顶端不要塌腰,核心持续绷紧"], + ["脚垫太高变成倒立撑姿势,肩负荷骤增", "髋下沉导致腰椎超伸"], + std=("1组×8次", "2组×15次", "2组×25次")), + + E("0259", "窄距俯卧撑", "push", 3, ["手臂", "胸", "肩"], + ["双手间距略窄于肩,位于胸骨正下方", + "大臂全程贴紧躯干,肘向后走而不是向外张", + "下放到大臂与地面平行以下,感受三头被拉紧", + "推起时'用肘尖顶地'的意念,顶端伸直但不锁死"], + ["肘外展,变回普通俯卧撑", "手腕过度背屈疼痛", "肩前顶代偿三头"], + std=("1组×8次", "2组×15次", "2组×25次")), + + E("0283", "钻石俯卧撑", "push", 3, ["手臂", "胸"], + ["双手拇指与食指相触组成菱形,置于胸骨正下方", + "大臂夹紧躯干,肘尖朝正后方", + "下放至胸口触碰手背,停半拍", + "三头主导推起,避免耸肩借力"], + ["手位太靠腹部,手腕吃力", "肘外张卸掉三头负荷", "含胸驼背"], + std=("1组×5次", "2组×12次", "2组×20次"), + note="手腕不适时改用拳撑或俯卧撑架。"), + + E("0490", "上斜窄距俯卧撑", "push", 2, ["手臂", "胸"], + ["手撑高台成窄距,是钻石俯卧撑的减负版本", + "大臂贴身,肘向后", + "台面越高越轻松,逐步降低高度进阶"], + ["肘外展", "行程不足"], + std=("1组×10次", "2组×15次", "3组×25次")), + + E("0699", "拍肩俯卧撑", "push", 3, ["核心", "胸", "肩"], + ["撑在俯卧撑顶端,双脚略分开增加底面积", + "抬一只手拍对侧肩,重点是骨盆和肩带**不许**转动", + "支撑侧用力抓地推地,对抗身体倾倒", + "换手,节奏稳定不抢拍"], + ["骨盆左右摇摆(说明抗旋能力不足,先分开双脚)", "抬手过快靠甩"], + scheme="rep", std=("1组×10次(左右各5)", "2组×20次", "3组×30次"), + note="练的是抗旋核心,为单臂俯卧撑铺路。"), + + E("0258", "时钟俯卧撑", "push", 3, ["胸", "肩", "核心"], + ["每做一次俯卧撑,一只手向外'走'一格,像时针移动", + "手移动时另一侧保持稳定,身体不塌不扭", + "不同角度会打到胸大肌不同区域", + "绕一圈算一组"], + ["手走位时腰塌下去", "移动幅度过大导致肩不适"], + std=("半圈", "1圈", "2圈")), + + E("3294", "弓箭手俯卧撑", "push", 4, ["胸", "手臂", "核心"], + ["双手比肩宽很多,下放时重心移向一侧,该侧屈肘", + "另一侧手臂尽量伸直,像拉弓一样'撑开'", + "承重侧肘约 45°,伸直侧掌根压地帮助推起", + "推起主要靠承重侧,伸直侧只做辅助"], + ["伸直侧偷偷屈肘分担太多", "身体扭转,肩线不平行地面"], + std=("1组×5次(每侧)", "2组×10次", "2组×15次"), + note="通向单臂俯卧撑的关键一步。"), + + E("0666", "抬手俯卧撑", "push", 4, ["胸", "核心", "肩"], + ["单手撑地,另一只手抬离地面(背后或身侧)", + "双脚分开加大底面积,抗旋是本动作的核心", + "先只做顶端支撑,能稳住再逐步加下放幅度", + "承重侧肩胛全程下沉稳定,不要塌向地面"], + ["身体大幅扭转", "承重肩耸起", "急于做全程导致肩肘拉伤"], + std=("1组×3次(每侧)", "2组×6次", "2组×10次")), + + E("1273", "击掌俯卧撑", "push", 4, ["胸", "手臂", "全身"], + ["下放到底后爆发式推起,把身体推离地面", + "腾空瞬间快速击掌,落地时手先着地并主动屈肘缓冲", + "核心全程绷紧,落地不允许塌腰", + "追求'推得高'而不是'掌拍得响'"], + ["落地锁死肘关节冲击关节", "腰塌下去缓冲", "疲劳后继续做,容易摔"], + std=("1组×3次", "2组×8次", "3组×12次"), + note="爆发类动作放在训练前段做,力竭状态下别练。"), + + E("1306", "爆发俯卧撑", "push", 4, ["胸", "手臂", "全身"], + ["和击掌俯卧撑同理,但不强求击掌,专注推离地面的高度", + "下放阶段慢,推起阶段最快", + "双手离地后保持身体刚性,落地缓冲"], + ["用腰甩起身体", "落地无缓冲"], + std=("1组×5次", "2组×10次", "3组×15次")), + + E("0725", "单臂俯卧撑", "push", 5, ["胸", "手臂", "核心"], + ["双脚分开约与肩同宽或更宽(越宽越稳)", + "撑地手位于胸骨正下方偏外,不要在肩正下", + "另一只手放背后,全程禁止扭髋——肩线与地面平行", + "下放极慢,靠核心抗旋而不是靠转体卸力"], + ["转髋成侧向俯卧撑(等于作弊)", "行程不足只做上 1/3", "基础不够就上手,肘部易伤"], + std=("1组×1次(每侧)", "2组×5次", "2组×10次"), + note="前置条件:弓箭手俯卧撑 2×10 + 抬手俯卧撑能稳住全程。"), + + E("0803", "超人俯卧撑", "push", 5, ["胸", "核心", "全身"], + ["爆发推起,腾空瞬间双臂前伸、双腿后伸成'超人'姿态", + "落地立刻回到俯卧撑位并缓冲", + "需要极强的爆发力和落地控制"], + ["落地砸胸", "核心松散导致腰椎冲击"], + std=("1次", "3次", "2组×5次"), + note="高风险动作,建议在软垫上练习。"), + + E("3327", "直臂支撑俯卧撑", "push", 5, ["肩", "胸", "核心"], + ["先能稳定保持 full planche 15 秒以上再谈这个动作", + "全程肩胛前伸下压,身体前倾把肩推到手掌前方", + "双腿并拢绷直,臀部不高于肩", + "屈肘下放时肩持续前压,否则会掉髋"], + ["髋部高抬变成'高抬臀支撑'", "肩胛后缩失去前锯肌支撑"], + std=("1次", "3次", "2组×5次"), + note="街健顶级动作,需数年积累,务必循序渐进。"), +] + + +# ================================================================ 二、引体向上链(挂墙单杠) +PULL = [ + E("3166", "站姿自重划船", "pull", 1, ["背", "手臂"], + ["双手抓稳固支撑物(门框/低杠),身体后倾成斜板", + "先用肩胛后缩启动,再屈肘把胸口拉向手", + "身体越直立越轻松,越水平越难", + "顶端停 1 秒,感受两侧肩胛骨互相靠拢"], + ["先屈肘后收肩胛(顺序反了,二头代偿)", "耸肩", "腰塌下去"], + std=("1组×10次", "2组×20次", "3组×30次"), + note="完全没有拉力基础时的起点,可用毛巾挂门把手完成。"), + + E("0688", "肩胛引体", "pull", 1, ["背", "肩"], + ["悬挂在单杠上,手臂全程保持伸直", + "只靠肩胛下沉后收把身体'提'起 3-5 厘米", + "顶端肩远离耳朵,感受背阔和下斜方发力", + "缓慢放松回到被动悬挂,但不完全松掉张力"], + ["屈肘变成半个引体(那就不是这个动作了)", "耸肩用斜方上束"], + std=("1组×5次", "2组×10次", "3组×15次"), + need="单杠", + note="做不了引体向上的人,第一步应该是这个,先学会'用背'。"), + + E("2300", "屈膝反向划船", "pull", 1, ["背", "手臂", "核心"], + ["单杠调低或用桌沿,屈膝双脚踩地,身体成斜板", + "肩胛先后缩下沉,再拉肘向后", + "胸口尽量碰到杠,顶端停半拍", + "下放 2-3 秒,控制离心"], + ["用腿蹬地借力", "髋部下坠", "只拉一半"], + std=("1组×10次", "2组×20次", "3组×25次"), + need="单杠"), + + E("0499", "反向划船", "pull", 2, ["背", "手臂", "核心"], + ["双腿伸直,脚跟着地,身体从肩到脚一条直线", + "肩胛后缩启动,肘沿身体两侧向后拉", + "胸口触杠,全程臀腹绷紧不塌腰", + "离心 3 秒放回,肘伸直但肩胛保持一点张力"], + ["髋部先落地", "耸肩把杠拉向下巴", "行程不足"], + std=("1组×8次", "2组×15次", "3组×20次"), + need="单杠", + note="能做 3×20 之后,脚垫高继续加难度,是引体的最佳前置。"), + + E("3019", "脚踩辅助引体", "pull", 2, ["背", "手臂"], + ["单杠下放一把凳子,单脚或双脚踩上去分担体重", + "拉的时候腿只出'最小必要的力',主动权交给背", + "顶端下巴过杠,停 1 秒", + "下放全程用腿轻托,控制 3 秒落回"], + ["腿蹬得太狠,背几乎没参与", "顶端靠仰头凑数"], + std=("1组×5次", "2组×10次", "3组×15次"), + need="单杠 + 凳"), + + E("1772", "反向俯卧撑抬肘", "pull", 2, ["背", "肩"], + ["仰卧地面,屈肘撑地,用肘把上背推离地面", + "肩胛后缩下沉,胸口向上顶", + "顶端停 2 秒,感受中下斜方"], + ["用腰弓起来", "颈部后仰用力"], + std=("1组×8次", "2组×15次", "3组×20次")), + + E("1326", "反手引体向上", "pull", 3, ["背", "手臂"], + ["掌心朝向自己,握距与肩同宽", + "起始先做肩胛下沉,别直接屈肘", + "想象'把肘往地面拉',让背阔主导,二头是副手", + "下巴过杠停半拍,离心 2-3 秒放回全臂伸直"], + ["屈腿摆动借力(kipping)", "只做上半程不放到底", "耸肩"], + std=("1组×5次", "2组×10次", "2组×15次"), + need="单杠", + note="反手比正手轻松,是从辅助引体过渡到标准引体的中间站。"), + + E("1327", "窄距反手引体", "pull", 3, ["手臂", "背"], + ["双手间距约一拳,掌心朝自己", + "拉起时肘贴身体两侧向下走", + "顶端把胸口带向杠,二头收缩感最强"], + ["身体前后晃动", "手腕过度弯曲"], + std=("1组×5次", "2组×10次", "2组×15次"), + need="单杠"), + + E("0652", "标准引体向上", "pull", 3, ["背", "手臂", "核心"], + ["正握(掌心朝外),握距略宽于肩,拇指可绕杠也可与四指同侧", + "起始:死悬 → 先肩胛下沉后收,把肩从耳朵旁'拔'下来", + "肘尖朝下朝后走,想象'把杠掰弯'而不是'把下巴抬高'", + "顶端胸口靠近杠、下巴过杠,停半拍;离心 2-3 秒回到全臂伸直"], + ["借摆动/踢腿起身", "不放到底(半程引体)", "耸肩,肩带没参与", + "过度后仰用惯性"], + std=("1组×5次", "2组×10次", "2组×15次"), + need="单杠", + note="六艺第二式的核心。挂墙单杠请先确认膨胀螺栓牢固、承重≥150kg。"), + + E("0651", "对握引体向上", "pull", 3, ["背", "手臂"], + ["双手相对(需要平行握把或在杠上前后握)", + "对肩关节最友好的握法,肩不适时优先选它", + "拉起时肘贴身体,背阔与肱肌共同发力"], + ["耸肩", "行程不足"], + std=("1组×5次", "2组×10次", "2组×15次"), + need="单杠"), + + E("0674", "反握引体向上", "pull", 3, ["背", "手臂"], + ["掌心朝自己、握距略宽", + "背阔与二头共同发力,比正握容易几次", + "全程控制,不借力"], + ["肘部内扣压力大", "摆动"], + std=("1组×5次", "2组×10次", "2组×15次"), + need="单杠"), + + E("0627", "交叉握引体", "pull", 3, ["背", "手臂", "核心"], + ["一手正握一手反握,身体会自然偏向反握侧", + "刻意对抗这个偏移,保持身体正对杠", + "每组换手,两侧均衡"], + ["身体大幅侧偏", "只练强势侧"], + std=("1组×4次(每种握法)", "2组×8次", "2组×12次"), + need="单杠", + note="是通向单臂引体的过渡动作之一。"), + + E("1429", "宽距引体向上", "pull", 4, ["背", "肩"], + ["握距约 1.5 倍肩宽,肘尖朝外下方", + "行程比标准引体短,但背阔外侧刺激更强", + "肩胛后缩下沉后再拉,避免全靠肩关节硬拽", + "顶端上胸靠杠"], + ["握距过宽导致肩前侧疼", "只做很短的行程"], + std=("1组×4次", "2组×8次", "2组×12次"), + need="单杠"), + + E("3418", "L型引体向上", "pull", 4, ["背", "核心", "手臂"], + ["双腿并拢前抬与地面平行(L 形),全程保持", + "腹肌持续收紧防止腿掉下去,同时完成引体", + "身体不能前后摆动,这是本动作最难的部分", + "先能做 L 支撑悬挂 15 秒再合并引体"], + ["腿逐渐下垂", "靠摆动完成", "屈膝偷懒(可作为降阶)"], + std=("1组×3次", "2组×6次", "2组×10次"), + need="单杠"), + + E("0466", "胸骨引体", "pull", 4, ["背", "核心"], + ["拉到高位时身体后仰,让胸骨下缘(而非下巴)去够杠", + "整个过程背阔极限收缩,是行程最长的引体变式", + "核心与臀绷紧控制后仰角度,不是靠甩"], + ["靠腰甩成后空翻趋势", "肩后伸过度"], + std=("1组×3次", "2组×6次", "2组×10次"), + need="单杠"), + + E("3292", "分段停顿引体", "pull", 4, ["背", "手臂"], + ["把引体拆成若干段:拉起 1/3 停 1 秒,2/3 停 1 秒,顶端停 1 秒", + "下放同样分段停顿", + "全程消除惯性,暴露力量薄弱区间"], + ["某一段直接冲过去", "停顿时松掉肩胛张力"], + std=("1组×2次", "2组×4次", "2组×6次"), + need="单杠"), + + E("0678", "洛奇式引体", "pull", 4, ["背", "核心"], + ["拉到顶端后,头部在杠前/杠后交替切换", + "切换时靠核心和背控制,不是靠脖子", + "对肩灵活性要求较高"], + ["颈后位置压迫肩关节(有肩伤者跳过)", "靠摆动切换"], + std=("1组×3次", "2组×6次", "2组×10次"), + need="单杠", + note="肩关节前伸受限的人建议只做杠前版本。"), + + E("0670", "颈后引体向上", "pull", 4, ["背", "肩"], + ["宽握,拉起时杠经过头顶落到颈后", + "需要良好的肩外旋活动度,肩紧的人不要做", + "全程缓慢,禁止用头去撞杠"], + ["肩活动度不足硬做,肩袖受伤风险高", "低头躲杠"], + std=("1组×3次", "2组×6次", "2组×10次"), + need="单杠", + note="⚠️ 争议动作。收益有限而肩风险偏高,肩部有任何不适请直接跳过。"), + + E("3293", "弓箭手引体", "pull", 5, ["背", "手臂", "核心"], + ["超宽握,拉起时身体移向一侧,该侧屈肘承重", + "另一侧手臂保持伸直,沿杠滑动'拉弓'", + "伸直侧只提供极少辅助,主要负荷在承重侧", + "顶端承重侧肩靠近手"], + ["伸直侧屈肘分担太多", "身体没有真正横移"], + std=("1组×2次(每侧)", "2组×5次", "2组×8次"), + need="单杠"), + + E("0638", "单臂引体向上", "pull", 5, ["背", "手臂", "核心"], + ["单手全握杠,另一只手完全离杠(或握手腕作为辅助降阶)", + "起手先肩胛下沉,锁住肩关节再拉", + "身体会自然旋转,用核心抗旋保持正对", + "离心阶段最有价值,先从 5-10 秒慢放开始练"], + ["握力先崩(先单独练悬挂)", "用甩动完成", "肘部完全伸直状态猛发力,易致肘肌腱炎"], + std=("1次(每侧)", "2组×2次", "2组×5次"), + need="单杠", + note="需要长期积累。建议路径:弓箭手引体 → 单臂离心 → 辅助单臂 → 单臂。"), + + E("0631", "双力臂", "pull", 5, ["背", "手臂", "全身"], + ["高位引体拉到胸口位置,同时身体前倾越过杠", + "手腕快速翻转到杠上方(这是技术难点,不是力量难点)", + "翻上后接一个臂屈伸推起完成动作", + "先分别练成:高位引体(拉到腹部)+ 直杠臂屈伸"], + ["纯靠摆荡(kipping)而没有力量基础", "手腕翻转不及时导致撞杠", + "肩前侧代偿严重"], + std=("1次", "3次", "2组×5次"), + need="单杠", + note="街健标志性动作。前置:标准引体 2×15、直杠臂屈伸 2×10。"), + + E("0558", "摆动双力臂", "pull", 4, ["背", "全身"], + ["利用身体摆荡的惯性辅助过杠", + "摆荡幅度由核心控制,不是乱晃", + "翻腕时机与摆荡最高点一致"], + ["完全依赖惯性,肩肘承受冲击", "手腕拉伤"], + std=("1次", "3次", "2组×5次"), + need="单杠"), + + E("3304", "团身翻(穿猫)", "pull", 4, ["肩", "背", "核心"], + ["悬挂,屈膝团身,双腿从双臂之间穿过并继续后翻", + "全程直臂,靠肩关节大幅度后伸完成", + "缓慢进行,肩活动度不足不要硬翻", + "翻到底停 2 秒,再原路返回"], + ["速度过快导致肩关节被'甩'到极限位", "松手"], + std=("1次", "3次", "2组×5次"), + need="单杠", + note="是前水平/后水平的必备肩部准备动作。"), + + E("3296", "前水平", "pull", 5, ["背", "核心", "全身"], + ["悬挂直臂,身体后仰至与地面平行,面朝上", + "肩胛下沉后缩,直臂靠背阔'下压'而非屈肘", + "臀腹极限绷紧,防止腰下坠", + "降阶顺序:团身 → 分腿 → 单腿伸直 → 全水平"], + ["屈肘(不算标准)", "髋部下沉成'香蕉形'", "跳级练习导致肘肌腱炎"], + scheme="hold", std=("团身5秒", "分腿10秒", "全水平10秒"), + need="单杠"), + + E("3295", "前水平划船", "pull", 5, ["背", "核心"], + ["在前水平姿态下完成划船(屈肘把身体拉向杠)", + "全程保持身体水平,不允许髋部起伏", + "极高难度,需要先稳定保持前水平 15 秒以上"], + ["身体一屈一伸像'虫子'", "腰塌"], + std=("1次", "3次", "2组×5次"), + need="单杠"), + + E("3297", "后水平", "pull", 5, ["背", "肩", "核心"], + ["从团身翻的底部姿态展开,面朝下与地面平行", + "直臂,肩前侧和二头承受巨大拉力——务必充分热身", + "臀腹绷紧,身体成一条直线", + "降阶:团身 → 分腿 → 全水平"], + ["热身不足直接上(二头肌腱高风险)", "腰部松塌"], + scheme="hold", std=("团身5秒", "分腿10秒", "全水平10秒"), + need="单杠", + note="⚠️ 二头肌腱受伤高发动作,必须从团身开始逐步展开。"), +] + + +# ================================================================ 三、深蹲链 +SQUAT = [ + E("3132", "扶物深蹲", "squat", 1, ["腿", "臀"], + ["双手扶门框/桌沿分担体重,双脚与肩同宽、脚尖略外八", + "先向后坐髋(像坐椅子),再屈膝下蹲", + "膝盖始终指向脚尖方向,不要内扣", + "蹲到大腿低于水平,用扶手辅助起身"], + ["膝内扣(最常见,长期损伤半月板)", "脚跟离地", "只蹲一点点"], + std=("1组×10次", "2组×15次", "3组×30次"), + note="六艺第三式起点,膝伤恢复期或零基础从这里开始。"), + + E("3221", "半蹲", "squat", 1, ["腿", "臀"], + ["徒手下蹲到大腿约与地面成 45°,不需要蹲到底", + "重心落在全脚掌,膝盖不超过脚尖太多", + "起身时脚跟发力'把地面推开',臀部主动收紧", + "全程躯干挺直,眼看前方"], + ["弓背", "重心过度前移到脚尖", "膝内扣"], + std=("1组×10次", "2组×20次", "3组×40次")), + + E("3119", "全蹲(深蹲到底)", "squat", 2, ["腿", "臀", "核心"], + ["双脚与肩同宽或略宽,脚尖外展 15-30°", + "全程脚掌三点(拇趾球/小趾球/脚跟)抓地不塌足弓", + "下蹲到大腿贴小腿(ATG),底部保持躯干挺直不'屁股眨眼'", + "起身时髋膝同步伸展,顶端收紧臀部但不要过度后仰"], + ["底部骨盆后倾(屁股眨眼)压迫腰椎", "膝内扣", "脚跟离地(踝背屈不足,可先垫高脚跟)"], + std=("1组×10次", "2组×25次", "3组×50次"), + note="踝关节活动度不够时,脚跟垫 2cm 书本即可正常完成。"), + + E("0624", "靠墙静蹲", "squat", 2, ["腿"], + ["背贴墙下滑至大腿与地面平行,小腿垂直地面", + "膝盖正对脚尖,重心均匀分布", + "全程正常呼吸,不要憋气", + "感觉股四头肌灼烧是正常的"], + ["膝盖超过脚尖(脚往前挪一点)", "臀部离墙", "憋气"], + scheme="hold", std=("30秒", "60秒", "2分钟")), + + E("2368", "分腿蹲", "squat", 2, ["腿", "臀"], + ["前后开立约一大步,前脚全掌着地、后脚脚尖点地", + "垂直下沉,后膝轻触地面,前膝不超过脚尖过多", + "起身靠前腿脚跟蹬地,后腿只做平衡", + "躯干保持直立,不要前倾去够地"], + ["步幅太小导致前膝压力大", "身体左右晃", "后腿蹬地借力"], + std=("1组×8次(每侧)", "2组×15次", "3组×20次")), + + E("3470", "前弓步", "squat", 2, ["腿", "臀", "核心"], + ["站立位向前迈一大步,双膝同时下沉至约 90°", + "重心在两腿之间,躯干直立", + "用前腿脚跟蹬地推回起始位", + "落地要轻,膝盖不砸地"], + ["迈步太小", "前膝内扣", "上身前倾"], + std=("1组×8次(每侧)", "2组×15次", "3组×20次")), + + E("1460", "行走弓步", "squat", 2, ["腿", "臀", "核心"], + ["连续向前迈步做弓步,每一步都下沉到位", + "换腿时保持躯干稳定,不要靠上身摆动带动", + "全程核心收紧,视线平视"], + ["步幅忽大忽小", "上下起伏太快失去控制"], + std=("1组×10步", "2组×20步", "3组×30步")), + + E("3769", "屈膝礼深蹲", "squat", 3, ["臀", "腿"], + ["一腿向后斜跨到对侧后方,像行屈膝礼", + "下蹲时前腿膝盖对准脚尖,臀中肌有明显牵拉感", + "起身靠前腿臀部发力把身体'拧'回来"], + ["骨盆过度旋转", "重心不稳左右晃"], + std=("1组×8次(每侧)", "2组×15次", "3组×20次"), + note="针对臀中肌,能有效改善深蹲膝内扣。"), + + E("0514", "跳蹲", "squat", 3, ["腿", "臀", "全身"], + ["下蹲到 1/2 深度后爆发式向上跳起", + "落地时用前脚掌先着地,随即屈髋屈膝缓冲到下蹲位", + "落地无声是技术到位的标志", + "追求'跳得高',不是'跳得多'"], + ["落地直腿硬砸(膝踝冲击大)", "疲劳后继续做导致动作变形", "膝内扣落地"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("3582", "跳跃弓步", "squat", 3, ["腿", "臀", "全身"], + ["弓步位起跳,空中换腿,落地成另一侧弓步", + "落地缓冲深一点,保护膝关节", + "核心收紧维持躯干直立"], + ["落地过硬", "换腿时身体扭转失衡"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("3543", "落地跳蹲", "squat", 4, ["腿", "全身"], + ["从台阶落下,触地瞬间立刻爆发跳起(触地时间越短越好)", + "落地缓冲与蹬伸连贯,像弹簧", + "台阶高度从 20cm 起步"], + ["触地停留过久失去反弹意义", "台阶过高导致落地失控"], + std=("1组×5次", "2组×10次", "3组×12次"), + note="爆发力训练,放在训练最前段,组间充分休息。"), + + E("1476", "单腿蹲(辅助)", "squat", 4, ["腿", "臀", "核心"], + ["单腿站立,另一腿前伸,手扶支撑物或抓住门框辅助", + "向后坐髋下蹲,支撑腿膝盖对准脚尖", + "下蹲越深越难,先从蹲到凳子上开始", + "起身用脚跟发力,避免上身过度前倾"], + ["支撑腿膝内扣", "靠手臂拉起来(辅助只是防摔)", "脚跟离地"], + std=("1组×5次(每侧)", "2组×10次", "2组×15次")), + + E("1759", "手枪蹲", "squat", 5, ["腿", "臀", "核心"], + ["单腿站立,另一腿完全伸直前抬且全程不落地", + "下蹲到底时支撑腿完全折叠,需要极好的踝背屈与平衡", + "起身时脚跟牢牢抓地,核心带动躯干上抬", + "降阶:垫高脚跟 / 抓门框 / 蹲到箱子上"], + ["脚跟离地翻倒", "伸直腿落地借力", "底部弓背严重"], + std=("1组×1次(每侧)", "2组×5次", "2组×10次"), + note="六艺第三式终点。踝活动度往往比腿部力量更早成为瓶颈。"), + + E("1489", "西西里深蹲", "squat", 4, ["腿"], + ["跪姿或站姿,膝盖前移、身体后仰成一条直线", + "股四头肌离心拉长,膝关节承受较大剪切力", + "扶物辅助,缓慢进行", + "有膝伤者不要做"], + ["速度过快", "腰部代偿折叠"], + std=("1组×5次", "2组×10次", "2组×20次"), + note="⚠️ 对膝关节要求高,膝盖有伤史请跳过。"), + + E("0496", "北欧腿弯举", "squat", 4, ["腿", "臀"], + ["跪姿,脚踝被固定(塞沙发下或请人压住)", + "保持髋膝肩一条直线,身体缓慢向前倒", + "全靠腘绳肌离心控制,倒到极限后用手撑地", + "推起时腘绳肌尽力参与"], + ["屈髋'折刀'式下落(等于卸掉负荷)", "下落过快失控砸地"], + std=("1组×3次", "2组×6次", "3组×10次"), + note="预防腘绳肌拉伤效果极好,但离心负荷大,隔天酸痛正常。"), + + E("3193", "臀腿抬升", "squat", 4, ["腿", "臀"], + ["与北欧腿弯举同源,膝跪地、脚踝固定", + "髋部锁死不折叠,身体作为一个整体前倒后起", + "腘绳肌与臀大肌同时发力"], + ["屈髋作弊", "腰椎超伸"], + std=("1组×3次", "2组×8次", "3组×12次")), + + E("0730", "单腿滑板蹲", "squat", 3, ["腿", "臀"], + ["支撑腿站稳,另一腿向侧方或后方滑出(袜子+光滑地面)", + "支撑腿承担绝大部分体重,缓慢下蹲", + "滑出腿保持轻触地面,不承重"], + ["滑出腿撑地借力", "支撑腿膝内扣"], + std=("1组×6次(每侧)", "2组×12次", "3组×15次")), +] + + +# ================================================================ 四、举腿链(核心前链) +LEGRAISE = [ + E("0276", "死虫式", "legraise", 1, ["核心"], + ["仰卧,双臂垂直指向天花板,髋膝各屈 90°", + "腰部主动压向地面,全程不允许出现缝隙", + "缓慢伸出对侧手脚,接近地面时停住再收回", + "呼气时发力伸出,感受腹部深层持续收紧"], + ["腰部拱起离地(说明伸出幅度超过控制能力,减小幅度)", "憋气", "速度过快"], + std=("1组×10次", "2组×20次", "3组×30次"), + note="六艺第四式起点。学会'腰贴地'是后面所有举腿动作的前提。"), + + E("0872", "反向卷腹", "legraise", 1, ["核心"], + ["仰卧屈膝,双手放身体两侧压地", + "用下腹把骨盆向上'卷'起来,膝盖靠向胸口", + "不是靠腿甩,是靠腹肌把尾骨抬离地面", + "落回时慢,脚不落地保持张力"], + ["用腿部惯性甩起来", "手臂猛压地借力", "颈部紧张"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("0274", "地面卷腹", "legraise", 1, ["核心"], + ["仰卧屈膝,双手轻放耳侧(别抱头拉脖子)", + "下巴微收,用腹直肌把肩胛骨卷离地面即可", + "顶端呼气收紧停 1 秒,腰始终贴地", + "落回时脊柱一节一节铺回地面"], + ["双手拉扯脖子", "起太高变成髋屈肌主导的仰卧起坐", "靠甩"], + std=("1组×10次", "2组×25次", "3组×40次")), + + E("0484", "屈膝提臀", "legraise", 1, ["核心"], + ["仰卧屈膝,双腿抬起,用下腹把髋部垂直顶向天花板", + "幅度很小,几厘米即可,重点是腹部主导", + "顶端停 1 秒,缓慢落回"], + ["用手压地把腿甩上去", "幅度过大变成翻滚"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("0620", "平躺举腿", "legraise", 2, ["核心"], + ["仰卧,双手压在臀下或抓固定物,腰主动贴地", + "双腿并拢伸直,缓慢抬到垂直", + "下放是关键:慢到 3 秒,落到脚跟离地约 5cm 时停住", + "全程腰不许离地,一旦离地立刻屈膝降阶"], + ["腰部拱起(腰椎最大风险点)", "下放过快靠反弹", "膝盖弯曲卸力"], + std=("1组×8次", "2组×15次", "3组×25次")), + + E("0865", "举腿提臀", "legraise", 2, ["核心"], + ["平躺举腿到垂直后,再用下腹把臀部推离地面", + "推的方向是'把脚送向天花板',不是向头侧甩", + "顶端停半拍,缓慢落回"], + ["用手臂猛推地面", "身体向头侧翻滚"], + std=("1组×8次", "2组×15次", "3组×20次")), + + E("0003", "空中自行车", "legraise", 2, ["核心"], + ["仰卧,交替蹬腿并用对侧肘靠向膝盖", + "转体靠腹斜肌,不是靠手肘拽脖子", + "腰部尽量贴地,蹬出的腿越低越难", + "节奏放慢,每次转体停半拍"], + ["拉扯颈部", "速度飞快失去控制", "腰离地"], + std=("1组×20次", "2组×40次", "3组×60次")), + + E("0687", "俄罗斯转体", "legraise", 2, ["核心"], + ["坐姿,上身后倾约 45°,双腿抬离地面(降阶可落地)", + "转体时肩带和胸廓一起转,不只是手臂左右摆", + "腰背保持挺直,不要弓背", + "每侧触地或触髋侧停半拍"], + ["只摆手臂,躯干没转", "弓背", "速度过快"], + std=("1组×20次", "2组×40次", "3组×60次")), + + E("0507", "折刀仰卧起", "legraise", 3, ["核心"], + ["仰卧伸直,上身与双腿同时抬起在中间相遇成 V 形", + "手去够脚尖,顶端停 1 秒", + "落回时上身与腿同步下放,不要一先一后"], + ["靠甩起来", "腰部压力大(可屈膝降阶)"], + std=("1组×8次", "2组×15次", "3组×25次")), + + E("2963", "罗马椅直腿举", "legraise", 3, ["核心"], + ["前臂撑在扶手上,背贴靠垫,肩胛下沉不耸肩", + "腰部贴住靠垫,用下腹把并拢的直腿抬到水平以上", + "下放 3 秒,不要让腿'掉'下去", + "全程不摆动"], + ["靠身体前后摆动甩腿", "耸肩挂在扶手上", "只抬到很低的高度"], + std=("1组×5次", "2组×10次", "3组×15次"), + need="罗马椅/双杠"), + + E("0826", "双杠垂直举腿", "legraise", 3, ["核心"], + ["双手撑双杠,肩胛下沉把身体'撑'高,不要塌在肩上", + "用下腹把并拢的腿抬起,抬到水平或更高", + "下放缓慢,控制骨盆不要前倾", + "背部微贴,避免腰部反弓"], + ["前后摆动借力", "耸肩下沉塌肩", "腰部反弓"], + std=("1组×5次", "2组×12次", "3组×20次"), + need="双杠"), + + E("2355", "悬垂屈膝举腿", "legraise", 3, ["核心", "背"], + ["正握单杠悬挂,先做肩胛下沉锁住肩带", + "用下腹把膝盖提向胸口,骨盆要有向上卷的动作", + "顶端停半拍,下放 3 秒", + "全程不摆动——如果晃,说明重量还太大,先练悬挂"], + ["靠摆动荡上去", "只屈髋不卷骨盆(那是髋屈肌在练,不是腹肌)", "耸肩"], + std=("1组×8次", "2组×15次", "3组×25次"), + need="单杠"), + + E("0472", "悬垂举腿", "legraise", 3, ["核心", "背"], + ["悬挂,肩胛下沉,腿并拢", + "抬腿时先屈髋再卷骨盆,让下背轻微后凸,腹肌才真正参与", + "抬到与地面平行以上,停半拍", + "下放 3 秒,落到底不摆动再做下一次"], + ["摆荡(最普遍的问题)", "只抬到很低就算数", "握力先崩溃"], + std=("1组×5次", "2组×12次", "3组×20次"), + need="单杠", + note="挂墙单杠的核心训练首选。握力不足可用助力带,但别让它替你练腹。"), + + E("0475", "悬垂直腿举", "legraise", 4, ["核心", "背"], + ["与悬垂举腿相同,但双腿全程保持伸直", + "力臂变长,难度显著上升", + "抬到脚尖与杠齐平(L 位以上)", + "下放极慢,不允许摆动"], + ["屈膝偷懒", "摆荡", "腰部代偿"], + std=("1组×5次", "2组×10次", "3组×15次"), + need="单杠"), + + E("1764", "悬垂举腿提臀", "legraise", 4, ["核心", "背"], + ["悬垂举腿到水平后,继续用下腹把臀部向上卷", + "目标是让骨盆明显翻起,而不是腿抬得更高", + "顶端停 1 秒再缓慢下放"], + ["只抬腿不卷髋", "靠摆动"], + std=("1组×3次", "2组×8次", "2组×12次"), + need="单杠"), + + E("1761", "悬垂侧向提膝", "legraise", 3, ["核心", "背"], + ["悬挂,把膝盖斜向一侧胸口提起", + "转体由腹斜肌完成,肩带保持稳定", + "两侧交替,幅度对称"], + ["靠身体扭转甩腿", "一侧幅度明显大于另一侧"], + std=("1组×8次(左右各4)", "2组×16次", "3组×24次"), + need="单杠"), + + E("0473", "悬垂折体", "legraise", 5, ["核心", "背"], + ["悬垂直腿举到脚尖触杠(脚碰到手的位置)", + "需要极强的下腹力量与腘绳肌柔韧", + "全程直腿不摆动,下放缓慢受控"], + ["屈膝", "靠摆荡上去"], + std=("1组×3次", "2组×8次", "2组×12次"), + need="单杠", + note="六艺第四式终点级别的动作。"), + + E("0467", "大猩猩引体", "legraise", 4, ["核心", "背"], + ["悬垂状态下同时完成举腿与引体,膝盖靠向胸口", + "上下同时收缩,腹肌与背阔共同发力", + "顶端团成一团,停半拍"], + ["先拉后抬(拆成两个动作就失去意义)", "摆荡"], + std=("1组×3次", "2组×8次", "2组×12次"), + need="单杠"), + + E("3419", "地面 L 支撑", "legraise", 4, ["核心", "手臂"], + ["坐地,双手撑在髋两侧,肩胛下沉把身体撑离地面", + "双腿并拢伸直前举,与地面平行成 L 形", + "股四头肌主动收紧把膝盖压直,脚尖绷直", + "呼吸不能停,腰背不要过度弓起"], + ["屈膝(可作为降阶:团身 L 支撑)", "耸肩塌肩", "憋气"], + scheme="hold", std=("10秒", "30秒", "60秒")), + + E("3420", "V 字支撑", "legraise", 5, ["核心", "手臂"], + ["在 L 支撑基础上把双腿继续抬高至接近垂直", + "需要极强的髋屈与腹肌力量 + 腘绳肌柔韧", + "肩胛持续下沉,胸口打开"], + ["靠含胸缩肩假装抬高", "屈膝"], + scheme="hold", std=("5秒", "15秒", "30秒")), + + E("0871", "团身卷腹", "legraise", 2, ["核心"], + ["坐姿后倾,双膝收向胸口同时上身前卷", + "在中间'团'成一团,顶端停 1 秒", + "伸展时手脚同时展开,但腰不落地"], + ["靠惯性来回甩", "腰部完全落地失去张力"], + std=("1组×10次", "2组×20次", "3组×30次")), +] + + +# ================================================================ 五、桥链(后链与脊柱) +BRIDGE = [ + E("3147", "骨盆后倾", "bridge", 1, ["核心", "臀"], + ["仰卧屈膝,把腰部主动压向地面消除缝隙", + "动作靠下腹和臀部同时收紧完成,幅度很小", + "保持 3 秒再放松,呼吸不要停"], + ["用腿蹬地把骨盆推上去", "憋气"], + std=("1组×10次", "2组×20次", "3组×30次"), + note="六艺第五式起点,也是治疗久坐腰痛的基础动作。"), + + E("3013", "低位臀桥", "bridge", 1, ["臀", "核心"], + ["仰卧屈膝,脚跟距臀约一掌,脚掌踩实", + "先做骨盆后倾,再用脚跟发力把髋顶起", + "顶端膝-髋-肩一条直线,臀部夹紧停 2 秒", + "全程不靠腰椎反弓来'凑'高度"], + ["顶得过高造成腰椎超伸(顶端应是臀酸不是腰酸)", "膝盖外八", "用脚尖发力"], + std=("1组×10次", "2组×25次", "3组×40次")), + + E("1422", "卷起成桥", "bridge", 1, ["臀", "核心"], + ["从骨盆后倾开始,脊柱一节一节地卷离地面", + "落回时同样一节一节地铺回去", + "重点在脊柱分节控制,不是速度"], + ["整块板一样抬起落下", "速度过快"], + std=("1组×8次", "2组×15次", "3组×20次")), + + E("3645", "单腿臀桥", "bridge", 2, ["臀", "核心"], + ["臀桥姿势,抬起一条腿伸直", + "支撑侧脚跟发力顶髋,骨盆保持水平不许一高一低", + "顶端停 2 秒,感受支撑侧臀大肌", + "缓慢落回,不落地保持张力"], + ["骨盆倾斜(说明臀中肌不足,先做双腿版)", "腰部代偿"], + std=("1组×8次(每侧)", "2组×15次", "3组×20次")), + + E("3561", "臀桥行进", "bridge", 2, ["臀", "核心"], + ["保持臀桥顶端姿势,交替把膝盖提向胸口", + "换腿时骨盆绝不允许左右掉落", + "臀部全程保持收紧,不要在换腿间隙松掉"], + ["骨盆左右晃动", "髋部下沉"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("0668", "反向平板桥", "bridge", 2, ["臀", "核心", "肩"], + ["坐地,双手撑在肩后方,指尖朝脚", + "顶髋成一条直线,胸口打开,肩胛后缩下沉", + "颈部保持中立或轻微后仰,不要憋着", + "臀部与腘绳肌持续发力维持高度"], + ["髋部下沉", "耸肩夹脖子", "手腕不适(可改用拳撑或前臂撑)"], + scheme="hold", std=("20秒", "45秒", "90秒")), + + E("0870", "臀部上顶", "bridge", 2, ["臀", "核心"], + ["前臂撑地成平板,臀部向上顶成倒 V 后回落到平板位", + "回落时腰不许塌,靠核心控制", + "顶点肩胛主动前推"], + ["回落时腰部塌陷", "速度过快"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("0489", "俯卧背伸展", "bridge", 2, ["背", "臀"], + ["俯卧,双手轻放耳侧或体侧", + "用竖脊肌和臀把胸口抬离地面,幅度不必大", + "顶端停 2 秒,颈部跟随脊柱不要仰头", + "缓慢落回,全程臀部参与"], + ["幅度过大造成腰椎挤压", "猛甩上去", "只抬头不抬胸"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("1352", "俯卧腰部卷起", "bridge", 1, ["背"], + ["俯卧,缓慢抬起上身,脊柱逐节延展", + "手臂只做轻微辅助,主要靠背部肌肉", + "顶端停 2 秒"], + ["用手猛撑起来", "颈部过度后仰"], + std=("1组×8次", "2组×15次", "3组×25次")), + + E("1366", "上犬式", "bridge", 1, ["背", "胸"], + ["俯卧,双手撑在肋骨两侧,推起上身", + "肩胛下沉后缩,胸口向前打开,肩远离耳朵", + "大腿离地,脚背压地", + "颈部延长,视线略向上而非猛仰头"], + ["耸肩缩脖子", "腰部集中受力(应该整段胸椎均匀延展)"], + scheme="hold", std=("20秒", "45秒", "60秒")), + + E("1362", "狮身人面式", "bridge", 1, ["背"], + ["俯卧,前臂撑地,肘在肩正下方", + "胸口向前推,肩胛下沉,腰部放松不主动夹紧", + "这是最温和的脊柱后伸,适合久坐族每天做"], + ["耸肩", "屏住呼吸"], + scheme="hold", std=("30秒", "60秒", "2分钟")), + + E("3663", "反向平板抬腿", "bridge", 3, ["臀", "核心"], + ["反向平板姿势下交替抬起一条腿", + "抬腿时骨盆不掉、髋不下沉", + "顶端停 1 秒,缓慢换腿"], + ["髋部下坠", "肩部塌陷"], + std=("1组×8次", "2组×16次", "3组×24次")), + + E("1423", "反向挺身", "bridge", 3, ["臀", "背"], + ["上身俯卧在床沿/长凳,双腿悬空", + "用臀和腘绳肌把双腿抬到与躯干齐平", + "顶端停 1 秒,避免用腰椎超伸凑高度", + "缓慢落回"], + ["靠甩腿", "腰部过度反弓"], + std=("1组×10次", "2组×20次", "3组×25次")), + + E("0130", "凳上伸髋", "bridge", 2, ["臀", "腿"], + ["肩背靠在凳沿,双脚踩地成臀桥位(更大行程)", + "下放到臀部接近地面,再顶起到躯干水平", + "顶端下巴微收,肋骨下沉,避免腰椎代偿"], + ["顶端腰椎反弓", "膝外八", "行程不足"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("0459", "打水踢腿", "bridge", 2, ["臀", "核心"], + ["俯卧或仰卧,双腿伸直小幅快速交替上下", + "俯卧版练臀和下背,仰卧版练下腹", + "幅度小、频率稳,全程腿不落地"], + ["幅度过大靠甩", "腰部塌陷(仰卧版腰要贴地)"], + scheme="hold", std=("20秒", "45秒", "60秒")), + + E("1466", "扭转提髋", "bridge", 3, ["臀", "核心"], + ["侧向支撑基础上把髋部向上顶并伴随轻微转体", + "臀中肌与腹斜肌共同发力", + "顶端停 1 秒"], + ["靠手臂猛推", "转体幅度过大失去支撑"], + std=("1组×8次(每侧)", "2组×15次", "3组×20次")), +] + + +# ================================================================ 六、倒立撑链(垂直推) +HANDSTAND = [ + E("3021", "肩胛俯卧撑", "handstand", 1, ["肩", "核心"], + ["俯卧撑位,手肘全程锁直", + "只让肩胛在'塌下去'和'推出去'之间移动", + "推出时把上背推圆,感受前锯肌;塌下时肩胛夹紧", + "幅度不大,但每次都做到端点"], + ["屈肘变成小幅俯卧撑", "腰部代偿起伏"], + std=("1组×10次", "2组×20次", "3组×30次"), + note="六艺第六式起点。前锯肌和肩胛控制是倒立的真正地基。"), + + E("3011", "上斜肩胛俯卧撑", "handstand", 1, ["肩"], + ["手撑高台,其余同肩胛俯卧撑,负荷更低", + "适合完全没有肩胛控制概念的新手"], + ["屈肘", "耸肩"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("0658", "靠墙推肩", "handstand", 1, ["肩", "手臂"], + ["面墙站立,双手撑墙,屈肘让头顶靠近墙面", + "推起时手臂沿斜上方发力,模拟推举轨迹", + "脚离墙越远越难", + "肩胛在顶端主动上回旋"], + ["塌腰", "肘部大幅外展"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("3302", "倒立", "handstand", 4, ["肩", "核心", "手臂"], + ["面墙或背墙上墙,手距与肩同宽,手指张开抓地", + "肩胛主动上推('把地面推远'),耳朵夹在两臂之间", + "肋骨下沉、臀部收紧,让身体成一条直线而不是香蕉形", + "重心微调靠手指压地,不靠腰晃"], + ["香蕉倒立(腰部反弓)", "耸肩塌肩", "手腕未热身直接上(先做手腕环绕)"], + scheme="hold", std=("靠墙30秒", "靠墙60秒", "离墙30秒"), + note="先练靠墙倒立累计 60 秒,再谈倒立撑。手腕务必充分热身。"), + + E("0471", "倒立撑", "handstand", 5, ["肩", "手臂", "核心"], + ["靠墙倒立位,缓慢屈肘让头顶轻触地面", + "肘部朝前不外张,前臂垂直地面", + "推起时肩胛上回旋,把身体'顶'回直线", + "颈部只是轻触,绝不承重"], + ["用头顶硬撑(颈椎风险)", "腰部反弓卸力", "行程不足"], + std=("1组×3次", "2组×8次", "2组×12次"), + note="⚠️ 头部只做参照物。前置:靠墙倒立稳定 60 秒 + 肩关节无痛。"), + + E("3291", "斯塔尔德推起", "handstand", 5, ["肩", "核心", "手臂"], + ["从分腿坐姿直臂支撑,靠肩前压与核心把身体推到倒立", + "全程直臂,靠肩带与髋屈肌的极限控制", + "需要先具备分腿 L 支撑与稳定自由倒立"], + ["靠跳跃上倒立(那就不是压起)", "屈肘"], + std=("1次", "3次", "2组×5次")), +] + + +# ================================================================ 辅助:臂屈伸链 +DIP = [ + E("0129", "屈膝凳上臂屈伸", "dip", 1, ["手臂", "肩"], + ["背对凳子,手撑凳沿,指尖朝前,屈膝双脚踩地", + "臀部贴近凳子垂直下沉,不要往前跑", + "下放到大臂与地面平行即可,不要更深", + "推起靠三头,肩胛保持下沉不耸肩"], + ["身体前移导致肩前侧被顶(肩最易受伤的姿势)", "下放过深", "耸肩"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("0815", "地面反向撑起", "dip", 1, ["手臂"], + ["坐地,双手撑在臀后,指尖朝脚", + "顶起髋部,屈伸肘完成动作", + "全程肩胛下沉,手腕不适可改握拳"], + ["肩前顶", "髋部下坠"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("0812", "直腿凳上臂屈伸", "dip", 2, ["手臂", "肩"], + ["双腿伸直、脚跟着地,负荷比屈膝版明显更大", + "身体贴近凳子垂直升降", + "下放到大臂水平,停半拍再推"], + ["下放过深挤压肩关节", "身体前移"], + std=("1组×8次", "2组×15次", "3组×25次")), + + E("0813", "双凳臂屈伸", "dip", 2, ["手臂", "肩"], + ["双脚架在另一张凳上,行程与负荷进一步增大", + "严格控制下放深度,肩不适立刻停", + "核心收紧防止腰塌"], + ["深度过大", "耸肩"], + std=("1组×8次", "2组×15次", "3组×20次")), + + E("3012", "肩胛下沉(双杠)", "dip", 2, ["肩", "背"], + ["双臂在双杠上撑直,手肘锁死", + "只靠肩胛下沉把身体'顶'高几厘米,再控制下落", + "这是双杠臂屈伸的必修前置,能防肩伤"], + ["屈肘", "身体前后晃"], + std=("1组×8次", "2组×15次", "3组×20次"), + need="双杠"), + + E("0137", "反向支撑抬身", "dip", 2, ["手臂", "背"], + ["仰卧于低杠下方,手抓杠,用手臂把身体撑起", + "肩胛后缩下沉,胸口顶向杠", + "全程身体成直线"], + ["塌腰", "用腿蹬地"], + std=("1组×8次", "2组×15次", "3组×20次")), + + E("0251", "双杠臂屈伸", "dip", 3, ["胸", "手臂", "肩"], + ["双杠撑直,先做肩胛下沉锁住肩带(这一步决定安不安全)", + "身体略前倾练胸、保持直立练三头", + "下放到大臂与地面平行或略低,肩感到不适就停", + "推起时想象'把双杠向内夹',顶端不完全锁肘"], + ["下放过深(肩关节最大风险)", "肩胛没下沉,整个人'挂'在肩上", + "身体摇晃借力", "耸肩"], + std=("1组×5次", "2组×12次", "3组×20次"), + need="双杠", + note="肩前侧有任何刺痛都应立刻减小深度或改做凳上版本。"), + + E("0814", "三头臂屈伸(直立)", "dip", 3, ["手臂"], + ["双杠撑直,躯干保持垂直不前倾", + "肘部贴身向后,最大化三头参与", + "下放到大臂水平,停半拍推起"], + ["身体前倾变成练胸", "下放过深"], + std=("1组×5次", "2组×12次", "3组×20次"), + need="双杠"), + + E("2462", "直杠臂屈伸", "dip", 4, ["胸", "手臂", "核心"], + ["撑在单根直杠上,身体必须前倾越过杠面保持平衡", + "核心和臀绷紧,腿略前伸配平", + "下放时胸口贴近杠,推起时把杠'推离'身体", + "是双力臂的必备前置动作"], + ["身体没前倾导致向后掉", "手腕压力过大(先练直杠支撑)"], + std=("1组×3次", "2组×8次", "2组×12次"), + need="单杠"), + + E("0677", "吊环臂屈伸", "dip", 4, ["胸", "手臂", "肩"], + ["吊环不稳定,先能做吊环支撑(直臂稳住 30 秒)再做屈伸", + "全程主动外旋吊环,把环口转向前方", + "下放受控,肩胛保持下沉"], + ["吊环乱晃失控", "深度过大"], + std=("1组×3次", "2组×8次", "2组×12次"), + need="吊环"), + + E("3287", "肘杠臂屈伸", "dip", 4, ["手臂", "胸"], + ["下放到前臂贴杠,行程远大于常规臂屈伸", + "对肩与肘的柔韧要求高", + "推起时靠三头长头爆发"], + ["深度突然增大导致肩拉伤"], + std=("1组×3次", "2组×8次", "2组×12次"), + need="双杠"), + + E("3288", "韩式臂屈伸", "dip", 5, ["手臂", "胸", "核心"], + ["身体在杠后方,手在身体两侧靠后握杠,肩极度伸展", + "对肩前侧柔韧要求极高,必须充分热身", + "缓慢下放,绝不弹震"], + ["热身不足硬做(肩前侧高风险)", "弹震式下放"], + std=("1次", "3次", "2组×6次"), + need="双杠", + note="⚠️ 肩关节极限位动作,肩前侧有旧伤请跳过。"), + + E("0639", "单臂臂屈伸", "dip", 5, ["手臂", "核心"], + ["单手撑杠,另一只手辅助或完全离开", + "身体会强烈旋转,靠核心抗旋维持", + "先从离心(慢放 5 秒)开始练"], + ["身体大幅扭转", "肩带失稳"], + std=("1次(每侧)", "2组×3次", "2组×6次"), + need="双杠"), + + E("1771", "跪姿三头伸展", "dip", 2, ["手臂"], + ["跪姿,前臂撑地,靠三头把身体推起来", + "髋部固定不折叠,只有肘关节在动", + "这是徒手版的'仰卧臂屈伸'"], + ["屈髋作弊", "腰部塌陷"], + std=("1组×8次", "2组×15次", "3组×20次")), + + E("1753", "三点凳上臂屈伸", "dip", 3, ["手臂"], + ["凳上臂屈伸的单腿版本,负荷更集中", + "骨盆保持水平不侧倾"], + ["骨盆倾斜", "肩前顶"], + std=("1组×8次(每侧)", "2组×15次", "2组×20次")), +] + + +# ================================================================ 辅助:核心稳定 +CORE = [ + E("3239", "跪姿平板拍肩", "core", 1, ["核心", "肩"], + ["跪姿平板支撑,膝-髋-肩成一条直线", + "交替抬手拍对侧肩,骨盆不许左右晃", + "支撑侧手掌用力抓地对抗旋转"], + ["骨盆摇摆", "臀部翘起"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("3699", "平板拍肩", "core", 2, ["核心", "肩"], + ["标准平板支撑位,双脚分开增加稳定", + "交替拍肩,全程骨盆水平不动", + "呼吸均匀,不憋气"], + ["骨盆左右转", "塌腰"], + std=("1组×10次", "2组×20次", "3组×40次")), + + E("0464", "平板转体", "core", 2, ["核心"], + ["前臂平板支撑,髋部交替向两侧转动接近地面", + "转的是胸椎和髋,不是整个人翻过去", + "回到中立位停半拍"], + ["幅度过大失去支撑", "肩部塌陷"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("0705", "侧桥", "core", 2, ["核心", "臀"], + ["前臂撑地,肘在肩正下方,身体从头到脚一条直线", + "髋部主动向上顶,不许下沉", + "上方手可叉腰或指向天花板", + "颈部保持中立"], + ["髋部下坠", "身体前后倾斜不在一个平面", "肩塌进耳朵"], + scheme="hold", std=("20秒(每侧)", "45秒", "90秒")), + + E("3544", "上斜侧平板", "core", 1, ["核心"], + ["手撑高台做侧平板,负荷更低", + "台面越高越轻松,是侧桥的降阶版"], + ["髋下沉", "身体前倾"], + scheme="hold", std=("20秒(每侧)", "45秒", "60秒")), + + E("1775", "侧平板内收", "core", 3, ["核心", "臀", "腿"], + ["侧平板姿势,上方腿架高,用下方腿向上抬起靠拢", + "内收肌发力,髋保持顶起", + "顶端停 1 秒"], + ["髋部掉落", "靠甩腿"], + std=("1组×8次(每侧)", "2组×15次", "3组×20次")), + + E("1774", "侧桥外展", "core", 3, ["臀", "核心"], + ["侧桥姿势下把上方腿向上抬起", + "臀中肌主导,腿不许向前跑", + "顶端停 1 秒"], + ["腿向前摆(变成髋屈肌代偿)", "髋部下沉"], + std=("1组×8次(每侧)", "2组×15次", "3组×20次")), + + E("0664", "俯卧撑转侧平板", "core", 3, ["核心", "胸", "肩"], + ["做一个俯卧撑,起来后转体成侧平板并把手指向天花板", + "转体时支撑肩稳定,髋部顶起", + "两侧交替"], + ["转体时髋下沉", "支撑手腕过度扭转"], + std=("1组×8次", "2组×16次", "3组×24次")), + + E("3665", "尖点平板", "core", 3, ["核心"], + ["平板支撑基础上只用对角的一手一脚支撑", + "底面积变小,抗旋要求剧增", + "保持骨盆和肩线都水平"], + ["身体倾斜", "支撑侧塌陷"], + scheme="hold", std=("15秒", "30秒", "60秒")), + + E("0778", "蜘蛛爬俯卧撑", "core", 3, ["核心", "胸"], + ["下放时把一侧膝盖沿地面拉向同侧肘", + "髋部保持低位不翘起", + "推起时收回腿,两侧交替"], + ["髋部抬高", "膝盖没真正靠近肘"], + std=("1组×8次", "2组×16次", "3组×24次")), + + E("0500", "等长雨刷", "core", 3, ["核心"], + ["俯卧撑顶端位或平板位,双脚左右小幅摆动", + "肩带全程稳定,动作来自核心而非腰部甩动"], + ["腰部大幅扭转", "肩塌"], + std=("1组×10次", "2组×20次", "3组×30次")), + + E("0710", "侧卧外展", "core", 1, ["臀"], + ["侧卧,下方腿屈膝稳定,上方腿伸直向上抬", + "腿略向后而不是向前,才能真正打到臀中肌", + "顶端停 1 秒,缓慢落回"], + ["腿向前摆", "身体后仰"], + std=("1组×15次(每侧)", "2组×25次", "3组×30次")), + + E("1471", "毛毛虫爬行", "core", 1, ["核心", "全身"], + ["站立前屈,手沿地面向前走到平板位", + "全程腿尽量伸直,感受腘绳拉伸", + "走回起始位,节奏均匀"], + ["塌腰", "膝盖大幅弯曲失去拉伸意义"], + std=("1组×5次", "2组×10次", "3组×15次")), + + E("3300", "前倾直臂支撑", "core", 4, ["肩", "核心"], + ["俯卧撑顶端位,身体持续前倾让肩越过手掌", + "肩胛前伸下压,脚尖踮起", + "前倾越多越难,是直臂支撑的入门"], + ["塌腰", "肩胛后缩失去前锯肌张力"], + scheme="hold", std=("10秒", "20秒", "30秒")), + + E("3301", "蛙式直臂支撑", "core", 4, ["肩", "核心", "手臂"], + ["双手撑地,膝盖抵在肘部外侧,身体前倾把脚抬离地面", + "肩胛前伸下压,重心尽量前移", + "手指抓地微调平衡"], + ["肘部完全屈曲成'乌鸦式'(那是另一个动作)", "手腕未热身"], + scheme="hold", std=("10秒", "20秒", "40秒")), + + E("3298", "分腿直臂支撑", "core", 5, ["肩", "核心"], + ["直臂,双腿分开减小力臂,身体离地并保持水平", + "肩极度前压,臀腹绷紧", + "分腿越窄越难"], + ["屈肘", "髋部高于肩"], + scheme="hold", std=("5秒", "10秒", "20秒")), + + E("3299", "直臂支撑(全)", "core", 5, ["肩", "核心", "全身"], + ["双腿并拢伸直,全身与地面平行,仅靠双手支撑", + "肩前压到极限,前锯肌与三角肌前束承担主要负荷", + "臀腹极限收紧防止髋部下坠"], + ["屈肘", "髋部下坠或过高", "手腕准备不足"], + scheme="hold", std=("3秒", "8秒", "15秒"), + note="街健顶级静力动作,通常需要 2-3 年系统训练。"), + + E("3303", "人体旗帜", "core", 5, ["核心", "肩", "背"], + ["抓住竖杆,上手下拉、下手上推形成力偶", + "核心(尤其腹斜肌)把身体锁成一条直线", + "降阶:团身旗帜 → 分腿旗帜 → 全旗帜", + "两侧都要练,否则会明显不对称"], + ["只靠手臂硬撑(力偶没建立)", "髋部下坠", "只练强势侧"], + scheme="hold", std=("团身5秒", "分腿8秒", "全旗10秒"), + need="竖杆"), +] + + +# ================================================================ 辅助:小腿 +CALF = [ + E("1373", "站姿提踵", "calf", 1, ["小腿"], + ["站立,缓慢把脚跟抬到最高,顶端停 1 秒", + "重心均匀分布在拇趾球,不要向外侧滚", + "下放同样缓慢,脚跟落到最低感受跟腱拉伸"], + ["靠弹震反弹", "脚踝外翻", "行程不足"], + std=("1组×15次", "2组×30次", "3组×50次")), + + E("1490", "台阶提踵", "calf", 2, ["小腿"], + ["前脚掌踩台阶边缘,脚跟悬空", + "下放到脚跟低于台阶充分拉伸,再顶到最高", + "全程扶稳,速度放慢"], + ["下放弹震(跟腱风险)", "膝盖弯曲代偿"], + std=("1组×15次", "2组×25次", "3组×40次")), + + E("1387", "单腿提踵", "calf", 2, ["小腿"], + ["单腿站立完成提踵,另一腿勾在支撑腿后", + "手扶墙只为平衡,不承重", + "顶端停 1 秒"], + ["用手臂借力", "脚踝外翻"], + std=("1组×10次(每侧)", "2组×20次", "3组×30次")), + + E("0284", "驴式提踵", "calf", 2, ["小腿"], + ["屈髋前倾,上身趴在支撑物上,髋屈约 90°", + "这个角度下腓肠肌被预先拉长,刺激更强", + "缓慢提踵到顶端停 1 秒"], + ["膝盖弯曲", "弹震"], + std=("1组×15次", "2组×25次", "3组×40次")), + + E("1386", "单腿驴式提踵", "calf", 3, ["小腿"], + ["驴式提踵的单腿版,负荷加倍", + "全程控制,顶端充分收缩"], + ["弹震", "重心偏移"], + std=("1组×10次(每侧)", "2组×20次", "3组×25次")), + + E("1368", "踝关节环绕", "calf", 1, ["小腿"], + ["坐姿或单腿站立,脚踝画最大的圆", + "两个方向各转,动作缓慢到位", + "训练前必做,尤其在深蹲和跳跃前"], + ["幅度太小走过场"], + scheme="hold", std=("每向10圈", "每向15圈", "每向20圈")), +] + + +# ================================================================ 辅助:代谢体能 +CARDIO = [ + E("1160", "波比跳", "cardio", 2, ["全身"], + ["下蹲撑地 → 跳成平板 → 一个俯卧撑 → 收腿 → 起跳", + "平板阶段核心绷紧,腰不塌", + "落地用前脚掌缓冲,膝盖不内扣", + "累的时候可去掉俯卧撑或跳跃,但别让腰塌"], + ["平板阶段塌腰", "落地砸膝", "为了速度牺牲动作质量"], + scheme="time", std=("20秒×4轮", "30秒×6轮", "40秒×8轮")), + + E("0501", "开合波比", "cardio", 3, ["全身"], + ["波比跳基础上在平板位加一次开合跳(双脚分合)", + "开合时髋部保持稳定不上下起伏"], + ["腰部塌陷", "节奏散乱"], + scheme="time", std=("20秒×4轮", "30秒×6轮", "40秒×8轮")), + + E("0630", "登山跑", "cardio", 2, ["核心", "全身"], + ["俯卧撑顶端位,交替把膝盖快速拉向胸口", + "髋部保持低位不上下起伏(这是最关键的一点)", + "肩在手正上方,肩胛前推稳定"], + ["屁股翘得很高", "肩部前移超过手腕", "只动腿不收髋"], + scheme="time", std=("20秒×4轮", "40秒×5轮", "60秒×6轮")), + + E("3360", "熊爬", "cardio", 2, ["全身", "核心"], + ["四点支撑,膝盖离地约 5cm,对侧手脚同时移动", + "背部保持水平——放一杯水在背上不能洒", + "小步幅,节奏稳定"], + ["臀部上下摇摆", "膝盖离地太高", "同侧手脚一起动"], + scheme="time", std=("20秒×3轮", "40秒×4轮", "60秒×5轮")), + + E("3361", "滑冰跳", "cardio", 2, ["腿", "臀", "全身"], + ["向侧方蹬地跳出,单腿落地并缓冲稳住", + "落地时膝盖对准脚尖,臀中肌控制不内扣", + "稳住后再跳向另一侧"], + ["落地膝内扣", "没稳住就急着跳回"], + scheme="time", std=("20秒×3轮", "40秒×4轮", "60秒×5轮")), + + E("3223", "星跳", "cardio", 2, ["全身"], + ["半蹲后爆发跳起,空中四肢完全展开成星形", + "落地屈膝缓冲,立刻进入下一次", + "追求展开充分而不是频率"], + ["落地直腿", "展开不充分"], + scheme="time", std=("20秒×3轮", "30秒×5轮", "45秒×6轮")), + + E("3219", "剪刀跳", "cardio", 2, ["腿", "全身"], + ["弓步位起跳,空中前后换腿", + "落地缓冲深一点,躯干保持直立", + "膝盖不内扣"], + ["落地过硬", "上身前倾"], + scheme="time", std=("20秒×3轮", "30秒×5轮", "45秒×6轮")), + + E("3636", "靠墙高抬腿", "cardio", 2, ["全身", "腿"], + ["双手撑墙,身体前倾成一条斜线", + "交替快速高抬腿,大腿抬到水平以上", + "核心绷紧,腰不塌,落地用前脚掌"], + ["塌腰", "抬腿高度不够", "脚跟落地"], + scheme="time", std=("20秒×4轮", "30秒×5轮", "45秒×6轮")), + + E("3318", "360度转体跳", "cardio", 2, ["全身", "核心"], + ["原地起跳并在空中转体 360°", + "落地缓冲稳住再做下一次", + "两个方向都要练"], + ["落地失衡", "只练单一方向"], + scheme="time", std=("6次", "10次", "2组×10次")), + + E("0858", "冲刺间歇", "cardio", 3, ["全身", "腿"], + ["全力冲刺一段短距离后走回休息,如此反复", + "冲刺前务必充分热身,尤其腘绳肌", + "质量下降明显时就结束,不要硬拖"], + ["热身不足直接全速(腘绳拉伤高发)", "休息不足导致后段全是垃圾容量"], + scheme="time", std=("15秒×4组", "20秒×6组", "30秒×8组")), +] + + +# ================================================================ 恢复:拉伸与灵活 +MOBILITY = [ + E("1428", "手腕环绕", "mobility", 1, ["手臂"], + ["双手交握或单独转动,手腕画最大的圆", + "两个方向各转,再做手掌向前/向后压地的拉伸", + "所有俯卧撑、倒立、支撑类训练前必做"], + ["草草几下就开始训练"], + scheme="hold", std=("每向10圈", "每向15圈", "每向20圈"), + note="手腕是徒手训练最常见也最容易被忽视的伤病点。"), + + E("1604", "世界最佳拉伸", "mobility", 1, ["全身", "腿", "背"], + ["弓步位,同侧手肘下沉靠近地面", + "然后同侧手向上打开转体,视线跟随手", + "髋、胸椎、腘绳一次性打开", + "每侧停留 3-5 次呼吸"], + ["后腿膝盖塌地失去髋屈拉伸", "转体只转手臂不转胸椎"], + scheme="hold", std=("每侧30秒", "每侧45秒", "每侧60秒")), + + E("1511", "腘绳肌拉伸", "mobility", 1, ["腿"], + ["坐姿或站姿前屈,从髋部折叠而非弓腰", + "膝盖可微屈,重点是骨盆前倾", + "呼气时加深,不弹震"], + ["弓背去够脚尖(拉的是腰不是腿)", "弹震式拉伸"], + scheme="hold", std=("30秒", "45秒", "60秒")), + + E("1585", "跑者拉伸(髋屈)", "mobility", 1, ["腿", "臀"], + ["弓步位,后腿膝盖着地,骨盆后倾并向前推髋", + "同侧手上举可加深髂腰肌拉伸", + "久坐族必做"], + ["腰部反弓代替推髋(感觉不到髋前侧就是错的)"], + scheme="hold", std=("每侧30秒", "每侧45秒", "每侧60秒")), + + E("1512", "四足股四头拉伸", "mobility", 1, ["腿"], + ["俯卧或侧卧,手抓同侧脚背向臀部拉", + "骨盆保持后倾,避免腰椎代偿", + "膝盖并拢不外张"], + ["腰部反弓", "膝盖向外打开"], + scheme="hold", std=("每侧30秒", "每侧45秒", "每侧60秒")), + + E("1494", "蝴蝶式", "mobility", 1, ["腿"], + ["坐姿,脚掌相对,膝盖向两侧下沉", + "脊柱挺直从髋部前倾,不要弓背", + "可用手肘轻压大腿内侧"], + ["弓背前倾", "用力猛压造成内收肌拉伤"], + scheme="hold", std=("45秒", "60秒", "90秒")), + + E("1419", "铁十字拉伸", "mobility", 1, ["背", "臀"], + ["仰卧,双臂展开成十字,一腿跨过身体落到对侧", + "双肩尽量贴地,转的是腰椎和髋", + "呼气时放松下沉"], + ["肩膀离地(那就没拉到)", "用力压腿"], + scheme="hold", std=("每侧30秒", "每侧45秒", "每侧60秒")), + + E("1424", "坐姿臀部拉伸", "mobility", 1, ["臀"], + ["坐姿,一腿脚踝架在对侧膝上成 4 字", + "挺直背部从髋部前倾,感受臀部外侧", + "久坐导致的臀部僵硬用它最有效"], + ["弓背", "压膝过猛"], + scheme="hold", std=("每侧30秒", "每侧45秒", "每侧60秒")), + + E("2571", "摇摆蛙式", "mobility", 1, ["腿", "臀"], + ["四足跪姿,双膝向两侧打开,小腿与大腿成 90°", + "髋部缓慢前后摇摆,逐步加深", + "内收肌和髋关节内侧的经典拉伸"], + ["幅度过大造成内收肌拉伤", "腰部塌陷"], + scheme="hold", std=("45秒", "60秒", "90秒")), + + E("1271", "胸与肩前侧拉伸", "mobility", 1, ["胸", "肩"], + ["手扶门框或墙,前臂贴住,身体缓慢转开", + "手的高度决定拉伸的胸肌区域,高中低各做一次", + "肩胛保持下沉"], + ["耸肩", "转体过猛"], + scheme="hold", std=("每侧30秒", "每侧45秒", "每侧60秒")), + + E("1346", "跪姿背阔拉伸", "mobility", 1, ["背"], + ["跪姿,双手撑在高台或地面,臀部后坐", + "腋下向下沉,感受背阔被拉长", + "可略微侧倾加深单侧拉伸"], + ["耸肩", "腰部代偿"], + scheme="hold", std=("30秒", "45秒", "60秒")), + + E("0643", "过顶三头拉伸", "mobility", 1, ["手臂", "肩"], + ["一手举过头顶屈肘,另一手轻推肘部向后", + "肋骨下沉,不要靠腰部反弓凑幅度"], + ["腰部反弓", "用力过猛"], + scheme="hold", std=("每侧30秒", "每侧45秒", "每侧60秒")), + + E("1363", "脊柱扭转", "mobility", 1, ["背", "核心"], + ["坐姿或仰卧,缓慢向两侧扭转脊柱", + "从胸椎开始转,呼气时加深", + "颈部跟随转动方向"], + ["只转腰椎(腰椎旋转幅度本就很小,易伤)", "用力甩"], + scheme="hold", std=("每侧30秒", "每侧45秒", "每侧60秒")), + + E("1365", "上背拉伸", "mobility", 1, ["背", "肩"], + ["双手交握前推,上背拱起像抱住一个大球", + "肩胛充分打开,低头配合", + "菱形肌与中斜方的放松"], + ["只推手臂不拱背"], + scheme="hold", std=("30秒", "45秒", "60秒")), + + E("0669", "后三角拉伸", "mobility", 1, ["肩"], + ["一臂横过胸前,另一手扶其肘部靠向身体", + "肩胛保持下沉,不要耸肩", + "感受肩后侧被拉长"], + ["耸肩", "扶住手腕而非肘部(拉的位置会跑偏)"], + scheme="hold", std=("每侧30秒", "每侧45秒", "每侧60秒")), + + E("1377", "靠墙小腿拉伸", "mobility", 1, ["小腿"], + ["双手扶墙,后腿伸直脚跟压地", + "直腿版拉腓肠肌,屈膝版拉比目鱼肌,两个都做", + "脚尖朝正前方,不要外八"], + ["脚跟离地", "脚尖外八导致足弓塌陷"], + scheme="hold", std=("每侧30秒", "每侧45秒", "每侧60秒")), + + E("1403", "颈侧拉伸", "mobility", 1, ["肩"], + ["头缓慢侧倒,同侧手轻放头顶只给一点点重量", + "对侧肩主动下沉", + "绝不猛拉猛甩"], + ["用手猛拽头部", "耸肩"], + scheme="hold", std=("每侧20秒", "每侧30秒", "每侧45秒")), + + E("1587", "坐姿宽角序列", "mobility", 1, ["腿", "背"], + ["坐姿双腿大幅分开,脊柱挺直从髋部前倾", + "先向正前,再向左右腿方向各倾一次", + "膝盖朝上不内旋"], + ["弓背", "膝盖内扣"], + scheme="hold", std=("每向30秒", "每向45秒", "每向60秒")), +] + + +# ================================================================ 汇总 +CATALOG = PUSH + PULL + SQUAT + LEGRAISE + BRIDGE + HANDSTAND + DIP + CORE + CALF + CARDIO + MOBILITY + + diff --git a/app/public/app.js b/app/public/app.js new file mode 100644 index 00000000..efb943af --- /dev/null +++ b/app/public/app.js @@ -0,0 +1,344 @@ +/* 囚徒健身 · 徒手训练手册 — 前端逻辑(无依赖) */ +'use strict'; + +const App = (() => { + let DATA = null; + let byId = {}; + const state = { + parts: new Set(), + diff: new Set(), + fams: new Set(), + q: '', + goal: 0, // 推荐组数:0 打基础 / 1 进阶 / 2 精通 + plan: { level: 'beginner', focus: 'full', gear: 'bar' }, + }; + + const $ = s => document.querySelector(s); + const $$ = s => Array.from(document.querySelectorAll(s)); + const esc = s => String(s).replace(/[&<>"']/g, c => + ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); + + const famOf = k => DATA.families.find(f => f.key === k); + const diffOf = d => DATA.difficulty.find(x => x.level === d); + + /* ─────────────────────────── 路由 ─────────────────────────── */ + const TABS = ['system', 'library', 'plan']; + + function route() { + const h = (location.hash || '#/system').slice(2).split('/'); + const [head, arg] = [h[0] || 'system', h[1]]; + + $$('.view').forEach(v => v.classList.add('hidden')); + const isSub = head === 'chain' || head === 'ex'; + $('#backBtn').classList.toggle('hidden', !isSub); + + if (head === 'chain') { renderChain(arg); $('#view-chain').classList.remove('hidden'); } + else if (head === 'ex') { renderDetail(arg); $('#view-detail').classList.remove('hidden'); } + else { + const tab = TABS.includes(head) ? head : 'system'; + $('#view-' + tab).classList.remove('hidden'); + setTopbar( + { system: '囚徒健身', library: '动作库', plan: '今日训练' }[tab], + { system: '徒手 · 挂墙单杠训练手册', library: `${DATA.exercises.length} 个精选徒手动作`, plan: '按水平与目标排课' }[tab] + ); + } + $$('.tab').forEach(b => b.classList.toggle('active', + b.dataset.tab === (isSub ? '' : (TABS.includes(head) ? head : 'system')))); + window.scrollTo(0, 0); + } + + const go = hash => { location.hash = hash; }; + const setTopbar = (t, s) => { $('#tbTitle').textContent = t; $('#tbSub').textContent = s; }; + + /* ─────────────────────────── 六艺体系 ─────────────────────────── */ + function renderSystem() { + const card = f => { + const n = DATA.exercises.filter(e => e.fam === f.key).length; + return ``; + }; + $('#famGrid').innerHTML = DATA.families.filter(f => f.big6).map(card).join(''); + $('#famGridAux').innerHTML = DATA.families.filter(f => !f.big6).map(card).join(''); + } + + /* ─────────────────────────── 进阶链 ─────────────────────────── */ + function renderChain(key) { + const f = famOf(key); + if (!f) return go('#/system'); + setTopbar(f.zh, f.sub); + const list = DATA.exercises.filter(e => e.fam === key); + + $('#chainHead').innerHTML = `
+
${f.icon}
+

${esc(f.zh)}进阶链

+

共 ${list.length} 级 · 从入门到大师。每级都练到「精通」标准再进入下一级。

+
`; + + $('#chainList').innerHTML = list.map(e => ` + `).join(''); + } + + /* ─────────────────────────── 动作库筛选 ─────────────────────────── */ + function renderFilters() { + const mk = (row, items, cls) => $(row).innerHTML = items.join(''); + + mk('#filterParts', DATA.parts.map(p => + ``)); + + mk('#filterDiff', DATA.difficulty.map(d => + ``)); + + mk('#filterFam', DATA.families.map(f => + ``)); + } + + function filtered() { + const q = state.q.trim().toLowerCase(); + return DATA.exercises.filter(e => { + if (state.parts.size && !e.parts.some(p => state.parts.has(p))) return false; + if (state.diff.size && !state.diff.has(e.d)) return false; + if (state.fams.size && !state.fams.has(e.fam)) return false; + if (q && !(e.zh.toLowerCase().includes(q) || e.en.toLowerCase().includes(q) + || e.parts.some(p => p.includes(q)))) return false; + return true; + }); + } + + function renderLibrary() { + renderFilters(); + const list = filtered(); + const active = state.parts.size + state.diff.size + state.fams.size + (state.q ? 1 : 0); + $('#resultCount').textContent = `${list.length} 个动作`; + $('#clearFilters').classList.toggle('hidden', !active); + $('#emptyState').classList.toggle('hidden', list.length > 0); + $('#exList').innerHTML = list.map(itemHTML).join(''); + } + + const itemHTML = e => ` + `; + + /* ─────────────────────────── 动作详情 ─────────────────────────── */ + function renderDetail(id) { + const e = byId[id]; + if (!e) return go('#/library'); + setTopbar(e.zh, `${famOf(e.fam).zh} · 第 ${e.step}/${e.chain_len} 级`); + + const prev = DATA.exercises.find(x => x.fam === e.fam && x.step === e.step - 1); + const next = DATA.exercises.find(x => x.fam === e.fam && x.step === e.step + 1); + + $('#detailBody').innerHTML = ` +
${esc(e.zh)}
+ +
+

${esc(e.zh)}

+
${esc(e.en)}
+
+ ${esc(diffOf(e.d).zh)} + ${esc(e.need)} + ${e.parts.map(p => `${esc(p)}`).join('')} +
+
${esc(diffOf(e.d).desc)}
+
+ +
+
🎯 发力要点 教练
+
    ${e.cues.map(c => `
  1. ${esc(c)}
  2. `).join('')}
+
+ +
+
📊 推荐组数 教练
+
+ ${e.std.map((s, i) => ``).join('')} +
+
${esc(e.std[state.goal].text)}
+
+
节奏
${esc(e.tempo)}
+
休息
${esc(e.rest)}
+
频率
${esc(e.freq)}
+
主练
${esc(e.target)}${e.secondary.length ? '(协同:' + esc(e.secondary.join('、')) + ')' : ''}
+
+
+ + ${e.errors.length ? `
+
⚠️ 常见错误
+ +
` : ''} + + ${e.steps.length ? `
+
📝 动作步骤
+
    ${e.steps.map(s => `
  1. ${esc(s)}
  2. `).join('')}
+
` : ''} + + ${e.note ? `
💡 教练提示
+
${esc(e.note)}
` : ''} + +
+
${famOf(e.fam).icon} ${esc(famOf(e.fam).zh)}进阶链
+
+ 当前第 ${e.step} / ${e.chain_len} 级 + +
+ ${prev ? navRow('← 降阶', prev) : ''} + ${next ? navRow('进阶 →', next) : ''} +
`; + } + + const navRow = (label, e) => ` + `; + + /* ─────────────────────────── 今日训练 ─────────────────────────── */ + const LEVELS = [ + { key: 'beginner', zh: '入门', range: [1, 2], goal: 0 }, + { key: 'inter', zh: '进阶', range: [2, 3], goal: 1 }, + { key: 'advanced', zh: '高手', range: [3, 5], goal: 2 }, + ]; + const FOCUS = [ + { key: 'full', zh: '全身', fams: ['push', 'pull', 'squat', 'legraise', 'bridge'] }, + { key: 'upper', zh: '上肢', fams: ['push', 'pull', 'dip', 'handstand'] }, + { key: 'lower', zh: '下肢', fams: ['squat', 'bridge', 'calf'] }, + { key: 'core', zh: '核心', fams: ['legraise', 'core', 'bridge'] }, + { key: 'pull', zh: '单杠专项', fams: ['pull', 'legraise'] }, + { key: 'burn', zh: '燃脂', fams: ['cardio', 'squat', 'core'] }, + ]; + const GEAR = [ + { key: 'none', zh: '纯徒手', allow: ['徒手'] }, + { key: 'bar', zh: '单杠', allow: ['徒手', '单杠', '单杠 + 凳'] }, + { key: 'full', zh: '单杠+双杠', allow: ['徒手', '单杠', '单杠 + 凳', '双杠', '罗马椅/双杠'] }, + ]; + + function renderPlanForm() { + const seg = (sel, items, cur, field) => $(sel).innerHTML = items.map(i => + ``).join(''); + seg('#planLevel', LEVELS, state.plan.level, 'level'); + seg('#planFocus', FOCUS, state.plan.focus, 'focus'); + seg('#planGear', GEAR, state.plan.gear, 'gear'); + } + + function generatePlan() { + const lv = LEVELS.find(l => l.key === state.plan.level); + const fc = FOCUS.find(f => f.key === state.plan.focus); + const gr = GEAR.find(g => g.key === state.plan.gear); + const ok = e => gr.allow.includes(e.need); + + // 每个动作族里挑一个最贴近目标难度的动作 + const pick = fam => { + const pool = DATA.exercises.filter(e => e.fam === fam && ok(e) + && e.d >= lv.range[0] && e.d <= lv.range[1]); + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + }; + + const main = fc.fams.map(pick).filter(Boolean); + const warm = shuffle(DATA.exercises.filter(e => e.fam === 'mobility')).slice(0, 3); + const finish = shuffle(DATA.exercises.filter(e => + (e.fam === 'cardio' || e.fam === 'core') && ok(e) && e.d <= lv.range[1])).slice(0, 1); + + const block = (title, sub, items, showRx) => !items.length ? '' : ` +
+

${title} ${sub}

+ ${items.map(e => ` + `).join('')} +
`; + + $('#planResult').innerHTML = + block('热身 · WARM-UP', '5 分钟,唤醒关节', warm, false) + + block('主项 · MAIN', `${lv.zh}水平 · ${fc.zh}`, main, true) + + block('收尾 · FINISHER', '力竭收官', finish, true) + + `
+ · 主项按顺序完成,组间休息见各动作详情页。
+ · 任何一个动作做不到标准形态,就退回它的降阶动作,别硬撑。
+ · 同一动作能达到「精通」标准后,再进阶到链条的下一级。
+ · 再次点击「生成训练计划」可换一组动作。 +
`; + $('#planResult').scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + + const shuffle = a => a.map(v => [Math.random(), v]).sort((x, y) => x[0] - y[0]).map(v => v[1]); + + /* ─────────────────────────── 事件绑定 ─────────────────────────── */ + function bind() { + $$('.tab').forEach(b => b.onclick = () => go('#/' + b.dataset.tab)); + $('#backBtn').onclick = () => history.back(); + $('#search').oninput = ev => { state.q = ev.target.value; renderLibrary(); }; + $('#clearFilters').onclick = () => clearFilters(); + $('#genPlan').onclick = () => generatePlan(); + window.addEventListener('hashchange', route); + } + + function clearFilters() { + state.parts.clear(); state.diff.clear(); state.fams.clear(); + state.q = ''; $('#search').value = ''; + renderLibrary(); + } + + /* ─────────────────────────── 启动 ─────────────────────────── */ + async function init() { + DATA = await (await fetch('data/app-data.json')).json(); + DATA.exercises.forEach(e => byId[e.id] = e); + renderSystem(); + renderLibrary(); + renderPlanForm(); + bind(); + route(); + } + + return { + init, go, clearFilters, + toggle(field, val) { + const s = state[field]; + s.has(val) ? s.delete(val) : s.add(val); + renderLibrary(); + }, + setGoal(i, id) { state.goal = i; renderDetail(id); }, + setPlan(field, val) { state.plan[field] = val; renderPlanForm(); }, + }; +})(); + +App.init(); diff --git a/app/public/index.html b/app/public/index.html new file mode 100644 index 00000000..5a0c2e2d --- /dev/null +++ b/app/public/index.html @@ -0,0 +1,119 @@ + + + + + + + + +囚徒健身 · 徒手训练手册 + + + + +
+ +
+
+ +
+

囚徒健身

+

徒手 · 挂墙单杠训练手册

+
+
+
+ + +
+
+
Convict Conditioning
+

六艺进阶体系

+

六条主链,从零基础到大师级。每条链按难度排成阶梯,逐级攻克,不跳步。

+
+
+ + +
+ +
+
⚠️
+
+ 挂墙单杠安全提示 +

使用前确认膨胀螺栓打入实心承重墙、静态承重 ≥150kg,并每月检查一次紧固。空心砖 / 石膏板墙禁止安装。

+
+
+
+ + + + + + + + + + + + + + + +
+ + + + diff --git a/app/public/style.css b/app/public/style.css new file mode 100644 index 00000000..6d7a5efe --- /dev/null +++ b/app/public/style.css @@ -0,0 +1,303 @@ +/* ═══════════════════════════════ 基础 ═══════════════════════════════ */ +:root{ + --bg:#0d0f12; --surface:#16191f; --raised:#1e222b; --line:#2a2f3a; + --txt:#eef1f6; --txt2:#9aa3b2; --txt3:#68707e; + --accent:#ff8a3d; --accent2:#ffb072; + --d1:#4ade80; --d2:#38bdf8; --d3:#fbbf24; --d4:#fb7185; --d5:#a78bfa; + --tabh:60px; + --safeb:env(safe-area-inset-bottom,0px); + --safet:env(safe-area-inset-top,0px); +} +*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent} +html{-webkit-text-size-adjust:100%} +body{ + background:var(--bg); color:var(--txt); + font:15px/1.6 -apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK SC",sans-serif; + overscroll-behavior-y:none; +} +button{font:inherit;color:inherit;background:none;border:none;cursor:pointer} +input{font:inherit} +.hidden{display:none !important} + +/* ═══════════════════════════════ 顶栏 ═══════════════════════════════ */ +#topbar{ + position:sticky;top:0;z-index:50; + padding:calc(var(--safet) + 12px) 16px 12px; + background:rgba(13,15,18,.85); backdrop-filter:blur(14px); + border-bottom:1px solid var(--line); +} +.tb-main{display:flex;align-items:center;gap:6px} +.tb-title h1{font-size:19px;font-weight:700;letter-spacing:.5px} +.tb-title p{font-size:12px;color:var(--txt3);margin-top:1px} +.icon-btn{ + font-size:30px;line-height:1;width:34px;height:34px; + display:flex;align-items:center;justify-content:center; + color:var(--accent);margin-left:-8px;flex:none; +} + +/* ═══════════════════════════════ 布局 ═══════════════════════════════ */ +.view{padding:0 16px calc(var(--tabh) + var(--safeb) + 24px)} +.section-label{ + font-size:12px;color:var(--txt3);font-weight:600;letter-spacing:1px; + margin:26px 2px 12px;text-transform:uppercase; +} + +/* ═══════════════════════════════ Hero ═══════════════════════════════ */ +.hero{padding:22px 0 18px} +.hero.compact{padding:18px 0 14px} +.hero-badge{ + display:inline-block;font-size:10px;letter-spacing:1.6px;font-weight:700; + color:var(--accent);border:1px solid rgba(255,138,61,.35); + padding:3px 9px;border-radius:20px;margin-bottom:11px; +} +.hero h2{font-size:25px;font-weight:750;letter-spacing:.5px} +.hero p{color:var(--txt2);font-size:13.5px;margin-top:7px;max-width:34em} + +/* ═══════════════════════════════ 动作族卡片 ═══════════════════════════════ */ +.fam-grid{display:grid;grid-template-columns:1fr 1fr;gap:11px} +.fam-card{ + background:var(--surface);border:1px solid var(--line);border-radius:16px; + padding:15px 13px;text-align:left;position:relative;overflow:hidden; + transition:transform .12s,border-color .12s; +} +.fam-card:active{transform:scale(.97);border-color:var(--accent)} +.fam-card .fc-icon{font-size:26px;display:block;margin-bottom:8px} +.fam-card .fc-name{font-size:16px;font-weight:700} +.fam-card .fc-sub{font-size:11.5px;color:var(--txt3);margin-top:2px} +.fam-card .fc-count{font-size:11px;color:var(--accent);margin-top:9px;font-weight:600} +.fam-card.big6::after{ + content:"";position:absolute;top:0;right:0;width:52px;height:52px; + background:radial-gradient(circle at top right,rgba(255,138,61,.16),transparent 70%); +} +.fam-grid.aux .fam-card .fc-icon{font-size:22px} + +/* ═══════════════════════════════ 提示卡 ═══════════════════════════════ */ +.tip-card{ + display:flex;gap:11px;margin-top:24px;padding:14px; + background:rgba(251,113,133,.07);border:1px solid rgba(251,113,133,.24);border-radius:14px; +} +.tip-icon{font-size:18px;flex:none;line-height:1.4} +.tip-card strong{font-size:13.5px} +.tip-card p{font-size:12.5px;color:var(--txt2);margin-top:4px;line-height:1.65} + +/* ═══════════════════════════════ 搜索与筛选 ═══════════════════════════════ */ +.search-wrap{padding:14px 0 4px} +#search{ + width:100%;background:var(--surface);border:1px solid var(--line); + border-radius:12px;padding:11px 14px;color:var(--txt);font-size:15px;outline:none; +} +#search:focus{border-color:var(--accent)} +#search::placeholder{color:var(--txt3)} + +.filter-block{padding:8px 0 2px} +.filter-row{ + display:flex;gap:7px;overflow-x:auto;padding:5px 0; + scrollbar-width:none;-ms-overflow-style:none; +} +.filter-row::-webkit-scrollbar{display:none} +.chip{ + flex:none;padding:6px 13px;border-radius:20px;font-size:13px;white-space:nowrap; + background:var(--surface);border:1px solid var(--line);color:var(--txt2); + transition:background .12s,color .12s,border-color .12s; +} +.chip.on{background:var(--accent);border-color:var(--accent);color:#17130f;font-weight:600} +.chip.d1.on{background:var(--d1);border-color:var(--d1);color:#06240f} +.chip.d2.on{background:var(--d2);border-color:var(--d2);color:#03202e} +.chip.d3.on{background:var(--d3);border-color:var(--d3);color:#2a1c00} +.chip.d4.on{background:var(--d4);border-color:var(--d4);color:#2e0710} +.chip.d5.on{background:var(--d5);border-color:var(--d5);color:#1c0e33} + +.result-bar{ + display:flex;justify-content:space-between;align-items:center; + padding:12px 2px 8px;font-size:12.5px;color:var(--txt3); +} +.link-btn{color:var(--accent);font-size:12.5px;font-weight:600} + +/* ═══════════════════════════════ 动作列表 ═══════════════════════════════ */ +.ex-list{display:flex;flex-direction:column;gap:9px} +.ex-item{ + display:flex;gap:12px;align-items:center;padding:9px; + background:var(--surface);border:1px solid var(--line);border-radius:14px; + text-align:left;width:100%;transition:transform .1s,border-color .1s; +} +.ex-item:active{transform:scale(.985);border-color:var(--accent)} +.ex-thumb{ + width:62px;height:62px;border-radius:11px;flex:none;object-fit:cover; + background:#fff; +} +.ex-info{flex:1;min-width:0} +.ex-name{font-size:15px;font-weight:650;line-height:1.35} +.ex-en{font-size:11px;color:var(--txt3);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.ex-meta{display:flex;gap:6px;align-items:center;margin-top:6px;flex-wrap:wrap} +.tag{ + font-size:10.5px;padding:2px 7px;border-radius:6px; + background:var(--raised);color:var(--txt2);white-space:nowrap; +} +.tag.gear{background:rgba(255,138,61,.14);color:var(--accent2)} +.dot{ + font-size:10.5px;padding:2px 8px;border-radius:6px;font-weight:700;flex:none; +} +.dot.d1{background:rgba(74,222,128,.16);color:var(--d1)} +.dot.d2{background:rgba(56,189,248,.16);color:var(--d2)} +.dot.d3{background:rgba(251,191,36,.16);color:var(--d3)} +.dot.d4{background:rgba(251,113,133,.16);color:var(--d4)} +.dot.d5{background:rgba(167,139,250,.16);color:var(--d5)} + +.empty{text-align:center;padding:60px 20px;color:var(--txt3)} +.empty-icon{font-size:38px;margin-bottom:10px} +.btn-ghost{ + margin-top:14px;padding:9px 20px;border:1px solid var(--line); + border-radius:10px;color:var(--accent);font-size:13.5px; +} + +/* ═══════════════════════════════ 进阶链 ═══════════════════════════════ */ +.chain-head{padding:20px 0 6px} +.chain-head .ch-icon{font-size:34px} +.chain-head h2{font-size:24px;font-weight:750;margin-top:8px} +.chain-head p{color:var(--txt2);font-size:13.5px;margin-top:6px} +.chain-list{display:flex;flex-direction:column;gap:0;margin-top:14px} +.chain-item{ + display:flex;gap:12px;align-items:stretch;position:relative; + padding:0 0 14px 0;text-align:left;width:100%; +} +.chain-rail{display:flex;flex-direction:column;align-items:center;flex:none;width:30px} +.chain-num{ + width:26px;height:26px;border-radius:50%;display:flex;align-items:center; + justify-content:center;font-size:11.5px;font-weight:700;flex:none; + background:var(--raised);border:1.5px solid var(--line);color:var(--txt2); +} +.chain-item.d1 .chain-num{border-color:var(--d1);color:var(--d1)} +.chain-item.d2 .chain-num{border-color:var(--d2);color:var(--d2)} +.chain-item.d3 .chain-num{border-color:var(--d3);color:var(--d3)} +.chain-item.d4 .chain-num{border-color:var(--d4);color:var(--d4)} +.chain-item.d5 .chain-num{border-color:var(--d5);color:var(--d5)} +.chain-line{flex:1;width:2px;background:var(--line);margin:3px 0 -14px} +.chain-item:last-child .chain-line{display:none} +.chain-card{ + flex:1;display:flex;gap:11px;align-items:center;min-width:0; + background:var(--surface);border:1px solid var(--line); + border-radius:13px;padding:9px;transition:transform .1s,border-color .1s; +} +.chain-item:active .chain-card{transform:scale(.985);border-color:var(--accent)} +.chain-card img{width:54px;height:54px;border-radius:10px;background:#fff;flex:none;object-fit:cover} + +/* ═══════════════════════════════ 详情 ═══════════════════════════════ */ +.dt-media{ + margin:14px 0 0;border-radius:18px;overflow:hidden;background:#fff; + aspect-ratio:1/1;max-height:64vh; +} +.dt-media img{width:100%;height:100%;object-fit:contain;display:block} +.dt-title{margin-top:16px} +.dt-title h2{font-size:23px;font-weight:750;line-height:1.3} +.dt-title .dt-en{color:var(--txt3);font-size:12.5px;margin-top:4px} +.dt-tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:11px} +.dt-desc{font-size:12.5px;color:var(--txt3);margin-top:9px} + +.card{ + background:var(--surface);border:1px solid var(--line); + border-radius:16px;padding:15px;margin-top:14px; +} +.card-h{ + display:flex;align-items:center;gap:7px;font-size:14px; + font-weight:700;margin-bottom:11px; +} +.card-h .ch-badge{ + font-size:10px;font-weight:700;color:var(--accent); + border:1px solid rgba(255,138,61,.35);border-radius:5px;padding:1px 6px; +} +.card.coach{ + background:linear-gradient(160deg,rgba(255,138,61,.09),rgba(255,138,61,.02)); + border-color:rgba(255,138,61,.3); +} +.card.warn{background:rgba(251,113,133,.06);border-color:rgba(251,113,133,.22)} + +ol.cues{list-style:none;counter-reset:c;display:flex;flex-direction:column;gap:11px} +ol.cues li{counter-increment:c;display:flex;gap:10px;font-size:14px;line-height:1.65} +ol.cues li::before{ + content:counter(c);flex:none;width:20px;height:20px;border-radius:50%; + background:var(--accent);color:#17130f;font-size:11px;font-weight:700; + display:flex;align-items:center;justify-content:center;margin-top:3px; +} +ul.errs{list-style:none;display:flex;flex-direction:column;gap:9px} +ul.errs li{display:flex;gap:9px;font-size:13.5px;line-height:1.6;color:var(--txt2)} +ul.errs li::before{content:"✕";color:var(--d4);flex:none;font-weight:700;font-size:12px;margin-top:3px} +ol.steps{list-style:decimal;padding-left:19px;display:flex;flex-direction:column;gap:8px} +ol.steps li{font-size:13.5px;line-height:1.65;color:var(--txt2)} +ol.steps li::marker{color:var(--txt3);font-size:12px} + +/* 推荐组数 */ +.goal-tabs{display:flex;gap:6px;margin-bottom:13px} +.goal-tab{ + flex:1;padding:7px 4px;border-radius:9px;font-size:12.5px;text-align:center; + background:var(--raised);color:var(--txt2);border:1px solid transparent; +} +.goal-tab.on{background:var(--accent);color:#17130f;font-weight:700} +.rx-big{ + font-size:26px;font-weight:750;color:var(--accent2); + text-align:center;padding:8px 0 12px;letter-spacing:.5px; +} +.rx-rows{display:flex;flex-direction:column;gap:9px;border-top:1px solid var(--line);padding-top:12px} +.rx-row{display:flex;gap:10px;font-size:13px;align-items:flex-start} +.rx-k{color:var(--txt3);flex:none;width:4.5em} +.rx-v{color:var(--txt);flex:1;line-height:1.55} + +.note-box{ + font-size:13px;line-height:1.7;color:var(--txt2); + background:var(--raised);border-radius:11px;padding:12px;margin-top:12px; +} +.chain-pos{ + display:flex;align-items:center;justify-content:space-between; + font-size:12.5px;color:var(--txt2);margin-top:12px; +} +.chain-pos b{color:var(--accent)} + +/* ═══════════════════════════════ 训练计划 ═══════════════════════════════ */ +.plan-form{display:flex;flex-direction:column;gap:17px;margin-top:4px} +.pf-group label{font-size:12.5px;color:var(--txt3);font-weight:600;display:block;margin-bottom:8px} +.seg{display:flex;gap:6px;flex-wrap:wrap} +.seg button{ + flex:1;min-width:calc(33.33% - 4px);padding:9px 6px;border-radius:11px;font-size:13px; + background:var(--surface);border:1px solid var(--line);color:var(--txt2); +} +.seg button.on{background:var(--accent);border-color:var(--accent);color:#17130f;font-weight:650} +.btn-primary{ + width:100%;padding:14px;border-radius:13px;margin-top:6px; + background:linear-gradient(135deg,var(--accent),#ff6b1a); + color:#17130f;font-size:15.5px;font-weight:700; +} +.btn-primary:active{opacity:.85} + +#planResult{scroll-margin-top:78px} +.plan-block{margin-top:22px} +.plan-block h3{ + font-size:13px;color:var(--accent);font-weight:700;letter-spacing:.6px; + margin-bottom:10px;display:flex;align-items:center;gap:7px; +} +.plan-block h3 span{color:var(--txt3);font-weight:500;letter-spacing:0} +.plan-item{ + display:flex;gap:11px;align-items:center;padding:9px;width:100%;text-align:left; + background:var(--surface);border:1px solid var(--line);border-radius:13px;margin-bottom:8px; +} +.plan-item:active{border-color:var(--accent)} +.plan-item img{width:52px;height:52px;border-radius:10px;background:#fff;flex:none;object-fit:cover} +.plan-rx{font-size:12.5px;color:var(--accent2);font-weight:650;margin-top:3px} +.plan-note{font-size:12.5px;color:var(--txt3);line-height:1.65;margin-top:10px} + +/* ═══════════════════════════════ 底部导航 ═══════════════════════════════ */ +#tabbar{ + position:fixed;left:0;right:0;bottom:0;z-index:60;display:flex; + height:calc(var(--tabh) + var(--safeb));padding-bottom:var(--safeb); + background:rgba(13,15,18,.94);backdrop-filter:blur(14px); + border-top:1px solid var(--line); +} +.tab{ + flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center; + gap:3px;font-size:10.5px;color:var(--txt3); +} +.tab span{font-size:19px;line-height:1} +.tab.active{color:var(--accent)} + +@media(min-width:640px){ + .view{max-width:620px;margin:0 auto} + .fam-grid{grid-template-columns:repeat(3,1fr)} +} diff --git a/app/server.py b/app/server.py new file mode 100644 index 00000000..b9a47e18 --- /dev/null +++ b/app/server.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""囚徒健身 webapp 的静态服务器。 + +把 app/public 挂在 /,并把仓库根目录的 images/ 与 videos/ 直接映射出去, +这样前端可以用 /images/xxx.jpg、/videos/xxx.gif 引用数据集自带的媒体。 + + python3 app/server.py [--host 0.0.0.0] [--port 9876] +""" +import argparse +import os +import posixpath +import socket +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import unquote, urlparse + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +PUBLIC = os.path.join(ROOT, "app", "public") +# 允许从仓库根目录直接读取的媒体目录 +MEDIA_DIRS = ("images", "videos") + + +class Handler(SimpleHTTPRequestHandler): + def translate_path(self, path): + rel = unquote(urlparse(path).path).lstrip("/") + # 规范化,挡掉 ../ 之类的穿越尝试 + parts = [p for p in posixpath.normpath("/" + rel).split("/") if p and p != ".."] + rel = "/".join(parts) + base = ROOT if parts and parts[0] in MEDIA_DIRS else PUBLIC + full = os.path.join(base, *parts) if parts else os.path.join(PUBLIC, "index.html") + if os.path.isdir(full): + full = os.path.join(full, "index.html") + return full + + def end_headers(self): + p = self.path.split("?")[0] + if p.startswith(("/images/", "/videos/")): + self.send_header("Cache-Control", "public, max-age=604800") # 媒体缓存一周 + else: + self.send_header("Cache-Control", "no-cache") # 代码与数据不缓存 + super().end_headers() + + def log_message(self, fmt, *args): + if "?" not in self.path and not self.path.startswith(("/images/", "/videos/")): + super().log_message(fmt, *args) + + +def lan_ip(): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except OSError: + return "127.0.0.1" + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--host", default="0.0.0.0") + ap.add_argument("--port", type=int, default=9876) + a = ap.parse_args() + + data = os.path.join(PUBLIC, "data", "app-data.json") + if not os.path.exists(data): + raise SystemExit("缺少 app/public/data/app-data.json,请先运行: python3 app/build/build.py") + + srv = ThreadingHTTPServer((a.host, a.port), Handler) + print(f"💪 囚徒健身 webapp 已启动") + print(f" 本机 http://127.0.0.1:{a.port}") + print(f" 手机 http://{lan_ip()}:{a.port}") + print(" Ctrl+C 停止") + try: + srv.serve_forever() + except KeyboardInterrupt: + print("\n已停止") + srv.shutdown() + + +if __name__ == "__main__": + main()