feat(knowledge): 点开知识先看到排好版的正文,要改再点「编辑」
CI / backend (push) Canceled after 0s

之前抽屉一打开就是 Vditor 编辑器,正文是一堆 `##` 和 `-` 的原文,
导入的 84 条旧备忘看着尤其像草稿。现在分成「看」和「写」两态:

- 默认只读:按 Markdown 渲染(markdown-it + DOMPurify),分类与标签是 chip,
  附件列表只读;底部是「编辑」和状态动作,右侧显示发布于何时。
- 点「编辑」才挂编辑器;「取消」直接丢弃未保存改动(重新拉一次详情)。
- 安卓详情页同样用 Markwon 把正文排版后再显示,要改走「编辑正文」。

顺带修了 `web/e2e/knowledge.mjs` 里一条早就失效的断言:它等的是
「已发布 · 版本 N」,但界面上没有这行字(git 历史里也没有),
改成等抽屉底部的「发布于 …」。

验证:pytest 94 passed、ruff 全绿、tsc 0、vite build ✓;
`knowledge-notebook.mjs` 16 项、`knowledge.mjs` 7 项 e2e 全绿。
安卓本轮仍未编译(本机 JDK 25 与 Gradle 8.7/AGP 8.7.3 不兼容)。
This commit is contained in:
Steven
2026-10-31 01:34:25 +08:00
parent bbd30ec85e
commit 441b1b93e1
7 changed files with 222 additions and 25 deletions
+10
View File
@@ -170,6 +170,16 @@ try {
),
page.getByRole("button", { name: "打开", exact: true }).first().click(),
]);
// 详情默认是「看」:Markdown 渲染后的样子,不是编辑器
await page.locator(".prose-dadao h2").first().waitFor({ timeout: 10_000 });
check(
"详情抽屉默认渲染样式(不是进来就编辑)",
(await page.locator(".vditor-ir").count()) === 0 &&
(await page.locator(".prose-dadao h2").first().textContent())?.includes("步骤") === true,
);
// 要改才点「编辑」
await page.getByRole("button", { name: "编辑", exact: true }).last().click();
const tagInput = page.getByLabel("标签", { exact: true }).last();
await tagInput.waitFor({ timeout: 10_000 });
// 先等抽屉把自己拉回来的旧标签填上——那一刻之后才没有"迟到的响应"来覆盖输入
+2 -2
View File
@@ -104,8 +104,8 @@ try {
page.getByRole("button", { name: "发布", exact: true }).click(),
]);
check("发布请求成功", publishResp.ok());
await page.getByText(/已发布 · 版本 \d+/, { exact: false }).waitFor({ timeout: 10_000 });
const published = await page.getByText(/已发布 · 版本 \d+/, { exact: false }).first().isVisible();
await page.getByText(/发布于 /, { exact: false }).first().waitFor({ timeout: 10_000 });
const published = await page.getByText(/发布于 /, { exact: false }).first().isVisible();
check("核对后可以发布", published);
const again = await fetch(`${API}/workspaces/${ws.id}/knowledge`, {
+94
View File
@@ -0,0 +1,94 @@
/**
* 截图脚本:把导入的备忘录点开,看「浏览」样式是否真的渲染成 Markdown。
*
* 只用于出图,不做断言;账号用 dev 的 13900000001(导入数据的归属)。
*/
import { chromium } from "playwright-core";
import { mkdirSync } from "node:fs";
const WEB = process.env.WEB ?? "http://127.0.0.1:5180";
const API = process.env.API ?? "http://127.0.0.1:8100/api/v1";
const PHONE = process.env.PHONE ?? "13900000001";
const WS_NAME = process.env.WS_NAME ?? "我的备忘";
async function call(method, path, body, token) {
const res = await fetch(`${API}${path}`, {
method,
headers: {
"content-type": "application/json",
...(token ? { authorization: `Bearer ${token}` } : {}),
...(method === "GET" ? {} : { "idempotency-key": `shot-${Math.random()}` }),
},
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
const json = text ? JSON.parse(text) : {};
if (json.code !== 0) throw new Error(`${method} ${path} -> ${json.code} ${json.message}`);
return json.data;
}
async function loginAs(phone) {
// dev 下验证码固定,发码接口有 60 秒节流:能直接登录就直接登,被节流就等够了再发
try {
return await call("POST", "/auth/login", { phone, code: "123456" });
} catch {
/* 验证码过期或没发过,往下走发码 */
}
for (let i = 0; i < 2; i += 1) {
try {
await call("POST", "/auth/sms/send", { phone });
break;
} catch (err) {
if (!String(err.message).includes("2011") || i === 1) throw err;
await new Promise((r) => setTimeout(r, 61_000));
}
}
return await call("POST", "/auth/login", { phone, code: "123456" });
}
mkdirSync("e2e/artifacts", { recursive: true });
const login = await loginAs(PHONE);
const token = login.tokens.access_token;
const me = await call("GET", "/auth/me", undefined, token);
const ws = me.workspaces.find((w) => w.name === WS_NAME) ?? me.workspaces[0];
if (!ws) throw new Error(`${PHONE} 名下没有工作区`);
console.log(`工作区:${ws.name} ${ws.id}`);
const browser = await chromium.launch({
executablePath:
process.env.CHROME ??
`${process.env.LOCALAPPDATA}\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe`,
});
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
const page = await context.newPage();
await page.addInitScript(
([access, refresh]) => {
localStorage.setItem("dadao.access", access);
localStorage.setItem("dadao.refresh", refresh);
},
[token, login.tokens.refresh_token],
);
await page.goto(`${WEB}/w/${ws.id}/knowledge`, { waitUntil: "networkidle" });
await page.getByRole("heading", { name: "知识" }).waitFor({ timeout: 20_000 });
// 导入的备忘都是发布过的,切到「已发布」才看得到
await page.getByRole("button", { name: "已发布", exact: true }).first().click();
if (process.env.TITLE) {
// 挑一条带标题/列表的,截图里才看得出 Markdown 真的渲染了
const target = page.locator("li").filter({ hasText: process.env.TITLE }).first();
await target.waitFor({ timeout: 20_000 });
await page.screenshot({ path: "e2e/artifacts/memo-list.png" });
await target.getByRole("button", { name: "打开", exact: true }).click();
} else {
await page.getByRole("button", { name: "打开", exact: true }).first().waitFor({ timeout: 20_000 });
await page.waitForTimeout(600);
await page.screenshot({ path: "e2e/artifacts/memo-list.png" });
await page.getByRole("button", { name: "打开", exact: true }).first().click();
}
await page.locator(".prose-dadao").first().waitFor({ timeout: 20_000 });
await page.waitForTimeout(600);
await page.screenshot({ path: "e2e/artifacts/memo-read.png" });
await browser.close();
console.log("OK");