|
|
<template>
|
|
|
<div class="p-2">
|
|
|
<el-form :model="form" label-width="100px" class="dataset-form" style="margin-bottom: 0;">
|
|
|
<div class="form-row">
|
|
|
<el-form-item label="数据集名称">
|
|
|
<el-input v-model="form.name" placeholder="请输入数据集名称"/>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="数据源">
|
|
|
<el-select v-model="form.datasource" placeholder="请选择数据源" style="width: 240px;">
|
|
|
<el-option v-for="ds in datasourceList" :key="ds.value" :label="ds.label" :value="ds.value"/>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="是否分页">
|
|
|
<el-switch v-model="form.pagination"/>
|
|
|
<el-tooltip content="开启分页后,查询结果将按页返回,适用于大数据量场景。" placement="top">
|
|
|
<el-icon style="margin-left:4px;cursor:pointer;">
|
|
|
<QuestionFilled/>
|
|
|
</el-icon>
|
|
|
</el-tooltip>
|
|
|
</el-form-item>
|
|
|
</div>
|
|
|
<el-form-item label="查询SQL" class="sql-form-item">
|
|
|
<div style="display: flex; align-items: flex-start; width: 100%;">
|
|
|
<el-input
|
|
|
type="textarea"
|
|
|
v-model="form.sql"
|
|
|
:rows="3"
|
|
|
placeholder="请输入查询SQL"
|
|
|
style="flex:1"
|
|
|
/>
|
|
|
<el-button type="primary" @click="showAiDialog = true" style="margin-left:8px;">
|
|
|
<svg width="20" height="20" viewBox="0 0 1024 1024">
|
|
|
<circle cx="512" cy="512" r="512" fill="#409EFF"/>
|
|
|
<text x="50%" y="60%" text-anchor="middle" fill="#fff" font-size="400" font-family="Arial" dy=".3em">AI
|
|
|
</text>
|
|
|
</svg>
|
|
|
</el-button>
|
|
|
</div>
|
|
|
<div class="sql-desc">
|
|
|
<div class="sql-tips">
|
|
|
<div>• 请填写标准SQL,支持参数占位符</div>
|
|
|
<div>• 参数占位符格式:#{参数名},如:#{year}</div>
|
|
|
<div>• 支持SELECT、FROM、WHERE等标准SQL语法</div>
|
|
|
</div>
|
|
|
<el-button type="primary" size="small" @click="parseSql" style="margin-left:8px;">查询解析</el-button>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
|
|
|
<el-tabs v-model="activeTab" style="margin: 16px 0 0 10px;">
|
|
|
<el-tab-pane label="列表字段" name="fields">
|
|
|
<div style="margin-bottom:8px;display:flex;align-items:center;">
|
|
|
<el-button type="danger" size="small" :disabled="!multipleSelection.length" @click="batchRemoveFields">
|
|
|
批量删除
|
|
|
</el-button>
|
|
|
</div>
|
|
|
<el-table
|
|
|
:data="fields"
|
|
|
style="width: 100%"
|
|
|
border
|
|
|
ref="fieldsTableRef"
|
|
|
@selection-change="handleSelectionChange"
|
|
|
>
|
|
|
<el-table-column type="selection" width="50"/>
|
|
|
<el-table-column prop="name" label="字段名" min-width="120">
|
|
|
<template #default="scope">
|
|
|
<el-input v-model="scope.row.name" size="small"/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="text" label="字段文本" min-width="120">
|
|
|
<template #default="scope">
|
|
|
<el-input v-model="scope.row.text" size="small"/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="type" label="字段类型" min-width="100">
|
|
|
<template #default="scope">
|
|
|
<el-select v-model="scope.row.type" size="small" style="width:100px">
|
|
|
<el-option label="文本" value="varchar"/>
|
|
|
<el-option label="数值" value="int"/>
|
|
|
<el-option label="日期" value="date"/>
|
|
|
</el-select>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="order" label="排序" min-width="80">
|
|
|
<template #default="scope">
|
|
|
<el-input-number v-model="scope.row.order" :min="1" size="small" style="width:80px"/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="dictCode" label="字典编码" min-width="120">
|
|
|
<template #default="scope">
|
|
|
<el-input v-model="scope.row.dictCode" size="small"/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</el-tab-pane>
|
|
|
<el-tab-pane label="参数字段" name="params">
|
|
|
<div style="margin-bottom:8px;display:flex;align-items:center;">
|
|
|
<el-button type="primary" size="small" @click="addParam">新增参数</el-button>
|
|
|
<el-button type="danger" size="small" :disabled="!multipleParamSelection.length" @click="batchRemoveParams"
|
|
|
style="margin-left:8px;">批量删除
|
|
|
</el-button>
|
|
|
</div>
|
|
|
<el-table
|
|
|
:data="params"
|
|
|
style="width: 100%"
|
|
|
border
|
|
|
ref="paramsTableRef"
|
|
|
@selection-change="handleParamSelectionChange"
|
|
|
>
|
|
|
<el-table-column type="selection" width="50"/>
|
|
|
<el-table-column prop="param" label="参数" min-width="120">
|
|
|
<template #default="scope">
|
|
|
<el-input v-model="scope.row.param" size="small"/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="text" label="参数文本" min-width="120">
|
|
|
<template #default="scope">
|
|
|
<el-input v-model="scope.row.text" size="small"/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="type" label="类型" min-width="100">
|
|
|
<template #default="scope">
|
|
|
<el-select v-model="scope.row.type" size="small" style="width:100px">
|
|
|
<el-option label="文本" value="varchar"/>
|
|
|
<el-option label="数值" value="int"/>
|
|
|
<el-option label="日期" value="date"/>
|
|
|
</el-select>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="defaultValue" label="默认值" min-width="120">
|
|
|
<template #default="scope">
|
|
|
<el-input v-model="scope.row.defaultValue" size="small"/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</el-tab-pane>
|
|
|
<el-tab-pane label="数据预览" name="preview">
|
|
|
<div style="display:flex;align-items:center;margin-bottom:8px;">
|
|
|
<el-button type="primary" size="small" @click="fetchPreviewData">刷新预览</el-button>
|
|
|
<span
|
|
|
style="margin-left:8px;padding:2px 14px;background:#ecf5ff;color:#409EFF;font-weight:bold;border-radius:4px;font-size:14px;">仅预览前10条数据</span>
|
|
|
</div>
|
|
|
<el-table :data="previewData" style="width: 100%" border v-if="previewData.length">
|
|
|
<el-table-column v-for="col in previewColumns" :key="col.prop" :prop="col.prop" :label="col.label"/>
|
|
|
</el-table>
|
|
|
<div v-else style="color:#888;text-align:center;padding:24px 0;">暂无数据</div>
|
|
|
</el-tab-pane>
|
|
|
</el-tabs>
|
|
|
|
|
|
<div style="text-align:right;margin-top:24px;">
|
|
|
<el-button @click="onClose">关闭</el-button>
|
|
|
<el-button type="primary" @click="onSave">保存</el-button>
|
|
|
</div>
|
|
|
|
|
|
<!-- AI生成SQL弹窗 -->
|
|
|
<el-dialog v-model="showAiDialog" title="AI生成SQL" width="800px">
|
|
|
<el-input
|
|
|
v-model="aiPrompt"
|
|
|
type="textarea"
|
|
|
placeholder="请输入需求描述,如:查询年龄大于35的用户信息,包括所在部门的名称"
|
|
|
style="margin-bottom:12px;"
|
|
|
/>
|
|
|
<el-button type="primary" @click="getSql" :loading="aiLoading">生成SQL</el-button>
|
|
|
<el-input
|
|
|
v-model="aiSql"
|
|
|
type="textarea"
|
|
|
:rows="4"
|
|
|
readonly
|
|
|
style="margin-top:12px;"
|
|
|
/>
|
|
|
<div style="text-align:right;margin-top:8px;">
|
|
|
<el-button @click="showAiDialog=false">取消</el-button>
|
|
|
<el-button type="primary" @click="replaceSql">替换到查询SQL</el-button>
|
|
|
</div>
|
|
|
</el-dialog>
|
|
|
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import {ref, reactive, watch} from 'vue'
|
|
|
import {ElMessage} from 'element-plus'
|
|
|
import {QuestionFilled} from '@element-plus/icons-vue'
|
|
|
import {generateSql} from "@/api/ai/skill/aiChat/index";
|
|
|
|
|
|
const {proxy} = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
|
|
|
|
const form = reactive({
|
|
|
name: '',
|
|
|
datasource: '',
|
|
|
sql: '',
|
|
|
pagination: true
|
|
|
})
|
|
|
const datasourceList = ref([
|
|
|
{label: '数据源1', value: 'ds1'},
|
|
|
{label: '数据源2', value: 'ds2'}
|
|
|
])
|
|
|
|
|
|
const showAiDialog = ref(false)
|
|
|
const aiPrompt = ref('')
|
|
|
const aiSql = ref('')
|
|
|
const aiLoading = ref(false)
|
|
|
|
|
|
const activeTab = ref('fields')
|
|
|
const fields = ref<any[]>([])
|
|
|
const params = ref<any[]>([])
|
|
|
const multipleSelection = ref<any[]>([])
|
|
|
const fieldsTableRef = ref()
|
|
|
const multipleParamSelection = ref<any[]>([])
|
|
|
const paramsTableRef = ref()
|
|
|
|
|
|
const previewData = ref<any[]>([])
|
|
|
const previewColumns = ref<any[]>([])
|
|
|
|
|
|
const getSql = async () => {
|
|
|
if (!aiPrompt.value.trim() || aiLoading.value) return
|
|
|
|
|
|
aiLoading.value = true
|
|
|
|
|
|
// let sql1 = "```sql\nSELECT \n user_id,\n tenant_id,\n dept_id,\n user_name,\n nick_name,\n user_type,\n email,\n phonenumber,\n sex,\n avatar,\n password,\n status,\n del_flag,\n login_ip,\n login_date,\n create_dept,\n create_by,\n create_time,\n update_by,\n update_time,\n remark\nFROM \n sys_user WITH(NOLOCK)\nWHERE \n del_flag = '0'\n```";
|
|
|
|
|
|
// sql1 =sql1.replace(/\s+/g, ' ');
|
|
|
// aiSql.value = sql1;
|
|
|
// return;
|
|
|
|
|
|
try {
|
|
|
const response = await generateSql({text: aiPrompt.value,modelId:1,platformId:1})
|
|
|
|
|
|
// sql = sql.replaceAll("\"","");
|
|
|
// sql = sql.substring(String.prototype.toLowerCase(sql).indexOf("select"));
|
|
|
// 检查首尾是否是双引号或单引号
|
|
|
// if ((sql.startsWith("\"") && sql.endsWith("\"")) ||
|
|
|
// (sql.startsWith("'") && sql.endsWith("'"))) {
|
|
|
// sql = sql.substring(1, sql.length - 1);
|
|
|
// }
|
|
|
|
|
|
aiSql.value = response;
|
|
|
|
|
|
console.log(1)
|
|
|
console.log(response)
|
|
|
console.log(2)
|
|
|
|
|
|
} catch (error) {
|
|
|
proxy?.$modal.msgError(error);
|
|
|
console.log("Error:" + error)
|
|
|
|
|
|
} finally {
|
|
|
aiLoading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function replaceSql() {
|
|
|
form.sql = aiSql.value
|
|
|
showAiDialog.value = false
|
|
|
}
|
|
|
|
|
|
function parseSql() {
|
|
|
if (!form.sql) return ElMessage.warning('请先填写SQL')
|
|
|
fields.value = [
|
|
|
{name: 'id', text: '编号', type: 'int', order: 1, dictCode: ''},
|
|
|
{name: 'name', text: '名称', type: 'varchar', order: 2, dictCode: ''}
|
|
|
]
|
|
|
params.value = [
|
|
|
{param: 'year', text: '年份', type: 'int', defaultValue: 2023}
|
|
|
]
|
|
|
ElMessage.success('解析成功')
|
|
|
}
|
|
|
|
|
|
function handleSelectionChange(val: any[]) {
|
|
|
multipleSelection.value = val
|
|
|
}
|
|
|
|
|
|
function batchRemoveFields() {
|
|
|
if (!multipleSelection.value.length) return
|
|
|
fields.value = fields.value.filter(row => !multipleSelection.value.includes(row))
|
|
|
multipleSelection.value = []
|
|
|
}
|
|
|
|
|
|
function handleParamSelectionChange(val: any[]) {
|
|
|
multipleParamSelection.value = val
|
|
|
}
|
|
|
|
|
|
function batchRemoveParams() {
|
|
|
if (!multipleParamSelection.value.length) return
|
|
|
params.value = params.value.filter(row => !multipleParamSelection.value.includes(row))
|
|
|
multipleParamSelection.value = []
|
|
|
}
|
|
|
|
|
|
function addParam() {
|
|
|
params.value.push({param: '', text: '', type: '', defaultValue: ''})
|
|
|
}
|
|
|
|
|
|
function onSave() {
|
|
|
ElMessage.success('保存成功')
|
|
|
}
|
|
|
|
|
|
function onClose() {
|
|
|
ElMessage.info('已关闭')
|
|
|
}
|
|
|
|
|
|
function fetchPreviewData() {
|
|
|
// 模拟数据预览,实际可根据SQL和参数请求后端
|
|
|
if (!fields.value.length) {
|
|
|
previewData.value = []
|
|
|
previewColumns.value = []
|
|
|
return
|
|
|
}
|
|
|
// 构造表头
|
|
|
previewColumns.value = fields.value.map(f => ({prop: f.name, label: f.text}))
|
|
|
// 构造10条模拟数据
|
|
|
previewData.value = Array.from({length: 10}, (_, i) => {
|
|
|
const row: any = {}
|
|
|
fields.value.forEach(f => {
|
|
|
if (f.type === 'int') row[f.name] = i + 1
|
|
|
else if (f.type === 'date') row[f.name] = '2023-01-01'
|
|
|
else row[f.name] = f.text + (i + 1)
|
|
|
})
|
|
|
return row
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 默认进入时预览
|
|
|
if (activeTab.value === 'preview') fetchPreviewData()
|
|
|
|
|
|
watch(activeTab, (val) => {
|
|
|
if (val === 'preview') fetchPreviewData()
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
.dataset-form {
|
|
|
max-width: 100%;
|
|
|
margin: 0 auto;
|
|
|
background: #fff;
|
|
|
padding: 16px 24px 0 24px;
|
|
|
border-radius: 8px;
|
|
|
}
|
|
|
|
|
|
.form-row {
|
|
|
display: flex;
|
|
|
gap: 24px;
|
|
|
align-items: center;
|
|
|
margin-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.sql-form-item {
|
|
|
margin-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.sql-desc {
|
|
|
color: #888;
|
|
|
font-size: 12px;
|
|
|
margin-top: 4px;
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
}
|
|
|
|
|
|
.sql-tips {
|
|
|
flex: 1;
|
|
|
}
|
|
|
|
|
|
.sql-tips div {
|
|
|
line-height: 1.5;
|
|
|
margin-bottom: 2px;
|
|
|
}
|
|
|
|
|
|
.sql-desc .el-button {
|
|
|
margin-left: 8px;
|
|
|
padding: 0 10px;
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
</style>
|