第276集:Vue 3低代码平台数据源管理实现
一、数据源管理概述
数据源管理是低代码平台的重要组成部分,它允许用户可视化地配置和管理各种数据源连接,如数据库、API、云服务等。通过数据源管理,用户可以轻松地将应用与各种数据源集成,实现数据的读取、写入和更新操作。
1.1 核心功能
- 数据源类型支持:支持多种数据源类型,如关系型数据库、NoSQL数据库、REST API、GraphQL API等
- 可视化配置:通过可视化界面配置数据源连接参数
- 连接测试:测试数据源连接是否成功
- 数据源管理:创建、编辑、删除数据源
- 权限控制:管理数据源的访问权限
- 数据预览:预览数据源中的数据
- 查询构建器:可视化构建数据查询
- 动态数据绑定:将数据源与组件动态绑定
1.2 架构设计
数据源管理通常采用分层架构设计:
- 表现层:提供可视化配置界面
- 核心引擎层:处理数据源连接、查询执行、数据转换等核心功能
- 适配器层:为不同类型的数据源提供适配器
- 数据层:管理数据源配置和元数据
二、核心实现
2.1 数据模型设计
首先,我们需要设计数据源管理的数据模型,包括数据源配置、连接信息、查询配置等:
// 数据源类型枚举
export enum DataSourceType {
MYSQL = 'mysql',
POSTGRESQL = 'postgresql',
MONGODB = 'mongodb',
REST_API = 'rest-api',
GRAPHQL_API = 'graphql-api',
// 可以扩展更多数据源类型
}
// 数据源配置接口
export interface DataSourceConfig {
id: string;
name: string;
type: DataSourceType;
description?: string;
connectionConfig: ConnectionConfig;
createdAt: Date;
updatedAt: Date;
createdBy: string;
tags?: string[];
isActive: boolean;
}
// 连接配置接口
export interface ConnectionConfig {
// 通用连接参数
host?: string;
port?: number;
username?: string;
password?: string;
database?: string;
// API相关参数
url?: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
headers?: Record<string, string>;
// 其他连接参数
[key: string]: any;
}
// 查询配置接口
export interface QueryConfig {
id: string;
name: string;
dataSourceId: string;
query: string | Record<string, any>; // SQL语句或API请求配置
params?: Record<string, any>;
description?: string;
createdAt: Date;
updatedAt: Date;
}
// 数据查询结果接口
export interface QueryResult {
data: any[];
total: number;
columns?: ColumnMeta[];
error?: string;
}
// 列元数据接口
export interface ColumnMeta {
name: string;
type: string;
nullable: boolean;
defaultValue?: any;
description?: string;
}2.2 数据源适配器设计
为了支持多种数据源类型,我们设计了适配器模式,为每种数据源类型提供对应的适配器:
// 数据源适配器接口
export interface DataSourceAdapter {
// 测试连接
testConnection(config: ConnectionConfig): Promise<boolean>;
// 执行查询
executeQuery(config: ConnectionConfig, query: string | Record<string, any>, params?: Record<string, any>): Promise<QueryResult>;
// 获取数据源元数据
getMetadata(config: ConnectionConfig): Promise<DataSourceMetadata>;
// 获取表列表
getTables(config: ConnectionConfig): Promise<TableMeta[]>;
// 获取表结构
getTableStructure(config: ConnectionConfig, tableName: string): Promise<ColumnMeta[]>;
}
// 数据源元数据接口
export interface DataSourceMetadata {
name: string;
type: DataSourceType;
version?: string;
tables?: TableMeta[];
}
// 表元数据接口
export interface TableMeta {
name: string;
description?: string;
columns?: ColumnMeta[];
rowCount?: number;
}
// 数据源适配器工厂
export class DataSourceAdapterFactory {
private static adapters: Map<DataSourceType, DataSourceAdapter> = new Map();
// 注册适配器
static registerAdapter(type: DataSourceType, adapter: DataSourceAdapter): void {
this.adapters.set(type, adapter);
}
// 获取适配器
static getAdapter(type: DataSourceType): DataSourceAdapter | undefined {
return this.adapters.get(type);
}
}2.3 REST API适配器实现
下面是一个REST API适配器的实现示例:
import axios from 'axios';
import type { DataSourceAdapter, ConnectionConfig, QueryResult, DataSourceMetadata, TableMeta, ColumnMeta } from './types';
import { DataSourceType } from './types';
// REST API适配器实现
export class RestApiAdapter implements DataSourceAdapter {
async testConnection(config: ConnectionConfig): Promise<boolean> {
try {
const response = await axios({
url: config.url || '',
method: config.method || 'GET',
headers: config.headers,
timeout: 5000
});
return response.status >= 200 && response.status < 300;
} catch (error) {
console.error('Failed to test REST API connection:', error);
return false;
}
}
async executeQuery(config: ConnectionConfig, query: any, params?: Record<string, any>): Promise<QueryResult> {
try {
// 合并查询配置和连接配置
const requestConfig = {
url: query.url || config.url || '',
method: query.method || config.method || 'GET',
headers: { ...config.headers, ...query.headers },
data: query.data || params,
params: query.params || {},
timeout: 10000
};
const response = await axios(requestConfig);
// 处理响应数据
const data = Array.isArray(response.data) ? response.data : [response.data];
return {
data,
total: data.length,
columns: this.inferColumns(data)
};
} catch (error: any) {
return {
data: [],
total: 0,
error: error.message || 'Failed to execute query'
};
}
}
async getMetadata(config: ConnectionConfig): Promise<DataSourceMetadata> {
return {
name: 'REST API',
type: DataSourceType.REST_API,
tables: []
};
}
async getTables(config: ConnectionConfig): Promise<TableMeta[]> {
// REST API没有表的概念,返回空数组
return [];
}
async getTableStructure(config: ConnectionConfig, tableName: string): Promise<ColumnMeta[]> {
// REST API没有固定的表结构,返回空数组
return [];
}
// 从数据中推断列信息
private inferColumns(data: any[]): ColumnMeta[] {
if (!data || data.length === 0) {
return [];
}
const firstRow = data[0];
const columns: ColumnMeta[] = [];
for (const [key, value] of Object.entries(firstRow)) {
columns.push({
name: key,
type: typeof value,
nullable: true
});
}
return columns;
}
}
// 注册适配器
DataSourceAdapterFactory.registerAdapter(DataSourceType.REST_API, new RestApiAdapter());2.4 数据源管理组件
实现数据源管理的可视化组件,包括数据源列表、数据源配置表单等:
<template>
<div class="data-source-manager">
<div class="manager-header">
<h2>数据源管理</h2>
<button class="add-btn" @click="showAddModal = true">添加数据源</button>
</div>
<!-- 数据源列表 -->
<div class="data-source-list">
<div
v-for="source in dataSources"
:key="source.id"
class="data-source-item"
:class="{ 'active': selectedDataSource?.id === source.id }"
@click="selectDataSource(source)"
>
<div class="source-info">
<div class="source-name">{{ source.name }}</div>
<div class="source-type">{{ getDataSourceTypeName(source.type) }}</div>
<div class="source-description">{{ source.description || '无描述' }}</div>
</div>
<div class="source-status">
<span class="status-indicator" :class="{ 'active': source.isActive }"></span>
<span>{{ source.isActive ? '已启用' : '已禁用' }}</span>
</div>
<div class="source-actions">
<button class="action-btn edit-btn" @click.stop="editDataSource(source)">编辑</button>
<button class="action-btn delete-btn" @click.stop="deleteDataSource(source)">删除</button>
</div>
</div>
</div>
<!-- 数据源详情 -->
<div class="data-source-detail" v-if="selectedDataSource">
<div class="detail-header">
<h3>{{ selectedDataSource.name }}</h3>
<button class="test-btn" @click="testConnection" :disabled="isTesting">
{{ isTesting ? '测试中...' : '测试连接' }}
</button>
</div>
<div class="detail-content">
<div class="detail-section">
<h4>基本信息</h4>
<div class="info-item">
<label>类型:</label>
<span>{{ getDataSourceTypeName(selectedDataSource.type) }}</span>
</div>
<div class="info-item">
<label>创建时间:</label>
<span>{{ formatDate(selectedDataSource.createdAt) }}</span>
</div>
<div class="info-item">
<label>更新时间:</label>
<span>{{ formatDate(selectedDataSource.updatedAt) }}</span>
</div>
<div class="info-item">
<label>创建人:</label>
<span>{{ selectedDataSource.createdBy }}</span>
</div>
</div>
<div class="detail-section">
<h4>连接配置</h4>
<pre>{{ JSON.stringify(selectedDataSource.connectionConfig, null, 2) }}</pre>
</div>
<!-- 数据预览区域 -->
<div class="detail-section">
<h4>数据预览</h4>
<div class="preview-controls">
<input
type="text"
placeholder="输入查询..."
v-model="previewQuery"
class="query-input"
/>
<button class="execute-btn" @click="executePreviewQuery" :disabled="!previewQuery">
执行查询
</button>
</div>
<!-- 查询结果 -->
<div class="preview-result" v-if="previewResult">
<div class="result-header">
<span>查询结果 (共 {{ previewResult.total }} 条)</span>
<span v-if="previewResult.error" class="error-message">{{ previewResult.error }}</span>
</div>
<!-- 表格展示结果 -->
<table class="result-table" v-if="previewResult.data.length > 0 && !previewResult.error">
<thead>
<tr>
<th v-for="column in previewResult.columns" :key="column.name">
{{ column.name }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in previewResult.data" :key="index">
<td v-for="column in previewResult.columns" :key="column.name">
{{ row[column.name] }}
</td>
</tr>
</tbody>
</table>
<!-- 空结果 -->
<div class="empty-result" v-else-if="!previewResult.error">
没有查询到数据
</div>
</div>
</div>
</div>
</div>
<!-- 添加/编辑数据源模态框 -->
<div class="modal" v-if="showAddModal || showEditModal">
<div class="modal-overlay" @click="closeModal"></div>
<div class="modal-content">
<div class="modal-header">
<h3>{{ showEditModal ? '编辑数据源' : '添加数据源' }}</h3>
<button class="close-btn" @click="closeModal">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="saveDataSource">
<div class="form-group">
<label>名称 *</label>
<input
type="text"
v-model="formData.name"
required
placeholder="请输入数据源名称"
/>
</div>
<div class="form-group">
<label>类型 *</label>
<select v-model="formData.type" required @change="resetConnectionConfig">
<option
v-for="type in Object.values(DataSourceType)"
:key="type"
:value="type"
>
{{ getDataSourceTypeName(type) }}
</option>
</select>
</div>
<div class="form-group">
<label>描述</label>
<textarea
v-model="formData.description"
placeholder="请输入数据源描述"
rows="3"
></textarea>
</div>
<!-- 连接配置表单 -->
<div class="form-section">
<h4>连接配置</h4>
<!-- 数据库连接配置 -->
<template v-if="isDatabaseType(formData.type)">
<div class="form-group">
<label>主机 *</label>
<input
type="text"
v-model="formData.connectionConfig.host"
required
placeholder="请输入主机地址"
/>
</div>
<div class="form-group">
<label>端口 *</label>
<input
type="number"
v-model.number="formData.connectionConfig.port"
required
placeholder="请输入端口号"
/>
</div>
<div class="form-group">
<label>用户名 *</label>
<input
type="text"
v-model="formData.connectionConfig.username"
required
placeholder="请输入用户名"
/>
</div>
<div class="form-group">
<label>密码 *</label>
<input
type="password"
v-model="formData.connectionConfig.password"
required
placeholder="请输入密码"
/>
</div>
<div class="form-group">
<label>数据库 *</label>
<input
type="text"
v-model="formData.connectionConfig.database"
required
placeholder="请输入数据库名称"
/>
</div>
</template>
<!-- REST API连接配置 -->
<template v-else-if="formData.type === DataSourceType.REST_API">
<div class="form-group">
<label>URL *</label>
<input
type="url"
v-model="formData.connectionConfig.url"
required
placeholder="请输入API URL"
/>
</div>
<div class="form-group">
<label>默认方法</label>
<select v-model="formData.connectionConfig.method">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
</div>
<div class="form-group">
<label>请求头(JSON格式)</label>
<textarea
v-model="headersJson"
placeholder='例如:{"Authorization": "Bearer token"}'
rows="4"
></textarea>
</div>
</template>
</div>
<div class="form-actions">
<button type="button" class="cancel-btn" @click="closeModal">取消</button>
<button type="submit" class="save-btn">保存</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue';
import type { DataSourceConfig, DataSourceType, ConnectionConfig } from './types';
import { DataSourceAdapterFactory } from './adapters/DataSourceAdapterFactory';
import { DataSourceType as DsType } from './types';
// 数据源列表
const dataSources = ref<DataSourceConfig[]>([]);
// 选中的数据源
const selectedDataSource = ref<DataSourceConfig | null>(null);
// 显示添加模态框
const showAddModal = ref(false);
// 显示编辑模态框
const showEditModal = ref(false);
// 正在测试连接
const isTesting = ref(false);
// 预览查询
const previewQuery = ref('');
// 预览结果
const previewResult = ref<any>(null);
// 表单数据
const formData = reactive<Partial<DataSourceConfig>>({
name: '',
type: DsType.REST_API,
description: '',
connectionConfig: {},
isActive: true
});
// 请求头JSON字符串
const headersJson = ref('');
// 监听请求头JSON变化
watch(() => headersJson.value, (newValue) => {
try {
if (newValue) {
formData.connectionConfig.headers = JSON.parse(newValue);
} else {
formData.connectionConfig.headers = {};
}
} catch (e) {
// JSON格式错误,忽略
}
});
// 初始化
function init() {
// 从本地存储加载数据源配置(实际项目中应该从服务器加载)
const savedData = localStorage.getItem('data-sources');
if (savedData) {
try {
dataSources.value = JSON.parse(savedData);
} catch (e) {
console.error('Failed to load data sources:', e);
}
}
}
// 获取数据源类型名称
function getDataSourceTypeName(type: DataSourceType): string {
const typeNames: Record<DataSourceType, string> = {
[DsType.MYSQL]: 'MySQL',
[DsType.POSTGRESQL]: 'PostgreSQL',
[DsType.MONGODB]: 'MongoDB',
[DsType.REST_API]: 'REST API',
[DsType.GRAPHQL_API]: 'GraphQL API'
};
return typeNames[type] || type;
}
// 选择数据源
function selectDataSource(source: DataSourceConfig) {
selectedDataSource.value = source;
}
// 显示添加数据源模态框
function showAddDataSourceModal() {
resetForm();
showAddModal.value = true;
}
// 显示编辑数据源模态框
function editDataSource(source: DataSourceConfig) {
formData.name = source.name;
formData.type = source.type;
formData.description = source.description;
formData.connectionConfig = { ...source.connectionConfig };
formData.isActive = source.isActive;
// 设置请求头JSON
if (formData.connectionConfig.headers) {
headersJson.value = JSON.stringify(formData.connectionConfig.headers, null, 2);
}
showEditModal.value = true;
}
// 删除数据源
function deleteDataSource(source: DataSourceConfig) {
if (confirm(`确定要删除数据源"${source.name}"吗?`)) {
const index = dataSources.value.findIndex(s => s.id === source.id);
if (index >= 0) {
dataSources.value.splice(index, 1);
saveDataSources();
if (selectedDataSource.value?.id === source.id) {
selectedDataSource.value = null;
}
}
}
}
// 重置表单
function resetForm() {
formData.name = '';
formData.type = DsType.REST_API;
formData.description = '';
resetConnectionConfig();
formData.isActive = true;
headersJson.value = '';
}
// 重置连接配置
function resetConnectionConfig() {
formData.connectionConfig = {};
headersJson.value = '';
}
// 关闭模态框
function closeModal() {
showAddModal.value = false;
showEditModal.value = false;
resetForm();
}
// 保存数据源
async function saveDataSource() {
try {
if (showEditModal.value && selectedDataSource.value) {
// 编辑现有数据源
const index = dataSources.value.findIndex(s => s.id === selectedDataSource.value?.id);
if (index >= 0) {
dataSources.value[index] = {
...dataSources.value[index],
name: formData.name,
type: formData.type as DataSourceType,
description: formData.description,
connectionConfig: formData.connectionConfig as ConnectionConfig,
updatedAt: new Date()
};
}
} else {
// 添加新数据源
const newSource: DataSourceConfig = {
id: `ds_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
name: formData.name as string,
type: formData.type as DataSourceType,
description: formData.description,
connectionConfig: formData.connectionConfig as ConnectionConfig,
createdAt: new Date(),
updatedAt: new Date(),
createdBy: 'admin',
isActive: formData.isActive as boolean
};
dataSources.value.push(newSource);
}
saveDataSources();
closeModal();
// 如果是编辑,更新选中的数据源
if (showEditModal.value && selectedDataSource.value) {
selectDataSource(dataSources.value.find(s => s.id === selectedDataSource.value?.id) || null);
}
} catch (error) {
console.error('Failed to save data source:', error);
}
}
// 测试连接
async function testConnection() {
if (!selectedDataSource) return;
try {
isTesting.value = true;
const adapter = DataSourceAdapterFactory.getAdapter(selectedDataSource.type);
if (adapter) {
const success = await adapter.testConnection(selectedDataSource.connectionConfig);
if (success) {
alert('连接成功!');
} else {
alert('连接失败!');
}
} else {
alert('不支持的数据源类型!');
}
} catch (error) {
console.error('Failed to test connection:', error);
alert('连接测试失败!');
} finally {
isTesting.value = false;
}
}
// 保存数据源到本地存储
function saveDataSources() {
localStorage.setItem('data-sources', JSON.stringify(dataSources.value));
}
// 格式化日期
function formatDate(date: Date | string): string {
const d = new Date(date);
return d.toLocaleString('zh-CN');
}
// 判断是否为数据库类型
function isDatabaseType(type: DataSourceType): boolean {
return [
DsType.MYSQL,
DsType.POSTGRESQL,
DsType.MONGODB
].includes(type);
}
// 关闭模态框
function closeModal() {
showAddModal.value = false;
showEditModal.value = false;
resetForm();
}
// 初始化数据
init();
</script>
<style scoped>
.data-source-manager {
display: flex;
flex-direction: column;
height: 100%;
font-family: Arial, sans-serif;
}
/* 头部样式 */
.manager-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background-color: #1976d2;
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.manager-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.add-btn {
padding: 8px 16px;
background-color: white;
color: #1976d2;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.async-table th,
.async-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.async-table th {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
.load-more {
text-align: center;
padding: 16px;
}
.load-more button {
padding: 8px 16px;
background-color: #2196f3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.load-more button:hover:not(:disabled) {
background-color: #1976d2;
}
.load-more button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>五、最佳实践
5.1 数据源设计原则
- 统一管理:所有数据源集中管理,便于维护和监控
- 安全第一:敏感信息(如密码、密钥)应加密存储,避免明文保存
- 权限控制:为不同用户分配不同的数据源访问权限
- 连接池优化:对于数据库类型的数据源,使用连接池管理连接
- 定期测试:定期测试数据源连接,确保数据源可用性
- 监控告警:监控数据源的性能和可用性,设置告警机制
5.2 查询设计原则
- 性能优先:优化查询语句,避免全表扫描和复杂查询
- 缓存策略:对频繁执行的查询结果进行缓存
- 分页处理:对于大数据量查询,实现分页机制
- 异步执行:对于耗时查询,采用异步执行方式
- 错误处理:完善的错误处理机制,提供友好的错误提示
- 日志记录:记录查询执行日志,便于调试和监控
5.3 安全最佳实践
- 数据源隔离:不同环境(开发、测试、生产)使用不同的数据源
- 最小权限原则:为数据源用户分配最小必要权限
- 加密传输:使用HTTPS或其他加密方式传输数据
- 防止SQL注入:对于数据库查询,使用参数化查询
- API密钥管理:安全管理API密钥和访问令牌
- 定期审计:定期审计数据源访问日志,发现异常访问
六、总结
数据源管理是低代码平台的重要组成部分,它为应用提供了与各种数据源集成的能力。在本集中,我们学习了:
- 数据源管理的核心功能和架构设计
- 数据源配置的数据模型设计
- 数据源适配器模式的实现
- REST API适配器的具体实现
- 数据源管理可视化组件的实现
- 数据查询组件的实现
- 数据源与组件的动态绑定
- 性能优化策略,包括连接池管理、查询缓存和异步加载
- 数据源管理的最佳实践
通过合理设计和实现数据源管理功能,我们可以构建出高效、安全、易用的低代码平台,使用户能够轻松地将应用与各种数据源集成,实现数据的可视化展示和交互。
在下一集中,我们将学习如何实现低代码平台的工作流设计器,进一步完善低代码开发环境。
}
.data-source-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.data-source-item.active {
border-color: #1976d2;
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
}
.source-info {
flex: 1;
min-width: 0;
}
.source-name {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.source-type {
font-size: 12px;
color: #1976d2;
margin-bottom: 4px;
}
.source-description {
font-size: 14px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.source-status {
display: flex;
align-items: center;
gap: 8px;
margin: 0 20px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #ccc;
transition: all 0.3s ease;
}
.status-indicator.active {
background-color: #4caf50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.source-actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.edit-btn {
background-color: #2196f3;
color: white;
}
.edit-btn:hover {
background-color: #1976d2;
}
.delete-btn {
background-color: #f44336;
color: white;
}
.delete-btn:hover {
background-color: #d32f2f;
}
/* 数据源详情样式 */
.data-source-detail {
padding: 20px;
background-color: white;
border-top: 1px solid #e0e0e0;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.detail-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.test-btn {
padding: 8px 16px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.test-btn:hover:not(:disabled) {
background-color: #388e3c;
}
.test-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.detail-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.detail-section {
background-color: #fafafa;
padding: 16px;
border-radius: 8px;
}
.detail-section h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.info-item {
display: flex;
margin-bottom: 8px;
}
.info-item label {
width: 100px;
font-weight: 500;
color: #666;
}
.info-item span {
flex: 1;
color: #333;
}
/* 预览区域样式 */
.preview-controls {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.query-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
}
.execute-btn {
padding: 8px 16px;
background-color: #2196f3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.execute-btn:hover:not(:disabled) {
background-color: #1976d2;
}
.execute-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.preview-result {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.error-message {
color: #f44336;
font-size: 14px;
}
.result-table {
width: 100%;
border-collapse: collapse;
}
.result-table th,
.result-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.result-table th {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
.empty-result {
padding: 24px;
text-align: center;
color: #999;
}
/* 模态框样式 */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
width: 600px;
max-width: 90%;
max-height: 90vh;
overflow-y: auto;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.3s ease;
}
.close-btn:hover {
background-color: #f5f5f5;
color: #333;
}
.modal-body {
padding: 20px;
}
/* 表单样式 */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #333;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
transition: all 0.3s ease;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
}
.form-section {
margin: 20px 0;
padding: 16px;
background-color: #fafafa;
border-radius: 8px;
}
.form-section h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.cancel-btn,
.save-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.cancel-btn {
background-color: #f5f5f5;
color: #333;
}
.cancel-btn:hover {
background-color: #e0e0e0;
}
.save-btn {
background-color: #1976d2;
color: white;
}
.save-btn:hover {
background-color: #1565c0;
}
### 2.5 数据查询组件
实现数据查询组件,用于可视化构建和执行数据查询:
```vue
<template>
<div class="query-builder">
<div class="builder-header">
<h3>数据查询构建器</h3>
<div class="query-actions">
<button class="action-btn save-btn" @click="saveQuery">保存查询</button>
<button class="action-btn execute-btn" @click="executeQuery" :disabled="isExecuting">
{{ isExecuting ? '执行中...' : '执行查询' }}
</button>
</div>
</div>
<!-- 查询配置 -->
<div class="query-config">
<div class="form-row">
<div class="form-group">
<label>数据源 *</label>
<select v-model="queryConfig.dataSourceId" required>
<option value="">请选择数据源</option>
<option
v-for="source in dataSources"
:key="source.id"
:value="source.id"
>
{{ source.name }}
</option>
</select>
</div>
<div class="form-group">
<label>查询名称 *</label>
<input
type="text"
v-model="queryConfig.name"
required
placeholder="请输入查询名称"
/>
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label>查询描述</label>
<textarea
v-model="queryConfig.description"
placeholder="请输入查询描述"
rows="2"
></textarea>
</div>
</div>
<!-- 查询编辑器 -->
<div class="query-editor-section">
<h4>查询内容</h4>
<div class="query-editor">
<!-- SQL编辑器 -->
<textarea
v-if="isDatabaseQuery"
v-model="sqlQuery"
placeholder="请输入SQL查询语句"
rows="10"
class="sql-editor"
></textarea>
<!-- API请求配置 -->
<div v-else-if="isApiQuery" class="api-config">
<div class="form-row">
<div class="form-group">
<label>URL</label>
<input
type="url"
v-model="apiQuery.url"
placeholder="请输入API URL"
/>
</div>
<div class="form-group">
<label>方法</label>
<select v-model="apiQuery.method">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label>请求体(JSON)</label>
<textarea
v-model="apiQuery.body"
placeholder="请输入请求体JSON"
rows="6"
class="json-editor"
></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 查询结果 -->
<div class="query-result" v-if="queryResult">
<div class="result-header">
<span>查询结果 (共 {{ queryResult.total }} 条)</span>
<span v-if="queryResult.error" class="error-message">{{ queryResult.error }}</span>
</div>
<!-- 表格展示结果 -->
<table class="result-table" v-if="queryResult.data.length > 0 && !queryResult.error">
<thead>
<tr>
<th v-for="column in queryResult.columns" :key="column.name">
{{ column.name }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in queryResult.data" :key="index">
<td v-for="column in queryResult.columns" :key="column.name">
{{ row[column.name] }}
</td>
</tr>
</tbody>
</table>
<!-- 空结果 -->
<div class="empty-result" v-else-if="!queryResult.error">
没有查询到数据
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue';
import type { QueryConfig, QueryResult, DataSourceConfig } from './types';
import { DataSourceAdapterFactory } from './adapters/DataSourceAdapterFactory';
// 数据源列表
const dataSources = ref<DataSourceConfig[]>([]);
// 查询配置
const queryConfig = reactive<QueryConfig>({
id: `query_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
name: '',
dataSourceId: '',
query: '',
description: '',
createdAt: new Date(),
updatedAt: new Date()
});
// SQL查询
const sqlQuery = ref('');
// API查询配置
const apiQuery = reactive({
url: '',
method: 'GET',
body: '{}'
});
// 正在执行查询
const isExecuting = ref(false);
// 查询结果
const queryResult = ref<QueryResult | null>(null);
// 计算属性:是否为数据库查询
const isDatabaseQuery = computed(() => {
const source = dataSources.value.find(s => s.id === queryConfig.dataSourceId);
return source ? ['mysql', 'postgresql', 'mongodb'].includes(source.type) : false;
});
// 计算属性:是否为API查询
const isApiQuery = computed(() => {
const source = dataSources.value.find(s => s.id === queryConfig.dataSourceId);
return source ? ['rest-api', 'graphql-api'].includes(source.type) : false;
});
// 监听数据源变化
watch(() => queryConfig.dataSourceId, () => {
// 重置查询内容
sqlQuery.value = '';
apiQuery.url = '';
apiQuery.method = 'GET';
apiQuery.body = '{}';
queryConfig.query = '';
});
// 初始化
function init() {
// 从本地存储加载数据源配置
const savedData = localStorage.getItem('data-sources');
if (savedData) {
try {
dataSources.value = JSON.parse(savedData);
} catch (e) {
console.error('Failed to load data sources:', e);
}
}
}
// 执行查询
async function executeQuery() {
try {
isExecuting.value = true;
// 获取数据源
const source = dataSources.value.find(s => s.id === queryConfig.dataSourceId);
if (!source) {
alert('请选择数据源!');
return;
}
// 获取适配器
const adapter = DataSourceAdapterFactory.getAdapter(source.type);
if (!adapter) {
alert('不支持的数据源类型!');
return;
}
// 构建查询内容
let queryContent: string | any = '';
if (isDatabaseQuery.value) {
queryContent = sqlQuery.value;
queryConfig.query = sqlQuery.value;
} else if (isApiQuery.value) {
queryContent = {
url: apiQuery.url,
method: apiQuery.method,
data: JSON.parse(apiQuery.body)
};
queryConfig.query = queryContent;
}
if (!queryContent) {
alert('请输入查询内容!');
return;
}
// 执行查询
const result = await adapter.executeQuery(source.connectionConfig, queryContent);
queryResult.value = result;
} catch (error: any) {
console.error('Failed to execute query:', error);
queryResult.value = {
data: [],
total: 0,
error: error.message || '查询执行失败'
};
} finally {
isExecuting.value = false;
}
}
// 保存查询
function saveQuery() {
if (!queryConfig.name) {
alert('请输入查询名称!');
return;
}
if (!queryConfig.dataSourceId) {
alert('请选择数据源!');
return;
}
// 保存查询到本地存储(实际项目中应该保存到服务器)
const savedQueries = JSON.parse(localStorage.getItem('queries') || '[]');
savedQueries.push(queryConfig);
localStorage.setItem('queries', JSON.stringify(savedQueries));
alert('查询保存成功!');
}
// 初始化数据
init();
</script>
<style scoped>
.query-builder {
padding: 20px;
font-family: Arial, sans-serif;
}
.builder-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.builder-header h3 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
.query-actions {
display: flex;
gap: 12px;
}
.action-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
}
.save-btn {
background-color: #4caf50;
color: white;
}
.save-btn:hover {
background-color: #388e3c;
}
.execute-btn {
background-color: #2196f3;
color: white;
}
.execute-btn:hover:not(:disabled) {
background-color: #1976d2;
}
.execute-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 查询配置样式 */
.query-config {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.form-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.form-group {
flex: 1;
min-width: 0;
}
.form-group.full-width {
flex: 1 1 100%;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #333;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
transition: all 0.3s ease;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
}
/* 查询编辑器样式 */
.query-editor-section {
margin-top: 20px;
}
.query-editor-section h4 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.query-editor {
border: 1px solid #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.sql-editor,
.json-editor {
width: 100%;
padding: 12px;
border: none;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.5;
resize: vertical;
background-color: #fafafa;
}
.sql-editor:focus,
.json-editor:focus {
outline: none;
background-color: white;
}
/* API配置样式 */
.api-config {
padding: 16px;
background-color: #fafafa;
}
/* 查询结果样式 */
.query-result {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.error-message {
color: #f44336;
font-size: 14px;
}
.result-table {
width: 100%;
border-collapse: collapse;
}
.result-table th,
.result-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.result-table th {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
.empty-result {
padding: 24px;
text-align: center;
color: #999;
}
</style>三、使用示例
3.1 基本使用
添加数据源:
- 点击"添加数据源"按钮
- 选择数据源类型(如REST API)
- 配置连接参数
- 点击"保存"按钮
测试数据源连接:
- 选择添加的数据源
- 点击"测试连接"按钮
- 查看连接测试结果
构建和执行查询:
- 在查询构建器中选择数据源
- 配置查询内容
- 点击"执行查询"按钮
- 查看查询结果
保存查询:
- 输入查询名称和描述
- 点击"保存查询"按钮
3.2 与组件绑定
将查询结果与组件动态绑定,实现数据的可视化展示:
<template>
<div class="data-binding-example">
<h2>数据绑定示例</h2>
<!-- 选择查询 -->
<div class="query-selector">
<label>选择查询:</label>
<select v-model="selectedQueryId" @change="loadQueryResults">
<option value="">请选择查询</option>
<option
v-for="query in savedQueries"
:key="query.id"
:value="query.id"
>
{{ query.name }}
</option>
</select>
</div>
<!-- 数据展示组件 -->
<div class="data-display">
<h3>查询结果</h3>
<!-- 表格展示 -->
<table class="data-table" v-if="queryResults && queryResults.data.length > 0">
<thead>
<tr>
<th v-for="column in queryResults.columns" :key="column.name">
{{ column.name }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in queryResults.data" :key="index">
<td v-for="column in queryResults.columns" :key="column.name">
{{ row[column.name] }}
</td>
</tr>
</tbody>
</table>
<!-- 空结果 -->
<div class="empty-result" v-else-if="queryResults">
没有查询到数据
</div>
<!-- 加载状态 -->
<div class="loading" v-if="isLoading">
加载中...
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import type { QueryConfig, QueryResult } from './types';
import { DataSourceAdapterFactory } from './adapters/DataSourceAdapterFactory';
// 保存的查询列表
const savedQueries = ref<QueryConfig[]>([]);
// 选中的查询ID
const selectedQueryId = ref('');
// 查询结果
const queryResults = ref<QueryResult | null>(null);
// 加载状态
const isLoading = ref(false);
// 初始化
function init() {
// 从本地存储加载保存的查询
const savedData = localStorage.getItem('queries');
if (savedData) {
try {
savedQueries.value = JSON.parse(savedData);
} catch (e) {
console.error('Failed to load saved queries:', e);
}
}
}
// 加载查询结果
async function loadQueryResults() {
if (!selectedQueryId.value) {
queryResults.value = null;
return;
}
try {
isLoading.value = true;
// 获取查询配置
const query = savedQueries.value.find(q => q.id === selectedQueryId.value);
if (!query) return;
// 获取数据源配置
const dataSources = JSON.parse(localStorage.getItem('data-sources') || '[]');
const source = dataSources.find((s: any) => s.id === query.dataSourceId);
if (!source) return;
// 获取适配器并执行查询
const adapter = DataSourceAdapterFactory.getAdapter(source.type);
if (adapter) {
const results = await adapter.executeQuery(source.connectionConfig, query.query);
queryResults.value = results;
}
} catch (error) {
console.error('Failed to load query results:', error);
} finally {
isLoading.value = false;
}
}
onMounted(() => {
init();
});
</script>
<style scoped>
.data-binding-example {
padding: 20px;
font-family: Arial, sans-serif;
}
.data-binding-example h2 {
margin: 0 0 20px 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.query-selector {
margin-bottom: 20px;
}
.query-selector label {
margin-right: 12px;
font-weight: 500;
color: #333;
}
.query-selector select {
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
}
.data-display {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
}
.data-display h3 {
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.data-table th {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
.empty-result {
padding: 24px;
text-align: center;
color: #999;
}
.loading {
padding: 24px;
text-align: center;
color: #666;
}
</style>四、性能优化
4.1 连接池管理
对于数据库类型的数据源,实现连接池管理,复用数据库连接,减少连接创建和销毁的开销:
// 连接池管理示例
export class ConnectionPool {
private pool: any[] = [];
private maxSize: number;
private createConnection: () => Promise<any>;
private destroyConnection: (conn: any) => Promise<void>;
constructor(options: {
maxSize: number;
createConnection: () => Promise<any>;
destroyConnection: (conn: any) => Promise<void>;
}) {
this.maxSize = options.maxSize;
this.createConnection = options.createConnection;
this.destroyConnection = options.destroyConnection;
}
// 获取连接
async getConnection(): Promise<any> {
if (this.pool.length > 0) {
return this.pool.pop();
}
if (this.pool.length < this.maxSize) {
return this.createConnection();
}
// 等待连接可用(可以实现更复杂的等待机制)
await new Promise(resolve => setTimeout(resolve, 100));
return this.getConnection();
}
// 归还连接
releaseConnection(connection: any): void {
this.pool.push(connection);
}
// 关闭所有连接
async closeAll(): Promise<void> {
while (this.pool.length > 0) {
const conn = this.pool.pop();
if (conn) {
await this.destroyConnection(conn);
}
}
}
}4.2 查询缓存
实现查询缓存机制,缓存频繁执行的查询结果,减少数据源访问次数:
// 查询缓存示例
export class QueryCache {
private cache: Map<string, {
result: any;
timestamp: number;
ttl: number;
}> = new Map();
constructor(private defaultTtl: number = 5 * 60 * 1000) {} // 默认缓存5分钟
// 设置缓存
set(key: string, result: any, ttl?: number): void {
this.cache.set(key, {
result,
timestamp: Date.now(),
ttl: ttl || this.defaultTtl
});
}
// 获取缓存
get(key: string): any | null {
const cached = this.cache.get(key);
if (!cached) return null;
// 检查缓存是否过期
if (Date.now() - cached.timestamp > cached.ttl) {
this.cache.delete(key);
return null;
}
return cached.result;
}
// 清除缓存
clear(key?: string): void {
if (key) {
this.cache.delete(key);
} else {
this.cache.clear();
}
}
// 生成缓存键
generateKey(query: string | any, params?: any): string {
const queryStr = typeof query === 'string' ? query : JSON.stringify(query);
const paramsStr = params ? JSON.stringify(params) : '';
return `${queryStr}_${paramsStr}`;
}
}4.3 异步加载
对于大数据量的查询结果,实现异步加载和分页机制,提高用户体验:
<template>
<div class="async-data-loading">
<h2>异步数据加载示例</h2>
<!-- 数据表格 -->
<table class="async-table">
<thead>
<tr>
<th v-for="column in columns" :key="column.name">
{{ column.name }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in displayData" :key="index">
<td v-for="column in columns" :key="column.name">
{{ row[column.name] }}
</td>
</tr>
<!-- 加载更多行 -->
<tr v-if="hasMoreData">
<td :colspan="columns.length" class="load-more">
<button @click="loadMore" :disabled="isLoading">
{{ isLoading ? '加载中...' : '加载更多' }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
// 模拟数据
const allData = ref<any[]>([]);
const columns = ref([
{ name: 'id' },
{ name: 'name' },
{ name: 'email' },
{ name: 'createdAt' }
]);
const pageSize = 10;
const currentPage = ref(1);
const isLoading = ref(false);
// 显示的数据
const displayData = computed(() => {
const start = 0;
const end = currentPage.value * pageSize;
return allData.value.slice(start, end);
});
// 是否有更多数据
const hasMoreData = computed(() => {
return displayData.value.length < allData.value.length;
});
// 加载更多数据
async function loadMore() {
if (isLoading.value) return;
isLoading.value = true;
// 模拟异步加载
await new Promise(resolve => setTimeout(resolve, 1000));
currentPage.value++;
isLoading.value = false;
}
// 初始化数据
function initData() {
// 生成模拟数据
for (let i = 1; i <= 100; i++) {
allData.value.push({
id: i,
name: `User ${i}`,
email: `user${i}@example.com`,
createdAt: new Date().toISOString()
});
}
}
initData();
</script>
<style scoped>
.async-data-loading {
padding: 20px;
font-family: Arial, sans-serif;
}
.async-data-loading h2 {
margin: 0 0 20px 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.async-table {
width: 100%;
border-collapse: collapse;
border: