main
mula.liu 2025-12-02 19:20:33 +08:00
parent 8817389cab
commit f9e3cb0ceb
5 changed files with 170 additions and 19 deletions

View File

@ -56,7 +56,7 @@ export function FocusInfo({ body, onClose, toast }: FocusInfoProps) {
<div className="flex flex-col items-center -translate-y-24 pointer-events-none"> <div className="flex flex-col items-center -translate-y-24 pointer-events-none">
<style>{styles}</style> <style>{styles}</style>
{/* Main Info Card */} {/* Main Info Card */}
<div className="bg-black/80 backdrop-blur-xl border border-white/10 rounded-2xl p-5 min-w-[340px] max-w-md shadow-2xl pointer-events-auto relative group mb-2"> <div className="bg-black/80 backdrop-blur-xl border border-[#238636] rounded-2xl p-5 min-w-[340px] max-w-md shadow-2xl shadow-[#238636]/20 pointer-events-auto relative group mb-2">
{/* Close Button */} {/* Close Button */}
<button <button
@ -93,7 +93,7 @@ export function FocusInfo({ body, onClose, toast }: FocusInfoProps) {
{/* Stats and Actions Grid */} {/* Stats and Actions Grid */}
<div className="grid grid-cols-2 gap-2 mb-2"> <div className="grid grid-cols-2 gap-2 mb-2">
{/* Column 1: Heliocentric Distance Card */} {/* Column 1: Heliocentric Distance Card */}
<div className="bg-white/5 rounded-lg p-2 flex items-center gap-2.5 border border-white/5"> <div className="bg-white/5 rounded-lg p-2.5 flex items-center gap-2.5 border border-white/5 h-[52px]">
<div className="p-1.5 rounded-full bg-blue-500/20 text-blue-400"> <div className="p-1.5 rounded-full bg-blue-500/20 text-blue-400">
<Ruler size={14} /> <Ruler size={14} />
</div> </div>
@ -104,16 +104,14 @@ export function FocusInfo({ body, onClose, toast }: FocusInfoProps) {
</div> </div>
{/* Column 2: JPL Horizons Button */} {/* Column 2: JPL Horizons Button */}
<div className="flex items-center justify-end"> <button
<button onClick={fetchNasaData}
onClick={fetchNasaData} className="px-3 py-2.5 rounded-lg bg-cyan-950/30 text-cyan-400 border border-cyan-500/20 hover:bg-cyan-500/10 hover:border-cyan-500/50 transition-all flex items-center justify-center gap-2 text-[10px] font-mono uppercase tracking-widest group/btn h-[52px]"
className="px-3 py-1.5 rounded-lg bg-cyan-950/30 text-cyan-400 border border-cyan-500/20 hover:bg-cyan-500/10 hover:border-cyan-500/50 transition-all flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest group/btn w-full justify-end" title="连接 JPL Horizons System"
title="连接 JPL Horizons System" >
> <Radar size={12} className="group-hover/btn:animate-spin-slow" />
<Radar size={12} className="group-hover/btn:animate-spin-slow" /> <span>JPL Horizons</span>
<span>JPL Horizons</span> </button>
</button>
</div>
</div> </div>
{/* Conditional Probe Status Card (if isProbe is true, this goes in a new row) */} {/* Conditional Probe Status Card (if isProbe is true, this goes in a new row) */}
@ -135,7 +133,7 @@ export function FocusInfo({ body, onClose, toast }: FocusInfoProps) {
</div> </div>
{/* Connecting Line/Triangle pointing down to the body */} {/* Connecting Line/Triangle pointing down to the body */}
<div className="w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-t-[8px] border-t-black/80 backdrop-blur-xl mt-[-1px]"></div> <div className="w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-t-[8px] border-t-[#238636] backdrop-blur-xl mt-[-1px]"></div>
<TerminalModal <TerminalModal
open={showTerminal} open={showTerminal}

View File

@ -23,9 +23,7 @@ export function Header({
{/* Left: Branding */} {/* Left: Branding */}
<div className="flex items-center gap-4 pointer-events-auto inline-flex"> <div className="flex items-center gap-4 pointer-events-auto inline-flex">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center shadow-lg shadow-blue-500/30"> <span className="text-4xl">🌌</span>
<span className="text-2xl">🌌</span>
</div>
<div> <div>
<h1 className="text-2xl font-bold text-white tracking-tight drop-shadow-md">Cosmo</h1> <h1 className="text-2xl font-bold text-white tracking-tight drop-shadow-md">Cosmo</h1>
<p className="text-xs text-gray-400 font-medium tracking-wide">DEEP SPACE EXPLORER</p> <p className="text-xs text-gray-400 font-medium tracking-wide">DEEP SPACE EXPLORER</p>

View File

@ -67,6 +67,8 @@ export function TerminalModal({
} }
.terminal-modal .ant-modal-close { .terminal-modal .ant-modal-close {
color: #2ea043 !important; color: #2ea043 !important;
top: 24px !important;
inset-inline-end: 20px !important;
} }
.terminal-modal .ant-modal-close:hover { .terminal-modal .ant-modal-close:hover {
background-color: rgba(35, 134, 54, 0.2) !important; background-color: rgba(35, 134, 54, 0.2) !important;

View File

@ -96,7 +96,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
{children} {children}
{/* Toast Container - Top Right */} {/* Toast Container - Top Right */}
<div className="fixed top-24 right-6 z-[100] flex flex-col gap-3 pointer-events-none"> <div className="fixed top-24 right-6 z-[9999] flex flex-col gap-3 pointer-events-none">
{toasts.map((toast) => ( {toasts.map((toast) => (
<div <div
key={toast.id} key={toast.id}

View File

@ -3,7 +3,7 @@
*/ */
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { Layout, Menu, Avatar, Dropdown } from 'antd'; import { Layout, Menu, Avatar, Dropdown, Modal, Form, Input, Button, message } from 'antd';
import { import {
MenuFoldOutlined, MenuFoldOutlined,
MenuUnfoldOutlined, MenuUnfoldOutlined,
@ -19,7 +19,7 @@ import {
ControlOutlined, ControlOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
import { authAPI } from '../../utils/request'; import { authAPI, request } from '../../utils/request';
import { auth } from '../../utils/auth'; import { auth } from '../../utils/auth';
import { useToast } from '../../contexts/ToastContext'; import { useToast } from '../../contexts/ToastContext';
@ -41,6 +41,11 @@ export function AdminLayout() {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [menus, setMenus] = useState<any[]>([]); const [menus, setMenus] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [profileModalOpen, setProfileModalOpen] = useState(false);
const [passwordModalOpen, setPasswordModalOpen] = useState(false);
const [profileForm] = Form.useForm();
const [passwordForm] = Form.useForm();
const [userProfile, setUserProfile] = useState<any>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const user = auth.getUser(); const user = auth.getUser();
@ -96,11 +101,57 @@ export function AdminLayout() {
} }
}; };
const handleProfileClick = async () => {
try {
const { data } = await request.get('/users/me');
setUserProfile(data);
profileForm.setFieldsValue({
username: data.username,
email: data.email || '',
full_name: data.full_name || '',
});
setProfileModalOpen(true);
} catch (error) {
toast.error('获取用户信息失败');
}
};
const handleProfileUpdate = async (values: any) => {
try {
await request.put('/users/me/profile', {
full_name: values.full_name,
email: values.email || null,
});
toast.success('个人信息更新成功');
setProfileModalOpen(false);
// Update local user info
const updatedUser = { ...user, full_name: values.full_name, email: values.email };
auth.setUser(updatedUser);
} catch (error: any) {
toast.error(error.response?.data?.detail || '更新失败');
}
};
const handlePasswordChange = async (values: any) => {
try {
await request.put('/users/me/password', {
old_password: values.old_password,
new_password: values.new_password,
});
toast.success('密码修改成功');
setPasswordModalOpen(false);
passwordForm.resetFields();
} catch (error: any) {
toast.error(error.response?.data?.detail || '密码修改失败');
}
};
const userMenuItems: MenuProps['items'] = [ const userMenuItems: MenuProps['items'] = [
{ {
key: 'profile', key: 'profile',
icon: <UserOutlined />, icon: <UserOutlined />,
label: '个人信息', label: '个人信息',
onClick: handleProfileClick,
}, },
{ {
type: 'divider', type: 'divider',
@ -174,6 +225,108 @@ export function AdminLayout() {
<Outlet /> <Outlet />
</Content> </Content>
</Layout> </Layout>
{/* Profile Modal */}
<Modal
title="个人信息"
open={profileModalOpen}
onCancel={() => setProfileModalOpen(false)}
footer={null}
width={500}
>
<Form
form={profileForm}
layout="vertical"
onFinish={handleProfileUpdate}
>
<Form.Item label="用户名" name="username">
<Input disabled />
</Form.Item>
<Form.Item
label="昵称"
name="full_name"
rules={[{ max: 50, message: '昵称最长50个字符' }]}
>
<Input placeholder="请输入昵称" />
</Form.Item>
<Form.Item
label="邮箱"
name="email"
rules={[
{ type: 'email', message: '请输入有效的邮箱地址' }
]}
>
<Input placeholder="请输入邮箱" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" style={{ marginRight: 8 }}>
</Button>
<Button onClick={() => setPasswordModalOpen(true)}>
</Button>
</Form.Item>
</Form>
</Modal>
{/* Password Change Modal */}
<Modal
title="修改密码"
open={passwordModalOpen}
onCancel={() => {
setPasswordModalOpen(false);
passwordForm.resetFields();
}}
footer={null}
width={450}
>
<Form
form={passwordForm}
layout="vertical"
onFinish={handlePasswordChange}
>
<Form.Item
label="当前密码"
name="old_password"
rules={[{ required: true, message: '请输入当前密码' }]}
>
<Input.Password placeholder="请输入当前密码" />
</Form.Item>
<Form.Item
label="新密码"
name="new_password"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码至少6位' }
]}
>
<Input.Password placeholder="请输入新密码至少6位" />
</Form.Item>
<Form.Item
label="确认新密码"
name="confirm_password"
dependencies={['new_password']}
rules={[
{ required: true, message: '请确认新密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('new_password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致'));
},
}),
]}
>
<Input.Password placeholder="请再次输入新密码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block>
</Button>
</Form.Item>
</Form>
</Modal>
</Layout> </Layout>
); );
} }