添加前端项目和更新.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:
parent
805c4597af
commit
6120d051aa
7
.gitignore
vendored
7
.gitignore
vendored
@ -35,3 +35,10 @@ logs/
|
||||
# 临时文件
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# 前端构建产物和依赖
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
web/build/
|
||||
web/.vite/
|
||||
web/.cache/
|
||||
|
||||
20
web/.eslintrc.cjs
Normal file
20
web/.eslintrc.cjs
Normal 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
30
web/.gitignore
vendored
Normal 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
14
web/index.html
Normal 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
33
web/package.json
Normal 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
17
web/src/App.tsx
Normal 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
|
||||
3
web/src/components/DemoButton.module.css
Normal file
3
web/src/components/DemoButton.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
34
web/src/components/DemoButton.tsx
Normal file
34
web/src/components/DemoButton.tsx
Normal 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
18
web/src/index.css
Normal 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
10
web/src/main.tsx
Normal 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>,
|
||||
)
|
||||
12
web/src/pages/About.module.css
Normal file
12
web/src/pages/About.module.css
Normal 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
48
web/src/pages/About.tsx
Normal 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
|
||||
13
web/src/pages/Home.module.css
Normal file
13
web/src/pages/Home.module.css
Normal 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
95
web/src/pages/Home.tsx
Normal 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
79
web/src/utils/common.ts
Normal 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
75
web/src/utils/request.ts
Normal 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
1
web/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
31
web/tsconfig.json
Normal file
31
web/tsconfig.json
Normal 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
10
web/tsconfig.node.json
Normal 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
26
web/vite.config.ts
Normal 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
2067
web/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user