mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-07-28 01:11:21 +08:00
515 lines
15 KiB
JavaScript
515 lines
15 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require fs';
|
|
'require ui';
|
|
'require poll';
|
|
'require uci';
|
|
|
|
return view.extend({
|
|
load: function () {
|
|
var self = this;
|
|
// 清除 localStorage 中的排序设置
|
|
localStorage.removeItem('sortColumn');
|
|
localStorage.removeItem('sortDirection');
|
|
uci.load('wechatpush');
|
|
return this.fetchAndRenderDevices().then(function () {
|
|
self.setupAutoRefresh();
|
|
});
|
|
},
|
|
|
|
fetchAndRenderDevices: function () {
|
|
var self = this;
|
|
return this.fetchDevices().then(function (data) {
|
|
var container = self.render(data);
|
|
self.switchContent(container);
|
|
}).catch(function (error) {
|
|
console.error('Error fetching or rendering devices:', error);
|
|
});
|
|
},
|
|
|
|
fetchDevices: function () {
|
|
var devices_path = '/tmp/wechatpush/devices.json';
|
|
return fs.read(devices_path).then(function (content) {
|
|
try {
|
|
var data = JSON.parse(content);
|
|
var wlanMap = {};
|
|
|
|
// 如果存在无线接口信息,解析为频段
|
|
if (data.wlan && Array.isArray(data.wlan)) {
|
|
data.wlan.forEach(function (wlan) {
|
|
wlanMap[wlan.interface] = wlan.band;
|
|
});
|
|
}
|
|
|
|
// 解析设备的接口信息
|
|
data.devices.forEach(function (device) {
|
|
if (device.type) {
|
|
device.interface = device.type;
|
|
} else if (wlanMap[device.interface]) {
|
|
device.interface = wlanMap[device.interface];
|
|
} else {
|
|
device.interface = "LAN";
|
|
}
|
|
});
|
|
return { devices: data.devices };
|
|
} catch (e) {
|
|
console.error('Error parsing JSON:', e);
|
|
return { devices: [] };
|
|
}
|
|
});
|
|
},
|
|
|
|
render: function (data) {
|
|
if (!data || !data.devices || !Array.isArray(data.devices)) {
|
|
return document.createElement('div');
|
|
}
|
|
var devices = data.devices.filter(device => device.status === 'online' || device.status === 'unknown');
|
|
var totalDevices = devices.length;
|
|
var headers = [_('Hostname'), _('IPv4 address'), _('MAC address'), _('Interfaces'), _('Connection Point'), _('Online time'), _('Details')];
|
|
var columns = ['name', 'ip', 'mac', 'interface', 'parent', 'uptime', 'usage'];
|
|
var visibleColumns = [];
|
|
var hasData = false;
|
|
|
|
// 获取配置中的默认排序列
|
|
var defaultSortColumn = uci.get('wechatpush', 'config', 'defaultSortColumn') || 'ip';
|
|
var defaultSortDirection = (defaultSortColumn === 'uptime') ? 'desc' : 'asc';
|
|
|
|
// 获取存储的排序设置,如果没有则使用默认设置
|
|
var storedSortColumn = localStorage.getItem('sortColumn');
|
|
var storedSortDirection = localStorage.getItem('sortDirection');
|
|
|
|
var currentSortColumn = storedSortColumn || defaultSortColumn;
|
|
var currentSortDirection = storedSortDirection || defaultSortDirection;
|
|
|
|
devices.sort(function (a, b) {
|
|
return compareDevices(a, b, currentSortColumn, currentSortDirection);
|
|
});
|
|
|
|
// 根据数据源决定可见列
|
|
for (var i = 0; i < columns.length; i++) {
|
|
var column = columns[i];
|
|
var hasColumnData = false;
|
|
|
|
// 特殊处理 parent 列
|
|
if (column === 'parent') {
|
|
var hasNonLocalParent = false;
|
|
for (var j = 0; j < devices.length; j++) {
|
|
var parentValue = devices[j][column];
|
|
if (parentValue && parentValue !== "Local") {
|
|
hasNonLocalParent = true;
|
|
break;
|
|
}
|
|
}
|
|
// 如果存在非 "Local" 的 parent 值,则显示该列
|
|
if (hasNonLocalParent) {
|
|
visibleColumns.push(i);
|
|
}
|
|
} else {
|
|
// 其他列的正常逻辑
|
|
for (var j = 0; j < devices.length; j++) {
|
|
if (devices[j][column] !== undefined && devices[j][column] !== '') {
|
|
hasColumnData = true;
|
|
hasData = true;
|
|
break;
|
|
}
|
|
}
|
|
if (hasColumnData) {
|
|
visibleColumns.push(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
var style = `
|
|
/* 设备表格样式 */
|
|
.device-table {
|
|
width: 80%; /* 表格宽度占满父容器 */
|
|
border-collapse: collapse; /* 合并边框 */
|
|
margin-top: 10px; /* 顶部外边距 */
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* 阴影效果 */
|
|
border-radius: 8px; /* 圆角边框 */
|
|
overflow: hidden; /* 内容溢出隐藏 */
|
|
}
|
|
|
|
.device-table th,
|
|
.device-table td {
|
|
padding: 10px; /* 单元格内边距 */
|
|
text-align: center; /* 文本居中 */
|
|
border: 1px solid #ddd; /* 边框样式 */
|
|
}
|
|
|
|
.device-table th {
|
|
background-color: rgba(0, 0, 0, 0.05); /* 表头背景色,透明 */
|
|
font-weight: bold; /* 粗体字体 */
|
|
cursor: pointer; /* 鼠标指针样式为指针 */
|
|
position: relative; /* 相对定位 */
|
|
}
|
|
|
|
.device-table th.sortable::after {
|
|
right: 10px; /* 右侧距离 */
|
|
top: 50%; /* 顶部偏移50% */
|
|
transform: translateY(-50%); /* 垂直居中 */
|
|
border-width: 5px 5px 0; /* 边框宽度 */
|
|
border-style: solid; /* 边框样式为实线 */
|
|
opacity: 0.6; /* 透明度 */
|
|
}
|
|
|
|
.device-table th.asc::after {
|
|
content: '';
|
|
position: absolute;
|
|
right: 10px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
border-width: 6px;
|
|
border-style: solid;
|
|
border-color: #666 transparent transparent transparent; /* 向上箭头颜色 */
|
|
}
|
|
|
|
.device-table th.desc::after {
|
|
content: '';
|
|
position: absolute;
|
|
right: 10px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
border-width: 6px;
|
|
border-style: solid;
|
|
border-color: transparent transparent #666 transparent; /* 向下箭头颜色 */
|
|
}
|
|
|
|
.device-table tbody tr:nth-child(even) {
|
|
background-color: rgba(0, 0, 0, 0.05); /* 偶数行背景色,透明 */
|
|
}
|
|
|
|
.device-table td:first-child {
|
|
text-align: left; /* 第一列文本左对齐 */
|
|
padding-left: 20px; /* 第一列左侧内边距 */
|
|
}
|
|
|
|
.device-table td a {
|
|
color: #007bff; /* 链接颜色 */
|
|
text-decoration: none; /* 去掉下划线 */
|
|
}
|
|
|
|
.device-table td a:hover {
|
|
text-decoration: underline; /* 鼠标悬停下划线 */
|
|
}
|
|
|
|
.device-table .hide {
|
|
display: none; /* 隐藏元素 */
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.device-table {
|
|
width: 100%; /* 表格宽度占满父容器 */
|
|
overflow: hidden; /* 内容溢出隐藏 */
|
|
}
|
|
.device-table th,
|
|
.device-table td {
|
|
padding: 3px; /* 单元格内边距 */
|
|
text-align: center; /* 文本居中 */
|
|
border: 0.35px solid #ddd; /* 边框样式 */
|
|
}
|
|
.device-table td:first-child {
|
|
max-width: 80px;
|
|
}
|
|
.device-table td:first-child {
|
|
text-align: left; /* 第一列文本左对齐 */
|
|
padding-left: 2px; /* 第一列左侧内边距 */
|
|
overflow: hidden; /* 隐藏溢出内容 */
|
|
text-overflow: ellipsis; /* 显示省略号 */
|
|
}
|
|
/* 隐藏特定列 */
|
|
.device-table th[data-column="parent"],
|
|
.device-table td[data-column="parent"] {
|
|
display: none;
|
|
}
|
|
/* 隐藏接口列的文本部分 */
|
|
.device-table td[data-column="interface"] span:not(.iface-icon) {
|
|
display: none;
|
|
}
|
|
/* 调整图标样式 */
|
|
.device-table td[data-column="interface"] .iface-icon {
|
|
margin-right: 0; /* 去掉图标右侧的间距 */
|
|
}
|
|
}
|
|
`;
|
|
|
|
function createTable() {
|
|
var table = document.createElement('table');
|
|
table.classList.add('device-table');
|
|
|
|
var thead = document.createElement('thead');
|
|
var tr = document.createElement('tr');
|
|
|
|
for (var i = 0; i < headers.length; i++) {
|
|
var th = document.createElement('th');
|
|
th.textContent = headers[i];
|
|
|
|
if (visibleColumns.includes(i)) {
|
|
th.classList.add('sortable');
|
|
th.dataset.column = columns[i];
|
|
if (columns[i] === currentSortColumn) {
|
|
th.classList.add(currentSortDirection === 'asc' ? 'asc' : 'desc');
|
|
}
|
|
} else {
|
|
th.classList.add('hide');
|
|
}
|
|
|
|
tr.appendChild(th);
|
|
}
|
|
|
|
thead.appendChild(tr);
|
|
table.appendChild(thead);
|
|
|
|
var tbody = document.createElement('tbody');
|
|
devices.forEach(function (device) {
|
|
var row = document.createElement('tr');
|
|
for (var i = 0; i < columns.length; i++) {
|
|
if (visibleColumns.includes(i)) {
|
|
var cell = document.createElement('td');
|
|
cell.dataset.column = columns[i];
|
|
if (columns[i] === 'uptime') {
|
|
cell.textContent = calculateUptime(device['uptime'], window.innerWidth <= 767);
|
|
} else if (columns[i] === 'ip' && device['http_access']) {
|
|
var link = document.createElement('a');
|
|
link.href = `${device['http_access']}://${device['ip']}`;
|
|
link.textContent = device['ip'];
|
|
link.target = '_blank';
|
|
cell.appendChild(link);
|
|
} else if (columns[i] === 'interface') {
|
|
var icon = document.createElement('span');
|
|
icon.classList.add('iface-icon');
|
|
if (device['interface'] === '2.4G') {
|
|
icon.innerHTML = '📶';
|
|
} else if (device['interface'] === '5G') {
|
|
icon.innerHTML = '🛜';
|
|
} else if (device['interface'] === 'WiFi') {
|
|
icon.innerHTML = '🛜';
|
|
}
|
|
|
|
var text = document.createElement('span');
|
|
text.textContent = device['interface'];
|
|
|
|
cell.appendChild(icon);
|
|
cell.appendChild(text);
|
|
} else if (columns[i] === 'parent') {
|
|
if (device['parent']) {
|
|
var parentDevice = devices.find(d => {
|
|
// 统一转换为大写比较
|
|
var deviceMac = (d.mac || '').toUpperCase();
|
|
var parentMac = (device['parent'] || '').toUpperCase();
|
|
return deviceMac === parentMac || d.ip === device['parent'];
|
|
});
|
|
if (parentDevice) {
|
|
cell.textContent = parentDevice.name || parentDevice.ip;
|
|
} else {
|
|
cell.textContent = device['parent'];
|
|
}
|
|
} else {
|
|
cell.textContent = '';
|
|
}
|
|
} else {
|
|
cell.textContent = device[columns[i]];
|
|
}
|
|
row.appendChild(cell);
|
|
}
|
|
}
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
table.appendChild(tbody);
|
|
|
|
return table;
|
|
}
|
|
|
|
function calculateUptime(uptime, simpleFormat = false) {
|
|
var startTimeStamp = parseInt(uptime);
|
|
var currentTimeStamp = Math.floor(Date.now() / 1000);
|
|
var uptimeInSeconds = currentTimeStamp - startTimeStamp;
|
|
|
|
var days = Math.floor(uptimeInSeconds / (3600 * 24));
|
|
var hours = Math.floor((uptimeInSeconds % (3600 * 24)) / 3600);
|
|
var minutes = Math.floor((uptimeInSeconds % 3600) / 60);
|
|
var seconds = uptimeInSeconds % 60;
|
|
|
|
if (simpleFormat) {
|
|
return days > 0 ? `${days}d ${hours}h` :
|
|
hours > 0 ? `${hours}h ${minutes}m` :
|
|
minutes > 0 ? `${minutes}m ${seconds}s` :
|
|
`${seconds}s`;
|
|
} else {
|
|
if (days > 0) {
|
|
return _('%dd %dh').replace(/%d/g, match =>
|
|
match === '%d' ? days : hours);
|
|
} else if (hours > 0) {
|
|
return _('%dh %dm').replace(/%d/g, match =>
|
|
match === '%d' ? hours : minutes);
|
|
} else if (minutes > 0) {
|
|
return _('%dm %ds').replace(/%d/g, match =>
|
|
match === '%d' ? minutes : seconds);
|
|
} else {
|
|
return _('%ds').replace('%d', seconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
function compareDevices(a, b, column, direction) {
|
|
var value1 = getValueForSorting(a, column);
|
|
var value2 = getValueForSorting(b, column);
|
|
|
|
// 处理 name/mac 列的 "unknown" 优先级
|
|
if (column === 'name' || column === 'mac') {
|
|
const isUnknown1 = (value1 === "unknown");
|
|
const isUnknown2 = (value2 === "unknown");
|
|
|
|
if (isUnknown1 !== isUnknown2) {
|
|
return direction === 'asc'
|
|
// 升序时 unknown 排最后(视为最大值),降序时排最前
|
|
//? (isUnknown1 ? 1 : -1)
|
|
//: (isUnknown1 ? -1 : 1);
|
|
// 升序时 unknown 排最前(视为最小值)
|
|
? (isUnknown1 ? -1 : 1)
|
|
: (isUnknown1 ? 1 : -1);
|
|
}
|
|
}
|
|
|
|
// 处理 parent 列的优先级
|
|
if (column === 'parent') {
|
|
const aHasValue = a.parent ? 1 : 0;
|
|
const bHasValue = b.parent ? 1 : 0;
|
|
if (aHasValue !== bHasValue) {
|
|
return direction === 'desc'
|
|
? (bHasValue - aHasValue)
|
|
: (aHasValue - bHasValue);
|
|
}
|
|
value1 = a.parent || '';
|
|
value2 = b.parent || '';
|
|
}
|
|
|
|
// 通用比较逻辑
|
|
if (value1 < value2) {
|
|
return direction === 'asc' ? -1 : 1;
|
|
} else if (value1 > value2) {
|
|
return direction === 'asc' ? 1 : -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
var interfaceDisplayMap = {
|
|
'2.4G': '2.4G',
|
|
'5G': '5G',
|
|
'WiFi': 'WiFi'
|
|
};
|
|
|
|
// 排序
|
|
function getValueForSorting(device, column) {
|
|
if (column === 'uptime') {
|
|
return parseInt(device['uptime']);
|
|
} else if (column === 'ip') {
|
|
return ipToNumber(device['ip']);
|
|
} else if (column === 'interface') {
|
|
return interfaceDisplayMap[device['interface']] || 'LAN';
|
|
} else if (column === 'parent') {
|
|
// 使用 parent 列的实际显示值进行排序
|
|
if (device['parent']) {
|
|
var parentDevice = devices.find(d => {
|
|
var deviceMac = (d.mac || '').toUpperCase();
|
|
var parentMac = (device['parent'] || '').toUpperCase();
|
|
return deviceMac === parentMac || d.ip === device['parent'];
|
|
});
|
|
if (parentDevice) {
|
|
return parentDevice.name || parentDevice.ip;
|
|
} else {
|
|
return device['parent'];
|
|
}
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
return device[column];
|
|
}
|
|
|
|
function ipToNumber(ipAddress) {
|
|
var parts = ipAddress.split('.');
|
|
var number = 0;
|
|
|
|
for (var i = 0; i < parts.length; i++) {
|
|
number = number * 256 + parseInt(parts[i]);
|
|
}
|
|
|
|
return number;
|
|
}
|
|
|
|
var container = document.createElement('div');
|
|
refreshContainer();
|
|
|
|
container.addEventListener('click', function (event) {
|
|
if (event.target.tagName === 'TH' && event.target.parentNode.rowIndex === 0) {
|
|
var columnIndex = event.target.cellIndex;
|
|
var column = columns[columnIndex];
|
|
var direction = 'asc';
|
|
|
|
// 使在线时间第一次点击方向为倒序
|
|
if (column === 'uptime' || column === 'parent') {
|
|
if (currentSortColumn !== column) {
|
|
// 首次点击该列,默认方向为 desc
|
|
direction = 'desc';
|
|
} else {
|
|
// 切换方向
|
|
direction = currentSortDirection === 'desc' ? 'asc' : 'desc';
|
|
}
|
|
} else if (column === currentSortColumn) {
|
|
direction = currentSortDirection === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
direction = 'asc';
|
|
}
|
|
|
|
sortTable(column, direction);
|
|
}
|
|
});
|
|
|
|
function refreshContainer() {
|
|
container.innerHTML = '';
|
|
container.appendChild(document.createElement('h2')).textContent = _('Currently %s devices online').replace('%s', totalDevices);
|
|
container.appendChild(createTable());
|
|
container.appendChild(document.createElement('style')).textContent = style;
|
|
}
|
|
|
|
function sortTable(column, direction) {
|
|
devices.sort(function (a, b) {
|
|
return compareDevices(a, b, column, direction);
|
|
});
|
|
|
|
currentSortColumn = column;
|
|
currentSortDirection = direction;
|
|
|
|
// 存储排序设置
|
|
localStorage.setItem('sortColumn', currentSortColumn);
|
|
localStorage.setItem('sortDirection', currentSortDirection);
|
|
|
|
refreshContainer();
|
|
}
|
|
|
|
return container;
|
|
},
|
|
|
|
setupAutoRefresh: function () {
|
|
var self = this;
|
|
poll.add(L.bind(function () {
|
|
self.fetchAndRenderDevices();
|
|
}));
|
|
},
|
|
|
|
switchContent: function (newContent) {
|
|
var existingContainer = document.querySelector('#view');
|
|
if (!existingContainer) {
|
|
console.error('Table container not found.');
|
|
return;
|
|
}
|
|
existingContainer.innerHTML = '';
|
|
existingContainer.appendChild(newContent);
|
|
},
|
|
|
|
handleSave: null,
|
|
handleSaveApply: null,
|
|
handleReset: null
|
|
}); |