diff --git a/backend/alembic/versions/8dc3b1bdbfef_010_upgrade_user_language.py b/backend/alembic/versions/8dc3b1bdbfef_010_upgrade_user_language.py
new file mode 100644
index 00000000..cca99de1
--- /dev/null
+++ b/backend/alembic/versions/8dc3b1bdbfef_010_upgrade_user_language.py
@@ -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 ###
diff --git a/backend/apps/chat/api/chat.py b/backend/apps/chat/api/chat.py
index ed632f3a..e1f1d4dc 100644
--- a/backend/apps/chat/api/chat.py
+++ b/backend/apps/chat/api/chat.py
@@ -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)
diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py
index dee73241..c6fa330d 100644
--- a/backend/apps/chat/task/llm.py
+++ b/backend/apps/chat/task/llm.py
@@ -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)
diff --git a/backend/apps/datasource/api/datasource.py b/backend/apps/datasource/api/datasource.py
index b0921673..08f2e6f7 100644
--- a/backend/apps/datasource/api/datasource.py
+++ b/backend/apps/datasource/api/datasource.py
@@ -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)
diff --git a/backend/apps/system/api/user.py b/backend/apps/system/api/user.py
index 6992c883..754850e1 100644
--- a/backend/apps/system/api/user.py
+++ b/backend/apps/system/api/user.py
@@ -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)
\ No newline at end of file
+ **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}
\ No newline at end of file
diff --git a/backend/apps/system/crud/user.py b/backend/apps/system/crud/user.py
index a522fe24..89125544 100644
--- a/backend/apps/system/crud/user.py
+++ b/backend/apps/system/crud/user.py
@@ -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:
diff --git a/backend/apps/system/models/user.py b/backend/apps/system/models/user.py
index 7fc12dd7..9d2eddf1 100644
--- a/backend/apps/system/models/user.py
+++ b/backend/apps/system/models/user.py
@@ -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")
diff --git a/backend/apps/system/schemas/system_schema.py b/backend/apps/system/schemas/system_schema.py
index 237612cd..f7eab2f7 100644
--- a/backend/apps/system/schemas/system_schema.py
+++ b/backend/apps/system/schemas/system_schema.py
@@ -2,4 +2,8 @@ from pydantic import BaseModel
class model_status(BaseModel):
status: bool
- ids: list[int]
\ No newline at end of file
+ ids: list[int]
+
+
+class UserLanguage(BaseModel):
+ language: str
\ No newline at end of file
diff --git a/backend/apps/template/template.py b/backend/apps/template/template.py
index 7dd5b1cc..e342a84f 100644
--- a/backend/apps/template/template.py
+++ b/backend/apps/template/template.py
@@ -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)
diff --git a/backend/common/utils/whitelist.py b/backend/common/utils/whitelist.py
index c02dfe44..78010184 100644
--- a/backend/common/utils/whitelist.py
+++ b/backend/common/utils/whitelist.py
@@ -20,7 +20,9 @@ wlist = [
"*.woff2",
"*.ttf",
"*.eot",
- "*.otf"
+ "*.otf",
+ "/mcp",
+ "/mcp/message"
]
class WhitelistChecker:
diff --git a/backend/main.py b/backend/main.py
index 2cb6b8d3..bef058cb 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -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)
\ No newline at end of file
+
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 535c219b..41aaea6a 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -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"
diff --git a/backend/template.yaml b/backend/template.yaml
index 5256e6f2..300f7a82 100644
--- a/backend/template.yaml
+++ b/backend/template.yaml
@@ -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": "抱歉,我无法回答您的问题。"}}
diff --git a/frontend/package.json b/frontend/package.json
index 69e4e4b4..032ddb4f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts
index d3ebe6e8..3850463c 100644
--- a/frontend/src/api/auth.ts
+++ b/frontend/src/api/auth.ts
@@ -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),
}
\ No newline at end of file
diff --git a/frontend/src/api/login.ts b/frontend/src/api/login.ts
index ccd6284a..64c80cef 100644
--- a/frontend/src/api/login.ts
+++ b/frontend/src/api/login.ts
@@ -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')
}
\ No newline at end of file
diff --git a/frontend/src/components/Language-selector/index.vue b/frontend/src/components/Language-selector/index.vue
new file mode 100644
index 00000000..d9aaefda
--- /dev/null
+++ b/frontend/src/components/Language-selector/index.vue
@@ -0,0 +1,63 @@
+
+
+
+
{{ selectedLanguage === 'zh-CN' ? '中文' : 'English' }}
+
+
+
+
+
+
+
+ English
+
+
+ 中文
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/layout/index.vue b/frontend/src/components/layout/index.vue
index e91f5931..2fcfbe73 100644
--- a/frontend/src/components/layout/index.vue
+++ b/frontend/src/components/layout/index.vue
@@ -3,7 +3,7 @@
-->
- {{ item.meta.title }}
+ {{ t(`menu.${item.meta.title}`) }}
-
- {{ t('common.system_manage') }}
+ {{ t('common.system_manage') }}
@@ -59,12 +58,11 @@
-
+
@@ -77,21 +75,7 @@
Switch Layout
Logout
-
-
-
- Language
-
-
-
-
-
-
- English
- 中文
-
-
-
+
@@ -108,7 +92,7 @@
- System manage
+ {{ t('common.system_manage') }}
@@ -121,6 +105,8 @@
Switch Layout
Logout
+
+
@@ -138,7 +124,7 @@
- {{ item.meta.title }}
+ {{ t(`menu.${item.meta.title}`) }}
@@ -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);
- }
-}
\ No newline at end of file
diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json
index affc2b46..8e77ec92 100644
--- a/frontend/src/i18n/en.json
+++ b/frontend/src/i18n/en.json
@@ -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"
}
}
\ No newline at end of file
diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts
index 9fb39600..2166eede 100644
--- a/frontend/src/i18n/index.ts
+++ b/frontend/src/i18n/index.ts
@@ -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 = {
diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json
index 3207616a..7311c089 100644
--- a/frontend/src/i18n/zh-CN.json
+++ b/frontend/src/i18n/zh-CN.json
@@ -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": "连接失败,请检查配置"
}
}
\ No newline at end of file
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index c0fd1513..8fba4d81 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -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",
diff --git a/frontend/src/router/watch.ts b/frontend/src/router/watch.ts
index b13adfef..710d3c1d 100644
--- a/frontend/src/router/watch.ts
+++ b/frontend/src/router/watch.ts
@@ -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
diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts
index f4990698..a22a6237 100644
--- a/frontend/src/stores/user.ts
+++ b/frontend/src/stores/user.ts
@@ -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))
+ }
}
-})
\ No newline at end of file
+})
+
+export const useUserStore = () => {
+ return UserStore(store)
+}
\ No newline at end of file
diff --git a/frontend/src/views/chat/ChatAnswer.vue b/frontend/src/views/chat/ChatAnswer.vue
index 8630b785..4150305b 100644
--- a/frontend/src/views/chat/ChatAnswer.vue
+++ b/frontend/src/views/chat/ChatAnswer.vue
@@ -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(undefined)
-const chartType = computed<"table" | "bar" | 'column' | "line" | "pie">({
+const chartType = computed({
get() {
if (currentChartType.value) {
return currentChartType.value
@@ -148,16 +159,19 @@ function onTypeChange(type: string) {
-
@@ -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);
+}
\ No newline at end of file
diff --git a/frontend/src/views/chat/ChatList.vue b/frontend/src/views/chat/ChatList.vue
index b6066df1..b19902cf 100644
--- a/frontend/src/views/chat/ChatList.vue
+++ b/frontend/src/views/chat/ChatList.vue
@@ -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 {
diff --git a/frontend/src/views/chat/WelcomeBlock.vue b/frontend/src/views/chat/WelcomeBlock.vue
index b3deef5e..7fdc90ce 100644
--- a/frontend/src/views/chat/WelcomeBlock.vue
+++ b/frontend/src/views/chat/WelcomeBlock.vue
@@ -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(() => {
-
你好,我是SQLBot,很高兴为你服务
-
我可以帮忙查询数据、生成图表、检测数据异常、预测数据等,请选择一个数据源,开启智能问数吧~
+
{{ t('qa.greeting') }}
+
{{ t('qa.description') }}
-
选择数据源
-
查看更多
+
{{ t('qa.select_datasource') }}
+
{{ t('qa.view_more') }}
@@ -80,12 +81,12 @@ onMounted(() => {
- 数据源为空,请新建后再开启智能问数!
+ {{ t('qa.empty_datasource') }}
-
已选择数据源
+
{{ t('qa.selected_datasource') }}
diff --git a/frontend/src/views/chat/component/BaseChart.ts b/frontend/src/views/chat/component/BaseChart.ts
index d2ad74c7..8f1fdeff 100644
--- a/frontend/src/views/chat/component/BaseChart.ts
+++ b/frontend/src/views/chat/component/BaseChart.ts
@@ -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 = [];
+ data: Array = [];
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, data: Array): void {
this.axis = axis;
this.data = data;
}
diff --git a/frontend/src/views/chat/component/BaseG2Chart.ts b/frontend/src/views/chat/component/BaseG2Chart.ts
index 88177f88..6e4d7dbe 100644
--- a/frontend/src/views/chat/component/BaseG2Chart.ts
+++ b/frontend/src/views/chat/component/BaseG2Chart.ts
@@ -11,6 +11,12 @@ export abstract class BaseG2Chart extends BaseChart {
container: id,
autoFit: true,
})
+
+ this.chart.theme({
+ view: {
+ viewFill: '#FFFFFF',
+ },
+ })
}
render() {
diff --git a/frontend/src/views/chat/component/Component.vue b/frontend/src/views/chat/component/Component.vue
index 9b8e9f54..04aeaa43 100644
--- a/frontend/src/views/chat/component/Component.vue
+++ b/frontend/src/views/chat/component/Component.vue
@@ -1,20 +1,22 @@