Merge remote-tracking branch 'origin/main'

This commit is contained in:
wangjiahao
2025-05-23 17:32:25 +08:00
35 changed files with 1286 additions and 861 deletions
+1
View File
@@ -6,6 +6,7 @@ node_modules/
/playwright/.cache/
*.lock
*-lock.json
components.d.ts
# Byte-compiled / optimized / DLL files
__pycache__/
+2 -1
View File
@@ -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
+69 -17
View File
@@ -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")
View File
+132
View File
@@ -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
+49
View File
@@ -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] = []
+2 -1
View File
@@ -3,4 +3,5 @@ from pydantic import BaseModel
class ChatQuestion(BaseModel):
question: str
question: str
chat_id: int
+3 -3
View File
@@ -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)
+14 -5
View File
@@ -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
+2 -1
View File
@@ -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):
+10 -1
View File
@@ -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]
+1 -1
View File
@@ -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
View File
@@ -6,7 +6,7 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
components.d.ts
node_modules
dist
dist-ssr
+2 -2
View File
@@ -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']
}
+4 -3
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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>
+6 -6
View File
@@ -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;
}
}
+77 -70
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>')
.replace(/ {2}/g, '&nbsp;&nbsp;')
}
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>')
.replace(/ {2}/g, '&nbsp;&nbsp;')
}
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;
}
+134 -86
View File
@@ -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>&nbsp;|&nbsp;Comment:&nbsp;</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
View File
@@ -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>
+3 -3
View File
@@ -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{
+1 -1
View File
@@ -140,7 +140,7 @@ const submitForm = () => {
}
}
:deep(.el-input__wrapper) {
:deep(.ed-input__wrapper) {
background-color: #f5f7fa;
}
</style>
+5 -5
View File
@@ -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;
+3 -3
View File
@@ -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);
+3 -3
View File
@@ -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;
+3 -3
View File
@@ -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;
+4 -4
View File
@@ -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',
},
}
}
})