forked from 77media/video-flow
优化样式
This commit is contained in:
parent
df7d8c8a0d
commit
6022c6ec25
@ -104,7 +104,7 @@ export async function streamJsonPost<T = any>(
|
||||
onJson: (json: T) => void
|
||||
) {
|
||||
try {
|
||||
const token = localStorage?.getItem('token') || 'mock-token';
|
||||
const token = localStorage?.getItem('token') || '';
|
||||
const response = await fetch(`${BASE_URL}${url}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@ -133,8 +133,8 @@ export default function SmartChatBox({
|
||||
<span>Chat</span>
|
||||
{/* System push toggle */}
|
||||
<Switch
|
||||
checkedChildren="系统推送:开"
|
||||
unCheckedChildren="系统推送:关"
|
||||
checkedChildren="System push: On"
|
||||
unCheckedChildren="System push: Off"
|
||||
checked={systemPush}
|
||||
onChange={toggleSystemPush}
|
||||
className="ml-2"
|
||||
|
||||
@ -13,7 +13,6 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background" id="app">
|
||||
<Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />
|
||||
<TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} />
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -10,40 +10,73 @@ import {
|
||||
User,
|
||||
Sparkles,
|
||||
LogOut,
|
||||
Bell,
|
||||
PanelsLeftBottom,
|
||||
Library
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useLayoutEffect } from 'react';
|
||||
import { logoutUser } from '@/lib/auth';
|
||||
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onToggleSidebar: () => void }) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const router = useRouter();
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const currentUser = localStorage.getItem('currentUser');
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
const currentUser: User = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
console.log('Setting mounted state');
|
||||
setMounted(true);
|
||||
return () => console.log('Cleanup mounted effect');
|
||||
}, []);
|
||||
|
||||
|
||||
// 处理点击事件
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
menuRef.current &&
|
||||
!menuRef.current.contains(event.target as Node) &&
|
||||
buttonRef.current &&
|
||||
!buttonRef.current.contains(event.target as Node)
|
||||
) {
|
||||
if (!isOpen) return;
|
||||
|
||||
let isClickStartedInside = false;
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
isClickStartedInside = !!(
|
||||
menuRef.current?.contains(target) ||
|
||||
buttonRef.current?.contains(target)
|
||||
);
|
||||
};
|
||||
|
||||
const handleMouseUp = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
const isClickEndedInside = !!(
|
||||
menuRef.current?.contains(target) ||
|
||||
buttonRef.current?.contains(target)
|
||||
);
|
||||
|
||||
// 只有当点击开始和结束都在外部时才关闭
|
||||
if (!isClickStartedInside && !isClickEndedInside) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
isClickStartedInside = false;
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
// 在冒泡阶段处理事件
|
||||
document.addEventListener('mousedown', handleMouseDown);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('mousedown', handleMouseDown);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, []);
|
||||
}, [isOpen]);
|
||||
|
||||
const handleAnimationEnd = (event: React.AnimationEvent<HTMLDivElement>) => {
|
||||
const element = event.currentTarget;
|
||||
@ -56,14 +89,11 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed right-0 top-0 left-0 h-16 header z-50">
|
||||
<div className="h-full flex items-center justify-between pr-6 pl-2">
|
||||
<div className="fixed right-0 top-0 left-0 h-16 header z-[999]" style={{ isolation: 'isolate' }}>
|
||||
<div className="h-full flex items-center justify-between pr-6 pl-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button className='button-NxtqWZ' variant="ghost" size="sm" onClick={onToggleSidebar}>
|
||||
<PanelsLeftBottom className="h-4 w-4" />
|
||||
</Button>
|
||||
<div
|
||||
className={`flex items-center cursor-pointer space-x-4 link-logo roll event-on`}
|
||||
className={`flex items-center cursor-pointer space-x-1 link-logo roll event-on`}
|
||||
onClick={() => router.push('/')}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onAnimationEnd={handleAnimationEnd}
|
||||
@ -88,6 +118,12 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
|
||||
</h1>
|
||||
</span>
|
||||
</span>
|
||||
{/* beta标签 */}
|
||||
<div className="relative transform translate-y-[-1px]">
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 text-[10px] font-semibold tracking-wider text-[rgb(212 202 202)] border border-[rgba(106,244,249,0.2)] rounded-full shadow-[0_0_10px_rgba(106,244,249,0.1)]">
|
||||
Beta
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -118,37 +154,56 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
|
||||
</Button> */}
|
||||
|
||||
{/* User Menu */}
|
||||
<div className="relative">
|
||||
<div className="relative" style={{ isolation: 'isolate' }}>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
onClick={() => {
|
||||
console.log('Button clicked, current isOpen:', isOpen);
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
data-alt="user-menu-trigger"
|
||||
>
|
||||
<User className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
{mounted && isOpen ? ReactDOM.createPortal(
|
||||
<motion.div
|
||||
ref={menuRef}
|
||||
initial={{ opacity: 0, scale: 0.95, y: -20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="absolute right-0 mt-2 w-56 bg-[#1E1E1E] rounded-lg shadow-lg overflow-hidden z-50"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '4rem',
|
||||
right: '1rem',
|
||||
width: '18rem',
|
||||
zIndex: 9999
|
||||
}}
|
||||
className="bg-[#1E1E1E] rounded-lg shadow-lg overflow-hidden"
|
||||
data-alt="user-menu-dropdown"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* User Info */}
|
||||
<div className="p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="h-10 w-10 rounded-full bg-[#1E4D3E] flex items-center justify-center text-white font-semibold">
|
||||
A
|
||||
{currentUser.name ? currentUser.name.charAt(0) : ''}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">admin-live</p>
|
||||
<p className="text-xs text-gray-500">admin-live.com</p>
|
||||
<div className='flex-1'>
|
||||
<p className="text-sm font-medium">{currentUser.name}</p>
|
||||
<p className="text-xs text-gray-500">{currentUser.email}</p>
|
||||
</div>
|
||||
<div
|
||||
className='cursor-pointer hover:text-red-400 transition-colors duration-200'
|
||||
onClick={() => {
|
||||
logoutUser();
|
||||
}}
|
||||
title="退出登录"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -157,27 +212,28 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
|
||||
<div className="px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
<span className="text-white underline text-sm">100 点数</span>
|
||||
<span className="text-white underline text-sm">100 credits</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-white border-white hover:bg-white/10 rounded-full px-8"
|
||||
onClick={() => router.push('/pricing')}
|
||||
>
|
||||
升级
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Menu Items */}
|
||||
<div className="p-2">
|
||||
<motion.button
|
||||
{/* <motion.button
|
||||
whileHover={{ backgroundColor: 'rgba(255,255,255,0.1)' }}
|
||||
className="w-full flex items-center space-x-2 px-3 py-2 rounded-md text-sm text-white"
|
||||
onClick={() => router.push('/my-library')}
|
||||
data-alt="my-library-button"
|
||||
>
|
||||
<Library className="h-4 w-4" />
|
||||
<span>我的库</span>
|
||||
<span>My Library</span>
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
@ -190,21 +246,22 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
|
||||
data-alt="logout-button"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<span>退出账号</span>
|
||||
</motion.button>
|
||||
<span>Logout</span>
|
||||
</motion.button> */}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-4 px-3 py-2 text-xs text-gray-400 text-center">
|
||||
<div>隐私权和条款 · 许可</div>
|
||||
<div>Privacy Policy · Terms of Service</div>
|
||||
<div>250819215404 | 2025/8/20 06:00:50</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
, document.body)
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -81,6 +81,10 @@ export default function Login() {
|
||||
endPercentage={70}
|
||||
/>
|
||||
</span>
|
||||
{/* beta标签 */}
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 text-[8px] font-semibold tracking-wider text-[rgb(212 202 202)] border border-[rgba(106,244,249,0.2)] rounded-full shadow-[0_0_10px_rgba(106,244,249,0.1)]">
|
||||
Beta
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="left-panel">
|
||||
|
||||
@ -11,9 +11,10 @@ type FloatingGlassPanelProps = {
|
||||
width?: string;
|
||||
r_key?: string | number;
|
||||
panel_style?: React.CSSProperties;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function FloatingGlassPanel({ open, onClose, children, width = '320px', r_key, panel_style, clickMaskClose = true }: FloatingGlassPanelProps) {
|
||||
export default function FloatingGlassPanel({ open, onClose, children, width = '320px', r_key, panel_style, clickMaskClose = true, className }: FloatingGlassPanelProps) {
|
||||
// 定义弹出动画
|
||||
const bounceAnimation = {
|
||||
scale: [0.95, 1.02, 0.98, 1],
|
||||
@ -23,7 +24,7 @@ export default function FloatingGlassPanel({ open, onClose, children, width = '3
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className={`fixed inset-0 z-50 flex items-center justify-center ${className}`}>
|
||||
<motion.div
|
||||
key={r_key}
|
||||
className="cursor-grab active:cursor-grabbing"
|
||||
|
||||
152
lib/api.ts
152
lib/api.ts
@ -1,152 +0,0 @@
|
||||
import { getToken, clearAuthData } from './auth';
|
||||
|
||||
// API基础URL
|
||||
const API_BASE_URL = 'https://77.smartvideo.py.qikongjian.com';
|
||||
|
||||
/**
|
||||
* 统一的API请求方法
|
||||
*/
|
||||
export const apiRequest = async (
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<any> => {
|
||||
const token = getToken();
|
||||
|
||||
// 构建请求头
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
|
||||
// 添加token到请求头(如果存在)
|
||||
if (token) {
|
||||
headers['X-EASE-ADMIN-TOKEN'] = token;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
// 检查响应状态
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// Token过期或无效
|
||||
clearAuthData();
|
||||
window.location.href = '/login';
|
||||
throw new Error('身份验证失败,请重新登录');
|
||||
}
|
||||
throw new Error(`请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 检查业务状态码
|
||||
if (data.code === '401' || data.status === 401) {
|
||||
clearAuthData();
|
||||
window.location.href = '/login';
|
||||
throw new Error('身份验证失败,请重新登录');
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
*/
|
||||
export const apiGet = (endpoint: string, options: RequestInit = {}) => {
|
||||
return apiRequest(endpoint, {
|
||||
...options,
|
||||
method: 'GET',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
*/
|
||||
export const apiPost = (endpoint: string, data?: any, options: RequestInit = {}) => {
|
||||
return apiRequest(endpoint, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
*/
|
||||
export const apiPut = (endpoint: string, data?: any, options: RequestInit = {}) => {
|
||||
return apiRequest(endpoint, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
*/
|
||||
export const apiDelete = (endpoint: string, options: RequestInit = {}) => {
|
||||
return apiRequest(endpoint, {
|
||||
...options,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 文件上传请求
|
||||
*/
|
||||
export const apiUpload = async (endpoint: string, formData: FormData, options: RequestInit = {}) => {
|
||||
const token = getToken();
|
||||
|
||||
// 构建请求头(文件上传时不设置Content-Type,让浏览器自动设置)
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
|
||||
// 添加token到请求头(如果存在)
|
||||
if (token) {
|
||||
headers['X-EASE-ADMIN-TOKEN'] = token;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
|
||||
// 检查响应状态
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// Token过期或无效
|
||||
clearAuthData();
|
||||
window.location.href = '/login';
|
||||
throw new Error('身份验证失败,请重新登录');
|
||||
}
|
||||
throw new Error(`请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 检查业务状态码
|
||||
if (data.code === '401' || data.status === 401) {
|
||||
clearAuthData();
|
||||
window.location.href = '/login';
|
||||
throw new Error('身份验证失败,请重新登录');
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user