feat: full i18n support - all pages use Lang.t() for zh/en
This commit is contained in:
+87
-86
@@ -19,11 +19,11 @@ const PageMarket = {
|
||||
const grid = document.getElementById('market-grid');
|
||||
if (!grid) return;
|
||||
grid.style.display = 'grid';
|
||||
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);">' + Lang.t('loading') + '</div>';
|
||||
|
||||
if (this._tab === 'mine') {
|
||||
if (!this._marketToken) {
|
||||
grid.innerHTML = this._emptyHtml('请先登录');
|
||||
grid.innerHTML = this._emptyHtml(Lang.t('needLogin'));
|
||||
return;
|
||||
}
|
||||
return this._loadMine(grid);
|
||||
@@ -48,12 +48,12 @@ const PageMarket = {
|
||||
}
|
||||
} catch(e) { console.error('load local:', e); }
|
||||
|
||||
if (!items.length) { grid.innerHTML = this._emptyHtml(isScene ? '暂无场景' : '暂无仪表盘'); return; }
|
||||
if (!items.length) { grid.innerHTML = this._emptyHtml(isScene ? Lang.t('noScenesMarket') : Lang.t('noDashboardsMarket')); return; }
|
||||
grid.innerHTML = items.map(d => {
|
||||
const installed = d.manifest_id && localIds.includes(d.manifest_id);
|
||||
return this._cardHtml(d, isScene, installed);
|
||||
}).join('');
|
||||
} catch (e) { console.error(e); grid.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">加载失败' + (e.message?': '+e.message:'') + ' <a href="#" onclick="document.getElementById(\'btn-refresh-market\').click();return false" style="color:var(--accent)">重试</a></div>'; }
|
||||
} catch (e) { console.error(e); grid.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">' + Lang.t('loadFailed') + (e.message?': '+e.message:'') + ' <a href="#" onclick="document.getElementById(\'btn-refresh-market\').click();return false" style="color:var(--accent)">' + Lang.t('retry') + '</a></div>'; }
|
||||
},
|
||||
|
||||
async _loadMine(grid) {
|
||||
@@ -62,27 +62,27 @@ const PageMarket = {
|
||||
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('你还没有上传任何内容');
|
||||
grid.innerHTML = this._emptyHtml(Lang.t('noUploads'));
|
||||
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('');
|
||||
html += '<h3 style="margin-bottom:10px;font-size:16px;color:var(--text-secondary)">' + Lang.t('myDashboards') + '</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('');
|
||||
html += '<h3 style="margin:20px 0 10px;font-size:16px;color:var(--text-secondary)">' + Lang.t('myScenes') + '</h3>' + data.scenes.map(d => this._myCardHtml(d, 'scenes')).join('');
|
||||
}
|
||||
grid.innerHTML = html;
|
||||
} catch (e) { console.error(e); grid.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">加载失败' + (e.message?': '+e.message:'') + ' <a href="#" onclick="document.getElementById(\"btn-refresh-market\").click();return false" style="color:var(--accent)">重试</a></div>'; }
|
||||
} catch (e) { console.error(e); grid.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">' + Lang.t('loadFailed') + (e.message?': '+e.message:'') + ' <a href="#" onclick="document.getElementById(\"btn-refresh-market\").click();return false" style="color:var(--accent)">' + Lang.t('retry') + '</a></div>'; }
|
||||
},
|
||||
|
||||
_cardHtml(d, isScene, installed) {
|
||||
const icon = d.preview ? '' : (isScene ? '🎬' : (d.icon || '📦'));
|
||||
const previewUrl = d.preview ? `https://turbosu.yeij.top/api/files/${isScene?'scenes':'dashboards'}/${d.id}/${d.preview}` : '';
|
||||
const bgStyle = previewUrl ? `background-image:url(${previewUrl});background-size:cover;background-position:center` : '';
|
||||
const label = installed ? '已安装' : (isScene ? '安装场景' : '安装');
|
||||
const label = installed ? Lang.t('installed') : (isScene ? Lang.t('installScene') : Lang.t('install'));
|
||||
const btnClass = isScene ? 'btn-m-install-scene' : 'btn-m-install';
|
||||
const uninstallBtn = installed ? `<button class="btn btn-sm btn-danger btn-m-uninstall" data-name="${d.name||d.id}" style="margin-top:4px;width:100%">卸载</button>` : '';
|
||||
const uninstallBtn = installed ? `<button class="btn btn-sm btn-danger btn-m-uninstall" data-name="${d.name||d.id}" style="margin-top:4px;width:100%">` + Lang.t('uninstall') + '</button>' : '';
|
||||
return `<div class="glass-card theme-card">
|
||||
<div class="theme-card-preview" style="${bgStyle}">${icon}</div>
|
||||
<div class="theme-card-info">
|
||||
@@ -91,7 +91,7 @@ const PageMarket = {
|
||||
<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>
|
||||
<span>${d.author||''}</span><span style="color:var(--text-tertiary)">${Lang.t('downloadCountShort').replace('%d', 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%" ${installed?'disabled':''}>${label}</button>
|
||||
${uninstallBtn}
|
||||
@@ -99,18 +99,18 @@ const PageMarket = {
|
||||
},
|
||||
|
||||
_myCardHtml(d, collection) {
|
||||
const pending = d.dashb_pending ? '<span class="badge" style="background:rgba(255,159,10,.15);color:#FF9F0A">待审核</span>' : '<span class="badge" style="background:rgba(52,199,89,.12);color:#34C759">已通过</span>';
|
||||
const pending = d.dashb_pending ? '<span class="badge" style="background:rgba(255,159,10,.15);color:#FF9F0A">' + Lang.t('pending') + '</span>' : '<span class="badge" style="background:rgba(52,199,89,.12);color:#34C759">' + Lang.t('approved') + '</span>';
|
||||
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 `<div class="glass-card" style="padding:14px 16px;margin-bottom:8px">
|
||||
<div style="font-weight:600;font-size:14px;margin-bottom:4px">${d.name||'Unnamed'} <span style="color:var(--text-tertiary);font-weight:400;font-size:12px">v${d.version||'1.0'}</span></div>
|
||||
<div style="font-size:12px;color:var(--text-tertiary);margin-bottom:4px">
|
||||
${d.author||''} · ${d.downloads||0} 次下载 · ${pending} <br>
|
||||
${d.author||''} · ${Lang.t('downloadCount').replace('%d', d.downloads||0)} · ${pending} <br>
|
||||
<span style="font-size:11px">上传: ${created}${updated!==created?' · 更新: '+updated:''}</span>
|
||||
</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-preview="${d.preview||''}" 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>
|
||||
<button class="btn btn-sm btn-secondary btn-my-update" data-id="${d.id}" data-coll="${collection}" data-file="${d.file||''}" data-preview="${d.preview||''}" data-type="${collection==='scenes'?'.tss':'.tsd'}">${Lang.t('updateItem')}</button>
|
||||
<button class="btn btn-sm btn-danger btn-my-delete" data-id="${d.id}" data-coll="${collection}">${Lang.t('delete')}</button>
|
||||
</div></div>`;
|
||||
},
|
||||
|
||||
@@ -126,17 +126,17 @@ const PageMarket = {
|
||||
if (e.target.closest('.btn-m-uninstall')) {
|
||||
const ubtn = e.target.closest('.btn-m-uninstall');
|
||||
const uname = ubtn.dataset.name||'';
|
||||
if (!confirm('确定卸载 "' + uname + '"?')) return;
|
||||
if (!confirm(Lang.t('confirmUninstall').replace('"$1"', uname))) return;
|
||||
try {
|
||||
const r = await fetch('/api/dashboards?category=all');
|
||||
const list = (await r.json()) || [];
|
||||
const dash = list.find(d => d.id === uname || d.name === uname);
|
||||
if (dash) {
|
||||
await fetch('/api/dashboards/' + dash.id, { method: 'DELETE' });
|
||||
Toast.show('已卸载', 'success');
|
||||
Toast.show(Lang.t('uninstallSuccess'), 'success');
|
||||
this._loadItems();
|
||||
} else { Toast.show('未找到本地安装', 'error'); }
|
||||
} catch (e) { Toast.show('卸载失败', 'error'); }
|
||||
} else { Toast.show(Lang.t('uninstallNotFound'), 'error'); }
|
||||
} catch (e) { Toast.show(Lang.t('uninstallFailed'), 'error'); }
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,31 +145,31 @@ const PageMarket = {
|
||||
return;
|
||||
}
|
||||
if (btnDelete) {
|
||||
if (!confirm('确定删除?')) return;
|
||||
if (!confirm(Lang.t('confirmDelete'))) 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');
|
||||
Toast.show(Lang.t('deleted'), 'info');
|
||||
this._loadItems();
|
||||
} catch (e) { Toast.show('删除失败', 'error'); }
|
||||
} catch (e) { Toast.show(Lang.t('deleteFailed'), 'error'); }
|
||||
return;
|
||||
}
|
||||
|
||||
const id = (btnInstall || btnScene)?.dataset.id;
|
||||
const file = (btnInstall || btnScene)?.dataset.file;
|
||||
if (!id || !file) { Toast.show('安装失败:数据不完整', 'error'); return; }
|
||||
if (!id || !file) { Toast.show(Lang.t('installDataIncomplete'), '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)}`;
|
||||
: `/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);
|
||||
Toast.show(Lang.t('installedHint'), 'success', 4000);
|
||||
this._loadItems();
|
||||
} else {
|
||||
Toast.show('安装失败: ' + (data.error||'请求异常'), 'error');
|
||||
Toast.show(Lang.t('installFailedMsg') + (data.error||''), 'error');
|
||||
}
|
||||
} catch (e) { Toast.show('安装失败:网络错误', 'error'); }
|
||||
} catch (e) { Toast.show(Lang.t('installNetworkError'), 'error'); }
|
||||
});
|
||||
document.getElementById('tab-dashboards')?.addEventListener('click', () => {
|
||||
this._tab = 'dashboards';
|
||||
@@ -200,22 +200,22 @@ const PageMarket = {
|
||||
const statusEl = document.getElementById('m-status');
|
||||
if (!loginBtn || !uploadBtn) return;
|
||||
if (this._marketToken) {
|
||||
loginBtn.textContent = '已登录';
|
||||
loginBtn.textContent = Lang.t('loggedIn');
|
||||
loginBtn.className = 'btn btn-sm btn-secondary';
|
||||
uploadBtn.style.display = '';
|
||||
if (statusEl) statusEl.textContent = ' 已登录';
|
||||
if (statusEl) statusEl.textContent = ' ' + Lang.t('loggedIn');
|
||||
} else {
|
||||
loginBtn.textContent = '登录';
|
||||
loginBtn.textContent = Lang.t('login');
|
||||
loginBtn.className = 'btn btn-sm btn-primary';
|
||||
uploadBtn.style.display = 'none';
|
||||
if (statusEl) statusEl.textContent = ' 未登录';
|
||||
if (statusEl) statusEl.textContent = ' ' + Lang.t('notLoggedIn');
|
||||
}
|
||||
},
|
||||
|
||||
_upload() {
|
||||
if (!this._marketToken) { Toast.show('请先登录', 'error'); return; }
|
||||
if (!this._marketToken) { Toast.show(Lang.t('needLogin'), 'error'); return; }
|
||||
const overlay = document.createElement('div'); overlay.className = 'modal-overlay';
|
||||
overlay.innerHTML = '<div class="modal" style="min-width:480px;max-width:520px;padding:24px"><h3 style="font-size:18px;font-weight:600;margin-bottom:4px">发布作品</h3><p style="font-size:12px;color:var(--text-secondary);margin-bottom:18px">选择要发布的仪表盘或场景</p><div style="margin-bottom:12px"><label class="switch-label" style="font-size:13px"><input type="checkbox" id="pub-preview-toggle"><span class="switch-track"></span> 上传预览图</label></div><button id="pub-file-btn" class="btn btn-primary btn-sm" style="width:100%;margin-bottom:10px">📁 选择文件 (.tsd / .tss)</button><div style="font-size:13px;color:var(--text-tertiary);margin:10px 0;text-align:center">— 或从本地选择 —</div><div id="pub-local-list" style="max-height:300px;overflow-y:auto">加载中...</div></div>';
|
||||
overlay.innerHTML = '<div class="modal" style="min-width:480px;max-width:520px;padding:24px"><h3 style="font-size:18px;font-weight:600;margin-bottom:4px">' + Lang.t('publishTitle') + '</h3><p style="font-size:12px;color:var(--text-secondary);margin-bottom:18px">' + Lang.t('publishDesc') + '</p><div style="margin-bottom:12px"><label class="switch-label" style="font-size:13px"><input type="checkbox" id="pub-preview-toggle"><span class="switch-track"></span> ' + Lang.t('uploadPreview') + '</label></div><button id="pub-file-btn" class="btn btn-primary btn-sm" style="width:100%;margin-bottom:10px">' + Lang.t('chooseFile') + '</button><div style="font-size:13px;color:var(--text-tertiary);margin:10px 0;text-align:center">' + Lang.t('orChooseLocal') + '</div><div id="pub-local-list" style="max-height:300px;overflow-y:auto">' + Lang.t('loading') + '</div></div>';
|
||||
document.body.appendChild(overlay);
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||||
|
||||
@@ -223,7 +223,7 @@ const PageMarket = {
|
||||
const previewDrop = document.createElement('div');
|
||||
previewDrop.id = 'pub-preview-drop';
|
||||
previewDrop.style.cssText = 'display:none;border:2px dashed var(--border-normal);border-radius:8px;padding:20px;text-align:center;color:var(--text-tertiary);font-size:13px;cursor:pointer;margin-top:8px;transition:all .2s';
|
||||
previewDrop.textContent = '点击或拖放图片到此处 (最大 6MB)';
|
||||
previewDrop.textContent = Lang.t('dropPreview');
|
||||
overlay.querySelector('.modal').insertBefore(previewDrop, overlay.querySelector('#pub-file-btn'));
|
||||
|
||||
function pickPreview() {
|
||||
@@ -231,10 +231,10 @@ const PageMarket = {
|
||||
inp.onchange = function() {
|
||||
const f = this.files[0];
|
||||
if (!f) return;
|
||||
if (f.size > 6291456) { Toast.show('预览图不能超过 6MB', 'error'); return; }
|
||||
if (!f.type.startsWith('image/')) { Toast.show('仅支持图片格式', 'error'); return; }
|
||||
if (f.size > 6291456) { Toast.show(Lang.t('previewTooLarge'), 'error'); return; }
|
||||
if (!f.type.startsWith('image/')) { Toast.show(Lang.t('imageOnly'), 'error'); return; }
|
||||
previewFile = f;
|
||||
previewDrop.textContent = '✓ ' + f.name + ' (' + (f.size/1024/1024).toFixed(1) + ' MB)';
|
||||
previewDrop.textContent = '\u2713 ' + f.name + ' (' + (f.size/1024/1024).toFixed(1) + ' MB)';
|
||||
previewDrop.style.borderColor = 'var(--accent)'; previewDrop.style.color = 'var(--accent)';
|
||||
};
|
||||
document.body.appendChild(inp); inp.click();
|
||||
@@ -246,10 +246,10 @@ const PageMarket = {
|
||||
e.preventDefault();
|
||||
const f = e.dataTransfer.files[0];
|
||||
if (!f) return;
|
||||
if (!f.type.startsWith('image/')) { Toast.show('仅支持图片格式', 'error'); return; }
|
||||
if (f.size > 6291456) { Toast.show('预览图不能超过 6MB', 'error'); return; }
|
||||
if (!f.type.startsWith('image/')) { Toast.show(Lang.t('imageOnly'), 'error'); return; }
|
||||
if (f.size > 6291456) { Toast.show(Lang.t('previewTooLarge'), 'error'); return; }
|
||||
previewFile = f;
|
||||
previewDrop.textContent = '✓ ' + f.name + ' (' + (f.size/1024/1024).toFixed(1) + ' MB)';
|
||||
previewDrop.textContent = '\u2713 ' + f.name + ' (' + (f.size/1024/1024).toFixed(1) + ' MB)';
|
||||
previewDrop.style.borderColor = 'var(--accent)'; previewDrop.style.color = 'var(--accent)';
|
||||
});
|
||||
previewDrop.addEventListener('click', pickPreview);
|
||||
@@ -260,7 +260,7 @@ const PageMarket = {
|
||||
} else {
|
||||
previewFile = null;
|
||||
previewDrop.style.display = 'none';
|
||||
previewDrop.textContent = '点击或拖放图片到此处 (最大 6MB)';
|
||||
previewDrop.textContent = Lang.t('dropPreview');
|
||||
previewDrop.style.borderColor = 'var(--border-normal)'; previewDrop.style.color = 'var(--text-tertiary)';
|
||||
}
|
||||
});
|
||||
@@ -274,7 +274,7 @@ const PageMarket = {
|
||||
if (previewFile) fd.append('preview', previewFile);
|
||||
const res = await fetch(isTss ? '/api/market/scenes/upload' : '/api/market/upload', { method: 'POST', body: fd });
|
||||
const data = await res.json();
|
||||
Toast.show(data.error ? '失败: '+data.error : '发布成功', data.error ? 'error' : 'success');
|
||||
Toast.show(data.error ? Lang.t('publishFailed') + ': '+data.error : Lang.t('publishSuccess'), data.error ? 'error' : 'success');
|
||||
if (!data.error) { overlay.remove(); this._loadItems(); }
|
||||
}; input.click();
|
||||
});
|
||||
@@ -292,27 +292,27 @@ const PageMarket = {
|
||||
const upS = Array.isArray(scenes) ? scenes.filter(s => !s.is_builtin && !(s.id||'').match(/^[a-f0-9]{6}__/)) : [];
|
||||
let h = '';
|
||||
if (upD.length) {
|
||||
h += '<div class="pub-section" style="cursor:pointer;font-weight:600;font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:flex;align-items:center;gap:6px" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==\'none\'?\'\':\'none\';this.querySelector(\'.pub-arrow\').style.transform=this.nextElementSibling.style.display==\'none\'?\'rotate(-90deg)\':\'\'"><span class="pub-arrow" style="display:inline-block;transition:transform .2s">▼</span> 本地仪表盘 ('+upD.length+')</div>';
|
||||
h += '<div class="pub-group">' + upD.map(d => '<div class="glass-card" style="padding:10px 14px;margin-bottom:4px;display:flex;align-items:center;justify-content:space-between;gap:8px"><div><span style="font-weight:500">'+(d.icon||'📊')+' '+d.name+'</span></div><button class="btn btn-sm btn-primary pub-dash-btn" data-id="'+d.id+'">发布</button></div>').join('') + '</div>';
|
||||
h += '<div class="pub-section" style="cursor:pointer;font-weight:600;font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:flex;align-items:center;gap:6px" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==\'none\'?\'\':\'none\';this.querySelector(\'.pub-arrow\').style.transform=this.nextElementSibling.style.display==\'none\'?\'rotate(-90deg)\':\'\'"><span class="pub-arrow" style="display:inline-block;transition:transform .2s">▼</span> ' + Lang.t('localDashboards') + ' ('+upD.length+')</div>';
|
||||
h += '<div class="pub-group">' + upD.map(d => '<div class="glass-card" style="padding:10px 14px;margin-bottom:4px;display:flex;align-items:center;justify-content:space-between;gap:8px"><div><span style="font-weight:500">'+(d.icon||'📊')+' '+d.name+'</span></div><button class="btn btn-sm btn-primary pub-dash-btn" data-id="'+d.id+'">' + Lang.t('publish') + '</button></div>').join('') + '</div>';
|
||||
}
|
||||
if (upS.length) {
|
||||
h += '<div class="pub-section" style="cursor:pointer;font-weight:600;font-size:13px;color:var(--text-secondary);margin:10px 0 4px;display:flex;align-items:center;gap:6px" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==\'none\'?\'\':\'none\';this.querySelector(\'.pub-arrow\').style.transform=this.nextElementSibling.style.display==\'none\'?\'rotate(-90deg)\':\'\'"><span class="pub-arrow" style="display:inline-block;transition:transform .2s">▼</span> 本地场景 ('+upS.length+')</div>';
|
||||
h += '<div class="pub-group">' + upS.map(s => '<div class="glass-card" style="padding:10px 14px;margin-bottom:4px;display:flex;align-items:center;justify-content:space-between;gap:8px"><div><span style="font-weight:500">🎬 '+s.name+'</span></div><button class="btn btn-sm btn-primary pub-scene-btn" data-id="'+s.id+'">发布</button></div>').join('') + '</div>';
|
||||
h += '<div class="pub-section" style="cursor:pointer;font-weight:600;font-size:13px;color:var(--text-secondary);margin:10px 0 4px;display:flex;align-items:center;gap:6px" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==\'none\'?\'\':\'none\';this.querySelector(\'.pub-arrow\').style.transform=this.nextElementSibling.style.display==\'none\'?\'rotate(-90deg)\':\'\'"><span class="pub-arrow" style="display:inline-block;transition:transform .2s">▼</span> ' + Lang.t('localScenes') + ' ('+upS.length+')</div>';
|
||||
h += '<div class="pub-group">' + upS.map(s => '<div class="glass-card" style="padding:10px 14px;margin-bottom:4px;display:flex;align-items:center;justify-content:space-between;gap:8px"><div><span style="font-weight:500">🎬 '+s.name+'</span></div><button class="btn btn-sm btn-primary pub-scene-btn" data-id="'+s.id+'">' + Lang.t('publish') + '</button></div>').join('') + '</div>';
|
||||
}
|
||||
list.innerHTML = h || '<p style="color:var(--text-tertiary);font-size:12px;padding:10px">没有可发布的内容</p>';
|
||||
list.innerHTML = h || '<p style="color:var(--text-tertiary);font-size:12px;padding:10px">' + Lang.t('noPublishable') + '</p>';
|
||||
list.querySelectorAll('.pub-dash-btn').forEach(btn => btn.addEventListener('click', async () => {
|
||||
const r = await fetch('/api/market/publish-local', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:btn.dataset.id,type:'dashboard',token:this._marketToken})});
|
||||
const d = await r.json();
|
||||
Toast.show(d.error?'失败:'+d.error:'发布成功',d.error?'error':'success');
|
||||
Toast.show(d.error?Lang.t('publishFailed')+': '+d.error:Lang.t('publishSuccess'),d.error?'error':'success');
|
||||
if(!d.error){overlay.remove();this._loadItems();}
|
||||
}));
|
||||
list.querySelectorAll('.pub-scene-btn').forEach(btn => btn.addEventListener('click', async () => {
|
||||
const r = await fetch('/api/market/publish-local', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:btn.dataset.id,type:'scene',token:this._marketToken})});
|
||||
const d = await r.json();
|
||||
Toast.show(d.error?'失败:'+d.error:'发布成功',d.error?'error':'success');
|
||||
Toast.show(d.error?Lang.t('publishFailed')+': '+d.error:Lang.t('publishSuccess'),d.error?'error':'success');
|
||||
if(!d.error){overlay.remove();this._loadItems();}
|
||||
}));
|
||||
} catch(e) { list.innerHTML = '<p style="color:var(--text-tertiary);padding:10px">加载失败</p>'; }
|
||||
} catch(e) { list.innerHTML = '<p style="color:var(--text-tertiary);padding:10px">' + Lang.t('loadFailed') + '</p>'; }
|
||||
},
|
||||
|
||||
_showLogin() {
|
||||
@@ -321,16 +321,16 @@ const PageMarket = {
|
||||
const isLogin = !this._marketToken;
|
||||
overlay.innerHTML = `<div class="modal" style="min-width:380px;max-width:420px;padding:24px">
|
||||
<div style="display:flex;gap:0;margin-bottom:18px;border-bottom:1px solid var(--border-subtle)">
|
||||
<button id="ml-tab-login" class="btn btn-sm ${isLogin?'':'btn-secondary'}" style="border-radius:0;border-bottom:2px solid ${isLogin?'var(--accent)':'transparent'};color:${isLogin?'var(--accent)':'var(--text-secondary)'};background:transparent;padding:8px 16px">登录</button>
|
||||
<button id="ml-tab-register" class="btn btn-sm ${!isLogin?'':'btn-secondary'}" style="border-radius:0;border-bottom:2px solid ${!isLogin?'var(--accent)':'transparent'};color:${!isLogin?'var(--accent)':'var(--text-secondary)'};background:transparent;padding:8px 16px">注册</button>
|
||||
<button id="ml-tab-login" class="btn btn-sm ${isLogin?'':'btn-secondary'}" style="border-radius:0;border-bottom:2px solid ${isLogin?'var(--accent)':'transparent'};color:${isLogin?'var(--accent)':'var(--text-secondary)'};background:transparent;padding:8px 16px">${Lang.t('login')}</button>
|
||||
<button id="ml-tab-register" class="btn btn-sm ${!isLogin?'':'btn-secondary'}" style="border-radius:0;border-bottom:2px solid ${!isLogin?'var(--accent)':'transparent'};color:${!isLogin?'var(--accent)':'var(--text-secondary)'};background:transparent;padding:8px 16px">${Lang.t('register')}</button>
|
||||
</div>
|
||||
<div class="form-group"><label>邮箱</label><input id="ml-email" type="email" placeholder="your@email.com"></div>
|
||||
<div class="form-group" id="ml-name-group" style="display:${!isLogin?'':'none'}"><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="form-group"><label>${Lang.t('email')}</label><input id="ml-email" type="email" placeholder="your@email.com"></div>
|
||||
<div class="form-group" id="ml-name-group" style="display:${!isLogin?'':'none'}"><label>${Lang.t('username')}</label><input id="ml-name" type="text" placeholder="Required"></div>
|
||||
<div class="form-group"><label>${Lang.t('password')}</label><input id="ml-pass" type="password"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-sm" id="ml-logout" style="display:${this._marketToken?'':'none'};margin-right:auto;background:var(--bg2);color:var(--text2)">退出</button>
|
||||
<button class="btn btn-sm" id="ml-logout" style="display:${this._marketToken?'':'none'};margin-right:auto;background:var(--bg2);color:var(--text2)">${Lang.t('logout')}</button>
|
||||
<span style="flex:1"></span>
|
||||
<button class="btn btn-primary btn-sm" id="ml-submit">${isLogin?'登录':'注册'}</button>
|
||||
<button class="btn btn-primary btn-sm" id="ml-submit">${isLogin?Lang.t('login'):Lang.t('register')}</button>
|
||||
</div></div>`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
@@ -340,7 +340,7 @@ const PageMarket = {
|
||||
localStorage.removeItem('turbosu-market-token');
|
||||
overlay.remove();
|
||||
this._updateUI();
|
||||
Toast.show('已退出', 'info');
|
||||
Toast.show(Lang.t('loggedOut'), 'info');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@ const PageMarket = {
|
||||
tabLogin.style.cssText = `border-radius:0;border-bottom:2px solid ${isL?'var(--accent)':'transparent'};color:${isL?'var(--accent)':'var(--text-secondary)'};background:transparent;padding:8px 16px`;
|
||||
tabRegister.style.cssText = `border-radius:0;border-bottom:2px solid ${!isL?'var(--accent)':'transparent'};color:${!isL?'var(--accent)':'var(--text-secondary)'};background:transparent;padding:8px 16px`;
|
||||
nameGroup.style.display = isL ? 'none' : '';
|
||||
submitBtn.textContent = isL ? '登录' : '注册';
|
||||
submitBtn.textContent = isL ? Lang.t('login') : Lang.t('register');
|
||||
}
|
||||
|
||||
tabLogin.addEventListener('click', () => setTab('login'));
|
||||
@@ -367,22 +367,22 @@ const PageMarket = {
|
||||
const doAuth = async () => {
|
||||
const email = document.getElementById('ml-email').value;
|
||||
const pass = document.getElementById('ml-pass').value;
|
||||
if (!email || !pass) { Toast.show('请输入邮箱和密码', 'error'); return; }
|
||||
if (!email || !pass) { Toast.show(Lang.t('enterEmailPassword'), 'error'); return; }
|
||||
if (mode === 'login') {
|
||||
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');
|
||||
Toast.show(Lang.t('loginSuccess'), 'success');
|
||||
overlay.remove();
|
||||
this._updateUI();
|
||||
} else { Toast.show(data.message || '登录失败', 'error'); }
|
||||
} else { Toast.show(data.message || Lang.t('loginFailed'), 'error'); }
|
||||
} else {
|
||||
const name = document.getElementById('ml-name')?.value || '';
|
||||
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 : '注册成功,请查收验证邮件(点击关闭)', data.error ? 'error' : 'success', data.error ? 3000 : 0);
|
||||
Toast.show(data.error ? Lang.t('registerFailed') + ': ' + data.error : Lang.t('registerSuccess'), data.error ? 'error' : 'success', data.error ? 3000 : 0);
|
||||
if (!data.error) overlay.remove();
|
||||
}
|
||||
};
|
||||
@@ -392,30 +392,31 @@ const PageMarket = {
|
||||
},
|
||||
|
||||
_emptyHtml(msg) {
|
||||
return `<div class="empty-state" style="grid-column:1/-1"><h4>${msg||'暂无'}</h4></div>`;
|
||||
return `<div class="empty-state" style="grid-column:1/-1"><h4>${msg||Lang.t('marketNoItems')}</h4></div>`;
|
||||
},
|
||||
|
||||
_showUpdateModal(ds) {
|
||||
if (!this._marketToken) { Toast.show('请先登录', 'error'); return; }
|
||||
if (!this._marketToken) { Toast.show(Lang.t('needLogin'), 'error'); return; }
|
||||
const isScene = ds.coll === 'scenes';
|
||||
const typeLabel = isScene ? Lang.t('scene') : Lang.t('dashboard');
|
||||
const overlay = document.createElement('div'); overlay.className = 'modal-overlay';
|
||||
overlay.innerHTML = '<div class="modal" style="min-width:480px;max-width:520px;padding:24px"><h3 style="font-size:18px;font-weight:600;margin-bottom:4px">更新 '+(isScene?'场景':'仪表盘')+'</h3><p style="font-size:12px;color:var(--text-secondary);margin-bottom:18px">重新上传包文件或更换预览图</p><div style="margin-bottom:12px"><label class="switch-label" style="font-size:13px"><input type="checkbox" id="pub-preview-toggle" checked><span class="switch-track"></span> 更换预览图</label></div><button id="pub-update-btn" class="btn btn-primary btn-sm" style="width:100%;margin-bottom:10px">📁 选择新文件 ('+(isScene?'.tss':'.tsd')+')</button><div style="font-size:13px;color:var(--text-tertiary);margin-top:10px;text-align:center">注:仅更新预览图可不选文件</div></div>';
|
||||
overlay.innerHTML = '<div class="modal" style="min-width:480px;max-width:520px;padding:24px"><h3 style="font-size:18px;font-weight:600;margin-bottom:4px">' + Lang.t('updateItem') + ' ' + typeLabel + '</h3><p style="font-size:12px;color:var(--text-secondary);margin-bottom:18px">重新上传包文件或更换预览图</p><div style="margin-bottom:12px"><label class="switch-label" style="font-size:13px"><input type="checkbox" id="pub-preview-toggle" checked><span class="switch-track"></span> 更换预览图</label></div><button id="pub-update-btn" class="btn btn-primary btn-sm" style="width:100%;margin-bottom:10px">📁 选择新文件 ('+(isScene?'.tss':'.tsd')+')</button><div style="font-size:13px;color:var(--text-tertiary);margin-top:10px;text-align:center">注:仅更新预览图可不选文件</div></div>';
|
||||
document.body.appendChild(overlay);
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||||
|
||||
let updateFile = null, previewFile = null;
|
||||
const previewDrop = document.createElement('div');
|
||||
previewDrop.style.cssText = 'border:2px dashed var(--border-normal);border-radius:8px;padding:20px;text-align:center;color:var(--text-tertiary);font-size:13px;cursor:pointer;margin:8px 0;transition:all .2s';
|
||||
previewDrop.textContent = '点击或拖放新预览图 (最大 6MB)';
|
||||
previewDrop.textContent = Lang.t('dropPreview');
|
||||
overlay.querySelector('.modal').insertBefore(previewDrop, overlay.querySelector('#pub-update-btn'));
|
||||
|
||||
function pickPreview() {
|
||||
const inp = document.createElement('input'); inp.type = 'file'; inp.accept = 'image/*'; inp.style.display = 'none';
|
||||
inp.onchange = function() {
|
||||
const f = this.files[0]; if (!f) return;
|
||||
if (f.size > 6291456) { Toast.show('预览图不能超过 6MB', 'error'); return; }
|
||||
if (!f.type.startsWith('image/')) { Toast.show('仅支持图片格式', 'error'); return; }
|
||||
previewFile = f; previewDrop.textContent = '✓ '+f.name+' ('+(f.size/1024/1024).toFixed(1)+' MB)';
|
||||
if (f.size > 6291456) { Toast.show(Lang.t('previewTooLarge'), 'error'); return; }
|
||||
if (!f.type.startsWith('image/')) { Toast.show(Lang.t('imageOnly'), 'error'); return; }
|
||||
previewFile = f; previewDrop.textContent = '\u2713 '+f.name+' ('+(f.size/1024/1024).toFixed(1)+' MB)';
|
||||
previewDrop.style.borderColor = 'var(--accent)'; previewDrop.style.color = 'var(--accent)';
|
||||
};
|
||||
document.body.appendChild(inp); inp.click();
|
||||
@@ -424,27 +425,27 @@ const PageMarket = {
|
||||
['dragleave','drop'].forEach(ev => previewDrop.addEventListener(ev, e => { e.preventDefault(); previewDrop.style.borderColor = 'var(--border-normal)'; }));
|
||||
previewDrop.addEventListener('drop', function(e) {
|
||||
e.preventDefault(); const f = e.dataTransfer.files[0]; if (!f) return;
|
||||
if (!f.type.startsWith('image/')) { Toast.show('仅支持图片格式', 'error'); return; }
|
||||
if (f.size > 6291456) { Toast.show('预览图不能超过 6MB', 'error'); return; }
|
||||
previewFile = f; previewDrop.textContent = '✓ '+f.name+' ('+(f.size/1024/1024).toFixed(1)+' MB)';
|
||||
if (!f.type.startsWith('image/')) { Toast.show(Lang.t('imageOnly'), 'error'); return; }
|
||||
if (f.size > 6291456) { Toast.show(Lang.t('previewTooLarge'), 'error'); return; }
|
||||
previewFile = f; previewDrop.textContent = '\u2713 '+f.name+' ('+(f.size/1024/1024).toFixed(1)+' MB)';
|
||||
previewDrop.style.borderColor = 'var(--accent)'; previewDrop.style.color = 'var(--accent)';
|
||||
});
|
||||
previewDrop.addEventListener('click', pickPreview);
|
||||
|
||||
overlay.querySelector('#pub-preview-toggle').addEventListener('change', function() {
|
||||
previewDrop.style.display = this.checked ? '' : 'none';
|
||||
if (!this.checked) { previewFile = null; previewDrop.textContent = '点击或拖放新预览图 (最大 6MB)'; previewDrop.style.borderColor = 'var(--border-normal)'; previewDrop.style.color = 'var(--text-tertiary)'; }
|
||||
if (!this.checked) { previewFile = null; previewDrop.textContent = Lang.t('dropPreview'); previewDrop.style.borderColor = 'var(--border-normal)'; previewDrop.style.color = 'var(--text-tertiary)'; }
|
||||
});
|
||||
|
||||
overlay.querySelector('#pub-update-btn').addEventListener('click', () => {
|
||||
const input = document.createElement('input'); input.type = 'file'; input.accept = isScene ? '.tss' : '.tsd';
|
||||
input.onchange = function(e) { updateFile = e.target.files[0]; if (updateFile) { const btn = overlay.querySelector('#pub-update-btn'); btn.textContent = '✓ '+updateFile.name; btn.style.background = 'var(--accent)'; btn.style.color = '#fff'; } };
|
||||
input.onchange = function(e) { updateFile = e.target.files[0]; if (updateFile) { const btn = overlay.querySelector('#pub-update-btn'); btn.textContent = '\u2713 '+updateFile.name; btn.style.background = 'var(--accent)'; btn.style.color = '#fff'; } };
|
||||
input.click();
|
||||
});
|
||||
|
||||
overlay.querySelector('#pub-update-btn').insertAdjacentHTML('afterend', '<button id="pub-do-update" class="btn btn-primary btn-sm" style="width:100%;margin-top:10px">确认更新</button>');
|
||||
overlay.querySelector('#pub-update-btn').insertAdjacentHTML('afterend', '<button id="pub-do-update" class="btn btn-primary btn-sm" style="width:100%;margin-top:10px">' + Lang.t('confirmUpdate') + '</button>');
|
||||
overlay.querySelector('#pub-do-update').addEventListener('click', async () => {
|
||||
if (!updateFile && !previewFile) { Toast.show('请选择文件或预览图', 'error'); return; }
|
||||
if (!updateFile && !previewFile) { Toast.show(Lang.t('selectFileOrPreview'), 'error'); return; }
|
||||
const fd = new FormData(); fd.append('token', this._marketToken);
|
||||
if (updateFile) fd.append('file', updateFile);
|
||||
if (previewFile) fd.append('preview', previewFile);
|
||||
@@ -465,7 +466,7 @@ const PageMarket = {
|
||||
// Delete old record
|
||||
await fetch('/api/market/delete', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({id: ds.id, collection: ds.coll, token: this._marketToken}) });
|
||||
}
|
||||
Toast.show(data.error ? '更新失败: '+data.error : '更新成功', data.error ? 'error' : 'success');
|
||||
Toast.show(data.error ? Lang.t('updateFailed') + data.error : Lang.t('updated'), data.error ? 'error' : 'success');
|
||||
if (!data.error) { overlay.remove(); this._loadItems(); }
|
||||
});
|
||||
},
|
||||
@@ -474,20 +475,20 @@ const PageMarket = {
|
||||
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>
|
||||
<h2 style="font-size:24px;font-weight:700">${Lang.t('marketPage')}</h2>
|
||||
<p style="color:var(--text-secondary);font-size:14px;margin-top:4px">${Lang.t('marketDesc')} <span id="m-status" style="color:var(--text-tertiary)"> ${Lang.t('notLoggedIn')}</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>
|
||||
<button id="btn-m-upload" class="btn btn-sm btn-secondary" style="display:none">${Lang.t('publish')}</button>
|
||||
<button id="btn-m-login" class="btn btn-sm btn-primary">${Lang.t('login')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0;margin-bottom:16px;border-bottom:1px solid var(--border-subtle);align-items:flex-end">
|
||||
<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>
|
||||
<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">${Lang.t('dashboardsTab')}</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">${Lang.t('scenesTab')}</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">${Lang.t('myTab')}</button>
|
||||
<span style="flex:1"></span>
|
||||
<button id="btn-refresh-market" class="icon-btn" style="width:auto;padding:0 8px;margin-bottom:4px;font-size:12px" title="刷新">🔄</button>
|
||||
<button id="btn-refresh-market" class="icon-btn" style="width:auto;padding:0 8px;margin-bottom:4px;font-size:12px" title="${Lang.t('refresh')}">🔄</button>
|
||||
</div>
|
||||
<div id="market-grid" class="theme-grid"></div>
|
||||
</div>`;
|
||||
|
||||
Reference in New Issue
Block a user