| <!DOCTYPE html> |
| <html lang="zh-TW"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>管理中心 - KSTools</title> |
| <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔑</text></svg>"> |
| <link rel="stylesheet" href="css/style.css"> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| |
| .dashboard-container { |
| max-width: 1400px; |
| margin: 0 auto; |
| padding: 2rem; |
| } |
| |
| |
| .top-nav { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 2rem; |
| padding-bottom: 1rem; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| |
| |
| |
| .page-header { |
| text-align: center; |
| margin-bottom: 3rem; |
| } |
| |
| .page-header h1 { |
| margin: 0 0 0.5rem 0; |
| font-size: 2.5rem; |
| color: var(--text-primary); |
| } |
| |
| .page-header p { |
| margin: 0; |
| color: var(--text-secondary); |
| font-size: 1.1rem; |
| } |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
| gap: 1.5rem; |
| margin-bottom: 2rem; |
| } |
| |
| .stat-card { |
| background: var(--card-bg); |
| border: 1px solid var(--border-color); |
| border-radius: 8px; |
| padding: 1.5rem; |
| transition: transform 0.2s, box-shadow 0.2s; |
| } |
| |
| .stat-card:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
| } |
| |
| .stat-card .stat-icon { |
| width: 48px; |
| height: 48px; |
| border-radius: 8px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| margin-bottom: 1rem; |
| font-size: 1.5rem; |
| } |
| |
| .stat-card.licenses .stat-icon { |
| background: rgba(88, 166, 255, 0.1); |
| color: var(--accent-blue); |
| } |
| |
| .stat-card.versions .stat-icon { |
| background: rgba(63, 185, 80, 0.1); |
| color: var(--accent-green); |
| } |
| |
| .stat-card.users .stat-icon { |
| background: rgba(168, 85, 247, 0.1); |
| color: var(--accent-purple); |
| } |
| |
| .stat-card.downloads .stat-icon { |
| background: rgba(210, 153, 34, 0.1); |
| color: var(--accent-yellow); |
| } |
| |
| .stat-card h3 { |
| margin: 0; |
| color: var(--text-secondary); |
| font-size: 0.875rem; |
| font-weight: normal; |
| } |
| |
| .stat-card .stat-value { |
| font-size: 2rem; |
| font-weight: bold; |
| color: var(--text-primary); |
| margin: 0.5rem 0; |
| } |
| |
| .stat-card .stat-change { |
| font-size: 0.875rem; |
| color: var(--text-secondary); |
| } |
| |
| .feature-grid { |
| display: grid; |
| grid-template-columns: repeat(4, 1fr); |
| gap: 1.5rem; |
| max-width: 1200px; |
| margin: 0 auto; |
| } |
| |
| .feature-card { |
| background: var(--card-bg); |
| border: 1px solid var(--border-color); |
| border-radius: 12px; |
| padding: 1.5rem; |
| transition: all 0.3s; |
| cursor: pointer; |
| text-decoration: none; |
| color: inherit; |
| position: relative; |
| overflow: hidden; |
| text-align: center; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| min-height: 120px; |
| } |
| |
| .feature-card::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| height: 4px; |
| background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple)); |
| transform: scaleX(0); |
| transition: transform 0.3s; |
| } |
| |
| .feature-card:hover { |
| transform: translateY(-4px); |
| box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); |
| border-color: var(--accent-blue); |
| } |
| |
| .feature-card:hover::before { |
| transform: scaleX(1); |
| } |
| |
| .feature-card .feature-icon { |
| width: 64px; |
| height: 64px; |
| border-radius: 12px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| margin-bottom: 1.5rem; |
| font-size: 2rem; |
| } |
| |
| .feature-card.license-feature .feature-icon { |
| background: linear-gradient(135deg, #58a6ff, #1f6feb); |
| color: white; |
| } |
| |
| .feature-card.version-feature .feature-icon { |
| background: linear-gradient(135deg, #3fb950, #238636); |
| color: white; |
| } |
| |
| .feature-card { |
| background: rgba(255, 255, 255, 0.02); |
| border: 1px solid var(--border-color); |
| } |
| |
| .feature-card h2 { |
| margin: 0; |
| font-size: 1.1rem; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 0.5rem; |
| } |
| |
| .feature-card h2 i { |
| font-size: 2rem; |
| color: var(--accent-blue); |
| } |
| |
| .feature-card.license-feature h2 i { |
| color: var(--accent-blue); |
| } |
| |
| .feature-card.version-feature h2 i { |
| color: var(--accent-green); |
| } |
| |
| |
| .loading-skeleton { |
| background: linear-gradient(90deg, var(--card-bg) 25%, var(--border-color) 50%, var(--card-bg) 75%); |
| background-size: 200% 100%; |
| animation: loading 1.5s infinite; |
| border-radius: 4px; |
| height: 20px; |
| } |
| |
| @keyframes loading { |
| 0% { background-position: 200% 0; } |
| 100% { background-position: -200% 0; } |
| } |
| |
| |
| |
| @media (max-width: 768px) { |
| .dashboard-container { |
| padding: 1rem; |
| } |
| |
| .top-nav { |
| flex-direction: column; |
| gap: 1rem; |
| align-items: stretch; |
| } |
| |
| .nav-left, .nav-right { |
| display: flex; |
| justify-content: center; |
| } |
| |
| .page-header h1 { |
| font-size: 2rem; |
| } |
| |
| .feature-grid { |
| grid-template-columns: repeat(2, 1fr); |
| gap: 1rem; |
| } |
| |
| .feature-card { |
| min-height: 100px; |
| padding: 1rem; |
| } |
| |
| .feature-card h2 { |
| font-size: 1rem; |
| } |
| |
| .feature-card h2 i { |
| font-size: 1.5rem; |
| } |
| |
| } |
| </style> |
| </head> |
| <body> |
| |
| <div id="loading-overlay" class="loading-overlay"> |
| <div class="spinner"></div> |
| <p>載入中...</p> |
| </div> |
|
|
| |
| <div class="dashboard-container"> |
| |
| <div class="top-nav"> |
| <div class="nav-left"> |
| </div> |
| <div class="nav-right"> |
| <div class="header-actions"> |
| <div id="userInfo" class="user-info"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="page-header"> |
| <h1>KSTools 管理中心</h1> |
| <p>授權與版本管理系統</p> |
| </div> |
|
|
| |
| <div class="feature-grid"> |
| |
| <a href="/index.html" class="feature-card license-feature"> |
| <h2><i class="fas fa-shield-alt"></i> 授權管理</h2> |
| </a> |
|
|
| |
| <a href="/versions.html" class="feature-card version-feature"> |
| <h2><i class="fas fa-rocket"></i> 版本管理</h2> |
| </a> |
| </div> |
| </div> |
|
|
| |
| <script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script> |
| <script src="config.js"></script> |
| <script src="js/utils.js"></script> |
| <script src="js/auth.js"></script> |
| <script> |
| |
| let supabaseLicense = null; |
| let supabaseVersion = null; |
| |
| |
| async function initDashboard() { |
| try { |
| |
| if (window.APP_CONFIG?.CONFIG_LOADED) { |
| if (window.APP_CONFIG.SUPABASE_LICENSE_URL && window.APP_CONFIG.SUPABASE_LICENSE_ANON_KEY) { |
| supabaseLicense = window.supabase?.createClient( |
| window.APP_CONFIG.SUPABASE_LICENSE_URL, |
| window.APP_CONFIG.SUPABASE_LICENSE_ANON_KEY |
| ); |
| } |
| |
| if (window.APP_CONFIG.SUPABASE_VERSION_URL && window.APP_CONFIG.SUPABASE_VERSION_ANON_KEY) { |
| supabaseVersion = window.supabase?.createClient( |
| window.APP_CONFIG.SUPABASE_VERSION_URL, |
| window.APP_CONFIG.SUPABASE_VERSION_ANON_KEY |
| ); |
| } |
| } |
| |
| |
| await loadDashboardStats(); |
| |
| } catch (error) { |
| console.error('儀表板初始化失敗:', error); |
| Utils.showError('載入統計資料失敗'); |
| } |
| } |
| |
| |
| async function loadDashboardStats() { |
| try { |
| |
| if (window.authManager?.isDevMode) { |
| const mockLicenseStats = { |
| total: 25, |
| active: 18, |
| expiring: 3, |
| recentActive: 15 |
| }; |
| |
| const mockVersionStats = { |
| totalVersions: 5, |
| latestVersion: { |
| version: '1.2.0', |
| release_date: new Date().toISOString() |
| }, |
| totalDownloads: 156, |
| monthlyDownloads: 42 |
| }; |
| |
| updateStatsDisplay(mockLicenseStats, mockVersionStats); |
| return; |
| } |
| |
| |
| const [licenseStats, versionStats] = await Promise.all([ |
| loadLicenseStats(), |
| loadVersionStats() |
| ]); |
| |
| |
| updateStatsDisplay(licenseStats, versionStats); |
| |
| } catch (error) { |
| console.error('載入統計失敗:', error); |
| } |
| } |
| |
| |
| async function loadLicenseStats() { |
| if (!supabaseLicense) return null; |
| |
| try { |
| const { data: licenses, error } = await supabaseLicense |
| .from('licenses') |
| .select('*'); |
| |
| if (error) throw error; |
| |
| const now = new Date(); |
| const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); |
| const sevenDaysLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); |
| |
| const stats = { |
| total: licenses.length, |
| active: licenses.filter(l => l.status === 'active').length, |
| expiring: licenses.filter(l => { |
| if (l.status !== 'active' || !l.expires_at) return false; |
| const expiryDate = new Date(l.expires_at); |
| return expiryDate <= sevenDaysLater && expiryDate >= now; |
| }).length, |
| recentActive: licenses.filter(l => { |
| if (!l.last_used_at) return false; |
| return new Date(l.last_used_at) >= thirtyDaysAgo; |
| }).length |
| }; |
| |
| return stats; |
| |
| } catch (error) { |
| console.error('載入授權統計失敗:', error); |
| return null; |
| } |
| } |
| |
| |
| async function loadVersionStats() { |
| if (!supabaseVersion) return null; |
| |
| try { |
| |
| const { data: versions, error: versionError } = await supabaseVersion |
| .from('versions') |
| .select('*') |
| .eq('is_active', true) |
| .order('release_date', { ascending: false }); |
| |
| if (versionError) throw versionError; |
| |
| |
| const { data: downloads, error: downloadError } = await supabaseVersion |
| .from('download_logs') |
| .select('*'); |
| |
| if (downloadError) throw downloadError; |
| |
| const now = new Date(); |
| const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1); |
| |
| const stats = { |
| totalVersions: versions.length, |
| latestVersion: versions[0] || null, |
| totalDownloads: downloads.filter(d => d.action === 'download').length, |
| monthlyDownloads: downloads.filter(d => { |
| return d.action === 'download' && new Date(d.downloaded_at) >= thisMonth; |
| }).length |
| }; |
| |
| return stats; |
| |
| } catch (error) { |
| console.error('載入版本統計失敗:', error); |
| return null; |
| } |
| } |
| |
| |
| function updateStatsDisplay(licenseStats, versionStats) { |
| |
| if (licenseStats) { |
| const totalLicensesEl = document.getElementById('totalLicenses'); |
| const activeLicensesEl = document.getElementById('activeLicenses'); |
| const activeUsersEl = document.getElementById('activeUsers'); |
| const featureActiveLicensesEl = document.getElementById('featureActiveLicenses'); |
| const featureExpiringLicensesEl = document.getElementById('featureExpiringLicenses'); |
| |
| if (totalLicensesEl) totalLicensesEl.textContent = licenseStats.total; |
| if (activeLicensesEl) activeLicensesEl.textContent = `${licenseStats.active} 個有效`; |
| if (activeUsersEl) activeUsersEl.textContent = licenseStats.recentActive; |
| if (featureActiveLicensesEl) featureActiveLicensesEl.textContent = licenseStats.active; |
| if (featureExpiringLicensesEl) featureExpiringLicensesEl.textContent = licenseStats.expiring; |
| } |
| |
| |
| if (versionStats) { |
| const latestVersionEl = document.getElementById('latestVersion'); |
| const versionDateEl = document.getElementById('versionDate'); |
| const totalDownloadsEl = document.getElementById('totalDownloads'); |
| const monthlyDownloadsEl = document.getElementById('monthlyDownloads'); |
| const featureTotalVersionsEl = document.getElementById('featureTotalVersions'); |
| const featureMonthlyDownloadsEl = document.getElementById('featureMonthlyDownloads'); |
| |
| if (versionStats.latestVersion) { |
| if (latestVersionEl) latestVersionEl.textContent = versionStats.latestVersion.version; |
| if (versionDateEl) { |
| const releaseDate = new Date(versionStats.latestVersion.release_date); |
| versionDateEl.textContent = releaseDate.toLocaleDateString('zh-TW'); |
| } |
| } else { |
| if (latestVersionEl) latestVersionEl.textContent = '無'; |
| if (versionDateEl) versionDateEl.textContent = '--'; |
| } |
| |
| if (totalDownloadsEl) totalDownloadsEl.textContent = versionStats.totalDownloads; |
| if (monthlyDownloadsEl) monthlyDownloadsEl.textContent = `本月 ${versionStats.monthlyDownloads}`; |
| if (featureTotalVersionsEl) featureTotalVersionsEl.textContent = versionStats.totalVersions; |
| if (featureMonthlyDownloadsEl) featureMonthlyDownloadsEl.textContent = versionStats.monthlyDownloads; |
| } |
| } |
| |
| |
| async function logout() { |
| if (window.authManager) { |
| await window.authManager.handleLogout(); |
| } |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| if (window.authManager) { |
| await window.authManager.initProtectedPage('dashboard', initDashboard); |
| } else { |
| |
| window.addEventListener('supabaseReady', async () => { |
| if (window.authManager) { |
| await window.authManager.initProtectedPage('dashboard', initDashboard); |
| } |
| }); |
| } |
| }); |
| </script> |
| </body> |
| </html> |