更新
This commit is contained in:
@@ -38,6 +38,7 @@ body {
|
|||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
padding-top: env(safe-area-inset-top);
|
padding-top: env(safe-area-inset-top);
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
background: linear-gradient(to bottom, #0f172a 0%, #0f172a 5%, #581c87 50%, #0f172a 95%, #0f172a 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 自定义滚动条样式 - 深色主题 */
|
/* 自定义滚动条样式 - 深色主题 */
|
||||||
|
|||||||
@@ -33,14 +33,9 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" style={{ backgroundColor: '#0f172a' }}>
|
<html lang="en">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
minHeight: '100dvh',
|
|
||||||
background: 'linear-gradient(to bottom, #0f172a 0%, #0f172a 5%, #581c87 50%, #0f172a 95%, #0f172a 100%)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<TaskProvider>
|
<TaskProvider>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import Link from "next/link";
|
|||||||
import api from "@/lib/axios";
|
import api from "@/lib/axios";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { useTask } from "@/contexts/TaskContext";
|
import { useTask } from "@/contexts/TaskContext";
|
||||||
|
import AccountSettingsDropdown from "@/components/AccountSettingsDropdown";
|
||||||
|
import VideoPreviewModal from "@/components/VideoPreviewModal";
|
||||||
|
|
||||||
const API_BASE = typeof window === 'undefined'
|
const API_BASE = typeof window === 'undefined'
|
||||||
? 'http://localhost:8006'
|
? 'http://localhost:8006'
|
||||||
@@ -56,215 +58,12 @@ const formatDate = (timestamp: number) => {
|
|||||||
return `${year}/${month}/${day} ${hour}:${minute}`;
|
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<HTMLDivElement>(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 (
|
|
||||||
<div className="relative">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className="px-2 sm:px-4 py-1 sm:py-2 text-sm sm:text-base bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<span>⚙️</span>
|
|
||||||
<span className="hidden sm:inline">账户</span>
|
|
||||||
<svg className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* 下拉菜单 */}
|
|
||||||
{isOpen && (
|
|
||||||
<div ref={dropdownRef} className="absolute right-0 mt-2 bg-gray-800 border border-white/10 rounded-lg shadow-xl z-[160] overflow-hidden whitespace-nowrap">
|
|
||||||
{/* 有效期显示 */}
|
|
||||||
<div className="px-3 py-2 border-b border-white/10 text-center">
|
|
||||||
<div className="text-xs text-gray-400">账户有效期</div>
|
|
||||||
<div className="text-sm text-white font-medium">
|
|
||||||
{user?.expires_at ? formatExpiry(user.expires_at) : '永久有效'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
setShowPasswordModal(true);
|
|
||||||
}}
|
|
||||||
className="w-full px-3 py-2 text-left text-sm text-white hover:bg-white/10 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
🔐 修改密码
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="w-full px-3 py-2 text-left text-sm text-red-300 hover:bg-red-500/20 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
🚪 退出登录
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 修改密码弹窗 */}
|
|
||||||
{showPasswordModal && (
|
|
||||||
<div className="fixed inset-0 z-[200] flex items-start justify-center pt-20 bg-black/60 backdrop-blur-sm p-4">
|
|
||||||
<div className="w-full max-w-md p-6 bg-gray-900 border border-white/10 rounded-2xl shadow-2xl mx-4">
|
|
||||||
<h3 className="text-xl font-bold text-white mb-4">修改密码</h3>
|
|
||||||
<form onSubmit={handleChangePassword} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-gray-300 mb-1">当前密码</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={oldPassword}
|
|
||||||
onChange={(e) => 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="输入当前密码"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-gray-300 mb-1">新密码</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={newPassword}
|
|
||||||
onChange={(e) => 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位"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-gray-300 mb-1">确认新密码</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => 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="再次输入新密码"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="p-2 bg-red-500/20 border border-red-500/50 rounded text-red-200 text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<div className="p-2 bg-green-500/20 border border-green-500/50 rounded text-green-200 text-sm">
|
|
||||||
{success}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-3 pt-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setShowPasswordModal(false);
|
|
||||||
setError('');
|
|
||||||
setOldPassword('');
|
|
||||||
setNewPassword('');
|
|
||||||
setConfirmPassword('');
|
|
||||||
}}
|
|
||||||
className="flex-1 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="flex-1 py-2 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white rounded-lg transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? '修改中...' : '确认修改'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [materials, setMaterials] = useState<Material[]>([]);
|
const [materials, setMaterials] = useState<Material[]>([]);
|
||||||
const [selectedMaterial, setSelectedMaterial] = useState<string>("");
|
const [selectedMaterial, setSelectedMaterial] = useState<string>("");
|
||||||
|
const [previewMaterial, setPreviewMaterial] = useState<string | null>(null);
|
||||||
|
|
||||||
const [text, setText] = useState<string>("");
|
const [text, setText] = useState<string>("");
|
||||||
const [voice, setVoice] = useState<string>("zh-CN-YunxiNeural");
|
const [voice, setVoice] = useState<string>("zh-CN-YunxiNeural");
|
||||||
@@ -856,6 +655,18 @@ export default function Home() {
|
|||||||
{m.size_mb.toFixed(1)} MB
|
{m.size_mb.toFixed(1)} MB
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (m.path) {
|
||||||
|
setPreviewMaterial(`${API_BASE}${m.path}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="absolute top-2 right-10 p-1 text-gray-500 hover:text-white opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="预览视频"
|
||||||
|
>
|
||||||
|
👁️
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -1290,6 +1101,10 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main >
|
</main >
|
||||||
|
<VideoPreviewModal
|
||||||
|
videoUrl={previewMaterial}
|
||||||
|
onClose={() => setPreviewMaterial(null)}
|
||||||
|
/>
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
211
frontend/src/components/AccountSettingsDropdown.tsx
Normal file
211
frontend/src/components/AccountSettingsDropdown.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
import api from "@/lib/axios";
|
||||||
|
|
||||||
|
// 账户设置下拉菜单组件
|
||||||
|
export default 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<HTMLDivElement>(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 (
|
||||||
|
<div className="relative" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="px-2 sm:px-4 py-1 sm:py-2 text-sm sm:text-base bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span>⚙️</span>
|
||||||
|
<span className="hidden sm:inline">账户</span>
|
||||||
|
<svg className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 下拉菜单 */}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute right-0 mt-2 bg-gray-800 border border-white/10 rounded-lg shadow-xl z-[160] overflow-hidden whitespace-nowrap">
|
||||||
|
{/* 有效期显示 */}
|
||||||
|
<div className="px-3 py-2 border-b border-white/10 text-center">
|
||||||
|
<div className="text-xs text-gray-400">账户有效期</div>
|
||||||
|
<div className="text-sm text-white font-medium">
|
||||||
|
{user?.expires_at ? formatExpiry(user.expires_at) : '永久有效'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setShowPasswordModal(true);
|
||||||
|
}}
|
||||||
|
className="w-full px-3 py-2 text-left text-sm text-white hover:bg-white/10 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
🔐 修改密码
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="w-full px-3 py-2 text-left text-sm text-red-300 hover:bg-red-500/20 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
🚪 退出登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 修改密码弹窗 */}
|
||||||
|
{showPasswordModal && (
|
||||||
|
<div className="fixed inset-0 z-[200] flex items-start justify-center pt-20 bg-black/60 backdrop-blur-sm p-4">
|
||||||
|
<div className="w-full max-w-md p-6 bg-gray-900 border border-white/10 rounded-2xl shadow-2xl mx-4">
|
||||||
|
<h3 className="text-xl font-bold text-white mb-4">修改密码</h3>
|
||||||
|
<form onSubmit={handleChangePassword} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-300 mb-1">当前密码</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={oldPassword}
|
||||||
|
onChange={(e) => 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="输入当前密码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-300 mb-1">新密码</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => 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位"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-300 mb-1">确认新密码</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => 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="再次输入新密码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-2 bg-red-500/20 border border-red-500/50 rounded text-red-200 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{success && (
|
||||||
|
<div className="p-2 bg-green-500/20 border border-green-500/50 rounded text-green-200 text-sm">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setShowPasswordModal(false);
|
||||||
|
setError('');
|
||||||
|
setOldPassword('');
|
||||||
|
setNewPassword('');
|
||||||
|
setConfirmPassword('');
|
||||||
|
}}
|
||||||
|
className="flex-1 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="flex-1 py-2 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? '修改中...' : '确认修改'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
frontend/src/components/VideoPreviewModal.tsx
Normal file
64
frontend/src/components/VideoPreviewModal.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||||||
|
<div className="relative w-full max-w-4xl bg-gray-900 border border-white/10 rounded-2xl shadow-2xl overflow-hidden flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-2 border-b border-white/10 bg-white/5">
|
||||||
|
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||||
|
🎥 视频预览
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 text-gray-400 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Video Player */}
|
||||||
|
<div className="bg-black flex items-center justify-center min-h-[50vh] max-h-[80vh]">
|
||||||
|
<video
|
||||||
|
src={videoUrl}
|
||||||
|
controls
|
||||||
|
autoPlay
|
||||||
|
className="w-full h-full max-h-[80vh] object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Click outside to close */}
|
||||||
|
<div className="absolute inset-0 -z-10" onClick={onClose}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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']
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user