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'
# Conflicts: # frontend/src/i18n/en.json # frontend/src/i18n/zh-CN.json
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
"""010_upgrade_user_language
|
||||
|
||||
Revision ID: 8dc3b1bdbfef
|
||||
Revises: 804b08ac329d
|
||||
Create Date: 2025-06-10 11:21:35.257604
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel.sql.sqltypes
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8dc3b1bdbfef'
|
||||
down_revision = '804b08ac329d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
op.add_column('sys_user', sa.Column('language', sa.VARCHAR(length=255), server_default=sa.text("'zh-CN'::character varying"), nullable=False))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
op.drop_column('sys_user', 'language')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import orjson
|
||||
import traceback
|
||||
from typing import List
|
||||
|
||||
@@ -138,7 +139,7 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
|
||||
# execute sql
|
||||
result = llm_service.execute_sql(sql=sql)
|
||||
llm_service.save_sql_data(session=session, data_obj=result)
|
||||
yield json.dumps({'content': json.dumps(result, ensure_ascii=False), 'type': 'sql-data'}) + '\n\n'
|
||||
yield json.dumps({'content': orjson.dumps(result).decode(), 'type': 'sql-data'}) + '\n\n'
|
||||
|
||||
# generate chart
|
||||
chart_res = llm_service.generate_chart(session=session)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import orjson
|
||||
import logging
|
||||
import warnings
|
||||
from typing import Any, List, Union, Dict
|
||||
@@ -218,7 +219,7 @@ class LLMService:
|
||||
|
||||
def save_sql_data(self, session: SessionDep, data_obj: Dict[str, Any]):
|
||||
return save_sql_exec_data(session=session, record_id=self.record.id,
|
||||
data=json.dumps(data_obj, ensure_ascii=False))
|
||||
data=orjson.dumps(data_obj).decode())
|
||||
|
||||
def finish(self, session: SessionDep):
|
||||
return finish_record(session=session, record_id=self.record.id)
|
||||
|
||||
@@ -18,7 +18,7 @@ router = APIRouter(tags=["datasource"], prefix="/datasource")
|
||||
path = "/opt/sqlbot/data/excel"
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
@router.get("/list", operation_id="get_datasource_list")
|
||||
async def datasource_list(session: SessionDep):
|
||||
return get_datasource_list(session=session)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from apps.system.crud.user import get_user_info
|
||||
from apps.system.models.user import user_grid
|
||||
from apps.system.schemas.system_schema import UserLanguage
|
||||
from common.core.deps import CurrentUser, SessionDep
|
||||
from common.core.pagination import Paginator
|
||||
from common.core.schemas import PaginatedResponse, PaginationParams
|
||||
@@ -8,8 +10,12 @@ router = APIRouter(tags=["user"], prefix="/user")
|
||||
|
||||
|
||||
@router.get("/info")
|
||||
async def user_info(current_user: CurrentUser):
|
||||
return current_user.to_dict()
|
||||
async def user_info(session: SessionDep, current_user: CurrentUser):
|
||||
db_user = get_user_info(session=session, user_id=current_user.id)
|
||||
if not db_user:
|
||||
return {"message": "User not found"}
|
||||
db_user.password = None
|
||||
return db_user
|
||||
|
||||
|
||||
@router.get("/pager/{pageNum}/{pageSize}", response_model=PaginatedResponse[user_grid])
|
||||
@@ -24,4 +30,17 @@ async def pager(
|
||||
return await paginator.get_paginated_response(
|
||||
model=user_grid,
|
||||
pagination=pagination,
|
||||
**filters)
|
||||
**filters)
|
||||
|
||||
@router.put("/language")
|
||||
async def langChange(session: SessionDep, current_user: CurrentUser, language: UserLanguage):
|
||||
lang = language.language
|
||||
if lang not in ["zh-CN", "en"]:
|
||||
return {"message": "Language not supported"}
|
||||
db_user = get_user_info(session=session, user_id=current_user.id)
|
||||
if not db_user:
|
||||
return {"message": "User not found"}
|
||||
db_user.language = lang
|
||||
session.add(db_user)
|
||||
session.commit()
|
||||
return {"message": "Language changed successfully", "language": lang}
|
||||
@@ -10,6 +10,12 @@ def get_user_by_account(*, session: Session, account: str) -> sys_user | None:
|
||||
result_user = sys_user.model_validate(session_user)
|
||||
return result_user
|
||||
|
||||
def get_user_info(*, session: Session, user_id: int) -> sys_user | None:
|
||||
db_user = session.get(user_grid, user_id)
|
||||
if not db_user:
|
||||
return None
|
||||
return db_user
|
||||
|
||||
def authenticate(*, session: Session, account: str, password: str) -> sys_user | None:
|
||||
db_user = get_user_by_account(session=session, account=account)
|
||||
if not db_user:
|
||||
|
||||
@@ -21,4 +21,5 @@ class user_grid(sys_user, table=True):
|
||||
email: str
|
||||
status: int
|
||||
create_time: int
|
||||
language: str = Field(max_length=255, default="zh-CN")
|
||||
|
||||
|
||||
@@ -2,4 +2,8 @@ from pydantic import BaseModel
|
||||
|
||||
class model_status(BaseModel):
|
||||
status: bool
|
||||
ids: list[int]
|
||||
ids: list[int]
|
||||
|
||||
|
||||
class UserLanguage(BaseModel):
|
||||
language: str
|
||||
@@ -4,7 +4,7 @@ base_template = None
|
||||
|
||||
|
||||
def load():
|
||||
with open('./template.yaml', 'r') as f:
|
||||
with open('./template.yaml', 'r', encoding='utf-8') as f:
|
||||
global base_template
|
||||
base_template = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ wlist = [
|
||||
"*.woff2",
|
||||
"*.ttf",
|
||||
"*.eot",
|
||||
"*.otf"
|
||||
"*.otf",
|
||||
"/mcp",
|
||||
"/mcp/message"
|
||||
]
|
||||
|
||||
class WhitelistChecker:
|
||||
|
||||
+22
-2
@@ -14,15 +14,20 @@ from common.core.config import settings
|
||||
from common.core.response_middleware import ResponseMiddleware, exception_handler
|
||||
from alembic.config import Config
|
||||
from alembic import command
|
||||
from fastapi_mcp import FastApiMCP
|
||||
|
||||
|
||||
def run_migrations():
|
||||
alembic_cfg = Config("alembic.ini")
|
||||
command.upgrade(alembic_cfg, "head")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
run_migrations()
|
||||
yield
|
||||
|
||||
|
||||
def custom_generate_unique_id(route: APIRoute) -> str:
|
||||
tag = route.tags[0] if route.tags and len(route.tags) > 0 else ""
|
||||
return f"{tag}-{route.name}"
|
||||
@@ -38,6 +43,17 @@ app = FastAPI(
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
mcp_app = FastAPI()
|
||||
|
||||
mcp = FastApiMCP(
|
||||
app,
|
||||
name="SQLBot MCP Server",
|
||||
description="SQLBot MCP Server",
|
||||
include_operations=["get_datasource_list"]
|
||||
)
|
||||
|
||||
mcp.mount(mcp_app)
|
||||
|
||||
# Set all CORS enabled origins
|
||||
if settings.all_cors_origins:
|
||||
app.add_middleware(
|
||||
@@ -66,9 +82,13 @@ else:
|
||||
@app.get("/", include_in_schema=False)
|
||||
async def read_index():
|
||||
return FileResponse(path=os.path.join(frontend_dist, "index.html"))
|
||||
|
||||
|
||||
|
||||
app.mount("/", StaticFiles(directory=frontend_dist), name="static")
|
||||
|
||||
mcp.setup_server()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
|
||||
@@ -34,6 +34,7 @@ dependencies = [
|
||||
"psycopg2-binary (>=2.9.10,<3.0.0)",
|
||||
"oracledb (>=3.1.1,<4.0.0)",
|
||||
"pyyaml (>=6.0.2,<7.0.0)",
|
||||
"fastapi-mcp (>=0.3.4,<0.4.0)",
|
||||
]
|
||||
[[tool.uv.index]]
|
||||
url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
|
||||
|
||||
+12
-5
@@ -7,8 +7,12 @@ template:
|
||||
- 生成的SQL必须符合{engine}的规范。
|
||||
- 根据表结构生成SQL语句。
|
||||
- SQL查询中不能使用星号(*),必须明确指定字段名.
|
||||
- SQL查询的字段名和别名必须为英文。
|
||||
- SQL查询的字段名不要自动翻译,别名必须为英文。
|
||||
- 计算占比,百分比类型字段,保留两位小数,以%结尾。
|
||||
- 生成SQL时,必须避免关键字冲突:
|
||||
- 如{engine}是 PostgreSQL、Oracle,则在schema、表名、字段名、别名外层加双引号;
|
||||
- 如{engine}是 MySQL,则在表名、字段名、别名外层加反引号;
|
||||
- 如{engine}是 Microsoft SQL Server,则在schema、表名、字段名、别名外层加方括号。
|
||||
- 生成的SQL使用JSON格式返回:
|
||||
{{"success":true,"sql":"生成的SQL语句"}}
|
||||
- 问题与生成SQL无关时,直接回答:
|
||||
@@ -16,8 +20,8 @@ template:
|
||||
- 如果根据提供的表结构不能生成符合问题与条件的SQL,回答:
|
||||
{{"success":false,"message":"无法生成SQL的原因"}}
|
||||
- 如果问题是图表展示相关且与生成SQL查询无关时,请参考上一次回答的SQL来生成SQL
|
||||
- 如果问题是图表展示相关,可参考的图表类型为表格(table)、条形图(bar)、折线图(line)或饼图(pie),返回的JSON:
|
||||
{{"success":true,"sql":"生成的SQL语句","chart-type":"选择的图表类型(table/bar/line/pie)"}}
|
||||
- 如果问题是图表展示相关,可参考的图表类型为表格(table)、柱状图(column)、条形图(bar)、折线图(line)或饼图(pie),返回的JSON:
|
||||
{{"success":true,"sql":"生成的SQL语句","chart-type":"选择的图表类型(table/column/bar/line/pie)"}}
|
||||
|
||||
### 请遵守以下输出要求:
|
||||
请逐步推理后给出答案
|
||||
@@ -44,14 +48,17 @@ template:
|
||||
- 如果需要表格,则生成的 JSON 格式应为:
|
||||
{{"type":"table", "title": "标题", "columns": [{{"name":"中文字段名1", "value": "SQL 查询列 1(有别名用别名)"}}, {{"name": "中文字段名 2", "value": "SQL 查询列 2(有别名用别名)"}}]}}
|
||||
必须从 SQL 查询列中提取“columns”。
|
||||
- 如果需要柱状图,则生成的 JSON 格式应为:
|
||||
{{"type":"column", "title": "标题", "axis": {{"x": {{"name":"x轴的中文名称", "value": "SQL 查询 x 轴的列(有别名用别名)"}}, "y": {{"name":"y轴的中文名称","value": "SQL 查询 y 轴的列(有别名用别名)"}}}}
|
||||
必须从 SQL 查询列中提取“x”和“y”。
|
||||
- 如果需要条形图,则生成的 JSON 格式应为:
|
||||
{{"type":"bar", "title": "标题", "axis": {{"x": {{"name":"x轴的中文名称", "value": "SQL 查询 x 轴的列(有别名用别名)"}}, "y": {{"name":"y轴的中文名称","value": "SQL 查询 y 轴的列(有别名用别名)"}}}}
|
||||
必须从 SQL 查询列中提取“x”和“y”。
|
||||
- 如果需要折线图,则生成的 JSON 格式应为:
|
||||
{{"type":"line", "title": "标题", "axis": {{"x": {{"name":"x轴的中文名称","value": "x轴的SQL查询列(有别名用别名)"}}, "y": {{"name":"y轴的中文名称","value": "y轴的SQL查询列(有别名用别名)"}}}}
|
||||
{{"type":"line", "title": "标题", "axis": {{"x": {{"name":"x轴的中文名称","value": "SQL 查询 x 轴的列(有别名用别名)"}}, "y": {{"name":"y轴的中文名称","value": "SQL 查询 y 轴的列(有别名用别名)"}}}}
|
||||
其中“x”和“y”必须从SQL查询列中提取。
|
||||
- 如果需要饼图,则生成的 JSON 格式应为:
|
||||
{{"type":"pie", "title": "标题", "columns": [{{"name":"中文字段名1","value":"SQL查询列1(有别名用别名)"}},{{"name":"中文字段名2","value":"SQL查询列2(有别名用别名)"}}]}}
|
||||
{{"type":"pie", "title": "标题", "axis": {{"y": {{"值轴的中文名称","value":"SQL 查询数值的列(有别名用别名)"}}, "series": {{"name":"分类的中文名称","value":"SQL 查询分类的列(有别名用别名)"}}}}
|
||||
其中“column”必须从SQL查询列中提取。
|
||||
- 如果答案未知或者与生成JSON无关,则生成的 JSON 格式应为:
|
||||
{{"type":"error", "reason": "抱歉,我无法回答您的问题。"}}
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/g2": "^5.3.3",
|
||||
"@antv/s2": "^2.4.3",
|
||||
"@npkg/tinymce-plugins": "^0.0.7",
|
||||
"@tinymce/tinymce-vue": "^5.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"element-plus": "^2.10.1",
|
||||
"element-plus-secondary": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"snowflake-id": "^1.1.0",
|
||||
"tinymce": "^5.8.2",
|
||||
"vue": "^3.5.13",
|
||||
|
||||
@@ -5,5 +5,6 @@ export const userApi = {
|
||||
add: (data: any) => request.post('/settings/terminology', data),
|
||||
edit: (data: any) => request.put('/settings/terminology', data),
|
||||
delete: (id: number) => request.delete(`/settings/terminology/${id}`),
|
||||
query: (id: number) => request.get(`/settings/terminology/${id}`)
|
||||
query: (id: number) => request.get(`/settings/terminology/${id}`),
|
||||
language: (data: any) => request.put('/user/language', data),
|
||||
}
|
||||
@@ -8,5 +8,6 @@ export const AuthApi = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}),
|
||||
logout: () => request.post('/auth/logout')
|
||||
logout: () => request.post('/auth/logout'),
|
||||
info: () => request.get('/user/info')
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<el-dropdown @command="changeLanguage" trigger="hover">
|
||||
<div class="lang-switch">
|
||||
<span>{{ selectedLanguage === 'zh-CN' ? '中文' : 'English' }}</span>
|
||||
<el-icon class="el-icon--right">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="en" :class="{ 'selected-lang': selectedLanguage === 'en' }">
|
||||
English
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="zh-CN" :class="{ 'selected-lang': selectedLanguage === 'zh-CN' }">
|
||||
中文
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { userApi } from '@/api/auth'
|
||||
|
||||
const { locale } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const selectedLanguage = computed(() => {
|
||||
return userStore.language
|
||||
})
|
||||
|
||||
const changeLanguage = (lang: string) => {
|
||||
locale.value = lang
|
||||
userStore.setLanguage(lang)
|
||||
|
||||
const param = {
|
||||
language: lang
|
||||
}
|
||||
userApi.language(param)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.lang-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
.el-icon--right {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-lang {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="main-menu" :class="{ 'main-menu-sidebar': !topLayout, 'main-menu-topbar': topLayout }">
|
||||
<div class="logo">SQLBot</div>
|
||||
|
||||
<div v-if="!topLayout || !showSubmenu"
|
||||
<!-- <div v-if="!topLayout || !showSubmenu"
|
||||
:class="{ 'workspace-area': !topLayout, 'topbar-workspace-area': topLayout }">
|
||||
<el-select
|
||||
v-model="workspace"
|
||||
@@ -26,7 +26,7 @@
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div> -->
|
||||
<el-menu
|
||||
v-if="!topLayout || !showSubmenu"
|
||||
:default-active="activeMenu"
|
||||
@@ -37,14 +37,13 @@
|
||||
<el-icon v-if="item.meta.icon">
|
||||
<component :is="resolveIcon(item.meta.icon)"/>
|
||||
</el-icon>
|
||||
<span>{{ item.meta.title }}</span>
|
||||
<span>{{ t(`menu.${item.meta.title}`) }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
<div v-else class="top-bar-title">
|
||||
<span class="split"/>
|
||||
<!-- <span>System manage</span> -->
|
||||
<span>{{ t('common.system_manage') }}</span>
|
||||
<span>{{ t('common.system_manage') }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -59,12 +58,11 @@
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-tooltip content="System manage" placement="bottom" v-else>
|
||||
<el-tooltip :content="t('common.system_manage')" placement="bottom" v-else>
|
||||
<div class="header-icon-btn" @click="toSystem">
|
||||
<el-icon>
|
||||
<iconsystem/>
|
||||
</el-icon>
|
||||
<!-- <span>System manage</span> -->
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
@@ -77,21 +75,7 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="switchLayout">Switch Layout</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout">Logout</el-dropdown-item>
|
||||
|
||||
<el-dropdown @command="changeLanguage">
|
||||
<div class="lang-switch">
|
||||
Language
|
||||
<el-icon>
|
||||
<i class="el-icon-arrow-down"/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="en">English</el-dropdown-item>
|
||||
<el-dropdown-item command="zh-CN">中文</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown-item><language-selector /></el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -108,7 +92,7 @@
|
||||
<el-icon>
|
||||
<iconsystem/>
|
||||
</el-icon>
|
||||
<span>System manage</span>
|
||||
<span>{{ t('common.system_manage') }}</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
@@ -121,6 +105,8 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="switchLayout">Switch Layout</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout">Logout</el-dropdown-item>
|
||||
<el-dropdown-item><language-selector /></el-dropdown-item>
|
||||
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -138,7 +124,7 @@
|
||||
<el-icon v-if="item.meta.icon">
|
||||
<component :is="resolveIcon(item.meta.icon)"/>
|
||||
</el-icon>
|
||||
<span>{{ item.meta.title }}</span>
|
||||
<span>{{ t(`menu.${item.meta.title}`) }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
@@ -170,8 +156,9 @@ import icon_ai from '@/assets/svg/icon_ai.svg'
|
||||
import {ArrowLeftBold} from '@element-plus/icons-vue'
|
||||
import {useCache} from '@/utils/useCache'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import LanguageSelector from '@/components/Language-selector/index.vue'
|
||||
|
||||
const { locale, t } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const {wsCache} = useCache()
|
||||
const topLayout = ref(false)
|
||||
const router = useRouter()
|
||||
@@ -186,7 +173,8 @@ const routerList = computed(() => {
|
||||
})
|
||||
|
||||
const sysRouterList = computed(() => {
|
||||
return router.getRoutes().filter(route => route.path.includes('/system'))
|
||||
const result = router.getRoutes().filter(route => route.path.includes('/system') && !route.redirect)
|
||||
return result
|
||||
})
|
||||
|
||||
const showSubmenu = computed(() => {
|
||||
@@ -234,9 +222,6 @@ const switchLayout = () => {
|
||||
wsCache.set('sqlbot-topbar-layout', topLayout.value)
|
||||
}
|
||||
|
||||
const changeLanguage = (lang: string) => {
|
||||
locale.value = lang
|
||||
}
|
||||
onMounted(() => {
|
||||
topLayout.value = wsCache.get('sqlbot-topbar-layout') || true
|
||||
})
|
||||
@@ -299,6 +284,9 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
&:not(.ed-menu--vertical) {
|
||||
margin-left: 32px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,12 +546,4 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.lang-switch {
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Dashboard",
|
||||
"chat": "Chat",
|
||||
"datasource": "Data Source",
|
||||
"system": "System"
|
||||
"Data Q&A": "Data Q&A",
|
||||
"Data Connections": "Data Connections",
|
||||
"Dashboard": "Dashboard",
|
||||
"AI Model Configuration": "AI Model Configuration"
|
||||
},
|
||||
"common": {
|
||||
"back": "Back",
|
||||
@@ -22,5 +22,35 @@
|
||||
"dashboard": "Dashboard",
|
||||
"delete_warning": "Are you sure you want to delete?",
|
||||
"delete_success": "Successfully deleted"
|
||||
},
|
||||
"qa": {
|
||||
"New Conversation": "New Conversation",
|
||||
"title": "Smart Query",
|
||||
"placeholder": "Enter your question",
|
||||
"ask": "Ask",
|
||||
"loading": "Loading...",
|
||||
"no_data": "No Data",
|
||||
"error": "An error occurred, please try again later",
|
||||
"question_placeholder": "Press Enter to submit, or use Ctrl + Enter for new line",
|
||||
"greeting": "Hi, I'm SQLBot, nice to serve you",
|
||||
"description": "I can help query data, generate charts, detect data anomalies, predict data, etc. Please select a data source to start smart querying~",
|
||||
"select_datasource": "Select Data Source",
|
||||
"view_more": "View More",
|
||||
"selected_datasource": "Selected Data Source",
|
||||
"empty_datasource": "No data source available, please create one first!",
|
||||
"datasource_not_exist": "Data source does not exist"
|
||||
},
|
||||
"ds": {
|
||||
"title": "Data Source",
|
||||
"add": "Add Data Source",
|
||||
"edit": "Edit Data Source",
|
||||
"delete": "Delete Data Source",
|
||||
"name": "Data Source Name",
|
||||
"type": "Data Source Type",
|
||||
"status": "Status",
|
||||
"actions": "Actions",
|
||||
"test_connection": "Test Connection",
|
||||
"connection_success": "Connection Successful",
|
||||
"connection_failed": "Connection Failed, Please Check Configuration"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from './en.json'
|
||||
import zhCN from './zh-CN.json'
|
||||
import elementEnLocale from 'element-plus/es/locale/lang/en'
|
||||
import elementZhLocale from 'element-plus/es/locale/lang/zh-cn'
|
||||
import elementEnLocale from 'element-plus-secondary/es/locale/lang/en'
|
||||
import elementZhLocale from 'element-plus-secondary/es/locale/lang/zh-cn'
|
||||
import { useCache } from '@/utils/useCache'
|
||||
const { wsCache } = useCache()
|
||||
|
||||
const getDefaultLocale = () => {
|
||||
/* const savedLang = localStorage.getItem('lang')
|
||||
return savedLang || 'zh-CN' */
|
||||
return 'zh-CN'
|
||||
const language = wsCache.get('user.language')
|
||||
return language || 'zh-CN'
|
||||
}
|
||||
|
||||
const messages = {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "仪表盘",
|
||||
"chat": "聊天",
|
||||
"datasource": "数据源",
|
||||
"system": "系统"
|
||||
"Data Q&A": "智能问数",
|
||||
"Data Connections": "数据源",
|
||||
"Dashboard": "仪表盘",
|
||||
"AI Model Configuration": "模型配置"
|
||||
},
|
||||
"common": {
|
||||
"back": "返回",
|
||||
@@ -22,5 +22,35 @@
|
||||
"dashboard": "仪表板",
|
||||
"delete_warning": "确定要删除吗?",
|
||||
"delete_success": "删除成功"
|
||||
},
|
||||
"qa": {
|
||||
"New Conversation": "开启新问数",
|
||||
"title": "智能问数",
|
||||
"placeholder": "请输入您的问题",
|
||||
"ask": "提问",
|
||||
"loading": "加载中...",
|
||||
"no_data": "暂无数据",
|
||||
"error": "发生错误,请稍后再试",
|
||||
"question_placeholder": "按下 Enter 提交问题, 或使用 Ctrl + Enter 换行",
|
||||
"greeting": "你好,我是SQLBot,很高兴为你服务",
|
||||
"description": "我可以帮忙查询数据、生成图表、检测数据异常、预测数据等,请选择一个数据源,开启智能问数吧~",
|
||||
"select_datasource": "选择数据源",
|
||||
"view_more": "查看更多",
|
||||
"selected_datasource": "已选择数据源",
|
||||
"empty_datasource": "数据源为空,请新建后再开启智能问数!",
|
||||
"datasource_not_exist": "数据源不存在"
|
||||
},
|
||||
"ds": {
|
||||
"title": "数据源",
|
||||
"add": "添加数据源",
|
||||
"edit": "编辑数据源",
|
||||
"delete": "删除数据源",
|
||||
"name": "数据源名称",
|
||||
"type": "数据源类型",
|
||||
"status": "状态",
|
||||
"actions": "操作",
|
||||
"test_connection": "测试连接",
|
||||
"connection_success": "连接成功",
|
||||
"connection_failed": "连接失败,请检查配置"
|
||||
}
|
||||
}
|
||||
@@ -91,14 +91,14 @@ const router = createRouter({
|
||||
{
|
||||
path: "/system",
|
||||
component: Layout,
|
||||
redirect: "/system/user",
|
||||
redirect: "/system/model",
|
||||
children: [
|
||||
{
|
||||
/* {
|
||||
path: "user",
|
||||
name: "user",
|
||||
component: User,
|
||||
meta: { title: "User Management", icon: "icon_user" },
|
||||
},
|
||||
}, */
|
||||
{
|
||||
path: "model",
|
||||
name: "model",
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import { ElMessage } from 'element-plus-secondary'
|
||||
import { useCache } from '@/utils/useCache'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { wsCache } = useCache()
|
||||
const whiteList = ['/login']
|
||||
export const watchRouter = (router: any) => {
|
||||
router.beforeEach(async (to: any, from: any, next: any) => {
|
||||
const token = wsCache.get('user.token')
|
||||
if (!token) {
|
||||
if (whiteList.includes(to.path)) {
|
||||
next()
|
||||
} else {
|
||||
ElMessage.error('Please login first')
|
||||
next('/login')
|
||||
}
|
||||
if (whiteList.includes(to.path)) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
if (!token) {
|
||||
ElMessage.error('Please login first')
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
if (!wsCache.get('user.uid')) {
|
||||
await userStore.info()
|
||||
}
|
||||
if (to.path === '/') {
|
||||
next('/chat')
|
||||
return
|
||||
|
||||
+116
-21
@@ -2,29 +2,124 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { AuthApi } from '@/api/login'
|
||||
import { useCache } from '@/utils/useCache'
|
||||
import { i18n } from '@/i18n'
|
||||
import { store } from './index'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const token = ref('')
|
||||
const username = ref('')
|
||||
|
||||
const login = async (formData: { username: string; password: string }) => {
|
||||
username.value = formData.username
|
||||
const res: any = await AuthApi.login(formData)
|
||||
token.value = res.access_token
|
||||
wsCache.set('user.token', token.value)
|
||||
username.value = formData.username
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
token.value = ''
|
||||
username.value = ''
|
||||
wsCache.delete('user.token')
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
username,
|
||||
login,
|
||||
logout
|
||||
interface UserState {
|
||||
token: string
|
||||
uid: string
|
||||
name: string
|
||||
oid: string
|
||||
language: string
|
||||
exp: number
|
||||
time: number
|
||||
}
|
||||
|
||||
export const UserStore = defineStore('user', {
|
||||
state: (): UserState => {
|
||||
return {
|
||||
token: null,
|
||||
uid: null,
|
||||
name: null,
|
||||
oid: null,
|
||||
language: 'zh-CN',
|
||||
exp: null,
|
||||
time: null
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getToken(): string {
|
||||
return this.token
|
||||
},
|
||||
getUid(): string {
|
||||
return this.uid
|
||||
},
|
||||
getName(): string {
|
||||
return this.name
|
||||
},
|
||||
getOid(): string {
|
||||
return this.oid
|
||||
},
|
||||
getLanguage(): string {
|
||||
return this.language
|
||||
},
|
||||
getExp(): number {
|
||||
return this.exp
|
||||
},
|
||||
getTime(): number {
|
||||
return this.time
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
||||
async login(formData: { username: string; password: string }) {
|
||||
const res: any = await AuthApi.login(formData)
|
||||
this.setToken(res.access_token)
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.clear()
|
||||
},
|
||||
|
||||
async info () {
|
||||
const res: any = await AuthApi.info()
|
||||
const res_data = res || {}
|
||||
|
||||
const keys: string[] = ['uid', 'name', 'oid', 'language', 'exp', 'time']
|
||||
|
||||
keys.forEach(key => {
|
||||
const dkey = key === 'uid' ? 'id' : key
|
||||
this[key] = res_data[dkey]
|
||||
wsCache.set('user.' + key, this[key])
|
||||
})
|
||||
this.setLanguage(this.language)
|
||||
},
|
||||
setToken(token: string) {
|
||||
wsCache.set('user.token', token)
|
||||
this.token = token
|
||||
},
|
||||
setExp(exp: number) {
|
||||
wsCache.set('user.exp', exp)
|
||||
this.exp = exp
|
||||
},
|
||||
setTime(time: number) {
|
||||
wsCache.set('user.time', time)
|
||||
this.time = time
|
||||
},
|
||||
setUid(uid: string) {
|
||||
wsCache.set('user.uid', uid)
|
||||
this.uid = uid
|
||||
},
|
||||
setName(name: string) {
|
||||
wsCache.set('user.name', name)
|
||||
this.name = name
|
||||
},
|
||||
setOid(oid: string) {
|
||||
wsCache.set('user.oid', oid)
|
||||
this.oid = oid
|
||||
},
|
||||
setLanguage(language: string) {
|
||||
if (!language || language === 'zh_CN') {
|
||||
language = 'zh-CN'
|
||||
}
|
||||
wsCache.set('user.language', language)
|
||||
this.language = language
|
||||
i18n.global.locale.value = language
|
||||
/* const { locale } = useI18n()
|
||||
locale.value = language */
|
||||
// locale.setLang(language)
|
||||
},
|
||||
clear() {
|
||||
const keys: string[] = ['token', 'uid', 'name', 'oid', 'language', 'exp', 'time']
|
||||
keys.forEach(key => wsCache.delete('user.' + key))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export const useUserStore = () => {
|
||||
return UserStore(store)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {computed, nextTick, ref} from "vue";
|
||||
import type {TabsPaneContext} from 'element-plus-secondary'
|
||||
import {Loading} from "@element-plus/icons-vue";
|
||||
import Component from "./component/Component.vue"
|
||||
import type {ChartTypes} from "@/views/chat/component/BaseChart.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
message?: ChatMessage
|
||||
@@ -43,9 +44,13 @@ const dataObject = computed<{
|
||||
})
|
||||
|
||||
const chartObject = computed<{
|
||||
type: "table" | "bar" | "line" | "pie"
|
||||
type: ChartTypes
|
||||
title: string
|
||||
axis: { x: { name: string, value: string }, y: { name: string, value: string } }
|
||||
axis: {
|
||||
x: { name: string, value: string },
|
||||
y: { name: string, value: string },
|
||||
series: { name: string, value: string }
|
||||
}
|
||||
columns: Array<{ name: string, value: string }>
|
||||
}>(() => {
|
||||
if (props.message?.record?.chart) {
|
||||
@@ -66,10 +71,16 @@ const yAxis = computed(() => {
|
||||
}
|
||||
return []
|
||||
})
|
||||
const series = computed(() => {
|
||||
if (chartObject.value?.axis?.series) {
|
||||
return [chartObject.value.axis.series]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const currentChartType = ref<"table" | "bar" | 'column' | "line" | "pie" | undefined>(undefined)
|
||||
const currentChartType = ref<ChartTypes | undefined>(undefined)
|
||||
|
||||
const chartType = computed<"table" | "bar" | 'column' | "line" | "pie">({
|
||||
const chartType = computed<ChartTypes>({
|
||||
get() {
|
||||
if (currentChartType.value) {
|
||||
return currentChartType.value
|
||||
@@ -148,16 +159,19 @@ function onTypeChange(type: string) {
|
||||
</template>
|
||||
<template v-else-if="settings.type === 'chart'">
|
||||
<div>
|
||||
<div v-if="message.record.chart">
|
||||
<Component
|
||||
ref="chartRef"
|
||||
v-if="message.record.id"
|
||||
:id="message.record.id"
|
||||
:type="chartType"
|
||||
:columns="chartObject?.columns"
|
||||
:x="xAxis"
|
||||
:y="yAxis"
|
||||
:data="dataObject.data"/>
|
||||
<div v-if="message.record.chart" class="chart-base-container">
|
||||
<div>
|
||||
<Component
|
||||
ref="chartRef"
|
||||
v-if="message.record.id"
|
||||
:id="message.record.id"
|
||||
:type="chartType"
|
||||
:columns="chartObject?.columns"
|
||||
:x="xAxis"
|
||||
:y="yAxis"
|
||||
:series="series"
|
||||
:data="dataObject.data"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="message.record.error" style="color: red">
|
||||
@@ -184,4 +198,9 @@ function onTypeChange(type: string) {
|
||||
.type-tabs {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.chart-base-container {
|
||||
padding: 20px;
|
||||
background: rgba(224, 224, 226, 0.29);
|
||||
}
|
||||
</style>
|
||||
@@ -165,6 +165,10 @@ function handleCommand(command: string | number | object, chat: Chat) {
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon-more {
|
||||
|
||||
@@ -4,7 +4,8 @@ import {ChatInfo} from "@/api/chat.ts";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {datasourceApi} from "@/api/datasource.ts";
|
||||
import DatasourceItemCard from '../ds/DatasourceItemCard.vue'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: number
|
||||
currentChat: ChatInfo
|
||||
@@ -64,13 +65,13 @@ onMounted(() => {
|
||||
<template>
|
||||
<ChatBlock>
|
||||
<div class="welcome-content">
|
||||
<div>你好,我是SQLBot,很高兴为你服务</div>
|
||||
<div class="sub">我可以帮忙查询数据、生成图表、检测数据异常、预测数据等,请选择一个数据源,开启智能问数吧~</div>
|
||||
<div>{{ t('qa.greeting') }}</div>
|
||||
<div class="sub">{{ t('qa.description') }}</div>
|
||||
<template v-if="editable">
|
||||
<template v-if="dsList.length>0">
|
||||
<div class="ds-select-row">
|
||||
<div>选择数据源</div>
|
||||
<el-button @click="showDs" link type="primary">查看更多</el-button>
|
||||
<div>{{ t('qa.select_datasource') }}</div>
|
||||
<el-button @click="showDs" link type="primary">{{ t('qa.view_more') }}</el-button>
|
||||
</div>
|
||||
<div class="ds-row-container">
|
||||
<template v-for="(item, _index) in dsList" :key="_index">
|
||||
@@ -80,12 +81,12 @@ onMounted(() => {
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
数据源为空,请新建后再开启智能问数!
|
||||
{{ t('qa.empty_datasource') }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="ds-select-row">
|
||||
<div>已选择数据源</div>
|
||||
<div>{{ t('qa.selected_datasource') }}</div>
|
||||
</div>
|
||||
<div class="ds-row-container">
|
||||
<DatasourceItemCard :ds="ds"/>
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
export interface ChartAxis {
|
||||
name: string,
|
||||
value: string,
|
||||
type?: 'x' | 'y' | 'series'
|
||||
}
|
||||
|
||||
export interface ChartData {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type ChartTypes = "table" | "bar" | 'column' | "line" | "pie"
|
||||
|
||||
export abstract class BaseChart {
|
||||
id: string;
|
||||
_name: string = 'base-chart';
|
||||
axis: Array<{ name: string, value: string, type?: 'x' | 'y' }> = [];
|
||||
data: Array<{ [key: string]: any }> = [];
|
||||
axis: Array<ChartAxis> = [];
|
||||
data: Array<ChartData> = [];
|
||||
|
||||
constructor(id: string, name: string) {
|
||||
this.id = id;
|
||||
this._name = name;
|
||||
}
|
||||
|
||||
init(axis: Array<{ name: string, value: string, type?: 'x' | 'y' }>, data: Array<{ [key: string]: any }>): void {
|
||||
init(axis: Array<ChartAxis>, data: Array<ChartData>): void {
|
||||
this.axis = axis;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ export abstract class BaseG2Chart extends BaseChart {
|
||||
container: id,
|
||||
autoFit: true,
|
||||
})
|
||||
|
||||
this.chart.theme({
|
||||
view: {
|
||||
viewFill: '#FFFFFF',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, onUnmounted} from "vue";
|
||||
import {getChartInstance} from "@/views/chat/component/index.ts";
|
||||
import type {BaseChart} from "@/views/chat/component/BaseChart.ts";
|
||||
import type {BaseChart, ChartAxis, ChartData} from "@/views/chat/component/BaseChart.ts";
|
||||
|
||||
const params = withDefaults(defineProps<{
|
||||
id: string | number
|
||||
type: string
|
||||
data?: Array<{ [key: string]: any }>
|
||||
columns?: Array<{ name: string, value: string }>
|
||||
x?: Array<{ name: string, value: string }>
|
||||
y?: Array<{ name: string, value: string }>
|
||||
data?: Array<ChartData>
|
||||
columns?: Array<ChartAxis>
|
||||
x?: Array<ChartAxis>
|
||||
y?: Array<ChartAxis>
|
||||
series?: Array<ChartAxis>
|
||||
}>(), {
|
||||
data: () => [],
|
||||
columns: () => [],
|
||||
x: () => [],
|
||||
y: () => [],
|
||||
series: () => [],
|
||||
})
|
||||
|
||||
const chartId = computed(() => {
|
||||
@@ -22,7 +24,7 @@ const chartId = computed(() => {
|
||||
})
|
||||
|
||||
const axis = computed(() => {
|
||||
const _list: Array<{ name: string, value: string, type?: 'x' | 'y' }> = []
|
||||
const _list: Array<ChartAxis> = []
|
||||
params.columns.forEach(column => {
|
||||
_list.push({name: column.name, value: column.value})
|
||||
})
|
||||
@@ -32,6 +34,9 @@ const axis = computed(() => {
|
||||
params.y.forEach(column => {
|
||||
_list.push({name: column.name, value: column.value, type: 'y'})
|
||||
})
|
||||
params.series.forEach(column => {
|
||||
_list.push({name: column.name, value: column.value, type: 'series'})
|
||||
})
|
||||
return _list
|
||||
})
|
||||
|
||||
@@ -76,7 +81,7 @@ onUnmounted(() => {
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
min-width: 200px;
|
||||
min-height: 360px;
|
||||
min-width: 360px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
import {BaseG2Chart} from "@/views/chat/component/BaseG2Chart.ts";
|
||||
import type {ChartAxis, ChartData} from "@/views/chat/component/BaseChart.ts";
|
||||
|
||||
export class Bar extends BaseG2Chart {
|
||||
|
||||
@@ -6,7 +7,7 @@ export class Bar extends BaseG2Chart {
|
||||
super(id, "bar");
|
||||
}
|
||||
|
||||
init(axis: Array<{ name: string; value: string; type: "x" | "y" }>, data: Array<{ name: string; value: string }>) {
|
||||
init(axis: Array<ChartAxis>, data: Array<ChartData>) {
|
||||
super.init(axis, data);
|
||||
const x = this.axis.filter(item => item.type === "x");
|
||||
const y = this.axis.filter(item => item.type === "y");
|
||||
@@ -26,7 +27,7 @@ export class Bar extends BaseG2Chart {
|
||||
.scale('y', {
|
||||
nice: true,
|
||||
})
|
||||
.interaction('elementHighlight', { background: true });
|
||||
.interaction('elementHighlight', {background: true});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {BaseG2Chart} from "@/views/chat/component/BaseG2Chart.ts";
|
||||
import type {ChartAxis, ChartData} from "@/views/chat/component/BaseChart.ts";
|
||||
|
||||
export class Column extends BaseG2Chart {
|
||||
|
||||
@@ -6,7 +7,7 @@ export class Column extends BaseG2Chart {
|
||||
super(id, "column");
|
||||
}
|
||||
|
||||
init(axis: Array<{ name: string; value: string; type: "x" | "y" }>, data: Array<{ name: string; value: string }>) {
|
||||
init(axis: Array<ChartAxis>, data: Array<ChartData>) {
|
||||
super.init(axis, data);
|
||||
|
||||
const x = this.axis.filter(item => item.type === "x");
|
||||
@@ -26,7 +27,7 @@ export class Column extends BaseG2Chart {
|
||||
.scale('y', {
|
||||
nice: true,
|
||||
})
|
||||
.interaction('elementHighlight', { background: true });
|
||||
.interaction('elementHighlight', {background: true});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {BaseG2Chart} from "@/views/chat/component/BaseG2Chart.ts";
|
||||
import type {ChartAxis, ChartData} from "@/views/chat/component/BaseChart.ts";
|
||||
|
||||
export class Line extends BaseG2Chart {
|
||||
|
||||
@@ -6,7 +7,7 @@ export class Line extends BaseG2Chart {
|
||||
super(id, "line");
|
||||
}
|
||||
|
||||
init(axis: Array<{ name: string; value: string; type: "x" | "y" }>, data: Array<{ name: string; value: string }>) {
|
||||
init(axis: Array<ChartAxis>, data: Array<ChartData>) {
|
||||
super.init(axis, data);
|
||||
|
||||
const x = this.axis.filter(item => item.type === "x");
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import {BaseG2Chart} from "@/views/chat/component/BaseG2Chart.ts";
|
||||
import type {ChartAxis, ChartData} from "@/views/chat/component/BaseChart.ts";
|
||||
|
||||
export class Pie extends BaseG2Chart {
|
||||
|
||||
constructor(id: string) {
|
||||
super(id, "pie");
|
||||
}
|
||||
|
||||
init(axis: Array<ChartAxis>, data: Array<ChartData>) {
|
||||
super.init(axis, data);
|
||||
const y = this.axis.filter(item => item.type === "y");
|
||||
const series = this.axis.filter(item => item.type === "series");
|
||||
|
||||
if (series.length == 0 || y.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chart.coordinate({type: 'theta', outerRadius: 0.8});
|
||||
|
||||
this.chart?.interval()
|
||||
.transform({type: 'stackY'})
|
||||
.data(data)
|
||||
.encode('y', y[0].value)
|
||||
.encode('color', series[0].value)
|
||||
.legend('color', {position: 'bottom', layout: {justifyContent: 'center'}})
|
||||
;
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {BaseChart, type ChartAxis, type ChartData} from "@/views/chat/component/BaseChart.ts";
|
||||
import {TableSheet, type S2Options, type S2DataConfig, type S2MountContainer} from "@antv/s2";
|
||||
import {debounce} from "lodash-es";
|
||||
|
||||
export class Table extends BaseChart {
|
||||
|
||||
table?: TableSheet = undefined
|
||||
|
||||
container: S2MountContainer | null = null
|
||||
|
||||
debounceRender: any
|
||||
|
||||
resizeObserver: ResizeObserver
|
||||
|
||||
constructor(id: string) {
|
||||
super(id, "table");
|
||||
this.container = document.getElementById(id);
|
||||
|
||||
this.debounceRender = debounce(async (width?: number, height?: number) => {
|
||||
if (this.table) {
|
||||
this.table.changeSheetSize(width, height);
|
||||
await this.table.render(false);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(([entry] = []) => {
|
||||
const [size] = entry.borderBoxSize || [];
|
||||
this.debounceRender(size.inlineSize, size.blockSize);
|
||||
});
|
||||
|
||||
if (this.container?.parentElement) {
|
||||
this.resizeObserver.observe(this.container.parentElement);
|
||||
}
|
||||
}
|
||||
|
||||
init(axis: Array<ChartAxis>, data: Array<ChartData>) {
|
||||
super.init(axis, data);
|
||||
|
||||
const s2DataConfig: S2DataConfig = {
|
||||
fields: {
|
||||
columns: this.axis?.map(a => a.value) ?? [],
|
||||
},
|
||||
meta: this.axis?.map(a => {
|
||||
return {
|
||||
field: a.value,
|
||||
name: a.name
|
||||
}
|
||||
}) ?? [],
|
||||
data: this.data,
|
||||
};
|
||||
|
||||
const s2Options: S2Options = {
|
||||
width: 600,
|
||||
height: 360,
|
||||
placeholder: {
|
||||
cell: '-',
|
||||
empty: {
|
||||
icon: 'Empty',
|
||||
description: 'No Data',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (this.container) {
|
||||
this.table = new TableSheet(this.container, s2DataConfig, s2Options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
this.table?.render()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.table?.destroy()
|
||||
this.resizeObserver?.disconnect()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,11 +2,15 @@ import {BaseChart} from "@/views/chat/component/BaseChart.ts";
|
||||
import {Bar} from "@/views/chat/component/charts/Bar.ts";
|
||||
import {Column} from "@/views/chat/component/charts/Column.ts";
|
||||
import {Line} from "@/views/chat/component/charts/Line.ts";
|
||||
import {Table} from "@/views/chat/component/charts/Table.ts";
|
||||
import {Pie} from "@/views/chat/component/charts/Pie.ts";
|
||||
|
||||
const CHART_TYPE_MAP: { [key: string]: any } = {
|
||||
"table": Table,
|
||||
"column": Column,
|
||||
"bar": Bar,
|
||||
"line": Line,
|
||||
"pie": Pie
|
||||
}
|
||||
|
||||
const isParent = (type: any, parentType: any) => {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<el-icon>
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
New Conversation
|
||||
{{ t('qa.New Conversation') }}
|
||||
</el-button>
|
||||
</el-header>
|
||||
<el-main class="chat-list">
|
||||
@@ -46,7 +46,7 @@
|
||||
<template
|
||||
v-if="currentChat.datasource && currentChat.datasource_name"
|
||||
>
|
||||
Datasource:{{ currentChat.datasource_name }}
|
||||
{{ t('ds.title') }}:{{ currentChat.datasource_name }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
@@ -57,7 +57,7 @@
|
||||
type="textarea"
|
||||
:rows="1"
|
||||
:autosize="{ minRows: 1, maxRows: 8 }"
|
||||
placeholder="Press Enter to send, Ctrl + Enter for new line"
|
||||
:placeholder="t('qa.question_placeholder')"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
@keydown.ctrl.enter.exact.prevent="handleCtrlEnter"
|
||||
/>
|
||||
@@ -85,6 +85,8 @@ import {Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi,} fro
|
||||
import ChatList from "./ChatList.vue";
|
||||
import ChatRow from "./ChatRow.vue";
|
||||
import ChatAnswer from "./ChatAnswer.vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
const inputMessage = ref("");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user