feat: 初始化 Vite React 聊天应用骨架
搭建 React + Vite + React Router + TailwindCSS + Ant Design 基础工程,并通过 vite-plugin-pages 实现文件路由。新增首页响应式聊天布局,移动端在小于 750px 时使用左侧抽屉侧边栏。 Made-with: Cursor
This commit is contained in:
32
README.md
32
README.md
@@ -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
12
index.html
Normal 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
32
package.json
Normal 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
6
src/App.tsx
Normal 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
8
src/index.css
Normal file
@@ -0,0 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
14
src/main.tsx
Normal file
14
src/main.tsx
Normal 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
157
src/pages/index.tsx
Normal 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
2
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-pages/client-react" />
|
||||
18
tsconfig.app.json
Normal file
18
tsconfig.app.json
Normal 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
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
15
tsconfig.node.json
Normal file
15
tsconfig.node.json
Normal 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
15
vite.config.ts
Normal 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(),
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user