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 = '
加载中...
'; 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 = '
加载失败
'; } }, 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 += '

我的仪表盘

' + data.dashboards.map(d => this._myCardHtml(d, 'dashboards')).join(''); } if (data.scenes?.length) { html += '

我的场景

' + data.scenes.map(d => this._myCardHtml(d, 'scenes')).join(''); } grid.innerHTML = html; } catch (e) { grid.innerHTML = '
加载失败
'; } }, _cardHtml(d, isScene) { const icon = isScene ? '🎬' : (d.icon || '📦'); const btnLabel = isScene ? '安装场景' : '安装'; const btnClass = isScene ? 'btn-m-install-scene' : 'btn-m-install'; return `
${icon}
${d.name||'Unnamed'}
${isScene ? (d.game_id||'') : (d.category||'')}${isScene ? '' : 'v'+(d.version||'1.0')}
${d.author||''}${d.downloads||0} 次
`; }, _myCardHtml(d, collection) { const pending = d.dashb_pending ? '待审核' : '已通过'; const created = d.created ? new Date(d.created).toLocaleString('zh-CN', {month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '-'; const updated = d.updated ? new Date(d.updated).toLocaleString('zh-CN', {month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}) : created; return `
${d.name||'Unnamed'} v${d.version||'1.0'}
${d.author||''} · ${d.downloads||0} 次下载 · ${pending}
上传: ${created}${updated!==created?' · 更新: '+updated:''}
`; }, _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) { Toast.show('安装失败:数据不完整', 'error'); 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) { Toast.show('安装成功!请在仪表盘页面查看', 'success', 4000); } 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'; 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-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 = ``; 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 `

${msg||'暂无'}

`; }, _template() { return `

仪表盘市场

浏览并安装社区分享的仪表盘和场景 未登录

`; } }; window.PageMarket = PageMarket;