feat: 页面列表支持拖拽排序(order字段+order接口), 持久化到后端
This commit is contained in:
+51
-6
@@ -132,7 +132,10 @@ INDEX_HTML = r"""<!DOCTYPE html>
|
|||||||
button:hover,.btn:hover{opacity:.9}
|
button:hover,.btn:hover{opacity:.9}
|
||||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px}
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px}
|
||||||
.tile{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:16px;
|
.tile{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:16px;
|
||||||
text-decoration:none;color:var(--text);display:block;transition:.16s;box-shadow:0 2px 10px rgba(20,30,70,.05)}
|
text-decoration:none;color:var(--text);display:block;transition:.16s;box-shadow:0 2px 10px rgba(20,30,70,.05);
|
||||||
|
cursor:grab}
|
||||||
|
.tile:active{cursor:grabbing}
|
||||||
|
.tile.drag-over{border-color:var(--brand);box-shadow:0 0 0 2px rgba(99,102,241,.25)}
|
||||||
.tile:hover{transform:translateY(-2px);box-shadow:0 10px 24px rgba(20,30,70,.1)}
|
.tile:hover{transform:translateY(-2px);box-shadow:0 10px 24px rgba(20,30,70,.1)}
|
||||||
.tile h3{font-size:16px;margin-bottom:6px}
|
.tile h3{font-size:16px;margin-bottom:6px}
|
||||||
.tile p{font-size:13px;color:var(--muted);line-height:1.5;
|
.tile p{font-size:13px;color:var(--muted);line-height:1.5;
|
||||||
@@ -218,13 +221,32 @@ function renderList(){
|
|||||||
<button onclick="logout()">👤 退出</button>`;
|
<button onclick="logout()">👤 退出</button>`;
|
||||||
const a=$('#app');
|
const a=$('#app');
|
||||||
if(!state.pages.length){ a.innerHTML=`<div class="empty">还没有内容。<br><button class="primary" onclick="newPage()">创建第一个页面</button></div>`; return; }
|
if(!state.pages.length){ a.innerHTML=`<div class="empty">还没有内容。<br><button class="primary" onclick="newPage()">创建第一个页面</button></div>`; return; }
|
||||||
a.innerHTML = `<div class="grid">${state.pages.map(p=>`
|
a.innerHTML = `<div class="grid" ondrop="dropEv(event)" ondragover="event.preventDefault()">${state.pages.map(p=>`
|
||||||
<a class="tile" onclick="openPage('${p.id}')">
|
<a class="tile" draggable="true" data-id="${p.id}" ondragstart="dragStart('${p.id}')" ondragover="event.preventDefault()" onclick="openPage('${p.id}')">
|
||||||
<h3>${esc(p.title||'未命名')}</h3>
|
<h3>${esc(p.title||'未命名')}</h3>
|
||||||
<p>${esc(p.summary||'')}</p>
|
<p>${esc(p.summary||'')}</p>
|
||||||
<time>${fmt(p.updated)}</time>
|
<time>${fmt(p.updated)}</time>
|
||||||
</a>`).join('')}</div>`;
|
</a>`).join('')}</div>`;
|
||||||
}
|
}
|
||||||
|
let dragId=null;
|
||||||
|
function dragStart(id){ dragId=id; }
|
||||||
|
function dropEv(ev){
|
||||||
|
ev.preventDefault();
|
||||||
|
if(!dragId) return;
|
||||||
|
const tile=ev.target.closest('.tile');
|
||||||
|
if(!tile) return;
|
||||||
|
const tid=tile.getAttribute('data-id');
|
||||||
|
if(!tid || tid===dragId){ dragId=null; return; }
|
||||||
|
const from=state.pages.findIndex(p=>p.id===dragId);
|
||||||
|
const to=state.pages.findIndex(p=>p.id===tid);
|
||||||
|
if(from<0 || to<0){ dragId=null; return; }
|
||||||
|
const [moved]=state.pages.splice(from,1);
|
||||||
|
state.pages.splice(to,0,moved);
|
||||||
|
dragId=null;
|
||||||
|
renderList();
|
||||||
|
api('/api/pages/order',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({order:state.pages.map(p=>p.id)})})
|
||||||
|
.catch(e=>toast('排序保存失败: '+e.message));
|
||||||
|
}
|
||||||
async function openPage(id){
|
async function openPage(id){
|
||||||
try{
|
try{
|
||||||
const p = await api('/api/pages/'+encodeURIComponent(id));
|
const p = await api('/api/pages/'+encodeURIComponent(id));
|
||||||
@@ -475,6 +497,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
return self._logout()
|
return self._logout()
|
||||||
if not self._user():
|
if not self._user():
|
||||||
return self._auth_denied()
|
return self._auth_denied()
|
||||||
|
if path == "/api/pages/order":
|
||||||
|
return self._reorder()
|
||||||
if path == "/api/pages":
|
if path == "/api/pages":
|
||||||
return self._create_page()
|
return self._create_page()
|
||||||
if path == "/api/upload":
|
if path == "/api/upload":
|
||||||
@@ -514,9 +538,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
pages = _read_pages()
|
pages = _read_pages()
|
||||||
out = [{"id": pid, "title": p.get("title", ""),
|
out = [{"id": pid, "title": p.get("title", ""),
|
||||||
"summary": _summary(p.get("content", "")),
|
"summary": _summary(p.get("content", "")),
|
||||||
"updated": p.get("updated")}
|
"updated": p.get("updated"),
|
||||||
|
"order": p.get("order", 1 << 30)}
|
||||||
for pid, p in pages.items()]
|
for pid, p in pages.items()]
|
||||||
out.sort(key=lambda x: x["updated"] or "", reverse=True)
|
out.sort(key=lambda x: x.get("order", 1 << 30))
|
||||||
self._json(out)
|
self._json(out)
|
||||||
|
|
||||||
def _get_page(self, pid):
|
def _get_page(self, pid):
|
||||||
@@ -535,9 +560,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
pages = _read_pages()
|
pages = _read_pages()
|
||||||
pid = uuid.uuid4().hex[:12]
|
pid = uuid.uuid4().hex[:12]
|
||||||
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||||
|
nxt = max((p.get("order", 0) for p in pages.values()), default=-1) + 1
|
||||||
pages[pid] = {"title": data["title"].strip(),
|
pages[pid] = {"title": data["title"].strip(),
|
||||||
"content": data.get("content", ""),
|
"content": data.get("content", ""),
|
||||||
"created": now, "updated": now}
|
"created": now, "updated": now, "order": nxt}
|
||||||
_write_pages(pages)
|
_write_pages(pages)
|
||||||
self._json({"id": pid}, 201)
|
self._json({"id": pid}, 201)
|
||||||
|
|
||||||
@@ -566,6 +592,25 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
_write_pages(pages)
|
_write_pages(pages)
|
||||||
self._json({"ok": True})
|
self._json({"ok": True})
|
||||||
|
|
||||||
|
def _reorder(self):
|
||||||
|
data = self._read_json()
|
||||||
|
order = data.get("order") if isinstance(data, dict) else None
|
||||||
|
if not isinstance(order, list):
|
||||||
|
return self._json({"error": "bad order"}, 400)
|
||||||
|
with _lock:
|
||||||
|
pages = _read_pages()
|
||||||
|
for i, pid in enumerate(order):
|
||||||
|
if pid in pages:
|
||||||
|
pages[pid]["order"] = i
|
||||||
|
# 未在列表中的页面补一个靠后的 order
|
||||||
|
have = set(order)
|
||||||
|
big = len(order)
|
||||||
|
for pid in pages:
|
||||||
|
if pid not in have:
|
||||||
|
pages[pid]["order"] = pages[pid].get("order", big)
|
||||||
|
_write_pages(pages)
|
||||||
|
self._json({"ok": True})
|
||||||
|
|
||||||
# ---- 图片 ----
|
# ---- 图片 ----
|
||||||
def _upload(self):
|
def _upload(self):
|
||||||
data = self._read_json()
|
data = self._read_json()
|
||||||
|
|||||||
Reference in New Issue
Block a user