@code2rich/jpage 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +68 -0
- package/.dockerignore +8 -0
- package/.env.example +56 -0
- package/.github/workflows/ci.yml +43 -0
- package/CLAUDE.md +280 -0
- package/Dockerfile +44 -0
- package/LICENSE +21 -0
- package/README.md +433 -0
- package/README_EN.md +399 -0
- package/bin/args.js +64 -0
- package/bin/client.js +93 -0
- package/bin/commands/_shared.js +54 -0
- package/bin/commands/cat.js +23 -0
- package/bin/commands/ls.js +44 -0
- package/bin/commands/mv.js +20 -0
- package/bin/commands/rm.js +22 -0
- package/bin/commands/skills.js +70 -0
- package/bin/commands/star.js +23 -0
- package/bin/commands/tags.js +97 -0
- package/bin/commands/upload.js +84 -0
- package/bin/commands/url.js +25 -0
- package/bin/commands/whoami.js +29 -0
- package/bin/config.js +85 -0
- package/bin/jpage.js +168 -0
- package/build.js +112 -0
- package/docker-compose.yml +26 -0
- package/docs/api.md +438 -0
- package/docs/design/005-custom-modal.md +296 -0
- package/docs/design/013-file-version-history.md +324 -0
- package/docs/design/billing-system.md +600 -0
- package/docs/design/db-index-and-healthcheck.md +176 -0
- package/docs/design/loading-states.md +209 -0
- package/docs/virtual-hosting-feasibility.md +453 -0
- package/eslint.config.mjs +172 -0
- package/lib/auth-state.js +15 -0
- package/lib/categories.js +20 -0
- package/lib/crypto.js +85 -0
- package/lib/csp.js +66 -0
- package/lib/db.js +53 -0
- package/lib/dispatch.js +103 -0
- package/lib/fts.js +81 -0
- package/lib/middleware/auth.js +114 -0
- package/lib/middleware/files.js +42 -0
- package/lib/paths.js +9 -0
- package/lib/render-cache.js +48 -0
- package/lib/render.js +157 -0
- package/lib/templates.js +149 -0
- package/lib/util.js +66 -0
- package/lib/view-counts.js +59 -0
- package/lib/zip.js +192 -0
- package/logger.js +16 -0
- package/mailer.js +34 -0
- package/mcp/constants.js +16 -0
- package/mcp/resources.js +74 -0
- package/mcp/server.js +43 -0
- package/mcp/tools-categories.js +56 -0
- package/mcp/tools-content-templates.js +59 -0
- package/mcp/tools-files.js +245 -0
- package/mcp/tools-tags.js +41 -0
- package/mcp/tools-versions.js +57 -0
- package/mcp/transport.js +183 -0
- package/mcp/util.js +63 -0
- package/mcp-server.js +20 -0
- package/migrations/001_init_schema.js +25 -0
- package/migrations/002_add_share_key.js +33 -0
- package/migrations/003_add_roles_and_tokens.js +28 -0
- package/migrations/004_add_version_history.js +32 -0
- package/migrations/005_tags_starred_categories.js +49 -0
- package/migrations/006_zip_bundle.js +17 -0
- package/migrations/007_add_file_type_uploaded_by_indexes.js +7 -0
- package/migrations/008_add_fts5.js +6 -0
- package/migrations/009_add_link_visits.js +20 -0
- package/migrations/010_add_templates_system.js +34 -0
- package/migrations/011_content_templates.js +233 -0
- package/migrations/012_add_email_and_verification.js +35 -0
- package/migrations/013_add_token_encrypted.js +14 -0
- package/migrations.js +65 -0
- package/package.json +63 -0
- package/public/css/style.css +2915 -0
- package/public/index.html +855 -0
- package/public/js/api.js +22 -0
- package/public/js/app.js +94 -0
- package/public/js/components/dialog.js +106 -0
- package/public/js/components/toast.js +13 -0
- package/public/js/pages/content-templates.js +330 -0
- package/public/js/pages/home.js +1903 -0
- package/public/js/pages/landing.js +158 -0
- package/public/js/pages/login.js +175 -0
- package/public/js/pages/preview.js +713 -0
- package/public/js/theme.js +44 -0
- package/public/js/utils.js +67 -0
- package/routes/admin.js +136 -0
- package/routes/auth.js +365 -0
- package/routes/categories.js +90 -0
- package/routes/content-templates.js +215 -0
- package/routes/files/_shared.js +112 -0
- package/routes/files/associations.js +94 -0
- package/routes/files/crud.js +139 -0
- package/routes/files/detail-serve.js +178 -0
- package/routes/files/index.js +38 -0
- package/routes/files/list.js +200 -0
- package/routes/files/overwrite.js +114 -0
- package/routes/files/upload.js +204 -0
- package/routes/files/versions.js +166 -0
- package/routes/files.js +16 -0
- package/routes/skills.js +93 -0
- package/routes/tags.js +65 -0
- package/routes/tokens.js +110 -0
- package/routes/users.js +120 -0
- package/server.js +372 -0
- package/skills/jpage-content-template/SKILL.md +98 -0
- package/skills/jpage-upload/SKILL.md +247 -0
- package/skills-registry.js +135 -0
- package/templates/academic.html +41 -0
- package/templates/dark-pro.html +41 -0
- package/templates/default.html +56 -0
- package/templates/github.html +67 -0
- package/test/browser-harness.js +125 -0
- package/test/dispatch-bench.js +74 -0
- package/test/helpers/setup.js +45 -0
- package/test/integration/admin.test.js +108 -0
- package/test/integration/auth.test.js +93 -0
- package/test/integration/categories.test.js +103 -0
- package/test/integration/cli.test.js +310 -0
- package/test/integration/content-templates.test.js +147 -0
- package/test/integration/files-security.test.js +248 -0
- package/test/integration/files.test.js +139 -0
- package/test/integration/share.test.js +79 -0
- package/test/integration/skills.test.js +104 -0
- package/test/integration/tags.test.js +84 -0
- package/test/integration/tokens.test.js +89 -0
- package/test/integration/users.test.js +138 -0
- package/test/mcp-harness.js +152 -0
- package/test/perf-bench.js +108 -0
- package/test/perf-harness.js +198 -0
- package/test/run-server.sh +15 -0
- package/test/unit/cli-args.test.js +88 -0
- package/test/unit/cli-config.test.js +89 -0
- package/test/unit/crypto.test.js +100 -0
- package/test/unit/fts.test.js +52 -0
- package/test/unit/render-cache.test.js +76 -0
- package/test/unit/util.test.js +81 -0
- package/test/unit/zip.test.js +164 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# 设计文档:DB 索引 + Docker HEALTHCHECK
|
|
2
|
+
|
|
3
|
+
> 对应分析报告问题 #32、#34
|
|
4
|
+
> 状态:待实现
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 一、背景
|
|
9
|
+
|
|
10
|
+
### 当前问题
|
|
11
|
+
|
|
12
|
+
**#32 SQLite 无索引**
|
|
13
|
+
|
|
14
|
+
`files` 表除主键 `id` 外没有任何索引。主要查询路径:
|
|
15
|
+
|
|
16
|
+
| 查询 | 位置 | 频率 | 当前性能 |
|
|
17
|
+
|---|---|---|---|
|
|
18
|
+
| `SELECT ... FROM files ORDER BY created_at DESC` | 列表接口 | 每次加载首页 | 全表排序 |
|
|
19
|
+
| `SELECT ... FROM files WHERE id = ?` | 单文件操作 | 高频 | ✅ 主键已覆盖 |
|
|
20
|
+
| `SELECT ... FROM users WHERE username = ?` | 登录 | 低频 | ✅ UNIQUE 约束已隐式索引 |
|
|
21
|
+
|
|
22
|
+
未来多用户(P0)上线后,还会增加 `WHERE uploaded_by = ?` 和 `WHERE is_public = 1` 的过滤查询。文件数超过几百条后,全表排序的开销会变得可感知。
|
|
23
|
+
|
|
24
|
+
**#34 Docker 无 HEALTHCHECK**
|
|
25
|
+
|
|
26
|
+
Dockerfile 没有定义 `HEALTHCHECK`,容器编排环境(Docker Compose、K8s、Swarm)无法自动判断服务是否健康。`restart: unless-stopped` 只能处理进程崩溃,无法检测「进程在但服务卡死」的情况。
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 二、设计方案
|
|
31
|
+
|
|
32
|
+
### 2.1 DB 索引
|
|
33
|
+
|
|
34
|
+
#### 新增索引
|
|
35
|
+
|
|
36
|
+
在 `db.serialize()` 块中,建表之后、`PRAGMA table_info` 之前,用 `CREATE INDEX IF NOT EXISTS` 添加:
|
|
37
|
+
|
|
38
|
+
```sql
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(created_at DESC);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**理由**:这是当前唯一的排序查询,也是频率最高的查询。`DESC` 声明让 SQLite 可以直接按索引顺序返回,避免 filesort。
|
|
43
|
+
|
|
44
|
+
**暂不添加的索引及原因**:
|
|
45
|
+
|
|
46
|
+
| 字段 | 是否添加 | 理由 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `uploaded_by` | ❌ 暂不 | 当前单用户,区分度为 0,索引无收益。多用户功能实现时再加 |
|
|
49
|
+
| `file_type` | ❌ 暂不 | 区分度极低(只有 html / markdown 两种),索引收益可忽略。搜索筛选功能实现时再评估 |
|
|
50
|
+
| `is_public` | ❌ 暂不 | 当前列表接口不过滤此字段。隐私过滤在应用层做(单条查询后判断) |
|
|
51
|
+
| `(is_public, uploaded_by)` | ❌ 暂不 | 预留给多用户 + 三态权限模型,当前无查询命中 |
|
|
52
|
+
|
|
53
|
+
**扩展策略**:后续功能(搜索、多用户)引入新查询模式时,在对应的 `db.serialize()` 迁移块中按需追加索引,与当前 `ALTER TABLE ADD COLUMN` 的模式一致。
|
|
54
|
+
|
|
55
|
+
#### 实现位置
|
|
56
|
+
|
|
57
|
+
`server.js` 第 90–117 行的 `db.serialize()` 块内,在两个 `CREATE TABLE` 之后、`PRAGMA table_info` 之前插入一行 `CREATE INDEX`:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
db.serialize(() => {
|
|
61
|
+
// CREATE TABLE files ...
|
|
62
|
+
// CREATE TABLE users ...
|
|
63
|
+
|
|
64
|
+
db.run('CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(created_at DESC)'); // ← 新增
|
|
65
|
+
|
|
66
|
+
// PRAGMA table_info ... (已有迁移逻辑)
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### SQLite 注意事项
|
|
71
|
+
|
|
72
|
+
- `CREATE INDEX IF NOT EXISTS` 是幂等操作,重复执行不报错,不锁表(SQLite DDL 在 WAL 模式下不阻塞读)
|
|
73
|
+
- 数据量小时(< 1000 行)索引对写入有极微小的额外开销(每次 INSERT/UPDATE 需维护 B-tree),对读取提速也不明显。但提前加好是无成本的未来保障
|
|
74
|
+
- 不需要修改 WAL 模式——当前 SQLite 默认日志模式即可,高频写入场景是远期问题
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### 2.2 Docker HEALTHCHECK
|
|
79
|
+
|
|
80
|
+
#### 方案选择
|
|
81
|
+
|
|
82
|
+
| 方案 | 优点 | 缺点 |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `curl -f http://localhost:8858/api/auth/me` | 最标准 | alpine 无 curl,需额外安装 |
|
|
85
|
+
| `wget -q --spider http://localhost:8858/` | alpine 自带 wget | `--spider` 行为在 busybox wget 中不标准,可能误判 |
|
|
86
|
+
| `node -e "fetch('http://localhost:8858/').then(r => process.exit(r.ok?0:1))"` | 零依赖,Node 20 原生 fetch | 每次启动 Node 进程,开销稍大 |
|
|
87
|
+
| `wget -qO /dev/null http://localhost:8858/` | alpine 自带,可靠 | 会下载完整响应体(首页 HTML) |
|
|
88
|
+
|
|
89
|
+
**选择 `wget` 方案**:alpine 镜像自带 busybox wget,不增加镜像体积。用 `-qO /dev/null` 丢弃响应体,配合退出码判断健康状态。
|
|
90
|
+
|
|
91
|
+
#### 健康检查端点
|
|
92
|
+
|
|
93
|
+
使用 `/` (首页)而非 `/api/auth/me`:
|
|
94
|
+
- `/` 始终返回 200(无需鉴权)
|
|
95
|
+
- `/api/auth/me` 未登录返回 401,语义上是"正常响应"但会被 HEALTHCHECK 误判为不健康
|
|
96
|
+
- 如需更精确的检查,可后续添加 `/api/health` 专用端点,但当前无必要
|
|
97
|
+
|
|
98
|
+
#### Dockerfile 改动
|
|
99
|
+
|
|
100
|
+
在 `EXPOSE 8858` 之后、`CMD` 之前添加:
|
|
101
|
+
|
|
102
|
+
```dockerfile
|
|
103
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
104
|
+
CMD wget -qO /dev/null http://localhost:8858/ || exit 1
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
参数说明:
|
|
108
|
+
|
|
109
|
+
| 参数 | 值 | 说明 |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `--interval` | 30s | 每 30 秒检查一次,平衡响应速度和资源消耗 |
|
|
112
|
+
| `--timeout` | 3s | 单次请求超时,超时视为不健康 |
|
|
113
|
+
| `--start-period` | 5s | 容器启动后 5 秒内的失败不计入重试次数,给 Node 启动留时间 |
|
|
114
|
+
| `--retries` | 3 | 连续 3 次失败才标记为 unhealthy,避免偶发抖动 |
|
|
115
|
+
|
|
116
|
+
#### docker-compose.yml 联动
|
|
117
|
+
|
|
118
|
+
`docker-compose.yml` 无需改动——Dockerfile 中定义的 `HEALTHCHECK` 会被 Compose 自动识别。`docker ps` 会显示健康状态,`restart: unless-stopped` 不会因 unhealthy 自动重启(这是 Docker 引擎的默认行为,需要手动配置或使用 `restart: on-failure` + `healthcheck` 回调)。
|
|
119
|
+
|
|
120
|
+
如需在服务不健康时自动重启,可在 `docker-compose.yml` 中添加:
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
healthcheck:
|
|
124
|
+
# 覆盖 Dockerfile 中的配置(可选,通常不需要)
|
|
125
|
+
test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:8858/"]
|
|
126
|
+
interval: 30s
|
|
127
|
+
timeout: 3s
|
|
128
|
+
retries: 3
|
|
129
|
+
start_period: 5s
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
当前建议**不覆盖**,使用 Dockerfile 中的默认配置即可。
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 三、变更范围
|
|
137
|
+
|
|
138
|
+
| 文件 | 变更内容 |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `server.js` | `db.serialize()` 块内新增一行 `CREATE INDEX` |
|
|
141
|
+
| `Dockerfile` | `EXPOSE` 和 `CMD` 之间新增 `HEALTHCHECK` 指令 |
|
|
142
|
+
|
|
143
|
+
两个改动完全独立,可同时实施。
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 四、验证方法
|
|
148
|
+
|
|
149
|
+
### 索引验证
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# 启动服务后
|
|
153
|
+
sqlite3 data/database.sqlite ".indices files"
|
|
154
|
+
# 预期输出包含:idx_files_created_at
|
|
155
|
+
|
|
156
|
+
# 查看查询计划
|
|
157
|
+
sqlite3 data/database.sqlite "EXPLAIN QUERY PLAN SELECT * FROM files ORDER BY created_at DESC"
|
|
158
|
+
# 预期输出包含:USING INDEX idx_files_created_at
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### HEALTHCHECK 验证
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# 构建并启动
|
|
165
|
+
docker-compose up -d --build
|
|
166
|
+
|
|
167
|
+
# 查看健康状态(等待 30 秒后)
|
|
168
|
+
docker inspect --format='{{.State.Health.Status}}' jpage
|
|
169
|
+
# 预期输出:healthy
|
|
170
|
+
|
|
171
|
+
# 模拟服务异常,观察状态变化
|
|
172
|
+
docker exec jpage sh -c 'mv server.js server.js.bak && kill 1'
|
|
173
|
+
# 等待约 90 秒(3 × 30s retries)后再次查看
|
|
174
|
+
docker inspect --format='{{.State.Health.Status}}' jpage
|
|
175
|
+
# 预期输出:unhealthy
|
|
176
|
+
```
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# 补齐加载状态(骨架屏/进度条/spinner)
|
|
2
|
+
|
|
3
|
+
## 背景
|
|
4
|
+
|
|
5
|
+
当前前端为纯 vanilla JS,无框架无构建。部分场景有基础的加载提示(如文件列表显示"正在加载…"),但体验粗糙且不统一。需要在所有异步操作中补齐加载状态,统一视觉风格,提升用户感知。
|
|
6
|
+
|
|
7
|
+
## 现状分析
|
|
8
|
+
|
|
9
|
+
| 场景 | 当前行为 | 问题 |
|
|
10
|
+
|------|---------|------|
|
|
11
|
+
| 文件列表加载 | `list.textContent = '正在加载…'` 纯文字 | 无视觉节奏感,与卡片列表布局差距大 |
|
|
12
|
+
| Skills 列表加载 | 同上,纯文字 | 同上 |
|
|
13
|
+
| 文件上传 | 无进度反馈,仅禁用上传区 | 大文件上传时用户无感知 |
|
|
14
|
+
| 预览页 iframe 加载 | 无任何提示,iframe 空白 | 用户看到长时间白屏 |
|
|
15
|
+
| 预览页源码加载 | 无提示,直接填充 | 极端情况短暂空白 |
|
|
16
|
+
| 登录提交 | `submit.textContent = '登录中…'` | 文字替换,可接受 |
|
|
17
|
+
| 删除/重命名确认后 | 按钮 `disabled`,无额外反馈 | 可接受 |
|
|
18
|
+
| Skill 详情弹窗 | 先弹窗再加载内容,有闪烁 | 内容区域无加载占位 |
|
|
19
|
+
| MCP 配置弹窗 | `statusEl.innerHTML = '加载中…'` 纯文字 | 与弹窗风格不搭 |
|
|
20
|
+
| 页面初始化(`fetchCurrentUser`) | 无任何提示 | 白屏直到路由完成 |
|
|
21
|
+
|
|
22
|
+
## 设计方案
|
|
23
|
+
|
|
24
|
+
### 统一加载组件
|
|
25
|
+
|
|
26
|
+
新增 3 个可复用的 CSS 组件,全部纯 CSS 实现(无额外 JS 依赖):
|
|
27
|
+
|
|
28
|
+
#### 1. 骨架屏(Skeleton)
|
|
29
|
+
|
|
30
|
+
用于**列表类内容**的加载占位,模拟最终布局形状。
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<!-- 文件列表骨架示例 -->
|
|
34
|
+
<div class="skeleton skeleton-card" aria-hidden="true">
|
|
35
|
+
<div class="skeleton-avatar"></div>
|
|
36
|
+
<div class="skeleton-lines">
|
|
37
|
+
<div class="skeleton-line skeleton-line-lg"></div>
|
|
38
|
+
<div class="skeleton-line skeleton-line-sm"></div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```css
|
|
44
|
+
/* 基础 */
|
|
45
|
+
.skeleton {
|
|
46
|
+
background: var(--border);
|
|
47
|
+
border-radius: var(--radius-sm);
|
|
48
|
+
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
|
49
|
+
}
|
|
50
|
+
@keyframes skeleton-pulse {
|
|
51
|
+
0%, 100% { opacity: 0.4; }
|
|
52
|
+
50% { opacity: 1; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* 文件卡片骨架 */
|
|
56
|
+
.skeleton-card {
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: 12px;
|
|
60
|
+
padding: 12px;
|
|
61
|
+
}
|
|
62
|
+
.skeleton-avatar {
|
|
63
|
+
width: 40px; height: 40px;
|
|
64
|
+
border-radius: var(--radius-sm);
|
|
65
|
+
}
|
|
66
|
+
.skeleton-lines { flex: 1; display: flex; flex-direction: column; gap: 8px; }
|
|
67
|
+
.skeleton-line { height: 12px; width: 100%; }
|
|
68
|
+
.skeleton-line-lg { width: 60%; }
|
|
69
|
+
.skeleton-line-sm { width: 40%; }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### 2. 条形进度条(Progress Bar)
|
|
73
|
+
|
|
74
|
+
用于**文件上传**等有明确进度的操作。HTML 中已有 `#upload-progress` 容器,补充 CSS 动画即可。
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
.upload-progress {
|
|
78
|
+
height: 4px;
|
|
79
|
+
background: var(--border);
|
|
80
|
+
border-radius: 2px;
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
position: relative;
|
|
83
|
+
}
|
|
84
|
+
.upload-progress-bar {
|
|
85
|
+
height: 100%;
|
|
86
|
+
background: var(--primary);
|
|
87
|
+
border-radius: 2px;
|
|
88
|
+
transition: width 0.2s ease;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
JS 侧使用 `XMLHttpRequest` 替代当前 `fetch` 上传,监听 `progress` 事件获取百分比。
|
|
93
|
+
|
|
94
|
+
#### 3. Spinner(旋转指示器)
|
|
95
|
+
|
|
96
|
+
用于**页面级加载**和**弹窗内容加载**。
|
|
97
|
+
|
|
98
|
+
```html
|
|
99
|
+
<div class="spinner-container" role="status" aria-label="加载中">
|
|
100
|
+
<div class="spinner"></div>
|
|
101
|
+
</div>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```css
|
|
105
|
+
.spinner {
|
|
106
|
+
width: 28px; height: 28px;
|
|
107
|
+
border: 3px solid var(--border);
|
|
108
|
+
border-top-color: var(--primary);
|
|
109
|
+
border-radius: 50%;
|
|
110
|
+
animation: spin 0.7s linear infinite;
|
|
111
|
+
}
|
|
112
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
113
|
+
.spinner-container {
|
|
114
|
+
display: flex;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
align-items: center;
|
|
117
|
+
padding: 32px;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 各场景实施方案
|
|
122
|
+
|
|
123
|
+
#### S1. 文件列表加载 → 骨架屏
|
|
124
|
+
|
|
125
|
+
**改动文件**: `app.js` → `loadFiles()`
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
- list.textContent = '正在加载…';
|
|
129
|
+
+ list.innerHTML = buildSkeletonCards(5); // 生成 5 个骨架卡片
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`buildSkeletonCards(n)` 返回 n 个 `.skeleton-card` 的 HTML 字符串。数据返回后替换为真实卡片。
|
|
133
|
+
|
|
134
|
+
#### S2. Skills 列表加载 → 骨架屏
|
|
135
|
+
|
|
136
|
+
**改动文件**: `app.js` → `loadSkillsForModal()`
|
|
137
|
+
|
|
138
|
+
同样使用 `buildSkeletonCards(3)` 替换纯文字。
|
|
139
|
+
|
|
140
|
+
#### S3. 文件上传 → 进度条
|
|
141
|
+
|
|
142
|
+
**改动文件**: `app.js` → `uploadFile()`
|
|
143
|
+
|
|
144
|
+
- 将 `fetch` 替换为 `XMLHttpRequest`
|
|
145
|
+
- 监听 `xhr.upload.onprogress`,计算百分比更新 `#upload-progress-bar` 宽度和 `#upload-progress-text`
|
|
146
|
+
- 上传开始时 `display: block` 显示进度条,完成后隐藏
|
|
147
|
+
- 进度条 HTML 已存在于 `index.html:68-71`
|
|
148
|
+
|
|
149
|
+
#### S4. 预览页 iframe 加载 → Spinner 覆盖层
|
|
150
|
+
|
|
151
|
+
**改动文件**: `index.html`(preview-template 内)、`app.js` → `renderPreview()`、`style.css`
|
|
152
|
+
|
|
153
|
+
在 `.preview-container` 内新增 spinner 覆盖层:
|
|
154
|
+
|
|
155
|
+
```html
|
|
156
|
+
<div class="preview-loading" id="preview-loading">
|
|
157
|
+
<div class="spinner"></div>
|
|
158
|
+
<p>正在加载预览…</p>
|
|
159
|
+
</div>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
CSS 使其绝对定位覆盖 iframe 区域。iframe `load` 事件触发后移除覆盖层。
|
|
163
|
+
|
|
164
|
+
#### S5. Skill 详情弹窗 → Spinner
|
|
165
|
+
|
|
166
|
+
**改动文件**: `app.js` → `openSkillModal()`
|
|
167
|
+
|
|
168
|
+
弹窗 body 区域先渲染 spinner,数据返回后替换为真实内容。解决当前"先弹窗后加载"的闪烁问题。
|
|
169
|
+
|
|
170
|
+
#### S6. MCP 配置弹窗 → Spinner
|
|
171
|
+
|
|
172
|
+
**改动文件**: `app.js` → `openMcpConfigModal()`
|
|
173
|
+
|
|
174
|
+
`statusEl` 初始内容改为 spinner + "加载中…",替代纯文字。
|
|
175
|
+
|
|
176
|
+
#### S7. 页面初始化 → 全屏 Spinner(可选)
|
|
177
|
+
|
|
178
|
+
**改动文件**: `index.html`、`app.js`
|
|
179
|
+
|
|
180
|
+
在 `#app` 内放置初始 spinner,`fetchCurrentUser` + `route()` 完成后替换。优先级低(通常 < 200ms),可视情况省略。
|
|
181
|
+
|
|
182
|
+
### 不改动的场景
|
|
183
|
+
|
|
184
|
+
| 场景 | 原因 |
|
|
185
|
+
|------|------|
|
|
186
|
+
| 登录按钮 `登录中…` | 文字替换已足够,按钮有 disabled 状态 |
|
|
187
|
+
| 删除/重命名确认后 | 按钮 disabled + toast 提示已足够 |
|
|
188
|
+
| 预览页源码加载 | 与 iframe 加载同步,不会单独白屏 |
|
|
189
|
+
|
|
190
|
+
## CSS 深色模式适配
|
|
191
|
+
|
|
192
|
+
骨架屏和 spinner 使用 CSS 变量(`--border`、`--primary`),深色模式下自动适配,无需额外 media query。进度条同理。
|
|
193
|
+
|
|
194
|
+
## 无障碍
|
|
195
|
+
|
|
196
|
+
- 骨架屏标记 `aria-hidden="true"`(装饰性)
|
|
197
|
+
- Spinner 容器标记 `role="status"` + `aria-label="加载中"`
|
|
198
|
+
- 文件列表保持 `aria-busy="true"` 属性
|
|
199
|
+
- 进度条使用 `aria-valuenow`、`aria-valuemin`、`aria-valuemax`
|
|
200
|
+
|
|
201
|
+
## 改动范围汇总
|
|
202
|
+
|
|
203
|
+
| 文件 | 改动内容 |
|
|
204
|
+
|------|---------|
|
|
205
|
+
| `public/css/style.css` | 新增 skeleton / spinner / progress bar 样式(~60 行) |
|
|
206
|
+
| `public/js/app.js` | `loadFiles`、`loadSkillsForModal`、`uploadFile`、`renderPreview`、`openSkillModal`、`openMcpConfigModal` 中替换加载逻辑(~80 行改动) |
|
|
207
|
+
| `public/index.html` | preview-template 内新增 `#preview-loading` 覆盖层(~4 行) |
|
|
208
|
+
|
|
209
|
+
无新增依赖,无构建步骤变更,不影响后端。
|