本文将基于 Quasar 框架,针对表格(QTable)、选项卡(QTabs)、步进器(QStepper)三个高频组件,模拟真实业务场景开发简易 Demo,涵盖「数据表格筛选 + 分页联动」「表单分步提交」核心功能,帮助你掌握组件的实战用法。
<template> <q-card> <q-tab-panels v-model="tab" animated> <q-tab-panel name="mails" style="height: 600px" class="bg-blue-2"> <div class="q-pa-md"> <!-- 使用 q-table 组件创建一个数据表格 title="Treats" 设置表格标题 :rows="rows" 绑定表格数据 :columns="columns" 绑定列配置 row-key="name" 设置行的唯一标识 selection="multiple" 启用多选功能 v-model:selected="selected" 双向绑定选中的行 getSelectedString 用于显示选中行的数量信息--> <q-table title="表格标题" :rows="rows" :columns="columns" row-key="name" :selected-rows-label="getSelectedString" selection="multiple" v-model:selected="selected" class="bg-red-2" /> <!--JSON.stringify() 是JavaScript原生方法,将JavaScript对象转换为JSON字符串--> <q-card class="q-mt-md q-pa-md text-h5 rounded-lg" >已选择: {{ selected.map((item) => item.name).join(', ') }}</q-card > </div> </q-tab-panel> <q-tab-panel name="alarms" style="height: 600px" class="bg-blue-2"> <div class="q-pa-md"> <q-stepper v-model="step" ref="stepper" animated done-color="deep-orange" active-color="purple" inactive-color="secondary" style="height: 550px" class="text-h6 q-px-lg q-pt-md" > <q-step :name="1" title="请假信息" icon="event" :done="step > 1"> <div class="q-pa-md"> <q-form ref="step1Form" class="q-gutter-md"> <q-input filled v-model="leaveForm.type" label="请假类型" :options="['事假', '病假', '年假', '婚假', '产假']" emit-value map-options /> <q-input filled v-model="leaveForm.reason" type="textarea" label="请假原因" rows="4" /> </q-form> </div> </q-step> <q-step :name="2" title="请假时间" caption="选择起止时间" icon="date_range" :done="step > 2" > <div class="q-pa-md q-pa-xl"> <q-form ref="step2Form" class="q-gutter-md"> <q-input filled v-model="leaveForm.startDate" label="开始日期"> <template v-slot:append> <q-icon name="event" class="cursor-pointer"> <q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-date v-model="leaveForm.startDate" mask="YYYY-MM-DD"> <div class="row items-center justify-end"> <q-btn v-close-popup label="确定" color="primary" flat /> </div> </q-date> </q-popup-proxy> </q-icon> </template> </q-input> <q-input filled v-model="leaveForm.endDate" label="结束日期"> <template v-slot:append> <q-icon name="event" class="cursor-pointer"> <q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-date v-model="leaveForm.endDate" mask="YYYY-MM-DD"> <div class="row items-center justify-end"> <q-btn v-close-popup label="确定" color="primary" flat /> </div> </q-date> </q-popup-proxy> </q-icon> </template> </q-input> <q-input filled v-model="leaveForm.days" label="请假天数" type="number" min="1" /> </q-form> </div> </q-step> <q-step :name="3" title="确认提交" icon="fact_check"> <div class="q-pa-md"> <q-card flat bordered class="q-pa-md"> <q-card-section> <div class="text-h6">请假信息确认</div> </q-card-section> <q-card-section> <div class="q-gutter-md"> <div>请假类型:{{ leaveForm.type }}</div> <div>请假原因:{{ leaveForm.reason }}</div> <div>开始日期:{{ leaveForm.startDate }}</div> <div>结束日期:{{ leaveForm.endDate }}</div> <div>请假天数:{{ leaveForm.days }}天</div> </div> </q-card-section> </q-card> </div> </q-step> <template v-slot:navigation> <q-stepper-navigation class="flex"> <q-space/> <q-btn class="text-h6" @click="handleNext" color="deep-orange" :label="step === 3 ? '提交' : '下一步'" /> <q-btn v-if="step > 1" flat color="deep-orange" @click="$refs.stepper.previous()" label="上一步" class="q-ml-sm text-h6" /><q-space/> </q-stepper-navigation> </template> </q-stepper> </div> </q-tab-panel> <q-tab-panel name="movies" style="height: 600px" class="bg-blue-2"> <div class="text-h6">选项卡选项三</div> 选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三选项卡选项三 </q-tab-panel> </q-tab-panels> <q-separator /> <q-tabs v-model="tab" dense class="bg-grey-3" align="justify" narrow-indicator> <q-tab name="mails" label="零食含量表格分页阅览" class="q-pa-sm text-h4" /> <q-tab name="alarms" label="步进器请假" class="q-pa-sm text-h4" /> <q-tab name="movies" label="选项卡选项三" class="q-pa-sm text-h4" /> </q-tabs> </q-card> </template> <script setup> import { ref } from 'vue' const stepper = ref(null) const step1Form = ref(null) const step2Form = ref(null) const step = ref(1) const tab = ref('mails') const leaveForm = ref({ type: '', reason: '', startDate: '', endDate: '', days: '', }) const handleNext = async () => { if (step.value === 1) { const isValid = await step1Form.value.validate() if (!isValid) return } else if (step.value === 2) { const isValid = await step2Form.value.validate() if (!isValid) return } else if (step.value === 3) { // 提交表单 submitLeaveForm() return } stepper.value.next() } const submitLeaveForm = () => { // 这里添加提交逻辑 console.log('提交请假表单:', leaveForm.value) // 提交成功后可以重置表单或跳转页面 } /*定义了 columns 数组,配置表格的各个列: name: 列的唯一标识 required: 是否必填 label: 列标题 align: 对齐方式 field: 数据字段 format: 格式化函数 sortable: 是否可排序 sort: 自定义排序函数*/ const columns = [ { name: 'desc', required: true, label: '甜点(100克)', align: 'left', field: (row) => row.name, format: (val) => `${val}`, sortable: true, }, { name: 'calories', align: 'center', label: '卡路里', field: 'calories', sortable: true, }, { name: 'fat', label: '脂肪(克)', field: 'fat', sortable: true, }, { name: 'carbs', label: '碳水化合物(克)', field: 'carbs', }, { name: 'protein', label: '蛋白质(克)', field: 'protein', }, { name: 'sodium', label: '钠(毫克)', field: 'sodium', }, { name: 'calcium', label: '钙(%)', field: 'calcium', sortable: true, sort: (a, b) => parseInt(a, 10) - parseInt(b, 10), }, { name: 'iron', label: '铁(%)', field: 'iron', sortable: true, sort: (a, b) => parseInt(a, 10) - parseInt(b, 10), }, ] const rows = [ { name: '冻酸奶', calories: 159, fat: 6.0, carbs: 24, protein: 4.0, sodium: 87, calcium: '14%', iron: '1%', }, { name: '冰淇淋三明治', calories: 237, fat: 9.0, carbs: 37, protein: 4.3, sodium: 129, calcium: '8%', iron: '1%', }, { name: '闪电泡芙', calories: 262, fat: 16.0, carbs: 23, protein: 6.0, sodium: 337, calcium: '6%', iron: '7%', }, { name: '纸杯蛋糕', calories: 305, fat: 3.7, carbs: 67, protein: 4.3, sodium: 413, calcium: '3%', iron: '8%', }, { name: '姜饼', calories: 356, fat: 16.0, carbs: 49, protein: 3.9, sodium: 327, calcium: '7%', iron: '16%', }, { name: '软糖豆', calories: 375, fat: 0.0, carbs: 94, protein: 0.0, sodium: 50, calcium: '0%', iron: '0%', }, { name: '棒棒糖', calories: 392, fat: 0.2, carbs: 98, protein: 0, sodium: 38, calcium: '0%', iron: '2%', }, { name: '蜂窝糖', calories: 408, fat: 3.2, carbs: 87, protein: 6.5, sodium: 562, calcium: '0%', iron: '45%', }, { name: '甜甜圈', calories: 452, fat: 25.0, carbs: 51, protein: 4.9, sodium: 326, calcium: '2%', iron: '22%', }, { name: '奇巧巧克力', calories: 518, fat: 26.0, carbs: 65, protein: 7, sodium: 54, calcium: '12%', iron: '6%', }, ] const selected = ref([]) const getSelectedString = () => { return selected.value.length === 0 ? '' : `${selected.value.length} record${selected.value.length > 1 ? 's' : ''} selected of ${rows.length}` } </script> <style> .w-600 { width: 600px; } .h-600 { height: 600px; } </style>前置准备
- 确保已搭建 Quasar 项目(参考 Quasar 官方文档);
- 核心依赖:Quasar v2 + Vue 3(组合式 API);
- 模拟数据:使用
faker-js生成测试数据(可选,也可手动造数)。
安装测试数据依赖(可选):
npm install @faker-js/faker --save-dev场景一:QTable 数据表格(筛选 + 分页联动)
业务场景
实现「商品列表」功能:支持按商品名称 / 分类筛选、分页切换、每页条数调整,筛选条件变化时自动重置分页到第一页。
步骤 1:基础结构与数据准备
在src/pages/TableDemo.vue中编写基础代码,生成模拟商品数据:
<template> <q-page class="q-pa-md"> <!-- 筛选区域 --> <div class="q-mb-md row items-center gap-md"> <!-- 商品名称筛选 --> <q-input v-model="searchName" label="商品名称" placeholder="输入名称筛选" clearable @clear="handleFilter" @input="handleFilter" /> <!-- 商品分类筛选 --> <q-select v-model="searchCategory" label="商品分类" :options="categoryOptions" clearable placeholder="全部分类" @input="handleFilter" /> </div> <!-- 数据表格 --> <q-table :rows="filteredRows" <!-- 筛选后的数据源 --> :columns="columns" <!-- 列配置 --> :pagination="pagination" <!-- 分页配置 --> @pagination="onPagination" <!-- 分页变化回调 --> row-key="id" <!-- 行唯一标识 --> dense <!-- 紧凑模式 --> bordered <!-- 带边框 --> > <!-- 自定义操作列 --> <template #body-cell-actions="props"> <q-td :props="props"> <q-btn size="xs" label="编辑" color="primary" class="q-mr-xs" /> <q-btn size="xs" label="删除" color="negative" /> </q-td> </template> </q-table> </q-page> </template> <script setup> import { ref, computed, onMounted } from 'vue' import { faker } from '@faker-js/faker' // 若无faker,可手动造数 // 1. 模拟原始数据(100条商品数据) const rawData = ref([]) // 商品分类选项 const categoryOptions = ref([ { label: '电子产品', value: 'electronics' }, { label: '生活用品', value: 'daily' }, { label: '食品', value: 'food' }, { label: '服饰', value: 'clothes' } ]) // 2. 筛选条件 const searchName = ref('') // 名称筛选 const searchCategory = ref('') // 分类筛选 // 3. 分页配置(核心:与筛选联动) const pagination = ref({ page: 1, // 当前页 rowsPerPage: 10, // 每页条数 rowsNumber: 0 // 总条数(筛选后) }) // 4. 表格列配置 const columns = ref([ { name: 'id', label: 'ID', align: 'center', width: '80px' }, { name: 'name', label: '商品名称', align: 'left' }, { name: 'category', label: '分类', align: 'center', width: '120px' }, { name: 'price', label: '价格(元)', align: 'center', width: '100px' }, { name: 'stock', label: '库存', align: 'center', width: '100px' }, { name: 'actions', label: '操作', align: 'center', width: '180px' } ]) // 5. 生成模拟数据 onMounted(() => { rawData.value = Array.from({ length: 100 }, (_, index) => { const category = categoryOptions.value[Math.floor(Math.random() * 4)] return { id: index + 1, name: faker.commerce.productName(), // 随机商品名 category: category.label, // 分类名称 categoryValue: category.value, // 分类值(用于筛选) price: faker.commerce.price({ min: 10, max: 1000, dec: 2 }), stock: Math.floor(Math.random() * 1000) } }) // 初始化筛选 handleFilter() })步骤 2:实现筛选 + 分页联动逻辑
在上述代码的<script>中补充核心逻辑:
// 6. 筛选数据(计算属性:根据名称/分类过滤) const filteredRows = computed(() => { let result = rawData.value // 名称筛选(模糊匹配) if (searchName.value) { result = result.filter(item => item.name.toLowerCase().includes(searchName.value.toLowerCase()) ) } // 分类筛选(精确匹配) if (searchCategory.value) { result = result.filter(item => item.categoryValue === searchCategory.value) } // 更新总条数 pagination.value.rowsNumber = result.length // 分页截取数据 const start = (pagination.value.page - 1) * pagination.value.rowsPerPage const end = start + pagination.value.rowsPerPage return result.slice(start, end) }) // 7. 筛选条件变化时:重置分页到第1页 const handleFilter = () => { pagination.value.page = 1 // 筛选后默认回到第一页 } // 8. 分页变化回调(页码/每页条数改变时触发) const onMounted = (newPagination) => { pagination.value = { ...pagination.value, ...newPagination } }核心要点说明
- 筛选逻辑:通过
computed计算属性实时过滤原始数据,避免重复遍历; - 分页联动:筛选条件变化时重置
page为 1,防止筛选后数据不足导致分页异常; - row-key:必须设置唯一标识(如
id),否则表格渲染会出现复用异常; - 自定义列:通过
#body-cell-xxx插槽自定义操作列,适配业务按钮需求。
效果验证
- 输入商品名称关键词,表格实时筛选并重置到第一页;
- 选择分类筛选,仅显示对应分类商品;
- 切换页码 / 调整每页条数,数据正确分页展示。
场景二:QTabs + QStepper 表单分步提交
业务场景
实现「用户注册 + 信息完善」分步表单:
- 选项卡(QTabs):区分「基础信息」「联系方式」「确认提交」三个步骤;
- 步进器(QStepper):可视化展示步骤进度,支持下一步 / 上一步 / 提交操作;
- 表单校验:每一步必填项校验通过后才能进入下一步,最终提交所有数据。
步骤 1:基础结构搭建
在src/pages/StepperTabDemo.vue中编写基础代码:
<template> <q-page class="q-pa-md"> <div class="max-w-2xl mx-auto"> <!-- 步进器(可视化进度) --> <q-stepper v-model="currentStep" type="horizontal" :active-icon="currentStep" :done-icon="[1, 2, 3]" class="q-mb-lg" > <q-step name="1" label="基础信息" icon="person" /> <q-step name="2" label="联系方式" icon="phone" /> <q-step name="3" label="确认提交" icon="check_circle" /> </q-stepper> <!-- 选项卡(分步表单容器) --> <q-tabs v-model="currentStep" class="q-mb-md" align="justify" no-caps > <q-tab name="1" label="基础信息" /> <q-tab name="2" label="联系方式" /> <q-tab name="3" label="确认提交" /> </q-tabs> <!-- 选项卡面板(表单内容) --> <q-tab-panels v-model="currentStep" animated class="q-mb-md" > <!-- 步骤1:基础信息 --> <q-tab-panel name="1"> <q-form ref="form1" @submit.prevent="nextStep"> <q-input v-model="formData.username" label="用户名" placeholder="请输入用户名" :rules="[val => !!val || '用户名不能为空']" class="q-mb-md" /> <q-input v-model="formData.realName" label="真实姓名" placeholder="请输入真实姓名" :rules="[val => !!val || '真实姓名不能为空']" class="q-mb-md" /> <q-input v-model="formData.idCard" label="身份证号" placeholder="请输入18位身份证号" :rules="[ val => !!val || '身份证号不能为空', val => val.length === 18 || '身份证号必须为18位' ]" class="q-mb-md" /> </q-form> </q-tab-panel> <!-- 步骤2:联系方式 --> <q-tab-panel name="2"> <q-form ref="form2" @submit.prevent="nextStep"> <q-input v-model="formData.phone" label="手机号" placeholder="请输入11位手机号" :rules="[ val => !!val || '手机号不能为空', val => /^1[3-9]\\d{9}$/.test(val) || '手机号格式错误' ]" class="q-mb-md" /> <q-input v-model="formData.email" label="邮箱" placeholder="请输入邮箱" :rules="[ val => !!val || '邮箱不能为空', val => /^\\w+([.-]?\\w+)*@\\w+([.-]?\\w+)*(.\\w{2,3})+$/.test(val) || '邮箱格式错误' ]" class="q-mb-md" /> <q-input v-model="formData.address" label="详细地址" placeholder="请输入详细地址" :rules="[val => !!val || '详细地址不能为空']" class="q-mb-md" /> </q-form> </q-tab-panel> <!-- 步骤3:确认提交 --> <q-tab-panel name="3"> <div class="q-pa-md bg-grey-5 rounded-lg"> <h4 class="q-mb-md">提交信息确认</h4> <div class="row q-mb-sm"> <div class="col-4 text-grey-6">用户名:</div> <div class="col-8">{{ formData.username }}</div> </div> <div class="row q-mb-sm"> <div class="col-4 text-grey-6">真实姓名:</div> <div class="col-8">{{ formData.realName }}</div> </div> <div class="row q-mb-sm"> <div class="col-4 text-grey-6">手机号:</div> <div class="col-8">{{ formData.phone }}</div> </div> <div class="row q-mb-sm"> <div class="col-4 text-grey-6">邮箱:</div> <div class="col-8">{{ formData.email }}</div> </div> </div> </q-tab-panel> </q-tab-panels> <!-- 操作按钮 --> <div class="row justify-between"> <q-btn label="上一步" icon="arrow_back" @click="prevStep" :disabled="currentStep === '1'" /> <q-btn v-if="currentStep !== '3'" label="下一步" icon="arrow_forward" color="primary" @click="nextStep" /> <q-btn v-else label="提交" icon="send" color="positive" @click="submitForm" /> </div> </div> </q-page> </template> <script setup> import { ref, reactive } from 'vue' import { useQuasar } from 'quasar' // 1. 初始化Quasar通知(用于提交成功提示) const $q = useQuasar() // 2. 步骤控制 const currentStep = ref('1') // 绑定选项卡和步进器的当前步骤 // 3. 表单数据 const formData = reactive({ username: '', realName: '', idCard: '', phone: '', email: '', address: '' }) // 4. 表单引用(用于校验) const form1 = ref(null) const form2 = ref(null)步骤 2:实现分步校验与提交逻辑
在<script>中补充核心方法:
// 5. 下一步(含表单校验) const nextStep = async () => { let isValid = false // 步骤1校验 if (currentStep.value === '1') { isValid = await form1.value.validate() } // 步骤2校验 else if (currentStep.value === '2') { isValid = await form2.value.validate() } // 校验通过则切换步骤 if (isValid) { if (currentStep.value === '1') { currentStep.value = '2' } else if (currentStep.value === '2') { currentStep.value = '3' } } } // 6. 上一步(无需校验,直接切换) const prevStep = () => { if (currentStep.value === '2') { currentStep.value = '1' } else if (currentStep.value === '3') { currentStep.value = '2' } } // 7. 最终提交 const submitForm = () => { // 模拟接口提交 setTimeout(() => { $q.notify({ type: 'positive', message: '表单提交成功!', caption: '您的信息已保存' }) // 重置表单 form1.value.resetValidation() form2.value.resetValidation() Object.assign(formData, { username: '', realName: '', idCard: '', phone: '', email: '', address: '' }) currentStep.value = '1' // 回到第一步 }, 1000) }核心要点说明
- 组件联动:
currentStep同时绑定QStepper和QTabs,实现步骤与选项卡的同步切换; - 表单校验:每一步使用
QForm的validate()方法做异步校验,校验通过才允许下一步; - 步进器配置:
v-model:绑定当前步骤;active-icon:标记当前激活步骤;done-icon:标记已完成步骤;
- 用户体验:
- 上一步按钮在第一步禁用,避免无效操作;
- 提交后重置表单和步骤,方便再次填写;
- 使用 Quasar 通知组件反馈提交结果。
效果验证
- 第一步未填必填项点击「下一步」,触发表单校验提示;
- 校验通过后切换到第二步,步进器和选项卡同步更新;
- 第二步同理,校验通过后进入第三步预览所有信息;
- 点击「提交」,模拟接口请求后提示成功,并重置表单。
扩展优化建议
表格组件扩展
- 远程数据适配:若表格数据来自接口,将
filteredRows改为异步请求,筛选 / 分页时调用接口(传递searchNamepagerowsPerPage参数); - 多条件筛选:增加日期范围、价格区间等筛选条件,通过
URLSearchParams拼接参数; - 表格排序:开启 QTable 的
sort配置,支持点击列头排序:const columns = ref([ { name: 'price', label: '价格(元)', align: 'center', sortable: true } ])
步进器 / 选项卡扩展
- 步骤禁用:若某一步需要前置条件(如第二步需第一步审核通过),可通过
QStep的disable属性控制:<q-step name="2" label="联系方式" icon="phone" :disable="!formData.username" /> - 表单缓存:使用
localStorage缓存表单数据,避免页面刷新后数据丢失; - 自定义校验规则:抽离通用校验规则(如手机号、邮箱)到工具函数,提升复用性:
// src/utils/validate.js export const phoneRule = [ val => !!val || '手机号不能为空', val => /^1[3-9]\d{9}$/.test(val) || '手机号格式错误' ]
总结
本教程通过两个核心业务场景,覆盖了 Quasar 高频组件的核心用法:
- QTable:筛选、分页、自定义列的联动逻辑,适配大数据列表展示;
- QTabs + QStepper:分步表单的联动、校验、提交,适配多步骤业务流程;所有 Demo 均基于 Vue 3 组合式 API 编写,贴近实际项目开发模式,你可根据业务需求进一步扩展功能(如表格导出、表单回显、步骤进度保存等)。