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);
});