添加前端项目和更新.gitignore配置

- 添加基于 Vite + React + TypeScript 的前端项目
- 配置 .gitignore 排除 node_modules 和构建产物
- 添加 web/.gitignore 用于前端项目的忽略规则
- 包含完整的源代码、配置文件和依赖锁定文件

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
yanlongqi 2025-11-03 14:22:48 +08:00
parent 805c4597af
commit 6120d051aa
21 changed files with 2643 additions and 0 deletions

7
.gitignore vendored
View File

@ -35,3 +35,10 @@ logs/
# 临时文件 # 临时文件
tmp/ tmp/
temp/ temp/
# 前端构建产物和依赖
web/node_modules/
web/dist/
web/build/
web/.vite/
web/.cache/

20
web/.eslintrc.cjs Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
},
}

30
web/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# 依赖
node_modules/
# 构建产物
dist/
build/
.vite/
# 缓存
.cache/
.parcel-cache/
# 环境变量
.env.local
.env.development.local
.env.test.local
.env.production.local
# 日志
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# 编辑器
.DS_Store
*.swp
*.swo
# 测试覆盖率
coverage/

14
web/index.html Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description" content="AnKao 移动端应用" />
<title>AnKao</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

33
web/package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "ankao-web",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3",
"antd-mobile": "^5.37.1",
"antd-mobile-icons": "^0.3.0",
"axios": "^1.6.5"
},
"devDependencies": {
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.3.3",
"vite": "^5.0.11"
}
}

17
web/src/App.tsx Normal file
View File

@ -0,0 +1,17 @@
import React from 'react'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'
const App: React.FC = () => {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
)
}
export default App

View File

@ -0,0 +1,3 @@
.container {
width: 100%;
}

View File

@ -0,0 +1,34 @@
import React from 'react'
import { Button, Space } from 'antd-mobile'
import styles from './DemoButton.module.css'
interface DemoButtonProps {
text?: string
onClick?: () => void
}
const DemoButton: React.FC<DemoButtonProps> = ({
text = '示例按钮',
onClick
}) => {
return (
<div className={styles.container}>
<Space direction="vertical" block>
<Button color="primary" block onClick={onClick}>
{text} - Primary
</Button>
<Button color="success" block onClick={onClick}>
{text} - Success
</Button>
<Button color="warning" block onClick={onClick}>
{text} - Warning
</Button>
<Button color="danger" block onClick={onClick}>
{text} - Danger
</Button>
</Space>
</div>
)
}
export default DemoButton

18
web/src/index.css Normal file
View File

@ -0,0 +1,18 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f5f5f5;
}
#root {
min-height: 100vh;
}

10
web/src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@ -0,0 +1,12 @@
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.content {
flex: 1;
overflow-y: auto;
padding: 16px;
}

48
web/src/pages/About.tsx Normal file
View File

@ -0,0 +1,48 @@
import React from 'react'
import { useNavigate } from 'react-router-dom'
import { NavBar, Card, List, Space, Button } from 'antd-mobile'
import styles from './About.module.css'
const About: React.FC = () => {
const navigate = useNavigate()
return (
<div className={styles.container}>
<NavBar onBack={() => navigate(-1)}></NavBar>
<div className={styles.content}>
<Card title="项目信息">
<Space direction="vertical" block>
<List>
<List.Item extra="1.0.0"></List.Item>
<List.Item extra="AnKao Team"></List.Item>
<List.Item extra="MIT"></List.Item>
</List>
<Card title="功能特性">
<List>
<List.Item> </List.Item>
<List.Item> TypeScript </List.Item>
<List.Item> Vite </List.Item>
<List.Item> antd-mobile </List.Item>
<List.Item> React Router </List.Item>
<List.Item> API </List.Item>
</List>
</Card>
<Button
color="primary"
size="large"
block
onClick={() => navigate('/')}
>
</Button>
</Space>
</Card>
</div>
</div>
)
}
export default About

View File

@ -0,0 +1,13 @@
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.content {
flex: 1;
overflow-y: auto;
padding: 16px;
padding-bottom: 60px;
}

95
web/src/pages/Home.tsx Normal file
View File

@ -0,0 +1,95 @@
import React from 'react'
import { useNavigate } from 'react-router-dom'
import {
Button,
NavBar,
TabBar,
List,
Card,
Space,
Toast
} from 'antd-mobile'
import {
AppOutline,
UnorderedListOutline,
UserOutline
} from 'antd-mobile-icons'
import styles from './Home.module.css'
const Home: React.FC = () => {
const navigate = useNavigate()
const [activeKey, setActiveKey] = React.useState('home')
const handleButtonClick = () => {
Toast.show({
content: '欢迎使用 AnKao!',
duration: 2000,
})
}
const tabs = [
{
key: 'home',
title: '首页',
icon: <AppOutline />,
},
{
key: 'list',
title: '列表',
icon: <UnorderedListOutline />,
},
{
key: 'profile',
title: '我的',
icon: <UserOutline />,
},
]
return (
<div className={styles.container}>
<NavBar back={null}>AnKao</NavBar>
<div className={styles.content}>
<Card title="欢迎使用 AnKao">
<Space direction="vertical" block>
<p> React + TypeScript + Vite + antd-mobile </p>
<List header="技术栈">
<List.Item> React 18</List.Item>
<List.Item>📘 TypeScript</List.Item>
<List.Item> Vite</List.Item>
<List.Item>📱 antd-mobile 5</List.Item>
<List.Item>🔧 Go Gin </List.Item>
</List>
<Button
color="primary"
size="large"
block
onClick={handleButtonClick}
>
</Button>
<Button
color="default"
size="large"
block
onClick={() => navigate('/about')}
>
</Button>
</Space>
</Card>
</div>
<TabBar activeKey={activeKey} onChange={setActiveKey}>
{tabs.map(item => (
<TabBar.Item key={item.key} icon={item.icon} title={item.title} />
))}
</TabBar>
</div>
)
}
export default Home

79
web/src/utils/common.ts Normal file
View File

@ -0,0 +1,79 @@
/**
*
*/
export const formatDate = (date: Date | string | number, format = 'YYYY-MM-DD HH:mm:ss'): string => {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
}
/**
*
*/
export const debounce = <T extends (...args: any[]) => any>(
fn: T,
delay: number
): ((...args: Parameters<T>) => void) => {
let timer: NodeJS.Timeout | null = null
return (...args: Parameters<T>) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn(...args)
}, delay)
}
}
/**
*
*/
export const throttle = <T extends (...args: any[]) => any>(
fn: T,
delay: number
): ((...args: Parameters<T>) => void) => {
let lastTime = 0
return (...args: Parameters<T>) => {
const now = Date.now()
if (now - lastTime >= delay) {
fn(...args)
lastTime = now
}
}
}
/**
*
*/
export const storage = {
get: <T = any>(key: string): T | null => {
const value = localStorage.getItem(key)
if (value) {
try {
return JSON.parse(value) as T
} catch {
return value as T
}
}
return null
},
set: (key: string, value: any): void => {
localStorage.setItem(key, JSON.stringify(value))
},
remove: (key: string): void => {
localStorage.removeItem(key)
},
clear: (): void => {
localStorage.clear()
},
}

75
web/src/utils/request.ts Normal file
View File

@ -0,0 +1,75 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
// 创建 axios 实例
const instance: AxiosInstance = axios.create({
baseURL: '/api', // 通过 Vite 代理转发到 Go 后端
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// 可以在这里添加 token 等认证信息
const token = localStorage.getItem('token')
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
instance.interceptors.response.use(
(response: AxiosResponse) => {
return response.data
},
(error) => {
// 统一错误处理
if (error.response) {
switch (error.response.status) {
case 401:
// 未授权,跳转到登录页
console.error('未授权,请登录')
break
case 403:
console.error('没有权限访问')
break
case 404:
console.error('请求的资源不存在')
break
case 500:
console.error('服务器错误')
break
default:
console.error('请求失败:', error.response.data)
}
} else {
console.error('网络错误:', error.message)
}
return Promise.reject(error)
}
)
// 封装请求方法
export const request = {
get: <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
return instance.get(url, config)
},
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
return instance.post(url, data, config)
},
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
return instance.put(url, data, config)
},
delete: <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
return instance.delete(url, config)
},
}
export default instance

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

@ -0,0 +1 @@
/// <reference types="vite/client" />

31
web/tsconfig.json Normal file
View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
web/tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

26
web/vite.config.ts Normal file
View File

@ -0,0 +1,26 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
build: {
outDir: 'dist',
assetsDir: 'assets',
},
})

2067
web/yarn.lock Normal file

File diff suppressed because it is too large Load Diff