feat: 更新项目依赖并重构样式管理

- 添加 React Router 和 Tailwind CSS 依赖
- 移除旧的 CSS 文件,改用模块化样式
- 更新 lint-staged 配置以跳过 CSS 文件
- 重构 App 组件以使用新的导航和样式
This commit is contained in:
zhongyidian committed 2026-01-23 16:02:49 +08:00
1 parent e348e16170
commit 8b32e84206
36 files changed
+698 -144

No files matched your search

-42
View File
@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
+10
View File
@@ -0,0 +1,10 @@
@reference "tailwindcss";
.app {
@apply min-h-screen flex flex-col;
}
.main {
@apply flex-1 p-8;
}
+10 -28
View File
@@ -1,34 +1,16 @@
import { useState } from "react";
import viteLogo from "/vite.svg";
import reactLogo from "./assets/react.svg";
import "./App.css";
import { Outlet } from "react-router-dom";
import styles from "./App.module.css";
import { Navigation } from "./components/business";
import { cn } from "./lib/utils.ts";
function App() {
const [count, setCount] = useState(0);
return (
<>
<div>
<a href="https://vite.dev" target="_blank" rel="noopener">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank" rel="noopener">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button type="button" onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
<div className={cn(styles.app)}>
<Navigation />
<main className={cn(styles.main)}>
<Outlet />
</main>
</div>
);
}
+93
View File
@@ -0,0 +1,93 @@
/**
* API 客户端配置
* 可以根据项目需求使用 axios、fetch 或其他 HTTP 客户端
*/
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "/api";
export interface ApiResponse<T> {
data: T;
message?: string;
code?: number;
}
export interface ApiError {
message: string;
code?: number;
}
/**
* 基础请求函数
*/
export async function request<T>(
endpoint: string,
options?: RequestInit,
): Promise<ApiResponse<T>> {
const url = `${API_BASE_URL}${endpoint}`;
try {
const response = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = (await response.json()) as ApiResponse<T>;
return data;
} catch (error) {
throw {
message: error instanceof Error ? error.message : "Unknown error",
code: 500,
} as ApiError;
}
}
/**
* GET 请求
*/
export function get<T>(endpoint: string, options?: RequestInit) {
return request<T>(endpoint, { ...options, method: "GET" });
}
/**
* POST 请求
*/
export function post<T>(
endpoint: string,
data?: unknown,
options?: RequestInit,
) {
return request<T>(endpoint, {
...options,
method: "POST",
body: data ? JSON.stringify(data) : undefined,
});
}
/**
* PUT 请求
*/
export function put<T>(
endpoint: string,
data?: unknown,
options?: RequestInit,
) {
return request<T>(endpoint, {
...options,
method: "PUT",
body: data ? JSON.stringify(data) : undefined,
});
}
/**
* DELETE 请求
*/
export function del<T>(endpoint: string, options?: RequestInit) {
return request<T>(endpoint, { ...options, method: "DELETE" });
}
+9
View File
@@ -0,0 +1,9 @@
export {
type ApiError,
type ApiResponse,
del,
get,
post,
put,
request,
} from "./client.ts";
@@ -0,0 +1,15 @@
@reference "tailwindcss";
.navigation {
@apply flex gap-4 p-4 bg-gray-100 border-b border-gray-300;
}
.link {
@apply text-blue-600 no-underline px-4 py-2 rounded transition-colors;
@apply hover:bg-gray-200;
}
.active {
@apply bg-blue-100 text-blue-800 font-medium;
}
@@ -0,0 +1,42 @@
import { Link, useLocation } from "react-router-dom";
import { cn } from "../../../lib/utils.ts";
import styles from "./Navigation.module.css";
interface NavItem {
label: string;
path: string;
}
interface NavigationProps {
items?: NavItem[];
className?: string;
}
const defaultItems: NavItem[] = [
{ label: "首页", path: "/" },
{ label: "关于", path: "/about" },
];
export function Navigation({
items = defaultItems,
className,
}: NavigationProps) {
const location = useLocation();
return (
<nav className={cn(styles.navigation, className)}>
{items.map((item) => {
const isActive = location.pathname === item.path;
return (
<Link
key={item.path}
to={item.path}
className={cn(styles.link, isActive && styles.active)}
>
{item.label}
</Link>
);
})}
</nav>
);
}
@@ -0,0 +1 @@
export { Navigation } from "./Navigation.tsx";
+1
View File
@@ -0,0 +1 @@
export { Navigation } from "./Navigation";
@@ -0,0 +1,36 @@
@reference "tailwindcss";
.button {
@apply inline-flex items-center justify-center rounded-md font-medium transition-colors;
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
@apply disabled:pointer-events-none disabled:opacity-50;
}
.primary {
@apply bg-blue-600 text-white hover:bg-blue-700;
}
.secondary {
@apply bg-gray-200 text-gray-900 hover:bg-gray-300;
}
.outline {
@apply border border-gray-300 bg-transparent hover:bg-gray-100;
}
.ghost {
@apply bg-transparent hover:bg-gray-100;
}
.sm {
@apply h-8 px-3 text-sm;
}
.md {
@apply h-10 px-4 text-base;
}
.lg {
@apply h-12 px-6 text-lg;
}
+22
View File
@@ -0,0 +1,22 @@
import { type ButtonHTMLAttributes, forwardRef } from "react";
import { cn } from "../../../lib/utils.ts";
import styles from "./Button.module.css";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "primary", size = "md", ...props }, ref) => {
return (
<button
ref={ref}
className={cn(styles.button, styles[variant], styles[size], className)}
{...props}
/>
);
},
);
Button.displayName = "Button";
+2
View File
@@ -0,0 +1,2 @@
export type { ButtonProps } from "./Button.tsx";
export { Button } from "./Button.tsx";
+2
View File
@@ -0,0 +1,2 @@
export type { ButtonProps } from "./Button";
export { Button } from "./Button";
+1
View File
@@ -0,0 +1 @@
export { useLocalStorage } from "./useLocalStorage.ts";
+52
View File
@@ -0,0 +1,52 @@
import { useEffect, useState } from "react";
/**
* 自定义 Hook:使用 localStorage 存储状态
* @param key - localStorage 的 key
* @param initialValue - 初始值
* @returns [storedValue, setValue] - 存储的值和更新函数
*/
export function useLocalStorage<T>(
key: string,
initialValue: T,
): [T, (value: T | ((val: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
try {
setStoredValue(JSON.parse(e.newValue) as T);
} catch (error) {
console.error(`Error parsing storage event for key "${key}":`, error);
}
}
};
window.addEventListener("storage", handleStorageChange);
return () => {
window.removeEventListener("storage", handleStorageChange);
};
}, [key]);
return [storedValue, setValue];
}
-68
View File
@@ -1,68 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
+10
View File
@@ -0,0 +1,10 @@
import { twMerge } from "tailwind-merge";
/**
* 合并 Tailwind CSS 类名,自动处理冲突
* @param classes - 要合并的类名
* @returns 合并后的类名字符串
*/
export function cn(...classes: (string | undefined | null | false)[]): string {
return twMerge(classes.filter(Boolean).join(" "));
}
+4 -3
View File
@@ -1,7 +1,8 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { RouterProvider } from "react-router-dom";
import "./styles/index.css";
import { router } from "./router/index.tsx";
const rootElement = document.getElementById("root");
if (!rootElement) {
@@ -10,6 +11,6 @@ if (!rootElement) {
createRoot(rootElement).render(
<StrictMode>
<App />
<RouterProvider router={router} />
</StrictMode>,
);
+18
View File
@@ -0,0 +1,18 @@
@reference "tailwindcss";
.container {
@apply text-center;
}
.title {
@apply text-4xl font-bold mb-4;
}
.description {
@apply text-lg mb-4;
}
.actions {
@apply flex gap-4 justify-center;
}
+17
View File
@@ -0,0 +1,17 @@
import { Link } from "react-router-dom";
import { Button } from "../../components/common";
import styles from "./About.module.css";
export function About() {
return (
<div className={styles.container}>
<h1 className={styles.title}>关于我们</h1>
<p className={styles.description}>这是关于页面</p>
<div className={styles.actions}>
<Link to="/">
<Button variant="secondary">返回首页</Button>
</Link>
</div>
</div>
);
}
+1
View File
@@ -0,0 +1 @@
export { About } from "./About.tsx";
+22
View File
@@ -0,0 +1,22 @@
.container {
text-align: center;
}
.title {
margin-bottom: 1rem;
font-size: 2.25rem;
line-height: 2.5rem;
font-weight: 700;
}
.description {
margin-bottom: 1rem;
font-size: 1.125rem;
line-height: 1.75rem;
}
.actions {
display: flex;
gap: 1rem;
justify-content: center;
}
+17
View File
@@ -0,0 +1,17 @@
import { Link } from "react-router-dom";
import { Button } from "../../components/common";
import styles from "./Home.module.css";
export function Home() {
return (
<div className={styles.container}>
<h1 className={styles.title}>首页</h1>
<p className={styles.description}>欢迎来到首页</p>
<div className={styles.actions}>
<Link to="/about">
<Button>关于我们</Button>
</Link>
</div>
</div>
);
}
+1
View File
@@ -0,0 +1 @@
export { Home } from "./Home.tsx";
+2
View File
@@ -0,0 +1,2 @@
export { About } from "./About";
export { Home } from "./Home";
+20
View File
@@ -0,0 +1,20 @@
import { createHashRouter } from "react-router-dom";
import App from "../App.tsx";
import { About, Home } from "../pages";
export const router = createHashRouter([
{
path: "/",
element: <App />,
children: [
{
index: true,
element: <Home />,
},
{
path: "about",
element: <About />,
},
],
},
]);
+8
View File
@@ -0,0 +1,8 @@
/**
* 全局样式入口
* 导入 Tailwind CSS 和自定义样式
*/
@import "tailwindcss";
@import "./reset.css";
@import "./variables.css";
+50
View File
@@ -0,0 +1,50 @@
/**
* CSS Reset
* 基础样式重置
*/
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
-webkit-text-size-adjust: 100%;
-moz-tab-size: 4;
tab-size: 4;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
+37
View File
@@ -0,0 +1,37 @@
/**
* CSS 变量定义
* 可以与 Tailwind CSS 配合使用
*/
:root {
/* 颜色 */
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-secondary: #6b7280;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
/* 间距 */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* 圆角 */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-full: 9999px;
/* 阴影 */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
/* 过渡 */
--transition-fast: 150ms;
--transition-base: 200ms;
--transition-slow: 300ms;
}
+34
View File
@@ -0,0 +1,34 @@
/**
* 全局类型定义
*/
// 用户相关类型
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
// API 响应类型
export interface PaginationParams {
page: number;
pageSize: number;
}
export interface PaginationResponse<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
// 通用类型
export type Nullable<T> = T | null;
export type Optional<T> = T | undefined;
// 组件 Props 基础类型
export interface BaseComponentProps {
className?: string;
children?: React.ReactNode;
}