From b538239fe43843a62801c8469985822bd2ae0b4c Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 7 Aug 2026 21:25:48 +0800 Subject: [PATCH] =?UTF-8?q?www:=20=E9=A1=B9=E7=9B=AE=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20-=20=E5=B7=A6=E4=BE=A7=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E7=AD=9B=E9=80=89(=E5=85=A8=E9=83=A8/?= =?UTF-8?q?=E6=9C=AA=E5=BD=92=E7=B1=BB/=E5=88=86=E7=B1=BB)=E3=80=81?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=BC=B9=E7=AA=97=E6=94=BE=E5=A4=A7+?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E4=B8=8B=E6=8B=89=E3=80=81=E5=8F=B3=E4=B8=8A?= =?UTF-8?q?=E8=A7=92=E6=96=B0=E5=BB=BA=E5=88=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- www/index.html | 113 +++++++++++++++++++++++++++++++++++++++---------- www/server.py | 59 ++++++++++++++++++++++++-- 2 files changed, 145 insertions(+), 27 deletions(-) diff --git a/www/index.html b/www/index.html index c4ac941..5bb6c88 100644 --- a/www/index.html +++ b/www/index.html @@ -16,27 +16,37 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica N .btn-outline{background:transparent;border:1px solid #667eea;color:#667eea} .btn-outline:hover{background:#667eea;color:#fff} .btn-sm{padding:6px 14px;font-size:13px} -.main{padding:90px 24px 40px;max-width:1400px;margin:0 auto} +.main{padding:90px 24px 40px;max-width:1600px;margin:0 auto} +.layout{display:flex;gap:24px;align-items:flex-start} +.content{flex:1;min-width:0} +.sidebar{width:200px;flex-shrink:0;position:sticky;top:84px;background:rgba(255,255,255,.95);border-radius:16px;padding:12px;box-shadow:0 6px 14px rgba(0,0,0,.12);max-height:calc(100vh - 100px);overflow-y:auto} +.sidebar-item{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:10px;cursor:pointer;color:#374151;font-size:.92rem;font-weight:500;transition:background .15s,color .15s;margin-bottom:2px} +.sidebar-item:hover{background:#eef0ff;color:#667eea} +.sidebar-item.active{background:#667eea;color:#fff} +.sidebar-icon{font-size:.85rem;width:18px;text-align:center;flex-shrink:0} +.sidebar-count{margin-left:auto;font-size:.75rem;background:#e5e7eb;color:#6b7280;border-radius:10px;padding:1px 8px} +.sidebar-item.active .sidebar-count{background:rgba(255,255,255,.25);color:#fff} +.sidebar-divider{height:1px;background:#e5e7eb;margin:8px 4px} .section-title{font-size:1.5rem;font-weight:600;color:#fff;margin-bottom:24px;text-align:center;text-shadow:0 2px 4px rgba(0,0,0,.2)} -.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:24px} -@media(max-width:1200px){.grid{grid-template-columns:repeat(3,1fr)}} -@media(max-width:900px){.grid{grid-template-columns:repeat(2,1fr)}} -@media(max-width:600px){.grid{grid-template-columns:1fr}} -.card{background:#fff;border-radius:20px;padding:24px;display:flex;align-items:center;gap:16px;cursor:pointer;position:relative;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);transition:transform .1s ease,box-shadow .2s ease;transform-style:preserve-3d;will-change:transform} -.card:hover{box-shadow:0 20px 25px -5px rgba(0,0,0,.15),0 10px 10px -5px rgba(0,0,0,.04)} -.card-icon{width:56px;height:56px;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:transform .3s ease} +.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:20px} +.card{background:#fff;border-radius:16px;padding:20px;display:flex;align-items:center;gap:14px;cursor:pointer;position:relative;box-shadow:0 6px 14px rgba(0,0,0,.12),0 2px 4px rgba(0,0,0,.08);transition:transform .1s ease,box-shadow .2s ease;transform-style:preserve-3d;will-change:transform} +.card:hover{box-shadow:0 14px 24px rgba(0,0,0,.18),0 4px 8px rgba(0,0,0,.1)} +.card-icon{width:48px;height:48px;border-radius:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:transform .3s ease} .card:hover .card-icon{transform:scale(1.1)} -.card-icon svg{width:28px;height:28px;stroke:#fff;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round} +.card-icon svg{width:24px;height:24px;stroke:#fff;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round} .card-body{flex:1;min-width:0} -.card-title{font-size:1.05rem;font-weight:600;color:#1a1a2e;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.card-desc{font-size:.85rem;color:#6b7280;line-height:1.4;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical} +.card-title{font-size:.95rem;font-weight:600;color:#1a1a2e;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.card-desc{font-size:.8rem;color:#6b7280;line-height:1.4;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical} .card-close{position:absolute;top:12px;right:12px;width:24px;height:24px;border-radius:50%;background:#ef4444;color:#fff;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;line-height:1;opacity:0;transform:scale(.8);transition:all .2s ease;z-index:10} .card-close:hover{background:#dc2626;transform:scale(1.1)} .card:hover .card-close{opacity:1;transform:scale(1)} +.card-edit{position:absolute;top:12px;right:40px;width:24px;height:24px;border-radius:50%;background:#667eea;color:#fff;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;line-height:1;opacity:0;transform:scale(.8);transition:all .2s ease;z-index:10} +.card-edit:hover{background:#5a6fd6;transform:scale(1.1)} +.card:hover .card-edit{opacity:1;transform:scale(1)} .empty{text-align:center;color:rgba(255,255,255,.7);padding:60px 20px;font-size:1.1rem} .modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);backdrop-filter:blur(4px);display:none;align-items:center;justify-content:center;z-index:200} .modal-overlay.show{display:flex} -.modal{background:#fff;border-radius:16px;padding:28px;width:100%;max-width:400px;margin:20px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);animation:modalIn .3s ease} +.modal{background:#fff;border-radius:16px;padding:28px;width:100%;max-width:520px;margin:20px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);animation:modalIn .3s ease} @keyframes modalIn{from{opacity:0;transform:translateY(-20px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}} .modal-header{font-size:1.2rem;font-weight:600;margin-bottom:20px;color:#1a1a2e} .modal-body{display:flex;flex-direction:column;gap:14px} @@ -44,6 +54,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica N .input-group label{font-size:.85rem;color:#6b7280;font-weight:500} .input-group input{padding:10px 14px;border:1px solid #e5e7eb;border-radius:10px;font-size:14px;transition:border-color .2s,box-shadow .2s;outline:none} .input-group input:focus{border-color:#667eea;box-shadow:0 0 0 3px rgba(102,126,234,.1)} +.input-group select{padding:10px 14px;border:1px solid #e5e7eb;border-radius:10px;font-size:14px;background:#fff;color:#374151;outline:none;transition:border-color .2s,box-shadow .2s} +.input-group select:focus{border-color:#667eea;box-shadow:0 0 0 3px rgba(102,126,234,.1)} +.card-badge{display:inline-block;margin-top:8px;padding:2px 10px;border-radius:10px;font-size:.72rem;background:#eef0ff;color:#667eea;font-weight:600} +.modal-lg{max-width:640px} +.sidebar-label{font-size:.75rem;color:#9ca3af;font-weight:600;padding:8px 12px 4px;letter-spacing:.5px} .modal-footer{display:flex;gap:10px;justify-content:flex-end;margin-top:20px} .btn-secondary{background:#f3f4f6;color:#374151} .btn-secondary:hover{background:#e5e7eb} @@ -65,9 +80,14 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica N
-
我的项目导航
-
- +
+ +
+
我的项目导航
+
+ +
+
@@ -78,6 +98,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica N + + + + + + diff --git a/www/server.py b/www/server.py index d6197ca..a66b834 100644 --- a/www/server.py +++ b/www/server.py @@ -29,6 +29,16 @@ def init_db(): url TEXT NOT NULL, desc TEXT DEFAULT '', icon TEXT DEFAULT '', + category TEXT DEFAULT '', + sort_order INTEGER DEFAULT 0 + )""") + # 兼容迁移:旧库补充 category 列 + cols = [r[1] for r in conn.execute("PRAGMA table_info(links)").fetchall()] + if "category" not in cols: + conn.execute("ALTER TABLE links ADD COLUMN category TEXT DEFAULT ''") + conn.execute("""CREATE TABLE IF NOT EXISTS categories ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, sort_order INTEGER DEFAULT 0 )""") # 首次启动:如果表为空则写入默认数据 @@ -91,23 +101,50 @@ class LinkCreate(BaseModel): url: str desc: str = "" icon: str = "" + category: str = "" + +class CategoryCreate(BaseModel): + name: str @app.get("/api/links") def list_links(): with get_db() as conn: - rows = conn.execute("SELECT id,name,url,desc,icon FROM links ORDER BY sort_order, id").fetchall() + rows = conn.execute("SELECT id,name,url,desc,icon,category FROM links ORDER BY sort_order, id").fetchall() return [dict(r) for r in rows] +@app.get("/api/categories") +def list_categories(): + with get_db() as conn: + rows = conn.execute("SELECT id,name FROM categories ORDER BY sort_order, id").fetchall() + return [dict(r) for r in rows] + +@app.post("/api/categories") +def add_category(cat: CategoryCreate, username: str = Depends(require_sso)): + import time, random + name = cat.name.strip() + if not name: + raise HTTPException(400, "分类名称不能为空") + cid = f"c{int(time.time()):x}{random.randint(0,0xffffff):06x}" + with get_db() as conn: + try: + max_sort = conn.execute("SELECT COALESCE(MAX(sort_order),0) FROM categories").fetchone()[0] + conn.execute("INSERT INTO categories (id,name,sort_order) VALUES (?,?,?)", + (cid, name, max_sort + 1)) + conn.commit() + except sqlite3.IntegrityError: + raise HTTPException(400, "分类已存在") + return {"id": cid, "name": name} + @app.post("/api/links") def add_link(link: LinkCreate, username: str = Depends(require_sso)): import time, random lid = f"{int(time.time()):x}{random.randint(0,0xffffff):06x}" with get_db() as conn: max_sort = conn.execute("SELECT COALESCE(MAX(sort_order),0) FROM links").fetchone()[0] - conn.execute("INSERT INTO links (id,name,url,desc,icon,sort_order) VALUES (?,?,?,?,?,?)", - (lid, link.name, link.url, link.desc, link.icon, max_sort + 1)) + conn.execute("INSERT INTO links (id,name,url,desc,icon,category,sort_order) VALUES (?,?,?,?,?,?,?)", + (lid, link.name, link.url, link.desc, link.icon, link.category, max_sort + 1)) conn.commit() - return {"id": lid, "name": link.name, "url": link.url, "desc": link.desc, "icon": link.icon} + return {"id": lid, "name": link.name, "url": link.url, "desc": link.desc, "icon": link.icon, "category": link.category} @app.delete("/api/links/{link_id}") def delete_link(link_id: str, username: str = Depends(require_sso)): @@ -118,6 +155,20 @@ def delete_link(link_id: str, username: str = Depends(require_sso)): raise HTTPException(404, "链接不存在") return {"success": True} +@app.put("/api/links/{link_id}") +def update_link(link_id: str, link: LinkCreate, username: str = Depends(require_sso)): + with get_db() as conn: + row = conn.execute("SELECT * FROM links WHERE id=?", (link_id,)).fetchone() + if not row: + raise HTTPException(404, "链接不存在") + # 图标不传则保留原值,避免改名称时图标被重新生成 + icon = link.icon if link.icon else row["icon"] + # 分类始终按提交值更新(空字符串 = 未归类) + conn.execute("UPDATE links SET name=?, url=?, desc=?, icon=?, category=? WHERE id=?", + (link.name, link.url, link.desc, icon, link.category, link_id)) + conn.commit() + return {"id": link_id, "name": link.name, "url": link.url, "desc": link.desc, "icon": icon, "category": link.category} + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8887)