refactor: market as standalone sidebar page, removed from dashboard
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
PageScene.init();
|
PageScene.init();
|
||||||
PageDebug.init();
|
PageDebug.init();
|
||||||
PageRecord.init();
|
PageRecord.init();
|
||||||
|
PageMarket.init();
|
||||||
PageSettings.init();
|
PageSettings.init();
|
||||||
Router.init();
|
Router.init();
|
||||||
|
|
||||||
|
|||||||
@@ -313,15 +313,6 @@ const PageDashboard = {
|
|||||||
<div class="dashboard-layout">
|
<div class="dashboard-layout">
|
||||||
<div class="dashboard-sub-sidebar">
|
<div class="dashboard-sub-sidebar">
|
||||||
<div id="dash-category-list"></div>
|
<div id="dash-category-list"></div>
|
||||||
<div style="border-top:1px solid var(--border-subtle);margin:10px 8px 0;padding:12px 0 8px">
|
|
||||||
<button id="btn-market-tab" class="category-item" data-cat="market" style="width:100%;justify-content:flex-start;gap:8px">
|
|
||||||
<span>🏪</span> 市场
|
|
||||||
</button>
|
|
||||||
<div style="padding:4px 12px 0;display:flex;flex-direction:column;gap:4px">
|
|
||||||
<button id="btn-market-login" class="btn btn-sm btn-primary" style="width:100%">登录</button>
|
|
||||||
<button id="btn-market-upload" class="btn btn-sm btn-secondary" style="width:100%;display:none">发布到市场</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="padding:8px 12px;margin-top:auto">
|
<div style="padding:8px 12px;margin-top:auto">
|
||||||
<button id="btn-import-dashboard" class="btn btn-sm btn-secondary" style="width:100%;">导入 .tsd</button>
|
<button id="btn-import-dashboard" class="btn btn-sm btn-secondary" style="width:100%;">导入 .tsd</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
const PageMarket = {
|
||||||
|
_el: null,
|
||||||
|
_marketToken: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._el = document.getElementById('page-market');
|
||||||
|
this._marketToken = localStorage.getItem('turbosu-market-token');
|
||||||
|
},
|
||||||
|
|
||||||
|
async render() {
|
||||||
|
this._el.innerHTML = this._template();
|
||||||
|
this._bindEvents();
|
||||||
|
this._updateUI();
|
||||||
|
this._loadItems();
|
||||||
|
},
|
||||||
|
|
||||||
|
async _loadItems() {
|
||||||
|
const grid = document.getElementById('market-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
grid.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-tertiary);">加载中...</div>';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/market/dashboards');
|
||||||
|
const data = await res.json();
|
||||||
|
const items = data.items || [];
|
||||||
|
if (!items.length) { grid.innerHTML = this._emptyHtml('市场暂无仪表盘'); return; }
|
||||||
|
grid.innerHTML = items.map(d => {
|
||||||
|
const icon = d.icon || '📦';
|
||||||
|
return `<div class="glass-card theme-card">
|
||||||
|
<div class="theme-card-preview">${icon}</div>
|
||||||
|
<div class="theme-card-info">
|
||||||
|
<div class="theme-card-name">${d.name||'Unnamed'}</div>
|
||||||
|
<div class="theme-card-meta">
|
||||||
|
<span>${d.category||''}</span><span>v${d.version||'1.0'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="theme-card-meta" style="margin-top:4px">
|
||||||
|
<span>${d.author||''}</span><span style="color:var(--text-tertiary)">${d.downloads||0} 次</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary btn-m-install" data-id="${d.id}" data-file="${d.file||''}" style="margin-top:8px;width:100%">安装</button>
|
||||||
|
</div></div>`;
|
||||||
|
}).join('');
|
||||||
|
} catch (e) { grid.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">加载失败</div>'; }
|
||||||
|
},
|
||||||
|
|
||||||
|
_bindEvents() {
|
||||||
|
document.getElementById('btn-m-login')?.addEventListener('click', () => this._showLogin());
|
||||||
|
document.getElementById('btn-m-upload')?.addEventListener('click', () => this._upload());
|
||||||
|
document.getElementById('market-grid')?.addEventListener('click', async (e) => {
|
||||||
|
const btn = e.target.closest('.btn-m-install');
|
||||||
|
if (!btn) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/market/install?id=${encodeURIComponent(btn.dataset.id)}&filename=${encodeURIComponent(btn.dataset.file)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
Toast.show(data.ok ? '安装成功' : '安装失败: ' + (data.error||''), data.ok ? 'success' : 'error');
|
||||||
|
} catch (e) { Toast.show('安装失败', 'error'); }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateUI() {
|
||||||
|
const loginBtn = document.getElementById('btn-m-login');
|
||||||
|
const uploadBtn = document.getElementById('btn-m-upload');
|
||||||
|
const statusEl = document.getElementById('m-status');
|
||||||
|
if (!loginBtn || !uploadBtn) return;
|
||||||
|
if (this._marketToken) {
|
||||||
|
loginBtn.textContent = '已登录';
|
||||||
|
loginBtn.className = 'btn btn-sm btn-secondary';
|
||||||
|
uploadBtn.style.display = '';
|
||||||
|
if (statusEl) statusEl.textContent = ' 已登录';
|
||||||
|
} else {
|
||||||
|
loginBtn.textContent = '登录';
|
||||||
|
loginBtn.className = 'btn btn-sm btn-primary';
|
||||||
|
uploadBtn.style.display = 'none';
|
||||||
|
if (statusEl) statusEl.textContent = ' 未登录';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_upload() {
|
||||||
|
const input = document.createElement('input'); input.type = 'file'; input.accept = '.tsd';
|
||||||
|
input.onchange = async (e) => {
|
||||||
|
const file = e.target.files[0]; if (!file) return;
|
||||||
|
const fd = new FormData(); fd.append('file', file);
|
||||||
|
fd.append('name', file.name.replace('.tsd',''));
|
||||||
|
fd.append('category', 'community');
|
||||||
|
fd.append('author', 'TurboSu User');
|
||||||
|
fd.append('token', this._marketToken);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/market/upload', { method: 'POST', body: fd });
|
||||||
|
const data = await res.json();
|
||||||
|
Toast.show(data.error ? '上传失败: ' + data.error : '上传成功', data.error ? 'error' : 'success');
|
||||||
|
if (!data.error) this._loadItems();
|
||||||
|
} catch (e) { Toast.show('上传失败', 'error'); }
|
||||||
|
}; input.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
_showLogin() {
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'modal-overlay';
|
||||||
|
overlay.innerHTML = `<div class="modal" style="min-width:360px;max-width:400px">
|
||||||
|
<h3>市场账户</h3>
|
||||||
|
<div class="form-group"><label>邮箱</label><input id="ml-email" type="email" placeholder="yeij@yeij.top"></div>
|
||||||
|
<div class="form-group"><label>密码</label><input id="ml-pass" type="password"></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary btn-sm" id="ml-logout" style="display:none;margin-right:auto">退出</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" id="ml-register">注册</button>
|
||||||
|
<button class="btn btn-primary btn-sm" id="ml-login">登录</button>
|
||||||
|
</div></div>`;
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
if (this._marketToken) {
|
||||||
|
overlay.querySelector('#ml-login').style.display = 'none';
|
||||||
|
overlay.querySelector('#ml-register').style.display = 'none';
|
||||||
|
overlay.querySelector('#ml-logout').style.display = '';
|
||||||
|
overlay.querySelector('#ml-logout').addEventListener('click', () => {
|
||||||
|
this._marketToken = null;
|
||||||
|
localStorage.removeItem('turbosu-market-token');
|
||||||
|
overlay.remove();
|
||||||
|
this._updateUI();
|
||||||
|
Toast.show('已退出', 'info');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||||||
|
overlay.querySelector('#ml-login')?.addEventListener('click', async () => {
|
||||||
|
const email = document.getElementById('ml-email').value;
|
||||||
|
const pass = document.getElementById('ml-pass').value;
|
||||||
|
if (!email || !pass) { Toast.show('请输入邮箱和密码', 'error'); return; }
|
||||||
|
const res = await fetch('/api/market/auth', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({email, password: pass}) });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.token) {
|
||||||
|
this._marketToken = data.token;
|
||||||
|
localStorage.setItem('turbosu-market-token', data.token);
|
||||||
|
Toast.show('登录成功', 'success');
|
||||||
|
overlay.remove();
|
||||||
|
this._updateUI();
|
||||||
|
} else { Toast.show(data.message || '登录失败', 'error'); }
|
||||||
|
});
|
||||||
|
overlay.querySelector('#ml-register')?.addEventListener('click', async () => {
|
||||||
|
const email = document.getElementById('ml-email').value;
|
||||||
|
const pass = document.getElementById('ml-pass').value;
|
||||||
|
if (!email || !pass) { Toast.show('请输入邮箱和密码', 'error'); return; }
|
||||||
|
const res = await fetch('/api/market/register', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({email, password: pass, passwordConfirm: pass}) });
|
||||||
|
const data = await res.json();
|
||||||
|
Toast.show(data.error ? '注册失败' : '注册成功,请查收验证邮件', data.error ? 'error' : 'success');
|
||||||
|
if (!data.error) overlay.remove();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_emptyHtml(msg) {
|
||||||
|
return `<div class="empty-state" style="grid-column:1/-1"><h4>${msg||'暂无'}</h4></div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
_template() {
|
||||||
|
return `<div class="animated">
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||||
|
<div>
|
||||||
|
<h2 style="font-size:24px;font-weight:700">仪表盘市场</h2>
|
||||||
|
<p style="color:var(--text-secondary);font-size:14px;margin-top:4px">浏览并安装社区分享的仪表盘 <span id="m-status" style="color:var(--text-tertiary)"> 未登录</span></p>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px">
|
||||||
|
<button id="btn-m-upload" class="btn btn-sm btn-secondary" style="display:none">发布到市场</button>
|
||||||
|
<button id="btn-m-login" class="btn btn-sm btn-primary">登录</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="market-grid" class="theme-grid"></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.PageMarket = PageMarket;
|
||||||
@@ -5,6 +5,7 @@ const Router = {
|
|||||||
scene: PageScene,
|
scene: PageScene,
|
||||||
debug: PageDebug,
|
debug: PageDebug,
|
||||||
record: PageRecord,
|
record: PageRecord,
|
||||||
|
market: PageMarket,
|
||||||
settings: PageSettings,
|
settings: PageSettings,
|
||||||
},
|
},
|
||||||
_currentPage: 'home',
|
_currentPage: 'home',
|
||||||
|
|||||||
@@ -58,6 +58,10 @@
|
|||||||
<svg viewBox="0 0 24 24" width="20" height="20"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3" fill="currentColor"/></svg>
|
<svg viewBox="0 0 24 24" width="20" height="20"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3" fill="currentColor"/></svg>
|
||||||
<span class="nav-label">录制回放</span>
|
<span class="nav-label">录制回放</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="#/market" class="nav-item" data-route="market">
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z" fill="currentColor"/></svg>
|
||||||
|
<span class="nav-label">市场</span>
|
||||||
|
</a>
|
||||||
<a href="#/settings" class="nav-item" data-route="settings">
|
<a href="#/settings" class="nav-item" data-route="settings">
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 0 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" fill="currentColor"/></svg>
|
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 0 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" fill="currentColor"/></svg>
|
||||||
<span class="nav-label">设置</span>
|
<span class="nav-label">设置</span>
|
||||||
@@ -97,6 +101,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="page-record" class="page">
|
<div id="page-record" class="page">
|
||||||
</div>
|
</div>
|
||||||
|
<div id="page-market" class="page">
|
||||||
|
</div>
|
||||||
<div id="page-settings" class="page">
|
<div id="page-settings" class="page">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,6 +121,7 @@
|
|||||||
<script src="/static/js/pages/scene.js"></script>
|
<script src="/static/js/pages/scene.js"></script>
|
||||||
<script src="/static/js/pages/debug.js"></script>
|
<script src="/static/js/pages/debug.js"></script>
|
||||||
<script src="/static/js/pages/record.js"></script>
|
<script src="/static/js/pages/record.js"></script>
|
||||||
|
<script src="/static/js/pages/market.js"></script>
|
||||||
<script src="/static/js/pages/settings.js"></script>
|
<script src="/static/js/pages/settings.js"></script>
|
||||||
<script src="/static/js/router.js"></script>
|
<script src="/static/js/router.js"></script>
|
||||||
<script src="/static/js/app.js"></script>
|
<script src="/static/js/app.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user