Vue 3.0 与 Layui 融合的桌面端组件库实战

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介: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 组件的初始化函数,在组件实例创建之前执行。它有两个主要用途:

  1. 声明响应式状态
  2. 定义方法、计算属性、侦听器等

它的签名是:

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?

一个好的组合函数应该具备以下特征:

  1. 单一职责 :只解决一个问题(如表单验证、异步加载、节流防抖)
  2. 接口清晰 :输入参数明确,输出结构规范
  3. 无副作用 :不直接操作 DOM 或修改全局状态
  4. 可测试性强 :纯逻辑,便于单元测试

我们来写一个通用的表单验证 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
  }
}

是不是感觉有点长?别急,我们一步步分析它的价值:

  1. 类型安全 :用了泛型 T extends Record<string, any> ,确保 initialValues rules 字段一一对应;
  2. 灵活扩展 :支持同步和异步校验(比如检查用户名是否已被占用);
  3. 易于集成 :返回的 fields 对象可以直接用于 v-model 绑定;
  4. 独立可测 :不依赖任何组件上下文,可以直接写单元测试。
在组件中使用这个 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>

这看起来挺直观,但实际上隐藏着几个严重问题:

  1. 强依赖 DOM :必须先有 #user-table ,否则报错;
  2. 非响应式 :数据变了不会自动更新,得手动 reload()
  3. 配置即代码 cols 是 JS 对象,没法用模板语法渲染;
  4. 全局污染 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' })

我们改为状态

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Vue 3.0 作为 Vue.js 的重大升级,凭借 Composition API、重构的响应式系统和 Teleport 等新特性,显著提升了开发效率与性能。结合经典前端框架 Layui 丰富的桌面端 UI 组件(如表格、表单、弹窗等),开发者可通过插件化机制将 Layui 封装为 Vue 3.0 自定义组件,实现高效复用与统一管理。本文介绍如何在 Vue 3.0 项目中集成 Layui,涵盖安装、封装、样式引入与组件使用全流程,并参考 “layui-vue-master” 示例项目,助力构建高性能、易维护的企业级桌面应用。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值