1.0.1
parent
8817389cab
commit
f9e3cb0ceb
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue