摘要
本文介绍了一个基于Web的SQL执行工具的设计与实现,该工具允许用户通过浏览器直接执行SQL语句并查看结果。系统采用前后端分离架构,后端基于Spring Boot框架实现SQL执行引擎,前端使用Vue.js构建用户界面。该系统提供了直观的SQL编辑器、数据库表管理、历史查询记录和结果展示等功能,有效提高了数据库操作效率。
(本质就是客户端不知为何连接不上服务器的数据库,紧急情况下,手搓了一个sql 执行器)
正文
本文介绍的网页版SQL执行工具,通过整合Spring Boot后端与Vue前端,实现了数据库管理的范式转移,将复杂操作转化为直观的Web界面体验。
技术架构解析
后端核心设计:
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.web.bind.annotation.*;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.*;
/**
* @Author:Derek_Smart
* @Date:2025/8/4 17:17
*/
@RestController
@RequestMapping("/sqlUtil")
public class SqlUtilResource {
@Autowired
private JdbcTemplate jdbcTemplate;
private final Logger logger = LoggerFactory.getLogger(SqlUtilResource.class);
@PostMapping("/executeSqlp")
public Map<String, Object> executeSqlp(@RequestBody SqlRequest request) {
try {
String sql = request.getSql();
List<Object> params = request.getParams();
if (sql.trim().toUpperCase().startsWith("SELECT")) {
// 查询操作
List<Map<String, Object>> result = jdbcTemplate.query(sql, params.toArray(), (rs, rowNum) -> {
Map<String, Object> row = new LinkedHashMap<>();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnLabel(i);
row.put(columnName, rs.getObject(i));
}
return row;
});
return createSuccessResponse(result);
} else {
// 更新操作
int affectedRows = jdbcTemplate.update(sql, params.toArray());
return createSuccessResponse(Collections.singletonMap("affectedRows", affectedRows));
}
} catch (Exception e) {
logger.error("SQL执行错误: {}", e.getMessage(), e);
return createErrorResponse("SQL执行错误: " + e.getMessage());
}
}
@PostMapping("/executeSql")
public Map<String, Object> executeSql(@RequestBody SqlRequest request) {
try {
String sql = request.getSql();
List<Object> params = request.getParams();
if (sql.trim().toUpperCase().startsWith("SELECT")) {
// 查询操作 - 返回列和行数据
return handleQuery(sql, params);
} else {
// 更新操作 - 返回受影响行数
return handleUpdate(sql, params);
}
} catch (Exception e) {
logger.error("SQL执行错误: {}", e.getMessage(), e);
return createErrorResponse("SQL执行错误: " + e.getMessage());
}
}
/**
* 获取数据库所有表名
* @return 表名列表
*/
@GetMapping("/getTables")
public Map<String, Object> getDatabaseTables() {
try {
return jdbcTemplate.execute((ConnectionCallback<Map<String, Object>>) connection -> {
DatabaseMetaData metaData = connection.getMetaData();
// 获取所有表名(不包含系统表)
ResultSet tables = metaData.getTables(
null, null, null, new String[]{"TABLE"});
List<String> tableNames = new ArrayList<>();
while (tables.next()) {
String tableName = tables.getString("TABLE_NAME");
tableNames.add(tableName);
}
// 按字母顺序排序
Collections.sort(tableNames);
// 构建响应
Map<String, Object> response = new LinkedHashMap<>();
response.put("success", true);
response.put("data", tableNames);
return response;
});
} catch (Exception e) {
logger.error("获取数据库表失败: {}", e.getMessage(), e);
Map<String, Object> errorResponse = new LinkedHashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "获取数据库表失败: " + e.getMessage());
return errorResponse;
}
}
private Map<String, Object> handleQuery(String sql, List<Object> params) {
try {
return jdbcTemplate.query(sql, params.toArray(), (ResultSetExtractor<Map<String, Object>>) rs -> {
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
// 获取列名
List<String> columns = new ArrayList<>();
for (int i = 1; i <= columnCount; i++) {
columns.add(metaData.getColumnLabel(i));
}
// 获取行数据
List<List<Object>> rows = new ArrayList<>();
while (rs.next()) {
List<Object> row = new ArrayList<>();
for (int i = 1; i <= columnCount; i++) {
row.add(rs.getObject(i));
}
rows.add(row);
}
// 构建响应
Map<String, Object> data = new LinkedHashMap<>();
data.put("columns", columns);
data.put("rows", rows);
Map<String, Object> response = new LinkedHashMap<>();
response.put("success", true);
response.put("data", data);
return response;
});
} catch (Exception e) {
logger.error("查询处理失败: {}", e.getMessage(), e);
return createErrorResponse("查询处理失败: " + e.getMessage());
}
}
private Map<String, Object> handleUpdate(String sql, List<Object> params) {
int affectedRows = jdbcTemplate.update(sql, params.toArray());
Map<String, Object> data = new LinkedHashMap<>();
data.put("affectedRows", affectedRows);
Map<String, Object> response = new LinkedHashMap<>();
response.put("success", true);
response.put("data", data);
return response;
}
// 创建成功响应
private Map<String, Object> createSuccessResponse(Object data) {
Map<String, Object> response = new LinkedHashMap<>();
response.put("success", true);
response.put("timestamp", System.currentTimeMillis());
response.put("data", data);
return response;
}
// 创建错误响应
private Map<String, Object> createErrorResponse(String message) {
Map<String, Object> response = new LinkedHashMap<>();
response.put("success", false);
response.put("timestamp", System.currentTimeMillis());
response.put("message", message);
return response;
}
public static class SqlRequest {
private String sql;
private List<Object> params = new ArrayList<>();
// Getters and Setters
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public List<Object> getParams() {
return params;
}
public void setParams(List<Object> params) {
this.params = params;
}
}
}
后端采用策略模式实现SQL路由,自动区分查询与更新操作。通过JDBC元数据接口实现数据库自发现能力,为前端提供结构化数据支撑。
前端创新交互:
vue
<template>
<!-- 双面板设计 -->
<div class="editor-container">
<!-- 元数据导航区 -->
<div class="sidebar">
<div v-for="table in tables" @click="selectTable(table)">
<i class="fas fa-table"></i> {{ table }}
</div>
</div>
<!-- SQL工作区 -->
<textarea
v-model="sqlQuery"
@keydown.ctrl.enter="executeSql"></textarea>
<!-- 智能结果渲染 -->
<el-table :data="result.data.rows">
<el-table-column
v-for="(column, index) in result.data.columns"
:label="column">
<template v-slot="scope">
<span v-if="scope.row[index] === null" class="null-value">NULL</span>
<span v-else-if="typeof scope.row[index] === 'object'"
@click="showJsonDetail(scope.row[index])">
{{ jsonPreview(scope.row[index]) }}
</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
前端实现三区域布局:元数据导航、SQL编辑器和智能结果面板。采用动态类型检测技术,对NULL值、JSON对象等特殊数据类型进行可视化区分处理。
核心技术亮点
-
实时元数据发现
- 自动加载数据库表结构
- 表名智能排序与即时搜索
- 点击表名自动生成SELECT模板
-
智能SQL处理
javascript
// SQL执行核心逻辑
async executeSql() {
const paginatedSql = this.addPagination(this.sqlQuery);
const response = await executeSql(paginatedSql);
// 高级结果处理
if (response.success) {
this.result = {
...response,
data: {
...response.data,
executionTime: Date.now() - startTime
}
}
}
}
- 自动分页机制
- 执行耗时精准统计
- 语法错误实时反馈
3.历史版本管理
js
// 历史记录管理
addToHistory(sql) {
if (this.history.includes(sql)) return;
this.history.unshift(sql);
localStorage.setItem('sqlHistory', JSON.stringify(this.history));
}
-
- 本地存储自动持久化
- 智能去重机制
- 一键恢复历史查询
-
数据可视化增强
- JSON对象折叠预览
- NULL值特殊标识
- 分页控件动态加载
性能优化策略
-
查询优化
- 自动追加LIMIT子句
- 分页查询按需加载
- 结果集流式处理
-
缓存机制
js
// 表结构缓存
async fetchDatabaseTables() {
if (this.cachedTables) return this.cachedTables;
const response = await fetchTables();
this.cachedTables = response.data;
}
完整vue代码:
vue
<template>
<div class="sql-executor-container">
<!-- 头部 -->
<header>
<div class="logo">
<i class="fas fa-database logo-icon"></i>
<div>
<h1>SQL 执行工具</h1>
<p>网页版数据库客户端工具</p>
</div>
</div>
<div class="connection-info">
<i class="fas fa-plug"></i> 已连接到 {{ connectionName }} 数据库
</div>
</header>
<!-- 主体内容 -->
<div class="main-content">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-section">
<h3>数据库表</h3>
<div
v-for="table in tables"
:key="table"
class="schema-item"
:class="{ active: selectedTable === table }"
@click="selectTable(table)"
>
<i class="fas fa-table"></i> {{ table }}
</div>
</div>
<div class="sidebar-section">
<h3>历史查询</h3>
<div
v-for="(query, index) in history"
:key="index"
class="schema-item history-item"
@click="loadHistoryQuery(query)"
>
<i class="fas fa-history"></i> {{ query.substring(0, 40) }}{{ query.length > 40 ? '...' : '' }}
</div>
</div>
</div>
<!-- 编辑器区域 -->
<div class="editor-container">
<div class="sql-editor-container">
<div class="sql-editor-header">
<h2>SQL 编辑器</h2>
<div class="toolbar">
<el-button
class="toolbar-btn run"
@click="executeSql"
:loading="loading"
>
<i class="fas fa-play"></i> 执行
</el-button>
<el-button
class="toolbar-btn format"
@click="formatSql"
>
<i class="fas fa-indent"></i> 格式化
</el-button>
<el-button
class="toolbar-btn clear"
@click="clearEditor"
>
<i class="fas fa-trash-alt"></i> 清除
</el-button>
</div>
</div>
<textarea
class="sql-editor"
v-model="sqlQuery"
placeholder="输入 SQL 语句,例如:SELECT * FROM table_name"
@keydown.ctrl.enter="executeSql"
></textarea>
</div>
<!-- 结果区域 -->
<div class="result-container">
<div class="result-header">
<h2>执行结果</h2>
<div class="result-info">
<div
class="result-status"
:class="result.success ? 'status-success' : 'status-error'"
v-if="result"
>
{{ result.success ? '执行成功' : '执行失败' }}
</div>
<div class="rows-count" v-if="result && result.success">
共 {{ result.data.total }} 行数据 (显示 {{ result.data.rows.length }} 行)
</div>
<div class="execution-time" v-if="result && result.success">
<i class="fas fa-clock"></i> {{ result.data.executionTime }} ms
</div>
</div>
</div>
<div class="result-content">
<!-- 加载状态 -->
<div v-if="loading" class="no-data">
<i class="el-icon-loading" style="font-size: 48px;"></i>
<p>正在执行查询...</p>
</div>
<!-- 查询结果 -->
<div v-else-if="result && result.success && result.data.rows.length > 0" class="table-container">
<div class="table-wrapper">
<el-table
:data="result.data.rows"
height="100%"
style="width: 100%"
stripe
border
size="small"
:default-sort = "{prop: 'id', order: 'ascending'}"
>
<el-table-column
v-for="(column, index) in result.data.columns"
:key="index"
:prop="'row.' + index"
:label="column"
min-width="150"
>
<!-- <template slot-scope="scope">
<span v-if="scope.row[column] === null" class="null-value">NULL</span>
<span v-else-if="typeof scope.row[column] === 'object'" class="json-value" @click="showJsonDetail(scope.row[column])">
{{ jsonPreview(scope.row[column]) }}
</span>
<span v-else>{{ scope.row[column] }}</span>
</template>-->
<template slot-scope="scope">
<span v-if="scope.row[index] === null" class="null-value">NULL</span>
<span v-else-if="typeof scope.row[index] === 'object'" class="json-value" @click="showJsonDetail(scope.row[index])">
{{ jsonPreview(scope.row[index]) }}
</span>
<span v-else>{{ scope.row[index] }}</span>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页控件 -->
<el-pagination
v-if="result.data.total > pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="result.data.total"
class="pagination"
>
</el-pagination>
</div>
<!-- 空结果 -->
<div v-else-if="result && result.success && result.data.rows.length === 0" class="no-data">
<i class="el-icon-inbox" style="font-size: 48px;"></i>
<p>未查询到数据</p>
</div>
<!-- 错误信息 -->
<div v-else-if="result && !result.success" class="error-container">
<div class="error-header">
<i class="el-icon-warning-outline" style="font-size: 20px;"></i>
<h3>SQL 执行错误</h3>
</div>
<div class="error-details">
{{ result.message }}
</div>
</div>
<!-- 初始状态 -->
<div v-else class="no-data">
<i class="el-icon-document" style="font-size: 48px;"></i>
<p>输入 SQL 并执行以查看结果</p>
</div>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<div class="footer">
<div class="copyright">
© {{ new Date().getFullYear() }} SQL执行工具 - 网页版数据库客户端
</div>
<div class="shortcuts">
<div class="shortcut-item">
<span class="key">Ctrl</span> + <span class="key">Enter</span> 执行查询
</div>
<div class="shortcut-item">
<span class="key">Ctrl</span> + <span class="key">/</span> 格式化 SQL
</div>
</div>
</div>
<!-- JSON 详情弹窗 -->
<el-dialog
title="JSON 详情"
:visible.sync="showJsonModal"
width="70%"
top="5vh"
>
<pre class="json-content">{{ formatJson(currentJson) }}</pre>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="showJsonModal = false">关 闭</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { fetchTables, executeSql } from '@/api/sqlUtil/sqlUtil';
export default {
name: 'SqlUtil',
data() {
return {
sqlQuery: '',
result: null,
loading: false,
tables: [],
selectedTable: null,
history: [],
showJsonModal: false,
currentJson: null,
connectionName: '生产',
currentPage: 1,
pageSize: 20
}
},
mounted() {
// 从本地存储加载历史记录
const savedHistory = localStorage.getItem('sqlHistory');
if (savedHistory) {
this.history = JSON.parse(savedHistory);
}
// 获取当前连接信息
this.getConnectionInfo();
// 获取数据库表
this.fetchDatabaseTables();
},
methods: {
async fetchDatabaseTables() {
try {
this.loading = true;
const response = await fetchTables();
this.tables = response.data || [];
if (this.tables.length > 0) {
this.selectedTable = this.tables[0];
this.sqlQuery = `SELECT * FROM ${this.selectedTable}`;
}
} catch (error) {
console.error('获取数据库表失败:', error);
this.$message.error('获取数据库表失败: ' + error.message);
} finally {
this.loading = false;
}
},
async executeSql() {
if (!this.sqlQuery.trim()) {
this.result = {
success: false,
message: 'SQL 语句不能为空'
};
return;
}
this.loading = true;
this.result = null;
try {
// 添加分页参数
const paginatedSql = this.addPagination(this.sqlQuery);
const response = await executeSql(paginatedSql);
this.result = response;
// 保存到历史记录
this.addToHistory(this.sqlQuery);
} catch (error) {
this.result = {
success: false,
message: `请求失败: ${error.message || error}`
};
} finally {
this.loading = false;
}
},
// 添加分页到SQL
addPagination(sql) {
// 如果是SELECT查询,添加分页
/*if (sql.trim().toUpperCase().startsWith('SELECT')) {
const offset = (this.currentPage - 1) * this.pageSize;
return `${sql} LIMIT ${offset}, ${this.pageSize}`;
}*/
return sql;
},
handleSizeChange(val) {
this.pageSize = val;
this.currentPage = 1;
if (this.sqlQuery.trim()) {
this.executeSql();
}
},
handleCurrentChange(val) {
this.currentPage = val;
if (this.sqlQuery.trim()) {
this.executeSql();
}
},
// 获取数据库连接信息
getConnectionInfo() {
// 这里可以根据实际情况从API获取或从配置读取
const env = process.env.NODE_ENV;
this.connectionName = env === 'production' ? '生产' :
env === 'staging' ? '预发布' : '开发';
},
formatSql() {
// 简单的 SQL 格式化
this.sqlQuery = this.sqlQuery
.replace(/\b(SELECT|FROM|WHERE|AND|OR|ORDER BY|GROUP BY|HAVING|INSERT|UPDATE|DELETE|JOIN|INNER JOIN|LEFT JOIN|RIGHT JOIN|ON|AS|LIMIT)\b/gi, '\n$1')
.replace(/,/g, ',\n')
.replace(/;/g, ';\n')
.replace(/\n\s+\n/g, '\n')
.trim();
},
clearEditor() {
this.sqlQuery = '';
this.result = null;
},
selectTable(table) {
this.selectedTable = table;
this.sqlQuery = `SELECT * FROM ${table} LIMIT 100`;
},
addToHistory(sql) {
// 避免重复添加
if (this.history.includes(sql)) return;
this.history.unshift(sql);
// 限制历史记录数量
if (this.history.length > 10) {
this.history.pop();
}
// 保存到本地存储
localStorage.setItem('sqlHistory', JSON.stringify(this.history));
},
loadHistoryQuery(sql) {
this.sqlQuery = sql;
},
jsonPreview(obj) {
try {
const str = JSON.stringify(obj);
return str.length > 50 ? str.substring(0, 47) + '...' : str;
} catch {
return '[Object]';
}
},
showJsonDetail(obj) {
this.currentJson = obj;
this.showJsonModal = true;
},
formatJson(obj) {
return JSON.stringify(obj, null, 2);
}
}
}
</script>
<style scoped>
.sql-executor-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 100%;
margin: 0 auto;
background-color: rgba(255, 255, 255, 0.95);
overflow: hidden;
}
header {
background: linear-gradient(90deg, #2c3e50, #4a6491);
color: white;
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.logo {
display: flex;
align-items: center;
gap: 15px;
}
.logo-icon {
font-size: 28px;
color: #42b983;
}
.logo h1 {
font-weight: 600;
font-size: 22px;
}
.logo p {
opacity: 0.8;
font-size: 13px;
margin-top: 4px;
}
.connection-info {
background: rgba(255, 255, 255, 0.1);
padding: 8px 15px;
border-radius: 20px;
font-size: 13px;
}
.main-content {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
.sidebar {
width: 260px;
background: #f8f9fa;
border-right: 1px solid #eaeaea;
padding: 15px 0;
overflow-y: auto;
flex-shrink: 0;
}
.sidebar-section {
margin-bottom: 20px;
}
.sidebar-section h3 {
padding: 0 20px 10px;
font-size: 16px;
color: #4a5568;
border-bottom: 1px solid #eaeaea;
margin-bottom: 12px;
}
.schema-item {
padding: 8px 20px 8px 35px;
cursor: pointer;
transition: all 0.2s;
position: relative;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.schema-item i {
font-size: 14px;
width: 20px;
}
.schema-item:hover {
background: #e9ecef;
}
.schema-item.active {
background: #e3f2fd;
color: #1a73e8;
font-weight: 500;
}
.history-item {
font-size: 13px;
color: #666;
}
.editor-container {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.sql-editor-container {
flex: 0 0 40%;
display: flex;
flex-direction: column;
border-bottom: 1px solid #eaeaea;
min-height: 200px;
overflow: hidden;
}
.sql-editor-header {
padding: 12px 20px;
background: #f1f3f4;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eaeaea;
flex-shrink: 0;
}
.sql-editor-header h2 {
font-size: 17px;
color: #2d3748;
}
.toolbar {
display: flex;
gap: 8px;
}
.toolbar-btn {
padding: 7px 12px;
border: none;
border-radius: 6px;
background: #4a6491;
color: white;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
font-size: 13px;
transition: all 0.2s;
}
.toolbar-btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.toolbar-btn i {
font-size: 13px;
}
.toolbar-btn.run {
background: #42b983;
}
.toolbar-btn.format {
background: #f0ad4e;
}
.toolbar-btn.clear {
background: #e74c3c;
}
.sql-editor {
flex: 1;
padding: 15px;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 14px;
line-height: 1.5;
border: none;
resize: none;
outline: none;
background: #fcfcfc;
min-height: 100px;
overflow: auto;
}
.result-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.result-header {
padding: 12px 20px;
background: #f1f3f4;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eaeaea;
flex-shrink: 0;
}
.result-header h2 {
font-size: 17px;
color: #2d3748;
}
.result-info {
display: flex;
align-items: center;
gap: 15px;
font-size: 13px;
}
.result-status {
padding: 4px 10px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-success {
background: #e8f5e9;
color: #2e7d32;
}
.status-error {
background: #ffebee;
color: #c62828;
}
.rows-count, .execution-time {
color: #5f6368;
display: flex;
align-items: center;
gap: 4px;
}
.result-content {
flex: 1;
overflow: auto;
position: relative;
min-height: 0;
}
.table-container {
display: flex;
flex-direction: column;
height: 100%;
}
.table-wrapper {
flex: 1;
overflow: auto;
min-height: 200px;
}
.el-table {
width: 100%;
min-width: 1000px;
}
.pagination {
padding: 10px 15px;
border-top: 1px solid #ebeef5;
background: #fff;
display: flex;
justify-content: flex-end;
}
.null-value {
color: #b0b0b0;
font-style: italic;
}
.json-value {
color: #d35400;
cursor: pointer;
text-decoration: underline dotted;
}
.no-data {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #909399;
font-size: 14px;
}
.error-container {
padding: 20px;
background-color: #ffebee;
border-radius: 8px;
margin: 20px;
color: #c62828;
}
.error-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.error-details {
font-family: monospace;
font-size: 14px;
white-space: pre-wrap;
background: rgba(255, 255, 255, 0.7);
padding: 15px;
border-radius: 6px;
margin-top: 10px;
overflow-x: auto;
}
.footer {
padding: 12px 30px;
background: #f8f9fa;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #6c757d;
flex-shrink: 0;
}
.shortcuts {
display: flex;
gap: 15px;
}
.shortcut-item {
display: flex;
align-items: center;
gap: 5px;
}
.key {
background: #e9ecef;
padding: 3px 8px;
border-radius: 4px;
font-weight: 500;
font-size: 11px;
}
.json-content {
background: #f8f8f8;
padding: 15px;
border-radius: 4px;
max-height: 70vh;
overflow: auto;
font-family: monospace;
line-height: 1.5;
font-size: 14px;
}
@media (max-width: 992px) {
.main-content {
flex-direction: column;
}
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid #eaeaea;
height: 200px;
overflow: auto;
}
.result-info {
flex-wrap: wrap;
gap: 8px;
}
}
@media (max-width: 768px) {
header {
flex-direction: column;
gap: 10px;
text-align: center;
}
.logo {
flex-direction: column;
gap: 5px;
}
.connection-info {
margin-top: 5px;
}
.toolbar {
flex-wrap: wrap;
justify-content: center;
}
.footer {
flex-direction: column;
gap: 10px;
}
}
</style>
js
import request from '@/utils/request'
export function executeSql(sql) {
return request({
url: '/sqlUtil/executeSql',
method: 'post',
data: { sql: sql }
})
}
export function fetchTables() {
return request({
url: '/sqlUtil/getTables',
method: 'get'
})
}
js
import {Message} from 'element-ui'
import axios from 'axios'
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=utf-8'
import store from "@/store";
import router from '@/router';
// create an axios instance
const service = axios.create({
baseURL: `http://127.0.0.1:8080`,
timeout: 75000,
})
export default service
效果:

总结
该SQL执行工具通过四大创新设计重塑了数据库交互体验:
- 元数据驱动:将数据库结构转化为可视化导航
- 上下文感知:自动识别SQL类型并优化执行
- 渐进式渲染:平衡大数据量与用户体验
- 历史时空隧道:完整记录操作轨迹