chore: 初始化 React + TypeScript + SCSS + Ant Design 项目

This commit is contained in:
chener committed 2026-11-07 21:24:06 +08:00
commit 99b11d1a17
18 files changed
+3590

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
# 依赖
node_modules/
# 构建产物
dist/
dist-ssr/
*.local
# 日志
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# 编辑器与系统文件
.vscode/*
!.vscode/extensions.json
.idea/
.DS_Store
Thumbs.db
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# 测试与缓存
coverage/
.vite/
.eslintcache
*.tsbuildinfo
+77
View File
@@ -0,0 +1,77 @@
# Antd Starter
React + TypeScript + SCSS + Ant Design 的前端起始项目,使用 Vite 构建。示例页面演示了 Ant Design 组件、SCSS 变量与 mixin、CSS Modules 以及一键切换的暗色主题。
## 技术栈
| 能力 | 选型 | 版本 |
| --- | --- | --- |
| 构建工具 | Vite | 8.x |
| 框架 | React | 19.x |
| 语言 | TypeScript(严格模式) | 7.x |
| 样式 | Sass + CSS Modules | 1.x |
| 组件库 | Ant Design(含 @ant-design/icons) | 6.x |
## 快速开始
需要 Node.js 20.19+ 或 22.12+(Vite 8 的要求)。
```bash
npm install # 安装依赖
npm run dev # 启动开发服务器,默认 http://localhost:5173
```
其他脚本:
```bash
npm run build # 类型检查 + 生产构建,输出到 dist/
npm run preview # 本地预览 dist/ 构建结果
npm run typecheck # 仅做 TypeScript 类型检查
```
## 目录结构
```
.
├─ public/ # 静态资源,原样拷贝到构建产物
├─ src/
│ ├─ components/ # 可复用业务组件
│ │ └─ DemoForm.tsx # Ant Design 表单示例
│ ├─ pages/ # 页面
│ │ ├─ HomePage.tsx
│ │ └─ HomePage.module.scss
│ ├─ styles/
│ │ ├─ _variables.scss # 颜色、间距、圆角、断点等变量
│ │ ├─ _mixins.scss # 响应式、布局等 mixin
│ │ └─ global.scss # 全局基础样式
│ ├─ App.tsx # 主题 / 语言包 Provider 与页面外壳
│ ├─ App.module.scss
│ ├─ main.tsx # 入口
│ └─ vite-env.d.ts
├─ index.html
├─ tsconfig.json
└─ vite.config.ts
```
## 约定与说明
**路径别名**:`@` 指向 `src`,在 TypeScript(`tsconfig.json` 的 `paths`)和 Vite(`resolve.alias`)中都已配置,例如 `import DemoForm from '@/components/DemoForm'`。
**样式**:
- 全局变量用 `@use '../styles/variables' as *;` 引入,不要用已废弃的 `@import`。
- 组件级样式使用 CSS Modules(`*.module.scss`),类名通过 `styles.xxx` 访问,避免全局污染。
- 主题色通过 CSS 变量(`--app-bg`、`--app-text` 等)下发,定义在 `App.module.scss` 的 `.app` 与 `.app.dark` 中,自定义样式可随 antd 主题一起切换。
**Ant Design**:
- `ConfigProvider` 统一主题 token 与中文语言包(`antd/locale/zh_CN`)。
- 主题切换使用 `theme.defaultAlgorithm` / `theme.darkAlgorithm`。
- 通过 `App` 组件提供的 `App.useApp()` 获取 `message`、`notification`、`modal`,保证它们跟随当前主题上下文。
- 组件按需引入,例如 `import { Button, Form } from 'antd'`。
**构建拆分**:`vite.config.ts` 使用 Rolldown 的 `codeSplitting` 把 `react`、`antd` 拆成独立 chunk,便于长期缓存;antd 依赖体积较大,已相应放宽 chunk 体积告警阈值。
## 下一步
把 `src/pages/HomePage.tsx` 和 `src/components/DemoForm.tsx` 换成自己的页面即可。如需路由、状态管理或请求库,可自行添加 React Router、Zustand、TanStack Query 等依赖。
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="React + TypeScript + SCSS + Ant Design 起始项目" />
<title>React + TS + SCSS + Ant Design</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+2695
View File
File diff suppressed because it is too large. Load diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "demo",
"private": true,
"version": "0.0.0",
"type": "module",
"description": "React + TypeScript + SCSS + Ant Design 前端项目",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ant-design/icons": "^6.3.4",
"antd": "^6.7.0",
"react": "^19.3.1",
"react-dom": "^19.3.1"
},
"devDependencies": {
"@types/node": "^26.6.6",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.2.1",
"sass": "^1.108.1",
"typescript": "^7.0.2",
"vite": "^8.3.6"
}
}
+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#5b8ff9" />
<stop offset="100%" stop-color="#38bdf8" />
</linearGradient>
</defs>
<rect width="64" height="64" rx="16" fill="url(#g)" />
<path
d="M20 44 30 20h4l10 24h-5.2l-2.2-5.8H27.4L25.2 44H20zm9.2-10.2h5.6L32 26.4l-2.8 7.4z"
fill="#fff"
/>
</svg>

After

Width:  |  Height:  |  Size: 440 B

+36
View File
@@ -0,0 +1,36 @@
@use './styles/variables' as *;
// 应用外壳:主题色通过 CSS 变量下发,结构样式在 pages 目录
.app {
--app-bg: #f5f7fb;
--app-surface: #ffffff;
--app-border: rgba(15, 23, 42, 0.08);
--app-text: #1f2430;
--app-text-muted: #5b6577;
--app-header: rgba(255, 255, 255, 0.72);
--app-glow: rgba(91, 143, 249, 0.18);
display: flex;
flex-direction: column;
// antd 的 App 组件会额外包一层 div,这里直接用视口高度保证铺满
min-height: 100vh;
min-height: 100dvh;
color: var(--app-text);
background:
radial-gradient(1200px 600px at 12% -10%, var(--app-glow), transparent 60%),
radial-gradient(900px 500px at 100% 0%, rgba(56, 189, 248, 0.14), transparent 55%),
var(--app-bg);
transition:
background-color $transition-base,
color $transition-base;
&.dark {
--app-bg: #0b1020;
--app-surface: #121a2e;
--app-border: rgba(148, 163, 184, 0.18);
--app-text: #e5e9f2;
--app-text-muted: #93a0b8;
--app-header: rgba(11, 16, 32, 0.72);
--app-glow: rgba(91, 143, 249, 0.22);
}
}
+39
View File
@@ -0,0 +1,39 @@
import { useMemo, useState } from 'react'
import { App as AntdApp, ConfigProvider, theme as antdTheme } from 'antd'
import type { ThemeConfig } from 'antd'
import zhCN from 'antd/locale/zh_CN'
import HomePage from '@/pages/HomePage'
import styles from './App.module.scss'
const FONT_FAMILY =
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif"
export default function App() {
const [isDark, setIsDark] = useState(false)
const themeConfig = useMemo<ThemeConfig>(
() => ({
algorithm: isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
token: {
colorPrimary: '#5b8ff9',
colorInfo: '#5b8ff9',
borderRadius: 10,
fontFamily: FONT_FAMILY,
},
}),
[isDark],
)
return (
<ConfigProvider locale={zhCN} theme={themeConfig}>
<AntdApp>
<div className={`${styles.app} ${isDark ? styles.dark : ''}`}>
<HomePage isDark={isDark} onToggleTheme={() => setIsDark((value) => !value)} />
</div>
</AntdApp>
</ConfigProvider>
)
}
+62
View File
@@ -0,0 +1,62 @@
import { App as AntdApp, Button, Col, DatePicker, Form, Input, Row, Select, Space } from 'antd'
interface FormValues {
name: string
role: string
startDate?: unknown
}
const ROLE_OPTIONS = [
{ value: 'frontend', label: '前端工程师' },
{ value: 'backend', label: '后端工程师' },
{ value: 'designer', label: '交互设计师' },
]
export default function DemoForm() {
const { message } = AntdApp.useApp()
const [form] = Form.useForm<FormValues>()
const handleFinish = (values: FormValues) => {
console.log('表单提交:', values)
void message.success('提交成功,数据已输出到控制台')
}
return (
<Form
form={form}
layout="vertical"
requiredMark={false}
initialValues={{ role: 'frontend' }}
onFinish={handleFinish}
>
<Row gutter={16}>
<Col xs={24} md={8}>
<Form.Item name="name" label="姓名" rules={[{ required: true, message: '请输入姓名' }]}>
<Input placeholder="例如:张三" allowClear />
</Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item name="role" label="角色">
<Select options={ROLE_OPTIONS} />
</Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item name="startDate" label="计划开始日期">
<DatePicker style={{ width: '100%' }} placeholder="选择日期" />
</Form.Item>
</Col>
</Row>
<Form.Item style={{ marginBottom: 0 }}>
<Space>
<Button type="primary" htmlType="submit">
提交
</Button>
<Button onClick={() => form.resetFields()}>重置</Button>
</Space>
</Form.Item>
</Form>
)
}
+19
View File
@@ -0,0 +1,19 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'antd/dist/reset.css'
import '@/styles/global.scss'
import App from './App'
const container = document.getElementById('root')
if (!container) {
throw new Error('找不到 #root 挂载节点,请检查 index.html')
}
createRoot(container).render(
<StrictMode>
<App />
</StrictMode>,
)
+207
View File
@@ -0,0 +1,207 @@
@use '../styles/variables' as *;
@use '../styles/mixins' as *;
.header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
gap: $space-4;
padding: $space-3 $space-5;
background: var(--app-header);
border-bottom: 1px solid var(--app-border);
backdrop-filter: saturate(180%) blur(12px);
}
.brand {
display: inline-flex;
align-items: center;
gap: $space-3;
font-weight: 600;
}
.brandMark {
@include flex-center;
width: 34px;
height: 34px;
border-radius: 10px;
background: $gradient-brand;
box-shadow: $shadow-sm;
color: #fff;
font-size: 16px;
font-weight: 700;
}
.brandText {
display: flex;
flex-direction: column;
line-height: 1.25;
}
.brandName {
font-size: 15px;
}
.brandTag {
color: var(--app-text-muted);
font-size: 12px;
font-weight: 500;
}
.actions {
display: inline-flex;
align-items: center;
gap: $space-3;
}
.main {
flex: 1;
width: 100%;
max-width: 1120px;
margin: 0 auto;
padding: $space-7 $space-5;
}
.hero {
@include surface(var(--app-surface), var(--app-border));
position: relative;
overflow: hidden;
padding: $space-7;
box-shadow: $shadow-md;
// 右上角装饰光斑
&::after {
position: absolute;
inset: -140px -140px auto auto;
width: 380px;
height: 380px;
background: radial-gradient(circle, rgba(91, 143, 249, 0.32), transparent 68%);
content: '';
pointer-events: none;
}
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: $space-1;
padding: 2px 10px;
border-radius: $radius-pill;
background: $color-primary-soft;
color: $color-primary;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
}
.title {
margin: $space-4 0 $space-3;
font-size: clamp(26px, 4.2vw, 42px);
line-height: 1.2;
letter-spacing: -0.02em;
}
.gradientText {
background: $gradient-brand;
background-clip: text;
color: transparent;
-webkit-background-clip: text;
}
.lead {
max-width: 62ch;
margin: 0 0 $space-5;
color: var(--app-text-muted);
font-size: 15px;
}
.heroActions {
display: flex;
flex-wrap: wrap;
gap: $space-3;
}
// 技术栈标签行
.stack {
display: flex;
flex-wrap: wrap;
gap: $space-2;
margin-top: $space-5;
}
.sectionTitle {
margin: $space-7 0 $space-4;
font-size: 18px;
letter-spacing: -0.01em;
}
.grid {
display: grid;
gap: $space-4;
grid-template-columns: 1fr;
@include respond-to($bp-md) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@include respond-to($bp-lg) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.card {
@include surface(var(--app-surface), var(--app-border));
padding: $space-5;
transition:
transform $transition-base,
box-shadow $transition-base;
&:hover {
transform: translateY(-3px);
box-shadow: $shadow-md;
}
}
.cardIcon {
@include flex-center;
width: 40px;
height: 40px;
margin-bottom: $space-3;
border-radius: $radius-md;
background: $color-primary-soft;
color: $color-primary;
font-size: 20px;
}
.cardTitle {
margin: 0 0 $space-1;
font-size: 15px;
}
.cardDesc {
margin: 0;
color: var(--app-text-muted);
font-size: 13px;
line-height: 1.7;
}
.panel {
@include surface(var(--app-surface), var(--app-border));
margin-top: $space-6;
padding: $space-5;
}
.footer {
padding: $space-5;
border-top: 1px solid var(--app-border);
color: var(--app-text-muted);
font-size: 12px;
text-align: center;
}
+149
View File
@@ -0,0 +1,149 @@
import type { ReactNode } from 'react'
import {
ApiOutlined,
BgColorsOutlined,
CodeOutlined,
LayoutOutlined,
MoonOutlined,
RocketOutlined,
SunOutlined,
ThunderboltOutlined,
} from '@ant-design/icons'
import { App as AntdApp, Button, Switch, Tag } from 'antd'
import DemoForm from '@/components/DemoForm'
import styles from './HomePage.module.scss'
const STACK = ['Vite 8', 'React 19', 'TypeScript 7', 'Sass', 'Ant Design 6', 'CSS Modules']
const FEATURES: { icon: ReactNode; title: string; desc: string }[] = [
{
icon: <RocketOutlined />,
title: '极速构建',
desc: '基于 Vite 8,冷启动与热更新都很快;@ 别名已指向 src 目录。',
},
{
icon: <CodeOutlined />,
title: '类型安全',
desc: 'TypeScript 严格模式,npm run typecheck 即可做全量类型检查。',
},
{
icon: <BgColorsOutlined />,
title: '样式方案',
desc: 'Sass 配合 CSS Modules,内置颜色、间距变量与响应式 mixin。',
},
{
icon: <LayoutOutlined />,
title: '组件库',
desc: 'Ant Design 6 组件开箱即用,ConfigProvider 统一主题与中文语言包。',
},
{
icon: <ThunderboltOutlined />,
title: '暗色主题',
desc: '一键切换 antd 算法,自定义样式通过 CSS 变量同步跟随。',
},
{
icon: <ApiOutlined />,
title: '目录约定',
desc: 'pages 放页面、components 放复用组件、styles 放全局样式。',
},
]
interface HomePageProps {
isDark: boolean
onToggleTheme: () => void
}
export default function HomePage({ isDark, onToggleTheme }: HomePageProps) {
const { message } = AntdApp.useApp()
return (
<>
<header className={styles.header}>
<div className={styles.brand}>
<span className={styles.brandMark}>A</span>
<span className={styles.brandText}>
<span className={styles.brandName}>Antd Starter</span>
<span className={styles.brandTag}>React · TypeScript · SCSS</span>
</span>
</div>
<div className={styles.actions}>
<Switch
checked={isDark}
onChange={onToggleTheme}
checkedChildren={<MoonOutlined />}
unCheckedChildren={<SunOutlined />}
aria-label="切换暗色主题"
/>
<Button href="https://ant.design/components/overview-cn" target="_blank" rel="noreferrer">
文档
</Button>
</div>
</header>
<main className={styles.main}>
<section className={styles.hero}>
<span className={styles.eyebrow}>Get started</span>
<h1 className={styles.title}>
一个开箱即用的 <span className={styles.gradientText}>React + TypeScript</span> 起始项目
</h1>
<p className={styles.lead}>
已内置 Vite 构建、Sass 预处理器与 Ant Design 组件库,支持暗色主题与中文语言包。
删掉示例内容,就可以直接开始写业务代码。
</p>
<div className={styles.heroActions}>
<Button
type="primary"
size="large"
icon={<RocketOutlined />}
onClick={() => {
void message.success('环境已就绪,开始写代码吧 🎉')
}}
>
开始开发
</Button>
<Button size="large" href="https://vite.dev/guide/" target="_blank" rel="noreferrer">
Vite 文档
</Button>
</div>
<div className={styles.stack}>
{STACK.map((item) => (
<Tag key={item} color="blue">
{item}
</Tag>
))}
</div>
</section>
<h2 className={styles.sectionTitle}>项目内置能力</h2>
<div className={styles.grid}>
{FEATURES.map((feature) => (
<article className={styles.card} key={feature.title}>
<div className={styles.cardIcon}>{feature.icon}</div>
<h3 className={styles.cardTitle}>{feature.title}</h3>
<p className={styles.cardDesc}>{feature.desc}</p>
</article>
))}
</div>
<section className={styles.panel}>
<h2 className={styles.sectionTitle} style={{ marginTop: 0 }}>
表单示例
</h2>
<DemoForm />
</section>
</main>
<footer className={styles.footer}>
React + TypeScript + SCSS + Ant Design · 由 Vite 驱动
</footer>
</>
)
}
+31
View File
@@ -0,0 +1,31 @@
// 可复用的 SCSS mixin
// 使用方式:@use './mixins' as *;
@use './variables' as *;
/// 不小于指定断点生效(移动优先)
@mixin respond-to($breakpoint) {
@media (min-width: $breakpoint) {
@content;
}
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
/// 卡片类表面的基础样式,颜色交给调用方
@mixin surface($background, $border) {
background: $background;
border: 1px solid $border;
border-radius: $radius-lg;
box-shadow: $shadow-sm;
}
@mixin truncate {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
+43
View File
@@ -0,0 +1,43 @@
// 全局设计变量:颜色、间距、圆角、断点等
// 使用方式:@use './variables' as *;
// 字体
$font-sans:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
$font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
// 品牌色
$color-primary: #5b8ff9;
$color-primary-soft: rgba(91, 143, 249, 0.12);
$color-accent: #38bdf8;
$gradient-brand: linear-gradient(135deg, #5b8ff9 0%, #38bdf8 100%);
// 间距
$space-1: 4px;
$space-2: 8px;
$space-3: 12px;
$space-4: 16px;
$space-5: 24px;
$space-6: 32px;
$space-7: 48px;
// 圆角
$radius-sm: 8px;
$radius-md: 12px;
$radius-lg: 20px;
$radius-pill: 999px;
// 阴影
$shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
$shadow-md: 0 8px 24px -8px rgba(16, 24, 40, 0.16);
$shadow-lg: 0 24px 48px -24px rgba(16, 24, 40, 0.28);
// 动效
$ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
$transition-base: 180ms $ease-out;
// 响应式断点
$bp-sm: 576px;
$bp-md: 768px;
$bp-lg: 1024px;
+74
View File
@@ -0,0 +1,74 @@
@use './variables' as *;
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
font-family: $font-sans;
font-size: 14px;
line-height: 1.6;
color: #1f2430;
background: #f5f7fb;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
a {
color: $color-primary;
text-decoration: none;
transition: color $transition-base;
&:hover {
color: $color-accent;
}
}
code,
kbd,
pre {
font-family: $font-mono;
font-size: 0.92em;
}
img,
svg {
display: block;
max-width: 100%;
}
::selection {
background: $color-primary-soft;
}
:focus-visible {
outline: 2px solid $color-primary;
outline-offset: 2px;
}
// 细滚动条(WebKit)
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background: rgba(100, 116, 139, 0.35);
border: 3px solid transparent;
border-radius: $radius-pill;
background-clip: content-box;
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"jsx": "react-jsx",
"types": ["vite/client", "node"],
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"useDefineForClassFields": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src", "vite.config.ts"]
}
+41
View File
@@ -0,0 +1,41 @@
import { fileURLToPath, URL } from 'node:url'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 5173,
open: false,
},
build: {
outDir: 'dist',
sourcemap: false,
// antd 依赖本身较大,单独成包后放宽此告警阈值
chunkSizeWarningLimit: 700,
// 把体积较大的第三方依赖拆成独立 chunk,便于长期缓存
rolldownOptions: {
output: {
codeSplitting: {
groups: [
{
name: 'react-vendor',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
},
{
name: 'antd-vendor',
test: /[\\/]node_modules[\\/](antd|@ant-design|@rc-component|rc-[^\\/]+)[\\/]/,
},
],
},
},
},
},
})