| <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-card {
|
| background: white;
|
| padding: 20px;
|
| border-radius: 15px;
|
| border-left: 4px solid var(--primary);
|
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
| contain: layout style paint;
|
| }
|
|
|
| .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 => {
|
| console.error('复制失败:', 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;
|
| });
|
| console.log('✅ 频道映射加载完成:', Object.keys(channelMap).length, '个频道');
|
| }
|
| } 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) {
|
| console.log('📺 打开频道详情:', 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) {
|
| console.error(`获取 ${date} 数据失败:`, 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) {
|
| console.error('加载失败:', 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 }
|
| },
|
| {
|
| 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 = '';
|
|
|
| if (card.data.cached !== undefined) {
|
|
|
| content += `
|
| <div class="cache-detail">
|
| <span class="cache-label">状态</span>
|
| <span class="cache-value">${card.data.cached ? '✅ 已缓存' : '❌ 未缓存'}</span>
|
| </div>
|
| `;
|
|
|
|
|
| if (card.data.cached && card.data.value) {
|
| content += `
|
| <div class="cache-detail">
|
| <span class="cache-label">完整 CID</span>
|
| ${createCopyableValue(card.data.value, 'CID', false)}
|
| </div>
|
| `;
|
| }
|
|
|
|
|
| if (card.data.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> |