db7cef5f17
Replace all user-visible hardcoded Chinese strings in Vue/TS source files
with proper i18n function calls and add corresponding translation keys
to all 4 locale files (zh-CN, en-US, ko-KR, ru-RU).
Changes:
- Replace 80+ hardcoded Chinese strings across 30+ source files
- Add ~450 new translation keys per locale file
- Cover components, views, utils, hooks, and API modules
- Remove Chinese fallback patterns (|| '中文') from existing t() calls
- Add mermaid toolbar and ollama date format translations
Intentionally preserved (backend API values / backward compatibility):
- FAQ CSV column headers and parsing constants
- RelevanceLevel type and Record key mappings
- Graph database engine status comparison ('未启用')
- Language name labels ('简体中文')
466 lines
13 KiB
Vue
466 lines
13 KiB
Vue
<template>
|
|
<div class="user-menu" :class="{ 'user-menu--collapsed': uiStore.sidebarCollapsed }" ref="menuRef">
|
|
<!-- 用户按钮 -->
|
|
<div class="user-button" @click="toggleMenu">
|
|
<div class="user-avatar">
|
|
<img v-if="userAvatar" :src="userAvatar" :alt="$t('common.avatar')" />
|
|
<span v-else class="avatar-placeholder">{{ userInitial }}</span>
|
|
</div>
|
|
<template v-if="!uiStore.sidebarCollapsed">
|
|
<div class="user-info">
|
|
<div class="user-name">{{ userName }}</div>
|
|
<div class="user-email">{{ userEmail }}</div>
|
|
</div>
|
|
<t-icon :name="menuVisible ? 'chevron-up' : 'chevron-down'" class="dropdown-icon" />
|
|
</template>
|
|
</div>
|
|
|
|
<!-- 下拉菜单 -->
|
|
<Transition name="dropdown">
|
|
<div v-if="menuVisible" class="user-dropdown" @click.stop>
|
|
<div class="menu-item" @click="handleQuickNav('models')">
|
|
<t-icon name="control-platform" class="menu-icon" />
|
|
<span>{{ $t('settings.modelManagement') }}</span>
|
|
</div>
|
|
<div class="menu-item" @click="handleQuickNav('ollama')">
|
|
<t-icon name="server" class="menu-icon" />
|
|
<span>Ollama</span>
|
|
</div>
|
|
<div class="menu-item" @click="handleQuickNav('websearch')">
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 18 18"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="menu-icon svg-icon"
|
|
>
|
|
<circle cx="9" cy="9" r="7" stroke="currentColor" stroke-width="1.2" fill="none"/>
|
|
<path d="M 9 2 A 3.5 7 0 0 0 9 16" stroke="currentColor" stroke-width="1.2" fill="none"/>
|
|
<path d="M 9 2 A 3.5 7 0 0 1 9 16" stroke="currentColor" stroke-width="1.2" fill="none"/>
|
|
<line x1="2.94" y1="5.5" x2="15.06" y2="5.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
|
<line x1="2.94" y1="12.5" x2="15.06" y2="12.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
|
</svg>
|
|
<span>{{ $t('settings.webSearchConfig') }}</span>
|
|
</div>
|
|
<div class="menu-item" @click="handleQuickNav('mcp')">
|
|
<t-icon name="tools" class="menu-icon" />
|
|
<span>{{ $t('settings.mcpService') }}</span>
|
|
</div>
|
|
<div class="menu-divider"></div>
|
|
<div class="menu-item" @click="handleSettings">
|
|
<t-icon name="setting" class="menu-icon" />
|
|
<span>{{ $t('general.allSettings') }}</span>
|
|
</div>
|
|
<div class="menu-divider"></div>
|
|
<div class="menu-item" @click="openApiDoc">
|
|
<t-icon name="book" class="menu-icon" />
|
|
<span class="menu-text-with-icon">
|
|
<span>{{ $t('tenant.apiDocument') }}</span>
|
|
<svg class="menu-external-icon" viewBox="0 0 16 16" aria-hidden="true">
|
|
<path
|
|
fill="currentColor"
|
|
d="M12.667 8a.667.667 0 0 1 .666.667v4a2.667 2.667 0 0 1-2.666 2.666H4.667a2.667 2.667 0 0 1-2.667-2.666V5.333a2.667 2.667 0 0 1 2.667-2.666h4a.667.667 0 1 1 0 1.333h-4a1.333 1.333 0 0 0-1.333 1.333v7.334A1.333 1.333 0 0 0 4.667 13.333h6a1.333 1.333 0 0 0 1.333-1.333v-4A.667.667 0 0 1 12.667 8Zm2.666-6.667v4a.667.667 0 0 1-1.333 0V3.276l-5.195 5.195a.667.667 0 0 1-.943-.943l5.195-5.195h-2.057a.667.667 0 0 1 0-1.333h4a.667.667 0 0 1 .666.666Z"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
</div>
|
|
<div class="menu-item" @click="openWebsite">
|
|
<t-icon name="home" class="menu-icon" />
|
|
<span class="menu-text-with-icon">
|
|
<span>{{ $t('common.website') }}</span>
|
|
<svg class="menu-external-icon" viewBox="0 0 16 16" aria-hidden="true">
|
|
<path
|
|
fill="currentColor"
|
|
d="M12.667 8a.667.667 0 0 1 .666.667v4a2.667 2.667 0 0 1-2.666 2.666H4.667a2.667 2.667 0 0 1-2.667-2.666V5.333a2.667 2.667 0 0 1 2.667-2.666h4a.667.667 0 1 1 0 1.333h-4a1.333 1.333 0 0 0-1.333 1.333v7.334A1.333 1.333 0 0 0 4.667 13.333h6a1.333 1.333 0 0 0 1.333-1.333v-4A.667.667 0 0 1 12.667 8Zm2.666-6.667v4a.667.667 0 0 1-1.333 0V3.276l-5.195 5.195a.667.667 0 0 1-.943-.943l5.195-5.195h-2.057a.667.667 0 0 1 0-1.333h4a.667.667 0 0 1 .666.666Z"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
</div>
|
|
<div class="menu-item" @click="openGithub">
|
|
<t-icon name="logo-github" class="menu-icon" />
|
|
<span class="menu-text-with-icon">
|
|
<span>GitHub</span>
|
|
<svg class="menu-external-icon" viewBox="0 0 16 16" aria-hidden="true">
|
|
<path
|
|
fill="currentColor"
|
|
d="M12.667 8a.667.667 0 0 1 .666.667v4a2.667 2.667 0 0 1-2.666 2.666H4.667a2.667 2.667 0 0 1-2.667-2.666V5.333a2.667 2.667 0 0 1 2.667-2.666h4a.667.667 0 1 1 0 1.333h-4a1.333 1.333 0 0 0-1.333 1.333v7.334A1.333 1.333 0 0 0 4.667 13.333h6a1.333 1.333 0 0 0 1.333-1.333v-4A.667.667 0 0 1 12.667 8Zm2.666-6.667v4a.667.667 0 0 1-1.333 0V3.276l-5.195 5.195a.667.667 0 0 1-.943-.943l5.195-5.195h-2.057a.667.667 0 0 1 0-1.333h4a.667.667 0 0 1 .666.666Z"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
</div>
|
|
<div class="menu-divider"></div>
|
|
<div class="menu-item danger" @click="handleLogout">
|
|
<t-icon name="logout" class="menu-icon" />
|
|
<span>{{ $t('auth.logout') }}</span>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useUIStore } from '@/stores/ui'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { MessagePlugin } from 'tdesign-vue-next'
|
|
import { getCurrentUser, logout as logoutApi } from '@/api/auth'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
const { t } = useI18n()
|
|
|
|
const router = useRouter()
|
|
const uiStore = useUIStore()
|
|
const authStore = useAuthStore()
|
|
|
|
const menuRef = ref<HTMLElement>()
|
|
const menuVisible = ref(false)
|
|
|
|
// 用户信息
|
|
const userInfo = ref({
|
|
username: t('common.defaultUser'),
|
|
email: 'user@example.com',
|
|
avatar: ''
|
|
})
|
|
|
|
const userName = computed(() => userInfo.value.username)
|
|
const userEmail = computed(() => userInfo.value.email)
|
|
const userAvatar = computed(() => userInfo.value.avatar)
|
|
|
|
// 用户名首字母(用于无头像时显示)
|
|
const userInitial = computed(() => {
|
|
return userName.value.charAt(0).toUpperCase()
|
|
})
|
|
|
|
// 切换菜单显示
|
|
const toggleMenu = () => {
|
|
menuVisible.value = !menuVisible.value
|
|
}
|
|
|
|
// 快捷导航到设置的特定部分
|
|
const handleQuickNav = (section: string) => {
|
|
menuVisible.value = false
|
|
uiStore.openSettings()
|
|
router.push('/platform/settings')
|
|
|
|
// 延迟一下,确保设置页面已经渲染
|
|
setTimeout(() => {
|
|
// 触发设置页面切换到对应section
|
|
const event = new CustomEvent('settings-nav', { detail: { section } })
|
|
window.dispatchEvent(event)
|
|
}, 100)
|
|
}
|
|
|
|
// 打开设置
|
|
const handleSettings = () => {
|
|
menuVisible.value = false
|
|
uiStore.openSettings()
|
|
router.push('/platform/settings')
|
|
}
|
|
|
|
// 打开 API 文档
|
|
const openApiDoc = () => {
|
|
menuVisible.value = false
|
|
window.open('https://github.com/Tencent/WeKnora/blob/main/docs/api/README.md', '_blank')
|
|
}
|
|
|
|
// 打开官网
|
|
const openWebsite = () => {
|
|
menuVisible.value = false
|
|
window.open('https://weknora.weixin.qq.com/', '_blank')
|
|
}
|
|
|
|
// 打开 GitHub
|
|
const openGithub = () => {
|
|
menuVisible.value = false
|
|
window.open('https://github.com/Tencent/WeKnora', '_blank')
|
|
}
|
|
|
|
// 注销
|
|
const handleLogout = async () => {
|
|
menuVisible.value = false
|
|
|
|
try {
|
|
// 调用后端API注销
|
|
await logoutApi()
|
|
} catch (error) {
|
|
// 即使API调用失败,也继续执行本地清理
|
|
console.error('注销API调用失败:', error)
|
|
}
|
|
|
|
// 清理所有状态和本地存储
|
|
authStore.logout()
|
|
|
|
MessagePlugin.success(t('auth.logout'))
|
|
|
|
// 跳转到登录页
|
|
router.push('/login')
|
|
}
|
|
|
|
// 加载用户信息
|
|
const loadUserInfo = async () => {
|
|
try {
|
|
const response = await getCurrentUser()
|
|
if (response.success && response.data && response.data.user) {
|
|
const user = response.data.user
|
|
userInfo.value = {
|
|
username: user.username || t('common.info'),
|
|
email: user.email || 'user@example.com',
|
|
avatar: user.avatar || ''
|
|
}
|
|
// 同时更新 authStore 中的用户信息,确保包含 can_access_all_tenants 字段
|
|
authStore.setUser({
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
avatar: user.avatar,
|
|
tenant_id: user.tenant_id,
|
|
can_access_all_tenants: user.can_access_all_tenants || false,
|
|
created_at: user.created_at,
|
|
updated_at: user.updated_at
|
|
})
|
|
// 如果返回了租户信息,也更新租户信息
|
|
if (response.data.tenant) {
|
|
authStore.setTenant({
|
|
id: String(response.data.tenant.id),
|
|
name: response.data.tenant.name,
|
|
api_key: response.data.tenant.api_key || '',
|
|
owner_id: user.id,
|
|
created_at: response.data.tenant.created_at,
|
|
updated_at: response.data.tenant.updated_at
|
|
})
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load user info:', error)
|
|
}
|
|
}
|
|
|
|
// 点击外部关闭菜单
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (menuRef.value && !menuRef.value.contains(e.target as Node)) {
|
|
menuVisible.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('click', handleClickOutside)
|
|
loadUserInfo()
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('click', handleClickOutside)
|
|
})
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.user-menu {
|
|
position: relative;
|
|
width: 100%;
|
|
|
|
&--collapsed {
|
|
.user-button {
|
|
justify-content: center;
|
|
padding: 8px;
|
|
gap: 0;
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
|
|
.avatar-placeholder {
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
|
|
.user-dropdown {
|
|
left: calc(100% + 8px);
|
|
bottom: 0;
|
|
right: auto;
|
|
min-width: 200px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.user-button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
background: transparent;
|
|
|
|
&:hover {
|
|
background: var(--td-bg-color-container-hover);
|
|
}
|
|
|
|
&:active {
|
|
transform: scale(0.98);
|
|
}
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
overflow: hidden;
|
|
flex-shrink: 0;
|
|
background: linear-gradient(135deg, var(--td-brand-color) 0%, var(--td-brand-color-active) 100%);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: width 0.2s ease, height 0.2s ease;
|
|
|
|
img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.avatar-placeholder {
|
|
color: var(--td-text-color-anti);
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
|
|
.user-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
text-align: left;
|
|
|
|
.user-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--td-text-color-primary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.user-email {
|
|
font-size: 12px;
|
|
color: var(--td-text-color-secondary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
}
|
|
|
|
.dropdown-icon {
|
|
font-size: 16px;
|
|
color: var(--td-text-color-secondary);
|
|
flex-shrink: 0;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.user-dropdown {
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 8px;
|
|
right: 8px;
|
|
margin-bottom: 8px;
|
|
background: var(--td-bg-color-container);
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
|
border: 1px solid var(--td-component-stroke);
|
|
overflow: hidden;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 16px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 14px;
|
|
color: var(--td-text-color-primary);
|
|
|
|
&:hover {
|
|
background: var(--td-bg-color-container-hover);
|
|
}
|
|
|
|
&.danger {
|
|
color: var(--td-error-color);
|
|
|
|
&:hover {
|
|
background: var(--td-error-color-light);
|
|
}
|
|
|
|
.menu-icon {
|
|
color: var(--td-error-color);
|
|
}
|
|
}
|
|
|
|
.menu-icon {
|
|
font-size: 16px;
|
|
color: var(--td-text-color-secondary);
|
|
|
|
&.svg-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
}
|
|
|
|
.menu-text-with-icon {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
color: inherit;
|
|
min-width: 0;
|
|
|
|
span {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-width: 0;
|
|
}
|
|
}
|
|
|
|
.menu-external-icon {
|
|
width: 14px;
|
|
height: 14px;
|
|
color: var(--td-text-color-disabled);
|
|
flex-shrink: 0;
|
|
transition: color 0.2s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
&:hover .menu-external-icon {
|
|
color: var(--td-brand-color);
|
|
}
|
|
}
|
|
|
|
.menu-divider {
|
|
height: 1px;
|
|
background: var(--td-component-stroke);
|
|
margin: 4px 0;
|
|
}
|
|
|
|
// 下拉动画
|
|
.dropdown-enter-active,
|
|
.dropdown-leave-active {
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.dropdown-enter-from,
|
|
.dropdown-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(8px);
|
|
}
|
|
|
|
.dropdown-enter-to,
|
|
.dropdown-leave-from {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
</style>
|
|
|