diff --git a/frontend/src/components/layout/AdminLayout.css b/frontend/src/components/layout/AdminLayout.css index 6c98818..b44bf1e 100644 --- a/frontend/src/components/layout/AdminLayout.css +++ b/frontend/src/components/layout/AdminLayout.css @@ -1,911 +1,897 @@ -/* ===== Admin Layout - Light Theme V5 ===== */ - -:root { - --admin-sidebar-width: 240px; - --sidebar-collapsed: 64px; -} - -/* ========== Layout Container ========== */ -.admin-layout { - display: flex; - height: 100vh; - overflow: hidden; - background: #F5F7FC; - color: #1A1F36; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif; -} - -/* ========== Admin Sidebar ========== */ -.admin-sidebar { - width: var(--admin-sidebar-width); - min-width: var(--admin-sidebar-width); - background: #FFFFFF; - border-right: 1px solid rgba(0, 0, 0, 0.08); - display: flex; - flex-direction: column; - overflow: hidden; - z-index: 100; -} - -/* Sidebar Header */ -.admin-sidebar-header { - display: flex; - flex-direction: column; - padding: 14px 16px 10px; - border-bottom: 1px solid rgba(0, 0, 0, 0.06); - flex-shrink: 0; -} - -.admin-logo { - font-size: 18px; - font-weight: 800; - color: #1A1F36; - margin: 0; - letter-spacing: -0.02em; -} - -.admin-subtitle { - font-size: 11px; - color: #8892B0; - margin-top: 2px; -} - -/* Frontend link button */ -.admin-frontend-link { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 8px 16px; - margin: 8px 12px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - color: #6C63FF; - background: rgba(108, 99, 255, 0.08); - border: 1px solid rgba(108, 99, 255, 0.15); - cursor: pointer; - transition: all 0.2s; - width: calc(100% - 24px); -} - -.admin-frontend-link:hover { - background: rgba(108, 99, 255, 0.15); - border-color: rgba(108, 99, 255, 0.3); - color: #5B5FED; -} - -/* Sidebar Nav */ -.sidebar-nav { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding: 4px 8px; -} - -.sidebar-nav::-webkit-scrollbar { - width: 4px; -} - -.sidebar-nav::-webkit-scrollbar-track { - background: transparent; -} - -.sidebar-nav::-webkit-scrollbar-thumb { - background: rgba(108, 99, 255, 0.15); - border-radius: 2px; -} - -/* Nav Group */ -.nav-group { - margin-bottom: 0; -} - -.nav-group-header { - display: flex; - align-items: center; - gap: 4px; - padding: 7px 10px; - cursor: pointer; - border-radius: 6px; - transition: background 0.15s ease; - user-select: none; -} - -.nav-group-header:hover { - background: rgba(0, 0, 0, 0.04); -} - -.nav-group-header.expanded { - background: rgba(0, 0, 0, 0.02); -} - -.nav-group-chevron-wrap { - display: flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - flex-shrink: 0; -} - -.nav-group-chevron { - color: #8892B0; - transition: transform 0.2s ease; - display: flex; - align-items: center; - justify-content: center; -} - -.nav-group-chevron.chevron-expanded { - transform: rotate(0deg); -} - -.nav-group-header:not(.expanded) .nav-group-chevron { - transform: rotate(-90deg); -} - -.nav-group-title { - font-size: 11px; - font-weight: 700; - color: #5A6480; - text-transform: uppercase; - letter-spacing: 0.08em; - flex: 1; -} - -.nav-group-active .nav-group-title { - color: #6C63FF; -} - -/* Nav Group Items */ -.nav-group-items { - display: flex; - flex-direction: column; - gap: 1px; - padding: 2px 0 4px 0; - overflow: hidden; -} - -/* Nav Item */ -.nav-item { - display: flex; - align-items: center; - gap: 8px; - padding: 7px 10px 7px 30px; - border-radius: 6px; - color: #5A6480; - text-decoration: none; - font-size: 13px; - font-weight: 500; - transition: all 0.15s ease; - white-space: nowrap; - position: relative; -} - -.nav-item:hover { - background: rgba(108, 99, 255, 0.06); - color: #1A1F36; -} - -.nav-item-active { - background: rgba(108, 99, 255, 0.10); - color: #6C63FF; - font-weight: 600; -} - -.nav-item-active::before { - content: ''; - position: absolute; - left: 14px; - top: 50%; - transform: translateY(-50%); - width: 3px; - height: 16px; - border-radius: 2px; - background: #6C63FF; -} - -.nav-item-icon { - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - opacity: 0.7; -} - -.nav-item-active .nav-item-icon { - opacity: 1; -} - -.nav-item-label { - overflow: hidden; - text-overflow: ellipsis; -} - -/* Sidebar Footer */ -.admin-sidebar-footer { - padding: 8px 12px; - border-top: 1px solid rgba(0, 0, 0, 0.06); - flex-shrink: 0; -} - -.admin-user-info { - display: flex; - align-items: center; - gap: 10px; -} - -.admin-user-avatar { - width: 32px; - height: 32px; - border-radius: 50%; - background: linear-gradient(135deg, #6C63FF, #A78BFA); - color: #fff; - display: flex; - align-items: center; - justify-content: center; - font-size: 13px; - font-weight: 700; - flex-shrink: 0; -} - -.admin-user-detail { - flex: 1; - min-width: 0; -} - -.admin-user-name { - font-size: 13px; - font-weight: 600; - color: #1A1F36; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.admin-logout-btn { - font-size: 11px; - color: #8892B0; - background: none; - border: none; - cursor: pointer; - padding: 0; - margin-top: 2px; -} - -.admin-logout-btn:hover { - color: #EF4444; -} - -/* ========== Main Content ========== */ -.admin-content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding: 24px 32px; - background: #F5F7FC; -} - -.admin-content::-webkit-scrollbar { - width: 6px; -} - -.admin-content::-webkit-scrollbar-track { - background: transparent; -} - -.admin-content::-webkit-scrollbar-thumb { - background: rgba(108, 99, 255, 0.15); - border-radius: 3px; -} - -/* ========== Shared Admin Page Components ========== */ - -/* Admin page title */ -.admin-page-title { - font-size: 20px; - font-weight: 700; - color: #1A1F36; - margin-bottom: 20px; -} - -/* Stats grid */ -.admin-stats { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 16px; - margin-bottom: 24px; -} - -/* Stat card */ -.admin-stat-card { - background: #FFFFFF; - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 12px; - padding: 20px; - transition: all 0.2s ease; - box-shadow: 0 1px 3px rgba(0, 0,0,0.04); -} - -.admin-stat-card:hover { - border-color: rgba(108, 99, 255, 0.2); - box-shadow: 0 4px 16px rgba(0,0,0,0.06); - transform: translateY(-1px); -} - -.admin-stat-label { - font-size: 12px; - color: #8892B0; - margin-bottom: 4px; - font-weight: 500; -} - -.admin-stat-value { - font-size: 24px; - font-weight: 700; - color: #1A1F36; - line-height: 1.3; -} - -/* Page section card */ -.admin-section { - background: #FFFFFF; - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 12px; - padding: 24px; - margin-bottom: 20px; - box-shadow: 0 1px 3px rgba(0,0,0,0.04); -} - -.admin-section-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 20px; - padding-bottom: 16px; - border-bottom: 1px solid rgba(0, 0, 0, 0.06); -} - -.admin-section-header h3 { - font-size: 16px; - font-weight: 600; - color: #1A1F36; - margin: 0; -} - -/* Admin card (generic) */ -.admin-card { - background: #FFFFFF; - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 12px; - box-shadow: 0 1px 3px rgba(0,0,0,0.04); - overflow: hidden; -} - -.admin-card-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px 20px; - border-bottom: 1px solid rgba(0, 0, 0, 0.06); -} - -.admin-card-title { - font-size: 15px; - font-weight: 600; - color: #1A1F36; - display: flex; - align-items: center; - gap: 6px; -} - -/* Tab bar */ -.admin-tabs { - display: flex; - gap: 4px; - padding: 4px; - background: #F0F2F8; - border-radius: 10px; - margin-bottom: 20px; -} - -.admin-tab { - padding: 8px 16px; - border-radius: 8px; - font-size: 13px; - font-weight: 500; - color: #5A6480; - cursor: pointer; - transition: all 0.15s ease; - border: none; - background: transparent; -} - -.admin-tab:hover { - color: #6C63FF; - background: rgba(108, 99, 255, 0.06); -} - -.admin-tab.active { - background: #FFFFFF; - color: #6C63FF; - box-shadow: 0 1px 3px rgba(0,0,0,0.08); -} - -/* Data tables */ -.admin-table { - width: 100%; - border-collapse: collapse; -} - -.admin-table th { - padding: 12px 16px; - font-size: 12px; - font-weight: 600; - color: #5A6480; - text-transform: uppercase; - letter-spacing: 0.05em; - text-align: left; - border-bottom: 1px solid rgba(0, 0, 0, 0.08); - background: #F8F9FD; -} - -.admin-table td { - padding: 12px 16px; - font-size: 13px; - color: #1A1F36; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); -} - -.admin-table tr:hover td { - background: rgba(108, 99, 255, 0.03); -} - -/* Action buttons */ -.admin-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 16px; - border-radius: 8px; - font-size: 13px; - font-weight: 500; - border: none; - cursor: pointer; - transition: all 0.15s ease; - white-space: nowrap; -} - -.admin-btn-primary { - background: #6C63FF; - color: #fff; -} - -.admin-btn-primary:hover { - background: #5B5FED; - box-shadow: 0 2px 8px rgba(108, 99, 255, 0.3); -} - -.admin-btn-secondary { - background: #F0F2F8; - color: #5A6480; - border: 1px solid rgba(0, 0, 0, 0.08); -} - -.admin-btn-secondary:hover { - background: #E8EBF5; - color: #1A1F36; -} - -.admin-btn-danger { - background: rgba(239, 68, 68, 0.08); - color: #EF4444; - border: 1px solid rgba(239, 68, 68, 0.2); -} - -.admin-btn-danger:hover { - background: rgba(239, 68, 68, 0.15); -} - -.admin-btn-success { - background: rgba(16, 185, 129, 0.08); - color: #10B981; - border: 1px solid rgba(16, 185, 129, 0.2); -} - -.admin-btn-success:hover { - background: rgba(16, 185, 129, 0.15); -} - -.admin-btn-sm { - padding: 4px 10px; - font-size: 12px; - border-radius: 6px; -} - -/* Search + Filter bar */ -.admin-toolbar { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 20px; - flex-wrap: wrap; -} - -.admin-search { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 14px; - background: #FFFFFF; - border: 1px solid rgba(0, 0, 0, 0.10); - border-radius: 8px; - color: #5A6480; - flex: 1; - min-width: 200px; - max-width: 360px; - transition: border-color 0.15s ease; -} - -.admin-search:focus-within { - border-color: rgba(108, 99, 255, 0.4); - box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.08); -} - -.admin-search input { - flex: 1; - border: none; - background: none; - color: #1A1F36; - font-size: 13px; - outline: none; -} - -.admin-search input::placeholder { - color: #8892B0; -} - -/* Badge / Tag */ -.admin-badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 10px; - font-size: 11px; - font-weight: 600; -} - -.admin-badge-success { - background: rgba(16, 185, 129, 0.10); - color: #10B981; -} - -.admin-badge-warning { - background: rgba(245, 158, 11, 0.10); - color: #F59E0B; -} - -.admin-badge-danger { - background: rgba(239, 68, 68, 0.10); - color: #EF4444; -} - -.admin-badge-info { - background: rgba(59, 130, 246, 0.10); - color: #3B82F6; -} - -.admin-badge-neutral { - background: #F0F2F8; - color: #5A6480; -} - -/* Empty state */ -.admin-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 60px 20px; - color: #8892B0; -} - -.admin-empty svg { - margin-bottom: 16px; - opacity: 0.3; -} - -.admin-empty p { - font-size: 14px; - margin: 0; -} - -/* Modal / Dialog */ -.admin-modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.3); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.admin-modal { - background: #FFFFFF; - border: 1px solid rgba(0, 0, 0, 0.10); - border-radius: 16px; - padding: 24px; - min-width: 420px; - max-width: 640px; - max-height: 80vh; - overflow-y: auto; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); -} - -.admin-modal h3 { - font-size: 18px; - font-weight: 600; - color: #1A1F36; - margin: 0 0 20px 0; -} - -.admin-modal-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 20px; - padding-bottom: 16px; - border-bottom: 1px solid rgba(0, 0, 0, 0.06); -} - -.admin-modal-header h3 { - margin: 0 !important; -} - -.admin-modal-close { - background: none; - border: none; - color: #8892B0; - cursor: pointer; - padding: 4px; - border-radius: 6px; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s; -} - -.admin-modal-close:hover { - color: #1A1F36; - background: rgba(0, 0, 0, 0.06); -} - -.admin-modal-body { - margin-bottom: 20px; -} - -.admin-modal-footer { - display: flex; - gap: 8px; - justify-content: flex-end; - padding-top: 16px; - border-top: 1px solid rgba(0, 0, 0, 0.06); -} - -/* Form elements */ -.admin-form-group { - margin-bottom: 16px; -} - -.admin-form-group label { - display: block; - font-size: 13px; - font-weight: 500; - color: #1A1F36; - margin-bottom: 6px; -} - -.admin-form-group input, -.admin-form-group select, -.admin-form-group textarea { - width: 100%; - padding: 10px 14px; - background: #FFFFFF; - border: 1px solid rgba(0, 0, 0, 0.10); - border-radius: 8px; - color: #1A1F36; - font-size: 13px; - transition: border-color 0.15s ease, box-shadow 0.15s ease; - outline: none; - resize: vertical; -} - -.admin-form-group input:focus, -.admin-form-group select:focus, -.admin-form-group textarea:focus { - border-color: rgba(108, 99, 255, 0.4); - box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.08); -} - -.admin-form-group select { - cursor: pointer; -} - -.admin-form-group select option { - background: #FFFFFF; - color: #1A1F36; -} - -/* Toggle switch */ -.admin-toggle { - position: relative; - display: inline-block; - width: 44px; - height: 24px; -} - -.admin-toggle input { - opacity: 0; - width: 0; - height: 0; -} - -.admin-toggle .toggle-slider { - position: absolute; - cursor: pointer; - inset: 0; - background: #D1D5DB; - border-radius: 12px; - transition: 0.2s; -} - -.admin-toggle .toggle-slider::before { - content: ''; - position: absolute; - height: 18px; - width: 18px; - left: 3px; - bottom: 3px; - background: #fff; - border-radius: 50%; - transition: 0.2s; - box-shadow: 0 1px 3px rgba(0,0,0,0.2); -} - -.admin-toggle input:checked + .toggle-slider { - background: #6C63FF; -} - -.admin-toggle input:checked + .toggle-slider::before { - transform: translateX(20px); -} - -/* Loading spinner */ -.admin-spinner { - width: 24px; - height: 24px; - border: 2px solid rgba(108, 99, 255, 0.15); - border-top-color: #6C63FF; - border-radius: 50%; - animation: admin-spin 0.8s linear infinite; -} - -@keyframes admin-spin { - to { transform: rotate(360deg); } -} - -/* Pagination */ -.admin-pagination { - display: flex; - align-items: center; - justify-content: center; - gap: 4px; - margin-top: 20px; -} - -.admin-pagination button { - width: 32px; - height: 32px; - border-radius: 6px; - border: 1px solid rgba(0, 0, 0, 0.10); - background: #FFFFFF; - color: #5A6480; - font-size: 13px; - cursor: pointer; - transition: all 0.15s ease; -} - -.admin-pagination button:hover { - background: #F0F2F8; - color: #1A1F36; -} - -.admin-pagination button.active { - background: #6C63FF; - border-color: #6C63FF; - color: #FFFFFF; -} - -/* Toast notifications */ -.admin-toast { - position: fixed; - top: 24px; - right: 24px; - padding: 12px 20px; - border-radius: 10px; - font-size: 13px; - font-weight: 500; - z-index: 2000; - animation: toastSlideIn 0.3s ease; - backdrop-filter: blur(12px); - box-shadow: 0 4px 16px rgba(0,0,0,0.12); -} - -.admin-toast-success { - background: #FFFFFF; - border: 1px solid rgba(16, 185, 129, 0.3); - color: #10B981; -} - -.admin-toast-error { - background: #FFFFFF; - border: 1px solid rgba(239, 68, 68, 0.3); - color: #EF4444; -} - -.admin-toast-info { - background: #FFFFFF; - border: 1px solid rgba(108, 99, 255, 0.3); - color: #6C63FF; -} - -@keyframes toastSlideIn { - from { opacity: 0; transform: translateX(20px); } - to { opacity: 1; transform: translateX(0); } -} - -/* Spinner */ -.spinner { - width: 32px; - height: 32px; - border: 3px solid rgba(108, 99, 255, 0.15); - border-top-color: #6C63FF; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* ========== Responsive ========== */ -@media (max-width: 768px) { - .admin-sidebar { - position: fixed; - left: -100%; - top: 0; - bottom: 0; - transition: left 0.3s ease; - } - - .admin-sidebar.mobile-open { - left: 0; - } - - .admin-content { - padding: 16px; - } - - .admin-stats { - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - } - - .admin-modal { - min-width: auto; - width: 90vw; - max-width: 90vw; - } -} +/* ===== Admin Layout - Light Theme V5 ===== */ + +:root { + --admin-sidebar-width: 220px; + --sidebar-collapsed: 64px; +} + +/* ========== Layout Container ========== */ +.admin-layout { + display: flex; + height: 100vh; + overflow: hidden; + background: #F5F7FC; + color: #1A1F36; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif; +} + +/* ========== Admin Sidebar ========== */ +.admin-sidebar { + width: var(--admin-sidebar-width); + min-width: var(--admin-sidebar-width); + background: #FFFFFF; + border-right: 1px solid rgba(0, 0, 0, 0.08); + display: flex; + flex-direction: column; + overflow: hidden; + z-index: 100; +} + +/* Sidebar Header */ +.admin-sidebar-header { + display: flex; + flex-direction: column; + padding: 12px 16px 8px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + flex-shrink: 0; +} + +.admin-logo { + font-size: 18px; + font-weight: 800; + color: #1A1F36; + margin: 0; + letter-spacing: -0.02em; +} + +.admin-subtitle { + font-size: 11px; + color: #8892B0; + margin-top: 2px; +} + +/* Frontend link button */ +.admin-frontend-link { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + margin: 8px 12px; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + color: #6C63FF; + background: rgba(108, 99, 255, 0.08); + border: 1px solid rgba(108, 99, 255, 0.15); + cursor: pointer; + transition: all 0.2s; + width: calc(100% - 24px); +} + +.admin-frontend-link:hover { + background: rgba(108, 99, 255, 0.15); + border-color: rgba(108, 99, 255, 0.3); + color: #5B5FED; +} + +/* Sidebar Nav */ +.sidebar-nav { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 2px 6px; +} + +.sidebar-nav::-webkit-scrollbar { + width: 4px; +} + +.sidebar-nav::-webkit-scrollbar-track { + background: transparent; +} + +.sidebar-nav::-webkit-scrollbar-thumb { + background: rgba(108, 99, 255, 0.15); + border-radius: 2px; +} + +/* Nav Group */ +.nav-group { + margin-bottom: 2px; +} + +.nav-group-header { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 10px; + cursor: pointer; + border-radius: 6px; + transition: background 0.15s ease; + user-select: none; +} + +.nav-group-header:hover { + background: rgba(0, 0, 0, 0.04); +} + +.nav-group-header.expanded { + background: rgba(0, 0, 0, 0.02); +} + +.nav-group-chevron-wrap { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.nav-group-chevron { + color: #8892B0; + transition: transform 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.nav-group-chevron.chevron-expanded { + transform: rotate(0deg); +} + +.nav-group-header:not(.expanded) .nav-group-chevron { + transform: rotate(-90deg); +} + +.nav-group-title { + font-size: 11px; + font-weight: 700; + color: #5A6480; + text-transform: uppercase; + letter-spacing: 0.08em; + flex: 1; +} + +.nav-group-active .nav-group-title { + color: #6C63FF; +} + +/* Nav Group Items */ +.nav-group-items { + display: flex; + flex-direction: column; + gap: 1px; + padding: 1px 0 3px 0; + overflow: hidden; +} + +/* Nav Item */ +.nav-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px 6px 28px; + border-radius: 6px; + color: #5A6480; + text-decoration: none; + font-size: 13px; + font-weight: 500; + transition: all 0.15s ease; + white-space: nowrap; + position: relative; +} + +.nav-item:hover { + background: rgba(108, 99, 255, 0.06); + color: #1A1F36; +} + +.nav-item-active { + background: rgba(108, 99, 255, 0.10); + color: #6C63FF; + font-weight: 600; +} + +.nav-item-active::before { + content: ''; + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 16px; + border-radius: 2px; + background: #6C63FF; +} + +.nav-item-icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + opacity: 0.7; +} + +.nav-item-active .nav-item-icon { + opacity: 1; +} + +.nav-item-label { + overflow: hidden; + text-overflow: ellipsis; +} + +/* Sidebar Footer */ +.admin-sidebar-footer { + padding: 8px 12px; + border-top: 1px solid rgba(0, 0, 0, 0.06); + flex-shrink: 0; +} + +.admin-user-info { + display: flex; + align-items: center; + gap: 10px; +} + +.admin-user-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: linear-gradient(135deg, #6C63FF, #A78BFA); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 700; + flex-shrink: 0; +} + +.admin-user-detail { + flex: 1; + min-width: 0; +} + +.admin-user-name { + font-size: 13px; + font-weight: 600; + color: #1A1F36; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.admin-logout-btn { + font-size: 11px; + color: #8892B0; + background: none; + border: none; + cursor: pointer; + padding: 0; + margin-top: 2px; +} + +.admin-logout-btn:hover { + color: #EF4444; +} + +/* ========== Main Content ========== */ +.admin-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 24px 32px; + background: #F5F7FC; +} + +.admin-content::-webkit-scrollbar { + width: 6px; +} + +.admin-content::-webkit-scrollbar-track { + background: transparent; +} + +.admin-content::-webkit-scrollbar-thumb { + background: rgba(108, 99, 255, 0.15); + border-radius: 3px; +} + +/* ========== Shared Admin Page Components ========== */ + +/* Admin page title */ +.admin-page-title { + font-size: 20px; + font-weight: 700; + color: #1A1F36; + margin-bottom: 20px; +} + +/* Stats grid */ +.admin-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +/* Stat card */ +.admin-stat-card { + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 12px; + padding: 20px; + transition: all 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0,0,0.04); +} + +.admin-stat-card:hover { + border-color: rgba(108, 99, 255, 0.2); + box-shadow: 0 4px 16px rgba(0,0,0,0.06); + transform: translateY(-1px); +} + +.admin-stat-label { + font-size: 12px; + color: #8892B0; + margin-bottom: 4px; + font-weight: 500; +} + +.admin-stat-value { + font-size: 24px; + font-weight: 700; + color: #1A1F36; + line-height: 1.3; +} + +/* Page section card */ +.admin-section { + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 12px; + padding: 24px; + margin-bottom: 20px; + box-shadow: 0 1px 3px rgba(0,0,0,0.04); +} + +.admin-section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); +} + +.admin-section-header h3 { + font-size: 16px; + font-weight: 600; + color: #1A1F36; + margin: 0; +} + +/* Admin card (generic) */ +.admin-card { + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0,0,0,0.04); + overflow: hidden; +} + +.admin-card-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); +} + +.admin-card-title { + font-size: 15px; + font-weight: 600; + color: #1A1F36; + display: flex; + align-items: center; + gap: 6px; +} + +/* Tab bar */ +.admin-tabs { + display: flex; + gap: 4px; + padding: 4px; + background: #F0F2F8; + border-radius: 10px; + margin-bottom: 20px; +} + +.admin-tab { + padding: 8px 16px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + color: #5A6480; + cursor: pointer; + transition: all 0.15s ease; + border: none; + background: transparent; +} + +.admin-tab:hover { + color: #6C63FF; + background: rgba(108, 99, 255, 0.06); +} + +.admin-tab.active { + background: #FFFFFF; + color: #6C63FF; + box-shadow: 0 1px 3px rgba(0,0,0,0.08); +} + +/* Data tables */ +.admin-table { + width: 100%; + border-collapse: collapse; +} + +.admin-table th { + padding: 12px 16px; + font-size: 12px; + font-weight: 600; + color: #5A6480; + text-transform: uppercase; + letter-spacing: 0.05em; + text-align: left; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + background: #F8F9FD; +} + +.admin-table td { + padding: 12px 16px; + font-size: 13px; + color: #1A1F36; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.admin-table tr:hover td { + background: rgba(108, 99, 255, 0.03); +} + +/* Action buttons */ +.admin-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + border: none; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; +} + +.admin-btn-primary { + background: #6C63FF; + color: #fff; +} + +.admin-btn-primary:hover { + background: #5B5FED; + box-shadow: 0 2px 8px rgba(108, 99, 255, 0.3); +} + +.admin-btn-secondary { + background: #F0F2F8; + color: #5A6480; + border: 1px solid rgba(0, 0, 0, 0.08); +} + +.admin-btn-secondary:hover { + background: #E8EBF5; + color: #1A1F36; +} + +.admin-btn-danger { + background: rgba(239, 68, 68, 0.08); + color: #EF4444; + border: 1px solid rgba(239, 68, 68, 0.2); +} + +.admin-btn-danger:hover { + background: rgba(239, 68, 68, 0.15); +} + +.admin-btn-success { + background: rgba(16, 185, 129, 0.08); + color: #10B981; + border: 1px solid rgba(16, 185, 129, 0.2); +} + +.admin-btn-success:hover { + background: rgba(16, 185, 129, 0.15); +} + +.admin-btn-sm { + padding: 4px 10px; + font-size: 12px; + border-radius: 6px; +} + +/* Search + Filter bar */ +.admin-toolbar { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.admin-search { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.10); + border-radius: 8px; + color: #5A6480; + flex: 1; + min-width: 200px; + max-width: 360px; + transition: border-color 0.15s ease; +} + +.admin-search:focus-within { + border-color: rgba(108, 99, 255, 0.4); + box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.08); +} + +.admin-search input { + flex: 1; + border: none; + background: none; + color: #1A1F36; + font-size: 13px; + outline: none; +} + +.admin-search input::placeholder { + color: #8892B0; +} + +/* Badge / Tag */ +.admin-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 600; +} + +.admin-badge-success { + background: rgba(16, 185, 129, 0.10); + color: #10B981; +} + +.admin-badge-warning { + background: rgba(245, 158, 11, 0.10); + color: #F59E0B; +} + +.admin-badge-danger { + background: rgba(239, 68, 68, 0.10); + color: #EF4444; +} + +.admin-badge-info { + background: rgba(59, 130, 246, 0.10); + color: #3B82F6; +} + +.admin-badge-neutral { + background: #F0F2F8; + color: #5A6480; +} + +/* Empty state */ +.admin-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + color: #8892B0; +} + +.admin-empty svg { + margin-bottom: 16px; + opacity: 0.3; +} + +.admin-empty p { + font-size: 14px; + margin: 0; +} + +/* Modal / Dialog */ +.admin-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.3); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.admin-modal { + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.10); + border-radius: 16px; + padding: 24px; + min-width: 420px; + max-width: 640px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); +} + +.admin-modal h3 { + font-size: 18px; + font-weight: 600; + color: #1A1F36; + margin: 0 0 20px 0; +} + +.admin-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); +} + +.admin-modal-header h3 { + margin: 0 !important; +} + +.admin-modal-close { + background: none; + border: none; + color: #8892B0; + cursor: pointer; + padding: 4px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.admin-modal-close:hover { + color: #1A1F36; + background: rgba(0, 0, 0, 0.06); +} + +.admin-modal-body { + margin-bottom: 20px; +} + +.admin-modal-footer { + display: flex; + gap: 8px; + justify-content: flex-end; + padding-top: 16px; + border-top: 1px solid rgba(0, 0, 0, 0.06); +} + +/* Form elements */ +.admin-form-group { + margin-bottom: 16px; +} + +.admin-form-group label { + display: block; + font-size: 13px; + font-weight: 500; + color: #1A1F36; + margin-bottom: 6px; +} + +.admin-form-group input, +.admin-form-group select, +.admin-form-group textarea { + width: 100%; + padding: 10px 14px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.10); + border-radius: 8px; + color: #1A1F36; + font-size: 13px; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + outline: none; + resize: vertical; +} + +.admin-form-group input:focus, +.admin-form-group select:focus, +.admin-form-group textarea:focus { + border-color: rgba(108, 99, 255, 0.4); + box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.08); +} + +.admin-form-group select { + cursor: pointer; +} + +.admin-form-group select option { + background: #FFFFFF; + color: #1A1F36; +} + +/* Toggle switch */ +.admin-toggle { + position: relative; + display: inline-block; + width: 44px; + height: 24px; +} + +.admin-toggle input { + opacity: 0; + width: 0; + height: 0; +} + +.admin-toggle .toggle-slider { + position: absolute; + cursor: pointer; + inset: 0; + background: #D1D5DB; + border-radius: 12px; + transition: 0.2s; +} + +.admin-toggle .toggle-slider::before { + content: ''; + position: absolute; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background: #fff; + border-radius: 50%; + transition: 0.2s; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} + +.admin-toggle input:checked + .toggle-slider { + background: #6C63FF; +} + +.admin-toggle input:checked + .toggle-slider::before { + transform: translateX(20px); +} + +/* Loading spinner */ +.admin-spinner { + width: 24px; + height: 24px; + border: 2px solid rgba(108, 99, 255, 0.15); + border-top-color: #6C63FF; + border-radius: 50%; + animation: admin-spin 0.8s linear infinite; +} + +@keyframes admin-spin { + to { transform: rotate(360deg); } +} + +/* Pagination */ +.admin-pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + margin-top: 20px; +} + +.admin-pagination button { + width: 32px; + height: 32px; + border-radius: 6px; + border: 1px solid rgba(0, 0, 0, 0.10); + background: #FFFFFF; + color: #5A6480; + font-size: 13px; + cursor: pointer; + transition: all 0.15s ease; +} + +.admin-pagination button:hover { + background: #F0F2F8; + color: #1A1F36; +} + +.admin-pagination button.active { + background: #6C63FF; + border-color: #6C63FF; + color: #FFFFFF; +} + +/* Toast notifications */ +.admin-toast { + position: fixed; + top: 24px; + right: 24px; + padding: 12px 20px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + z-index: 2000; + animation: toastSlideIn 0.3s ease; + backdrop-filter: blur(12px); + box-shadow: 0 4px 16px rgba(0,0,0,0.12); +} + +.admin-toast-success { + background: #FFFFFF; + border: 1px solid rgba(16, 185, 129, 0.3); + color: #10B981; +} + +.admin-toast-error { + background: #FFFFFF; + border: 1px solid rgba(239, 68, 68, 0.3); + color: #EF4444; +} + +.admin-toast-info { + background: #FFFFFF; + border: 1px solid rgba(108, 99, 255, 0.3); + color: #6C63FF; +} + +@keyframes toastSlideIn { + from { opacity: 0; transform: translateX(20px); } + to { opacity: 1; transform: translateX(0); } +} + +/* Spinner */ +.spinner { + width: 32px; + height: 32px; + border: 3px solid rgba(108, 99, 255, 0.15); + border-top-color: #6C63FF; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ========== Responsive ========== */ +@media (max-width: 1024px) { + .admin-content { padding: 16px 20px; } + .admin-stats { grid-template-columns: repeat(2, 1fr); } + .admin-stats-responsive { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 768px) { + .admin-sidebar { position: fixed; left: -100%; top: 0; bottom: 0; transition: left 0.3s ease; z-index: 1000; } + .admin-sidebar.mobile-open { left: 0; } + .admin-content { padding: 12px; } + .admin-stats { grid-template-columns: 1fr; } + .admin-stats-responsive { grid-template-columns: 1fr; } + .admin-modal { min-width: auto; width: 90vw; max-width: 90vw; } +} diff --git a/frontend/src/pages/admin/Analytics.tsx b/frontend/src/pages/admin/Analytics.tsx index 4846ce1..7b0cdea 100644 --- a/frontend/src/pages/admin/Analytics.tsx +++ b/frontend/src/pages/admin/Analytics.tsx @@ -1,207 +1,207 @@ -import { useState, useEffect, useCallback } from 'react'; -import { adminAPI } from '@/api'; - -interface Overview { - total_users: number; active_users: number; new_users: number; - api_calls: number; avg_response_time: number; error_rate: number; - tokens_used: number; total_cost: number; total_chats: number; total_messages: number; revenue: number; -} -interface TrendItem { date: string; calls?: number; errors?: number; tokens?: number; new_users?: number; cumulative?: number; revenue?: number; consumption?: number; } -interface ModelUsage { model_name: string; call_count: number; total_tokens: number; total_cost: number; avg_tokens: number; } - -const Analytics: React.FC = () => { - const [days, setDays] = useState(7); - const [loading, setLoading] = useState(false); - const [overview, setOverview] = useState(null); - const [apiTrend, setApiTrend] = useState([]); - const [tokenTrend, setTokenTrend] = useState<{ daily: any[]; model_distribution: Record }>({ daily: [], model_distribution: {} }); - const [userTrend, setUserTrend] = useState([]); - const [revenueTrend, setRevenueTrend] = useState([]); - const [modelUsage, setModelUsage] = useState([]); - - const fetchAll = useCallback(async () => { - setLoading(true); - try { - const [ov, api, tok, usr, rev, mu] = await Promise.all([ - adminAPI.getAnalyticsOverview(days), - adminAPI.getAnalyticsApiTrends(days), - adminAPI.getAnalyticsTokenTrends(days), - adminAPI.getAnalyticsUserTrends(days), - adminAPI.getAnalyticsRevenueTrends(days), - adminAPI.getAnalyticsModelUsage(days), - ]); - setOverview((ov as any)?.data || ov); - setApiTrend((api as any)?.data || []); - const td = (tok as any)?.data || { daily: [], model_distribution: {} }; - setTokenTrend(td); - setUserTrend((usr as any)?.data || []); - setRevenueTrend((rev as any)?.data || []); - setModelUsage((mu as any)?.data || []); - } catch (e) { console.error('fetchAnalytics:', e); } - setLoading(false); - }, [days]); - - useEffect(() => { fetchAll(); }, [fetchAll]); - - const renderBar = (data: TrendItem[], key: string, color: string, label: string) => { - if (!data.length) return
暂无数据
; - const values = data.map((d: any) => d[key] || 0); - const max = Math.max(...values, 1); - return ( -
- {data.map((d: any, i) => { - const v = d[key] || 0; - const h = (v / max) * 140; - return ( -
-
{v > 9999 ? `${(v/1000).toFixed(0)}k` : v}
-
-
{d.date?.substring(5)}
-
- ); - })} -
- ); - }; - - const renderLine = (data: TrendItem[], key: string, color: string) => { - if (!data.length) return
暂无数据
; - const values = data.map((d: any) => d[key] || 0); - const max = Math.max(...values, 1); - const w = 100 / (data.length - 1 || 1); - const points = data.map((d: any, i) => `${i * w},${100 - ((d[key] || 0) / max) * 90}`).join(' '); - return ( - - - {data.map((d: any, i) => ( - - ))} - - ); - }; - - const statCards = overview ? [ - { label: '总用户数', value: overview.total_users, color: '#6366f1', icon: 'U' }, - { label: '活跃用户', value: overview.active_users, color: '#10b981', icon: 'A' }, - { label: '新增用户', value: overview.new_users, color: '#3b82f6', icon: '+' }, - { label: 'API调用数', value: overview.api_calls, color: '#f59e0b', icon: 'API' }, - { label: 'Token用量', value: overview.tokens_used, color: '#ec4899', icon: 'T' }, - { label: '总费用(元)', value: overview.total_cost?.toFixed(2), color: '#ef4444', icon: '¥' }, - { label: '对话总数', value: overview.total_chats, color: '#8b5cf6', icon: 'C' }, - { label: '消息总数', value: overview.total_messages, color: '#06b6d4', icon: 'M' }, - ] : []; - - return ( -
-
-

数据分析面板

-
- {[7, 14, 30, 90].map(d => ( - - ))} -
-
- - {loading ?
加载中...
: ( - <> - {/* Stats grid */} -
- {statCards.map((s, i) => ( -
-
{s.icon}
-
-
{typeof s.value === 'number' ? s.value.toLocaleString() : s.value}
-
{s.label}
-
-
- ))} -
- - {/* Charts grid */} -
-
-

API调用趋势 ({days}天)

- {renderBar(apiTrend, 'calls', '#f59e0b', '调用次数')} -
-
-

Token使用趋势 ({days}天)

- {renderLine(tokenTrend.daily, 'total_tokens', '#ec4899')} -
-
- -
-
-

用户增长 ({days}天)

- {renderBar(userTrend, 'new_users', '#3b82f6', '新增用户')} -
-
-

收入趋势 ({days}天)

- {renderBar(revenueTrend, 'revenue', '#10b981', '收入')} -
-
- - {/* Model usage */} -
-

模型使用分析 ({days}天)

- {modelUsage.length > 0 ? ( - - - {['模型', '调用次数', '总Tokens', '平均Tokens', '费用(元)'].map(h => ( - - ))} - - - {modelUsage.map((m, i) => ( - - - - - - - - ))} - -
{h}
{m.model_name}{m.call_count.toLocaleString()}{m.total_tokens.toLocaleString()}{m.avg_tokens.toLocaleString()}¥{m.total_cost.toFixed(4)}
- ) :
暂无模型使用数据
} -
- - {/* Model distribution pie (CSS) */} - {Object.keys(tokenTrend.model_distribution || {}).length > 0 && ( -
-

模型使用分布

-
-
- {(() => { - const entries = Object.entries(tokenTrend.model_distribution); - const total = entries.reduce((s, [, v]) => s + v, 1); - const colors = ['#6366f1', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444']; - return entries.map(([model, val], i) => { - const pct = (val / total) * 100; - return
; - }); - })()} -
-
- {Object.entries(tokenTrend.model_distribution).map(([model, val], i) => { - const colors = ['#6366f1', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444']; - const total = Object.values(tokenTrend.model_distribution).reduce((s, v) => s + v, 1); - return ( -
-
- {model} - {((val / total) * 100).toFixed(1)}% -
- ); - })} -
-
-
- )} - - )} -
- ); -}; - -export default Analytics; +import { useState, useEffect, useCallback } from 'react'; +import { adminAPI } from '@/api'; + +interface Overview { + total_users: number; active_users: number; new_users: number; + api_calls: number; avg_response_time: number; error_rate: number; + tokens_used: number; total_cost: number; total_chats: number; total_messages: number; revenue: number; +} +interface TrendItem { date: string; calls?: number; errors?: number; tokens?: number; new_users?: number; cumulative?: number; revenue?: number; consumption?: number; } +interface ModelUsage { model_name: string; call_count: number; total_tokens: number; total_cost: number; avg_tokens: number; } + +const Analytics: React.FC = () => { + const [days, setDays] = useState(7); + const [loading, setLoading] = useState(false); + const [overview, setOverview] = useState(null); + const [apiTrend, setApiTrend] = useState([]); + const [tokenTrend, setTokenTrend] = useState<{ daily: any[]; model_distribution: Record }>({ daily: [], model_distribution: {} }); + const [userTrend, setUserTrend] = useState([]); + const [revenueTrend, setRevenueTrend] = useState([]); + const [modelUsage, setModelUsage] = useState([]); + + const fetchAll = useCallback(async () => { + setLoading(true); + try { + const [ov, api, tok, usr, rev, mu] = await Promise.all([ + adminAPI.getAnalyticsOverview(days), + adminAPI.getAnalyticsApiTrends(days), + adminAPI.getAnalyticsTokenTrends(days), + adminAPI.getAnalyticsUserTrends(days), + adminAPI.getAnalyticsRevenueTrends(days), + adminAPI.getAnalyticsModelUsage(days), + ]); + setOverview((ov as any)?.data || ov); + setApiTrend((api as any)?.data || []); + const td = (tok as any)?.data || { daily: [], model_distribution: {} }; + setTokenTrend(td); + setUserTrend((usr as any)?.data || []); + setRevenueTrend((rev as any)?.data || []); + setModelUsage((mu as any)?.data || []); + } catch (e) { console.error('fetchAnalytics:', e); } + setLoading(false); + }, [days]); + + useEffect(() => { fetchAll(); }, [fetchAll]); + + const renderBar = (data: TrendItem[], key: string, color: string, label: string) => { + if (!data.length) return
暂无数据
; + const values = data.map((d: any) => d[key] || 0); + const max = Math.max(...values, 1); + return ( +
+ {data.map((d: any, i) => { + const v = d[key] || 0; + const h = (v / max) * 140; + return ( +
+
{v > 9999 ? `${(v/1000).toFixed(0)}k` : v}
+
+
{d.date?.substring(5)}
+
+ ); + })} +
+ ); + }; + + const renderLine = (data: TrendItem[], key: string, color: string) => { + if (!data.length) return
暂无数据
; + const values = data.map((d: any) => d[key] || 0); + const max = Math.max(...values, 1); + const w = 100 / (data.length - 1 || 1); + const points = data.map((d: any, i) => `${i * w},${100 - ((d[key] || 0) / max) * 90}`).join(' '); + return ( + + + {data.map((d: any, i) => ( + + ))} + + ); + }; + + const statCards = overview ? [ + { label: '总用户数', value: overview.total_users, color: '#6C63FF', icon: 'U' }, + { label: '活跃用户', value: overview.active_users, color: '#10b981', icon: 'A' }, + { label: '新增用户', value: overview.new_users, color: '#3b82f6', icon: '+' }, + { label: 'API调用数', value: overview.api_calls, color: '#f59e0b', icon: 'API' }, + { label: 'Token用量', value: overview.tokens_used, color: '#ec4899', icon: 'T' }, + { label: '总费用(元)', value: overview.total_cost?.toFixed(2), color: '#ef4444', icon: '¥' }, + { label: '对话总数', value: overview.total_chats, color: '#8b5cf6', icon: 'C' }, + { label: '消息总数', value: overview.total_messages, color: '#06b6d4', icon: 'M' }, + ] : []; + + return ( +
+
+

数据分析面板

+
+ {[7, 14, 30, 90].map(d => ( + + ))} +
+
+ + {loading ?
加载中...
: ( + <> + {/* Stats grid */} +
+ {statCards.map((s, i) => ( +
+
{s.icon}
+
+
{typeof s.value === 'number' ? s.value.toLocaleString() : s.value}
+
{s.label}
+
+
+ ))} +
+ + {/* Charts grid */} +
+
+

API调用趋势 ({days}天)

+ {renderBar(apiTrend, 'calls', '#f59e0b', '调用次数')} +
+
+

Token使用趋势 ({days}天)

+ {renderLine(tokenTrend.daily, 'total_tokens', '#ec4899')} +
+
+ +
+
+

用户增长 ({days}天)

+ {renderBar(userTrend, 'new_users', '#3b82f6', '新增用户')} +
+
+

收入趋势 ({days}天)

+ {renderBar(revenueTrend, 'revenue', '#10b981', '收入')} +
+
+ + {/* Model usage */} +
+

模型使用分析 ({days}天)

+ {modelUsage.length > 0 ? ( + + + {['模型', '调用次数', '总Tokens', '平均Tokens', '费用(元)'].map(h => ( + + ))} + + + {modelUsage.map((m, i) => ( + + + + + + + + ))} + +
{h}
{m.model_name}{m.call_count.toLocaleString()}{m.total_tokens.toLocaleString()}{m.avg_tokens.toLocaleString()}¥{m.total_cost.toFixed(4)}
+ ) :
暂无模型使用数据
} +
+ + {/* Model distribution pie (CSS) */} + {Object.keys(tokenTrend.model_distribution || {}).length > 0 && ( +
+

模型使用分布

+
+
+ {(() => { + const entries = Object.entries(tokenTrend.model_distribution); + const total = entries.reduce((s, [, v]) => s + v, 1); + const colors = ['#6C63FF', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444']; + return entries.map(([model, val], i) => { + const pct = (val / total) * 100; + return
; + }); + })()} +
+
+ {Object.entries(tokenTrend.model_distribution).map(([model, val], i) => { + const colors = ['#6C63FF', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444']; + const total = Object.values(tokenTrend.model_distribution).reduce((s, v) => s + v, 1); + return ( +
+
+ {model} + {((val / total) * 100).toFixed(1)}% +
+ ); + })} +
+
+
+ )} + + )} +
+ ); +}; + +export default Analytics; diff --git a/frontend/src/pages/admin/Dashboard.tsx b/frontend/src/pages/admin/Dashboard.tsx index e6c255e..3aaeb90 100644 --- a/frontend/src/pages/admin/Dashboard.tsx +++ b/frontend/src/pages/admin/Dashboard.tsx @@ -1,385 +1,385 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { adminAPI } from '@/api'; - -const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 20 }) => ( - - {children} - -); - -interface DashboardStats { - total_users: number; - active_users: number; - total_models: number; - enabled_models: number; - total_conversations: number; - active_conversations: number; - total_knowledge_bases: number; - total_documents: number; - today_orders: number; - today_tokens: number; - week_tokens: number; - total_tokens: number; - today_conversations: number; - server_time: string; -} - -interface DailyStat { - date: string; - tokens: number; -} - -const quickEntries = [ - { path: '/admin/models', label: '大模型库', desc: '管理AI模型与供应商', icon: }, - { path: '/admin/settings', label: '网站设置', desc: '品牌、邮件、短信配置', icon: }, - { path: '/admin/payment', label: '支付配置', desc: '微信、支付宝渠道配置', icon: }, - { path: '/admin/services', label: '增值服务', desc: '服务管理与定制订单', icon: }, - { path: '/admin/membership', label: '会员体系', desc: '订阅计划与自动化规则', icon: }, - { path: '/admin/market', label: '应用市场', desc: '应用审核与优惠券管理', icon: }, - { path: '/admin/logs', label: '日志风控', desc: '操作/调用/付费日志与风控', icon: }, -]; - -// SVG Bar Chart Component -const BarChart: React.FC<{ data: DailyStat[] }> = ({ data }) => { - if (!data || data.length === 0) return
暂无数据
; - - const maxVal = Math.max(...data.map(d => d.tokens), 1); - const w = 600, h = 200, padLeft = 55, padRight = 10, padTop = 10, padBottom = 25; - const barAreaW = w - padLeft - padRight; - const barAreaH = h - padTop - padBottom; - const barGap = 2; - const barW = Math.max(2, (barAreaW / data.length) - barGap); - - // Y axis ticks - const yTicks = [0, Math.round(maxVal * 0.25), Math.round(maxVal * 0.5), Math.round(maxVal * 0.75), maxVal]; - - const formatK = (n: number) => n >= 10000 ? (n / 1000).toFixed(0) + 'k' : String(n); - const formatDate = (d: string) => { - const parts = d.split('-'); - return parts.length >= 2 ? `${parts[1]}/${parts[2]}` : d; - }; - - return ( - - {/* Grid lines */} - {yTicks.map((tick, i) => { - const y = padTop + barAreaH - (tick / maxVal) * barAreaH; - return ( - - - {formatK(tick)} - - ); - })} - {/* Bars */} - {data.map((d, i) => { - const barH = (d.tokens / maxVal) * barAreaH; - const x = padLeft + i * (barAreaW / data.length); - const y = padTop + barAreaH - barH; - return ( - - - {`${d.date}: ${d.tokens.toLocaleString()} tokens`} - - {/* X label - every 7th bar */} - {i % Math.max(1, Math.floor(data.length / 8)) === 0 && ( - {formatDate(d.date)} - )} - - ); - })} - - ); -}; - -// Mini Donut for stat cards -const MiniDonut: React.FC<{ value: number; max: number; color: string }> = ({ value, max, color }) => { - const pct = max > 0 ? Math.min(value / max, 1) : 0; - const r = 16, c = 20, circumference = 2 * Math.PI * r; - const dash = circumference * pct; - return ( - - - - - {Math.round(pct * 100)}% - - - ); -}; - -const Dashboard: React.FC = () => { - const navigate = useNavigate(); - const [stats, setStats] = useState({ - total_users: 0, active_users: 0, total_models: 0, enabled_models: 0, - total_conversations: 0, active_conversations: 0, total_knowledge_bases: 0, total_documents: 0, - today_orders: 0, today_tokens: 0, week_tokens: 0, total_tokens: 0, - today_conversations: 0, server_time: '', - }); - const [dailyStats, setDailyStats] = useState([]); - const [operationLogs, setOperationLogs] = useState([]); - const [auditLogs, setAuditLogs] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const fetchData = async () => { - setLoading(true); - try { - const [statsRes, dailyRes, logsRes, auditRes] = await Promise.all([ - adminAPI.getStats() as any, - (adminAPI as any).getDailyStats?.(30) || Promise.resolve({ data: [] }), - adminAPI.getRecentLogs() as any, - adminAPI.getRecentAudit() as any, - ]); - // Stats: backend returns raw dict (no {code,data} wrapper) — use .data or the response itself - const statsData = statsRes?.data || statsRes; - if (statsData && typeof statsData === 'object') setStats(statsData); - - // Daily stats: backend returns {code, data: {days, data: [...]}} — access nested .data.data - const dailyData = dailyRes?.data?.data || (Array.isArray(dailyRes?.data) ? dailyRes.data : []) || []; - setDailyStats(Array.isArray(dailyData) ? dailyData : []); - - // Recent logs: backend returns raw list (no wrapper) — use the response directly if it's an array - const logsData = Array.isArray(logsRes) ? logsRes : (Array.isArray(logsRes?.data) ? logsRes.data : []); - setOperationLogs(logsData); - - // Recent audit: same as logs - const auditData = Array.isArray(auditRes) ? auditRes : (Array.isArray(auditRes?.data) ? auditRes.data : []); - setAuditLogs(auditData); - } catch (err) { - console.error('Dashboard数据加载失败:', err); - } finally { - setLoading(false); - } - }; - fetchData(); - }, []); - - const formatDate = (d: string) => { - if (!d) return '-'; - const dt = new Date(d); - if (isNaN(dt.getTime())) return d.slice(0, 19) || '-'; - return dt.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); - }; - - const formatTokens = (n: number): string => { - if (n >= 100000000) return (n / 100000000).toFixed(2) + '亿'; - if (n >= 10000) return (n / 10000).toFixed(1) + '万'; - return n.toLocaleString('en-US'); - }; - - const statCardDefs = [ - { label: '总用户', value: stats.total_users, sub: `活跃 ${stats.active_users}`, color: '#6C63FF', bg: 'rgba(108,99,255,0.12)', subBg: 'rgba(108,99,255,0.08)', - icon: }, - { label: 'AI模型', value: stats.total_models, sub: `启用 ${stats.enabled_models}`, color: '#219B5E', bg: 'rgba(33,155,94,0.12)', subBg: 'rgba(33,155,94,0.08)', - icon: }, - { label: '对话数', value: stats.total_conversations, sub: `活跃 ${stats.active_conversations}`, color: '#E8922C', bg: 'rgba(232,146,44,0.12)', subBg: 'rgba(232,146,44,0.08)', - icon: }, - { label: '知识库', value: stats.total_knowledge_bases, sub: `文档 ${stats.total_documents}`, color: '#3B82F6', bg: 'rgba(59,130,246,0.12)', subBg: 'rgba(59,130,246,0.08)', - icon: }, - { label: '今日对话', value: stats.today_conversations, sub: `订单 ${stats.today_orders}`, color: '#8B5CF6', bg: 'rgba(139,92,246,0.12)', subBg: 'rgba(139,92,246,0.08)', - icon: }, - { label: '今日Token', value: stats.today_tokens, sub: `本周 ${formatTokens(stats.week_tokens)}`, color: '#EC4899', bg: 'rgba(236,72,153,0.12)', subBg: 'rgba(236,72,153,0.08)', format: true, - icon: }, - ]; - - if (loading) { - return ( -
-
- 加载仪表盘数据... -
- ); - } - - return ( -
-

平台驾驶舱

- - {/* Stats Cards Row */} -
- {statCardDefs.map((s, i) => ( -
-
- {s.icon} -
-
-
{s.label}
-
- {s.format ? formatTokens(s.value as number) : (s.value as number).toLocaleString()} -
-
{s.sub}
-
-
- ))} -
- - {/* Charts Row */} -
- {/* Token Trend Chart */} -
-
-

- - {' '}Token消耗趋势(近30天) -

-
-
- -
-
- - {/* System Health */} -
-
-

- - {' '}平台概览 -

-
-
- {[ - { label: '累计Token消耗', value: formatTokens(stats.total_tokens), color: '#6C63FF', pct: 100 }, - { label: '模型利用率', value: `${stats.enabled_models}/${stats.total_models}`, color: '#219B5E', pct: stats.total_models > 0 ? stats.enabled_models / stats.total_models : 0 }, - { label: '用户活跃率', value: `${stats.active_users}/${stats.total_users}`, color: '#E8922C', pct: stats.total_users > 0 ? stats.active_users / stats.total_users : 0 }, - { label: '对话活跃率', value: `${stats.active_conversations}/${stats.total_conversations}`, color: '#3B82F6', pct: stats.total_conversations > 0 ? stats.active_conversations / stats.total_conversations : 0 }, - ].map((item, i) => ( -
-
- - {item.label} -
- {item.value} -
- ))} -
- 服务器时间: {stats.server_time ? new Date(stats.server_time).toLocaleString('zh-CN') : '-'} -
-
-
-
- - {/* Quick Actions */} -
-

快捷入口

-
- {quickEntries.map((entry) => ( -
navigate(entry.path)} - style={{ - background: '#FFFFFF', borderRadius: 'var(--radius-lg)', border: '1px solid rgba(0,0,0,0.08)', - padding: 'var(--space-md) var(--space-lg)', cursor: 'pointer', transition: 'all 0.2s', - display: 'flex', alignItems: 'center', gap: 'var(--space-md)', - boxShadow: '0 1px 3px rgba(0,0,0,0.04)', - }} - onMouseEnter={(e) => { - e.currentTarget.style.borderColor = '#6C63FF'; - e.currentTarget.style.boxShadow = '0 2px 8px rgba(108,99,255,0.15)'; - e.currentTarget.style.transform = 'translateY(-1px)'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.borderColor = 'var(--color-border)'; - e.currentTarget.style.boxShadow = 'none'; - e.currentTarget.style.transform = 'none'; - }} - > -
{entry.icon}
-
-
{entry.label}
-
{entry.desc}
-
-
- ))} -
-
- - {/* Recent Logs */} -
-
-

最近操作日志

- 最近10条 -
-
- - - - - - - - - - - - - {operationLogs.length > 0 ? operationLogs.map(log => ( - - - - - - - - - )) : ( - - )} - -
用户操作目标详情IP时间
{log.username}{log.action}{log.target}{log.detail}{log.ip}{formatDate(log.created_at)}
暂无操作日志
-
-
- - {/* Recent Audit Logs */} -
-
-

最近审计日志

- 最近10条 -
-
- - - - - - - - - - - - {auditLogs.length > 0 ? auditLogs.map(log => ( - - - - - - - - )) : ( - - )} - -
操作人操作类型目标类型详情时间
{log.admin_name}{log.action}{log.target_type}{log.detail}{formatDate(log.created_at)}
暂无审计日志
-
-
-
- ); -}; - -export default Dashboard; +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { adminAPI } from '@/api'; + +const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 20 }) => ( + + {children} + +); + +interface DashboardStats { + total_users: number; + active_users: number; + total_models: number; + enabled_models: number; + total_conversations: number; + active_conversations: number; + total_knowledge_bases: number; + total_documents: number; + today_orders: number; + today_tokens: number; + week_tokens: number; + total_tokens: number; + today_conversations: number; + server_time: string; +} + +interface DailyStat { + date: string; + tokens: number; +} + +const quickEntries = [ + { path: '/admin/models', label: '大模型库', desc: '管理AI模型与供应商', icon: }, + { path: '/admin/settings', label: '网站设置', desc: '品牌、邮件、短信配置', icon: }, + { path: '/admin/payment', label: '支付配置', desc: '微信、支付宝渠道配置', icon: }, + { path: '/admin/services', label: '增值服务', desc: '服务管理与定制订单', icon: }, + { path: '/admin/membership', label: '会员体系', desc: '订阅计划与自动化规则', icon: }, + { path: '/admin/market', label: '应用市场', desc: '应用审核与优惠券管理', icon: }, + { path: '/admin/logs', label: '日志风控', desc: '操作/调用/付费日志与风控', icon: }, +]; + +// SVG Bar Chart Component +const BarChart: React.FC<{ data: DailyStat[] }> = ({ data }) => { + if (!data || data.length === 0) return
暂无数据
; + + const maxVal = Math.max(...data.map(d => d.tokens), 1); + const w = 600, h = 200, padLeft = 55, padRight = 10, padTop = 10, padBottom = 25; + const barAreaW = w - padLeft - padRight; + const barAreaH = h - padTop - padBottom; + const barGap = 2; + const barW = Math.max(2, (barAreaW / data.length) - barGap); + + // Y axis ticks + const yTicks = [0, Math.round(maxVal * 0.25), Math.round(maxVal * 0.5), Math.round(maxVal * 0.75), maxVal]; + + const formatK = (n: number) => n >= 10000 ? (n / 1000).toFixed(0) + 'k' : String(n); + const formatDate = (d: string) => { + const parts = d.split('-'); + return parts.length >= 2 ? `${parts[1]}/${parts[2]}` : d; + }; + + return ( + + {/* Grid lines */} + {yTicks.map((tick, i) => { + const y = padTop + barAreaH - (tick / maxVal) * barAreaH; + return ( + + + {formatK(tick)} + + ); + })} + {/* Bars */} + {data.map((d, i) => { + const barH = (d.tokens / maxVal) * barAreaH; + const x = padLeft + i * (barAreaW / data.length); + const y = padTop + barAreaH - barH; + return ( + + + {`${d.date}: ${d.tokens.toLocaleString()} tokens`} + + {/* X label - every 7th bar */} + {i % Math.max(1, Math.floor(data.length / 8)) === 0 && ( + {formatDate(d.date)} + )} + + ); + })} + + ); +}; + +// Mini Donut for stat cards +const MiniDonut: React.FC<{ value: number; max: number; color: string }> = ({ value, max, color }) => { + const pct = max > 0 ? Math.min(value / max, 1) : 0; + const r = 16, c = 20, circumference = 2 * Math.PI * r; + const dash = circumference * pct; + return ( + + + + + {Math.round(pct * 100)}% + + + ); +}; + +const Dashboard: React.FC = () => { + const navigate = useNavigate(); + const [stats, setStats] = useState({ + total_users: 0, active_users: 0, total_models: 0, enabled_models: 0, + total_conversations: 0, active_conversations: 0, total_knowledge_bases: 0, total_documents: 0, + today_orders: 0, today_tokens: 0, week_tokens: 0, total_tokens: 0, + today_conversations: 0, server_time: '', + }); + const [dailyStats, setDailyStats] = useState([]); + const [operationLogs, setOperationLogs] = useState([]); + const [auditLogs, setAuditLogs] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + setLoading(true); + try { + const [statsRes, dailyRes, logsRes, auditRes] = await Promise.all([ + adminAPI.getStats() as any, + (adminAPI as any).getDailyStats?.(30) || Promise.resolve({ data: [] }), + adminAPI.getRecentLogs() as any, + adminAPI.getRecentAudit() as any, + ]); + // Stats: backend returns raw dict (no {code,data} wrapper) — use .data or the response itself + const statsData = statsRes?.data || statsRes; + if (statsData && typeof statsData === 'object') setStats(statsData); + + // Daily stats: backend returns {code, data: {days, data: [...]}} — access nested .data.data + const dailyData = dailyRes?.data?.data || (Array.isArray(dailyRes?.data) ? dailyRes.data : []) || []; + setDailyStats(Array.isArray(dailyData) ? dailyData : []); + + // Recent logs: backend returns raw list (no wrapper) — use the response directly if it's an array + const logsData = Array.isArray(logsRes) ? logsRes : (Array.isArray(logsRes?.data) ? logsRes.data : []); + setOperationLogs(logsData); + + // Recent audit: same as logs + const auditData = Array.isArray(auditRes) ? auditRes : (Array.isArray(auditRes?.data) ? auditRes.data : []); + setAuditLogs(auditData); + } catch (err) { + console.error('Dashboard数据加载失败:', err); + } finally { + setLoading(false); + } + }; + fetchData(); + }, []); + + const formatDate = (d: string) => { + if (!d) return '-'; + const dt = new Date(d); + if (isNaN(dt.getTime())) return d.slice(0, 19) || '-'; + return dt.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); + }; + + const formatTokens = (n: number): string => { + if (n >= 100000000) return (n / 100000000).toFixed(2) + '亿'; + if (n >= 10000) return (n / 10000).toFixed(1) + '万'; + return n.toLocaleString('en-US'); + }; + + const statCardDefs = [ + { label: '总用户', value: stats.total_users, sub: `活跃 ${stats.active_users}`, color: '#6C63FF', bg: 'rgba(108,99,255,0.12)', subBg: 'rgba(108,99,255,0.08)', + icon: }, + { label: 'AI模型', value: stats.total_models, sub: `启用 ${stats.enabled_models}`, color: '#219B5E', bg: 'rgba(33,155,94,0.12)', subBg: 'rgba(33,155,94,0.08)', + icon: }, + { label: '对话数', value: stats.total_conversations, sub: `活跃 ${stats.active_conversations}`, color: '#E8922C', bg: 'rgba(232,146,44,0.12)', subBg: 'rgba(232,146,44,0.08)', + icon: }, + { label: '知识库', value: stats.total_knowledge_bases, sub: `文档 ${stats.total_documents}`, color: '#3B82F6', bg: 'rgba(59,130,246,0.12)', subBg: 'rgba(59,130,246,0.08)', + icon: }, + { label: '今日对话', value: stats.today_conversations, sub: `订单 ${stats.today_orders}`, color: '#8B5CF6', bg: 'rgba(139,92,246,0.12)', subBg: 'rgba(139,92,246,0.08)', + icon: }, + { label: '今日Token', value: stats.today_tokens, sub: `本周 ${formatTokens(stats.week_tokens)}`, color: '#EC4899', bg: 'rgba(236,72,153,0.12)', subBg: 'rgba(236,72,153,0.08)', format: true, + icon: }, + ]; + + if (loading) { + return ( +
+
+ 加载仪表盘数据... +
+ ); + } + + return ( +
+

平台驾驶舱

+ + {/* Stats Cards Row */} +
+ {statCardDefs.map((s, i) => ( +
+
+ {s.icon} +
+
+
{s.label}
+
+ {s.format ? formatTokens(s.value as number) : (s.value as number).toLocaleString()} +
+
{s.sub}
+
+
+ ))} +
+ + {/* Charts Row */} +
+ {/* Token Trend Chart */} +
+
+

+ + {' '}Token消耗趋势(近30天) +

+
+
+ +
+
+ + {/* System Health */} +
+
+

+ + {' '}平台概览 +

+
+
+ {[ + { label: '累计Token消耗', value: formatTokens(stats.total_tokens), color: '#6C63FF', pct: 100 }, + { label: '模型利用率', value: `${stats.enabled_models}/${stats.total_models}`, color: '#219B5E', pct: stats.total_models > 0 ? stats.enabled_models / stats.total_models : 0 }, + { label: '用户活跃率', value: `${stats.active_users}/${stats.total_users}`, color: '#E8922C', pct: stats.total_users > 0 ? stats.active_users / stats.total_users : 0 }, + { label: '对话活跃率', value: `${stats.active_conversations}/${stats.total_conversations}`, color: '#3B82F6', pct: stats.total_conversations > 0 ? stats.active_conversations / stats.total_conversations : 0 }, + ].map((item, i) => ( +
+
+ + {item.label} +
+ {item.value} +
+ ))} +
+ 服务器时间: {stats.server_time ? new Date(stats.server_time).toLocaleString('zh-CN') : '-'} +
+
+
+
+ + {/* Quick Actions */} +
+

快捷入口

+
+ {quickEntries.map((entry) => ( +
navigate(entry.path)} + style={{ + background: '#FFFFFF', borderRadius: 'var(--radius-lg)', border: '1px solid rgba(0,0,0,0.08)', + padding: 'var(--space-md) var(--space-lg)', cursor: 'pointer', transition: 'all 0.2s', + display: 'flex', alignItems: 'center', gap: 'var(--space-md)', + boxShadow: '0 1px 3px rgba(0,0,0,0.04)', + }} + onMouseEnter={(e) => { + e.currentTarget.style.borderColor = '#6C63FF'; + e.currentTarget.style.boxShadow = '0 2px 8px rgba(108,99,255,0.15)'; + e.currentTarget.style.transform = 'translateY(-1px)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = 'var(--color-border)'; + e.currentTarget.style.boxShadow = 'none'; + e.currentTarget.style.transform = 'none'; + }} + > +
{entry.icon}
+
+
{entry.label}
+
{entry.desc}
+
+
+ ))} +
+
+ + {/* Recent Logs */} +
+
+

最近操作日志

+ 最近10条 +
+
+ + + + + + + + + + + + + {operationLogs.length > 0 ? operationLogs.map(log => ( + + + + + + + + + )) : ( + + )} + +
用户操作目标详情IP时间
{log.username}{log.action}{log.target}{log.detail}{log.ip}{formatDate(log.created_at)}
暂无操作日志
+
+
+ + {/* Recent Audit Logs */} +
+
+

最近审计日志

+ 最近10条 +
+
+ + + + + + + + + + + + {auditLogs.length > 0 ? auditLogs.map(log => ( + + + + + + + + )) : ( + + )} + +
操作人操作类型目标类型详情时间
{log.admin_name}{log.action}{log.target_type}{log.detail}{formatDate(log.created_at)}
暂无审计日志
+
+
+
+ ); +}; + +export default Dashboard; diff --git a/frontend/src/pages/front/LandingPage.tsx b/frontend/src/pages/front/LandingPage.tsx index f21465f..80daa7b 100644 --- a/frontend/src/pages/front/LandingPage.tsx +++ b/frontend/src/pages/front/LandingPage.tsx @@ -1,451 +1,486 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { mockAdBanners, mockApps } from '@/mock/data'; -import type { AdBanner, MarketApp } from '@/types'; -import './LandingPage.css'; - -const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => ( - - {children} - -); - -const typeLabels: Record = { skill: 'Skill', agent: 'Agent', mcp: 'MCP' }; -const typeColors: Record = { skill: '#10B981', agent: '#5B5FED', mcp: '#F59E0B' }; - -const LandingPage: React.FC = () => { - const navigate = useNavigate(); - const [currentSlide, setCurrentSlide] = useState(0); - const [isPaused, setIsPaused] = useState(false); - const banners = mockAdBanners.filter(b => b.status === 'active'); - const publishedApps = mockApps.filter(a => a.status === 'published'); - const [appTypeFilter, setAppTypeFilter] = useState<'' | 'skill' | 'agent' | 'mcp'>(''); - const [toastMsg, setToastMsg] = useState(''); - const [scrolled, setScrolled] = useState(false); - - const filteredApps = appTypeFilter - ? publishedApps.filter(a => a.type === appTypeFilter) - : publishedApps; - - useEffect(() => { - const handleScroll = () => setScrolled(window.scrollY > 60); - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); - }, []); - - const showToast = (msg: string) => { - setToastMsg(msg); - setTimeout(() => setToastMsg(''), 2000); - }; - - const handleAppClick = (appName: string) => { - showToast(`"${appName}" 需登录后使用,即将跳转登录页`); - setTimeout(() => navigate('/login'), 800); - }; - - const nextSlide = useCallback(() => { - if (banners.length === 0) return; - setCurrentSlide(prev => (prev + 1) % banners.length); - }, [banners.length]); - - useEffect(() => { - if (banners.length <= 1 || isPaused) return; - const timer = setInterval(nextSlide, 4000); - return () => clearInterval(timer); - }, [banners.length, isPaused, nextSlide]); - - const goToSlide = (idx: number) => setCurrentSlide(idx); - - const getGradient = (app: MarketApp): string => { - const gradients: Record = { - a1: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - a2: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', - a3: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', - a4: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', - a5: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', - a6: 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)', - a8: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)', - }; - return gradients[app.id] || 'linear-gradient(135deg, #5B5FED 0%, #A78BFA 100%)'; - }; - - // Platform Architecture - layers from bottom to top - const archLayers = [ - { - label: '应用层', - color: '#6C63FF', - items: ['AI 对话工作台', '知识库问答', '自动化流程', 'IM Bot 接入', 'API 集成'], - }, - { - label: '能力层', - color: '#10B981', - items: ['技能市场 (Skill)', '智能体 (Agent)', 'MCP 协议', 'RAG 检索增强', '工具链编排'], - }, - { - label: '模型层', - color: '#F59E0B', - items: ['DeepSeek', 'GPT-4o', 'Claude', 'Qwen', '多模态模型'], - }, - { - label: '基础设施', - color: '#3B82F6', - items: ['向量数据库', '对象存储', '任务调度', 'API 网关', '监控告警'], - }, - ]; - - // Capability groups - const capabilities = [ - { - icon: , - title: 'AI 对话', - desc: '接入 50+ 主流大模型,支持流式输出、多轮对话、Function Calling,你的专属 AI 助手', - }, - { - icon: , - title: '知识库', - desc: '上传文档、网页、代码构建个人知识库,RAG 增强检索让 AI 回答更精准可靠', - }, - { - icon: , - title: '技能市场', - desc: '100+ 预制 AI 技能,一键安装即用。支持 Skill、Agent、MCP 三种形态,持续扩展', - }, - { - icon: , - title: 'IM Bot', - desc: '将 AI 能力接入飞书、微信、钉钉,在聊天中调用 AI,无缝融入日常工作流', - }, - { - icon: , - title: 'AI 自进化', - desc: '从每次对话中持续学习,自动沉淀知识图谱,你的 AI 越用越懂你', - }, - { - icon: , - title: 'Token 驾驶舱', - desc: '实时监控调用量、Token 消耗与成本,精细化管理你的 AI 使用预算', - }, - ]; - - // Scenarios - individual practitioner focused - const scenarios = [ - { icon: '📚', title: '学习助手', desc: '构建课程知识库,AI 辅助理解复杂概念、整理笔记、解答疑问' }, - { icon: '💼', title: '工作效率', desc: '自动化重复任务、智能撰写文档邮件、快速检索信息,效率翻倍' }, - { icon: '🤖', title: 'AI Agent 开发', desc: '用 Skill + Agent + MCP 搭建专属 AI 助手,从原型到生产一站完成' }, - { icon: '📊', title: '数据分析', desc: '自然语言查询数据库,自动生成可视化报告,让数据说话更简单' }, - { icon: '✍️', title: '内容创作', desc: 'AI 辅助写文章、做翻译、润色文案,让创作更有灵感' }, - { icon: '🔬', title: '技术探索', desc: '接入最新模型做实验,对比不同 LLM 效果,探索 AI 的前沿能力' }, - ]; - - return ( -
- {/* Toast */} - {toastMsg &&
{toastMsg}
} - - {/* Header */} -
- -
- - {/* Hero Section — What is 七虎AI */} -
-
-
-
-
-
- - AI 个人工作台 · 为创造者而生 -
-

- 每一个 AI 创造者 - 的超级工作台 -

-

- 七虎AI 是一站式 AI 能力集成平台,让你在一个地方完成对话、知识库、技能编排、模型切换、自动化搭建
- 无需拼接多个工具,从想法到落地,一个平台就够了 -

-
- - -
- {/* Stats */} -
-
- 100+ - AI 技能 -
-
-
- 50+ - 大模型接入 -
-
-
- 6 - IM 平台 -
-
-
- 99.9% - 可用性 -
-
-
-
- - {/* What is 七虎AI — Platform Introduction */} -
-
- 平台介绍 -

七虎AI 是什么?

-

一个面向 AI 爱好者和从业者的个人 AI 能力集成平台

-
-
-
-
🎯
-

能力聚合

-

将大模型对话、知识库、技能编排、自动化、IM Bot 等能力整合在一个平台,告别工具碎片化

-
-
-
⚡
-

开箱即用

-

预设 100+ AI 技能模板,50+ 大模型即配即用,无需从零搭建,注册即可上手

-
-
-
🔌
-

开放扩展

-

支持 MCP 协议、Skill 插件、Agent 编排、API 开放,你可以自由扩展和定制

-
-
-
🚀
-

持续进化

-

AI 自进化引擎从使用中持续学习,知识库自动更新,模型热切换,你的工作台越来越强

-
-
-
- - {/* Platform Architecture */} -
-
- 平台架构 -

四层架构 · 层层赋能

-

从基础设施到上层应用,理解 七虎AI 的技术全貌

-
-
- {archLayers.map((layer, idx) => ( -
-
-
- {layer.label} -
-
-
- {layer.items.map((item, i) => ( - {item} - ))} -
-
- ))} - {/* Downward arrows between layers */} -
-
- - {/* Banner Carousel */} - {banners.length > 0 && ( -
-
setIsPaused(true)} - onMouseLeave={() => setIsPaused(false)} - > -
- {banners.map((banner, idx) => ( -
-
- AI 精选 -

{banner.title}

-

{banner.title}

-
-
-
-
-
- ))} -
-
- {banners.map((_, idx) => ( -
-
-
- )} - - {/* Capabilities Section */} -
-
- 能力体系 -

六大核心能力

-

从对话到自动化,覆盖 AI 从业者的全部工作流

-
-
- {capabilities.map((c, idx) => ( -
-
{c.icon}
-

{c.title}

-

{c.desc}

-
- ))} -
-
- - {/* Scenarios Section */} -
-
- 应用场景 -

每个 AI 创作者都用得上

-

无论你是学习者、开发者、还是内容创作者,都能找到属于你的打开方式

-
-
- {scenarios.map((s, idx) => ( -
-
{s.icon}
-

{s.title}

-

{s.desc}

-
- ))} -
-
- - {/* Market Preview */} -
-
- 技能生态 -

丰富的 AI 技能生态

-

Skills、Agent、MCP 工具一键安装,持续扩展你的 AI 能力边界

-
- {(['', 'skill', 'agent', 'mcp'] as const).map(t => ( - - ))} -
-
-
- {filteredApps.slice(0, 6).map(app => ( -
handleAppClick(app.name)}> -
- {app.name.charAt(0)} -
-
-
-

{app.name}

- - {typeLabels[app.type]} - -
-

{app.description}

-
- ⭐ 4.8 - 📥 1.2k - {app.price === 0 ? '免费' : `¥${app.price}`} -
-
-
- ))} -
-
- - {/* CTA */} -
-
-

准备好开始你的 AI 之旅了吗?

-

免费注册,即刻拥有属于你自己的 AI 超级工作台

- -
- - {/* Footer */} - -
- ); -}; - -export default LandingPage; +import React, { useState, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { mockAdBanners, mockApps } from '@/mock/data'; +import type { AdBanner, MarketApp } from '@/types'; +import './LandingPage.css'; + +const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => ( + + {children} + +); + +const typeLabels: Record = { skill: 'Skill', agent: 'Agent', mcp: 'MCP' }; +const typeColors: Record = { skill: '#10B981', agent: '#5B5FED', mcp: '#F59E0B' }; + +const LandingPage: React.FC = () => { + const navigate = useNavigate(); + const [currentSlide, setCurrentSlide] = useState(0); + const [isPaused, setIsPaused] = useState(false); + const banners = mockAdBanners.filter(b => b.status === 'active'); + const publishedApps = mockApps.filter(a => a.status === 'published'); + const [appTypeFilter, setAppTypeFilter] = useState<'' | 'skill' | 'agent' | 'mcp'>(''); + const [skillData, setSkillData] = useState<{skills: any[], agents: any[], mcps: any[]}>({skills: [], agents: [], mcps: []}); + useEffect(() => { + (async () => { + try { + const apiModule = await import('@/api'); + const res = await apiModule.adminAPI.getAdminSkills({page: 1, page_size: 50, status: 'published'}); + const allApps: any[] = (res as any)?.data?.items || (res as any)?.data || []; + const sortFn = (a: any, b: any) => (b.usage_count || b.install_count || 0) - (a.usage_count || a.install_count || 0); + const skills = allApps.filter(a => a.type === 'skill').sort(sortFn).slice(0, 5); + const agents = allApps.filter(a => a.type === 'agent').sort(sortFn).slice(0, 5); + const mcps = allApps.filter(a => a.type === 'mcp').sort(sortFn).slice(0, 5); + if (skills.length + agents.length + mcps.length === 0) { + setSkillData({skills: publishedApps.filter(a => a.type === 'skill').slice(0, 5), agents: publishedApps.filter(a => a.type === 'agent').slice(0, 5), mcps: publishedApps.filter(a => a.type === 'mcp').slice(0, 5)}); + } else { setSkillData({skills, agents, mcps}); } + } catch (e) { + setSkillData({skills: publishedApps.filter(a => a.type === 'skill').slice(0, 5), agents: publishedApps.filter(a => a.type === 'agent').slice(0, 5), mcps: publishedApps.filter(a => a.type === 'mcp').slice(0, 5)}); + } + })(); + }, []); + const [toastMsg, setToastMsg] = useState(''); + const [scrolled, setScrolled] = useState(false); + + const filteredApps = appTypeFilter + ? publishedApps.filter(a => a.type === appTypeFilter) + : publishedApps; + + useEffect(() => { + const handleScroll = () => setScrolled(window.scrollY > 60); + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + const showToast = (msg: string) => { + setToastMsg(msg); + setTimeout(() => setToastMsg(''), 2000); + }; + + const handleAppClick = (appName: string) => { + showToast(`"${appName}" 需登录后使用,即将跳转登录页`); + setTimeout(() => navigate('/login'), 800); + }; + + const nextSlide = useCallback(() => { + if (banners.length === 0) return; + setCurrentSlide(prev => (prev + 1) % banners.length); + }, [banners.length]); + + useEffect(() => { + if (banners.length <= 1 || isPaused) return; + const timer = setInterval(nextSlide, 4000); + return () => clearInterval(timer); + }, [banners.length, isPaused, nextSlide]); + + const goToSlide = (idx: number) => setCurrentSlide(idx); + + const getGradient = (app: MarketApp): string => { + const gradients: Record = { + a1: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + a2: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', + a3: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', + a4: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', + a5: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', + a6: 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)', + a8: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)', + }; + return gradients[app.id] || 'linear-gradient(135deg, #5B5FED 0%, #A78BFA 100%)'; + }; + + // Platform Architecture - layers from bottom to top + const archLayers = [ + { + label: '应用层', + color: '#6C63FF', + items: ['AI 对话工作台', '知识库问答', '自动化流程', 'IM Bot 接入', 'API 集成'], + }, + { + label: '能力层', + color: '#10B981', + items: ['技能市场 (Skill)', '智能体 (Agent)', 'MCP 协议', 'RAG 检索增强', '工具链编排'], + }, + { + label: '模型层', + color: '#F59E0B', + items: ['DeepSeek', 'GPT-4o', 'Claude', 'Qwen', '多模态模型'], + }, + { + label: '基础设施', + color: '#3B82F6', + items: ['向量数据库', '对象存储', '任务调度', 'API 网关', '监控告警'], + }, + ]; + + // Capability groups + const capabilities = [ + { + icon: , + title: 'AI 对话', + desc: '接入 50+ 主流大模型,支持流式输出、多轮对话、Function Calling,你的专属 AI 助手', + }, + { + icon: , + title: '知识库', + desc: '上传文档、网页、代码构建个人知识库,RAG 增强检索让 AI 回答更精准可靠', + }, + { + icon: , + title: '技能市场', + desc: '100+ 预制 AI 技能,一键安装即用。支持 Skill、Agent、MCP 三种形态,持续扩展', + }, + { + icon: , + title: 'IM Bot', + desc: '将 AI 能力接入飞书、微信、钉钉,在聊天中调用 AI,无缝融入日常工作流', + }, + { + icon: , + title: 'AI 自进化', + desc: '从每次对话中持续学习,自动沉淀知识图谱,你的 AI 越用越懂你', + }, + { + icon: , + title: 'Token 驾驶舱', + desc: '实时监控调用量、Token 消耗与成本,精细化管理你的 AI 使用预算', + }, + ]; + + // Scenarios - individual practitioner focused + const scenarios = [ + { icon: '📚', title: '学习助手', desc: '构建课程知识库,AI 辅助理解复杂概念、整理笔记、解答疑问' }, + { icon: '💼', title: '工作效率', desc: '自动化重复任务、智能撰写文档邮件、快速检索信息,效率翻倍' }, + { icon: '🤖', title: 'AI Agent 开发', desc: '用 Skill + Agent + MCP 搭建专属 AI 助手,从原型到生产一站完成' }, + { icon: '📊', title: '数据分析', desc: '自然语言查询数据库,自动生成可视化报告,让数据说话更简单' }, + { icon: '✍️', title: '内容创作', desc: 'AI 辅助写文章、做翻译、润色文案,让创作更有灵感' }, + { icon: '🔬', title: '技术探索', desc: '接入最新模型做实验,对比不同 LLM 效果,探索 AI 的前沿能力' }, + ]; + + return ( +
+ {/* Toast */} + {toastMsg &&
{toastMsg}
} + + {/* Header */} +
+ +
+ + {/* Hero Section — What is 七虎AI */} +
+
+
+
+
+
+ + AI 个人工作台 · 为创造者而生 +
+

+ 每一个 AI 创造者 + 的超级工作台 +

+

+ 七虎AI 是一站式 AI 能力集成平台,让你在一个地方完成对话、知识库、技能编排、模型切换、自动化搭建
+ 无需拼接多个工具,从想法到落地,一个平台就够了 +

+
+ + +
+ {/* Stats */} +
+
+ 100+ + AI 技能 +
+
+
+ 50+ + 大模型接入 +
+
+
+ 6 + IM 平台 +
+
+
+ 99.9% + 可用性 +
+
+
+
+ + {/* What is 七虎AI — Platform Introduction */} +
+
+ 平台介绍 +

七虎AI 是什么?

+

一个面向 AI 爱好者和从业者的个人 AI 能力集成平台

+
+
+
+
🎯
+

能力聚合

+

将大模型对话、知识库、技能编排、自动化、IM Bot 等能力整合在一个平台,告别工具碎片化

+
+
+
⚡
+

开箱即用

+

预设 100+ AI 技能模板,50+ 大模型即配即用,无需从零搭建,注册即可上手

+
+
+
🔌
+

开放扩展

+

支持 MCP 协议、Skill 插件、Agent 编排、API 开放,你可以自由扩展和定制

+
+
+
🚀
+

持续进化

+

AI 自进化引擎从使用中持续学习,知识库自动更新,模型热切换,你的工作台越来越强

+
+
+
+ + {/* Platform Architecture */} +
+
+ 平台架构 +

四层架构 · 层层赋能

+

从基础设施到上层应用,理解 七虎AI 的技术全貌

+
+
+ {archLayers.map((layer, idx) => ( +
+
+
+ {layer.label} +
+
+
+ {layer.items.map((item, i) => ( + {item} + ))} +
+
+ ))} + {/* Downward arrows between layers */} +
+
+ + {/* Banner Carousel */} + {banners.length > 0 && ( +
+
setIsPaused(true)} + onMouseLeave={() => setIsPaused(false)} + > +
+ {banners.map((banner, idx) => ( +
+
+ AI 精选 +

{banner.title}

+

{banner.title}

+
+
+
+
+
+ ))} +
+
+ {banners.map((_, idx) => ( +
+
+
+ )} + + {/* Capabilities Section */} +
+
+ 能力体系 +

六大核心能力

+

从对话到自动化,覆盖 AI 从业者的全部工作流

+
+
+ {capabilities.map((c, idx) => ( +
+
{c.icon}
+

{c.title}

+

{c.desc}

+
+ ))} +
+
+ + {/* Scenarios Section */} +
+
+ 应用场景 +

每个 AI 创作者都用得上

+

无论你是学习者、开发者、还是内容创作者,都能找到属于你的打开方式

+
+
+ {scenarios.map((s, idx) => ( +
+
{s.icon}
+

{s.title}

+

{s.desc}

+
+ ))} +
+
+ + {/* Market Preview */} +
+
+ 技能生态 +

丰富的 AI 技能生态

+

Skills、Agent、MCP 工具一键安装,持续扩展你的 AI 能力边界

+
+ {(['', 'skill', 'agent', 'mcp'] as const).map(t => ( + + ))} +
+
+
+ {(() => { + const hasRealData = skillData.skills.length + skillData.agents.length + skillData.mcps.length > 0; + let displayApps: any[]; + if (appTypeFilter) { + const key = appTypeFilter === 'skill' ? 'skills' : appTypeFilter === 'agent' ? 'agents' : 'mcps'; + displayApps = hasRealData ? skillData[key] : filteredApps.slice(0, 5); + } else { + displayApps = hasRealData ? [...skillData.skills, ...skillData.agents, ...skillData.mcps] : filteredApps.slice(0, 6); + } + return displayApps.map((app: any) => { + const appType = app.type || 'skill'; + const appColor = typeColors[appType] || '#6C63FF'; + const appLabel = typeLabels[appType] || 'Skill'; + const appName = app.name || app.title || '未命名'; + const appDesc = app.description || app.desc || ''; + const appUsage = app.usage_count || app.install_count || 0; + const appPrice = app.price || 0; + return ( +
handleAppClick(appName)}> +
+ {appName.charAt(0)} +
+
+
+

{appName}

+ {appLabel} +
+

{appDesc}

+
+ {appUsage > 0 && 📥 {appUsage >= 1000 ? `${(appUsage/1000).toFixed(1)}k` : appUsage}次} + {appPrice === 0 ? '免费' : `¥${appPrice}`} +
+
+
+ ); + }); + })()} +
+
+ + {/* CTA */} +
+
+

准备好开始你的 AI 之旅了吗?

+

免费注册,即刻拥有属于你自己的 AI 超级工作台

+ +
+ + {/* Footer */} + +
+ ); +}; + +export default LandingPage; diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index d40e372..ddb6a31 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -1,747 +1,769 @@ -/* ===== 七虎AI Design System v2 - Light Theme ===== */ -/* 设计方向:明亮简洁 + 透明玻璃态 */ - -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap'); - -/* CSS Custom Properties - Light Theme */ -:root { - /* Colors - Dark Theme */ - --color-bg-primary: #FFFFFF; - --color-bg-secondary: #F5F7FC; - --color-bg-tertiary: #FFFFFF; - --color-bg-hover: #F0F2F8; - --color-bg-active: #E8EBF5; - - --color-text-primary: #1A1F36; - --color-text-secondary: #5A6480; - --color-text-tertiary: #8892B0; - --color-text-disabled: #C4C9D9; - --color-text-inverse: #FFFFFF; - - --color-accent: #6C63FF; - --color-accent-hover: #8B85FF; - --color-accent-light: rgba(108, 99, 255, 0.15); - --color-accent-subtle: rgba(108, 99, 255, 0.08); - - --color-success: #10B981; - --color-success-light: rgba(16, 185, 129, 0.12); - --color-warning: #F59E0B; - --color-warning-light: rgba(245, 158, 11, 0.12); - --color-error: #EF4444; - --color-error-light: rgba(239, 68, 68, 0.12); - --color-info: #3B82F6; - --color-info-light: rgba(59, 130, 246, 0.12); - - --color-border: rgba(0, 0, 0, 0.10); - --color-border-light: rgba(0, 0, 0, 0.06); - --color-border-hover: rgba(0, 0, 0, 0.16); - - --color-sidebar-bg: #FFFFFF; - --color-sidebar-hover: rgba(108, 99, 255, 0.06); - --color-sidebar-active: rgba(108, 99, 255, 0.12); - - /* Surface/card colors */ - --color-surface: #FFFFFF; - --color-surface-hover: #F5F7FC; - --color-surface-elevated: #FFFFFF; - - /* Shadows - Dark theme */ - --shadow-xs: 0 1px 2px rgba(0,0,0,0.06); - --shadow-sm: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); - --shadow-md: 0 4px 12px rgba(0,0,0,0.10), 0 1px 3px rgba(0,0,0,0.06); - --shadow-lg: 0 12px 32px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.06); - --shadow-xl: 0 20px 48px rgba(0,0,0,0.15), 0 4px 12px rgba(0,0,0,0.08); - - /* Spacing */ - --space-xs: 4px; - --space-sm: 8px; - --space-md: 12px; - --space-lg: 16px; - --space-xl: 20px; - --space-2xl: 24px; - --space-3xl: 32px; - --space-4xl: 40px; - --space-5xl: 48px; - - /* Border Radius */ - --radius-sm: 6px; - --radius-md: 8px; - --radius-lg: 12px; - --radius-xl: 16px; - --radius-2xl: 20px; - --radius-full: 9999px; - - /* Typography */ - --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace; - - --text-xs: 11px; - --text-sm: 13px; - --text-base: 14px; - --text-md: 15px; - --text-lg: 18px; - --text-xl: 22px; - --text-2xl: 28px; - --text-3xl: 36px; - - /* Layout */ - --sidebar-width: 240px; - --sidebar-collapsed: 64px; - --header-height: 56px; - --admin-sidebar-width: 220px; - - /* Transitions */ - --transition-fast: 120ms ease; - --transition-normal: 200ms ease; - --transition-slow: 300ms ease; - - /* Z-index */ - --z-base: 1; - --z-dropdown: 100; - --z-sticky: 200; - --z-overlay: 300; - --z-modal: 400; - --z-toast: 500; -} - -/* Reset & Base */ -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: var(--text-base); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-rendering: optimizeLegibility; -} - -body { - font-family: var(--font-sans); - color: var(--color-text-primary); - background: var(--color-bg-primary); - line-height: 1.6; - overflow: hidden; - height: 100vh; -} - -#root { - height: 100vh; - overflow: hidden; -} - -a { - color: var(--color-accent); - text-decoration: none; -} - -a:hover { - color: var(--color-accent-hover); -} - -button { - font-family: inherit; - cursor: pointer; - border: none; - background: none; -} - -input, textarea, select { - font-family: inherit; - font-size: inherit; -} - -/* Scrollbar - Dark theme */ -::-webkit-scrollbar { - width: 5px; - height: 5px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: var(--color-text-disabled); - border-radius: var(--radius-full); -} - -::-webkit-scrollbar-thumb:hover { - background: var(--color-text-tertiary); -} - -/* Utility Classes */ -.flex { display: flex; } -.flex-col { flex-direction: column; } -.flex-1 { flex: 1; } -.items-center { align-items: center; } -.justify-between { justify-content: space-between; } -.justify-center { justify-content: center; } -.gap-xs { gap: var(--space-xs); } -.gap-sm { gap: var(--space-sm); } -.gap-md { gap: var(--space-md); } -.gap-lg { gap: var(--space-lg); } -.gap-xl { gap: var(--space-xl); } -.text-center { text-align: center; } -.text-right { text-align: right; } -.truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.w-full { width: 100%; } -.h-full { height: 100%; } -.relative { position: relative; } -.absolute { position: absolute; } -.hidden { display: none; } -.pointer { cursor: pointer; } -.select-none { user-select: none; } - -/* Animation utilities */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideUp { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } -} - -@keyframes slideDown { - from { opacity: 0; transform: translateY(-8px); } - to { opacity: 1; transform: translateY(0); } -} - -@keyframes slideLeft { - from { opacity: 0; transform: translateX(8px); } - to { opacity: 1; transform: translateX(0); } -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -@keyframes shimmer { - 0% { background-position: -200% 0; } - 100% { background-position: 200% 0; } -} - -@keyframes glow { - 0%, 100% { box-shadow: 0 0 5px rgba(108, 99, 255, 0.3); } - 50% { box-shadow: 0 0 20px rgba(108, 99, 255, 0.5); } -} - -.animate-fadeIn { animation: fadeIn var(--transition-normal); } -.animate-slideUp { animation: slideUp var(--transition-normal); } -.animate-slideDown { animation: slideDown var(--transition-normal); } -.animate-pulse { animation: pulse 2s ease infinite; } - -/* Skeleton loading */ -.skeleton { - background: linear-gradient(90deg, var(--color-bg-tertiary) 25%, var(--color-bg-hover) 50%, var(--color-bg-tertiary) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s infinite; - border-radius: var(--radius-sm); -} - -/* Spinner */ -.audit-spinner { - width: 24px; - height: 24px; - border: 2px solid rgba(108, 99, 255, 0.2); - border-top-color: var(--color-accent); - border-radius: 50%; - animation: spin 0.6s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* Loading screen */ -.loading-screen { - display: flex; - align-items: center; - justify-content: center; - height: 100vh; - font-size: 18px; - color: var(--color-text-secondary); - background: var(--color-bg-primary); -} - -/* Responsive breakpoints */ -@media (max-width: 768px) { - :root { - --sidebar-width: 0px; - } -} - -@media (min-width: 769px) and (max-width: 1024px) { - :root { - --sidebar-width: 64px; - } -} - -/* === Glass card effect === */ -.glass-card { - background: rgba(255, 255, 255, 0.8); - backdrop-filter: blur(16px); - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: var(--radius-lg); -} - -/* === Gradient text === */ -.gradient-text { - background: linear-gradient(135deg, #6C63FF, #A78BFA, #C084FC); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -/* === Admin stats card grid === */ -.admin-stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: var(--space-lg); - margin-bottom: var(--space-2xl); -} - -.stat-card { - padding: var(--space-xl); - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - transition: all var(--transition-normal); -} - -.stat-card:hover { - border-color: rgba(108, 99, 255, 0.3); - transform: translateY(-2px); - box-shadow: 0 4px 16px rgba(108, 99, 255, 0.08); -} - -.stat-card .stat-label { - font-size: var(--text-xs); - color: var(--color-text-tertiary); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: var(--space-xs); -} - -.stat-card .stat-value { - font-size: var(--text-2xl); - font-weight: 700; - color: var(--color-text-primary); - line-height: 1.2; -} - -.stat-card .stat-sub { - font-size: var(--text-xs); - color: var(--color-text-tertiary); - margin-top: var(--space-xs); -} - -/* === Common button styles === */ -.btn-primary { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 10px 20px; - border-radius: var(--radius-md); - font-size: var(--text-base); - font-weight: 600; - color: white; - background: var(--color-accent); - border: none; - cursor: pointer; - transition: all var(--transition-fast); -} - -.btn-primary:hover { - background: var(--color-accent-hover); - box-shadow: 0 4px 16px rgba(108, 99, 255, 0.3); -} - -.btn-secondary { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 10px 20px; - border-radius: var(--radius-md); - font-size: var(--text-base); - font-weight: 500; - color: var(--color-text-secondary); - background: transparent; - border: 1px solid var(--color-border); - cursor: pointer; - transition: all var(--transition-fast); -} - -.btn-secondary:hover { - color: var(--color-text-primary); - border-color: var(--color-border-hover); - background: var(--color-bg-hover); -} - -.btn-danger { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 10px 20px; - border-radius: var(--radius-md); - font-size: var(--text-base); - font-weight: 500; - color: white; - background: var(--color-error); - border: none; - cursor: pointer; - transition: all var(--transition-fast); -} - -.btn-danger:hover { - background: #DC2626; -} - -.btn-sm { - padding: 6px 12px; - font-size: var(--text-sm); -} - -/* === Form elements === */ -.form-input-dark { - padding: 10px 14px; - border-radius: var(--radius-md); - border: 1px solid var(--color-border); - background: var(--color-bg-tertiary); - color: var(--color-text-primary); - font-size: var(--text-base); - outline: none; - transition: all var(--transition-fast); - width: 100%; -} - -.form-input-dark:focus { - border-color: rgba(108, 99, 255, 0.4); - box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.1); -} - -.form-input-dark::placeholder { - color: var(--color-text-disabled); -} - -.form-select-dark { - padding: 10px 14px; - border-radius: var(--radius-md); - border: 1px solid var(--color-border); - background: var(--color-bg-tertiary); - color: var(--color-text-primary); - font-size: var(--text-base); - outline: none; - cursor: pointer; - transition: all var(--transition-fast); -} - -.form-select-dark:focus { - border-color: rgba(108, 99, 255, 0.4); -} - -/* === Table (dark theme) === */ -.data-table { - width: 100%; - border-collapse: collapse; -} - -.data-table th { - padding: 12px 16px; - text-align: left; - font-size: var(--text-xs); - font-weight: 600; - color: var(--color-text-tertiary); - text-transform: uppercase; - letter-spacing: 0.05em; - border-bottom: 1px solid var(--color-border); -} - -.data-table td { - padding: 12px 16px; - font-size: var(--text-sm); - color: var(--color-text-primary); - border-bottom: 1px solid var(--color-border-light); -} - -.data-table tr:hover td { - background: var(--color-bg-hover); -} - -/* === Badge === */ -.badge { - display: inline-block; - padding: 2px 10px; - border-radius: 100px; - font-size: var(--text-xs); - font-weight: 600; -} - -.badge-success { background: var(--color-success-light); color: var(--color-success); } -.badge-warning { background: var(--color-warning-light); color: var(--color-warning); } -.badge-error { background: var(--color-error-light); color: var(--color-error); } -.badge-info { background: var(--color-info-light); color: var(--color-info); } -.badge-accent { background: var(--color-accent-light); color: var(--color-accent); } - -/* === Modal/Dialog === */ -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: var(--z-modal); -} - -.modal-content { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-xl); - padding: var(--space-2xl); - max-width: 90vw; - max-height: 90vh; - overflow-y: auto; - box-shadow: var(--shadow-xl); -} - -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-xl); -} - -.modal-header h3 { - font-size: var(--text-lg); - font-weight: 600; - color: var(--color-text-primary); -} - -/* === Toast === */ -.toast { - position: fixed; - top: 24px; - left: 50%; - transform: translateX(-50%); - padding: 12px 24px; - border-radius: var(--radius-lg); - background: var(--color-surface-elevated); - border: 1px solid var(--color-border); - color: var(--color-text-primary); - font-size: var(--text-sm); - z-index: var(--z-toast); - animation: slideDown var(--transition-normal); - box-shadow: var(--shadow-lg); -} - -/* ===== Color Scheme Improvements v2 ===== */ -/* Glassmorphism + Text Highlighting + Consistency Fixes */ - -/* Glass card effect */ -.admin-page-card, -.admin-card, -.stat-card, -.dashboard-card { - background: rgba(255, 255, 255, 0.7) !important; - backdrop-filter: blur(12px) !important; - -webkit-backdrop-filter: blur(12px) !important; - border: 1px solid rgba(0, 0, 0, 0.08) !important; -} - -/* Stat number highlight */ -.stat-value, -.stat-number, -.metric-value { - color: var(--color-text-primary) !important; - font-weight: 700 !important; - text-shadow: 0 0 20px rgba(108, 99, 255, 0.15); -} - -/* Section title highlight */ -.section-title, -.page-title, -.card-title, -h1, h2, h3 { - color: var(--color-text-primary) !important; -} - -/* Accent text highlight */ -.text-accent, -.text-primary, -.highlight-text { - color: var(--color-accent) !important; - font-weight: 600; -} - -/* Table header improvements */ -table th, -.table-header { - background: rgba(108, 99, 255, 0.08) !important; - color: var(--color-text-primary) !important; - font-weight: 600 !important; -} - -/* Table row hover */ -table tbody tr:hover, -.table-row:hover { - background: rgba(108, 99, 255, 0.04) !important; -} - -/* Input/Select consistency */ -input, -textarea, -select, -.form-input, -.form-control { - background: #FFFFFF !important; - border: 1px solid rgba(0, 0, 0, 0.10) !important; - color: var(--color-text-primary) !important; - border-radius: var(--radius-md) !important; - transition: border-color 0.2s, box-shadow 0.2s !important; -} - -input:focus, -textarea:focus, -select:focus, -.form-input:focus, -.form-control:focus { - border-color: var(--color-accent) !important; - box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.12) !important; - outline: none !important; -} - -/* Button consistency */ -button, -.btn, -.admin-btn { - border-radius: var(--radius-md) !important; - transition: all 0.2s !important; -} - -/* Primary button */ -.btn-primary, -.admin-btn-primary, -button[type="submit"] { - background: var(--color-accent) !important; - color: white !important; - border: none !important; -} - -.btn-primary:hover, -.admin-btn-primary:hover { - background: var(--color-accent-hover) !important; - box-shadow: 0 4px 12px rgba(108, 99, 255, 0.3) !important; -} - -/* Secondary/ghost button */ -.btn-secondary, -.admin-btn-secondary { - background: rgba(108, 99, 255, 0.06) !important; - color: var(--color-text-secondary) !important; - border: 1px solid rgba(0, 0, 0, 0.08) !important; -} - -.btn-secondary:hover, -.admin-btn-secondary:hover { - background: rgba(108, 99, 255, 0.10) !important; - color: var(--color-text-primary) !important; -} - -/* Badge/tag improvements */ -.badge, -.tag, -.admin-badge { - border-radius: var(--radius-full) !important; - font-weight: 600 !important; - padding: 2px 10px !important; -} - -/* Badge variants */ -.badge-success, -.admin-badge-success { - background: rgba(16, 185, 129, 0.12) !important; - color: #10B981 !important; -} - -.badge-warning, -.admin-badge-warning { - background: rgba(245, 158, 11, 0.12) !important; - color: #F59E0B !important; -} - -.badge-error, -.admin-badge-danger { - background: rgba(239, 68, 68, 0.12) !important; - color: #EF4444 !important; -} - -.badge-info, -.admin-badge-info { - background: rgba(59, 130, 246, 0.12) !important; - color: #3B82F6 !important; -} - -/* Empty state */ -.empty-state, -.admin-empty { - color: var(--color-text-tertiary) !important; - text-align: center; - padding: 48px 24px !important; -} - -/* Scrollbar improvement */ -::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -::-webkit-scrollbar-thumb { - background: rgba(108, 99, 255, 0.2); - border-radius: 3px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(108, 99, 255, 0.4); -} - -/* Link color */ -a { - color: var(--color-accent); - transition: color 0.2s; -} - -a:hover { - color: var(--color-accent-hover); -} - -/* Tab improvements */ -.tab-active, -.tab-item.active { - color: var(--color-accent) !important; - border-bottom-color: var(--color-accent) !important; -} - -/* Modal/dialog overlay */ -.modal-overlay, -.dialog-overlay { - background: rgba(0, 0, 0, 0.3) !important; - backdrop-filter: blur(4px) !important; -} - -/* Card hover effect */ -.admin-card:hover, -.stat-card:hover { - border-color: rgba(108, 99, 255, 0.2) !important; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08) !important; -} +/* ===== 七虎AI Design System v2 - Light Theme ===== */ +/* 设计方向:明亮简洁 + 透明玻璃态 */ + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap'); + +/* CSS Custom Properties - Light Theme */ +:root { + /* Colors - Dark Theme */ + --color-bg-primary: #FFFFFF; + --color-bg-secondary: #F5F7FC; + --color-bg-tertiary: #FFFFFF; + --color-bg-hover: #F0F2F8; + --color-bg-active: #E8EBF5; + + --color-text-primary: #1A1F36; + --color-text-secondary: #5A6480; + --color-text-tertiary: #8892B0; + --color-text-disabled: #C4C9D9; + --color-text-inverse: #FFFFFF; + + --color-accent: #6C63FF; + --color-accent-hover: #8B85FF; + --color-accent-light: rgba(108, 99, 255, 0.15); + --color-accent-subtle: rgba(108, 99, 255, 0.08); + + --color-success: #10B981; + --color-success-light: rgba(16, 185, 129, 0.12); + --color-warning: #F59E0B; + --color-warning-light: rgba(245, 158, 11, 0.12); + --color-error: #EF4444; + --color-error-light: rgba(239, 68, 68, 0.12); + --color-info: #3B82F6; + --color-info-light: rgba(59, 130, 246, 0.12); + + --color-border: rgba(0, 0, 0, 0.10); + --color-border-light: rgba(0, 0, 0, 0.06); + --color-border-hover: rgba(0, 0, 0, 0.16); + + --color-sidebar-bg: #FFFFFF; + --color-sidebar-hover: rgba(108, 99, 255, 0.06); + --color-sidebar-active: rgba(108, 99, 255, 0.12); + + /* Surface/card colors */ + --color-surface: #FFFFFF; + --color-surface-hover: #F5F7FC; + --color-surface-elevated: #FFFFFF; + + /* Shadows - Dark theme */ + --shadow-xs: 0 1px 2px rgba(0,0,0,0.06); + --shadow-sm: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); + --shadow-md: 0 4px 12px rgba(0,0,0,0.10), 0 1px 3px rgba(0,0,0,0.06); + --shadow-lg: 0 12px 32px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.06); + --shadow-xl: 0 20px 48px rgba(0,0,0,0.15), 0 4px 12px rgba(0,0,0,0.08); + + /* Spacing */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 12px; + --space-lg: 16px; + --space-xl: 20px; + --space-2xl: 24px; + --space-3xl: 32px; + --space-4xl: 40px; + --space-5xl: 48px; + + /* Border Radius */ + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-2xl: 20px; + --radius-full: 9999px; + + /* Typography */ + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace; + + --text-xs: 11px; + --text-sm: 13px; + --text-base: 14px; + --text-md: 15px; + --text-lg: 18px; + --text-xl: 22px; + --text-2xl: 28px; + --text-3xl: 36px; + + /* Layout */ + --sidebar-width: 240px; + --sidebar-collapsed: 64px; + --header-height: 56px; + --admin-sidebar-width: 220px; + + /* Transitions */ + --transition-fast: 120ms ease; + --transition-normal: 200ms ease; + --transition-slow: 300ms ease; + + /* Z-index */ + --z-base: 1; + --z-dropdown: 100; + --z-sticky: 200; + --z-overlay: 300; + --z-modal: 400; + --z-toast: 500; +} + +/* Reset & Base */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: var(--text-base); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +body { + font-family: var(--font-sans); + color: var(--color-text-primary); + background: var(--color-bg-primary); + line-height: 1.6; + min-height: 100vh; +} + +#root { + min-height: 100vh; +} + +a { + color: var(--color-accent); + text-decoration: none; +} + +a:hover { + color: var(--color-accent-hover); +} + +button { + font-family: inherit; + cursor: pointer; + border: none; + background: none; +} + +input, textarea, select { + font-family: inherit; + font-size: inherit; +} + +/* Scrollbar - Dark theme */ +::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--color-text-disabled); + border-radius: var(--radius-full); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-text-tertiary); +} + +/* Utility Classes */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.flex-1 { flex: 1; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.justify-center { justify-content: center; } +.gap-xs { gap: var(--space-xs); } +.gap-sm { gap: var(--space-sm); } +.gap-md { gap: var(--space-md); } +.gap-lg { gap: var(--space-lg); } +.gap-xl { gap: var(--space-xl); } +.text-center { text-align: center; } +.text-right { text-align: right; } +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.w-full { width: 100%; } +.h-full { height: 100%; } +.relative { position: relative; } +.absolute { position: absolute; } +.hidden { display: none; } +.pointer { cursor: pointer; } +.select-none { user-select: none; } + +/* Animation utilities */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes slideDown { + from { opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes slideLeft { + from { opacity: 0; transform: translateX(8px); } + to { opacity: 1; transform: translateX(0); } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +@keyframes glow { + 0%, 100% { box-shadow: 0 0 5px rgba(108, 99, 255, 0.3); } + 50% { box-shadow: 0 0 20px rgba(108, 99, 255, 0.5); } +} + +.animate-fadeIn { animation: fadeIn var(--transition-normal); } +.animate-slideUp { animation: slideUp var(--transition-normal); } +.animate-slideDown { animation: slideDown var(--transition-normal); } +.animate-pulse { animation: pulse 2s ease infinite; } + +/* Skeleton loading */ +.skeleton { + background: linear-gradient(90deg, var(--color-bg-tertiary) 25%, var(--color-bg-hover) 50%, var(--color-bg-tertiary) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: var(--radius-sm); +} + +/* Spinner */ +.audit-spinner { + width: 24px; + height: 24px; + border: 2px solid rgba(108, 99, 255, 0.2); + border-top-color: var(--color-accent); + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Loading screen */ +.loading-screen { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + font-size: 18px; + color: var(--color-text-secondary); + background: var(--color-bg-primary); +} + +/* Responsive breakpoints */ +@media (max-width: 768px) { + :root { + --sidebar-width: 0px; + } +} + +@media (min-width: 769px) and (max-width: 1024px) { + :root { + --sidebar-width: 64px; + } +} + +/* === Glass card effect === */ +.glass-card { + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(16px); + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: var(--radius-lg); +} + +/* === Gradient text === */ +.gradient-text { + background: linear-gradient(135deg, #6C63FF, #A78BFA, #C084FC); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* === Admin stats card grid === */ +.admin-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: var(--space-lg); + margin-bottom: var(--space-2xl); +} + +.stat-card { + padding: var(--space-xl); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + transition: all var(--transition-normal); +} + +.stat-card:hover { + border-color: rgba(108, 99, 255, 0.3); + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(108, 99, 255, 0.08); +} + +.stat-card .stat-label { + font-size: var(--text-xs); + color: var(--color-text-tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: var(--space-xs); +} + +.stat-card .stat-value { + font-size: var(--text-2xl); + font-weight: 700; + color: var(--color-text-primary); + line-height: 1.2; +} + +.stat-card .stat-sub { + font-size: var(--text-xs); + color: var(--color-text-tertiary); + margin-top: var(--space-xs); +} + +/* === Common button styles === */ +.btn-primary { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + border-radius: var(--radius-md); + font-size: var(--text-base); + font-weight: 600; + color: white; + background: var(--color-accent); + border: none; + cursor: pointer; + transition: all var(--transition-fast); +} + +.btn-primary:hover { + background: var(--color-accent-hover); + box-shadow: 0 4px 16px rgba(108, 99, 255, 0.3); +} + +.btn-secondary { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + border-radius: var(--radius-md); + font-size: var(--text-base); + font-weight: 500; + color: var(--color-text-secondary); + background: transparent; + border: 1px solid var(--color-border); + cursor: pointer; + transition: all var(--transition-fast); +} + +.btn-secondary:hover { + color: var(--color-text-primary); + border-color: var(--color-border-hover); + background: var(--color-bg-hover); +} + +.btn-danger { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + border-radius: var(--radius-md); + font-size: var(--text-base); + font-weight: 500; + color: white; + background: var(--color-error); + border: none; + cursor: pointer; + transition: all var(--transition-fast); +} + +.btn-danger:hover { + background: #DC2626; +} + +.btn-sm { + padding: 6px 12px; + font-size: var(--text-sm); +} + +/* === Form elements === */ +.form-input-dark { + padding: 10px 14px; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + background: var(--color-bg-tertiary); + color: var(--color-text-primary); + font-size: var(--text-base); + outline: none; + transition: all var(--transition-fast); + width: 100%; +} + +.form-input-dark:focus { + border-color: rgba(108, 99, 255, 0.4); + box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.1); +} + +.form-input-dark::placeholder { + color: var(--color-text-disabled); +} + +.form-select-dark { + padding: 10px 14px; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + background: var(--color-bg-tertiary); + color: var(--color-text-primary); + font-size: var(--text-base); + outline: none; + cursor: pointer; + transition: all var(--transition-fast); +} + +.form-select-dark:focus { + border-color: rgba(108, 99, 255, 0.4); +} + +/* === Table (dark theme) === */ +.data-table { + width: 100%; + border-collapse: collapse; +} + +.data-table th { + padding: 12px 16px; + text-align: left; + font-size: var(--text-xs); + font-weight: 600; + color: var(--color-text-tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--color-border); +} + +.data-table td { + padding: 12px 16px; + font-size: var(--text-sm); + color: var(--color-text-primary); + border-bottom: 1px solid var(--color-border-light); +} + +.data-table tr:hover td { + background: var(--color-bg-hover); +} + +/* === Badge === */ +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 100px; + font-size: var(--text-xs); + font-weight: 600; +} + +.badge-success { background: var(--color-success-light); color: var(--color-success); } +.badge-warning { background: var(--color-warning-light); color: var(--color-warning); } +.badge-error { background: var(--color-error-light); color: var(--color-error); } +.badge-info { background: var(--color-info-light); color: var(--color-info); } +.badge-accent { background: var(--color-accent-light); color: var(--color-accent); } + +/* === Modal/Dialog === */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: var(--z-modal); +} + +.modal-content { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + padding: var(--space-2xl); + max-width: 90vw; + max-height: 90vh; + overflow-y: auto; + box-shadow: var(--shadow-xl); +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--space-xl); +} + +.modal-header h3 { + font-size: var(--text-lg); + font-weight: 600; + color: var(--color-text-primary); +} + +/* === Toast === */ +.toast { + position: fixed; + top: 24px; + left: 50%; + transform: translateX(-50%); + padding: 12px 24px; + border-radius: var(--radius-lg); + background: var(--color-surface-elevated); + border: 1px solid var(--color-border); + color: var(--color-text-primary); + font-size: var(--text-sm); + z-index: var(--z-toast); + animation: slideDown var(--transition-normal); + box-shadow: var(--shadow-lg); +} + +/* ===== Color Scheme Improvements v2 ===== */ +/* Glassmorphism + Text Highlighting + Consistency Fixes */ + +/* Glass card effect */ +.admin-page-card, +.admin-card, +.stat-card, +.dashboard-card { + background: rgba(255, 255, 255, 0.7) !important; + backdrop-filter: blur(12px) !important; + -webkit-backdrop-filter: blur(12px) !important; + border: 1px solid rgba(0, 0, 0, 0.08) !important; +} + +/* Stat number highlight */ +.stat-value, +.stat-number, +.metric-value { + color: var(--color-text-primary) !important; + font-weight: 700 !important; + text-shadow: 0 0 20px rgba(108, 99, 255, 0.15); +} + +/* Section title highlight */ +.section-title, +.page-title, +.card-title, +h1, h2, h3 { + color: var(--color-text-primary) !important; +} + +/* Accent text highlight */ +.text-accent, +.text-primary, +.highlight-text { + color: var(--color-accent) !important; + font-weight: 600; +} + +/* Table header improvements */ +table th, +.table-header { + background: rgba(108, 99, 255, 0.08) !important; + color: var(--color-text-primary) !important; + font-weight: 600 !important; +} + +/* Table row hover */ +table tbody tr:hover, +.table-row:hover { + background: rgba(108, 99, 255, 0.04) !important; +} + +/* Input/Select consistency */ +input, +textarea, +select, +.form-input, +.form-control { + background: #FFFFFF !important; + border: 1px solid rgba(0, 0, 0, 0.10) !important; + color: var(--color-text-primary) !important; + border-radius: var(--radius-md) !important; + transition: border-color 0.2s, box-shadow 0.2s !important; +} + +input:focus, +textarea:focus, +select:focus, +.form-input:focus, +.form-control:focus { + border-color: var(--color-accent) !important; + box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.12) !important; + outline: none !important; +} + +/* Button consistency */ +button, +.btn, +.admin-btn { + border-radius: var(--radius-md) !important; + transition: all 0.2s !important; +} + +/* Primary button */ +.btn-primary, +.admin-btn-primary, +button[type="submit"] { + background: var(--color-accent) !important; + color: white !important; + border: none !important; +} + +.btn-primary:hover, +.admin-btn-primary:hover { + background: var(--color-accent-hover) !important; + box-shadow: 0 4px 12px rgba(108, 99, 255, 0.3) !important; +} + +/* Secondary/ghost button */ +.btn-secondary, +.admin-btn-secondary { + background: rgba(108, 99, 255, 0.06) !important; + color: var(--color-text-secondary) !important; + border: 1px solid rgba(0, 0, 0, 0.08) !important; +} + +.btn-secondary:hover, +.admin-btn-secondary:hover { + background: rgba(108, 99, 255, 0.10) !important; + color: var(--color-text-primary) !important; +} + +/* Badge/tag improvements */ +.badge, +.tag, +.admin-badge { + border-radius: var(--radius-full) !important; + font-weight: 600 !important; + padding: 2px 10px !important; +} + +/* Badge variants */ +.badge-success, +.admin-badge-success { + background: rgba(16, 185, 129, 0.12) !important; + color: #10B981 !important; +} + +.badge-warning, +.admin-badge-warning { + background: rgba(245, 158, 11, 0.12) !important; + color: #F59E0B !important; +} + +.badge-error, +.admin-badge-danger { + background: rgba(239, 68, 68, 0.12) !important; + color: #EF4444 !important; +} + +.badge-info, +.admin-badge-info { + background: rgba(59, 130, 246, 0.12) !important; + color: #3B82F6 !important; +} + +/* Empty state */ +.empty-state, +.admin-empty { + color: var(--color-text-tertiary) !important; + text-align: center; + padding: 48px 24px !important; +} + +/* Scrollbar improvement */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-thumb { + background: rgba(108, 99, 255, 0.2); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(108, 99, 255, 0.4); +} + +/* Link color */ +a { + color: var(--color-accent); + transition: color 0.2s; +} + +a:hover { + color: var(--color-accent-hover); +} + +/* Tab improvements */ +.tab-active, +.tab-item.active { + color: var(--color-accent) !important; + border-bottom-color: var(--color-accent) !important; +} + +/* Modal/dialog overlay */ +.modal-overlay, +.dialog-overlay { + background: rgba(0, 0, 0, 0.3) !important; + backdrop-filter: blur(4px) !important; +} + +/* Card hover effect */ +.admin-card:hover, +.stat-card:hover { + border-color: rgba(108, 99, 255, 0.2) !important; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08) !important; +} + +/* ===== Responsive Grid Patterns ===== */ +.responsive-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; } +@media (max-width: 768px) { .responsive-grid-2 { grid-template-columns: 1fr; } } +.responsive-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } +@media (max-width: 1024px) { .responsive-grid-3 { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 768px) { .responsive-grid-3 { grid-template-columns: 1fr; } } +.responsive-grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; } +@media (max-width: 1200px) { .responsive-grid-4 { grid-template-columns: repeat(3, 1fr); } } +@media (max-width: 900px) { .responsive-grid-4 { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 600px) { .responsive-grid-4 { grid-template-columns: 1fr; } } +.responsive-grid-auto { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; } +@media (max-width: 600px) { .responsive-grid-auto { grid-template-columns: 1fr; } } +.responsive-flex { display: flex; flex-wrap: wrap; gap: 16px; } +.responsive-flex > * { flex: 1 1 280px; min-width: 0; } +.pagination-bar { display: flex; align-items: center; justify-content: center; gap: 4px; margin-top: 20px; padding: 8px 0; } +.pagination-bar button { min-width: 32px; height: 32px; border-radius: 6px; border: 1px solid rgba(0,0,0,0.10); background: #FFFFFF; color: #5A6480; font-size: 13px; cursor: pointer; transition: all 0.15s ease; padding: 0 8px; } +.pagination-bar button:hover { background: #F0F2F8; color: #1A1F36; } +.pagination-bar button.active { background: #6C63FF; border-color: #6C63FF; color: #FFFFFF; } +.pagination-bar button:disabled { opacity: 0.4; cursor: not-allowed; } +.pagination-bar .page-info { font-size: 12px; color: #8892B0; margin: 0 8px; } +.admin-stats-responsive { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; } +@media (max-width: 900px) { .admin-stats-responsive { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 600px) { .admin-stats-responsive { grid-template-columns: 1fr; } }