diff --git a/static/public/css/theme.css b/static/public/css/theme.css
index 0ace46c..1e4ccfe 100644
--- a/static/public/css/theme.css
+++ b/static/public/css/theme.css
@@ -327,6 +327,43 @@ input[type="button"]:disabled {
.act-btn:hover { border-color: var(--accent); }
.act-del { background: var(--error-bg); color: var(--error); }
.act-del:hover { border-color: var(--error); }
+.act-move { background: transparent; color: var(--fg-secondary); border-color: var(--border); }
+.act-move:hover { border-color: var(--accent); color: var(--accent-dark); }
+
+/* ===== Move Bar (dnfs 节点移动条) ===== */
+.movebar {
+ position: fixed;
+ left: 50%;
+ bottom: 1.25rem;
+ transform: translateX(-50%);
+ z-index: 110;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ padding: 0.5rem 0.8rem;
+ border-radius: var(--radius-md);
+ background: rgba(253, 246, 238, 0.85);
+ border: 1px solid var(--accent);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ box-shadow: var(--shadow-md);
+ font-size: 0.82rem;
+ max-width: 92vw;
+}
+@media (prefers-color-scheme: dark) {
+ .movebar { background: rgba(30, 23, 18, 0.85); }
+}
+.movebar:empty { display: none; }
+.move-title {
+ max-width: 180px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--fg-secondary);
+ font-weight: 600;
+}
/* ===== Minimap (dnfs) ===== */
.minimap {
diff --git a/static/public/dnfs.html b/static/public/dnfs.html
index 5ea84ae..56d0f4e 100644
--- a/static/public/dnfs.html
+++ b/static/public/dnfs.html
@@ -10,6 +10,7 @@
+
节点操作
diff --git a/static/public/js/dnfs.js b/static/public/js/dnfs.js
index aa27232..5336449 100644
--- a/static/public/js/dnfs.js
+++ b/static/public/js/dnfs.js
@@ -280,6 +280,110 @@ async function updateNode(id, pid, title, content) {
}
}
+/* by qing — 节点移动:本质是 pid 变更的 update_note,title/content 原样携带 */
+async function moveNode(id, pid, title, content) {
+ try {
+ obj = {
+ "id": Number(id),
+ "pid": pid,
+ "title": title,
+ "content": content
+ }
+ jsonBody = JSON.stringify(obj)
+ const res = await fetch('/update_note', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Target-Url': 'http://127.0.0.1:8006'
+ },
+ body: jsonBody
+ });
+
+ const responseText = await res.text();
+ let result;
+
+ try {
+ result = JSON.parse(responseText);
+ } catch {
+ console.error(`更新笔记接口返回非 JSON 内容(HTTP ${res.status}):`, responseText);
+ throw new Error('服务器响应格式异常,请稍后重试');
+ }
+
+ if (res.status === 401) {
+ // 凭证失效(过期或网关重启),跳转重新登录
+ location.href = '/login.html';
+ return;
+ }
+
+ if (!res.ok) {
+ throw new Error(result.error === "It's not your note" ? '只能移动自己的节点' : (result.error || `移动节点失败(HTTP ${res.status})`));
+ }
+
+ var cur = (stack.length == 0) ? 0 : stack[stack.length - 1];
+ getNode(cur).then(load_notes);
+ getNode(cur).then(loadMap);
+
+ alert('移动成功');
+ return result;
+
+ } catch (err) {
+ alert(err.message || '接口请求失败,请检查后端服务是否启动');
+ console.error('移动节点请求异常:', err);
+ }
+}
+
+/* by qing — 点击卡片"移动":暂存节点信息,底部弹出移动条 */
+function startMove(item) {
+ moving_node = { id: Number(item.id), pid: item.pid, title: item.title, content: item.content };
+ renderMoveBar();
+}
+
+/* by qing — 渲染底部移动条:显示待移动节点,提供"移动到当前层"和"取消" */
+function renderMoveBar() {
+ movebar.replaceChildren();
+ if (moving_node == null) {
+ return;
+ }
+ let tip = document.createElement('span');
+ tip.className = "move-title";
+ tip.textContent = "移动中:" + moving_node.title;
+ let ok_btn = document.createElement('button');
+ ok_btn.className = "act-btn act-edit";
+ ok_btn.textContent = "移动到当前层";
+ ok_btn.addEventListener('click', confirmMove);
+ let cancel_btn = document.createElement('button');
+ cancel_btn.className = "act-btn act-del";
+ cancel_btn.textContent = "取消";
+ cancel_btn.addEventListener('click', function() {
+ moving_node = null;
+ renderMoveBar();
+ });
+ movebar.appendChild(tip);
+ movebar.appendChild(ok_btn);
+ movebar.appendChild(cancel_btn);
+}
+
+/* by qing — 执行移动:把待移动节点挂到当前层(stack 顶,根为 0) */
+async function confirmMove() {
+ if (moving_node == null) {
+ return;
+ }
+ var target_pid = (stack.length == 0) ? 0 : stack[stack.length - 1];
+ if (target_pid == moving_node.id) {
+ alert('不能移动到节点自身内部');
+ return;
+ }
+ if (target_pid == moving_node.pid) {
+ alert('该节点已在当前层');
+ return;
+ }
+ var result = await moveNode(moving_node.id, target_pid, moving_node.title, moving_node.content);
+ if (result != null) {
+ moving_node = null;
+ renderMoveBar();
+ }
+}
+
/* by shufeng2012 */
function renderMarkdown(element, content) {
/*
@@ -295,6 +399,7 @@ const ICON_HEART = '