面向 Schema 编程,Vue 3 页面级渲染引擎!轻松玩转各场景 Validation!

基于 Vue 3 技术栈,无论是 UI 框架,还是低代码平台,很少有能将校验的功能做到既非常简单又方便灵活扩展的。这不,小卷拍着胸脯骄傲的说,咱们 vue-juan-form 做到了!废话不多说,上例子!

整合 vue-juan-form

先确保工程里安装 Vue 3 和 Element Plus,因为目前该框架初版还仅支持 Element Plus,内置的组件均为按需显式引入其相关的 UI 组件。先安装依赖:

shell 复制代码
npm i vue-juan-form

入口 main.ts 引入样式:

ts 复制代码
...
import 'vue-juan-form/style.css'
...

vue 页面引入组件很简单:

html 复制代码
<script setup lang="ts">
import JuanForm from 'vue-juan-form'
import { schema } from '@/schemas/validation/test-01.ts'
</script>

<template>
  <JuanForm :schema="schema" />
</template>

各示例页面结构都同上,区别只是各个 schema 不同。先看基本的校验。

基础校验

页面的展示效果如下,必填字段的标签上会有星号。

初始化不会显示错误提示。编辑字段时只校验当前字段,提交时校验整份表单。重置会清空数据,并清除错误提示。

字段上写 validation。多种规则用 | 分隔,规则名和参数用 : 分隔,多个参数用 , 分隔。

提交按钮写 nativeType: 'submit',检查整份表单。重置注入 form,调用 reset()。

schema 如下:

ts 复制代码
import type { FormApi, FormSchema, ScopeState } from 'vue-juan-form'

export const schema: FormSchema = {
  children: [
    {
      items: [
        {
          prop: 'name',
          label: '姓名',
          type: 'Input',
          props: { placeholder: '请输入姓名' },
          validation: 'required',
        },
        {
          prop: 'email',
          label: '邮箱',
          type: 'Input',
          props: { placeholder: '请输入邮箱' },
          validation: 'required|email',
        },
      ],
    },
    {
      items: [
        {
          prop: 'age',
          label: '年龄',
          type: 'Input',
          props: { type: 'number', placeholder: '18~60' },
          validation: 'min:18|max:60',
        },
        {
          prop: 'code',
          label: '验证码',
          type: 'Input',
          props: { placeholder: '4 位' },
          validation: 'required|length:4,4',
        },
        {
          prop: 'mobile',
          label: '手机',
          type: 'Input',
          props: { placeholder: '11 位手机号' },
          validation: 'required|matches:/^1[3-9]\d{9}$/',
        },
      ],
    },
    {
      items: [
        {
          type: 'Html',
          col: 'auto',
          plain: true,
          attrs: { style: { justifyContent: 'flex-end', gap: '8px' } },
          children: [
            {
              items: [
                {
                  label: '提交',
                  type: 'Button',
                  props: { type: 'primary', nativeType: 'submit' },
                },
                {
                  label: '重置',
                  type: 'Button',
                  props: {
                    nativeType: 'button',
                    onClick: {
                      inject: ['form'],
                      handle: (_scope: ScopeState, form: FormApi) => {
                        form.reset()
                      },
                    },
                  },
                },
              ],
            },
          ],
        },
      ],
    },
  ],
}

栅格 :children 是一层,其中每一项是一行,items 是这一行的字段。行上 cols 默认 16;字段 col 默认 auto,平均分配。提交和重置放在同一个 Html 里,写 plain: true、col: 'auto',两个按钮并排靠右。

自定义校验

内置规则不够时,在字段上写 validationRules,失败就抛 RuleError。

确认密码

先看要实现的场景:

关键配置:确认密码写 validation: 'required|same',validationRules.same 里比较两个字段,失败抛 RuleError。两个值都不为空才比较。规则写在一个字段上,改密码或确认密码都会执行。

schema 如下:

ts 复制代码
import { RuleError, type FormSchema, type ScopeState } from 'vue-juan-form'

export const schema: FormSchema = {
  children: [
    {
      items: [
        {
          prop: 'password',
          label: '密码',
          labelWidth: '5em',
          type: 'Input',
          props: { type: 'password', placeholder: '请输入密码' },
          validation: 'required',
        },
        {
          prop: 'confirm',
          label: '确认密码',
          labelWidth: '5em',
          type: 'Input',
          props: { type: 'password', placeholder: '请再输入一次' },
          validation: 'required|same',
          validationRules: {
            same(scope: ScopeState) {
              if (!scope.password || !scope.confirm) return
              if (scope.password !== scope.confirm) throw new RuleError('两次输入的密码不一致')
            },
          },
        },
      ],
    },
    // 提交、重置:Html 写 plain: true、col: 'auto',两个按钮放在里面靠右。与基础校验的按钮行相同,所以省略
  ],
}

上面是写在这一张表里的内联规则。多处复用时用 defineRule 登记一次。handle 第一个参数仍是作用域;当前值和 validation 里 : 后面的参数,注入 validationContext 读取。本层直接读作用域,跨层用 $parent、$root。validation 写成 same:password,表示和密码字段比较。

ts 复制代码
import { defineRule, RuleError, type FormSchema, type ScopeState, type ValidationContext } from 'vue-juan-form'

defineRule('same', {
  inject: ['validationContext'],
  handle: (scope: ScopeState, v: ValidationContext) => {
    const other = scope[v.args[0]!]
    if (!v.value || !other) return
    if (v.value !== other) throw new RuleError('两次输入的密码不一致')
  },
})

export const schema: FormSchema = {
  children: [
    {
      items: [
        // 密码字段:prop 为 password,validation 为 required。与上面内联规则那一段相同,所以省略
        {
          prop: 'confirm',
          label: '确认密码',
          labelWidth: '5em',
          type: 'Input',
          props: { type: 'password', placeholder: '请再输入一次' },
          validation: 'required|same:password',
        },
      ],
    },
    // 提交、重置:Html 写 plain: true、col: 'auto',两个按钮放在里面靠右。与上面内联规则那一段相同,所以省略
  ],
}

校验不都为空

用户名、邮箱、手机号不能全部为空。先看要实现的场景:

关键配置:规则写在用户名上,validation 写成 length:2,16|+notAllEmpty。+ 表示当前值为空时仍然执行。邮箱、手机号只在有值时校验 email、matches。

schema 如下:

ts 复制代码
import { RuleError, type FormSchema, type ScopeState } from 'vue-juan-form'

export const schema: FormSchema = {
  children: [
    {
      items: [
        {
          prop: 'username',
          label: '用户名',
          type: 'Input',
          props: { placeholder: '请输入用户名' },
          validation: 'length:2,16|+notAllEmpty',
          validationRules: {
            notAllEmpty(scope: ScopeState) {
              if (!scope.username && !scope.email && !scope.phone) {
                throw new RuleError('用户名、邮箱、手机号不能都为空')
              }
            },
          },
        },
        {
          prop: 'email',
          label: '邮箱',
          type: 'Input',
          props: { placeholder: '请输入邮箱' },
          validation: 'email',
        },
        {
          prop: 'phone',
          label: '手机号',
          type: 'Input',
          props: { placeholder: '请输入手机号' },
          validation: 'matches:/^1[3-9]\d{9}$/',
        },
      ],
    },
    // 提交、重置:Html 写 plain: true、col: 'auto',两个按钮放在里面靠右。与基础校验的按钮行相同,所以省略
  ],
}

多处复用同样用 defineRule。validation 写成 +notAllEmpty:email,phone,参数名从作用域上取邮箱和手机号。

ts 复制代码
import { defineRule, RuleError, type FormSchema, type ScopeState, type ValidationContext } from 'vue-juan-form'

defineRule('notAllEmpty', {
  inject: ['validationContext'],
  handle: (scope: ScopeState, v: ValidationContext) => {
    if (!v.value && v.args.every((name) => !scope[name])) {
      throw new RuleError('用户名、邮箱、手机号不能都为空')
    }
  },
})

export const schema: FormSchema = {
  children: [
    {
      items: [
        {
          prop: 'username',
          label: '用户名',
          type: 'Input',
          props: { placeholder: '请输入用户名' },
          validation: 'length:2,16|+notAllEmpty:email,phone',
        },
        // 邮箱、手机号:validation 分别为 email、matches。与上面内联规则那一段相同,所以省略
      ],
    },
    // 提交、重置:Html 写 plain: true、col: 'auto',两个按钮放在里面靠右。与上面内联规则那一段相同,所以省略
  ],
}

分层校验

整份表单的提交前面已经写过:按钮 nativeType: 'submit' 会检查整个 JuanForm。这一节换成各层自己提交,注入 layer,调用 check(),只检查当前这一层。

投保人、被保人各是一组,组里各有一个提交。受益人在列表里点击修改,弹框里再提交一次。三次提交分别检查各自所在的层。

先看要实现的场景:

关键配置:组里的按钮写 nativeType: 'button',onClick 注入 layer,调用 layer.check()。不要写成 submit,否则会检查整份表单。弹框确定按钮检查的是弹框这一层。

schema 如下:

ts 复制代码
import type { DialogApi, FormSchema, LayerApi, ScopeState } from 'vue-juan-form'

const relations = [
  { label: '配偶', value: 'spouse' },
  { label: '子女', value: 'child' },
  { label: '父母', value: 'parent' },
]

export const schema: FormSchema = {
  children: [
    {
      items: [
        {
          prop: 'applicant',
          label: '投保人',
          type: 'Group',
          children: [
            {
              items: [
                {
                  prop: 'name',
                  label: '姓名',
                  type: 'Input',
                  props: { placeholder: '请输入投保人姓名' },
                  validation: 'required',
                },
              ],
            },
            {
              items: [
                {
                  label: '提交',
                  type: 'Button',
                  props: {
                    type: 'primary',
                    nativeType: 'button',
                    style: { textAlign: 'right' },
                    onClick: {
                      inject: ['layer'],
                      handle: (scope: ScopeState, layer: LayerApi) => {
                        if (layer.check()) console.log('投保人通过', scope.name)
                      },
                    },
                  },
                },
              ],
            },
          ],
        },
      ],
    },
    {
      items: [
        {
          prop: 'insured',
          label: '被保人',
          type: 'Group',
          children: [
            {
              items: [
                {
                  prop: 'name',
                  label: '姓名',
                  type: 'Input',
                  props: { placeholder: '请输入被保人姓名' },
                  validation: 'required',
                },
              ],
            },
            {
              items: [
                {
                  label: '提交',
                  type: 'Button',
                  props: {
                    type: 'primary',
                    nativeType: 'button',
                    style: { textAlign: 'right' },
                    onClick: {
                      inject: ['layer'],
                      handle: (scope: ScopeState, layer: LayerApi) => {
                        if (layer.check()) console.log('被保人通过', scope.name)
                      },
                    },
                  },
                },
              ],
            },
          ],
        },
      ],
    },
    {
      items: [
        {
          prop: 'beneficiaries',
          label: '受益人',
          type: 'List',
          col: 'auto',
          key: 'id',
          children: [
            {
              items: [
                {
                  prop: 'name',
                  label: '姓名',
                  type: 'Input',
                  col: 6,
                  props: { placeholder: '受益人姓名' },
                },
                {
                  prop: 'relation',
                  label: '关系',
                  type: 'Select',
                  col: 6,
                  props: {
                    placeholder: '请选择关系',
                    clearable: true,
                    options: relations,
                  },
                },
                {
                  label: '修改',
                  type: 'Button',
                  col: 4,
                  props: {
                    nativeType: 'button',
                    onClick: {
                      inject: ['dialog'],
                      handle: (_scope: ScopeState, dialog: DialogApi) => {
                        dialog.open('editBeneficiary')
                      },
                    },
                  },
                },
              ],
            },
          ],
        },
        {
          name: 'editBeneficiary',
          type: 'Dialog',
          label: '修改受益人',
          children: [
            {
              items: [
                {
                  prop: 'name',
                  label: '姓名',
                  type: 'Input',
                  props: { placeholder: '受益人姓名' },
                  validation: 'required',
                },
                {
                  prop: 'relation',
                  label: '关系',
                  type: 'Select',
                  props: {
                    placeholder: '请选择关系',
                    clearable: true,
                    options: relations,
                  },
                  validation: 'required',
                },
              ],
            },
          ],
        },
      ],
    },
    {
      items: [
        {
          label: '添加受益人',
          type: 'Button',
          props: {
            nativeType: 'button',
            onClick: {
              handle: (scope: ScopeState) => {
                const list = scope.beneficiaries as Record<string, unknown>[]
                list.push({})
              },
            },
          },
        },
      ],
    },
  ],
}

行内校验

列表、表格的行内编辑,校验仍写在字段的 validation 上:姓名 required,电话 matches。错误提示由引擎自己展示。行上的检查按钮注入 layer,调用 check(),只检查这一行。表格的添加按钮写在表格上面。

先看要实现的场景:

schema 如下:

ts 复制代码
import type { FormSchema, LayerApi, ScopeState } from 'vue-juan-form'

const phone = 'matches:/^1[3-9]\d{9}$/'

function checkRow(_scope: ScopeState, layer: LayerApi) {
  layer.check()
}

function addRow(scope: ScopeState, prop: string) {
  const list = scope[prop] as Record<string, unknown>[]
  list.push({})
}

export const schema: FormSchema = {
  children: [
    {
      items: [
        {
          prop: 'listContacts',
          label: '列表',
          type: 'List',
          col: 'auto',
          key: 'id',
          children: [
            {
              items: [
                {
                  prop: 'name',
                  label: '姓名',
                  type: 'Input',
                  col: 6,
                  props: { placeholder: '请输入姓名' },
                  validation: 'required',
                },
                {
                  prop: 'phone',
                  label: '电话',
                  type: 'Input',
                  col: 6,
                  props: { placeholder: '11 位手机号' },
                  validation: phone,
                },
                {
                  label: '检查',
                  type: 'Button',
                  col: 4,
                  props: {
                    nativeType: 'button',
                    onClick: { inject: ['layer'], handle: checkRow },
                  },
                },
              ],
            },
          ],
        },
      ],
    },
    {
      items: [
        {
          label: '添加一行',
          type: 'Button',
          props: {
            nativeType: 'button',
            onClick: {
              handle: (scope: ScopeState) => addRow(scope, 'listContacts'),
            },
          },
        },
      ],
    },
    {
      items: [
        {
          label: '添加一行',
          type: 'Button',
          props: {
            nativeType: 'button',
            onClick: {
              handle: (scope: ScopeState) => addRow(scope, 'tableContacts'),
            },
          },
        },
      ],
    },
    {
      items: [
        {
          prop: 'tableContacts',
          label: '表格',
          type: 'Table',
          col: 'auto',
          key: 'id',
          props: { border: true, style: { width: '100%' } },
          columns: [
            {
              prop: 'name',
              label: '姓名',
              type: 'Input',
              minWidth: 160,
              props: { placeholder: '请输入姓名' },
              validation: 'required',
            },
            {
              prop: 'phone',
              label: '电话',
              type: 'Input',
              minWidth: 180,
              props: { placeholder: '11 位手机号' },
              validation: phone,
            },
            {
              label: '检查',
              width: 100,
              custom: {
                items: [
                  {
                    label: '检查',
                    type: 'Button',
                    props: {
                      nativeType: 'button',
                      onClick: { inject: ['layer'], handle: checkRow },
                    },
                  },
                ],
              },
            },
          ],
        },
      ],
    },
  ],
}

带嵌套的外层校验

组和行各自检查前面已经写过。最外层的提交仍然注入 layer,调用 check() ,和分层校验相同。按钮写在最外层,当前层就是整份表单。

先看要实现的场景:

schema 如下:

ts 复制代码
import type { FormApi, FormSchema, LayerApi, ScopeState } from 'vue-juan-form'

const phone = 'matches:/^1[3-9]\d{9}$/'

function checkRow(_scope: ScopeState, layer: LayerApi) {
  layer.check()
}

export const schema: FormSchema = {
  children: [
    {
      items: [
        {
          prop: 'applicant',
          label: '投保人',
          type: 'Group',
          children: [
            {
              items: [
                {
                  prop: 'name',
                  label: '姓名',
                  type: 'Input',
                  props: { placeholder: '请输入投保人姓名' },
                  validation: 'required',
                },
              ],
            },
            {
              items: [
                {
                  type: 'Html',
                  col: 'auto',
                  plain: true,
                  attrs: { style: { justifyContent: 'flex-end', gap: '8px' } },
                  children: [
                    {
                      items: [
                        {
                          label: '提交',
                          type: 'Button',
                          props: {
                            type: 'primary',
                            nativeType: 'button',
                            onClick: {
                              inject: ['layer'],
                              handle: (scope: ScopeState, layer: LayerApi) => {
                                if (layer.check()) console.log('投保人通过', scope.name)
                              },
                            },
                          },
                        },
                        {
                          label: '重置',
                          type: 'Button',
                          props: {
                            nativeType: 'button',
                            onClick: {
                              inject: ['form'],
                              handle: (_scope: ScopeState, form: FormApi) => {
                                form.reset()
                              },
                            },
                          },
                        },
                      ],
                    },
                  ],
                },
              ],
            },
          ],
        },
      ],
    },
    {
      items: [
        {
          label: '添加一行',
          type: 'Button',
          props: {
            nativeType: 'button',
            onClick: {
              handle: (scope: ScopeState) => {
                const list = scope.contacts as Record<string, unknown>[]
                list.push({})
              },
            },
          },
        },
      ],
    },
    {
      items: [
        {
          prop: 'contacts',
          label: '表格',
          type: 'Table',
          col: 'auto',
          key: 'id',
          props: { border: true, style: { width: '100%' } },
          columns: [
            {
              prop: 'name',
              label: '姓名',
              type: 'Input',
              minWidth: 160,
              props: { placeholder: '请输入姓名' },
              validation: 'required',
            },
            {
              prop: 'phone',
              label: '电话',
              type: 'Input',
              minWidth: 180,
              props: { placeholder: '11 位手机号' },
              validation: phone,
            },
            {
              label: '检查',
              width: 100,
              custom: {
                items: [
                  {
                    label: '检查',
                    type: 'Button',
                    props: {
                      nativeType: 'button',
                      onClick: { inject: ['layer'], handle: checkRow },
                    },
                  },
                ],
              },
            },
          ],
        },
      ],
    },
    {
      items: [
        {
          label: '提交',
          type: 'Button',
          props: {
            type: 'primary',
            nativeType: 'button',
            style: { textAlign: 'right' },
            onClick: {
              inject: ['layer'],
              handle: (_scope: ScopeState, layer: LayerApi) => {
                if (layer.check()) console.log('总提交通过')
              },
            },
          },
        },
      ],
    },
  ],
}

行间校验定制需求

表格里的姓名不能重复。姓名为空时不判断重复。规则仍写在姓名的 validation 上。

先看要实现的场景:

关键配置:姓名写 validation: '+unique'。规则注入 node,find 取出同名单元格,设置背景色,并抛 RuleError。+ 表示值为空时仍然执行,用来清除背景色。添加按钮写在表格上面。

schema 如下:

ts 复制代码
import { RuleError, type FormSchema, type NodeApi, type ScopeState } from 'vue-juan-form'

export const schema: FormSchema = {
  children: [
    {
      items: [
        {
          label: '添加一行',
          type: 'Button',
          props: {
            nativeType: 'button',
            onClick: {
              handle: (scope: ScopeState) => {
                const list = scope.beneficiaries as Record<string, unknown>[]
                list.push({})
              },
            },
          },
        },
      ],
    },
    {
      items: [
        {
          prop: 'beneficiaries',
          label: '受益人',
          type: 'Table',
          col: 'auto',
          key: 'id',
          props: { border: true, style: { width: '100%' } },
          columns: [
            {
              prop: 'name',
              label: '姓名',
              type: 'Input',
              minWidth: 200,
              props: { placeholder: '请输入姓名' },
              validation: '+unique',
              validationRules: {
                unique: {
                  inject: ['node'],
                  handle(scope: ScopeState, node: NodeApi) {
                    const cells = node.find('$parent.beneficiaries', { name: scope.name }, 'name')
                    if (!scope.name || cells.length < 2) {
                      for (const cell of cells) cell.props.style = undefined
                      return
                    }
                    let n = 0
                    for (const ch of String(scope.name)) n = n * 31 + ch.charCodeAt(0)
                    for (const cell of cells) cell.props.style = { background: `hsl(${(n * 137) % 360} 70% 90%)` }
                    throw new RuleError('受益人重复')
                  },
                },
              },
            },
            {
              prop: 'share',
              label: '份额',
              type: 'Input',
              minWidth: 120,
              props: { type: 'number', min: 1, max: 10, placeholder: '份额' },
              defaultValue: 1,
            },
          ],
        },
      ],
    },
  ],
}

表级校验

各行份额合计不能超过 10。校验写在表格的 validation 上,和字段相同,不另放输入框。合计超过 10 时,添加按钮不再加行。

先看要实现的场景:

关键配置:表格写 validation: 'cap',validationRules.cap 里对数组求和,超过 10 抛 RuleError。添加按钮写在表格上面,handle 读同一项合计,超过 10 就返回。

schema 如下:

ts 复制代码
import { RuleError, type FormSchema, type ScopeState } from 'vue-juan-form'

export const schema: FormSchema = {
  children: [
    {
      items: [
        {
          label: '添加一行',
          type: 'Button',
          props: {
            nativeType: 'button',
            onClick: {
              handle: (scope: ScopeState) => {
                const list = scope.beneficiaries as Record<string, unknown>[]
                if (list.reduce((n, row) => n + Number(row.share || 0), 0) > 10) return
                list.push({})
              },
            },
          },
        },
      ],
    },
    {
      items: [
        {
          prop: 'beneficiaries',
          label: '受益人',
          type: 'Table',
          col: 'auto',
          key: 'id',
          props: { border: true, style: { width: '100%' } },
          validation: 'cap',
          validationRules: {
            cap(scope: ScopeState) {
              const list = scope.beneficiaries as Record<string, unknown>[]
              if (list.reduce((n, row) => n + Number(row.share || 0), 0) > 10) {
                throw new RuleError('总份额不能超过10')
              }
            },
          },
          columns: [
            {
              prop: 'name',
              label: '姓名',
              type: 'Input',
              minWidth: 160,
              props: { placeholder: '请输入姓名' },
            },
            {
              prop: 'share',
              label: '份额',
              type: 'Input',
              minWidth: 120,
              props: { type: 'number', min: 1, max: 10, placeholder: '份额' },
              defaultValue: 1,
            },
          ],
        },
      ],
    },
  ],
}

自动定位错误

提交检查没有通过时,自动滚动到第一个出错的字段,并让输入框获得焦点 。没有新配置,表单默认提交和层上的 check() 都是这样。

先看要实现的场景:

schema 如下:

ts 复制代码
import type { FormSchema } from 'vue-juan-form'

export const schema: FormSchema = {
  children: [
    // 外层 Html 限制高度、字段之间拉开距离,只为演示滚动,与定位无关,所以省略
    {
      items: [
        {
          prop: 'name',
          label: '姓名',
          type: 'Input',
          props: { placeholder: '请输入姓名' },
          validation: 'required',
        },
      ],
    },
    {
      items: [
        {
          prop: 'email',
          label: '邮箱',
          type: 'Input',
          props: { placeholder: '请输入邮箱' },
          validation: 'required|email',
        },
      ],
    },
    {
      items: [
        {
          prop: 'phone',
          label: '手机',
          type: 'Input',
          props: { placeholder: '11 位手机号' },
          validation: 'required',
        },
      ],
    },
    {
      items: [
        {
          label: '提交',
          type: 'Button',
          props: { type: 'primary', nativeType: 'submit', style: { textAlign: 'right' } },
        },
      ],
    },
  ],
}

到这里,校验各场景都覆盖完了。欢迎小伙伴自己试。有新的校验需求搞不定,评论区找我。你的建议和意见,会写进咱们 vue-juan-form 后面的版本里。

相关推荐
SL_staff36 分钟前
私有化低代码:面向强监管行业的可控数字化实践
java·低代码
FungLeo3 小时前
成为全栈·Vue 管理后台篇·用 fetch 建一层可控的请求内核
前端·javascript·vue.js·http·fetch·formdata·接口封装
#六脉神剑4 小时前
myBuilder 11 月新版本:AI辅助设计
ide·人工智能·低代码·ai·mybuilder
FungLeo5 小时前
成为全栈·Vue 管理后台篇·Vue Router如何守住多角色后台边界
前端·javascript·vue.js·rbac·vue router·路由守卫·前端权限
FungLeo5 小时前
成为全栈·Vue 管理后台篇·分页只显示第一页:从接口total 定位到前端修复
前端·vue.js·状态模式·分页·naive ui·服务端分页·前端问题排查
FungLeo6 小时前
成为全栈·Vue管理后台篇·仪表盘不是几张卡片:组合统计接口与角色视图
前端·javascript·vue.js·rbac·仪表盘·管理后台·tanstack query
ruanno_top6 小时前
可搜索的下拉框,键盘按不动:焦点进了下拉,事件还挂在触发器上
vue.js
FungLeo7 小时前
成为全栈·Vue 管理后台篇·Vue Query:服务端数据缓存、query key与变更失效
vue.js·数据获取·前端缓存·前端状态管理·tanstack query·vue query
北城笑笑7 小时前
Vue 109 ,从零搭建 Vue3 + Electron 工程:生态、安全桥接与打包选型( Vue + Electron )
前端·javascript·vue.js·安全·electron·vue