diff --git a/video-gen-admin/index.html b/video-gen-admin/index.html index 6cb27e43..3dbba5d3 100644 --- a/video-gen-admin/index.html +++ b/video-gen-admin/index.html @@ -4,7 +4,10 @@ - VideoGen.AI 管理后台 + + + + 后台管理
diff --git a/video-gen-admin/src/index.css b/video-gen-admin/src/index.css new file mode 100644 index 00000000..b9aecffd --- /dev/null +++ b/video-gen-admin/src/index.css @@ -0,0 +1,27 @@ +@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap'); + +:root { + --font-sans: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --nav-bg: #08080c; + --nav-surface: rgba(255,255,255,0.04); + --nav-border: rgba(255,255,255,0.06); + --nav-text: #e8e8ec; + --nav-text-muted: #8b8fa3; + --nav-hover: rgba(255,255,255,0.08); + --nav-active: rgba(255,255,255,0.12); +} + +*, +*::before, +*::after { box-sizing: border-box; } + +html, body { + margin: 0; padding: 0; + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: #f8f9fc; + color: #1a1a2e; + letter-spacing: -0.01em; +} +#root { min-height: 100vh; } diff --git a/video-gen-admin/src/main.tsx b/video-gen-admin/src/main.tsx index 7fdd520a..0f6abd5d 100644 --- a/video-gen-admin/src/main.tsx +++ b/video-gen-admin/src/main.tsx @@ -1,4 +1,5 @@ import { createRoot } from 'react-dom/client' +import './index.css' import App from './App' createRoot(document.getElementById('root')!).render() diff --git a/video-gen-admin/src/pages/AdminLayout.tsx b/video-gen-admin/src/pages/AdminLayout.tsx index 3dab2ec6..0132da02 100644 --- a/video-gen-admin/src/pages/AdminLayout.tsx +++ b/video-gen-admin/src/pages/AdminLayout.tsx @@ -181,26 +181,28 @@ const AdminLayout: React.FC = () => { collapsed={collapsed} onCollapse={setCollapsed} width={220} - theme="dark" + theme="light" style={{ - background: 'linear-gradient(180deg, #0f0f23 0%, #1a1a35 100%)', + background: '#ffffff', + borderRight: '1px solid #e5e7eb', }} > {/* Logo */}
{!collapsed && ( - + 管理后台 )} @@ -212,12 +214,17 @@ const AdminLayout: React.FC = () => { selectedKeys={[activeKey]} defaultOpenKeys={openKeys} items={antMenuItems} + style={{ + background: 'transparent', + border: 'none', + paddingTop: 8, + }} onClick={({ key }) => { if (key.startsWith('group-')) return; navigate(key); }} style={{ background: 'transparent', borderRight: 0, marginTop: 8 }} - theme="dark" + theme="light" /> @@ -228,6 +235,7 @@ const AdminLayout: React.FC = () => { height: 56, background: '#fff', borderBottom: '1px solid #f0f0f5', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 24px', + fontFamily: 'var(--font-sans)', }}> {antMenuItems.find(m => m.key === activeKey)?.label diff --git a/video-gen-api/app/api/v1/admin.py b/video-gen-api/app/api/v1/admin.py index 802d46ab..12961243 100644 --- a/video-gen-api/app/api/v1/admin.py +++ b/video-gen-api/app/api/v1/admin.py @@ -80,7 +80,7 @@ router = APIRouter(prefix="/admin", tags=["admin"]) @router.get("/users") async def list_users( page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), + page_size: int = Query(20, ge=1, le=1000), search: str = Query(""), admin: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), @@ -248,7 +248,7 @@ async def admin_change_password( @router.get("/credit-records") async def list_credit_records( page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), + page_size: int = Query(20, ge=1, le=500), user_id: str | None = Query(None), user_name: str | None = Query(None), type: str | None = Query(None), @@ -315,18 +315,23 @@ async def list_credit_records( @router.get("/notifications") async def list_admin_notifications( page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), + page_size: int = Query(20, ge=1, le=500), user_id: str | None = Query(None), + is_read: bool = Query(None), admin: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """List all notifications (including broadcasts) with optional user_id filter.""" + """List all notifications with optional filters.""" query = select(Notification).order_by(Notification.created_at.desc()) count_query = select(func.count(Notification.id)) if user_id: query = query.where(Notification.user_id == user_id) count_query = count_query.where(Notification.user_id == user_id) + + if is_read is not None: + query = query.where(Notification.is_read == is_read) + count_query = count_query.where(Notification.is_read == is_read) total = (await db.execute(count_query)).scalar() or 0 result = await db.execute(query.offset((page - 1) * page_size).limit(page_size)) @@ -573,7 +578,7 @@ async def get_payment_stats( @router.get("/payment-orders") async def list_payment_orders( page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), + page_size: int = Query(20, ge=1, le=500), payment_method: str | None = Query(None), status: str | None = Query(None), start_date: str | None = Query(None), @@ -1086,7 +1091,7 @@ async def update_system_config( @router.get("/operation-logs") async def list_operation_logs( page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), + page_size: int = Query(20, ge=1, le=500), admin: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): @@ -1279,7 +1284,7 @@ async def get_stats( @router.get("/token-usage") async def list_token_usage( page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), + page_size: int = Query(20, ge=1, le=500), user_id: str | None = Query(None), admin: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), @@ -1321,7 +1326,7 @@ async def admin_list_generation_records( user_id: str | None = Query(None), status: str | None = Query(None), page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), + page_size: int = Query(20, ge=1, le=500), admin: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): diff --git a/video-gen-app/.gitignore b/video-gen-app/.gitignore index a547bf36..4047ea09 100644 --- a/video-gen-app/.gitignore +++ b/video-gen-app/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? +*.bak +*.zip \ No newline at end of file diff --git a/video-gen-app/dist.zip b/video-gen-app/dist.zip deleted file mode 100644 index e41d884d..00000000 Binary files a/video-gen-app/dist.zip and /dev/null differ diff --git a/video-gen-app/index.html b/video-gen-app/index.html index b16b38dc..61ea36c7 100644 --- a/video-gen-app/index.html +++ b/video-gen-app/index.html @@ -4,7 +4,10 @@ - video-gen-app + + + + 民众智创
diff --git a/video-gen-app/src/components/Layout/AppLayout.tsx b/video-gen-app/src/components/Layout/AppLayout.tsx index b1ac9c48..4568b181 100644 --- a/video-gen-app/src/components/Layout/AppLayout.tsx +++ b/video-gen-app/src/components/Layout/AppLayout.tsx @@ -119,10 +119,10 @@ const EXPANDED_W = 240; const COLLAPSED_W = 68; const GRADIENTS = [ - { gradient: 'linear-gradient(135deg, #f59e0b, #f97316)', shadow: 'rgba(245,158,11,0.3)', icon: }, - { gradient: 'linear-gradient(135deg, #6366f1, #8b5cf6)', shadow: 'rgba(99,102,241,0.3)', icon: }, - { gradient: 'linear-gradient(135deg, #06b6d4, #0ea5e9)', shadow: 'rgba(6,182,212,0.3)', icon: }, - { gradient: 'linear-gradient(135deg, #10b981, #059669)', shadow: 'rgba(16,185,129,0.3)', icon: }, + { gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,0.25)', icon: }, + { gradient: 'linear-gradient(135deg, #4a5568, #2d3748)', shadow: 'rgba(74,85,104,0.25)', icon: }, + { gradient: 'linear-gradient(135deg, #718096, #4a5568)', shadow: 'rgba(113,128,150,0.25)', icon: }, + { gradient: 'linear-gradient(135deg, #5a67d8, #434190)', shadow: 'rgba(90,103,216,0.25)', icon: }, ]; const AppLayout: React.FC = () => { @@ -139,8 +139,15 @@ const AppLayout: React.FC = () => { const [rechargeOptions, setRechargeOptions] = useState([]); const [collapsedGroups, setCollapsedGroups] = useState>({}); const [unreadCount, setUnreadCount] = useState(0); - const [siteName, setSiteName] = useState('VideoGen.AI'); - const [siteLogo, setSiteLogo] = useState(''); + const [siteName, setSiteName] = useState(() => { + const cached = localStorage.getItem('siteInfo'); + return cached ? JSON.parse(cached).siteName || '' : ''; + }); + const [siteLogo, setSiteLogo] = useState(() => { + const cached = localStorage.getItem('siteInfo'); + return cached ? JSON.parse(cached).siteLogo || '' : ''; + }); + const [siteInfoLoading, setSiteInfoLoading] = useState(!localStorage.getItem('siteInfo')); const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false); const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{ price: number; credits: number; qrCode: string; method: string } | null>(null); const [paymentMethod, setPaymentMethod] = useState('alipay'); @@ -164,11 +171,29 @@ const AppLayout: React.FC = () => { }, []); useEffect(() => { + const cached = localStorage.getItem('siteInfo'); + if (!cached) { + setSiteName('民众智创'); + setSiteLogo(''); + document.title = '民众智创'; + } + getSiteInfo().then(info => { - setSiteName(info.siteName || 'VideoGen.AI'); - setSiteLogo(info.siteLogo || ''); - document.title = info.siteName || 'VideoGen.AI'; - }).catch(() => {}); + const siteName = info.siteName || '民众智创'; + const siteLogo = info.siteLogo || ''; + setSiteName(siteName); + setSiteLogo(siteLogo); + document.title = siteName; + localStorage.setItem('siteInfo', JSON.stringify({ siteName, siteLogo })); + }).catch(() => { + // 如果没有缓存且请求失败,使用默认值 + if (!cached) { + setSiteName('民众智创'); + document.title = '民众智创'; + } + }).finally(() => { + setSiteInfoLoading(false); + }); }, []); const loadUnreadCount = () => { @@ -264,7 +289,7 @@ const AppLayout: React.FC = () => { const userMenuItems = [ { key: 'profile', icon: , label: `账号: ${user?.username}`, disabled: true }, - { key: 'credits', icon: , label: `可用积分: ${user?.credits ?? 0}`, disabled: true }, + { key: 'credits', icon: , label: `积分: ${user?.credits ?? 0}`, disabled: true }, { type: 'divider' as const }, { key: 'myCredits', icon: , label: '积分明细' }, { key: 'orderRecords', icon: , label: '订单记录' }, @@ -369,8 +394,8 @@ const AppLayout: React.FC = () => { {/* Desktop Sidebar */}
{
-
+
{siteLogo ? ( logo ) : ( - + )}
{!collapsed && ( @@ -411,7 +436,7 @@ const AppLayout: React.FC = () => { {/* Menu */}
{/* {!collapsed && ( -
+
导航
)} */} @@ -434,13 +459,12 @@ const AppLayout: React.FC = () => {
item.path && navigate(item.path)} style={{ display: 'flex', alignItems: 'center', justifyContent: collapsed ? 'center' : 'flex-start', - gap: collapsed ? 0 : 10, - padding: collapsed ? '10px 0' : depth > 0 ? '8px 14px 8px 32px' : '10px 14px', - borderRadius: 10, margin: '2px 0', cursor: 'pointer', - fontSize: depth > 0 ? 12 : 13, fontWeight: isActive ? 600 : 400, - color: isActive ? '#fff' : 'rgba(148,163,184,0.75)', - background: isActive ? 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.15))' : 'transparent', - border: isActive ? '1px solid rgba(99,102,241,0.2)' : '1px solid transparent', + gap: collapsed ? 0 : 12, + padding: collapsed ? '10px 0' : depth > 0 ? '7px 14px 7px 32px' : '9px 16px', + borderRadius: 8, margin: '1px 0', cursor: 'pointer', + fontSize: depth > 0 ? 12 : 13, fontWeight: isActive ? 500 : 400, + color: isActive ? 'var(--nav-text)' : 'var(--nav-text-muted)', + background: isActive ? 'var(--nav-active)' : 'transparent', transition: 'all 0.2s ease', }}> 0 ? 14 : 16, flexShrink: 0 }}>{menuIcon} @@ -457,8 +481,8 @@ const AppLayout: React.FC = () => { items.push(
{g.label}
@@ -483,23 +507,23 @@ const AppLayout: React.FC = () => {
setRechargeModalOpen(true)} style={{ margin: collapsed ? '4px auto' : '4px 14px', padding: collapsed ? '10px' : '10px 14px', - borderRadius: 12, cursor: 'pointer', + borderRadius: 10, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: collapsed ? 'center' : 'flex-start', gap: collapsed ? 0 : 10, - color: '#818cf8', fontSize: 13, fontWeight: 600, - background: 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.2))', - border: '1px solid rgba(99,102,241,0.25)', + color: '#c9a96e', fontSize: 13, fontWeight: 500, + background: 'rgba(201,169,110,0.08)', + border: '1px solid rgba(201,169,110,0.15)', transition: 'all 0.2s', }} - onMouseEnter={(e) => { e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.3), rgba(139,92,246,0.3))'; }} - onMouseLeave={(e) => { e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.2))'; }} + onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(201,169,110,0.15)'; }} + onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(201,169,110,0.08)'; }} > {!collapsed && 充值积分}
{/* User block at bottom-left */} -
+
{ gap: collapsed ? 0 : 10, padding: collapsed ? '8px' : '8px 10px', borderRadius: 12, cursor: 'pointer', transition: 'background 0.2s', - background: 'rgba(255,255,255,0.03)', + background: 'rgba(0,0,0,0.02)', }} - onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.06)'} - onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.03)'} + onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.04)'} + onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.02)'} > } - style={{ background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', flexShrink: 0 }} /> + style={{ background: 'linear-gradient(135deg, #4a5568, #2d3748)', flexShrink: 0 }} /> {!collapsed && (
-
+
{user?.username}
-
可用积分:{user?.credits || 0}
+
积分: {user?.credits || 0}
)}
@@ -543,7 +567,7 @@ const AppLayout: React.FC = () => { position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', width: 24, height: 56, borderRadius: '0 8px 8px 0', - background: '#1a1a35', border: '1px solid rgba(255,255,255,0.1)', borderLeft: 'none', + background: '#f8f9fb', border: '1px solid #e5e7eb', borderLeft: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', boxShadow: '2px 0 12px rgba(0,0,0,0.2)', animation: 'fadeInRight 0.2s ease both', @@ -557,7 +581,7 @@ const AppLayout: React.FC = () => { {/* Main Content */}
@@ -618,8 +642,8 @@ const AppLayout: React.FC = () => {
- 当前积分余额: - {user?.credits ?? 0} + 当前积分余额 + {user?.credits ?? 0}
{rechargeOptions.map((opt, idx) => { diff --git a/video-gen-app/src/components/NotificationPopup.tsx b/video-gen-app/src/components/NotificationPopup.tsx index 2a29d15a..6c198f2a 100644 --- a/video-gen-app/src/components/NotificationPopup.tsx +++ b/video-gen-app/src/components/NotificationPopup.tsx @@ -27,11 +27,10 @@ const NotificationPopup: React.FC = () => { const fetchNotifications = useCallback(async () => { try { - const result = await getNotifications(); + const result = await getNotifications(1, 20, false); const data = result.items || []; - const unread = data.filter((n: Notification) => !n.isRead); - setNotifications(unread); - if (unread.length > 0 && !visible) { + setNotifications(data); + if (data.length > 0 && !visible) { setVisible(true); setCurrentIndex(0); } diff --git a/video-gen-app/src/index.css b/video-gen-app/src/index.css index 0e58969c..634ebb66 100644 --- a/video-gen-app/src/index.css +++ b/video-gen-app/src/index.css @@ -1,4 +1,15 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap'); + +:root { + --font-sans: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --nav-bg: #f8f9fb; + --nav-surface: rgba(0,0,0,0.02); + --nav-border: #e5e7eb; + --nav-text: #1f2937; + --nav-text-muted: #6b7280; + --nav-hover: rgba(0,0,0,0.04); + --nav-active: rgba(99,102,241,0.08); +} *, *::before, @@ -6,11 +17,12 @@ html, body { margin: 0; padding: 0; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + font-family: var(--font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - background: #f5f6fa; + background: #f8f9fc; color: #1a1a2e; + letter-spacing: -0.01em; } #root { min-height: 100vh; } @@ -120,26 +132,27 @@ html, body { .mobile-bottom-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; z-index: 200; - height: 64px; - background: rgba(255,255,255,0.92); - backdrop-filter: blur(20px); - border-top: 1px solid rgba(0,0,0,0.06); - box-shadow: 0 -2px 12px rgba(0,0,0,0.06); + height: 60px; + background: rgba(255,255,255,0.95); + backdrop-filter: blur(24px); + border-top: 1px solid rgba(0,0,0,0.05); + box-shadow: 0 -4px 20px rgba(0,0,0,0.06); } .mobile-bottom-nav .nav-item { flex: 1; display: flex; flex-direction: column; - align-items: center; justify-content: center; gap: 3px; + align-items: center; justify-content: center; gap: 2px; cursor: pointer; transition: all 0.2s; - color: #94a3b8; font-size: 10px; font-weight: 500; + color: #8b8fa3; font-size: 9px; font-weight: 400; + letter-spacing: 0; } .mobile-bottom-nav .nav-item.active { - color: #6366f1; + color: #08080c; } .mobile-bottom-nav .nav-item .nav-icon { - font-size: 20px; transition: transform 0.2s; + font-size: 18px; transition: transform 0.2s; } .mobile-bottom-nav .nav-item.active .nav-icon { - transform: scale(1.1); + transform: scale(1.05); } /* ── Responsive Breakpoints ────────────── */