145 lines
6.3 KiB
JavaScript
145 lines
6.3 KiB
JavaScript
"use strict";
|
||||
|
|
const get = (id) => document.getElementById(id);
|
|||
|
|
const form = get('credential-form');
|
|||
|
|
const button = get('save');
|
|||
|
|
let inputs = [];
|
|||
|
|
let meta;
|
|||
|
|
let busy = false;
|
|||
|
|
const saveLabel = () => inputs.some((input, index) => input.value.trim() && meta.fields[index].configured)
|
|||
|
|
? '替换并保存' : (meta.page.saveLabel ?? (meta.fields.length === 1 ? meta.fields[0].ui?.saveLabel : undefined) ?? '保存');
|
|||
|
|
async function request(url, options = {}) {
|
|||
|
|
let response;
|
|||
|
|
try {
|
|||
|
|
response = await fetch(url, { ...options, credentials: 'same-origin', cache: 'no-store',
|
|||
|
|
headers: { 'Content-Type': 'application/json', 'X-Local-Request': '1', ...options.headers }, signal: AbortSignal.timeout(25_000) });
|
|||
|
|
}
|
|||
|
|
catch {
|
|||
|
|
throw new Error('未收到服务回复,请刷新确认保存结果,不要重复提交。');
|
|||
|
|
}
|
|||
|
|
const result = await response.json();
|
|||
|
|
if (!response.ok)
|
|||
|
|
throw new Error(result.error ?? '操作未完成,请重试。');
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
function message(text) { const node = get('message'); node.textContent = text; node.hidden = false; }
|
|||
|
|
function clearInputs() { inputs.forEach(input => { input.value = ''; }); }
|
|||
|
|
function done() {
|
|||
|
|
clearInputs();
|
|||
|
|
form.hidden = true;
|
|||
|
|
get('heading').textContent = '已保存';
|
|||
|
|
get('hint').hidden = true;
|
|||
|
|
message('可以关闭此页,回到对话继续。');
|
|||
|
|
}
|
|||
|
|
function update() {
|
|||
|
|
button.disabled = busy || !meta || !['waiting', 'partial'].includes(meta.outcome)
|
|||
|
|
|| !inputs.some(input => input.value.trim())
|
|||
|
|
|| inputs.some((input, index) => !meta.fields[index].configured && !input.value.trim());
|
|||
|
|
if (meta && !busy)
|
|||
|
|
button.textContent = saveLabel();
|
|||
|
|
}
|
|||
|
|
function render() {
|
|||
|
|
clearInputs();
|
|||
|
|
inputs = [];
|
|||
|
|
const multi = meta.fields.length > 1;
|
|||
|
|
form.classList.toggle('multi', multi);
|
|||
|
|
const container = get('fields');
|
|||
|
|
container.replaceChildren();
|
|||
|
|
meta.fields.forEach((field, index) => {
|
|||
|
|
const wrapper = document.createElement('div');
|
|||
|
|
wrapper.className = 'field';
|
|||
|
|
const label = document.createElement('label');
|
|||
|
|
label.htmlFor = 'secret-' + index;
|
|||
|
|
label.textContent = field.label;
|
|||
|
|
if (!multi)
|
|||
|
|
label.className = 'visually-hidden';
|
|||
|
|
if (field.configured) {
|
|||
|
|
const state = document.createElement('span');
|
|||
|
|
state.className = 'field-state';
|
|||
|
|
state.textContent = '已配置';
|
|||
|
|
label.append(state);
|
|||
|
|
}
|
|||
|
|
const input = document.createElement('input');
|
|||
|
|
input.id = label.htmlFor;
|
|||
|
|
input.type = 'password';
|
|||
|
|
input.autocomplete = 'new-password';
|
|||
|
|
input.autocapitalize = 'off';
|
|||
|
|
input.spellcheck = false;
|
|||
|
|
input.maxLength = 2500;
|
|||
|
|
input.required = !field.configured;
|
|||
|
|
input.setAttribute('aria-describedby', 'hint message');
|
|||
|
|
input.placeholder = field.configured ? '留空保留,输入则替换' : (field.ui?.placeholder ?? '粘贴 API Key');
|
|||
|
|
input.disabled = !['waiting', 'partial'].includes(meta.outcome);
|
|||
|
|
wrapper.append(label, input);
|
|||
|
|
container.append(wrapper);
|
|||
|
|
inputs.push(input);
|
|||
|
|
});
|
|||
|
|
get('context').textContent = meta.page.label ?? (multi ? meta.fields.length + ' 项凭据' : meta.fields[0].label);
|
|||
|
|
const title = meta.page.title ?? (multi ? '输入密钥' : meta.fields[0].ui?.title) ?? '输入密钥';
|
|||
|
|
get('heading').textContent = title;
|
|||
|
|
document.title = title;
|
|||
|
|
const storage = [...new Set(meta.fields.map(field => field.storage))].join('、');
|
|||
|
|
get('hint').textContent = '仅保存到' + storage + (meta.fields.some(field => field.configured) ? ' · 已配置项留空保留' : '');
|
|||
|
|
update();
|
|||
|
|
if (meta.outcome === 'saved')
|
|||
|
|
done();
|
|||
|
|
else if (meta.outcome === 'partial')
|
|||
|
|
message('上次仅部分保存。请核对已配置项,补填未完成项后重试。');
|
|||
|
|
else if (meta.outcome !== 'waiting')
|
|||
|
|
message('本次配置已结束,请重新打开入口。');
|
|||
|
|
}
|
|||
|
|
form.addEventListener('input', () => { update(); get('message').hidden = true; });
|
|||
|
|
form.addEventListener('submit', async (event) => {
|
|||
|
|
event.preventDefault();
|
|||
|
|
if (busy || button.disabled)
|
|||
|
|
return;
|
|||
|
|
busy = true;
|
|||
|
|
update();
|
|||
|
|
inputs.forEach(input => { input.disabled = true; });
|
|||
|
|
button.textContent = '正在保存…';
|
|||
|
|
try {
|
|||
|
|
const entries = inputs.flatMap((input, index) => input.value.trim() ? [{ credential: meta.fields[index].credential,
|
|||
|
|
value: input.value, revision: meta.fields[index].revision, replaceExisting: meta.fields[index].configured }] : []);
|
|||
|
|
const result = await request('/api/save', { method: 'POST', body: JSON.stringify({ entries }) });
|
|||
|
|
if (result.status === 'saved') {
|
|||
|
|
meta.outcome = 'saved';
|
|||
|
|
done();
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
clearInputs();
|
|||
|
|
meta = await request('/api/meta');
|
|||
|
|
render();
|
|||
|
|
const statuses = { saved: '已保存', failed: '未确认成功', not_attempted: '未尝试' };
|
|||
|
|
message((result.results ?? []).map(item => (meta.fields.find(field => field.credential === item.credential)?.label ?? '凭据') + ':' + (statuses[item.status] ?? '请核对')).join(';')
|
|||
|
|
+ '。请核对状态后重新填写未完成项。');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
clearInputs();
|
|||
|
|
try {
|
|||
|
|
meta = await request('/api/meta');
|
|||
|
|
render();
|
|||
|
|
}
|
|||
|
|
catch {
|
|||
|
|
meta.outcome = 'unknown';
|
|||
|
|
}
|
|||
|
|
if (meta.outcome !== 'saved')
|
|||
|
|
message(error instanceof Error ? error.message : '保存未完成,请刷新确认结果。');
|
|||
|
|
}
|
|||
|
|
finally {
|
|||
|
|
clearInputs();
|
|||
|
|
busy = false;
|
|||
|
|
inputs.forEach(input => { input.disabled = !['waiting', 'partial'].includes(meta.outcome); });
|
|||
|
|
update();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
window.addEventListener('pagehide', clearInputs);
|
|||
|
|
async function initialize() {
|
|||
|
|
const token = location.hash.slice(1);
|
|||
|
|
if (token) {
|
|||
|
|
history.replaceState(null, '', location.pathname);
|
|||
|
|
await request('/api/session', { method: 'POST', headers: { Authorization: 'Bearer ' + token }, body: '{}' });
|
|||
|
|
}
|
|||
|
|
meta = await request('/api/meta');
|
|||
|
|
render();
|
|||
|
|
}
|
|||
|
|
initialize().catch(error => { get('context').textContent = '暂时无法连接'; message(error instanceof Error ? error.message : '请重新打开配置入口。'); });
|