feat: 初始化 Vite React 聊天应用骨架

搭建 React + Vite + React Router + TailwindCSS + Ant Design 基础工程,并通过 vite-plugin-pages 实现文件路由。新增首页响应式聊天布局,移动端在小于 750px 时使用左侧抽屉侧边栏。

Made-with: Cursor
This commit is contained in:
2026-04-01 01:51:04 +08:00
parent d476042856
commit e48462cf4a
13 changed files with 1639 additions and 1 deletions

View File

@@ -1,3 +1,33 @@
# chat-one-web
集成多个AI聊天工具
基于 React + Vite + React Router + TailwindCSS + Ant Design 的 ChatOne Web 项目。
## 技术栈
- React
- Vite
- React Router
- vite-plugin-pages基于文件自动生成路由
- TailwindCSS
- Ant Design
## 本地开发
```bash
yarn
yarn dev
```
默认启动地址:`http://localhost:5173`
## 构建
```bash
yarn build
```
## 路由说明
本项目使用 `vite-plugin-pages` 自动扫描 `src/pages` 目录生成路由:
- `src/pages/index.tsx` -> `/`

12
index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chat One Web</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "chat-one-web",
"version": "1.0.0",
"description": "ChatOne web端",
"type": "module",
"repository": "git@github.com:alboped/chat-one-web.git",
"author": "alboped <shi_zhaojun@aliyun.com>",
"license": "UNLICENSED",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"antd": "^6.3.5",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.2"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"tailwindcss": "^4.2.2",
"typescript": "^6.0.2",
"vite": "^8.0.3",
"vite-plugin-pages": "^0.33.3"
}
}

6
src/App.tsx Normal file
View File

@@ -0,0 +1,6 @@
import { useRoutes } from "react-router-dom";
import routes from "~react-pages";
export default function App() {
return useRoutes(routes);
}

8
src/index.css Normal file
View File

@@ -0,0 +1,8 @@
@import "tailwindcss";
html,
body,
#root {
margin: 0;
min-height: 100%;
}

14
src/main.tsx Normal file
View File

@@ -0,0 +1,14 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "antd/dist/reset.css";
import "./index.css";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
);

157
src/pages/index.tsx Normal file
View File

@@ -0,0 +1,157 @@
import { useEffect, useMemo, useState } from "react";
import { Button, Drawer, Input, Layout, List, Space, Typography } from "antd";
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
MessageOutlined,
SettingOutlined,
UserOutlined,
} from "@ant-design/icons";
const { Header, Sider, Content } = Layout;
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 [isMobile, setIsMobile] = useState(() => window.innerWidth < 750);
const [inputValue, setInputValue] = useState("");
const menuItems = useMemo(
() => [
{ icon: <MessageOutlined />, label: "新建会话" },
{ icon: <UserOutlined />, label: "我的对话" },
{ icon: <SettingOutlined />, label: "设置" },
],
[],
);
useEffect(() => {
const onResize = () => {
const mobile = window.innerWidth < 750;
setIsMobile(mobile);
if (!mobile) {
setMobileSidebarOpen(false);
}
};
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}, []);
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 direction="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" }}>
{!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);
}
}}
/>
<Typography.Text type="secondary">ChatOne Web</Typography.Text>
</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">
<List
split={false}
dataSource={mockMessages}
renderItem={(item) => (
<List.Item>
<div
className={`max-w-[80%] rounded-lg px-3 py-2 ${
item.role === "user"
? "ml-auto bg-blue-50 border border-blue-100"
: "bg-gray-50 border border-gray-100"
}`}
>
<Typography.Text>{item.content}</Typography.Text>
</div>
</List.Item>
)}
/>
</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}
width={260}
>
{sidebarContent}
</Drawer>
</Layout>
);
}

2
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pages/client-react" />

18
tsconfig.app.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

15
tsconfig.node.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true
},
"include": ["vite.config.ts"]
}

15
vite.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import Pages from "vite-plugin-pages";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [
react(),
Pages({
dirs: "src/pages",
extensions: ["tsx"],
}),
tailwindcss(),
],
});

1322
yarn.lock Normal file

File diff suppressed because it is too large Load Diff