新增:连接管理、数据查询等功能
This commit is contained in:
529
web/src/views/db-cli/components/MySQLCreate.vue
Normal file
529
web/src/views/db-cli/components/MySQLCreate.vue
Normal file
@@ -0,0 +1,529 @@
|
||||
<template>
|
||||
<div class="mysql-create">
|
||||
<a-tabs
|
||||
v-model:active-key="activeTab"
|
||||
type="line"
|
||||
class="create-tabs"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<a-tab-pane key="basic" title="基本信息">
|
||||
<div class="tab-content basic-info-content">
|
||||
<a-form :model="formData" layout="vertical" :label-col-props="{ span: 6 }">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="数据库" field="database">
|
||||
<a-input v-model="formData.database" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="表名" field="tableName" :rules="[{ required: true, message: '请输入表名' }]">
|
||||
<a-input v-model="formData.tableName" placeholder="请输入表名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="字符集" field="charset">
|
||||
<a-select v-model="formData.charset" placeholder="选择字符集">
|
||||
<a-option value="utf8mb4">utf8mb4</a-option>
|
||||
<a-option value="utf8">utf8</a-option>
|
||||
<a-option value="latin1">latin1</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="排序规则" field="collation">
|
||||
<a-select v-model="formData.collation" placeholder="选择排序规则">
|
||||
<a-option value="utf8mb4_general_ci">utf8mb4_general_ci</a-option>
|
||||
<a-option value="utf8mb4_unicode_ci">utf8mb4_unicode_ci</a-option>
|
||||
<a-option value="utf8_general_ci">utf8_general_ci</a-option>
|
||||
<a-option value="utf8_unicode_ci">utf8_unicode_ci</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 字段列表 -->
|
||||
<a-tab-pane key="fields" title="字段列表">
|
||||
<div class="tab-content fields-content">
|
||||
<MySQLFieldList
|
||||
mode="create"
|
||||
:fields="formData.fields"
|
||||
@add-field="handleAddField"
|
||||
@remove-field="handleRemoveField"
|
||||
@move-field="handleMoveField"
|
||||
@update-field="handleUpdateField"
|
||||
/>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 索引列表 -->
|
||||
<a-tab-pane key="indexes" title="索引列表">
|
||||
<div class="tab-content indexes-content">
|
||||
<div class="section-header">
|
||||
<a-button type="primary" size="small" @click="showIndexDialog">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
添加索引
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-if="formData.indexes.length === 0" class="empty-tip">
|
||||
<a-empty description="暂无索引(可选)" :image="false" />
|
||||
</div>
|
||||
<a-table
|
||||
v-else
|
||||
:columns="indexColumns"
|
||||
:data="formData.indexes"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
:bordered="true"
|
||||
>
|
||||
<template #unique="{ record }">
|
||||
<a-tag :color="record.unique ? 'blue' : 'default'">
|
||||
{{ record.unique ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #fields="{ record }">
|
||||
{{ record.fields.map((f: any) => f.name).join(', ') }}
|
||||
</template>
|
||||
<template #operations="{ record, rowIndex }">
|
||||
<a-button type="text" size="small" status="danger" @click="removeIndex(rowIndex)">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- SQL预览 -->
|
||||
<a-tab-pane key="sql" title="SQL预览">
|
||||
<div class="tab-content sql-preview-content">
|
||||
<div class="sql-preview-header">
|
||||
<a-button type="text" size="small" @click="copySQL">
|
||||
<template #icon>
|
||||
<icon-copy />
|
||||
</template>
|
||||
复制
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="sql-preview-wrapper">
|
||||
<pre class="sql-code">{{ sqlPreview }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<!-- 索引定义对话框 -->
|
||||
<a-modal
|
||||
v-model:visible="indexDialogVisible"
|
||||
title="添加索引"
|
||||
:width="600"
|
||||
@ok="handleIndexDialogOk"
|
||||
@cancel="handleIndexDialogCancel"
|
||||
>
|
||||
<a-form :model="indexForm" layout="vertical" ref="indexFormRef">
|
||||
<a-form-item label="索引名" field="name" :rules="[{ required: true, message: '请输入索引名' }]">
|
||||
<a-input v-model="indexForm.name" placeholder="请输入索引名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="唯一索引" field="unique">
|
||||
<a-checkbox v-model="indexForm.unique">唯一索引</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-form-item label="索引字段" field="fields" :rules="[{ required: true, message: '请至少选择一个字段' }]">
|
||||
<a-select
|
||||
v-model="indexForm.fields"
|
||||
mode="multiple"
|
||||
placeholder="选择索引字段"
|
||||
:max-tag-count="3"
|
||||
>
|
||||
<a-option
|
||||
v-for="field in formData.fields"
|
||||
:key="field.name"
|
||||
:value="field.name"
|
||||
>
|
||||
{{ field.name }} ({{ field.type }}{{ field.length ? `(${field.length})` : '' }})
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, reactive } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
IconCopy
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import MySQLFieldList from './MySQLFieldList.vue'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
connectionId: number
|
||||
database: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'cancel'): void
|
||||
(e: 'create', data: any): void
|
||||
}>()
|
||||
|
||||
// 当前激活的 tab
|
||||
const activeTab = ref<string>('basic')
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
database: props.database,
|
||||
tableName: '',
|
||||
charset: 'utf8mb4',
|
||||
collation: 'utf8mb4_general_ci',
|
||||
fields: [] as any[],
|
||||
indexes: [] as any[]
|
||||
})
|
||||
|
||||
// SQL 预览
|
||||
const sqlPreview = computed(() => {
|
||||
if (formData.fields.length === 0) {
|
||||
return '-- 请先添加字段'
|
||||
}
|
||||
return generateSQL()
|
||||
})
|
||||
|
||||
// 索引表格列
|
||||
const indexColumns = [
|
||||
{ title: '索引名', dataIndex: 'name', width: 150 },
|
||||
{ title: '唯一', dataIndex: 'unique', slotName: 'unique', width: 80 },
|
||||
{ title: '字段', slotName: 'fields', width: 200 },
|
||||
{ title: '操作', slotName: 'operations', width: 100, fixed: 'right' }
|
||||
]
|
||||
|
||||
// 索引对话框
|
||||
const indexDialogVisible = ref(false)
|
||||
const indexFormRef = ref()
|
||||
const indexForm = reactive({
|
||||
name: '',
|
||||
unique: false,
|
||||
fields: [] as string[]
|
||||
})
|
||||
|
||||
// 字段列表事件处理
|
||||
const handleAddField = (field: any) => {
|
||||
formData.fields.push(field)
|
||||
// 自动切换到字段列表 tab
|
||||
if (activeTab.value !== 'fields') {
|
||||
activeTab.value = 'fields'
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateField = (index: number, field: string, value: any) => {
|
||||
if (formData.fields[index]) {
|
||||
formData.fields[index] = { ...formData.fields[index], [field]: value }
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveField = (index: number) => {
|
||||
formData.fields.splice(index, 1)
|
||||
// 同时移除相关索引
|
||||
formData.indexes = formData.indexes.filter((idx: any) => {
|
||||
return idx.fields.every((f: any) => {
|
||||
const fieldName = typeof f === 'string' ? f : f.name
|
||||
return fieldName !== formData.fields[index]?.name
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleMoveField = (index: number, direction: 'up' | 'down') => {
|
||||
if (direction === 'up' && index > 0) {
|
||||
const temp = formData.fields[index]
|
||||
formData.fields[index] = formData.fields[index - 1]
|
||||
formData.fields[index - 1] = temp
|
||||
} else if (direction === 'down' && index < formData.fields.length - 1) {
|
||||
const temp = formData.fields[index]
|
||||
formData.fields[index] = formData.fields[index + 1]
|
||||
formData.fields[index + 1] = temp
|
||||
}
|
||||
}
|
||||
|
||||
const showIndexDialog = () => {
|
||||
if (formData.fields.length === 0) {
|
||||
Message.warning('请先添加字段')
|
||||
return
|
||||
}
|
||||
// 重置表单
|
||||
Object.assign(indexForm, {
|
||||
name: '',
|
||||
unique: false,
|
||||
fields: []
|
||||
})
|
||||
indexDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleIndexDialogOk = async () => {
|
||||
try {
|
||||
// 验证表单
|
||||
await indexFormRef.value?.validate()
|
||||
|
||||
// 检查索引名是否重复
|
||||
if (formData.indexes.some((idx: any) => idx.name === indexForm.name)) {
|
||||
Message.error('索引名已存在')
|
||||
return false // 阻止对话框关闭
|
||||
}
|
||||
|
||||
// 添加索引(深拷贝避免引用问题)
|
||||
const newIndex = {
|
||||
name: indexForm.name,
|
||||
unique: indexForm.unique,
|
||||
fields: indexForm.fields.map((name: string) => ({ name, order: 'ASC' }))
|
||||
}
|
||||
|
||||
formData.indexes.push(newIndex)
|
||||
|
||||
Message.success('索引添加成功')
|
||||
indexDialogVisible.value = false
|
||||
|
||||
// 自动切换到索引列表 tab
|
||||
if (activeTab.value !== 'indexes') {
|
||||
activeTab.value = 'indexes'
|
||||
}
|
||||
} catch (error) {
|
||||
// 表单验证失败时会抛出错误
|
||||
console.error('索引表单验证失败:', error)
|
||||
return false // 阻止对话框关闭
|
||||
}
|
||||
}
|
||||
|
||||
const handleIndexDialogCancel = () => {
|
||||
indexDialogVisible.value = false
|
||||
}
|
||||
|
||||
const removeIndex = (index: number) => {
|
||||
formData.indexes.splice(index, 1)
|
||||
}
|
||||
|
||||
// 复制 SQL
|
||||
const copySQL = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(sqlPreview.value)
|
||||
Message.success('SQL已复制到剪贴板')
|
||||
} catch (error) {
|
||||
Message.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 验证表单
|
||||
const validate = (): boolean => {
|
||||
if (!formData.tableName) {
|
||||
Message.error('请输入表名')
|
||||
return false
|
||||
}
|
||||
|
||||
if (formData.fields.length === 0) {
|
||||
Message.error('请至少添加一个字段')
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否有主键
|
||||
const hasPrimaryKey = formData.fields.some((f: any) => f.primaryKey)
|
||||
if (!hasPrimaryKey) {
|
||||
Message.warning('建议设置主键')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 转义 SQL 字符串(转义单引号)
|
||||
const escapeSQLString = (str: string): string => {
|
||||
return str.replace(/'/g, "''")
|
||||
}
|
||||
|
||||
// 生成 SQL
|
||||
const generateSQL = (): string => {
|
||||
const fieldsSQL = formData.fields.map((field: any) => {
|
||||
let sql = `\`${field.name}\` ${field.type}`
|
||||
if (field.length) {
|
||||
sql += `(${field.length})`
|
||||
}
|
||||
if (!field.nullable) {
|
||||
sql += ' NOT NULL'
|
||||
}
|
||||
// 处理默认值
|
||||
if (field.defaultValue !== null && field.defaultValue !== undefined) {
|
||||
if (field.defaultValue === '') {
|
||||
// 空字符串默认值
|
||||
sql += ` DEFAULT ''`
|
||||
} else {
|
||||
// 转义单引号
|
||||
const escapedDefault = escapeSQLString(String(field.defaultValue))
|
||||
sql += ` DEFAULT '${escapedDefault}'`
|
||||
}
|
||||
}
|
||||
if (field.autoIncrement) {
|
||||
sql += ' AUTO_INCREMENT'
|
||||
}
|
||||
if (field.comment) {
|
||||
// 转义注释中的单引号
|
||||
const escapedComment = escapeSQLString(field.comment)
|
||||
sql += ` COMMENT '${escapedComment}'`
|
||||
}
|
||||
return sql
|
||||
}).join(',\n ')
|
||||
|
||||
// 主键
|
||||
const primaryKeys = formData.fields.filter((f: any) => f.primaryKey).map((f: any) => `\`${f.name}\``)
|
||||
let primaryKeySQL = ''
|
||||
if (primaryKeys.length > 0) {
|
||||
primaryKeySQL = `,\n PRIMARY KEY (${primaryKeys.join(', ')})`
|
||||
}
|
||||
|
||||
// 索引
|
||||
const indexesSQL = formData.indexes.map((idx: any) => {
|
||||
const fields = idx.fields.map((f: any) => `\`${typeof f === 'string' ? f : f.name}\``).join(', ')
|
||||
const unique = idx.unique ? 'UNIQUE ' : ''
|
||||
return ` ${unique}KEY \`${idx.name}\` (${fields})`
|
||||
}).join(',\n')
|
||||
|
||||
const sql = `CREATE TABLE \`${formData.database}\`.\`${formData.tableName}\` (
|
||||
${fieldsSQL}${primaryKeySQL}${indexesSQL ? ',\n' + indexesSQL : ''}
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=${formData.charset} COLLATE=${formData.collation};`
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
validate,
|
||||
generateSQL,
|
||||
getFormData: () => formData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mysql-create {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Tabs 容器 */
|
||||
.create-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.create-tabs :deep(.arco-tabs) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.create-tabs :deep(.arco-tabs-content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.create-tabs :deep(.arco-tabs-content-list) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.create-tabs :deep(.arco-tabs-content-item) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Tab 内容通用样式 */
|
||||
.tab-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing-md, 12px);
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 基本信息内容 */
|
||||
.basic-info-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.basic-info-content :deep(.arco-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 字段列表和索引列表内容 */
|
||||
.fields-content,
|
||||
.indexes-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fields-content :deep(.arco-table),
|
||||
.indexes-content :deep(.arco-table) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* SQL预览内容 */
|
||||
.sql-preview-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sql-preview-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sql-preview-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
background: var(--color-fill-2, #f2f3f5);
|
||||
border: 1px solid var(--color-border-2, #e5e6eb);
|
||||
border-radius: var(--border-radius-small, 2px);
|
||||
padding: var(--spacing-md, 12px);
|
||||
}
|
||||
|
||||
.sql-code {
|
||||
margin: 0;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-1, #1d2129);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user