9379bc4f4f
Co-authored-by: Cursor <cursoragent@cursor.com>
279 lines
13 KiB
JavaScript
279 lines
13 KiB
JavaScript
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
|
* 专有软件 — 未经授权禁止复制、传播、转售。
|
|
* 详见 LICENSE.zh-CN.txt
|
|
*/
|
|
(function () {
|
|
function bootSettingsPage() {
|
|
if (!document.querySelector('.settings-page')) return;
|
|
|
|
var sel = document.getElementById('position-sizing-mode');
|
|
var lotsField = document.getElementById('field-fixed-lots');
|
|
var amountField = document.getElementById('field-fixed-amount');
|
|
function syncSizingFields() {
|
|
if (!sel) return;
|
|
var isAmount = sel.value === 'amount';
|
|
if (lotsField) lotsField.hidden = isAmount;
|
|
if (amountField) amountField.hidden = !isAmount;
|
|
}
|
|
if (sel && !sel.dataset.settingsBound) {
|
|
sel.dataset.settingsBound = '1';
|
|
sel.addEventListener('change', syncSizingFields);
|
|
}
|
|
syncSizingFields();
|
|
|
|
var aiProviderSel = document.getElementById('ai-provider-select');
|
|
function syncAiProviderCards() {
|
|
if (!aiProviderSel) return;
|
|
var val = aiProviderSel.value;
|
|
document.querySelectorAll('.settings-ai-card[data-ai-provider]').forEach(function (card) {
|
|
var active = card.getAttribute('data-ai-provider') === val;
|
|
card.classList.toggle('is-active', active);
|
|
var badge = card.querySelector('.settings-ai-card-head .badge');
|
|
if (badge) badge.style.display = active ? '' : 'none';
|
|
});
|
|
}
|
|
if (aiProviderSel && !aiProviderSel.dataset.settingsBound) {
|
|
aiProviderSel.dataset.settingsBound = '1';
|
|
aiProviderSel.addEventListener('change', syncAiProviderCards);
|
|
}
|
|
syncAiProviderCards();
|
|
|
|
var SETTINGS_FOLD_KEY = 'qihuo_settings_fold';
|
|
function setSettingsFold(el, collapsed) {
|
|
if (!el) return;
|
|
el.classList.toggle('is-collapsed', collapsed);
|
|
var head = el.querySelector('.settings-fold-head');
|
|
if (head) head.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
}
|
|
function saveSettingsFoldState() {
|
|
var state = {};
|
|
document.querySelectorAll('[data-settings-fold]').forEach(function (el) {
|
|
state[el.getAttribute('data-settings-fold')] = el.classList.contains('is-collapsed');
|
|
});
|
|
try { localStorage.setItem(SETTINGS_FOLD_KEY, JSON.stringify(state)); } catch (e) { /* ignore */ }
|
|
}
|
|
function loadSettingsFoldState() {
|
|
try {
|
|
var raw = localStorage.getItem(SETTINGS_FOLD_KEY);
|
|
if (!raw) return;
|
|
var state = JSON.parse(raw);
|
|
document.querySelectorAll('[data-settings-fold]').forEach(function (el) {
|
|
var key = el.getAttribute('data-settings-fold');
|
|
if (Object.prototype.hasOwnProperty.call(state, key)) {
|
|
setSettingsFold(el, !!state[key]);
|
|
}
|
|
});
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
document.querySelectorAll('.settings-fold-head').forEach(function (btn) {
|
|
if (btn.dataset.settingsBound) return;
|
|
btn.dataset.settingsBound = '1';
|
|
btn.addEventListener('click', function () {
|
|
var panel = btn.closest('[data-settings-fold]');
|
|
if (!panel) return;
|
|
setSettingsFold(panel, !panel.classList.contains('is-collapsed'));
|
|
saveSettingsFoldState();
|
|
});
|
|
});
|
|
loadSettingsFoldState();
|
|
|
|
var CTP_FOLD_KEY = 'qihuo_ctp_fold';
|
|
function setCtpFold(el, collapsed) {
|
|
if (!el) return;
|
|
el.classList.toggle('is-collapsed', collapsed);
|
|
var head = el.querySelector('.settings-ctp-fold-head');
|
|
if (head) head.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
}
|
|
function saveCtpFoldState() {
|
|
var state = {};
|
|
document.querySelectorAll('[data-ctp-fold]').forEach(function (el) {
|
|
state[el.getAttribute('data-ctp-fold')] = el.classList.contains('is-collapsed');
|
|
});
|
|
try { localStorage.setItem(CTP_FOLD_KEY, JSON.stringify(state)); } catch (e) { /* ignore */ }
|
|
}
|
|
function loadCtpFoldState() {
|
|
try {
|
|
var raw = localStorage.getItem(CTP_FOLD_KEY);
|
|
if (!raw) return;
|
|
var state = JSON.parse(raw);
|
|
document.querySelectorAll('[data-ctp-fold]').forEach(function (el) {
|
|
var key = el.getAttribute('data-ctp-fold');
|
|
if (Object.prototype.hasOwnProperty.call(state, key)) {
|
|
setCtpFold(el, !!state[key]);
|
|
}
|
|
});
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
document.querySelectorAll('.settings-ctp-fold-head').forEach(function (btn) {
|
|
if (btn.dataset.settingsBound) return;
|
|
btn.dataset.settingsBound = '1';
|
|
btn.addEventListener('click', function () {
|
|
var panel = btn.closest('[data-ctp-fold]');
|
|
if (!panel) return;
|
|
setCtpFold(panel, !panel.classList.contains('is-collapsed'));
|
|
saveCtpFoldState();
|
|
});
|
|
});
|
|
loadCtpFoldState();
|
|
|
|
function initBackupPanel() {
|
|
var uploadBtn = document.getElementById('backup-upload-btn');
|
|
var uploadInput = document.getElementById('backup-upload-file');
|
|
var statusEl = document.getElementById('backup-restore-status');
|
|
var pollTimer = null;
|
|
|
|
function setRestoreStatus(data) {
|
|
if (!statusEl || !data) return;
|
|
var state = data.state || 'idle';
|
|
statusEl.hidden = state === 'idle';
|
|
statusEl.classList.remove('is-running', 'is-error', 'is-done');
|
|
if (state === 'pending' || state === 'running') {
|
|
statusEl.classList.add('is-running');
|
|
statusEl.textContent = data.message || '恢复进行中…';
|
|
} else if (state === 'done') {
|
|
statusEl.classList.add('is-done');
|
|
statusEl.textContent = data.message || '恢复完成';
|
|
} else if (state === 'error') {
|
|
statusEl.classList.add('is-error');
|
|
statusEl.textContent = '恢复失败:' + (data.message || '未知错误');
|
|
} else {
|
|
statusEl.textContent = data.message || '';
|
|
}
|
|
}
|
|
|
|
function setBusy(busy) {
|
|
document.querySelectorAll('[data-backup-restore], #backup-upload-btn').forEach(function (btn) {
|
|
btn.disabled = !!busy;
|
|
});
|
|
}
|
|
|
|
function pollRestoreStatus() {
|
|
fetch('/api/backup/restore/status', { credentials: 'same-origin' })
|
|
.then(function (res) { return res.json(); })
|
|
.then(function (data) {
|
|
setRestoreStatus(data);
|
|
var active = data.state === 'pending' || data.state === 'running';
|
|
setBusy(active);
|
|
if (active) {
|
|
if (!pollTimer) {
|
|
pollTimer = window.setInterval(pollRestoreStatus, 2500);
|
|
}
|
|
} else if (pollTimer) {
|
|
window.clearInterval(pollTimer);
|
|
pollTimer = null;
|
|
if (data.state === 'done') {
|
|
window.setTimeout(function () { window.location.reload(); }, 1200);
|
|
}
|
|
}
|
|
})
|
|
.catch(function () { /* ignore */ });
|
|
}
|
|
|
|
if (uploadBtn && uploadInput && !uploadBtn.dataset.settingsBound) {
|
|
uploadBtn.dataset.settingsBound = '1';
|
|
uploadBtn.addEventListener('click', function () {
|
|
var file = uploadInput.files && uploadInput.files[0];
|
|
if (!file) {
|
|
window.alert('请先选择 .tar.gz 备份文件');
|
|
return;
|
|
}
|
|
if (!/\.tar\.gz$/i.test(file.name)) {
|
|
window.alert('仅支持 .tar.gz 格式');
|
|
return;
|
|
}
|
|
var form = new FormData();
|
|
form.append('file', file);
|
|
uploadBtn.disabled = true;
|
|
uploadBtn.textContent = '上传中…';
|
|
fetch('/api/backup/upload', { method: 'POST', body: form, credentials: 'same-origin' })
|
|
.then(function (res) { return res.json().then(function (body) { return { ok: res.ok, body: body }; }); })
|
|
.then(function (result) {
|
|
if (!result.ok) {
|
|
throw new Error((result.body && result.body.error) || '上传失败');
|
|
}
|
|
window.alert('上传成功:' + (result.body.name || file.name));
|
|
window.location.reload();
|
|
})
|
|
.catch(function (err) {
|
|
window.alert(err.message || '上传失败');
|
|
})
|
|
.finally(function () {
|
|
uploadBtn.disabled = false;
|
|
uploadBtn.textContent = '上传并校验';
|
|
});
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('[data-backup-restore]').forEach(function (btn) {
|
|
if (btn.dataset.settingsBound) return;
|
|
btn.dataset.settingsBound = '1';
|
|
btn.addEventListener('click', function () {
|
|
var name = btn.getAttribute('data-backup-restore') || '';
|
|
if (!name) return;
|
|
var ok = window.confirm(
|
|
'确定要恢复备份「' + name + '」吗?\n\n'
|
|
+ '将停止服务并覆盖当前数据库、uploads 与 .env,完成后自动重启。\n'
|
|
+ '此操作不可撤销,请确认已做好当前数据备份。'
|
|
);
|
|
if (!ok) return;
|
|
var typed = window.prompt('请输入 RESTORE 确认恢复:');
|
|
if (typed !== 'RESTORE') {
|
|
if (typed !== null) window.alert('确认文字不正确,已取消');
|
|
return;
|
|
}
|
|
btn.disabled = true;
|
|
fetch('/api/backup/restore', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ filename: name, confirm: 'RESTORE' })
|
|
})
|
|
.then(function (res) { return res.json().then(function (body) { return { ok: res.ok, body: body }; }); })
|
|
.then(function (result) {
|
|
if (!result.ok) {
|
|
throw new Error((result.body && result.body.error) || '恢复启动失败');
|
|
}
|
|
setRestoreStatus({ state: 'pending', message: result.body.message || '恢复已开始…' });
|
|
setBusy(true);
|
|
pollRestoreStatus();
|
|
})
|
|
.catch(function (err) {
|
|
window.alert(err.message || '恢复启动失败');
|
|
btn.disabled = false;
|
|
});
|
|
});
|
|
});
|
|
|
|
if (statusEl && !statusEl.hidden) {
|
|
pollRestoreStatus();
|
|
}
|
|
}
|
|
initBackupPanel();
|
|
|
|
var ctpForm = document.getElementById('ctp-settings-form');
|
|
if (ctpForm && !ctpForm.dataset.settingsBound) {
|
|
ctpForm.dataset.settingsBound = '1';
|
|
ctpForm.addEventListener('submit', function (ev) {
|
|
var ctpCard = document.querySelector('[data-settings-fold="ctp"]');
|
|
if (ctpCard) setSettingsFold(ctpCard, false);
|
|
var simnowFold = document.querySelector('[data-ctp-fold="simnow"]');
|
|
if (simnowFold) setCtpFold(simnowFold, false);
|
|
var pwd = document.getElementById('simnow_password');
|
|
var pwdVal = pwd && pwd.value ? pwd.value.trim() : '';
|
|
var pwdWasSet = ctpForm.getAttribute('data-simnow-pwd-set') === '1';
|
|
if (pwdWasSet && !pwdVal) {
|
|
var ok = window.confirm(
|
|
'SimNow 交易密码为空,保存后不会更新密码(仍用旧密码)。\n\n'
|
|
+ '若快期已改密,请取消后在「交易密码」框手打新密码再保存。\n\n仍要保存其他项?'
|
|
);
|
|
if (!ok) ev.preventDefault();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (window.qihuoPageBoot) window.qihuoPageBoot(bootSettingsPage, '.settings-page');
|
|
else if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootSettingsPage);
|
|
else bootSettingsPage();
|
|
})();
|