第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">&times;</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密钥和访问令牌
  • 定期审计:定期审计数据源访问日志,发现异常访问

六、总结

数据源管理是低代码平台的重要组成部分,它为应用提供了与各种数据源集成的能力。在本集中,我们学习了:

  1. 数据源管理的核心功能和架构设计
  2. 数据源配置的数据模型设计
  3. 数据源适配器模式的实现
  4. REST API适配器的具体实现
  5. 数据源管理可视化组件的实现
  6. 数据查询组件的实现
  7. 数据源与组件的动态绑定
  8. 性能优化策略,包括连接池管理、查询缓存和异步加载
  9. 数据源管理的最佳实践

通过合理设计和实现数据源管理功能,我们可以构建出高效、安全、易用的低代码平台,使用户能够轻松地将应用与各种数据源集成,实现数据的可视化展示和交互。

在下一集中,我们将学习如何实现低代码平台的工作流设计器,进一步完善低代码开发环境。
}

.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 基本使用

  1. 添加数据源

    • 点击"添加数据源"按钮
    • 选择数据源类型(如REST API)
    • 配置连接参数
    • 点击"保存"按钮
  2. 测试数据源连接

    • 选择添加的数据源
    • 点击"测试连接"按钮
    • 查看连接测试结果
  3. 构建和执行查询

    • 在查询构建器中选择数据源
    • 配置查询内容
    • 点击"执行查询"按钮
    • 查看查询结果
  4. 保存查询

    • 输入查询名称和描述
    • 点击"保存查询"按钮

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:
« 上一篇 Vue 3低代码平台可视化编辑器实现 下一篇 » Vue 3低代码平台版本管理与回滚