Merge pull request #2216 from xinnan-tech/manager-web-i18n

Manager web i18n
This commit is contained in:
欣南科技
2025-09-12 18:11:15 +08:00
committed by GitHub
8 changed files with 386 additions and 283 deletions
+5 -11
View File
@@ -20,16 +20,17 @@
{{ $t('home.voiceModel') }}{{ device.ttsModelName }} ({{ device.ttsVoiceName }}) {{ $t('home.voiceModel') }}{{ device.ttsModelName }} ({{ device.ttsVoiceName }})
</div> </div>
<div style="display: flex;gap: 10px;align-items: center;"> <div style="display: flex;gap: 10px;align-items: center;">
<div :class="['settings-btn', {'settings-btn-en': isEnglish}]" @click="handleConfigure"> <div class="settings-btn" @click="handleConfigure">
{{ $t('home.configureRole') }} {{ $t('home.configureRole') }}
</div> </div>
<div :class="['settings-btn', {'settings-btn-en': isEnglish}]" @click="handleVoicePrint"> <div class="settings-btn" @click="handleVoicePrint">
{{ $t('home.voiceprintRecognition') }} {{ $t('home.voiceprintRecognition') }}
</div> </div>
<div :class="['settings-btn', {'settings-btn-en': isEnglish}]" @click="handleDeviceManage"> <div class="settings-btn" @click="handleDeviceManage">
{{ $t('home.deviceManagement') }}({{ device.deviceCount }}) {{ $t('home.deviceManagement') }}({{ device.deviceCount }})
</div> </div>
<div :class="['settings-btn', {'settings-btn-en': isEnglish, 'disabled-btn': device.memModelId === 'Memory_nomem'}]" @click="handleChatHistory"> <div :class="['settings-btn', { 'disabled-btn': device.memModelId === 'Memory_nomem' }]"
@click="handleChatHistory">
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" :content="$t('home.enableMemory')" placement="top"> <el-tooltip v-if="device.memModelId === 'Memory_nomem'" :content="$t('home.enableMemory')" placement="top">
<span>{{ $t('home.chatHistory') }}</span> <span>{{ $t('home.chatHistory') }}</span>
</el-tooltip> </el-tooltip>
@@ -130,13 +131,6 @@ export default {
border-radius: 14px; border-radius: 14px;
} }
.settings-btn.settings-btn-en {
font-size: 11px;
height: 39px;
min-width: 65px;
padding: 0 16px;
}
.version-info { .version-info {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
+25 -4
View File
@@ -72,7 +72,7 @@
<!-- 语言切换下拉菜单 --> <!-- 语言切换下拉菜单 -->
<el-dropdown trigger="click" class="language-dropdown" @visible-change="handleLanguageDropdownVisibleChange"> <el-dropdown trigger="click" class="language-dropdown" @visible-change="handleLanguageDropdownVisibleChange">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<i class="el-icon-document"></i> <span class="current-language-text">{{ currentLanguageText }}</span>
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i> <i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
@@ -95,7 +95,8 @@
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i> <i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="showChangePasswordDialog">{{ $t('header.changePassword') }}</el-dropdown-item> <el-dropdown-item @click.native="showChangePasswordDialog">{{ $t('header.changePassword')
}}</el-dropdown-item>
<el-dropdown-item @click.native="handleLogout">{{ $t('header.logout') }}</el-dropdown-item> <el-dropdown-item @click.native="handleLogout">{{ $t('header.logout') }}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
@@ -109,10 +110,9 @@
<script> <script>
import userApi from '@/apis/module/user'; import userApi from '@/apis/module/user';
import i18n, { changeLanguage } from '@/i18n';
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件 import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
import { changeLanguage } from '@/i18n';
import i18n from '@/i18n';
export default { export default {
name: 'HeaderBar', name: 'HeaderBar',
@@ -142,6 +142,20 @@ export default {
// 获取当前语言 // 获取当前语言
currentLanguage() { currentLanguage() {
return i18n.locale || 'zh_CN'; return i18n.locale || 'zh_CN';
},
// 获取当前语言显示文本
currentLanguageText() {
const currentLang = this.currentLanguage;
switch (currentLang) {
case 'zh_CN':
return this.$t('language.zhCN');
case 'zh_TW':
return this.$t('language.zhTW');
case 'en':
return this.$t('language.en');
default:
return this.$t('language.zhCN');
}
} }
}, },
mounted() { mounted() {
@@ -390,6 +404,13 @@ export default {
margin-right: 5px; margin-right: 5px;
} }
.current-language-text {
margin-left: 4px;
margin-right: 4px;
font-size: 12px;
color: #3d4566;
}
.more-dropdown { .more-dropdown {
padding-right: 20px; padding-right: 20px;
} }
+28 -28
View File
@@ -1,12 +1,12 @@
export default { export default {
// HeaderBar组件文本 // HeaderBar组件文本
'header.smartManagement': 'Smart Agent Management', 'header.smartManagement': 'Agents',
'header.modelConfig': 'Model Configuration', 'header.modelConfig': 'Models',
'header.userManagement': 'User Management', 'header.userManagement': 'Users',
'header.otaManagement': 'OTA Management', 'header.otaManagement': 'OTA',
'header.paramDictionary': 'Parameter Dictionary', 'header.paramDictionary': 'More',
'header.paramManagement': 'Parameter Management', 'header.paramManagement': 'Params Management',
'header.dictManagement': 'Dictionary Management', 'header.dictManagement': 'Dict Management',
// Dictionary data dialog related // Dictionary data dialog related
'dictDataDialog.addDictData': 'Add Dictionary Data', 'dictDataDialog.addDictData': 'Add Dictionary Data',
@@ -142,13 +142,13 @@ export default {
'firmwareDialog.uploadSuccess': 'Firmware file uploaded successfully', 'firmwareDialog.uploadSuccess': 'Firmware file uploaded successfully',
'firmwareDialog.uploadFailed': 'File upload failed', 'firmwareDialog.uploadFailed': 'File upload failed',
'header.providerManagement': 'Provider Management', 'header.providerManagement': 'Provider Management',
'header.serverSideManagement': 'Server-side Management', 'header.serverSideManagement': 'Server Management',
'header.changePassword': 'Change Password', 'header.changePassword': 'Change Password',
'header.logout': 'Logout', 'header.logout': 'Logout',
'header.searchPlaceholder': 'Search by name..', 'header.searchPlaceholder': 'Search by name..',
// Login page text // Login page text
'login.title': 'XiaoZhi Service Login', 'login.title': 'Login',
'login.welcome': 'WELCOME TO LOGIN', 'login.welcome': 'WELCOME TO LOGIN',
'login.username': 'Username', 'login.username': 'Username',
'login.usernamePlaceholder': 'Please enter username', 'login.usernamePlaceholder': 'Please enter username',
@@ -163,7 +163,7 @@ export default {
'login.forgetPassword': 'Forgot Password?', 'login.forgetPassword': 'Forgot Password?',
'login.mobileLogin': 'Mobile Login', 'login.mobileLogin': 'Mobile Login',
'login.usernameLogin': 'Username Login', 'login.usernameLogin': 'Username Login',
'login.agreeTo': 'Logging in means you agree to the', 'login.agreeTo': 'Login implies acceptance of',
'login.userAgreement': 'User Agreement', 'login.userAgreement': 'User Agreement',
'login.and': 'and', 'login.and': 'and',
'login.privacyPolicy': 'Privacy Policy', 'login.privacyPolicy': 'Privacy Policy',
@@ -483,17 +483,17 @@ export default {
'user.search': 'Search', 'user.search': 'Search',
// Language switch // Language switch
'language.zhCN': 'Simplified Chinese', 'language.zhCN': '中文简体',
'language.zhTW': 'Traditional Chinese', 'language.zhTW': '中文繁體',
'language.en': 'English', 'language.en': 'English',
// Home page text // Home page text
'home.addAgent': 'Add Agent', 'home.addAgent': 'Add Agent',
'home.languageModel': 'Language Model', 'home.languageModel': 'LLM',
'home.voiceModel': 'Voice Model', 'home.voiceModel': 'TTS',
'home.configureRole': 'Configure Role', 'home.configureRole': 'Config Role',
'home.voiceprintRecognition': 'Voiceprint Recognition', 'home.voiceprintRecognition': 'Voiceprint',
'home.deviceManagement': 'Device Management', 'home.deviceManagement': 'Devices',
'home.chatHistory': 'Chat History', 'home.chatHistory': 'Chat History',
'home.lastConversation': 'Last Conversation', 'home.lastConversation': 'Last Conversation',
'home.noConversation': 'No conversation yet', 'home.noConversation': 'No conversation yet',
@@ -539,7 +539,7 @@ export default {
'home.confirmDeleteAgent': 'Are you sure you want to delete this agent?', 'home.confirmDeleteAgent': 'Are you sure you want to delete this agent?',
'home.deleteSuccess': 'Delete successful', 'home.deleteSuccess': 'Delete successful',
'home.deleteFailed': 'Delete failed', 'home.deleteFailed': 'Delete failed',
'home.enableMemory': 'Please enable memory in the "Configure Role" page first', 'home.enableMemory': 'Please enable memory in the "Config Role" page first',
// Server side management page text // Server side management page text
'serverSideManager.pageTitle': 'Server Management', 'serverSideManager.pageTitle': 'Server Management',
@@ -571,18 +571,18 @@ export default {
'roleConfig.restartNotice': 'After saving the configuration, you need to restart the device for the new configuration to take effect.', 'roleConfig.restartNotice': 'After saving the configuration, you need to restart the device for the new configuration to take effect.',
'roleConfig.saveConfig': 'Save Configuration', 'roleConfig.saveConfig': 'Save Configuration',
'roleConfig.reset': 'Reset', 'roleConfig.reset': 'Reset',
'roleConfig.agentName': 'Agent Nickname', 'roleConfig.agentName': 'Nickname',
'roleConfig.roleTemplate': 'Role Template', 'roleConfig.roleTemplate': 'Template',
'roleConfig.roleIntroduction': 'Role Introduction', 'roleConfig.roleIntroduction': 'Introduction',
'roleConfig.languageCode': 'Language Code', 'roleConfig.languageCode': 'Language Code',
'roleConfig.interactionLanguage': 'Interaction Language', 'roleConfig.interactionLanguage': 'Interaction Language',
'roleConfig.vad': 'VAD', 'roleConfig.vad': 'Voice Detect',
'roleConfig.asr': 'ASR', 'roleConfig.asr': 'Speech Recognition',
'roleConfig.llm': 'LLM', 'roleConfig.llm': 'Language Model',
'roleConfig.vllm': 'VLLM', 'roleConfig.vllm': 'Vision Model',
'roleConfig.tts': 'TTS', 'roleConfig.tts': 'Text-to-Speech',
'roleConfig.memory': 'Memory', 'roleConfig.memory': 'Memory',
'roleConfig.intent': 'Intent', 'roleConfig.intent': 'Intent Recognition',
'roleConfig.voiceType': 'Voice Type', 'roleConfig.voiceType': 'Voice Type',
'roleConfig.pleaseEnterContent': 'Please enter content', 'roleConfig.pleaseEnterContent': 'Please enter content',
'roleConfig.pleaseEnterLangCode': 'Please enter language code, e.g.: en_US', 'roleConfig.pleaseEnterLangCode': 'Please enter language code, e.g.: en_US',
@@ -814,4 +814,4 @@ export default {
'providerDialog.selectFieldsToDelete': 'Please select fields to delete first', 'providerDialog.selectFieldsToDelete': 'Please select fields to delete first',
'providerDialog.confirmBatchDeleteFields': 'Are you sure to delete {count} selected fields?', 'providerDialog.confirmBatchDeleteFields': 'Are you sure to delete {count} selected fields?',
'providerDialog.batchDeleteFieldsSuccess': 'Successfully deleted {count} fields', 'providerDialog.batchDeleteFieldsSuccess': 'Successfully deleted {count} fields',
}; };
+3 -3
View File
@@ -148,8 +148,8 @@ export default {
'header.searchPlaceholder': '输入名称搜索..', 'header.searchPlaceholder': '输入名称搜索..',
// 登录页面文本 // 登录页面文本
'login.title': '小智服务登录', 'login.title': '登录',
'login.welcome': '欢迎登录', 'login.welcome': 'WELCOME TO LOGIN',
'login.username': '用户名', 'login.username': '用户名',
'login.usernamePlaceholder': '请输入用户名', 'login.usernamePlaceholder': '请输入用户名',
'login.mobilePlaceholder': '请输入手机号码', 'login.mobilePlaceholder': '请输入手机号码',
@@ -484,7 +484,7 @@ export default {
// 语言切换 // 语言切换
'language.zhCN': '中文简体', 'language.zhCN': '中文简体',
'language.zhTW': '中文繁', 'language.zhTW': '中文繁',
'language.en': 'English', 'language.en': 'English',
// 首页文本 // 首页文本
+3 -3
View File
@@ -148,8 +148,8 @@ export default {
'header.searchPlaceholder': '輸入名稱搜索..', 'header.searchPlaceholder': '輸入名稱搜索..',
// 登录页面文本 // 登录页面文本
'login.title': '小智服務登錄', 'login.title': '登錄',
'login.welcome': '歡迎登錄', 'login.welcome': 'WELCOME TO LOGIN',
'login.username': '用戶名', 'login.username': '用戶名',
'login.usernamePlaceholder': '請輸入用戶名', 'login.usernamePlaceholder': '請輸入用戶名',
'login.mobilePlaceholder': '請輸入手機號碼', 'login.mobilePlaceholder': '請輸入手機號碼',
@@ -411,7 +411,7 @@ export default {
'user.search': '搜索', 'user.search': '搜索',
// 語言切換 // 語言切換
'language.zhCN': '中文簡體', 'language.zhCN': '中文简体',
'language.zhTW': '中文繁體', 'language.zhTW': '中文繁體',
'language.en': 'English', 'language.en': 'English',
+35 -24
View File
@@ -47,12 +47,13 @@
<!-- 右侧内容 --> <!-- 右侧内容 -->
<div class="content-area"> <div class="content-area">
<el-card class="model-card" shadow="never"> <el-card class="model-card" shadow="never">
<el-table ref="modelTable" style="width: 100%" v-loading="loading" :element-loading-text="$t('modelConfig.loading')" <el-table ref="modelTable" style="width: 100%" v-loading="loading"
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)" :element-loading-text="$t('modelConfig.loading')" element-loading-spinner="el-icon-loading"
:header-cell-style="{ background: 'transparent' }" :data="modelList" class="data-table" element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-style="{ background: 'transparent' }"
header-row-class-name="table-header" :header-cell-class-name="headerCellClassName" :data="modelList" class="data-table" header-row-class-name="table-header"
@selection-change="handleSelectionChange"> :header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" :cell-class-name="selectionCellClassName"></el-table-column> <el-table-column type="selection" width="55" align="center"
:cell-class-name="selectionCellClassName"></el-table-column>
<el-table-column :label="$t('modelConfig.modelId')" prop="id" align="center"></el-table-column> <el-table-column :label="$t('modelConfig.modelId')" prop="id" align="center"></el-table-column>
<el-table-column :label="$t('modelConfig.modelName')" prop="modelName" align="center"></el-table-column> <el-table-column :label="$t('modelConfig.modelName')" prop="modelName" align="center"></el-table-column>
<el-table-column :label="$t('modelConfig.provider')" align="center"> <el-table-column :label="$t('modelConfig.provider')" align="center">
@@ -79,7 +80,7 @@
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('modelConfig.action')" align="center" width="180px"> <el-table-column :label="$t('modelConfig.action')" align="center" width="210px">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn"> <el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
{{ $t('modelConfig.edit') }} {{ $t('modelConfig.edit') }}
@@ -109,19 +110,26 @@
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> <el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('modelConfig.itemsPerPage', { items: item })" :value="item"> <el-option v-for="item in pageSizeOptions" :key="item"
:label="$t('modelConfig.itemsPerPage', { items: item })" :value="item">
</el-option> </el-option>
</el-select> </el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{ $t('modelConfig.firstPage') }}</button> <button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">{{ $t('modelConfig.prevPage') }}</button> $t('modelConfig.firstPage')
}}</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">{{
$t('modelConfig.prevPage')
}}</button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn" <button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)"> :class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }} {{ page }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{ $t('modelConfig.nextPage') }}</button> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{
$t('modelConfig.nextPage')
}}</button>
<span class="total-text">{{ $t('modelConfig.totalRecords', { total }) }}</span> <span class="total-text">{{ $t('modelConfig.totalRecords', { total }) }}</span>
</div> </div>
</div> </div>
@@ -345,17 +353,17 @@ export default {
const id = formData.id; const id = formData.id;
if (this.editModelData.duplicateMode) { if (this.editModelData.duplicateMode) {
Api.model.addModel({modelType, provideCode, formData}, Api.model.addModel({ modelType, provideCode, formData },
({ data }) => { ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success(this.$t('modelConfig.duplicateSuccess')); this.$message.success(this.$t('modelConfig.duplicateSuccess'));
this.loadData(); this.loadData();
this.editDialogVisible = false; this.editDialogVisible = false;
} else { } else {
this.$message.error(data.msg || this.$t('modelConfig.duplicateFailed')); this.$message.error(data.msg || this.$t('modelConfig.duplicateFailed'));
} }
done && done(); // 调用done回调关闭加载状态 done && done(); // 调用done回调关闭加载状态
}) })
} }
else { else {
Api.model.updateModel( Api.model.updateModel(
@@ -790,9 +798,11 @@ export default {
content: attr(data-content); content: attr(data-content);
display: block; display: block;
text-align: center; text-align: center;
line-height: 32px; /* 设置合适的行高,确保文本完整显示 */ line-height: 32px;
/* 设置合适的行高,确保文本完整显示 */
color: black; color: black;
margin-top: 0; /* 移除可能导致偏移的上边距 */ margin-top: 0;
/* 移除可能导致偏移的上边距 */
height: 32px; height: 32px;
position: absolute; position: absolute;
top: 50%; top: 50%;
@@ -804,6 +814,7 @@ export default {
.custom-selection-header .cell { .custom-selection-header .cell {
position: relative; position: relative;
} }
/* 已移除可能影响文本显示的空伪元素 */ /* 已移除可能影响文本显示的空伪元素 */
::v-deep .el-table__body .el-checkbox__inner { ::v-deep .el-table__body .el-checkbox__inner {
+18 -10
View File
@@ -33,14 +33,16 @@
<el-tag v-else type="danger">{{ $t('user.disabled') }}</el-tag> <el-tag v-else type="danger">{{ $t('user.disabled') }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('modelConfig.action')" align="center"> <el-table-column :label="$t('modelConfig.action')" align="center" width="300px">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" @click="resetPassword(scope.row)">{{ $t('user.resetPassword') }}</el-button> <el-button size="mini" type="text" @click="resetPassword(scope.row)">{{ $t('user.resetPassword')
}}</el-button>
<el-button size="mini" type="text" v-if="scope.row.status === 1" <el-button size="mini" type="text" v-if="scope.row.status === 1"
@click="handleChangeStatus(scope.row, 0)">{{ $t('user.disableAccount') }}</el-button> @click="handleChangeStatus(scope.row, 0)">{{ $t('user.disableAccount') }}</el-button>
<el-button size="mini" type="text" v-if="scope.row.status === 0" <el-button size="mini" type="text" v-if="scope.row.status === 0"
@click="handleChangeStatus(scope.row, 1)">{{ $t('user.enableAccount') }}</el-button> @click="handleChangeStatus(scope.row, 1)">{{ $t('user.enableAccount') }}</el-button>
<el-button size="mini" type="text" @click="deleteUser(scope.row)">{{ $t('user.deleteUser') }}</el-button> <el-button size="mini" type="text" @click="deleteUser(scope.row)">{{ $t('user.deleteUser')
}}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -50,14 +52,20 @@
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll"> <el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isAllSelected ? $t('user.deselectAll') : $t('user.selectAll') }} {{ isAllSelected ? $t('user.deselectAll') : $t('user.selectAll') }}
</el-button> </el-button>
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">{{ $t('user.enable') }}</el-button> <el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">{{
$t('user.enable')
}}</el-button>
<el-button size="mini" type="warning" @click="batchDisable"><i <el-button size="mini" type="warning" @click="batchDisable"><i
class="el-icon-remove-outline rotated-icon"></i>{{ $t('user.disable') }}</el-button> class="el-icon-remove-outline rotated-icon"></i>{{
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">{{ $t('user.delete') }}</el-button> $t('user.disable') }}</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">{{ $t('user.delete')
}}</el-button>
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" :class="['page-size-select', { 'page-size-select-en': isEnglish }]"> <el-select v-model="pageSize" @change="handlePageSizeChange"
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('modelConfig.itemsPerPage', { count: item })" :value="item"> :class="['page-size-select', { 'page-size-select-en': isEnglish }]">
<el-option v-for="item in pageSizeOptions" :key="item"
:label="$t('modelConfig.itemsPerPage', { count: item })" :value="item">
</el-option> </el-option>
</el-select> </el-select>
+153 -84
View File
@@ -1,45 +1,59 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<el-container style="height: 100%;"> <el-container style="height: 100%">
<el-header> <el-header>
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;"> <div style="
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" /> display: flex;
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px;" /> align-items: center;
margin-top: 15px;
margin-left: 10px;
gap: 10px;
">
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px; height: 45px" />
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px" />
</div> </div>
</el-header> </el-header>
<div class="login-person"> <div class="login-person">
<img loading="lazy" alt="" src="@/assets/login/login-person.png" style="width: 100%;" /> <img loading="lazy" alt="" src="@/assets/login/login-person.png" style="width: 100%" />
</div> </div>
<el-main style="position: relative;"> <el-main style="position: relative">
<div class="login-box" @keyup.enter="login"> <div class="login-box" @keyup.enter="login">
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;"> <div style="
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" /> display: flex;
<div class="login-text">{{ $t('login.title') }}</div> align-items: center;
gap: 20px;
margin-bottom: 39px;
padding: 0 30px;
">
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px; height: 34px" />
<div class="login-text">{{ $t("login.title") }}</div>
<div class="login-welcome"> <div class="login-welcome">
{{ $t('login.welcome') }} {{ $t("login.welcome") }}
</div> </div>
<!-- 语言切换下拉菜单 --> <!-- 语言切换下拉菜单 -->
<el-dropdown trigger="click" class="title-language-dropdown" @visible-change="handleLanguageDropdownVisibleChange"> <el-dropdown trigger="click" class="title-language-dropdown"
@visible-change="handleLanguageDropdownVisibleChange">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<i class="el-icon-document"></i> <i class="el-icon-document"></i>
<span class="current-language-text">{{ currentLanguageText }}</span>
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i> <i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="changeLanguage('zh_CN')"> <el-dropdown-item @click.native="changeLanguage('zh_CN')">
{{ $t('language.zhCN') }} {{ $t("language.zhCN") }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="changeLanguage('zh_TW')"> <el-dropdown-item @click.native="changeLanguage('zh_TW')">
{{ $t('language.zhTW') }} {{ $t("language.zhTW") }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="changeLanguage('en')"> <el-dropdown-item @click.native="changeLanguage('en')">
{{ $t('language.en') }} {{ $t("language.en") }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> </div>
<div style="padding: 0 30px;"> <div style="padding: 0 30px">
<!-- 用户名登录 --> <!-- 用户名登录 -->
<template v-if="!isMobileLogin"> <template v-if="!isMobileLogin">
<div class="input-box"> <div class="input-box">
@@ -51,8 +65,8 @@
<!-- 手机号登录 --> <!-- 手机号登录 -->
<template v-else> <template v-else>
<div class="input-box"> <div class="input-box">
<div style="display: flex; align-items: center; width: 100%;"> <div style="display: flex; align-items: center; width: 100%">
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px;"> <el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px">
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`" <el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
:value="item.key" /> :value="item.key" />
</el-select> </el-select>
@@ -63,27 +77,45 @@
<div class="input-box"> <div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" /> <img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
<el-input v-model="form.password" :placeholder="$t('login.passwordPlaceholder')" type="password" show-password /> <el-input v-model="form.password" :placeholder="$t('login.passwordPlaceholder')" type="password"
show-password />
</div> </div>
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;"> <div style="
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;"> display: flex;
align-items: center;
margin-top: 20px;
width: 100%;
gap: 10px;
">
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" /> <img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
<el-input v-model="form.captcha" :placeholder="$t('login.captchaPlaceholder')" style="flex: 1;" /> <el-input v-model="form.captcha" :placeholder="$t('login.captchaPlaceholder')" style="flex: 1" />
</div> </div>
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码" <img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" /> style="width: 150px; height: 40px; cursor: pointer" @click="fetchCaptcha" />
</div> </div>
<div <div style="
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;"> font-weight: 400;
<div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">{{ $t('login.register') }}</div> font-size: 14px;
<div style="cursor: pointer;" @click="goToForgetPassword" v-if="enableMobileRegister">{{ $t('login.forgetPassword') }}</div> text-align: left;
color: #5778ff;
display: flex;
justify-content: space-between;
margin-top: 20px;
">
<div v-if="allowUserRegister" style="cursor: pointer" @click="goToRegister">
{{ $t("login.register") }}
</div>
<div style="cursor: pointer" @click="goToForgetPassword" v-if="enableMobileRegister">
{{ $t("login.forgetPassword") }}
</div>
</div> </div>
</div> </div>
<div class="login-btn" @click="login">{{ $t('login.login') }}</div> <div class="login-btn" @click="login">{{ $t("login.login") }}</div>
<!-- 登录方式切换按钮 --> <!-- 登录方式切换按钮 -->
<div class="login-type-container" v-if="enableMobileRegister"> <div class="login-type-container" v-if="enableMobileRegister">
<div style="display: flex; gap: 10px;"> <div style="display: flex; gap: 10px">
<el-tooltip :content="$t('login.mobileLogin')" placement="bottom"> <el-tooltip :content="$t('login.mobileLogin')" placement="bottom">
<el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle <el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle
@click="switchLoginType('mobile')"></el-button> @click="switchLoginType('mobile')"></el-button>
@@ -94,11 +126,15 @@
</el-tooltip> </el-tooltip>
</div> </div>
</div> </div>
<div style="font-size: 14px;color: #979db1;"> <div style="font-size: 14px; color: #979db1">
{{ $t('login.agreeTo') }} {{ $t("login.agreeTo") }}
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('login.userAgreement') }}</div> <div style="display: inline-block; color: #5778ff; cursor: pointer">
{{ $t('login.and') }} {{ $t("login.userAgreement") }}
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('login.privacyPolicy') }}</div> </div>
{{ $t("login.and") }}
<div style="display: inline-block; color: #5778ff; cursor: pointer">
{{ $t("login.privacyPolicy") }}
</div>
</div> </div>
</div> </div>
</el-main> </el-main>
@@ -110,44 +146,62 @@
</template> </template>
<script> <script>
import Api from '@/apis/api'; import Api from "@/apis/api";
import VersionFooter from '@/components/VersionFooter.vue'; import VersionFooter from "@/components/VersionFooter.vue";
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils'; import i18n, { changeLanguage } from "@/i18n";
import { mapState } from 'vuex'; import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from "@/utils";
import { changeLanguage } from '@/i18n'; import { mapState } from "vuex";
export default { export default {
name: 'login', name: "login",
components: { components: {
VersionFooter VersionFooter,
}, },
computed: { computed: {
...mapState({ ...mapState({
allowUserRegister: state => state.pubConfig.allowUserRegister, allowUserRegister: (state) => state.pubConfig.allowUserRegister,
enableMobileRegister: state => state.pubConfig.enableMobileRegister, enableMobileRegister: (state) => state.pubConfig.enableMobileRegister,
mobileAreaList: state => state.pubConfig.mobileAreaList mobileAreaList: (state) => state.pubConfig.mobileAreaList,
}) }),
// 获取当前语言
currentLanguage() {
return i18n.locale || "zh_CN";
},
// 获取当前语言显示文本
currentLanguageText() {
const currentLang = this.currentLanguage;
switch (currentLang) {
case "zh_CN":
return this.$t("language.zhCN");
case "zh_TW":
return this.$t("language.zhTW");
case "en":
return this.$t("language.en");
default:
return this.$t("language.zhCN");
}
},
}, },
data() { data() {
return { return {
activeName: "username", activeName: "username",
form: { form: {
username: '', username: "",
password: '', password: "",
captcha: '', captcha: "",
captchaId: '', captchaId: "",
areaCode: '+86', areaCode: "+86",
mobile: '' mobile: "",
}, },
captchaUuid: '', captchaUuid: "",
captchaUrl: '', captchaUrl: "",
isMobileLogin: false, isMobileLogin: false,
languageDropdownVisible: false languageDropdownVisible: false,
} };
}, },
mounted() { mounted() {
this.fetchCaptcha(); this.fetchCaptcha();
this.$store.dispatch('fetchPubConfig').then(() => { this.$store.dispatch("fetchPubConfig").then(() => {
// 根据配置决定默认登录方式 // 根据配置决定默认登录方式
this.isMobileLogin = this.enableMobileRegister; this.isMobileLogin = this.enableMobileRegister;
}); });
@@ -155,8 +209,8 @@ export default {
methods: { methods: {
fetchCaptcha() { fetchCaptcha() {
if (this.$store.getters.getToken) { if (this.$store.getters.getToken) {
if (this.$route.path !== '/home') { if (this.$route.path !== "/home") {
this.$router.push('/home') this.$router.push("/home");
} }
} else { } else {
this.captchaUuid = getUUID(); this.captchaUuid = getUUID();
@@ -166,7 +220,7 @@ export default {
const blob = new Blob([res.data], { type: res.data.type }); const blob = new Blob([res.data], { type: res.data.type });
this.captchaUrl = URL.createObjectURL(blob); this.captchaUrl = URL.createObjectURL(blob);
} else { } else {
showDanger('验证码加载失败,点击刷新'); showDanger("验证码加载失败,点击刷新");
} }
}); });
} }
@@ -182,19 +236,19 @@ export default {
changeLanguage(lang); changeLanguage(lang);
this.languageDropdownVisible = false; this.languageDropdownVisible = false;
this.$message.success({ this.$message.success({
message: this.$t('message.success'), message: this.$t("message.success"),
showClose: true showClose: true,
}); });
}, },
// 切换登录方式 // 切换登录方式
switchLoginType(type) { switchLoginType(type) {
this.isMobileLogin = type === 'mobile'; this.isMobileLogin = type === "mobile";
// 清空表单 // 清空表单
this.form.username = ''; this.form.username = "";
this.form.mobile = ''; this.form.mobile = "";
this.form.password = ''; this.form.password = "";
this.form.captcha = ''; this.form.captcha = "";
this.fetchCaptcha(); this.fetchCaptcha();
}, },
@@ -211,38 +265,46 @@ export default {
if (this.isMobileLogin) { if (this.isMobileLogin) {
// 手机号登录验证 // 手机号登录验证
if (!validateMobile(this.form.mobile, this.form.areaCode)) { if (!validateMobile(this.form.mobile, this.form.areaCode)) {
showDanger('请输入正确的手机号码'); showDanger("请输入正确的手机号码");
return; return;
} }
// 拼接手机号作为用户名 // 拼接手机号作为用户名
this.form.username = this.form.areaCode + this.form.mobile; this.form.username = this.form.areaCode + this.form.mobile;
} else { } else {
// 用户名登录验证 // 用户名登录验证
if (!this.validateInput(this.form.username, '用户名不能为空')) { if (!this.validateInput(this.form.username, "用户名不能为空")) {
return; return;
} }
} }
// 验证密码 // 验证密码
if (!this.validateInput(this.form.password, '密码不能为空')) { if (!this.validateInput(this.form.password, "密码不能为空")) {
return; return;
} }
// 验证验证码 // 验证验证码
if (!this.validateInput(this.form.captcha, '验证码不能为空')) { if (!this.validateInput(this.form.captcha, "验证码不能为空")) {
return; return;
} }
this.form.captchaId = this.captchaUuid this.form.captchaId = this.captchaUuid;
Api.user.login(this.form, ({ data }) => { Api.user.login(
showSuccess('登录成功!'); this.form,
this.$store.commit('setToken', JSON.stringify(data.data)); ({ data }) => {
goToPage('/home'); showSuccess("登录成功!");
}, (err) => { this.$store.commit("setToken", JSON.stringify(data.data));
showDanger(err.data.msg || '登录失败') goToPage("/home");
if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) { },
this.fetchCaptcha() (err) => {
showDanger(err.data.msg || "登录失败");
if (
err.data != null &&
err.data.msg != null &&
err.data.msg.indexOf("图形验证码") > -1
) {
this.fetchCaptcha();
}
} }
}) );
// 重新获取验证码 // 重新获取验证码
setTimeout(() => { setTimeout(() => {
@@ -251,16 +313,16 @@ export default {
}, },
goToRegister() { goToRegister() {
goToPage('/register') goToPage("/register");
}, },
goToForgetPassword() { goToForgetPassword() {
goToPage('/retrieve-password') goToPage("/retrieve-password");
}, },
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import './auth.scss'; @import "./auth.scss";
.login-type-container { .login-type-container {
margin: 10px 20px; margin: 10px 20px;
@@ -273,6 +335,13 @@ export default {
margin-left: 20px; margin-left: 20px;
} }
.current-language-text {
margin-left: 4px;
margin-right: 4px;
font-size: 12px;
color: #3d4566;
}
.language-dropdown { .language-dropdown {
margin-left: auto; margin-left: auto;
} }