mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -6,6 +6,7 @@ node_modules/
|
||||
/playwright/.cache/
|
||||
*.lock
|
||||
*-lock.json
|
||||
components.d.ts
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
@@ -25,7 +25,8 @@ fileConfig(config.config_file_name)
|
||||
# target_metadata = None
|
||||
|
||||
# from apps.system.models.user import SQLModel # noqa
|
||||
from apps.settings.models.setting_models import SQLModel
|
||||
# from apps.settings.models.setting_models import SQLModel
|
||||
from apps.chat.models.chat_model import SQLModel
|
||||
from common.core.config import settings # noqa
|
||||
|
||||
target_metadata = SQLModel.metadata
|
||||
|
||||
@@ -1,37 +1,68 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlmodel import select
|
||||
|
||||
from apps.chat.curd.chat import list_chats, get_chat_with_records, create_chat, save_question, save_answer
|
||||
from apps.chat.models.chat_model import CreateChat, ChatRecord
|
||||
from apps.chat.schemas.chat_base_schema import LLMConfig
|
||||
from apps.chat.schemas.chat_schema import ChatQuestion
|
||||
from apps.chat.schemas.llm import AgentService
|
||||
from apps.datasource.models.datasource import CoreDatasource
|
||||
from apps.system.models.system_model import AiModelDetail
|
||||
from common.core.deps import SessionDep
|
||||
from common.core.deps import SessionDep, CurrentUser
|
||||
import json
|
||||
import asyncio
|
||||
|
||||
router = APIRouter(tags=["Data Q&A"], prefix="/chat")
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
async def chats(session: SessionDep, current_user: CurrentUser):
|
||||
return list_chats(session, current_user)
|
||||
|
||||
|
||||
@router.get("/get/{chart_id}")
|
||||
async def list_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:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/start")
|
||||
async def start_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj: CreateChat):
|
||||
try:
|
||||
return create_chat(session, current_user, create_chat_obj)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/question")
|
||||
async def stream_sql(session: SessionDep, requestQuestion: ChatQuestion):
|
||||
async def stream_sql(session: SessionDep, current_user: CurrentUser, request_question: ChatQuestion):
|
||||
"""Stream SQL analysis results
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
requestQuestion: User question model
|
||||
current_user: CurrentUser
|
||||
request_question: User question model
|
||||
|
||||
Returns:
|
||||
Streaming response with analysis results
|
||||
"""
|
||||
question = requestQuestion.question
|
||||
|
||||
question = request_question.question
|
||||
|
||||
# Get available AI model
|
||||
aimodel = session.exec(select(AiModelDetail).where(
|
||||
AiModelDetail.status == True,
|
||||
AiModelDetail.status == True,
|
||||
AiModelDetail.api_key.is_not(None)
|
||||
)).first()
|
||||
|
||||
|
||||
# Get available datasource
|
||||
ds = session.exec(select(CoreDatasource).where(
|
||||
CoreDatasource.status == 'Success'
|
||||
@@ -42,13 +73,22 @@ async def stream_sql(session: SessionDep, requestQuestion: ChatQuestion):
|
||||
status_code=400,
|
||||
detail="No available AI model configuration found"
|
||||
)
|
||||
|
||||
|
||||
if not ds:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No available datasource configuration found"
|
||||
)
|
||||
|
||||
|
||||
record: ChatRecord
|
||||
try:
|
||||
record = save_question(session=session, current_user=current_user, question=request_question)
|
||||
except Exception as e1:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=str(e1)
|
||||
)
|
||||
|
||||
# Use Tongyi Qianwen
|
||||
tongyi_config = LLMConfig(
|
||||
model_type="openai",
|
||||
@@ -72,25 +112,37 @@ async def stream_sql(session: SessionDep, requestQuestion: ChatQuestion):
|
||||
vllm_service = LLMService(vllm_config) """
|
||||
""" result = llm_service.generate_sql(question)
|
||||
return result """
|
||||
|
||||
|
||||
async def event_generator():
|
||||
all_text = ''
|
||||
try:
|
||||
async for chunk in llm_service.async_generate(question):
|
||||
data = json.loads(chunk.replace('data: ', ''))
|
||||
|
||||
|
||||
if data['type'] in ['final', 'tool_result']:
|
||||
content = data['content']
|
||||
print('-- ' + content)
|
||||
all_text += content
|
||||
for char in content:
|
||||
yield f"data: {json.dumps({'type': 'char', 'content': char})}\n\n"
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
if 'html' in data:
|
||||
yield f"data: {json.dumps({'type': 'html', 'content': data['html']})}\n\n"
|
||||
else:
|
||||
yield chunk
|
||||
|
||||
|
||||
except Exception as e:
|
||||
all_text = 'Exception:' + str(e)
|
||||
yield f"data: {json.dumps({'type': 'error', 'content': str(e)})}\n\n"
|
||||
|
||||
#return EventSourceResponse(event_generator(), headers={"Content-Type": "text/event-stream"})
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
|
||||
try:
|
||||
save_answer(session=session, id=record.id, answer=all_text)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
# return EventSourceResponse(event_generator(), headers={"Content-Type": "text/event-stream"})
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import datetime
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy import text, and_
|
||||
from sqlmodel import select
|
||||
|
||||
from apps.chat.models.chat_model import Chat, ChatRecord, CreateChat, ChatInfo
|
||||
from apps.chat.schemas.chat_schema import ChatQuestion
|
||||
from apps.datasource.models.datasource import CoreDatasource
|
||||
from common.core.deps import SessionDep, CurrentUser
|
||||
|
||||
|
||||
def list_chats(session: SessionDep, current_user: CurrentUser) -> List[Chat]:
|
||||
chart_list = session.query(Chat).filter(Chat.create_by == current_user.id).order_by(
|
||||
Chat.create_time.desc()).all()
|
||||
return chart_list
|
||||
|
||||
|
||||
def get_chat_with_records(session: SessionDep, chart_id: int, current_user: CurrentUser) -> ChatInfo:
|
||||
chat = session.query(Chat).filter(Chat.id == chart_id).first()
|
||||
if not chat:
|
||||
raise Exception(f"Chat with id {chart_id} not found")
|
||||
|
||||
chat_info = ChatInfo(**chat.model_dump())
|
||||
|
||||
ds = session.query(CoreDatasource).filter(CoreDatasource.id == chat.datasource).first()
|
||||
if not ds:
|
||||
chat_info.datasource_exists = False
|
||||
chat_info.datasource_name = 'Datasource not exist'
|
||||
else:
|
||||
chat_info.datasource_exists = True
|
||||
chat_info.datasource_name = ds.name
|
||||
|
||||
record_list = session.query(ChatRecord).filter(
|
||||
and_(Chat.create_by == current_user.id, ChatRecord.id == chart_id)).order_by(ChatRecord.create_time).all()
|
||||
|
||||
chat_info.records = record_list
|
||||
|
||||
return chat_info
|
||||
|
||||
|
||||
def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj: CreateChat) -> ChatInfo:
|
||||
if not create_chat_obj.datasource:
|
||||
raise Exception("Datasource cannot be None")
|
||||
|
||||
if not create_chat_obj.question or create_chat_obj.question.strip() == '':
|
||||
raise Exception("Question cannot be Empty")
|
||||
|
||||
chat = Chat(create_time=datetime.datetime.now(),
|
||||
create_by=current_user.id,
|
||||
brief=create_chat_obj.question.strip()[:20],
|
||||
datasource=create_chat_obj.datasource)
|
||||
|
||||
ds = session.query(CoreDatasource).filter(CoreDatasource.id == create_chat_obj.datasource).first()
|
||||
|
||||
if not ds:
|
||||
raise Exception(f"Datasource with id {create_chat_obj.datasource} not found")
|
||||
|
||||
chat.engine_type = ds.type_name
|
||||
|
||||
chat_info = ChatInfo(**chat.model_dump())
|
||||
|
||||
session.add(chat)
|
||||
session.flush()
|
||||
session.refresh(chat)
|
||||
chat_info.id = chat.id
|
||||
session.commit()
|
||||
|
||||
return chat_info
|
||||
|
||||
|
||||
def save_question(session: SessionDep, current_user: CurrentUser, question: ChatQuestion) -> ChatRecord:
|
||||
if not question.chat_id:
|
||||
raise Exception("ChatId cannot be None")
|
||||
if not question.question or question.question.strip() == '':
|
||||
raise Exception("Question cannot be Empty")
|
||||
|
||||
chat = session.query(Chat).filter(Chat.id == question.chat_id).first()
|
||||
if not chat:
|
||||
raise Exception(f"Chat with id {question.chat_id} not found")
|
||||
|
||||
record = ChatRecord()
|
||||
record.question = question.question
|
||||
record.chat_id = chat.id
|
||||
record.create_time = datetime.datetime.now()
|
||||
record.create_by = current_user.id
|
||||
record.datasource = chat.datasource
|
||||
record.engine_type = chat.engine_type
|
||||
|
||||
result = ChatRecord(**record.model_dump())
|
||||
|
||||
session.add(record)
|
||||
session.flush()
|
||||
session.refresh(record)
|
||||
result.id = record.id
|
||||
session.commit()
|
||||
|
||||
return result
|
||||
|
||||
def save_full_question(session: SessionDep, id: int, full_question: str) -> ChatRecord:
|
||||
if not id:
|
||||
raise Exception("Record id cannot be None")
|
||||
record = session.query(ChatRecord).filter(Chat.id == id).first()
|
||||
record.full_question = full_question
|
||||
|
||||
result = ChatRecord(**record.model_dump())
|
||||
|
||||
session.add(record)
|
||||
session.flush()
|
||||
session.refresh(record)
|
||||
|
||||
session.commit()
|
||||
|
||||
return result
|
||||
|
||||
def save_answer(session: SessionDep, id: int, answer: str) -> ChatRecord:
|
||||
if not id:
|
||||
raise Exception("Record id cannot be None")
|
||||
|
||||
record = session.query(ChatRecord).filter(Chat.id == id).first()
|
||||
record.answer = answer
|
||||
|
||||
result = ChatRecord(**record.model_dump())
|
||||
|
||||
session.add(record)
|
||||
session.flush()
|
||||
session.refresh(record)
|
||||
|
||||
session.commit()
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,49 @@
|
||||
from sqlmodel import SQLModel, Field
|
||||
from sqlalchemy import Column, Text, BigInteger, DateTime, Integer, Identity
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class Chat(SQLModel, table=True):
|
||||
__tablename__ = "chat"
|
||||
id: Optional[int] = Field(sa_column=Column(Integer, Identity(always=True), primary_key=True))
|
||||
create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
|
||||
create_by: int = Field(sa_column=Column(BigInteger, nullable=True))
|
||||
brief: str = Field(max_length=64, nullable=True)
|
||||
chat_type: str = Field(max_length=20, default="chat") # chat, datasource
|
||||
datasource: int = Field(sa_column=Column(Integer, nullable=False))
|
||||
engine_type: str = Field(max_length=64)
|
||||
|
||||
|
||||
class ChatRecord(SQLModel, table=True):
|
||||
__tablename__ = "chat_record"
|
||||
id: Optional[int] = Field(sa_column=Column(Integer, Identity(always=True), primary_key=True))
|
||||
chat_id: int = Field(sa_column=Column(Integer))
|
||||
create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
|
||||
create_by: int = Field(sa_column=Column(BigInteger, nullable=True))
|
||||
datasource: int = Field(sa_column=Column(Integer, nullable=False))
|
||||
engine_type: str = Field(max_length=64)
|
||||
question: str = Field(sa_column=Column(Text, nullable=True))
|
||||
full_question: str = Field(sa_column=Column(Text, nullable=True))
|
||||
answer: str = Field(sa_column=Column(Text, nullable=True))
|
||||
run_time: float = Field(default=0)
|
||||
|
||||
|
||||
class CreateChat(BaseModel):
|
||||
id: int = None
|
||||
question: str = ''
|
||||
datasource: int = None
|
||||
|
||||
|
||||
class ChatInfo(BaseModel):
|
||||
id: Optional[int] = None
|
||||
create_time: datetime = None
|
||||
create_by: int = None
|
||||
brief: str = ''
|
||||
chat_type: str = "chat"
|
||||
datasource: int = None
|
||||
engine_type: str = ''
|
||||
datasource_name: str = ''
|
||||
datasource_exists: bool = True
|
||||
records: List[ChatRecord] = []
|
||||
@@ -3,4 +3,5 @@ from pydantic import BaseModel
|
||||
|
||||
|
||||
class ChatQuestion(BaseModel):
|
||||
question: str
|
||||
question: str
|
||||
chat_id: int
|
||||
@@ -12,7 +12,7 @@ from ..crud.datasource import get_datasource_list, check_status, create_ds, upda
|
||||
execSql, update_table_and_fields, getTablesByDs, chooseTables, preview
|
||||
from ..crud.field import get_fields_by_table_id
|
||||
from ..crud.table import get_tables_by_ds_id
|
||||
from ..models.datasource import CoreDatasource, CreateDatasource, EditObj, CoreTable
|
||||
from ..models.datasource import CoreDatasource, CreateDatasource, TableObj, CoreTable
|
||||
|
||||
router = APIRouter(tags=["datasource"], prefix="/datasource")
|
||||
path = "/opt/sqlbot/data/excel"
|
||||
@@ -79,12 +79,12 @@ async def field_list(session: SessionDep, id: int):
|
||||
|
||||
|
||||
@router.post("/editLocalComment")
|
||||
async def edit_local(session: SessionDep, data: EditObj):
|
||||
async def edit_local(session: SessionDep, data: TableObj):
|
||||
update_table_and_fields(session, data)
|
||||
|
||||
|
||||
@router.post("/previewData/{id}")
|
||||
async def edit_local(session: SessionDep, id: int, data: EditObj):
|
||||
async def edit_local(session: SessionDep, id: int, data: TableObj):
|
||||
return preview(session, id, data)
|
||||
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ from apps.datasource.utils.utils import aes_decrypt
|
||||
from apps.db.db import get_engine, get_tables, get_fields, exec_sql
|
||||
from apps.db.engine import get_engine_config
|
||||
from apps.db.engine import get_engine_conn
|
||||
from apps.db.type import db_type_relation
|
||||
from common.core.deps import SessionDep, CurrentUser
|
||||
from common.utils.utils import deepcopy_ignore_extra
|
||||
from ..crud.field import delete_field_by_ds_id, update_field
|
||||
from ..crud.table import delete_table_by_ds_id, update_table
|
||||
from ..models.datasource import CoreDatasource, CreateDatasource, CoreTable, CoreField, ColumnSchema, EditObj, \
|
||||
DatasourceConf
|
||||
from apps.db.type import db_type_relation
|
||||
from ..models.datasource import CoreDatasource, CreateDatasource, CoreTable, CoreField, ColumnSchema, TableObj, \
|
||||
DatasourceConf, TableAndFields
|
||||
|
||||
|
||||
def get_datasource_list(session: SessionDep):
|
||||
@@ -186,13 +186,13 @@ def sync_fields(session: SessionDep, ds: CoreDatasource, table: CoreTable, field
|
||||
session.commit()
|
||||
|
||||
|
||||
def update_table_and_fields(session: SessionDep, data: EditObj):
|
||||
def update_table_and_fields(session: SessionDep, data: TableObj):
|
||||
update_table(session, data.table)
|
||||
for field in data.fields:
|
||||
update_field(session, field)
|
||||
|
||||
|
||||
def preview(session: SessionDep, id: int, data: EditObj):
|
||||
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()
|
||||
sql: str = ""
|
||||
@@ -209,3 +209,12 @@ def preview(session: SessionDep, id: int, data: EditObj):
|
||||
ORDER BY "{data.fields[0].field_name}"
|
||||
OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY"""
|
||||
return exec_sql(ds, sql)
|
||||
|
||||
|
||||
def get_tableobj_by_ds(session: SessionDep, id: int) -> List[TableAndFields]:
|
||||
list: List = []
|
||||
tables = session.query(CoreTable).filter(CoreTable.ds_id == id).all()
|
||||
for table in tables:
|
||||
fields = session.query(CoreField).filter(and_(CoreField.table_id == table.id, CoreField.checked == True)).all()
|
||||
list.append(TableAndFields(table=table, fields=fields))
|
||||
return list
|
||||
|
||||
@@ -9,7 +9,8 @@ def delete_field_by_ds_id(session: SessionDep, id: int):
|
||||
|
||||
|
||||
def get_fields_by_table_id(session: SessionDep, id: int):
|
||||
return session.query(CoreField).filter(CoreField.table_id == id).all()
|
||||
return session.query(CoreField).filter(CoreField.table_id == id).order_by(
|
||||
CoreField.field_name.asc()).all()
|
||||
|
||||
|
||||
def update_field(session: SessionDep, item: CoreField):
|
||||
|
||||
@@ -55,7 +55,7 @@ class CreateDatasource(BaseModel):
|
||||
|
||||
|
||||
# edit local saved table and fields
|
||||
class EditObj(BaseModel):
|
||||
class TableObj(BaseModel):
|
||||
table: CoreTable = None
|
||||
fields: List[CoreField] = []
|
||||
|
||||
@@ -108,3 +108,12 @@ class ColumnSchema:
|
||||
fieldName: str
|
||||
fieldType: str
|
||||
fieldComment: str
|
||||
|
||||
|
||||
class TableAndFields:
|
||||
def __init__(self, table, fields):
|
||||
self.table = table
|
||||
self.fields = fields
|
||||
|
||||
table: CoreTable
|
||||
fields: List[CoreField]
|
||||
|
||||
@@ -36,7 +36,7 @@ dependencies = [
|
||||
"oracledb (>=3.1.1,<4.0.0)"
|
||||
]
|
||||
[[tool.uv.index]]
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple"
|
||||
default = true
|
||||
|
||||
[tool.uv]
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
components.d.ts
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
|
||||
Vendored
+2
-2
@@ -6,6 +6,6 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
const ElMessage: typeof import('element-plus-secondary/es')['ElMessage']
|
||||
const ElMessageBox: typeof import('element-plus-secondary/es')['ElMessageBox']
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"snowflake-id": "^1.1.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"web-storage-cache": "^1.1.1"
|
||||
"web-storage-cache": "^1.1.1",
|
||||
"dayjs": "^1.11.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
@@ -23,12 +24,12 @@
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"axios": "^1.8.4",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-plus": "^2.9.7",
|
||||
"element-plus-secondary": "^1.0.0",
|
||||
"less": "^4.3.0",
|
||||
"pinia": "^3.0.2",
|
||||
"typescript": "~5.7.2",
|
||||
"unplugin-auto-import": "^19.1.2",
|
||||
"unplugin-vue-components": "^28.5.0",
|
||||
"unplugin-vue-components-secondary": "^0.24.6",
|
||||
"vite": "^6.3.1",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
|
||||
+155
-12
@@ -1,15 +1,158 @@
|
||||
import { request } from '@/utils/request'
|
||||
import {request} from '@/utils/request'
|
||||
import {getDate} from "@/utils/utils.ts";
|
||||
|
||||
export const questionApi = {
|
||||
pager: (pageNumber: number, pageSize: number) => request.get(`/chat/question/pager/${pageNumber}/${pageSize}`),
|
||||
/* add: (data: any) => new Promise((resolve, reject) => {
|
||||
request.post('/chat/question', data, { responseType: 'stream', timeout: 0, onDownloadProgress: p => {
|
||||
resolve(p)
|
||||
}}).catch(e => reject(e))
|
||||
}), */
|
||||
// add: (data: any) => request.post('/chat/question', data),
|
||||
add: (data: any) => request.fetchStream('/chat/question', data),
|
||||
edit: (data: any) => request.put('/chat/question', data),
|
||||
delete: (id: number) => request.delete(`/chat/question/${id}`),
|
||||
query: (id: number) => request.get(`/chat/question/${id}`)
|
||||
|
||||
pager: (pageNumber: number, pageSize: number) => request.get(`/chat/question/pager/${pageNumber}/${pageSize}`),
|
||||
/* add: (data: any) => new Promise((resolve, reject) => {
|
||||
request.post('/chat/question', data, { responseType: 'stream', timeout: 0, onDownloadProgress: p => {
|
||||
resolve(p)
|
||||
}}).catch(e => reject(e))
|
||||
}), */
|
||||
// add: (data: any) => request.post('/chat/question', data),
|
||||
add: (data: any) => request.fetchStream('/chat/question', data),
|
||||
edit: (data: any) => request.put('/chat/question', data),
|
||||
delete: (id: number) => request.delete(`/chat/question/${id}`),
|
||||
query: (id: number) => request.get(`/chat/question/${id}`)
|
||||
}
|
||||
|
||||
export class ChatRecord {
|
||||
id?: number
|
||||
chat_id?: number
|
||||
create_time?: Date | string
|
||||
create_by?: number
|
||||
datasource?: number
|
||||
engine_type?: string
|
||||
question?: string
|
||||
answer?: 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) {
|
||||
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.question = question
|
||||
this.answer = answer
|
||||
this.run_time = run_time ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
export class Chat {
|
||||
id?: number
|
||||
create_time?: Date | string
|
||||
create_by?: number
|
||||
brief?: string
|
||||
chat_type?: string
|
||||
datasource?: number
|
||||
engine_type?: string
|
||||
|
||||
constructor()
|
||||
constructor(id: number, create_time: Date | string, create_by: number, brief: string, chat_type: string, datasource: number, engine_type: string)
|
||||
constructor(id?: number, create_time?: Date | string, create_by?: number, brief?: string, chat_type?: string, datasource?: number, engine_type?: string) {
|
||||
this.id = id
|
||||
this.create_time = getDate(create_time)
|
||||
this.create_by = create_by
|
||||
this.brief = brief
|
||||
this.chat_type = chat_type
|
||||
this.datasource = datasource
|
||||
this.engine_type = engine_type
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatInfo extends Chat {
|
||||
datasource_name?: string
|
||||
datasource_exists: boolean = true
|
||||
records: Array<ChatRecord> = []
|
||||
|
||||
constructor()
|
||||
constructor(chat: Chat)
|
||||
constructor(id: number, create_time: Date | string, create_by: number, brief: string, chat_type: string, datasource: number, engine_type: string, datasource_name: string, datasource_exists: boolean, records: Array<ChatRecord>)
|
||||
constructor(param1?: number | Chat, create_time?: Date | string, create_by?: number, brief?: string, chat_type?: string, datasource?: number, engine_type?: string, datasource_name?: string, datasource_exists: boolean = true, records: Array<ChatRecord> = []) {
|
||||
super()
|
||||
if (param1 !== undefined) {
|
||||
if (param1 instanceof Chat) {
|
||||
this.id = param1.id
|
||||
this.create_time = getDate(param1.create_time)
|
||||
this.create_by = param1.create_by
|
||||
this.brief = param1.brief
|
||||
this.chat_type = param1.chat_type
|
||||
this.datasource = param1.datasource
|
||||
this.engine_type = param1.engine_type
|
||||
} else {
|
||||
this.id = param1
|
||||
this.create_time = getDate(create_time)
|
||||
this.create_by = create_by
|
||||
this.brief = brief
|
||||
this.chat_type = chat_type
|
||||
this.datasource = datasource
|
||||
this.engine_type = engine_type
|
||||
}
|
||||
}
|
||||
this.datasource_name = datasource_name
|
||||
this.datasource_exists = datasource_exists
|
||||
this.records = records
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function toChatRecordList(list: any = []): ChatRecord[] {
|
||||
const records: Array<ChatRecord> = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const record = toChatRecord(list[i])
|
||||
if (record) {
|
||||
records.push(record)
|
||||
}
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
function toChatInfo(data?: any): ChatInfo | undefined {
|
||||
if (!data) {
|
||||
return undefined
|
||||
}
|
||||
return new ChatInfo(data.id, data.create_time, data.create_by, data.brief, data.chat_type, data.datasource, data.engine_type, data.datasource_name, data.datasource_exists, toChatRecordList(data.records))
|
||||
}
|
||||
|
||||
function toChatInfoList(list: any[] = []): ChatInfo[] {
|
||||
const infos: Array<ChatInfo> = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const chatInfo = toChatInfo(list[i])
|
||||
if (chatInfo) {
|
||||
infos.push(chatInfo)
|
||||
}
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
function list(): Promise<Array<ChatInfo>> {
|
||||
return request.get('/chat/list')
|
||||
}
|
||||
|
||||
function get(id: number): Promise<ChatInfo> {
|
||||
return request.get(`/chat/get/${id}`)
|
||||
}
|
||||
|
||||
function startChat(data: any): Promise<ChatInfo> {
|
||||
return request.post('/chat/start', data)
|
||||
}
|
||||
|
||||
export const chatApi = {
|
||||
toChatRecord,
|
||||
toChatRecordList,
|
||||
toChatInfo,
|
||||
toChatInfoList,
|
||||
list,
|
||||
get,
|
||||
startChat
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h } from 'vue'
|
||||
import { ElIcon } from 'element-plus'
|
||||
import { ElIcon } from 'element-plus-secondary'
|
||||
import Icon from './src/Icon.vue'
|
||||
const hIcon = (name: string) => {
|
||||
return h(ElIcon, null, {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="single-page_layout">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.single-page_layout {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@@ -225,13 +225,13 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
.workspace-select {
|
||||
width: 100% !important;
|
||||
:deep(.el-select__wrapper) {
|
||||
:deep(.ed-select__wrapper) {
|
||||
border-radius: 10px;
|
||||
box-shadow: none !important;
|
||||
background-color: #f1f3f4;
|
||||
line-height: 32px;
|
||||
min-height: 48px;
|
||||
.el-select__selected-item {
|
||||
.ed-select__selected-item {
|
||||
height: 32px;
|
||||
}
|
||||
.workspace-label {
|
||||
@@ -308,7 +308,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
align-items: center;
|
||||
.el-avatar {
|
||||
.ed-avatar {
|
||||
background-color: var(--el-color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
@@ -330,7 +330,7 @@ onMounted(() => {
|
||||
line-height: 54px;
|
||||
.workspace-select {
|
||||
width: 100% !important;
|
||||
:deep(.el-select__wrapper) {
|
||||
:deep(.ed-select__wrapper) {
|
||||
border-radius: 10px;
|
||||
box-shadow: none !important;
|
||||
background-color: #f1f3f4;
|
||||
@@ -416,7 +416,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
align-items: center;
|
||||
.el-avatar {
|
||||
.ed-avatar {
|
||||
background-color: var(--el-color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
@@ -467,7 +467,7 @@ onMounted(() => {
|
||||
border-right: 1px solid var(--el-menu-border-color);
|
||||
border-radius: 0;
|
||||
background-color: var(--white);
|
||||
:deep(.el-menu) {
|
||||
:deep(.ed-menu) {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,112 +1,119 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
|
||||
import Layout from '@/components/layout/index.vue'
|
||||
import login from '@/views/login/index.vue'
|
||||
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 { watchRouter } from './watch'
|
||||
import Layout from "@/components/layout/index.vue";
|
||||
import SinglePage from "@/components/layout/SinglePage.vue";
|
||||
import login from "@/views/login/index.vue";
|
||||
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 { watchRouter } from "./watch";
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: login
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: login,
|
||||
},
|
||||
{
|
||||
path: '/chat',
|
||||
path: "/chat",
|
||||
component: Layout,
|
||||
redirect: '/chat/index',
|
||||
redirect: "/chat/index",
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'chat',
|
||||
path: "index",
|
||||
name: "chat",
|
||||
component: chat,
|
||||
meta: { title: 'Data Q&A', icon: 'chat' }
|
||||
}
|
||||
]
|
||||
meta: { title: "Data Q&A", icon: "chat" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/ds',
|
||||
component: Layout,
|
||||
redirect: '/ds/index',
|
||||
path: "/dsTable",
|
||||
component: SinglePage,
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'ds',
|
||||
path: ":dsId/:dsName",
|
||||
name: "dsTable",
|
||||
component: () => import("@/views/ds/TableList.vue"),
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/ds",
|
||||
component: Layout,
|
||||
redirect: "/ds/index",
|
||||
children: [
|
||||
{
|
||||
path: "index",
|
||||
name: "ds",
|
||||
component: ds,
|
||||
meta: { title: 'Data Connections', icon: 'ds' }
|
||||
meta: { title: "Data Connections", icon: "ds" },
|
||||
},
|
||||
{
|
||||
path: 'dsTable/:dsId/:dsName',
|
||||
name: 'dsTable',
|
||||
component: () => import('@/views/ds/TableList.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
path: "/dashboard",
|
||||
component: Layout,
|
||||
redirect: '/dashboard/index',
|
||||
redirect: "/dashboard/index",
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'dashboard',
|
||||
path: "index",
|
||||
name: "dashboard",
|
||||
component: Dashboard,
|
||||
meta: { title: 'Dashboard', icon: 'dashboard' }
|
||||
}
|
||||
]
|
||||
meta: { title: "Dashboard", icon: "dashboard" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/dashboard-edit',
|
||||
redirect: '/dashboard-edit/index',
|
||||
path: "/dashboard-edit",
|
||||
redirect: "/dashboard-edit/index",
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'dashboard-edit',
|
||||
path: "index",
|
||||
name: "dashboard-edit",
|
||||
component: DashboardEditor,
|
||||
meta: { title: 'dashboard-edit', icon: 'dashboard' }
|
||||
}
|
||||
]
|
||||
meta: { title: "dashboard-edit", icon: "dashboard" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/setting',
|
||||
path: "/setting",
|
||||
component: Layout,
|
||||
redirect: '/setting/index',
|
||||
redirect: "/setting/index",
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'setting',
|
||||
path: "index",
|
||||
name: "setting",
|
||||
component: setting,
|
||||
meta: { title: 'Settings', icon: 'setting' }
|
||||
}
|
||||
]
|
||||
meta: { title: "Settings", icon: "setting" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
path: "/system",
|
||||
component: Layout,
|
||||
redirect: '/system/user',
|
||||
redirect: "/system/user",
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'user',
|
||||
component: () => import('@/views/system/user/index.vue'),
|
||||
meta: { title: 'User Management', icon: 'icon_user' }
|
||||
path: "user",
|
||||
name: "user",
|
||||
component: () => import("@/views/system/user/index.vue"),
|
||||
meta: { title: "User Management", icon: "icon_user" },
|
||||
},
|
||||
{
|
||||
path: 'model',
|
||||
name: 'model',
|
||||
component: () => import('@/views/system/model/index.vue'),
|
||||
meta: { title: 'AI Model Configuration', icon: 'icon_ai' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
watchRouter(router)
|
||||
export default router
|
||||
path: "model",
|
||||
name: "model",
|
||||
component: () => import("@/views/system/model/index.vue"),
|
||||
meta: { title: "AI Model Configuration", icon: "icon_ai" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
watchRouter(router);
|
||||
export default router;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus-secondary'
|
||||
import { useCache } from '@/utils/useCache'
|
||||
const { wsCache } = useCache()
|
||||
const whiteList = ['/login']
|
||||
|
||||
+45
-6
@@ -3,18 +3,14 @@
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
|
||||
/* --primary-color: var(--el-color-primary); */
|
||||
--primary-color: #1CBA90;
|
||||
--primary-color: #1cba90;
|
||||
--primary-light: #e8f0fe;
|
||||
--secondary-color: #5f6368;
|
||||
--danger-color: #ea4335;
|
||||
@@ -30,7 +26,7 @@
|
||||
--border-radius: 8px;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
--el-color-primary: #1CBA90 !important;
|
||||
--el-color-primary: #1cba90 !important;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -50,6 +46,12 @@ body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -67,3 +69,40 @@ body {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.list-item_primary {
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ed-icon + .label {
|
||||
margin-left: 5.33px;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 85%;
|
||||
font-size: 14px;
|
||||
.ellipsis;
|
||||
}
|
||||
|
||||
&:not(.active):hover {
|
||||
background: rgba(31, 35, 41, 0.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--ed-color-primary-1a, rgba(51, 112, 255, 0.1));
|
||||
color: var(--ed-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+31
-14
@@ -1,20 +1,37 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const getCheckDate = (timestamp: any) => {
|
||||
if (!timestamp) return false
|
||||
const dt = new Date(timestamp)
|
||||
if (isNaN(dt.getTime())) return false
|
||||
return dt
|
||||
if (!timestamp) return false
|
||||
const dt = new Date(timestamp)
|
||||
if (isNaN(dt.getTime())) return false
|
||||
return dt
|
||||
}
|
||||
|
||||
export const datetimeFormat = (timestamp: any) => {
|
||||
const dt = getCheckDate(timestamp)
|
||||
if (!dt) return timestamp
|
||||
const dt = getCheckDate(timestamp)
|
||||
if (!dt) return timestamp
|
||||
|
||||
const y = dt.getFullYear()
|
||||
const m = (dt.getMonth() + 1 + '').padStart(2, '0')
|
||||
const d = (dt.getDate() + '').padStart(2, '0')
|
||||
const hh = (dt.getHours() + '').padStart(2, '0')
|
||||
const mm = (dt.getMinutes() + '').padStart(2, '0')
|
||||
const ss = (dt.getSeconds() + '').padStart(2, '0')
|
||||
const y = dt.getFullYear()
|
||||
const m = (dt.getMonth() + 1 + '').padStart(2, '0')
|
||||
const d = (dt.getDate() + '').padStart(2, '0')
|
||||
const hh = (dt.getHours() + '').padStart(2, '0')
|
||||
const mm = (dt.getMinutes() + '').padStart(2, '0')
|
||||
const ss = (dt.getSeconds() + '').padStart(2, '0')
|
||||
|
||||
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
|
||||
}
|
||||
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* string: only accept ISO 8601, example: '2018-04-04T16:00:00.000Z'
|
||||
* number: timestamp
|
||||
* @param time
|
||||
*/
|
||||
export function getDate(time?: Date | string | number) {
|
||||
if (!time) return undefined
|
||||
if (time instanceof Date) return time
|
||||
if (typeof time === 'string') {
|
||||
return dayjs(time).toDate()
|
||||
}
|
||||
return new Date(time)
|
||||
}
|
||||
|
||||
+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;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import Components from 'unplugin-vue-components-secondary/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components-secondary/resolvers'
|
||||
import path from 'path'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
export default defineConfig(({ mode }) => {
|
||||
@@ -38,7 +38,7 @@ export default defineConfig(({ mode }) => {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'element-plus': ['element-plus'],
|
||||
'element-plus-secondary': ['element-plus-secondary'],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -46,6 +46,6 @@ export default defineConfig(({ mode }) => {
|
||||
esbuild: {
|
||||
jsxFactory: 'h',
|
||||
jsxFragment: 'Fragment',
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user