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/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/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..68cbf836 100644 --- a/frontend/src/components/layout/index.vue +++ b/frontend/src/components/layout/index.vue @@ -77,21 +77,7 @@ Switch Layout Logout - - -
- Language - - - -
- -
+
@@ -121,6 +107,8 @@ Switch Layout Logout + + @@ -170,8 +158,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() @@ -234,9 +223,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 }) @@ -558,12 +544,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/index.ts b/frontend/src/i18n/index.ts index 10cd6420..2166eede 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -3,11 +3,12 @@ import en from './en.json' import zhCN from './zh-CN.json' 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/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