init2
This commit is contained in:
@@ -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
5337
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
76
src/index.tsx
Normal 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
58
src/services/api.ts
Normal 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;
|
||||
17
src/store/useCounterStore.ts
Normal file
17
src/store/useCounterStore.ts
Normal 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
133
src/styles/main.scss
Normal 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
28
tsconfig.json
Normal 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
10
tsconfig.node.json
Normal 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
74
webpack.config.js
Normal 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'],
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user