Commit 15fd3ece by 周田

fix: 修复公开下载页不显示版本更新内容

公开应用列表的 SQL 中应用介绍与版本说明使用同名列 description,
合并为字典时版本说明被覆盖,前端因此拿不到更新内容。

- 将版本说明列改为 release_description,避免与应用介绍冲突
- 下载页分别渲染“程序介绍”和“版本更新内容”两个区块
- 更新静态资源版本号并补充相应测试断言
parent cacd1900
...@@ -15,7 +15,7 @@ distribution-server/ ...@@ -15,7 +15,7 @@ distribution-server/
└─ settings.py └─ settings.py
``` ```
管理 API 以 `/api` 开头;创建应用时可填写应用说明。上传版本必须填写 Markdown 更新内容;服务器流式计算文件大小和 SHA-256,并记录文件名、格式、平台、架构、更新内容、上传时间和发布状态。支持 EXE、MSI、ZIP、7Z、DMG、PKG、DEB、RPM、AppImage 和 TAR.GZ。Markdown 说明支持标题、列表、粗体、链接和代码。 管理 API 以 `/api` 开头;创建应用时可填写应用说明,公开下载页会将其标记为“程序介绍”,与各版本的更新内容分开展示。上传版本必须填写 Markdown 更新内容;服务器流式计算文件大小和 SHA-256,并记录文件名、格式、平台、架构、更新内容、上传时间和发布状态。支持 EXE、MSI、ZIP、7Z、DMG、PKG、DEB、RPM、AppImage 和 TAR.GZ。Markdown 说明支持标题、列表、粗体、链接和代码。
版本说明也可以在管理页的程序包记录中单独编辑,无需重新上传程序包;保存后会同时显示在管理记录和公开下载页。 版本说明也可以在管理页的程序包记录中单独编辑,无需重新上传程序包;保存后会同时显示在管理记录和公开下载页。
......
...@@ -304,8 +304,8 @@ class DistributionStore: ...@@ -304,8 +304,8 @@ class DistributionStore:
rows = connection.execute( rows = connection.execute(
""" """
SELECT a.app_id, a.display_name, a.description, a.current_version, SELECT a.app_id, a.display_name, a.description, a.current_version,
r.filename, r.description, r.platform, r.architecture, r.filename, r.description AS release_description,
r.size, r.sha256, r.platform, r.architecture, r.size, r.sha256,
r.published_at r.published_at
FROM applications AS a FROM applications AS a
JOIN releases AS r JOIN releases AS r
......
...@@ -33,6 +33,8 @@ h1 { margin: 8px 0 7px; font-size: clamp(24px, 4vw, 32px); letter-spacing: -.03e ...@@ -33,6 +33,8 @@ h1 { margin: 8px 0 7px; font-size: clamp(24px, 4vw, 32px); letter-spacing: -.03e
.package-version { flex: 0 0 auto; padding: 4px 8px; border-radius: 999px; color: #536077; background: #f0f2f6; font-size: 11px; font-weight: 700; } .package-version { flex: 0 0 auto; padding: 4px 8px; border-radius: 999px; color: #536077; background: #f0f2f6; font-size: 11px; font-weight: 700; }
.package-name { min-width: 0; color: var(--ink); font-size: 18px; font-weight: 700; letter-spacing: -.02em; overflow-wrap: anywhere; } .package-name { min-width: 0; color: var(--ink); font-size: 18px; font-weight: 700; letter-spacing: -.02em; overflow-wrap: anywhere; }
.package-app-description { margin: 10px 0 0; color: var(--muted); font-size: 13px; line-height: 1.6; overflow-wrap: anywhere; } .package-app-description { margin: 10px 0 0; color: var(--muted); font-size: 13px; line-height: 1.6; overflow-wrap: anywhere; }
.package-app-description-title, .application-description-title { display: block; color: #536077; font-size: 13px; }
.package-app-description-content { margin: 4px 0 0; }
.package-badge { flex: 0 0 auto; padding: 5px 9px; border-radius: 999px; color: #56647a; background: #f0f2f6; font-size: 10px; font-weight: 700; } .package-badge { flex: 0 0 auto; padding: 5px 9px; border-radius: 999px; color: #56647a; background: #f0f2f6; font-size: 10px; font-weight: 700; }
.package-badge-helper { color: #6652b8; background: #f0edff; } .package-badge-helper { color: #6652b8; background: #f0edff; }
.package-metadata { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 18px 0; padding: 13px 0; border-top: 1px solid #eff1f5; border-bottom: 1px solid #eff1f5; } .package-metadata { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 18px 0; padding: 13px 0; border-top: 1px solid #eff1f5; border-bottom: 1px solid #eff1f5; }
...@@ -86,7 +88,8 @@ footer { font-size: 12px; } ...@@ -86,7 +88,8 @@ footer { font-size: 12px; }
.package-description { margin: 14px 0 0; color: #68758a; font-size: 13px; } .package-description { margin: 14px 0 0; color: #68758a; font-size: 13px; }
.package-description-title { display: block; margin-bottom: 6px; color: #536077; font-size: 13px; } .package-description-title { display: block; margin-bottom: 6px; color: #536077; font-size: 13px; }
.package-description-content { padding: 13px 15px; border-radius: 8px; background: #f7f8fc; } .package-description-content { padding: 13px 15px; border-radius: 8px; background: #f7f8fc; }
.application-description { margin-top: 8px !important; color: var(--muted); font-size: 15px; line-height: 1.7; overflow-wrap: anywhere; } .application-description { margin-top: 8px; color: var(--muted); font-size: 15px; line-height: 1.7; overflow-wrap: anywhere; }
.application-description p { margin: 4px 0 0; }
.markdown-body { color: #465268; font-size: 14px; line-height: 1.7; overflow-wrap: anywhere; } .markdown-body { color: #465268; font-size: 14px; line-height: 1.7; overflow-wrap: anywhere; }
.markdown-body > :first-child { margin-top: 0; } .markdown-body > :first-child { margin-top: 0; }
.markdown-body > :last-child { margin-bottom: 0; } .markdown-body > :last-child { margin-bottom: 0; }
......
...@@ -39,7 +39,7 @@ function formatSize(bytes) { ...@@ -39,7 +39,7 @@ function formatSize(bytes) {
function packageCard( function packageCard(
packageInfo, packageInfo,
appName, appName,
{ compact = false, applicationDescription = "" } = {}, { compact = false, applicationDescription = "", releaseNotes = "" } = {},
) { ) {
const card = node("article", `package-card${compact ? " package-card-compact" : ""}`); const card = node("article", `package-card${compact ? " package-card-compact" : ""}`);
const heading = node("div", "package-heading"); const heading = node("div", "package-heading");
...@@ -56,12 +56,17 @@ function packageCard( ...@@ -56,12 +56,17 @@ function packageCard(
heading.append(identity, badge); heading.append(identity, badge);
card.append(heading); card.append(heading);
if (applicationDescription) { if (applicationDescription) {
card.append(node("p", "package-app-description", applicationDescription)); const description = node("section", "package-app-description");
description.append(
node("strong", "package-app-description-title", "程序介绍"),
node("p", "package-app-description-content", applicationDescription),
);
card.append(description);
} }
if (packageInfo.description) { if (releaseNotes) {
const notes = node("section", "package-description"); const notes = node("section", "package-description");
const content = node("div", "package-description-content"); const content = node("div", "package-description-content");
content.append(renderMarkdown(packageInfo.description)); content.append(renderMarkdown(releaseNotes));
notes.append( notes.append(
node("strong", "package-description-title", "版本更新内容"), node("strong", "package-description-title", "版本更新内容"),
content, content,
...@@ -116,6 +121,7 @@ function renderLibrary(applications) { ...@@ -116,6 +121,7 @@ function renderLibrary(applications) {
}, application.display_name, { }, application.display_name, {
compact: true, compact: true,
applicationDescription: application.description, applicationDescription: application.description,
releaseNotes: application.release_description,
}); });
const detail = node("a", "history-link", "查看已发布版本"); const detail = node("a", "history-link", "查看已发布版本");
detail.href = `/download/${encodeURIComponent(application.app_id)}`; detail.href = `/download/${encodeURIComponent(application.app_id)}`;
...@@ -129,11 +135,16 @@ function renderApplication(application) { ...@@ -129,11 +135,16 @@ function renderApplication(application) {
document.querySelector("#page-description").textContent = document.querySelector("#page-description").textContent =
`${application.packages.length} 个已发布程序包 · App ID:${application.app_id}`; `${application.packages.length} 个已发布程序包 · App ID:${application.app_id}`;
const applicationDescription = document.querySelector("#application-description"); const applicationDescription = document.querySelector("#application-description");
applicationDescription.textContent = application.description || ""; document.querySelector("#application-description-content").textContent =
application.description || "";
applicationDescription.hidden = !application.description; applicationDescription.hidden = !application.description;
packageList.replaceChildren(); packageList.replaceChildren();
for (const packageInfo of application.packages) { for (const packageInfo of application.packages) {
packageList.append(packageCard(packageInfo, application.display_name)); packageList.append(
packageCard(packageInfo, application.display_name, {
releaseNotes: packageInfo.description,
}),
);
} }
} }
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light"> <meta name="color-scheme" content="light">
<title>程序下载中心</title> <title>程序下载中心</title>
<link rel="stylesheet" href="/assets/downloads.css?v=20261028-1"> <link rel="stylesheet" href="/assets/downloads.css?v=20261029-2">
<script type="module" src="/assets/downloads.js?v=20261028-1"></script> <script type="module" src="/assets/downloads.js?v=20261029-2"></script>
</head> </head>
<body> <body>
<main class="page"> <main class="page">
...@@ -21,7 +21,10 @@ ...@@ -21,7 +21,10 @@
<div class="eyebrow">DOWNLOAD LIBRARY</div> <div class="eyebrow">DOWNLOAD LIBRARY</div>
<h1 id="page-title">获取程序与更新包</h1> <h1 id="page-title">获取程序与更新包</h1>
<p id="page-description">浏览已发布的程序版本,下载前请核对平台与架构。</p> <p id="page-description">浏览已发布的程序版本,下载前请核对平台与架构。</p>
<p id="application-description" class="application-description" hidden></p> <section id="application-description" class="application-description" hidden>
<strong class="application-description-title">程序介绍</strong>
<p id="application-description-content"></p>
</section>
</section> </section>
<div id="error" class="error-card" role="status" hidden></div> <div id="error" class="error-card" role="status" hidden></div>
<section id="package-list" class="package-list" aria-live="polite"></section> <section id="package-list" class="package-list" aria-live="polite"></section>
......
...@@ -142,6 +142,8 @@ class DistributionTests(unittest.TestCase): ...@@ -142,6 +142,8 @@ class DistributionTests(unittest.TestCase):
public_library = self.client.get("/api/public/apps") public_library = self.client.get("/api/public/apps")
self.assertEqual(public_application.json()["description"], description) self.assertEqual(public_application.json()["description"], description)
self.assertEqual(public_library.json()[0]["description"], description) self.assertEqual(public_library.json()[0]["description"], description)
self.assertEqual(public_library.json()[0]["release_description"], "Release notes.")
self.assertEqual(public_application.json()["packages"][0]["description"], "Release notes.")
def test_generic_package_is_recorded_and_published_for_public_download(self) -> None: def test_generic_package_is_recorded_and_published_for_public_download(self) -> None:
self.create_application() self.create_application()
...@@ -388,6 +390,7 @@ class DistributionTests(unittest.TestCase): ...@@ -388,6 +390,7 @@ class DistributionTests(unittest.TestCase):
self.assertIn(".shell", stylesheet.text) self.assertIn(".shell", stylesheet.text)
self.assertEqual(download_script.status_code, 200) self.assertEqual(download_script.status_code, 200)
self.assertIn("renderLibrary", download_script.text) self.assertIn("renderLibrary", download_script.text)
self.assertIn('"程序介绍"', download_script.text)
self.assertEqual(download_stylesheet.status_code, 200) self.assertEqual(download_stylesheet.status_code, 200)
self.assertIn(".package-card", download_stylesheet.text) self.assertIn(".package-card", download_stylesheet.text)
self.assertEqual(markdown_script.status_code, 200) self.assertEqual(markdown_script.status_code, 200)
...@@ -402,8 +405,8 @@ class DistributionTests(unittest.TestCase): ...@@ -402,8 +405,8 @@ class DistributionTests(unittest.TestCase):
"/download/my-program", "/download/my-program",
"/assets/app.js?v=20261028-1", "/assets/app.js?v=20261028-1",
"/assets/app.css?v=20261028-1", "/assets/app.css?v=20261028-1",
"/assets/downloads.js?v=20261028-1", "/assets/downloads.js?v=20261029-2",
"/assets/downloads.css?v=20261028-1", "/assets/downloads.css?v=20261029-2",
): ):
with self.subTest(path=path): with self.subTest(path=path):
response = self.client.get(path) response = self.client.get(path)
...@@ -418,10 +421,12 @@ class DistributionTests(unittest.TestCase): ...@@ -418,10 +421,12 @@ class DistributionTests(unittest.TestCase):
dashboard = self.client.get("/") dashboard = self.client.get("/")
downloads = self.client.get("/downloads") downloads = self.client.get("/downloads")
package_page = self.client.get("/download/my-program")
for asset in ("app.js", "app.css"): for asset in ("app.js", "app.css"):
self.assertIn(f"/assets/{asset}?v=20261028-1", dashboard.text) self.assertIn(f"/assets/{asset}?v=20261028-1", dashboard.text)
for asset in ("downloads.js", "downloads.css"): for asset in ("downloads.js", "downloads.css"):
self.assertIn(f"/assets/{asset}?v=20261028-1", downloads.text) self.assertIn(f"/assets/{asset}?v=20261029-2", downloads.text)
self.assertIn("程序介绍", package_page.text)
def test_public_download_surfaces_are_remote_but_management_is_not(self) -> None: def test_public_download_surfaces_are_remote_but_management_is_not(self) -> None:
self.create_application() self.create_application()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment