简介:Vue 3.0 作为 Vue.js 的重大升级,凭借 Composition API、重构的响应式系统和 Teleport 等新特性,显著提升了开发效率与性能。结合经典前端框架 Layui 丰富的桌面端 UI 组件(如表格、表单、弹窗等),开发者可通过插件化机制将 Layui 封装为 Vue 3.0 自定义组件,实现高效复用与统一管理。本文介绍如何在 Vue 3.0 项目中集成 Layui,涵盖安装、封装、样式引入与组件使用全流程,并参考 “layui-vue-master” 示例项目,助力构建高性能、易维护的企业级桌面应用。
Vue 3.0 核心新特性与桌面端组件库的设计理念
你有没有遇到过这样的情况:一个表单组件写到一半,突然发现校验逻辑散落在 data 、 methods 和 watch 里,想找某个字段的验证规则得翻三四个地方?🤯 或者团队里两个人同时改同一个大组件,结果 merge 的时候冲突一堆,根本不知道谁动了哪块逻辑?
这在 Vue 2 的 Options API 时代太常见了。尤其是当我们面对像 Layui 这种功能丰富、交互复杂的传统 UI 库时,想要把它现代化迁移到 Vue 生态,光靠“重写 HTML 结构 + 套一层 template”是远远不够的——我们必须从根本上重构它的设计哲学。
而 Vue 3.0 的发布,就像一场及时雨 🌧️,带来了 Composition API、基于 Proxy 的响应式系统、Teleport 等一系列杀手级特性,让我们终于有机会把那些“命令式操作 DOM”的老古董,升级成真正意义上的“数据驱动组件”。
想象一下:你现在要封装一个带分页、排序、筛选、多选、批量操作的表格组件。如果是 Vue 2,你会怎么做?大概率是:
-
data里放一堆状态(current page, sort field, filters…) -
methods写一堆函数(handlePageChange, handleSort, fetchList…) -
computed放点衍生数据 -
watch监听某些参数变化触发请求
然后某天产品经理说:“这个表格也用在用户管理页,那个也在订单中心要用。” 于是你开始复制粘贴,接着就是噩梦般的维护成本 💣。
但在 Vue 3 的世界里?我们换一种思路:
// composables/useTable.js
import { ref, watch } from 'vue'
export function useTable(apiUrl, initialParams = {}) {
const data = ref([])
const loading = ref(false)
const params = ref({ page: 1, limit: 10, ...initialParams })
const fetchData = async () => {
loading.value = true
try {
const res = await fetch(`${apiUrl}?${new URLSearchParams(params.value)}`)
const result = await res.json()
data.value = result.list
} finally {
loading.value = false
}
}
watch(params, fetchData, { deep: true })
const setPage = (page) => {
params.value.page = page
}
const setFilter = (filters) => {
Object.assign(params.value, filters)
params.value.page = 1 // 重置页码
}
return {
data,
loading,
params,
setPage,
setFilter,
refresh: fetchData
}
}
看到没?整个表格的核心逻辑被封装成了一个可复用的 Hook!它不关心你是 <table> 还是 <div> 渲染,也不绑定任何样式——只专注一件事: 如何管理表格的状态和行为 。
然后你在不同的组件中轻松调用:
<script setup>
import { useTable } from '@/composables/useTable'
const { data, loading, setPage, setFilter } = useTable('/api/users', {
role: 'admin'
})
</script>
<template>
<div v-if="loading">加载中...</div>
<ul>
<li v-for="user in data" :key="user.id">{{ user.name }}</li>
</ul>
<Pagination @change="setPage" />
</template>
这才是现代前端开发该有的样子啊!✨ 不再是“把 JS 拆成几块塞进 options”,而是“按功能组织代码”,想看分页逻辑就去看分页那块,想调试筛选就直接定位到 filter 相关函数。
这种转变的背后,其实是两个核心技术的支撑: Composition API 和 基于 Proxy 的响应式系统 。
以前我们用 Object.defineProperty 实现响应式,只能监听已存在的属性,动态添加对象字段还得用 $set ;数组方法也只能劫持那七个变异方法……各种限制让人头疼。
现在呢?Vue 3 直接上了 Proxy 👑,不仅能监听对象所有属性(包括新增的),还能完美追踪数组索引变化、length 变化,深层嵌套也不怕。这意味着你可以自由地做 state.user.profile.avatar = 'xxx' ,不用再担心视图不更新。
而且,这一切都和 Composition API 天然契合。 ref 、 reactive 创建的数据天生就是响应式的,配合 computed 、 watch 构成完整的反应式流。你在 setup() 里写的每一个变量、函数,都可以按业务模块打包成独立的 .ts 文件,想怎么组合就怎么组合。
比如做一个登录弹窗,你可以这样拆解:
useFormValidation() // 表单校验
useAsyncAction() // 提交防抖 + loading 状态
useModalVisibility() // 控制显示隐藏 + 动画钩子
useKeyboardSupport() // esc 关闭、回车提交
每个都是独立可测试的小单元,最后拼起来就是一个完整功能。这种“乐高式开发”,才是大型项目可持续维护的关键!
更酷的是,Vue 3 还加了个叫 Teleport 的功能。还记得 Layui 里的 layer.open() 吗?那个全局弹出层经常因为父容器 overflow: hidden 被裁剪,或者 z-index 层级打架搞得焦头烂额?
现在你可以这么写:
<Teleport to="body">
<div class="modal" v-show="visible">
我会脱离当前 DOM 结构,直接挂载到 body 下!🎉
</div>
</Teleport>
一句话解决所有层级问题。语义清晰、样式可控,再也不用手动 appendChild 到 body 上去了。
所以说,Vue 3 不只是性能提升了 1.5 倍那么简单(虽然确实很快⚡),更重要的是它提供了一套全新的思维方式: 以组合式逻辑驱动 UI 封装 。
这对像 Layui 这样的传统组件库意味着什么?
意味着我们可以不再局限于“HTML 结构 + JS 初始化”的老旧范式,而是真正构建出:
- 类型安全(TypeScript 友好)
- 按需引入(Tree-shakable)
- 高度可复用(Composable)
- 易于测试(Logic Isolation)
- 主题可定制(CSS Variable + provide/inject)
的现代化 Vue 组件库。
接下来我们就深入看看,这些理念是如何一步步落地的——从最基础的 Composition API 设计思想,到具体迁移 Layui 表格、表单、弹窗的实际工程实践,再到最终通过插件机制打造出一个完整的 layui-vue 解决方案。
准备好了吗?🚀 Let’s dive in.
Composition API 与响应式系统的实践整合
说实话,刚接触 Vue 3 的时候,我也有点懵。特别是看到 setup() 函数里一堆 ref() 、 .value ,总觉得别扭:“为啥不能像 reactive 那样直接访问属性?”、“每次都要写 .value 不累吗?”
但当你真正用过一次就会明白:这不是语法负担,而是一种 精确控制粒度的设计哲学 。
Composition API 的设计哲学与编码范式
让我们回到最初的问题:为什么需要 Composition API?
答案很简单: 随着组件复杂度上升,Options API 的结构性缺陷越来越明显。
Options API 的“四分五裂”困境
假设你要做一个注册表单,包含用户名、邮箱、密码、确认密码、验证码、协议勾选等功能。如果用 Options API,你的代码会长这样:
export default {
data() {
return {
username: '',
email: '',
password: '',
confirmPassword: '',
code: '',
agree: false,
errors: {},
sending: false,
timer: null
}
},
computed: {
isFormValid() {
// 判断所有字段是否合法
},
canSendCode() {
// 是否可以发送验证码(倒计时中不能发)
}
},
methods: {
validateUsername() { /* 校验逻辑 */ },
validateEmail() { { /* 校验逻辑 */ },
validatePassword() { /* 校验逻辑 */ },
sendVerificationCode() { /* 发送验证码 */ },
handleSubmit() { /* 提交表单 */ }
},
watch: {
username: 'validateUsername',
email: 'validateEmail',
password(val) { this.validatePassword(val) }
}
}
发现问题了吗?
👉 所有跟“用户名”相关的逻辑分散在四个地方:
- data 中定义值
- watch 中监听变化
- methods 中写校验函数
- computed 中可能还要参与整体有效性判断
当你要修改用户名校验规则时,得在多个选项之间来回跳转,稍不注意就漏掉一处。更别说多人协作时,没人知道这段逻辑到底归谁管 😵💫。
这就是所谓的“横向切分”带来的维护难题——我们本应按“功能”组织代码,却被迫按照“类型”分类。
Composition API:让逻辑回归本来的样子
换成 Composition API 后,一切都变得清爽了:
<script setup>
import { ref, computed, watch } from 'vue'
// 用户名逻辑块 ✅
const username = ref('')
const usernameError = ref('')
const isUsernameValid = computed(() => {
if (!username.value) return false
if (username.value.length < 3) {
usernameError.value = '用户名至少3位'
return false
}
usernameError.value = ''
return true
})
watch(username, () => console.log('用户名变了'))
// 密码逻辑块 ✅
const password = ref('')
const confirmPassword = ref('')
const passwordError = ref('')
const isPasswordValid = computed(() => {
if (password.value.length < 6) {
passwordError.value = '密码至少6位'
return false
}
if (password.value !== confirmPassword.value) {
passwordError.value = '两次输入不一致'
return false
}
passwordError.value = ''
return true
})
// 整体表单有效性 ✅
const isValid = computed(() => isUsernameValid.value && isPasswordValid.value)
</script>
看到了吗?现在每个字段的逻辑都自成一体,你想看用户名怎么处理的,直接找到那一段就行。不需要跨区域查找,也不会误改其他字段的行为。
这不仅仅是“写法不同”,而是 思维方式的根本转变 :
| 维度 | Options API | Composition API |
|---|---|---|
| 组织方式 | 按选项分类(data/methods/computed) | 按功能聚合(form field / modal logic) |
| 复用机制 | mixins(易冲突) | composable functions(干净隔离) |
| TS 支持 | 弱,this 类型推导困难 | 强,天然支持泛型与类型注解 |
| 调试体验 | 分散,需上下文切换 | 集中,局部闭环 |
💡 小贴士:如果你正在使用 TypeScript,Composition API 的优势会更加明显。比如你可以给
useFormValidation<T>加上泛型约束,确保传入的初始值和字段名完全匹配接口定义,编译期就能发现问题。
从“配置式”到“组合式”:一场编程范式的演进
很多人觉得 Composition API 只是“语法糖”,其实不然。
它背后代表的是前端开发从“声明式配置”向“函数式组合”的进化趋势。就像 React Hooks 推动了 Function Component 成为主流一样,Vue 的 setup() 让我们也进入了“组合即能力”的新时代。
举个例子,你可以在项目中创建这样一个目录结构:
composables/
├── useFormValidation.ts
├── useAsyncAction.ts
├── useModal.ts
├── usePagination.ts
└── useDebounce.ts
每一个 .ts 文件都是一个独立的功能单元,它们之间没有耦合,却可以通过 return 暴露出来的响应式状态和方法自由组合。
这就像是在搭建一套属于自己的“前端 SDK”,而不是零散地写组件逻辑。
抽离通用逻辑:告别 Mixins 的泥潭
还记得 Vue 2 时代的 mixins 吗?虽然能实现一定程度的复用,但它有几个致命问题:
- 命名冲突 :两个 mixin 都定义了
data()里的loading,会发生覆盖。 - 隐式依赖 :A mixin 依赖 B mixin 设置的某个 state,但文档没写清楚,新人很难理解。
- 执行顺序不确定 :多个 mixin 的生命周期钩子执行顺序依赖注册顺序,容易出 bug。
而 Composition API 完美避开了这些问题。因为它本质上是 显式导入 + 显式调用 :
import { useLoading } from '@/composables/useLoading'
import { useValidation } from '@/composables/useValidation'
const { loading, start, end } = useLoading()
const { errors, validate } = useValidation(rules)
谁用了什么、返回了啥,一目了然。不存在“偷偷摸摸改状态”的情况,调试起来也方便得多。
setup() 函数与响应式核心:ref vs reactive
既然 Composition API 是入口,那我们就得搞清楚 setup() 到底是怎么工作的。
setup() 是什么?什么时候执行?
简单来说, setup() 是 Vue 3 组件的初始化函数,在组件实例创建之前执行。它有两个主要用途:
- 声明响应式状态
- 定义方法、计算属性、侦听器等
它的签名是:
setup(props: Readonly<Props>, context: SetupContext): any
其中 context 包含 { emit, slots, attrs } ,用来替代 Vue 2 中的 this.$emit 、 this.$slots 等。
重要提醒 ⚠️: 在 setup() 中不能访问 this ,因为此时组件实例还没建立。所有东西都必须通过返回值暴露给模板使用。
export default {
setup() {
const count = ref(0)
const increment = () => count.value++
return {
count,
increment
}
}
}
模板中可以直接使用 count 和 increment ,就像它们是普通的 data 和 method 一样。
ref 与 reactive:选择合适的响应式容器
Vue 3 提供了两种创建响应式数据的方式: ref 和 reactive 。它们各有适用场景,不能混为一谈。
| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 基本类型(string/number/boolean)或对象 | 仅对象(包括数组) |
| 访问方式 | .value 显式读写 | 直接属性访问 |
| 解构后是否保持响应性 | ❌(需 toRefs ) | ❌(解构丢失代理) |
| 内部实现 | 包装为 RefImpl 对象 | 使用 Proxy 代理原对象 |
来看几个实际例子:
// ✅ ref 适合简单值
const name = ref('Alice')
name.value = 'Bob' // 必须 .value
// ✅ reactive 适合复杂对象
const user = reactive({ name: 'Alice', age: 25 })
user.name = 'Bob' // 直接改
// ❌ 解构 reactive 会导致失去响应性
const { name } = user
name = 'Charlie' // ❌ 不会触发更新!
// ✅ 正确做法:使用 toRefs
const { name } = toRefs(user)
name.value = 'Charlie' // ✅ 触发更新
所以最佳实践建议:
- 简单状态(开关、计数器、字符串)→ 优先用
ref- 复杂对象(表单数据、用户信息、配置项)→ 用
reactive- 返回
reactive对象前若需解构 → 一定要用toRefs
还有一个冷知识: ref 在模板中会被自动“解包”(unwrap),所以你不需要写 {{ count.value }} ,直接 {{ count }} 就行!
<template>
<div>{{ count }}</div> <!-- 自动解包 -->
<button @click="increment">+1</button>
</template>
这是 Vue 编译器做的优化,但在 JS 中仍需手动 .value 。
函数式思维:构建可复用的组合逻辑
Composition API 最强大的地方,不是它让你能把代码写在一起,而是它鼓励你把逻辑拆出去。
如何设计一个好的 Composable?
一个好的组合函数应该具备以下特征:
- 单一职责 :只解决一个问题(如表单验证、异步加载、节流防抖)
- 接口清晰 :输入参数明确,输出结构规范
- 无副作用 :不直接操作 DOM 或修改全局状态
- 可测试性强 :纯逻辑,便于单元测试
我们来写一个通用的表单验证 Hook:
// composables/useFormValidation.ts
import { ref, computed } from 'vue'
export interface FieldRule {
required?: boolean
minLength?: number
validator?: (value: any) => boolean | Promise<boolean>
message: string
}
export interface FormField {
value: any
rules: FieldRule[]
errors: string[]
touched: boolean
}
export function useFormValidation<T extends Record<string, any>>(
initialValues: T,
rules: Partial<Record<keyof T, FieldRule[]>>
) {
const fields = {} as Record<string, FormField>
// 初始化每个字段
for (const key in initialValues) {
fields[key] = {
value: ref(initialValues[key]),
rules: rules[key] || [],
errors: ref([]),
touched: ref(false)
}
}
// 单字段校验
const validateField = async (field: string) => {
const f = fields[field]
const value = f.value.value
const fieldRules = f.rules
const newErrors: string[] = []
for (const rule of fieldRules) {
if (rule.required && !value) {
newErrors.push(rule.message)
continue
}
if (rule.minLength && value?.length < rule.minLength) {
newErrors.push(rule.message)
continue
}
if (rule.validator) {
const valid = await rule.validator(value)
if (!valid) newErrors.push(rule.message)
}
}
f.errors.value = newErrors
return newErrors.length === 0
}
// 全局校验
const validateAll = async () => {
const results = await Promise.all(
Object.keys(fields).map(field => validateField(field))
)
return results.every(r => r)
}
// 获取所有字段值
const getValues = () => {
const values: any = {}
for (const key in fields) {
values[key] = fields[key].value.value
}
return values
}
// 重置表单
const reset = () => {
for (const key in fields) {
fields[key].value.value = initialValues[key]
fields[key].errors.value = []
fields[key].touched.value = false
}
}
return {
fields,
validateField,
validateAll,
getValues,
reset
}
}
是不是感觉有点长?别急,我们一步步分析它的价值:
- 类型安全 :用了泛型
T extends Record<string, any>,确保initialValues和rules字段一一对应; - 灵活扩展 :支持同步和异步校验(比如检查用户名是否已被占用);
- 易于集成 :返回的
fields对象可以直接用于 v-model 绑定; - 独立可测 :不依赖任何组件上下文,可以直接写单元测试。
在组件中使用这个 Hook
<script setup lang="ts">
import { useFormValidation } from '@/composables/useFormValidation'
const initialValues = {
username: '',
password: ''
}
const rules = {
username: [
{ required: true, message: '请输入用户名' },
{ minLength: 3, message: '用户名不能少于3位' }
],
password: [
{ required: true, message: '请输入密码' },
{
validator: async (val) => val.length >= 6,
message: '密码至少6位'
}
]
}
const {
fields,
validateAll,
getValues,
reset
} = useFormValidation(initialValues, rules)
</script>
<template>
<form @submit.prevent="handleSubmit">
<!-- 用户名 -->
<div class="field">
<input
v-model="fields.username.value"
placeholder="用户名"
@blur="() => fields.username.touched = true"
>
<span v-if="fields.username.errors.length" class="error">
{{ fields.username.errors[0] }}
</span>
</div>
<!-- 密码 -->
<div class="field">
<input
v-model="fields.password.value"
type="password"
placeholder="密码"
@blur="() => fields.password.touched = true"
>
<span v-if="fields.password.errors.length" class="error">
{{ fields.password.errors[0] }}
</span>
</div>
<!-- 操作按钮 -->
<button type="submit">登录</button>
<button type="button" @click="reset">重置</button>
</form>
</template>
瞧,视图层变得极其简洁,所有的业务逻辑都被抽到了外面。如果你想把这个表单搬到另一个项目,只需要拷贝 useFormValidation 和这里的模板,几乎不需要调整。
数据流闭环:从用户输入到提交验证
整个流程可以用一张 Mermaid 图来概括:
graph TD
A[组件调用 useFormValidation] --> B[传入初始值与规则]
B --> C[返回 fields 对象及方法]
C --> D[模板绑定 v-model]
D --> E[用户输入触发变更]
E --> F[失焦时调用 validateField]
F --> G[错误信息实时更新]
G --> H[点击提交触发 validateAll]
H --> I[全部通过则获取 getValues()]
I --> J[提交数据]
每一步都职责分明,数据流动清晰可见。这种“响应式流水线”模型,正是现代前端架构追求的理想状态。
总结一下,Composition API 不只是一个新语法,它代表着一种更高级的抽象能力。通过函数式组合,我们可以:
- 把复杂组件拆解为若干小函数
- 实现逻辑的高度复用
- 提升类型安全性与可维护性
- 构建出真正意义上的“组件工厂”
而这,正是我们将 Layui 这类传统库迁移到 Vue 3 生态的技术基石。
Layui 组件向 Vue 3.0 自定义组件的工程化迁移
现在我们进入实战环节。
假设你是一家企业的前端负责人,公司 legacy 系统大量使用 Layui,但新项目都用 Vue 3。老板说:“能不能做个兼容方案,让老组件也能在新框架里跑起来?”
这时候你就不能再停留在“会不会用 Vue 3”的层面了,而是要思考: 如何系统性地完成一次技术栈迁移?
这不是简单的“重写”,而是一场涉及架构设计、API 抽象、样式兼容、构建流程的全链路改造。
我们的目标很明确:
将 Layui 的典型组件(表格、表单、弹窗)转化为符合 Vue 3 开发范式的现代化组件,支持 Composition API、TypeScript、按需引入,并保留原有视觉风格。
听起来挑战不小,但我们一步步来。
Layui 原生组件结构分析与抽象建模
首先得搞清楚 Layui 到底是怎么工作的。
Layui 的“命令式”本质
Layui 的核心模式是:“写 HTML + JS 初始化”。
比如表格:
<table id="user-table"></table>
<script>
layui.use('table', function(){
table.render({
elem: '#user-table',
url: '/api/users',
cols: [[
{field:'name', title:'姓名'},
{field:'email', title:'邮箱'}
]]
})
})
</script>
这看起来挺直观,但实际上隐藏着几个严重问题:
- 强依赖 DOM :必须先有
#user-table,否则报错; - 非响应式 :数据变了不会自动更新,得手动
reload(); - 配置即代码 :
cols是 JS 对象,没法用模板语法渲染; - 全局污染 :
layui.table是单例,多个表格容易互相干扰。
换句话说,它是典型的“命令式编程”——告诉浏览器“去这么做”,而不是“描述成什么样”。
而 Vue 的哲学是“声明式”——你只需要描述状态和视图的关系,剩下的交给框架自动同步。
所以我们迁移的第一步,就是 把命令式逻辑转化为数据驱动模型 。
表格组件的数据驱动重构
我们先定义类型:
// types/column.ts
export interface Column<T = any> {
field: keyof T
title: string
width?: number
render?: (row: T) => string | HTMLElement
}
然后创建 Vue 组件:
<!-- components/LTable.vue -->
<template>
<div class="l-table-container">
<!-- 表格主体 -->
<table class="l-table">
<thead>
<tr>
<th
v-for="col in columns"
:key="col.field"
:style="{ width: col.width ? `${col.width}px` : 'auto' }"
>
{{ col.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in data" :key="i">
<td
v-for="col in columns"
:key="col.field"
>
<!-- 支持自定义渲染 -->
<template v-if="col.render">
<span v-html="col.render(row)"></span>
</template>
<template v-else>
{{ row[col.field] }}
</template>
</td>
</tr>
</tbody>
</table>
<!-- 分页组件 -->
<LPagination
v-if="pagination"
:total="total"
:page-size="pageSize"
:current-page="currentPage"
@update:current-page="handlePageChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import LPagination from './LPagination.vue'
import type { Column } from '@/types/column'
interface Props<T> {
data: T[]
columns: Column<T>[]
pagination?: boolean
pageSize?: number
}
const props = withDefaults(defineProps<Props<any>>(), {
pagination: false,
pageSize: 10
})
const currentPage = ref(1)
const total = computed(() => props.data.length)
// 当前页数据(可用于本地分页)
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * props.pageSize!
return props.data.slice(start, start + props.pageSize!)
})
const emit = defineEmits<{
(e: 'page-change', page: number): void
}>()
function handlePageChange(page: number) {
currentPage.value = page
emit('page-change', page)
}
</script>
<style scoped>
.l-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.l-table th,
.l-table td {
padding: 8px;
border: 1px solid #e6e6e6;
text-align: left;
}
</style>
关键点解析:
- 泛型支持 :
Props<T>确保列字段与数据类型匹配; - 默认值处理 :
withDefaults避免运行时错误; - 响应式分页 :
paginatedData是计算属性,数据变自动更新; - 事件契约明确 :
defineEmits定义通信接口; - 样式隔离 :
scoped防止污染全局。
使用方式超级简单:
<template>
<LTable
:data="users"
:columns="columns"
pagination
@page-change="loadUsers"
/>
</template>
比起原始的 table.render({...}) ,这种方式更清晰、更可控、更容易测试。
数据流如下图所示:
graph TD
A[父组件传入 data & columns] --> B(LTable 接收 Props)
B --> C{是否启用分页?}
C -->|是| D[渲染 LPagination]
C -->|否| E[仅渲染表格]
D --> F[用户翻页]
F --> G[触发 page-change]
G --> H[父组件请求新数据]
H --> A
完全符合 Vue 的“单向数据流”原则。
表单元素的 Vue 化映射
Layui 表单靠 lay-verify="required|email" 这种 attribute 来配置校验规则,非常不灵活。
我们改成 Composition API + 自定义 Hook 的方式:
// composables/useFormField.ts
import { ref, computed } from 'vue'
export function useFormField<T>(
initialValue: T,
rules: Array<(value: T) => string | null>
) {
const value = ref(initialValue)
const errors = ref<string[]>([])
const touched = ref(false)
const isValid = computed(() => errors.value.length === 0)
const validate = () => {
const newErrors = rules
.map(rule => rule(value.value))
.filter(msg => msg !== null) as string[]
errors.value = newErrors
return newErrors.length === 0
}
const onBlur = () => {
touched.value = true
validate()
}
const reset = (val?: T) => {
value.value = val ?? initialValue
errors.value = []
touched.value = false
}
return {
value,
errors,
touched,
isValid,
validate,
onBlur,
reset
}
}
然后封装输入框组件:
<!-- components/LInput.vue -->
<template>
<div class="l-input-wrapper" :class="{ error: !isValid, touched }">
<input
:value="modelValue"
@input="$event => $emit('update:modelValue', ($event.target as HTMLInputElement).value)"
@blur="onBlur"
:placeholder="placeholder"
class="l-input"
>
<div v-if="!isValid && touched" class="error-tip">
{{ errors[0] }}
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
modelValue: string
placeholder?: string
}>()
const emit = defineEmits(['update:modelValue'])
// 使用外部注入的校验逻辑(可通过 provide/inject 传递)
</script>
这样,表单就成了真正的“数据驱动”体系,而不是靠 attribute 驱动。
弹窗组件的声明式改造
Layui 的 layer.open() 是典型的命令式调用:
layer.open({ content: 'Hello' })
我们改为状态
简介:Vue 3.0 作为 Vue.js 的重大升级,凭借 Composition API、重构的响应式系统和 Teleport 等新特性,显著提升了开发效率与性能。结合经典前端框架 Layui 丰富的桌面端 UI 组件(如表格、表单、弹窗等),开发者可通过插件化机制将 Layui 封装为 Vue 3.0 自定义组件,实现高效复用与统一管理。本文介绍如何在 Vue 3.0 项目中集成 Layui,涵盖安装、封装、样式引入与组件使用全流程,并参考 “layui-vue-master” 示例项目,助力构建高性能、易维护的企业级桌面应用。

被折叠的 条评论
为什么被折叠?



