Files
op-packages/luci-app-sysctl/htdocs/luci-static/resources/view/sysctl.js
T

1384 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* SPDX-License-Identifier: Apache-2.0
*
* luci-app-sysctl - LuCI view for kernel sysctl parameters (OpenWrt 24.10)
* v1.4.0: modal-free architecture - all views, editors, confirmations and
* results render inline on the page (robust against theme quirks on
* third-party firmwares where ui.showModal may be invisible).
*
* Section 1: custom parameters (/etc/sysctl.d/99-luci-sysctl.conf)
* Section 2: online preset (/etc/sysctl.d/98-online-preset.conf)
* Section 3: browse/search live kernel parameters from /proc/sys
*/
'use strict';
'require view';
'require rpc';
'require ui';
'require dom';
var callList = rpc.declare({ object: 'luci.sysctl', method: 'list' });
var callStatus = rpc.declare({ object: 'luci.sysctl', method: 'status' });
var callBrowse = rpc.declare({ object: 'luci.sysctl', method: 'browse', params: [ 'prefix' ] });
var callSearch = rpc.declare({ object: 'luci.sysctl', method: 'search', params: [ 'query', 'limit' ] });
var callSet = rpc.declare({ object: 'luci.sysctl', method: 'set', params: [ 'key', 'value', 'disabled', 'apply' ] });
var callRemove = rpc.declare({ object: 'luci.sysctl', method: 'remove', params: [ 'key' ] });
var callApply = rpc.declare({ object: 'luci.sysctl', method: 'apply' });
var callPresetStatus = rpc.declare({ object: 'luci.sysctl', method: 'preset_status' });
var callPresetFetch = rpc.declare({ object: 'luci.sysctl', method: 'preset_fetch', params: [ 'url', 'prefix' ] });
var callPresetImport = rpc.declare({ object: 'luci.sysctl', method: 'preset_import', params: [ 'url', 'prefix' ] });
var callPresetCheck = rpc.declare({ object: 'luci.sysctl', method: 'preset_check' });
var callPresetRemove = rpc.declare({
object: 'luci.sysctl',
method: 'preset_remove'
});
var callPresetList = rpc.declare({
object: 'luci.sysctl',
method: 'preset_list',
params: [ 'url', 'prefix' ]
});
var callFileView = rpc.declare({ object: 'luci.sysctl', method: 'file_view', params: [ 'path' ] });
var callFileSet = rpc.declare({ object: 'luci.sysctl', method: 'file_set', params: [ 'path', 'key', 'value', 'disabled' ] });
var callFileDelete = rpc.declare({ object: 'luci.sysctl', method: 'file_delete', params: [ 'path', 'key' ] });
var KEY_RE = /^[A-Za-z0-9_][A-Za-z0-9_.\/-]*$/;
function badge(text, kind) {
return E('span', { 'class': 'lsc-badge lsc-badge-' + kind }, text);
}
var LSC_CSS = [
'.lsc-root { font-size: 15px; }',
'.lsc-root .lsc-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 6px 0 2px; }',
'.lsc-root .lsc-chip { appearance: none; border: 1px solid #d0d7de; background: #fff; color: #24292f; border-radius: 999px; padding: 4px 12px; font-size: 12.5px; line-height: 1.5; cursor: pointer; text-transform: none; font-weight: 500; transition: background .15s, color .15s, border-color .15s, box-shadow .15s; box-shadow: 0 1px 2px rgba(16,24,40,.04); }',
'.lsc-root .lsc-chip:hover { border-color: #0969da; color: #0969da; background: #f3f8ff; }',
'.lsc-root .lsc-chip.lsc-chip-active { background: #0969da; border-color: #0969da; color: #fff; }',
'.lsc-root .lsc-tablewrap { border: 1px solid #e4e7ec; border-radius: 8px; overflow: auto; background: #fff; margin: 8px 0; }',
'.lsc-root table.lsc-table { width: 100%; border-collapse: collapse; font-size: 15px; margin: 0; }',
'.lsc-root .lsc-table thead th { background: #f6f8fa; text-align: center !important; font-weight: 600; color: #57606a; padding: 10px 12px; border-bottom: 1px solid #e4e7ec; white-space: nowrap; font-size: 15px; }',
'.lsc-root .lsc-table tbody td { padding: 10px 12px; border-bottom: 1px solid #f0f2f4; vertical-align: middle; text-align: center !important; }',
'.lsc-root .lsc-table tbody td .cbi-button { margin: 2px 3px; }',
'.lsc-root .lsc-table tbody tr:last-child td { border-bottom: none; }',
'.lsc-root .lsc-table tbody tr:nth-child(even) td { background: #fafbfc; }',
'.lsc-root .lsc-table tbody tr:hover td { background: #f0f6ff; }',
'.lsc-root .lsc-badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 13px; font-weight: 500; line-height: 1.6; white-space: nowrap; }',
'.lsc-root .lsc-badge-ok { background: #e6f6ec; color: #18794e; }',
'.lsc-root .lsc-badge-warn { background: #fff3e0; color: #a35a00; }',
'.lsc-root .lsc-badge-err { background: #fdecec; color: #b3261e; }',
'.lsc-root .lsc-badge-off { background: #eef0f2; color: #6e7781; }',
'.lsc-root .lsc-badge-main { background: #eef4fb; color: #34618e; }',
'.lsc-root .lsc-card { border: 1px solid #e4e7ec; border-radius: 10px; padding: 14px 16px; background: #fff; box-shadow: 0 1px 3px rgba(16,24,40,.06); margin: 10px 0; }',
'.lsc-root .lsc-card h3 { margin: 0 0 8px; font-size: 15px; font-weight: 600; }',
'.lsc-root .lsc-card h4 { margin: 10px 0 4px; font-size: 13.5px; color: #57606a; }',
'.lsc-root code { background: #f6f8fa; border: 1px solid #eef0f2; border-radius: 4px; padding: 1px 6px; font-size: 1em; word-break: break-all; }',
'.lsc-root .cbi-button { border-radius: 6px; transition: filter .15s, box-shadow .15s, background .15s; text-transform: none; }',
'.lsc-root .cbi-button:hover { filter: brightness(1.06); }',
'.lsc-root h2, .lsc-root h3, .lsc-root h4, .lsc-root th, .lsc-root .lsc-badge, .lsc-root code, .lsc-root .lsc-muted, .lsc-root .cbi-section-descr, .lsc-root .lsc-card * { text-transform: none !important; }',
'.lsc-root .lsc-muted { color: #777; }',
'.lsc-root .cbi-page-actions { float: none !important; text-align: center !important; }'
].join('\n');
return view.extend({
ensureStyle: function() {
if (document.getElementById('luci-app-sysctl-style') != null)
return;
document.head.appendChild(E('style', { 'id': 'luci-app-sysctl-style' }, [ LSC_CSS ]));
},
load: function() {
return Promise.all([ callList(), callStatus(), callPresetStatus() ]);
},
render: function(data) {
this.ensureStyle();
var list = (data[0] != null) ? data[0] : {};
var status = (data[1] != null) ? data[1] : {};
var preset = (data[2] != null) ? data[2] : {};
this.customEntries = list.custom || [];
this.mainEntries = list.main || [];
this.statusData = status;
this.presetData = preset;
this.browsePrefix = '';
this.searchTimer = null;
this.editing = null;
this.fileViewPath = null;
this.fileEditing = null;
this._armTimers = {};
var root = E('div', { 'class': 'cbi-map lsc-root' }, [
E('h2', {}, _('内核参数(Sysctl)管理')),
E('div', { 'class': 'cbi-map-descr' }, _(
'自定义参数保存在 %s,开机时由系统自动加载,也可随时点击“应用配置”立即生效。').format('/etc/sysctl.d/99-luci-sysctl.conf')),
this.renderCustomSection(),
this.renderPresetSection(),
this.renderBrowseSection()
]);
this.refreshCustomTable();
this.refreshSourceChips();
this.loadBrowse('');
return root;
},
/* ---------- shared helpers ---------- */
errorBox: function(msg, hint) {
var children = [ E('p', { 'style': 'margin:2px 0' }, msg) ];
if (hint)
children.push(E('p', { 'style': 'margin:2px 0;color:#666' }, hint));
return E('div', { 'class': 'alert-message error', 'style': 'margin:6px 0' }, children);
},
backendErrorHint: function(msg) {
if (/not found/i.test(msg || ''))
return _('后端缺少该方法,通常是 rpcd 尚未重新加载插件所致。请在 SSH 执行:%s 后刷新页面。').format('/etc/init.d/rpcd restart');
return _('可在 SSH 用 %s 验证后端。').format('ubus call luci.sysctl status');
},
/* Two-step inline confirmation: first click arms the button,
* second click (within 4s) runs the action. */
armButton: function(btn, confirmText, action) {
if (btn.getAttribute('data-armed') == '1') {
btn.setAttribute('data-armed', '0');
btn.textContent = btn.getAttribute('data-orig');
action();
return;
}
btn.setAttribute('data-armed', '1');
btn.setAttribute('data-orig', btn.textContent);
btn.textContent = confirmText;
this._armTimers[btn.getAttribute('data-arm-id')] = window.setTimeout(function() {
if (btn.getAttribute('data-armed') == '1') {
btn.setAttribute('data-armed', '0');
btn.textContent = btn.getAttribute('data-orig');
}
}, 4000);
},
nextArmId: function() {
this._armSeq = (this._armSeq || 0) + 1;
return String(this._armSeq);
},
/* ---------- section 1: custom parameters ---------- */
renderCustomSection: function() {
this.customTableBody = E('tbody', {});
this.tableWrapBox = E('div', { 'class': 'lsc-tablewrap' });
this.customEmptyBox = E('div', {
'style': 'display:none;text-align:center;color:#777;padding:10px 0'
}, _('暂无自定义参数,点击下方“添加参数”开始。'));
this.editFormBox = E('div', { 'style': 'display:none' });
this.fileViewBox = E('div', { 'style': 'display:none' });
this.applyResultBox = E('div', { 'style': 'display:none' });
this.sourceChipsBox = E('div', { 'class': 'lsc-chips' });
var descr = [
E('p', { 'style': 'margin:4px 0' }, [
_('配置源(点击文件名可在下方查看/编辑,按顺序应用,后面的文件覆盖前面的同名参数):'),
this.sourceChipsBox
])
];
var table = E('table', { 'class': 'lsc-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, _('参数名')),
E('th', {}, _('配置值')),
E('th', {}, _('当前值')),
E('th', {}, _('状态')),
E('th', {}, _('操作'))
])),
this.customTableBody
]);
this.tableWrapBox.appendChild(table);
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('自定义参数')),
E('div', { 'class': 'cbi-section-descr' }, descr),
this.editFormBox,
this.tableWrapBox,
this.customEmptyBox,
this.fileViewBox,
this.applyResultBox,
E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'cbi-button cbi-button-add',
'click': ui.createHandlerFn(this, 'showEditForm', null)
}, _('添加参数')),
' ',
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': ui.createHandlerFn(this, 'applyConfig')
}, _('应用配置')),
' ',
E('button', {
'class': 'cbi-button cbi-button-negative',
'click': ui.createHandlerFn(this, 'reloadList')
}, _('刷新'))
])
]);
},
refreshCustomTable: function() {
var rows = [];
var i, e;
/* Main-config entries (/etc/sysctl.conf) are intentionally NOT listed
* here anymore: they are reachable through the source chips above,
* showing them twice was redundant. */
if (this.customEntries.length == 0) {
/* hide the whole (empty) table incl. header, show a one-liner */
this.tableWrapBox.style.display = 'none';
this.customEmptyBox.style.display = '';
dom.content(this.customTableBody, rows);
return;
}
this.tableWrapBox.style.display = '';
this.customEmptyBox.style.display = 'none';
for (i = 0; i < this.customEntries.length; i++) {
e = this.customEntries[i];
var badgeEl;
if (e.disabled)
badgeEl = badge(_('已禁用'), 'off');
else if (!e.exists)
badgeEl = badge(_('无效参数'), 'err');
else if (e.match)
badgeEl = badge(_('已生效'), 'ok');
else
badgeEl = badge(_('未应用'), 'warn');
var delBtn = E('button', { 'class': 'cbi-button cbi-button-remove' }, _('删除'));
rows.push(E('tr', {}, [
E('td', {}, [ E('code', {}, e.key) ]),
E('td', {}, [ E('code', {}, e.value) ]),
E('td', {}, [ E('code', {}, (e.current == null) ? '—' : e.current) ]),
E('td', {}, badgeEl),
E('td', { 'style': 'white-space:nowrap' }, [
E('button', {
'class': 'cbi-button cbi-button-edit',
'click': ui.createHandlerFn(this, 'showEditForm', { isNew: false, entry: e })
}, _('编辑')),
' ',
e.disabled
? E('button', { 'class': 'cbi-button cbi-button-apply', 'click': ui.createHandlerFn(this, 'toggleEntry', e, false) }, _('启用'))
: E('button', { 'class': 'cbi-button cbi-button-remove', 'click': ui.createHandlerFn(this, 'toggleEntry', e, true) }, _('禁用')),
' ',
delBtn
])
]));
(function(view, entry, btn) {
btn.addEventListener('click', function() {
view.armButton(btn, _('确认删除?'), function() { view.removeEntry(entry); });
});
})(this, e, delBtn);
}
dom.content(this.customTableBody, rows);
},
showEditForm: function(entry) {
this.editing = (entry != null) ? entry : null;
this.editFromBrowse = (entry != null && entry.fromBrowse === true);
this.renderEditForm();
if (this.editing != null && this.editFormBox.scrollIntoView)
this.editFormBox.scrollIntoView({ block: 'nearest' });
},
renderEditForm: function() {
var self = this;
var st = this.editing;
if (st == null) {
this.editFormBox.style.display = 'none';
dom.content(this.editFormBox, []);
return;
}
this.editFormBox.style.display = '';
var isNew = (st.isNew === true);
var key = (st.entry != null) ? st.entry.key : (st.key || '');
var keyInput = E('input', {
'type': 'text', 'class': 'cbi-input-text', 'style': 'width:100%',
'placeholder': 'net.ipv4.tcp_fastopen', 'value': key
});
var valInput = E('input', {
'type': 'text', 'class': 'cbi-input-text', 'style': 'width:100%',
'placeholder': '3', 'value': (st.entry != null) ? st.entry.value : (st.value || '')
});
var applyChk = E('input', { 'type': 'checkbox' });
applyChk.checked = true;
var disabledChk = E('input', { 'type': 'checkbox' });
disabledChk.checked = (!isNew && st.entry.disabled == true);
var errBox = this.errorBox('', '');
errBox.style.display = 'none';
var title = isNew
? (st.fromMain ? _('自定义覆盖主配置:%s').format(key) : _('添加内核参数'))
: _('编辑内核参数:%s').format(key);
var note = isNew
? (st.fromMain
? _('将把该参数写入自定义配置以覆盖主配置取值,主配置文件本身保持不变。')
: _('若参数在当前内核中不存在(例如模块未加载),仍可保存,但会标记为无效。'))
: _('修改参数名保存后将替换原条目(先写入新参数名,再删除旧参数名)。');
var saveBtn = E('button', { 'class': 'btn cbi-button-positive important' }, _('保存'));
saveBtn.addEventListener('click', function() { self.saveEditForm(keyInput, valInput, applyChk, disabledChk, errBox); });
dom.content(this.editFormBox, [
E('div', { 'class': 'lsc-card' }, [
E('strong', {}, title),
E('div', { 'class': 'cbi-value', 'style': 'margin-top:8px' }, [
E('label', { 'class': 'cbi-value-title' }, _('参数名')),
E('div', { 'class': 'cbi-value-field' }, keyInput)
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('值')),
E('div', { 'class': 'cbi-value-field' }, valInput)
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, ' '),
E('div', { 'class': 'cbi-value-field' }, [
E('label', { 'style': 'display:inline-flex;align-items:center;gap:6px' }, [ applyChk, _('立即写入内核(运行时生效)') ]),
E('label', { 'style': 'display:inline-flex;align-items:center;gap:6px;margin-left:16px' }, [ disabledChk, _('禁用该条目(写入配置但注释掉)') ])
])
]),
E('div', { 'style': 'color:#666;margin-top:4px' }, note),
errBox,
E('div', { 'style': 'margin-top:8px' }, [
saveBtn, ' ',
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(self, 'hideEditForm') }, _('取消'))
])
])
]);
},
hideEditForm: function() {
this.editing = null;
this.renderEditForm();
/* edit was started from the browse section: scroll back there so the
* user does not get lost at the top of the page */
if (this.editFromBrowse) {
this.editFromBrowse = false;
if (this.browseSectionBox != null && this.browseSectionBox.scrollIntoView)
this.browseSectionBox.scrollIntoView({ block: 'start' });
}
},
saveEditForm: function(keyInput, valInput, applyChk, disabledChk, errBox) {
var self = this;
var st = this.editing;
if (st == null)
return;
var isNew = (st.isNew === true);
var origKey = (st.entry != null) ? st.entry.key : null;
var key = (keyInput.value || '').trim();
var val = (valInput.value || '').trim();
var disabled = disabledChk.checked;
var applyNow = applyChk.checked && !disabled;
errBox.style.display = 'none';
if (!KEY_RE.test(key)) {
errBox.textContent = '';
errBox.appendChild(E('p', {}, _('参数名格式无效,仅允许字母、数字、点、下划线和连字符。')));
errBox.style.display = '';
return;
}
if (val == '') {
errBox.textContent = '';
errBox.appendChild(E('p', {}, _('值不能为空。')));
errBox.style.display = '';
return;
}
var p = callSet(key, val, disabled, applyNow);
if (!isNew && key != origKey) {
p = p.then(function(res) {
if (res == null || res.code != 0)
return res;
return callRemove(origKey).then(function() {
return res;
}).catch(function() {
res.rename_leftover = origKey;
return res;
});
});
}
p.then(function(res) {
if (res == null || res.code != 0) {
errBox.textContent = '';
errBox.appendChild(self.errorBox(_('保存失败:%s').format((res != null && res.error) ? res.error : _('未知错误'))));
errBox.style.display = '';
return;
}
var notes = [];
if (res.rename_leftover)
notes.push(E('p', { 'style': 'color:#a80' }, _('旧参数 %s 删除失败,请在本页手动删除该条目。').format(res.rename_leftover)));
if (!res.exists)
notes.push(E('p', { 'style': 'color:#a80' }, _('参数 %s 在当前内核中不存在(可能模块未加载),配置已保存但不会生效。').format(key)));
else if (applyNow && res.applied === 'readonly')
notes.push(E('p', { 'style': 'color:#a80' }, _('参数 %s 为只读,无法在运行时修改(配置已保存,重启后尝试应用)。').format(key)));
else if (applyNow && res.applied === false)
notes.push(E('p', { 'style': 'color:#a80' }, _('参数 %s 已写入配置,但运行时校验不一致,请检查取值格式。').format(key)));
if (notes.length == 0)
notes.push(E('p', {}, (st.fromMain && isNew) ? _('已创建覆盖条目:%s。') : _('参数 %s 已保存。').format(key)));
self.hideEditForm();
self.reloadList();
dom.content(self.applyResultBox, [ E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, notes) ]);
self.applyResultBox.style.display = '';
}).catch(function(e) {
errBox.textContent = '';
errBox.appendChild(self.errorBox(_('保存失败:%s').format(e.message), self.backendErrorHint(e.message)));
errBox.style.display = '';
});
},
reloadList: function() {
var self = this;
return Promise.all([ callList(), callStatus(), callPresetStatus() ]).then(function(data) {
var list = (data[0] != null) ? data[0] : {};
self.customEntries = list.custom || [];
self.mainEntries = list.main || [];
self.statusData = (data[1] != null) ? data[1] : {};
self.presetData = (data[2] != null) ? data[2] : {};
self.refreshCustomTable();
self.refreshPresetStatus();
self.refreshSourceChips();
}).catch(function(e) {
ui.addNotification(null, E('p', {}, _('加载失败:%s').format(e.message)), 'error');
});
},
refreshSourceChips: function() {
var self = this;
var files = (this.statusData != null && this.statusData.files != null) ? this.statusData.files : [];
if (this.sourceChipsBox == null)
return;
var chips = [];
/* Plugin-managed files (98-online-preset.conf, 99-luci-sysctl.conf)
* are managed through their dedicated sections; opening them via the
* file panel is refused by the backend, so hide their chips. */
var pluginManaged = {
'/etc/sysctl.d/99-luci-sysctl.conf': true,
'/etc/sysctl.d/98-online-preset.conf': true
};
for (var i = 0; i < files.length; i++) {
var f = files[i];
if (pluginManaged[f.path])
continue;
chips.push(E('button', {
'class': 'lsc-chip' + (this.fileViewPath == f.path ? ' lsc-chip-active' : ''),
'title': _('点击查看并可编辑该文件内的参数'),
'click': ui.createHandlerFn(self, 'showFileView', f.path)
}, '%s (%d)'.format(f.path.replace('/etc/', ''), f.count)));
}
dom.content(this.sourceChipsBox, chips);
},
toggleEntry: function(entry, disabled) {
var self = this;
return callSet(entry.key, entry.value, disabled, false).then(function(res) {
if (res == null || res.code != 0) {
ui.addNotification(null,
E('p', {}, _('操作失败:%s').format((res != null && res.error) ? res.error : 'unknown error')),
'error');
}
self.reloadList();
}).catch(function(e) {
ui.addNotification(null, E('p', {}, _('操作失败:%s').format(e.message)), 'error');
});
},
removeEntry: function(entry) {
var self = this;
return callRemove(entry.key).then(function(res) {
if (res == null || res.code != 0) {
ui.addNotification(null,
E('p', {}, _('删除失败:%s').format((res != null && res.error) ? res.error : 'unknown error')),
'error');
}
self.reloadList();
}).catch(function(e) {
ui.addNotification(null, E('p', {}, _('删除失败:%s').format(e.message)), 'error');
});
},
applyConfig: function() {
var self = this;
dom.content(this.applyResultBox, [ E('p', { 'style': 'color:#777;padding:6px' }, _('正在应用全部配置,请稍候…')) ]);
this.applyResultBox.style.display = '';
return callApply().then(function(res) {
var errors = (res != null && res.errors != null) ? res.errors : [];
var content;
if (res == null || res.code != 0) {
content = self.errorBox(_('应用失败:%s').format((res != null && res.error) ? res.error : 'unknown error'));
}
else if (errors.length == 0) {
content = E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, [
E('p', {}, _('全部配置已成功应用。'))
]);
}
else {
var items = [];
for (var i = 0; i < errors.length; i++)
items.push(E('li', {}, [
E('strong', {}, errors[i].file),
E('pre', { 'style': 'white-space:pre-wrap;margin:4px 0' }, errors[i].output || '')
]));
content = E('div', { 'class': 'alert-message warning', 'style': 'margin:6px 0' }, [
E('p', {}, _('应用完成,以下文件存在报错(常见原因:参数不存在、只读或权限不足):')),
E('ul', {}, items)
]);
}
dom.content(self.applyResultBox, [ content ]);
self.reloadList();
}).catch(function(e) {
dom.content(self.applyResultBox, [ self.errorBox(_('应用失败:%s').format(e.message), self.backendErrorHint(e.message)) ]);
});
},
/* ---------- per-file viewing / editing (inline) ---------- */
showFileView: function(path) {
this.fileViewPath = (this.fileViewPath == path) ? null : path;
this.fileEditing = null;
this.refreshSourceChips();
this.renderFilePanel();
},
renderFilePanel: function() {
var self = this;
var path = this.fileViewPath;
var box = this.fileViewBox;
if (path == null) {
box.style.display = 'none';
dom.content(box, []);
return;
}
box.style.display = '';
dom.content(box, [
E('h3', { 'style': 'text-align:center' }, _('文件内容:%s').format(path)),
E('p', { 'style': 'color:#777;text-align:center' }, _('加载中…'))
]);
callFileView(path).then(function(res) {
if (self.fileViewPath != path)
return;
if (res == null || res.code != 0) {
dom.content(box, [
E('h3', { 'style': 'text-align:center' }, _('文件内容:%s').format(path)),
self.errorBox((res != null && res.error) ? res.error : _('未知错误'), self.backendErrorHint(res != null ? res.error : ''))
]);
return;
}
var rows = [];
for (var i = 0; i < (res.entries || []).length; i++) {
var e = res.entries[i];
var editing = (self.fileEditing != null && self.fileEditing.key == e.key);
if (editing) {
rows.push(self.renderFileEditRow(res.path, e));
continue;
}
var ops;
if (res.editable) {
var delBtn = E('button', { 'class': 'cbi-button cbi-button-remove' }, _('删除'));
ops = [
E('button', { 'class': 'cbi-button cbi-button-edit', 'click': ui.createHandlerFn(self, 'editFileEntry', path, e) }, _('编辑')),
' ',
e.disabled
? E('button', { 'class': 'cbi-button cbi-button-apply', 'click': ui.createHandlerFn(self, 'fileToggle', path, e, false) }, _('启用'))
: E('button', { 'class': 'cbi-button cbi-button-remove', 'click': ui.createHandlerFn(self, 'fileToggle', path, e, true) }, _('禁用')),
' ',
delBtn
];
(function(view, p, entry, btn) {
btn.addEventListener('click', function() {
view.armButton(btn, _('确认删除?'), function() { view.fileDelete(p, entry); });
});
})(self, res.path, e, delBtn);
}
else {
ops = [ E('button', {
'class': 'cbi-button cbi-button-add',
'click': ui.createHandlerFn(self, 'showEditForm', { isNew: true, key: e.key, value: e.value, fromMain: true })
}, _('自定义')) ];
}
rows.push(E('tr', {}, [
E('td', {}, [ E('code', {}, (e.disabled ? '# ' : '') + e.key) ]),
E('td', {}, [ E('code', {}, e.value) ]),
E('td', {}, [ E('code', {}, (e.disabled || e.current == null) ? '—' : e.current) ]),
E('td', { 'style': 'white-space:nowrap' }, ops)
]));
}
if (rows.length == 0)
rows.push(E('tr', {}, E('td', { 'colspan': 4, 'style': 'text-align:center;color:#777;padding:12px' },
_('(空文件)'))));
var note = res.editable
? _('此文件中的参数可直接编辑/禁用/删除,保存即写入 %s;点击"应用配置"后对内核生效。').format(res.path)
: _('此文件由插件管理,不支持直接编辑:在线预设请在下方"在线预设"区更新,自定义参数请在上方表格中管理。');
dom.content(box, [
E('h3', { 'style': 'text-align:center' }, _('文件内容:%s').format(res.path)),
E('p', { 'style': 'margin:4px 0;color:#666;text-align:center' }, note),
E('div', { 'class': 'lsc-tablewrap' }, E('table', { 'class': 'lsc-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, _('参数')),
E('th', {}, _('配置值')),
E('th', {}, _('当前值')),
E('th', {}, _('操作'))
])),
E('tbody', rows)
])),
E('div', { 'style': 'margin-top:8px;text-align:center' }, [
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(self, 'hideFileView') }, _('收起'))
])
]);
}).catch(function(e) {
if (self.fileViewPath != path)
return;
dom.content(box, [
E('h3', { 'style': 'text-align:center' }, _('文件内容:%s').format(path)),
self.errorBox(_('读取失败:%s').format(e.message), self.backendErrorHint(e.message))
]);
});
},
renderFileEditRow: function(path, entry) {
var self = this;
var valInput = E('input', {
'type': 'text', 'class': 'cbi-input-text', 'style': 'width:95%', 'value': entry.value
});
var disabledChk = E('input', { 'type': 'checkbox' });
disabledChk.checked = (entry.disabled == true);
var saveBtn = E('button', { 'class': 'cbi-button cbi-button-positive important' }, _('保存'));
saveBtn.addEventListener('click', function() {
var val = (valInput.value || '').trim();
if (val == '') {
alert(_('值不能为空。'));
return;
}
callFileSet(path, entry.key, val, disabledChk.checked).then(function(res) {
if (res == null || res.code != 0) {
alert(_('保存失败:%s').format((res != null && res.error) ? res.error : _('未知错误')));
return;
}
self.fileEditing = null;
self.renderFilePanel();
self.reloadList();
}).catch(function(e) {
alert(_('保存失败:%s').format(e.message));
});
});
return E('tr', { 'style': 'background:#fafafa' }, [
E('td', {}, [ E('code', {}, entry.key) ]),
E('td', { 'colspan': 2 }, [
valInput, ' ',
E('label', { 'style': 'display:inline-flex;align-items:center;gap:4px' }, [ disabledChk, _('禁用') ])
]),
E('td', { 'style': 'white-space:nowrap' }, [
saveBtn, ' ',
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(self, 'cancelFileEdit') }, _('取消'))
])
]);
},
editFileEntry: function(path, entry) {
this.fileEditing = entry;
this.renderFilePanel();
},
cancelFileEdit: function() {
this.fileEditing = null;
this.renderFilePanel();
},
fileToggle: function(path, entry, disabled) {
var self = this;
return callFileSet(path, entry.key, entry.value, disabled).then(function(res) {
if (res == null || res.code != 0) {
ui.addNotification(null,
E('p', {}, _('操作失败:%s').format((res != null && res.error) ? res.error : _('未知错误'))),
'error');
}
self.renderFilePanel();
self.reloadList();
}).catch(function(e) {
ui.addNotification(null, E('p', {}, _('操作失败:%s').format(e.message)), 'error');
});
},
fileDelete: function(path, entry) {
var self = this;
return callFileDelete(path, entry.key).then(function(res) {
if (res == null || res.code != 0) {
ui.addNotification(null,
E('p', {}, _('删除失败:%s').format((res != null && res.error) ? res.error : _('未知错误'))),
'error');
}
self.renderFilePanel();
self.reloadList();
}).catch(function(e) {
ui.addNotification(null, E('p', {}, _('删除失败:%s').format(e.message)), 'error');
});
},
hideFileView: function() {
this.fileViewPath = null;
this.fileEditing = null;
this.refreshSourceChips();
this.renderFilePanel();
},
/* ---------- section 2: online preset ---------- */
renderPresetSection: function() {
var self = this;
this.presetStatusLine = E('div', { 'style': 'margin:6px 0;padding:6px 10px;background:#f5f5f5;border-radius:3px' });
this.presetResultBox = E('div', { 'style': 'display:none' });
this.presetUrlInput = E('input', {
'type': 'text', 'class': 'cbi-input-text', 'style': 'width:100%',
'placeholder': 'https://github.com/user/repo/blob/main/sysctl.conf(也支持 raw 地址)'
});
this.presetPrefixInput = E('input', {
'type': 'text', 'class': 'cbi-input-text', 'style': 'width:260px',
'placeholder': 'https://gh-proxy.org/'
});
var delBtn = E('button', { 'class': 'cbi-button cbi-button-remove' }, _('移除预设'));
delBtn.addEventListener('click', function() {
self.armButton(delBtn, _('确认移除?'), function() { self.presetRemove(); });
});
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('在线预设')),
E('div', { 'class': 'cbi-section-descr' }, [
E('p', { 'style': 'margin:4px 0' }, _(
'从在线源(GitHub 等)获取现成的 sysctl 优化配置,预览确认后一键导入。')),
E('p', { 'style': 'margin:4px 0' }, _(
'预设保存于 %s,由本插件整体管理(更新时全量替换)。如需覆盖个别条目,请在上方"自定义参数"中添加,其加载顺序更靠后、优先级更高。').format('/etc/sysctl.d/98-online-preset.conf'))
]),
this.presetStatusLine,
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('配置源 URL')),
E('div', { 'class': 'cbi-value-field' }, this.presetUrlInput)
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('镜像前缀(可选)')),
E('div', { 'class': 'cbi-value-field' }, [
this.presetPrefixInput,
E('div', { 'style': 'color:#777;margin-top:2px' },
_('直连 GitHub 不可用时填写镜像前缀,最终地址为 前缀 + 原始 URL。'))
])
]),
E('div', { 'class': 'cbi-page-actions' }, [
E('button', { 'class': 'cbi-button cbi-button-apply', 'click': ui.createHandlerFn(this, 'presetFetchPreview') }, _('获取并预览')),
' ',
E('button', { 'class': 'cbi-button cbi-button-find', 'click': ui.createHandlerFn(this, 'presetCheckUpdates') }, _('检查更新')),
' ',
delBtn
]),
this.presetResultBox
]);
},
refreshPresetStatus: function() {
var p = this.presetData || {};
var line;
if (p.present) {
line = [ E('strong', {}, _('已导入') + ''), _('%d 条参数').format(p.count || 0) ];
if (p.source) {
line.push(E('span', { 'style': 'color:#777' }, ' · ' + _('来源') + ' '));
line.push(E('span', { 'style': 'word-break:break-all' }, p.source));
}
if (p.fetched) {
line.push(E('span', { 'style': 'color:#777' }, ' · ' + _('更新于') + ' '));
line.push(this.formatEpoch(p.fetched));
}
}
else {
line = [ E('span', { 'style': 'color:#777' }, _('尚未导入在线预设。')) ];
}
dom.content(this.presetStatusLine, line);
},
formatEpoch: function(s) {
var n = Number(s);
if (s == null || s === '' || isNaN(n) || n <= 0)
return '—';
try {
return new Date(n * 1000).toLocaleString();
} catch (e) {
return String(s);
}
},
presetFetchPreview: function() {
var self = this;
var url = (this.presetUrlInput.value || '').trim();
if (url == '') {
this.showPresetResult(this.errorBox(_('请先填写配置源 URL。')));
return;
}
this.showPresetResult(E('p', { 'style': 'color:#777;padding:6px' }, _('正在获取配置源信息,请稍候…')));
return callPresetList(url, (this.presetPrefixInput.value || '').trim()).then(function(lres) {
if (lres != null && lres.is_dir) {
self.renderPresetDirList(lres);
return;
}
return self.fetchAndPreviewPresetFile(url);
}).catch(function(e) {
self.showPresetResult(self.errorBox(_('获取失败:%s').format(e.message), self.backendErrorHint(e.message)));
});
},
showPresetResult: function(content) {
this.presetResultBox.style.display = '';
dom.content(this.presetResultBox, [ content ]);
},
fetchAndPreviewPresetFile: function(url) {
var self = this;
var prefix = (this.presetPrefixInput.value || '').trim();
this.showPresetResult(E('p', { 'style': 'color:#777;padding:6px' }, _('正在下载并解析预设,请稍候…')));
return callPresetFetch(url, prefix).then(function(res) {
if (res == null || res.code != 0) {
self.showPresetResult(self.errorBox(
(res != null && res.error) ? res.error : _('未知错误'),
self.backendErrorHint(res != null ? res.error : '')));
return;
}
self.renderPresetPreview(res);
}).catch(function(e) {
self.showPresetResult(self.errorBox(_('获取失败:%s').format(e.message), self.backendErrorHint(e.message)));
});
},
renderPresetDirList: function(res) {
var self = this;
var rows = [];
for (var i = 0; i < (res.files || []).length; i++) {
(function(f) {
var btn = E('button', {
'class': 'lsc-chip',
'style': 'font-size:13px;padding:5px 14px'
}, f.name + (f.size > 0 ? ' · ' + (f.size / 1024).toFixed(1) + ' KB' : ''));
btn.addEventListener('click', function() { self.fetchAndPreviewPresetFile(f.url); });
rows.push(E('li', { 'style': 'margin:2px 0' }, btn));
})(res.files[i]);
}
this.presetResultBox.style.display = '';
dom.content(this.presetResultBox, [
E('h3', {}, _('预设目录')),
E('p', { 'style': 'margin:4px 0;color:#666' },
_('该地址是一个目录,发现 %d 个预设文件。点击要导入的文件(下载时同样使用镜像前缀):').format(res.files.length)),
E('ul', { 'style': 'margin:8px 0;padding-left:4px;list-style:none' }, rows),
E('div', { 'style': 'margin-top:10px' }, [
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(this, 'hidePresetResult') }, _('收起'))
])
]);
},
renderPresetPreview: function(res) {
var self = this;
var rows = [];
var notes = [];
var i;
var cmap = {};
for (i = 0; i < (res.conflicts || []).length; i++)
cmap[res.conflicts[i].key] = res.conflicts[i].file;
for (i = 0; i < (res.params || []).length; i++) {
var it = res.params[i];
var conflictFile = cmap[it.key] || '';
rows.push(E('tr', {}, [
E('td', {}, [ E('code', {}, it.key) ]),
E('td', {}, [ E('code', {}, it.value) ]),
E('td', { 'style': 'color:#a80' }, conflictFile ? _('覆盖本地 %s').format(conflictFile.replace('/etc/', '')) : '')
]));
}
notes.push(E('p', {}, _('共解析出 %d 条参数,来源:%s。').format(res.params.length, res.url)));
if ((res.conflicts || []).length > 0)
notes.push(E('p', { 'class': 'alert-message warning', 'style': 'margin:6px 0' },
_('%d 条参数与本地现有配置同名,导入后将覆盖其取值。').format(res.conflicts.length)));
if ((res.invalid || []).length > 0)
notes.push(E('p', { 'class': 'alert-message warning', 'style': 'margin:6px 0' },
_('%d 行无法解析已忽略(常见为说明文字或格式错误的行)。').format(res.invalid.length)));
var importBtn = E('button', { 'class': 'btn cbi-button-positive important' }, _('导入到路由器'));
importBtn.addEventListener('click', function() { self.presetImport(res.url); });
dom.content(this.presetResultBox, [
E('h3', {}, _('预设预览')),
E('div', {}, notes),
E('div', { 'style': 'max-height:320px;overflow:auto' }, E('div', { 'class': 'lsc-tablewrap' }, E('table', { 'class': 'lsc-table' }, [
E('thead', {}, E('tr', {}, [ E('th', {}, _('参数')), E('th', {}, _('值')), E('th', {}, _('冲突')) ])),
E('tbody', rows)
]))),
E('div', { 'style': 'margin-top:10px' }, [
importBtn, ' ',
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(this, 'hidePresetResult') }, _('取消'))
])
]);
},
presetImport: function(url) {
var self = this;
return callPresetImport(url, '').then(function(res) {
if (res == null || res.code != 0) {
dom.content(self.presetResultBox, [ self.errorBox(_('导入失败:%s').format((res != null && res.error) ? res.error : _('未知错误'))) ]);
return;
}
dom.content(self.presetResultBox, [
E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, [
E('p', {}, _('已导入 %d 条预设参数到 %s。点击"应用配置"可立即生效。').format(res.count, res.path))
])
]);
self.reloadList();
}).catch(function(e) {
dom.content(self.presetResultBox, [ self.errorBox(_('导入失败:%s').format(e.message), self.backendErrorHint(e.message)) ]);
});
},
presetCheckUpdates: function() {
var self = this;
dom.content(this.presetResultBox, [ E('p', { 'style': 'color:#777;padding:6px' }, _('正在对比在线版本与本地预设,请稍候…')) ]);
this.presetResultBox.style.display = '';
return callPresetCheck().then(function(res) {
if (res == null || res.code != 0) {
dom.content(self.presetResultBox, [ self.errorBox(
(res != null && res.error) ? res.error : _('未知错误'),
self.backendErrorHint(res != null ? res.error : '')) ]);
return;
}
if ((res.added || []).length == 0 && (res.changed || []).length == 0 && (res.removed || []).length == 0) {
dom.content(self.presetResultBox, [
E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, [
E('p', {}, _('在线预设已是最新,共 %d 条参数。').format(res.remote_count))
])
]);
return;
}
self.renderPresetDiff(res);
}).catch(function(e) {
dom.content(self.presetResultBox, [ self.errorBox(_('检查更新失败:%s').format(e.message), self.backendErrorHint(e.message)) ]);
});
},
renderPresetDiff: function(res) {
var self = this;
var blocks = [];
var i;
var mkTable = function(headers, rows) {
return E('div', { 'class': 'lsc-tablewrap' }, E('table', { 'class': 'lsc-table' }, [
E('thead', {}, E('tr', {}, headers.map(function(h) { return E('th', {}, h); }))),
E('tbody', rows)
]));
};
if ((res.added || []).length > 0) {
var addRows = [];
for (i = 0; i < res.added.length; i++)
addRows.push(E('tr', {}, [
E('td', {}, [ E('code', {}, res.added[i].key) ]),
E('td', {}, [ E('code', {}, res.added[i].value) ])
]));
blocks.push(E('div', {}, [
E('h4', {}, _('新增 %d 条').format(res.added.length)),
mkTable([ _('参数'), _('值') ], addRows)
]));
}
if ((res.changed || []).length > 0) {
var chgRows = [];
for (i = 0; i < res.changed.length; i++)
chgRows.push(E('tr', {}, [
E('td', {}, [ E('code', {}, res.changed[i].key) ]),
E('td', {}, [ E('code', {}, res.changed[i].from) ]),
E('td', {}, [ E('code', {}, res.changed[i].to) ])
]));
blocks.push(E('div', {}, [
E('h4', {}, _('变更 %d 条').format(res.changed.length)),
mkTable([ _('参数'), _('当前值'), _('新值') ], chgRows)
]));
}
if ((res.removed || []).length > 0) {
var rmRows = [];
for (i = 0; i < res.removed.length; i++)
rmRows.push(E('tr', {}, [
E('td', {}, [ E('code', {}, res.removed[i].key) ]),
E('td', {}, [ E('code', {}, res.removed[i].old) ])
]));
blocks.push(E('div', {}, [
E('h4', {}, _('移除 %d 条').format(res.removed.length)),
mkTable([ _('参数'), _('当前值') ], rmRows)
]));
}
var applyBtn = E('button', { 'class': 'btn cbi-button-positive important' }, _('应用更新'));
applyBtn.addEventListener('click', function() { self.presetImport(res.url); });
dom.content(this.presetResultBox, [
E('h3', {}, _('发现在线预设更新')),
E('p', {}, _('在线版本共 %d 条,本地 %d 条。更新会整体替换 %s。').format(res.remote_count, res.local_count, '/etc/sysctl.d/98-online-preset.conf')),
E('div', { 'style': 'max-height:380px;overflow:auto' }, blocks),
E('div', { 'style': 'margin-top:10px' }, [
applyBtn, ' ',
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(this, 'hidePresetResult') }, _('取消'))
])
]);
},
hidePresetResult: function() {
this.presetResultBox.style.display = 'none';
dom.content(this.presetResultBox, []);
},
presetRemove: function() {
var self = this;
return callPresetRemove().then(function(res) {
if (res == null || res.code != 0) {
ui.addNotification(null,
E('p', {}, _('移除失败:%s').format((res != null && res.error) ? res.error : 'unknown error')),
'error');
}
self.reloadList();
}).catch(function(e) {
ui.addNotification(null, E('p', {}, _('移除失败:%s').format(e.message)), 'error');
});
},
/* ---------- section 3: browse / search ---------- */
renderBrowseSection: function() {
var self = this;
this.browseTableBody = E('tbody', {});
this.crumbs = E('div', { 'style': 'margin:6px 0' });
this.searchInput = E('input', {
'type': 'text',
'class': 'cbi-input-text',
'style': 'width:280px;display:inline-block',
'placeholder': _('搜索参数名或值(至少 2 个字符)')
});
this.searchInput.addEventListener('input', function() { self.onSearchInput(); });
var searchBtn = E('button', { 'class': 'cbi-button cbi-button-find', 'click': ui.createHandlerFn(this, 'doSearch') }, _('搜索'));
var table = E('table', { 'class': 'lsc-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, _('参数')),
E('th', {}, _('当前值')),
E('th', {}, _('操作'))
])),
this.browseTableBody
]);
var box = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('浏览内核参数')),
E('div', { 'class': 'cbi-section-descr' },
_('浏览 /proc/sys 下的实时参数。点击目录进入子级,也可直接搜索;点击“自定义”可将参数加入上方自定义列表。')),
E('div', { 'style': 'margin:8px 0' }, [ this.searchInput, ' ', searchBtn ]),
this.crumbs,
E('div', { 'class': 'lsc-tablewrap' }, table)
]);
this.browseSectionBox = box;
return box;
},
onSearchInput: function() {
var self = this;
if (this.searchTimer != null)
window.clearTimeout(this.searchTimer);
this.searchTimer = window.setTimeout(function() { self.doSearch(); }, 500);
},
doSearch: function() {
var self = this;
var q = (this.searchInput.value || '').trim();
if (this.searchTimer != null) {
window.clearTimeout(this.searchTimer);
this.searchTimer = null;
}
if (q.length < 2) {
this.browsePrefix = '';
this.loadBrowse('');
return;
}
this.browsePrefix = null;
this.crumbsUpdate();
dom.content(this.browseTableBody, E('tr', {},
E('td', { 'colspan': 3, 'style': 'text-align:center;color:#777;padding:12px' }, _('搜索中…'))));
callSearch(q, 500).then(function(res) {
var items = (res != null && res.items != null) ? res.items : [];
self.renderSearchResults(items, (res != null && res.truncated == true), q);
}).catch(function(e) {
dom.content(self.browseTableBody, E('tr', {},
E('td', { 'colspan': 3, 'class': 'alert-message error' }, _('搜索失败:%s').format(e.message))));
});
},
renderSearchResults: function(items, truncated, q) {
var rows = [];
items.sort(function(a, b) { return (a.key < b.key) ? -1 : (a.key > b.key) ? 1 : 0; });
for (var i = 0; i < items.length; i++)
rows.push(this.renderParamRow(items[i].key, items[i].value));
if (rows.length == 0)
rows.push(E('tr', {}, E('td', { 'colspan': 3, 'style': 'text-align:center;color:#777;padding:12px' },
_('没有匹配“%s”的参数。').format(q))));
if (truncated)
rows.push(E('tr', {}, E('td', { 'colspan': 3, 'style': 'color:#e80' },
_('结果过多已截断,请输入更精确的关键词。'))));
dom.content(this.browseTableBody, rows);
},
renderParamRow: function(key, value) {
var self = this;
return E('tr', { 'class': 'cbi-section-table-row' }, [
E('td', {}, [ E('code', {}, key) ]),
E('td', {}, [ E('code', {}, value) ]),
E('td', {}, E('button', {
'class': 'cbi-button cbi-button-add',
'click': ui.createHandlerFn(self, 'showEditForm', { isNew: true, key: key, value: value, fromBrowse: true })
}, _('自定义')))
]);
},
loadBrowse: function(prefix) {
var self = this;
dom.content(this.browseTableBody, E('tr', {},
E('td', { 'colspan': 3, 'style': 'text-align:center;color:#777;padding:12px' }, _('加载中…'))));
this.crumbsUpdate();
callBrowse(prefix).then(function(res) {
var items = (res != null && res.items != null) ? res.items : [];
var rows = [];
for (var i = 0; i < items.length; i++) {
var it = items[i];
if (it.type == 'group') {
rows.push(E('tr', { 'class': 'cbi-section-table-row' }, [
E('td', {}, E('a', {
'href': '#',
'click': ui.createHandlerFn(self, 'browseInto', it.prefix)
}, [ E('strong', {}, it.name + '/') ])),
E('td', {}, E('span', { 'style': 'color:#999' }, _('目录'))),
E('td', {}, E('a', {
'href': '#',
'style': 'text-decoration:none',
'click': ui.createHandlerFn(self, 'browseInto', it.prefix)
}, _('进入')))
]));
}
else {
rows.push(self.renderParamRow(it.key, it.value));
}
}
if (rows.length == 0)
rows.push(E('tr', {}, E('td', { 'colspan': 3, 'style': 'text-align:center;color:#777;padding:12px' },
_('(空)'))));
dom.content(self.browseTableBody, rows);
}).catch(function(e) {
dom.content(self.browseTableBody, E('tr', {},
E('td', { 'colspan': 3, 'class': 'alert-message error' }, _('加载失败:%s').format(e.message))));
});
},
browseInto: function(prefix) {
this.browsePrefix = prefix;
this.loadBrowse(prefix);
},
crumbsUpdate: function() {
var self = this;
var parts = [ E('a', {
'href': '#',
'click': ui.createHandlerFn(this, 'browseInto', '')
}, _('根目录')) ];
if (this.browsePrefix == null) {
parts.push(E('span', { 'style': 'color:#777' }, ' ' + _('搜索') + ': ' + this.searchInput.value));
}
else if (this.browsePrefix != '') {
var segs = this.browsePrefix.replace(/\.$/, '').split('.');
var acc = '';
for (var i = 0; i < segs.length; i++) {
acc += segs[i] + '.';
parts.push(E('span', { 'style': 'color:#777' }, ' '));
parts.push(E('a', {
'href': '#',
'click': ui.createHandlerFn(this, 'browseInto', acc)
}, segs[i]));
}
/* one-click step back to the parent directory */
var parentPrefix = (segs.length > 1)
? segs.slice(0, segs.length - 1).join('.') + '.'
: '';
parts.push(E('span', { 'style': 'color:#777' }, ' '));
parts.push(E('a', {
'href': '#',
'style': 'margin-left:12px',
'click': ui.createHandlerFn(this, 'browseInto', parentPrefix)
}, _('← 返回上一级')));
}
dom.content(this.crumbs, parts);
}
});