diff --git a/src/zotero_kb/templates/index.html b/src/zotero_kb/templates/index.html index 8ce8f5a..e7ceef8 100644 --- a/src/zotero_kb/templates/index.html +++ b/src/zotero_kb/templates/index.html @@ -141,67 +141,6 @@ border-radius: 16px; color: var(--muted); } - .tree-node { - display: grid; - gap: 0.35rem; - } - .tree-row { - display: flex; - align-items: center; - gap: 0.45rem; - } - .tree-toggle { - width: 1.6rem; - min-width: 1.6rem; - height: 1.6rem; - padding: 0; - border-radius: 10px; - background: rgba(44, 110, 73, 0.08); - color: var(--accent-strong); - } - .tree-label { - width: 100%; - text-align: left; - border: 1px solid transparent; - border-radius: 12px; - padding: 0.55rem 0.7rem; - background: rgba(255, 255, 255, 0.6); - color: var(--ink); - } - .tree-label.active { - border-color: rgba(44, 110, 73, 0.22); - background: rgba(44, 110, 73, 0.12); - } - .tree-children { - display: grid; - gap: 0.35rem; - margin-left: 1rem; - padding-left: 0.6rem; - border-left: 1px dashed var(--line); - } - .collection-item { - display: grid; - gap: 0.35rem; - padding: 0.75rem; - border: 1px solid var(--line); - border-radius: 14px; - background: rgba(255, 255, 255, 0.62); - } - .checkbox-row { - display: flex; - align-items: flex-start; - gap: 0.6rem; - } - .checkbox-row input[type="checkbox"] { - width: auto; - margin-top: 0.3rem; - } - .action-bar { - display: grid; - gap: 0.55rem; - padding-top: 0.4rem; - border-top: 1px solid var(--line); - } .modal-shell[hidden] { display: none; } @@ -242,6 +181,10 @@ .modal-action-bar { border-top: 1px solid var(--line); border-bottom: 0; + position: sticky; + bottom: 0; + z-index: 2; + background: var(--surface); } .modal-header-actions { display: flex; @@ -439,25 +382,25 @@ projects: [], currentProjectId: null, currentCards: [], - collectionTree: [], isImportModalOpen: false, - selectedCollectionKey: null, - expandedCollectionKeys: new Set(), - selectedItemKeys: new Set(), - visibleCollectionItems: [], }; const elements = { projectForm: document.getElementById("create-project-form"), projectStatus: document.getElementById("project-status"), projectList: document.getElementById("project-list"), - collectionStatus: document.getElementById("collection-status"), - collectionTree: document.getElementById("collection-tree"), - collectionItems: document.getElementById("collection-items"), - selectedCount: document.getElementById("selected-count"), - selectDescendantsButton: document.getElementById("select-descendants-button"), - clearSelectionButton: document.getElementById("clear-selection-button"), - importSelectedItemsButton: document.getElementById("import-selected-items-button"), + importModal: document.getElementById("import-modal"), + importModalOverlay: document.getElementById("import-modal-overlay"), + openImportModalButton: document.getElementById("open-import-modal-button"), + closeImportModalButton: document.getElementById("close-import-modal-button"), + importModalProjectLabel: document.getElementById("import-modal-project-label"), + modalSelectedCount: document.getElementById("modal-selected-count"), + modalCollectionTree: document.getElementById("modal-collection-tree"), + modalCollectionItems: document.getElementById("modal-collection-items"), + modalSelectDescendantsButton: document.getElementById("modal-select-descendants-button"), + modalClearSelectionButton: document.getElementById("modal-clear-selection-button"), + modalImportSelectedItemsButton: document.getElementById("modal-import-selected-items-button"), + previewPopover: document.getElementById("item-preview-popover"), currentProjectLabel: document.getElementById("current-project-label"), cardList: document.getElementById("card-list"), cardDetail: document.getElementById("card-detail"), @@ -589,112 +532,6 @@ `).join(""); } - function renderCollectionTree() { - if (!state.collectionTree.length) { - elements.collectionTree.innerHTML = '
没有读取到 Zotero collections。
'; - return; - } - elements.collectionTree.innerHTML = ""; - for (const node of state.collectionTree) { - elements.collectionTree.appendChild(renderCollectionNode(node)); - } - } - - function renderCollectionNode(node) { - const wrapper = document.createElement("div"); - wrapper.className = "tree-node"; - - const row = document.createElement("div"); - row.className = "tree-row"; - - const toggle = document.createElement("button"); - toggle.type = "button"; - toggle.className = "tree-toggle"; - toggle.textContent = node.children && node.children.length - ? (state.expandedCollectionKeys.has(node.collection_key) ? "−" : "+") - : "·"; - toggle.disabled = !(node.children && node.children.length); - toggle.addEventListener("click", () => toggleCollectionExpanded(node.collection_key)); - - const label = document.createElement("button"); - label.type = "button"; - label.className = "tree-label" + (state.selectedCollectionKey === node.collection_key ? " active" : ""); - label.innerHTML = ` - ${node.name} -
${node.descendant_item_count} 篇
- `; - label.addEventListener("click", () => selectCollection(node.collection_key)); - - row.appendChild(toggle); - row.appendChild(label); - wrapper.appendChild(row); - - if (node.children && node.children.length && state.expandedCollectionKeys.has(node.collection_key)) { - const children = document.createElement("div"); - children.className = "tree-children"; - for (const child of node.children) { - children.appendChild(renderCollectionNode(child)); - } - wrapper.appendChild(children); - } - return wrapper; - } - - function renderCollectionItems() { - renderSelectedCount(); - if (!state.selectedCollectionKey) { - elements.collectionItems.innerHTML = '
先点击一个 collection。
'; - return; - } - if (!state.visibleCollectionItems.length) { - elements.collectionItems.innerHTML = '
当前目录及子目录下没有可导入文献。
'; - return; - } - - elements.collectionItems.innerHTML = ""; - for (const item of state.visibleCollectionItems) { - const row = document.createElement("label"); - row.className = "collection-item"; - row.innerHTML = ` -
- -
- ${item.title} -
${item.item_key} · ${item.year || "-"} · ${item.item_type || "-"}
-

${item.abstract || "暂无摘要"}

-
-
- `; - const checkbox = row.querySelector('input[type="checkbox"]'); - checkbox.addEventListener("change", (event) => { - toggleItemSelection(item.item_key, event.target.checked); - }); - elements.collectionItems.appendChild(row); - } - } - - function renderSelectedCount() { - elements.selectedCount.textContent = `已选 ${state.selectedItemKeys.size} 篇`; - } - - function toggleItemSelection(itemKey, checked) { - if (checked) { - state.selectedItemKeys.add(itemKey); - } else { - state.selectedItemKeys.delete(itemKey); - } - renderSelectedCount(); - } - - function toggleCollectionExpanded(collectionKey) { - if (state.expandedCollectionKeys.has(collectionKey)) { - state.expandedCollectionKeys.delete(collectionKey); - } else { - state.expandedCollectionKeys.add(collectionKey); - } - renderCollectionTree(); - } - async function loadProjects() { const projects = await api("/api/projects"); state.projects = projects; @@ -725,29 +562,6 @@ await selectProject(state.currentProjectId); } - async function loadCollectionTree() { - const payload = await api("/api/zotero/collections/tree"); - state.collectionTree = payload.collections || []; - if (!state.selectedCollectionKey && state.collectionTree.length) { - state.selectedCollectionKey = state.collectionTree[0].collection_key; - state.expandedCollectionKeys.add(state.selectedCollectionKey); - } - renderCollectionTree(); - if (state.selectedCollectionKey) { - await selectCollection(state.selectedCollectionKey, false); - } - } - - async function selectCollection(collectionKey, rerenderTree = true) { - state.selectedCollectionKey = collectionKey; - const payload = await api(`/api/zotero/collections/${collectionKey}/items?include_descendants=true`); - state.visibleCollectionItems = payload.items || []; - if (rerenderTree) { - renderCollectionTree(); - } - renderCollectionItems(); - } - elements.projectForm.addEventListener("submit", async (event) => { event.preventDefault(); const formData = new FormData(elements.projectForm); @@ -766,25 +580,6 @@ } }); - async function importItemKeys(itemKeys) { - if (!state.currentProjectId) { - setStatus(elements.collectionStatus, "请先选择项目。", true); - return; - } - try { - const payload = await api(`/api/projects/${state.currentProjectId}/imports/item-keys`, { - method: "POST", - body: JSON.stringify({ item_keys: itemKeys }), - }); - setStatus(elements.collectionStatus, `已导入 ${payload.imported_item_keys.length} 篇文献。`); - renderCards(payload.project_view.cards || []); - state.selectedItemKeys.clear(); - renderCollectionItems(); - } catch (error) { - setStatus(elements.collectionStatus, error.message, true); - } - } - elements.reloadCardsButton.addEventListener("click", async () => { if (!state.currentProjectId) { return; @@ -794,22 +589,6 @@ elements.refreshProjectsButton.addEventListener("click", loadProjects); - elements.selectDescendantsButton.addEventListener("click", () => { - for (const item of state.visibleCollectionItems) { - state.selectedItemKeys.add(item.item_key); - } - renderCollectionItems(); - }); - - elements.clearSelectionButton.addEventListener("click", () => { - state.selectedItemKeys.clear(); - renderCollectionItems(); - }); - - elements.importSelectedItemsButton.addEventListener("click", async () => { - await importItemKeys(Array.from(state.selectedItemKeys)); - }); - elements.recommendForm.addEventListener("submit", async (event) => { event.preventDefault(); if (!state.currentProjectId) { @@ -859,9 +638,8 @@ elements.recommendTab.addEventListener("click", () => activateTab("recommend")); elements.planTab.addEventListener("click", () => activateTab("plan")); - Promise.all([loadProjects(), loadCollectionTree()]).catch((error) => { + loadProjects().catch((error) => { setStatus(elements.projectStatus, error.message, true); - setStatus(elements.collectionStatus, error.message, true); });