187 lines
6.2 KiB
TypeScript
187 lines
6.2 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
||
import { Button, Drawer, Input, Layout, Space, Typography } from "antd";
|
||
import {
|
||
MenuFoldOutlined,
|
||
MenuUnfoldOutlined,
|
||
MessageOutlined,
|
||
SettingOutlined,
|
||
UserOutlined,
|
||
} from "@ant-design/icons";
|
||
|
||
const { Header, Sider, Content } = Layout;
|
||
const MOBILE_WIDTH = 750;
|
||
|
||
const mockMessages = [
|
||
{ role: "assistant", content: "你好,我是 ChatOne 助手,有什么可以帮你?" },
|
||
{ role: "user", content: "请帮我总结今天的工作内容。" },
|
||
{ role: "assistant", content: "当然可以,请先告诉我今天完成了哪些任务。" },
|
||
];
|
||
|
||
export default function HomePage() {
|
||
// 桌面端侧边栏折叠状态(移动端不使用该状态)
|
||
const [collapsed, setCollapsed] = useState(false);
|
||
// 移动端抽屉侧边栏显示状态
|
||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||
// 当前视口宽度(用于调试响应式是否生效)
|
||
const [viewportWidth, setViewportWidth] = useState(0);
|
||
// 是否为移动端(小于 750px)
|
||
const [isMobile, setIsMobile] = useState(false);
|
||
const [inputValue, setInputValue] = useState("");
|
||
|
||
const menuItems = useMemo(
|
||
() => [
|
||
{ icon: <MessageOutlined />, label: "新建会话" },
|
||
{ icon: <UserOutlined />, label: "我的对话" },
|
||
{ icon: <SettingOutlined />, label: "设置" },
|
||
],
|
||
[],
|
||
);
|
||
|
||
useEffect(() => {
|
||
const updateIsMobile = () => {
|
||
// 兼容不同浏览器/缩放场景下的视口宽度读取
|
||
const viewportWidth = Math.min(
|
||
window.innerWidth,
|
||
document.documentElement.clientWidth || window.innerWidth,
|
||
);
|
||
setViewportWidth(viewportWidth);
|
||
const mobile = viewportWidth < MOBILE_WIDTH;
|
||
setIsMobile(mobile);
|
||
if (!mobile) {
|
||
// 切回桌面端时关闭移动抽屉,避免状态残留
|
||
setMobileSidebarOpen(false);
|
||
}
|
||
};
|
||
|
||
updateIsMobile();
|
||
window.addEventListener("resize", updateIsMobile);
|
||
window.addEventListener("orientationchange", updateIsMobile);
|
||
|
||
return () => {
|
||
window.removeEventListener("resize", updateIsMobile);
|
||
window.removeEventListener("orientationchange", updateIsMobile);
|
||
};
|
||
}, []);
|
||
|
||
const sidebarContent = (
|
||
<>
|
||
{/* 侧边栏头部:桌面折叠时显示缩写,其他情况显示完整标题 */}
|
||
<div className="h-14 border-b border-gray-100 px-4 flex items-center">
|
||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||
{collapsed && !isMobile ? "CO" : "ChatOne"}
|
||
</Typography.Title>
|
||
</div>
|
||
<div className="px-2 py-3">
|
||
<Space orientation="vertical" className="w-full">
|
||
{menuItems.map((item) => (
|
||
<Button key={item.label} type="text" className="w-full text-left!">
|
||
<Space>
|
||
{item.icon}
|
||
{/* 桌面折叠时隐藏文字;移动端抽屉内始终显示文字 */}
|
||
{(!collapsed || isMobile) && <span>{item.label}</span>}
|
||
</Space>
|
||
</Button>
|
||
))}
|
||
</Space>
|
||
</div>
|
||
</>
|
||
);
|
||
|
||
return (
|
||
<Layout style={{ minHeight: "100vh" }}>
|
||
{/* 桌面端固定侧边栏;移动端改为 Drawer 抽屉 */}
|
||
{!isMobile && (
|
||
<Sider trigger={null} collapsible collapsed={collapsed} theme="light">
|
||
{sidebarContent}
|
||
</Sider>
|
||
)}
|
||
|
||
<Layout>
|
||
<Header className="bg-white! px-4! border-b! border-gray-100! flex items-center justify-between">
|
||
<Button
|
||
type="text"
|
||
icon={
|
||
isMobile ? (
|
||
<MenuUnfoldOutlined />
|
||
) : collapsed ? (
|
||
<MenuUnfoldOutlined />
|
||
) : (
|
||
<MenuFoldOutlined />
|
||
)
|
||
}
|
||
onClick={() => {
|
||
if (isMobile) {
|
||
// 移动端点击按钮:打开左侧抽屉
|
||
setMobileSidebarOpen(true);
|
||
} else {
|
||
// 桌面端点击按钮:切换侧边栏折叠
|
||
setCollapsed((v) => !v);
|
||
}
|
||
}}
|
||
/>
|
||
<Space size={12}>
|
||
<Typography.Text type="secondary">ChatOne Web</Typography.Text>
|
||
<Typography.Text type="secondary">
|
||
{`W:${viewportWidth}px / ${isMobile ? "mobile" : "desktop"}`}
|
||
</Typography.Text>
|
||
</Space>
|
||
</Header>
|
||
|
||
<Content className="bg-gray-50 p-6">
|
||
{/* 聊天窗口:消息列表 + 底部输入区 */}
|
||
<div className="h-[calc(100vh-112px)] rounded-lg bg-white border border-gray-100 flex flex-col">
|
||
<div className="border-b border-gray-100 px-5 py-3">
|
||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||
聊天窗口
|
||
</Typography.Title>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-auto px-5 py-4 flex flex-col gap-2">
|
||
{mockMessages.map((item, index) => (
|
||
<div
|
||
key={index}
|
||
className={`flex ${item.role === "user" ? "justify-end" : "justify-start"}`}
|
||
>
|
||
<div
|
||
className={`max-w-[80%] rounded-lg px-3 py-2 ${
|
||
item.role === "user"
|
||
? "bg-blue-50 border border-blue-100"
|
||
: "bg-gray-50 border border-gray-100"
|
||
}`}
|
||
>
|
||
<Typography.Text>{item.content}</Typography.Text>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="border-t border-gray-100 p-4">
|
||
<Space.Compact className="w-full">
|
||
<Input
|
||
value={inputValue}
|
||
onChange={(e) => setInputValue(e.target.value)}
|
||
placeholder="输入消息..."
|
||
/>
|
||
<Button type="primary">发送</Button>
|
||
</Space.Compact>
|
||
</div>
|
||
</div>
|
||
</Content>
|
||
</Layout>
|
||
|
||
<Drawer
|
||
title="ChatOne"
|
||
placement="left"
|
||
closable
|
||
onClose={() => setMobileSidebarOpen(false)}
|
||
open={isMobile && mobileSidebarOpen}
|
||
size={260}
|
||
>
|
||
{sidebarContent}
|
||
|
||
{sidebarContent}
|
||
</Drawer>
|
||
</Layout>
|
||
);
|
||
}
|