chore: 增加代码规范和提交校验
Made-with: Cursor
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
5
.husky/pre-commit
Normal file
5
.husky/pre-commit
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
yarn lint
|
||||||
|
yarn format:check
|
||||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
yarn.lock
|
||||||
|
*.min.js
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
36
eslint.config.js
Normal file
36
eslint.config.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import eslint from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import react from "eslint-plugin-react";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import prettier from "eslint-config-prettier";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist/**", "node_modules/**"] },
|
||||||
|
{
|
||||||
|
settings: {
|
||||||
|
react: { version: "detect" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
react.configs.flat.recommended,
|
||||||
|
react.configs.flat["jsx-runtime"],
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
plugins: {
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prettier,
|
||||||
|
);
|
||||||
17
package.json
17
package.json
@@ -10,7 +10,12 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"antd": "^6.3.5",
|
"antd": "^6.3.5",
|
||||||
@@ -19,13 +24,23 @@
|
|||||||
"react-router-dom": "^7.13.2"
|
"react-router-dom": "^7.13.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^16.4.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
|
"typescript-eslint": "^8.58.0",
|
||||||
"vite": "^8.0.3",
|
"vite": "^8.0.3",
|
||||||
"vite-plugin-pages": "^0.33.3"
|
"vite-plugin-pages": "^0.33.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Button, Drawer, Input, Layout, List, Space, Typography } from "antd";
|
import { Button, Drawer, Input, Layout, Space, Typography } from "antd";
|
||||||
import {
|
import {
|
||||||
MenuFoldOutlined,
|
MenuFoldOutlined,
|
||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
|
const MOBILE_WIDTH = 750;
|
||||||
|
|
||||||
const mockMessages = [
|
const mockMessages = [
|
||||||
{ role: "assistant", content: "你好,我是 ChatOne 助手,有什么可以帮你?" },
|
{ role: "assistant", content: "你好,我是 ChatOne 助手,有什么可以帮你?" },
|
||||||
@@ -17,9 +18,14 @@ const mockMessages = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
|
// 桌面端侧边栏折叠状态(移动端不使用该状态)
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
// 移动端抽屉侧边栏显示状态
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(() => window.innerWidth < 750);
|
// 当前视口宽度(用于调试响应式是否生效)
|
||||||
|
const [viewportWidth, setViewportWidth] = useState(0);
|
||||||
|
// 是否为移动端(小于 750px)
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
|
||||||
const menuItems = useMemo(
|
const menuItems = useMemo(
|
||||||
@@ -32,33 +38,46 @@ export default function HomePage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onResize = () => {
|
const updateIsMobile = () => {
|
||||||
const mobile = window.innerWidth < 750;
|
// 兼容不同浏览器/缩放场景下的视口宽度读取
|
||||||
|
const viewportWidth = Math.min(
|
||||||
|
window.innerWidth,
|
||||||
|
document.documentElement.clientWidth || window.innerWidth,
|
||||||
|
);
|
||||||
|
setViewportWidth(viewportWidth);
|
||||||
|
const mobile = viewportWidth < MOBILE_WIDTH;
|
||||||
setIsMobile(mobile);
|
setIsMobile(mobile);
|
||||||
if (!mobile) {
|
if (!mobile) {
|
||||||
|
// 切回桌面端时关闭移动抽屉,避免状态残留
|
||||||
setMobileSidebarOpen(false);
|
setMobileSidebarOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("resize", onResize);
|
updateIsMobile();
|
||||||
|
window.addEventListener("resize", updateIsMobile);
|
||||||
|
window.addEventListener("orientationchange", updateIsMobile);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("resize", onResize);
|
window.removeEventListener("resize", updateIsMobile);
|
||||||
|
window.removeEventListener("orientationchange", updateIsMobile);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sidebarContent = (
|
const sidebarContent = (
|
||||||
<>
|
<>
|
||||||
|
{/* 侧边栏头部:桌面折叠时显示缩写,其他情况显示完整标题 */}
|
||||||
<div className="h-14 border-b border-gray-100 px-4 flex items-center">
|
<div className="h-14 border-b border-gray-100 px-4 flex items-center">
|
||||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||||
{collapsed && !isMobile ? "CO" : "ChatOne"}
|
{collapsed && !isMobile ? "CO" : "ChatOne"}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-3">
|
<div className="px-2 py-3">
|
||||||
<Space direction="vertical" className="w-full">
|
<Space orientation="vertical" className="w-full">
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<Button key={item.label} type="text" className="w-full text-left!">
|
<Button key={item.label} type="text" className="w-full text-left!">
|
||||||
<Space>
|
<Space>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
|
{/* 桌面折叠时隐藏文字;移动端抽屉内始终显示文字 */}
|
||||||
{(!collapsed || isMobile) && <span>{item.label}</span>}
|
{(!collapsed || isMobile) && <span>{item.label}</span>}
|
||||||
</Space>
|
</Space>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -70,6 +89,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: "100vh" }}>
|
<Layout style={{ minHeight: "100vh" }}>
|
||||||
|
{/* 桌面端固定侧边栏;移动端改为 Drawer 抽屉 */}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Sider trigger={null} collapsible collapsed={collapsed} theme="light">
|
<Sider trigger={null} collapsible collapsed={collapsed} theme="light">
|
||||||
{sidebarContent}
|
{sidebarContent}
|
||||||
@@ -91,16 +111,24 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
|
// 移动端点击按钮:打开左侧抽屉
|
||||||
setMobileSidebarOpen(true);
|
setMobileSidebarOpen(true);
|
||||||
} else {
|
} else {
|
||||||
|
// 桌面端点击按钮:切换侧边栏折叠
|
||||||
setCollapsed((v) => !v);
|
setCollapsed((v) => !v);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography.Text type="secondary">ChatOne Web</Typography.Text>
|
<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>
|
</Header>
|
||||||
|
|
||||||
<Content className="bg-gray-50 p-6">
|
<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="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">
|
<div className="border-b border-gray-100 px-5 py-3">
|
||||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||||
@@ -108,24 +136,23 @@ export default function HomePage() {
|
|||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto px-5 py-4">
|
<div className="flex-1 overflow-auto px-5 py-4 flex flex-col gap-2">
|
||||||
<List
|
{mockMessages.map((item, index) => (
|
||||||
split={false}
|
<div
|
||||||
dataSource={mockMessages}
|
key={index}
|
||||||
renderItem={(item) => (
|
className={`flex ${item.role === "user" ? "justify-end" : "justify-start"}`}
|
||||||
<List.Item>
|
>
|
||||||
<div
|
<div
|
||||||
className={`max-w-[80%] rounded-lg px-3 py-2 ${
|
className={`max-w-[80%] rounded-lg px-3 py-2 ${
|
||||||
item.role === "user"
|
item.role === "user"
|
||||||
? "ml-auto bg-blue-50 border border-blue-100"
|
? "bg-blue-50 border border-blue-100"
|
||||||
: "bg-gray-50 border border-gray-100"
|
: "bg-gray-50 border border-gray-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Typography.Text>{item.content}</Typography.Text>
|
<Typography.Text>{item.content}</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</List.Item>
|
</div>
|
||||||
)}
|
))}
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-gray-100 p-4">
|
<div className="border-t border-gray-100 p-4">
|
||||||
@@ -148,7 +175,7 @@ export default function HomePage() {
|
|||||||
closable
|
closable
|
||||||
onClose={() => setMobileSidebarOpen(false)}
|
onClose={() => setMobileSidebarOpen(false)}
|
||||||
open={isMobile && mobileSidebarOpen}
|
open={isMobile && mobileSidebarOpen}
|
||||||
width={260}
|
size={260}
|
||||||
>
|
>
|
||||||
{sidebarContent}
|
{sidebarContent}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||||
{ "path": "./tsconfig.app.json" },
|
|
||||||
{ "path": "./tsconfig.node.json" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user