fix: trim zotero import modal shell

This commit is contained in:
Saberlve 2026-04-15 12:18:34 +08:00
parent 6a2250ef1a
commit 4db4632ac8

View File

@ -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 = '<div class="empty">没有读取到 Zotero collections。</div>';
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 = `
<strong>${node.name}</strong>
<div class="meta">${node.descendant_item_count} 篇</div>
`;
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 = '<div class="empty">先点击一个 collection。</div>';
return;
}
if (!state.visibleCollectionItems.length) {
elements.collectionItems.innerHTML = '<div class="empty">当前目录及子目录下没有可导入文献。</div>';
return;
}
elements.collectionItems.innerHTML = "";
for (const item of state.visibleCollectionItems) {
const row = document.createElement("label");
row.className = "collection-item";
row.innerHTML = `
<div class="checkbox-row">
<input type="checkbox" ${state.selectedItemKeys.has(item.item_key) ? "checked" : ""} />
<div>
<strong>${item.title}</strong>
<div class="meta">${item.item_key} · ${item.year || "-"} · ${item.item_type || "-"}</div>
<p>${item.abstract || "暂无摘要"}</p>
</div>
</div>
`;
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);
});
</script>
</body>