small-packages/luci-app-oaf/luasrc/view/admin_network/user_status.htm
2026-05-10 09:48:30 +08:00

1538 lines
48 KiB
HTML

<link rel="stylesheet" href="<%=resource%>/css/common.css">
<% local dsp=require "luci.dispatcher" -%>
<script type="text/javascript" src="<%=resource%>/echarts.min.js?v=5.0"></script>
<script type="text/javascript">//<![CDATA[
let visit_time_data = [];
let user_list_data = {
list: [
]
}
let cur_page = 1; // Start from page 1
let page_size = 10; // Fixed page size
let total_pages = 1;
let total_count = 0;
// Visit list pagination variables
let visit_cur_page = 1;
let visit_page_size = 10;
let visit_total_pages = 1;
let visit_total_count = 0;
let current_mac = '';
function getAllUsersData(page, size) {
if (page === undefined) page = cur_page;
if (size === undefined) size = page_size;
new XHR().get('<%=url('admin/network/get_all_users')%>', {flag: 3, page: page, page_size: size},
function (x, data) {
user_list_data = data.data;
if (data.data.total !== undefined) {
total_count = data.data.total;
total_pages = data.data.total_pages || 1;
cur_page = data.data.page || 1;
// page_size is fixed at 15, don't update from response
}
render_user_list(user_list_data);
updatePagination();
}
);
}
function updatePagination() {
var paginationDiv = document.getElementById('pagination');
if (!paginationDiv || total_pages <= 0) {
if (paginationDiv) paginationDiv.innerHTML = '';
return;
}
// Calculate page numbers to display (max 10 pages)
// Try to show 5 pages on left and 5 pages on right of current page
var startPage = Math.max(1, cur_page - 5);
var endPage = Math.min(total_pages, cur_page + 5);
// Adjust if we don't have 10 pages
var pageCount = endPage - startPage + 1;
if (pageCount < 10) {
if (startPage == 1) {
// If at the beginning, extend to the right
endPage = Math.min(total_pages, startPage + 9);
} else if (endPage == total_pages) {
// If at the end, extend to the left
startPage = Math.max(1, endPage - 9);
} else {
// In the middle, try to balance
var needed = 10 - pageCount;
var canExtendLeft = startPage - 1;
var canExtendRight = total_pages - endPage;
if (canExtendLeft >= canExtendRight) {
startPage = Math.max(1, startPage - Math.min(needed, canExtendLeft));
endPage = Math.min(total_pages, startPage + 9);
} else {
endPage = Math.min(total_pages, endPage + Math.min(needed, canExtendRight));
startPage = Math.max(1, endPage - 9);
}
}
}
var html = '<div style="display: flex; align-items: center; justify-content: center; margin-top: 20px; padding: 15px 0;">';
html += '<div style="display: flex; align-items: center; gap: 4px;">';
// Previous page link
if (cur_page > 1) {
html += '<a href="javascript:void(0)" onclick="goToPage(' + (cur_page - 1) + ')" style="padding: 8px 12px; color: #1a73e8; text-decoration: none; cursor: pointer; border-radius: 4px;"><%:Previous%></a>';
} else {
html += '<span style="padding: 8px 12px; opacity: 0.4; cursor: default;"><%:Previous%></span>';
}
// Page numbers
for (var i = startPage; i <= endPage; i++) {
if (i == cur_page) {
html += '<span style="padding: 8px 12px; color: #1a73e8; font-weight: 500; cursor: default; border-radius: 4px;">' + i + '</span>';
} else {
html += '<a href="javascript:void(0)" onclick="goToPage(' + i + ')" style="padding: 8px 12px; text-decoration: none; cursor: pointer; border-radius: 4px;">' + i + '</a>';
}
}
// Next page link
if (cur_page < total_pages) {
html += '<a href="javascript:void(0)" onclick="goToPage(' + (cur_page + 1) + ')" style="padding: 8px 12px; color: #1a73e8; text-decoration: none; cursor: pointer; border-radius: 4px;"><%:Next%></a>';
} else {
html += '<span style="padding: 8px 12px; opacity: 0.4; cursor: default;"><%:Next%></span>';
}
html += '</div>';
html += '<div style="margin-left: 20px; opacity: 0.6; font-size: 14px;">';
html += '<%:Total%>: ' + total_count + ' | <%:Page%>: ' + cur_page + ' / ' + total_pages;
html += '</div>';
html += '</div>';
paginationDiv.innerHTML = html;
}
function goToPage(page) {
if (page < 1) page = 1;
if (page > total_pages && total_pages > 0) page = total_pages;
cur_page = page;
getAllUsersData(cur_page, page_size);
}
let currentView = 'table'; // 'table' or 'card'
function switchView(view) {
currentView = view;
var tableWrapper = document.getElementById('tableViewWrapper');
var cardWrapper = document.getElementById('cardViewWrapper');
var btnTable = document.getElementById('btnTableView');
var btnCard = document.getElementById('btnCardView');
if (view === 'card') {
tableWrapper.style.display = 'none';
cardWrapper.style.display = 'grid';
btnTable.classList.remove('active');
btnCard.classList.add('active');
} else {
tableWrapper.style.display = 'block';
cardWrapper.style.display = 'none';
btnTable.classList.add('active');
btnCard.classList.remove('active');
}
if (user_list_data && user_list_data.list) {
if (view === 'card') render_card_view(user_list_data);
}
}
function render_card_view(data) {
var container = document.getElementById('cardViewWrapper');
if (!container || !data || !data.list) return;
var user_list = data.list;
var html = '';
for (var i = 0; i < user_list.length; i++) {
var u = user_list[i];
var nickname = u.nickname || "";
var hostname = u.hostname || "";
var displayName = nickname || hostname || "--";
var mac = u.mac || "--";
// 在线状态
var statusClass = 'offline';
var statusText = '<%:Offline%>';
if (u.online == 2) { statusClass = 'active'; statusText = '<%:Active%>'; }
else if (u.online == 1) { statusClass = 'online'; statusText = '<%:Online%>'; }
var upRate = formatRate(u.up_rate, u.online);
var downRate = formatRate(u.down_rate, u.online);
var am_time = u.today_am_active_time || 0;
var pm_time = u.today_pm_active_time || 0;
var total_time = am_time + pm_time;
var time_str = formatTime(total_time);
var tooltip_text = "<%:Morning%>: " + formatTime(am_time) + ", <%:Afternoon%>: " + formatTime(pm_time);
// 今日流量
var today_up_flow = u.today_up_flow || 0;
var today_down_flow = u.today_down_flow || 0;
var total_flow = today_up_flow + today_down_flow;
var flow_str = formatFlow(total_flow);
if (!Array.isArray(u.applist)) u.applist = [];
var app_icons = u.applist.slice(0, 5).map(function(app) {
var iconSrc = app.icon === 0 ? '<%=resource%>/app_icons/default.png' : '<%=resource%>/app_icons/' + app.id + '.png';
return '<img src="' + iconSrc + '" alt="' + app.name + '" title="' + app.name + '" class="card-app-icon">';
}).join('');
if (!app_icons) app_icons = '<span style="opacity:0.4;font-size:12px;">--</span>';
html += '<div class="user-card ' + (u.online == 0 ? 'user-card--offline' : '') + '" onclick="showDetails(\'' + mac + '\')">';
html += '<span class="card-status card-status--' + statusClass + '">' + statusText + '</span>';
html += '<div class="card-device">';
html += '<div class="card-name">' + displayName + '</div>';
html += '<div class="card-mac">' + mac + '</div>';
html += '</div>';
html += '<div class="card-rate">';
html += '<span class="card-rate-item" title="<%:Up Rate%>"><svg width="12" height="12" viewBox="0 0 12 12" fill="#34a853"><path d="M6 1L10 6H7v5H5V6H2z"/></svg> ' + upRate + '</span>';
html += '<span class="card-rate-item" title="<%:Down Rate%>"><svg width="12" height="12" viewBox="0 0 12 12" fill="#4285f4"><path d="M6 11L2 6h3V1h2v5h3z"/></svg> ' + downRate + '</span>';
html += '</div>';
var current_app = u.app || "";
var current_url = u.url || "";
var display_url = current_url;
if (current_url.length > 18) {
display_url = current_url.substring(0, 8) + '...' + current_url.substring(current_url.length - 8);
}
html += '<div class="card-visiting">';
html += '<span class="card-visiting-label"><%:Visiting%>: </span>';
if (current_app || current_url) {
if (current_app) html += '<span class="card-visiting-app">' + current_app + '</span>';
if (current_url) html += '<span class="card-visiting-url" title="' + current_url + '">' + display_url + '</span>';
} else {
html += '<span class="card-visiting-none">--</span>';
}
html += '</div>';
html += '<div class="card-info">';
html += '<div class="card-apps">' + app_icons + '</div>';
html += '<div style="display: flex; gap: 12px; align-items: center;">';
html += '<div class="card-time" title="' + tooltip_text + '">' + time_str + '</div>';
html += '<div class="card-flow" title="<%:Today Flow%>">' + flow_str + '</div>';
html += '</div>';
html += '</div>';
html += '</div>';
}
container.innerHTML = html;
}
window.onload = function() {
cur_page = 1;
getAllUsersData(cur_page, page_size);
if (window.innerWidth <= 768) {
switchView('card');
}
setInterval(function() {
getAllUsersData(cur_page, page_size);
}, 3000);
}
function showSuccessMessage(message = '<%:Settings saved successfully%>') {
const modal = document.getElementById('modal');
const messageElement = modal.querySelector('p');
messageElement.textContent = message;
modal.style.display = 'flex';
setTimeout(() => {
modal.style.display = 'none';
}, 1000);
}
function formatTime(minutes) {
if (minutes === undefined || minutes === null || minutes === '') {
return '0m';
}
var mins = parseInt(minutes);
if (isNaN(mins) || mins < 0) {
return '0m';
}
if (mins < 60) {
return mins + 'm';
}
var hours = Math.floor(mins / 60);
var remainingMins = mins % 60;
if (remainingMins === 0) {
return hours + 'h';
} else {
return hours + 'h ' + remainingMins + 'm';
}
}
function formatRate(kbps, isOnline) {
if (isOnline !== undefined && isOnline == 0) {
return '--';
}
var rate = 0;
if (kbps !== undefined && kbps !== null && kbps !== '') {
rate = parseFloat(kbps);
if (isNaN(rate) || rate < 0) {
rate = 0;
}
}
rate = Math.round(rate);
if (rate >= 1024) {
return (rate / 1024).toFixed(1) + ' MB/s';
} else {
return rate + ' KB/s';
}
}
function formatFlow(kb) {
if (kb === undefined || kb === null || kb === '') {
return '0 KB';
}
// Use parseFloat to handle large numbers correctly
var totalKB = parseFloat(kb);
if (isNaN(totalKB) || totalKB < 0) {
return '0 KB';
}
// Round to integer for KB
totalKB = Math.round(totalKB);
if (totalKB === 0) {
return '0 KB';
}
// Convert to appropriate unit (input is in KB)
if (totalKB < 1024) {
// KB - no decimal
return totalKB + ' KB';
} else if (totalKB < 1024 * 1024) {
// MB - one decimal
return (totalKB / 1024).toFixed(1) + ' MB';
} else if (totalKB < 1024 * 1024 * 1024) {
// GB - one decimal
return (totalKB / (1024 * 1024)).toFixed(1) + ' GB';
} else {
// TB - one decimal
return (totalKB / (1024 * 1024 * 1024)).toFixed(1) + ' TB';
}
}
function render_user_list(data) {
var tb = document.getElementById('user_status_table');
var tbody = tb ? tb.querySelector('tbody') : null;
var user_list = data.list;
if (user_list && tbody) {
tbody.innerHTML = '';
for (var i = 0; i < user_list.length; i++) {
var nickname = user_list[i].nickname || "";
var hostname = user_list[i].hostname || "";
var displayName = nickname || hostname || "--";
var tr = tbody.insertRow(-1);
tr.className = 'tr';
if (user_list[i].online == 0) {
tr.style.color = '#A9A9A9';
}
tr.insertCell(-1).innerHTML = `
<div style="display: flex; align-items: center;">
<div>
<div>${displayName}</div>
<div>${user_list[i].mac}</div>
</div>
</div>
`;
tr.insertCell(-1).innerHTML = user_list[i].ip;
tr.insertCell(-1).innerHTML = formatRate(user_list[i].up_rate, user_list[i].online);
tr.insertCell(-1).innerHTML = formatRate(user_list[i].down_rate, user_list[i].online);
var am_time = user_list[i].today_am_active_time || 0;
var pm_time = user_list[i].today_pm_active_time || 0;
var total_time = am_time + pm_time;
var time_str = formatTime(total_time);
var tooltip_text = "<%:Morning%>: " + formatTime(am_time) + ", <%:Afternoon%>: " + formatTime(pm_time);
tr.insertCell(-1).innerHTML = `<span title="${tooltip_text}" style="cursor: help;">${time_str}</span>`;
// 今日流量
var today_up_flow = user_list[i].today_up_flow || 0;
var today_down_flow = user_list[i].today_down_flow || 0;
var total_flow = today_up_flow + today_down_flow;
var flow_str = formatFlow(total_flow);
tr.insertCell(-1).innerHTML = flow_str;
if (!Array.isArray(user_list[i].applist))
user_list[i].applist = []
var app_list_str = user_list[i].applist.map(app => {
var iconSrc = app.icon === 0 ? '<%=resource%>/app_icons/default.png' : `<%=resource%>/app_icons/${app.id}.png`;
return `<img src="${iconSrc}" alt="${app.name}" title="${app.name}" style="width: 20px; height: 20px; border-radius: 5px; margin-right: 8px;">`;
}).join("") || "--";
tr.insertCell(-1).innerHTML = app_list_str;
var current_app = user_list[i].app || "--";
tr.insertCell(-1).innerHTML = current_app;
var current_url = user_list[i].url || "--";
var display_url = current_url;
if (current_url !== "--" && current_url.length > 18) {
display_url = current_url.substring(0, 8) + "..." + current_url.substring(current_url.length - 8);
}
tr.insertCell(-1).innerHTML = `<span title="${current_url}">${display_url}</span>`;
var online_status_html = "";
if (user_list[i].online == 2) {
online_status_html = `<span style="color: #1a73e8;"><%:Active%></span>`;
} else if (user_list[i].online == 1) {
online_status_html = `<span style="color: green;"><%:Online%></span>`;
} else {
online_status_html = "<%:Offline%>";
}
tr.insertCell(-1).innerHTML = online_status_html;
tr.insertCell(-1).innerHTML = `
<button type="button" class="cbi-button cbi-button-add" onclick="showDetails('${user_list[i].mac}')" style="margin-right: 5px;"><%:Details%></button>
<button type="button" class="cbi-button cbi-button-add" onclick="showModifyNickname('${user_list[i].mac}')"><%:Remark%></button>
`;
var childs = tr.childNodes;
Array.prototype.forEach.call(childs, function (child) {
child.className = 'td';
});
}
}
if (currentView === 'card') {
render_card_view(data);
}
}
function render_visit_list_table(data) {
var tb = document.getElementById('visit_list_table');
var tbody = tb ? tb.querySelector('tbody') : null;
var visit_list = data.list;
if (visit_list && tbody) {
tbody.innerHTML = '';
for (var i = 0; i < visit_list.length; i++) {
var action_status = visit_list[i].act == 1 ?
'<span style="color: red;"><%:Filtered%></span>' :
'<span style="color: green;"><%:Unfiltered%></span>';
var hostname = visit_list[i].hostname == "" || visit_list[i].hostname == "*" ? "--" : visit_list[i].hostname;
var tr = tbody.insertRow(-1);
tr.className = 'tr';
var iconSrc = visit_list[i].icon === 0 ? '<%=resource%>/app_icons/default.png' : `<%=resource%>/app_icons/${visit_list[i].id}.png`;
tr.insertCell(-1).innerHTML = `
<div style="height: 24px; display: flex; align-items: center;">
<img src="${iconSrc}" alt="${visit_list[i].name}" title="${visit_list[i].name}" style="width: 20px; height: 20px; border-radius: 5px; margin-right: 4px;">
<span>${visit_list[i].name}</span>
</div>
`;
var first_time_date = new Date(visit_list[i].ft * 1000);
var first_time_str = first_time_date.toLocaleString();
var latest_time_date = new Date(visit_list[i].lt * 1000);
var latest_time_str = latest_time_date.toLocaleString();
tr.insertCell(-1).innerHTML = first_time_str;
tr.insertCell(-1).innerHTML = latest_time_str;
var hour = parseInt(visit_list[i].tt / 3600);
var seconds = visit_list[i].tt % 3600;
var min = parseInt(seconds / 60)
var total_time_str;
if (visit_list[i].act == 1)
total_time_str = "-"
else {
if (hour > 0)
total_time_str = hour + "<%:h%>" + min + "<%:m%>"
else {
if (min == 0)
min = 1;
total_time_str = min + "<%:m%>"
}
}
tr.insertCell(-1).innerHTML = total_time_str;
tr.insertCell(-1).innerHTML = action_status;
var childs = tr.childNodes;
Array.prototype.forEach.call(childs, function (child) {
child.className = 'td';
});
}
}
}
function showDetails(mac) {
var modal = document.getElementById('detailsModal');
modal.style.display = 'flex';
updateDeviceInfoTitle(mac);
current_mac = mac;
visit_cur_page = 1; // Reset to first page when opening details
new XHR().get('<%=url('admin/network/dev_visit_time')%>/' + mac, null,
function (x, st) {
visit_time_data = st;
display_app_visit_view(visit_time_data);
}
);
getVisitListData(mac, visit_cur_page, visit_page_size);
}
function getVisitListData(mac, page, size) {
if (page === undefined) page = visit_cur_page;
if (size === undefined) size = visit_page_size;
new XHR().get('<%=url('admin/network/dev_visit_list')%>/' + mac, {page: page, page_size: size},
function (x, st) {
visit_list_data = st;
if (st.total !== undefined) {
visit_total_count = st.total;
visit_total_pages = st.total_pages || 1;
visit_cur_page = st.page || 1;
visit_page_size = st.page_size || 10;
}
render_visit_list_table(visit_list_data);
updateVisitListPagination();
}
);
}
function updateVisitListPagination() {
var paginationDiv = document.getElementById('visitListPagination');
if (!paginationDiv || visit_total_pages <= 0) {
if (paginationDiv) paginationDiv.innerHTML = '';
return;
}
// Calculate page numbers to display (max 10 pages)
// Try to show 5 pages on left and 5 pages on right of current page
var startPage = Math.max(1, visit_cur_page - 5);
var endPage = Math.min(visit_total_pages, visit_cur_page + 5);
// Adjust if we don't have 10 pages
var pageCount = endPage - startPage + 1;
if (pageCount < 10) {
if (startPage == 1) {
// If at the beginning, extend to the right
endPage = Math.min(visit_total_pages, startPage + 9);
} else if (endPage == visit_total_pages) {
// If at the end, extend to the left
startPage = Math.max(1, endPage - 9);
} else {
// In the middle, try to balance
var needed = 10 - pageCount;
var canExtendLeft = startPage - 1;
var canExtendRight = visit_total_pages - endPage;
if (canExtendLeft >= canExtendRight) {
startPage = Math.max(1, startPage - Math.min(needed, canExtendLeft));
endPage = Math.min(visit_total_pages, startPage + 9);
} else {
endPage = Math.min(visit_total_pages, endPage + Math.min(needed, canExtendRight));
startPage = Math.max(1, endPage - 9);
}
}
}
var html = '<div style="display: flex; align-items: center; justify-content: center; margin-top: 20px; padding: 15px 0;">';
html += '<div style="display: flex; align-items: center; gap: 4px;">';
// Previous page link
if (visit_cur_page > 1) {
html += '<a href="javascript:void(0)" onclick="goToVisitPage(' + (visit_cur_page - 1) + ')" style="padding: 8px 12px; color: #1a73e8; text-decoration: none; cursor: pointer; border-radius: 4px;"><%:Previous%></a>';
} else {
html += '<span style="padding: 8px 12px; opacity: 0.4; cursor: default;"><%:Previous%></span>';
}
// Page numbers
for (var i = startPage; i <= endPage; i++) {
if (i == visit_cur_page) {
html += '<span style="padding: 8px 12px; color: #1a73e8; font-weight: 500; cursor: default; border-radius: 4px;">' + i + '</span>';
} else {
html += '<a href="javascript:void(0)" onclick="goToVisitPage(' + i + ')" style="padding: 8px 12px; text-decoration: none; cursor: pointer; border-radius: 4px;">' + i + '</a>';
}
}
// Next page link
if (visit_cur_page < visit_total_pages) {
html += '<a href="javascript:void(0)" onclick="goToVisitPage(' + (visit_cur_page + 1) + ')" style="padding: 8px 12px; color: #1a73e8; text-decoration: none; cursor: pointer; border-radius: 4px;"><%:Next%></a>';
} else {
html += '<span style="padding: 8px 12px; opacity: 0.4; cursor: default;"><%:Next%></span>';
}
html += '</div>';
html += '<div style="margin-left: 20px; opacity: 0.6; font-size: 14px;">';
html += '<%:Total%>: ' + visit_total_count + ' | <%:Page%>: ' + visit_cur_page + ' / ' + visit_total_pages;
html += '</div>';
html += '</div>';
paginationDiv.innerHTML = html;
}
function goToVisitPage(page) {
if (page < 1) page = 1;
if (page > visit_total_pages && visit_total_pages > 0) page = visit_total_pages;
visit_cur_page = page;
getVisitListData(current_mac, visit_cur_page, visit_page_size);
}
function updateDeviceInfoTitle(mac) {
var deviceInfo = '(' + mac + ')';
if (user_list_data && user_list_data.list) {
var device = user_list_data.list.find(function(user) {
return user.mac === mac;
});
if (device) {
if (device.nickname && device.nickname.trim() !== '') {
deviceInfo = '(' + device.nickname + ')';
} else if (device.hostname && device.hostname.trim() !== '') {
deviceInfo = '(' + device.hostname + ')';
}
}
}
document.getElementById('deviceInfo').textContent = deviceInfo;
}
function showModifyNickname(mac) {
var modal = document.getElementById('nicknameModal');
modal.style.display = 'flex';
document.getElementById('nicknameMac').value = mac;
document.getElementById('nicknameMacDisplay').textContent = mac;
document.getElementById('nicknameInput').value = '';
}
function validateNickname(nickname) {
const invalidChars = /[\s'"]/;
return !invalidChars.test(nickname) && nickname.length <= 32;
}
function submitNicknameChange() {
var mac = document.getElementById('nicknameMac').value;
var nickname = document.getElementById('nicknameInput').value;
if (!validateNickname(nickname)) {
alert('<%:Please enter a valid remark%>');
return;
}
new XHR().post('<%=url('admin/network/set_nickname')%>',
{ mac: mac, nickname: nickname },
function(x, data) {
console.log('Nickname updated successfully');
closeModal('nicknameModal');
showSuccessMessage('<%:Settings saved successfully%>');
getAllUsersData(cur_page, page_size);
});
}
function closeModal(modalId) {
var modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'none';
}
}
function showTabContent(tabId) {
var tabs = document.querySelectorAll('.tab-body');
var tabItems = document.querySelectorAll('.tab-item');
tabs.forEach(function(tab) {
tab.classList.remove('active');
});
tabItems.forEach(function(item) {
item.classList.remove('active');
});
document.getElementById(tabId).classList.add('active');
document.querySelector('.tab-item[onclick="showTabContent(\'' + tabId + '\')"]').classList.add('active');
}
function get_display_time(total_time) {
var hour = parseInt(total_time / 3600);
var seconds = total_time % 3600;
var min = parseInt(seconds / 60)
var seconds2 = seconds % 60;
var total_time_str;
if (hour > 0)
total_time_str = hour + "<%:h%>" + min + "<%:m%>"
else {
if (min == 0 && seconds2 != 0)
min = 1;
total_time_str = min + "<%:m%>"
}
return total_time_str;
}
function display_app_visit_view(data) {
var chartElement = document.getElementById('app_time_chart');
if (!chartElement) {
console.error("Chart element not found");
return;
}
var myChart = echarts.init(chartElement);
if (!data) {
return;
}
var total_time = 0
var app_stat_array = new Array();
if (data.length == 0){
var app_obj ={}
app_obj.name = "<%:Unknown App%>"
app_obj.value = 0
app_obj.legendname = app_obj.name
app_stat_array.push(app_obj)
}
else{
for (var i = 0; i < data.length; i++) {
var app_obj = {};
app_obj.value = data[i].t;
app_obj.legendname = data[i].name;
var tmp_time = get_display_time(data[i].t);
app_obj.name = data[i].name + " " + tmp_time;
total_time += data[i].t
app_stat_array.push(app_obj);
}
}
var total_time_str = get_display_time(total_time);
var option = {
grid:{
},
color: [
'#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de',
'#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#ff9f7f',
'#ffdb5c', '#c23531', '#2f4554', '#61a0a8', '#d48265',
'#91c7ae', '#749f83', '#ca8622', '#bda29a', '#6e7074'
],
title: [
{
text: "<%:App Time Statistics%>",
textStyle: {
fontSize: 16,
color: "black"
},
left: "2%"
},
{
text: '',
subtext: total_time_str,
textStyle: {
fontSize: 15,
color: "black"
},
subtextStyle: {
fontSize: 15,
color: 'black'
},
textAlign: "center",
x: '34.5%',
y: '44%',
}],
tooltip: {
trigger: 'item',
formatter: function (parms) {
var total_time = get_display_time(parms.data.value);
var str = parms.seriesName + "</br>" +
parms.marker + "" + parms.data.legendname + "</br>" +
"<%:Visit Time%>: " + total_time + "</br>" +
"<%:Percentage%>: " + parms.percent + "%";
return str;
}
},
legend: {
type: "scroll",
orient: 'vertical',
left: '75%',
align: 'left',
top: 'middle',
textStyle: {
color: '#8C8C8C'
},
height: 250
},
series: [
{
name: "<%:Visit Time%>",
type: 'pie',
radius: ['58%', '70%'],
center: ['35%', '50%'],
clockwise: false,
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 1,
borderColor: "#fff",
borderWidth: 1,
},
label: {
show: true,
position: 'outside',
formatter: '{b}: {c} ({d}%)',
normal: {
show: true,
position: 'outter',
formatter: function (parms) {
return parms.data.legendname
}
}
},
labelLine: {
show: true,
length: 8,
length2: 7,
smooth: true,
},
data: app_stat_array
}
]
};
myChart.setOption(option);
}
//]]></script>
<div class="view-toggle">
<button type="button" class="view-toggle-btn active" id="btnTableView" onclick="switchView('table')" title="<%:Table View%>">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm15 2h-4v3h4V4zm0 4h-4v3h4V8zm0 4h-4v3h3a1 1 0 0 0 1-1v-2zm-5 3v-3H6v3h4zm-5 0v-3H1v2a1 1 0 0 0 1 1h3zm-4-4h4V8H1v3zm0-4h4V4H1v3zm5-3v3h4V4H6zm4 4H6v3h4V8z"/></svg>
</button>
<button type="button" class="view-toggle-btn" id="btnCardView" onclick="switchView('card')" title="<%:Card View%>">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M1 2a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2zm5 0a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V2zm5 0a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V2zM1 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V7zm5 0a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm5 0a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7zM1 12a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-2zm5 0a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2zm5 0a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2z"/></svg>
</button>
</div>
<div style="max-height: 750px; overflow-y: auto;padding-right: 20px;">
<div class="cbi-section cbi-tblsection">
<div id="tableViewWrapper" style="overflow-x: auto; min-width: 1200px;">
<table class="table cbi-section-table" id="user_status_table" style="table-layout: fixed; min-width: 1200px; width: 100%;">
<thead>
<tr class="tr">
<th class="th" style="width: 115px;">
<%:Device Info%>
</th>
<th class="th" style="width: 100px;">
<%:IP Address%>
</th>
<th class="th" style="width: 80px;">
<%:Up Rate%>
</th>
<th class="th" style="width: 80px;">
<%:Down Rate%>
</th>
<th class="th" style="width: 100px;">
<%:Today Active Time%>
</th>
<th class="th" style="width: 100px;">
<%:Today Flow%>
</th>
<th class="th" style="width: 160px; white-space: nowrap;">
<%:Common App(TOP5)%>
</th>
<th class="th" style="width: 85px;">
<%:Active App%>
</th>
<th class="th" style="width: 155px; white-space: nowrap;">
<%:Current URL%>
</th>
<th class="th" style="width: 70px;">
<%:Online Status%>
</th>
<th class="th" style="width: 120px;">
<%:Actions%>
</th>
</tr>
</thead>
<tbody>
<tr class="tr">
<td class="td" colspan="11"><em><br />
<%:Collecting data...%>
</em></td>
</tr>
</tbody>
</table>
</div>
<div id="cardViewWrapper" class="card-grid" style="display: none;"></div>
<!-- Pagination -->
<div id="pagination"></div>
</div>
</div>
<!-- Details Modal -->
<div id="detailsModal" class="oaf-modal-overlay">
<div class="oaf-modal-box" style="width: 820px; height: 620px;">
<button type="button" onclick="closeModal('detailsModal')" class="oaf-modal-close">&times;</button>
<h4 class="oaf-modal-title"><%:Device Details%> <span id="deviceInfo" style="opacity: 0.5; font-size: 14px; font-weight: normal;"></span></h4>
<!-- Tab List -->
<ul class="tab-list">
<!-- <li class="tab-item onclick="showTabContent('tab1')">基本信息</li> -->
<li class="tab-item active" onclick="showTabContent('tab2')"><%:App Statistics%></li>
<li class="tab-item" onclick="showTabContent('tab3')"><%:Access Records%></li>
</ul>
<div id="tab2" class="tab-body active">
<div class="pie-chart">
<div id="app_time_chart" style="width:100%;height: 350px;">
</div>
</div>
</div>
<div id="tab3" class="tab-body">
<div style="max-height: 320px; overflow-y: auto;padding-right: 20px;"> <!-- Added container with fixed height and overflow, reduced height to make room for pagination -->
<table class="table cbi-section-table" id="visit_list_table">
<thead>
<tr class="tr table-titles">
<th class="th">
<%:App Name%>
</th>
<th class="th">
<%:Start Visit Time%>
</th>
<th class="th">
<%:Last Visit Time%>
</th>
<th class="th">
<%:Duration%>
</th>
<th class="th">
<%:Filter Status%>
</th>
</tr>
</thead>
<tbody>
<tr class="tr">
<td class="td" colspan="5"><em><br />
<%:Collecting data...%>
</em></td>
</tr>
</tbody>
</table>
</div>
<!-- Visit List Pagination -->
<div id="visitListPagination" style="flex-shrink: 0; margin-top: 10px;"></div>
</div>
</div>
</div>
<!-- Nickname Modal -->
<div id="nicknameModal" class="oaf-modal-overlay">
<div class="oaf-modal-box" style="width: 460px;">
<button type="button" onclick="closeModal('nicknameModal')" class="oaf-modal-close">&times;</button>
<h4 class="oaf-modal-title"><%:Modify Remark%></h4>
<div style="margin-bottom: 20px;">
<p style="margin: 0 0 8px 0; opacity: 0.7; font-size: 14px;"><span class="field-label"><%:MAC Address%>:</span> <span id="nicknameMacDisplay" style="font-weight: 500; opacity: 1;">--</span></p>
</div>
<input type="hidden" id="nicknameMac" value="">
<div style="margin-bottom: 30px;">
<p style="margin: 0 0 8px 0; opacity: 0.7; font-size: 14px;"><span class="field-label"><%:Remark%>:</span></p>
<input type="text" id="nicknameInput" class="oaf-input" placeholder="请输入备注信息">
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px;">
<button type="button" class="oaf-btn oaf-btn-cancel" onclick="closeModal('nicknameModal')"><%:Cancel%></button>
<button type="button" class="oaf-btn oaf-btn-primary" onclick="submitNicknameChange()"><%:OK%></button>
</div>
</div>
</div>
<div id="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: transparent; z-index: 1000; justify-content: center; align-items: center;">
<div style="background-color: rgba(0, 0, 0, 0.65); padding: 14px 24px; border-radius: 6px; text-align: center; color: white; display: flex; justify-content: center; align-items: center;">
<p style="margin: 0; color: white; font-size: 14px;"><%:Settings saved successfully%></p>
</div>
</div>
<style>
.cbi-button.cbi-button-add {
background-color: #1a73e8 !important;
color: #ffffff !important;
border: none !important;
padding: 6px 12px !important;
border-radius: 4px !important;
cursor: pointer !important;
font-size: 13px !important;
font-weight: 500 !important;
transition: background-color 0.2s ease !important;
}
.cbi-button.cbi-button-add:hover {
background-color: #1557b0 !important;
color: #ffffff !important;
}
.cbi-button.cbi-button-add:active {
background-color: #0d47a1 !important;
color: #ffffff !important;
}
.cbi-section-table {
width: 100%;
border-collapse: collapse;
border-radius: 0;
border: none;
}
.cbi-section-table thead .th {
background: var(--background-color-low, #f5f5f5);
color: var(--text-color-medium, #6b7280);
font-weight: 600;
font-size: 12.5px;
letter-spacing: 0.2px;
text-transform: none;
padding: 10px 12px;
border-top: none;
border-bottom: 1px solid var(--border-color-medium, #d1d5db);
white-space: nowrap;
position: sticky;
top: 0;
z-index: 1;
text-align: left;
}
.cbi-section-table tbody .td {
padding: 10px 12px;
border-bottom: 1px solid var(--border-color-low, #e5e7eb);
font-size: 13px;
vertical-align: middle;
line-height: 1.5;
text-align: left;
}
.cbi-section-table tbody tr:last-child .td {
border-bottom: none;
}
.cbi-section-table tbody tr:hover .td {
background-color: var(--background-color-low, #f5f5f5);
}
.cbi-section-table tbody tr:nth-child(even) .td {
background-color: var(--background-color-low, #fafbfc);
}
.cbi-section-table tbody tr:nth-child(even):hover .td {
background-color: var(--background-color-low, #f5f5f5);
}
#user_status_table {
min-width: 1000px;
}
#user_status_table .td {
padding-left: 12px !important;
}
#user_status_table .td:last-child {
white-space: nowrap;
}
#user_status_table td:nth-child(8),
#user_status_table .td:nth-child(8) {
white-space: nowrap;
min-width: 175px;
}
#user_status_table .cbi-button {
padding: 5px 12px;
font-size: 12px;
border-radius: 4px;
border: 1px solid var(--border-color-medium, #d1d5db);
background: transparent;
color: #1a73e8;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
}
#user_status_table .cbi-button:hover {
background-color: var(--background-color-low, #f5f5f5);
border-color: #1a73e8;
}
#visit_list_table .td {
padding: 6px 10px !important;
height: 32px !important;
vertical-align: middle !important;
font-size: 12.5px;
}
#visit_list_table .th {
padding: 8px 10px !important;
height: 36px !important;
vertical-align: middle !important;
}
.tab-container {
margin-top: 10px;
}
.tab-list {
display: flex;
list-style-type: none;
padding: 0;
margin: 0 0 0 0;
border-bottom: 2px solid var(--border-color-low, #e5e7eb);
gap: 2px;
}
.tab-item {
padding: 8px 18px;
cursor: pointer;
border: none;
background-color: transparent;
border-radius: 6px 6px 0 0;
transition: all 0.2s ease;
opacity: 0.6;
font-size: 13px;
font-weight: 500;
position: relative;
}
.tab-item.active {
color: #1a73e8;
font-weight: 600;
opacity: 1;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background-color: #1a73e8;
border-radius: 2px 2px 0 0;
}
.tab-item:hover {
background-color: var(--background-color-low, #f5f5f5);
opacity: 0.8;
}
.tab-body {
margin-top: 5px;
display: none;
padding: 20px;
}
.tab-body.active {
display: block;
}
.pie-chart {
width: 600px;
max-width: 100%;
}
.oaf-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(3px);
z-index: 1000;
justify-content: center;
align-items: center;
animation: oafFadeIn 0.25s ease;
}
.oaf-modal-box {
background-color: var(--background-color-high, #fff);
padding: 28px;
border-radius: 12px;
text-align: left;
display: flex;
flex-direction: column;
position: relative;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
animation: oafSlideUp 0.3s ease;
max-width: 95vw;
max-height: 90vh;
overflow: hidden;
}
.oaf-modal-close {
position: absolute;
top: 14px;
right: 14px;
background-color: transparent;
border: none;
font-size: 22px;
opacity: 0.5;
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
line-height: 1;
}
.oaf-modal-close:hover {
background-color: var(--background-color-low, #f5f5f5);
opacity: 1;
}
.oaf-modal-title {
margin: 0 0 20px 0;
font-size: 17px;
font-weight: 600;
}
.oaf-btn {
padding: 8px 20px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.oaf-btn-primary {
background-color: #1a73e8;
color: #fff;
}
.oaf-btn-primary:hover {
background-color: #1557b0;
box-shadow: 0 2px 6px rgba(26, 115, 232, 0.3);
}
.oaf-btn-cancel {
background: transparent;
border: 1px solid var(--border-color-medium, #d1d5db);
}
.oaf-btn-cancel:hover {
background-color: var(--background-color-low, #f5f5f5);
}
.oaf-input {
padding: 10px 12px;
background: transparent;
border: 1px solid var(--border-color-medium, #d1d5db);
border-radius: 6px;
width: 100%;
font-size: 14px;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
color: inherit;
}
.oaf-input:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.12);
}
#pagination, #visitListPagination {
display: flex;
align-items: center;
justify-content: center;
padding: 12px 0;
margin-top: 8px;
}
.field-label {
font-weight: 600;
width: 75px;
display: inline-block;
}
.field-value {
color: #4CAF50;
}
.info-container {
display: flex;
flex-direction: column;
gap: 10px;
margin-left: 20px;
}
.info-row {
display: flex;
}
.info-column {
width: 50%;
text-align: left;
}
.info-column p {
margin-bottom: 20px;
}
.view-toggle {
position: relative;
display: flex;
justify-content: flex-end;
gap: 4px;
margin-bottom: 10px;
margin-left: auto;
width: fit-content;
}
.view-toggle-btn {
padding: 6px 10px;
border: 1px solid var(--border-color-medium, #d1d5db);
background: transparent;
opacity: 0.5;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.view-toggle-btn:first-child {
border-radius: 4px 0 0 4px;
}
.view-toggle-btn:last-child {
border-radius: 0 4px 4px 0;
margin-left: -1px;
}
.view-toggle-btn.active {
color: #1a73e8;
border-color: #1a73e8;
z-index: 1;
position: relative;
opacity: 1;
}
.view-toggle-btn:hover:not(.active) {
opacity: 0.7;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 12px;
padding: 4px 0;
}
.user-card {
border: 1px solid var(--border-color-low, #e5e7eb);
border-radius: 10px;
padding: 14px 16px;
position: relative;
cursor: pointer;
transition: box-shadow 0.2s, border-color 0.2s;
display: flex;
flex-direction: column;
gap: 10px;
}
.user-card:hover {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border-color: var(--border-color-medium, #d1d5db);
}
.user-card--offline {
opacity: 0.55;
}
.user-card--offline:hover {
opacity: 0.75;
}
.card-status {
position: absolute;
top: 10px;
right: 12px;
font-size: 11px;
font-weight: 500;
padding: 2px 8px;
border-radius: 10px;
line-height: 1.4;
}
.card-status--active {
background: #e8f0fe;
color: #1a73e8;
}
.card-status--online {
background: #e6f4ea;
color: #1e8e3e;
}
.card-status--offline {
background: var(--background-color-low, #f5f5f5);
opacity: 0.6;
}
.card-device {
padding-right: 60px;
}
.card-name {
font-size: 14px;
font-weight: 600;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-mac {
font-size: 11.5px;
opacity: 0.5;
margin-top: 2px;
font-family: monospace;
}
.card-rate {
display: flex;
gap: 16px;
align-items: center;
}
.card-rate-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12.5px;
opacity: 0.7;
font-variant-numeric: tabular-nums;
}
.card-rate-item svg {
flex-shrink: 0;
}
.card-visiting {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
opacity: 0.6;
overflow: hidden;
white-space: nowrap;
}
.card-visiting-label {
opacity: 0.7;
flex-shrink: 0;
}
.card-visiting-app {
font-weight: 500;
flex-shrink: 0;
opacity: 1;
}
.card-visiting-none {
opacity: 0.4;
}
.card-visiting-url {
opacity: 0.7;
overflow: hidden;
text-overflow: ellipsis;
cursor: help;
}
.card-info {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--border-color-low, #e5e7eb);
padding-top: 8px;
}
.card-apps {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 0;
overflow: hidden;
}
.card-app-icon {
width: 20px;
height: 20px;
border-radius: 4px;
flex-shrink: 0;
}
.card-time {
font-size: 12px;
opacity: 0.6;
cursor: help;
flex-shrink: 0;
padding: 2px 8px;
border-radius: 4px;
}
.card-flow {
font-size: 12px;
opacity: 0.6;
cursor: help;
flex-shrink: 0;
padding: 2px 8px;
border-radius: 4px;
}
@media (max-width: 768px) {
.oaf-modal-box {
width: 100vw !important;
height: 100vh !important;
max-width: 100vw !important;
max-height: 100vh !important;
border-radius: 0 !important;
padding: 16px !important;
margin: 0 !important;
}
.oaf-modal-overlay {
align-items: stretch !important;
}
.oaf-modal-close {
top: 10px !important;
right: 10px !important;
z-index: 10;
background-color: var(--background-color-low, #f5f5f5) !important;
}
.oaf-modal-title {
font-size: 15px !important;
padding-right: 40px;
}
.pie-chart {
width: 100% !important;
}
#tab2 .pie-chart #app_time_chart {
height: 260px !important;
}
#tab3 > div {
max-height: none !important;
overflow: auto !important;
}
.tab-body {
padding: 10px 0 !important;
}
#visit_list_table {
min-width: 500px;
}
.tab-body {
flex: 1;
overflow-y: auto;
min-height: 0;
}
#tableViewWrapper {
display: none !important;
}
#cardViewWrapper {
display: grid !important;
}
.view-toggle {
display: none;
}
.card-grid {
grid-template-columns: 1fr;
}
}
@media (min-width: 769px) and (max-width: 1200px) {
.card-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
}
@keyframes oafFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes oafSlideUp {
from { transform: translateY(16px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
</style>