mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-23 01:39:37 +08:00
feat: 搭建多语言框架
This commit is contained in:
@@ -2,6 +2,7 @@ import { resolve } from 'path'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import ui from '@nuxt/ui/vite'
|
||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
@@ -47,6 +48,14 @@ export default defineConfig(() => {
|
||||
},
|
||||
},
|
||||
}),
|
||||
VueI18nPlugin({
|
||||
// 指定全局语言包目录
|
||||
include: [resolve(__dirname, 'src/i18n/locales/**')],
|
||||
// 默认语言
|
||||
defaultSFCLang: 'json',
|
||||
// 启用组件内 <i18n> 块
|
||||
compositionOnly: true,
|
||||
}),
|
||||
],
|
||||
root: 'src/',
|
||||
build: {
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"main": "./out/main/index.js",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"electron",
|
||||
"better-sqlite3"
|
||||
"better-sqlite3",
|
||||
"electron"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
@@ -34,13 +34,15 @@
|
||||
"better-sqlite3": "^12.4.6",
|
||||
"electron-updater": "^6.6.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"stream-json": "^1.9.1"
|
||||
"stream-json": "^1.9.1",
|
||||
"vue-i18n": "^11.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@electron/rebuild": "^4.0.1",
|
||||
"@intlify/unplugin-vue-i18n": "^11.0.3",
|
||||
"@nuxt/ui": "^4.2.1",
|
||||
"@rushstack/eslint-patch": "^1.15.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
|
||||
9916
pnpm-lock.yaml
generated
9916
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,12 @@ import { ChatRecordDrawer } from '@/components/common/ChatRecord'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
const sessionStore = useSessionStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
const promptStore = usePromptStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { isInitialized } = storeToRefs(sessionStore)
|
||||
const route = useRoute()
|
||||
|
||||
@@ -20,8 +22,11 @@ const tooltip = {
|
||||
delayDuration: 100,
|
||||
}
|
||||
|
||||
// 应用启动时从数据库加载会话列表
|
||||
// 应用启动时初始化
|
||||
onMounted(async () => {
|
||||
// 初始化语言设置(同步 i18n 和 dayjs)
|
||||
settingsStore.initLocale()
|
||||
// 从数据库加载会话列表
|
||||
await sessionStore.loadSessions()
|
||||
})
|
||||
</script>
|
||||
|
||||
30
src/components.d.ts
vendored
30
src/components.d.ts
vendored
@@ -13,25 +13,25 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
UAlert: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
|
||||
UApp: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default']
|
||||
UAlert: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
|
||||
UApp: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default']
|
||||
UAvatar: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Avatar.vue')['default']
|
||||
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
|
||||
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
|
||||
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
|
||||
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
|
||||
UChatPrompt: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/ChatPrompt.vue')['default']
|
||||
UChatPromptSubmit: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/ChatPromptSubmit.vue')['default']
|
||||
UContextMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/ContextMenu.vue')['default']
|
||||
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
|
||||
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
|
||||
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
||||
UContextMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/ContextMenu.vue')['default']
|
||||
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
|
||||
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
|
||||
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
||||
UInputTags: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/InputTags.vue')['default']
|
||||
UModal: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
|
||||
UPopover: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Popover.vue')['default']
|
||||
UProgress: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Progress.vue')['default']
|
||||
UModal: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
|
||||
UPopover: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Popover.vue')['default']
|
||||
UProgress: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Progress.vue')['default']
|
||||
USelect: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
|
||||
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
|
||||
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
||||
UTextarea: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
|
||||
UTooltip: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Tooltip.vue')['default']
|
||||
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
|
||||
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
||||
UTextarea: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
|
||||
UTooltip: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Tooltip.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AnalysisSession } from '@/types/base'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import 'dayjs/locale/en'
|
||||
import SidebarFooter from './sidebar/SidebarFooter.vue'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
const { t } = useI18n()
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const sessionStore = useSessionStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
@@ -103,19 +108,19 @@ function getContextMenuItems(session: AnalysisSession) {
|
||||
return [
|
||||
[
|
||||
{
|
||||
label: '重命名',
|
||||
label: t('sidebar.contextMenu.rename'),
|
||||
icon: 'i-lucide-pencil',
|
||||
class: 'p-2',
|
||||
onSelect: () => openRenameModal(session),
|
||||
},
|
||||
{
|
||||
label: isPinned ? '取消置顶' : '置顶',
|
||||
label: isPinned ? t('sidebar.contextMenu.unpin') : t('sidebar.contextMenu.pin'),
|
||||
icon: isPinned ? 'i-lucide-pin-off' : 'i-lucide-pin',
|
||||
class: 'p-2',
|
||||
onSelect: () => sessionStore.togglePinSession(session.id),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
label: t('sidebar.contextMenu.delete'),
|
||||
icon: 'i-lucide-trash',
|
||||
color: 'error' as const,
|
||||
class: 'p-2',
|
||||
@@ -158,8 +163,8 @@ function getSessionAvatarText(session: AnalysisSession): string {
|
||||
<div class="flex flex-col p-4">
|
||||
<!-- Header / Toggle -->
|
||||
<div class="mb-2 flex items-center" :class="[isCollapsed ? 'justify-center' : 'justify-between']">
|
||||
<div v-if="!isCollapsed" class="text-2xl font-black tracking-tight text-pink-500 ml-2">ChatLab</div>
|
||||
<UTooltip :text="isCollapsed ? '展开侧边栏' : '收起侧边栏'" :popper="{ placement: 'right' }">
|
||||
<div v-if="!isCollapsed" class="text-2xl font-black tracking-tight text-pink-500 ml-2">{{ t('sidebar.brand') }}</div>
|
||||
<UTooltip :text="isCollapsed ? t('sidebar.tooltip.expand') : t('sidebar.tooltip.collapse')" :popper="{ placement: 'right' }">
|
||||
<UButton
|
||||
icon="i-heroicons-bars-3"
|
||||
color="gray"
|
||||
@@ -172,7 +177,7 @@ function getSessionAvatarText(session: AnalysisSession): string {
|
||||
</div>
|
||||
|
||||
<!-- New Analysis Button -->
|
||||
<UTooltip :text="isCollapsed ? '分析新聊天' : ''" :popper="{ placement: 'right' }">
|
||||
<UTooltip :text="isCollapsed ? t('sidebar.newAnalysis') : ''" :popper="{ placement: 'right' }">
|
||||
<UButton
|
||||
:block="!isCollapsed"
|
||||
class="transition-all rounded-full hover:bg-gray-200/60 dark:hover:bg-gray-800 h-12 cursor-pointer"
|
||||
@@ -182,12 +187,12 @@ function getSessionAvatarText(session: AnalysisSession): string {
|
||||
@click="handleImport"
|
||||
>
|
||||
<UIcon name="i-heroicons-plus" class="h-5 w-5 shrink-0" :class="[isCollapsed ? '' : 'mr-2']" />
|
||||
<span v-if="!isCollapsed" class="truncate">分析新聊天</span>
|
||||
<span v-if="!isCollapsed" class="truncate">{{ t('sidebar.newAnalysis') }}</span>
|
||||
</UButton>
|
||||
</UTooltip>
|
||||
|
||||
<!-- Tools Button -->
|
||||
<UTooltip :text="isCollapsed ? '实用工具' : ''" :popper="{ placement: 'right' }">
|
||||
<UTooltip :text="isCollapsed ? t('sidebar.tools') : ''" :popper="{ placement: 'right' }">
|
||||
<UButton
|
||||
:block="!isCollapsed"
|
||||
class="transition-all rounded-full hover:bg-gray-200/60 dark:hover:bg-gray-800 h-12 cursor-pointer mt-2"
|
||||
@@ -202,7 +207,7 @@ function getSessionAvatarText(session: AnalysisSession): string {
|
||||
@click="router.push({ name: 'tools' })"
|
||||
>
|
||||
<UIcon name="i-heroicons-wrench-screwdriver" class="h-4 w-4 shrink-0" :class="[isCollapsed ? '' : 'mr-2']" />
|
||||
<span v-if="!isCollapsed" class="truncate">实用工具</span>
|
||||
<span v-if="!isCollapsed" class="truncate">{{ t('sidebar.tools') }}</span>
|
||||
</UButton>
|
||||
</UTooltip>
|
||||
</div>
|
||||
@@ -212,18 +217,18 @@ function getSessionAvatarText(session: AnalysisSession): string {
|
||||
<!-- 聊天记录标题 - 固定在顶部,不随列表滚动 -->
|
||||
<UTooltip
|
||||
v-if="!isCollapsed && sessions.length > 0"
|
||||
text="右键可删除或重命名聊天记录"
|
||||
:text="t('sidebar.tooltip.hint')"
|
||||
:popper="{ placement: 'right' }"
|
||||
>
|
||||
<div class="px-3 mb-2 flex items-center gap-1">
|
||||
<div class="text-sm font-medium text-gray-500">聊天记录</div>
|
||||
<div class="text-sm font-medium text-gray-500">{{ t('sidebar.chatHistory') }}</div>
|
||||
<UIcon name="i-heroicons-question-mark-circle" class="size-3.5 text-gray-400" />
|
||||
</div>
|
||||
</UTooltip>
|
||||
|
||||
<!-- 聊天记录列表 - 可滚动区域 -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div v-if="sessions.length === 0 && !isCollapsed" class="py-8 text-center text-sm text-gray-500">暂无记录</div>
|
||||
<div v-if="sessions.length === 0 && !isCollapsed" class="py-8 text-center text-sm text-gray-500">{{ t('sidebar.noRecords') }}</div>
|
||||
|
||||
<div class="space-y-1 pb-8">
|
||||
<UTooltip
|
||||
@@ -298,17 +303,17 @@ function getSessionAvatarText(session: AnalysisSession): string {
|
||||
<UModal v-model:open="showRenameModal">
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
<h3 class="mb-3 font-semibold text-gray-900 dark:text-white">重命名</h3>
|
||||
<h3 class="mb-3 font-semibold text-gray-900 dark:text-white">{{ t('sidebar.renameModal.title') }}</h3>
|
||||
<UInput
|
||||
ref="renameInputRef"
|
||||
v-model="newName"
|
||||
placeholder="请输入新名称"
|
||||
:placeholder="t('sidebar.renameModal.placeholder')"
|
||||
class="mb-4 w-100"
|
||||
@keydown.enter="handleRename"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton variant="soft" @click="closeRenameModal">取消</UButton>
|
||||
<UButton color="primary" :disabled="!newName.trim()" @click="handleRename">确定</UButton>
|
||||
<UButton variant="soft" @click="closeRenameModal">{{ t('common.cancel') }}</UButton>
|
||||
<UButton color="primary" :disabled="!newName.trim()" @click="handleRename">{{ t('common.confirm') }}</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -318,15 +323,13 @@ function getSessionAvatarText(session: AnalysisSession): string {
|
||||
<UModal v-model:open="showDeleteModal">
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
<h3 class="mb-3 font-semibold text-gray-900 dark:text-white">确认删除</h3>
|
||||
<h3 class="mb-3 font-semibold text-gray-900 dark:text-white">{{ t('sidebar.deleteModal.title') }}</h3>
|
||||
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
确定要删除聊天记录
|
||||
<span class="font-medium text-gray-900 dark:text-white">"{{ deleteTarget?.name }}"</span>
|
||||
吗?此操作无法撤销。
|
||||
{{ t('sidebar.deleteModal.message', { name: deleteTarget?.name }) }}
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton variant="soft" @click="closeDeleteModal">取消</UButton>
|
||||
<UButton color="error" @click="confirmDelete">删除</UButton>
|
||||
<UButton variant="soft" @click="closeDeleteModal">{{ t('common.cancel') }}</UButton>
|
||||
<UButton color="error" @click="confirmDelete">{{ t('common.delete') }}</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { availableLocales, type LocaleType } from '@/i18n'
|
||||
import NetworkSettingsSection from './NetworkSettingsSection.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Store
|
||||
const layoutStore = useLayoutStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { screenshotMobileAdapt } = storeToRefs(layoutStore)
|
||||
const { locale } = storeToRefs(settingsStore)
|
||||
|
||||
// Color Mode
|
||||
const colorMode = useColorMode({
|
||||
@@ -19,10 +27,43 @@ const colorModeOptions = [
|
||||
{ label: '浅色模式', value: 'light' },
|
||||
{ label: '深色模式', value: 'dark' },
|
||||
]
|
||||
|
||||
// Language options
|
||||
const languageOptions = computed(() =>
|
||||
availableLocales.map((l) => ({
|
||||
label: l.nativeName,
|
||||
value: l.code,
|
||||
}))
|
||||
)
|
||||
|
||||
// Handle language change
|
||||
function handleLocaleChange(newLocale: LocaleType) {
|
||||
settingsStore.setLocale(newLocale)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 语言设置 -->
|
||||
<div>
|
||||
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-language" class="h-4 w-4 text-green-500" />
|
||||
{{ t('settings.basic.language.title') }}
|
||||
</h3>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.basic.language.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<UTabs :model-value="locale" :items="languageOptions" @update:model-value="handleLocaleChange"></UTabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 外观设置 -->
|
||||
<div>
|
||||
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
|
||||
44
src/i18n/index.ts
Normal file
44
src/i18n/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import zhCN from './locales/zh-CN'
|
||||
import enUS from './locales/en-US'
|
||||
import { defaultLocale, type LocaleType } from './types'
|
||||
|
||||
// 导出类型
|
||||
export type { LocaleType } from './types'
|
||||
export {
|
||||
availableLocales,
|
||||
defaultLocale,
|
||||
detectSystemLocale,
|
||||
isFeatureSupported,
|
||||
featureLocaleRestrictions,
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
* 创建 i18n 实例
|
||||
*/
|
||||
export const i18n = createI18n({
|
||||
legacy: false, // 使用 Composition API 模式
|
||||
locale: defaultLocale, // 默认语言
|
||||
fallbackLocale: 'en-US', // 回退语言
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* 动态切换语言
|
||||
*/
|
||||
export function setLocale(locale: LocaleType) {
|
||||
i18n.global.locale.value = locale
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
*/
|
||||
export function getLocale(): LocaleType {
|
||||
return i18n.global.locale.value as LocaleType
|
||||
}
|
||||
|
||||
export default i18n
|
||||
|
||||
21
src/i18n/locales/en-US/common.json
Normal file
21
src/i18n/locales/en-US/common.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"confirm": "OK",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"edit": "Edit",
|
||||
"rename": "Rename",
|
||||
"close": "Close",
|
||||
"loading": "Loading...",
|
||||
"noData": "No data",
|
||||
"error": {
|
||||
"general": "Operation failed, please try again",
|
||||
"network": "Network error"
|
||||
},
|
||||
"unit": {
|
||||
"messages": "messages",
|
||||
"members": "members",
|
||||
"days": "days"
|
||||
}
|
||||
}
|
||||
|
||||
10
src/i18n/locales/en-US/index.ts
Normal file
10
src/i18n/locales/en-US/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import common from './common.json'
|
||||
import sidebar from './sidebar.json'
|
||||
import settings from './settings.json'
|
||||
|
||||
export default {
|
||||
common,
|
||||
sidebar,
|
||||
settings,
|
||||
}
|
||||
|
||||
21
src/i18n/locales/en-US/settings.json
Normal file
21
src/i18n/locales/en-US/settings.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"title": "Settings",
|
||||
"tabs": {
|
||||
"basic": "General",
|
||||
"aiConfig": "AI Models",
|
||||
"aiPrompt": "AI Chat Config",
|
||||
"storage": "Storage",
|
||||
"about": "About"
|
||||
},
|
||||
"basic": {
|
||||
"language": {
|
||||
"title": "Language",
|
||||
"description": "Choose display language"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"description": "Choose theme mode"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
src/i18n/locales/en-US/sidebar.json
Normal file
28
src/i18n/locales/en-US/sidebar.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"brand": "ChatLab",
|
||||
"newAnalysis": "New Analysis",
|
||||
"tools": "Tools",
|
||||
"chatHistory": "Chat History",
|
||||
"noRecords": "No records",
|
||||
"contextMenu": {
|
||||
"rename": "Rename",
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"renameModal": {
|
||||
"title": "Rename",
|
||||
"placeholder": "Enter new name"
|
||||
},
|
||||
"deleteModal": {
|
||||
"title": "Confirm Delete",
|
||||
"message": "Are you sure you want to delete \"{name}\"? This action cannot be undone."
|
||||
},
|
||||
"tooltip": {
|
||||
"expand": "Expand sidebar",
|
||||
"collapse": "Collapse sidebar",
|
||||
"hint": "Right-click to rename or delete"
|
||||
},
|
||||
"sessionInfo": "{count} messages · {time}"
|
||||
}
|
||||
|
||||
21
src/i18n/locales/zh-CN/common.json
Normal file
21
src/i18n/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"confirm": "确定",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"save": "保存",
|
||||
"edit": "编辑",
|
||||
"rename": "重命名",
|
||||
"close": "关闭",
|
||||
"loading": "加载中...",
|
||||
"noData": "暂无数据",
|
||||
"error": {
|
||||
"general": "操作失败,请重试",
|
||||
"network": "网络错误"
|
||||
},
|
||||
"unit": {
|
||||
"messages": "条消息",
|
||||
"members": "个成员",
|
||||
"days": "天"
|
||||
}
|
||||
}
|
||||
|
||||
10
src/i18n/locales/zh-CN/index.ts
Normal file
10
src/i18n/locales/zh-CN/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import common from './common.json'
|
||||
import sidebar from './sidebar.json'
|
||||
import settings from './settings.json'
|
||||
|
||||
export default {
|
||||
common,
|
||||
sidebar,
|
||||
settings,
|
||||
}
|
||||
|
||||
21
src/i18n/locales/zh-CN/settings.json
Normal file
21
src/i18n/locales/zh-CN/settings.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"title": "全局设置",
|
||||
"tabs": {
|
||||
"basic": "基础设置",
|
||||
"aiConfig": "模型配置",
|
||||
"aiPrompt": "AI 对话配置",
|
||||
"storage": "存储管理",
|
||||
"about": "关于"
|
||||
},
|
||||
"basic": {
|
||||
"language": {
|
||||
"title": "语言",
|
||||
"description": "选择界面显示语言"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "外观",
|
||||
"description": "选择主题模式"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
src/i18n/locales/zh-CN/sidebar.json
Normal file
28
src/i18n/locales/zh-CN/sidebar.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"brand": "ChatLab",
|
||||
"newAnalysis": "分析新聊天",
|
||||
"tools": "实用工具",
|
||||
"chatHistory": "聊天记录",
|
||||
"noRecords": "暂无记录",
|
||||
"contextMenu": {
|
||||
"rename": "重命名",
|
||||
"pin": "置顶",
|
||||
"unpin": "取消置顶",
|
||||
"delete": "删除"
|
||||
},
|
||||
"renameModal": {
|
||||
"title": "重命名",
|
||||
"placeholder": "请输入新名称"
|
||||
},
|
||||
"deleteModal": {
|
||||
"title": "确认删除",
|
||||
"message": "确定要删除聊天记录 \"{name}\" 吗?此操作无法撤销。"
|
||||
},
|
||||
"tooltip": {
|
||||
"expand": "展开侧边栏",
|
||||
"collapse": "收起侧边栏",
|
||||
"hint": "右键可删除或重命名聊天记录"
|
||||
},
|
||||
"sessionInfo": "{count} 条消息 · {time}"
|
||||
}
|
||||
|
||||
71
src/i18n/types.ts
Normal file
71
src/i18n/types.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 支持的语言类型
|
||||
*/
|
||||
export type LocaleType = 'zh-CN' | 'en-US'
|
||||
|
||||
/**
|
||||
* 语言配置项
|
||||
*/
|
||||
export interface LocaleOption {
|
||||
code: LocaleType
|
||||
name: string
|
||||
nativeName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 可用的语言列表
|
||||
*/
|
||||
export const availableLocales: LocaleOption[] = [
|
||||
{ code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文' },
|
||||
{ code: 'en-US', name: 'English (US)', nativeName: 'English' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 默认语言
|
||||
*/
|
||||
export const defaultLocale: LocaleType = 'zh-CN'
|
||||
|
||||
/**
|
||||
* 检测系统语言
|
||||
*/
|
||||
export function detectSystemLocale(): LocaleType {
|
||||
const systemLocale = navigator.language
|
||||
if (systemLocale.startsWith('zh')) {
|
||||
return 'zh-CN'
|
||||
}
|
||||
return 'en-US'
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能模块的语言支持配置
|
||||
* 某些功能可能只支持特定语言
|
||||
*/
|
||||
export interface FeatureLocaleSupport {
|
||||
/** 功能标识 */
|
||||
feature: string
|
||||
/** 支持的语言列表,如果为空则支持所有语言 */
|
||||
supportedLocales: LocaleType[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能语言限制配置
|
||||
* 用于控制某些功能只在特定语言下显示
|
||||
*/
|
||||
export const featureLocaleRestrictions: Record<string, LocaleType[]> = {
|
||||
// 群榜单(龙王、夜猫等)只在中文下显示
|
||||
groupRanking: ['zh-CN'],
|
||||
// 以后可以在这里添加更多限制
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查功能是否支持当前语言
|
||||
*/
|
||||
export function isFeatureSupported(feature: string, currentLocale: LocaleType): boolean {
|
||||
const supportedLocales = featureLocaleRestrictions[feature]
|
||||
// 如果没有配置限制,则支持所有语言
|
||||
if (!supportedLocales || supportedLocales.length === 0) {
|
||||
return true
|
||||
}
|
||||
return supportedLocales.includes(currentLocale)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { router } from './routes/'
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import i18n from './i18n'
|
||||
import './assets/styles/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
@@ -14,5 +15,6 @@ pinia.use(piniaPluginPersistedstate)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ui)
|
||||
app.use(i18n)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
55
src/stores/settings.ts
Normal file
55
src/stores/settings.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import 'dayjs/locale/en'
|
||||
import { type LocaleType, detectSystemLocale, setLocale as setI18nLocale } from '@/i18n'
|
||||
|
||||
/**
|
||||
* 全局设置 Store
|
||||
* 管理语言偏好、外观设置等
|
||||
*/
|
||||
export const useSettingsStore = defineStore(
|
||||
'settings',
|
||||
() => {
|
||||
// 语言设置(默认检测系统语言)
|
||||
const locale = ref<LocaleType>(detectSystemLocale())
|
||||
|
||||
/**
|
||||
* 切换语言
|
||||
*/
|
||||
function setLocale(newLocale: LocaleType) {
|
||||
locale.value = newLocale
|
||||
|
||||
// 同步更新 vue-i18n
|
||||
setI18nLocale(newLocale)
|
||||
|
||||
// 同步更新 dayjs
|
||||
dayjs.locale(newLocale === 'zh-CN' ? 'zh-cn' : 'en')
|
||||
|
||||
// 通知主进程(用于对话框等)
|
||||
window.electron?.ipcRenderer.send('locale:change', newLocale)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化语言设置
|
||||
* 应在应用启动时调用
|
||||
*/
|
||||
function initLocale() {
|
||||
// 同步 i18n 和 dayjs 到当前保存的语言
|
||||
setI18nLocale(locale.value)
|
||||
dayjs.locale(locale.value === 'zh-CN' ? 'zh-cn' : 'en')
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
setLocale,
|
||||
initLocale,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: true, // 持久化到 localStorage
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user