基于SpringBoot與Vue開發(fā)Web版SQL執(zhí)行工具
簡(jiǎn)介
本文介紹了一個(gè)基于Web的SQL執(zhí)行工具的設(shè)計(jì)與實(shí)現(xiàn),該工具允許用戶通過瀏覽器直接執(zhí)行SQL語句并查看結(jié)果。系統(tǒng)采用前后端分離架構(gòu),后端基于Spring Boot框架實(shí)現(xiàn)SQL執(zhí)行引擎,前端使用Vue.js構(gòu)建用戶界面。該系統(tǒng)提供了直觀的SQL編輯器、數(shù)據(jù)庫表管理、歷史查詢記錄和結(jié)果展示等功能,有效提高了數(shù)據(jù)庫操作效率。
(本質(zhì)就是客戶端不知為何連接不上服務(wù)器的數(shù)據(jù)庫,緊急情況下,手搓了一個(gè)sql 執(zhí)行器)
本文介紹的網(wǎng)頁版SQL執(zhí)行工具,通過整合Spring Boot后端與Vue前端,實(shí)現(xiàn)了數(shù)據(jù)庫管理的范式轉(zhuǎn)移,將復(fù)雜操作轉(zhuǎn)化為直觀的Web界面體驗(yàn)。
技術(shù)架構(gòu)解析
后端核心設(shè)計(jì):
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執(zhí)行錯(cuò)誤: {}", e.getMessage(), e);
return createErrorResponse("SQL執(zhí)行錯(cuò)誤: " + 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")) {
// 查詢操作 - 返回列和行數(shù)據(jù)
return handleQuery(sql, params);
} else {
// 更新操作 - 返回受影響行數(shù)
return handleUpdate(sql, params);
}
} catch (Exception e) {
logger.error("SQL執(zhí)行錯(cuò)誤: {}", e.getMessage(), e);
return createErrorResponse("SQL執(zhí)行錯(cuò)誤: " + e.getMessage());
}
}
/**
* 獲取數(shù)據(jù)庫所有表名
* @return 表名列表
*/
@GetMapping("/getTables")
public Map<String, Object> getDatabaseTables() {
try {
return jdbcTemplate.execute((ConnectionCallback<Map<String, Object>>) connection -> {
DatabaseMetaData metaData = connection.getMetaData();
// 獲取所有表名(不包含系統(tǒng)表)
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);
// 構(gòu)建響應(yīng)
Map<String, Object> response = new LinkedHashMap<>();
response.put("success", true);
response.put("data", tableNames);
return response;
});
} catch (Exception e) {
logger.error("獲取數(shù)據(jù)庫表失敗: {}", e.getMessage(), e);
Map<String, Object> errorResponse = new LinkedHashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "獲取數(shù)據(jù)庫表失敗: " + 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));
}
// 獲取行數(shù)據(jù)
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);
}
// 構(gòu)建響應(yīng)
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;
}
// 創(chuàng)建成功響應(yīng)
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;
}
// 創(chuàng)建錯(cuò)誤響應(yīng)
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;
}
}
}
后端采用策略模式實(shí)現(xiàn)SQL路由,自動(dòng)區(qū)分查詢與更新操作。通過JDBC元數(shù)據(jù)接口實(shí)現(xiàn)數(shù)據(jù)庫自發(fā)現(xiàn)能力,為前端提供結(jié)構(gòu)化數(shù)據(jù)支撐。
前端創(chuàng)新交互:
<template>
<!-- 雙面板設(shè)計(jì) -->
<div class="editor-container">
<!-- 元數(shù)據(jù)導(dǎo)航區(qū) -->
<div class="sidebar">
<div v-for="table in tables" @click="selectTable(table)">
<i class="fas fa-table"></i> {{ table }}
</div>
</div>
<!-- SQL工作區(qū) -->
<textarea
v-model="sqlQuery"
@keydown.ctrl.enter="executeSql"></textarea>
<!-- 智能結(jié)果渲染 -->
<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>
前端實(shí)現(xiàn)三區(qū)域布局:元數(shù)據(jù)導(dǎo)航、SQL編輯器和智能結(jié)果面板。采用動(dòng)態(tài)類型檢測(cè)技術(shù),對(duì)NULL值、JSON對(duì)象等特殊數(shù)據(jù)類型進(jìn)行可視化區(qū)分處理。
核心技術(shù)亮點(diǎn)
實(shí)時(shí)元數(shù)據(jù)發(fā)現(xiàn)
- 自動(dòng)加載數(shù)據(jù)庫表結(jié)構(gòu)
- 表名智能排序與即時(shí)搜索
- 點(diǎn)擊表名自動(dòng)生成SELECT模板
智能SQL處理
// SQL執(zhí)行核心邏輯
async executeSql() {
const paginatedSql = this.addPagination(this.sqlQuery);
const response = await executeSql(paginatedSql);
// 高級(jí)結(jié)果處理
if (response.success) {
this.result = {
...response,
data: {
...response.data,
executionTime: Date.now() - startTime
}
}
}
}
- 自動(dòng)分頁機(jī)制
- 執(zhí)行耗時(shí)精準(zhǔn)統(tǒng)計(jì)
- 語法錯(cuò)誤實(shí)時(shí)反饋
歷史版本管理
// 歷史記錄管理
addToHistory(sql) {
if (this.history.includes(sql)) return;
this.history.unshift(sql);
localStorage.setItem('sqlHistory', JSON.stringify(this.history));
}
本地存儲(chǔ)自動(dòng)持久化
智能去重機(jī)制
一鍵恢復(fù)歷史查詢
數(shù)據(jù)可視化增強(qiáng)
- JSON對(duì)象折疊預(yù)覽
- NULL值特殊標(biāo)識(shí)
- 分頁控件動(dòng)態(tài)加載
性能優(yōu)化策略
查詢優(yōu)化
- 自動(dòng)追加LIMIT子句
- 分頁查詢按需加載
- 結(jié)果集流式處理
緩存機(jī)制
// 表結(jié)構(gòu)緩存
async fetchDatabaseTables() {
if (this.cachedTables) return this.cachedTables;
const response = await fetchTables();
this.cachedTables = response.data;
}
完整vue代碼:
<template>
<div class="sql-executor-container">
<!-- 頭部 -->
<header>
<div class="logo">
<i class="fas fa-database logo-icon"></i>
<div>
<h1>SQL 執(zhí)行工具</h1>
<p>網(wǎng)頁版數(shù)據(jù)庫客戶端工具</p>
</div>
</div>
<div class="connection-info">
<i class="fas fa-plug"></i> 已連接到 {{ connectionName }} 數(shù)據(jù)庫
</div>
</header>
<!-- 主體內(nèi)容 -->
<div class="main-content">
<!-- 側(cè)邊欄 -->
<div class="sidebar">
<div class="sidebar-section">
<h3>數(shù)據(jù)庫表</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>
<!-- 編輯器區(qū)域 -->
<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> 執(zhí)行
</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>
<!-- 結(jié)果區(qū)域 -->
<div class="result-container">
<div class="result-header">
<h2>執(zhí)行結(jié)果</h2>
<div class="result-info">
<div
class="result-status"
:class="result.success ? 'status-success' : 'status-error'"
v-if="result"
>
{{ result.success ? '執(zhí)行成功' : '執(zhí)行失敗' }}
</div>
<div class="rows-count" v-if="result && result.success">
共 {{ result.data.total }} 行數(shù)據(jù) (顯示 {{ 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">
<!-- 加載狀態(tài) -->
<div v-if="loading" class="no-data">
<i class="el-icon-loading" style="font-size: 48px;"></i>
<p>正在執(zhí)行查詢...</p>
</div>
<!-- 查詢結(jié)果 -->
<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>
<!-- 空結(jié)果 -->
<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>未查詢到數(shù)據(jù)</p>
</div>
<!-- 錯(cuò)誤信息 -->
<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 執(zhí)行錯(cuò)誤</h3>
</div>
<div class="error-details">
{{ result.message }}
</div>
</div>
<!-- 初始狀態(tài) -->
<div v-else class="no-data">
<i class="el-icon-document" style="font-size: 48px;"></i>
<p>輸入 SQL 并執(zhí)行以查看結(jié)果</p>
</div>
</div>
</div>
</div>
</div>
<!-- 頁腳 -->
<div class="footer">
<div class="copyright">
? {{ new Date().getFullYear() }} SQL執(zhí)行工具 - 網(wǎng)頁版數(shù)據(jù)庫客戶端
</div>
<div class="shortcuts">
<div class="shortcut-item">
<span class="key">Ctrl</span> + <span class="key">Enter</span> 執(zhí)行查詢
</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">關(guān) 閉</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: '生產(chǎn)',
currentPage: 1,
pageSize: 20
}
},
mounted() {
// 從本地存儲(chǔ)加載歷史記錄
const savedHistory = localStorage.getItem('sqlHistory');
if (savedHistory) {
this.history = JSON.parse(savedHistory);
}
// 獲取當(dāng)前連接信息
this.getConnectionInfo();
// 獲取數(shù)據(jù)庫表
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('獲取數(shù)據(jù)庫表失敗:', error);
this.$message.error('獲取數(shù)據(jù)庫表失敗: ' + 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 {
// 添加分頁參數(shù)
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: `請(qǐng)求失敗: ${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();
}
},
// 獲取數(shù)據(jù)庫連接信息
getConnectionInfo() {
// 這里可以根據(jù)實(shí)際情況從API獲取或從配置讀取
const env = process.env.NODE_ENV;
this.connectionName = env === 'production' ? '生產(chǎn)' :
env === 'staging' ? '預(yù)發(fā)布' : '開發(fā)';
},
formatSql() {
// 簡(jiǎn)單的 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) {
// 避免重復(fù)添加
if (this.history.includes(sql)) return;
this.history.unshift(sql);
// 限制歷史記錄數(shù)量
if (this.history.length > 10) {
this.history.pop();
}
// 保存到本地存儲(chǔ)
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>
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'
})
}
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
效果:

總結(jié)
該SQL執(zhí)行工具通過四大創(chuàng)新設(shè)計(jì)重塑了數(shù)據(jù)庫交互體驗(yàn):
- 元數(shù)據(jù)驅(qū)動(dòng):將數(shù)據(jù)庫結(jié)構(gòu)轉(zhuǎn)化為可視化導(dǎo)航
- 上下文感知:自動(dòng)識(shí)別SQL類型并優(yōu)化執(zhí)行
- 漸進(jìn)式渲染:平衡大數(shù)據(jù)量與用戶體驗(yàn)
- 歷史時(shí)空隧道:完整記錄操作軌跡
到此這篇關(guān)于基于SpringBoot與Vue開發(fā)Web版SQL執(zhí)行工具的文章就介紹到這了,更多相關(guān)SpringBoot開發(fā)SQL執(zhí)行工具內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java反射機(jī)制詳解_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Java 反射機(jī)制。通俗來講呢,就是在運(yùn)行狀態(tài)中,我們可以根據(jù)“類的部分已經(jīng)的信息”來還原“類的全部的信息”。這篇文章給大家詳細(xì)介紹了java反射機(jī)制的知識(shí),感興趣的朋友一起看看吧2017-06-06
Javaweb中使用Servlet編寫簡(jiǎn)單的接口案例詳解
文章介紹了如何使用Servlet編寫一個(gè)簡(jiǎn)單的接口來校驗(yàn)用戶提交的密碼長(zhǎng)度是否在6到12位之間,代碼分為后端部分和前端部分,給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2025-02-02
Java中CaffeineCache自定義緩存時(shí)間的實(shí)現(xiàn)
本文主要介紹了Java中CaffeineCache自定義緩存時(shí)間的實(shí)現(xiàn),通過聲明緩存value值holder對(duì)象并創(chuàng)建緩存容器,可以為不同的key值指定不同的過期時(shí)間,具有一定的參考價(jià)值,感興趣的可以了解一下2025-02-02
聊聊SpringBoot使用Nacos進(jìn)行服務(wù)注冊(cè)發(fā)現(xiàn)與配置管理問題
Nacos支持基于DNS和基于RPC的服務(wù)發(fā)現(xiàn)(可以作為springcloud的注冊(cè)中心)、動(dòng)態(tài)配置服務(wù)(可以做配置中心)、動(dòng)態(tài)?DNS?服務(wù)。本文重點(diǎn)給大家介紹SpringBoot使用Nacos進(jìn)行服務(wù)注冊(cè)發(fā)現(xiàn)與配置管理,感興趣的朋友一起看看吧2022-01-01
SpringBoot項(xiàng)目導(dǎo)出jar包及瘦身部署方式
今天項(xiàng)目要求Nginx+jar包運(yùn)行多個(gè)項(xiàng)目,在此記錄一下部署的過程,其中借鑒了好多網(wǎng)上前輩的經(jīng)驗(yàn),感謝各位的無私分享2024-07-07
SpringBoot接入釘釘自定義機(jī)器人預(yù)警通知
本文主要介紹了SpringBoot接入釘釘自定義機(jī)器人預(yù)警通知,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07

