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

348 lines
12 KiB
HTML
Executable File

<link rel="stylesheet" href="<%=resource%>/css/common.css">
<div class="feature-container">
<!-- 基本信息卡片 -->
<div id="feature-info" class="feature-info-card">
<div class="feature-info-grid">
<div class="feature-info-item">
<span class="feature-info-label"><%:Current version%></span>
<span class="feature-info-value" id="info-version">--</span>
</div>
<div class="feature-info-item">
<span class="feature-info-label"><%:Feature format%></span>
<span class="feature-info-value" id="info-format">--</span>
</div>
<div class="feature-info-item">
<span class="feature-info-label"><%:App number%></span>
<span class="feature-info-value" id="info-appnum">--</span>
</div>
<div class="feature-info-item">
<span class="feature-info-label"><%:Feature download%></span>
<span class="feature-info-value"><a href="https://www.openappfilter.com" target="_blank">www.openappfilter.com</a></span>
</div>
</div>
<div class="feature-card">
<div class="feature-row">
<label class="feature-label" for="ulfile"><%:Feature Upgrade%>:</label>
<div class="feature-inputs">
<input class="cbi-input-file feature-file" type="file" id="ulfile" name="ulfile" />
<input type="submit" class="cbi-button cbi-input-apply feature-btn" name="upload" value="<%:Upload%>" onclick="startUpgradeStatusPoll(); return true;"/>
</div>
</div>
<div class="feature-hint" id="upload-hint">
<%:You can download the App feature library from the website(www.openappfilter.com),then upload it here.Please note that after downloading, you need to extract the files and select the one in .bin format.%>
</div>
<div class="feature-status" id="upgrade-status" style="display:none;"></div>
</div>
<div class="feature-card" style="margin-top: 16px;">
<div class="feature-row" style="margin-bottom: 8px;">
<label class="feature-label"><%:App List%>:</label>
</div>
<div id="app-list" class="app-list"></div>
</div>
<div style="margin-top: 20px; display: flex; justify-content: flex-end;">
<button type="button" class="submit-button" onclick="location.reload()" style="padding: 8px 20px; width: 150px; background: transparent; color: #1a73e8; border: 1px solid var(--border-color-medium, #d1d5db); border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 500; display: inline-flex; align-items: center; justify-content: center; line-height: 1.4;"><%:Refresh%></button>
</div>
</div>
<div id="modal" class="feature-modal">
<div class="feature-modal-content">
<p><%:Updating, please wait...%></p>
</div>
</div>
<style>
/* 让顶部描述和下方内容左侧对齐 */
.cbi-map-descr {
margin: 0 10px 10px 10px;
font-size: 13px;
line-height: 1.6;
}
.feature-container { padding: 10px; }
.feature-info-card{padding:16px;border:1px solid var(--border-color-low, #e5e7eb);border-radius:8px;background:var(--background-color-high, #ffffff);margin-bottom:12px;}
.feature-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;}
.feature-info-item{padding:10px 12px;border:1px solid var(--border-color-low, #e5e7eb);border-radius:6px;background:var(--background-color-low, #f5f5f5);}
.feature-info-label{display:block;font-size:13px;color:var(--text-color-medium, #6b7280);margin-bottom:4px;}
.feature-info-value{font-weight:600;font-size:15px;color:var(--text-color-high, #333);}
.feature-card {background: var(--background-color-high, #ffffff);border: 1px solid var(--border-color-low, #e5e7eb);border-radius: 8px;padding: 16px;margin-top: 12px;}
.feature-row {display: flex;align-items: center;gap: 12px;flex-wrap: wrap;}
.feature-label {min-width: 120px;font-weight: 600;}
.feature-inputs {display: flex;align-items: center;gap: 10px;flex: 1;}
.feature-file {width: 100%;max-width: 360px;}
.feature-btn {min-width: 110px;}
.feature-hint {margin-top: 10px;color: var(--text-color-medium, #6b7280);font-size: 13px;line-height: 1.5;}
.feature-status {margin-top: 8px;font-size: 14px;font-weight: 600;}
.feature-status.success {color: #34d399;}
.feature-status.error {color: #f87171;}
.feature-modal {display: none;position: fixed;top: 0; left: 0; right: 0; bottom: 0;background-color: rgba(0, 0, 0, 0.5);z-index: 1000;justify-content: center;align-items: center;}
.feature-modal-content {background: var(--background-color-high, #ffffff);padding: 16px 20px;border-radius: 6px;color: var(--text-color-high, #333);min-width: 200px;text-align: center;box-shadow: 0 4px 12px rgba(0,0,0,0.4);border: 1px solid var(--border-color-low, #e5e7eb);}
.app-list {margin-top: 10px;}
.class-card {margin-bottom: 16px;border:1px solid var(--border-color-low, #e5e7eb);border-radius:8px;background:var(--background-color-high, #ffffff);}
.class-header {padding:12px 16px;font-weight:600;font-size:15px;border-bottom:1px solid var(--border-color-low, #e5e7eb);color:var(--text-color-high, #333);}
.app-items {display:grid;grid-template-columns:repeat(4,1fr);gap:10px;padding:12px 16px;}
.app-item {display:flex;align-items:center;gap:8px;padding:8px 12px;border:1px solid var(--border-color-medium, #d1d5db);border-radius:6px;background:var(--background-color-low, #f5f5f5);font-size:13px;cursor:default;transition:all 0.2s ease;}
.app-item:hover {background:var(--background-color-medium, #eeeeee);border-color:var(--border-color-high, #9ca3af);transform:translateY(-1px);}
.app-item img {width:20px;height:20px;object-fit:contain;border-radius:4px;}
.app-item span {color:var(--text-color-medium, #6b7280);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100px;}
@media (min-width: 800px) {
.app-items {grid-template-columns:repeat(6,1fr);}
}
@media (min-width: 1200px) {
.app-items {grid-template-columns:repeat(8,1fr);}
}
</style>
<script>
var pollTimer = null;
var pollTimeout = null;
var classListLoaded = false;
// Helper functions
function $(selector) {
return document.querySelector(selector);
}
function $$(selector) {
return document.querySelectorAll(selector);
}
function showModal() {
var modal = $('#modal');
if (modal) {
modal.style.display = 'flex';
setTimeout(function() {
modal.style.display = 'none';
}, 15000);
}
}
function hideModal() {
var modal = $('#modal');
if (modal) {
modal.style.display = 'none';
}
}
function showToast(msg, type) {
var hint = $('#upload-hint');
if (!hint) return;
hint.textContent = msg || '';
hint.classList.remove('text-error', 'text-success');
if (type === 'error') {
hint.classList.add('text-error');
} else if (type === 'success') {
hint.classList.add('text-success');
}
}
function stopPoll() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
if (pollTimeout) {
clearTimeout(pollTimeout);
pollTimeout = null;
}
}
function statusMessage(status) {
if (status === 200) return '<%:Update the feature file successfully%>';
if (status === 401) return '<%:Failed to update feature file, format error%>';
if (status === 402) return '<%:Failed to update feature file%> (<%:File too large%>)';
if (status === 1) return '<%:Updating, please wait...%>';
return '<%:Failed to update feature file%>';
}
function renderUpgradeStatus(status) {
var box = $('#upgrade-status');
if (!box) return;
if (!status || status === 0) {
box.style.display = 'none';
box.textContent = '';
box.classList.remove('success', 'error');
return;
}
var msg = statusMessage(status);
box.textContent = msg;
box.style.display = '';
box.classList.remove('success', 'error');
if (status === 200) {
box.classList.add('success');
} else {
box.classList.add('error');
}
}
function fetchUpgradeStatusOnce() {
fetch('<%=url("admin/appfilter/feature/upgrade_status")%>')
.then(function(response) {
return response.json();
})
.then(function(resp) {
var status = (resp && resp.status) ? resp.status : 0;
renderUpgradeStatus(status);
})
.catch(function(error) {
console.error('Failed to fetch upgrade status:', error);
});
}
function startUpgradeStatusPoll() {
showModal();
pollTimeout = setTimeout(function() {
stopPoll();
hideModal();
}, 60000);
}
function loadFeatureInfo() {
new XHR().get('<%=url("admin/appfilter/feature/info")%>', null, function(x, data) {
if (data && data.code === 0 && data.data) {
var versionEl = $('#info-version');
var formatEl = $('#info-format');
var appnumEl = $('#info-appnum');
if (versionEl) versionEl.textContent = data.data.version || '--';
if (formatEl) formatEl.textContent = data.data.format || '--';
if (appnumEl) appnumEl.textContent = data.data.app_count || 0;
}
});
}
function loadClassList() {
fetch('<%=url("admin/network/class_list")%>')
.then(function(response) {
return response.json();
})
.then(function(data) {
var list = [];
if (data && data.class_list) {
list = data.class_list;
} else if (data && data.data && Array.isArray(data.data)) {
list = data.data;
} else if (data && data.classes && Array.isArray(data.classes)) {
list = data.classes;
}
renderClassList(list);
classListLoaded = true;
})
.catch(function(error) {
console.error('Failed to load class list:', error);
var appList = $('#app-list');
if (appList) {
appList.innerHTML = '<div class="feature-hint"><%:Failed to load app list%></div>';
}
});
}
function updateAppItemsColumns() {
var container = $('.feature-container');
var containerWidth = container ? container.offsetWidth : window.innerWidth;
var appItems = $$('.app-items');
var cols = 4;
if (containerWidth >= 1200) {
cols = 8;
} else if (containerWidth >= 800) {
cols = 6;
}
appItems.forEach(function(item) {
item.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
});
}
function renderClassList(list) {
var container = $('#app-list');
if (!container) return;
container.innerHTML = '';
if (!list || list.length === 0) {
var noDataDiv = document.createElement('div');
noDataDiv.className = 'feature-hint';
noDataDiv.textContent = '<%:No data%>';
container.appendChild(noDataDiv);
return;
}
list.forEach(function(category) {
var appList = category.app_list || [];
var card = document.createElement('div');
card.className = 'class-card';
var categoryName = category.name || ('Class ' + (category.id || ''));
var appCount = appList.length;
var header = document.createElement('div');
header.className = 'class-header';
header.textContent = categoryName + ' (' + appCount + ')';
card.appendChild(header);
var apps = document.createElement('div');
apps.className = 'app-items';
if (appList.length === 0) {
var noAppsDiv = document.createElement('div');
noAppsDiv.className = 'feature-hint';
noAppsDiv.textContent = '<%:No apps%>';
apps.appendChild(noAppsDiv);
} else {
appList.forEach(function(app) {
var parts = app.split(',');
var appId = parts[0] || '';
var appName = parts[1] || 'Unknown';
var hasIcon = parts[2] === '1';
var appItem = document.createElement('div');
appItem.className = 'app-item';
var iconSrc = hasIcon ? '<%=resource%>/app_icons/' + appId + '.png' : '<%=resource%>/app_icons/default.png';
var icon = document.createElement('img');
icon.src = iconSrc;
icon.alt = appName;
icon.onerror = function() {
this.src = '<%=resource%>/app_icons/default.png';
};
var name = document.createElement('span');
name.textContent = appName;
appItem.appendChild(icon);
appItem.appendChild(name);
apps.appendChild(appItem);
});
}
card.appendChild(apps);
container.appendChild(card);
});
setTimeout(function() {
updateAppItemsColumns();
}, 100);
}
document.addEventListener('DOMContentLoaded', function() {
fetchUpgradeStatusOnce();
loadFeatureInfo();
loadClassList();
var resizeTimer;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
updateAppItemsColumns();
}, 200);
});
});
</script>