修改数据库管理交换

This commit is contained in:
lj124 2026-05-12 23:52:25 +08:00
parent 99dc835233
commit 7890bca4e2
2 changed files with 604 additions and 398 deletions

View File

@ -1,185 +1,211 @@
<template>
<div class="db-manage-container">
<el-row :gutter="20">
<!-- 右侧主内容区 -->
<el-col :span="24">
<!-- 顶部下拉选择框 -->
<el-row :gutter="20" class="mb-20">
<el-col :span="10">
<el-select
v-model="selectedDatabase"
filterable
placeholder="请选择数据库"
class="w-100"
@change="getRunSQLHistory"
<el-card class="header-card" shadow="never">
<el-row :gutter="20" align="middle">
<el-col :span="8">
<el-select
v-model="selectedDatabase"
filterable
clearable
placeholder="请选择数据库"
class="w-100"
@change="getRunSQLHistory"
>
<template #prefix>
<el-icon><Folder /></el-icon>
</template>
<el-option
v-for="item in databases"
:key="item.ID"
:label="item.Name"
:value="item.ID"
>
<template #prefix>
<el-icon><search /></el-icon>
</template>
<el-option
v-for="item in databases"
:key="item.ID"
:label="item.Name"
:value="item.ID"
>
<span>{{ item.Name }}</span>
<div class="database-option">
<span class="db-name">{{ item.Name }}</span>
<span class="option-actions">
<el-icon @click.stop="editDatabase(item)"><edit /></el-icon>
<el-icon @click.stop="deleteDatabase(item)"
><delete
/></el-icon>
<el-tooltip content="编辑" placement="top">
<el-icon @click.stop="editDatabase(item)"><Edit /></el-icon>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-icon class="delete-icon" @click.stop="deleteDatabase(item)"><Delete /></el-icon>
</el-tooltip>
</span>
</el-option>
<template #append>
<el-button icon="plus" @click="showAddDatabaseDialog" />
</template>
</el-select>
</el-col>
<el-col :span="1">
<el-icon @click="showAddDatabaseDialog"><FolderAdd /></el-icon>
</el-col>
<el-col :span="6">
<el-select
v-model="selectedConnection"
filterable
placeholder="请选择服务器"
class="w-100"
</div>
</el-option>
<template #append>
<el-button icon="Plus" @click="showAddDatabaseDialog" />
</template>
</el-select>
</el-col>
<el-col :span="6">
<el-select
v-model="selectedConnection"
filterable
clearable
placeholder="请选择服务器"
class="w-100"
>
<template #prefix>
<el-icon><Monitor /></el-icon>
</template>
<el-option
v-for="item in connections"
:key="item.domain"
:label="item.name"
:value="item.domain"
>
<template #prefix>
<el-icon><search /></el-icon>
</template>
<el-option
v-for="item in connections"
:key="item.domain"
:label="item.name"
:value="item.domain"
>
<span>{{ item.name }}</span>
</el-option>
<template #append>
<el-button circle @click="showAddDatabaseDialog" />
</template>
</el-select>
</el-col>
<el-col :span="3">
<el-button
type="primary"
@click="showSQLHistoryDialog"
class="w-100"
:loading="executing"
>执行历史SQL</el-button
>
</el-col>
</el-row>
<span>{{ item.name }}</span>
</el-option>
</el-select>
</el-col>
<el-col :span="4">
<el-button
type="primary"
@click="showSQLHistoryDialog"
class="w-100"
>
<el-icon><Document /></el-icon>
执行历史
</el-button>
</el-col>
<el-col :span="4">
<el-button
type="success"
@click="showAddDatabaseDialog"
class="w-100"
>
<el-icon><Plus /></el-icon>
新建数据库
</el-button>
</el-col>
</el-row>
</el-card>
<!-- SQL输入框和执行按钮 -->
<el-row class="mb-20">
<el-col :span="20">
<el-input
v-model="sqlQuery"
type="textarea"
:rows="3"
placeholder="请输入SQL语句"
/>
<!-- <SqlEditor v-model="sqlQuery" /> -->
</el-col>
<el-col :span="4">
<el-button
type="primary"
@click="executeSql"
class="w-100"
:disabled="executing"
>
执行
<el-card class="sql-card" shadow="never">
<template #header>
<div class="card-header">
<span><el-icon><EditPen /></el-icon> SQL </span>
<div class="header-actions">
<el-button text @click="clearSql">
<el-icon><Delete /></el-icon>
</el-button>
</el-col>
</el-row>
<!-- 结果表格 -->
<el-table
:data="tableData"
style="width: 100%"
height="400px"
v-horizontal-scroll="'always'"
v-loading="loading"
<el-button text @click="formatSql" v-if="false">
<el-icon><Operation /></el-icon>
</el-button>
</div>
</div>
</template>
<SqlEditor
v-model="sqlQuery"
@execute="executeSql"
/>
<div class="sql-actions">
<el-button
type="primary"
size="large"
@click="executeSql"
:loading="executing"
:disabled="!sqlQuery.trim()"
>
<el-table-column
v-for="column in tableColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
/>
</el-table>
</el-col>
</el-row>
<el-icon><VideoPlay /></el-icon>
执行 SQL
</el-button>
</div>
</el-card>
<el-card class="result-card" shadow="never" v-loading="loading">
<template #header>
<div class="card-header">
<span><el-icon><Tickets /></el-icon> </span>
<div class="header-actions">
<el-button text @click="exportData" v-if="tableData.length > 0">
<el-icon><Download /></el-icon>
</el-button>
</div>
</div>
</template>
<el-table
:data="tableData"
stripe
border
height="400"
style="width: 100%"
>
<el-table-column
v-for="column in tableColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
show-overflow-tooltip
/>
<template #empty>
<el-empty description="暂无数据,请先执行 SQL 查询" />
</template>
</el-table>
</el-card>
<!-- 添加/编辑数据库对话框 -->
<el-dialog
v-model="databaseDialogVisible"
:title="isEditDatabase ? '编辑数据库' : '添加数据库'"
width="50%"
width="600px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form :model="databaseForm" label-width="100px">
<el-form-item label="数据库名称" required>
<el-input
v-model="databaseForm.DB_NAME"
placeholder="请输入数据库名称"
/>
</el-form-item>
</el-form>
<el-form :model="databaseForm" label-width="100px">
<el-form-item label="数据库密码" required>
<el-input
v-model="databaseForm.DB_Password"
type="password"
placeholder="请输入数据库密码"
show-password
/>
</el-form-item>
</el-form>
<el-form :model="databaseForm" label-width="100px">
<el-form-item label="用户名" required>
<el-input
v-model="databaseForm.DB_User"
placeholder="请输入用户名称"
/>
</el-form-item>
</el-form>
<el-form :model="databaseForm" label-width="100px">
<el-form-item label="IP或域名" required>
<el-input
v-model="databaseForm.DB_IP"
placeholder="请输入数据库ip或域名"
/>
</el-form-item>
</el-form>
<el-form :model="databaseForm" label-width="100px">
<el-form-item label="数据库端口" required>
<el-input
v-model="databaseForm.DB_Port"
placeholder="请输入数据库端口"
/>
</el-form-item>
</el-form>
<el-form :model="databaseForm" label-width="100px">
<el-form-item label="数据库类型" required>
<el-select
v-model="databaseForm.DB_Type"
placeholder="请选择数据库类型"
>
<el-option label="MySQL" value="0" />
<el-option label="PostgreSQL" value="1" />
</el-select>
</el-form-item>
</el-form>
<el-form :model="databaseForm" label-width="100px">
<el-form-item label="描述信息">
<el-form
ref="databaseFormRef"
:model="databaseForm"
:rules="databaseRules"
label-width="100px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="数据库名称" prop="DB_NAME">
<el-input v-model="databaseForm.DB_NAME" placeholder="请输入数据库名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="数据库类型" prop="DB_Type">
<el-select v-model="databaseForm.DB_Type" placeholder="请选择数据库类型" class="w-100">
<el-option label="MySQL" :value="0" />
<el-option label="PostgreSQL" :value="1" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="IP或域名" prop="DB_IP">
<el-input v-model="databaseForm.DB_IP" placeholder="请输入数据库 IP 或域名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="端口" prop="DB_Port">
<el-input v-model="databaseForm.DB_Port" placeholder="请输入数据库端口" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="用户名" prop="DB_User">
<el-input v-model="databaseForm.DB_User" placeholder="请输入用户名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="密码" prop="DB_Password">
<el-input
v-model="databaseForm.DB_Password"
type="password"
show-password
placeholder="请输入密码"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="描述信息" prop="DB_Desc">
<el-input
v-model="databaseForm.DB_Desc"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</el-form-item>
@ -192,32 +218,35 @@
<el-dialog
v-model="sqlHistoryDialogVisible"
title="SQL执行历史"
width="50%"
title="SQL 执行历史"
width="800px"
:close-on-click-modal="false"
>
<el-table :data="sqlHistory" stripe width="100%" height="600px" fit>
<el-table-column prop="ID" label="SQLID" width="150"></el-table-column>
<el-table-column prop="SQL" label="SQL" width="500"> </el-table-column>
<el-table-column label="操作" width="270">
<template #default="scope">
<el-button
type="primary"
size="mini"
@click.prevent="useCurrSql(scope.$index)"
>引用</el-button
>
<el-button
type="primary"
size="mini"
@click.prevent="DelSqlHistory(scope.$index)"
>删除</el-button
>
<el-table :data="sqlHistory" stripe height="500">
<el-table-column prop="ID" label="ID" width="80" />
<el-table-column prop="SQL" label="SQL 语句" min-width="300" show-overflow-tooltip>
<template #default="{ row }">
<el-tooltip placement="top" :disabled="row.SQL.length <= 50">
<template #content>
<div class="sql-tooltip">{{ row.SQL }}</div>
</template>
<span class="sql-content">{{ row.SQL }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row, $index }">
<el-button link type="primary" size="small" @click="useCurrSql($index)">
<el-icon><RefreshLeft /></el-icon>
</el-button>
<el-button link type="danger" size="small" @click="DelSqlHistory($index)">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="sqlHistoryDialogVisible = false">取消</el-button>
<el-button @click="sqlHistoryDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
@ -233,15 +262,28 @@ import {
GetSQLRunHistoryService,
} from "@/api/dbm";
import { DatabaseConfig, ISQLOperation } from "@/types/dbm";
import { Search, Edit, Delete, Plus } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import {
Edit,
Delete,
Plus,
Monitor,
Document,
EditPen,
Tickets,
VideoPlay,
RefreshLeft,
Download,
Operation,
Folder,
} from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
import SqlEditor from './sqlEditor.vue';
//
const databaseFormRef = ref<FormInstance>();
const databases = ref<DatabaseConfig[]>([]);
const selectedDatabase = ref("");
//
const connections = ref([
{ name: "腾讯服务器", domain: "tx.ljsea.top" },
{ name: "阿里云服务器", domain: "al.ljsea.top" },
@ -250,41 +292,32 @@ const connections = ref([
const supportdedDBTypes = ref({ 0: "mysql", 1: "postgresql" });
const selectedConnection = ref("");
// SQL
const sqlQuery = ref("");
const executing = ref(false);
const tableData = ref([]);
const tableColumns = ref([]);
const tableData = ref<any[]>([]);
const tableColumns = ref<any[]>([]);
const loading = ref(false);
//
//
const databaseDialogVisible = ref(false);
const sqlHistoryDialogVisible = ref(false);
const isEditDatabase = ref(false);
const databaseForm = ref<DatabaseConfig>();
const sqlHistory = ref<ISQLOperation[]>([]);
//
const connectionDialogVisible = ref(false);
const isEditConnection = ref(false);
const connectionForm = reactive({
id: 0,
name: "",
host: "",
port: "",
username: "",
password: "",
const databaseRules = reactive<FormRules<DatabaseConfig>>({
DB_NAME: [{ required: true, message: "请输入数据库名称", trigger: "blur" }],
DB_IP: [{ required: true, message: "请输入 IP 或域名", trigger: "blur" }],
DB_Port: [{ required: true, message: "请输入端口", trigger: "blur" }],
DB_User: [{ required: true, message: "请输入用户名", trigger: "blur" }],
DB_Password: [{ required: true, message: "请输入密码", trigger: "blur" }],
DB_Type: [{ required: true, message: "请选择数据库类型", trigger: "change" }],
});
const showSQLHistoryDialog = async () => {
sqlHistoryDialogVisible.value = true;
console.log('SQL history dialog opened:', sqlHistoryDialogVisible.value);
await getRunSQLHistory();
};
//
const showAddDatabaseDialog = () => {
isEditDatabase.value = false;
databaseForm.value = {
@ -306,13 +339,85 @@ const showAddDatabaseDialog = () => {
databaseDialogVisible.value = true;
};
const editDatabase = (item) => {
const editDatabase = (item: DatabaseConfig) => {
isEditDatabase.value = true;
console.log("editDatabase:", item);
databaseForm.value = item;
databaseForm.value = { ...item };
databaseDialogVisible.value = true;
};
const deleteDatabase = async (item: DatabaseConfig) => {
try {
await ElMessageBox.confirm(`确认删除数据库 "${item.Name}"`, "提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning",
});
databases.value = databases.value.filter((db) => db.ID !== item.ID);
ElMessage.success("删除成功");
} catch {
//
}
};
const saveDatabase = async () => {
if (!databaseFormRef.value) return;
await databaseFormRef.value.validate(async (valid) => {
if (!valid) return;
let req = {};
if (isEditDatabase.value) {
req = {
token: localStorage.getItem("token"),
db_id: databaseForm.value!.ID,
db_name: databaseForm.value!.DB_NAME,
db_type: databaseForm.value!.DB_Type,
db_ip: databaseForm.value!.DB_IP,
db_port: databaseForm.value!.DB_Port,
db_user: databaseForm.value!.DB_User,
db_password: databaseForm.value!.DB_Password,
db_desc: databaseForm.value!.DB_Desc,
};
await UpdateDBManageService(req)
.then((res: any) => {
if (res.code === 0) {
ElMessage.success("更新成功");
GetDBManageList();
} else {
ElMessage.error("更新失败");
}
})
.catch((error: any) => {
console.error("请求错误:", error);
});
} else {
req = {
token: localStorage.getItem("token"),
db_id: databaseForm.value!.ID,
db_name: databaseForm.value!.DB_NAME,
db_type: databaseForm.value!.DB_Type,
db_ip: databaseForm.value!.DB_IP,
db_port: databaseForm.value!.DB_Port,
db_user: databaseForm.value!.DB_User,
db_password: databaseForm.value!.DB_Password,
db_desc: databaseForm.value!.DB_Desc,
};
await AddDBManageService(req)
.then((res: any) => {
if (res.code === 0) {
ElMessage.success("添加成功");
GetDBManageList();
} else {
ElMessage.error("添加失败");
}
})
.catch((error: any) => {
console.error("请求错误:", error);
});
}
databaseDialogVisible.value = false;
});
};
const GetDBManageList = async () => {
let req = {
get_type: 0,
@ -334,16 +439,6 @@ const GetDBManageList = async () => {
" - " +
databases.value[i].DB_NAME;
}
ElMessage.success("获取数据库列表成功");
console.log("获取数据库列表成功:", databases.value);
// console.log(":", res.data);
// console.log(":", res.data);
// console.log(":", res.data);
} else {
console.error("获取数据库列表失败:", res.message);
}
})
.catch((error: any) => {
@ -352,89 +447,28 @@ const GetDBManageList = async () => {
};
GetDBManageList();
const useCurrSql = (index) => {
const useCurrSql = (index: number) => {
sqlQuery.value = sqlHistory.value[index].SQL;
sqlHistoryDialogVisible.value = false;
};
const DelSqlHistory = (index) => {
sqlHistory.value.splice(index, 1);
};
const deleteDatabase = (item) => {
// API
databases.value = databases.value.filter((db) => db.ID !== item.id);
};
const saveDatabase = () => {
let req = {};
if (isEditDatabase.value) {
//
// const index = databases.value.findIndex(db => db.ID === databaseForm.id);
// if (index !== -1) {
// databases.value[index].Name = databaseForm.name;
// }]
req = {
token: localStorage.getItem("token"),
db_id: databaseForm.value.ID,
db_name: databaseForm.value.DB_NAME,
db_type: databaseForm.value.DB_Type,
db_ip: databaseForm.value.DB_IP,
db_port: databaseForm.value.DB_Port,
db_user: databaseForm.value.DB_User,
db_password: databaseForm.value.DB_Password,
db_desc: databaseForm.value.DB_Desc,
};
UpdateDBManageService(req)
.then((res: any) => {
if (res.code === 0) {
ElMessage.success("更新成功");
GetDBManageList();
} else {
ElMessage.error("更新失败");
}
})
.catch((error: any) => {
console.error("请求错误:", error);
});
} else {
//
// databases.value.push({
// id: databases.value.length + 1,
// name: databaseForm.name,
// });
req = {
token: localStorage.getItem("token"),
db_id: databaseForm.value.ID,
db_name: databaseForm.value.DB_NAME,
db_type: databaseForm.value.DB_Type,
db_ip: databaseForm.value.DB_IP,
db_port: databaseForm.value.DB_Port,
db_user: databaseForm.value.DB_User,
db_password: databaseForm.value.DB_Password,
db_desc: databaseForm.value.DB_Desc,
};
AddDBManageService(req)
.then((res: any) => {
if (res.code === 0) {
ElMessage.success("添加成功");
GetDBManageList();
} else {
ElMessage.error("添加失败");
}
})
.catch((error: any) => {
console.error("请求错误:", error);
});
const DelSqlHistory = async (index: number) => {
try {
await ElMessageBox.confirm("确认删除这条 SQL 记录?", "提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning",
});
sqlHistory.value.splice(index, 1);
ElMessage.success("删除成功");
} catch {
//
}
databaseDialogVisible.value = false;
};
const getRunSQLHistory = async () => {
if (!selectedDatabase.value) {
ElMessage({
type: "error",
message: "请选择数据库",
});
ElMessage.error("请先选择数据库");
return;
}
let req = {
@ -446,12 +480,6 @@ const getRunSQLHistory = async () => {
.then((res: any) => {
if (res.code === 0) {
sqlHistory.value = res.data;
ElMessage({
type: "success",
message: "获取SQL执行历史成功",
});
} else {
console.error("获取SQL执行历史失败:", res.message);
}
})
.catch((error: any) => {
@ -459,12 +487,13 @@ const getRunSQLHistory = async () => {
});
};
const executeSql = () => {
const executeSql = async () => {
if (!selectedDatabase.value) {
ElMessage({
type: "error",
message: "请选择数据库",
});
ElMessage.error("请先选择数据库");
return;
}
if (!sqlQuery.value.trim()) {
ElMessage.warning("请输入 SQL 语句");
return;
}
executing.value = true;
@ -477,70 +506,148 @@ const executeSql = () => {
db_id: selectedDatabase.value,
};
try {
RunSQLService(req)
await RunSQLService(req)
.then((res: any) => {
if (res.code === 0) {
tableData.value = res.data.Rows;
tableColumns.value = res.data.Columns;
// let keys = Object.keys(res.data.Rows[0]);
//console.log( 'columns:',tableColumns.value);
// tableColumns.value = res.columns;
// tableColumns.value = keys.map((key) => {
// return { prop: key, label: key };
// });
// console.log(res.data);
ElMessage({
type: "success",
message: "执行SQL成功",
});
tableData.value = res.data.Rows || [];
tableColumns.value = res.data.Columns || [];
ElMessage.success("执行 SQL 成功");
} else {
console.error("执行SQL失败:", res.message);
ElMessage({
type: "error",
message: res.message,
});
ElMessage.error(res.message || "执行 SQL 失败");
}
})
.catch((error: any) => {
console.error("请求错误:", error);
ElMessage.error("请求失败");
});
} catch (e) {
console.log(e);
} finally {
executing.value = false;
loading.value = false;
}
executing.value = false;
loading.value = false;
};
const clearSql = () => {
sqlQuery.value = "";
};
const formatSql = () => {
ElMessage.info("格式化功能开发中");
};
const exportData = () => {
if (tableData.value.length === 0) return;
try {
const headers = tableColumns.value.map((col) => col.label).join(",");
const csvContent =
headers +
"\n" +
tableData.value
.map((row) =>
tableColumns.value.map((col) => JSON.stringify(row[col.prop] ?? "")).join(",")
)
.join("\n");
const blob = new Blob(["" + csvContent], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = `query_result_${Date.now()}.csv`;
link.click();
ElMessage.success("导出成功");
} catch (e) {
ElMessage.error("导出失败");
}
};
</script>
<style scoped>
.db-manage-container {
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.table-list {
background: #fff;
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.header-card,
.sql-card,
.result-card {
border-radius: 8px;
}
.mb-20 {
margin-bottom: 20px;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
}
.header-actions {
display: flex;
gap: 8px;
}
.w-100 {
width: 100%;
}
.database-option {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.database-option .db-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.option-actions {
float: right;
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.2s;
}
.el-option:hover .option-actions {
opacity: 1;
}
.option-actions .el-icon {
margin-left: 10px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
}
.option-actions .el-icon:hover {
background-color: var(--el-fill-color-light);
}
.option-actions .delete-icon:hover {
color: var(--el-color-danger);
}
.sql-actions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.sql-content {
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-all;
}
.sql-tooltip {
max-width: 600px;
white-space: pre-wrap;
word-break: break-all;
}
</style>

View File

@ -1,88 +1,187 @@
<template>
<div ref="editorContainer" class="sql-editor-container" />
<codemirror
v-model="code"
:placeholder="placeholder"
:style="{ height: '200px' }"
:autofocus="false"
:indent-with-tab="true"
:tab-size="2"
:extensions="extensions"
@keydown="handleKeydown"
@update:value="handleChange"
/>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, type Ref } from 'vue';
import { EditorState, EditorView, basicSetup } from '@codemirror/basic-setup';
import { sql } from '@codemirror/lang-sql';
import { oneDark } from '@codemirror/theme-one-dark'; //
import { ref, computed, watch } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { sql, MySQL } from '@codemirror/lang-sql';
import { keymap } from '@codemirror/view';
import { autocompletion, completeFromList } from '@codemirror/autocomplete';
import { basicSetup } from 'codemirror';
const sqlKeywords = [
'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'INSERT', 'INTO', 'VALUES',
'UPDATE', 'SET', 'DELETE', 'CREATE', 'TABLE', 'ALTER', 'DROP', 'TRUNCATE',
'DATABASE', 'USE', 'INDEX', 'VIEW', 'PROCEDURE', 'FUNCTION', 'RETURN',
'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'FULL', 'ON', 'GROUP', 'BY',
'HAVING', 'ORDER', 'ASC', 'DESC', 'LIMIT', 'OFFSET', 'DISTINCT', 'AS',
'UNION', 'ALL', 'EXCEPT', 'INTERSECT', 'IN', 'EXISTS', 'BETWEEN', 'LIKE',
'IS', 'NULL', 'TRUE', 'FALSE', 'DEFAULT', 'PRIMARY', 'KEY', 'FOREIGN',
'REFERENCES', 'CHECK', 'CONSTRAINT', 'UNIQUE', 'AUTO_INCREMENT', 'SERIAL',
'CASCADE', 'RESTRICT', 'NO', 'ACTION', 'WITH', 'TIME', 'ZONE', 'PARTITION',
'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'MERGE', 'UPSERT', 'REPLACE',
'BEGIN', 'COMMIT', 'ROLLBACK', 'TRANSACTION', 'SAVEPOINT', 'LOCK',
'GRANT', 'REVOKE', 'DENY', 'EXEC', 'EXECUTE', 'DECLARE', 'SET',
'IF', 'ELSE', 'CASE', 'WHEN', 'THEN', 'END', 'WHILE', 'FOR', 'LOOP',
'CURSOR', 'OPEN', 'FETCH', 'CLOSE', 'DEALLOCATE', 'PREPARE', 'EXECUTE',
'SHOW', 'DESCRIBE', 'DESC', 'EXPLAIN', 'USE', 'HELP', 'SOURCE',
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'ROUND', 'FLOOR', 'CEIL', 'CEILING',
'ABS', 'MOD', 'POWER', 'SQRT', 'EXP', 'LOG', 'LOG10', 'LN', 'RAND',
'CONCAT', 'SUBSTRING', 'LEFT', 'RIGHT', 'TRIM', 'LTRIM', 'RTRIM',
'UPPER', 'LOWER', 'INITCAP', 'LENGTH', 'CHAR_LENGTH', 'CHARACTER_LENGTH',
'POSITION', 'STRPOS', 'INSTR', 'LOCATE', 'REPLACE', 'TRANSLATE',
'DATE', 'TIME', 'DATETIME', 'TIMESTAMP', 'YEAR', 'MONTH', 'DAY',
'HOUR', 'MINUTE', 'SECOND', 'NOW', 'CURDATE', 'CURRENT_DATE',
'CURTIME', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'DATE_ADD', 'DATE_SUB',
'DATEDIFF', 'TIMESTAMPDIFF', 'DATE_FORMAT', 'STR_TO_DATE',
'CAST', 'CONVERT', 'COALESCE', 'NULLIF', 'IFNULL', 'ISNULL',
'FIRST', 'LAST', 'FIRST_VALUE', 'LAST_VALUE', 'NTH_VALUE',
'ROW_NUMBER', 'RANK', 'DENSE_RANK', 'PERCENT_RANK', 'CUME_DIST',
'NTILE', 'LEAD', 'LAG', 'PERCENTILE_CONT', 'PERCENTILE_DISC',
'ARRAY', 'JSON', 'XML', 'GEOMETRY', 'POINT', 'LINESTRING', 'POLYGON',
'BOOLEAN', 'BOOL', 'INT', 'INTEGER', 'TINYINT', 'SMALLINT', 'MEDIUMINT',
'BIGINT', 'DECIMAL', 'NUMERIC', 'FLOAT', 'DOUBLE', 'REAL', 'BIT',
'CHAR', 'VARCHAR', 'BINARY', 'VARBINARY', 'TINYTEXT', 'TEXT',
'MEDIUMTEXT', 'LONGTEXT', 'ENUM', 'SET', 'DATE', 'DATETIME', 'TIMESTAMP',
'TIME', 'YEAR', 'JSON', 'JSONB', 'XML', 'UUID', 'INET', 'CIDR',
'MACADDR', 'TSVECTOR', 'TSQUERY', 'INT4RANGE', 'INT8RANGE', 'NUMRANGE',
'TSRANGE', 'TSTZRANGE', 'DATERANGE', 'POINT', 'LINE', 'LSEG', 'BOX',
'PATH', 'POLYGON', 'CIRCLE', 'MULTIPOINT', 'MULTILINESTRING',
'MULTIPOLYGON', 'GEOMETRYCOLLECTION'
];
const sqlCompletions = sqlKeywords.map((keyword) => ({
label: keyword,
type: 'keyword',
apply: keyword + ' ',
}));
// props v-model
const props = defineProps<{
modelValue: string;
placeholder?: string;
}>();
// emits
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
(e: 'execute'): void;
}>();
//
const editorContainer: Ref<HTMLElement | null> = ref(null);
let editorView: EditorView | null = null;
const code = ref(props.modelValue || '');
const placeholder = ref(props.placeholder || '请输入 SQL 语句');
//
const initEditor = () => {
if (!editorContainer.value) return;
const extensions = [
basicSetup,
sql({ dialect: MySQL, upperCaseKeywords: true }),
autocompletion({
override: [completeFromList(sqlCompletions)],
defaultKeymap: true,
activateOnTyping: true,
}),
keymap.of([
{
key: 'Ctrl-Enter',
run: () => {
emit('execute');
return true;
},
},
{
key: 'Cmd-Enter',
run: () => {
emit('execute');
return true;
},
},
]),
];
//
const extensions = [
basicSetup, //
sql(), // SQL
oneDark, // 使
EditorView.lineWrapping, //
EditorView.updateListener.of((update) => {
// modelValue
if (update.docChanged) {
const newValue = update.state.doc.toString();
emit('update:modelValue', newValue);
}
}),
];
// 使 modelValue
const state = EditorState.create({
doc: props.modelValue,
extensions,
});
// EditorView
editorView = new EditorView({
state,
parent: editorContainer.value,
});
const handleChange = (value: string) => {
emit('update:modelValue', value);
};
const handleKeydown = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
event.preventDefault();
emit('execute');
}
};
// modelValue
watch(
() => props.modelValue,
(newValue) => {
if (editorView && newValue !== editorView.state.doc.toString()) {
editorView.dispatch({
changes: { from: 0, to: editorView.state.doc.length, insert: newValue },
});
if (newValue !== code.value) {
code.value = newValue;
}
},
{ deep: true }
}
);
//
onMounted(() => {
initEditor();
});
//
onUnmounted(() => {
editorView?.destroy();
});
</script>
<style scoped>
.sql-editor-container {
height: 400px; /* 必须指定高度 */
border: 1px solid #e5e7eb;
border-radius: 4px;
:deep(.cm-editor) {
height: 100%;
border: 1px solid #dcdfe6;
border-radius: 8px;
overflow: hidden;
transition: border-color 0.2s;
}
:deep(.cm-editor:hover) {
border-color: #c0c4cc;
}
:deep(.cm-focused) {
border-color: #409eff !important;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
outline: none !important;
}
:deep(.cm-scroller) {
font-family: '"JetBrains Mono", "Fira Code", Consolas, monospace';
font-size: 14px;
line-height: 1.6;
}
:deep(.cm-gutters) {
background-color: #f5f7fa;
border-right: 1px solid #e4e7ed;
color: #909399;
}
:deep(.cm-activeLineGutter) {
background-color: #ecf5ff;
}
:deep(.cm-activeLine) {
background-color: rgba(64, 158, 255, 0.05);
}
:deep(.cm-tooltip) {
background-color: #ffffff !important;
border: 1px solid #e4e7ed !important;
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
}
:deep(.cm-tooltip-autocomplete ul li[aria-selected]) {
background-color: #ecf5ff !important;
color: #409eff !important;
}
:deep(.cm-completionIcon-keyword)::before {
content: 'K';
color: #409eff;
font-weight: bold;
font-size: 12px;
}
</style>