feat: '我的' tab - user content management with update/delete

This commit is contained in:
2026-07-26 21:46:24 +08:00
parent 59887d7d46
commit b9359b3b47
2 changed files with 132 additions and 17 deletions
+38
View File
@@ -705,3 +705,41 @@ async def api_market_install_scene(id: str = "", filename: str = ""):
return {"ok": ok, "missing_dashboards": missing} return {"ok": ok, "missing_dashboards": missing}
except Exception as e: except Exception as e:
return {"ok": False, "error": str(e)} return {"ok": False, "error": str(e)}
@router.get("/market/mine")
async def api_market_mine(token: str = ""):
import httpx
cfg = get_config()
url = cfg.get("market_url", "http://127.0.0.1:5301")
result = {"dashboards": [], "scenes": []}
try:
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(f"{url}/api/collections/dashboards/records?sort=-created&perPage=100",
headers={"Authorization": f"Bearer {token}"})
result["dashboards"] = r.json().get("items", [])
r = await client.get(f"{url}/api/collections/scenes/records?sort=-created&perPage=100",
headers={"Authorization": f"Bearer {token}"})
result["scenes"] = r.json().get("items", [])
except Exception:
pass
return result
@router.post("/market/delete")
async def api_market_delete(data: dict):
import httpx
cfg = get_config()
url = cfg.get("market_url", "http://127.0.0.1:5301")
try:
async with httpx.AsyncClient(timeout=10) as client:
collection = data.get("collection", "dashboards")
record_id = data.get("id", "")
token = data.get("token", "")
resp = await client.delete(
f"{url}/api/collections/{collection}/records/{record_id}",
headers={"Authorization": f"Bearer {token}"}
)
return {"ok": resp.status_code < 400}
except Exception as e:
return {"ok": False, "error": str(e)}
+80 -3
View File
@@ -20,6 +20,14 @@ const PageMarket = {
if (!grid) return; if (!grid) return;
grid.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-tertiary);">加载中...</div>'; grid.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-tertiary);">加载中...</div>';
if (this._tab === 'mine') {
if (!this._marketToken) {
grid.innerHTML = this._emptyHtml('请先登录');
return;
}
return this._loadMine(grid);
}
const isScene = this._tab === 'scenes'; const isScene = this._tab === 'scenes';
const endpoint = isScene ? '/api/market/scenes' : '/api/market/dashboards'; const endpoint = isScene ? '/api/market/scenes' : '/api/market/dashboards';
try { try {
@@ -27,7 +35,30 @@ const PageMarket = {
const data = await res.json(); const data = await res.json();
const items = data.items || []; const items = data.items || [];
if (!items.length) { grid.innerHTML = this._emptyHtml(isScene ? '暂无场景' : '暂无仪表盘'); return; } if (!items.length) { grid.innerHTML = this._emptyHtml(isScene ? '暂无场景' : '暂无仪表盘'); return; }
grid.innerHTML = items.map(d => { grid.innerHTML = items.map(d => this._cardHtml(d, isScene)).join('');
} catch (e) { grid.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">加载失败</div>'; }
},
async _loadMine(grid) {
try {
const res = await fetch('/api/market/mine?token=' + encodeURIComponent(this._marketToken));
const data = await res.json();
if (!data.dashboards?.length && !data.scenes?.length) {
grid.innerHTML = this._emptyHtml('你还没有上传任何内容');
return;
}
let html = '';
if (data.dashboards?.length) {
html += '<h3 style="margin-bottom:10px;font-size:16px;color:var(--text-secondary)">我的仪表盘</h3>' + data.dashboards.map(d => this._myCardHtml(d, 'dashboards')).join('');
}
if (data.scenes?.length) {
html += '<h3 style="margin:20px 0 10px;font-size:16px;color:var(--text-secondary)">我的场景</h3>' + data.scenes.map(d => this._myCardHtml(d, 'scenes')).join('');
}
grid.innerHTML = html;
} catch (e) { grid.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">加载失败</div>'; }
},
_cardHtml(d, isScene) {
const icon = isScene ? '🎬' : (d.icon || '📦'); const icon = isScene ? '🎬' : (d.icon || '📦');
const btnLabel = isScene ? '安装场景' : '安装'; const btnLabel = isScene ? '安装场景' : '安装';
const btnClass = isScene ? 'btn-m-install-scene' : 'btn-m-install'; const btnClass = isScene ? 'btn-m-install-scene' : 'btn-m-install';
@@ -43,8 +74,19 @@ const PageMarket = {
</div> </div>
<button class="btn btn-sm btn-primary ${btnClass}" data-id="${d.id}" data-file="${d.file||''}" style="margin-top:8px;width:100%">${btnLabel}</button> <button class="btn btn-sm btn-primary ${btnClass}" data-id="${d.id}" data-file="${d.file||''}" style="margin-top:8px;width:100%">${btnLabel}</button>
</div></div>`; </div></div>`;
}).join(''); },
} catch (e) { grid.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">加载失败</div>'; }
_myCardHtml(d, collection) {
const pending = d.dashb_pending ? '<span class="badge badge-warning">待审核</span>' : '<span class="badge badge-success">已通过</span>';
return `<div class="glass-card" style="padding:12px 16px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap">
<div style="flex:1;min-width:150px">
<div style="font-weight:600;font-size:14px">${d.name||'Unnamed'}</div>
<div style="font-size:12px;color:var(--text-tertiary)">${d.author||''} · ${d.downloads||0} 次下载 ${pending}</div>
</div>
<div style="display:flex;gap:6px">
<button class="btn btn-sm btn-secondary btn-my-update" data-id="${d.id}" data-coll="${collection}" data-file="${d.file||''}" data-type="${collection==='scenes'?'.tss':'.tsd'}">更新</button>
<button class="btn btn-sm btn-danger btn-my-delete" data-id="${d.id}" data-coll="${collection}">删除</button>
</div></div>`;
}, },
_bindEvents() { _bindEvents() {
@@ -53,6 +95,32 @@ const PageMarket = {
document.getElementById('market-grid')?.addEventListener('click', async (e) => { document.getElementById('market-grid')?.addEventListener('click', async (e) => {
const btnInstall = e.target.closest('.btn-m-install'); const btnInstall = e.target.closest('.btn-m-install');
const btnScene = e.target.closest('.btn-m-install-scene'); const btnScene = e.target.closest('.btn-m-install-scene');
const btnUpdate = e.target.closest('.btn-my-update');
const btnDelete = e.target.closest('.btn-my-delete');
if (btnUpdate) {
const input = document.createElement('input'); input.type = 'file'; input.accept = btnUpdate.dataset.type;
input.onchange = async (ev) => {
const file = ev.target.files[0]; if (!file) return;
const fd = new FormData(); fd.append('file', file); fd.append('token', this._marketToken);
const url = btnUpdate.dataset.coll === 'scenes' ? '/api/market/scenes/upload' : '/api/market/upload';
const res = await fetch(url, { method: 'POST', body: fd });
const data = await res.json();
Toast.show(data.error ? '更新失败: ' + data.error : '上传成功(新记录)', data.error ? 'error' : 'success');
if (!data.error) this._loadItems();
}; input.click();
return;
}
if (btnDelete) {
if (!confirm('确定删除?')) return;
try {
await fetch('/api/market/delete', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({id: btnDelete.dataset.id, collection: btnDelete.dataset.coll, token: this._marketToken}) });
Toast.show('已删除', 'info');
this._loadItems();
} catch (e) { Toast.show('删除失败', 'error'); }
return;
}
const id = (btnInstall || btnScene)?.dataset.id; const id = (btnInstall || btnScene)?.dataset.id;
const file = (btnInstall || btnScene)?.dataset.file; const file = (btnInstall || btnScene)?.dataset.file;
if (!id || !file) return; if (!id || !file) return;
@@ -82,6 +150,14 @@ const PageMarket = {
this._tab = 'scenes'; this._tab = 'scenes';
document.getElementById('tab-scenes').style.cssText = 'border-radius:0;border-bottom:2px solid var(--accent);color:var(--accent);background:transparent;padding:8px 16px'; document.getElementById('tab-scenes').style.cssText = 'border-radius:0;border-bottom:2px solid var(--accent);color:var(--accent);background:transparent;padding:8px 16px';
document.getElementById('tab-dashboards').style.cssText = 'border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px'; document.getElementById('tab-dashboards').style.cssText = 'border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px';
document.getElementById('tab-mine').style.cssText = 'border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px';
this._loadItems();
});
document.getElementById('tab-mine')?.addEventListener('click', () => {
this._tab = 'mine';
document.getElementById('tab-mine').style.cssText = 'border-radius:0;border-bottom:2px solid var(--accent);color:var(--accent);background:transparent;padding:8px 16px';
document.getElementById('tab-dashboards').style.cssText = 'border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px';
document.getElementById('tab-scenes').style.cssText = 'border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px';
this._loadItems(); this._loadItems();
}); });
}, },
@@ -197,6 +273,7 @@ const PageMarket = {
<div style="display:flex;gap:0;margin-bottom:16px;border-bottom:1px solid var(--border-subtle)"> <div style="display:flex;gap:0;margin-bottom:16px;border-bottom:1px solid var(--border-subtle)">
<button id="tab-dashboards" class="btn btn-sm active" style="border-radius:0;border-bottom:2px solid var(--accent);color:var(--accent);background:transparent;padding:8px 16px">仪表盘</button> <button id="tab-dashboards" class="btn btn-sm active" style="border-radius:0;border-bottom:2px solid var(--accent);color:var(--accent);background:transparent;padding:8px 16px">仪表盘</button>
<button id="tab-scenes" class="btn btn-sm" style="border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px">场景</button> <button id="tab-scenes" class="btn btn-sm" style="border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px">场景</button>
<button id="tab-mine" class="btn btn-sm" style="border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px">我的</button>
</div> </div>
<div id="market-grid" class="theme-grid"></div> <div id="market-grid" class="theme-grid"></div>
</div>`; </div>`;