mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-10 02:07:12 +08:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
+228
-404
@@ -1,279 +1,288 @@
|
||||
<template>
|
||||
<div class="chat-container">
|
||||
<!-- Sidebar for chat history -->
|
||||
<div class="chat-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<el-button type="primary" class="new-chat" @click="createNewChat">
|
||||
<el-icon><Plus /></el-icon>
|
||||
New Conversation
|
||||
</el-button>
|
||||
<div class="search-box">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="Search conversation history"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
<el-container class="chat-container">
|
||||
<el-aside class="chat-container-left">
|
||||
<el-container class="chat-container-right-container">
|
||||
<el-header class="chat-list-header">
|
||||
<el-button type="primary" @click="createNewChat">
|
||||
<el-icon>
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
New Conversation
|
||||
</el-button>
|
||||
</el-header>
|
||||
<el-main class="chat-list">
|
||||
<el-scrollbar>
|
||||
<div v-for="chat in chatList" @click="onClickHistory(chat)"
|
||||
:style="chat.id === currentChatId ? {border: 'solid 1px red'} : {}">
|
||||
{{ chat.create_time }}<br/>
|
||||
{{ chat.brief }}
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-aside>
|
||||
<el-container :loading="loading">
|
||||
<el-main>
|
||||
<div v-for="message in computedMessages">
|
||||
{{ message.role }}: {{ message.content }}
|
||||
<div v-if="message.isWelcome">
|
||||
<el-select v-model="currentChat.datasource" :disabled="currentChat.id!==undefined">
|
||||
<el-option v-for="item in dsList"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
:label="item.name"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="history-list">
|
||||
<div
|
||||
v-for="chat in filteredHistory"
|
||||
:key="chat.id"
|
||||
class="history-item"
|
||||
:class="{ active: currentChatId === chat.id }"
|
||||
@click="switchChat(chat.id)"
|
||||
>
|
||||
<span class="chat-title">{{ chat.title }}</span>
|
||||
<el-dropdown trigger="hover" @command="handleCommand($event, chat.id)">
|
||||
<span class="more-actions">
|
||||
<el-icon><More /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="rename">Rename</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>Delete</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main chat area -->
|
||||
<div class="chat-main">
|
||||
<div class="chat-messages" ref="messagesRef">
|
||||
<template v-if="currentChat?.messages?.length">
|
||||
<div
|
||||
v-for="(msg, index) in currentChat.messages"
|
||||
:key="index"
|
||||
class="message"
|
||||
:class="[msg.role, { 'is-typing': msg.isTyping }]"
|
||||
>
|
||||
<div
|
||||
v-if="msg.role === 'assistant'"
|
||||
class="message-content"
|
||||
:class="{ 'typing': msg.isTyping }"
|
||||
v-html="formatMessage(msg.content)"
|
||||
></div>
|
||||
<div v-else class="message-content">
|
||||
{{ msg.content }}
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<div class="chat-input">
|
||||
<div class="input-wrapper">
|
||||
<el-input
|
||||
v-model="inputMessage"
|
||||
type="textarea"
|
||||
:rows="1"
|
||||
:autosize="{ minRows: 1, maxRows: 8 }"
|
||||
placeholder="Press Enter to send, Ctrl + Enter for new line"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
@keydown.ctrl.enter.exact.prevent="handleCtrlEnter"
|
||||
/>
|
||||
<div class="input-actions">
|
||||
<el-button circle type="primary" class="send-btn" @click="sendMessage">
|
||||
<el-icon>
|
||||
<Position/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="empty-state">
|
||||
<p>Welcome to SQLBot</p>
|
||||
<p class="sub-text">You can ask questions in natural language, for example:</p>
|
||||
<div class="examples">
|
||||
<div class="example-item">View this month's sales growth compared to last month</div>
|
||||
<div class="example-item">Analyze sales staff performance by region</div>
|
||||
<div class="example-item">Calculate the relationship between customer purchase frequency and average order value</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<div class="input-wrapper">
|
||||
<el-input
|
||||
v-model="inputMessage"
|
||||
type="textarea"
|
||||
:rows="1"
|
||||
:autosize="{ minRows: 1, maxRows: 8 }"
|
||||
placeholder="Press Enter to send, Ctrl + Enter for new line"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
@keydown.ctrl.enter.exact.prevent="handleCtrlEnter"
|
||||
/>
|
||||
<div class="input-actions">
|
||||
<el-button circle type="primary" class="send-btn" @click="sendMessage">
|
||||
<el-icon><Position /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, watch } from 'vue'
|
||||
import { Plus, Search, More, Position } from '@element-plus/icons-vue'
|
||||
import { questionApi } from '@/api/chat'
|
||||
import {ref, computed, nextTick, watch, onMounted} from 'vue'
|
||||
import {Plus, Position} from '@element-plus/icons-vue'
|
||||
import {Chat, chatApi, ChatInfo, ChatRecord, questionApi} from '@/api/chat'
|
||||
import {datasourceApi} from "@/api/datasource.ts";
|
||||
|
||||
interface ChatMessage {
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
isTyping?: boolean
|
||||
create_time?: Date | string
|
||||
content?: string | number
|
||||
isTyping?: boolean
|
||||
isWelcome?: boolean
|
||||
}
|
||||
|
||||
interface Chat {
|
||||
id: string
|
||||
title: string
|
||||
messages: ChatMessage[]
|
||||
}
|
||||
|
||||
const chatHistory = ref<Chat[]>([])
|
||||
const currentChatId = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const inputMessage = ref('')
|
||||
const messagesRef = ref<HTMLDivElement>()
|
||||
|
||||
const filteredHistory = computed(() => {
|
||||
return chatHistory.value.filter(chat =>
|
||||
chat.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
const loading = ref<boolean>(false);
|
||||
const chatList = ref<Array<ChatInfo>>([])
|
||||
const dsList = ref<any>([])
|
||||
|
||||
const currentChat = computed(() => {
|
||||
return chatHistory.value.find(chat => chat.id === currentChatId.value)
|
||||
const currentChatId = ref<number | undefined>()
|
||||
const currentChat = ref<ChatInfo>(new ChatInfo())
|
||||
const isTyping = ref<boolean>(false)
|
||||
|
||||
const computedMessages = computed<Array<ChatMessage>>(() => {
|
||||
const welcome: ChatMessage = {
|
||||
role: 'assistant',
|
||||
create_time: currentChat.value?.create_time,
|
||||
content: currentChat.value?.datasource,
|
||||
isTyping: false,
|
||||
isWelcome: true,
|
||||
}
|
||||
const messages: Array<ChatMessage> = [welcome]
|
||||
if (currentChatId.value === undefined) {
|
||||
return messages
|
||||
}
|
||||
let appendThinking = false
|
||||
for (let i = 0; i < currentChat.value.records.length; i++) {
|
||||
const record = currentChat.value.records[i]
|
||||
if (record.question !== undefined) {
|
||||
messages.push({
|
||||
role: 'user',
|
||||
create_time: record.create_time,
|
||||
content: record.question,
|
||||
})
|
||||
}
|
||||
if (record.answer !== undefined && record.answer !== '') {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
create_time: record.create_time,
|
||||
content: record.answer,
|
||||
isTyping: i === currentChat.value.records.length - 1 && isTyping.value
|
||||
})
|
||||
} else {
|
||||
appendThinking = true
|
||||
}
|
||||
}
|
||||
if (isTyping.value && appendThinking) {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: 'Thinking...',
|
||||
isTyping: true
|
||||
})
|
||||
}
|
||||
return messages
|
||||
})
|
||||
|
||||
const createNewChat = () => {
|
||||
const newChat: Chat = {
|
||||
id: Date.now().toString(),
|
||||
title: `New Chat ${chatHistory.value.length + 1}`,
|
||||
messages: []
|
||||
currentChat.value = new ChatInfo()
|
||||
currentChatId.value = undefined
|
||||
listDs()
|
||||
}
|
||||
|
||||
function getChatList() {
|
||||
loading.value = true
|
||||
chatApi.list()
|
||||
.then((res) => {
|
||||
chatList.value = chatApi.toChatInfoList(res)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onClickHistory(chat: Chat) {
|
||||
currentChat.value = new ChatInfo(chat)
|
||||
if (chat !== undefined && chat.id !== undefined && !loading.value) {
|
||||
currentChatId.value = chat.id
|
||||
loading.value = true
|
||||
chatApi.get(chat.id)
|
||||
.then((res) => {
|
||||
const info = chatApi.toChatInfo(res)
|
||||
if (info) {
|
||||
currentChat.value = info
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
chatHistory.value.unshift(newChat)
|
||||
currentChatId.value = newChat.id
|
||||
}
|
||||
|
||||
const switchChat = (chatId: string) => {
|
||||
currentChatId.value = chatId
|
||||
function listDs() {
|
||||
datasourceApi.list().then((res) => {
|
||||
console.log(res)
|
||||
dsList.value = res
|
||||
})
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
/* nextTick(() => {
|
||||
if (messagesRef.value) {
|
||||
const scrollHeight = messagesRef.value.scrollHeight
|
||||
const animateScroll = () => {
|
||||
const currentScroll = messagesRef.value!.scrollTop
|
||||
const distance = scrollHeight - currentScroll
|
||||
if (distance > 0) {
|
||||
window.requestAnimationFrame(animateScroll)
|
||||
messagesRef.value!.scrollTop = currentScroll + distance / 8
|
||||
}
|
||||
}
|
||||
animateScroll()
|
||||
}
|
||||
}) */
|
||||
}
|
||||
onMounted(() => {
|
||||
getChatList()
|
||||
listDs()
|
||||
})
|
||||
|
||||
const updateMessageContent = (index: number, content: string) => {
|
||||
|
||||
const updateMessageContent = (content: string) => {
|
||||
if (currentChat.value) {
|
||||
currentChat.value.messages[index] = {
|
||||
...currentChat.value.messages[index],
|
||||
content
|
||||
}
|
||||
currentChat.value.records[currentChat.value.records.length - 1].answer = content
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!inputMessage.value.trim()) return
|
||||
|
||||
let chat = currentChat.value
|
||||
if (!chat) {
|
||||
chat = {
|
||||
id: Date.now().toString(),
|
||||
title: 'New Chat',
|
||||
messages: []
|
||||
}
|
||||
chatHistory.value.unshift(chat)
|
||||
currentChatId.value = chat.id
|
||||
if (computedMessages.value[0].content === undefined) return
|
||||
|
||||
loading.value = true
|
||||
isTyping.value = true
|
||||
|
||||
const currentRecord = new ChatRecord()
|
||||
currentRecord.create_time = new Date()
|
||||
currentRecord.chat_id = currentChatId.value
|
||||
currentRecord.question = inputMessage.value
|
||||
currentRecord.answer = ''
|
||||
|
||||
currentChat.value.records.push(currentRecord)
|
||||
|
||||
let error = false
|
||||
if (currentChatId.value === undefined) {
|
||||
chatApi.startChat({question: currentRecord.question.trim(), datasource: currentChat.value.datasource})
|
||||
.then((res) => {
|
||||
const chat = chatApi.toChatInfo(res)
|
||||
if (chat !== undefined) {
|
||||
chatList.value.unshift(chat)
|
||||
currentChatId.value = chat.id
|
||||
chat.records.push(currentRecord)
|
||||
currentChat.value = chat
|
||||
} else {
|
||||
error = true
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
isTyping.value = false
|
||||
error = true
|
||||
console.error(e)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
chat.messages.push({
|
||||
role: 'user',
|
||||
content: inputMessage.value
|
||||
})
|
||||
|
||||
const defaultMsg = 'Thinking...'
|
||||
chat.messages.push({
|
||||
role: 'assistant',
|
||||
content: defaultMsg,
|
||||
isTyping: true
|
||||
})
|
||||
|
||||
const userInput = inputMessage.value
|
||||
inputMessage.value = ''
|
||||
|
||||
const index = chat.messages.length - 1
|
||||
|
||||
if (error) return
|
||||
|
||||
try {
|
||||
const response = await questionApi.add({ question: userInput })
|
||||
const response = await questionApi.add({question: currentRecord.question, chat_id: currentChatId.value})
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
const {done, value} = await reader.read()
|
||||
if (done) {
|
||||
chat.messages[index].isTyping = false
|
||||
isTyping.value = false
|
||||
break
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value)
|
||||
const lines = chunk.split('\n\n')
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = JSON.parse(line.replace('data:', '').trim())
|
||||
let realContent = data.content
|
||||
|
||||
|
||||
switch (data.type) {
|
||||
case 'html':
|
||||
realContent = '\n' + data.content
|
||||
break
|
||||
|
||||
|
||||
case 'error':
|
||||
realContent = '\nError: ' + data.content
|
||||
break
|
||||
|
||||
|
||||
default:
|
||||
if (data.content) {
|
||||
const currentContent = chat.messages[index].content
|
||||
const newContent = currentContent.startsWith(defaultMsg)
|
||||
? realContent
|
||||
: currentContent + realContent
|
||||
|
||||
updateMessageContent(index, newContent)
|
||||
const newContent = currentRecord.answer + realContent
|
||||
|
||||
updateMessageContent(newContent)
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
updateMessageContent(currentRecord.answer + '\nError: Failed to get response')
|
||||
console.error('Error:', error)
|
||||
updateMessageContent(index, chat.messages[index].content + '\nError: Failed to get response')
|
||||
chat.messages[index].isTyping = false
|
||||
isTyping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => currentChat.value?.messages, () => {
|
||||
scrollToBottom()
|
||||
}, { deep: true })
|
||||
watch(() => currentChat.value?.records[currentChat.value.records.length - 1]?.answer, () => {
|
||||
//scrollToBottom()
|
||||
}, {deep: true})
|
||||
|
||||
const formatMessage = (content: string) => {
|
||||
if (!content) return ''
|
||||
return content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/ {2}/g, ' ')
|
||||
}
|
||||
|
||||
const handleCommand = (command: string, chatId: string) => {
|
||||
switch (command) {
|
||||
case 'rename':
|
||||
break
|
||||
case 'delete':
|
||||
chatHistory.value = chatHistory.value.filter(chat => chat.id !== chatId)
|
||||
if (currentChatId.value === chatId) {
|
||||
currentChatId.value = chatHistory.value[0]?.id || ''
|
||||
}
|
||||
break
|
||||
}
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/ {2}/g, ' ')
|
||||
}
|
||||
|
||||
const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
@@ -281,9 +290,9 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
const start = textarea.selectionStart
|
||||
const end = textarea.selectionEnd
|
||||
const value = textarea.value
|
||||
|
||||
|
||||
inputMessage.value = value.substring(0, start) + '\n' + value.substring(end)
|
||||
|
||||
|
||||
nextTick(() => {
|
||||
textarea.selectionStart = textarea.selectionEnd = start + 1
|
||||
})
|
||||
@@ -292,215 +301,30 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chat-container {
|
||||
height: calc(100vh - 61px);
|
||||
display: flex;
|
||||
background-color: var(--white);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
.chat-sidebar {
|
||||
width: 260px;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
.chat-container-left {
|
||||
|
||||
.new-chat {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
--el-aside-width: 260px;
|
||||
|
||||
.search-box {
|
||||
:deep(.el-input__wrapper) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
border-right: solid 1px rgba(0, 0, 0, 0.3);
|
||||
|
||||
.history-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
.chat-container-right-container {
|
||||
height: 100%;
|
||||
|
||||
.history-item {
|
||||
.chat-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #e6f4ff;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.more-actions {
|
||||
opacity: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
&:hover .more-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-list {
|
||||
padding-top: unset;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.chat-messages {
|
||||
// flex: 1;
|
||||
height: calc(100% - 188px);
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:not(:hover)::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 24px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.user {
|
||||
text-align: right;
|
||||
.message-content {
|
||||
background-color: #e6f4ff;
|
||||
}
|
||||
}
|
||||
|
||||
&.assistant {
|
||||
.message-content {
|
||||
background-color: #f5f5f5;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: inline-block;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
max-width: 80%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 40px 0;
|
||||
|
||||
.sub-text {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.examples {
|
||||
.example-item {
|
||||
background-color: #f5f5f5;
|
||||
padding: 12px;
|
||||
margin: 8px auto;
|
||||
max-width: 400px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #e6f4ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
padding: 16px 24px;
|
||||
z-index: 10;
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
background: #f5f5f5;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover, &:focus-within {
|
||||
border-color: var(--el-border-color-darker);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
resize: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-textarea__wrapper) {
|
||||
box-shadow: none !important;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.input-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
height: 36px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -160,7 +160,7 @@ const isEditMode = computed(() => true)
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.el-dropdown-link{
|
||||
.ed-dropdown-link{
|
||||
margin-top: 3px !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,56 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<el-row class="header">
|
||||
<div class="table-list_layout">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<el-button text :icon="ArrowLeft" @click="back()" />
|
||||
<el-button text style="color: #fff" :icon="ArrowLeft" @click="back()" />
|
||||
{{ props.dsName }}
|
||||
</div>
|
||||
</el-row>
|
||||
<el-row class="container" :gutter="20">
|
||||
<el-col :span="4">
|
||||
<el-button type="primary" @click="save()"> Save </el-button>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="left-side">
|
||||
Tables
|
||||
<el-input v-model="searchValue" placeholder="Search" />
|
||||
<el-input
|
||||
style="margin: 16px 0"
|
||||
v-model="searchValue"
|
||||
placeholder="Search"
|
||||
/>
|
||||
<div>
|
||||
<div v-for="item in tableList" class="table-item" @click="clickTable(item)">{{ item.table_name }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<div v-if="fieldList.length === 0">No data, select a table from left</div>
|
||||
<div v-else>
|
||||
<div style="display: flex;justify-content: space-between;align-items: center;">
|
||||
<div style="display: flex;justify-content: start;align-items: center;">
|
||||
<span>{{ currentTable.table_name }}</span>
|
||||
<span> | Comment: </span>
|
||||
<span>
|
||||
<el-input v-model="currentTable.custom_comment" />
|
||||
</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="save()">
|
||||
Save
|
||||
</el-button>
|
||||
<div
|
||||
v-for="item in tableList"
|
||||
class="list-item_primary"
|
||||
@click="clickTable(item)"
|
||||
>
|
||||
{{ item.table_name }}
|
||||
</div>
|
||||
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<div v-if="fieldList.length === 0">
|
||||
No data, select a table from left
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="display: flex; justify-content: start; align-items: center"
|
||||
>
|
||||
<span>{{ currentTable.table_name }}</span>
|
||||
<el-divider direction="vertical" />
|
||||
<span>Comment:</span>
|
||||
<el-input style="margin-left: 12px;" v-model="currentTable.custom_comment" />
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs
|
||||
v-model="activeName"
|
||||
class="demo-tabs"
|
||||
@tab-click="handleClick"
|
||||
>
|
||||
<el-tab-pane label="Table Schema" name="schema">
|
||||
<el-table :data="fieldList" style="width: 100%">
|
||||
<el-table-column prop="field_name" label="Name" width="180" />
|
||||
@@ -45,111 +66,138 @@
|
||||
<el-table-column label="Status" width="180">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-switch
|
||||
v-model="scope.row.checked"
|
||||
size="small"
|
||||
/>
|
||||
<el-switch v-model="scope.row.checked" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Preview" name="preview">
|
||||
<div>Preview 100 items</div>
|
||||
<el-table :data="previewData.data" style="width: 100%;height: 600px;">
|
||||
<el-table-column v-for="c in previewData.fields" :prop="c" :label="c"/>
|
||||
<div style="margin: 16px 0;">Preview 100 items</div>
|
||||
<el-table
|
||||
:data="previewData.data"
|
||||
style="width: 100%; height: 600px"
|
||||
>
|
||||
<el-table-column
|
||||
v-for="c in previewData.fields"
|
||||
:prop="c"
|
||||
:label="c"
|
||||
/>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="tsx" setup>
|
||||
import { ref } from 'vue'
|
||||
import { datasourceApi } from '@/api/datasource'
|
||||
import { onMounted } from 'vue'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import type { TabsPaneContext } from 'element-plus'
|
||||
import { ref } from "vue";
|
||||
import { datasourceApi } from "@/api/datasource";
|
||||
import { onMounted } from "vue";
|
||||
import { ArrowLeft } from "@element-plus/icons-vue";
|
||||
import type { TabsPaneContext } from "element-plus-secondary";
|
||||
|
||||
const props = defineProps({
|
||||
dsId: { type: [Number], required: true },
|
||||
dsName: { type: [String], required: true }
|
||||
})
|
||||
dsName: { type: [String], required: true },
|
||||
});
|
||||
|
||||
const dsId = ref<Number>(0)
|
||||
const searchValue = ref('')
|
||||
const tableList = ref<any>([])
|
||||
const currentTable = ref<any>({})
|
||||
const fieldList = ref<any>([])
|
||||
const previewData = ref<any>({})
|
||||
const dsId = ref<Number>(0);
|
||||
const searchValue = ref("");
|
||||
const tableList = ref<any>([]);
|
||||
const currentTable = ref<any>({});
|
||||
const fieldList = ref<any>([]);
|
||||
const previewData = ref<any>({});
|
||||
|
||||
const activeName = ref('schema')
|
||||
const activeName = ref("schema");
|
||||
|
||||
const buildData = () => {
|
||||
return {"table": currentTable.value, "fields": fieldList.value}
|
||||
}
|
||||
return { table: currentTable.value, fields: fieldList.value };
|
||||
};
|
||||
|
||||
const back = () => {
|
||||
history.back()
|
||||
}
|
||||
history.back();
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
datasourceApi.edit(buildData()).then(() => {
|
||||
ElMessage({
|
||||
message: 'Save success',
|
||||
type: 'success',
|
||||
showClose: true
|
||||
})
|
||||
})
|
||||
}
|
||||
message: "Save success",
|
||||
type: "success",
|
||||
showClose: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const clickTable = (table: any) => {
|
||||
currentTable.value = table
|
||||
currentTable.value = table;
|
||||
datasourceApi.fieldList(table.id).then((res) => {
|
||||
fieldList.value = res
|
||||
fieldList.value = res;
|
||||
datasourceApi.previewData(dsId.value, buildData()).then((res) => {
|
||||
previewData.value = res
|
||||
})
|
||||
})
|
||||
}
|
||||
previewData.value = res;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = (tab: TabsPaneContext) => {
|
||||
if(tab.paneName === 'preview') {
|
||||
if (tab.paneName === "preview") {
|
||||
datasourceApi.previewData(dsId.value, buildData()).then((res) => {
|
||||
previewData.value = res
|
||||
})
|
||||
previewData.value = res;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
dsId.value = props.dsId
|
||||
fieldList.value = []
|
||||
dsId.value = props.dsId;
|
||||
fieldList.value = [];
|
||||
datasourceApi.tableList(props.dsId).then((res) => {
|
||||
tableList.value = res
|
||||
})
|
||||
})
|
||||
tableList.value = res;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout{
|
||||
padding: 20px 40px;
|
||||
.header{
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.title{
|
||||
.table-list_layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.header {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
background: #050e21;
|
||||
box-shadow: 0 2px 4px #1f23291f;
|
||||
}
|
||||
.container{
|
||||
height: 100%;
|
||||
.table-item{
|
||||
margin: 10px 0;
|
||||
.title {
|
||||
color: #fff;
|
||||
font-family: var(--de-custom_font, "PingFang");
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
position: relative;
|
||||
}
|
||||
.container {
|
||||
height: calc(100% - 56px);
|
||||
width: 100%;
|
||||
.left-side {
|
||||
width: 246px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
border-right: 1px solid #ccc;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.right-side {
|
||||
width: calc(100% - 246px);
|
||||
height: 100%;
|
||||
float: right;
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
+282
-202
@@ -2,17 +2,36 @@
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="Add Datasource"
|
||||
width="500"
|
||||
width="600"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
@closed="close"
|
||||
modal-class="add-datasource_dialog"
|
||||
>
|
||||
<el-steps v-show="isCreate" style="max-width: 600px" :active="active" align-center>
|
||||
<el-step title="Base Info" />
|
||||
<el-step title="Choose Tables" />
|
||||
</el-steps>
|
||||
<div v-show="active === 1" class="container">
|
||||
<el-form :model="form" label-width="auto" ref="dsFormRef" :rules="rules">
|
||||
<template #header="{ close, titleId, titleClass }">
|
||||
<div style="display: flex">
|
||||
<div style="margin-right: 24px">Add Datasource</div>
|
||||
<el-steps
|
||||
v-show="isCreate"
|
||||
:active="active"
|
||||
align-center
|
||||
custom
|
||||
style="max-width: 400px; flex: 1"
|
||||
>
|
||||
<el-step title="Base Info" />
|
||||
<el-step title="Choose Tables" />
|
||||
</el-steps>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-show="active === 0" class="container">
|
||||
<el-form
|
||||
:model="form"
|
||||
label-position="top"
|
||||
label-width="auto"
|
||||
ref="dsFormRef"
|
||||
:rules="rules"
|
||||
>
|
||||
<el-form-item label="Name" prop="name">
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
@@ -20,7 +39,11 @@
|
||||
<el-input v-model="form.description" :rows="2" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Type">
|
||||
<el-select v-model="form.type" placeholder="Select Type" :disabled="!isCreate">
|
||||
<el-select
|
||||
v-model="form.type"
|
||||
placeholder="Select Type"
|
||||
:disabled="!isCreate"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in dsType"
|
||||
:key="item.value"
|
||||
@@ -75,290 +98,347 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="Schema" v-if="haveSchema.includes(form.type)">
|
||||
<el-input v-model="config.dbSchema" />
|
||||
<el-button link type="primary" :icon="Plus" v-if="false">Get Schema</el-button>
|
||||
<el-button link type="primary" :icon="Plus" v-if="false"
|
||||
>Get Schema</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
<span v-if="form.type === 'sqlServer'">Supported version: 2012+</span>
|
||||
<span v-else-if="form.type === 'oracle'">Supported version: 12+</span>
|
||||
<span v-else-if="form.type === 'mysql'">Supported version: 5.6+</span>
|
||||
<span v-else-if="form.type === 'pg'">Supported version: 9.6+</span>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-show="active === 2" class="container">
|
||||
<el-scrollbar>
|
||||
<el-checkbox-group v-model="checkList">
|
||||
<el-row :gutter="10">
|
||||
<el-col v-for="item in tableList" :key="item.value" :span="12">
|
||||
<el-checkbox :label="item.tableName">{{ item.tableName }}</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-checkbox-group>
|
||||
</el-scrollbar>
|
||||
<div v-show="active === 1" class="container" v-loading="tableListLoading">
|
||||
<el-checkbox-group v-model="checkList" style="position: relative">
|
||||
<FixedSizeList
|
||||
:itemSize="40"
|
||||
:data="tableList"
|
||||
:total="tableList.length"
|
||||
:width="560"
|
||||
:height="400"
|
||||
:scrollbarAlwaysOn="true"
|
||||
class-name="el-select-dropdown__list"
|
||||
layout="vertical"
|
||||
>
|
||||
<template #default="{ index, style }">
|
||||
<div class="list-item_primary" :style="style">
|
||||
<el-checkbox :label="tableList[index].tableName">{{
|
||||
tableList[index].tableName
|
||||
}}</el-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
</FixedSizeList>
|
||||
</el-checkbox-group>
|
||||
<span>Selected: {{ checkList.length }}/{{ tableList.length }}</span>
|
||||
</div>
|
||||
<div style="display: flex;justify-content: flex-end;margin-top: 20px;">
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
|
||||
<el-button @click="close">Cancel</el-button>
|
||||
<el-button v-show="!isCreate && !isEditTable && form.type !== 'excel'" @click="check">Test Connect</el-button>
|
||||
<el-button v-show="active === 1 && isCreate" type="primary" @click="next(dsFormRef)">Next</el-button>
|
||||
<el-button v-show="active === 2 && isCreate" @click="preview">Preview</el-button>
|
||||
<el-button v-show="active === 2 || !isCreate" type="primary" @click="save(dsFormRef)">Save</el-button>
|
||||
<el-button
|
||||
v-show="!isCreate && !isEditTable && form.type !== 'excel'"
|
||||
@click="check"
|
||||
>Test Connect</el-button
|
||||
>
|
||||
<el-button
|
||||
v-show="active === 0 && isCreate"
|
||||
type="primary"
|
||||
@click="next(dsFormRef)"
|
||||
>Next</el-button
|
||||
>
|
||||
<el-button v-show="active === 1 && isCreate" @click="preview"
|
||||
>Preview</el-button
|
||||
>
|
||||
<el-button
|
||||
v-show="active === 1 || !isCreate"
|
||||
type="primary"
|
||||
@click="save(dsFormRef)"
|
||||
>Save</el-button
|
||||
>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { datasourceApi } from '@/api/datasource'
|
||||
import { encrypted, decrypted } from './js/aes'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { useCache } from '@/utils/useCache'
|
||||
import { dsType, haveSchema} from '@/views/ds/js/ds-type'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
const dsFormRef = ref<FormInstance>()
|
||||
const emit = defineEmits(['refresh'])
|
||||
const active = ref(1)
|
||||
const isCreate = ref(true)
|
||||
const isEditTable = ref(false)
|
||||
const checkList = ref<any>([])
|
||||
const tableList = ref<any>([])
|
||||
const excelUploadSuccess = ref(false)
|
||||
|
||||
const token = wsCache.get('user.token')
|
||||
const headers = ref<any>({'X-SQLBOT-TOKEN': `Bearer ${token}`})
|
||||
import { ref, reactive } from "vue";
|
||||
import { datasourceApi } from "@/api/datasource";
|
||||
import { encrypted, decrypted } from "./js/aes";
|
||||
import { ElMessage } from "element-plus-secondary";
|
||||
import type { FormInstance, FormRules } from "element-plus-secondary";
|
||||
import FixedSizeList from "element-plus-secondary/es/components/virtual-list/src/components/fixed-size-list.mjs";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { useCache } from "@/utils/useCache";
|
||||
import { dsType, haveSchema } from "@/views/ds/js/ds-type";
|
||||
|
||||
const { wsCache } = useCache();
|
||||
const dsFormRef = ref<FormInstance>();
|
||||
const emit = defineEmits(["refresh"]);
|
||||
const active = ref(0);
|
||||
const isCreate = ref(true);
|
||||
const isEditTable = ref(false);
|
||||
const checkList = ref<any>([]);
|
||||
const tableList = ref<any>([]);
|
||||
const excelUploadSuccess = ref(false);
|
||||
const tableListLoading = ref(true);
|
||||
const token = wsCache.get("user.token");
|
||||
const headers = ref<any>({ "X-SQLBOT-TOKEN": `Bearer ${token}` });
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: 'Please input name', trigger: 'blur' },
|
||||
{ min: 1, max: 50, message: 'Length should be 1 to 50', trigger: 'blur' },
|
||||
{ required: true, message: "Please input name", trigger: "blur" },
|
||||
{ min: 1, max: 50, message: "Length should be 1 to 50", trigger: "blur" },
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const dialogVisible = ref<boolean>(false);
|
||||
const form = ref<any>({
|
||||
name:'',
|
||||
description:'',
|
||||
type:'mysql',
|
||||
configuration: ''
|
||||
})
|
||||
name: "",
|
||||
description: "",
|
||||
type: "mysql",
|
||||
configuration: "",
|
||||
});
|
||||
const config = ref<any>({
|
||||
driver:'',
|
||||
host:'',
|
||||
port:0,
|
||||
username:'',
|
||||
password:'',
|
||||
database:'',
|
||||
extraJdbc:'',
|
||||
dbSchema:'',
|
||||
filename:'',
|
||||
driver: "",
|
||||
host: "",
|
||||
port: 0,
|
||||
username: "",
|
||||
password: "",
|
||||
database: "",
|
||||
extraJdbc: "",
|
||||
dbSchema: "",
|
||||
filename: "",
|
||||
sheets: [],
|
||||
mode:'service_name'
|
||||
})
|
||||
mode: "service_name",
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
dialogVisible.value = false
|
||||
isCreate.value = true
|
||||
active.value = 1
|
||||
isEditTable.value = false
|
||||
checkList.value = []
|
||||
tableList.value = []
|
||||
excelUploadSuccess.value = false
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
isCreate.value = true;
|
||||
active.value = 0;
|
||||
isEditTable.value = false;
|
||||
checkList.value = [];
|
||||
tableList.value = [];
|
||||
excelUploadSuccess.value = false;
|
||||
};
|
||||
|
||||
const open = (item: any, editTable: boolean = false) => {
|
||||
isEditTable.value = false
|
||||
isEditTable.value = false;
|
||||
if (item) {
|
||||
isCreate.value = false
|
||||
form.value.id = item.id
|
||||
form.value.name = item.name
|
||||
form.value.description = item.description
|
||||
form.value.type = item.type
|
||||
form.value.configuration = item.configuration
|
||||
if(item.configuration) {
|
||||
const configuration = JSON.parse(decrypted(item.configuration))
|
||||
config.value.host = configuration.host
|
||||
config.value.port = configuration.port
|
||||
config.value.username = configuration.username
|
||||
config.value.password = configuration.password
|
||||
config.value.database = configuration.database
|
||||
config.value.extraJdbc = configuration.extraJdbc
|
||||
config.value.dbSchema = configuration.dbSchema
|
||||
config.value.filename = configuration.filename
|
||||
config.value.sheets = configuration.sheets
|
||||
config.value.mode = configuration.mode
|
||||
isCreate.value = false;
|
||||
form.value.id = item.id;
|
||||
form.value.name = item.name;
|
||||
form.value.description = item.description;
|
||||
form.value.type = item.type;
|
||||
form.value.configuration = item.configuration;
|
||||
if (item.configuration) {
|
||||
const configuration = JSON.parse(decrypted(item.configuration));
|
||||
config.value.host = configuration.host;
|
||||
config.value.port = configuration.port;
|
||||
config.value.username = configuration.username;
|
||||
config.value.password = configuration.password;
|
||||
config.value.database = configuration.database;
|
||||
config.value.extraJdbc = configuration.extraJdbc;
|
||||
config.value.dbSchema = configuration.dbSchema;
|
||||
config.value.filename = configuration.filename;
|
||||
config.value.sheets = configuration.sheets;
|
||||
config.value.mode = configuration.mode;
|
||||
}
|
||||
|
||||
if (editTable) {
|
||||
active.value = 2
|
||||
isEditTable.value = true
|
||||
isCreate.value = false
|
||||
active.value = 1;
|
||||
isEditTable.value = true;
|
||||
isCreate.value = false;
|
||||
// request tables and check tables
|
||||
|
||||
|
||||
datasourceApi.tableList(item.id).then((res) => {
|
||||
checkList.value = res.map((ele:any) => {return ele.table_name})
|
||||
if(item.type === 'excel') {
|
||||
tableList.value = config.value.sheets
|
||||
checkList.value = res.map((ele: any) => {
|
||||
return ele.table_name;
|
||||
});
|
||||
if (item.type === "excel") {
|
||||
tableList.value = config.value.sheets;
|
||||
} else {
|
||||
datasourceApi.getTablesByConf(form.value).then((table) => {
|
||||
tableList.value = table
|
||||
checkList.value = checkList.value.filter((ele:string) => {return table.map((ele:any) => {return ele.tableName}).includes(ele)})
|
||||
})
|
||||
tableListLoading.value = true;
|
||||
datasourceApi
|
||||
.getTablesByConf(form.value)
|
||||
.then((table) => {
|
||||
tableList.value = table;
|
||||
checkList.value = checkList.value.filter((ele: string) => {
|
||||
return table
|
||||
.map((ele: any) => {
|
||||
return ele.tableName;
|
||||
})
|
||||
.includes(ele);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
tableListLoading.value = false;
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
} else {
|
||||
isCreate.value = true
|
||||
isEditTable.value = false
|
||||
checkList.value = []
|
||||
tableList.value = []
|
||||
isCreate.value = true;
|
||||
isEditTable.value = false;
|
||||
checkList.value = [];
|
||||
tableList.value = [];
|
||||
form.value = {
|
||||
name:'',
|
||||
description:'',
|
||||
type:'mysql',
|
||||
configuration: ''
|
||||
}
|
||||
name: "",
|
||||
description: "",
|
||||
type: "mysql",
|
||||
configuration: "",
|
||||
};
|
||||
config.value = {
|
||||
driver:'',
|
||||
host:'',
|
||||
port:0,
|
||||
username:'',
|
||||
password:'',
|
||||
database:'',
|
||||
extraJdbc:'',
|
||||
dbSchema:'',
|
||||
filename:'',
|
||||
driver: "",
|
||||
host: "",
|
||||
port: 0,
|
||||
username: "",
|
||||
password: "",
|
||||
database: "",
|
||||
extraJdbc: "",
|
||||
dbSchema: "",
|
||||
filename: "",
|
||||
sheets: [],
|
||||
mode:'service_name'
|
||||
}
|
||||
mode: "service_name",
|
||||
};
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const save = async(formEl: FormInstance | undefined) => {
|
||||
if(!formEl) return
|
||||
const save = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
const list = tableList.value
|
||||
.filter((ele: any) => {return checkList.value.includes(ele.tableName)})
|
||||
.map((ele: any) => {return {"table_name": ele.tableName, "table_comment": ele.tableComment}})
|
||||
.filter((ele: any) => {
|
||||
return checkList.value.includes(ele.tableName);
|
||||
})
|
||||
.map((ele: any) => {
|
||||
return { table_name: ele.tableName, table_comment: ele.tableComment };
|
||||
});
|
||||
|
||||
buildConf()
|
||||
buildConf();
|
||||
if (form.value.id) {
|
||||
if (!isEditTable.value) {
|
||||
// only update datasource config info
|
||||
datasourceApi.update(form.value).then((res) => {
|
||||
console.log(res)
|
||||
close()
|
||||
emit('refresh')
|
||||
})
|
||||
console.log(res);
|
||||
close();
|
||||
emit("refresh");
|
||||
});
|
||||
} else {
|
||||
// save table and field
|
||||
datasourceApi.chooseTables(form.value.id, list).then(() => {
|
||||
close()
|
||||
})
|
||||
close();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
form.value.tables = list
|
||||
form.value.tables = list;
|
||||
datasourceApi.add(form.value).then((res: any) => {
|
||||
console.log(res)
|
||||
close()
|
||||
emit('refresh')
|
||||
})
|
||||
console.log(res);
|
||||
close();
|
||||
emit("refresh");
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const buildConf = () => {
|
||||
form.value.configuration = encrypted(JSON.stringify({
|
||||
host:config.value.host,
|
||||
port:config.value.port,
|
||||
username:config.value.username,
|
||||
password:config.value.password,
|
||||
database:config.value.database,
|
||||
extraJdbc:config.value.extraJdbc,
|
||||
dbSchema:config.value.dbSchema,
|
||||
filename:config.value.filename,
|
||||
sheets:config.value.sheets,
|
||||
mode:config.value.mode
|
||||
}))
|
||||
}
|
||||
form.value.configuration = encrypted(
|
||||
JSON.stringify({
|
||||
host: config.value.host,
|
||||
port: config.value.port,
|
||||
username: config.value.username,
|
||||
password: config.value.password,
|
||||
database: config.value.database,
|
||||
extraJdbc: config.value.extraJdbc,
|
||||
dbSchema: config.value.dbSchema,
|
||||
filename: config.value.filename,
|
||||
sheets: config.value.sheets,
|
||||
mode: config.value.mode,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const check = () => {
|
||||
buildConf()
|
||||
buildConf();
|
||||
datasourceApi.check(form.value).then((res: any) => {
|
||||
if(res) {
|
||||
if (res) {
|
||||
ElMessage({
|
||||
message: 'Connect success',
|
||||
type: 'success',
|
||||
showClose: true
|
||||
})
|
||||
message: "Connect success",
|
||||
type: "success",
|
||||
showClose: true,
|
||||
});
|
||||
} else {
|
||||
ElMessage({
|
||||
message: 'Connect failed',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
message: "Connect failed",
|
||||
type: "error",
|
||||
showClose: true,
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const next = async(formEl: FormInstance | undefined) => {
|
||||
if(!formEl) return
|
||||
const next = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (form.value.type === "excel") {
|
||||
// next, show tables
|
||||
// next, show tables
|
||||
if (excelUploadSuccess.value) {
|
||||
active.value++
|
||||
active.value++;
|
||||
}
|
||||
} else {
|
||||
// check status if success do next
|
||||
buildConf()
|
||||
buildConf();
|
||||
datasourceApi.check(form.value).then((res: boolean) => {
|
||||
if(res) {
|
||||
active.value++
|
||||
if (res) {
|
||||
active.value++;
|
||||
// request tables
|
||||
datasourceApi.getTablesByConf(form.value).then((res) => {
|
||||
tableList.value = res
|
||||
})
|
||||
tableList.value = res;
|
||||
});
|
||||
} else {
|
||||
ElMessage({
|
||||
message: 'Connect failed',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
message: "Connect failed",
|
||||
type: "error",
|
||||
showClose: true,
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const preview = () => {
|
||||
active.value--
|
||||
}
|
||||
active.value--;
|
||||
};
|
||||
|
||||
const beforeUpload = (rawFile: any) => {
|
||||
if (rawFile.size / 1024 / 1024 > 50) {
|
||||
ElMessage.error('File size can not exceed 50MB!')
|
||||
return false
|
||||
ElMessage.error("File size can not exceed 50MB!");
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const onSuccess = (response: any) => {
|
||||
config.value.filename = response.data.filename
|
||||
config.value.sheets = response.data.sheets
|
||||
tableList.value = response.data.sheets
|
||||
excelUploadSuccess.value = true
|
||||
}
|
||||
config.value.filename = response.data.filename;
|
||||
config.value.sheets = response.data.sheets;
|
||||
tableList.value = response.data.sheets;
|
||||
excelUploadSuccess.value = true;
|
||||
};
|
||||
|
||||
defineExpose({ open })
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.container{
|
||||
height: 600px;
|
||||
margin-top: 20px;
|
||||
<style lang="less">
|
||||
.add-datasource_dialog {
|
||||
.container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
.ed-vl__window.ed-select-dropdown__list::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -65,7 +65,7 @@ import { ref, onMounted } from 'vue'
|
||||
import DsForm from './form.vue'
|
||||
import { datasourceApi } from '@/api/datasource'
|
||||
import { datetimeFormat } from '@/utils/utils'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { ElMessageBox } from 'element-plus-secondary'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const searchValue = ref<string>('')
|
||||
@@ -148,7 +148,7 @@ const getTables = (id: number, name: string) => {
|
||||
// datasourceApi.execSql(id,'select id,name,table_name from core_dataset_table limit 10').then((res) => {
|
||||
// console.log(res)
|
||||
// })
|
||||
router.push(`/ds/dsTable/${id}/${name}`)
|
||||
router.push(`/dsTable/${id}/${name}`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -180,7 +180,6 @@ onMounted(() => {
|
||||
position: relative;
|
||||
border: 1px solid #dadce0;
|
||||
transition: all 0.2s ease;
|
||||
height: 110px;
|
||||
align-items: center;
|
||||
}
|
||||
.connection-icon {
|
||||
@@ -264,6 +263,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
.action-btn {
|
||||
display: none;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
:hover .action-btn{
|
||||
|
||||
@@ -140,7 +140,7 @@ const submitForm = () => {
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
:deep(.ed-input__wrapper) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
@@ -262,7 +262,7 @@ onMounted(() => {
|
||||
}
|
||||
.sqlbot-table {
|
||||
border-radius: 6px;
|
||||
:deep(.el-table) {
|
||||
:deep(.ed-table) {
|
||||
--el-table-header-bg-color: #f5f7fa;
|
||||
--el-table-border-color: #ebeef5;
|
||||
--el-table-header-text-color: #606266;
|
||||
@@ -303,19 +303,19 @@ onMounted(() => {
|
||||
}
|
||||
.sqlbot-form {
|
||||
width: 100%;
|
||||
.el-form-item {
|
||||
.ed-form-item {
|
||||
margin-bottom: 16px;
|
||||
text-align: left;
|
||||
label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
/* .el-input {
|
||||
/* .ed-input {
|
||||
--el-input-border-color: #e5e8ed;
|
||||
--el-input-border-radius: 4px;
|
||||
--el-input-inner-height: 40px !important;
|
||||
}
|
||||
.el-select {
|
||||
.ed-select {
|
||||
--el-select-bg-color: #f5f7fa;
|
||||
--el-select-border-color: #e5e8ed;
|
||||
--el-select-border-radius: 4px;
|
||||
@@ -326,7 +326,7 @@ onMounted(() => {
|
||||
.sqlbot-dialog {
|
||||
width: 500px;
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
:deep(.ed-dialog__header) {
|
||||
text-align: left !important;
|
||||
font-size: 16px;
|
||||
background-color: #f5f7fa;
|
||||
|
||||
@@ -43,9 +43,9 @@ const handleClick = (tab: any, event: MouseEvent) => {
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
padding: 24px;
|
||||
:deep(.el-tabs) {
|
||||
:deep(.ed-tabs) {
|
||||
--el-tabs-header-height: 55px;
|
||||
.el-tabs__header {
|
||||
.ed-tabs__header {
|
||||
margin: 0 0 24px !important;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ const handleClick = (tab: any, event: MouseEvent) => {
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
:deep(.el-tabs__item) {
|
||||
:deep(.ed-tabs__item) {
|
||||
padding: 0 16px !important;
|
||||
&:hover {
|
||||
// color: var(--primary-color);
|
||||
|
||||
@@ -433,7 +433,7 @@ const batchEnableHandler = () => {
|
||||
}
|
||||
.sqlbot-table {
|
||||
border-radius: 6px;
|
||||
:deep(.el-table) {
|
||||
:deep(.ed-table) {
|
||||
--el-table-header-bg-color: #f5f7fa;
|
||||
--el-table-border-color: #ebeef5;
|
||||
--el-table-header-text-color: #606266;
|
||||
@@ -487,7 +487,7 @@ const batchEnableHandler = () => {
|
||||
}
|
||||
.sqlbot-form {
|
||||
width: 100%;
|
||||
.el-form-item {
|
||||
.ed-form-item {
|
||||
margin-bottom: 16px;
|
||||
text-align: left;
|
||||
label {
|
||||
@@ -499,7 +499,7 @@ const batchEnableHandler = () => {
|
||||
.sqlbot-dialog {
|
||||
width: 500px;
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
:deep(.ed-dialog__header) {
|
||||
text-align: left !important;
|
||||
font-size: 16px;
|
||||
background-color: #f5f7fa;
|
||||
|
||||
@@ -315,7 +315,7 @@ onMounted(() => {
|
||||
}
|
||||
.sqlbot-table {
|
||||
border-radius: 6px;
|
||||
:deep(.el-table) {
|
||||
:deep(.ed-table) {
|
||||
--el-table-header-bg-color: #f5f7fa;
|
||||
--el-table-border-color: #ebeef5;
|
||||
--el-table-header-text-color: #606266;
|
||||
@@ -369,7 +369,7 @@ onMounted(() => {
|
||||
}
|
||||
.sqlbot-form {
|
||||
width: 100%;
|
||||
.el-form-item {
|
||||
.ed-form-item {
|
||||
margin-bottom: 16px;
|
||||
text-align: left;
|
||||
label {
|
||||
@@ -381,7 +381,7 @@ onMounted(() => {
|
||||
.sqlbot-dialog {
|
||||
width: 500px;
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
:deep(.ed-dialog__header) {
|
||||
text-align: left !important;
|
||||
font-size: 16px;
|
||||
background-color: #f5f7fa;
|
||||
|
||||
Reference in New Issue
Block a user