mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-14 12:18:06 +08:00
update 2026-11-07 22:36:55
This commit is contained in:
1 parent
dad27f3210
commit
ec19855d48
25 files changed
+1388
-445
No files matched your search
@@ -382,7 +382,6 @@ return view.extend({
|
||||
const restoreCfgBtn = E('button', { 'class': 'cbi-button cbi-button-negative', 'type': 'button' }, _('Restore config'));
|
||||
/* 忘记密码用:只清管理员记录,不动订阅和规则 */
|
||||
const resetAdminBtn = E('button', { 'class': 'cbi-button', 'type': 'button' }, _('Reset administrator'));
|
||||
const fileInput = E('input', { 'type': 'file', 'accept': '.tar.gz,.gz,application/gzip', 'style': 'display:none' });
|
||||
let backupBusy = false;
|
||||
writeControls.push(exportBtn, importBtn, restoreCfgBtn, resetAdminBtn);
|
||||
|
||||
@@ -419,18 +418,12 @@ return view.extend({
|
||||
}).finally(function() { setBackupBusy(false); });
|
||||
});
|
||||
|
||||
/* 只让用户选一次文件:上传对话框自己会弹选择框。 */
|
||||
importBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (backupBusy) return;
|
||||
fileInput.click();
|
||||
});
|
||||
fileInput.addEventListener('change', function(ev) {
|
||||
const file = ev.target.files && ev.target.files[0];
|
||||
if (!file) return;
|
||||
if (!confirm(_('Stop Honk before restoring. Restoring replaces Honk user data. Continue?'))) {
|
||||
fileInput.value = '';
|
||||
if (!confirm(_('Stop Honk before restoring. Restoring replaces Honk user data. Continue?')))
|
||||
return;
|
||||
}
|
||||
setBackupBusy(true);
|
||||
backupMessage.textContent = _('Restoring…');
|
||||
honk.restorePrepare().then(honk.ensureOk).then(function() {
|
||||
@@ -449,7 +442,6 @@ return view.extend({
|
||||
backupMessage.textContent = honk.errorMessage(error, _('Restore failed.'));
|
||||
}).finally(function() {
|
||||
setBackupBusy(false);
|
||||
fileInput.value = '';
|
||||
});
|
||||
});
|
||||
|
||||
@@ -493,7 +485,6 @@ return view.extend({
|
||||
backupSection.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Config backup')));
|
||||
backupSection.appendChild(E('p', { 'class': 'honk-note' }, _('Export a backup archive, import one from your computer, or restore Honk to its default configuration.')));
|
||||
backupSection.appendChild(E('div', { 'class': 'honk-actions' }, [ exportBtn, importBtn, restoreCfgBtn, resetAdminBtn ]));
|
||||
backupSection.appendChild(fileInput);
|
||||
backupSection.appendChild(backupMessage);
|
||||
|
||||
/* --- System check and repair card --- */
|
||||
|
||||
@@ -31,13 +31,14 @@ function metaItem(label, value) {
|
||||
]);
|
||||
}
|
||||
|
||||
function field(label, input, help) {
|
||||
function field(label, control, help) {
|
||||
/* 说明文字放进控件列下方,两栏布局时才不会挤到右边去 */
|
||||
const cell = E('div', { 'class': 'cbi-value-field' }, [ input ]);
|
||||
const controlId = control.id || (control.querySelector && control.querySelector('input, select, textarea') || {}).id;
|
||||
const cell = E('div', { 'class': 'cbi-value-field' }, [ control ]);
|
||||
if (help)
|
||||
cell.appendChild(E('div', { 'class': 'cbi-value-description honk-note' }, help));
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title', 'for': input.id }, label),
|
||||
E('label', { 'class': 'cbi-value-title', 'for': controlId || null }, label),
|
||||
cell
|
||||
]);
|
||||
}
|
||||
@@ -68,6 +69,9 @@ function restoreFields(root, values) {
|
||||
|
||||
input.value = values[key].value;
|
||||
input.checked = values[key].checked;
|
||||
/* 自绘开关听的是 change,回填不会自己触发。 */
|
||||
if (input.classList.contains('honk-switch-input'))
|
||||
input.dispatchEvent(new Event('change'));
|
||||
if (values[key].focused) {
|
||||
input.focus();
|
||||
if (values[key].selectionStart !== null && typeof input.setSelectionRange === 'function')
|
||||
@@ -212,7 +216,8 @@ return view.extend({
|
||||
const form = E('form');
|
||||
const networkSelect = E('select', { 'class': 'cbi-input-select', 'name': 'lan_network', 'id': 'honk-lan-network' });
|
||||
const portInput = E('input', { 'class': 'cbi-input-text', 'type': 'number', 'name': 'listen_port', 'id': 'honk-listen-port', 'min': '1024', 'max': '65535', 'step': '1' });
|
||||
const clashEnabledInput = E('input', { 'type': 'checkbox', 'name': 'clash_api_enabled', 'id': 'honk-clash-enabled' });
|
||||
const clashEnabledInput = E('input', { 'type': 'checkbox', 'name': 'clash_api_enabled', 'id': 'honk-clash-enabled' });
|
||||
const clashSwitch = honk.switchControl(clashEnabledInput);
|
||||
const clashPortInput = E('input', { 'class': 'cbi-input-text', 'type': 'number', 'name': 'clash_api_port', 'id': 'honk-clash-port', 'min': '1024', 'max': '65535', 'step': '1' });
|
||||
const clashSecretInput = E('input', { 'class': 'cbi-input-password', 'type': 'password', 'name': 'clash_api_secret', 'id': 'honk-clash-secret', 'autocomplete': 'new-password', 'readonly': 'readonly' });
|
||||
clashSecretInput.addEventListener('focus', function() { clashSecretInput.removeAttribute('readonly'); });
|
||||
@@ -227,7 +232,7 @@ return view.extend({
|
||||
if (status.lan_network)
|
||||
networkSelect.value = status.lan_network;
|
||||
portInput.value = status.listen_port ? String(status.listen_port) : '9527';
|
||||
clashEnabledInput.checked = status.clash_api_enabled === true;
|
||||
clashEnabledInput.checked = status.clash_api_enabled === true;
|
||||
clashPortInput.value = status.clash_api_port ? String(status.clash_api_port) : '9090';
|
||||
|
||||
card.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Service settings')));
|
||||
@@ -238,12 +243,13 @@ return view.extend({
|
||||
input.disabled = true;
|
||||
});
|
||||
save.disabled = true;
|
||||
clashSwitch.syncSwitch();
|
||||
}
|
||||
if (!(data.networks || []).length)
|
||||
networkSelect.appendChild(E('option', { 'value': '' }, _('No network interfaces found')));
|
||||
form.appendChild(field(_('LAN network'), networkSelect));
|
||||
form.appendChild(field(_('Listen port'), portInput));
|
||||
form.appendChild(field(_('Enable Clash API'), clashEnabledInput));
|
||||
form.appendChild(field(_('Enable Clash API'), clashSwitch));
|
||||
form.appendChild(field(_('Clash API port'), clashPortInput));
|
||||
form.appendChild(field(_('Clash API secret'), clashSecretInput, status.clash_api_secret_set ? _('A secret is set. Leave blank to keep it.') : _('Required when enabling. Use 6–128 letters, numbers, dots, underscores or hyphens.')));
|
||||
form.appendChild(E('p', { 'class': 'honk-note' }, _('Use your existing HTTPS reverse proxy for external access. Enter the secret in Zashboard when connecting.')));
|
||||
|
||||
@@ -92,6 +92,73 @@ const callUpdateCheck = rpc.declare({ object: 'honk', method: 'update_check', ex
|
||||
const callUpdateApply = rpc.declare({ object: 'honk', method: 'update_apply', expect: { '': {} } });
|
||||
|
||||
let pageStylesInstalled = false;
|
||||
let switchStylesInstalled = false;
|
||||
|
||||
/* 开关样式随 JS 注入:CSS 与 JS 分开缓存,主题又各改各的 checkbox/button。 */
|
||||
const SWITCH_CSS = [
|
||||
'.honk-page .honk-switch-wrap{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;vertical-align:middle;line-height:1}',
|
||||
'.honk-page .honk-switch-label{font-size:11px;font-weight:600;letter-spacing:.3px;opacity:.62}',
|
||||
'.honk-page .honk-switch{position:relative!important;flex:0 0 auto;width:42px!important;height:22px!important;min-width:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;border-radius:999px!important;background:rgba(128,128,128,.28)!important;background-image:none!important;box-shadow:none!important;line-height:1!important;text-transform:none!important;transform:none!important;overflow:hidden;cursor:pointer;transition:background .18s ease,opacity .18s ease}',
|
||||
'.honk-page .honk-switch .honk-switch-knob{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:rgba(255,255,255,.96);box-shadow:0 1px 4px rgba(0,0,0,.2);transition:left .18s ease}',
|
||||
'.honk-page .honk-switch.is-on{background:rgba(74,160,101,.65)!important}',
|
||||
'.honk-page .honk-switch:disabled{opacity:.45;cursor:not-allowed}',
|
||||
'.honk-page .honk-switch:focus-visible{outline:2px solid rgba(74,160,101,.75);outline-offset:2px}',
|
||||
/* 0,3,1 才压得过 honk.css 里那颗 46px !important 的 checkbox。 */
|
||||
'.honk-page .honk-switch-wrap>input.honk-switch-input{position:absolute!important;width:1px!important;height:1px!important;margin:0!important;padding:0!important;border:0!important;opacity:0!important;pointer-events:none!important;appearance:none!important;-webkit-appearance:none!important;box-shadow:none!important;background:none!important}'
|
||||
].join('\n');
|
||||
|
||||
function installSwitchStyles() {
|
||||
if (switchStylesInstalled || document.getElementById('honk-switch-styles'))
|
||||
return;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'honk-switch-styles';
|
||||
style.appendChild(document.createTextNode(SWITCH_CSS));
|
||||
document.head.appendChild(style);
|
||||
switchStylesInstalled = true;
|
||||
}
|
||||
|
||||
/* 主题对 input[type=checkbox] 的处理各不相同(Argon 会放大成错位的胶囊,
|
||||
Bootstrap 干脆是原生小方框),所以开关自己画:真 input 留在表单里保证
|
||||
取值/回填逻辑不变,外面套一层可点的胶囊和 ON/OFF 文字。 */
|
||||
function switchControl(input) {
|
||||
installSwitchStyles();
|
||||
input.classList.add('honk-switch-input');
|
||||
input.setAttribute('tabindex', '-1');
|
||||
|
||||
const caption = E('span', { 'class': 'honk-switch-label' }, '');
|
||||
const knob = E('span', { 'class': 'honk-switch-knob' });
|
||||
const toggle = E('button', {
|
||||
'class': 'honk-switch',
|
||||
'type': 'button',
|
||||
'role': 'switch',
|
||||
'aria-checked': input.checked ? 'true' : 'false'
|
||||
}, knob);
|
||||
|
||||
const sync = function() {
|
||||
toggle.classList.toggle('is-on', !!input.checked);
|
||||
toggle.setAttribute('aria-checked', input.checked ? 'true' : 'false');
|
||||
toggle.disabled = !!input.disabled;
|
||||
caption.textContent = input.checked ? 'ON' : 'OFF';
|
||||
knob.style.left = input.checked ? '23px' : '3px';
|
||||
};
|
||||
|
||||
toggle.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (input.disabled)
|
||||
return;
|
||||
input.checked = !input.checked;
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
sync();
|
||||
});
|
||||
input.addEventListener('change', sync);
|
||||
|
||||
/* 表单回填(restoreFields)直接改 input.checked,不会触发 change。 */
|
||||
const wrapper = E('span', { 'class': 'honk-switch-wrap' }, [ input, caption, toggle ]);
|
||||
wrapper.syncSwitch = sync;
|
||||
sync();
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function installStyles() {
|
||||
if (pageStylesInstalled || document.getElementById('honk-page-styles'))
|
||||
@@ -100,7 +167,7 @@ function installStyles() {
|
||||
const stylesheet = document.createElement('link');
|
||||
stylesheet.id = 'honk-page-styles';
|
||||
stylesheet.rel = 'stylesheet';
|
||||
stylesheet.href = L.resource('view/honk/honk.css');
|
||||
stylesheet.href = L.resource('view/honk/honk.css') + (L.env.resource_version ? '?v=' + L.env.resource_version : '');
|
||||
document.head.appendChild(stylesheet);
|
||||
pageStylesInstalled = true;
|
||||
}
|
||||
@@ -205,6 +272,16 @@ function tokenMessage(token) {
|
||||
data_reset: _('Honk data reset. The service is stopped.'),
|
||||
worker_lost: _('The maintenance worker stopped unexpectedly; check system recovery status before retrying.'),
|
||||
operation_failed: _('The maintenance operation failed.'),
|
||||
maintenance_no_result: _('The maintenance script stopped without reporting a result. Check Maintenance → Logs → Maintenance for the reason.'),
|
||||
honk_tree_missing: _('The /etc/honk directory is missing or is a symlink.'),
|
||||
honk_tree_has_symlink: _('There is a symlink under /etc/honk. The backup cannot carry it; remove it and try again.'),
|
||||
honk_tree_has_special_file: _('There is a non-regular file (device, socket, fifo) under /etc/honk. Remove it and try again.'),
|
||||
honk_tree_unreadable: _('A file under /etc/honk could not be read. Check the filesystem and permissions.'),
|
||||
honk_backup_file_too_large: _('A single file under /etc/honk is larger than 64 MB. The service log names it.'),
|
||||
honk_backup_tree_too_large: _('The backup would exceed 512 files or 127 MB. The service log names the entry that crossed the limit.'),
|
||||
honk_config_incomplete: _('config.dae or system.dae is missing under /etc/honk. Reinstall or repair Honk before backing up.'),
|
||||
unsupported_honk_tree_entry: _('The backup found an entry under /etc/honk it cannot archive (a symlink, a special file, or too many files).'),
|
||||
unsafe_honk_file_name: _('A file under /etc/honk has a name the backup cannot carry (spaces, wildcards or "../"). The service log names it; rename it and try again.'),
|
||||
job_timeout: _('The maintenance operation timed out.'),
|
||||
maintenance_recovery_required: _('Recovery is required. Run system recovery before starting another maintenance operation.'),
|
||||
update_recovery_required: _('The signed update did not finish. Keep Honk stopped and reinstall a complete verified package set before retrying.'),
|
||||
@@ -502,5 +579,6 @@ return baseclass.extend({
|
||||
resultMessage: resultMessage,
|
||||
errorMessage: errorMessage,
|
||||
jobPhaseMessage: jobPhaseMessage,
|
||||
switchControl: switchControl,
|
||||
installStyles: installStyles
|
||||
});
|
||||
@@ -21,10 +21,14 @@ function getNetworkNames(networks) {
|
||||
}).sort();
|
||||
}
|
||||
|
||||
function field(label, input) {
|
||||
function field(label, control, help) {
|
||||
const controlId = control.id || (control.querySelector && control.querySelector('input, select, textarea') || {}).id;
|
||||
const cell = E('div', { 'class': 'cbi-value-field' }, [ control ]);
|
||||
if (help)
|
||||
cell.appendChild(E('div', { 'class': 'cbi-value-description honk-note' }, help));
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title', 'for': input.id }, label),
|
||||
E('div', { 'class': 'cbi-value-field' }, input)
|
||||
E('label', { 'class': 'cbi-value-title', 'for': controlId || null }, label),
|
||||
cell
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -48,7 +52,8 @@ return view.extend({
|
||||
const section = E('form', { 'class': 'honk-card' });
|
||||
const networkSelect = E('select', { 'class': 'cbi-input-select', 'name': 'lan_network', 'id': 'honk-lan-network' });
|
||||
const portInput = E('input', { 'class': 'cbi-input-text', 'type': 'number', 'name': 'listen_port', 'id': 'honk-listen-port', 'min': '1024', 'max': '65535', 'step': '1' });
|
||||
const clashEnabledInput = E('input', { 'type': 'checkbox', 'name': 'clash_api_enabled', 'id': 'honk-clash-enabled' });
|
||||
const clashEnabledInput = E('input', { 'type': 'checkbox', 'name': 'clash_api_enabled', 'id': 'honk-clash-enabled' });
|
||||
const clashSwitch = honk.switchControl(clashEnabledInput);
|
||||
const clashPortInput = E('input', { 'class': 'cbi-input-text', 'type': 'number', 'name': 'clash_api_port', 'id': 'honk-clash-port', 'min': '1024', 'max': '65535', 'step': '1' });
|
||||
const clashSecretInput = E('input', { 'class': 'cbi-input-password', 'type': 'password', 'name': 'clash_api_secret', 'id': 'honk-clash-secret', 'autocomplete': 'new-password', 'readonly': 'readonly' });
|
||||
clashSecretInput.addEventListener('focus', function() { clashSecretInput.removeAttribute('readonly'); });
|
||||
@@ -64,7 +69,8 @@ return view.extend({
|
||||
if (data.status && data.status.lan_network)
|
||||
networkSelect.value = data.status.lan_network;
|
||||
portInput.value = data.status && data.status.listen_port ? String(data.status.listen_port) : '9527';
|
||||
clashEnabledInput.checked = !!(data.status && data.status.clash_api_enabled === true);
|
||||
clashEnabledInput.checked = !!(data.status && data.status.clash_api_enabled === true);
|
||||
clashSwitch.syncSwitch();
|
||||
clashPortInput.value = data.status && data.status.clash_api_port ? String(data.status.clash_api_port) : '9090';
|
||||
|
||||
section.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Honk settings')));
|
||||
@@ -76,6 +82,7 @@ return view.extend({
|
||||
input.disabled = true;
|
||||
});
|
||||
save.disabled = true;
|
||||
clashSwitch.syncSwitch();
|
||||
}
|
||||
if (data.statusError)
|
||||
section.appendChild(E('div', { 'class': 'alert-message' }, _('Could not read current settings: %s').format(data.statusError)));
|
||||
@@ -85,7 +92,7 @@ return view.extend({
|
||||
networkSelect.appendChild(E('option', { 'value': '' }, _('No network interfaces found')));
|
||||
section.appendChild(field(_('LAN network'), networkSelect));
|
||||
section.appendChild(field(_('Listen port'), portInput));
|
||||
section.appendChild(field(_('Enable Clash API'), clashEnabledInput));
|
||||
section.appendChild(field(_('Enable Clash API'), clashSwitch));
|
||||
section.appendChild(field(_('Clash API port'), clashPortInput));
|
||||
section.appendChild(field(_('Clash API secret'), clashSecretInput));
|
||||
section.appendChild(E('p', { 'class': 'honk-note' }, data.status && data.status.clash_api_secret_set ? _('A secret is set. Leave blank to keep it. Use your existing HTTPS reverse proxy for external access.') : _('A secret is required when enabling Clash API. Use 6–128 letters, numbers, dots, underscores or hyphens.')));
|
||||
|
||||
Reference in new issue
Block a user