文章目录
前端用的vue3+Element Plus
可以自行创建脚手架vue3 命令使用手册及常用概念
也可以用已经创建的,Vue3-Project-Basic在github上;(注意安装node,版本建议16以上)
下载后直接
npm i
后
npm run dev
即可,组件路由都配了,快速验证,如果用到,帮忙点个Follow(可能要代理)
下载后把Emp.vue原本的代码删除即可。
一、最终效果
当然实际中是从后端拿数据,本案例为模拟数据。
二、源代码
2.1 AddDataSource.vue
<template>
<el-dialog
v-model="localVisible"
title="新增数据源"
width="500px"
:close-on-click-modal="false"
class="add-dialog"
>
<el-form :model="form" ref="formRef" label-width="100px" class="add-form">
<el-form-item label="数据源名称" required>
<el-input
v-model="form.name"
placeholder="请输入数据源名称"
maxlength="50"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="localVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
modelValue: { type: Boolean, default: false }
})
const emit = defineEmits(['update:modelValue', 'success'])
const localVisible = ref(props.modelValue)
const form = reactive({ name: '' })
const formRef = ref()
function handleSubmit() {
if (form.name.trim() === '') {
ElMessage.error('请输入数据源名称')
return
}
ElMessage.success('新增数据源成功')
// 模拟返回新增数据源对象,使用当前时间戳作为 id
emit('success', { id: Date.now(), db_name: form.name, label: form.name })
localVisible.value = false
form.name = '' // 清空表单
}
watch(() => props.modelValue, (newVal) => {
localVisible.value = newVal
})
watch(localVisible, (newVal) => {
emit('update:modelValue', newVal)
if (!newVal) {
form.name = '' // 关闭弹窗时清空表单
}
})
</script>
<style scoped>
.add-dialog {
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid #e6e6e6;
}
:deep(.el-dialog__body) {
padding: 30px 20px;
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid #e6e6e6;
}
}
.add-form {
:deep(.el-form-item__label) {
font-weight: 500;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
2.2 LeftTree.vue
<template>
<div class="left-tree">
<div class="tree-header">
<el-button type="primary" @click="handleAdd" class="add-btn">
<el-icon><Plus /></el-icon>新增数据源
</el-button>
</div>
<el-tree
:data="treeData"
:props="defaultProps"
@node-click="handleNodeClick"
node-key="id"
class="custom-tree"
>
<template #default="{ node }">
<span class="custom-tree-node">
<el-icon><Connection /></el-icon>
<span>{{ node.label }}</span>
</span>
</template>
</el-tree>
<AddDataSource v-model="addDialogVisible" @success="handleAddSuccess" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Plus, Connection } from '@element-plus/icons-vue'
import AddDataSource from './AddDataSource.vue'
// 此处使用硬编码的数据模拟分组情况
const treeData = ref([
{ id: 1, db_name: '数据源A', label: '数据源A' },
{ id: 2, db_name: '数据源B', label: '数据源B' }
])
const defaultProps = {
children: 'children',
label: 'db_name'
}
const addDialogVisible = ref(false)
const emit = defineEmits(['node-click'])
const handleNodeClick = (data) => {
// 若节点存在 children,则为组节点,不触发点击事件
if (data.children) return
emit('node-click', data)
}
const handleAdd = () => {
addDialogVisible.value = true
}
const handleAddSuccess = (newDataSource) => {
// 将新增的数据源添加到树中
treeData.value.push(newDataSource)
}
onMounted(() => {
// 这里不从后端加载,而是使用硬编码数据
})
</script>
<style scoped>
.left-tree {
height: 100%;
display: flex;
flex-direction: column;
padding: 16px;
}
.tree-header {
margin-bottom: 16px;
}
.add-btn {
width: 100%;
}
.custom-tree {
flex: 1;
overflow: auto;
}
.custom-tree-node {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
:deep(.el-tree-node__content) {
height: 36px;
}
:deep(.el-tree-node:focus > .el-tree-node__content) {
background-color: #f0f7ff;
}
:deep(.el-tree-node__content:hover) {
background-color: #f5f7fa;
}
</style>
2.3 FieldDrawer.vue
<template>
<el-drawer
v-model="localVisible"
title="字段信息"
direction="rtl"
size="30%"
:before-close="handleClose"
>
<el-table :data="fieldList" border style="width: 100%;">
<el-table-column prop="fieldEnglishName" label="字段英文名称" />
<el-table-column prop="fieldType" label="字段类型" />
<el-table-column prop="fieldRemark" label="字段备注" />
</el-table>
</el-drawer>
</template>
<script setup>
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
// props: tableId 用于选择不同的模拟数据;
// 使用 modelValue 来实现 v-model 绑定
const props = defineProps({
tableId: { type: [Number, String], required: true },
modelValue: { type: Boolean, default: false }
})
const emit = defineEmits(['update:modelValue'])
const localVisible = ref(props.modelValue)
const fieldList = ref([])
// 模拟数据,根据不同的 tableId 返回不同字段数据
const simulateFieldData = (tableId) => {
if (tableId == 101) {
return [
{ id: 1, fieldEnglishName: 'empId', fieldType: 'INT', fieldRemark: '员工ID' },
{ id: 2, fieldEnglishName: 'name', fieldType: 'VARCHAR', fieldRemark: '姓名' },
{ id: 3, fieldEnglishName: 'age', fieldType: 'INT', fieldRemark: '年龄' }
]
} else if (tableId == 102) {
return [
{ id: 4, fieldEnglishName: 'salaryId', fieldType: 'INT', fieldRemark: '工资ID' },
{ id: 5, fieldEnglishName: 'amount', fieldType: 'DECIMAL', fieldRemark: '金额' }
]
} else if (tableId == 201) {
return [
{ id: 6, fieldEnglishName: 'orderId', fieldType: 'INT', fieldRemark: '订单ID' },
{ id: 7, fieldEnglishName: 'customerId', fieldType: 'INT', fieldRemark: '客户ID' }
]
} else if (tableId == 202) {
return [
{ id: 8, fieldEnglishName: 'custId', fieldType: 'INT', fieldRemark: '客户ID' },
{ id: 9, fieldEnglishName: 'custName', fieldType: 'VARCHAR', fieldRemark: '客户姓名' }
]
} else {
return []
}
}
const loadFieldList = () => {
fieldList.value = simulateFieldData(props.tableId)
if (fieldList.value.length === 0) {
ElMessage.warning('未获取到字段信息')
}
}
const handleClose = (done) => {
done()
}
watch(() => props.modelValue, (newVal) => {
localVisible.value = newVal
if (newVal) {
loadFieldList()
}
})
watch(localVisible, (newVal) => {
emit('update:modelValue', newVal)
})
</script>
<style scoped>
/* 根据需要添加样式 */
</style>
2.4 RightTable.vue
<template>
<div class="right-table">
<div class="table-header">
<el-input
v-model="searchKeyword"
placeholder="搜索表名"
class="search-input"
clearable
@input="handleSearch"
/>
</div>
<el-table :data="pagedTableList" border style="width: 100%;" @row-click="handleRowClick">
<el-table-column prop="tableEnglishName" label="表名" />
<el-table-column prop="tableRemark" label="表备注" />
<el-table-column prop="fieldCount" label="字段数量" width="100" />
<el-table-column prop="dataCount" label="数据量" width="100" />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button link type="primary" @click.stop="handleViewDetails(row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
background
:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="total, prev, pager, next, jumper"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
const props = defineProps({
dataSourceId: { type: [Number, String], required: true }
})
const emit = defineEmits(['row-select'])
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 模拟不同数据源的表数据,这里硬编码两组数据
const tableList = ref([])
const loadTableData = () => {
// 根据 dataSourceId 模拟不同数据
if (props.dataSourceId === 1) {
tableList.value = [
{ id: 101, tableEnglishName: '员工表', tableRemark: '员工基本信息', fieldCount: 5, dataCount: 10, createTime: '2022-01-01' },
{ id: 102, tableEnglishName: '工资表', tableRemark: '员工工资信息', fieldCount: 3, dataCount: 10, createTime: '2022-02-01' }
]
} else if (props.dataSourceId === 2) {
tableList.value = [
{ id: 201, tableEnglishName: '订单表', tableRemark: '订单数据', fieldCount: 8, dataCount: 20, createTime: '2022-03-01' },
{ id: 202, tableEnglishName: '客户表', tableRemark: '客户信息', fieldCount: 6, dataCount: 15, createTime: '2022-04-01' }
]
} else {
tableList.value = []
}
total.value = tableList.value.length
}
const filteredTableList = computed(() => {
if (!searchKeyword.value) return tableList.value
return tableList.value.filter(item =>
item.tableEnglishName.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
const pagedTableList = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return filteredTableList.value.slice(start, start + pageSize.value)
})
function handleSearch() {
currentPage.value = 1
}
function handlePageChange(page) {
currentPage.value = page
}
function handleRowClick(row) {
// 如果直接点击行,也可触发详情显示
emit('row-select', row.id)
}
function handleViewDetails(row) {
// 点击“查看详情”按钮
emit('row-select', row.id)
}
watch(() => props.dataSourceId, () => {
currentPage.value = 1
loadTableData()
})
onMounted(() => {
loadTableData()
})
</script>
<style scoped>
.right-table {
height: 100%;
padding: 10px;
display: flex;
flex-direction: column;
}
.table-header {
margin-bottom: 10px;
}
.search-input {
width: 300px;
}
.pagination-container {
margin-top: 15px;
display: flex;
justify-content: flex-end;
}
</style>
2.5 Emp.vue
<template>
<div class="conn-container">
<div class="left-panel">
<LeftTree @node-click="handleNodeClick" />
</div>
<div class="right-panel">
<RightTable v-if="currentDataSourceId" :dataSourceId="currentDataSourceId" @row-select="handleRowSelect" />
<div v-else class="empty-tip">
<el-empty description="请选择左侧数据源查看表信息" />
</div>
</div>
<FieldDrawer v-model="drawerVisible" :tableId="currentTableId" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import LeftTree from './LeftTree.vue'
import RightTable from './RightTable.vue'
import FieldDrawer from './FieldDrawer.vue'
const currentDataSourceId = ref(null)
const currentTableId = ref(null)
const drawerVisible = ref(false)
const handleNodeClick = (data) => {
currentDataSourceId.value = data.id
}
const handleRowSelect = (tableId) => {
currentTableId.value = tableId
drawerVisible.value = true
}
</script>
<style scoped>
.conn-container {
display: flex;
height: 100%;
background-color: #f5f7fa;
padding: 20px;
gap: 20px;
}
.left-panel {
width: 300px;
height: 100%;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.right-panel {
flex: 1;
height: 100%;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.empty-tip {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
三、代码解读
在项目中,我们使用了 Vue3 的组合式 API(Composition API)与 Element Plus UI 组件库,完成了一个数据管理的基本示例。整个示例主要包含五个组件,每个组件承担着特定的业务逻辑与视图展现,下面详细解释各模块的技术亮点和实现思路。
3.1 AddDataSource.vue —— 数据源新增对话框
- 业务功能与交互
该组件通过 Element Plus 的el-dialog
实现模态对话框,为用户提供了添加数据源的入口。通过v-model
实现双向绑定,保证父子组件之间的数据同步。 - 核心代码讲解
- 双向绑定与响应式数据:利用
ref
和reactive
建立响应数据,props
接收父组件传入的modelValue
并通过watch
监听实现状态同步。 - 表单验证:在提交前先进行字符串去除空格的简单校验,通过 Element Plus 的
ElMessage
提供即时的反馈提醒用户。 - 事件通信:通过
emit
触发success
事件,将新数据源对象传递给父组件;同时利用update:modelValue
实现 v-model 的模式更新。
- 双向绑定与响应式数据:利用
- 技术术语说明
- ref 与 reactive:
ref
用于基本类型或单一对象的响应式包装,reactive
则适合包装较复杂的对象。 - watch:实现数据副作用处理,在响应式数据发生变化时自动执行指定函数。
- ref 与 reactive:
3.2 LeftTree.vue —— 数据源树视图
- 业务功能与交互
使用 Element Plus 的el-tree
展示数据源树,并通过按钮触发新增数据源对话框。 - 核心代码讲解
- 硬编码数据模拟:在初始加载时,通过预设数据构成树结构,后续可根据业务需求替换为后端数据。
- 自定义节点模板:通过
#default
插槽自定义树节点的展示,采用图标和文本结合的形式,提升用户体验。 - 事件传递:点击节点或新增成功后,通过事件向父组件传递数据(例如
node-click
和success
事件),实现组件间解耦协同。
- 专业名词解释
- 插槽(slot):Vue 中的一种内容分发机制,用于灵活地将父组件的内容传递给子组件指定位置,从而实现高度复用的自定义模板。
3.3 FieldDrawer.vue —— 字段详情抽屉
- 业务功能与交互
该组件通过抽屉(Drawer)展示选中表的字段详细信息,同样采用了 Element Plus 的el-drawer
与el-table
组合展示数据。 - 核心代码讲解
- 根据 tableId 加载模拟数据:通过对不同 tableId 传入对应的硬编码数据,示例化如何根据参数返回不同字段列表。
- 生命周期与状态监听:使用
watch
监听modelValue
,当抽屉状态变为打开时触发加载数据,确保数据与组件状态同步。 - 模块隔离与复用:由于字段详情只关注数据展现,整个组件实现了高内聚、低耦合,可以方便地在其他业务场景中复用。
- 专业术语解释
- 抽屉(Drawer):常用于移动端或桌面端 UI 设计中的侧边弹出窗口,简化界面操作同时不干扰主体页面的展示。
3.4 RightTable.vue —— 表数据展示
- 业务功能与交互
主要负责展示选中数据源下的表信息,并提供搜索、分页以及“查看详情”功能。 - 核心代码讲解
- 数据处理与分页:通过
computed
属性实现数据过滤与分页处理,确保在搜索和分页操作中数据始终正确渲染。 - 事件处理:监听行点击以及按钮点击,通过自定义事件
row-select
通知父组件显示详细信息。 - 表格组件:Element Plus 的
el-table
提供丰富的功能支持,如边框、固定列、行点击事件等,便于根据业务需求进一步扩展交互。
- 数据处理与分页:通过
- 专业术语解释
- 计算属性(computed):在 Vue 中用于声明依赖其他响应式数据并返回计算结果的属性,具有缓存功能,有效提升性能。
3.5 Emp.vue —— 综合展示视图
- 业务功能与交互
综合展示左侧数据源树和右侧表数据,在用户选择左侧数据源后展示右侧表信息,并通过抽屉展示字段详情。 - 核心代码讲解
- 组件间通信:主要体现在子组件事件传递到父组件,通过监听
node-click
和row-select
事件动态更新当前数据源和选中表 ID,进而控制抽屉的显示。 - 状态管理:利用局部状态
ref
保存当前选中的数据源和表 ID,实现页面整体交互的状态联动。 - 布局设计:左右分栏布局利用 Flex 布局实现,同时引入盒子阴影、边框圆角等视觉提升元素,保证企业级系统页面整洁美观。
- 组件间通信:主要体现在子组件事件传递到父组件,通过监听
- 专业术语解释
- 状态管理:在前端应用中,指的是数据的集中处理与管理方式,通过响应式数据管理,使应用对外部事件有一致且稳定的响应。
通过以上解析,我们不仅明确了每个组件的技术实现,也揭示了 Vue3 生态中重要的设计理念和最佳实践。希望通过这一章的深入解读,能帮助开发者在面对更复杂业务时能够灵活应用这些技术,打破常规思维,实现更高效的工程交付。