feat: 更新项目依赖并重构样式管理
- 添加 React Router 和 Tailwind CSS 依赖 - 移除旧的 CSS 文件,改用模块化样式 - 更新 lint-staged 配置以跳过 CSS 文件 - 重构 App 组件以使用新的导航和样式
This commit is contained in:
1 parent
e348e16170
commit
8b32e84206
36 files changed
+698
-144
No files matched your search
-42
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
@reference "tailwindcss";
|
||||
|
||||
.app {
|
||||
@apply min-h-screen flex flex-col;
|
||||
}
|
||||
|
||||
.main {
|
||||
@apply flex-1 p-8;
|
||||
}
|
||||
|
||||
+10
-28
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { ButtonProps } from "./Button.tsx";
|
||||
export { Button } from "./Button.tsx";
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { ButtonProps } from "./Button";
|
||||
export { Button } from "./Button";
|
||||
@@ -0,0 +1 @@
|
||||
export { useLocalStorage } from "./useLocalStorage.ts";
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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>,
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { About } from "./About.tsx";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Home } from "./Home.tsx";
|
||||
@@ -0,0 +1,2 @@
|
||||
export { About } from "./About";
|
||||
export { Home } from "./Home";
|
||||
@@ -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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 全局样式入口
|
||||
* 导入 Tailwind CSS 和自定义样式
|
||||
*/
|
||||
|
||||
@import "tailwindcss";
|
||||
@import "./reset.css";
|
||||
@import "./variables.css";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user