complete init

This commit is contained in:
wxhao
2025-10-31 15:12:57 +08:00
parent 43534e3ef4
commit 2ef5c6b3f5
16 changed files with 2424 additions and 119 deletions

14
index.tsx Normal file
View File

@@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './src/App';
// 使用React 18+的createRoot API
const rootElement = document.getElementById('root');
if (rootElement) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}

2112
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,8 @@
"lint": "eslint src/**/*.{js,jsx}",
"lint:fix": "eslint src/**/*.{js,jsx} --fix",
"format": "prettier --write src/**/*.{js,jsx,json,css,md}",
"dev": "webpack serve --mode development --open"
"dev": "webpack serve --mode development --open",
"build": "webpack --mode production"
},
"devDependencies": {
"@babel/core": "^7.28.5",
@@ -19,6 +20,7 @@
"@babel/preset-react": "^7.28.5",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0",
"css-loader": "^7.1.2",
"eslint": "^9.38.0",
@@ -26,10 +28,14 @@
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.4.0",
"mini-css-extract-plugin": "^2.9.4",
"postcss": "^8.5.6",
"postcss-loader": "^8.2.0",
"prettier": "^3.6.2",
"sass": "^1.93.2",
"sass-loader": "^16.0.6",
"style-loader": "^4.0.0",
"tailwindcss": "^4.1.16",
"ts-loader": "^9.5.4",
"typescript": "^5.9.3",
"webpack": "^5.102.1",
@@ -37,9 +43,14 @@
"webpack-dev-server": "^5.2.2"
},
"dependencies": {
"@chakra-ui/react": "^3.28.0",
"@emotion/react": "^11.14.0",
"@tailwindcss/postcss": "^4.1.16",
"axios": "^1.13.1",
"next-themes": "^0.4.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-icons": "^5.5.0",
"zustand": "^5.0.8"
}
}

6
postcss.config.mjs Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
autoprefixer: {},
}
}

14
src/App.tsx Normal file
View File

@@ -0,0 +1,14 @@
import Demo from '@/pages/demo';
import { Provider } from '@/components/ui/provider';
import './styles/main.scss';
export default function App() {
return (
<Provider>
<div className="app-container">
<Demo />
</div>
</Provider>
);
}

View File

@@ -0,0 +1,108 @@
"use client"
import type { IconButtonProps, SpanProps } from "@chakra-ui/react"
import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react"
import { ThemeProvider, useTheme } from "next-themes"
import type { ThemeProviderProps } from "next-themes"
import * as React from "react"
import { LuMoon, LuSun } from "react-icons/lu"
export interface ColorModeProviderProps extends ThemeProviderProps {}
export function ColorModeProvider(props: ColorModeProviderProps) {
return (
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
)
}
export type ColorMode = "light" | "dark"
export interface UseColorModeReturn {
colorMode: ColorMode
setColorMode: (colorMode: ColorMode) => void
toggleColorMode: () => void
}
export function useColorMode(): UseColorModeReturn {
const { resolvedTheme, setTheme, forcedTheme } = useTheme()
const colorMode = forcedTheme || resolvedTheme
const toggleColorMode = () => {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}
return {
colorMode: colorMode as ColorMode,
setColorMode: setTheme,
toggleColorMode,
}
}
export function useColorModeValue<T>(light: T, dark: T) {
const { colorMode } = useColorMode()
return colorMode === "dark" ? dark : light
}
export function ColorModeIcon() {
const { colorMode } = useColorMode()
return colorMode === "dark" ? <LuMoon /> : <LuSun />
}
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
export const ColorModeButton = React.forwardRef<
HTMLButtonElement,
ColorModeButtonProps
>(function ColorModeButton(props, ref) {
const { toggleColorMode } = useColorMode()
return (
<ClientOnly fallback={<Skeleton boxSize="9" />}>
<IconButton
onClick={toggleColorMode}
variant="ghost"
aria-label="Toggle color mode"
size="sm"
ref={ref}
{...props}
css={{
_icon: {
width: "5",
height: "5",
},
}}
>
<ColorModeIcon />
</IconButton>
</ClientOnly>
)
})
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(
function LightMode(props, ref) {
return (
<Span
color="fg"
display="contents"
className="chakra-theme light"
colorPalette="gray"
colorScheme="light"
ref={ref}
{...props}
/>
)
},
)
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(
function DarkMode(props, ref) {
return (
<Span
color="fg"
display="contents"
className="chakra-theme dark"
colorPalette="gray"
colorScheme="dark"
ref={ref}
{...props}
/>
)
},
)

View File

@@ -0,0 +1,15 @@
"use client"
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
import {
ColorModeProvider,
type ColorModeProviderProps,
} from "./color-mode"
export function Provider(props: ColorModeProviderProps) {
return (
<ChakraProvider value={defaultSystem}>
<ColorModeProvider {...props} />
</ChakraProvider>
)
}

View File

@@ -0,0 +1,43 @@
"use client"
import {
Toaster as ChakraToaster,
Portal,
Spinner,
Stack,
Toast,
createToaster,
} from "@chakra-ui/react"
export const toaster = createToaster({
placement: "bottom-end",
pauseOnPageIdle: true,
})
export const Toaster = () => {
return (
<Portal>
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
{(toast) => (
<Toast.Root width={{ md: "sm" }}>
{toast.type === "loading" ? (
<Spinner size="sm" color="blue.solid" />
) : (
<Toast.Indicator />
)}
<Stack gap="1" flex="1" maxWidth="100%">
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
{toast.description && (
<Toast.Description>{toast.description}</Toast.Description>
)}
</Stack>
{toast.action && (
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
)}
{toast.closable && <Toast.CloseTrigger />}
</Toast.Root>
)}
</ChakraToaster>
</Portal>
)
}

View File

@@ -0,0 +1,46 @@
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
import * as React from "react"
export interface TooltipProps extends ChakraTooltip.RootProps {
showArrow?: boolean
portalled?: boolean
portalRef?: React.RefObject<HTMLElement | null>
content: React.ReactNode
contentProps?: ChakraTooltip.ContentProps
disabled?: boolean
}
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
function Tooltip(props, ref) {
const {
showArrow,
children,
disabled,
portalled = true,
content,
contentProps,
portalRef,
...rest
} = props
if (disabled) return children
return (
<ChakraTooltip.Root {...rest}>
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
<Portal disabled={!portalled} container={portalRef}>
<ChakraTooltip.Positioner>
<ChakraTooltip.Content ref={ref} {...contentProps}>
{showArrow && (
<ChakraTooltip.Arrow>
<ChakraTooltip.ArrowTip />
</ChakraTooltip.Arrow>
)}
{content}
</ChakraTooltip.Content>
</ChakraTooltip.Positioner>
</Portal>
</ChakraTooltip.Root>
)
},
)

View File

@@ -1,76 +0,0 @@
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>
);
}

63
src/pages/demo.tsx Normal file
View File

@@ -0,0 +1,63 @@
import React from 'react';
import { useCounterStore } from '@/store/useCounterStore';
import { fetchTodos, type Todo } from '@/services/api';
export default function Demo() {
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>
<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>
);
}

View File

@@ -1,3 +1,6 @@
// 或者保留命名空间(更推荐,避免全局污染)
@use "tailwindcss";
// 全局样式重置
* {
margin: 0;

10
tailwind.config.js Normal file
View File

@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,js,ts,jsx,tsx,scss}", // 包含你的 .scss 文件
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -5,6 +5,9 @@
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"]
},
/* Bundler mode */
"moduleResolution": "node",

View File

@@ -4,7 +4,13 @@
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"target": "ES2020",
"lib": ["ES2020"],
"strict": true,
"noEmit": false,
"outDir": "./dist/types",
"allowJs": true
},
"include": ["webpack.config.js"]
}

View File

@@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/index.tsx',
entry: './index.tsx',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
@@ -20,7 +20,7 @@ export default {
directory: __dirname,
},
hot: true,
open: true,
open: false,
port: 3000,
historyApiFallback: true,
},
@@ -56,7 +56,7 @@ export default {
},
{
test: /\.(scss|sass)$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
},
{
test: /\.css$/,
@@ -69,6 +69,9 @@ export default {
],
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
};