diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 2b1b002..bcc1964 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -38,6 +38,7 @@ body { font-family: Arial, Helvetica, sans-serif; padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); + background: linear-gradient(to bottom, #0f172a 0%, #0f172a 5%, #581c87 50%, #0f172a 95%, #0f172a 100%); } /* 自定义滚动条样式 - 深色主题 */ diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index afd5c76..20a3dc2 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -33,14 +33,9 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index a200c91..d6d415a 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -6,6 +6,8 @@ import Link from "next/link"; import api from "@/lib/axios"; import { useAuth } from "@/contexts/AuthContext"; import { useTask } from "@/contexts/TaskContext"; +import AccountSettingsDropdown from "@/components/AccountSettingsDropdown"; +import VideoPreviewModal from "@/components/VideoPreviewModal"; const API_BASE = typeof window === 'undefined' ? 'http://localhost:8006' @@ -56,215 +58,12 @@ const formatDate = (timestamp: number) => { return `${year}/${month}/${day} ${hour}:${minute}`; }; -// 账户设置下拉菜单组件 -function AccountSettingsDropdown() { - const { user } = useAuth(); - const [isOpen, setIsOpen] = useState(false); - const [showPasswordModal, setShowPasswordModal] = useState(false); - const [oldPassword, setOldPassword] = useState(''); - const [newPassword, setNewPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); - const [loading, setLoading] = useState(false); - const dropdownRef = useRef(null); - // 点击外部关闭菜单 - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setIsOpen(false); - } - }; - if (isOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen]); - - // 格式化有效期 - const formatExpiry = (expiresAt: string | null) => { - if (!expiresAt) return '永久有效'; - const date = new Date(expiresAt); - return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; - }; - - const handleLogout = async () => { - if (confirm('确定要退出登录吗?')) { - try { - await api.post('/api/auth/logout'); - } catch (e) { } - window.location.href = '/login'; - } - }; - - const handleChangePassword = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - setSuccess(''); - - if (newPassword !== confirmPassword) { - setError('两次输入的新密码不一致'); - return; - } - - if (newPassword.length < 6) { - setError('新密码长度至少6位'); - return; - } - - setLoading(true); - try { - const res = await api.post('/api/auth/change-password', { - old_password: oldPassword, - new_password: newPassword - }); - if (res.data.success) { - setSuccess('密码修改成功,正在跳转登录页...'); - // 清除登录状态并跳转 - setTimeout(async () => { - try { - await api.post('/api/auth/logout'); - } catch (e) { } - window.location.href = '/login'; - }, 1500); - } else { - setError(res.data.message || '修改失败'); - } - } catch (err: any) { - setError(err.response?.data?.detail || '修改失败,请重试'); - } finally { - setLoading(false); - } - }; - - return ( -
- - - {/* 下拉菜单 */} - {isOpen && ( -
- {/* 有效期显示 */} -
-
账户有效期
-
- {user?.expires_at ? formatExpiry(user.expires_at) : '永久有效'} -
-
- - -
- )} - - {/* 修改密码弹窗 */} - {showPasswordModal && ( -
-
-

修改密码

-
-
- - setOldPassword(e.target.value)} - required - className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500" - placeholder="输入当前密码" - /> -
-
- - setNewPassword(e.target.value)} - required - className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500" - placeholder="至少6位" - /> -
-
- - setConfirmPassword(e.target.value)} - required - className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500" - placeholder="再次输入新密码" - /> -
- - {error && ( -
- {error} -
- )} - {success && ( -
- {success} -
- )} - -
- - -
-
-
-
- )} -
- ); -} export default function Home() { const [materials, setMaterials] = useState([]); const [selectedMaterial, setSelectedMaterial] = useState(""); + const [previewMaterial, setPreviewMaterial] = useState(null); const [text, setText] = useState(""); const [voice, setVoice] = useState("zh-CN-YunxiNeural"); @@ -856,6 +655,18 @@ export default function Home() { {m.size_mb.toFixed(1)} MB + + + {/* 下拉菜单 */} + {isOpen && ( +
+ {/* 有效期显示 */} +
+
账户有效期
+
+ {user?.expires_at ? formatExpiry(user.expires_at) : '永久有效'} +
+
+ + +
+ )} + + {/* 修改密码弹窗 */} + {showPasswordModal && ( +
+
+

修改密码

+
+
+ + setOldPassword(e.target.value)} + required + className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500" + placeholder="输入当前密码" + /> +
+
+ + setNewPassword(e.target.value)} + required + className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500" + placeholder="至少6位" + /> +
+
+ + setConfirmPassword(e.target.value)} + required + className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500" + placeholder="再次输入新密码" + /> +
+ + {error && ( +
+ {error} +
+ )} + {success && ( +
+ {success} +
+ )} + +
+ + +
+
+
+
+ )} + + ); +} diff --git a/frontend/src/components/VideoPreviewModal.tsx b/frontend/src/components/VideoPreviewModal.tsx new file mode 100644 index 0000000..8b62061 --- /dev/null +++ b/frontend/src/components/VideoPreviewModal.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { useEffect } from "react"; + +interface VideoPreviewModalProps { + videoUrl: string | null; + onClose: () => void; +} + +export default function VideoPreviewModal({ videoUrl, onClose }: VideoPreviewModalProps) { + useEffect(() => { + // 按 ESC 关闭 + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + if (videoUrl) { + document.addEventListener('keydown', handleEsc); + // 禁止背景滚动 + document.body.style.overflow = 'hidden'; + } + return () => { + document.removeEventListener('keydown', handleEsc); + document.body.style.overflow = 'unset'; + }; + }, [videoUrl, onClose]); + + if (!videoUrl) return null; + + return ( +
+
+ {/* Header */} +
+

+ 🎥 视频预览 +

+ +
+ + {/* Video Player */} +
+
+ + +
+ + {/* Click outside to close */} +
+
+ ); +} diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts deleted file mode 100644 index e2e27cd..0000000 --- a/frontend/src/middleware.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; - -// 需要登录才能访问的路径 -const protectedPaths = ['/', '/publish', '/admin']; - -// 公开路径 (无需登录) -const publicPaths = ['/login', '/register']; - -export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - // 检查是否有 access_token cookie - const token = request.cookies.get('access_token'); - - // 访问受保护页面但未登录 → 重定向到登录页 - if (protectedPaths.some(path => pathname === path || pathname.startsWith(path + '/')) && !token) { - const loginUrl = new URL('/login', request.url); - loginUrl.searchParams.set('from', pathname); - return NextResponse.redirect(loginUrl); - } - - // 已登录用户访问登录/注册页 → 重定向到首页 - if (publicPaths.includes(pathname) && token) { - return NextResponse.redirect(new URL('/', request.url)); - } - - return NextResponse.next(); -} - -export const config = { - matcher: ['/', '/publish/:path*', '/admin/:path*', '/login', '/register'] -};