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 @@ + + + + + \ 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 - - - -
- -
+
@@ -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) {