Feature/add multilingual support (#384)

* feat: add multilingual support (English and Russian)

- Add i18n infrastructure with vue-i18n
- Implement language switcher component
- Add Russian (ru-RU) and English (en-US) translations
- Configure TDesign locale for proper UI component translation
- Replace hardcoded Chinese strings with i18n keys
- Support dynamic language switching in all components
- Add translations for all UI elements including:
  - Menu items and navigation
  - Knowledge base management
  - Chat interface
  - Settings and initialization
  - Authentication pages
  - Separator options in document splitting

This enables users to use the application in Chinese, English, or Russian.

* chore: add vue-i18n dependency and fix Input-field i18n integration

- Add vue-i18n package to frontend dependencies
- Fix Input-field component i18n integration for multilingual support

* chore: add PROGRESS_RU.md to .gitignore

- Exclude personal progress tracking file from git

* rearrange the order of the multilingual languages: Chinese, English, Russian

* Delete docker-compose.yml

* Replaced hardcoded messages with the t() function in the following files:  all error messages, 14 console.error ,messages session creation messages
, login/registration errors

* fix: restore docker-compose.yml and update .gitignore

* restore docker-compose.yml latest

* add multilingual support
This commit is contained in:
Aleksandr
2025-11-05 07:32:16 +03:00
committed by GitHub
parent 1fd2de5a64
commit da640d1d33
27 changed files with 2385 additions and 534 deletions

4
.gitignore vendored
View File

@@ -24,6 +24,9 @@ node_modules/
tmp/
temp/
# Docker compose файл (локальные настройки)
# docker-compose.yml
WeKnora
/models/
services/docreader/src/proto/__pycache__
@@ -36,3 +39,4 @@ data/files/
### macOS
# General
.DS_Store
PROGRESS_RU.md

View File

@@ -22,6 +22,7 @@
"tdesign-icons-vue-next": "^0.4.1",
"tdesign-vue-next": "^1.11.5",
"vue": "^3.5.13",
"vue-i18n": "^9.9.0",
"vue-router": "^4.5.0",
"webpack": "^5.94.0"
},

View File

@@ -1,9 +1,31 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { ConfigProvider } from 'tdesign-vue-next'
import enUS from 'tdesign-vue-next/es/locale/en_US'
import zhCN from 'tdesign-vue-next/es/locale/zh_CN'
import ruRU from 'tdesign-vue-next/es/locale/ru_RU'
const { locale } = useI18n()
const tdesignLocale = computed(() => {
switch (locale.value) {
case 'en-US':
return enUS
case 'ru-RU':
return ruRU
case 'zh-CN':
default:
return zhCN
}
})
</script>
<template>
<div id="app">
<RouterView />
</div>
<ConfigProvider :global-config="tdesignLocale">
<div id="app">
<RouterView />
</div>
</ConfigProvider>
</template>
<style>
body,

View File

@@ -1,8 +1,11 @@
<script setup lang="ts">
import { ref, defineEmits, onMounted, defineProps, defineExpose } from "vue";
import { useI18n } from 'vue-i18n';
import useKnowledgeBase from '@/hooks/useKnowledgeBase';
import { onBeforeRouteUpdate } from 'vue-router';
import { MessagePlugin } from "tdesign-vue-next";
const { t } = useI18n();
let { cardList, total, getKnowled } = useKnowledgeBase()
let query = ref("");
const props = defineProps({
@@ -17,15 +20,15 @@ onMounted(() => {
const emit = defineEmits(['send-msg']);
const createSession = (val: string) => {
if (!val.trim()) {
MessagePlugin.info("请先输入内容!");
MessagePlugin.info(t('chat.pleaseEnterContent'));
return
}
if (!query.value && cardList.value.length == 0) {
MessagePlugin.info("请先上传知识库!");
MessagePlugin.info(t('chat.pleaseUploadKnowledgeBase'));
return;
}
if (props.isReplying) {
return MessagePlugin.error("正在回复中,请稍后再试!");
return MessagePlugin.error(t('chat.replyingPleaseWait'));
}
emit('send-msg', val);
clearvalue();
@@ -50,9 +53,9 @@ onBeforeRouteUpdate((to, from, next) => {
</script>
<template>
<div class="answers-input">
<t-textarea v-model="query" placeholder="基于知识库提问" name="description" :autosize="true" @keydown="onKeydown" />
<t-textarea v-model="query" :placeholder="t('chat.askKnowledgeBase')" name="description" :autosize="true" @keydown="onKeydown" />
<div class="answers-input-source">
<span>{{ total }}个来源</span>
<span>{{ t('chat.sourcesCount', { count: total }) }}</span>
</div>
<div @click="createSession(query)" class="answers-input-send"
:class="[query.length && total ? '' : 'grey-out']">

View File

@@ -0,0 +1,67 @@
<template>
<div class="language-switcher">
<t-select
v-model="selectedLanguage"
:options="languageOptions"
@change="handleLanguageChange"
:popup-props="{ overlayClassName: 'language-select-popup' }"
size="small"
>
<template #prefixIcon>
<t-icon name="translate" />
</template>
</t-select>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
const languageOptions = [
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
{ label: 'Русский', value: 'ru-RU' }
]
const selectedLanguage = ref(localStorage.getItem('locale') || 'zh-CN')
const handleLanguageChange = (value: string) => {
console.log('Язык изменен на:', value)
if (value && ['ru-RU', 'en-US', 'zh-CN'].includes(value)) {
locale.value = value
localStorage.setItem('locale', value)
// Перезагрузка страницы для применения нового языка
setTimeout(() => {
window.location.reload()
}, 100)
}
}
// Синхронизация с i18n при инициализации
watch(() => locale.value, (newLocale) => {
if (selectedLanguage.value !== newLocale) {
selectedLanguage.value = newLocale
}
}, { immediate: true })
</script>
<style lang="less" scoped>
.language-switcher {
.t-button {
color: #666;
font-size: 14px;
&:hover {
color: #333;
background-color: rgba(0, 0, 0, 0.04);
}
}
.t-icon {
margin-right: 4px;
}
}
</style>

View File

@@ -1,11 +1,14 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<div class="empty">
<img class="empty-img" src="@/assets/img/upload.svg" alt="">
<span class="empty-txt">知识为空拖放上传</span>
<span class="empty-type-txt">pdfdoc 格式文件不超过10M</span>
<span class="empty-type-txt">textmarkdown格式文件不超过200K</span>
<span class="empty-txt">{{ t('knowledgeBase.emptyKnowledgeDragDrop') }}</span>
<span class="empty-type-txt">{{ t('knowledgeBase.pdfDocFormat') }}</span>
<span class="empty-type-txt">{{ t('knowledgeBase.textMarkdownFormat') }}</span>
</div>
</template>
<style scoped lang="less">

View File

@@ -14,7 +14,7 @@
<div class="menu_icon">
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'logout' ? logoutIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
</div>
<span class="menu_title" :title="item.path === 'knowledge-bases' && kbMenuItem ? kbMenuItem.title : item.title">{{ item.path === 'knowledge-bases' && kbMenuItem ? kbMenuItem.title : item.title }}</span>
<span class="menu_title" :title="item.path === 'knowledge-bases' && kbMenuItem?.title ? kbMenuItem.title : t(item.titleKey)">{{ item.path === 'knowledge-bases' && kbMenuItem?.title ? kbMenuItem.title : t(item.titleKey) }}</span>
<!-- 知识库切换下拉箭头 -->
<div v-if="item.path === 'knowledge-bases' && isInKnowledgeBase"
class="kb-dropdown-icon"
@@ -39,7 +39,7 @@
{{ kb.name }}
</div>
</div>
<t-popup overlayInnerClassName="upload-popup" class="placement top center" content="上传知识"
<t-popup overlayInnerClassName="upload-popup" class="placement top center" :content="t('menu.uploadKnowledge')"
placement="top" show-arrow destroy-on-close>
<div class="upload-file-wrap" @click.stop="uploadFile" variant="outline"
v-if="item.path === 'knowledge-bases' && $route.name === 'knowledgeBaseDetail'">
@@ -66,7 +66,7 @@
<t-icon name="ellipsis" class="menu-more" />
</div>
<template #content>
<span class="del_submenu">删除记录</span>
<span class="del_submenu">{{ t('menu.deleteRecord') }}</span>
</template>
</t-popup>
</div>
@@ -74,13 +74,13 @@
</div>
</div>
</div>
<!-- 下半部分账户信息系统设置退出登录 -->
<div class="menu_bottom">
<div class="menu_box" v-for="(item, index) in bottomMenuItems" :key="'bottom-' + index">
<div v-if="item.path === 'logout'">
<t-popconfirm
content="确定要退出登录吗?"
<t-popconfirm
:content="t('menu.confirmLogout')"
@confirm="handleLogout"
placement="top"
:show-arrow="true"
@@ -91,7 +91,7 @@
<div class="menu_icon">
<img class="icon" :src="getImgSrc(logoutIcon)" alt="">
</div>
<span class="menu_title">{{ item.title }}</span>
<span class="menu_title">{{ t(item.titleKey) }}</span>
</div>
</div>
</t-popconfirm>
@@ -103,7 +103,7 @@
<div class="menu_icon">
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
</div>
<span class="menu_title">{{ item.path === 'knowledge-bases' && kbMenuItem ? kbMenuItem.title : item.title }}</span>
<span class="menu_title">{{ item.path === 'knowledge-bases' && kbMenuItem?.title ? kbMenuItem.title : t(item.titleKey) }}</span>
</div>
</div>
</div>
@@ -118,12 +118,14 @@
import { storeToRefs } from 'pinia';
import { onMounted, watch, computed, ref, reactive, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { getSessionsList, delSession } from "@/api/chat/index";
import { getKnowledgeBaseById, listKnowledgeBases, uploadKnowledgeFile } from '@/api/knowledge-base';
import { kbFileTypeVerification } from '@/utils/index';
import { useMenuStore } from '@/stores/menu';
import { useAuthStore } from '@/stores/auth';
import { MessagePlugin } from "tdesign-vue-next";
const { t } = useI18n();
let uploadInput = ref();
const usemenuStore = useMenuStore();
const authStore = useAuthStore();
@@ -234,14 +236,14 @@ const uploadFile = async () => {
const kb = kbResponse.data;
// 检查知识库是否已初始化(有 EmbeddingModelID 和 SummaryModelID
if (!kb.embedding_model_id || kb.embedding_model_id === '' ||
if (!kb.embedding_model_id || kb.embedding_model_id === '' ||
!kb.summary_model_id || kb.summary_model_id === '') {
MessagePlugin.warning("该知识库尚未完成初始化配置,请先前往设置页面配置模型信息后再上传文件");
MessagePlugin.warning(t('knowledgeBase.notInitialized'));
return;
}
} catch (error) {
console.error('获取知识库信息失败:', error);
MessagePlugin.error("获取知识库信息失败,无法上传文件");
MessagePlugin.error(t('knowledgeBase.getInfoFailed'));
return;
}
}
@@ -260,7 +262,7 @@ const upload = async (e: any) => {
// 获取当前知识库ID
const currentKbId = (route.params as any)?.kbId as string;
if (!currentKbId) {
MessagePlugin.error("缺少知识库ID");
MessagePlugin.error(t('knowledgeBase.missingId'));
return;
}
@@ -280,24 +282,24 @@ const upload = async (e: any) => {
const isSuccess = responseData.success || responseData.code === 200 || responseData.status === 'success' || (!responseData.error && responseData);
if (isSuccess) {
MessagePlugin.info("上传成功!");
MessagePlugin.info(t('file.uploadSuccess'));
} else {
// 改进错误信息提取逻辑
let errorMessage = "上传失败!";
let errorMessage = t('file.uploadFailed');
if (responseData.error && responseData.error.message) {
errorMessage = responseData.error.message;
} else if (responseData.message) {
errorMessage = responseData.message;
}
if (responseData.code === 'duplicate_file' || (responseData.error && responseData.error.code === 'duplicate_file')) {
errorMessage = "文件已存在";
errorMessage = t('file.fileExists');
}
MessagePlugin.error(errorMessage);
}
} catch (err: any) {
let errorMessage = "上传失败!";
let errorMessage = t('file.uploadFailed');
if (err.code === 'duplicate_file') {
errorMessage = "文件已存在";
errorMessage = t('file.fileExists');
} else if (err.error && err.error.message) {
errorMessage = err.error.message;
} else if (err.message) {
@@ -331,7 +333,7 @@ const delCard = (index: number, item: any) => {
}
}
} else {
MessagePlugin.error("删除失败,请稍后再试!");
MessagePlugin.error(t('knowledgeBase.deleteFailed'));
}
})
}
@@ -392,7 +394,7 @@ const getMessageList = async () => {
// 过滤出当前知识库的会话
const filtered = res.data.filter((s: any) => s.knowledge_base_id === kbId)
filtered.forEach((item: any) => {
let obj = { title: item.title ? item.title : "新会话", path: `chat/${kbId}/${item.id}`, id: item.id, isMore: false, isNoTitle: item.title ? false : true }
let obj = { title: item.title ? item.title : t('menu.newSession'), path: `chat/${kbId}/${item.id}`, id: item.id, isMore: false, isNoTitle: item.title ? false : true }
usemenuStore.updatemenuArr(obj)
});
loading.value = false;

View File

@@ -0,0 +1,24 @@
import { createI18n } from 'vue-i18n'
import zhCN from './locales/zh-CN.ts'
import ruRU from './locales/ru-RU.ts'
import enUS from './locales/en-US.ts'
const messages = {
'zh-CN': zhCN,
'en-US': enUS,
'ru-RU': ruRU
}
// Получаем сохраненный язык из localStorage или используем китайский по умолчанию
const savedLocale = localStorage.getItem('locale') || 'zh-CN'
console.log('i18n инициализация с языком:', savedLocale)
const i18n = createI18n({
legacy: false,
locale: savedLocale,
fallbackLocale: 'zh-CN',
globalInjection: true,
messages
})
export default i18n

View File

@@ -0,0 +1,553 @@
export default {
menu: {
knowledgeBase: 'Knowledge Base',
chat: 'Chat',
createChat: 'Create Chat',
tenant: 'Account Info',
settings: 'System Settings',
logout: 'Logout',
uploadKnowledge: 'Upload Knowledge',
deleteRecord: 'Delete Record',
newSession: 'New Chat',
confirmLogout: 'Are you sure you want to logout?',
systemInfo: 'System Information'
},
knowledgeBase: {
title: 'Knowledge Base',
list: 'Knowledge Base List',
detail: 'Knowledge Base Details',
create: 'Create Knowledge Base',
edit: 'Edit Knowledge Base',
delete: 'Delete Knowledge Base',
name: 'Name',
description: 'Description',
files: 'Files',
settings: 'Settings',
upload: 'Upload File',
uploadSuccess: 'File uploaded successfully!',
uploadFailed: 'File upload failed!',
fileExists: 'File already exists',
notInitialized: 'Knowledge base is not initialized. Please configure models in settings before uploading files',
getInfoFailed: 'Failed to get knowledge base information, file upload is not possible',
missingId: 'Knowledge base ID is missing',
deleteFailed: 'Delete failed. Please try again later!',
createKnowledgeBase: 'Create Knowledge Base',
knowledgeBaseName: 'Knowledge Base Name',
enterName: 'Enter knowledge base name',
embeddingModel: 'Embedding Model',
selectEmbeddingModel: 'Select embedding model',
summaryModel: 'Summary Model',
selectSummaryModel: 'Select summary model',
rerankModel: 'Rerank Model',
selectRerankModel: 'Select rerank model (optional)',
createSuccess: 'Knowledge base created successfully',
createFailed: 'Failed to create knowledge base',
updateSuccess: 'Knowledge base updated successfully',
updateFailed: 'Failed to update knowledge base',
deleteSuccess: 'Knowledge base deleted successfully',
deleteConfirm: 'Are you sure you want to delete this knowledge base?',
fileName: 'File Name',
fileSize: 'File Size',
uploadTime: 'Upload Time',
status: 'Status',
actions: 'Actions',
processing: 'Processing',
completed: 'Completed',
failed: 'Failed',
noFiles: 'No files',
dragFilesHere: 'Drag files here or',
clickToUpload: 'click to upload',
supportedFormats: 'Supported formats',
maxFileSize: 'Max file size',
viewDetails: 'View Details',
downloadFile: 'Download File',
deleteFile: 'Delete File',
confirmDeleteFile: 'Are you sure you want to delete this file?',
totalFiles: 'Total files',
totalSize: 'Total size',
// Additional translations for KnowledgeBase.vue
newSession: 'New Chat',
deleteDocument: 'Delete Document',
parsingFailed: 'Parsing failed',
parsingInProgress: 'Parsing...',
deleteConfirmation: 'Delete Confirmation',
confirmDeleteDocument: 'Confirm deletion of document "{fileName}", recovery will be impossible after deletion',
cancel: 'Cancel',
confirmDelete: 'Confirm Delete',
selectKnowledgeBaseFirst: 'Please select a knowledge base first',
sessionCreationFailed: 'Failed to create chat session',
sessionCreationError: 'Chat session creation error',
settingsParsingFailed: 'Failed to parse settings',
fileUploadEventReceived: 'File upload event received, uploaded knowledge base ID: {uploadedKbId}, current knowledge base ID: {currentKbId}',
matchingKnowledgeBase: 'Matching knowledge base, starting file list update',
routeParamChange: 'Route parameter change, re-fetching knowledge base content',
fileUploadEventListening: 'Listening for file upload events',
apiCallKnowledgeFiles: 'Direct API call to get knowledge base file list',
responseInterceptorData: 'Since the response interceptor has already returned data, result is part of the response data',
hookProcessing: 'Processing according to useKnowledgeBase hook method',
errorHandling: 'Error handling',
priorityCurrentPageKbId: 'Priority to use knowledge base ID of current page',
fallbackLocalStorageKbId: 'If current page has no knowledge base ID, attempt to get knowledge base ID from settings in localStorage',
// Additional translations for KnowledgeBaseList.vue
createNewKnowledgeBase: 'Create Knowledge Base',
uninitializedWarning: 'Some knowledge bases are not initialized, you need to configure model information in settings first to add knowledge documents',
initializedStatus: 'Initialized',
notInitializedStatus: 'Not Initialized',
needSettingsFirst: 'You need to configure model information in settings first to add knowledge',
documents: 'Documents',
configureModelsFirst: 'Please configure model information in settings first',
confirmDeleteKnowledgeBase: 'Confirm deletion of this knowledge base?',
createKnowledgeBaseDialog: 'Create Knowledge Base',
enterNameKb: 'Enter name',
enterDescriptionKb: 'Enter description',
createKb: 'Create',
deleted: 'Deleted',
deleteFailedKb: 'Delete failed',
noDescription: 'No description',
emptyKnowledgeDragDrop: 'Knowledge is empty, drag and drop to upload',
pdfDocFormat: 'pdf, doc format files, max 10M',
textMarkdownFormat: 'text, markdown format files, max 200K',
dragFileNotText: 'Please drag files instead of text or links'
},
chat: {
title: 'Chat',
newChat: 'New Chat',
inputPlaceholder: 'Enter your message...',
send: 'Send',
thinking: 'Thinking...',
regenerate: 'Regenerate',
copy: 'Copy',
delete: 'Delete',
reference: 'Reference',
noMessages: 'No messages',
// Additional translations for chat components
waitingForAnswer: 'Waiting for answer...',
cannotAnswer: 'Sorry, I cannot answer this question.',
summarizingAnswer: 'Summarizing answer...',
loading: 'Loading...',
enterDescription: 'Enter description',
referencedContent: '{count} related materials used',
deepThinking: 'Deep thinking completed',
knowledgeBaseQandA: 'Knowledge Base Q&A',
askKnowledgeBase: 'Ask the knowledge base',
sourcesCount: '{count} sources',
pleaseEnterContent: 'Please enter content!',
pleaseUploadKnowledgeBase: 'Please upload knowledge base first!',
replyingPleaseWait: 'Replying, please try again later!',
createSessionFailed: 'Failed to create session',
createSessionError: 'Session creation error',
unableToGetKnowledgeBaseId: 'Unable to get knowledge base ID'
},
settings: {
title: 'Settings',
system: 'System Settings',
systemConfig: 'System Configuration',
knowledgeBaseSettings: 'Knowledge Base Settings',
configureKbModels: 'Configure models and document splitting parameters for this knowledge base',
manageSystemModels: 'Manage and update system models and service configurations',
basicInfo: 'Basic Information',
documentSplitting: 'Document Splitting',
apiEndpoint: 'API Endpoint',
enterApiEndpoint: 'Enter API endpoint, e.g.: http://localhost',
enterApiKey: 'Enter API key',
enterKnowledgeBaseId: 'Enter knowledge base ID',
saveConfig: 'Save Configuration',
reset: 'Reset',
configSaved: 'Configuration saved successfully',
enterApiEndpointRequired: 'Enter API endpoint',
enterApiKeyRequired: 'Enter API key',
enterKnowledgeBaseIdRequired: 'Enter knowledge base ID',
name: 'Name',
enterName: 'Enter name',
description: 'Description',
chunkSize: 'Chunk Size',
chunkOverlap: 'Chunk Overlap',
save: 'Save',
saving: 'Saving...',
saveSuccess: 'Saved successfully',
saveFailed: 'Failed to save',
model: 'Model',
llmModel: 'LLM Model',
embeddingModel: 'Embedding Model',
rerankModel: 'Rerank Model',
vlmModel: 'Multimodal Model',
modelName: 'Model Name',
modelUrl: 'Model URL',
apiKey: 'API Key',
cancel: 'Cancel',
saveFailedSettings: 'Failed to save settings',
enterNameRequired: 'Enter name'
},
initialization: {
title: 'Initialization',
welcome: 'Welcome to WeKnora',
description: 'Please configure the system before starting',
step1: 'Step 1: Configure LLM Model',
step2: 'Step 2: Configure Embedding Model',
step3: 'Step 3: Configure Additional Models',
complete: 'Complete Initialization',
skip: 'Skip',
next: 'Next',
previous: 'Previous',
// Ollama service
ollamaServiceStatus: 'Ollama Service Status',
refreshStatus: 'Refresh Status',
ollamaServiceAddress: 'Ollama Service Address',
notConfigured: 'Not Configured',
notRunning: 'Not Running',
normal: 'Normal',
installedModels: 'Installed Models',
none: 'None temporarily',
// Knowledge base
knowledgeBaseInfo: 'Knowledge Base Information',
knowledgeBaseName: 'Knowledge Base Name',
knowledgeBaseNamePlaceholder: 'Enter knowledge base name',
knowledgeBaseDescription: 'Knowledge Base Description',
knowledgeBaseDescriptionPlaceholder: 'Enter knowledge base description',
// LLM model
llmModelConfig: 'LLM Large Language Model Configuration',
modelSource: 'Model Source',
local: 'Ollama (Local)',
remote: 'Remote API (Remote)',
modelName: 'Model Name',
modelNamePlaceholder: 'E.g.: qwen3:0.6b',
baseUrl: 'Base URL',
baseUrlPlaceholder: 'E.g.: https://api.openai.com/v1, remove /chat/completions from the end of URL',
apiKey: 'API Key (Optional)',
apiKeyPlaceholder: 'Enter API Key (Optional)',
downloadModel: 'Download Model',
installed: 'Installed',
notInstalled: 'Not Installed',
notChecked: 'Not Checked',
checkConnection: 'Check Connection',
connectionNormal: 'Connection Normal',
connectionFailed: 'Connection Failed',
checkingConnection: 'Checking Connection',
// Embedding model
embeddingModelConfig: 'Embedding Model Configuration',
embeddingWarning: 'Knowledge base already has files, cannot change embedding model configuration',
dimension: 'Dimension',
dimensionPlaceholder: 'Enter vector dimension',
detectDimension: 'Detect Dimension',
// Rerank model
rerankModelConfig: 'Rerank Model Configuration',
enableRerank: 'Enable Rerank Model',
// Multimodal settings
multimodalConfig: 'Multimodal Configuration',
enableMultimodal: 'Enable image information extraction',
visualLanguageModelConfig: 'Visual Language Model Configuration',
interfaceType: 'Interface Type',
openaiCompatible: 'OpenAI Compatible Interface',
// Storage settings
storageServiceConfig: 'Storage Service Configuration',
storageType: 'Storage Type',
bucketName: 'Bucket Name',
bucketNamePlaceholder: 'Enter Bucket name',
pathPrefix: 'Path Prefix',
pathPrefixPlaceholder: 'E.g.: images',
secretId: 'Secret ID',
secretIdPlaceholder: 'Enter COS Secret ID',
secretKey: 'Secret Key',
secretKeyPlaceholder: 'Enter COS Secret Key',
region: 'Region',
regionPlaceholder: 'E.g.: ap-beijing',
appId: 'App ID',
appIdPlaceholder: 'Enter App ID',
// Multimodal function testing
functionTest: 'Function Test',
testDescription: 'Upload an image to test the model\'s image description and text recognition functions',
selectImage: 'Select Image',
startTest: 'Start Test',
testResult: 'Test Result',
imageDescription: 'Image Description:',
textRecognition: 'Text Recognition:',
processingTime: 'Processing Time:',
testFailed: 'Test Failed',
multimodalProcessingFailed: 'Multimodal processing failed',
// Document splitting
documentSplittingConfig: 'Document Splitting Configuration',
splittingStrategy: 'Splitting Strategy',
balancedMode: 'Balanced Mode',
balancedModeDesc: 'Chunk size: 1000 / Overlap: 200',
precisionMode: 'Precision Mode',
precisionModeDesc: 'Chunk size: 512 / Overlap: 100',
contextMode: 'Context Mode',
contextModeDesc: 'Chunk size: 2048 / Overlap: 400',
custom: 'Custom',
customDesc: 'Configure parameters manually',
chunkSize: 'Chunk Size',
chunkOverlap: 'Chunk Overlap',
separatorSettings: 'Separator Settings',
selectOrCustomSeparators: 'Select or customize separators',
characters: 'characters',
separatorParagraph: 'Paragraph separator (\\n\\n)',
separatorNewline: 'Newline (\\n)',
separatorPeriod: 'Period (。)',
separatorExclamation: 'Exclamation mark ()',
separatorQuestion: 'Question mark ()',
separatorSemicolon: 'Semicolon (;)',
separatorChineseSemicolon: 'Chinese semicolon ()',
separatorComma: 'Comma (,)',
separatorChineseComma: 'Chinese comma ()',
// Entity and relation extraction
entityRelationExtraction: 'Entity and Relation Extraction',
enableEntityRelationExtraction: 'Enable entity and relation extraction',
relationTypeConfig: 'Relation Type Configuration',
relationType: 'Relation Type',
generateRandomTags: 'Generate Random Tags',
completeModelConfig: 'Please complete model configuration',
systemWillExtract: 'The system will extract corresponding entities and relations from the text according to the selected relation types',
extractionExample: 'Extraction Example',
sampleText: 'Sample Text',
sampleTextPlaceholder: 'Enter text for analysis, e.g.: "Red Mansion", also known as "Dream of the Red Chamber", is one of the four great classical novels of Chinese literature, written by Cao Xueqin during the Qing Dynasty...',
generateRandomText: 'Generate Random Text',
entityList: 'Entity List',
nodeName: 'Node Name',
nodeNamePlaceholder: 'Node name',
addAttribute: 'Add Attribute',
attributeValue: 'Attribute Value',
attributeValuePlaceholder: 'Attribute value',
addEntity: 'Add Entity',
completeEntityInfo: 'Please complete entity information',
relationConnection: 'Relation Connection',
selectEntity: 'Select Entity',
addRelation: 'Add Relation',
completeRelationInfo: 'Please complete relation information',
startExtraction: 'Start Extraction',
extracting: 'Extracting...',
defaultExample: 'Default Example',
clearExample: 'Clear Example',
// Buttons and messages
updateKnowledgeBaseSettings: 'Update Knowledge Base Settings',
updateConfigInfo: 'Update Configuration Information',
completeConfig: 'Complete Configuration',
waitForDownloads: 'Please wait for all Ollama models to finish downloading before updating configuration',
completeModelConfigInfo: 'Please complete model configuration information',
knowledgeBaseIdMissing: 'Knowledge base ID is missing',
knowledgeBaseSettingsUpdateSuccess: 'Knowledge base settings updated successfully',
configUpdateSuccess: 'Configuration updated successfully',
systemInitComplete: 'System initialization completed',
operationFailed: 'Operation failed',
updateKnowledgeBaseInfoFailed: 'Failed to update knowledge base basic information',
knowledgeBaseIdMissingCannotSave: 'Knowledge base ID is missing, cannot save configuration',
operationFailedCheckNetwork: 'Operation failed, please check network connection',
imageUploadSuccess: 'Image uploaded successfully, testing can begin',
multimodalConfigIncomplete: 'Multimodal configuration incomplete, please complete multimodal configuration before uploading images',
pleaseSelectImage: 'Please select an image',
multimodalTestSuccess: 'Multimodal test successful',
multimodalTestFailed: 'Multimodal test failed',
pleaseEnterSampleText: 'Please enter sample text',
pleaseEnterRelationType: 'Please enter relation type',
pleaseEnterLLMModelConfig: 'Please enter LLM large language model configuration',
noValidNodesExtracted: 'No valid nodes extracted',
noValidRelationsExtracted: 'No valid relations extracted',
extractionFailedCheckNetwork: 'Extraction failed, please check network or text format',
generateFailedRetry: 'Generation failed, please try again',
pleaseCheckForm: 'Please check form correctness',
detectionSuccessful: 'Detection successful, dimension automatically filled as',
detectionFailed: 'Detection failed',
detectionFailedCheckConfig: 'Detection failed, please check configuration',
modelDownloadSuccess: 'Model downloaded successfully',
modelDownloadFailed: 'Model download failed',
downloadStartFailed: 'Download start failed',
queryProgressFailed: 'Progress query failed',
checkOllamaStatusFailed: 'Ollama status check failed',
getKnowledgeBaseInfoFailed: 'Failed to get knowledge base information',
textRelationExtractionFailed: 'Text relation extraction failed',
// Validation
pleaseEnterKnowledgeBaseName: 'Please enter knowledge base name',
knowledgeBaseNameLength: 'Knowledge base name length must be 1-50 characters',
knowledgeBaseDescriptionLength: 'Knowledge base description cannot exceed 200 characters',
pleaseEnterLLMModelName: 'Please enter LLM model name',
pleaseEnterBaseURL: 'Please enter BaseURL',
pleaseEnterEmbeddingModelName: 'Please enter embedding model name',
pleaseEnterEmbeddingDimension: 'Please enter embedding dimension',
dimensionMustBeInteger: 'Dimension must be a valid integer, usually 768, 1024, 1536, 3584, etc.',
pleaseEnterTextContent: 'Please enter text content',
textContentMinLength: 'Text content must contain at least 10 characters',
pleaseEnterValidTag: 'Please enter a valid tag',
tagAlreadyExists: 'This tag already exists',
// Additional translations for InitializationContent.vue
checkFailed: 'Check failed',
startingDownload: 'Starting download...',
downloadStarted: 'Download started',
model: 'Model',
startModelDownloadFailed: 'Failed to start model download',
downloadCompleted: 'Download completed',
downloadFailed: 'Download failed',
knowledgeBaseSettingsModeMissingId: 'Knowledge base settings mode missing ID',
completeEmbeddingConfig: 'Please complete embedding configuration first',
detectionSuccess: 'Detection successful,',
dimensionAutoFilled: 'dimension automatically filled:',
checkFormCorrectness: 'Please check form correctness',
systemInitializationCompleted: 'System initialization completed',
generationFailedRetry: 'Generation failed, please try again',
chunkSizeDesc: 'Size of each text chunk. Larger chunks preserve more context but may reduce search accuracy.',
chunkOverlapDesc: 'Number of characters overlapping between adjacent chunks. Helps maintain context at chunk boundaries.',
selectRelationType: 'Select relation type'
},
auth: {
login: 'Login',
logout: 'Logout',
username: 'Username',
email: 'Email',
password: 'Password',
confirmPassword: 'Confirm Password',
rememberMe: 'Remember Me',
forgotPassword: 'Forgot Password?',
loginSuccess: 'Login successful!',
loginFailed: 'Login failed',
loggingIn: 'Logging in...',
register: 'Register',
registering: 'Registering...',
createAccount: 'Create Account',
haveAccount: 'Already have an account?',
noAccount: 'Don\'t have an account?',
backToLogin: 'Back to Login',
registerNow: 'Register Now',
registerSuccess: 'Registration successful! The system has created an exclusive tenant for you, please login',
registerFailed: 'Registration failed',
subtitle: 'Document understanding and semantic search framework based on large models',
registerSubtitle: 'The system will create an exclusive tenant for you after registration',
emailPlaceholder: 'Enter email address',
passwordPlaceholder: 'Enter password (8-32 characters, including letters and numbers)',
confirmPasswordPlaceholder: 'Enter password again',
usernamePlaceholder: 'Enter username',
emailRequired: 'Enter email address',
emailInvalid: 'Enter correct email format',
passwordRequired: 'Enter password',
passwordMinLength: 'Password must be at least 8 characters',
passwordMaxLength: 'Password cannot exceed 32 characters',
passwordMustContainLetter: 'Password must contain letters',
passwordMustContainNumber: 'Password must contain numbers',
usernameRequired: 'Enter username',
usernameMinLength: 'Username must be at least 2 characters',
usernameMaxLength: 'Username cannot exceed 20 characters',
usernameInvalid: 'Username can only contain letters, numbers, underscores and Chinese characters',
confirmPasswordRequired: 'Confirm password',
passwordMismatch: 'Entered passwords do not match',
loginError: 'Login error, please check email or password',
loginErrorRetry: 'Login error, please try again later',
registerError: 'Registration error, please try again later',
forgotPasswordNotAvailable: 'Password recovery function is temporarily unavailable, please contact administrator'
},
common: {
confirm: 'Confirm',
cancel: 'Cancel',
save: 'Save',
delete: 'Delete',
edit: 'Edit',
create: 'Create',
search: 'Search',
filter: 'Filter',
export: 'Export',
import: 'Import',
upload: 'Upload',
download: 'Download',
refresh: 'Refresh',
loading: 'Loading...',
noData: 'No data',
error: 'Error',
success: 'Success',
warning: 'Warning',
info: 'Information',
yes: 'Yes',
no: 'No',
ok: 'OK',
close: 'Close',
back: 'Back',
next: 'Next',
finish: 'Finish',
all: 'All',
reset: 'Reset',
clear: 'Clear'
},
file: {
upload: 'Upload File',
uploadSuccess: 'File uploaded successfully',
uploadFailed: 'File upload failed',
delete: 'Delete File',
deleteSuccess: 'File deleted successfully',
deleteFailed: 'File deletion failed',
download: 'Download File',
preview: 'Preview',
unsupportedFormat: 'Unsupported file format',
maxSizeExceeded: 'Maximum file size exceeded',
selectFile: 'Select File'
},
tenant: {
title: 'Tenant Information',
name: 'Tenant Name',
id: 'Tenant ID',
createdAt: 'Created At',
updatedAt: 'Updated At',
status: 'Status',
active: 'Active',
inactive: 'Inactive',
// Additional translations for TenantInfo.vue
systemInfo: 'System Information',
viewSystemInfo: 'View system version and user account configuration information',
version: 'Version',
buildTime: 'Build Time',
goVersion: 'Go Version',
userInfo: 'User Information',
userId: 'User ID',
username: 'Username',
email: 'Email',
tenantInfo: 'Tenant Information',
tenantId: 'Tenant ID',
tenantName: 'Tenant Name',
description: 'Description',
business: 'Business',
noDescription: 'No description',
noBusiness: 'None',
statusActive: 'Active',
statusInactive: 'Not activated',
statusSuspended: 'Suspended',
statusUnknown: 'Unknown',
apiKey: 'API Key',
keepApiKeySafe: 'Please keep your API Key safe, do not disclose it in public places or code repositories',
storageInfo: 'Storage Information',
storageQuota: 'Storage Quota',
used: 'Used',
usage: 'Usage',
apiDevDocs: 'API Developer Documentation',
useApiKey: 'Use your API Key to start development, view complete API documentation and code examples.',
viewApiDoc: 'View API Documentation',
loadingAccountInfo: 'Loading account information...',
loadFailed: 'Load failed',
retry: 'Retry',
apiKeyCopied: 'API Key copied to clipboard',
unknown: 'Unknown',
formatError: 'Format error'
},
error: {
network: 'Network error',
server: 'Server error',
notFound: 'Not found',
unauthorized: 'Unauthorized',
forbidden: 'Access forbidden',
unknown: 'Unknown error',
tryAgain: 'Please try again'
},
model: {
llmModel: 'LLM Model',
embeddingModel: 'Embedding Model',
rerankModel: 'Rerank Model',
vlmModel: 'Multimodal Model',
modelName: 'Model Name',
modelProvider: 'Model Provider',
modelUrl: 'Model URL',
apiKey: 'API Key',
testConnection: 'Test Connection',
connectionSuccess: 'Connection successful',
connectionFailed: 'Connection failed',
dimension: 'Dimension',
maxTokens: 'Max Tokens',
temperature: 'Temperature',
topP: 'Top P',
selectModel: 'Select Model',
customModel: 'Custom Model',
builtinModel: 'Built-in Model'
}
}

View File

@@ -0,0 +1,553 @@
export default {
menu: {
knowledgeBase: 'База знаний',
chat: 'Диалог',
createChat: 'Создать диалог',
tenant: 'Информация об аккаунте',
settings: 'Настройки системы',
logout: 'Выход',
uploadKnowledge: 'Загрузить знания',
deleteRecord: 'Удалить запись',
newSession: 'Новый диалог',
confirmLogout: 'Вы уверены, что хотите выйти?',
systemInfo: 'Информация о системе'
},
knowledgeBase: {
title: 'База знаний',
list: 'Список баз знаний',
detail: 'Детали базы знаний',
create: 'Создать базу знаний',
edit: 'Редактировать базу знаний',
delete: 'Удалить базу знаний',
name: 'Название',
description: 'Описание',
files: 'Файлы',
settings: 'Настройки',
upload: 'Загрузить файл',
uploadSuccess: 'Файл успешно загружен!',
uploadFailed: 'Ошибка загрузки файла!',
fileExists: 'Файл уже существует',
notInitialized: 'База знаний не инициализирована. Пожалуйста, настройте модели в разделе настроек перед загрузкой файлов',
getInfoFailed: 'Не удалось получить информацию о базе знаний, загрузка файла невозможна',
missingId: 'Отсутствует ID базы знаний',
deleteFailed: 'Не удалось удалить. Пожалуйста, попробуйте позже!',
createKnowledgeBase: 'Создать базу знаний',
knowledgeBaseName: 'Название базы знаний',
enterName: 'Введите название базы знаний',
embeddingModel: 'Модель встраивания',
selectEmbeddingModel: 'Выберите модель встраивания',
summaryModel: 'Модель суммаризации',
selectSummaryModel: 'Выберите модель суммаризации',
rerankModel: 'Модель ранжирования',
selectRerankModel: 'Выберите модель ранжирования (опционально)',
createSuccess: 'База знаний успешно создана',
createFailed: 'Не удалось создать базу знаний',
updateSuccess: 'База знаний успешно обновлена',
updateFailed: 'Не удалось обновить базу знаний',
deleteSuccess: 'База знаний успешно удалена',
deleteConfirm: 'Вы уверены, что хотите удалить эту базу знаний?',
fileName: 'Имя файла',
fileSize: 'Размер файла',
uploadTime: 'Время загрузки',
status: 'Статус',
actions: 'Действия',
processing: 'Обработка',
completed: 'Завершено',
failed: 'Ошибка',
noFiles: 'Нет файлов',
dragFilesHere: 'Перетащите файлы сюда или',
clickToUpload: 'нажмите для загрузки',
supportedFormats: 'Поддерживаемые форматы',
maxFileSize: 'Макс. размер файла',
viewDetails: 'Просмотр деталей',
downloadFile: 'Скачать файл',
deleteFile: 'Удалить файл',
confirmDeleteFile: 'Вы уверены, что хотите удалить этот файл?',
totalFiles: 'Всего файлов',
totalSize: 'Общий размер',
// Дополнительные переводы для KnowledgeBase.vue
newSession: 'Новый диалог',
deleteDocument: 'Удалить документ',
parsingFailed: 'Парсинг не удался',
parsingInProgress: 'Парсинг...',
deleteConfirmation: 'Подтверждение удаления',
confirmDeleteDocument: 'Подтвердить удаление документа "{fileName}", после удаления восстановление невозможно',
cancel: 'Отмена',
confirmDelete: 'Подтвердить удаление',
selectKnowledgeBaseFirst: 'Пожалуйста, сначала выберите базу знаний',
sessionCreationFailed: 'Не удалось создать диалог',
sessionCreationError: 'Ошибка создания диалога',
settingsParsingFailed: 'Не удалось разобрать настройки',
fileUploadEventReceived: 'Получено событие загрузки файла, загруженный ID базы знаний: {uploadedKbId}, текущий ID базы знаний: {currentKbId}',
matchingKnowledgeBase: 'Совпадающая база знаний, начинаем обновление списка файлов',
routeParamChange: 'Изменение параметров маршрута, повторное получение содержимого базы знаний',
fileUploadEventListening: 'Прослушивание события загрузки файла',
apiCallKnowledgeFiles: 'Прямой вызов API для получения списка файлов базы знаний',
responseInterceptorData: 'Поскольку перехватчик ответа уже вернул data, result является частью данных ответа',
hookProcessing: 'Обработка в соответствии со способом useKnowledgeBase hook',
errorHandling: 'Обработка ошибок',
priorityCurrentPageKbId: 'Приоритет использования ID базы знаний текущей страницы',
fallbackLocalStorageKbId: 'Если на текущей странице нет ID базы знаний, попытка получить ID базы знаний из настроек в localStorage',
// Дополнительные переводы для KnowledgeBaseList.vue
createNewKnowledgeBase: 'Создать базу знаний',
uninitializedWarning: 'Некоторые базы знаний не инициализированы, необходимо сначала настроить информацию о моделях в настройках, чтобы добавить документы знаний',
initializedStatus: 'Инициализирована',
notInitializedStatus: 'Не инициализирована',
needSettingsFirst: 'Необходимо сначала настроить информацию о моделях в настройках, чтобы добавить знания',
documents: 'Документы',
configureModelsFirst: 'Пожалуйста, сначала настройте информацию о моделях в настройках',
confirmDeleteKnowledgeBase: 'Подтвердить удаление этой базы знаний?',
createKnowledgeBaseDialog: 'Создать базу знаний',
enterNameKb: 'Введите название',
enterDescriptionKb: 'Введите описание',
createKb: 'Создать',
deleted: 'Удалено',
deleteFailedKb: 'Не удалось удалить',
noDescription: 'Нет описания',
emptyKnowledgeDragDrop: 'База знаний пуста, перетащите файлы для загрузки',
pdfDocFormat: 'Файлы pdf, doc формата, не более 10 МБ',
textMarkdownFormat: 'Файлы text, markdown формата, не более 200 КБ',
dragFileNotText: 'Пожалуйста, перетащите файлы, а не текст или ссылки'
},
chat: {
title: 'Диалог',
newChat: 'Новый чат',
inputPlaceholder: 'Введите ваше сообщение...',
send: 'Отправить',
thinking: 'Думаю...',
regenerate: 'Сгенерировать заново',
copy: 'Копировать',
delete: 'Удалить',
reference: 'Ссылка',
noMessages: 'Нет сообщений',
// Дополнительные переводы для компонентов чата
waitingForAnswer: 'Ожидание ответа...',
cannotAnswer: 'Извините, я не могу ответить на этот вопрос.',
summarizingAnswer: 'Подведение итогов ответа...',
loading: 'Загрузка...',
enterDescription: 'Введите описание',
referencedContent: 'Использовано {count} связанных материалов',
deepThinking: 'Глубокое мышление завершено',
knowledgeBaseQandA: 'Вопросы и ответы на основе базы знаний',
askKnowledgeBase: 'Задайте вопрос базе знаний',
sourcesCount: '{count} источников',
pleaseEnterContent: 'Пожалуйста, введите содержимое!',
pleaseUploadKnowledgeBase: 'Пожалуйста, сначала загрузите базу знаний!',
replyingPleaseWait: 'Идёт ответ, пожалуйста, попробуйте позже!',
createSessionFailed: 'Не удалось создать сеанс',
createSessionError: 'Ошибка создания сеанса',
unableToGetKnowledgeBaseId: 'Невозможно получить ID базы знаний'
},
settings: {
title: 'Настройки',
system: 'Настройки системы',
systemConfig: 'Системная конфигурация',
knowledgeBaseSettings: 'Настройки базы знаний',
configureKbModels: 'Настройка моделей и параметров разделения документов для этой базы знаний',
manageSystemModels: 'Управление и обновление системных моделей и конфигураций сервисов',
basicInfo: 'Основная информация',
documentSplitting: 'Разделение документов',
apiEndpoint: 'API конечная точка',
enterApiEndpoint: 'Введите API конечную точку, например: http://localhost',
enterApiKey: 'Введите API ключ',
enterKnowledgeBaseId: 'Введите ID базы знаний',
saveConfig: 'Сохранить конфигурацию',
reset: 'Сбросить',
configSaved: 'Конфигурация сохранена успешно',
enterApiEndpointRequired: 'Введите API конечную точку',
enterApiKeyRequired: 'Введите API ключ',
enterKnowledgeBaseIdRequired: 'Введите ID базы знаний',
name: 'Название',
enterName: 'Введите название',
description: 'Описание',
chunkSize: 'Размер блока',
chunkOverlap: 'Перекрытие блоков',
save: 'Сохранить',
saving: 'Сохранение...',
saveSuccess: 'Сохранено успешно',
saveFailed: 'Не удалось сохранить',
model: 'Модель',
llmModel: 'LLM модель',
embeddingModel: 'Модель встраивания',
rerankModel: 'Модель ранжирования',
vlmModel: 'Мультимодальная модель',
modelName: 'Название модели',
modelUrl: 'URL модели',
apiKey: 'API ключ',
cancel: 'Отмена',
saveFailedSettings: 'Не удалось сохранить настройки',
enterNameRequired: 'Введите название'
},
initialization: {
title: 'Инициализация',
welcome: 'Добро пожаловать в WeKnora',
description: 'Пожалуйста, настройте систему перед началом работы',
step1: 'Шаг 1: Настройка LLM модели',
step2: 'Шаг 2: Настройка модели встраивания',
step3: 'Шаг 3: Настройка дополнительных моделей',
complete: 'Завершить инициализацию',
skip: 'Пропустить',
next: 'Далее',
previous: 'Назад',
// Ollama сервис
ollamaServiceStatus: 'Статус службы Ollama',
refreshStatus: 'Обновить статус',
ollamaServiceAddress: 'Адрес службы Ollama',
notConfigured: 'Не настроено',
notRunning: 'Не запущено',
normal: 'Нормально',
installedModels: 'Установленные модели',
none: 'Временно отсутствует',
// База знаний
knowledgeBaseInfo: 'Информация о базе знаний',
knowledgeBaseName: 'Название базы знаний',
knowledgeBaseNamePlaceholder: 'Введите название базы знаний',
knowledgeBaseDescription: 'Описание базы знаний',
knowledgeBaseDescriptionPlaceholder: 'Введите описание базы знаний',
// LLM модель
llmModelConfig: 'Конфигурация LLM большой языковой модели',
modelSource: 'Источник модели',
local: 'Ollama (локальный)',
remote: 'Remote API (удаленный)',
modelName: 'Название модели',
modelNamePlaceholder: 'Например: qwen3:0.6b',
baseUrl: 'Base URL',
baseUrlPlaceholder: 'Например: https://api.openai.com/v1, удалите часть /chat/completions в конце URL',
apiKey: 'API Key (необязательно)',
apiKeyPlaceholder: 'Введите API Key (необязательно)',
downloadModel: 'Скачать модель',
installed: 'Установлено',
notInstalled: 'Не установлено',
notChecked: 'Не проверено',
checkConnection: 'Проверить соединение',
connectionNormal: 'Соединение в норме',
connectionFailed: 'Ошибка соединения',
checkingConnection: 'Проверка соединения',
// Embedding модель
embeddingModelConfig: 'Конфигурация модели встраивания',
embeddingWarning: 'В базе знаний уже есть файлы, невозможно изменить конфигурацию модели встраивания',
dimension: 'Размерность',
dimensionPlaceholder: 'Введите размерность вектора',
detectDimension: 'Определить размерность',
// Rerank модель
rerankModelConfig: 'Конфигурация модели ранжирования',
enableRerank: 'Включить модель ранжирования',
// Мультимодальные настройки
multimodalConfig: 'Мультимодальная конфигурация',
enableMultimodal: 'Включить извлечение информации из изображений',
visualLanguageModelConfig: 'Конфигурация визуально-языковой модели',
interfaceType: 'Тип интерфейса',
openaiCompatible: 'Совместимый с OpenAI интерфейс',
// Настройки хранилища
storageServiceConfig: 'Конфигурация службы хранения',
storageType: 'Тип хранения',
bucketName: 'Bucket Name',
bucketNamePlaceholder: 'Введите имя Bucket',
pathPrefix: 'Path Prefix',
pathPrefixPlaceholder: 'Например: images',
secretId: 'Secret ID',
secretIdPlaceholder: 'Введите COS Secret ID',
secretKey: 'Secret Key',
secretKeyPlaceholder: 'Введите COS Secret Key',
region: 'Region',
regionPlaceholder: 'Например: ap-beijing',
appId: 'App ID',
appIdPlaceholder: 'Введите App ID',
// Тестирование мультимодальных функций
functionTest: 'Тест функции',
testDescription: 'Загрузите изображение для тестирования функций описания изображений и распознавания текста модели VLM',
selectImage: 'Выбрать изображение',
startTest: 'Начать тест',
testResult: 'Результат теста',
imageDescription: 'Описание изображения:',
textRecognition: 'Распознавание текста:',
processingTime: 'Время обработки:',
testFailed: 'Тест не удался',
multimodalProcessingFailed: 'Ошибка мультимодальной обработки',
// Разделение документов
documentSplittingConfig: 'Конфигурация разделения документов',
splittingStrategy: 'Стратегия разделения',
balancedMode: 'Сбалансированный режим',
balancedModeDesc: 'Размер блока: 1000 / Перекрытие: 200',
precisionMode: 'Точный режим',
precisionModeDesc: 'Размер блока: 512 / Перекрытие: 100',
contextMode: 'Контекстный режим',
contextModeDesc: 'Размер блока: 2048 / Перекрытие: 400',
custom: 'Пользовательский',
customDesc: 'Настроить параметры вручную',
chunkSize: 'Размер блока',
chunkOverlap: 'Перекрытие блоков',
separatorSettings: 'Настройки разделителей',
selectOrCustomSeparators: 'Выберите или настройте разделители',
characters: 'символов',
separatorParagraph: 'Разделитель абзацев (\\n\\n)',
separatorNewline: 'Перевод строки (\\n)',
separatorPeriod: 'Точка (。)',
separatorExclamation: 'Восклицательный знак ()',
separatorQuestion: 'Вопросительный знак ()',
separatorSemicolon: 'Точка с запятой (;)',
separatorChineseSemicolon: 'Китайская точка с запятой ()',
separatorComma: 'Запятая (,)',
separatorChineseComma: 'Китайская запятая ()',
// Извлечение сущностей и отношений
entityRelationExtraction: 'Извлечение сущностей и отношений',
enableEntityRelationExtraction: 'Включить извлечение сущностей и отношений',
relationTypeConfig: 'Конфигурация типов отношений',
relationType: 'Тип отношения',
generateRandomTags: 'Сгенерировать случайные теги',
completeModelConfig: 'Пожалуйста, завершите конфигурацию модели',
systemWillExtract: 'Система будет извлекать соответствующие сущности и отношения из текста в соответствии с выбранными типами отношений',
extractionExample: 'Пример извлечения',
sampleText: 'Пример текста',
sampleTextPlaceholder: 'Введите текст для анализа, например: "Красный особняк", также известный как "Сон в красном тереме", является одним из четырех великих классических произведений китайской литературы, написанным Цинь Сюэцином в династии Цин...',
generateRandomText: 'Сгенерировать случайный текст',
entityList: 'Список сущностей',
nodeName: 'Имя узла',
nodeNamePlaceholder: 'Имя узла',
addAttribute: 'Добавить атрибут',
attributeValue: 'Значение атрибута',
attributeValuePlaceholder: 'Значение атрибута',
addEntity: 'Добавить сущность',
completeEntityInfo: 'Пожалуйста, завершите информацию о сущности',
relationConnection: 'Соединение отношений',
selectEntity: 'Выберите сущность',
addRelation: 'Добавить отношение',
completeRelationInfo: 'Пожалуйста, завершите информацию об отношении',
startExtraction: 'Начать извлечение',
extracting: 'Извлечение...',
defaultExample: 'Пример по умолчанию',
clearExample: 'Очистить пример',
// Кнопки и сообщения
updateKnowledgeBaseSettings: 'Обновить настройки базы знаний',
updateConfigInfo: 'Обновить информацию о конфигурации',
completeConfig: 'Завершить конфигурацию',
waitForDownloads: 'Пожалуйста, дождитесь завершения загрузки всех моделей Ollama перед обновлением конфигурации',
completeModelConfigInfo: 'Пожалуйста, завершите информацию о конфигурации модели',
knowledgeBaseIdMissing: 'Отсутствует ID базы знаний',
knowledgeBaseSettingsUpdateSuccess: 'Настройки базы знаний успешно обновлены',
configUpdateSuccess: 'Конфигурация успешно обновлена',
systemInitComplete: 'Инициализация системы завершена',
operationFailed: 'Операция не удалась',
updateKnowledgeBaseInfoFailed: 'Не удалось обновить базовую информацию о базе знаний',
knowledgeBaseIdMissingCannotSave: 'Отсутствует ID базы знаний, невозможно сохранить конфигурацию',
operationFailedCheckNetwork: 'Операция не удалась, проверьте сетевое соединение',
imageUploadSuccess: 'Изображение успешно загружено, можно начать тестирование',
multimodalConfigIncomplete: 'Мультимодальная конфигурация неполная, пожалуйста, завершите мультимодальную конфигурацию перед загрузкой изображения',
pleaseSelectImage: 'Пожалуйста, выберите изображение',
multimodalTestSuccess: 'Мультимодальный тест успешен',
multimodalTestFailed: 'Мультимодальный тест не удался',
pleaseEnterSampleText: 'Пожалуйста, введите текст примера',
pleaseEnterRelationType: 'Пожалуйста, введите тип отношения',
pleaseEnterLLMModelConfig: 'Пожалуйста, введите конфигурацию LLM большой языковой модели',
noValidNodesExtracted: 'Не извлечено допустимых узлов',
noValidRelationsExtracted: 'Не извлечено допустимых отношений',
extractionFailedCheckNetwork: 'Извлечение не удалось, проверьте сетевое соединение или формат текста',
generateFailedRetry: 'Генерация не удалась, попробуйте еще раз',
pleaseCheckForm: 'Пожалуйста, проверьте правильность заполнения формы',
detectionSuccessful: 'Обнаружение успешно, размерность автоматически заполнена как',
detectionFailed: 'Обнаружение не удалось',
detectionFailedCheckConfig: 'Обнаружение не удалось, проверьте конфигурацию',
modelDownloadSuccess: 'Модель успешно загружена',
modelDownloadFailed: 'Не удалось загрузить модель',
downloadStartFailed: 'Не удалось начать загрузку',
queryProgressFailed: 'Не удалось запросить прогресс',
checkOllamaStatusFailed: 'Не удалось проверить статус Ollama',
getKnowledgeBaseInfoFailed: 'Не удалось получить информацию о базе знаний',
textRelationExtractionFailed: 'Не удалось извлечь текстовые отношения',
// Валидация
pleaseEnterKnowledgeBaseName: 'Пожалуйста, введите название базы знаний',
knowledgeBaseNameLength: 'Длина названия базы знаний должна быть от 1 до 50 символов',
knowledgeBaseDescriptionLength: 'Длина описания базы знаний не может превышать 200 символов',
pleaseEnterLLMModelName: 'Пожалуйста, введите название LLM модели',
pleaseEnterBaseURL: 'Пожалуйста, введите BaseURL',
pleaseEnterEmbeddingModelName: 'Пожалуйста, введите название модели встраивания',
pleaseEnterEmbeddingDimension: 'Пожалуйста, введите размерность встраивания',
dimensionMustBeInteger: 'Размерность должна быть допустимым целым числом, обычно 768, 1024, 1536, 3584 и т.д.',
pleaseEnterTextContent: 'Пожалуйста, введите текстовое содержание',
textContentMinLength: 'Текстовое содержание должно содержать не менее 10 символов',
pleaseEnterValidTag: 'Пожалуйста, введите действительный тег',
tagAlreadyExists: 'Этот тег уже существует',
// Дополнительные переводы для InitializationContent.vue
checkFailed: 'Проверка не удалась',
startingDownload: 'Запуск загрузки...',
downloadStarted: 'Загрузка началась',
model: 'Модель',
startModelDownloadFailed: 'Не удалось запустить загрузку модели',
downloadCompleted: 'Загрузка завершена',
downloadFailed: 'Загрузка не удалась',
knowledgeBaseSettingsModeMissingId: 'В режиме настроек базы знаний отсутствует ID базы знаний',
completeEmbeddingConfig: 'Пожалуйста, сначала полностью заполните конфигурацию встраивания',
detectionSuccess: 'Обнаружение успешно,',
dimensionAutoFilled: 'размерность автоматически заполнена:',
checkFormCorrectness: 'Пожалуйста, проверьте правильность заполнения формы',
systemInitializationCompleted: 'Инициализация системы завершена',
generationFailedRetry: 'Генерация не удалась, пожалуйста, попробуйте еще раз',
chunkSizeDesc: 'Размер каждого текстового блока. Большие блоки сохраняют больше контекста, но могут снизить точность поиска.',
chunkOverlapDesc: 'Количество символов, перекрывающихся между соседними блоками. Помогает сохранить контекст на границах блоков.',
selectRelationType: 'Выберите тип отношения'
},
auth: {
login: 'Вход',
logout: 'Выход',
username: 'Имя пользователя',
email: 'Почта Email',
password: 'Пароль',
confirmPassword: 'Подтвердите пароль',
rememberMe: 'Запомнить меня',
forgotPassword: 'Забыли пароль?',
loginSuccess: 'Вход выполнен успешно!',
loginFailed: 'Ошибка входа',
loggingIn: 'Вход...',
register: 'Регистрация',
registering: 'Регистрация...',
createAccount: 'Создать аккаунт',
haveAccount: 'Уже есть аккаунт?',
noAccount: 'Ещё нет аккаунта?',
backToLogin: 'Вернуться ко входу',
registerNow: 'Зарегистрироваться',
registerSuccess: 'Регистрация успешна! Система создала для вас эксклюзивного арендатора, пожалуйста, войдите',
registerFailed: 'Ошибка регистрации',
subtitle: 'Фреймворк понимания документов и семантического поиска на основе больших моделей',
registerSubtitle: 'После регистрации система создаст для вас эксклюзивного арендатора',
emailPlaceholder: 'Введите адрес электронной почты',
passwordPlaceholder: 'Введите пароль (8-32 символа, включая буквы и цифры)',
confirmPasswordPlaceholder: 'Введите пароль ещё раз',
usernamePlaceholder: 'Введите имя пользователя',
emailRequired: 'Введите адрес электронной почты',
emailInvalid: 'Введите правильный формат электронной почты',
passwordRequired: 'Введите пароль',
passwordMinLength: 'Пароль должен быть не менее 8 символов',
passwordMaxLength: 'Пароль не может превышать 32 символа',
passwordMustContainLetter: 'Пароль должен содержать буквы',
passwordMustContainNumber: 'Пароль должен содержать цифры',
usernameRequired: 'Введите имя пользователя',
usernameMinLength: 'Имя пользователя должно быть не менее 2 символов',
usernameMaxLength: 'Имя пользователя не может превышать 20 символов',
usernameInvalid: 'Имя пользователя может содержать только буквы, цифры, подчёркивания и китайские иероглифы',
confirmPasswordRequired: 'Подтвердите пароль',
passwordMismatch: 'Введённые пароли не совпадают',
loginError: 'Ошибка входа, пожалуйста, проверьте электронную почту или пароль',
loginErrorRetry: 'Ошибка входа, пожалуйста, повторите попытку позже',
registerError: 'Ошибка регистрации, пожалуйста, повторите попытку позже',
forgotPasswordNotAvailable: 'Функция восстановления пароля временно недоступна, пожалуйста, свяжитесь с администратором'
},
common: {
confirm: 'Подтвердить',
cancel: 'Отмена',
save: 'Сохранить',
delete: 'Удалить',
edit: 'Редактировать',
create: 'Создать',
search: 'Поиск',
filter: 'Фильтр',
export: 'Экспорт',
import: 'Импорт',
upload: 'Загрузить',
download: 'Скачать',
refresh: 'Обновить',
loading: 'Загрузка...',
noData: 'Нет данных',
error: 'Ошибка',
success: 'Успешно',
warning: 'Предупреждение',
info: 'Информация',
yes: 'Да',
no: 'Нет',
ok: 'OK',
close: 'Закрыть',
back: 'Назад',
next: 'Далее',
finish: 'Завершить',
all: 'Все',
reset: 'Сбросить',
clear: 'Очистить'
},
file: {
upload: 'Загрузить файл',
uploadSuccess: 'Файл успешно загружен',
uploadFailed: 'Ошибка загрузки файла',
delete: 'Удалить файл',
deleteSuccess: 'Файл успешно удален',
deleteFailed: 'Ошибка удаления файла',
download: 'Скачать файл',
preview: 'Предпросмотр',
unsupportedFormat: 'Неподдерживаемый формат файла',
maxSizeExceeded: 'Превышен максимальный размер файла',
selectFile: 'Выберите файл'
},
tenant: {
title: 'Информация об арендаторе',
name: 'Имя арендатора',
id: 'ID арендатора',
createdAt: 'Дата создания',
updatedAt: 'Дата обновления',
status: 'Статус',
active: 'Активен',
inactive: 'Неактивен',
// Дополнительные переводы для TenantInfo.vue
systemInfo: 'Системная информация',
viewSystemInfo: 'Просмотр информации о версии системы и конфигурации учётной записи пользователя',
version: 'Версия',
buildTime: 'Время сборки',
goVersion: 'Версия Go',
userInfo: 'Информация о пользователе',
userId: 'ID пользователя',
username: 'Имя пользователя',
email: 'Электронная почта',
tenantInfo: 'Информация об арендаторе',
tenantId: 'ID арендатора',
tenantName: 'Название арендатора',
description: 'Описание',
business: 'Бизнес',
noDescription: 'Нет описания',
noBusiness: 'Нет',
statusActive: 'Активен',
statusInactive: 'Не активирован',
statusSuspended: 'Приостановлен',
statusUnknown: 'Неизвестен',
apiKey: 'API Key',
keepApiKeySafe: 'Пожалуйста, храните ваш API Key в безопасности, не раскрывайте его в общественных местах или репозиториях кода',
storageInfo: 'Информация о хранилище',
storageQuota: 'Квота хранилища',
used: 'Использовано',
usage: 'Использование',
apiDevDocs: 'Документация для разработчиков API',
useApiKey: 'Используйте ваш API Key для начала разработки, просмотрите полную документацию API и примеры кода.',
viewApiDoc: 'Просмотреть документацию API',
loadingAccountInfo: 'Загрузка информации об учётной записи...',
loadFailed: 'Загрузка не удалась',
retry: 'Повторить',
apiKeyCopied: 'API Key скопирован в буфер обмена',
unknown: 'Неизвестно',
formatError: 'Ошибка формата'
},
error: {
network: 'Ошибка сети',
server: 'Ошибка сервера',
notFound: 'Не найдено',
unauthorized: 'Не авторизован',
forbidden: 'Доступ запрещен',
unknown: 'Неизвестная ошибка',
tryAgain: 'Пожалуйста, попробуйте еще раз'
},
model: {
llmModel: 'LLM модель',
embeddingModel: 'Модель встраивания',
rerankModel: 'Модель ранжирования',
vlmModel: 'Мультимодальная модель',
modelName: 'Название модели',
modelProvider: 'Поставщик модели',
modelUrl: 'URL модели',
apiKey: 'API ключ',
testConnection: 'Проверить соединение',
connectionSuccess: 'Соединение успешно',
connectionFailed: 'Ошибка соединения',
dimension: 'Размерность',
maxTokens: 'Макс. токенов',
temperature: 'Температура',
topP: 'Top P',
selectModel: 'Выберите модель',
customModel: 'Пользовательская модель',
builtinModel: 'Встроенная модель'
}
}

View File

@@ -0,0 +1,536 @@
export default {
menu: {
knowledgeBase: '知识库',
chat: '对话',
createChat: '创建对话',
tenant: '账户信息',
settings: '系统设置',
logout: '退出登录',
uploadKnowledge: '上传知识',
deleteRecord: '删除记录',
newSession: '新会话',
confirmLogout: '确定要退出登录吗?',
systemInfo: '系统信息'
},
knowledgeBase: {
title: '知识库',
list: '知识库列表',
detail: '知识库详情',
create: '创建知识库',
edit: '编辑知识库',
delete: '删除知识库',
name: '名称',
description: '描述',
files: '文件',
settings: '设置',
upload: '上传文件',
uploadSuccess: '文件上传成功!',
uploadFailed: '文件上传失败!',
fileExists: '文件已存在',
notInitialized: '该知识库尚未完成初始化配置,请先前往设置页面配置模型信息后再上传文件',
getInfoFailed: '获取知识库信息失败,无法上传文件',
missingId: '缺少知识库ID',
deleteFailed: '删除失败,请稍后再试!',
createKnowledgeBase: '创建知识库',
knowledgeBaseName: '知识库名称',
enterName: '输入知识库名称',
embeddingModel: '嵌入模型',
selectEmbeddingModel: '选择嵌入模型',
summaryModel: '摘要模型',
selectSummaryModel: '选择摘要模型',
rerankModel: '重排序模型',
selectRerankModel: '选择重排序模型(可选)',
createSuccess: '知识库创建成功',
createFailed: '知识库创建失败',
updateSuccess: '知识库更新成功',
updateFailed: '知识库更新失败',
deleteSuccess: '知识库删除成功',
deleteConfirm: '确定要删除此知识库吗?',
fileName: '文件名',
fileSize: '文件大小',
uploadTime: '上传时间',
status: '状态',
actions: '操作',
processing: '处理中',
completed: '已完成',
failed: '失败',
noFiles: '暂无文件',
dragFilesHere: '拖拽文件至此或',
clickToUpload: '点击上传',
supportedFormats: '支持格式',
maxFileSize: '最大文件大小',
viewDetails: '查看详情',
downloadFile: '下载文件',
deleteFile: '删除文件',
confirmDeleteFile: '确定要删除此文件吗?',
totalFiles: '文件总数',
totalSize: '总大小',
newSession: '新会话',
deleteDocument: '删除文档',
parsingFailed: '解析失败',
parsingInProgress: '解析中...',
deleteConfirmation: '删除确认',
confirmDeleteDocument: '确认删除文档"{fileName}",删除后将无法恢复',
cancel: '取消',
confirmDelete: '确认删除',
selectKnowledgeBaseFirst: '请先选择知识库',
sessionCreationFailed: '创建会话失败',
sessionCreationError: '会话创建错误',
settingsParsingFailed: '设置解析失败',
fileUploadEventReceived: '收到文件上传事件上传的知识库ID{uploadedKbId}当前知识库ID{currentKbId}',
matchingKnowledgeBase: '知识库匹配,开始更新文件列表',
routeParamChange: '路由参数变化,重新获取知识库内容',
fileUploadEventListening: '监听文件上传事件',
apiCallKnowledgeFiles: '直接调用API获取知识库文件列表',
responseInterceptorData: '由于响应拦截器已返回dataresult是响应数据的一部分',
hookProcessing: '按照useKnowledgeBase hook方法处理',
errorHandling: '错误处理',
priorityCurrentPageKbId: '优先使用当前页面的知识库ID',
fallbackLocalStorageKbId: '如果当前页面没有知识库ID尝试从localStorage的设置中获取知识库ID',
createNewKnowledgeBase: '创建知识库',
uninitializedWarning: '部分知识库未初始化,需要先在设置中配置模型信息才能添加知识文档',
initializedStatus: '已初始化',
notInitializedStatus: '未初始化',
needSettingsFirst: '需要先在设置中配置模型信息才能添加知识',
documents: '文档',
configureModelsFirst: '请先在设置中配置模型信息',
confirmDeleteKnowledgeBase: '确认删除此知识库?',
createKnowledgeBaseDialog: '创建知识库',
enterNameKb: '输入名称',
enterDescriptionKb: '输入描述',
createKb: '创建',
deleted: '已删除',
deleteFailedKb: '删除失败',
noDescription: '无描述',
emptyKnowledgeDragDrop: '知识为空,拖放上传',
pdfDocFormat: 'pdf、doc 格式文件不超过10M',
textMarkdownFormat: 'text、markdown格式文件不超过200K',
dragFileNotText: '请拖拽文件而不是文本或链接'
},
chat: {
title: '对话',
newChat: '新对话',
inputPlaceholder: '请输入您的消息...',
send: '发送',
thinking: '思考中...',
regenerate: '重新生成',
copy: '复制',
delete: '删除',
reference: '引用',
noMessages: '暂无消息',
waitingForAnswer: '等待回答...',
cannotAnswer: '抱歉,我无法回答这个问题。',
summarizingAnswer: '总结答案中...',
loading: '加载中...',
enterDescription: '输入描述',
referencedContent: '引用了 {count} 个相关资料',
deepThinking: '深度思考完成',
knowledgeBaseQandA: '知识库问答',
askKnowledgeBase: '向知识库提问',
sourcesCount: '{count} 个来源',
pleaseEnterContent: '请输入内容!',
pleaseUploadKnowledgeBase: '请先上传知识库!',
replyingPleaseWait: '正在回复,请稍后再试!',
createSessionFailed: '创建会话失败',
createSessionError: '创建会话出错',
unableToGetKnowledgeBaseId: '无法获取知识库ID'
},
settings: {
title: '设置',
system: '系统设置',
systemConfig: '系统配置',
knowledgeBaseSettings: '知识库设置',
configureKbModels: '为此知识库配置模型和文档分割参数',
manageSystemModels: '管理和更新系统模型及服务配置',
basicInfo: '基本信息',
documentSplitting: '文档分割',
apiEndpoint: 'API端点',
enterApiEndpoint: '输入API端点例如http://localhost',
enterApiKey: '输入API密钥',
enterKnowledgeBaseId: '输入知识库ID',
saveConfig: '保存配置',
reset: '重置',
configSaved: '配置保存成功',
enterApiEndpointRequired: '请输入API端点',
enterApiKeyRequired: '请输入API密钥',
enterKnowledgeBaseIdRequired: '请输入知识库ID',
name: '名称',
enterName: '输入名称',
description: '描述',
chunkSize: '分块大小',
chunkOverlap: '分块重叠',
save: '保存',
saving: '保存中...',
saveSuccess: '保存成功',
saveFailed: '保存失败',
model: '模型',
llmModel: 'LLM模型',
embeddingModel: '嵌入模型',
rerankModel: '重排序模型',
vlmModel: '多模态模型',
modelName: '模型名称',
modelUrl: '模型地址',
apiKey: 'API密钥',
cancel: '取消',
saveFailedSettings: '设置保存失败',
enterNameRequired: '请输入名称'
},
initialization: {
title: '初始化',
welcome: '欢迎使用WeKnora',
description: '请先配置系统以开始使用',
step1: '步骤1配置LLM模型',
step2: '步骤2配置嵌入模型',
step3: '步骤3配置其他模型',
complete: '完成初始化',
skip: '跳过',
next: '下一步',
previous: '上一步',
ollamaServiceStatus: 'Ollama服务状态',
refreshStatus: '刷新状态',
ollamaServiceAddress: 'Ollama服务地址',
notConfigured: '未配置',
notRunning: '未运行',
normal: '正常',
installedModels: '已安装模型',
none: '暂无',
knowledgeBaseInfo: '知识库信息',
knowledgeBaseName: '知识库名称',
knowledgeBaseNamePlaceholder: '输入知识库名称',
knowledgeBaseDescription: '知识库描述',
knowledgeBaseDescriptionPlaceholder: '输入知识库描述',
llmModelConfig: 'LLM大语言模型配置',
modelSource: '模型来源',
local: 'Ollama本地',
remote: 'Remote API远程',
modelName: '模型名称',
modelNamePlaceholder: '例如qwen3:0.6b',
baseUrl: 'Base URL',
baseUrlPlaceholder: '例如https://api.openai.com/v1去掉URL末尾的/chat/completions部分',
apiKey: 'API Key可选',
apiKeyPlaceholder: '输入API Key可选',
downloadModel: '下载模型',
installed: '已安装',
notInstalled: '未安装',
notChecked: '未检查',
checkConnection: '检查连接',
connectionNormal: '连接正常',
connectionFailed: '连接失败',
checkingConnection: '正在检查连接',
embeddingModelConfig: '嵌入模型配置',
embeddingWarning: '知识库已有文件,无法更改嵌入模型配置',
dimension: '维度',
dimensionPlaceholder: '输入向量维度',
detectDimension: '检测维度',
rerankModelConfig: '重排序模型配置',
enableRerank: '启用重排序模型',
multimodalConfig: '多模态配置',
enableMultimodal: '启用图像信息提取',
visualLanguageModelConfig: '视觉语言模型配置',
interfaceType: '接口类型',
openaiCompatible: 'OpenAI兼容接口',
storageServiceConfig: '存储服务配置',
storageType: '存储类型',
bucketName: 'Bucket名称',
bucketNamePlaceholder: '输入Bucket名称',
pathPrefix: '路径前缀',
pathPrefixPlaceholder: '例如images',
secretId: 'Secret ID',
secretIdPlaceholder: '输入COS Secret ID',
secretKey: 'Secret Key',
secretKeyPlaceholder: '输入COS Secret Key',
region: 'Region',
regionPlaceholder: '例如ap-beijing',
appId: 'App ID',
appIdPlaceholder: '输入App ID',
functionTest: '功能测试',
testDescription: '上传图片测试VLM模型的图像描述和文字识别功能',
selectImage: '选择图片',
startTest: '开始测试',
testResult: '测试结果',
imageDescription: '图像描述:',
textRecognition: '文字识别:',
processingTime: '处理时间:',
testFailed: '测试失败',
multimodalProcessingFailed: '多模态处理失败',
documentSplittingConfig: '文档分割配置',
splittingStrategy: '分割策略',
balancedMode: '平衡模式',
balancedModeDesc: '分块大小1000 / 重叠200',
precisionMode: '精确模式',
precisionModeDesc: '分块大小512 / 重叠100',
contextMode: '上下文模式',
contextModeDesc: '分块大小2048 / 重叠400',
custom: '自定义',
customDesc: '手动配置参数',
chunkSize: '分块大小',
chunkOverlap: '分块重叠',
separatorSettings: '分隔符设置',
selectOrCustomSeparators: '选择或自定义分隔符',
characters: '个字符',
separatorParagraph: '段落分隔符 (\\n\\n)',
separatorNewline: '换行符 (\\n)',
separatorPeriod: '句号 (。)',
separatorExclamation: '感叹号 ()',
separatorQuestion: '问号 ()',
separatorSemicolon: '分号 (;)',
separatorChineseSemicolon: '中文分号 ()',
separatorComma: '逗号 (,)',
separatorChineseComma: '中文逗号 ()',
entityRelationExtraction: '实体和关系提取',
enableEntityRelationExtraction: '启用实体和关系提取',
relationTypeConfig: '关系类型配置',
relationType: '关系类型',
generateRandomTags: '生成随机标签',
completeModelConfig: '请完成模型配置',
systemWillExtract: '系统将根据所选关系类型从文本中提取相应的实体和关系',
extractionExample: '提取示例',
sampleText: '示例文本',
sampleTextPlaceholder: '输入用于分析的文本,例如:"红楼梦",又名"石头记",是中国四大名著之一,清代曹雪芹所著...',
generateRandomText: '生成随机文本',
entityList: '实体列表',
nodeName: '节点名称',
nodeNamePlaceholder: '节点名称',
addAttribute: '添加属性',
attributeValue: '属性值',
attributeValuePlaceholder: '属性值',
addEntity: '添加实体',
completeEntityInfo: '请完成实体信息',
relationConnection: '关系连接',
selectEntity: '选择实体',
addRelation: '添加关系',
completeRelationInfo: '请完成关系信息',
startExtraction: '开始提取',
extracting: '提取中...',
defaultExample: '默认示例',
clearExample: '清除示例',
updateKnowledgeBaseSettings: '更新知识库设置',
updateConfigInfo: '更新配置信息',
completeConfig: '完成配置',
waitForDownloads: '请等待所有Ollama模型下载完成后再更新配置',
completeModelConfigInfo: '请完成模型配置信息',
knowledgeBaseIdMissing: '知识库ID缺失',
knowledgeBaseSettingsUpdateSuccess: '知识库设置更新成功',
configUpdateSuccess: '配置更新成功',
systemInitComplete: '系统初始化完成',
operationFailed: '操作失败',
updateKnowledgeBaseInfoFailed: '更新知识库基本信息失败',
knowledgeBaseIdMissingCannotSave: '知识库ID缺失无法保存配置',
operationFailedCheckNetwork: '操作失败,请检查网络连接',
imageUploadSuccess: '图片上传成功,可以开始测试',
multimodalConfigIncomplete: '多模态配置不完整,请先完成多模态配置后再上传图片',
pleaseSelectImage: '请选择图片',
multimodalTestSuccess: '多模态测试成功',
multimodalTestFailed: '多模态测试失败',
pleaseEnterSampleText: '请输入示例文本',
pleaseEnterRelationType: '请输入关系类型',
pleaseEnterLLMModelConfig: '请输入LLM大语言模型配置',
noValidNodesExtracted: '未提取到有效节点',
noValidRelationsExtracted: '未提取到有效关系',
extractionFailedCheckNetwork: '提取失败,请检查网络或文本格式',
generateFailedRetry: '生成失败,请重试',
pleaseCheckForm: '请检查表单填写是否正确',
detectionSuccessful: '检测成功,维度自动填充为',
detectionFailed: '检测失败',
detectionFailedCheckConfig: '检测失败,请检查配置',
modelDownloadSuccess: '模型下载成功',
modelDownloadFailed: '模型下载失败',
downloadStartFailed: '下载启动失败',
queryProgressFailed: '进度查询失败',
checkOllamaStatusFailed: 'Ollama状态检查失败',
getKnowledgeBaseInfoFailed: '获取知识库信息失败',
textRelationExtractionFailed: '文本关系提取失败',
pleaseEnterKnowledgeBaseName: '请输入知识库名称',
knowledgeBaseNameLength: '知识库名称长度必须为1-50个字符',
knowledgeBaseDescriptionLength: '知识库描述不能超过200个字符',
pleaseEnterLLMModelName: '请输入LLM模型名称',
pleaseEnterBaseURL: '请输入BaseURL',
pleaseEnterEmbeddingModelName: '请输入嵌入模型名称',
pleaseEnterEmbeddingDimension: '请输入嵌入维度',
dimensionMustBeInteger: '维度必须是有效整数通常为768、1024、1536、3584等',
pleaseEnterTextContent: '请输入文本内容',
textContentMinLength: '文本内容必须包含至少10个字符',
pleaseEnterValidTag: '请输入有效标签',
tagAlreadyExists: '此标签已存在',
checkFailed: '检查失败',
startingDownload: '正在启动下载...',
downloadStarted: '下载已开始',
model: '模型',
startModelDownloadFailed: '启动模型下载失败',
downloadCompleted: '下载完成',
downloadFailed: '下载失败',
knowledgeBaseSettingsModeMissingId: '知识库设置模式缺少知识库ID',
completeEmbeddingConfig: '请先完成嵌入配置',
detectionSuccess: '检测成功,',
dimensionAutoFilled: '维度已自动填充:',
checkFormCorrectness: '请检查表单填写是否正确',
systemInitializationCompleted: '系统初始化完成',
generationFailedRetry: '生成失败,请重试',
chunkSizeDesc: '每个文本块的大小。较大的块保留更多上下文,但可能降低搜索准确性。',
chunkOverlapDesc: '相邻块之间重叠的字符数。有助于保持块边界处的上下文。',
selectRelationType: '选择关系类型'
},
auth: {
login: '登录',
logout: '退出',
username: '用户名',
email: '邮箱',
password: '密码',
confirmPassword: '确认密码',
rememberMe: '记住我',
forgotPassword: '忘记密码?',
loginSuccess: '登录成功!',
loginFailed: '登录失败',
loggingIn: '登录中...',
register: '注册',
registering: '注册中...',
createAccount: '创建账户',
haveAccount: '已有账户?',
noAccount: '还没有账户?',
backToLogin: '返回登录',
registerNow: '立即注册',
registerSuccess: '注册成功!系统已为您创建专属租户,请登录',
registerFailed: '注册失败',
subtitle: '基于大模型的文档理解和语义搜索框架',
registerSubtitle: '注册后系统将为您创建专属租户',
emailPlaceholder: '输入邮箱地址',
passwordPlaceholder: '输入密码8-32个字符包含字母和数字',
confirmPasswordPlaceholder: '再次输入密码',
usernamePlaceholder: '输入用户名',
emailRequired: '请输入邮箱地址',
emailInvalid: '请输入正确的邮箱格式',
passwordRequired: '请输入密码',
passwordMinLength: '密码至少8个字符',
passwordMaxLength: '密码不能超过32个字符',
passwordMustContainLetter: '密码必须包含字母',
passwordMustContainNumber: '密码必须包含数字',
usernameRequired: '请输入用户名',
usernameMinLength: '用户名至少2个字符',
usernameMaxLength: '用户名不能超过20个字符',
usernameInvalid: '用户名只能包含字母、数字、下划线和中文字符',
confirmPasswordRequired: '请确认密码',
passwordMismatch: '两次输入的密码不一致',
loginError: '登录错误,请检查邮箱或密码',
loginErrorRetry: '登录错误,请稍后重试',
registerError: '注册错误,请稍后重试',
forgotPasswordNotAvailable: '密码找回功能暂不可用,请联系管理员'
},
common: {
confirm: '确认',
cancel: '取消',
save: '保存',
delete: '删除',
edit: '编辑',
create: '创建',
search: '搜索',
filter: '筛选',
export: '导出',
import: '导入',
upload: '上传',
download: '下载',
refresh: '刷新',
loading: '加载中...',
noData: '暂无数据',
error: '错误',
success: '成功',
warning: '警告',
info: '信息',
yes: '是',
no: '否',
ok: '确定',
close: '关闭',
back: '返回',
next: '下一步',
finish: '完成',
all: '全部',
reset: '重置',
clear: '清空'
},
file: {
upload: '上传文件',
uploadSuccess: '文件上传成功',
uploadFailed: '文件上传失败',
delete: '删除文件',
deleteSuccess: '文件删除成功',
deleteFailed: '文件删除失败',
download: '下载文件',
preview: '预览',
unsupportedFormat: '不支持的文件格式',
maxSizeExceeded: '文件大小超过限制',
selectFile: '选择文件'
},
tenant: {
title: '租户信息',
name: '租户名称',
id: '租户ID',
createdAt: '创建时间',
updatedAt: '更新时间',
status: '状态',
active: '活跃',
inactive: '未活跃',
systemInfo: '系统信息',
viewSystemInfo: '查看系统版本和用户账户配置信息',
version: '版本',
buildTime: '构建时间',
goVersion: 'Go版本',
userInfo: '用户信息',
userId: '用户ID',
username: '用户名',
email: '邮箱',
tenantInfo: '租户信息',
tenantId: '租户ID',
tenantName: '租户名称',
description: '描述',
business: '业务',
noDescription: '无描述',
noBusiness: '无',
statusActive: '活跃',
statusInactive: '未激活',
statusSuspended: '已暂停',
statusUnknown: '未知',
apiKey: 'API密钥',
keepApiKeySafe: '请妥善保管您的API密钥不要在公共场所或代码仓库中泄露',
storageInfo: '存储信息',
storageQuota: '存储配额',
used: '已使用',
usage: '使用率',
apiDevDocs: 'API开发文档',
useApiKey: '使用您的API密钥开始开发查看完整的API文档和代码示例。',
viewApiDoc: '查看API文档',
loadingAccountInfo: '加载账户信息中...',
loadFailed: '加载失败',
retry: '重试',
apiKeyCopied: 'API密钥已复制到剪贴板',
unknown: '未知',
formatError: '格式错误'
},
error: {
network: '网络错误',
server: '服务器错误',
notFound: '未找到',
unauthorized: '未授权',
forbidden: '禁止访问',
unknown: '未知错误',
tryAgain: '请重试'
},
model: {
llmModel: 'LLM模型',
embeddingModel: '嵌入模型',
rerankModel: '重排序模型',
vlmModel: '多模态模型',
modelName: '模型名称',
modelProvider: '模型提供商',
modelUrl: '模型地址',
apiKey: 'API密钥',
testConnection: '测试连接',
connectionSuccess: '连接成功',
connectionFailed: '连接失败',
dimension: '维度',
maxTokens: '最大令牌数',
temperature: '温度',
topP: 'Top P',
selectModel: '选择模型',
customModel: '自定义模型',
builtinModel: '内置模型'
}
}

View File

@@ -2,6 +2,7 @@ import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
import i18n from "./i18n";
import "./assets/fonts.css";
import TDesign from "tdesign-vue-next";
// 引入组件库的少量全局样式变量
@@ -12,5 +13,6 @@ const app = createApp(App);
app.use(TDesign);
app.use(createPinia());
app.use(router);
app.use(i18n);
app.mount("#app");

View File

@@ -5,16 +5,16 @@ import { defineStore } from 'pinia';
export const useMenuStore = defineStore('menuStore', {
state: () => ({
menuArr: reactive([
{ title: '知识库', icon: 'zhishiku', path: 'knowledge-bases' },
{ titleKey: 'menu.knowledgeBase', icon: 'zhishiku', path: 'knowledge-bases' },
{
title: '对话',
titleKey: 'menu.chat',
icon: 'prefixIcon',
path: 'creatChat',
childrenPath: 'chat',
children: reactive<object[]>([]),
},
{ title: '系统信息', icon: 'tenant', path: 'tenant' },
{ title: '退出登录', icon: 'logout', path: 'logout' }
{ titleKey: 'menu.tenant', icon: 'tenant', path: 'tenant' },
{ titleKey: 'menu.logout', icon: 'logout', path: 'logout' }
]),
isFirstSession: false,
firstQuery: ''

View File

@@ -1,5 +1,10 @@
<template>
<div class="login-container">
<!-- Переключатель языка -->
<div class="language-switcher-wrapper">
<LanguageSwitcher />
</div>
<!-- 登录表单 -->
<div class="login-card" v-if="!isRegisterMode">
<!-- 系统Logo和标题 -->
@@ -7,7 +12,7 @@
<div class="logo">
<img src="@/assets/img/weknora.png" alt="WeKnora" class="logo-img" />
</div>
<p class="login-subtitle">基于大模型的文档理解与语义检索框架</p>
<p class="login-subtitle">{{ t('auth.subtitle') }}</p>
</div>
<div class="login-form">
@@ -18,20 +23,20 @@
@submit="handleLogin"
layout="vertical"
>
<t-form-item label="邮箱" name="email">
<t-form-item :label="t('auth.email')" name="email">
<t-input
v-model="formData.email"
placeholder="请输入邮箱地址"
:placeholder="t('auth.emailPlaceholder')"
type="email"
size="large"
:disabled="loading"
/>
</t-form-item>
<t-form-item label="密码" name="password">
<t-form-item :label="t('auth.password')" name="password">
<t-input
v-model="formData.password"
placeholder="请输入密码8-32位包含字母和数字"
:placeholder="t('auth.passwordPlaceholder')"
type="password"
size="large"
:disabled="loading"
@@ -47,15 +52,15 @@
:loading="loading"
class="login-button"
>
{{ loading ? '登录中...' : '登录' }}
{{ loading ? t('auth.loggingIn') : t('auth.login') }}
</t-button>
</t-form>
<!-- 注册链接 -->
<div class="register-link">
<span>还没有账号</span>
<span>{{ t('auth.noAccount') }} </span>
<a href="#" @click.prevent="toggleMode" class="register-btn">
立即注册
{{ t('auth.registerNow') }}
</a>
</div>
</div>
@@ -64,8 +69,8 @@
<!-- 注册表单 -->
<div class="register-card" v-if="isRegisterMode">
<div class="login-header">
<h1 class="login-title">创建账号</h1>
<p class="login-subtitle">注册后系统将为您创建专属租户</p>
<h1 class="login-title">{{ t('auth.createAccount') }}</h1>
<p class="login-subtitle">{{ t('auth.registerSubtitle') }}</p>
</div>
<div class="login-form">
@@ -76,39 +81,39 @@
@submit="handleRegister"
layout="vertical"
>
<t-form-item label="用户名" name="username">
<t-form-item :label="t('auth.username')" name="username">
<t-input
v-model="registerData.username"
placeholder="请输入用户名"
:placeholder="t('auth.usernamePlaceholder')"
size="large"
:disabled="loading"
/>
</t-form-item>
<t-form-item label="邮箱" name="email">
<t-form-item :label="t('auth.email')" name="email">
<t-input
v-model="registerData.email"
placeholder="请输入邮箱地址"
:placeholder="t('auth.emailPlaceholder')"
type="email"
size="large"
:disabled="loading"
/>
</t-form-item>
<t-form-item label="密码" name="password">
<t-form-item :label="t('auth.password')" name="password">
<t-input
v-model="registerData.password"
placeholder="请输入密码8-32位包含字母和数字"
:placeholder="t('auth.passwordPlaceholder')"
type="password"
size="large"
:disabled="loading"
/>
</t-form-item>
<t-form-item label="确认密码" name="confirmPassword">
<t-form-item :label="t('auth.confirmPassword')" name="confirmPassword">
<t-input
v-model="registerData.confirmPassword"
placeholder="请再次输入密码"
:placeholder="t('auth.confirmPasswordPlaceholder')"
type="password"
size="large"
:disabled="loading"
@@ -124,15 +129,15 @@
:loading="loading"
class="login-button"
>
{{ loading ? '注册中...' : '注册' }}
{{ loading ? t('auth.registering') : t('auth.register') }}
</t-button>
</t-form>
<!-- 返回登录 -->
<div class="register-link">
<span>已有账号</span>
<span>{{ t('auth.haveAccount') }} </span>
<a href="#" @click.prevent="toggleMode" class="register-btn">
返回登录
{{ t('auth.backToLogin') }}
</a>
</div>
</div>
@@ -143,9 +148,13 @@
<script setup lang="ts">
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { MessagePlugin } from 'tdesign-vue-next'
import { login, register } from '@/api/auth'
import { useAuthStore } from '@/stores/auth'
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
const { t } = useI18n()
const router = useRouter()
const authStore = useAuthStore()
@@ -174,52 +183,52 @@ const registerData = reactive<{[key: string]: any}>({
})
// 登录表单验证规则
const formRules = {
const formRules = computed(() => ({
email: [
{ required: true, message: '请输入邮箱地址', type: 'error' },
{ email: true, message: '请输入正确的邮箱格式', type: 'error' }
{ required: true, message: t('auth.emailRequired'), type: 'error' },
{ email: true, message: t('auth.emailInvalid'), type: 'error' }
],
password: [
{ required: true, message: '请输入密码', type: 'error' },
{ min: 8, message: '密码至少8位', type: 'error' },
{ max: 32, message: '密码不能超过32位', type: 'error' },
{ pattern: /[a-zA-Z]/, message: '密码必须包含字母', type: 'error' },
{ pattern: /\d/, message: '密码必须包含数字', type: 'error' }
{ required: true, message: t('auth.passwordRequired'), type: 'error' },
{ min: 8, message: t('auth.passwordMinLength'), type: 'error' },
{ max: 32, message: t('auth.passwordMaxLength'), type: 'error' },
{ pattern: /[a-zA-Z]/, message: t('auth.passwordMustContainLetter'), type: 'error' },
{ pattern: /\d/, message: t('auth.passwordMustContainNumber'), type: 'error' }
]
}
}))
// 注册表单验证规则
const registerRules = {
const registerRules = computed(() => ({
username: [
{ required: true, message: '请输入用户名', type: 'error' },
{ min: 2, message: '用户名至少2位', type: 'error' },
{ max: 20, message: '用户名不能超过20位', type: 'error' },
{
pattern: /^[a-zA-Z0-9_\u4e00-\u9fa5]+$/,
message: '用户名只能包含字母、数字、下划线和中文',
type: 'error'
{ required: true, message: t('auth.usernameRequired'), type: 'error' },
{ min: 2, message: t('auth.usernameMinLength'), type: 'error' },
{ max: 20, message: t('auth.usernameMaxLength'), type: 'error' },
{
pattern: /^[a-zA-Z0-9_\u4e00-\u9fa5]+$/,
message: t('auth.usernameInvalid'),
type: 'error'
}
],
email: [
{ required: true, message: '请输入邮箱地址', type: 'error' },
{ email: true, message: '请输入正确的邮箱格式', type: 'error' }
{ required: true, message: t('auth.emailRequired'), type: 'error' },
{ email: true, message: t('auth.emailInvalid'), type: 'error' }
],
password: [
{ required: true, message: '请输入密码', type: 'error' },
{ min: 8, message: '密码至少8位', type: 'error' },
{ max: 32, message: '密码不能超过32位', type: 'error' },
{ pattern: /[a-zA-Z]/, message: '密码必须包含字母', type: 'error' },
{ pattern: /\d/, message: '密码必须包含数字', type: 'error' }
{ required: true, message: t('auth.passwordRequired'), type: 'error' },
{ min: 8, message: t('auth.passwordMinLength'), type: 'error' },
{ max: 32, message: t('auth.passwordMaxLength'), type: 'error' },
{ pattern: /[a-zA-Z]/, message: t('auth.passwordMustContainLetter'), type: 'error' },
{ pattern: /\d/, message: t('auth.passwordMustContainNumber'), type: 'error' }
],
confirmPassword: [
{ required: true, message: '请确认密码', type: 'error' },
{ required: true, message: t('auth.confirmPasswordRequired'), type: 'error' },
{
validator: (val: string) => val === registerData.password,
message: '两次输入的密码不一致',
message: t('auth.passwordMismatch'),
type: 'error'
}
]
}
}))
// 切换登录/注册模式
const toggleMode = () => {
@@ -269,18 +278,18 @@ const handleLogin = async () => {
})
}
MessagePlugin.success('登录成功!')
MessagePlugin.success(t('auth.loginSuccess'))
// 等待状态更新完成后再跳转
await nextTick()
router.replace('/platform/knowledge-bases')
} else {
MessagePlugin.error(response.message || '登录失败,请检查邮箱或密码')
MessagePlugin.error(response.message || t('auth.loginError'))
}
} catch (error: any) {
console.error('登录错误:', error)
MessagePlugin.error(error.message || '登录失败,请稍后重试')
console.error(t('auth.loginError') + ':', error)
MessagePlugin.error(error.message || t('auth.loginErrorRetry'))
} finally {
loading.value = false
}
@@ -301,7 +310,7 @@ const handleRegister = async () => {
})
if (response.success) {
MessagePlugin.success('注册成功!系统已为您创建专属租户,请登录使用')
MessagePlugin.success(t('auth.registerSuccess'))
// 切换到登录模式并填入邮箱
isRegisterMode.value = false
@@ -312,11 +321,11 @@ const handleRegister = async () => {
(registerData as any)[key] = ''
})
} else {
MessagePlugin.error(response.message || '注册失败')
MessagePlugin.error(response.message || t('auth.registerFailed'))
}
} catch (error: any) {
console.error('注册错误:', error)
MessagePlugin.error(error.message || '注册失败,请稍后重试')
console.error(t('auth.registerError') + ':', error)
MessagePlugin.error(error.message || t('auth.registerError'))
} finally {
loading.value = false
}
@@ -324,7 +333,7 @@ const handleRegister = async () => {
// 处理忘记密码
const handleForgotPassword = () => {
MessagePlugin.info('忘记密码功能暂未开放,请联系管理员')
MessagePlugin.info(t('auth.forgotPasswordNotAvailable'))
}
// 检查是否已登录
@@ -344,6 +353,34 @@ onMounted(() => {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px;
box-sizing: border-box;
position: relative;
}
.language-switcher-wrapper {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
:deep(.t-select) {
min-width: 140px;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
:deep(.t-select__wrap) {
border-color: #e7e7e7;
&:hover {
border-color: #07C05F;
}
}
:deep(.t-is-focused .t-select__wrap) {
border-color: #07C05F;
box-shadow: 0 0 0 2px rgba(7, 192, 95, 0.1);
}
}
.login-card,
@@ -525,6 +562,15 @@ onMounted(() => {
.login-container {
padding: 16px;
}
.language-switcher-wrapper {
top: 12px;
right: 12px;
:deep(.t-select) {
min-width: 120px;
}
}
.login-card,
.register-card {

View File

@@ -7,12 +7,12 @@
<div ref="parentMd">
<!-- 消息正在总结中则渲染加载gif -->
<img v-if="session.thinking" class="botanswer_laoding_gif" src="@/assets/img/botanswer_loading.gif"
alt="正在总结答案……">
:alt="$t('chat.summarizingAnswer')">
<div v-for="(item, index) in processedMarkdown" :key="index">
<img class="ai-markdown-img" @click="preview(item)" v-if="isLink(item)" :src="item" alt="">
<div v-else class="ai-markdown-template" v-html="processMarkdown(item)"></div>
</div>
<div v-if="isImgLoading" class="img_loading"><t-loading size="small"></t-loading><span>加载中...</span></div>
<div v-if="isImgLoading" class="img_loading"><t-loading size="small"></t-loading><span>{{ $t('chat.loading') }}</span></div>
</div>
<picturePreview :reviewImg="reviewImg" :reviewUrl="reviewUrl" @closePreImg="closePreImg"></picturePreview>
</div>

View File

@@ -11,10 +11,10 @@
<template #header>
<div class="deep-title">
<div v-if="deepSession.thinking" class="thinking">
<img class="img_gif" src="@/assets/img/think.gif" alt="">思考中···
<img class="img_gif" src="@/assets/img/think.gif" :alt="$t('chat.thinking')">
</div>
<div v-else class="done">
<img class="icon deep_icon" src="@/assets/img/Frame3718.svg" alt=""></img>已深度思考
<img class="icon deep_icon" src="@/assets/img/Frame3718.svg" :alt="$t('chat.deepThinking')">
</div>
</div>
</template>
@@ -29,7 +29,9 @@
</template>
<script setup>
import { onMounted, watch, computed, ref, reactive, defineProps } from 'vue';
import { useI18n } from 'vue-i18n';
import { sanitizeHTML } from '@/utils/security';
const { t } = useI18n();
const isFold = ref(true)
const props = defineProps({

View File

@@ -3,7 +3,7 @@
<div class="refer_header" @click="referBoxSwitch" v-if="session.knowledge_references && session.knowledge_references.length">
<div class="refer_title">
<img src="@/assets/img/ziliao.svg" alt="" />
<span>参考了{{ session.knowledge_references && session.knowledge_references.length }}个相关内容</span>
<span>{{ $t('chat.referencedContent', { count: session.knowledge_references && session.knowledge_references.length }) }}</span>
</div>
<div class="refer_show_icon">
<t-icon :name="showReferBox ? 'chevron-up' : 'chevron-down'" />
@@ -28,7 +28,9 @@
</template>
<script setup>
import { onMounted, defineProps, computed, ref, reactive } from "vue";
import { useI18n } from 'vue-i18n';
import { sanitizeHTML } from '@/utils/security';
const { t } = useI18n();
const props = defineProps({
// 必填项
content: {

View File

@@ -1,11 +1,13 @@
<template>
<div>
<t-textarea resize="none" :autosize="false" v-model="value" placeholder="请输入描述文案" name="description" @change="onChange" />
<t-textarea resize="none" :autosize="false" v-model="value" :placeholder="$t('chat.enterDescription')" name="description" @change="onChange" />
</div>
</template>
<script setup>
import { onMounted, watch, computed, ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const value = ref('');
const onChange = (value,e) => {
console.log(value)

View File

@@ -13,7 +13,7 @@
</div>
<div v-if="loading"
style="height: 41px;display: flex;align-items: center;background: #fff;width: 58px;">
<img class="botanswer_laoding_gif" src="@/assets/img/botanswer_loading.gif" alt="正在等待答案……">
<img class="botanswer_laoding_gif" src="@/assets/img/botanswer_loading.gif" :alt="$t('chat.waitingForAnswer')">
</div>
</div>
</div>
@@ -26,12 +26,14 @@
import { storeToRefs } from 'pinia';
import { ref, onMounted, onUnmounted, nextTick, watch, reactive, onBeforeUnmount } from 'vue';
import { useRoute, useRouter, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
import { useI18n } from 'vue-i18n';
import InputField from '../../components/Input-field.vue';
import botmsg from './components/botmsg.vue';
import usermsg from './components/usermsg.vue';
import { getMessageList, generateSessionsTitle } from "@/api/chat/index";
import { useStream } from '../../api/chat/streame'
import { useMenuStore } from '@/stores/menu';
const { t } = useI18n();
const usemenuStore = useMenuStore();
const { menuArr, isFirstSession, firstQuery } = storeToRefs(usemenuStore);
const { output, onChunk, isStreaming, isLoading, error, startStream, stopStream } = useStream();
@@ -124,7 +126,7 @@ const handleMsgList = async (data, isScrollType = false, newScrollHeight) => {
}
}
if (item.is_completed && !item.content) {
item.content = "抱歉,我无法回答这个问题。";
item.content = t('chat.cannotAnswer');
}
messagesList.unshift(item);
if (isFirstEnter.value) {

View File

@@ -2,17 +2,17 @@
<div class="dialogue-wrap">
<div class="dialogue-answers">
<div class="dialogue-title">
<span>基于知识库内容问答</span>
<span>{{ t('chat.knowledgeBaseQandA') }}</span>
</div>
<InputField @send-msg="sendMsg"></InputField>
</div>
</div>
<t-dialog v-model:visible="selectVisible" header="选择知识库" :confirmBtn="{ content: '开始对话', theme: 'primary' }" :onConfirm="confirmSelect" :onCancel="() => selectVisible = false">
<t-dialog v-model:visible="selectVisible" :header="t('knowledgeBase.title')" :confirmBtn="{ content: t('chat.newChat'), theme: 'primary' }" :onConfirm="confirmSelect" :onCancel="() => selectVisible = false">
<t-form :data="{ kb: selectedKbId }">
<t-form-item label="知识库">
<t-select v-model="selectedKbId" :loading="kbLoading" placeholder="请选择知识库">
<t-form-item :label="t('knowledgeBase.title')">
<t-select v-model="selectedKbId" :loading="kbLoading" :placeholder="t('knowledgeBase.selectKnowledgeBaseFirst')">
<t-option v-for="kb in kbList" :key="kb.id" :value="kb.id" :label="kb.name" />
</t-select>
</t-form-item>
@@ -21,6 +21,7 @@
</template>
<script setup lang="ts">
import { ref, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import InputField from '@/components/Input-field.vue';
import EmptyKnowledge from '@/components/empty-knowledge.vue';
import { getSessionsList, createSessions, generateSessionsTitle } from "@/api/chat/index";
@@ -29,6 +30,8 @@ import { useRoute, useRouter } from 'vue-router';
import useKnowledgeBase from '@/hooks/useKnowledgeBase';
import { listKnowledgeBases } from '@/api/knowledge-base';
const { t } = useI18n();
let { cardList } = useKnowledgeBase()
const router = useRouter();
const route = useRoute();
@@ -75,10 +78,10 @@ async function createNewSession(value: string) {
await getTitle(res.data.id, value)
} else {
// 错误处理
console.error("创建会话失败");
console.error(t('chat.createSessionFailed'));
}
}).catch(error => {
console.error("创建会话出错:", error);
console.error(t('chat.createSessionError') + ':', error);
})
}
@@ -92,19 +95,19 @@ const confirmSelect = async () => {
if (res.data && res.data.id) {
await getTitle(res.data.id, value, selectedKbId.value)
} else {
console.error('创建会话失败')
console.error(t('chat.createSessionFailed'))
}
}).catch((e:any) => console.error('创建会话出错:', e))
}).catch((e:any) => console.error(t('chat.createSessionError') + ':', e))
}
const getTitle = async (session_id: string, value: string, kbId?: string) => {
const finalKbId = kbId || await ensureKbId();
if (!finalKbId) {
console.error('无法获取知识库ID');
console.error(t('chat.unableToGetKnowledgeBaseId'));
return;
}
let obj = { title: '新会话', path: `chat/${finalKbId}/${session_id}`, id: session_id, isMore: false, isNoTitle: true }
let obj = { title: t('menu.newSession'), path: `chat/${finalKbId}/${session_id}`, id: session_id, isMore: false, isNoTitle: true }
usemenuStore.updataMenuChildren(obj);
usemenuStore.changeIsFirstSession(true);
usemenuStore.changeFirstQuery(value);

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,8 @@ import EmptyKnowledge from '@/components/empty-knowledge.vue';
import { getSessionsList, createSessions, generateSessionsTitle } from "@/api/chat/index";
import { useMenuStore } from '@/stores/menu';
import { MessagePlugin } from 'tdesign-vue-next';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const usemenuStore = useMenuStore();
const router = useRouter();
import {
@@ -34,14 +36,17 @@ const getPageSize = () => {
}
getPageSize()
// 直接调用 API 获取知识库文件列表
console.log(t('knowledgeBase.apiCallKnowledgeFiles'));
const loadKnowledgeFiles = async (kbIdValue: string) => {
if (!kbIdValue) return;
try {
const result = await listKnowledgeFiles(kbIdValue, { page: 1, page_size: pageSize });
// 由于响应拦截器已经返回了 data所以 result 就是响应的 data 部分
// 按照 useKnowledgeBase hook 中的方式处理
console.log(t('knowledgeBase.responseInterceptorData'));
console.log(t('knowledgeBase.hookProcessing'));
const { data, total: totalResult } = result as any;
if (!data || !Array.isArray(data)) {
@@ -65,32 +70,34 @@ const loadKnowledgeFiles = async (kbIdValue: string) => {
cardList.value = cardList_ as any[];
total.value = totalResult;
} catch (err) {
console.error('Failed to load knowledge files:', err);
console.error(t('knowledgeBase.errorHandling') + ':', err);
}
};
// 监听路由参数变化,重新获取知识库内容
watch(() => kbId.value, (newKbId, oldKbId) => {
if (newKbId && newKbId !== oldKbId) {
console.log(t('knowledgeBase.routeParamChange'));
loadKnowledgeFiles(newKbId);
}
}, { immediate: false });
// 监听文件上传事件
const handleFileUploaded = (event: CustomEvent) => {
const uploadedKbId = event.detail.kbId;
console.log('接收到文件上传事件上传的知识库ID:', uploadedKbId, '当前知识库ID:', kbId.value);
if (uploadedKbId && uploadedKbId === kbId.value) {
console.log('匹配当前知识库,开始刷新文件列表');
// 如果上传的文件属于当前知识库,使用 loadKnowledgeFiles 刷新文件列表
loadKnowledgeFiles(uploadedKbId);
}
const uploadedKbId = event.detail.kbId;
console.log(t('knowledgeBase.fileUploadEventReceived', { uploadedKbId, currentKbId: kbId.value }));
if (uploadedKbId && uploadedKbId === kbId.value) {
console.log(t('knowledgeBase.matchingKnowledgeBase'));
// 如果上传的文件属于当前知识库,使用 loadKnowledgeFiles 刷新文件列表
loadKnowledgeFiles(uploadedKbId);
}
};
onMounted(() => {
getKnowled({ page: 1, page_size: pageSize });
// 监听文件上传事件
console.log(t('knowledgeBase.fileUploadEventListening'));
window.addEventListener('knowledgeFileUploaded', handleFileUploaded as EventListener);
});
@@ -176,7 +183,7 @@ const sendMsg = (value: string) => {
};
const getTitle = (session_id: string, value: string) => {
let obj = { title: '新会话', path: `chat/${kbId.value}/${session_id}`, id: session_id, isMore: false, isNoTitle: true };
let obj = { title: t('knowledgeBase.newSession'), path: `chat/${kbId.value}/${session_id}`, id: session_id, isMore: false, isNoTitle: true };
usemenuStore.updataMenuChildren(obj);
usemenuStore.changeIsFirstSession(true);
usemenuStore.changeFirstQuery(value);
@@ -185,23 +192,25 @@ const getTitle = (session_id: string, value: string) => {
async function createNewSession(value: string): Promise<void> {
// 优先使用当前页面的知识库ID
console.log(t('knowledgeBase.priorityCurrentPageKbId'));
let sessionKbId = kbId.value;
// 如果当前页面没有知识库ID尝试从localStorage获取设置中的知识库ID
if (!sessionKbId) {
console.log(t('knowledgeBase.fallbackLocalStorageKbId'));
const settingsStr = localStorage.getItem("WeKnora_settings");
if (settingsStr) {
try {
const settings = JSON.parse(settingsStr);
sessionKbId = settings.knowledgeBaseId;
} catch (e) {
console.error("解析设置失败:", e);
console.error(t('knowledgeBase.settingsParsingFailed') + ":", e);
}
}
}
if (!sessionKbId) {
MessagePlugin.warning("请先选择一个知识库");
MessagePlugin.warning(t('knowledgeBase.selectKnowledgeBaseFirst'));
return;
}
@@ -210,10 +219,10 @@ async function createNewSession(value: string): Promise<void> {
getTitle(res.data.id, value);
} else {
// 错误处理
console.error("创建会话失败");
console.error(t('knowledgeBase.sessionCreationFailed'));
}
}).catch(error => {
console.error("创建会话出错:", error);
console.error(t('knowledgeBase.sessionCreationError') + ":", error);
});
}
</script>
@@ -233,7 +242,7 @@ async function createNewSession(value: string): Promise<void> {
</div>
<template #content>
<t-icon class="icon svg-icon del-card" name="delete" />
<span class="del-card" style="margin-left: 8px">删除文档</span>
<span class="del-card" style="margin-left: 8px">{{ t('knowledgeBase.deleteDocument') }}</span>
</template>
</t-popup>
</div>
@@ -241,7 +250,7 @@ async function createNewSession(value: string): Promise<void> {
<t-icon :name="item.parse_status == 'failed' ? 'close-circle' : 'loading'" class="card-analyze-loading"
:class="[item.parse_status == 'failed' ? 'failure' : '']"></t-icon>
<span class="card-analyze-txt" :class="[item.parse_status == 'failed' ? 'failure' : '']">{{
item.parse_status == "failed" ? "解析失败" : "解析中..."
item.parse_status == "failed" ? t('knowledgeBase.parsingFailed') : t('knowledgeBase.parsingInProgress')
}}</span>
</div>
<div v-show="item.parse_status == 'completed'" class="card-content-txt">
@@ -260,14 +269,14 @@ async function createNewSession(value: string): Promise<void> {
<div class="circle-wrap">
<div class="header">
<img class="circle-img" src="@/assets/img/circle.png" alt="">
<span class="circle-title">删除确认</span>
<span class="circle-title">{{ t('knowledgeBase.deleteConfirmation') }}</span>
</div>
<span class="del-circle-txt">
{{ `确认要删除技能"${knowledge.file_name}",删除后不可恢复` }}
{{ t('knowledgeBase.confirmDeleteDocument', { fileName: knowledge.file_name }) }}
</span>
<div class="circle-btn">
<span class="circle-btn-txt" @click="delDialog = false">取消</span>
<span class="circle-btn-txt confirm" @click="delCardConfirm">确认删除</span>
<span class="circle-btn-txt" @click="delDialog = false">{{ t('knowledgeBase.cancel') }}</span>
<span class="circle-btn-txt confirm" @click="delCardConfirm">{{ t('knowledgeBase.confirmDelete') }}</span>
</div>
</div>
</t-dialog>

View File

@@ -1,27 +1,27 @@
<template>
<div class="kb-list-container">
<div class="header">
<h2>知识库</h2>
<t-button theme="primary" @click="openCreate">新建知识库</t-button>
<h2>{{ t('knowledgeBase.title') }}</h2>
<t-button theme="primary" @click="openCreate">{{ t('knowledgeBase.createNewKnowledgeBase') }}</t-button>
</div>
<!-- 未初始化知识库提示 -->
<div v-if="hasUninitializedKbs" class="warning-banner">
<t-icon name="info-circle" size="16px" />
<span>部分知识库尚未初始化需要先在设置中配置模型信息才能添加知识文档</span>
<span>{{ t('knowledgeBase.uninitializedWarning') }}</span>
</div>
<t-table :data="kbs" :columns="columns" row-key="id" size="medium" hover>
<template #status="{ row }">
<div class="status-cell">
<t-tag
<t-tag
:theme="isInitialized(row) ? 'success' : 'warning'"
size="small"
>
{{ isInitialized(row) ? '已初始化' : '未初始化' }}
{{ isInitialized(row) ? t('knowledgeBase.initializedStatus') : t('knowledgeBase.notInitializedStatus') }}
</t-tag>
<t-tooltip
v-if="!isInitialized(row)"
content="需要先在设置中配置模型信息才能添加知识"
<t-tooltip
v-if="!isInitialized(row)"
:content="t('knowledgeBase.needSettingsFirst')"
placement="top"
>
<span class="warning-icon"></span>
@@ -29,7 +29,7 @@
</div>
</template>
<template #description="{ row }">
<div class="description-text">{{ row.description || '暂无描述' }}</div>
<div class="description-text">{{ row.description || t('knowledgeBase.noDescription') }}</div>
</template>
<template #op="{ row }">
<t-space size="small">
@@ -39,30 +39,30 @@
:disabled="!isInitialized(row)"
:theme="isInitialized(row) ? 'primary' : 'default'"
:variant="isInitialized(row) ? 'base' : 'outline'"
:title="!isInitialized(row) ? '请先在设置中配置模型信息' : ''"
:title="!isInitialized(row) ? t('knowledgeBase.configureModelsFirst') : ''"
>
文档
{{ t('knowledgeBase.documents') }}
</t-button>
<t-button size="small" variant="outline" @click="goSettings(row.id)">设置</t-button>
<t-popconfirm content="确认删除该知识库?" @confirm="remove(row.id)">
<t-button size="small" theme="danger" variant="text">删除</t-button>
<t-button size="small" variant="outline" @click="goSettings(row.id)">{{ t('knowledgeBase.settings') }}</t-button>
<t-popconfirm :content="t('knowledgeBase.confirmDeleteKnowledgeBase')" @confirm="remove(row.id)">
<t-button size="small" theme="danger" variant="text">{{ t('knowledgeBase.delete') }}</t-button>
</t-popconfirm>
</t-space>
</template>
</t-table>
<t-dialog v-model:visible="createVisible" header="新建知识库" :footer="false">
<t-dialog v-model:visible="createVisible" :header="t('knowledgeBase.createKnowledgeBaseDialog')" :footer="false">
<t-form :data="createForm" @submit="create">
<t-form-item label="名称" name="name" :rules="[{ required: true, message: '请输入名称' }]">
<t-form-item :label="t('knowledgeBase.name')" name="name" :rules="[{ required: true, message: t('knowledgeBase.enterNameKb') }]">
<t-input v-model="createForm.name" />
</t-form-item>
<t-form-item label="描述" name="description">
<t-form-item :label="t('knowledgeBase.description')" name="description">
<t-textarea v-model="createForm.description" />
</t-form-item>
<t-form-item>
<t-space>
<t-button theme="primary" type="submit" :loading="creating">创建</t-button>
<t-button variant="outline" @click="createVisible = false">取消</t-button>
<t-button theme="primary" type="submit" :loading="creating">{{ t('knowledgeBase.createKb') }}</t-button>
<t-button variant="outline" @click="createVisible = false">{{ t('common.cancel') }}</t-button>
</t-space>
</t-form-item>
</t-form>
@@ -76,6 +76,8 @@ import { useRouter } from 'vue-router'
import { MessagePlugin } from 'tdesign-vue-next'
import { listKnowledgeBases, createKnowledgeBase, deleteKnowledgeBase } from '@/api/knowledge-base'
import { formatStringDate } from '@/utils/index'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const router = useRouter()
@@ -91,11 +93,11 @@ const kbs = ref<KB[]>([])
const loading = ref(false)
const columns = [
{ colKey: 'name', title: '名称' },
{ colKey: 'description', title: '描述', cell: 'description', width: 300 },
{ colKey: 'status', title: '状态', cell: 'status', width: 100 },
{ colKey: 'updated_at', title: '更新时间' },
{ colKey: 'op', title: '操作', cell: 'op', width: 220 },
{ colKey: 'name', title: t('knowledgeBase.name') },
{ colKey: 'description', title: t('knowledgeBase.description'), cell: 'description', width: 300 },
{ colKey: 'status', title: t('knowledgeBase.status'), cell: 'status', width: 100 },
{ colKey: 'updated_at', title: t('knowledgeBase.uploadTime') },
{ colKey: 'op', title: t('knowledgeBase.actions'), cell: 'op', width: 220 },
]
const fetchList = () => {
@@ -131,26 +133,26 @@ const create = () => {
}
createKnowledgeBase({ name: createForm.name, description: createForm.description, chunking_config }).then((res: any) => {
if (res.success) {
MessagePlugin.success('创建成功')
MessagePlugin.success(t('knowledgeBase.createSuccess'))
createVisible.value = false
fetchList()
} else {
MessagePlugin.error(res.message || '创建失败')
MessagePlugin.error(res.message || t('knowledgeBase.createFailed'))
}
}).catch((e: any) => {
MessagePlugin.error(e?.message || '创建失败')
MessagePlugin.error(e?.message || t('knowledgeBase.createFailed'))
}).finally(() => creating.value = false)
}
const remove = (id: string) => {
deleteKnowledgeBase(id).then((res: any) => {
if (res.success) {
MessagePlugin.success('已删除')
MessagePlugin.success(t('knowledgeBase.deleted'))
fetchList()
} else {
MessagePlugin.error(res.message || '删除失败')
MessagePlugin.error(res.message || t('knowledgeBase.deleteFailedKb'))
}
}).catch((e: any) => MessagePlugin.error(e?.message || '删除失败'))
}).catch((e: any) => MessagePlugin.error(e?.message || t('knowledgeBase.deleteFailedKb')))
}
const isInitialized = (kb: KB) => {

View File

@@ -12,11 +12,14 @@
import Menu from '@/components/menu.vue'
import { ref, onMounted, onUnmounted } from 'vue';
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import useKnowledgeBase from '@/hooks/useKnowledgeBase'
import UploadMask from '@/components/upload-mask.vue'
import { getKnowledgeBaseById } from '@/api/knowledge-base/index'
import { MessagePlugin } from 'tdesign-vue-next'
const { t } = useI18n()
let { requestMethod } = useKnowledgeBase()
const route = useRoute();
let ismask = ref(false)
@@ -32,7 +35,7 @@ const checkKnowledgeBaseInitialization = async (): Promise<boolean> => {
const currentKbId = getCurrentKbId();
if (!currentKbId) {
MessagePlugin.error("缺少知识库ID");
MessagePlugin.error(t('knowledgeBase.missingId'));
return false;
}
@@ -41,12 +44,12 @@ const checkKnowledgeBaseInitialization = async (): Promise<boolean> => {
const kb = kbResponse.data;
if (!kb.embedding_model_id || !kb.summary_model_id) {
MessagePlugin.warning("该知识库尚未完成初始化配置,请先前往设置页面配置模型信息后再上传文件");
MessagePlugin.warning(t('knowledgeBase.notInitialized'));
return false;
}
return true;
} catch (error) {
MessagePlugin.error("获取知识库信息失败,无法上传文件");
MessagePlugin.error(t('knowledgeBase.getInfoFailed'));
return false;
}
}
@@ -91,7 +94,7 @@ const handleGlobalDrop = async (event: DragEvent) => {
}
});
} else {
MessagePlugin.warning('请拖拽文件而不是文本或链接');
MessagePlugin.warning(t('knowledgeBase.dragFileNotText'));
}
}

View File

@@ -1,23 +1,23 @@
<template>
<div class="settings-container">
<div class="settings-header">
<h2>系统配置</h2>
<h2>{{ t('settings.systemConfig') }}</h2>
</div>
<div class="settings-form">
<t-form ref="form" :data="formData" :rules="rules" @submit="onSubmit">
<t-form-item label="API 服务端点" name="endpoint">
<t-input v-model="formData.endpoint" placeholder="请输入API服务端点例如http://localhost" />
<t-form-item :label="t('settings.apiEndpoint')" name="endpoint">
<t-input v-model="formData.endpoint" :placeholder="t('settings.enterApiEndpoint')" />
</t-form-item>
<t-form-item label="API Key" name="apiKey">
<t-input v-model="formData.apiKey" placeholder="请输入API Key" />
<t-form-item :label="t('settings.apiKey')" name="apiKey">
<t-input v-model="formData.apiKey" :placeholder="t('settings.enterApiKey')" />
</t-form-item>
<t-form-item label="知识库ID" name="knowledgeBaseId">
<t-input v-model="formData.knowledgeBaseId" placeholder="请输入知识库ID" />
<t-form-item :label="t('settings.knowledgeBaseId')" name="knowledgeBaseId">
<t-input v-model="formData.knowledgeBaseId" :placeholder="t('settings.enterKnowledgeBaseId')" />
</t-form-item>
<t-form-item>
<t-space>
<t-button theme="primary" type="submit">保存配置</t-button>
<t-button theme="default" @click="resetForm">重置</t-button>
<t-button theme="primary" type="submit">{{ t('settings.saveConfig') }}</t-button>
<t-button theme="default" @click="resetForm">{{ t('settings.reset') }}</t-button>
</t-space>
</t-form-item>
</t-form>
@@ -26,10 +26,13 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, onMounted, computed } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { useI18n } from 'vue-i18n';
import { useSettingsStore } from '@/stores/settings';
const { t } = useI18n();
const settingsStore = useSettingsStore();
const form = ref(null);
@@ -39,11 +42,11 @@ const formData = reactive({
knowledgeBaseId: ''
});
const rules = {
endpoint: [{ required: true, message: '请输入API服务端点', trigger: 'blur' }],
apiKey: [{ required: true, message: '请输入API Key', trigger: 'blur' }],
knowledgeBaseId: [{ required: true, message: '请输入知识库ID', trigger: 'blur' }]
};
const rules = computed(() => ({
endpoint: [{ required: true, message: t('settings.enterApiEndpointRequired'), trigger: 'blur' }],
apiKey: [{ required: true, message: t('settings.enterApiKeyRequired'), trigger: 'blur' }],
knowledgeBaseId: [{ required: true, message: t('settings.enterKnowledgeBaseIdRequired'), trigger: 'blur' }]
}));
onMounted(() => {
// 初始化表单数据
@@ -60,7 +63,7 @@ const onSubmit = ({ validateResult }) => {
apiKey: formData.apiKey,
knowledgeBaseId: formData.knowledgeBaseId
});
MessagePlugin.success('配置保存成功');
MessagePlugin.success(t('settings.configSaved'));
}
};

View File

@@ -2,8 +2,8 @@
<div class="system-settings-container">
<!-- 页面标题区域 -->
<div class="settings-header">
<h2>{{ isKbSettings ? '知识库设置' : '系统设置' }}</h2>
<p class="settings-subtitle">{{ isKbSettings ? '配置该知识库的模型与文档切分参数' : '管理和更新系统模型与服务配置' }}</p>
<h2>{{ isKbSettings ? t('settings.knowledgeBaseSettings') : t('settings.system') }}</h2>
<p class="settings-subtitle">{{ isKbSettings ? t('settings.configureKbModels') : t('settings.manageSystemModels') }}</p>
</div>
<!-- 配置内容 -->
@@ -14,31 +14,31 @@
<div v-else>
<t-form :data="kbForm" @submit="saveKb">
<div class="config-section">
<h3><span class="section-icon"></span>基础信息</h3>
<t-form-item label="名称" name="name" :rules="[{ required: true, message: '请输入名称' }]">
<h3><span class="section-icon"></span>{{ t('settings.basicInfo') }}</h3>
<t-form-item :label="t('settings.name')" name="name" :rules="[{ required: true, message: t('settings.enterNameRequired') }]">
<t-input v-model="kbForm.name" />
</t-form-item>
<t-form-item label="描述" name="description">
<t-form-item :label="t('settings.description')" name="description">
<t-textarea v-model="kbForm.description" />
</t-form-item>
</div>
<div class="config-section">
<h3><span class="section-icon">📄</span>文档切分</h3>
<h3><span class="section-icon">📄</span>{{ t('settings.documentSplitting') }}</h3>
<t-row :gutter="16">
<t-col :span="6">
<t-form-item label="Chunk Size" name="chunkSize">
<t-form-item :label="t('settings.chunkSize')" name="chunkSize">
<t-input-number v-model="kbForm.config.chunking_config.chunk_size" :min="1" />
</t-form-item>
</t-col>
<t-col :span="6">
<t-form-item label="Chunk Overlap" name="chunkOverlap">
<t-form-item :label="t('settings.chunkOverlap')" name="chunkOverlap">
<t-input-number v-model="kbForm.config.chunking_config.chunk_overlap" :min="0" />
</t-form-item>
</t-col>
</t-row>
</div>
<div class="submit-section">
<t-button theme="primary" type="submit" :loading="saving">保存</t-button>
<t-button theme="primary" type="submit" :loading="saving">{{ t('settings.save') }}</t-button>
</div>
</t-form>
</div>
@@ -47,11 +47,14 @@
</template>
<script setup lang="ts">
import { defineAsyncComponent, onMounted, reactive, ref } from 'vue'
import { defineAsyncComponent, onMounted, reactive, ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { MessagePlugin } from 'tdesign-vue-next'
import { useI18n } from 'vue-i18n'
import { getKnowledgeBaseById, updateKnowledgeBase } from '@/api/knowledge-base'
const { t } = useI18n()
// 异步加载初始化配置组件
const InitializationContent = defineAsyncComponent(() => import('../initialization/InitializationContent.vue'))
@@ -97,12 +100,12 @@ const saveKb = () => {
updateKnowledgeBase(kbId, { name: kbForm.name, description: kbForm.description, config: { chunking_config: { chunk_size: kbForm.config.chunking_config.chunk_size, chunk_overlap: kbForm.config.chunking_config.chunk_overlap, separators: [], enable_multimodal: false }, image_processing_config: { model_id: '' } } })
.then((res: any) => {
if (res.success) {
MessagePlugin.success('保存成功')
MessagePlugin.success(t('settings.saveSuccess'))
} else {
MessagePlugin.error(res.message || '保存失败')
MessagePlugin.error(res.message || t('settings.saveFailed'))
}
})
.catch((e: any) => MessagePlugin.error(e?.message || '保存失败'))
.catch((e: any) => MessagePlugin.error(e?.message || t('settings.saveFailed')))
.finally(() => saving.value = false)
}
</script>

View File

@@ -1,28 +1,28 @@
<template>
<div class="tenant-info-container">
<div class="tenant-header">
<h2>系统信息</h2>
<p class="tenant-subtitle">查看系统版本信息和用户账户配置</p>
<h2>{{ $t('tenant.systemInfo') }}</h2>
<p class="tenant-subtitle">{{ $t('tenant.viewSystemInfo') }}</p>
</div>
<div class="tenant-content" v-if="!loading && !error">
<!-- 系统信息卡片 -->
<t-card class="info-card" :bordered="false">
<template #header>
<div class="card-title">系统信息</div>
<div class="card-title">{{ $t('tenant.systemInfo') }}</div>
</template>
<div class="info-content">
<t-descriptions :column="1" layout="vertical">
<t-descriptions-item label="版本号">
{{ systemInfo?.version || '未知' }}
<t-descriptions-item :label="$t('tenant.version')">
{{ systemInfo?.version || $t('tenant.unknown') }}
<span v-if="systemInfo?.commit_id" class="commit-info">
({{ systemInfo.commit_id }})
</span>
</t-descriptions-item>
<t-descriptions-item label="构建时间" v-if="systemInfo?.build_time">
<t-descriptions-item :label="$t('tenant.buildTime')" v-if="systemInfo?.build_time">
{{ systemInfo.build_time }}
</t-descriptions-item>
<t-descriptions-item label="Go版本" v-if="systemInfo?.go_version">
<t-descriptions-item :label="$t('tenant.goVersion')" v-if="systemInfo?.go_version">
{{ systemInfo.go_version }}
</t-descriptions-item>
</t-descriptions>
@@ -32,20 +32,20 @@
<!-- 用户信息卡片 -->
<t-card class="info-card" :bordered="false">
<template #header>
<div class="card-title">用户信息</div>
<div class="card-title">{{ $t('tenant.userInfo') }}</div>
</template>
<div class="info-content">
<t-descriptions :column="1" layout="vertical">
<t-descriptions-item label="用户 ID">
<t-descriptions-item :label="$t('tenant.userId')">
{{ userInfo?.id }}
</t-descriptions-item>
<t-descriptions-item label="用户名">
<t-descriptions-item :label="$t('tenant.username')">
{{ userInfo?.username }}
</t-descriptions-item>
<t-descriptions-item label="邮箱">
<t-descriptions-item :label="$t('tenant.email')">
{{ userInfo?.email }}
</t-descriptions-item>
<t-descriptions-item label="创建时间">
<t-descriptions-item :label="$t('tenant.createdAt')">
{{ formatDate(userInfo?.created_at) }}
</t-descriptions-item>
</t-descriptions>
@@ -55,31 +55,31 @@
<!-- 租户信息卡片 -->
<t-card class="info-card" :bordered="false">
<template #header>
<div class="card-title">租户信息</div>
<div class="card-title">{{ $t('tenant.tenantInfo') }}</div>
</template>
<div class="info-content">
<t-descriptions :column="1" layout="vertical">
<t-descriptions-item label="租户 ID">
<t-descriptions-item :label="$t('tenant.tenantId')">
{{ tenantInfo?.id }}
</t-descriptions-item>
<t-descriptions-item label="租户名称">
<t-descriptions-item :label="$t('tenant.tenantName')">
{{ tenantInfo?.name }}
</t-descriptions-item>
<t-descriptions-item label="描述">
{{ tenantInfo?.description || '暂无描述' }}
<t-descriptions-item :label="$t('tenant.description')">
{{ tenantInfo?.description || $t('tenant.noDescription') }}
</t-descriptions-item>
<t-descriptions-item label="业务">
{{ tenantInfo?.business || '暂无' }}
<t-descriptions-item :label="$t('tenant.business')">
{{ tenantInfo?.business || $t('tenant.noBusiness') }}
</t-descriptions-item>
<t-descriptions-item label="状态">
<t-tag
:theme="getStatusTheme(tenantInfo?.status)"
<t-descriptions-item :label="$t('tenant.status')">
<t-tag
:theme="getStatusTheme(tenantInfo?.status)"
variant="light"
>
{{ getStatusText(tenantInfo?.status) }}
</t-tag>
</t-descriptions-item>
<t-descriptions-item label="创建时间">
<t-descriptions-item :label="$t('tenant.createdAt')">
{{ formatDate(tenantInfo?.created_at) }}
</t-descriptions-item>
</t-descriptions>
@@ -90,7 +90,7 @@
<t-card class="info-card" :bordered="false">
<template #header>
<div class="card-header-with-actions">
<div class="card-title">API Key</div>
<div class="card-title">{{ $t('tenant.apiKey') }}</div>
</div>
</template>
<div class="api-key-content">
@@ -104,7 +104,7 @@
<template #icon>
<t-icon name="error-circle" />
</template>
请妥善保管您的 API Key不要在公共场所或代码仓库中暴露
{{ $t('tenant.keepApiKeySafe') }}
</t-alert>
</div>
</t-card>
@@ -116,17 +116,17 @@
v-if="tenantInfo?.storage_quota !== undefined"
>
<template #header>
<div class="card-title">存储信息</div>
<div class="card-title">{{ $t('tenant.storageInfo') }}</div>
</template>
<div class="storage-content">
<t-descriptions :column="1" layout="vertical">
<t-descriptions-item label="存储配额">
<t-descriptions-item :label="$t('tenant.storageQuota')">
{{ formatBytes(tenantInfo.storage_quota) }}
</t-descriptions-item>
<t-descriptions-item label="已使用">
<t-descriptions-item :label="$t('tenant.used')">
{{ formatBytes(tenantInfo.storage_used || 0) }}
</t-descriptions-item>
<t-descriptions-item label="使用率">
<t-descriptions-item :label="$t('tenant.usage')">
<div class="usage-info">
<span class="usage-text">{{ getUsagePercentage() }}%</span>
<t-progress
@@ -144,10 +144,10 @@
<!-- API 开发文档卡片 -->
<t-card class="info-card" :bordered="false">
<template #header>
<div class="card-title">API 开发文档</div>
<div class="card-title">{{ $t('tenant.apiDevDocs') }}</div>
</template>
<div class="doc-content">
<p class="doc-description">使用您的 API Key 开始开发查看完整的 API 文档和示例代码</p>
<p class="doc-description">{{ $t('tenant.useApiKey') }}</p>
<t-space class="doc-actions">
<t-button
theme="primary"
@@ -156,7 +156,7 @@
<template #icon>
<t-icon name="link" />
</template>
查看 API 文档
{{ $t('tenant.viewApiDoc') }}
</t-button>
</t-space>
@@ -167,14 +167,14 @@
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<t-loading size="large" />
<p class="loading-text">正在加载账户信息...</p>
<p class="loading-text">{{ $t('tenant.loadingAccountInfo') }}</p>
</div>
<!-- 错误状态 -->
<div v-if="error" class="error-container">
<t-result theme="error" title="加载失败" :description="error">
<t-result theme="error" :title="$t('tenant.loadFailed')" :description="error">
<template #extra>
<t-button theme="primary" @click="loadTenantInfo">重试</t-button>
<t-button theme="primary" @click="loadTenantInfo">{{ $t('tenant.retry') }}</t-button>
</template>
</t-result>
</div>
@@ -183,8 +183,10 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { getCurrentUser, type TenantInfo, type UserInfo } from '@/api/auth'
import { getSystemInfo, type SystemInfo } from '@/api/system'
const { t } = useI18n()
// 响应式数据
const tenantInfo = ref<TenantInfo | null>(null)
@@ -262,7 +264,7 @@ const copyApiKey = async () => {
document.execCommand('copy')
document.body.removeChild(textArea)
import('tdesign-vue-next').then(({ MessagePlugin }) => {
MessagePlugin.success('API Key 已复制到剪贴板')
MessagePlugin.success($t('tenant.apiKeyCopied'))
})
}
}
@@ -274,13 +276,13 @@ const openApiDoc = () => {
const getStatusText = (status: string | undefined) => {
switch (status) {
case 'active':
return '活跃'
return $t('tenant.statusActive')
case 'inactive':
return '未激活'
return $t('tenant.statusInactive')
case 'suspended':
return '已暂停'
return $t('tenant.statusSuspended')
default:
return '未知'
return $t('tenant.statusUnknown')
}
}
@@ -298,11 +300,11 @@ const getStatusTheme = (status: string | undefined) => {
}
const formatDate = (dateStr: string | undefined) => {
if (!dateStr) return '未知'
if (!dateStr) return $t('tenant.unknown')
try {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
return date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -311,7 +313,7 @@ const formatDate = (dateStr: string | undefined) => {
second: '2-digit'
})
} catch {
return '格式错误'
return $t('tenant.formatError')
}
}