组件库名称: GloryCloud UI
当前版本: 0.0.1
一句话简介: 基于 Nuxt3 + Vue3 + Element Plus 的企业级组件库,专注于提供统一、高效、可维护的 UI 解决方案
技术栈: Nuxt3 + Vue3 + TypeScript + TailwindCSS + Element Plus + Storybook
背景 & 痛点
我们为什么要做这个组件库?
在项目发展过程中,我们遇到了以下痛点:
- 重复造轮子:各个页面重复实现相似的组件,代码冗余严重
- 样式不统一:缺乏统一的设计规范,UI 风格不一致
- 维护成本高:组件散落在各处,修改样式需要改动多个文件
- 开发效率低:缺乏标准化的组件文档和使用规范
- 类型安全不足:缺乏完整的 TypeScript 类型定义
为什么不直接用 Element Plus?
虽然 Element Plus 是优秀的组件库,但在实际业务中存在以下局限:
- 设计风格固化:难以深度定制符合企业品牌的设计风格
- 功能局限性:某些业务场景需要更复杂的组件功能
- 样式覆盖困难:深度定制样式时优先级冲突频繁
- 主题系统不够灵活:暗色模式和主题切换支持有限
设计原则
可组合性(Composability)
基于 Vue 3 Composition API 设计,每个组件都是独立、可复用的功能单元。
// 组件内部使用 Composition API
export default defineComponent({
setup(props, { emit }) {
const { formData, handleSearch, handleReset } = useTableSearch(props, emit);
return { formData, handleSearch, handleReset };
},
});
类型安全与开发者体验
全面的 TypeScript 支持,提供完整的类型定义和智能提示。
interface TabItem {
name: string;
label: string;
icon?: string;
count?: number;
}
interface XxTabsProps {
type: "card" | "nav" | "toggle" | "line";
tabs: TabItem[];
modelValue: string | number;
}
无运行时样式污染(Runtime-style isolation)
采用 CSS-in-JS 和 Scoped CSS 相结合的方式,确保组件样式不会相互污染。
.xx-tabs {
// 组件样式完全封装
:deep(.el-tabs__item) {
// 使用 :deep() 穿透样式,避免全局污染
padding: 0 16px !important;
}
}
主题化与暗色模式
基于 CSS 变量的主题。
:root {
--xx-color-primary: #1cb1c8;
--xx-color-primary-hover: #40bdd1;
--xx-bg-color: #ffffff;
}
无障碍访问(a11y)
遵循 WCAG 2.1 标准,提供完整的键盘导航、屏幕阅读器支持和焦点管理。
<template>
<button
:aria-label="buttonLabel"
:aria-pressed="isActive"
@keydown.enter="handleClick"
@keydown.space="handleClick"
>
<slot />
</button>
</template>
架构总览
graph TB
A[GloryCloud UI 组件库] --> B[核心包结构]
B --> C[storybook-repo<br/>📚 文档与演示]
B --> D[components/<br/>🧩 组件源码]
B --> E[assets/<br/>🎨 样式资源]
C --> C1[stories/ - 组件故事]
C --> C2[.storybook/ - 配置]
C --> C3[docs/ - 文档]
D --> D1[global/Xx/ - 基础组件]
D --> D2[global/ - 业务组件]
E --> E1[scss/ - 样式文件]
E --> E2[fonts/ - 字体资源]
F[构建工具链] --> G[Vite + Vue3]
F --> H[Storybook 8.x]
F --> I[TypeScript]
F --> J[TailwindCSS]
K[开发流程] --> L[Git Submodule]
K --> M[独立仓库管理]
K --> N[CI/CD 自动化]
style A fill:#667eea,stroke:#333,stroke-width:3px,color:#fff
style F fill:#764ba2,stroke:#333,stroke-width:2px,color:#fff
style K fill:#f093fb,stroke:#333,stroke-width:2px,color:#fff
Monorepo 结构说明
- 独立仓库管理:Storybook 采用独立仓库,通过 Git Submodule 集成
- 组件源码分离:业务代码与组件文档完全分离,保持代码仓库整洁
- 构建工具统一:Vite + TypeScript + TailwindCSS 提供现代化构建体验
- 文档自动化:Storybook + Chromatic 实现组件文档的自动构建和部署
核心技术方案
TailwindCSS + clsx + cva 实现变体
使用 class-variance-authority 实现类型安全的样式变体管理:
import { cva, type VariantProps } from "class-variance-authority";
import { clsx } from "clsx";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input hover:bg-accent hover:text-accent-foreground",
},
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",
},
}
);
export interface ButtonProps extends VariantProps<typeof buttonVariants> {
// 其他 props
}
主题系统(CSS 变量 + Tailwind plugin)
基于 CSS 变量的动态主题系统:
// tailwind.config.js
module.exports = {
darkMode: "class",
theme: {
extend: {
colors: {
primary: {
DEFAULT: "var(--xx-color-primary)",
hover: "var(--xx-color-primary-hover)",
active: "var(--xx-color-primary-active)",
},
background: "var(--xx-bg-color)",
foreground: "var(--xx-text-color)",
},
},
},
plugins: [
function ({ addBase }) {
addBase({
":root": {
"--xx-color-primary": "#1cb1c8",
"--xx-color-primary-hover": "#40bdd1",
"--xx-bg-color": "#ffffff",
"--xx-text-color": "#000000",
},
'[data-theme="dark"]': {
"--xx-bg-color": "#1a1a1a",
"--xx-text-color": "#ffffff",
},
});
},
],
};
暗色模式自动探测与切换
// composables/useTheme.ts
export const useTheme = () => {
const theme = ref<"light" | "dark" | "auto">("auto");
const applyTheme = (newTheme: string) => {
const root = document.documentElement;
if (newTheme === "auto") {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
root.setAttribute("data-theme", prefersDark ? "dark" : "light");
} else {
root.setAttribute("data-theme", newTheme);
}
};
const toggleTheme = () => {
theme.value = theme.value === "light" ? "dark" : "light";
applyTheme(theme.value);
};
// 监听系统主题变化
watchEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => {
if (theme.value === "auto") {
applyTheme("auto");
}
};
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
});
return { theme, toggleTheme, applyTheme };
};
按需加载与 Tree-shaking 方案
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [
vue(),
// 自动导入组件
Components({
resolvers: [
// 自定义解析器
(componentName) => {
if (componentName.startsWith("Xx")) {
return {
name: componentName,
from: `@/components/global/Xx/${componentName.slice(2)}/index.vue`,
};
}
},
],
}),
],
build: {
lib: {
entry: "src/index.ts",
formats: ["es", "cjs"],
},
rollupOptions: {
external: ["vue", "element-plus"],
output: {
globals: {
vue: "Vue",
"element-plus": "ElementPlus",
},
},
},
},
});
组件总览
| 组件名 | 状态 | Storybook 链接 | 备注 |
|---|---|---|---|
| XxButton | ✅ Completed | 查看文档 | 基础按钮组件 |
| XxInput | ✅ Completed | 查看文档 | 输入框组件 |
| XxSelect | ✅ Completed | 查看文档 | 下拉选择器 |
| XxTabs | ✅ Completed | 查看文档 | 多样式标签页 |
| XxTable | ✅ Completed | 查看文档 | 数据表格 |
| XxTableSearchForm | ✅ Completed | 查看文档 | 表格搜索表单 |
| XxDialog | ✅ Completed | 查看文档 | 对话框组件 |
| XxForm | ✅ Completed | 查看文档 | 表单组件 |
| XxTag | ✅ Completed | 查看文档 | 标签组件 |
| XxAlert | ✅ Completed | 查看文档 | 警告提示 |
| XxBreadcrumb | ✅ Completed | 查看文档 | 面包屑导航 |
| XxCascader | ✅ Completed | 查看文档 | 级联选择器 |
| XxInputNumber | ✅ Completed | 查看文档 | 数字输入框 |
| XxRadio | ✅ Completed | 查看文档 | 单选框组件 |
| XxSlider | ✅ Completed | 查看文档 | 滑块组件 |
| XxText | ✅ Completed | 查看文档 | 文本组件 |
| AvifPicture | ✅ Completed | 查看文档 | 现代图片格式组件 |
| PayMethod | ✅ Completed | 查看文档 | 支付方式选择 |
| CommonTip | ✅ Completed | 查看文档 | 通用提示组件 |
| SvgIcon | ✅ Completed | 查看文档 | SVG 图标组件 |
| XxLink | ✅ Completed | 查看文档 | 链接组件 |
| XxCheckbox | 📋 Planned | 查看文档 | 复选框组件 |
| XxViewToggle | 📋 Planned | 查看文档 | 视图切换组件 |
| XxMobileDrawer | 📋 Planned | 查看文档 | 移动端抽屉 |
拿手好戏:3 个最得意的组件深度拆解
1. XxTabs - 多样式标签页组件
设计思路
XxTabs 是我们最引以为豪的组件之一,它解决了不同场景下标签页样式需求的问题。通过一个组件支持四种不同的视觉风格:
- Card 卡片式:适合桌面端,有明显的卡片边框
- Line 线条式:适合移动端,支持数量显示
- Nav 导航式:适合页面导航,简洁的分隔线设计
- Toggle 切换式:适合工具栏,紧凑的按钮组样式
变体实现
<template>
<div class="xx-tabs" :class="tabsClass">
<component
:is="tabComponent"
v-model="currentValue"
:tabs="tabs"
@change="handleChange"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import XxCardTabs from "./CardTabs.vue";
import XxLineTabs from "./LineTabs.vue";
import XxNavTabs from "./NavTabs.vue";
import XxToggleTabs from "./ToggleTabs.vue";
interface TabItem {
name: string;
label: string;
icon?: string;
count?: number;
}
interface Props {
type: "card" | "nav" | "toggle" | "line";
tabs: TabItem[];
modelValue: string | number;
}
const props = withDefaults(defineProps<Props>(), {
type: "card",
});
// 动态组件映射
const tabComponent = computed(() => {
const componentMap = {
card: XxCardTabs,
line: XxLineTabs,
nav: XxNavTabs,
toggle: XxToggleTabs,
};
return componentMap[props.type];
});
// 样式类计算
const tabsClass = computed(() => [
`xx-tabs--${props.type}`,
{
"xx-tabs--mobile": isMobile.value,
},
]);
</script>
性能优化点
- 动态组件加载:根据 type 动态加载对应的子组件,减少初始包体积
- 样式按需加载:每种类型的样式独立,避免样式冗余
- 响应式优化:使用
computed缓存计算结果,避免重复计算
2. XxTableSearchForm - 配置化搜索表单
设计思路
XxTableSearchForm 解决了表格页面搜索表单的标准化问题。通过配置化的方式,开发者只需要定义字段配置,就能快速生成功能完整的搜索表单。
核心实现
<template>
<el-form
ref="formRef"
:model="formData"
class="xx-table-search-form"
:class="formClass"
>
<el-row :gutter="16">
<el-col
v-for="field in visibleFields"
:key="field.prop"
:span="field.span || 6"
>
<el-form-item :label="field.label" :prop="field.prop">
<!-- 动态渲染不同类型的表单控件 -->
<component
:is="getFieldComponent(field.type)"
v-model="formData[field.prop]"
v-bind="getFieldProps(field)"
@change="handleFieldChange(field, $event)"
/>
</el-form-item>
</el-col>
<!-- 操作按钮 -->
<el-col :span="actionSpan">
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSearch">
{{ searchText }}
</el-button>
<el-button @click="handleReset">
{{ resetText }}
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
interface FieldConfig {
prop: string;
label: string;
type: "input" | "select" | "date-range" | "cascader";
placeholder?: string;
options?: Array<{ label: string; value: any }>;
span?: number;
[key: string]: any;
}
// 动态组件映射
const getFieldComponent = (type: string) => {
const componentMap = {
input: "el-input",
select: "el-select",
"date-range": "el-date-picker",
cascader: "el-cascader",
};
return componentMap[type] || "el-input";
};
// 动态属性计算
const getFieldProps = (field: FieldConfig) => {
const baseProps = {
placeholder: field.placeholder,
clearable: true,
};
// 根据类型添加特定属性
switch (field.type) {
case "select":
return {
...baseProps,
options: field.options,
};
case "date-range":
return {
...baseProps,
type: "daterange",
rangeSeparator: "至",
startPlaceholder: "开始日期",
endPlaceholder: "结束日期",
};
default:
return baseProps;
}
};
</script>
性能优化点
- 虚拟滚动支持:当字段数量过多时,支持虚拟滚动渲染
- 防抖搜索:搜索操作自动防抖,避免频繁请求
- 缓存机制:表单数据支持本地缓存,提升用户体验
3. AvifPicture - 现代图片格式组件
设计思路
AvifPicture 组件解决了现代 Web 应用中图片格式优化的问题。它支持 AVIF、WebP、JPG 三种格式的智能回退,在保证兼容性的同时最大化性能优势。
核心实现
<template>
<picture :class="pictureClass">
<!-- AVIF 格式 - 最优压缩比 -->
<source v-if="avifSrc" :srcset="avifSrc" type="image/avif" />
<!-- WebP 格式 - 广泛支持 -->
<source v-if="webpSrc" :srcset="webpSrc" type="image/webp" />
<!-- 回退格式 - 兼容性保证 -->
<img
:src="fallbackSrc"
:alt="alt"
:loading="loading"
:class="imgClass"
@load="handleLoad"
@error="handleError"
/>
</picture>
</template>
<script setup lang="ts">
interface Props {
avifSrc?: string;
webpSrc?: string;
fallbackSrc: string;
alt: string;
loading?: "lazy" | "eager" | "auto";
pictureClass?: string;
imgClass?: string;
}
const props = withDefaults(defineProps<Props>(), {
loading: "lazy",
});
// 智能格式推断
const generateSources = () => {
if (!props.avifSrc && !props.webpSrc) {
// 自动生成现代格式路径
const basePath = props.fallbackSrc.replace(/\.(jpg|jpeg|png)$/i, "");
return {
avif: `${basePath}.avif`,
webp: `${basePath}.webp`,
};
}
return {
avif: props.avifSrc,
webp: props.webpSrc,
};
};
// 加载状态管理
const loadingState = ref<"loading" | "loaded" | "error">("loading");
const handleLoad = () => {
loadingState.value = "loaded";
emit("load");
};
const handleError = () => {
loadingState.value = "error";
emit("error");
};
</script>
性能优化点
- 格式智能选择:浏览器自动选择支持的最优格式
- 懒加载支持:默认启用懒加载,提升页面加载速度
- 错误处理:完善的错误处理和回退机制
- 尺寸优化:支持响应式图片,根据屏幕尺寸加载合适的图片
开发与贡献指南
本地开发流程
# 1. 克隆项目
git clone [项目地址]
cd glorycloud-nuxt-web
# 2. 初始化 submodule
git submodule update --init --recursive
# 3. 安装依赖
pnpm install
# 4. 进入 storybook 仓库安装依赖
cd storybook-repo
pnpm install
cd ..
# 5. 启动开发环境
pnpm run storybook:dev
Storybook 地址
- 生产环境: 项目地址
如何添加新组件
1. 创建组件文件
# 在 components/global/Xx/ 目录下创建新组件
mkdir components/global/Xx/YourComponent
cd components/global/Xx/YourComponent
2. 组件模板代码
<!-- index.vue -->
<template>
<div class="xx-your-component" :class="componentClass">
<slot />
</div>
</template>
<script setup lang="ts">
interface Props {
variant?: "default" | "primary" | "secondary";
size?: "small" | "medium" | "large";
}
const props = withDefaults(defineProps<Props>(), {
variant: "default",
size: "medium",
});
const componentClass = computed(() => [
`xx-your-component--${props.variant}`,
`xx-your-component--${props.size}`,
]);
</script>
<style lang="scss" scoped>
.xx-your-component {
// 组件样式
&--default {
// 默认变体样式
}
&--primary {
// 主要变体样式
}
&--small {
// 小尺寸样式
}
}
</style>
3. 创建 Storybook 故事
// storybook-repo/stories/components/YourComponent.stories.ts
import type { Meta, StoryObj } from "@storybook/vue3";
import YourComponent from "@/components/global/Xx/YourComponent/index.vue";
const meta: Meta<typeof YourComponent> = {
title: "基础组件/你的组件 YourComponent",
component: YourComponent,
parameters: {
layout: "padded",
docs: {
description: {
component: "组件描述和使用说明",
},
},
},
argTypes: {
variant: {
control: "select",
options: ["default", "primary", "secondary"],
description: "组件变体",
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const 默认用法: Story = {
args: {
variant: "default",
},
};
发版流程
我们使用 Git Submodule + 独立仓库的方式管理版本:
# 1. 开发完成后,提交 Storybook 仓库
cd storybook-repo
git add .
git commit -m "feat: 添加新组件 YourComponent"
git push origin main
# 2. 回到主项目,更新 submodule 引用
cd ..
git add storybook-repo
git commit -m "docs: 更新 storybook 文档"
git push origin main
# 3. 自动部署
# GitLab CI/CD 会自动构建和部署 Storybook
未来规划(Roadmap)
timeline
title GloryCloud UI 发展规划
section 2024 Q1
组件库基础建设 : 完成核心组件开发
: 建立 Storybook 文档体系
: 实现主题系统
section 2024 Q2
功能完善阶段 : 新增 15+ 业务组件
: 完善 TypeScript 类型定义
: 优化构建和打包流程
section 2024 Q3
生态建设 : 发布 npm 包
: 提供 CLI 工具
: 建立组件市场
section 2024 Q4
性能优化 : 实现按需加载
: 优化包体积
: 提升运行时性能
section 2025 Q1
移动端适配 : 完善响应式设计
: 新增移动端专用组件
: 支持触摸手势
section 2025 Q2
国际化支持 : 多语言支持
: 全球化主题
: 文档国际化
近期目标(Q1 2024)
- ✅ 完成 25+ 核心组件开发
- ✅ 建立完整的 Storybook 文档体系
- ✅ 实现主题系统和暗色模式
- 🔄 优化组件 API 设计
- 📋 完善单元测试覆盖
中期目标(Q2-Q3 2024)
- 📋 发布正式版本到 npm
- 📋 提供 Vue DevTools 插件
- 📋 建立组件使用统计和反馈机制
- 📋 开发 CLI 工具简化组件使用
- 📋 建立设计系统文档
长期目标(Q4 2024 - Q2 2025)
- 📋 支持 React 版本组件库
- 📋 提供 Figma 设计资源
- 📋 建立组件市场和插件生态
- 📋 支持低代码平台集成
- 📋 提供企业级定制服务
技术亮点总结
🎨 设计系统完整性
- 统一的设计语言和视觉规范
- 完整的主题系统
- 响应式设计和移动端适配
⚡ 开发体验优化
- 完整的 TypeScript 类型支持
- 丰富的 Storybook 文档和示例
- 热重载和快速开发反馈
🔧 工程化水平
- 独立仓库管理和版本控制
- 自动化构建和部署流程
- 完善的代码规范和质量保证
📈 性能表现
- 按需加载和 Tree-shaking 支持
- 现代图片格式优化
- 运行时性能优化