Improve recent documents handling (#16727)

* merge

* Update RecentDoc struct to make timestamp fields optional

* GetDoc is solely responsible for retrieving document content and does not handle business logic

* Remove RemoveRecentDoc function and its calls from multiple files to streamline document handling

* Ensure the API correctly returns an empty array, add deduplication logic, and remove redundant sorting steps when updating fields

* 🎨 Supports configuring the maximum number of `Recent documents` to be listed https://github.com/siyuan-note/siyuan/issues/16720

* merge

* 🎨 Supports configuring the maximum number of `Recent documents` to be listed https://github.com/siyuan-note/siyuan/issues/16720

* 🐛 Fix browsing time not sorted

* 🎨 Supports configuring the maximum number of `Recent documents` to be listed https://github.com/siyuan-note/siyuan/issues/16720

* merge

* remove async

* try catch
This commit is contained in:
Jeffrey Chen 2026-01-21 08:52:05 +08:00 committed by GitHub
parent fc5a79ff16
commit b0f71123a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 398 additions and 220 deletions

View file

@ -10,44 +10,39 @@ import {focusByRange} from "../protyle/util/selection";
import {hasClosestByClassName} from "../protyle/util/hasClosest";
import {hideElements} from "../protyle/ui/hideElements";
const getHTML = async (data: {
const renderRecentDocsContent = async (data: {
rootID: string,
icon: string,
title: string,
viewedAt?: number,
closedAt?: number,
openAt?: number,
updated?: number
}[], element: Element, key?: string, sortBy: TRecentDocsSort = "viewedAt") => {
}[], element: Element, key?: string) => {
let tabHtml = "";
let index = 0;
// 根据排序字段对数据进行排序
const sortedData = [...data].sort((a, b) => {
const aValue = a[sortBy] || 0;
const bValue = b[sortBy] || 0;
return bValue - aValue; // 降序排序
});
sortedData.forEach((item) => {
if (!key || item.title.toLowerCase().includes(key.toLowerCase())) {
tabHtml += `<li data-index="${index}" data-node-id="${item.rootID}" class="b3-list-item${index === 0 ? " b3-list-item--focus" : ""}">
${unicode2Emoji(item.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file, "b3-list-item__graphic", true)}
<span class="b3-list-item__text">${escapeHtml(item.title)}</span>
if (key) {
data = data.filter((item) => {
return item.title.toLowerCase().includes(key.toLowerCase());
});
}
data.forEach((item) => {
tabHtml += `<li data-index="${index}" data-node-id="${item.rootID}" class="b3-list-item${index === 0 ? " b3-list-item--focus" : ""}">
${unicode2Emoji(item.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file, "b3-list-item__graphic", true)}
<span class="b3-list-item__text">${escapeHtml(item.title)}</span>
</li>`;
index++;
}
index++;
});
let switchPath = "";
if (tabHtml) {
const pathResponse = await fetchSyncPost("/api/filetree/getFullHPathByID", {
id: data[0].rootID
id: data[0].rootID // 过滤后的第一个文档 ID
});
switchPath = escapeHtml(pathResponse.data);
}
let dockHtml = "";
if (!isWindow()) {
dockHtml = '<ul class="b3-list b3-list--background" style="overflow: auto;width: 200px;">';
let docIndex = 0;
if (!key || window.siyuan.languages.riffCard.toLowerCase().includes(key.toLowerCase())) {
dockHtml += `<li data-type="riffCard" data-index="0" class="b3-list-item${!switchPath ? " b3-list-item--focus" : ""}">
<svg class="b3-list-item__graphic"><use xlink:href="#iconRiffCard"></use></svg>
@ -57,30 +52,30 @@ ${unicode2Emoji(item.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file,
if (!switchPath) {
switchPath = window.siyuan.languages.riffCard;
}
docIndex++;
}
let docIndex = 1;
getAllDocks().forEach((item) => {
if (!key || item.title.toLowerCase().includes(key.toLowerCase())) {
dockHtml += `<li data-type="${item.type}" data-index="${docIndex}" class="b3-list-item${!switchPath ? " b3-list-item--focus" : ""}">
<svg class="b3-list-item__graphic"><use xlink:href="#${item.icon}"></use></svg>
<span class="b3-list-item__text">${item.title}</span>
<span class="b3-list-item__meta">${updateHotkeyTip(item.hotkey || "")}</span>
<span class="b3-list-item__meta">${updateHotkeyTip(item.hotkey)}</span>
</li>`;
docIndex++;
if (!switchPath) {
switchPath = window.siyuan.languages.riffCard;
switchPath = item.title;
}
docIndex++;
}
});
dockHtml = dockHtml + "</ul>";
dockHtml = '<ul class="b3-list b3-list--background" style="overflow: auto;width: 200px;">' + dockHtml + "</ul>";
}
const pathElement = element.querySelector(".switch-doc__path");
pathElement.innerHTML = switchPath;
pathElement.previousElementSibling.innerHTML = `<div class="fn__flex fn__flex-1" style="overflow:auto;">
${dockHtml}
<ul style="${isWindow() ? "border-left:0;" : ""}min-width:360px;" class="b3-list b3-list--background fn__flex-1">${tabHtml}</ul>
</div>`;
pathElement.previousElementSibling.innerHTML = `<div class="fn__flex fn__flex-1" style="overflow: auto;">
${dockHtml}
<ul style="${isWindow() ? "border-left: 0;" : ""}min-width: 360px;" class="b3-list b3-list--background fn__flex-1">${tabHtml}</ul>
</div>`;
};
export const openRecentDocs = () => {
@ -93,7 +88,8 @@ export const openRecentDocs = () => {
hideElements(["dialog"]);
return;
}
fetchPost("/api/storage/getRecentDocs", {sortBy: "viewedAt"}, (response) => {
const sortBy = window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type as TRecentDocsSort;
fetchPost("/api/storage/getRecentDocs", {sortBy}, (response) => {
let range: Range;
if (getSelection().rangeCount > 0) {
range = getSelection().getRangeAt(0);
@ -110,15 +106,15 @@ export const openRecentDocs = () => {
<span class="fn__space"></span>
<div class="fn__flex-center">
<select class="b3-select" id="recentDocsSort">
<option value="viewedAt"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "viewedAt" ? " selected" : ""}>${window.siyuan.languages.recentViewed}</option>
<option value="updated"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "updated" ? " selected" : ""}>${window.siyuan.languages.recentModified}</option>
<option value="openAt"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "openAt" ? " selected" : ""}>${window.siyuan.languages.recentOpened}</option>
<option value="closedAt"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "closedAt" ? " selected" : ""}>${window.siyuan.languages.recentClosed}</option>
<option value="viewedAt">${window.siyuan.languages.recentViewed}</option>
<option value="updated">${window.siyuan.languages.recentModified}</option>
<option value="openAt">${window.siyuan.languages.recentOpened}</option>
<option value="closedAt">${window.siyuan.languages.recentClosed}</option>
</select>
</div>
</div>`,
content: `<div class="fn__flex-column switch-doc">
<div class="fn__flex fn__flex-1" style="overflow:auto;"></div>
<div class="fn__flex fn__flex-1" style="overflow: auto;"></div>
<div class="switch-doc__path"></div>
</div>`,
height: "80vh",
@ -128,16 +124,18 @@ export const openRecentDocs = () => {
}
}
});
const sortSelect = dialog.element.querySelector("#recentDocsSort") as HTMLSelectElement;
sortSelect.value = sortBy;
const searchElement = dialog.element.querySelector("input");
searchElement.focus();
searchElement.addEventListener("compositionend", () => {
getHTML(response.data, dialog.element, searchElement.value, sortSelect.value as TRecentDocsSort);
renderRecentDocsContent(response.data, dialog.element, searchElement.value);
});
searchElement.addEventListener("input", (event: InputEvent) => {
if (event.isComposing) {
return;
}
getHTML(response.data, dialog.element, searchElement.value, sortSelect.value as TRecentDocsSort);
renderRecentDocsContent(response.data, dialog.element, searchElement.value);
});
dialog.element.setAttribute("data-key", Constants.DIALOG_RECENTDOCS);
dialog.element.addEventListener("click", (event) => {
@ -152,45 +150,16 @@ export const openRecentDocs = () => {
});
// 添加排序下拉框事件监听
const sortSelect = dialog.element.querySelector("#recentDocsSort") as HTMLSelectElement;
sortSelect.addEventListener("change", () => {
// 重新调用API获取排序后的数据
if (sortSelect.value === "updated") {
// 使用SQL查询获取最近修改的文档
const data = {
stmt: "SELECT * FROM blocks WHERE type = 'd' ORDER BY updated DESC LIMIT 33"
};
fetchSyncPost("/api/query/sql", data).then((sqlResponse) => {
if (sqlResponse.data && sqlResponse.data.length > 0) {
// 转换SQL查询结果格式
const recentModifiedDocs = sqlResponse.data.map((block: any) => {
// 从ial中解析icon
let icon = "";
if (block.ial) {
const iconMatch = block.ial.match(/icon="([^"]*)"/);
if (iconMatch) {
icon = iconMatch[1];
}
}
return {
rootID: block.id,
icon,
title: block.content,
updated: block.updated
};
});
getHTML(recentModifiedDocs, dialog.element, searchElement.value, "updated");
}
});
} else {
fetchPost("/api/storage/getRecentDocs", {sortBy: sortSelect.value}, (newResponse) => {
getHTML(newResponse.data, dialog.element, searchElement.value, sortSelect.value as TRecentDocsSort);
});
}
// 重新调用 API 获取排序后的数据
fetchPost("/api/storage/getRecentDocs", {sortBy: sortSelect.value}, (newResponse) => {
response = newResponse;
renderRecentDocsContent(newResponse.data, dialog.element, searchElement.value);
});
window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type = sortSelect.value;
setStorageVal(Constants.LOCAL_RECENT_DOCS, window.siyuan.storage[Constants.LOCAL_RECENT_DOCS]);
});
getHTML(response.data, dialog.element);
renderRecentDocsContent(response.data, dialog.element);
});
};

View file

@ -779,7 +779,7 @@ export class Wnd {
model.send("closews", {});
}
private removeTabAction = (id: string, closeAll = false, animate = true, isSaveLayout = true) => {
private removeTabAction = (id: string, isBatchClose = false, animate = true, isSaveLayout = true) => {
clearCounter();
this.children.find((item, index) => {
if (item.id === id) {
@ -795,8 +795,10 @@ export class Wnd {
}
if (item.model instanceof Editor) {
saveScroll(item.model.editor.protyle);
// 更新文档关闭时间
fetchPost("/api/storage/updateRecentDocCloseTime", {rootID: item.model.editor.protyle.block.rootID});
// 更新文档关闭时间(批量关闭页签时由 closeTabByType 批量处理,这里不单独调用)
if (!isBatchClose) {
fetchPost("/api/storage/updateRecentDocCloseTime", {rootID: item.model.editor.protyle.block.rootID});
}
}
if (this.children.length === 1) {
this.destroyModel(this.children[0].model);
@ -842,7 +844,7 @@ export class Wnd {
}
}
});
if (latestHeadElement && !closeAll) {
if (latestHeadElement && !isBatchClose) {
this.switchTab(latestHeadElement, true, true, false, false);
this.showHeading();
}
@ -890,7 +892,7 @@ export class Wnd {
/// #endif
};
public removeTab(id: string, closeAll = false, animate = true, isSaveLayout = true) {
public removeTab(id: string, isBatchClose = false, animate = true, isSaveLayout = true) {
for (let index = 0; index < this.children.length; index++) {
const item = this.children[index];
if (item.id === id) {
@ -899,9 +901,9 @@ export class Wnd {
showMessage(window.siyuan.languages.uploading);
return;
}
this.removeTabAction(id, closeAll, animate, isSaveLayout);
this.removeTabAction(id, isBatchClose, animate, isSaveLayout);
} else {
this.removeTabAction(id, closeAll, animate, isSaveLayout);
this.removeTabAction(id, isBatchClose, animate, isSaveLayout);
}
return;
}

View file

@ -23,6 +23,7 @@ import {openHistory} from "../history/history";
import {newFile} from "../util/newFile";
import {mountHelp, newNotebook} from "../util/mount";
import {Constants} from "../constants";
import {fetchPost} from "../util/fetch";
export const getActiveTab = (wndActive = true) => {
const activeTabElement = document.querySelector(".layout__wnd--active .item--focus");
@ -359,29 +360,63 @@ export const copyTab = (app: App, tab: Tab) => {
});
};
export const closeTabByType = async (tab: Tab, type: "closeOthers" | "closeAll" | "other", tabs?: Tab[]) => {
export const closeTabByType = (tab: Tab, type: "closeOthers" | "closeAll" | "other", tabs?: Tab[]) => {
const tabsToClose: Tab[] = [];
if (type === "closeOthers") {
for (let index = 0; index < tab.parent.children.length; index++) {
if (tab.parent.children[index].id !== tab.id && !tab.parent.children[index].headElement.classList.contains("item--pin")) {
await tab.parent.children[index].parent.removeTab(tab.parent.children[index].id, true, false);
index--;
for (const item of tab.parent.children) {
if (item.id !== tab.id && !item.headElement.classList.contains("item--pin")) {
tabsToClose.push(item);
}
}
} else if (type === "closeAll") {
for (let index = 0; index < tab.parent.children.length; index++) {
if (!tab.parent.children[index].headElement.classList.contains("item--pin")) {
await tab.parent.children[index].parent.removeTab(tab.parent.children[index].id, true);
index--;
for (const item of tab.parent.children) {
if (!item.headElement.classList.contains("item--pin")) {
tabsToClose.push(item);
}
}
} else if (tabs.length > 0) {
for (let index = 0; index < tabs.length; index++) {
if (!tabs[index].headElement.classList.contains("item--pin")) {
await tabs[index].parent.removeTab(tabs[index].id);
} else if (tabs && tabs.length > 0) {
for (const item of tabs) {
if (!item.headElement.classList.contains("item--pin")) {
tabsToClose.push(item);
}
}
}
// 收集所有需要关闭的文档 rootID 并批量关闭页签
const rootIDs: string[] = [];
for (const item of tabsToClose) {
let rootID;
if (item.model instanceof Editor) {
rootID = item.model.editor.protyle.block.rootID;
} else if (!item.model) {
const initTab = item.headElement.getAttribute("data-initdata");
if (initTab) {
try {
const initTabData = JSON.parse(initTab);
if (initTabData && initTabData.instance === "Editor" && initTabData.rootId) {
rootID = initTabData.rootId;
}
} catch (e) {
console.warn("Failed to parse tab init data:", e);
}
}
}
if (rootID) {
rootIDs.push(rootID);
}
if (type === "closeOthers") {
item.parent.removeTab(item.id, true, false);
} else {
item.parent.removeTab(item.id, true);
}
}
// 批量更新文档关闭时间
if (rootIDs.length > 0) {
fetchPost("/api/storage/batchUpdateRecentDocCloseTime", {rootIDs});
}
if (tab.headElement.parentElement && !tab.headElement.parentElement.querySelector(".item--focus")) {
tab.parent.switchTab(tab.headElement, true);
} else if (tab.parent.children.length > 0) {