Skip to content

Commit ad55738

Browse files
FXFX
authored andcommitted
Polish UI and docs for production readiness.
Make detail panel on-demand (toggle row, close button, Esc), add site favicons, and refresh README for public GHCR image plus optional panel password.
1 parent b08bb47 commit ad55738

9 files changed

Lines changed: 165 additions & 69 deletions

File tree

README.md

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,38 @@
44

55
## 功能
66

7-
- GitHub 公开仓库:正式版 + 预发布;按扩展名 / 系统 / 架构别名 / 包含排除筛选附件
8-
- 单篇文章:标题变化则整页重抓,提取百度 / 阿里 / 夸克 / 123 / 天翼 / 蓝奏
9-
- 只保留当前最新状态
7+
- **GitHub 公开仓库**:正式版 + 预发布;按扩展名 / 系统 / 架构别名 / 包含·排除筛选附件
8+
- **单篇文章**:标题变化则整页重抓,提取百度 / 阿里 / 夸克 / 123 / 天翼 / 蓝奏(链接、提取码、标题备注)
9+
- 只保留当前最新状态(有变化才推送;首次检查不刷屏)
1010
- 固定间隔检查(默认 6 小时,可在设置里改)+ 立即检查
1111
- 可停用不删除
12-
- Docker 一键部署,无登录
12+
- 可选面板访问密码
13+
- 详情按需展开(点击列表行查看,再点同一行或 × / Esc 收起)
14+
- Docker 一键部署
1315

1416
## 上线部署(推荐)
1517

18+
### 方式 A:拉取公开镜像(最快)
19+
20+
镜像已公开,无需登录即可拉取:
21+
22+
```bash
23+
mkdir -p data
24+
docker run -d --name appupdate \
25+
-p 8000:8000 \
26+
-v "$PWD/data:/data" \
27+
--restart unless-stopped \
28+
ghcr.io/fatelightx/appupdate:latest
29+
```
30+
31+
或使用 compose,编辑 `docker-compose.yml`:注释 `build`,取消注释 `image`,然后:
32+
33+
```bash
34+
docker compose up -d
35+
```
36+
37+
### 方式 B:本地构建
38+
1639
```bash
1740
# 若使用 Colima(无 Docker Desktop 时)
1841
# brew install colima docker docker-compose && colima start
@@ -24,63 +47,46 @@ docker compose up -d --build
2447

2548
首次打开为空列表。在网页中:
2649

27-
1. **设置**填写检查间隔、Telegram Bot Token / Chat ID(可选)、**面板访问密码(可选)**
50+
1. **设置**检查间隔、Telegram Bot Token / Chat ID(可选)、**面板访问密码(可选)**
2851
2. **添加**:GitHub 仓库或文章链接,并配置筛选规则
29-
3. 添加后会自动检查;也可随时点 **立即检查**
52+
3. 添加后会自动检查;也可随时点 **立即检查** 或单项 **检查**
3053

31-
数据持久化目录`./data`(SQLite 文件 `appupdate.db`,首次运行自动创建)
54+
数据目录`./data`(SQLite`appupdate.db`,首次运行自动创建)
3255

3356
停止:
3457

3558
```bash
3659
docker compose down
60+
#
61+
docker rm -f appupdate
3762
```
3863

3964
## GitHub Actions 镜像
4065

4166
推送到 `main` 或打 `v*` 标签后,会自动构建并推送多架构镜像到 GHCR:
4267

43-
- `ghcr.io/fatelightx/appupdate:latest`(main)
44-
- `ghcr.io/fatelightx/appupdate:sha-<短提交>`
45-
- `ghcr.io/fatelightx/appupdate:1.0.0`(标签 `v1.0.0`
46-
47-
### 使用预构建镜像
48-
49-
```bash
50-
# 私有仓库镜像需先登录(用有 read:packages 的 Token)
51-
echo YOUR_GITHUB_TOKEN | docker login ghcr.io -u FateLightX --password-stdin
52-
53-
docker pull ghcr.io/fatelightx/appupdate:latest
54-
55-
docker run -d --name appupdate \
56-
-p 8000:8000 \
57-
-v "$PWD/data:/data" \
58-
--restart unless-stopped \
59-
ghcr.io/fatelightx/appupdate:latest
60-
```
61-
62-
或在 `docker-compose.yml` 中注释 `build`,启用:
63-
64-
```yaml
65-
image: ghcr.io/fatelightx/appupdate:latest
66-
```
68+
| 标签 | 说明 |
69+
|------|------|
70+
| `ghcr.io/fatelightx/appupdate:latest` | `main` 最新 |
71+
| `ghcr.io/fatelightx/appupdate:main` | 同 main |
72+
| `ghcr.io/fatelightx/appupdate:sha-<短提交>` | 按提交 |
73+
| `ghcr.io/fatelightx/appupdate:1.0.0` | 标签 `v1.0.0` |
6774

68-
工作流文件`.github/workflows/docker.yml`
75+
工作流`.github/workflows/docker.yml`
6976
也可在 GitHub → Actions → **Docker Image****Run workflow** 手动触发。
7077

71-
若镜像拉不下来:到仓库 **Packages** 将 `appupdate` 包设为 Public,或保持 Private 并用 Token 登录。
72-
78+
包页面:https://github.com/users/FateLightX/packages/container/package/appupdate
7379

7480
## 可选环境变量
7581

7682
| 变量 | 说明 |
7783
|------|------|
78-
| `GITHUB_TOKEN` | 提高 GitHub API 限额(建议内网服务器配置) |
84+
| `GITHUB_TOKEN` | 提高 GitHub API 限额(建议服务器配置|
7985
| `APPUPDATE_PANEL_PASSWORD` | 首次初始化时写入面板密码(库中已有密码时不覆盖;日常请在网页设置) |
8086
| `APPUPDATE_DATA_DIR` | 数据目录,默认 `/data`(容器内) |
8187
| `APPUPDATE_INTERVAL_HOURS` | 仅影响**首次初始化**时的默认间隔,之后以网页设置为准 |
8288

83-
在 `docker-compose.yml` 中取消 `GITHUB_TOKEN` 注释即可注入
89+
`docker-compose.yml` 中取消对应注释即可注入
8490

8591
## 本地开发
8692

@@ -97,5 +103,11 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
97103

98104
- 仅支持公开 GitHub 仓库
99105
- 不做自动下载、不自动转存网盘
100-
- 无登录(仅限内网)
101-
- 设计稿与交互原型在 `design-options/`、`prototype/`,上线部署不需要,已排除在 Docker 构建之外
106+
- 默认无密码;可在设置中开启面板密码
107+
- 设计稿与交互原型在 `design-options/``prototype/`,部署不需要,已排除在 Docker 构建之外
108+
109+
## 界面要点
110+
111+
- 列表展示名称、类型、结果摘要、状态与操作
112+
- 点击某一行展开详情(下载链接 / 网盘与提取码);关闭后界面更干净
113+
- 设置可随时改检查间隔、Telegram、面板密码

app/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,14 @@ def health():
6262
return {"ok": True}
6363

6464

65+
6566
if WEB_DIR.exists():
6667
app.mount("/static", StaticFiles(directory=WEB_DIR), name="static")
6768

6869
@app.get("/")
6970
def index():
7071
return FileResponse(WEB_DIR / "index.html")
72+
73+
@app.get("/favicon.ico")
74+
def favicon():
75+
return FileResponse(WEB_DIR / "favicon.ico")

web/app.js

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -218,23 +218,40 @@ function renderFilters() {
218218
.join("");
219219
}
220220

221-
function metricBars(item) {
221+
function metricFacts(item) {
222222
if (item.type === "github") {
223-
const n = (item.assets || []).length;
224-
const w = Math.min(100, 20 + n * 20);
225-
const warn = item.hasUpdate || item.status === "update";
223+
const matched = (item.assets || []).length;
224+
const unmatched = (item.unmatchedAssets || []).length;
225+
const total = matched + unmatched;
226+
const version = item.version || "尚未检查";
227+
const matchText =
228+
total > 0
229+
? `匹配 ${matched}${total !== matched ? ` / ${total}` : ""} 个文件`
230+
: matched > 0
231+
? `匹配 ${matched} 个文件`
232+
: item.lastCheck
233+
? "无匹配文件"
234+
: "等待检查";
226235
return `
227-
<div class="bars">
228-
<div class="mini"><span>版本</span><div class="bar ${warn ? "warn" : ""}"><i style="width:${warn ? 100 : 70}%"></i></div></div>
229-
<div class="mini"><span>匹配</span><div class="bar"><i style="width:${w}%"></i></div></div>
236+
<div class="facts">
237+
<div class="fact-main ${item.hasUpdate || item.status === "update" ? "warn" : ""}">${escapeHtml(version)}</div>
238+
<div class="fact-sub">${escapeHtml(matchText)}</div>
230239
</div>`;
231240
}
232-
const n = (item.netdisks || []).length;
233-
const warn = item.hasUpdate || item.status === "update";
241+
const disks = item.netdisks || [];
242+
const providers = [...new Set(disks.map((d) => d.provider).filter(Boolean))];
243+
const title = item.title || item.version || "尚未检查";
244+
const short =
245+
title.length > 28 ? `${title.slice(0, 28)}…` : title;
246+
const diskText = disks.length
247+
? `${disks.length} 个网盘${providers.length ? " · " + providers.slice(0, 3).join("/") : ""}`
248+
: item.lastCheck
249+
? "未发现网盘"
250+
: "等待检查";
234251
return `
235-
<div class="bars">
236-
<div class="mini"><span>标题</span><div class="bar ${warn ? "warn" : ""}"><i style="width:${warn ? 100 : 80}%"></i></div></div>
237-
<div class="mini"><span>网盘</span><div class="bar"><i style="width:${Math.min(100, n * 30 || 15)}%"></i></div></div>
252+
<div class="facts">
253+
<div class="fact-main ${item.hasUpdate || item.status === "update" ? "warn" : ""}" title="${escapeHtml(title)}">${escapeHtml(short)}</div>
254+
<div class="fact-sub">${escapeHtml(diskText)}</div>
238255
</div>`;
239256
}
240257

@@ -258,10 +275,10 @@ function renderList() {
258275
<div><span class="${dotClass(item)}"></span></div>
259276
<div>
260277
<div class="name">${escapeHtml(item.name)}</div>
261-
<div class="sub">${escapeHtml(item.summary || item.url)}</div>
278+
<div class="sub">${escapeHtml(formatTime(item.lastCheck))} · ${escapeHtml(item.url)}</div>
262279
</div>
263280
<div><span class="tag">${item.type === "github" ? "GitHub" : "文章"}</span></div>
264-
<div>${metricBars(item)}</div>
281+
<div>${metricFacts(item)}</div>
265282
<div class="${statusClass(item)}">${statusLabel(item)}</div>
266283
<div class="row-actions" data-stop>
267284
<button type="button" class="link-btn" data-act="check" data-id="${item.id}">检查</button>
@@ -377,7 +394,10 @@ function renderDetail() {
377394
<h3>${escapeHtml(item.name)}</h3>
378395
<div class="url">${escapeHtml(item.url)}</div>
379396
</div>
380-
<button type="button" class="switch ${item.enabled ? "on" : ""}" data-act="toggle" data-id="${item.id}" aria-label="启用停用"></button>
397+
<div class="detail-actions">
398+
<button type="button" class="switch ${item.enabled ? "on" : ""}" data-act="toggle" data-id="${item.id}" aria-label="启用停用"></button>
399+
<button type="button" class="icon-btn" data-act="close-detail" aria-label="关闭详情">×</button>
400+
</div>
381401
</div>
382402
<div class="kv">
383403
<span>类型</span><div>${item.type === "github" ? "GitHub" : "文章"}</div>
@@ -404,9 +424,9 @@ async function loadAll() {
404424
state.sources = sources;
405425
state.settings = settings;
406426
state.meta = meta;
407-
if (state.selectedId == null && sources[0]) state.selectedId = sources[0].id;
427+
// 不自动展开详情:只有用户点选时才显示
408428
if (state.selectedId != null && !sources.find((s) => s.id === state.selectedId)) {
409-
state.selectedId = sources[0]?.id ?? null;
429+
state.selectedId = null;
410430
}
411431
renderAll();
412432
renderAddChips();
@@ -754,7 +774,9 @@ function bindEvents() {
754774
if (stop) return;
755775
const row = e.target.closest(".row[data-id]");
756776
if (!row) return;
757-
state.selectedId = Number(row.dataset.id);
777+
const id = Number(row.dataset.id);
778+
// 再点同一行则收起详情
779+
state.selectedId = state.selectedId === id ? null : id;
758780
renderAll();
759781
});
760782

@@ -765,10 +787,16 @@ function bindEvents() {
765787
return;
766788
}
767789
const act = e.target.closest("[data-act]");
768-
if (act && act.dataset.act === "toggle") {
790+
if (!act) return;
791+
if (act.dataset.act === "close-detail") {
792+
state.selectedId = null;
793+
renderAll();
794+
return;
795+
}
796+
if (act.dataset.act === "toggle") {
769797
toggleEnabled(Number(act.dataset.id));
770798
}
771-
if (act && act.dataset.act === "edit-detail") {
799+
if (act.dataset.act === "edit-detail") {
772800
openEdit(Number(act.dataset.id));
773801
}
774802
});
@@ -778,6 +806,14 @@ function bindEvents() {
778806
if (e.target === mask) mask.classList.remove("show");
779807
});
780808
});
809+
810+
document.addEventListener("keydown", (e) => {
811+
if (e.key !== "Escape") return;
812+
if (document.querySelector(".modal-mask.show, .login-gate:not([hidden])")) return;
813+
if (state.selectedId == null) return;
814+
state.selectedId = null;
815+
renderAll();
816+
});
781817
}
782818

783819
bindEvents();

web/apple-touch-icon.png

10.8 KB
Loading

web/favicon-32.png

1.39 KB
Loading

web/favicon.ico

4.37 KB
Binary file not shown.

web/favicon.svg

Lines changed: 8 additions & 0 deletions
Loading

web/index.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1" />
66
<title>更新追踪</title>
7+
<link rel="icon" href="/favicon.ico" sizes="any" />
8+
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
9+
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32.png" />
10+
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png" />
711
<link rel="stylesheet" href="/static/styles.css" />
812
</head>
913
<body>
@@ -29,7 +33,7 @@
2933
<div></div>
3034
<div>名称</div>
3135
<div>类型</div>
32-
<div>摘要</div>
36+
<div>结果</div>
3337
<div>状态</div>
3438
<div>操作</div>
3539
</div>

web/styles.css

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,28 @@ button, input, select, textarea { font: inherit; }
123123
.link-btn:hover { text-decoration: underline; }
124124
.link-btn.danger { color: #b91c1c; }
125125

126-
.bars { display: flex; flex-direction: column; gap: 4px; }
127-
.mini {
128-
display: grid; grid-template-columns: 36px 1fr; gap: 6px; align-items: center;
129-
font-size: 11px; color: var(--muted);
130-
}
131-
.bar { height: 5px; background: #f5f5f4; border-radius: 999px; overflow: hidden; }
132-
.bar > i { display: block; height: 100%; background: #22c55e; border-radius: 999px; }
133-
.bar.warn > i { background: #f59e0b; }
126+
.facts {
127+
display: flex;
128+
flex-direction: column;
129+
gap: 2px;
130+
min-width: 0;
131+
}
132+
.fact-main {
133+
font-size: 13px;
134+
font-weight: 600;
135+
color: var(--text);
136+
white-space: nowrap;
137+
overflow: hidden;
138+
text-overflow: ellipsis;
139+
}
140+
.fact-main.warn { color: var(--danger); }
141+
.fact-sub {
142+
font-size: 12px;
143+
color: var(--muted);
144+
white-space: nowrap;
145+
overflow: hidden;
146+
text-overflow: ellipsis;
147+
}
134148

135149
.detail {
136150
margin-top: 14px;
@@ -143,6 +157,23 @@ button, input, select, textarea { font: inherit; }
143157
display: flex; justify-content: space-between; gap: 12px; align-items: flex-start;
144158
margin-bottom: 12px;
145159
}
160+
.detail-actions {
161+
display: flex; align-items: center; gap: 10px; flex: none;
162+
}
163+
.icon-btn {
164+
width: 32px; height: 32px;
165+
border: 1px solid var(--line);
166+
background: #fff;
167+
border-radius: 8px;
168+
color: var(--muted);
169+
font-size: 20px;
170+
line-height: 1;
171+
cursor: pointer;
172+
display: grid;
173+
place-items: center;
174+
padding: 0;
175+
}
176+
.icon-btn:hover { color: var(--text); background: #fafaf9; }
146177
.detail h3 { margin: 0 0 4px; font-size: 16px; }
147178
.detail .url { color: var(--muted); font-size: 12px; word-break: break-all; }
148179
.kv {
@@ -274,14 +305,14 @@ button, input, select, textarea { font: inherit; }
274305
grid-template-areas:
275306
"dot name status"
276307
"dot type type"
277-
"dot bars bars"
308+
"dot facts facts"
278309
"dot actions actions";
279310
}
280311
.row.head { display: none; }
281312
.row > :nth-child(1) { grid-area: dot; }
282313
.row > :nth-child(2) { grid-area: name; }
283314
.row > :nth-child(3) { grid-area: type; }
284-
.row > :nth-child(4) { grid-area: bars; }
315+
.row > :nth-child(4) { grid-area: facts; }
285316
.row > :nth-child(5) { grid-area: status; justify-self: end; }
286317
.row > :nth-child(6) { grid-area: actions; }
287318
.field-row { grid-template-columns: 1fr; }

0 commit comments

Comments
 (0)