本文介紹了基于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>