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">
|
<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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue