Archived
Compare commits
39
Commits
906265229c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3baec3b346 | ||
|
|
ac72e536f7 | ||
|
|
351a6a2a41 | ||
|
|
0e4fa065a3 | ||
|
|
073072f525 | ||
|
|
95e84ead38 | ||
|
|
e577556b60 | ||
|
|
aebb59390c | ||
|
|
c5dc6d2a17 | ||
|
|
10390fee42 | ||
|
|
3da54a1827 | ||
|
|
d9c47298a1 | ||
|
|
1ff1b335fc | ||
|
|
3bb1070582 | ||
|
|
f787a942ee | ||
|
|
762915ab71 | ||
|
|
398404e45b | ||
|
|
5233a0f10a | ||
|
|
0a6fb44a6a | ||
|
|
0df26a370b | ||
|
|
902fb7808c | ||
|
|
748644f350 | ||
|
|
ce79460294 | ||
|
|
669b54ae0a | ||
|
|
3ddaa671c8 | ||
|
|
3320094814 | ||
|
|
9c2c626c12 | ||
|
|
5897e45782 | ||
|
|
474b47501b | ||
|
|
53395bc827 | ||
|
|
0e984e008b | ||
|
|
dffad202f5 | ||
|
|
5a735e150c | ||
|
|
a584567ab3 | ||
|
|
f3ce125c1b | ||
|
|
1838d33275 | ||
|
|
d85e2ead34 | ||
|
|
7e1392cfad | ||
|
|
1f5e6a970a |
@@ -0,0 +1,182 @@
|
||||
name: PR合并自动部署
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 配置SSH环境
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: 远程执行部署
|
||||
run: |
|
||||
ssh -T -i ~/.ssh/deploy_key \
|
||||
${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} <<'EOF'
|
||||
|
||||
set -e
|
||||
|
||||
# ==============================
|
||||
# 项目目录
|
||||
# ==============================
|
||||
PROJECT="/www/wwwroot/Hulumath-Web-Demo"
|
||||
|
||||
echo "========================================"
|
||||
echo "🚀 开始部署"
|
||||
echo "项目目录: $PROJECT"
|
||||
echo "========================================"
|
||||
|
||||
cd "$PROJECT"
|
||||
|
||||
echo "当前目录:"
|
||||
pwd
|
||||
|
||||
# 防止 Git 因目录所有者不同而报 dubious ownership
|
||||
git config --global --add safe.directory "$PROJECT" || true
|
||||
|
||||
# ==============================
|
||||
# 检查 Git 仓库
|
||||
# ==============================
|
||||
if [ ! -d ".git" ]; then
|
||||
echo "❌ 当前目录不是 Git 仓库"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Git 仓库检查通过"
|
||||
|
||||
# ==============================
|
||||
# 拉取远程 main
|
||||
# ==============================
|
||||
echo "📥 获取远程 main 分支..."
|
||||
git fetch origin main
|
||||
|
||||
# ==============================
|
||||
# 判断本次提交是否修改数据库
|
||||
# ==============================
|
||||
DB_CHANGED=0
|
||||
|
||||
if git diff --name-only HEAD origin/main | grep -qx "data/miniapp_v3.db"; then
|
||||
DB_CHANGED=1
|
||||
echo "⚠️ 本次代码提交包含数据库变更"
|
||||
else
|
||||
echo "✅ 本次代码提交未修改数据库"
|
||||
fi
|
||||
|
||||
# ==============================
|
||||
# 备份当前运行时数据库
|
||||
# ==============================
|
||||
DB_BACKUP="/tmp/miniapp_v3.db.deploy_backup"
|
||||
|
||||
rm -f "$DB_BACKUP"
|
||||
|
||||
if [ -f "data/miniapp_v3.db" ]; then
|
||||
cp "data/miniapp_v3.db" "$DB_BACKUP"
|
||||
echo "✅ 已备份当前运行时数据库"
|
||||
else
|
||||
echo "ℹ️ 当前不存在运行时数据库"
|
||||
fi
|
||||
|
||||
# ==============================
|
||||
# 更新代码
|
||||
# ==============================
|
||||
echo "🔄 更新代码到 origin/main..."
|
||||
|
||||
git reset --hard origin/main
|
||||
|
||||
echo "✅ 代码更新完成"
|
||||
|
||||
# ==============================
|
||||
# 恢复运行时数据库
|
||||
#
|
||||
# 如果本次 PR 没修改数据库:
|
||||
# 保留服务器当前运行时数据库
|
||||
#
|
||||
# 如果本次 PR 修改了数据库:
|
||||
# 使用仓库中新版本数据库
|
||||
# ==============================
|
||||
if [ "$DB_CHANGED" -eq 0 ]; then
|
||||
if [ -f "$DB_BACKUP" ]; then
|
||||
cp "$DB_BACKUP" "data/miniapp_v3.db"
|
||||
echo "✅ 已恢复运行时数据库"
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ 仓库包含数据库更新,不恢复旧数据库"
|
||||
fi
|
||||
|
||||
rm -f "$DB_BACKUP"
|
||||
|
||||
# ==============================
|
||||
# 安装 Python 依赖
|
||||
# ==============================
|
||||
echo "📦 安装 Python 依赖..."
|
||||
|
||||
/www/server/pyporject_evn/versions/3.12.13/bin/pip \
|
||||
install -r requirements.txt
|
||||
|
||||
echo "✅ Python 依赖安装完成"
|
||||
|
||||
# ==============================
|
||||
# 修正权限
|
||||
# ==============================
|
||||
chown -R www:www "$PROJECT"
|
||||
|
||||
# ==============================
|
||||
# Gunicorn 优雅重载
|
||||
# ==============================
|
||||
echo "🔄 准备重载 Gunicorn..."
|
||||
|
||||
if [ -f "$PROJECT/gunicorn.pid" ]; then
|
||||
|
||||
MASTER_PID=$(cat "$PROJECT/gunicorn.pid")
|
||||
|
||||
echo "Gunicorn PID 文件中的进程: $MASTER_PID"
|
||||
|
||||
if kill -0 "$MASTER_PID" 2>/dev/null; then
|
||||
|
||||
kill -HUP "$MASTER_PID"
|
||||
|
||||
echo "✅ Gunicorn 已发送 HUP,优雅重载完成"
|
||||
|
||||
else
|
||||
|
||||
echo "⚠️ gunicorn.pid 中的进程不存在"
|
||||
|
||||
fi
|
||||
|
||||
else
|
||||
|
||||
# PID 文件不存在时备用查找
|
||||
MASTER_PID=$(pgrep -f "gunicorn.*gunicorn_conf.py" | head -n 1 || true)
|
||||
|
||||
if [ -n "$MASTER_PID" ]; then
|
||||
|
||||
echo "检测到 Gunicorn PID: $MASTER_PID"
|
||||
|
||||
kill -HUP "$MASTER_PID"
|
||||
|
||||
echo "✅ Gunicorn 已发送 HUP,优雅重载完成"
|
||||
|
||||
else
|
||||
|
||||
echo "⚠️ 未找到 Gunicorn 进程,跳过重载"
|
||||
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
echo "========================================"
|
||||
echo "✅ 部署完成"
|
||||
echo "========================================"
|
||||
|
||||
EOF
|
||||
@@ -1,4 +1,208 @@
|
||||
# Hulumath-Web-Demo
|
||||
# 葫芦数学 · Hulu Math
|
||||
|
||||
Hulumath Web Demo Version
|
||||
2026.8.8.15:58
|
||||
Flask 单体应用,提供网页端 + MathBTI 数学人格测试。一个 `app.py` 包含路由、API、数据库、大模型对接。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
py -m pip install -r requirements.txt
|
||||
|
||||
# 2. 启动服务
|
||||
py app.py
|
||||
|
||||
# 3. 浏览器访问
|
||||
# http://127.0.0.1:5000/ → 自动跳转 /web(桌面端网页)
|
||||
# http://127.0.0.1:5000/mathbti → MathBTI 数学人格测试
|
||||
# http://127.0.0.1:5000/math_teen → 数学少年线独立剧本页
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
Hulumath-Web-Demo/
|
||||
├── app.py # Flask 后端主文件(路由 + API + SQLite + DeepSeek 对接)
|
||||
├── requirements.txt # Python 依赖
|
||||
├── vercel.json # Vercel 部署路由配置
|
||||
│
|
||||
├── templates/ # Jinja2 模板
|
||||
│ ├── desktop.html # 网页端(桌面布局,引用 desktop.css + desktop.js)
|
||||
│ ├── mathbti.html # MathBTI 数学人格测试页(引用 mathbti.css + mathbti.js)
|
||||
│ └── math_teen.html # 数学少年线独立播放页(自包含,无外部 CSS/JS)
|
||||
│
|
||||
├── static/ # 静态资源
|
||||
│ ├── css/
|
||||
│ │ ├── desktop.css # 网页端样式
|
||||
│ │ └── mathbti.css # MathBTI 页样式
|
||||
│ ├── js/
|
||||
│ │ ├── desktop.js # 网页端逻辑
|
||||
│ │ └── mathbti.js # MathBTI 页逻辑
|
||||
│ └── img/
|
||||
│ └── mathematicians/ # 16 位数学人物肖像图片(0000~1111.png)
|
||||
│
|
||||
├── data/ # 运行时数据(SQLite + 种子 JSON)
|
||||
│ ├── miniapp_v3.db # SQLite 数据库(用户、收藏、历史、知识卡片)
|
||||
│ ├── seed_videos.json # 视频种子数据(54 条)
|
||||
│ ├── seed_story.json # 葫芦侠行剧本数据(8 章 93 节点)
|
||||
│ ├── seed_simulator.json # 数学专业模拟器剧本数据
|
||||
│ ├── seed_mathbti.json # MathBTI 测试题目 + 16 种结果数据
|
||||
│ └── seed_alumni.json # 数学校友访谈数据
|
||||
│
|
||||
├── docs/ # 内容创作文档(不是运行时依赖)
|
||||
│ ├── 数学少年线_story.json # 数学少年线剧本数据(被 app.py 直接读取)
|
||||
│ ├── novel/ # 小说原稿(葫芦侠行 8 章 txt + 设定)
|
||||
│ ├── 产品文档/ # PRD、需求、开发记录
|
||||
│ └── 会议记录/ # 需求评审纪要
|
||||
│
|
||||
└── scripts/ # 构建/导出脚本
|
||||
├── build_mathbti.py
|
||||
├── build_story.py
|
||||
├── export_mathbti_doc.py
|
||||
└── export_mathematicians_docx.py
|
||||
```
|
||||
|
||||
## data/ 下的 JSON 文件说明
|
||||
|
||||
这些是应用运行时通过 API 读取的种子数据。`app.py` 在启动时根据路径加载它们。
|
||||
|
||||
| 文件 | 对应 API 路由 | 用途 | 数据结构概要 |
|
||||
|------|-------------|------|-------------|
|
||||
| `seed_videos.json` | `/api/videos` | 发现页视频列表 | 数组,每条含 id/title/author/discipline/video_url/duration_min 等 |
|
||||
| `seed_story.json` | `/api/story` | 葫芦侠行互动剧本 | `{title, start_node, nodes: {node_id: {scene, character, choices: [{text, next, effects}]}}}` |
|
||||
| `seed_simulator.json` | `/api/simulator` | 数学专业模拟器 | 同 story 结构,属性为 gpa/interest/skill |
|
||||
| `seed_mathbti.json` | `/api/math_personality` | MathBTI 测试题+结果 | `{axes, clans, questions, results: {binary_code: {name, clan, stats5, ...}}}` |
|
||||
| `seed_alumni.json` | `/api/alumni` | 数学校友访谈 | `{alumni: [{id, name, major, four_years: {nodes}}], meta}` |
|
||||
|
||||
> **注意**:`数学少年线_story.json` 在 `docs/` 目录下,不在 `data/`。`app.py` 第 37 行直接指向 `docs/数学少年线_story.json`。
|
||||
|
||||
## docs/ 目录说明
|
||||
|
||||
`docs/` 是内容创作区,**不影响应用运行**(除了数学少年线剧本文件被直接读取)。
|
||||
|
||||
- `docs/novel/` — 葫芦侠行的小说原稿和设定文档(txt/md/docx)
|
||||
- `docs/产品文档/` — 产品需求文档、PRD、开发记录
|
||||
- `docs/会议记录/` — 需求评审会议纪要
|
||||
- `docs/数学少年线_story.json` — 数学少年线互动剧本数据,**被 app.py 直接读取**,API `/api/story/math_teen` 返回此文件内容
|
||||
|
||||
### 在新项目中复用 docs/
|
||||
|
||||
如果你想在新项目里使用这些内容:
|
||||
|
||||
1. **互动剧本**:复制 `docs/数学少年线_story.json` 或 `data/seed_story.json` 到新项目,按 `{start_node, nodes: {id: {scene, character, choices}}}` 格式读取即可。前端用 JS 根据 `currentNode` 渲染 `scene` 文字和 `choices` 按钮即可。
|
||||
2. **MathBTI 数据**:复制 `data/seed_mathbti.json`,包含 4 轴 12 题 + 16 种结果(含数学家小传、五维能力值、肖像图路径)。
|
||||
3. **小说原稿**:`docs/novel/第0X章.txt` 是纯文本,可直接用于任何文本展示场景。
|
||||
|
||||
## scripts/ 脚本说明
|
||||
|
||||
这些脚本用于**生成种子 JSON** 或**导出 Word 文档**。在项目根目录运行。
|
||||
|
||||
### build_mathbti.py — 生成 MathBTI 种子数据
|
||||
|
||||
将硬编码的题目、4 轴、4 族、16 种结果写入 `data/seed_mathbti.json`。
|
||||
|
||||
```bash
|
||||
py scripts/build_mathbti.py
|
||||
# 输出: data/seed_mathbti.json
|
||||
# 打印: OK: seed_mathbti.json generated, 16 results, 4 clans
|
||||
```
|
||||
|
||||
> 脚本内用 Python dict 写死全部数据,`json.dump` 输出。修改题目/结果后重跑即可刷新 `data/seed_mathbti.json`。
|
||||
|
||||
### build_story.py — 生成葫芦侠行剧本
|
||||
|
||||
根据大纲原文构建 8 章 93 节点的互动剧情 JSON。
|
||||
|
||||
```bash
|
||||
py scripts/build_story.py
|
||||
# 输出路径写死在脚本末尾(第 1006 行),需手动改为你的项目路径
|
||||
```
|
||||
|
||||
> 脚本提供 `N(node_id, scene, character, choices)` 和 `C(text, next, shuli, xiayi, xinzhi)` 两个辅助函数来逐节点构建故事。**输出路径硬编码在脚本中**,复制到新项目后需修改 `out_path`。
|
||||
|
||||
### export_mathbti_doc.py — 导出 Word 设定文档
|
||||
|
||||
将 16 位数学家设定导出为 Word 文档。
|
||||
|
||||
```bash
|
||||
py scripts/export_mathbti_doc.py
|
||||
# 输出: scripts/万理之城_设定圣经.docx
|
||||
# 依赖: python-docx
|
||||
```
|
||||
|
||||
### export_mathematicians_docx.py — 导出数学家小传 Word
|
||||
|
||||
将 16 位数学家小传 + 六维雷达数据导出为 Word。
|
||||
|
||||
```bash
|
||||
py scripts/export_mathematicians_docx.py
|
||||
# 输出路径硬编码在脚本第 528 行,需手动修改
|
||||
# 依赖: python-docx
|
||||
```
|
||||
|
||||
> **注意**:两个导出脚本的输出路径是硬编码的绝对路径,复制到新项目后需要修改脚本中的路径。
|
||||
|
||||
## 在新项目中使用
|
||||
|
||||
### 最小化移植(只要网页端)
|
||||
|
||||
1. 复制 `app.py`、`templates/desktop.html`、`static/css/desktop.css`、`static/js/desktop.js`、`data/` 目录
|
||||
2. `py -m pip install flask requests`
|
||||
3. `py app.py`
|
||||
|
||||
### 只要互动剧本引擎
|
||||
|
||||
剧本数据格式是通用的 JSON,任何语言都能读:
|
||||
|
||||
```python
|
||||
import json
|
||||
with open('seed_story.json', 'r', encoding='utf-8') as f:
|
||||
story = json.load(f)
|
||||
|
||||
current_node = story['start_node'] # 例如 'ch1_start'
|
||||
node = story['nodes'][current_node]
|
||||
print(node['scene']) # 场景文字
|
||||
print(node['character']) # 说话人
|
||||
for choice in node.get('choices', []):
|
||||
print(choice['text']) # 选项文字
|
||||
print(choice['next']) # 下一节点 ID
|
||||
print(choice.get('effects', {})) # 属性变化
|
||||
```
|
||||
|
||||
前端渲染逻辑参考 `static/js/desktop.js` 中的 `loadStory()` 和 `rSN()` 函数。
|
||||
|
||||
### 只要 MathBTI 测试
|
||||
|
||||
1. 复制 `templates/mathbti.html`、`static/css/mathbti.css`、`static/js/mathbti.js`、`data/seed_mathbti.json`
|
||||
2. MathBTI 前端是自包含的,只需能 fetch 到 `seed_mathbti.json` 即可运行
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Python 3.12+
|
||||
- Flask 3.x
|
||||
- requests(DeepSeek AI 对话用)
|
||||
- python-docx(仅 scripts/ 导出 Word 用)
|
||||
|
||||
## API 路由一览
|
||||
|
||||
| 路由 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/` | GET | 重定向到 `/web` |
|
||||
| `/web` | GET | 桌面端网页 |
|
||||
| `/mathbti` | GET | MathBTI 测试页 |
|
||||
| `/math_teen` | GET | 数学少年线独立剧本页 |
|
||||
| `/api/videos` | GET | 视频列表 |
|
||||
| `/api/knowledge` | GET | 知识卡片列表 |
|
||||
| `/api/knowledge/<id>` | GET | 单个知识卡片详情 |
|
||||
| `/api/story` | GET | 葫芦侠行剧本数据 |
|
||||
| `/api/story/math_teen` | GET | 数学少年线剧本数据 |
|
||||
| `/api/simulator` | GET | 模拟器剧本数据 |
|
||||
| `/api/alumni` | GET | 校友访谈数据 |
|
||||
| `/api/math_personality` | GET | MathBTI 题目+结果数据 |
|
||||
| `/api/math_personality/calculate` | POST | 计算数学人格结果 |
|
||||
| `/api/chat` | POST | AI 数学助教对话(对接 DeepSeek) |
|
||||
| `/api/favorites` | GET/POST/DELETE | 收藏管理 |
|
||||
| `/api/history` | GET/POST | 观看历史 |
|
||||
| `/api/user/profile` | GET/POST | 用户资料 |
|
||||
| `/api/user/stats` | GET | 用户统计 |
|
||||
| `/api/simulator/save` | GET/POST | 模拟器存档 |
|
||||
| `/data/<filename>` | GET | 直接访问 data/ 下的 JSON 文件 |
|
||||
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
# 新项目复用教程
|
||||
|
||||
> 假设你要开一个新仓库,做一个类似的"Python(原Flask)+ 互动剧本 + 人格测试"项目。本教程告诉你从这个老仓库拿什么、怎么拿、怎么改。
|
||||
|
||||
---
|
||||
|
||||
## 一、先搞清楚这个项目有哪几块可复用
|
||||
|
||||
| 模块 | 核心文件 | 能干嘛 |
|
||||
|------|---------|--------|
|
||||
| **Flask 后端骨架** | `app.py` | 路由、API、SQLite、gzip 压缩、静态缓存 |
|
||||
| **互动剧本引擎** | `data/seed_story.json` + `desktop.js` 中的 story 相关函数 | 读取 JSON 剧本 → 前端渲染场景文字 + 选项按钮 + 属性变化 |
|
||||
| **MathBTI 人格测试** | `data/seed_mathbti.json` + `templates/mathbti.html` + `mathbti.css/js` | 12 题测出 16 种结果,前端自包含可独立部署 |
|
||||
| **卡片/抽卡系统** | `desktop.js` 中的 card/gacha 相关函数 | 16 张人物卡,读知识卡片攒抽卡次数,保底机制 |
|
||||
| **内容构建脚本** | `scripts/build_*.py` | 用 Python dict 生成种子 JSON |
|
||||
| **Word 导出脚本** | `scripts/export_*.py` | 把人物设定导出为 docx |
|
||||
| **小说/设定原稿** | `docs/novel/*.txt` | 纯文本,可直接喂给任何文本处理流程 |
|
||||
|
||||
---
|
||||
|
||||
## 二、新项目规划建议
|
||||
|
||||
### 场景 A:做一个新互动剧本项目(最常见)
|
||||
|
||||
你只需要:**Flask 骨架 + 剧本引擎 + 剧本数据**。
|
||||
|
||||
```
|
||||
new-project/
|
||||
├── app.py # 从老项目复制,改掉路径和 API key
|
||||
├── data/
|
||||
│ └── my_story.json # 你的新剧本(照着老格式写)
|
||||
├── templates/
|
||||
│ └── index.html # 从 desktop.html 简化,或自己写
|
||||
├── static/
|
||||
│ ├── css/
|
||||
│ └── js/
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
### 场景 B:做一个人格/心理测试项目
|
||||
|
||||
你只需要:**MathBTI 前端 + 数据格式**。
|
||||
|
||||
```
|
||||
new-project/
|
||||
├── data/
|
||||
│ └── seed_mytest.json # 你的测试题+结果(照 mathbti 格式写)
|
||||
├── templates/
|
||||
│ └── test.html # 从 mathbti.html 改
|
||||
├── static/
|
||||
│ ├── css/
|
||||
│ └── js/
|
||||
└── scripts/
|
||||
└── build_mytest.py # 从 build_mathbti.py 改
|
||||
```
|
||||
|
||||
### 场景 C:纯内容搬运(不要代码)
|
||||
|
||||
你只需要 `docs/` 和 `data/*.json` 里的文本和数据,用自己的技术栈重新实现前端。
|
||||
|
||||
---
|
||||
|
||||
## 三、逐步操作
|
||||
|
||||
### Step 1 — 拿 Flask 骨架
|
||||
|
||||
从老项目复制 `app.py`,然后改这几处:
|
||||
|
||||
```python
|
||||
# 1. 改 DeepSeek API Key(第 83 行附近)
|
||||
DEEPSEEK_API_KEY = "你的key"
|
||||
|
||||
# 2. 改数据路径(第 30~40 行)
|
||||
# 删掉你不需要的路径变量,加上你自己的
|
||||
DATA_DIR = os.path.join(BASE_DIR, "data").replace("\\", "/")
|
||||
MY_STORY_PATH = os.path.join(DATA_DIR, "my_story.json").replace("\\", "/")
|
||||
|
||||
# 3. 改路由(第 258 行起)
|
||||
# 删掉不需要的 @app.route,加上你自己的
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
# 4. 改 API 函数
|
||||
# 照着 api_story() 的写法,load 你的 JSON 返回给前端
|
||||
```
|
||||
|
||||
### Step 2 — 拿剧本引擎
|
||||
|
||||
**数据格式**(这是最关键的,任何语言都能读):
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "我的剧本",
|
||||
"start_node": "ch1_start",
|
||||
"nodes": {
|
||||
"ch1_start": {
|
||||
"scene": "场景描述文字。用 \\n 换行。",
|
||||
"character": "旁白",
|
||||
"choices": [
|
||||
{
|
||||
"text": "选项A的文字",
|
||||
"next": "ch1_choice_a",
|
||||
"effects": { "str": 5 }
|
||||
},
|
||||
{
|
||||
"text": "选项B的文字",
|
||||
"next": "ch1_choice_b",
|
||||
"effects": { "str": -3 }
|
||||
}
|
||||
]
|
||||
},
|
||||
"ch1_choice_a": {
|
||||
"scene": "你选了A之后的故事...",
|
||||
"character": "NPC",
|
||||
"choices": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**前端渲染逻辑**(从 `desktop.js` 提取的核心循环):
|
||||
|
||||
```javascript
|
||||
let storyData = null;
|
||||
let currentState = { currentNode: null, stats: {} };
|
||||
|
||||
async function loadStory() {
|
||||
const r = await fetch('/api/story');
|
||||
storyData = (await r.json()).data;
|
||||
const start = storyData.start_node;
|
||||
const node = storyData.nodes[start];
|
||||
renderNode(start);
|
||||
}
|
||||
|
||||
function renderNode(nodeId) {
|
||||
const node = storyData.nodes[nodeId];
|
||||
if (!node) return;
|
||||
currentState.currentNode = nodeId;
|
||||
localStorage.setItem('my_story_save', JSON.stringify(currentState));
|
||||
|
||||
// 渲染场景文字
|
||||
document.getElementById('scene').textContent = node.scene || '';
|
||||
// 渲染说话人
|
||||
document.getElementById('character').textContent = node.character || '';
|
||||
|
||||
// 渲染选项按钮
|
||||
const choicesEl = document.getElementById('choices');
|
||||
choicesEl.innerHTML = '';
|
||||
for (const choice of (node.choices || [])) {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = choice.text;
|
||||
btn.onclick = () => makeChoice(choice);
|
||||
choicesEl.appendChild(btn);
|
||||
}
|
||||
|
||||
// 没有选项 = 故事结束
|
||||
if (!node.choices || node.choices.length === 0) {
|
||||
choicesEl.innerHTML = '<div>故事结束</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function makeChoice(choice) {
|
||||
// 应用属性变化
|
||||
if (choice.effects) {
|
||||
for (const key in choice.effects) {
|
||||
currentState.stats[key] = (currentState.stats[key] || 0) + choice.effects[key];
|
||||
}
|
||||
}
|
||||
// 跳到下一节点
|
||||
renderNode(choice.next);
|
||||
}
|
||||
```
|
||||
|
||||
**HTML 骨架**:
|
||||
|
||||
```html
|
||||
<div id="stats"></div>
|
||||
<div id="character"></div>
|
||||
<div id="scene"></div>
|
||||
<div id="choices"></div>
|
||||
```
|
||||
|
||||
### Step 3 — 拿剧本构建脚本
|
||||
|
||||
从 `scripts/build_story.py` 复制,它提供了两个辅助函数让你不用手写 JSON:
|
||||
|
||||
```python
|
||||
# N(node_id, 场景文字, 说话人, 选项列表)
|
||||
# C(选项文字, 下一节点ID, 属性变化...)
|
||||
|
||||
N("ch1_start", "你站在路口。", "旁白", [
|
||||
C("往左走", "ch1_left", str=5),
|
||||
C("往右走", "ch1_right", str=-2),
|
||||
])
|
||||
```
|
||||
|
||||
在脚本末尾改输出路径后运行 `py scripts/build_story.py` 就会生成 JSON 文件。
|
||||
|
||||
### Step 4 — 拿 MathBTI 测试
|
||||
|
||||
1. 复制 `templates/mathbti.html`、`static/css/mathbti.css`、`static/js/mathbti.js`、`data/seed_mathbti.json`
|
||||
2. 前端逻辑在 `mathbti.js` 里,读 JSON → 逐题渲染 → 算 4 轴分数 → 映射到 16 种结果(4 位二进制)
|
||||
3. 如果你要改题,直接改 `data/seed_mathbti.json`,或用 `scripts/build_mathbti.py` 重新生成
|
||||
|
||||
### Step 5 — 拿 Word 导出脚本
|
||||
|
||||
`scripts/export_*.py` 用 `python-docx` 把设定文字导出为 Word。复制后改两处:
|
||||
- 脚本内的内容文字(硬编码在 Python dict/string 里)
|
||||
- 输出路径(脚本末尾,硬编码的绝对路径)
|
||||
|
||||
```bash
|
||||
py -m pip install python-docx
|
||||
py scripts/export_mathbti_doc.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、内容文件怎么搬
|
||||
|
||||
| 你要什么 | 从哪拿 | 怎么用 |
|
||||
|---------|-------|--------|
|
||||
| 葫芦侠行剧本(8 章 93 节点) | `data/seed_story.json` | 直接当 JSON 读,格式见上 |
|
||||
| 数学少年线剧本(85 节点) | `docs/数学少年线_story.json` | 同上格式,但属性名不同(shuli/xiayi/xinzhi) |
|
||||
| MathBTI 全套数据(4 轴 12 题 16 结果) | `data/seed_mathbti.json` | 前端 fetch 后渲染问卷 |
|
||||
| 模拟器剧本 | `data/seed_simulator.json` | 同剧本格式,属性为 gpa/interest/skill |
|
||||
| 校友访谈数据 | `data/seed_alumni.json` | 线性叙事,four_years.nodes 数组 |
|
||||
| 16 位数学家肖像图 | `static/img/mathematicians/` | 文件名是 4 位二进制(0000~1111.png) |
|
||||
| 小说原稿 | `docs/novel/第0X章.txt` | 纯文本,随便用 |
|
||||
| 人物设定文档 | `docs/novel/数学家小传_*.md` | Markdown,随便用 |
|
||||
|
||||
---
|
||||
|
||||
## 五、新项目清单
|
||||
|
||||
开新仓库时按这个清单走:
|
||||
|
||||
- [ ] 复制 `app.py`,改掉 API key、数据路径、路由
|
||||
- [ ] 复制 `requirements.txt`,删掉不需要的包
|
||||
- [ ] 建 `data/` 目录,放你的 JSON 种子数据
|
||||
- [ ] 建 `templates/` 目录,放你的 HTML 模板
|
||||
- [ ] 建 `static/css/` 和 `static/js/`,放样式和脚本
|
||||
- [ ] 如果有剧本,照 Step 2 的格式写 JSON,或用 `build_story.py` 生成
|
||||
- [ ] 如果有 Word 导出需求,复制 `scripts/export_*.py` 改内容
|
||||
- [ ] `py app.py` 启动,浏览器验证
|
||||
|
||||
---
|
||||
|
||||
## 六、常见坑
|
||||
|
||||
1. **旧 Flask 进程占端口**:改完代码后页面不更新?`Get-Process python | Stop-Process -Force` 杀掉旧进程再重启。
|
||||
2. **剧本 JSON 里的中文**:文件编码必须是 UTF-8,`json.load(f)` 时加 `encoding='utf-8'`。
|
||||
3. **build_story.py 输出路径**:脚本末尾的 `out_path` 是硬编码绝对路径,换机器必改。
|
||||
4. **math_teen 故事文件位置**:`数学少年线_story.json` 在 `docs/` 不在 `data/`,`app.py` 第 37 行直接指向 docs 目录。
|
||||
5. **localStorage 存档**:前端用 localStorage 保存剧本进度,key 名在 JS 里定义(如 `mb_story_save`),换项目时注意别冲突。
|
||||
6. **静态资源缓存**:HTML 里引用 CSS/JS 带 `?v=数字` 版本号,改了前端代码后递增这个数字,否则浏览器用缓存。
|
||||
@@ -20,7 +20,7 @@ import sqlite3
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
from flask import Flask, render_template, request, jsonify, send_from_directory
|
||||
from flask import Flask, render_template, request, jsonify, send_from_directory, redirect
|
||||
|
||||
# ---- 初始化 Flask 应用 ----
|
||||
app = Flask(__name__)
|
||||
@@ -34,7 +34,7 @@ DATA_DIR = os.path.join(BASE_DIR, "data").replace("\\", "/")
|
||||
DB_PATH = os.path.join(DATA_DIR, "miniapp_v3.db").replace("\\", "/")
|
||||
VIDEOS_PATH = os.path.join(DATA_DIR, "seed_videos.json").replace("\\", "/")
|
||||
STORY_PATH = os.path.join(DATA_DIR, "seed_story.json").replace("\\", "/")
|
||||
MATH_TEEN_STORY_PATH = os.path.join(DATA_DIR, "数学少年线_story.json").replace("\\", "/")
|
||||
MATH_TEEN_STORY_PATH = os.path.join(BASE_DIR, "docs", "数学少年线_story.json").replace("\\", "/")
|
||||
SIMULATOR_PATH = os.path.join(DATA_DIR, "seed_simulator.json").replace("\\", "/")
|
||||
MATHBTI_PATH = os.path.join(DATA_DIR, "seed_mathbti.json").replace("\\", "/")
|
||||
ALUMNI_PATH = os.path.join(DATA_DIR, "seed_alumni.json").replace("\\", "/")
|
||||
@@ -257,17 +257,8 @@ def load_seed_videos():
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
"""
|
||||
主页路由 — 返回 SPA(单页应用)的 HTML。
|
||||
前端所有页面切换都在这一个 HTML 里完成,不刷新浏览器。
|
||||
"""
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/demo")
|
||||
def demo():
|
||||
"""返回独立静态 Demo 页面"""
|
||||
return send_from_directory(BASE_DIR, "mathbrain-demo.html")
|
||||
"""主页路由 — 重定向到桌面端网页版"""
|
||||
return redirect("/web", code=302)
|
||||
|
||||
|
||||
@app.route("/web")
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
Flask==3.1.3
|
||||
requests==2.33.1
|
||||
python-docx==1.2.0
|
||||
@@ -553,3 +553,18 @@ button:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:2px so
|
||||
.cc-card-name{font-size:13px;font-weight:600;color:#3A3632;margin-bottom:2px}
|
||||
.cc-card-type{font-size:10px;color:#ABA6A1;letter-spacing:1px;margin-bottom:4px}
|
||||
.cc-card-clan{font-size:10px;padding:2px 8px;border-radius:8px;display:inline-block}
|
||||
|
||||
/* ========== 我的页面入口卡片 ========== */
|
||||
.mine-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}
|
||||
@media(max-width:640px){.mine-grid{grid-template-columns:1fr}}
|
||||
.mine-entry{display:flex;align-items:center;gap:14px;padding:20px;border:1px solid #D6D1CA;border-radius:8px;background:#FDFCFA;cursor:pointer;transition:all .15s;font:inherit;text-align:left}
|
||||
.mine-entry:hover{border-color:#808C95;background:#F0EEEA;box-shadow:0 3px 12px rgba(0,0,0,.06)}
|
||||
.mine-entry-icon{width:46px;height:46px;border-radius:8px;background:#EAE7E2;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0}
|
||||
.mine-entry-body{flex:1;min-width:0}
|
||||
.mine-entry-name{font-size:15px;font-weight:700;color:#3A3632;margin-bottom:3px}
|
||||
.mine-entry-desc{font-size:12px;color:#8A8580}
|
||||
.mine-entry-arrow{font-size:18px;color:#ABA6A1;font-weight:700}
|
||||
/* 数学人生页 · 葫芦侠行入口 */
|
||||
.ml-story-entry{border-color:#A8907E}
|
||||
.ml-story-entry:hover{background:#FBF8F3;border-color:#A8907E}
|
||||
.ml-story-entry .ml-alumni-entry-icon{background:#F5EDE4}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Vendored
-1
File diff suppressed because one or more lines are too long
-1509
File diff suppressed because it is too large
Load Diff
Vendored
-1263
File diff suppressed because it is too large
Load Diff
+32
-4
@@ -7,7 +7,7 @@ let cardState={owned:[],draws:0,lastDaily:'',knowledgeRead:0,drawHistory:[]};
|
||||
let _lang=localStorage.getItem('mb_lang')||'zh';
|
||||
|
||||
// ===== i18n =====
|
||||
const I18N={zh:{home:'首页',discover:'发现',toolbox:'工具箱',knowledge:'知识卡片',story:'葫芦侠行',simulator:'数学人生',favorites:'我的收藏',profile:'个人中心',search:'搜索内容...',guest:'游客用户',
|
||||
const I18N={zh:{home:'首页',discover:'发现',toolbox:'工具箱',knowledge:'知识卡片',story:'葫芦侠行',simulator:'数学人生',favorites:'我的收藏',profile:'个人中心',mine:'我的',search:'搜索内容...',guest:'游客用户',
|
||||
heroTitle:'葫芦数学 · 用玩的方式打开数学',heroSub:'数学不是公式,是你理解世界的方式',heroCTA:'开始探索 →',
|
||||
exploreTitle:'探索数学的万千可能',tryTitle:'试试这些',
|
||||
mbtiName:'MathBTI 数学人格',mbtiDesc:'12题发现你的数学人格,领养专属数学小人 →',
|
||||
@@ -66,7 +66,7 @@ const I18N={zh:{home:'首页',discover:'发现',toolbox:'工具箱',knowledge:'
|
||||
staticAIToggleTitle:'AI数学助教',staticAIHeader:'💬 AI 数学助教',
|
||||
staticAIWelcome:'你好!我是葫芦数学的AI助教 👋<br><br>试试问我:学数学对学医有什么用?',
|
||||
staticAIPlaceholder:'输入问题...',staticMbtiTooltip:'测测你的数学人格'},
|
||||
en:{home:'Home',discover:'Discover',toolbox:'Toolbox',knowledge:'Cards',story:'HuLu Quest',simulator:'Math Life',favorites:'Favorites',profile:'Profile',search:'Search...',guest:'Guest',
|
||||
en:{home:'Home',discover:'Discover',toolbox:'Toolbox',knowledge:'Cards',story:'HuLu Quest',simulator:'Math Life',favorites:'Favorites',profile:'Profile',mine:'Mine',search:'Search...',guest:'Guest',
|
||||
heroTitle:'Hulu Math · Play your way into math',heroSub:'Math isn\'t formulas. It\'s how you make sense of things.',heroCTA:'Start Exploring →',
|
||||
exploreTitle:'Explore Math',tryTitle:'Try These',
|
||||
mbtiName:'MathBTI Personality Test',mbtiDesc:'12 questions. One math soul. Meet your sprite. →',
|
||||
@@ -202,7 +202,7 @@ function navigateTo(p){
|
||||
const t=document.getElementById('page-'+p);if(t)t.classList.add('active');
|
||||
document.getElementById('mainContent').scrollTop=0;
|
||||
if(p==='home')loadHome();else if(p==='discover')loadDiscover();else if(p==='toolbox')loadToolbox();
|
||||
else if(p==='knowledge')loadKnowledgePage();else if(p==='story')loadStory();else if(p==='simulator')loadSimulator();else if(p==='favorites')loadFavorites();else if(p==='profile')loadProfile();else if(p==='cards')loadCardsPage();
|
||||
else if(p==='knowledge')loadKnowledgePage();else if(p==='story')loadStory();else if(p==='simulator')loadSimulator();else if(p==='favorites')loadFavorites();else if(p==='profile')loadProfile();else if(p==='cards')loadCardsPage();else if(p==='mine')loadMine();
|
||||
}
|
||||
|
||||
// ===== Home (Desmos-style) =====
|
||||
@@ -528,7 +528,7 @@ async function rH(type,id,title){try{await api('/api/history',{method:'POST',bod
|
||||
|
||||
// ===== Profile =====
|
||||
async function loadProfile(){chatCtx={};try{const[pr,sr]=await Promise.all([api('/api/user/profile?user_id='+USER_ID),api('/api/user/stats?user_id='+USER_ID)]);const p=pr.code===0?pr.data:{nickname:'游客用户',avatar_emoji:'葫',avatar_color:'#5B6AF0'};const s=sr.code===0?sr.data:{watch_count:0,favorite_count:0};document.getElementById('profileContent').innerHTML='<div class="profile-card"><div class="profile-avatar-lg" style="background:rgba(255,255,255,.2)">'+esc(p.avatar_emoji||'葫')+'</div><div><div class="profile-name-lg">'+esc(p.nickname||'游客用户')+'</div><div class="profile-id-lg">ID: '+USER_ID+'</div></div></div><div class="profile-stats-grid"><div class="profile-stat-card"><div class="num">'+s.watch_count+'</div><div class="label">观看次数</div></div><div class="profile-stat-card"><div class="num">'+s.favorite_count+'</div><div class="label">收藏数量</div></div></div><div style="background:#fff;border-radius:16px;padding:16px 20px;box-shadow:0 1px 3px rgba(0,0,0,.04)"><div style="font-weight:600;margin-bottom:12px">编辑资料</div><div style="display:flex;gap:12px;align-items:center;margin-bottom:12px"><span style="font-size:13px;color:#64748B">头像:</span><input id="editAvatar" style="padding:8px 12px;border:1.5px solid #E2E8F0;border-radius:10px;font-size:14px;width:80px" value="'+esc(p.avatar_emoji||'葫')+'"></div><div style="display:flex;gap:12px;align-items:center;margin-bottom:16px"><span style="font-size:13px;color:#64748B">昵称:</span><input id="editNickname" style="flex:1;padding:8px 12px;border:1.5px solid #E2E8F0;border-radius:10px;font-size:14px" value="'+esc(p.nickname||'')+'"></div><button onclick="sP()" style="padding:10px 28px;border-radius:12px;border:none;background:#5B6AF0;color:#fff;font-size:14px;font-weight:600;cursor:pointer">保存</button></div>'}catch(e){document.getElementById('profileContent').innerHTML='<p style="color:#94A3B8">加载失败</p>'}}
|
||||
async function sP(){const n=document.getElementById('editNickname').value.trim();const a=document.getElementById('editAvatar').value.trim();try{await api('/api/user/profile',{method:'POST',body:{user_id:USER_ID,nickname:n,avatar_emoji:a}});document.getElementById('sidebarName').textContent=n||'游客用户';document.getElementById('sidebarAvatar').textContent=a||'葫';alert('保存成功');loadProfile()}catch(e){alert('保存失败')}}
|
||||
async function sP(){const n=document.getElementById('editNickname').value.trim();const a=document.getElementById('editAvatar').value.trim();try{await api('/api/user/profile',{method:'POST',body:{user_id:USER_ID,nickname:n,avatar_emoji:a}});document.getElementById('sidebarName').textContent=n||'游客用户';document.getElementById('sidebarAvatar').textContent=a||'葫';alert('保存成功');if(CP==='mine')loadMine();else loadProfile()}catch(e){alert('保存失败')}}
|
||||
|
||||
// ===== Card Collection Page =====
|
||||
async function loadCardsPage(){
|
||||
@@ -557,6 +557,29 @@ async function loadCardsPage(){
|
||||
grid.innerHTML=h;
|
||||
}
|
||||
|
||||
// ===== Mine Page (我的 — 个人中心 + 收藏 + 卡册入口) =====
|
||||
async function loadMine(){
|
||||
chatCtx={};
|
||||
const c=document.getElementById('mineContent');
|
||||
if(!c)return;
|
||||
let p={nickname:'游客用户',avatar_emoji:'葫',avatar_color:'#5B6AF0'};
|
||||
let s={watch_count:0,favorite_count:0};
|
||||
try{
|
||||
const[pr,sr]=await Promise.all([api('/api/user/profile?user_id='+USER_ID),api('/api/user/stats?user_id='+USER_ID)]);
|
||||
if(pr.code===0&&pr.data)p=pr.data;
|
||||
if(sr.code===0&&sr.data)s=sr.data;
|
||||
}catch(e){}
|
||||
c.innerHTML=
|
||||
'<div class="page-header"><h1 class="page-title">👤 我的</h1></div>'+
|
||||
'<div class="profile-card"><div class="profile-avatar-lg" style="background:rgba(255,255,255,.2)">'+esc(p.avatar_emoji||'葫')+'</div><div><div class="profile-name-lg">'+esc(p.nickname||'游客用户')+'</div><div class="profile-id-lg">ID: '+USER_ID+'</div></div></div>'+
|
||||
'<div class="profile-stats-grid"><div class="profile-stat-card"><div class="num">'+s.watch_count+'</div><div class="label">观看次数</div></div><div class="profile-stat-card"><div class="num">'+s.favorite_count+'</div><div class="label">收藏数量</div></div></div>'+
|
||||
'<div style="background:#fff;border-radius:16px;padding:16px 20px;box-shadow:0 1px 3px rgba(0,0,0,.04);margin-bottom:20px"><div style="font-weight:600;margin-bottom:12px">编辑资料</div><div style="display:flex;gap:12px;align-items:center;margin-bottom:12px"><span style="font-size:13px;color:#64748B">头像:</span><input id="editAvatar" style="padding:8px 12px;border:1.5px solid #E2E8F0;border-radius:10px;font-size:14px;width:80px" value="'+esc(p.avatar_emoji||'葫')+'"></div><div style="display:flex;gap:12px;align-items:center;margin-bottom:16px"><span style="font-size:13px;color:#64748B">昵称:</span><input id="editNickname" style="flex:1;padding:8px 12px;border:1.5px solid #E2E8F0;border-radius:10px;font-size:14px" value="'+esc(p.nickname||'')+'"></div><button onclick="sP()" style="padding:10px 28px;border-radius:12px;border:none;background:#5B6AF0;color:#fff;font-size:14px;font-weight:600;cursor:pointer">保存</button></div>'+
|
||||
'<div class="mine-grid">'+
|
||||
'<div class="mine-entry" onclick="navigateTo(\'favorites\')"><div class="mine-entry-icon">❤️</div><div class="mine-entry-body"><div class="mine-entry-name">我的收藏</div><div class="mine-entry-desc">已收藏的视频和知识卡片</div></div><span class="mine-entry-arrow">→</span></div>'+
|
||||
'<div class="mine-entry" onclick="navigateTo(\'cards\')"><div class="mine-entry-icon">🎴</div><div class="mine-entry-body"><div class="mine-entry-name">我的卡册</div><div class="mine-entry-desc">收集全部16位数学人物 · '+cardState.owned.length+'/16</div></div><span class="mine-entry-arrow">→</span></div>'+
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// ===== Pet Experience =====
|
||||
function getPet(){
|
||||
let pet=null;
|
||||
@@ -923,6 +946,11 @@ function loadMathLife(){
|
||||
(t.ready?'':'<div class="ml-coming-badge">敬请期待</div>')+
|
||||
'</div>').join('')+
|
||||
'</div>'+
|
||||
'<button class="ml-alumni-entry ml-story-entry" onclick="navigateTo(\'story\')">'+
|
||||
'<span class="ml-alumni-entry-icon">⚔️</span>'+
|
||||
'<span class="ml-alumni-entry-body"><b>葫芦侠行</b><small>北宋数学武侠互动文字冒险 · 8章93节点 · 8+种结局</small></span>'+
|
||||
'<span class="ml-alumni-entry-arrow">→</span>'+
|
||||
'</button>'+
|
||||
'<button class="ml-alumni-entry" onclick="openMathAlumni()">'+
|
||||
'<span class="ml-alumni-entry-icon">🎓</span>'+
|
||||
'<span class="ml-alumni-entry-body"><b>数学校友访谈</b><small>真实校友的大学四年 · Skill 剧本包</small></span>'+
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>葫芦数学 · Hulu Math</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0%' stop-color='%23808C95'/><stop offset='100%' stop-color='%239AA5AE'/></linearGradient></defs><ellipse cx='32' cy='22' rx='14' ry='10' fill='url(%23g)'/><rect x='18' y='28' width='28' height='28' rx='14' fill='url(%23g)'/><text x='32' y='47' text-anchor='middle' font-size='20' font-weight='bold' fill='white' font-family='sans-serif'>葫</text></svg>">
|
||||
<link rel="stylesheet" href="/static/css/desktop.css?v=19">
|
||||
<link rel="stylesheet" href="/static/css/desktop.css?v=20">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -25,12 +25,8 @@
|
||||
<button class="nav-item active" data-page="home" onclick="navigateTo('home')"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/><path d="M9 21v-6h6v6"/></svg></span> 首页</button>
|
||||
<button class="nav-item" data-page="discover" onclick="navigateTo('discover')"><span class="nav-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="m15.5 8.5-2 5-5 2 2-5z"/></svg></span> 发现</button>
|
||||
<button class="nav-item" data-page="toolbox" onclick="navigateTo('toolbox')"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a4.5 4.5 0 0 0-6 6L3 18l3 3 5.7-5.7a4.5 4.5 0 0 0 6-6L14 13l-3-3z"/></svg></span> 工具箱</button>
|
||||
<button class="nav-item" data-page="knowledge" onclick="navigateTo('knowledge')"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M2 5h7a3 3 0 0 1 3 3v13a2.5 2.5 0 0 0-2.5-2.5H2z"/><path d="M22 5h-7a3 3 0 0 0-3 3v13a2.5 2.5 0 0 1 2.5-2.5H22z"/></svg></span> 知识卡片</button>
|
||||
<button class="nav-item" data-page="story" onclick="navigateTo('story')"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M4 4l11 11"/><path d="M4 4v4l3 3"/><path d="M20 4l-11 11"/><path d="M20 4v4l-3 3"/><path d="m5 15 4 4"/><path d="m19 15-4 4"/><path d="m3 21 3-3"/><path d="m21 21-3-3"/></svg></span> 葫芦侠行</button>
|
||||
<button class="nav-item" data-page="simulator" onclick="navigateTo('simulator')"><span class="nav-icon"><svg viewBox="0 0 24 24"><rect x="2" y="7" width="20" height="10" rx="5"/><path d="M7 11v2"/><path d="M6 12h2"/><circle cx="16.5" cy="11" r=".6"/><circle cx="18.5" cy="13" r=".6"/></svg></span> 数学人生</button>
|
||||
<button class="nav-item" data-page="favorites" onclick="navigateTo('favorites')"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M12 20.5C7 16.5 3 13.3 3 9.3 3 6.4 5.2 4.5 7.7 4.5c1.7 0 3.3.9 4.3 2.4 1-1.5 2.6-2.4 4.3-2.4 2.5 0 4.7 1.9 4.7 4.8 0 4-4 7.2-9 11.2z"/></svg></span> 我的收藏</button>
|
||||
<button class="nav-item" data-page="cards" onclick="navigateTo('cards')"><span class="nav-icon"><svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="3"/><path d="M8 10h8M8 14h5"/><circle cx="18" cy="9" r="2"/></svg></span> 我的卡册</button>
|
||||
<button class="nav-item" data-page="profile" onclick="navigateTo('profile')"><span class="nav-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21c1.5-3.5 4.5-5 8-5s6.5 1.5 8 5"/></svg></span> 个人中心</button>
|
||||
<button class="nav-item" data-page="simulator" onclick="navigateTo('simulator')"><span class="nav-icon"><svg viewBox="0 0 24 24"><rect x="2" y="7" width="20" height="10" rx="5"/><path d="M7 11v2"/><path d="M6 12h2"/><circle cx="16.5" cy="11" r=".6"/><circle cx="18.5" cy="13" r=".6"/></svg></span> 数学人生</button>
|
||||
<button class="nav-item" data-page="mine" onclick="navigateTo('mine')"><span class="nav-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21c1.5-3.5 4.5-5 8-5s6.5 1.5 8 5"/></svg></span> 我的</button>
|
||||
</nav>
|
||||
<div class="sidebar-footer"><div class="sidebar-avatar" id="sidebarAvatar" style="background:#5B6AF0">葫</div><div><div class="sidebar-user-name" id="sidebarName">游客用户</div><div class="sidebar-user-id">Hulu Math</div></div></div>
|
||||
</aside>
|
||||
@@ -135,6 +131,11 @@
|
||||
<div class="cards-collection-grid" id="cardsCollectionGrid"><div class="spinner"></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Mine (我的) -->
|
||||
<div class="page" id="page-mine">
|
||||
<div id="mineContent"><div class="spinner"></div></div>
|
||||
</div>
|
||||
|
||||
</div></main>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,7 +165,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/desktop.js?v=21"></script>
|
||||
<script src="/static/js/desktop.js?v=23"></script>
|
||||
|
||||
<!-- 抽卡弹窗 -->
|
||||
<div class="gacha-overlay" id="gachaOverlay" style="display:none" onclick="if(event.target===this)closeGacha()">
|
||||
|
||||
@@ -1,386 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>葫芦数学 · Hulu Math</title>
|
||||
<link rel="stylesheet" href="/static/css/style.min.css?v=10.0">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- =========================================================
|
||||
葫芦数学 · MVP v6.0 — 底部TabBar布局
|
||||
仿微信小程序风格:底部导航 + 顶部内容区
|
||||
========================================================= -->
|
||||
|
||||
<div class="app-container">
|
||||
|
||||
<!-- ====== 顶部状态栏 ====== -->
|
||||
<header class="top-bar" id="topBar">
|
||||
<div class="top-bar-brand">
|
||||
<span class="brand-icon-sm">葫</span>
|
||||
<span class="brand-text-sm">葫芦数学</span>
|
||||
</div>
|
||||
<div class="top-bar-actions">
|
||||
<button class="icon-btn" onclick="openChat()" title="AI问答">?</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ====== 内容区 ====== -->
|
||||
<main class="content-area">
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-header" id="searchHeader">
|
||||
<div class="search-bar-wrap">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input type="text" id="videoSearch" placeholder="搜索你想看的数学内容..." oninput="handleSearch()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子导航 (热门/推荐/专业分区) -->
|
||||
<div class="sub-nav" id="subNav">
|
||||
<button class="sub-nav-btn active" data-tab="recommend" onclick="switchSubTab('recommend')">推荐</button>
|
||||
<button class="sub-nav-btn" data-tab="hot" onclick="switchSubTab('hot')">热门</button>
|
||||
<button class="sub-nav-btn" data-tab="discipline" onclick="switchSubTab('discipline')">专业分区</button>
|
||||
</div>
|
||||
|
||||
<!-- 知识 / 我的 标题 -->
|
||||
<div class="section-title-bar" id="sectionTitleBar" style="display:none;">
|
||||
<h1 class="section-title" id="contentTitle">知识 · 数学发现</h1>
|
||||
</div>
|
||||
|
||||
<!-- 剧本广场(数学人生入口) -->
|
||||
<div class="script-square" id="scriptSquare" style="display:none;">
|
||||
<!-- 快捷继续 -->
|
||||
<div class="ss-continue" id="ssContinue" style="display:none;">
|
||||
<div class="ss-continue-card" id="ssContinueCard" onclick="">
|
||||
<div class="ss-continue-badge">▶ 继续冒险</div>
|
||||
<div class="ss-continue-info">
|
||||
<div class="ss-continue-title" id="ssContinueTitle"></div>
|
||||
<div class="ss-continue-sub" id="ssContinueSub"></div>
|
||||
</div>
|
||||
<div class="ss-continue-arrow">→</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 精选剧本大卡 -->
|
||||
<div class="ss-section-title">精选剧本</div>
|
||||
<div class="ss-hero-cards" id="ssHeroCards">
|
||||
<!-- JS 动态渲染 -->
|
||||
</div>
|
||||
|
||||
<!-- 即将上线小卡 -->
|
||||
<div class="ss-section-title">即将上线</div>
|
||||
<div class="ss-coming-cards" id="ssComingCards">
|
||||
<!-- JS 动态渲染 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 剧本内子导航(退出剧本用) -->
|
||||
<div class="life-sub-nav" id="lifeSubNav" style="display:none;">
|
||||
<button class="life-sub-btn" onclick="backToSquare()">← 剧本广场</button>
|
||||
<button class="life-sub-btn active" data-life="story" onclick="switchLifeTab('story')">📖 葫芦侠行</button>
|
||||
<button class="life-sub-btn" data-life="simulator" onclick="switchLifeTab('simulator')">🎓 专业模拟器</button>
|
||||
</div>
|
||||
|
||||
<!-- 模块简介 -->
|
||||
<div class="module-banner" id="moduleBanner">
|
||||
<span class="banner-icon">◎</span>
|
||||
<div class="banner-text">
|
||||
<div class="banner-title">发现值得一看的数学内容</div>
|
||||
<div class="banner-sub">从大学专业到现实问题,快速进入你感兴趣的主题</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 学科分类筛选 (仅在点击具体分区后显示) -->
|
||||
<div class="category-filter" id="categoryFilter" style="display: none;">
|
||||
<button class="cat-btn active" data-cat="all" onclick="filterCategory('all')">全部</button>
|
||||
</div>
|
||||
|
||||
<!-- 专业分区网格 (类似B站分区页) -->
|
||||
<div class="discipline-grid-container" id="disciplineGridContainer" style="display: none;">
|
||||
<div class="grid-section-title">全部分区</div>
|
||||
<div class="discipline-grid" id="disciplineGrid">
|
||||
<!-- JS 动态渲染图标 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中分区后的顶部导航 -->
|
||||
<div class="discipline-active-header" id="disciplineActiveHeader" style="display: none;">
|
||||
<button class="back-to-grid" onclick="backToDisciplineGrid()">
|
||||
<span class="back-icon">←</span>
|
||||
<span id="activeDisciplineName">专业名</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div class="loading" id="loadingArea">
|
||||
<div class="spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 视频列表(数人/数思用) -->
|
||||
<div class="video-list" id="videoList"></div>
|
||||
|
||||
<!-- ====== 数说专属:发现Feed ====== -->
|
||||
<!-- Hero大卡 -->
|
||||
<div class="shushuo-hero" id="shushuoHero" style="display:none;">
|
||||
<div class="hero-card">
|
||||
<div class="hero-badge">✦ 今日数学发现</div>
|
||||
<div class="hero-title" id="heroTitle">数学不是公式,是你理解世界的方式</div>
|
||||
<div class="hero-desc" id="heroDesc">每天一个数学概念,让思维变得更清晰</div>
|
||||
<div class="hero-stats">
|
||||
<span>📖 3个概念</span><span>⏱ 阅读约15分钟</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 横向滚动话题 -->
|
||||
<div class="shushuo-topics" id="shushuoTopics" style="display:none;">
|
||||
<div class="topics-scroll" id="topicsScroll">
|
||||
<!-- JS 动态渲染 -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- 信息流卡片 -->
|
||||
<div class="shushuo-feed" id="shushuoFeed" style="display:none;">
|
||||
<!-- JS 动态渲染 -->
|
||||
</div>
|
||||
|
||||
<!-- 数学人生故事容器 -->
|
||||
<div id="storyContainer" style="display:none;"></div>
|
||||
|
||||
<!-- 模拟器容器 -->
|
||||
<div id="simulatorContainer" style="display:none;"></div>
|
||||
|
||||
<!-- 我的页面 -->
|
||||
<div class="profile-page" id="profilePage" style="display:none;">
|
||||
<div class="profile-card" onclick="openAvatarPicker()">
|
||||
<div class="profile-main">
|
||||
<div class="profile-avatar" id="profileAvatar">葫</div>
|
||||
<div class="profile-meta">
|
||||
<div class="profile-name" id="profileName">游客用户</div>
|
||||
<div class="profile-id" id="profileId">ID: ---</div>
|
||||
<div class="profile-tags">
|
||||
<span>数学爱好者</span>
|
||||
<span>开放体验版</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-edit">编辑资料</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-stats">
|
||||
<div class="profile-stat" onclick="showHistory()">
|
||||
<div class="profile-stat-num">0</div>
|
||||
<div class="profile-stat-label">已看内容</div>
|
||||
</div>
|
||||
<div class="profile-stat" onclick="showFavorites()">
|
||||
<div class="profile-stat-num">0</div>
|
||||
<div class="profile-stat-label">收藏专题</div>
|
||||
</div>
|
||||
<div class="profile-stat">
|
||||
<div class="profile-stat-num">2</div>
|
||||
<div class="profile-stat-label">剧情存档</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<div class="profile-section-title">个人信息</div>
|
||||
<div class="profile-menu-card">
|
||||
<div class="profile-menu-item" onclick="editNickname()">
|
||||
<div class="profile-menu-left"><span class="menu-icon">◉</span><span>昵称与头像</span></div>
|
||||
<span class="menu-arrow">›</span>
|
||||
</div>
|
||||
<div class="profile-menu-item">
|
||||
<div class="profile-menu-left"><span class="menu-icon">◎</span><span>兴趣方向</span></div>
|
||||
<span class="menu-arrow">›</span>
|
||||
</div>
|
||||
<div class="profile-menu-item">
|
||||
<div class="profile-menu-left"><span class="menu-icon">◌</span><span>学习偏好</span></div>
|
||||
<span class="menu-arrow">›</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<div class="profile-section-title">我的内容</div>
|
||||
<div class="profile-menu-card">
|
||||
<div class="profile-menu-item" onclick="showFavorites()">
|
||||
<div class="profile-menu-left"><span class="menu-icon">◇</span><span>我的收藏</span></div>
|
||||
<span class="menu-arrow">›</span>
|
||||
</div>
|
||||
<div class="profile-menu-item" onclick="showHistory()">
|
||||
<div class="profile-menu-left"><span class="menu-icon">▣</span><span>观看记录</span></div>
|
||||
<span class="menu-arrow">›</span>
|
||||
</div>
|
||||
<div class="profile-menu-item">
|
||||
<div class="profile-menu-left"><span class="menu-icon">◆</span><span>数学人生存档</span></div>
|
||||
<span class="menu-arrow">›</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<div class="profile-section-title">更多</div>
|
||||
<div class="profile-menu-card">
|
||||
<div class="profile-menu-item">
|
||||
<div class="profile-menu-left"><span class="menu-icon">✦</span><span>设置与帮助</span></div>
|
||||
<span class="menu-arrow">›</span>
|
||||
</div>
|
||||
<div class="profile-menu-item">
|
||||
<div class="profile-menu-left"><span class="menu-icon">⊕</span><span>意见反馈</span></div>
|
||||
<span class="menu-arrow">›</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ====== 底部导航栏 TabBar ====== -->
|
||||
<nav class="bottom-tab-bar">
|
||||
<button class="tab-btn active" data-module="发现" onclick="switchModule('发现')">
|
||||
<span class="tab-icon tab-icon-discover"></span>
|
||||
<span class="tab-label">发现</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-module="知识" onclick="switchModule('知识')">
|
||||
<span class="tab-icon tab-icon-knowledge"></span>
|
||||
<span class="tab-label">知识</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-module="数学人生" onclick="switchModule('数学人生')">
|
||||
<span class="tab-icon tab-icon-life"></span>
|
||||
<span class="tab-label">数学人生</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-module="我的" onclick="switchModule('我的')">
|
||||
<span class="tab-icon tab-icon-mine"></span>
|
||||
<span class="tab-label">我的</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- ====== AI 聊天弹出层 ====== -->
|
||||
<div class="chat-overlay" id="chatOverlay">
|
||||
<div class="chat-panel">
|
||||
<div class="chat-header">
|
||||
<div class="chat-header-left">
|
||||
<div class="ai-avatar">?</div>
|
||||
<span>AI 数学助教</span>
|
||||
</div>
|
||||
<button class="chat-close" onclick="closeChat()">✕</button>
|
||||
</div>
|
||||
<div class="chat-messages" id="chatMessages">
|
||||
<div class="chat-msg assistant">
|
||||
<div class="msg-bubble">
|
||||
你好!我是数学桥梁计划的 AI 助教<br><br>
|
||||
<b>试试问我:</b><br>
|
||||
· 学数学对学医有什么用?<br>
|
||||
· 数学在建筑里怎么用?<br>
|
||||
· 为什么学数学很重要?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-asks">
|
||||
<button class="quick-ask" onclick="quickAsk('学数学对学医有什么用?')">数学与医学</button>
|
||||
<button class="quick-ask" onclick="quickAsk('数学在建筑里怎么用?')">数学与建筑</button>
|
||||
<button class="quick-ask" onclick="quickAsk('AI和数学有什么关系?')">数学与AI</button>
|
||||
<button class="quick-ask" onclick="quickAsk('为什么学数学很重要?')">为什么要学</button>
|
||||
</div>
|
||||
<div class="chat-input-area">
|
||||
<input type="text" id="chatInput" placeholder="输入你的问题..." onkeydown="if(event.key==='Enter')sendMessage()">
|
||||
<button class="send-btn" onclick="sendMessage()">➤</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ====== 视频弹窗 ====== -->
|
||||
<div class="video-modal" id="videoModal">
|
||||
<div class="video-modal-content">
|
||||
<div class="video-modal-header">
|
||||
<h3 id="videoModalTitle">视频标题</h3>
|
||||
<button onclick="closeVideo()">✕</button>
|
||||
</div>
|
||||
<div class="video-player-area">
|
||||
<div class="video-placeholder">
|
||||
<div class="play-icon">▶</div>
|
||||
<p id="videoPlaceholderText">点击下方按钮在浏览器中查看完整课程</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<p class="video-desc" id="videoModalDesc"></p>
|
||||
<div class="video-meta">
|
||||
<span id="videoModalAuthor">--</span>
|
||||
<span id="videoModalCat">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-actions">
|
||||
<button class="btn-secondary" onclick="toggleVideoFav()" id="videoModalFav">☆ 收藏</button>
|
||||
<button class="btn-secondary" onclick="closeVideo();openChat();quickAsk(document.getElementById('videoModalTitle').textContent+' 这个视频讲了什么?')">? 问AI</button>
|
||||
<button class="btn-primary" id="videoLinkBtn" onclick="openVideoLink()">查看原视频</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ====== 个人中心列表面板 ====== -->
|
||||
<div class="profile-list-panel" id="profileListPanel" style="display:none;">
|
||||
<div class="plp-header">
|
||||
<button class="plp-back" onclick="hideProfileListPanel()">← 返回</button>
|
||||
<span class="plp-title" id="profileListTitle">列表</span>
|
||||
</div>
|
||||
<div class="plp-content" id="profileListContent"></div>
|
||||
</div>
|
||||
|
||||
<!-- ====== 头像选择器 ====== -->
|
||||
<div class="avatar-picker-overlay" id="avatarPickerOverlay" onclick="closeAvatarPicker(event)">
|
||||
<div class="avatar-picker-card" onclick="event.stopPropagation()">
|
||||
<div class="picker-title">选择头像</div>
|
||||
<div class="picker-emoji-grid" id="pickerEmojiGrid"></div>
|
||||
<div class="picker-color-row" id="pickerColorRow"></div>
|
||||
<div class="picker-actions">
|
||||
<button class="picker-btn" onclick="editNickname()">✏️ 改昵称</button>
|
||||
<button class="picker-btn primary" onclick="closeAvatarPicker()">完成</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ====== 模拟器结局面板 ====== -->
|
||||
<div class="ending-overlay" id="endingOverlay" onclick="closeEnding()">
|
||||
<div class="ending-card" onclick="event.stopPropagation()">
|
||||
<div class="ending-badge" id="endingBadge">🎓</div>
|
||||
<h3 class="ending-type" id="endingType">结局</h3>
|
||||
<p class="ending-title" id="endingTitle"></p>
|
||||
<div class="ending-stats" id="endingStats"></div>
|
||||
<p class="ending-scene" id="endingScene"></p>
|
||||
<button class="picker-btn primary" onclick="closeEnding();resetSimulator()">重新开始</button>
|
||||
<button class="picker-btn" onclick="closeEnding()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ====== 存档面板 ====== -->
|
||||
<div class="save-panel-overlay" id="savePanelOverlay" onclick="closeSavePanel()">
|
||||
<div class="save-panel" onclick="event.stopPropagation()">
|
||||
<div class="save-panel-header">
|
||||
<button class="sp-back" onclick="closeSavePanel()">← 返回</button>
|
||||
<span class="sp-title">存档管理</span>
|
||||
</div>
|
||||
<div class="save-slots" id="saveSlots">
|
||||
<!-- JS 动态渲染 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ====== 结局画廊 ====== -->
|
||||
<div class="gallery-overlay" id="galleryOverlay" onclick="closeGallery()">
|
||||
<div class="gallery-panel" onclick="event.stopPropagation()">
|
||||
<div class="gallery-header">
|
||||
<button class="sp-back" onclick="closeGallery()">← 返回</button>
|
||||
<span class="sp-title">结局画廊</span>
|
||||
<span class="gallery-progress" id="galleryProgress"></span>
|
||||
</div>
|
||||
<div class="gallery-grid" id="galleryGrid">
|
||||
<!-- JS 动态渲染 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.min.js?v=10.1"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{ "source": "/", "destination": "/templates/index.html" },
|
||||
{ "source": "/", "destination": "/templates/desktop.html" },
|
||||
{ "source": "/mathbti", "destination": "/templates/mathbti.html" },
|
||||
{ "source": "/web", "destination": "/templates/desktop.html" },
|
||||
{ "source": "/desktop", "destination": "/templates/desktop.html" }
|
||||
],
|
||||
"headers": [
|
||||
|
||||
Reference in New Issue
Block a user