op-packages/luci-app-wechatpush/htdocs/luci-static/resources/view/wechatpush/client.js
github-actions[bot] 32600acc60 🌴 Sync 2026-03-05 23:51:42
2026-03-05 23:51:42 +08:00

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
});