refactor: add top bar and adjust home page layout
- add top bar to show the title of web site - move ai input and input image into right side drawer
This commit is contained in:
@@ -12,7 +12,7 @@ import type { PostInputRequest, ApiResponse } from './types';
|
||||
export async function postInput(text: string): Promise<ApiResponse> {
|
||||
const requestData: PostInputRequest = { text };
|
||||
// 为 postInput 设置 30 秒超时时间
|
||||
return post<ApiResponse>(API_ENDPOINTS.INPUT, requestData, { timeout: 30000 });
|
||||
return post<ApiResponse>(API_ENDPOINTS.INPUT, requestData, { timeout: 120000 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,5 +22,5 @@ export async function postInput(text: string): Promise<ApiResponse> {
|
||||
*/
|
||||
export async function postInputData(data: PostInputRequest): Promise<ApiResponse> {
|
||||
// 为 postInputData 设置 30 秒超时时间
|
||||
return post<ApiResponse>(API_ENDPOINTS.INPUT, data, { timeout: 30000 });
|
||||
return post<ApiResponse>(API_ENDPOINTS.INPUT, data, { timeout: 120000 });
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export async function postInputImage(file: File): Promise<ApiResponse> {
|
||||
throw new Error('只能上传图片文件');
|
||||
}
|
||||
|
||||
return upload<ApiResponse>(API_ENDPOINTS.INPUT_IMAGE, file, 'image', { timeout: 30000 });
|
||||
return upload<ApiResponse>(API_ENDPOINTS.INPUT_IMAGE, file, 'image', { timeout: 120000 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +75,7 @@ export async function postInputImageWithProgress(
|
||||
|
||||
// 发送请求
|
||||
xhr.open('POST', `http://127.0.0.1:8099${API_ENDPOINTS.INPUT_IMAGE}`);
|
||||
xhr.timeout = 30000; // 30秒超时
|
||||
xhr.timeout = 120000; // 30秒超时
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
40
src/components/InputDrawer.css
Normal file
40
src/components/InputDrawer.css
Normal file
@@ -0,0 +1,40 @@
|
||||
/* 右侧输入抽屉样式(参考示例配色) */
|
||||
.input-drawer .ant-drawer-body {
|
||||
background: #e9b6b6; /* 淡粉色背景,贴近参考图 */
|
||||
}
|
||||
|
||||
.input-drawer-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: center; /* 居中内部内容 */
|
||||
}
|
||||
.input-drawer-title {
|
||||
font-weight: 700;
|
||||
color: #3b3b3b;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-drawer-box {
|
||||
background: rgba(255,255,255,0.75);
|
||||
border-radius: 12px;
|
||||
padding: 16px 18px; /* 增大内边距 */
|
||||
box-shadow: 0 1px 6px rgba(0,0,0,0.08);
|
||||
width: 100%;
|
||||
max-width: 680px; /* 桌面居中显示更宽 */
|
||||
margin: 0 auto; /* 水平居中 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 抽屉底部按钮区域与页面底栏保持间距(如有) */
|
||||
.input-drawer .ant-drawer-footer { border-top: none; }
|
||||
|
||||
/* 抽屉与遮罩不再额外向下偏移,依赖 getContainer 挂载到标题栏下方的容器 */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.input-drawer-box {
|
||||
max-width: 100%;
|
||||
padding: 14px; /* 移动端更紧凑 */
|
||||
}
|
||||
}
|
||||
75
src/components/InputDrawer.tsx
Normal file
75
src/components/InputDrawer.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { Drawer, Grid } from 'antd';
|
||||
import InputPanel from './InputPanel.tsx';
|
||||
import HintText from './HintText.tsx';
|
||||
import './InputDrawer.css';
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onResult?: (data: any) => void;
|
||||
containerEl?: HTMLElement | null; // 抽屉挂载容器(用于放在标题栏下方)
|
||||
};
|
||||
|
||||
const InputDrawer: React.FC<Props> = ({ open, onClose, onResult, containerEl }) => {
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isMobile = !screens.md;
|
||||
const [topbarHeight, setTopbarHeight] = React.useState<number>(56);
|
||||
|
||||
React.useEffect(() => {
|
||||
const update = () => {
|
||||
const el = document.querySelector('.topbar') as HTMLElement | null;
|
||||
const h = el?.clientHeight || 56;
|
||||
setTopbarHeight(h);
|
||||
};
|
||||
update();
|
||||
window.addEventListener('resize', update);
|
||||
return () => window.removeEventListener('resize', update);
|
||||
}, []);
|
||||
|
||||
// 在输入处理成功(onResult 被调用)后,自动关闭抽屉
|
||||
const handleResult = React.useCallback(
|
||||
(data: any) => {
|
||||
onResult?.(data);
|
||||
onClose();
|
||||
},
|
||||
[onResult, onClose]
|
||||
);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className="input-drawer"
|
||||
placement="right"
|
||||
width={isMobile ? '100%' : '33%'}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
mask={false}
|
||||
getContainer={containerEl ? () => containerEl : undefined}
|
||||
closable={false}
|
||||
zIndex={1500}
|
||||
rootStyle={{ top: topbarHeight, height: `calc(100% - ${topbarHeight}px)` }}
|
||||
styles={{
|
||||
header: { display: 'none' },
|
||||
body: {
|
||||
padding: 16,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
mask: { top: topbarHeight, height: `calc(100% - ${topbarHeight}px)` },
|
||||
}}
|
||||
>
|
||||
<div className="input-drawer-inner">
|
||||
<div className="input-drawer-title">AI FIND U</div>
|
||||
<div className="input-drawer-box">
|
||||
<InputPanel onResult={handleResult} />
|
||||
<HintText />
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputDrawer;
|
||||
@@ -2,13 +2,14 @@
|
||||
.input-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 12px; /* 增大间距 */
|
||||
}
|
||||
|
||||
.input-panel .ant-input-outlined,
|
||||
.input-panel .ant-input {
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: #e5e7eb;
|
||||
min-height: 180px; /* 提升基础高度,配合 autoSize 更宽裕 */
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
|
||||
@@ -88,8 +88,9 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult }) => {
|
||||
<TextArea
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="请输入个人信息描述,或点击右侧上传图片…"
|
||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
||||
placeholder="请输入个人信息描述,或上传图片…"
|
||||
autoSize={{ minRows: 6, maxRows: 12 }}
|
||||
style={{ fontSize: 14 }}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
@@ -4,12 +4,17 @@ import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
||||
import SiderMenu from './SiderMenu.tsx';
|
||||
import MainContent from './MainContent.tsx';
|
||||
import ResourceList from './ResourceList.tsx';
|
||||
import TopBar from './TopBar.tsx';
|
||||
import '../styles/base.css';
|
||||
import '../styles/layout.css';
|
||||
|
||||
const LayoutWrapper: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
|
||||
const [inputOpen, setInputOpen] = React.useState(false);
|
||||
const isHome = location.pathname === '/';
|
||||
const layoutShellRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const pathToKey = (path: string) => {
|
||||
switch (path) {
|
||||
@@ -39,17 +44,42 @@ const LayoutWrapper: React.FC = () => {
|
||||
navigate('/');
|
||||
break;
|
||||
}
|
||||
// 切换页面时收起输入抽屉
|
||||
setInputOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout className="layout-wrapper app-root">
|
||||
<SiderMenu onNavigate={handleNavigate} selectedKey={selectedKey} />
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<MainContent />} />
|
||||
<Route path="/resources" element={<ResourceList />} />
|
||||
<Route path="/menu2" element={<div style={{ padding: 32, color: '#cbd5e1' }}>菜单2的内容暂未实现</div>} />
|
||||
</Routes>
|
||||
{/* 顶部标题栏,位于左侧菜单栏之上 */}
|
||||
<TopBar
|
||||
onToggleMenu={() => setMobileMenuOpen((v) => !v)}
|
||||
onToggleInput={() => isHome && setInputOpen((v) => !v)}
|
||||
isHome={isHome}
|
||||
/>
|
||||
{/* 下方为主布局:左侧菜单 + 右侧内容 */}
|
||||
<Layout ref={layoutShellRef as any}>
|
||||
<SiderMenu
|
||||
onNavigate={handleNavigate}
|
||||
selectedKey={selectedKey}
|
||||
mobileOpen={mobileMenuOpen}
|
||||
onMobileToggle={(open) => setMobileMenuOpen(open)}
|
||||
/>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<MainContent
|
||||
inputOpen={inputOpen}
|
||||
onCloseInput={() => setInputOpen(false)}
|
||||
containerEl={layoutShellRef.current}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="/resources" element={<ResourceList />} />
|
||||
<Route path="/menu2" element={<div style={{ padding: 32, color: '#cbd5e1' }}>菜单2的内容暂未实现</div>} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Layout, Typography } from 'antd';
|
||||
import PeopleForm from './PeopleForm.tsx';
|
||||
import InputPanel from './InputPanel.tsx';
|
||||
import HintText from './HintText.tsx';
|
||||
import InputDrawer from './InputDrawer.tsx';
|
||||
import './MainContent.css';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
const MainContent: React.FC = () => {
|
||||
type Props = { inputOpen?: boolean; onCloseInput?: () => void; containerEl?: HTMLElement | null };
|
||||
const MainContent: React.FC<Props> = ({ inputOpen = false, onCloseInput, containerEl }) => {
|
||||
const [formData, setFormData] = React.useState<any>(null);
|
||||
|
||||
const handleInputResult = (data: any) => {
|
||||
@@ -27,10 +27,13 @@ const MainContent: React.FC = () => {
|
||||
<PeopleForm initialData={formData} />
|
||||
</div>
|
||||
|
||||
<div className="input-panel-wrapper">
|
||||
<InputPanel onResult={handleInputResult} />
|
||||
<HintText />
|
||||
</div>
|
||||
{/* 首页右侧输入抽屉,仅在顶栏点击后弹出;挂载到标题栏下方容器 */}
|
||||
<InputDrawer
|
||||
open={inputOpen}
|
||||
onClose={onCloseInput || (() => {})}
|
||||
onResult={handleInputResult}
|
||||
containerEl={containerEl}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,14 +6,19 @@ import './SiderMenu.css';
|
||||
const { Sider } = Layout;
|
||||
|
||||
// 新增:支持外部导航回调 + 受控选中态
|
||||
type Props = { onNavigate?: (key: string) => void; selectedKey?: string };
|
||||
type Props = {
|
||||
onNavigate?: (key: string) => void;
|
||||
selectedKey?: string;
|
||||
mobileOpen?: boolean; // 外部控制移动端抽屉开关
|
||||
onMobileToggle?: (open: boolean) => void; // 顶栏触发开关
|
||||
};
|
||||
|
||||
const SiderMenu: React.FC<Props> = ({ onNavigate, selectedKey }) => {
|
||||
const SiderMenu: React.FC<Props> = ({ onNavigate, selectedKey, mobileOpen, onMobileToggle }) => {
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isMobile = !screens.md;
|
||||
const [collapsed, setCollapsed] = React.useState(false);
|
||||
const [selectedKeys, setSelectedKeys] = React.useState<string[]>(['home']);
|
||||
const [mobileOpen, setMobileOpen] = React.useState(false);
|
||||
const [internalMobileOpen, setInternalMobileOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setCollapsed(isMobile);
|
||||
@@ -34,20 +39,25 @@ const SiderMenu: React.FC<Props> = ({ onNavigate, selectedKey }) => {
|
||||
|
||||
// 移动端:使用 Drawer 覆盖主内容
|
||||
if (isMobile) {
|
||||
const open = mobileOpen ?? internalMobileOpen;
|
||||
const setOpen = (v: boolean) => (onMobileToggle ? onMobileToggle(v) : setInternalMobileOpen(v));
|
||||
const showInternalTrigger = !onMobileToggle; // 若无外部控制,则显示内部按钮
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className="mobile-menu-trigger"
|
||||
type="default"
|
||||
icon={<MenuOutlined />}
|
||||
onClick={() => setMobileOpen((open) => !open)}
|
||||
/>
|
||||
{showInternalTrigger && (
|
||||
<Button
|
||||
className="mobile-menu-trigger"
|
||||
type="default"
|
||||
icon={<MenuOutlined />}
|
||||
onClick={() => setInternalMobileOpen((o) => !o)}
|
||||
/>
|
||||
)}
|
||||
<Drawer
|
||||
className="mobile-menu-drawer"
|
||||
placement="left"
|
||||
width="100%"
|
||||
open={mobileOpen}
|
||||
onClose={() => setMobileOpen(false)}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<div className="sider-header">
|
||||
@@ -64,7 +74,7 @@ const SiderMenu: React.FC<Props> = ({ onNavigate, selectedKey }) => {
|
||||
onClick={({ key }) => {
|
||||
const k = String(key);
|
||||
setSelectedKeys([k]);
|
||||
setMobileOpen(false); // 选择后自动收起
|
||||
setOpen(false); // 选择后自动收起
|
||||
onNavigate?.(k);
|
||||
}}
|
||||
items={items}
|
||||
|
||||
46
src/components/TopBar.css
Normal file
46
src/components/TopBar.css
Normal file
@@ -0,0 +1,46 @@
|
||||
/* 顶部网站标题栏样式 */
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2000; /* 保证位于抽屉与遮罩之上 */
|
||||
height: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr 56px;
|
||||
align-items: center;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.35) 100%);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.topbar-title {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
letter-spacing: 1px;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.topbar-left,
|
||||
.topbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: #93c5fd; /* 主题蓝 */
|
||||
}
|
||||
.icon-btn:hover { background: rgba(255,255,255,0.08); }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.topbar { grid-template-columns: 56px 1fr 56px; }
|
||||
}
|
||||
41
src/components/TopBar.tsx
Normal file
41
src/components/TopBar.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { Grid } from 'antd';
|
||||
import { MenuOutlined, RobotOutlined } from '@ant-design/icons';
|
||||
import './TopBar.css';
|
||||
|
||||
type Props = {
|
||||
onToggleMenu?: () => void;
|
||||
onToggleInput?: () => void;
|
||||
isHome?: boolean;
|
||||
};
|
||||
|
||||
const TopBar: React.FC<Props> = ({ onToggleMenu, onToggleInput, isHome }) => {
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isMobile = !screens.md;
|
||||
|
||||
return (
|
||||
<div className="topbar">
|
||||
<div className="topbar-left">
|
||||
{isMobile && (
|
||||
<button className="icon-btn" onClick={onToggleMenu} aria-label="打开/收起菜单">
|
||||
<MenuOutlined />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="topbar-title" role="heading" aria-level={1}>
|
||||
I FIND U
|
||||
</div>
|
||||
|
||||
<div className="topbar-right">
|
||||
{isHome && (
|
||||
<button className="icon-btn" onClick={onToggleInput} aria-label="打开/收起输入">
|
||||
<RobotOutlined />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopBar;
|
||||
Reference in New Issue
Block a user