UI 框架集成

选讲

Tailwind CSS、Ant Design、Element Plus——优雅接入 TS 项目

🎯 学习目标

学完本章,你将能够:

  • 能够配置和使用 Tailwind CSS 实现响应式设计与暗黑模式
  • 能够集成 Ant Design(React)或 Element Plus(Vue 3)组件库
  • 能够使用 Zod 实现类型安全的表单验证
  • 能够理解 Headless UI 概念并集成 Shadcn 组件
  • 能够定制组件库主题与实现图标系统

你有没有想过

为什么别人的界面好看又统一,你的页面样式东拼西凑?

从头写 CSS 太慢,但直接用 UI 框架又怕类型不支持。好消息是:现代 UI 框架都有完整的 TypeScript 类型定义,你可以在类型安全的保护下使用它们。

本章覆盖三个主题:Tailwind CSS 的原子化样式、Ant Design 和 Element Plus 的组件库集成、图标库的类型安全使用。

Part 1: Tailwind CSS

原子化 CSS 框架,用类名代替写样式。

Tailwind 是目前最流行的原子化 CSS 方案。你不需要写 .card { padding: 16px; },而是直接写 class="p-4"。所有样式都在 HTML 里,改起来一目了然。

本部分从安装配置开始,逐步讲到响应式设计、自定义主题和暗黑模式。学完你就能独立配置一个完整的 Tailwind 项目。

Tailwind 安装

三步接入 Vite + TS 项目

版本选择:本章基于 Tailwind v3。Tailwind v4(2025.1 发布)改为 CSS-first 配置(用 @import "tailwindcss" 和 @theme {} 替代 tailwind.config.ts 和 @tailwind 指令)。为了用上下面讲解的 tailwind.config.ts 工作流,请安装时显式锁定 v3:
$ npm install -D tailwindcss@3 postcss autoprefixer
如果你正在新建项目想直接用 v4,请查阅 Tailwind 官方迁移指南(https://tailwindcss.com/docs/upgrade-guide),v4 的 CSS-first 写法更简洁但配置方式与本章不同。
$ npm install -D tailwindcss@3 postcss autoprefixer
$ npx tailwindcss init -p

初始化后会生成 tailwind.config.js。如果你的项目用 TypeScript,可以改用 .ts 格式:

// tailwind.config.ts
import type { Config } from "tailwindcss";

export default {
    content: [
        "./index.html",
        "./src/**/*.{vue,js,ts,jsx,tsx}",
    ],
    theme: {
        extend: {
            colors: {
                primary: "#0f3460",
                accent: "#e94560",
            },
        },
    },
} satisfies Config;
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Tailwind + React

用类名组合出任何样式,不用写一行 CSS

// 类型安全的 Button 组件
function Button({
    children,
    variant = "primary",
}: {
    children: React.ReactNode;
    variant?: "primary" | "secondary";
}) {
    const base = "px-4 py-2 rounded font-medium transition-colors";
    const variants = {
        primary: "bg-primary text-white hover:bg-primary/90",
        secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
    };

    return (
        <button className={`${base} ${variants[variant]}`}>
            {children}
        </button>
    );
}

用联合类型约束 variant 属性,编辑器会提示可选的值,传错了立刻标红。

Tailwind + Vue

在模板中直接使用 Tailwind 类名

<template>
    <button class="px-4 py-2 bg-primary text-white rounded
        hover:bg-primary/90 transition-colors">
        <slot />
    </button>
</template>

Tailwind 的核心理念:用类名描述样式,而不是用 CSS 文件。好处是改样式不用在 HTML 和 CSS 之间跳来跳去,所有信息都在一个地方。

Tailwind 类型扩展

自定义 theme 时,让 TS 也认识你的配置

// tailwind.config.ts —— 使用 satisfies 获得类型检查
import type { Config } from "tailwindcss";

export default {
    content: ["./src/**/*.{vue,tsx}"],
    theme: {
        extend: {
            colors: {
                primary: "#0f3460",
                accent: "#e94560",
            },
        },
    },
} satisfies Config;

// 使用 satisfies 而非 as Config 的好处:
// 编辑器既能自动补全 theme 的所有选项,
// 又能在你写错配置项时报错

响应式设计

一套代码适配手机、平板、桌面

Tailwind 采用移动优先策略:裸类名对应手机,加前缀对应更大屏幕。

断点前缀

sm: 640px

md: 768px

lg: 1024px

xl: 1280px

2xl: 1536px

理解方式

p-2 → 所有屏幕

md:p-4 → 768px 以上

lg:p-6 → 1024px 以上

小屏幕是基础,大屏幕是增强。

// 响应式卡片网格:手机单列,平板两列,桌面三列
function CardGrid({ items }: { items: CardData[] }) {
    return (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
            {items.map((item) => (
                <div key={item.id} className="p-4 rounded-lg border">
                    <h3 className="text-base md:text-lg font-bold">
                        {item.title}
                    </h3>
                    <p className="text-sm md:text-base text-gray-600">
                        {item.desc}
                    </p>
                </div>
            ))}
        </div>
    );
}

响应式常用模式

侧边栏折叠、导航切换、容器宽度

// 模式一:侧边栏在大屏展开,小屏隐藏
<div className="flex">
    <aside className="hidden lg:block w-64 shrink-0">
        侧边栏
    </aside>
    <main className="flex-1 p-4">
        主内容
    </main>
</div>

// 模式二:导航栏手机端显示汉堡菜单
<nav>
    <div className="flex md:hidden">汉堡按钮</div>
    <div className="hidden md:flex gap-4">桌面导航</div>
</nav>

// 模式三:container 类自动居中
<div className="container mx-auto px-4">
    内容自动限制宽度并居中
</div>

核心思路:先写手机版样式,再用 md: / lg: 覆盖大屏样式。不要反过来。这样即使忘了加大屏样式,手机版也能正常显示。

常见错误:把 md:p-4 写成 p-4 md:p-2,结果大屏幕反而更小。记住 Tailwind 后面的类不会覆盖前面的——它们是不同条件下的规则。md: 只在 768px 以上生效。

自定义主题

让 Tailwind 适配你的设计规范

项目有自己的品牌色、字号和间距。通过 tailwind.config.ts 的 theme.extend 把设计规范注入 Tailwind。

// tailwind.config.ts
import type { Config } from "tailwindcss";

export default {
    content: ["./src/**/*.{vue,tsx,jsx}"],
    theme: {
        extend: {
            // 自定义颜色
            colors: {
                brand: {
                    50: "#eff6ff",
                    500: "#3b82f6",
                    900: "#1e3a5f",
                },
            },
            // 自定义字体
            fontFamily: {
                sans: ["Inter", "system-ui", "sans-serif"],
                mono: ["Fira Code", "monospace"],
            },
            // 自定义间距
            spacing: {
                18: "4.5rem",   // tailwind 没有的尺寸
                88: "22rem",
            },
        },
    },
    plugins: [],
} satisfies Config;

自定义工具类

用 plugin 和 CSS 变量建立设计体系

// tailwind.config.ts —— 添加自定义工具类
import type { Config } from "tailwindcss";
import plugin from "tailwindcss/plugin";

export default {
    // ...
    plugins: [
        plugin(function ({ addUtilities }) {
            addUtilities({
                ".text-balance": {
                    "text-wrap": "balance",
                },
                ".glass": {
                    "background": "rgba(255,255,255,0.1)",
                    "backdrop-filter": "blur(10px)",
                    "border": "1px solid rgba(255,255,255,0.2)",
                },
            });
        }),
    ],
} satisfies Config;

CSS 变量 + Tailwind

/* index.css */
:root {
    --color-brand: #3b82f6;
    --color-surface: #ffffff;
}

在 Tailwind 中引用

// tailwind.config.ts
colors: {
    brand: "var(--color-brand)",
    surface: "var(--color-surface)",
}

用 CSS 变量做 token,Tailwind 做工具类。这样切主题时只需改 CSS 变量,Tailwind 类名不用动。

暗黑模式

dark: 前缀让暗色适配变得简单

class 策略(推荐)

// tailwind.config.ts
export default {
    darkMode: "class",
    // ...
} satisfies Config;

在 <html> 上加 class="dark" 即可切换。

media 策略

// tailwind.config.ts
export default {
    darkMode: "media",
    // ...
} satisfies Config;

跟随系统偏好,无需 JS。

// 使用 dark: 前缀
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
    <h2 className="text-xl font-bold text-gray-800 dark:text-white">
        标题
    </h2>
    <p className="text-gray-600 dark:text-gray-400">
        内容自动适配明暗主题。
    </p>
</div>

主题切换组件

React 和 Vue 版本的暗色切换按钮

React 版

function ThemeToggle() {
    const [dark, setDark] = useState(
        localStorage.getItem("theme") === "dark"
    );

    useEffect(() => {
        document.documentElement
            .classList.toggle("dark", dark);
        localStorage.setItem(
            "theme", dark ? "dark" : "light"
        );
    }, [dark]);

    return (
        <button onClick={() => setDark(!dark)}>
            {dark ? "浅色" : "深色"}
        </button>
    );
}

Vue 版

<script setup lang="ts">
import { ref, watchEffect } from "vue";

const dark = ref(
    localStorage.getItem("theme") === "dark"
);

watchEffect(() => {
    document.documentElement
        .classList.toggle("dark", dark.value);
    localStorage.setItem(
        "theme", dark.value ? "dark" : "light"
    );
});
</script>

<template>
    <button @click="dark = !dark">
        {{ dark ? "浅色" : "深色" }}
    </button>
</template>
闪烁问题:页面加载时如果从 localStorage 读主题是异步的,会出现白屏闪烁。解决方案:在 <head> 中加一段同步脚本,在 DOM 渲染前就读取并设置 dark 类名。

组件库暗色适配

Ant Design 和 Element Plus 的暗色模式

Ant Design v5

import { ConfigProvider,
    theme } from "antd";

function App() {
    return (
        <ConfigProvider
            theme={{
                algorithm:
                    dark
                    ? theme.darkAlgorithm
                    : theme.defaultAlgorithm,
                token: {
                    colorPrimary: "#3b82f6",
                },
            }}
        >
            {children}
        </ConfigProvider>
    );
}

Element Plus

$ npm install darkreader
// 引入 Element Plus 暗色 CSS
import "element-plus/theme-chalk/
    dark/css-vars.css";

// 切换只需在 html 上加 dark 类
document.documentElement
    .classList.toggle("dark");
// Tailwind 和 Element Plus
// 同时生效

关键是统一切换入口。用 Tailwind 的 dark: 处理自定义样式,用组件库自身的暗色方案处理组件。两个系统通过同一个 dark class 联动。

Part 2: 组件库集成

Ant Design(React)和 Element Plus(Vue)的 TypeScript 用法。

组件库提供按钮、表格、表单、弹窗等现成组件,能帮你节省大量开发时间。但直接 npm install 还不够——你还需要了解按需导入、表单验证和主题定制的正确做法。

本部分重点讲类型安全的组件用法和生产级配置,不只是 API 罗列。

Ant Design (React)

组件自带完整 TS 类型,开箱即用

$ npm install antd @ant-design/icons
import { Button, Table, Form, Input, Modal, message } from "antd";
import type { ColumnsType } from "antd/es/table";

// Table 列定义——类型安全
interface User {
    key: string;
    name: string;
    age: number;
    email: string;
}

const columns: ColumnsType<User> = [
    { title: "姓名", dataIndex: "name", key: "name" },
    { title: "年龄", dataIndex: "age", key: "age" },
    { title: "邮箱", dataIndex: "email", key: "email" },
];

const data: User[] = [
    { key: "1", name: "张三", age: 25, email: "a@b.c" },
];

// <Table columns={columns} dataSource={data} />

ColumnsType<User> 让列定义和数据类型绑定。如果你写了 dataIndex: "phone",TS 会报错,因为 User 里没有 phone 字段。

Ant Design 表单

Form 和 message 的类型用法

// Form 类型安全
const onFinish = (values: {
    username: string;
    password: string;
}) => {
    console.log(values);
};

// <Form onFinish={onFinish}>
//   <Form.Item name="username" rules={[{ required: true }]}>
//     <Input />
//   </Form.Item>
//   <Form.Item name="password" rules={[{ required: true }]}>
//     <Input.Password />
//   </Form.Item>
// </Form>

// message 的类型
const [messageApi, contextHolder] = message.useMessage();
messageApi.success("操作成功");
messageApi.error("操作失败");

Element Plus (Vue 3)

两种注册方式:全局注册和按需导入

$ npm install element-plus
// 方式一:全局注册(简单但体积大)
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
app.use(ElementPlus);
# 方式二:按需导入(推荐,体积小)
$ npm install -D unplugin-vue-components unplugin-auto-import

按需导入配置好后,在模板中直接使用 <el-button> 等组件,构建工具会自动导入对应的组件代码和样式。

推荐按需导入。 全局注册会把整个组件库打包进去,即使用了几个组件也会包含全部代码。按需导入只打包你实际用到的组件。

Element Plus 使用

类型自动推断,无需额外配置

<template>
    <el-table :data="tableData">
        <el-table-column prop="name" label="姓名" />
        <el-table-column prop="age" label="年龄" />
    </el-table>
</template>

<script setup lang="ts">
import { ElMessage, ElMessageBox } from "element-plus";

ElMessage.success("操作成功");

// 类型安全的方法
interface FormRules {
    username: Array<{
        required: boolean;
        message: string;
        trigger: string;
    }>;
}
</script>

按需导入配置

vite.config.ts 完整配置,告别手动 import

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";

export default defineConfig({
    plugins: [
        vue(),
        // 自动导入 API:ref, computed 等
        AutoImport({
            resolvers: [ElementPlusResolver()],
            dts: "src/auto-imports.d.ts",
        }),
        // 自动注册组件:el-button 等
        Components({
            resolvers: [ElementPlusResolver()],
            dts: "src/components.d.ts",
        }),
    ],
});

自动生成的类型文件

auto-imports.d.ts:API 的类型声明

components.d.ts:组件的类型声明

这两个文件不用手动编辑,加入 .gitignore 即可。

常见问题排查

组件没样式?检查 resolver 配置。

TS 报错找不到组件?重启 TS 服务。

样式重复引入?确认只配了一处 resolver。

表单验证深入

不只是 required,还有自定义规则和异步校验

Ant Design 自定义验证

const rules: Rule[] = [
    { required: true,
      message: "请输入用户名" },
    { min: 3, max: 20,
      message: "3-20个字符" },
    {
        validator: (_, value) => {
            if (/[^a-zA-Z0-9]/.test(value)) {
                return Promise.reject(
                    "只能包含字母和数字"
                );
            }
            return Promise.resolve();
        },
    },
];

Element Plus 自定义验证

const rules: FormRules = {
    username: [
        { required: true,
          message: "请输入用户名",
          trigger: "blur" },
        { min: 3, max: 20,
          message: "3-20个字符",
          trigger: "blur" },
        {
            validator: (_rule, value, callback) => {
                if (/[^a-zA-Z0-9]/.test(value)) {
                    callback(new Error(
                        "只能包含字母和数字"
                    ));
                } else {
                    callback();
                }
            },
            trigger: "blur",
        },
    ],
};

Zod 表单验证

用 schema 统一前后端验证规则

$ npm install zod
// 1. 定义 schema——前后端共享
import { z } from "zod";

const registerSchema = z.object({
    username: z.string()
        .min(3, "用户名至少3个字符")
        .max(20, "用户名最多20个字符")
        .regex(/^[a-zA-Z0-9]+$/, "只能包含字母和数字"),
    email: z.string().email("邮箱格式不正确"),
    password: z.string()
        .min(8, "密码至少8位")
        .regex(/[A-Z]/, "需要包含大写字母")
        .regex(/[0-9]/, "需要包含数字"),
    confirmPassword: z.string(),
}).refine(
    (data) => data.password === data.confirmPassword,
    { message: "两次密码不一致", path: ["confirmPassword"] }
);

// 2. 推导 TS 类型
type RegisterForm = z.infer<typeof registerSchema>;

// 3. 在提交时验证
function handleSubmit(values: unknown) {
    const result = registerSchema.safeParse(values);
    if (!result.success) {
        // result.error.issues 包含所有错误信息
        const errors = result.error.flatten().fieldErrors;
        return;
    }
    // result.data 是类型安全的
    console.log(result.data);
}

Zod 的优势:一个 schema 同时用于前端表单验证和后端 API 参数校验。不用在两个地方各写一遍验证逻辑。

组件库主题定制

让组件库融入你的设计系统

Ant Design v5

import { ConfigProvider,
    theme } from "antd";

function App({ children }) {
    return (
        <ConfigProvider
            theme={{
                token: {
                    colorPrimary: "#6366f1",
                    borderRadius: 8,
                    fontSize: 14,
                    fontFamily:
                        "Inter, system-ui, sans-serif",
                },
                components: {
                    Button: {
                        controlHeight: 40,
                    },
                    Table: {
                        headerBg: "#f8fafc",
                    },
                },
            }}
        >
            {children}
        </ConfigProvider>
    );
}

Element Plus

/* 方式一:CSS 变量覆盖 */
:root {
    --el-color-primary: #6366f1;
    --el-border-radius-base: 8px;
    --el-font-size-base: 14px;
}

/* 方式二:SCSS 变量(需配置) */
// vite.config.ts 加 scss 预处理
css: {
    preprocessorOptions: {
        scss: {
            additionalData: `
                $--color-primary: #6366f1;
                $--border-radius-base: 8px;
            `,
        },
    },
}

核心思路:先定好品牌色、圆角、字号三个 token,然后在 ConfigProvider 或 CSS 变量中统一设置。组件会自动继承这些 token。

Part 3: 图标库

在 TS 项目中类型安全地使用图标。

图标看似简单,但在 TypeScript 项目中有很多细节:如何类型安全地传递图标组件?如何动态选择图标?如何封装自定义 SVG?本部分给出实用的模式。

React 图标

lucide-react 和 @ant-design/icons

import { Home, User, Settings } from "lucide-react";

// 类型安全的导航配置
interface NavItem {
    icon: React.ComponentType<{ size?: number }>;
    label: string;
    path: string;
}

const nav: NavItem[] = [
    { icon: Home, label: "首页", path: "/" },
    { icon: User, label: "用户", path: "/users" },
    { icon: Settings, label: "设置", path: "/settings" },
];

// 图标类型映射——动态选择图标
type IconName = "home" | "user" | "settings";
const iconMap: Record<IconName, React.ComponentType> = {
    home: Home,
    user: User,
    settings: Settings,
};

function DynamicIcon({ name }: { name: IconName }) {
    const Icon = iconMap[name];
    return <Icon />;
}

Vue 图标

@element-plus/icons-vue

<script setup lang="ts">
import { HomeFilled, User, Setting } from "@element-plus/icons-vue";
import type { Component } from "vue";

interface NavItem {
    icon: Component;
    label: string;
    path: string;
}

const nav: NavItem[] = [
    { icon: HomeFilled, label: "首页", path: "/" },
    { icon: User, label: "用户", path: "/users" },
    { icon: Setting, label: "设置", path: "/settings" },
];
</script>

自定义 SVG 图标

封装自己的图标组件

// React 自定义图标组件
interface IconProps {
    size?: number;
    color?: string;
    className?: string;
}

function CustomIcon({
    size = 24,
    color = "currentColor",
    className,
}: IconProps) {
    return (
        <svg width={size} height={size} fill={color} className={className}>
            {/* SVG path */}
        </svg>
    );
}

自定义图标的 Props 用 interface 定义好类型,其他组件引用时就能获得完整的类型提示。这就是 TypeScript 在 UI 开发中的价值:每一处输入都有类型保护。

Part 4: Headless UI

当组件库满足不了你的设计需求

Ant Design 和 Element Plus 功能全面,但样式定制有时不够灵活。Headless UI 提供了另一种思路:只提供行为逻辑,样式完全由你控制。本部分简要介绍这个方向。

什么是 Headless

逻辑和样式分离的组件设计模式

Headless 组件只提供交互逻辑(打开/关闭、键盘导航、ARIA 属性),不提供任何默认样式。你用 Tailwind 或自定义 CSS 来决定外观。

Radix UI

React 生态最流行的 headless 库。提供 Dialog、Popover、Tooltip 等 20+ 组件。Shadcn/ui 就是基于 Radix UI 构建的。

Headless UI

Tailwind 官方团队维护。同时支持 React 和 Vue。提供 Listbox、Combobox、Switch 等表单增强组件。

Shadcn/ui

不是npm 包,而是一套可复制的组件代码。基于 Radix UI + Tailwind。你拥有完整源码,可以随意修改。

何时选择 Headless

Styled 库 vs Headless 库的取舍

选 Styled 组件库

Ant Design、Element Plus 这类:

  • 后台管理系统,追求开发速度
  • 设计规范不严格,默认样式能接受
  • 需要表格、表单等复杂组件
  • 团队成员 UI 经验较少

选 Headless 方案

Radix、Shadcn/ui 这类:

  • 面向用户的产品,有严格的设计稿
  • 默认样式无法满足品牌要求
  • 需要极致的包体积优化
  • 团队有专职设计师

两者并不冲突。很多项目用 Ant Design 做后台页面,同时用 Shadcn/ui 做面向用户的前台。按场景选择最合适的工具。

Shadcn 组件结构

看看一个 Shadcn Button 长什么样

# 用 CLI 添加组件(不是 npm install)
$ npx shadcn@latest add button
// src/components/ui/button.tsx
// 这是复制到你项目里的源码,你可以随意改
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const buttonVariants = cva(
    "inline-flex items-center justify-center rounded-md text-sm font-medium",
    {
        variants: {
            variant: {
                default: "bg-primary text-white hover:bg-primary/90",
                destructive: "bg-red-500 text-white hover:bg-red-600",
                outline: "border border-gray-300 hover:bg-gray-100",
                ghost: "hover:bg-gray-100",
            },
            size: {
                default: "h-10 px-4 py-2",
                sm: "h-9 rounded-md px-3",
                lg: "h-11 rounded-md px-8",
            },
        },
        defaultVariants: {
            variant: "default",
            size: "default",
        },
    }
);

interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {
    asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
    ({ className, variant, size, asChild = false, ...props }, ref) => {
        const Comp = asChild ? Slot : "button";
        return (
            <Comp
                className={cn(buttonVariants({ variant, size, className }))}
                ref={ref}
                {...props}
            />
        );
    }
);

关键点:cva 管理变体样式,cn 合并类名,VariantProps 自动推导类型。你可以直接修改这段代码来适配设计稿。

常见排错

UI 框架升级时注意破坏性变更。 Ant Design v4 到 v5、Element Plus 的小版本更新都可能改变 API。升级前先看 changelog。

问题一:Tailwind 类名不生效

原因:tailwind.config.ts 的 content 路径没配对,Tailwind 扫描不到你的组件文件。

修复:确认 content 数组包含了所有使用 Tailwind 类名的文件路径。

问题二:Ant Design 组件样式错乱

原因:CSS 导入顺序不对,或者和全局样式冲突。

修复:确保 import "antd/dist/reset.css" 在全局样式之前导入。

问题三:Element Plus 按需导入不工作

原因:unplugin-vue-components 的 resolver 没有配置。

修复:在 vite.config.ts 中添加 Element Plus resolver 配置。

问题四:暗黑模式切换后组件库没反应

原因:只加了 Tailwind 的 dark class,但没有配置组件库的暗色方案。

修复:Ant Design 需要切换 algorithm,Element Plus 需要引入 dark/css-vars.css。

问题五:Shadcn 组件 TS 报类型错误

原因:缺少 class-variance-authority 或 @radix-ui/* 的类型依赖。

修复:确保 npx shadcn@latest add button 时所有依赖都已自动安装。手动安装可以用 npm install class-variance-authority clsx tailwind-merge。

试一试

练习一

任务:在 React + TS 项目中接入 Tailwind CSS,创建一个 Card 组件,支持 title、description 和 variant("outlined" | "elevated")三个 Props,用 Tailwind 类名实现不同样式。

// 提示:用联合类型约束 variant
// 你的代码:

参考答案

interface CardProps {
    title: string;
    description: string;
    variant?: "outlined" | "elevated";
}

function Card({ title, description, variant = "outlined" }: CardProps) {
    const base = "p-4 rounded-lg";
    const variants = {
        outlined: "border border-gray-300",
        elevated: "shadow-lg",
    };

    return (
        <div className={`${base} ${variants[variant]}`}>
            <h3 className="text-lg font-bold">{title}</h3>
            <p className="text-gray-600">{description}</p>
        </div>
    );
}

练习二

任务:在 Vue 3 + TS 项目中接入 Element Plus,创建一个用户列表页面,使用 el-table 展示数据,用 ElMessage 在删除时弹出提示。

<!-- 提示:interface User { id: string; name: string; age: number }
<!-- 你的代码: -->

参考答案

<template>
    <el-table :data="users">
        <el-table-column prop="name" label="姓名" />
        <el-table-column prop="age" label="年龄" />
        <el-table-column label="操作">
            <template #default="{ row }">
                <el-button @click="handleDelete(row.id)">
                    删除
                </el-button>
            </template>
        </el-table-column>
    </el-table>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { ElMessage } from "element-plus";

interface User {
    id: string; name: string; age: number;
}

const users = ref<User[]>([
    { id: "1", name: "张三", age: 25 },
    { id: "2", name: "李四", age: 30 },
]);

function handleDelete(id: string) {
    users.value = users.value.filter(u => u.id !== id);
    ElMessage.success("删除成功");
}
</script>

📝 要点总结

本章核心知识点

Tailwind CSS

原子化 CSS、响应式断点、暗黑模式、主题扩展

组件库

Ant Design(React)、Element Plus(Vue 3)、按需导入

表单验证

Zod schema 定义、类型推导、与表单库集成

Headless UI

无样式组件、Shadcn 架构、自定义扩展

主题与图标

CSS 变量主题定制、图标库集成、SVG 图标组件

下一章

UI 框架集成搞定了。下一章我们回到后端,学习 Node.js 与 TypeScript 服务端基础——把 TS 的类型安全带到服务器端。

1 / X