基于element-plus和IndexedDB数据库的基础表单
本文介绍了基于Vue 3和Element Plus的表单项目配置页面实现。页面包含搜索栏、操作按钮、数据表格和分页组件,使用IndexedDB进行本地数据存储。主要功能包括:1) 通过模糊查询搜索项目;2) 分页显示项目数据;3) 添加/编辑/删除项目操作。核心代码展示了如何通过IndexedDB API实现数据查询、分页和模糊搜索,并采用响应式设计计算表格高度。技术栈包含Vue 3组合式API、Element Plus组件库和IndexedDB数据库操作。
依赖安装:
npm i idb
npm install element-plus --save
<template><div class="config-page"><!-- 面包屑 --><div class="breadcrumb-header">表单项目配置</div><!-- 搜索栏 --><div ref="searchRef" class="search-container"><el-form :model="searchForm" label-width="auto" class="search-form"><el-form-item label="项目名称"><el-input v-model="searchForm.projectName" placeholder="请输入项目名称" /></el-form-item><el-form-item class="search-actions"><el-button type="primary" @click="handleSearch">查 询</el-button><el-button @click="handleReset">重 置</el-button></el-form-item></el-form></div><!-- 操作栏 --><div class="operation-btns"><el-button @click="handleAdd">添 加</el-button></div><!-- table表格 --><div class="main" :style="{ height: tableHeight }"><el-table :data="tableData" stripe v-loading="loading" style="width: 100%;height: 100%"><el-table-column prop="projectName" label="项目名称" /><el-table-column label="操作" width="120" fixed="right"><template #default="scope"><el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button><el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button></template></el-table-column></el-table></div><!-- 分页 --><div class="footer"><el-pagination v-model:current-page="searchForm.currentPage" v-model:page-size="searchForm.pageSize":page-sizes="[10, 20, 50, 100, 200]" :total="total" layout="total, sizes, prev, pager, next, jumper"@current-change="handleCurrentChange" @size-change="handleSizeChange" /></div><el-dialog v-model="dialogVisible" :title="dialogTitle" width="500"><el-form ref="ruleFormRef" :model="form" label-width="auto" :rules="rules"><el-form-item label="项目名称" prop="projectName"><el-input v-model="form.projectName" autocomplete="off" /></el-form-item></el-form><template #footer><div class="dialog-footer"><el-button @click="dialogVisible = false">取 消</el-button><el-button type="primary" @click="handleConfirm">确 定</el-button></div></template></el-dialog></div>
</template><script setup>
import { ref, useTemplateRef, onMounted, onBeforeUnmount } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { debounce } from 'lodash-es'
import { openDB } from 'idb'// 表格容器高度
const tableHeight = ref('auto')// 计算表格高度
const calculateTableHeight = () => {if (!searchRef.value) return// 面包屑高度const headerHeight = 41// 搜索栏高度const searchHeight = searchRef.value.offsetHeight// 操作按钮栏高度const operationHeight = 56// 分页高度const footerHeight = 56tableHeight.value = `calc(100% - ${headerHeight + searchHeight + operationHeight + footerHeight}px)`
}// 防抖的resize处理
const debouncedResize = debounce(() => {calculateTableHeight()
}, 200)// 查询条件
const searchForm = ref({projectName: '',currentPage: 1,pageSize: 10
})const loading = ref(false)
const tableData = ref([])
const total = ref(0)// 获取表格数据方法
const queryProjects = async (conditions = {}, pagination = {}) => {try {if (!db) await initDB()const { currentPage = 1, pageSize = 10 } = paginationconst { projectName = '' } = conditions// 开启一个只读事务const tx = db.transaction(STORE_NAME, 'readonly')// 并获取指定对象存储的引用const store = tx.objectStore(STORE_NAME)// 获取总数和分页查询数据const skip = (currentPage - 1) * pageSizelet data = []let totalCount = 0if (projectName) {/*** 在IndexedDB数据库中打开一个游标,用于遍历或操作存储对象中的数据。* 支持查询、更新或删除数据等操作*/let cursor = await store.openCursor()let matchedCount = 0while (cursor && data.length < pageSize) {const record = cursor.value// 模糊匹配项目名(不区分大小写)if (record.projectName && record.projectName.toLowerCase().includes(projectName.toLowerCase())) {if (totalCount >= skip) {data.push(record)matchedCount++}totalCount++ // 统计总匹配数}cursor = await cursor.continue()}// 继续遍历剩余记录以计算准确的总数while (cursor) {const record = cursor.valueif (record.projectName && record.projectName.toLowerCase().includes(projectName.toLowerCase())) {totalCount++}cursor = await cursor.continue()}} else {// 无项目名条件时,获取总数并进行分页查询totalCount = await store.count()/*** 在IndexedDB数据库中打开一个游标,用于遍历或操作存储对象中的数据。* 支持查询、更新或删除数据等操作*/let cursor = await store.openCursor()let count = 0while (cursor && data.length < pageSize) {if (count >= skip) {data.push(cursor.value)}count++cursor = await cursor.continue()}}return { data, totalCount }} catch (error) {console.error('查询失败:', error)throw error}
}// 获取表格数据
const fetchTableData = async () => {try {loading.value = trueconst { data, totalCount } = await queryProjects({ projectName: searchForm.value.projectName },{currentPage: Math.max(1, searchForm.value.currentPage),pageSize: searchForm.value.pageSize})tableData.value = datatotal.value = totalCount} catch (error) {console.error('获取数据失败:', error)ElMessage.error('获取数据失败')} finally {loading.value = false}
}const searchRef = useTemplateRef('searchRef')
// 搜索
const handleSearch = () => {searchForm.value.currentPage = 1fetchTableData()
}// 重置
const handleReset = () => {searchForm.value = {projectName: '',currentPage: 1,pageSize: 10}fetchTableData()
}// current-page 改变时触发
const handleCurrentChange = (val) => {searchForm.value.currentPage = valfetchTableData()
}// 分页大小变化
const handleSizeChange = (size) => {searchForm.value.pageSize = sizefetchTableData()
}const dialogVisible = ref(false)
const dialogTitle = ref('添加')const ruleFormRef = useTemplateRef('ruleFormRef')
const form = ref({projectName: '',
})
const rules = {projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }]
}const isAdd = ref(true)// 添加
const handleAdd = () => {form.value = {projectName: '',}isAdd.value = truedialogTitle.value = '添加'dialogVisible.value = true
}// 编辑
const handleEdit = (val) => {form.value = { ...val }isAdd.value = falsedialogTitle.value = '编辑'dialogVisible.value = true
}// 确定按钮点击事件
const handleConfirm = () => {ruleFormRef.value.validate(async (valid, fields) => {if (valid) {try {if (isAdd.value) {// 添加数据await db.add(STORE_NAME, {...form.value})ElMessage.success('添加成功')} else {// 编辑数据await db.put(STORE_NAME, { ...form.value })ElMessage.success('编辑成功')}dialogVisible.value = false// 重新获取数据fetchTableData()} catch (error) {console.error('添加数据失败:', error)if (error.name === 'ConstraintError') {ElMessage.error('该项目已存在')} else {ElMessage.error('添加失败')}}} else {console.log('error submit!', fields)}})
}// 删除
const handleDelete = ({ id }) => {console.log(id)ElMessageBox.confirm('是否确定删除?',{confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning',}).then(async () => {try {await db.delete(STORE_NAME, id)// 重新获取数据fetchTableData()ElMessage({type: 'success',message: '删除成功',})} catch (error) {ElMessage.error('删除失败')console.error('删除失败:', error)}}).catch(() => {ElMessage({type: 'info',message: '已取消',})})
}// 数据库名称
const DB_NAME = 'LowCodeAppDatabase'
// 对象存储(数据库表名称)
const STORE_NAME = 'projects'
let db = null
// 初始化数据库
const initDB = async () => {if (db) return dbtry {/*** DB_NAME:数据库名称* 1:数据库版本号* 第三个参数是升级回调函数,在数据库首次创建或版本升级时执行*/db = await openDB(DB_NAME, 1, {upgrade(db) {// 创建一个名为STORE_NAME的对象存储, 设置id为主键,且自动递增const store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true })// 在projectName字段上创建唯一索引, 以便查询store.createIndex('projectName', 'projectName', { unique: true })}})console.log('数据库初始化成功')return db} catch (err) {ElMessage.error('数据库初始化失败,请刷新页面重试')console.error('数据库初始化失败:', err)}
}onMounted(async () => {try {// 初始化数据库await initDB()// 初始化数据await fetchTableData()// 计算表格高度calculateTableHeight()window.addEventListener('resize', debouncedResize)} catch (error) {console.error('初始化失败:', error)}
})onBeforeUnmount(() => {window.removeEventListener('resize', debouncedResize)
})</script><style scoped>
.config-page {width: 100%;height: 100%;display: flex;flex-direction: column;overflow: hidden;
}.breadcrumb-header {width: 100%;height: 41px;display: flex;align-items: center;border-bottom: 1px solid #e0e0e0;padding: 0 10px;color: #606266;
}.search-container {width: 100%;height: auto;border-bottom: 1px solid #e0e0e0;display: flex;flex-wrap: wrap;align-items: center;
}.main {padding: 0 10px 10px 10px;
}.operation-btns {width: 100%;height: 56px;display: flex;align-items: center;padding: 0 10px;
}.footer {width: 100%;height: 56px;display: flex;justify-content: flex-end;align-items: center;padding: 0 10px;border-top: 1px solid #e0e0e0;
}.search-form {width: 100%;display: flex;flex-wrap: wrap;padding-top: 10px;
}.el-form-item {margin: 0 10px 10px 10px;
}.search-actions {margin-left: auto;
}:deep(.el-table--fit .el-table__inner-wrapper:before) {display: none;
}
</style>