284 lines
16 KiB
JavaScript
284 lines
16 KiB
JavaScript
const PageMarket = {
|
|
_el: null,
|
|
_marketToken: null,
|
|
_tab: 'dashboards',
|
|
|
|
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>';
|
|
|
|
if (this._tab === 'mine') {
|
|
if (!this._marketToken) {
|
|
grid.innerHTML = this._emptyHtml('请先登录');
|
|
return;
|
|
}
|
|
return this._loadMine(grid);
|
|
}
|
|
|
|
const isScene = this._tab === 'scenes';
|
|
const endpoint = isScene ? '/api/market/scenes' : '/api/market/dashboards';
|
|
try {
|
|
const res = await fetch(endpoint);
|
|
const data = await res.json();
|
|
const items = data.items || [];
|
|
if (!items.length) { grid.innerHTML = this._emptyHtml(isScene ? '暂无场景' : '暂无仪表盘'); return; }
|
|
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 btnLabel = isScene ? '安装场景' : '安装';
|
|
const btnClass = isScene ? 'btn-m-install-scene' : 'btn-m-install';
|
|
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>${isScene ? (d.game_id||'') : (d.category||'')}</span><span>${isScene ? '' : '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 ${btnClass}" data-id="${d.id}" data-file="${d.file||''}" style="margin-top:8px;width:100%">${btnLabel}</button>
|
|
</div></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() {
|
|
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 btnInstall = e.target.closest('.btn-m-install');
|
|
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 file = (btnInstall || btnScene)?.dataset.file;
|
|
if (!id || !file) return;
|
|
const isScene = !!btnScene;
|
|
const url = isScene ? `/api/market/scenes/install?id=${encodeURIComponent(id)}&filename=${encodeURIComponent(file)}`
|
|
: `/api/market/install?id=${encodeURIComponent(id)}&filename=${encodeURIComponent(file)}`;
|
|
try {
|
|
const res = await fetch(url);
|
|
const data = await res.json();
|
|
if (data.ok) {
|
|
let msg = '安装成功';
|
|
if (data.missing_dashboards && data.missing_dashboards.length > 0)
|
|
msg += ',缺少 ' + data.missing_dashboards.length + ' 个依赖仪表盘';
|
|
Toast.show(msg, 'success');
|
|
} else {
|
|
Toast.show('安装失败: ' + (data.error||''), 'error');
|
|
}
|
|
} catch (e) { Toast.show('安装失败', 'error'); }
|
|
});
|
|
document.getElementById('tab-dashboards')?.addEventListener('click', () => {
|
|
this._tab = 'dashboards';
|
|
document.getElementById('tab-dashboards').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 transparent;color:var(--text-secondary);background:transparent;padding:8px 16px';
|
|
this._loadItems();
|
|
});
|
|
document.getElementById('tab-scenes')?.addEventListener('click', () => {
|
|
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-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();
|
|
});
|
|
},
|
|
|
|
_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 = this._tab === 'scenes' ? '.tss' : '.tsd';
|
|
const endpoint = this._tab === 'scenes' ? '/api/market/scenes/upload' : '/api/market/upload';
|
|
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="your@email.com"></div>
|
|
<div class="form-group"><label>用户名</label><input id="ml-name" type="text" placeholder="可选"></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;
|
|
const name = document.getElementById('ml-name')?.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, name}) });
|
|
const data = await res.json();
|
|
Toast.show(data.error ? '注册失败' : '注册成功,请查收验证邮件(点击关闭)', data.error ? 'error' : 'success', data.error ? 3000 : 0);
|
|
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;flex-wrap:wrap;gap:10px">
|
|
<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 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-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 id="market-grid" class="theme-grid"></div>
|
|
</div>`;
|
|
}
|
|
};
|
|
|
|
window.PageMarket = PageMarket;
|