chore: 初始化 React + TypeScript + SCSS + Ant Design 项目
This commit is contained in:
commit
99b11d1a17
18 files changed
+3590
No files matched your search
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in new issue
Block a user