fix: trim zotero import modal shell
This commit is contained in:
parent
6a2250ef1a
commit
4db4632ac8
@ -141,67 +141,6 @@
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
color: var(--muted);
|
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] {
|
.modal-shell[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -242,6 +181,10 @@
|
|||||||
.modal-action-bar {
|
.modal-action-bar {
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
.modal-header-actions {
|
.modal-header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -439,25 +382,25 @@
|
|||||||
projects: [],
|
projects: [],
|
||||||
currentProjectId: null,
|
currentProjectId: null,
|
||||||
currentCards: [],
|
currentCards: [],
|
||||||
collectionTree: [],
|
|
||||||
isImportModalOpen: false,
|
isImportModalOpen: false,
|
||||||
selectedCollectionKey: null,
|
|
||||||
expandedCollectionKeys: new Set(),
|
|
||||||
selectedItemKeys: new Set(),
|
|
||||||
visibleCollectionItems: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
projectForm: document.getElementById("create-project-form"),
|
projectForm: document.getElementById("create-project-form"),
|
||||||
projectStatus: document.getElementById("project-status"),
|
projectStatus: document.getElementById("project-status"),
|
||||||
projectList: document.getElementById("project-list"),
|
projectList: document.getElementById("project-list"),
|
||||||
collectionStatus: document.getElementById("collection-status"),
|
importModal: document.getElementById("import-modal"),
|
||||||
collectionTree: document.getElementById("collection-tree"),
|
importModalOverlay: document.getElementById("import-modal-overlay"),
|
||||||
collectionItems: document.getElementById("collection-items"),
|
openImportModalButton: document.getElementById("open-import-modal-button"),
|
||||||
selectedCount: document.getElementById("selected-count"),
|
closeImportModalButton: document.getElementById("close-import-modal-button"),
|
||||||
selectDescendantsButton: document.getElementById("select-descendants-button"),
|
importModalProjectLabel: document.getElementById("import-modal-project-label"),
|
||||||
clearSelectionButton: document.getElementById("clear-selection-button"),
|
modalSelectedCount: document.getElementById("modal-selected-count"),
|
||||||
importSelectedItemsButton: document.getElementById("import-selected-items-button"),
|
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"),
|
currentProjectLabel: document.getElementById("current-project-label"),
|
||||||
cardList: document.getElementById("card-list"),
|
cardList: document.getElementById("card-list"),
|
||||||
cardDetail: document.getElementById("card-detail"),
|
cardDetail: document.getElementById("card-detail"),
|
||||||
@ -589,112 +532,6 @@
|
|||||||
`).join("");
|
`).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() {
|
async function loadProjects() {
|
||||||
const projects = await api("/api/projects");
|
const projects = await api("/api/projects");
|
||||||
state.projects = projects;
|
state.projects = projects;
|
||||||
@ -725,29 +562,6 @@
|
|||||||
await selectProject(state.currentProjectId);
|
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) => {
|
elements.projectForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const formData = new FormData(elements.projectForm);
|
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 () => {
|
elements.reloadCardsButton.addEventListener("click", async () => {
|
||||||
if (!state.currentProjectId) {
|
if (!state.currentProjectId) {
|
||||||
return;
|
return;
|
||||||
@ -794,22 +589,6 @@
|
|||||||
|
|
||||||
elements.refreshProjectsButton.addEventListener("click", loadProjects);
|
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) => {
|
elements.recommendForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!state.currentProjectId) {
|
if (!state.currentProjectId) {
|
||||||
@ -859,9 +638,8 @@
|
|||||||
elements.recommendTab.addEventListener("click", () => activateTab("recommend"));
|
elements.recommendTab.addEventListener("click", () => activateTab("recommend"));
|
||||||
elements.planTab.addEventListener("click", () => activateTab("plan"));
|
elements.planTab.addEventListener("click", () => activateTab("plan"));
|
||||||
|
|
||||||
Promise.all([loadProjects(), loadCollectionTree()]).catch((error) => {
|
loadProjects().catch((error) => {
|
||||||
setStatus(elements.projectStatus, error.message, true);
|
setStatus(elements.projectStatus, error.message, true);
|
||||||
setStatus(elements.collectionStatus, error.message, true);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user