fix(ui): clipboard fallback for non-secure contexts; touch-friendly delete button on key scope blocks; append named model after AUTO when adding (+ docs sync: encrypted storage, AUTO chain, live probing, image endpoint)

This commit is contained in:
root
2026-08-10 11:10:59 +08:00
parent 62c8a07b88
commit e2dd4d9727
4 changed files with 163 additions and 35 deletions

View File

@ -172,6 +172,9 @@ td.t-tag { white-space:nowrap; }
border-radius:99px; white-space:nowrap; }
.mb .copy-b { cursor:pointer; opacity:.8; font-size:12px; padding:0 3px; }
.mb .copy-b:hover { opacity:1; }
.mb .mb-x { cursor:pointer; font-size:15px; line-height:1; padding:4px 5px; margin:0 2px; border-radius:6px;
color:rgba(255,255,255,.85); background:rgba(0,0,0,.18); user-select:none; }
.mb .mb-x:hover { background:rgba(220,53,69,.85); color:#fff; }
.mb-ghost { position:fixed; pointer-events:none; opacity:.85; z-index:60; transform:rotate(2deg); }
.mb-empty { color:var(--muted); font-size:12.5px; }
.add-brick { display:inline-flex; align-items:center; gap:6px; padding:9px 16px; border-radius:12px;
@ -582,9 +585,29 @@ function toast(m) { const el = $('#toast'); el.textContent = m; el.style.display
function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function escAttr(s) { return esc(s).replace(/"/g, '&quot;'); }
function copyText(txt, okMsg) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(txt).then(() => toast(okMsg || t('toastCopied')), () => toast(t('toastCopyFail')));
} else { toast(t('toastCopyFail')); }
const done = () => toast(okMsg || t('toastCopied'));
const fail = () => toast(t('toastCopyFail'));
if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
navigator.clipboard.writeText(txt).then(done, () => legacyCopy(txt) ? done() : fail());
return;
}
legacyCopy(txt) ? done() : fail();
}
function legacyCopy(txt) {
try {
const ta = document.createElement('textarea');
ta.value = txt;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.opacity = '0';
ta.style.pointerEvents = 'none';
document.body.appendChild(ta);
ta.select();
ta.setSelectionRange(0, ta.value.length);
const ok = document.execCommand('copy');
document.body.removeChild(ta);
return ok;
} catch (e) { return false; }
}
/* ---------- status tab ---------- */
@ -1779,6 +1802,7 @@ function scopeHtml(key, m) {
<span class="mb-name">${esc(m.model)}${src ? `<em class="mb-src">${esc(src)}</em>` : ''}</span>
<span class="mb-quota">${esc(quantBadge(m.token_quota, m.period, m.hours))}</span>
<span class="copy-b" title="${escAttr(t('kCopyB'))}" onclick="event.stopPropagation();scopeDup('${escAttr(key)}','${escAttr(comb)}')">⧉</span>
<span class="mb-x" title="${escAttr(t('kDelB'))}" onclick="event.stopPropagation();scopeRm('${escAttr(key)}','${escAttr(comb)}')">×</span>
</span>`;
}
function normSrc(s) { return (!s || s === 'undefined' || s === 'null') ? '' : s; }
@ -1824,10 +1848,18 @@ async function scopePush(key) {
const canvas = document.querySelector(`.key-canvas[data-key="${CSS.escape(key)}"]`);
if (!canvas) return;
const scopes = readScopes(canvas);
if (!scopes.some(s => s.model === 'AUTO')) scopes.push({ model: 'AUTO', token_quota: 0 });
let added;
if (!scopes.some(s => s.model === 'AUTO')) {
added = { model: 'AUTO', token_quota: 0 };
scopes.push(added);
} else {
const pick = (allModels || []).find(m => m.id && m.id !== 'AUTO' && !scopes.some(s => s.model === m.id && normSrc(s.source) === m.src));
added = pick ? { model: pick.id, source: pick.src, token_quota: 0 } : { model: 'AUTO', token_quota: 0 };
scopes.push(added);
}
try { await putScope(key, scopes); } catch (e) { toast(e.message); return; }
await loadKeys();
scopeEdit(key, 'AUTO');
scopeEdit(key, scopeComb(added));
}
async function scopeDup(key, comb) {
const canvas = document.querySelector(`.key-canvas[data-key="${CSS.escape(key)}"]`);