This commit is contained in:
wxhao
2025-10-31 14:26:32 +08:00
parent ba4e7b55e2
commit 43534e3ef4
11 changed files with 5738 additions and 31 deletions

View File

@@ -3,9 +3,10 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<title>Main</title>
</head>
<body>
<h1>hello world</h1>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>

5337
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,19 +10,36 @@
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint src/**/*.{js,jsx}",
"lint:fix": "eslint src/**/*.{js,jsx} --fix",
"format": "prettier --write src/**/*.{js,jsx,json,css,md}"
"format": "prettier --write src/**/*.{js,jsx,json,css,md}",
"dev": "webpack serve --mode development --open"
},
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@babel/preset-react": "^7.28.5",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"babel-loader": "^10.0.0",
"css-loader": "^7.1.2",
"eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.4.0",
"prettier": "^3.6.2",
"webpack": "^5.102.1"
"sass": "^1.93.2",
"sass-loader": "^16.0.6",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.4",
"typescript": "^5.9.3",
"webpack": "^5.102.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2"
},
"dependencies": {
"axios": "^1.13.1",
"react": "^19.2.0",
"react-dom": "^19.2.0"
"react-dom": "^19.2.0",
"zustand": "^5.0.8"
}
}

View File

@@ -1,8 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
function App() {
return <h1>hello world</h1>;
}
ReactDOM.render(<App />, document.body);

76
src/index.tsx Normal file
View File

@@ -0,0 +1,76 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './styles/main.scss';
import { useCounterStore } from './store/useCounterStore';
import { fetchTodos, type Todo } from './services/api';
function App() {
const { count, increment, decrement, reset } = useCounterStore();
const [todos, setTodos] = React.useState<Todo[]>([]);
const [loading, setLoading] = React.useState<boolean>(false);
const [error, setError] = React.useState<string | null>(null);
// 加载示例数据
React.useEffect(() => {
const loadTodos = async () => {
try {
setLoading(true);
setError(null);
const data = await fetchTodos();
if (Array.isArray(data)) {
setTodos(data.slice(0, 5)); // 只显示前5个
}
} catch (err) {
setError('Failed to load todos');
console.error(err);
} finally {
setLoading(false);
}
};
loadTodos();
}, []);
return (
<div className="app-container">
<h1>Hello TypeScript + React + Zustand + Axios!</h1>
{/* Zustand 计数器示例 */}
<div className="counter-section">
<h2>Counter: {count}</h2>
<div className="counter-buttons">
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
<button onClick={increment}>+</button>
</div>
</div>
{/* Axios API 示例 */}
<div className="todos-section">
<h2>Sample Todos</h2>
{loading && <p>Loading...</p>}
{error && <p className="error">{error}</p>}
{!loading && !error && (
<ul className="todo-list">
{todos.map((todo) => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
{todo.title}
</li>
))}
</ul>
)}
</div>
</div>
);
}
// 使用React 18+的createRoot API
const rootElement = document.getElementById('root');
if (rootElement) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}

58
src/services/api.ts Normal file
View File

@@ -0,0 +1,58 @@
import axios from 'axios';
// 定义Todo接口
export interface Todo {
id: number;
title: string;
completed: boolean;
userId?: number;
}
// 创建axios实例
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com', // 使用示例API
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 可以在这里添加token等认证信息
// const token = localStorage.getItem('token');
// if (token) {
// config.headers.Authorization = `Bearer ${token}`;
// }
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
console.error('API Error:', error);
return Promise.reject(error);
}
);
// 示例API方法
export const fetchTodos = async (): Promise<Todo[]> => {
// 直接使用axios而不是我们的拦截器实例以确保类型正确
const response = await axios.get<Todo[]>('https://jsonplaceholder.typicode.com/todos');
return response.data;
};
export const fetchTodo = async (id: number): Promise<Todo> => {
const response = await axios.get<Todo>(`https://jsonplaceholder.typicode.com/todos/${id}`);
return response.data;
};
export default api;

View File

@@ -0,0 +1,17 @@
import { create } from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
setCount: (count: number) => void;
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: Math.max(0, state.count - 1) })),
reset: () => set({ count: 0 }),
setCount: (count) => set({ count }),
}));

133
src/styles/main.scss Normal file
View File

@@ -0,0 +1,133 @@
// 全局样式重置
* {
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;
color: #333;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.app-container {
background-color: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
max-width: 800px;
width: 100%;
animation: fadeIn 0.5s ease-in;
}
h1 {
font-size: 2.5rem;
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
}
h2 {
font-size: 1.8rem;
color: #34495e;
margin-bottom: 20px;
}
// 计数器部分样式
.counter-section {
text-align: center;
margin-bottom: 40px;
padding: 20px;
background-color: #ecf0f1;
border-radius: 8px;
}
.counter-buttons {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 15px;
}
button {
padding: 10px 20px;
font-size: 1rem;
border: none;
border-radius: 6px;
cursor: pointer;
background-color: #3498db;
color: white;
transition: background-color 0.3s ease;
&:hover {
background-color: #2980b9;
}
&:active {
transform: translateY(1px);
}
}
// Todo列表部分样式
.todos-section {
& h2 {
text-align: center;
}
& p {
text-align: center;
margin-bottom: 20px;
}
}
.todo-list {
list-style: none;
padding: 0;
& li {
padding: 12px 16px;
margin-bottom: 8px;
background-color: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #3498db;
transition: all 0.2s ease;
&:hover {
background-color: #e9ecef;
transform: translateX(4px);
}
&.completed {
text-decoration: line-through;
color: #6c757d;
border-left-color: #28a745;
}
}
}
.error {
color: #dc3545;
font-weight: bold;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

28
tsconfig.json Normal file
View File

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

10
tsconfig.node.json Normal file
View File

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

74
webpack.config.js Normal file
View File

@@ -0,0 +1,74 @@
import path from 'path';
import { fileURLToPath } from 'url';
// 处理ES模块中的__dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/index.tsx',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
clean: true,
},
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: {
static: {
directory: __dirname,
},
hot: true,
open: true,
port: 3000,
historyApiFallback: true,
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
compilerOptions: {
noEmit: false
}
}
}
],
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: 'defaults' }],
'@babel/preset-react',
],
},
},
},
{
test: /\.(scss|sass)$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
};