gt / static /templates /cache.html
harii66's picture
Update static/templates/cache.html
52ed419 verified
Raw
History Blame Contribute Delete
24.4 kB
<div class="cache-page">
<div class="section-header">
<div>
<h2 style="font-size: 1.5em; color: var(--dark); margin: 0;">🗄️ 缓存管理面板</h2>
<p style="color: #64748b; margin-top: 5px;">查看和管理系统缓存状态</p>
</div>
<button id="refreshBtn" class="btn btn-primary">🔄 刷新状态</button>
</div>
<div id="cacheStats" class="cache-grid">
<div class="skeleton skeleton-card"></div>
<div class="skeleton skeleton-card"></div>
<div class="skeleton skeleton-card"></div>
<div class="skeleton skeleton-card"></div>
<div class="skeleton skeleton-card"></div>
</div>
<div class="epg-cache-section" id="epgCacheSection" style="display: none;">
<h3 style="margin-bottom: 20px; color: var(--dark); font-size: 1.3em;">📅 EPG 缓存详情</h3>
<div id="epgCacheDetails" class="epg-cache-details"></div>
</div>
<div class="cache-actions">
<h3>🧹 清理缓存</h3>
<p style="color: #64748b; margin-bottom: 15px; font-size: 0.9em;">选择要清理的缓存类型,清理后将重新获取数据</p>
<div class="button-group">
<button class="btn btn-warning" onclick="clearCache('cid')">🔑 清理 CID</button>
<button class="btn btn-warning" onclick="clearCache('auth')">🎫 清理认证</button>
<button class="btn btn-warning" onclick="clearCache('channels')">📺 清理频道</button>
<button class="btn btn-warning" onclick="clearCache('streams')">🎬 清理流</button>
<button class="btn btn-warning" onclick="clearCache('epg')">📅 清理 EPG</button>
<button class="btn btn-danger" onclick="clearCache('all')">🗑️ 清理全部</button>
</div>
</div>
</div>
<div class="epg-detail-modal" id="epgDetailModal">
<div class="epg-detail-container">
<div class="epg-detail-header">
<h3 id="epgDetailTitle">📺 节目详情</h3>
<button class="epg-detail-close" onclick="closeEpgDetail()"></button>
</div>
<div class="epg-detail-body">
<div id="epgDetailContent" class="epg-detail-content"></div>
</div>
</div>
</div>
<style>
.cache-page{display:flex;flex-direction:column;gap:16px;padding:14px;background:linear-gradient(180deg,#eef2ff 0%,#f1f5f9 240px);min-height:calc(100vh - 80px);font-feature-settings:"tnum"}
.cache-page .section-header{display:flex;justify-content:space-between;align-items:center;gap:12px;background:#fff;padding:14px 20px;border-radius:14px;box-shadow:0 4px 14px rgba(15,23,42,.06),0 1px 4px rgba(15,23,42,.04);border:1px solid #f1f5f9}
.cache-page .section-header h2{font-size:18px!important;font-weight:700;color:#1e293b!important;letter-spacing:.2px}
.cache-page .section-header .btn{padding:9px 18px;border:none;border-radius:8px;font-weight:600;font-size:13px;cursor:pointer;transition:all .15s;color:#fff;background:#6366f1;box-shadow:0 1px 3px rgba(15,23,42,.08);white-space:nowrap}
.cache-page .section-header .btn:hover{transform:translateY(-1px);filter:brightness(1.05);box-shadow:0 4px 12px rgba(99,102,241,.35)}
.cache-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:14px}
.cache-page .skeleton{background:linear-gradient(90deg,#f1f5f9,#e2e8f0,#f1f5f9);background-size:200% 100%;animation:caSkel 1.4s infinite;border-radius:14px}
.cache-page .skeleton-card{height:200px}
@keyframes caSkel{0%{background-position:200% 0}100%{background-position:-200% 0}}
.cache-actions{background:#fff;padding:22px 24px;border-radius:14px;box-shadow:0 4px 14px rgba(15,23,42,.06),0 1px 4px rgba(15,23,42,.04);border:1px solid #f1f5f9}
.cache-actions h3{margin:0;color:#1e293b;font-size:16px;font-weight:700}
.cache-actions p{margin-top:6px;margin-bottom:0}
.cache-actions .button-group{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}
.cache-actions .btn{padding:8px 16px;border:none;border-radius:8px;font-weight:600;font-size:13px;cursor:pointer;color:#fff;transition:all .15s;box-shadow:0 1px 3px rgba(15,23,42,.06)}
.cache-actions .btn:hover{transform:translateY(-1px);filter:brightness(1.06);box-shadow:0 4px 12px rgba(15,23,42,.15)}
.cache-actions .btn:active{transform:translateY(0)}
.cache-actions .btn-warning{background:#f59e0b}
.cache-actions .btn-danger{background:#ef4444}
.copy-btn{transition:all .15s!important}
.copy-btn:hover{filter:brightness(1.1);transform:translateY(-50%) scale(1.06)!important}
.cache-card { background: white; padding: 20px; border-radius: 14px; border-left: 4px solid var(--primary); box-shadow: 0 1px 3px rgba(15,23,42,.04),0 4px 14px rgba(15,23,42,.06); contain: layout style paint; transition: all .2s ease; border-top:1px solid #f1f5f9; border-right:1px solid #f1f5f9; border-bottom:1px solid #f1f5f9 }
.cache-card:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(15,23,42,.08) }
.cache-card h3 { color: var(--dark); margin-bottom: 15px; font-size: 1.1em; font-weight: 700; display: flex; align-items: center; gap: 8px; }
.cache-detail { display: flex; flex-direction: column; padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 0.85em; }
.cache-detail:last-child { border-bottom: none; }
.cache-label { font-weight: 600; color: #64748b; margin-bottom: 5px; font-size: 0.9em; }
.cache-value { font-family: 'Courier New', monospace; color: var(--dark); font-weight: 600; word-break: break-all; background: #f8fafc; padding: 8px 10px; border-radius: 6px; font-size: 0.85em; line-height: 1.5; border: 1px solid #e2e8f0; }
.cache-value.clickable { cursor: pointer; position: relative; padding-right: 35px; }
.cache-value.clickable:hover { background: #f1f5f9; border-color: #cbd5e1; }
.copy-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); background: var(--primary); color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 0.8em; transition: all 0.2s; }
.copy-btn:hover { background: var(--primary-dark); }
.copy-btn:active { transform: translateY(-50%) scale(0.95); }
.cache-value-short { color: #94a3b8; font-style: italic; }
.epg-cache-section { background: white; padding: 25px; border-radius: 15px; margin-bottom: 25px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }
.epg-cache-details { display: grid; gap: 20px; }
.epg-summary-box { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; }
.epg-summary-item { text-align: center; }
.epg-summary-item .label { font-size: 0.85em; opacity: 0.9; margin-bottom: 5px; }
.epg-summary-item .value { font-size: 1.8em; font-weight: 700; }
.epg-detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; }
.epg-detail-box { background: #f8fafc; padding: 15px; border-radius: 10px; border: 1px solid #e2e8f0; }
.epg-detail-box h4 { margin: 0 0 15px 0; color: var(--dark); font-size: 1em; font-weight: 700; }
.epg-list { max-height: 300px; overflow-y: auto; }
.epg-list-item { padding: 10px; background: white; border-radius: 8px; margin-bottom: 8px; font-size: 0.85em; border: 1px solid #e2e8f0; cursor: pointer; transition: all 0.2s ease; }
.epg-list-item:hover { border-color: var(--primary); box-shadow: 0 2px 8px rgba(99, 102, 241, 0.2); transform: translateX(4px); }
.epg-list-item .channel-id { font-weight: 700; color: var(--primary); margin-bottom: 5px; }
.epg-list-item .info { color: #64748b; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
.epg-detail-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(10px); z-index: 9999; align-items: center; justify-content: center; padding: 20px; overflow-y: auto; }
.epg-detail-modal.show { display: flex; }
.epg-detail-container { background: white; border-radius: 20px; max-width: 1000px; width: 100%; max-height: 90vh; overflow: hidden; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); animation: modalSlideIn 0.3s ease; }
@keyframes modalSlideIn { from { opacity: 0; transform: translateY(-30px); } to { opacity: 1; transform: translateY(0); } }
.epg-detail-header { padding: 20px 25px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; display: flex; justify-content: space-between; align-items: center; }
.epg-detail-header h3 { margin: 0; font-size: 1.4em; font-weight: 700; }
.epg-detail-close { background: rgba(255, 255, 255, 0.2); border: none; color: white; font-size: 1.5em; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; }
.epg-detail-close:hover { background: rgba(255, 255, 255, 0.3); transform: rotate(90deg); }
.epg-detail-body { padding: 25px; max-height: calc(90vh - 80px); overflow-y: auto; }
.epg-detail-content { display: flex; flex-direction: column; gap: 15px; }
.date-section { background: #f8fafc; padding: 15px; border-radius: 10px; border-left: 4px solid var(--primary); }
.date-section h4 { margin: 0 0 15px 0; color: var(--primary); font-size: 1.1em; font-weight: 700; display: flex; align-items: center; gap: 8px; }
.program-item { background: white; padding: 12px; border-radius: 8px; margin-bottom: 8px; border: 1px solid #e2e8f0; transition: all 0.2s ease; }
.program-item:hover { border-color: var(--primary); box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1); }
.program-time { font-weight: 700; color: var(--primary); margin-bottom: 5px; font-size: 0.9em; }
.program-title { color: var(--dark); font-weight: 600; margin-bottom: 3px; }
.program-desc { color: #64748b; font-size: 0.85em; line-height: 1.4; }
@media (max-width: 768px) { .epg-detail-grid { grid-template-columns: 1fr; } .epg-summary-box { grid-template-columns: 1fr; } }
</style>
<script>
(function() {
'use strict';
const API = window.location.origin;
let channelMap = {};
const debounce = window.MediaGatewayUtils?.debounce || function(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; };
function copyToClipboard(text, btnElement) {
navigator.clipboard.writeText(text).then(() => {
const originalText = btnElement.textContent;
btnElement.textContent = '✓';
btnElement.style.background = '#10b981';
setTimeout(() => {
btnElement.textContent = originalText;
btnElement.style.background = '';
}, 1500);
if (window.MediaGatewayUtils) window.MediaGatewayUtils.showNotification('✅ 已复制到剪贴板', 'success');
}).catch(err => {
if (window.MediaGatewayUtils) window.MediaGatewayUtils.showNotification('❌ 复制失败', 'error');
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function createCopyableValue(value, label, showPartial = false) {
if (!value) return '<span class="cache-value-short">未缓存</span>';
const id = 'copy_' + Math.random().toString(36).substr(2, 9);
let displayValue = value;
if (showPartial && value.length > 40) displayValue = value.substring(0, 20) + '...' + value.substring(value.length - 20);
return `<div class="cache-value clickable" style="position: relative;"><span id="${id}_text" data-full-value="${escapeHtml(value)}">${escapeHtml(displayValue)}</span><button class="copy-btn" onclick="window.copyCache('${id}_text')">📋 复制</button></div>`;
}
window.copyCache = function(elementId) {
const element = document.getElementById(elementId);
if (element) {
const fullValue = element.getAttribute('data-full-value');
const text = fullValue || element.textContent;
const btn = element.nextElementSibling;
copyToClipboard(text, btn);
}
};
async function loadChannelMap() {
try {
const res = await fetch(`${API}/api/list`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (data.success) {
const channels = data.channels || [];
channelMap = {};
channels.forEach(ch => { channelMap[ch.id] = ch.name; });
}
} catch (e) { console.error('❌ 加载频道列表失败:', e); }
}
function getChannelName(channelId) { return channelMap[channelId] || `频道 ${channelId}`; }
function formatTime(timestamp) {
const d = new Date(timestamp * 1000);
return d.toLocaleTimeString('ja-JP', { timeZone: 'Asia/Tokyo', hour: '2-digit', minute: '2-digit', hour12: false });
}
window.openEpgDetail = async function(channelId) {
const modal = document.getElementById('epgDetailModal');
const title = document.getElementById('epgDetailTitle');
const content = document.getElementById('epgDetailContent');
if (!modal || !content) return;
const channelName = getChannelName(channelId);
if (title) title.textContent = `📺 ${channelName} - 缓存节目`;
content.innerHTML = `<div style="text-align: center; padding: 40px;"><div class="loading-spinner-large"></div><p style="margin-top: 15px; color: #64748b;">加载中...</p></div>`;
modal.classList.add('show');
try {
const res = await fetch(`${API}/health`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const epgDetail = data.cache?.epg_detail;
if (!epgDetail || !epgDetail.by_channel || !epgDetail.by_channel[channelId]) {
content.innerHTML = `<div class="empty-state"><div class="empty-state-icon">📅</div><div class="empty-state-text">该频道暂无缓存数据</div></div>`;
return;
}
const channelData = epgDetail.by_channel[channelId];
const dates = channelData.dates.sort();
const dateDetails = await Promise.all(dates.map(async (date) => {
try {
const epgRes = await fetch(`${API}/api/epg?vid=${channelId}&date=${date}`);
if (!epgRes.ok) return null;
const epgData = await epgRes.json();
if (!epgData.success) return null;
return { date: date, programs: epgData.epg || [] };
} catch (e) { return null; }
}));
let html = '';
dateDetails.filter(d => d !== null).forEach(dateData => {
const { date, programs } = dateData;
const programCount = programs.length;
html += `<div class="date-section"><h4>📅 ${date} <span style="font-size: 0.85em; opacity: 0.8; font-weight: 600;">(${programCount} 个节目)</span></h4>`;
if (programs.length === 0) {
html += `<p style="color: #94a3b8; text-align: center; padding: 20px;">暂无节目数据</p>`;
} else {
programs.forEach(prog => {
const startTime = formatTime(prog.time);
const endTime = prog.time_end ? formatTime(prog.time_end) : '未知';
const title = prog.title || prog.name || '未知节目';
const desc = prog.description || '';
html += `<div class="program-item"><div class="program-time">⏰ ${startTime} - ${endTime}</div><div class="program-title">${escapeHtml(title)}</div>${desc ? `<div class="program-desc">📝 ${escapeHtml(desc)}</div>` : ''}</div>`;
});
}
html += `</div>`;
});
if (html === '') html = `<div class="empty-state"><div class="empty-state-icon">📅</div><div class="empty-state-text">暂无节目数据</div></div>`;
content.innerHTML = html;
} catch (e) {
content.innerHTML = `<div class="empty-state"><div class="empty-state-icon" style="color: var(--danger);">❌</div><div class="empty-state-text">加载失败</div><div class="empty-state-subtitle">${e.message}</div></div>`;
}
};
window.closeEpgDetail = function() {
const modal = document.getElementById('epgDetailModal');
if (modal) modal.classList.remove('show');
};
function renderEPGDetails(epgDetail) {
const section = document.getElementById('epgCacheSection');
const details = document.getElementById('epgCacheDetails');
if (!epgDetail || epgDetail.total_entries === 0) { section.style.display = 'none'; return; }
section.style.display = 'block';
let html = '';
if (epgDetail.summary) {
html += `<div class="epg-summary-box"><div class="epg-summary-item"><div class="label">📦 缓存条目</div><div class="value">${epgDetail.total_entries}</div></div><div class="epg-summary-item"><div class="label">📺 频道数</div><div class="value">${epgDetail.summary.total_channels}</div></div><div class="epg-summary-item"><div class="label">📅 日期数</div><div class="value">${epgDetail.summary.total_dates}</div></div><div class="epg-summary-item"><div class="label">🎬 节目数</div><div class="value">${epgDetail.summary.total_programs}</div></div></div>`;
}
if (epgDetail.full_cache_available) {
html += `<div style="background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%); padding: 15px; border-radius: 10px; margin-top: 20px; border: 2px solid #6ee7b7;"><div style="display: flex; align-items: center; gap: 10px; color: #065f46;"><span style="font-size: 1.5em;">✅</span><div><div style="font-weight: 700; margin-bottom: 5px;">全量缓存已就绪</div><div style="font-size: 0.85em; opacity: 0.9;">缓存时间: ${epgDetail.full_cache_time} (${epgDetail.full_cache_age} 前)</div></div></div></div>`;
}
html += '<div class="epg-detail-grid">';
if (epgDetail.by_channel && Object.keys(epgDetail.by_channel).length > 0) {
html += `<div class="epg-detail-box"><h4>📺 按频道统计 (前10个)</h4><div class="epg-list">`;
const channels = Object.entries(epgDetail.by_channel).sort((a, b) => b[1].program_count - a[1].program_count).slice(0, 10);
channels.forEach(([channelId, info]) => {
const channelName = getChannelName(channelId);
html += `<div class="epg-list-item" onclick="openEpgDetail('${channelId}')"><div class="channel-id">${channelName}</div><div class="info"><span>📅 ${info.dates.length} 个日期</span><span>🎬 ${info.program_count} 个节目</span></div></div>`;
});
html += `</div></div>`;
}
if (epgDetail.by_date && Object.keys(epgDetail.by_date).length > 0) {
html += `<div class="epg-detail-box"><h4>📅 按日期统计</h4><div class="epg-list">`;
const dates = Object.entries(epgDetail.by_date).sort((a, b) => b[0].localeCompare(a[0]));
dates.forEach(([date, info]) => {
html += `<div class="epg-list-item"><div class="channel-id">${date}</div><div class="info"><span>📺 ${info.channels.length} 个频道</span><span>🎬 ${info.program_count} 个节目</span></div></div>`;
});
html += `</div></div>`;
}
html += '</div>';
details.innerHTML = html;
}
async function load() {
const stats = document.getElementById('cacheStats');
if (!stats) return;
stats.innerHTML = `<div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div>`;
await loadChannelMap();
try {
const token = sessionStorage.getItem('admin_token');
const headers = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${API}/health`, { headers });
const data = await res.json();
if (!data.cache) throw new Error('无法获取缓存信息');
const cache = data.cache;
const cacheCards = [
{ icon: '🔑', title: 'CID 缓存', color: '#6366f1', data: cache.cid || {} },
{ icon: '🎫', title: '认证缓存', color: '#8b5cf6', data: cache.auth || {} },
{ icon: '📺', title: '频道缓存', color: '#ec4899', data: { cached: cache.channels !== undefined ? (typeof cache.channels === 'boolean' ? cache.channels : cache.channels > 0) : false } },
{ icon: '🎬', title: '流缓存', color: '#f59e0b', data: { cached: cache.streams > 0, count: cache.streams } },
{ icon: '📅', title: 'EPG 缓存', color: '#10b981', data: { cached: cache.epg > 0, count: cache.epg } }
];
stats.innerHTML = cacheCards.map((card) => {
let content = '';
const cached = card.data.cached !== undefined ? card.data.cached : !!card.data.value || !!card.data.token;
content += `<div class="cache-detail"><span class="cache-label">状态</span><span class="cache-value">${cached ? '✅ 已缓存' : '❌ 未缓存'}</span></div>`;
if (cached && card.data.value) content += `<div class="cache-detail"><span class="cache-label">完整 CID</span>${createCopyableValue(card.data.value, 'CID', false)}</div>`;
if (cached && card.data.token) content += `<div class="cache-detail"><span class="cache-label">Access Token</span>${createCopyableValue(card.data.token, 'Token', true)}</div>`;
if (card.data.age) content += `<div class="cache-detail"><span class="cache-label">缓存时长</span><span class="cache-value">${card.data.age}</span></div>`;
if (card.data.ttl) content += `<div class="cache-detail"><span class="cache-label">剩余有效期</span><span class="cache-value">${card.data.ttl}</span></div>`;
if (card.data.storage) content += `<div class="cache-detail"><span class="cache-label">存储位置</span><span class="cache-value">${card.data.storage}</span></div>`;
if (card.data.count !== undefined) content += `<div class="cache-detail"><span class="cache-label">缓存数量</span><span class="cache-value">${card.data.count} 个</span></div>`;
return `<div class="cache-card" style="border-left-color: ${card.color};"><h3>${card.icon} ${card.title}</h3>${content}</div>`;
}).join('');
if (cache.epg_detail) renderEPGDetails(cache.epg_detail);
if (window.MediaGatewayUtils) window.MediaGatewayUtils.showNotification('✅ 缓存状态已更新', 'success');
} catch (e) {
stats.innerHTML = `<div style="grid-column: 1/-1; text-align: center; padding: 60px 30px; color: var(--danger);"><div style="font-size: 60px; margin-bottom: 20px;">❌</div><h3>加载失败</h3><p style="color: #64748b; margin-top: 10px;">${e.message}</p></div>`;
if (window.MediaGatewayUtils) window.MediaGatewayUtils.showNotification('❌ 加载失败', 'error');
}
}
window.clearCache = debounce(async function(type) {
const typeNames = { 'cid': 'CID', 'auth': '认证', 'channels': '频道', 'streams': '流', 'epg': 'EPG', 'all': '所有' };
const msg = type === 'all' ? '⚠️ 确定要清理所有缓存吗?这将导致所有数据重新获取。' : `确定要清理 ${typeNames[type]} 缓存吗?`;
if (!confirm(msg)) return;
try {
const token = sessionStorage.getItem('admin_token');
const headers = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${API}/api/refresh?type=${type}`, { headers });
const data = await res.json();
if (!data.success) throw new Error(data.error || '清理失败');
if (window.MediaGatewayUtils) window.MediaGatewayUtils.showNotification(`✅ ${data.message}`, 'success');
await load();
} catch (e) {
if (window.MediaGatewayUtils) window.MediaGatewayUtils.showNotification('❌ 清理失败: ' + e.message, 'error');
}
}, 300);
window.initCachePage = function() {
load();
const btn = document.getElementById('refreshBtn');
if (btn) btn.addEventListener('click', debounce(load, 300));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const modal = document.getElementById('epgDetailModal');
if (modal && modal.classList.contains('show')) closeEpgDetail();
}
});
const modal = document.getElementById('epgDetailModal');
if (modal) modal.addEventListener('click', (e) => { if (e.target === modal) closeEpgDetail(); });
};
setTimeout(window.initCachePage, 0);
})();
</script>