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">
<style>{styles}</style>
{/* 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 */}
<button
@ -93,7 +93,7 @@ export function FocusInfo({ body, onClose, toast }: FocusInfoProps) {
{/* Stats and Actions Grid */}
<div className="grid grid-cols-2 gap-2 mb-2">
{/* 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">
<Ruler size={14} />
</div>
@ -104,16 +104,14 @@ export function FocusInfo({ body, onClose, toast }: FocusInfoProps) {
</div>
{/* Column 2: JPL Horizons Button */}
<div className="flex items-center justify-end">
<button
onClick={fetchNasaData}
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"
>
<Radar size={12} className="group-hover/btn:animate-spin-slow" />
<span>JPL Horizons</span>
</button>
</div>
<button
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]"
title="连接 JPL Horizons System"
>
<Radar size={12} className="group-hover/btn:animate-spin-slow" />
<span>JPL Horizons</span>
</button>
</div>
{/* 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>
{/* 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
open={showTerminal}

View File

@ -23,9 +23,7 @@ export function Header({
{/* Left: Branding */}
<div className="flex items-center gap-4 pointer-events-auto inline-flex">
<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-2xl">🌌</span>
</div>
<span className="text-4xl">🌌</span>
<div>
<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>

View File

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

View File

@ -96,7 +96,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
{children}
{/* 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) => (
<div
key={toast.id}

View File

@ -3,7 +3,7 @@
*/
import { useState, useEffect } from 'react';
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 {
MenuFoldOutlined,
MenuUnfoldOutlined,
@ -19,7 +19,7 @@ import {
ControlOutlined,
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { authAPI } from '../../utils/request';
import { authAPI, request } from '../../utils/request';
import { auth } from '../../utils/auth';
import { useToast } from '../../contexts/ToastContext';
@ -41,6 +41,11 @@ export function AdminLayout() {
const [collapsed, setCollapsed] = useState(false);
const [menus, setMenus] = useState<any[]>([]);
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 location = useLocation();
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'] = [
{
key: 'profile',
icon: <UserOutlined />,
label: '个人信息',
onClick: handleProfileClick,
},
{
type: 'divider',
@ -174,6 +225,108 @@ export function AdminLayout() {
<Outlet />
</Content>
</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>
);
}