Merge remote-tracking branch 'origin/main'

This commit is contained in:
wangjiahao
2025-06-05 16:10:06 +08:00
13 changed files with 510 additions and 231 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ async def chats(session: SessionDep, current_user: CurrentUser):
@router.get("/get/{chart_id}")
async def list_chat(session: SessionDep, current_user: CurrentUser, chart_id: int):
async def get_chat(session: SessionDep, current_user: CurrentUser, chart_id: int):
try:
return get_chat_with_records(chart_id=chart_id, session=session, current_user=current_user)
except Exception as e:
+12 -2
View File
@@ -2,6 +2,7 @@ import datetime
from typing import List
from sqlalchemy import and_
from sqlalchemy.orm import load_only
from apps.chat.models.chat_model import Chat, ChatRecord, CreateChat, ChatInfo, RenameChat, ChatQuestion
from apps.datasource.models.datasource import CoreDatasource
@@ -55,7 +56,11 @@ def get_chat_with_records(session: SessionDep, chart_id: int, current_user: Curr
chat_info.datasource_exists = True
chat_info.datasource_name = ds.name
record_list = session.query(ChatRecord).filter(
record_list = session.query(ChatRecord).options(
load_only(ChatRecord.id, ChatRecord.chat_id, ChatRecord.create_time, ChatRecord.finish_time,
ChatRecord.question, ChatRecord.sql_answer, ChatRecord.sql, ChatRecord.data,
ChatRecord.chart_answer, ChatRecord.chart, ChatRecord.finish, ChatRecord.error,
ChatRecord.run_time)).filter(
and_(Chat.create_by == current_user.id, ChatRecord.chat_id == chart_id)).order_by(ChatRecord.create_time).all()
chat_info.records = record_list
@@ -96,6 +101,9 @@ def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj:
chat_info.id = chat.id
session.commit()
chat_info.datasource_exists = True
chat_info.datasource_name = ds.name
return chat_info
@@ -214,6 +222,7 @@ def save_error_message(session: SessionDep, record_id: int, message: str) -> Cha
record = session.query(ChatRecord).filter(ChatRecord.id == record_id).first()
record.error = message
record.finish = True
record.finish_time = datetime.datetime.now()
result = ChatRecord(**record.model_dump())
@@ -242,11 +251,13 @@ def save_sql_exec_data(session: SessionDep, record_id: int, data: str) -> ChatRe
return result
def finish_record(session: SessionDep, record_id: int) -> ChatRecord:
if not record_id:
raise Exception("Record id cannot be None")
record = session.query(ChatRecord).filter(ChatRecord.id == record_id).first()
record.finish = True
record.finish_time = datetime.datetime.now()
result = ChatRecord(**record.model_dump())
@@ -257,4 +268,3 @@ def finish_record(session: SessionDep, record_id: int) -> ChatRecord:
session.commit()
return result
+1 -1
View File
@@ -2,7 +2,7 @@ from sqlmodel import SQLModel, Field
from sqlalchemy import Column, Text, BigInteger, DateTime, Integer, Identity, Boolean
from datetime import datetime
from pydantic import BaseModel
from typing import List, Optional
from typing import List, Optional, Any
from apps.template.generate_chart.generator import get_chart_template
from apps.template.generate_sql.generator import get_sql_template
+12 -2
View File
@@ -9,10 +9,10 @@ from fastapi import APIRouter, File, UploadFile, HTTPException
from apps.db.engine import create_table, get_data_engine, insert_data
from common.core.deps import SessionDep, CurrentUser
from ..crud.datasource import get_datasource_list, check_status, create_ds, update_ds, delete_ds, getTables, getFields, \
execSql, update_table_and_fields, getTablesByDs, chooseTables, preview
execSql, update_table_and_fields, getTablesByDs, chooseTables, preview, updateTable, updateField
from ..crud.field import get_fields_by_table_id
from ..crud.table import get_tables_by_ds_id
from ..models.datasource import CoreDatasource, CreateDatasource, TableObj, CoreTable
from ..models.datasource import CoreDatasource, CreateDatasource, TableObj, CoreTable, CoreField
router = APIRouter(tags=["datasource"], prefix="/datasource")
path = "/opt/sqlbot/data/excel"
@@ -83,6 +83,16 @@ async def edit_local(session: SessionDep, data: TableObj):
update_table_and_fields(session, data)
@router.post("/editTable")
async def edit_table(session: SessionDep, table: CoreTable):
updateTable(session, table)
@router.post("/editField")
async def edit_field(session: SessionDep, field: CoreField):
updateField(session, field)
@router.post("/previewData/{id}")
async def edit_local(session: SessionDep, id: int, data: TableObj):
return preview(session, id, data)
@@ -192,6 +192,14 @@ def update_table_and_fields(session: SessionDep, data: TableObj):
update_field(session, field)
def updateTable(session: SessionDep, table: CoreTable):
update_table(session, table)
def updateField(session: SessionDep, field: CoreField):
update_field(session, field)
def preview(session: SessionDep, id: int, data: TableObj):
ds = session.query(CoreDatasource).filter(CoreDatasource.id == id).first()
conf = DatasourceConf(**json.loads(aes_decrypt(ds.configuration))) if ds.type != "excel" else get_engine_config()
@@ -225,6 +233,8 @@ def get_table_obj_by_ds(session: SessionDep, ds: CoreDatasource) -> List[TableAn
def get_table_schema(session: SessionDep, ds: CoreDatasource) -> str:
schema_str = ""
table_objs = get_table_obj_by_ds(session=session, ds=ds)
if len(table_objs) == 0:
return schema_str
db_name = table_objs[0].schema
schema_str += f"【DB_ID】 {db_name}\n【Schema】\n"
for obj in table_objs:
+1 -1
View File
@@ -12,7 +12,7 @@ from common.core.config import settings
def get_engine_config():
return DatasourceConf(username=settings.POSTGRES_USER, password=settings.POSTGRES_PASSWORD,
host="127.0.0.1", port=settings.POSTGRES_PORT, database=settings.POSTGRES_DB,
host=settings.POSTGRES_SERVER, port=settings.POSTGRES_PORT, database=settings.POSTGRES_DB,
dbSchema="public")
+20 -11
View File
@@ -20,6 +20,7 @@ export interface ChatMessage {
role: 'user' | 'assistant'
create_time?: Date | string
content?: string | number
record?: ChatRecord
isTyping?: boolean
isWelcome?: boolean
}
@@ -28,24 +29,32 @@ export class ChatRecord {
id?: number
chat_id?: number
create_time?: Date | string
create_by?: number
datasource?: number
engine_type?: string
finish_time?: Date | string
question?: string
answer?: string
sql_answer?: string
sql?: string
data?: string
chart_answer?: string
chart?: string
finish?: boolean = false
error?: string
run_time: number = 0
constructor()
constructor(id: number, chat_id: number, create_time: Date | string, create_by: number, datasource: number, engine_type: string, question: string, answer: string, run_time: number)
constructor(id?: number, chat_id?: number, create_time?: Date | string, create_by?: number, datasource?: number, engine_type?: string, question?: string, answer?: string, run_time?: number) {
constructor(id: number, chat_id: number, create_time: Date | string, finish_time: Date | string | undefined, question: string, sql_answer: string | undefined, sql: string | undefined, data: string | undefined, chart_answer: string | undefined, chart: string | undefined, finish: boolean, error: string | undefined, run_time: number)
constructor(id?: number, chat_id?: number, create_time?: Date | string, finish_time?: Date | string, question?: string, sql_answer?: string, sql?: string, data?: string, chart_answer?: string, chart?: string, finish?: boolean, error?: string, run_time?: number) {
this.id = id
this.chat_id = chat_id
this.create_time = getDate(create_time)
this.create_by = create_by
this.datasource = datasource
this.engine_type = engine_type
this.finish_time = getDate(finish_time)
this.question = question
this.answer = answer
this.sql_answer = sql_answer
this.sql = sql
this.data = data
this.chart_answer = chart_answer
this.chart = chart
this.finish = finish
this.error = error
this.run_time = run_time ?? 0
}
}
@@ -111,7 +120,7 @@ function toChatRecord(data?: any): ChatRecord | undefined {
if (!data) {
return undefined
}
return new ChatRecord(data.id, data.number, data.create_time, data.create_by, data.datasource, data.engine_type, data.question, data.answer, data.run_time)
return new ChatRecord(data.id, data.chat_id, data.create_time, data.finish_time, data.question, data.sql_answer, data.sql, data.data, data.chart_answer, data.chart, data.finish, data.error, data.run_time)
}
function toChatRecordList(list: any = []): ChatRecord[] {
+2
View File
@@ -15,4 +15,6 @@ export const datasourceApi = {
fieldList: (id: Number) => request.post(`/datasource/fieldList/${id}`),
edit: (data: any) => request.post('/datasource/editLocalComment', data),
previewData: (id: Number, data: any) => request.post(`/datasource/previewData/${id}`, data),
saveTable: (data: any) => request.post('/datasource/editTable', data),
saveField: (data: any) => request.post('/datasource/editField', data)
}
+3 -3
View File
@@ -7,7 +7,7 @@ import chat from "@/views/chat/index.vue";
import ds from "@/views/ds/index.vue";
import DashboardEditor from "@/views/dashboard/editor/index.vue";
import Dashboard from "@/views/dashboard/index.vue";
import setting from "@/views/setting/index.vue";
// import setting from "@/views/setting/index.vue";
import { watchRouter } from "./watch";
const router = createRouter({
history: createWebHashHistory(),
@@ -81,7 +81,7 @@ const router = createRouter({
},
],
},
{
/* {
path: "/setting",
component: Layout,
redirect: "/setting/index",
@@ -93,7 +93,7 @@ const router = createRouter({
meta: { title: "Settings", icon: "setting" },
},
],
},
}, */
{
path: "/system",
component: Layout,
+257 -187
View File
@@ -1,18 +1,25 @@
<template>
<el-container class="chat-container">
{{ typedContent }} {{ isTypings }}
<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/>
<Plus />
</el-icon>
New Conversation
</el-button>
</el-header>
<el-main class="chat-list">
<ChatList :current-chat-id="currentChatId" v-model:loading="loading" :chat-list="chatList"
@chat-selected="onClickHistory" @chat-deleted="onChatDeleted" @chat-renamed="onChatRenamed"/>
<ChatList
:current-chat-id="currentChatId"
v-model:loading="loading"
:chat-list="chatList"
@chat-selected="onClickHistory"
@chat-deleted="onChatDeleted"
@chat-renamed="onChatRenamed"
/>
</el-main>
</el-container>
</el-aside>
@@ -20,139 +27,182 @@
<el-main class="chat-record-list">
<el-scrollbar>
<template v-for="(message, _index) in computedMessages" :key="_index">
<ChatRow :current-chat="currentChat" v-model:datasource="currentChat.datasource" :msg="message"/>
<ChatRow
:current-chat="currentChat"
v-model:datasource="currentChat.datasource"
:msg="message"
>
<template v-if="message.role === 'assistant'">
<div v-if="message.isTyping">Thinking ...</div>
<template v-if="message.record">
<div>
<div v-if="message.record.sql_answer">
{{ message.record.sql_answer }}
</div>
<div v-if="message.record.sql">
{{ message.record.sql }}
</div>
</div>
<div>
<div v-if="message.record.data">
{{ message.record.data }}
</div>
</div>
<div>
<div v-if="message.record.chart_answer">
{{ message.record.chart_answer }}
</div>
<div v-if="message.record.chart">
{{ message.record.chart }}
</div>
</div>
<div v-if="message.record.error" style="color: red">
{{ message.record.error }}
</div>
</template>
</template>
</ChatRow>
</template>
</el-scrollbar>
</el-main>
<el-footer class="chat-footer">
<div style="height: 24px;">
<template v-if="currentChat.datasource">
<div style="height: 24px">
<template
v-if="currentChat.datasource && currentChat.datasource_name"
>
使用数据源:{{ currentChat.datasource_name }}
</template>
</div>
<div class="input-wrapper">
<el-input
:disabled="isTyping"
class="input-area"
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"
:disabled="isTyping"
class="input-area"
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"
/>
<el-button link type="primary" class="input-icon" @click="sendMessage" :disabled="isTyping">
<el-button
link
type="primary"
class="input-icon"
@click="sendMessage"
:disabled="isTyping"
>
<el-icon size="20">
<Position/>
<Position />
</el-icon>
</el-button>
</div>
</el-footer>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import {ref, computed, nextTick, watch, onMounted} from 'vue'
import {Plus, Position} from '@element-plus/icons-vue'
import {Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi} from '@/api/chat'
import ChatList from './ChatList.vue'
import ChatRow from './ChatRow.vue'
import { computed, nextTick, onMounted, ref } from "vue";
import { Plus, Position } from "@element-plus/icons-vue";
import {
Chat,
chatApi,
ChatInfo,
type ChatMessage,
ChatRecord,
questionApi,
} from "@/api/chat";
import ChatList from "./ChatList.vue";
import ChatRow from "./ChatRow.vue";
import useTypedEffect from "./typed";
const inputMessage = ref('')
const inputMessage = ref("");
const loading = ref<boolean>(false);
const chatList = ref<Array<ChatInfo>>([])
const chatList = ref<Array<ChatInfo>>([]);
const currentChatId = ref<number | undefined>()
const currentChat = ref<ChatInfo>(new ChatInfo())
const isTyping = ref<boolean>(false)
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',
role: "assistant",
create_time: currentChat.value?.create_time,
content: currentChat.value?.datasource,
isTyping: false,
isWelcome: true,
}
const messages: Array<ChatMessage> = [welcome]
};
const messages: Array<ChatMessage> = [welcome];
if (currentChatId.value === undefined) {
return messages
return messages;
}
let appendThinking = false
for (let i = 0; i < currentChat.value.records.length; i++) {
const record = currentChat.value.records[i]
const record = currentChat.value.records[i];
if (record.question !== undefined) {
messages.push({
role: 'user',
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
})
role: "assistant",
create_time: record.create_time,
record: record,
isTyping: i === currentChat.value.records.length - 1 && isTyping.value,
});
}
return messages
})
console.log(messages);
return messages;
});
const createNewChat = () => {
currentChat.value = new ChatInfo()
currentChatId.value = undefined
inputMessage.value = ''
}
currentChat.value = new ChatInfo();
currentChatId.value = undefined;
inputMessage.value = "";
};
function getChatList() {
loading.value = true
chatApi.list()
.then((res) => {
chatList.value = chatApi.toChatInfoList(res)
})
.finally(() => {
loading.value = false
})
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)
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
})
currentChatId.value = chat.id;
loading.value = true;
chatApi
.get(chat.id)
.then((res) => {
const info = chatApi.toChatInfo(res);
console.log(info);
if (info) {
currentChat.value = info;
}
})
.finally(() => {
loading.value = false;
});
}
}
function onChatDeleted(id: number) {
for (let i = 0; i < chatList.value.length; i++) {
if (chatList.value[i].id === id) {
chatList.value.splice(i, 1)
return
chatList.value.splice(i, 1);
return;
}
}
}
@@ -160,141 +210,164 @@ function onChatDeleted(id: number) {
function onChatRenamed(chat: Chat) {
chatList.value.forEach((c: Chat) => {
if (c.id === chat.id) {
c.brief = chat.brief
c.brief = chat.brief;
}
})
});
if (currentChat.value.id === chat.id) {
currentChat.value.brief = chat.brief
currentChat.value.brief = chat.brief;
}
}
onMounted(() => {
getChatList()
})
getChatList();
});
const updateMessageContent = (content: string) => {
if (currentChat.value) {
currentChat.value.records[currentChat.value.records.length - 1].answer = content
}
}
const content = ref("");
const typingEnabled = ref(true);
const typingStep = ref(1);
const typingInterval = ref(50);
const [typedContent, isTypings] = useTypedEffect(
content,
typingEnabled,
typingStep,
typingInterval
);
const sendMessage = async () => {
if (!inputMessage.value.trim()) return
if (computedMessages.value[0].content === undefined) return
if (!inputMessage.value.trim()) return;
if (computedMessages.value[0].content === undefined) return;
loading.value = true
isTyping.value = true
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 = ''
const currentRecord = new ChatRecord();
currentRecord.create_time = new Date();
currentRecord.chat_id = currentChatId.value;
currentRecord.question = inputMessage.value;
currentRecord.sql_answer = "";
currentRecord.sql = "";
currentRecord.chart_answer = "";
currentRecord.chart = "";
currentChat.value.records.push(currentRecord)
inputMessage.value = ''
currentChat.value.records.push(currentRecord);
inputMessage.value = "";
let error = false
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
})
await 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;
});
}
if (error) return
if (error) return;
try {
const response = await questionApi.add({question: currentRecord.question, chat_id: currentChatId.value})
const reader = response.body.getReader()
const decoder = new TextDecoder()
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();
console.log(done);
if (done) {
isTyping.value = false
break
isTyping.value = false;
break;
}
const chunk = decoder.decode(value)
const lines = chunk.split('\n\n')
const chunk = decoder.decode(value);
console.log(chunk);
const data = JSON.parse(chunk);
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 newContent = currentRecord.answer + realContent
updateMessageContent(newContent)
await nextTick()
}
}
await nextTick(() => {
switch (data.type) {
case "info":
console.log(data.msg);
break;
case "error":
currentRecord.error = data.content;
isTyping.value = false;
break;
case "sql-result":
currentRecord.sql_answer = currentRecord.sql_answer + data.content;
break;
case "sql":
currentRecord.sql = data.content;
break;
case "sql-data":
currentRecord.data = data.content;
break;
case "chart-result":
currentRecord.chart_answer =
currentRecord.chart_answer + data.content;
break;
case "chart":
currentRecord.chart = data.content;
break;
case "finish":
isTyping.value = false;
break;
}
}
});
}
} catch (error) {
updateMessageContent(currentRecord.answer + '\nError: Failed to get response')
console.error('Error:', error)
isTyping.value = false
if (!currentRecord.error) {
currentRecord.error = "";
}
if (currentRecord.error.trim().length !== 0) {
currentRecord.error = currentRecord.error + "\n";
}
currentRecord.error = currentRecord.error + "Error:" + error;
console.error("Error:", error);
isTyping.value = false;
}
}
watch(() => currentChat.value?.records[currentChat.value.records.length - 1]?.answer, () => {
//scrollToBottom()
}, {deep: true})
};
//@ts-ignore
const formatMessage = (content: string) => {
if (!content) return ''
if (!content) return "";
return content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>')
.replace(/ {2}/g, '&nbsp;&nbsp;')
}
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\n/g, "<br>")
.replace(/ {2}/g, "&nbsp;&nbsp;");
};
const handleCtrlEnter = (e: KeyboardEvent) => {
const textarea = e.target as HTMLTextAreaElement
const start = textarea.selectionStart
const end = textarea.selectionEnd
const value = textarea.value
const textarea = e.target as HTMLTextAreaElement;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const value = textarea.value;
inputMessage.value = value.substring(0, start) + '\n' + value.substring(end)
inputMessage.value = value.substring(0, start) + "\n" + value.substring(end);
nextTick(() => {
textarea.selectionStart = textarea.selectionEnd = start + 1
})
}
textarea.selectionStart = textarea.selectionEnd = start + 1;
});
};
</script>
<style lang="less" scoped>
@@ -302,7 +375,6 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
height: 100%;
.chat-container-left {
--el-aside-width: 260px;
border-right: solid 1px rgba(0, 0, 0, 0.3);
@@ -321,7 +393,6 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
.chat-list {
padding: 0 0 20px 0;
}
}
}
@@ -355,12 +426,11 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
bottom: 14px;
right: 8px;
}
}
}
.send-btn {
min-width: 0;
}
}
</style>
</style>
+67
View File
@@ -0,0 +1,67 @@
import { computed, onWatcherCleanup, unref, watch, ref } from 'vue';
import type { Ref, VNode } from 'vue'
function isString(str: any): str is string {
return typeof str === 'string';
}
function useState<T, R = Ref<T>>(
defaultStateValue?: T | (() => T),
): [R, (val: T) => void] {
const initValue: T =
typeof defaultStateValue === 'function' ? (defaultStateValue as any)() : defaultStateValue;
const innerValue = ref(initValue) as Ref<T>;
function triggerChange(newValue: T) {
innerValue.value = newValue;
}
return [innerValue as unknown as R, triggerChange];
}
/**
* Return typed content and typing status when typing is enabled.
* Or return content directly.
*/
const useTypedEffect = (
content: Ref<VNode | object | string>,
typingEnabled: Ref<boolean>,
typingStep: Ref<number>,
typingInterval: Ref<number>,
): [typedContent: Ref<VNode | object | string>, isTyping: Ref<boolean>] => {
const [prevContent, setPrevContent] = useState<VNode | object | string>('');
const [typingIndex, setTypingIndex] = useState<number>(1);
const mergedTypingEnabled = computed(() => typingEnabled.value && isString(content.value));
// Reset typing index when content changed
watch(content, () => {
const prevContentValue = unref(prevContent);
setPrevContent(content.value);
if (!mergedTypingEnabled.value && isString(content.value)) {
setTypingIndex(content.value.length);
} else if (isString(content.value) && isString(prevContentValue) && content.value.indexOf(prevContentValue) !== 0) {
setTypingIndex(1);
}
});
// Start typing
watch([typingIndex, typingEnabled, content], () => {
if (mergedTypingEnabled.value && isString(content.value) && unref(typingIndex) < content.value.length) {
const id = setTimeout(() => {
setTypingIndex(unref(typingIndex) + typingStep.value);
}, typingInterval.value);
onWatcherCleanup(() => {
clearTimeout(id);
});
}
}, { immediate: true });
const mergedTypingContent = computed(() => mergedTypingEnabled.value && isString(content.value) ? content.value.slice(0, unref(typingIndex)) : content.value);
return [mergedTypingContent, computed(() => mergedTypingEnabled.value && isString(content.value) && unref(typingIndex) < content.value.length)];
};
export default useTypedEffect;
+12 -12
View File
@@ -11,17 +11,17 @@ defineProps<{
ds: any
}>()
const getStatus = (status: string) => {
if (status === 'Success') {
return 'connected'
}
if (status === 'Fail') {
return 'failed'
}
if (status === 'Checking') {
return 'needs-verification'
}
}
// const getStatus = (status: string) => {
// if (status === 'Success') {
// return 'connected'
// }
// if (status === 'Fail') {
// return 'failed'
// }
// if (status === 'Checking') {
// return 'needs-verification'
// }
// }
</script>
@@ -44,7 +44,7 @@ const getStatus = (status: string) => {
<div class="connection-host">{{ ds.description }}</div>
<div class="connection-last">{{ datetimeFormat(ds.create_time) }}</div>
</div>
<div class="connection-status" :class="`${getStatus(ds.status)}`">{{ ds.status }}</div>
<!-- <div class="connection-status" :class="`${getStatus(ds.status)}`">{{ ds.status }}</div> -->
<slot></slot>
</div>
</template>
+112 -11
View File
@@ -5,7 +5,7 @@
<el-button text style="color: #fff" :icon="ArrowLeft" @click="back()" />
{{ props.dsName }}
</div>
<el-button type="primary" @click="save()"> Save </el-button>
<!-- <el-button type="primary" @click="save()"> Save </el-button> -->
</div>
<div class="container">
<div class="left-side">
@@ -43,7 +43,8 @@
<span>{{ currentTable.table_name }}</span>
<el-divider direction="vertical" />
<span>Comment:</span>
<el-input style="margin-left: 12px;" v-model="currentTable.custom_comment" />
<span>{{ currentTable.custom_comment }}</span>
<el-button style="margin-left: 10px;" text class="action-btn" :icon="IconOpeEdit" @click="editTable"/>
</div>
</div>
<el-tabs
@@ -58,15 +59,16 @@
<el-table-column prop="field_comment" label="Comment" />
<el-table-column label="Custom Comment">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-input v-model="scope.row.custom_comment" />
<div class="field-comment">
<span>{{ scope.row.custom_comment }}</span>
<el-button text class="action-btn" :icon="IconOpeEdit" @click="editField(scope.row)"/>
</div>
</template>
</el-table-column>
<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" @change="changeStatus(scope.row)"/>
</div>
</template>
</el-table-column>
@@ -89,6 +91,48 @@
</div>
</div>
</div>
<el-dialog
v-model="tableDialog"
title="Edit Table Comment"
width="600"
:destroy-on-close="true"
:close-on-click-modal="false"
@closed="closeTable"
>
<div>Table Comment</div>
<el-input v-model="currentTable.custom_comment" />
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
<el-button @click="closeTable">Cancel</el-button>
<el-button
type="primary"
@click="saveTable"
>Confirm</el-button
>
</div>
</el-dialog>
<el-dialog
v-model="fieldDialog"
title="Edit Field Comment"
width="600"
:destroy-on-close="true"
:close-on-click-modal="false"
@closed="closeField"
>
<div>Field Comment</div>
<el-input v-model="currentField.custom_comment" />
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
<el-button @click="closeField">Cancel</el-button>
<el-button
type="primary"
@click="saveField"
>Confirm</el-button
>
</div>
</el-dialog>
</div>
</template>
@@ -98,6 +142,7 @@ import { datasourceApi } from "@/api/datasource";
import { onMounted } from "vue";
import { ArrowLeft } from "@element-plus/icons-vue";
import type { TabsPaneContext } from "element-plus-secondary";
import IconOpeEdit from '@/assets/svg/operate/ope-edit.svg'
const props = defineProps({
dsId: { type: [Number], required: true },
@@ -108,10 +153,13 @@ const dsId = ref<Number>(0);
const searchValue = ref("");
const tableList = ref<any>([]);
const currentTable = ref<any>({});
const currentField = ref<any>({});
const fieldList = ref<any>([]);
const previewData = ref<any>({});
const activeName = ref("schema");
const tableDialog = ref<boolean>(false)
const fieldDialog = ref<boolean>(false)
const buildData = () => {
return { table: currentTable.value, fields: fieldList.value };
@@ -121,15 +169,59 @@ const back = () => {
history.back();
};
const save = () => {
datasourceApi.edit(buildData()).then(() => {
// const save = () => {
// datasourceApi.edit(buildData()).then(() => {
// ElMessage({
// message: "Save success",
// type: "success",
// showClose: true,
// });
// });
// };
const editTable = () => {
tableDialog.value = true
}
const closeTable = () => {
tableDialog.value = false
}
const saveTable = () => {
datasourceApi.saveTable(currentTable.value).then(() => {
closeTable()
ElMessage({
message: "Save success",
message: "Edit Success",
type: "success",
showClose: true,
});
});
};
})
})
}
const editField = (row: any) => {
currentField.value = row
fieldDialog.value = true
}
const changeStatus = (row: any) => {
currentField.value = row
saveField()
}
const closeField = () => {
fieldDialog.value = false
}
const saveField = () => {
datasourceApi.saveField(currentField.value).then(() => {
closeField()
ElMessage({
message: "Edit Success",
type: "success",
showClose: true,
})
})
}
const clickTable = (table: any) => {
currentTable.value = table;
@@ -200,4 +292,13 @@ onMounted(() => {
}
}
}
.field-comment{
display: flex;
align-items: center;
.action-btn{
margin-left: 10px;
}
}
</style>