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:
2025-10-28 16:46:45 +08:00
parent 1284698948
commit bd817279db
11 changed files with 280 additions and 33 deletions

View File

@@ -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 });
}

View File

@@ -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);
});
}

View 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; /* 移动端更紧凑 */
}
}

View 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;

View File

@@ -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 {

View File

@@ -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}
/>

View File

@@ -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,19 +44,44 @@ const LayoutWrapper: React.FC = () => {
navigate('/');
break;
}
// 切换页面时收起输入抽屉
setInputOpen(false);
};
return (
<Layout className="layout-wrapper app-root">
<SiderMenu onNavigate={handleNavigate} selectedKey={selectedKey} />
{/* 顶部标题栏,位于左侧菜单栏之上 */}
<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 />} />
<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>
);
};

View File

@@ -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>
);
};

View File

@@ -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 (
<>
{showInternalTrigger && (
<Button
className="mobile-menu-trigger"
type="default"
icon={<MenuOutlined />}
onClick={() => setMobileOpen((open) => !open)}
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
View 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
View 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;